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
This commit is contained in:
Claude
2026-01-09 08:25:04 +00:00
parent 57ba2528b2
commit a6e718a00a
3 changed files with 147 additions and 10 deletions
+71 -1
View File
@@ -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");
```
<Note>The root of the project is the directory that contains the trigger.config.ts file</Note>
## 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: "<project ref>",
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.
<Note>
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`.
</Note>
### 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: "<project ref>",
build: {
extensions: [
additionalFiles({
files: ["../shared/**"],
destination: "libs/shared",
}),
additionalFiles({
files: ["../templates/**"],
destination: "assets/templates",
}),
],
},
});
```
@@ -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 {
+57 -9
View File
@@ -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<FoundStaticAssetFiles> {
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<MatchedStaticAssets> {
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);