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:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user