-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathadditionalFiles.ts
More file actions
163 lines (137 loc) · 5.16 KB
/
additionalFiles.ts
File metadata and controls
163 lines (137 loc) · 5.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import { BuildManifest } from "@trigger.dev/core/v3";
import { BuildContext } from "@trigger.dev/core/v3/build";
import { copyFile, mkdir } from "node:fs/promises";
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(
source: string,
options: AdditionalFilesOptions,
context: BuildContext,
manifest: BuildManifest
) {
// 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) {
if (assets.length === 0) {
context.logger.warn(`[${source}] No files found for matcher`, matcher);
} else {
context.logger.debug(`[${source}] Found ${assets.length} files for matcher`, matcher);
}
}
await copyStaticAssets(staticAssets, source, context);
}
type MatchedStaticAssets = { source: string; destination: string }[];
type FoundStaticAssetFiles = Array<{
matcher: string;
assets: MatchedStaticAssets;
}>;
async function findStaticAssetFiles(
matchers: string[],
destinationPath: string,
options?: { cwd?: string; ignore?: string[]; destination?: string }
): Promise<FoundStaticAssetFiles> {
const result: FoundStaticAssetFiles = [];
for (const matcher of matchers) {
const assets = await findStaticAssetsForMatcher(matcher, destinationPath, options);
result.push({ matcher, assets });
}
return result;
}
// Extracts the base directory from a glob pattern (the non-wildcard prefix).
// For example: "../shared/**" -> "../shared", "./assets/*.txt" -> "./assets"
// For specific files without globs: "./config/settings.json" -> "./config" (parent dir)
function getGlobBase(pattern: string): string {
const parts = pattern.split(/[/\\]/);
const baseParts: string[] = [];
let hasGlobCharacters = false;
for (const part of parts) {
// Stop at the first part that contains glob characters
if (part.includes("*") || part.includes("?") || part.includes("[") || part.includes("{")) {
hasGlobCharacters = true;
break;
}
baseParts.push(part);
}
// If no glob characters were found, the pattern is a specific file path.
// Return the parent directory so that relative() preserves the filename.
if (!hasGlobCharacters && baseParts.length > 1) {
baseParts.pop(); // Remove the filename, keep the directory
}
return baseParts.length > 0 ? baseParts.join(posix.sep) : ".";
}
async function findStaticAssetsForMatcher(
matcher: string,
destinationPath: string,
options?: { cwd?: string; ignore?: string[]; destination?: string }
): Promise<MatchedStaticAssets> {
const result: MatchedStaticAssets = [];
const files = await glob({
patterns: [matcher],
cwd: options?.cwd,
ignore: options?.ignore ?? [],
onlyFiles: true,
absolute: true,
});
const cwd = options?.cwd ?? process.cwd();
for (const file of files) {
let pathInsideDestinationDir: string;
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);
result.push({
source: file,
destination: relativeDestinationPath,
});
}
return result;
}
async function copyStaticAssets(
staticAssetFiles: FoundStaticAssetFiles,
sourceName: string,
context: BuildContext
): Promise<void> {
for (const { assets } of staticAssetFiles) {
for (const { source, destination } of assets) {
await mkdir(dirname(destination), { recursive: true });
context.logger.debug(`[${sourceName}] Copying ${source} to ${destination}`);
await copyFile(source, destination);
}
}
}