Skip to content

Commit a6e718a

Browse files
committed
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
1 parent 57ba252 commit a6e718a

File tree

3 files changed

+147
-10
lines changed

3 files changed

+147
-10
lines changed

docs/config/extensions/additionalFiles.mdx

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default defineConfig({
2424

2525
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.
2626

27-
This extension effects both the `dev` and the `deploy` commands, and the resulting paths will be the same for both.
27+
This extension affects both the `dev` and the `deploy` commands, and the resulting paths will be the same for both.
2828

2929
If you use `legacyDevProcessCwdBehaviour: false`, you can then do this:
3030

@@ -36,3 +36,73 @@ const interRegularFont = path.join(process.cwd(), "assets/Inter-Regular.ttf");
3636
```
3737

3838
<Note>The root of the project is the directory that contains the trigger.config.ts file</Note>
39+
40+
## Copying files from parent directories (monorepos)
41+
42+
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.
43+
44+
For example, if your monorepo structure looks like this:
45+
46+
```
47+
monorepo/
48+
├── apps/
49+
│ ├── trigger/ # Contains trigger.config.ts
50+
│ │ └── trigger.config.ts
51+
│ └── shared/ # Directory you want to copy
52+
│ └── utils.ts
53+
```
54+
55+
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.
56+
57+
### Using the `destination` option
58+
59+
To control exactly where files are placed, use the `destination` option:
60+
61+
```ts
62+
import { defineConfig } from "@trigger.dev/sdk";
63+
import { additionalFiles } from "@trigger.dev/build/extensions/core";
64+
65+
export default defineConfig({
66+
project: "<project ref>",
67+
build: {
68+
extensions: [
69+
additionalFiles({
70+
files: ["../shared/**"],
71+
destination: "apps/shared", // Files will be placed under apps/shared/
72+
}),
73+
],
74+
},
75+
});
76+
```
77+
78+
With this configuration, `../shared/utils.ts` will be copied to `apps/shared/utils.ts` in the build directory.
79+
80+
<Note>
81+
When using `destination`, the file structure relative to the glob pattern's base directory is preserved.
82+
For example, `../shared/nested/file.ts` with `destination: "libs"` will be copied to `libs/nested/file.ts`.
83+
</Note>
84+
85+
### Multiple directories with different destinations
86+
87+
If you need to copy multiple directories to different locations, use multiple `additionalFiles` extensions:
88+
89+
```ts
90+
import { defineConfig } from "@trigger.dev/sdk";
91+
import { additionalFiles } from "@trigger.dev/build/extensions/core";
92+
93+
export default defineConfig({
94+
project: "<project ref>",
95+
build: {
96+
extensions: [
97+
additionalFiles({
98+
files: ["../shared/**"],
99+
destination: "libs/shared",
100+
}),
101+
additionalFiles({
102+
files: ["../templates/**"],
103+
destination: "assets/templates",
104+
}),
105+
],
106+
},
107+
});
108+
```

packages/build/src/extensions/core/additionalFiles.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,25 @@ import { addAdditionalFilesToBuild } from "../../internal/additionalFiles.js";
33

44
export type AdditionalFilesOptions = {
55
files: string[];
6+
/**
7+
* Optional destination directory for the matched files.
8+
*
9+
* When specified, files will be placed under this directory while preserving
10+
* their structure relative to the glob pattern's base directory.
11+
*
12+
* This is useful when including files from parent directories (using `..` in the glob pattern),
13+
* as the default behavior strips `..` segments which can result in unexpected destination paths.
14+
*
15+
* @example
16+
* // In a monorepo with structure: apps/trigger, apps/shared
17+
* // From apps/trigger/trigger.config.ts:
18+
* additionalFiles({
19+
* files: ["../shared/**"],
20+
* destination: "apps/shared"
21+
* })
22+
* // Files from ../shared/utils.ts will be copied to apps/shared/utils.ts
23+
*/
24+
destination?: string;
625
};
726

827
export function additionalFiles(options: AdditionalFilesOptions): BuildExtension {

packages/build/src/internal/additionalFiles.ts

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
11
import { BuildManifest } from "@trigger.dev/core/v3";
22
import { BuildContext } from "@trigger.dev/core/v3/build";
33
import { copyFile, mkdir } from "node:fs/promises";
4-
import { dirname, join, posix, relative } from "node:path";
4+
import { dirname, isAbsolute, join, posix, relative, resolve } from "node:path";
55
import { glob } from "tinyglobby";
66

77
export type AdditionalFilesOptions = {
88
files: string[];
9+
/**
10+
* Optional destination directory for the matched files.
11+
*
12+
* When specified, files will be placed under this directory while preserving
13+
* their structure relative to the glob pattern's base directory.
14+
*
15+
* This is useful when including files from parent directories (using `..` in the glob pattern),
16+
* as the default behavior strips `..` segments which can result in unexpected destination paths.
17+
*
18+
* @example
19+
* // In a monorepo with structure: apps/trigger, apps/shared
20+
* // From apps/trigger/trigger.config.ts:
21+
* additionalFiles({
22+
* files: ["../shared/**"],
23+
* destination: "apps/shared"
24+
* })
25+
* // Files from ../shared/utils.ts will be copied to apps/shared/utils.ts
26+
*/
27+
destination?: string;
928
};
1029

1130
export async function addAdditionalFilesToBuild(
@@ -17,6 +36,7 @@ export async function addAdditionalFilesToBuild(
1736
// Copy any static assets to the destination
1837
const staticAssets = await findStaticAssetFiles(options.files ?? [], manifest.outputPath, {
1938
cwd: context.workingDir,
39+
destination: options.destination,
2040
});
2141

2242
for (const { assets, matcher } of staticAssets) {
@@ -40,7 +60,7 @@ type FoundStaticAssetFiles = Array<{
4060
async function findStaticAssetFiles(
4161
matchers: string[],
4262
destinationPath: string,
43-
options?: { cwd?: string; ignore?: string[] }
63+
options?: { cwd?: string; ignore?: string[]; destination?: string }
4464
): Promise<FoundStaticAssetFiles> {
4565
const result: FoundStaticAssetFiles = [];
4666

@@ -53,10 +73,27 @@ async function findStaticAssetFiles(
5373
return result;
5474
}
5575

76+
// Extracts the base directory from a glob pattern (the non-wildcard prefix).
77+
// For example: "../shared/**" -> "../shared", "./assets/*.txt" -> "./assets"
78+
function getGlobBase(pattern: string): string {
79+
const parts = pattern.split(/[/\\]/);
80+
const baseParts: string[] = [];
81+
82+
for (const part of parts) {
83+
// Stop at the first part that contains glob characters
84+
if (part.includes("*") || part.includes("?") || part.includes("[") || part.includes("{")) {
85+
break;
86+
}
87+
baseParts.push(part);
88+
}
89+
90+
return baseParts.length > 0 ? baseParts.join(posix.sep) : ".";
91+
}
92+
5693
async function findStaticAssetsForMatcher(
5794
matcher: string,
5895
destinationPath: string,
59-
options?: { cwd?: string; ignore?: string[] }
96+
options?: { cwd?: string; ignore?: string[]; destination?: string }
6097
): Promise<MatchedStaticAssets> {
6198
const result: MatchedStaticAssets = [];
6299

@@ -68,15 +105,26 @@ async function findStaticAssetsForMatcher(
68105
absolute: true,
69106
});
70107

71-
let matches = 0;
108+
const cwd = options?.cwd ?? process.cwd();
72109

73110
for (const file of files) {
74-
matches++;
111+
let pathInsideDestinationDir: string;
112+
113+
if (options?.destination) {
114+
// When destination is specified, compute path relative to the glob pattern's base directory
115+
const globBase = getGlobBase(matcher);
116+
const absoluteGlobBase = isAbsolute(globBase) ? globBase : resolve(cwd, globBase);
117+
const relativeToGlobBase = relative(absoluteGlobBase, file);
75118

76-
const pathInsideDestinationDir = relative(options?.cwd ?? process.cwd(), file)
77-
.split(posix.sep)
78-
.filter((p) => p !== "..")
79-
.join(posix.sep);
119+
// Place files under the specified destination directory
120+
pathInsideDestinationDir = join(options.destination, relativeToGlobBase);
121+
} else {
122+
// Default behavior: compute relative path from cwd and strip ".." segments
123+
pathInsideDestinationDir = relative(cwd, file)
124+
.split(posix.sep)
125+
.filter((p) => p !== "..")
126+
.join(posix.sep);
127+
}
80128

81129
const relativeDestinationPath = join(destinationPath, pathInsideDestinationDir);
82130

0 commit comments

Comments
 (0)