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";...@@ -10,7 +10,7 @@ import { Command, Flag } from "effect/unstable/cli";
10import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";10import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1111
12import { DEVELOPMENT_ICON_OVERRIDES } from "../../../scripts/lib/brand-assets.ts";12import { 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";
14import { resolveSpawnCommand } from "@t3tools/shared/shell";14import { resolveSpawnCommand } from "@t3tools/shared/shell";
15import {15import {
16 ServerCliBuildAssetMissingError,16 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";...@@ -9,9 +9,10 @@ import * as Schema from "effect/Schema";
99
10import serverPackageJson from "../../apps/server/package.json" with { type: "json" };10import serverPackageJson from "../../apps/server/package.json" with { type: "json" };
1111
12import { findEsmImportsOfExternalPackages } from "./cli-executable-imports.ts";
13
12import {14import {
13 CLI_RUNTIME_EXTERNAL_PREFIXES,15 CLI_RUNTIME_EXTERNAL_PREFIXES,
14 findEsmImportsOfExternalPackages,
15 findInlinedExternalPackages,16 findInlinedExternalPackages,
16 selectCliRuntimeExternalDependencies,17 selectCliRuntimeExternalDependencies,
17 shouldBundleCliDependency,18 shouldBundleCliDependency,
...@@ -310,6 +311,27 @@ describe("findEsmImportsOfExternalPackages", () => {...@@ -310,6 +311,27 @@ describe("findEsmImportsOfExternalPackages", () => {
310 ]);311 ]);
311 });312 });
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
313 it("does not mistake createRequire calls for imports", () => {335 it("does not mistake createRequire calls for imports", () => {
314 const source = 'const { FileFinder } = createRequire(import.meta.url)("@ff-labs/fff-node");';336 const source = 'const { FileFinder } = createRequire(import.meta.url)("@ff-labs/fff-node");';
315 assert.deepStrictEqual(findEsmImportsOfExternalPackages(source), []);337 assert.deepStrictEqual(findEsmImportsOfExternalPackages(source), []);
scripts/lib/cli-external-packages.ts-37
...@@ -1,6 +1,3 @@...@@ -1,6 +1,3 @@
1// @effect-diagnostics nodeBuiltinImport:off
2import * as NodeModule from "node:module";
3
4/**1/**
5 * The single source of truth for packages the server CLI bundle must NOT inline.2 * The single source of truth for packages the server CLI bundle must NOT inline.
6 *3 *
...@@ -81,40 +78,6 @@ export function selectCliRuntimeExternalDependencies(...@@ -81,40 +78,6 @@ export function selectCliRuntimeExternalDependencies(
81 );78 );
82}79}
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
118/**81/**
119 * Scan an emitted bundle chunk for runtime-external packages that were inlined.82 * Scan an emitted bundle chunk for runtime-external packages that were inlined.
120 *83 *