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}