Skip to content

Commit c37c822

Browse files
committed
Use locked package versions when resolving dependencies in deployed workers
1 parent 2dfe191 commit c37c822

File tree

8 files changed

+526
-125
lines changed

8 files changed

+526
-125
lines changed

.changeset/tender-moose-tell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
Use locked package versions when resolving dependencies in deployed workers

packages/cli-v3/src/commands/deploy.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,8 @@ async function compileProject(
12831283

12841284
const dependencies = await gatherRequiredDependencies(allImports, config, javascriptProject);
12851285

1286+
logger.debug("gatherRequiredDependencies()", { dependencies });
1287+
12861288
const packageJsonContents = {
12871289
name: "trigger-worker",
12881290
version: "0.0.0",
@@ -1547,6 +1549,7 @@ async function gatherRequiredDependencies(
15471549
project: JavascriptProject
15481550
) {
15491551
const dependencies: Record<string, string> = {};
1552+
const resolvablePackageNames = new Set<string>();
15501553

15511554
for (const file of imports) {
15521555
if ((file.kind !== "require-call" && file.kind !== "dynamic-import") || !file.external) {
@@ -1555,26 +1558,32 @@ async function gatherRequiredDependencies(
15551558

15561559
const packageName = detectPackageNameFromImportPath(file.path);
15571560

1558-
if (dependencies[packageName]) {
1561+
if (!packageName) {
15591562
continue;
15601563
}
15611564

1562-
const externalDependencyVersion = await project.resolve(packageName);
1565+
resolvablePackageNames.add(packageName);
1566+
}
15631567

1564-
if (externalDependencyVersion) {
1565-
dependencies[packageName] = stripWorkspaceFromVersion(externalDependencyVersion);
1566-
continue;
1567-
}
1568+
const resolvedPackageVersions = await project.resolveAll(Array.from(resolvablePackageNames));
1569+
const missingPackages = Array.from(resolvablePackageNames).filter(
1570+
(packageName) => !resolvedPackageVersions[packageName]
1571+
);
15681572

1573+
for (const missingPackage of missingPackages) {
15691574
const internalDependencyVersion =
1570-
(packageJson.dependencies as Record<string, string>)[packageName] ??
1571-
detectDependencyVersion(packageName);
1575+
(packageJson.dependencies as Record<string, string>)[missingPackage] ??
1576+
detectDependencyVersion(missingPackage);
15721577

15731578
if (internalDependencyVersion) {
1574-
dependencies[packageName] = stripWorkspaceFromVersion(internalDependencyVersion);
1579+
dependencies[missingPackage] = stripWorkspaceFromVersion(internalDependencyVersion);
15751580
}
15761581
}
15771582

1583+
for (const [packageName, version] of Object.entries(resolvedPackageVersions)) {
1584+
dependencies[packageName] = version;
1585+
}
1586+
15781587
if (config.additionalPackages) {
15791588
for (const packageName of config.additionalPackages) {
15801589
if (dependencies[packageName]) {

packages/cli-v3/src/utilities/javascriptProject.ts

Lines changed: 185 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -5,51 +5,10 @@ import { logger } from "./logger";
55
import { PackageManager, getUserPackageManager } from "./getUserPackageManager";
66
import { PackageJson } from "type-fest";
77
import { assertExhaustive } from "./assertExhaustive";
8+
import { builtinModules } from "node:module";
89

910
export type ResolveOptions = { allowDev: boolean };
1011

11-
const BuiltInModules = new Set([
12-
"assert",
13-
"async_hooks",
14-
"buffer",
15-
"child_process",
16-
"cluster",
17-
"console",
18-
"constants",
19-
"crypto",
20-
"dgram",
21-
"dns",
22-
"domain",
23-
"events",
24-
"fs",
25-
"http",
26-
"http2",
27-
"https",
28-
"inspector",
29-
"module",
30-
"net",
31-
"os",
32-
"path",
33-
"perf_hooks",
34-
"process",
35-
"punycode",
36-
"querystring",
37-
"readline",
38-
"repl",
39-
"stream",
40-
"string_decoder",
41-
"timers",
42-
"tls",
43-
"trace_events",
44-
"tty",
45-
"url",
46-
"util",
47-
"v8",
48-
"vm",
49-
"worker_threads",
50-
"zlib",
51-
]);
52-
5312
export class JavascriptProject {
5413
private _packageJson?: PackageJson;
5514
private _packageManager?: PackageManager;
@@ -84,26 +43,72 @@ export class JavascriptProject {
8443
}
8544
}
8645

87-
async resolve(packageName: string, options?: ResolveOptions): Promise<string | undefined> {
88-
if (BuiltInModules.has(packageName)) {
89-
return undefined;
90-
}
46+
async resolveAll(
47+
packageNames: string[],
48+
options?: ResolveOptions
49+
): Promise<Record<string, string>> {
50+
const externalPackages = packageNames.filter((packageName) => !isBuiltInModule(packageName));
9151

9252
const opts = { allowDev: false, ...options };
9353

94-
const packageJsonVersion = this.packageJson.dependencies?.[packageName];
54+
const command = await this.#getCommand();
9555

96-
if (typeof packageJsonVersion === "string") {
97-
return packageJsonVersion;
98-
}
56+
try {
57+
const versions = await command.resolveDependencyVersions(externalPackages, {
58+
cwd: this.projectPath,
59+
});
60+
61+
if (versions) {
62+
logger.debug(`Resolved [${externalPackages.join(", ")}] version using ${command.name}`, {
63+
versions,
64+
});
65+
}
66+
67+
// Merge the resolved versions with the package.json dependencies
68+
const missingPackages = externalPackages.filter((packageName) => !versions[packageName]);
69+
const missingPackageVersions: Record<string, string> = {};
9970

100-
if (opts.allowDev) {
101-
const devPackageJsonVersion = this.packageJson.devDependencies?.[packageName];
71+
for (const packageName of missingPackages) {
72+
const packageJsonVersion = this.packageJson.dependencies?.[packageName];
10273

103-
if (typeof devPackageJsonVersion === "string") {
104-
return devPackageJsonVersion;
74+
if (typeof packageJsonVersion === "string") {
75+
logger.debug(`Resolved ${packageName} version using package.json`, {
76+
packageJsonVersion,
77+
});
78+
79+
missingPackageVersions[packageName] = packageJsonVersion;
80+
}
81+
82+
if (opts.allowDev) {
83+
const devPackageJsonVersion = this.packageJson.devDependencies?.[packageName];
84+
85+
if (typeof devPackageJsonVersion === "string") {
86+
logger.debug(`Resolved ${packageName} version using devDependencies`, {
87+
devPackageJsonVersion,
88+
});
89+
90+
missingPackageVersions[packageName] = devPackageJsonVersion;
91+
}
92+
}
10593
}
94+
95+
return { ...versions, ...missingPackageVersions };
96+
} catch (error) {
97+
logger.debug(`Failed to resolve dependency versions using ${command.name}`, {
98+
packageNames,
99+
error,
100+
});
101+
102+
return {};
106103
}
104+
}
105+
106+
async resolve(packageName: string, options?: ResolveOptions): Promise<string | undefined> {
107+
if (isBuiltInModule(packageName)) {
108+
return undefined;
109+
}
110+
111+
const opts = { allowDev: false, ...options };
107112

108113
const command = await this.#getCommand();
109114

@@ -113,8 +118,30 @@ export class JavascriptProject {
113118
});
114119

115120
if (version) {
121+
logger.debug(`Resolved ${packageName} version using ${command.name}`, { version });
122+
116123
return version;
117124
}
125+
126+
const packageJsonVersion = this.packageJson.dependencies?.[packageName];
127+
128+
if (typeof packageJsonVersion === "string") {
129+
logger.debug(`Resolved ${packageName} version using package.json`, { packageJsonVersion });
130+
131+
return packageJsonVersion;
132+
}
133+
134+
if (opts.allowDev) {
135+
const devPackageJsonVersion = this.packageJson.devDependencies?.[packageName];
136+
137+
if (typeof devPackageJsonVersion === "string") {
138+
logger.debug(`Resolved ${packageName} version using devDependencies`, {
139+
devPackageJsonVersion,
140+
});
141+
142+
return devPackageJsonVersion;
143+
}
144+
}
118145
} catch (error) {
119146
logger.debug(`Failed to resolve dependency version using ${command.name}`, {
120147
packageName,
@@ -176,6 +203,11 @@ interface PackageManagerCommands {
176203
packageName: string,
177204
options: PackageManagerOptions
178205
): Promise<string | undefined>;
206+
207+
resolveDependencyVersions(
208+
packageNames: string[],
209+
options: PackageManagerOptions
210+
): Promise<Record<string, string>>;
179211
}
180212

181213
class PNPMCommands implements PackageManagerCommands {
@@ -197,7 +229,7 @@ class PNPMCommands implements PackageManagerCommands {
197229
const { stdout } = await $({ cwd: options.cwd })`${this.cmd} list ${packageName} -r --json`;
198230
const result = JSON.parse(stdout) as PnpmList;
199231

200-
logger.debug(`Resolving ${packageName} version using ${this.name}`, { result });
232+
logger.debug(`Resolving ${packageName} version using ${this.name}`);
201233

202234
// Return the first dependency version that matches the package name
203235
for (const dep of result) {
@@ -208,6 +240,31 @@ class PNPMCommands implements PackageManagerCommands {
208240
}
209241
}
210242
}
243+
244+
async resolveDependencyVersions(
245+
packageNames: string[],
246+
options: PackageManagerOptions
247+
): Promise<Record<string, string>> {
248+
const { stdout } = await $({ cwd: options.cwd })`${this.cmd} list ${packageNames} -r --json`;
249+
const result = JSON.parse(stdout) as PnpmList;
250+
251+
logger.debug(`Resolving ${packageNames.join(" ")} version using ${this.name}`);
252+
253+
const results: Record<string, string> = {};
254+
255+
// Return the first dependency version that matches the package name
256+
for (const dep of result) {
257+
for (const packageName of packageNames) {
258+
const dependency = dep.dependencies?.[packageName];
259+
260+
if (dependency) {
261+
results[packageName] = dependency.version;
262+
}
263+
}
264+
}
265+
266+
return results;
267+
}
211268
}
212269

213270
type NpmDependency = {
@@ -246,6 +303,28 @@ class NPMCommands implements PackageManagerCommands {
246303
return this.#recursivelySearchDependencies(output.dependencies, packageName);
247304
}
248305

306+
async resolveDependencyVersions(
307+
packageNames: string[],
308+
options: PackageManagerOptions
309+
): Promise<Record<string, string>> {
310+
const { stdout } = await $({ cwd: options.cwd })`${this.cmd} list ${packageNames} --json`;
311+
const output = JSON.parse(stdout) as NpmListOutput;
312+
313+
logger.debug(`Resolving ${packageNames.join(" ")} version using ${this.name}`, { output });
314+
315+
const results: Record<string, string> = {};
316+
317+
for (const packageName of packageNames) {
318+
const version = this.#recursivelySearchDependencies(output.dependencies, packageName);
319+
320+
if (version) {
321+
results[packageName] = version;
322+
}
323+
}
324+
325+
return results;
326+
}
327+
249328
#recursivelySearchDependencies(
250329
dependencies: Record<string, NpmDependency>,
251330
packageName: string
@@ -286,7 +365,7 @@ class YarnCommands implements PackageManagerCommands {
286365

287366
const lines = stdout.split("\n");
288367

289-
logger.debug(`Resolving ${packageName} version using ${this.name}`, { lines });
368+
logger.debug(`Resolving ${packageName} version using ${this.name}`);
290369

291370
for (const line of lines) {
292371
const json = JSON.parse(line);
@@ -296,4 +375,54 @@ class YarnCommands implements PackageManagerCommands {
296375
}
297376
}
298377
}
378+
379+
async resolveDependencyVersions(
380+
packageNames: string[],
381+
options: PackageManagerOptions
382+
): Promise<Record<string, string>> {
383+
const { stdout } = await $({ cwd: options.cwd })`${this.cmd} info ${packageNames} --json`;
384+
385+
const lines = stdout.split("\n");
386+
387+
logger.debug(`Resolving ${packageNames.join(" ")} version using ${this.name}`);
388+
389+
const results: Record<string, string> = {};
390+
391+
for (const line of lines) {
392+
const json = JSON.parse(line);
393+
394+
const packageName = this.#parseYarnValueIntoPackageName(json.value);
395+
396+
if (packageNames.includes(packageName)) {
397+
results[packageName] = json.children.Version;
398+
}
399+
}
400+
401+
return results;
402+
}
403+
404+
// The "value" when doing yarn info is formatted like this:
405+
// "package-name@npm:version" or "package-name@workspace:version"
406+
// This function will parse the value into just the package name.
407+
// This correctly handles scoped packages as well e.g. @scope/package-name@npm:version
408+
#parseYarnValueIntoPackageName(value: string): string {
409+
const parts = value.split("@");
410+
411+
// If the value does not contain an "@" symbol, then it's just the package name
412+
if (parts.length === 3) {
413+
return parts[1] as string;
414+
}
415+
416+
// If the value contains an "@" symbol, then the package name is the first part
417+
return parts[0] as string;
418+
}
419+
}
420+
421+
function isBuiltInModule(module: string): boolean {
422+
// if the module has node: prefix, it's a built-in module
423+
if (module.startsWith("node:")) {
424+
return true;
425+
}
426+
427+
return builtinModules.includes(module);
299428
}

0 commit comments

Comments
 (0)