From a6e718a00a5bbe662f0d45c48b419b2d935a4791 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Jan 2026 08:25:04 +0000 Subject: [PATCH] fix(build): add destination option to additionalFiles extension When using glob patterns with parent directory references (../), the default behavior strips ".." segments resulting in unexpected paths. For example, "../shared/**" would place files at "shared/" instead of preserving the original path structure. This adds an optional "destination" parameter that allows users to explicitly specify where matched files should be placed: additionalFiles({ files: ["../shared/**"], destination: "apps/shared" }) When destination is specified, files are placed relative to the glob pattern's base directory under the destination path. This is useful in monorepo setups where files need to maintain their structure. Also updates documentation to explain this behavior and the new option. Slack thread: https://triggerdotdev.slack.com/archives/C08N6PJTK2Q/p1767946469914139?thread_ts=1756405171.439939&cid=C08N6PJTK2Q --- docs/config/extensions/additionalFiles.mdx | 72 ++++++++++++++++++- .../src/extensions/core/additionalFiles.ts | 19 +++++ .../build/src/internal/additionalFiles.ts | 66 ++++++++++++++--- 3 files changed, 147 insertions(+), 10 deletions(-) diff --git a/docs/config/extensions/additionalFiles.mdx b/docs/config/extensions/additionalFiles.mdx index 038bf185a..5503b641e 100644 --- a/docs/config/extensions/additionalFiles.mdx +++ b/docs/config/extensions/additionalFiles.mdx @@ -24,7 +24,7 @@ export default defineConfig({ This will copy the files specified in the `files` array to the build directory. The `files` array can contain globs. The output paths will match the path of the file, relative to the root of the project. -This extension effects both the `dev` and the `deploy` commands, and the resulting paths will be the same for both. +This extension affects both the `dev` and the `deploy` commands, and the resulting paths will be the same for both. If you use `legacyDevProcessCwdBehaviour: false`, you can then do this: @@ -36,3 +36,73 @@ const interRegularFont = path.join(process.cwd(), "assets/Inter-Regular.ttf"); ``` The root of the project is the directory that contains the trigger.config.ts file + +## Copying files from parent directories (monorepos) + +When copying files from parent directories using `..` in your glob patterns, the default behavior strips the `..` segments from the destination path. This can lead to unexpected results in monorepo setups. + +For example, if your monorepo structure looks like this: + +``` +monorepo/ +├── apps/ +│ ├── trigger/ # Contains trigger.config.ts +│ │ └── trigger.config.ts +│ └── shared/ # Directory you want to copy +│ └── utils.ts +``` + +Using `additionalFiles({ files: ["../shared/**"] })` would copy `utils.ts` to `shared/utils.ts` in the build directory (not `apps/shared/utils.ts`), because the `..` segment is stripped. + +### Using the `destination` option + +To control exactly where files are placed, use the `destination` option: + +```ts +import { defineConfig } from "@trigger.dev/sdk"; +import { additionalFiles } from "@trigger.dev/build/extensions/core"; + +export default defineConfig({ + project: "", + build: { + extensions: [ + additionalFiles({ + files: ["../shared/**"], + destination: "apps/shared", // Files will be placed under apps/shared/ + }), + ], + }, +}); +``` + +With this configuration, `../shared/utils.ts` will be copied to `apps/shared/utils.ts` in the build directory. + + +When using `destination`, the file structure relative to the glob pattern's base directory is preserved. +For example, `../shared/nested/file.ts` with `destination: "libs"` will be copied to `libs/nested/file.ts`. + + +### Multiple directories with different destinations + +If you need to copy multiple directories to different locations, use multiple `additionalFiles` extensions: + +```ts +import { defineConfig } from "@trigger.dev/sdk"; +import { additionalFiles } from "@trigger.dev/build/extensions/core"; + +export default defineConfig({ + project: "", + build: { + extensions: [ + additionalFiles({ + files: ["../shared/**"], + destination: "libs/shared", + }), + additionalFiles({ + files: ["../templates/**"], + destination: "assets/templates", + }), + ], + }, +}); +``` diff --git a/packages/build/src/extensions/core/additionalFiles.ts b/packages/build/src/extensions/core/additionalFiles.ts index cc2a04e0e..a71f42287 100644 --- a/packages/build/src/extensions/core/additionalFiles.ts +++ b/packages/build/src/extensions/core/additionalFiles.ts @@ -3,6 +3,25 @@ import { addAdditionalFilesToBuild } from "../../internal/additionalFiles.js"; export type AdditionalFilesOptions = { files: string[]; + /** + * Optional destination directory for the matched files. + * + * When specified, files will be placed under this directory while preserving + * their structure relative to the glob pattern's base directory. + * + * This is useful when including files from parent directories (using `..` in the glob pattern), + * as the default behavior strips `..` segments which can result in unexpected destination paths. + * + * @example + * // In a monorepo with structure: apps/trigger, apps/shared + * // From apps/trigger/trigger.config.ts: + * additionalFiles({ + * files: ["../shared/**"], + * destination: "apps/shared" + * }) + * // Files from ../shared/utils.ts will be copied to apps/shared/utils.ts + */ + destination?: string; }; export function additionalFiles(options: AdditionalFilesOptions): BuildExtension { diff --git a/packages/build/src/internal/additionalFiles.ts b/packages/build/src/internal/additionalFiles.ts index a815b53c9..b4f0a618f 100644 --- a/packages/build/src/internal/additionalFiles.ts +++ b/packages/build/src/internal/additionalFiles.ts @@ -1,11 +1,30 @@ import { BuildManifest } from "@trigger.dev/core/v3"; import { BuildContext } from "@trigger.dev/core/v3/build"; import { copyFile, mkdir } from "node:fs/promises"; -import { dirname, join, posix, relative } from "node:path"; +import { dirname, isAbsolute, join, posix, relative, resolve } from "node:path"; import { glob } from "tinyglobby"; export type AdditionalFilesOptions = { files: string[]; + /** + * Optional destination directory for the matched files. + * + * When specified, files will be placed under this directory while preserving + * their structure relative to the glob pattern's base directory. + * + * This is useful when including files from parent directories (using `..` in the glob pattern), + * as the default behavior strips `..` segments which can result in unexpected destination paths. + * + * @example + * // In a monorepo with structure: apps/trigger, apps/shared + * // From apps/trigger/trigger.config.ts: + * additionalFiles({ + * files: ["../shared/**"], + * destination: "apps/shared" + * }) + * // Files from ../shared/utils.ts will be copied to apps/shared/utils.ts + */ + destination?: string; }; export async function addAdditionalFilesToBuild( @@ -17,6 +36,7 @@ export async function addAdditionalFilesToBuild( // Copy any static assets to the destination const staticAssets = await findStaticAssetFiles(options.files ?? [], manifest.outputPath, { cwd: context.workingDir, + destination: options.destination, }); for (const { assets, matcher } of staticAssets) { @@ -40,7 +60,7 @@ type FoundStaticAssetFiles = Array<{ async function findStaticAssetFiles( matchers: string[], destinationPath: string, - options?: { cwd?: string; ignore?: string[] } + options?: { cwd?: string; ignore?: string[]; destination?: string } ): Promise { const result: FoundStaticAssetFiles = []; @@ -53,10 +73,27 @@ async function findStaticAssetFiles( return result; } +// Extracts the base directory from a glob pattern (the non-wildcard prefix). +// For example: "../shared/**" -> "../shared", "./assets/*.txt" -> "./assets" +function getGlobBase(pattern: string): string { + const parts = pattern.split(/[/\\]/); + const baseParts: string[] = []; + + for (const part of parts) { + // Stop at the first part that contains glob characters + if (part.includes("*") || part.includes("?") || part.includes("[") || part.includes("{")) { + break; + } + baseParts.push(part); + } + + return baseParts.length > 0 ? baseParts.join(posix.sep) : "."; +} + async function findStaticAssetsForMatcher( matcher: string, destinationPath: string, - options?: { cwd?: string; ignore?: string[] } + options?: { cwd?: string; ignore?: string[]; destination?: string } ): Promise { const result: MatchedStaticAssets = []; @@ -68,15 +105,26 @@ async function findStaticAssetsForMatcher( absolute: true, }); - let matches = 0; + const cwd = options?.cwd ?? process.cwd(); for (const file of files) { - matches++; + let pathInsideDestinationDir: string; - const pathInsideDestinationDir = relative(options?.cwd ?? process.cwd(), file) - .split(posix.sep) - .filter((p) => p !== "..") - .join(posix.sep); + if (options?.destination) { + // When destination is specified, compute path relative to the glob pattern's base directory + const globBase = getGlobBase(matcher); + const absoluteGlobBase = isAbsolute(globBase) ? globBase : resolve(cwd, globBase); + const relativeToGlobBase = relative(absoluteGlobBase, file); + + // Place files under the specified destination directory + pathInsideDestinationDir = join(options.destination, relativeToGlobBase); + } else { + // Default behavior: compute relative path from cwd and strip ".." segments + pathInsideDestinationDir = relative(cwd, file) + .split(posix.sep) + .filter((p) => p !== "..") + .join(posix.sep); + } const relativeDestinationPath = join(destinationPath, pathInsideDestinationDir);