authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-18 11:04:16-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-18 11:04:16-07:00
logdcdf5e6a013607b42836a270165286181da48296
treeef12fef925cde0ab83b8870e7751227cafe8b94b
parented6fe339d71863f39a1d5405554a3bac892e55b6
signature Signed by PGP key B5690EEEBB952194

fix(build): parse executable imports without matching source strings (#12488)


4 files changed, 73 insertions(+), 39 deletions(-)

apps/server/scripts/cli.ts+1-1
......@@ -10,7 +10,7 @@ import { Command, Flag } from "effect/unstable/cli";
1010import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1111
1212import { DEVELOPMENT_ICON_OVERRIDES } from "../../../scripts/lib/brand-assets.ts";
13import { findEsmImportsOfExternalPackages } from "../../../scripts/lib/cli-external-packages.ts";
13import { findEsmImportsOfExternalPackages } from "../../../scripts/lib/cli-executable-imports.ts";
1414import { resolveSpawnCommand } from "@t3tools/shared/shell";
1515import {
1616 ServerCliBuildAssetMissingError,
scripts/lib/cli-executable-imports.ts created+49
......@@ -0,0 +1,49 @@
1// @effect-diagnostics nodeBuiltinImport:off
2import * as NodeModule from "node:module";
3import ts from "typescript-legacy";
4
5/**
6 * Scan an emitted bundle chunk for ESM imports of packages that are not Node
7 * built-ins.
8 *
9 * Inside a Node single-executable, `import` statements and `import()` can only
10 * resolve built-in modules; any file-backed specifier throws at module
11 * evaluation (static) or at first use (dynamic). External packages therefore
12 * have to be reached through `createRequire`, which reads the real filesystem
13 * in every runtime. The bundler cannot enforce this, so the check reads what it
14 * produced.
15 */
16export function findEsmImportsOfExternalPackages(source: string): ReadonlyArray<string> {
17 const specifiers = new Set<string>();
18 const module = ts.createSourceFile(
19 "bundle.mjs",
20 source,
21 ts.ScriptTarget.Latest,
22 false,
23 ts.ScriptKind.JS,
24 );
25 const visit = (node: ts.Node): void => {
26 const dynamic =
27 ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword;
28 const specifierNode =
29 ts.isImportDeclaration(node) || ts.isExportDeclaration(node)
30 ? node.moduleSpecifier
31 : dynamic
32 ? node.arguments[0]
33 : undefined;
34 if (specifierNode && ts.isStringLiteralLike(specifierNode)) {
35 const specifier = specifierNode.text;
36 // Multi-runtime SDKs can retain optional Bun imports. Those are runtime
37 // built-ins, not packages to stage beside the executable. Static imports
38 // still fail here because Node would evaluate them unconditionally.
39 const runtimeBuiltin =
40 NodeModule.isBuiltin(specifier) || (dynamic && specifier.startsWith("bun:"));
41 if (!runtimeBuiltin && !specifier.startsWith("./") && !specifier.startsWith("../")) {
42 specifiers.add(specifier);
43 }
44 }
45 ts.forEachChild(node, visit);
46 };
47 visit(module);
48 return [...specifiers].sort();
49}
scripts/lib/cli-external-packages.test.ts+23-1
......@@ -9,9 +9,10 @@ import * as Schema from "effect/Schema";
99
1010import serverPackageJson from "../../apps/server/package.json" with { type: "json" };
1111
12import { findEsmImportsOfExternalPackages } from "./cli-executable-imports.ts";
13
1214import {
1315 CLI_RUNTIME_EXTERNAL_PREFIXES,
14 findEsmImportsOfExternalPackages,
1516 findInlinedExternalPackages,
1617 selectCliRuntimeExternalDependencies,
1718 shouldBundleCliDependency,
......@@ -310,6 +311,27 @@ describe("findEsmImportsOfExternalPackages", () => {
310311 ]);
311312 });
312313
314 it("ignores imports inside generated extension source and comments", () => {
315 const source = [
316 'const extension = `import { Type } from "typebox";\nimport type { ExtensionAPI } from "@earendil-works/pi-coding-agent";`;',
317 '// import "comment-only";',
318 "const example = 'import(\"string-only\")';",
319 'const interpolated = `source ${import("real-package")}`;',
320 ].join("\n");
321 assert.deepStrictEqual(findEsmImportsOfExternalPackages(source), ["real-package"]);
322 });
323
324 it("allows optional dynamic Bun built-ins but rejects static imports", () => {
325 assert.deepStrictEqual(
326 findEsmImportsOfExternalPackages('const load = () => import("bun:sqlite");'),
327 [],
328 );
329 assert.deepStrictEqual(
330 findEsmImportsOfExternalPackages('import { Database } from "bun:sqlite";'),
331 ["bun:sqlite"],
332 );
333 });
334
313335 it("does not mistake createRequire calls for imports", () => {
314336 const source = 'const { FileFinder } = createRequire(import.meta.url)("@ff-labs/fff-node");';
315337 assert.deepStrictEqual(findEsmImportsOfExternalPackages(source), []);
scripts/lib/cli-external-packages.ts-37
......@@ -1,6 +1,3 @@
1// @effect-diagnostics nodeBuiltinImport:off
2import * as NodeModule from "node:module";
3
41/**
52 * The single source of truth for packages the server CLI bundle must NOT inline.
63 *
......@@ -81,40 +78,6 @@ export function selectCliRuntimeExternalDependencies(
8178 );
8279}
8380
84/**
85 * Scan an emitted bundle chunk for ESM imports of packages that are not Node
86 * built-ins.
87 *
88 * Inside a Node single-executable, `import` statements and `import()` can only
89 * resolve built-in modules; any file-backed specifier throws at module
90 * evaluation (static) or at first use (dynamic). External packages therefore
91 * have to be reached through `createRequire`, which reads the real filesystem
92 * in every runtime. The bundler cannot enforce this, so the check reads what it
93 * produced.
94 */
95export function findEsmImportsOfExternalPackages(source: string): ReadonlyArray<string> {
96 const specifiers = new Set<string>();
97 // `import x from`, `import "side-effect"`, `export ... from`, and `import()`
98 // all resolve through the module loader.
99 const patterns = [
100 /^import\s[^;]*?\sfrom\s+["']([^"']+)["']/gm,
101 /^import\s+["']([^"']+)["']/gm,
102 /^export\s[^;]*?\sfrom\s+["']([^"']+)["']/gm,
103 // Rolldown may leave a `/* @vite-ignore */` style comment before the specifier.
104 /\bimport\(\s*(?:\/\*[\s\S]*?\*\/\s*)*["']([^"']+)["']\s*[,)]/g,
105 ];
106 for (const pattern of patterns) {
107 for (const match of source.matchAll(pattern)) {
108 const specifier = match[1];
109 if (specifier === undefined) continue;
110 if (NodeModule.isBuiltin(specifier)) continue;
111 if (specifier.startsWith("./") || specifier.startsWith("../")) continue;
112 specifiers.add(specifier);
113 }
114 }
115 return [...specifiers].sort();
116}
117
11881/**
11982 * Scan an emitted bundle chunk for runtime-external packages that were inlined.
12083 *