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);