mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Compile the packages an import()-loaded module requires; compile TypeScript enums in packages (#1517)
This commit is contained in:
@@ -156,6 +156,49 @@ export interface FileParts {
|
||||
topStmts: ts.Statement[];
|
||||
}
|
||||
|
||||
/** Preflight's module graph (LoadResult.moduleEdges), for the walks that
|
||||
* append modules outside the startup order, and the set collecting the
|
||||
* inline require() calls that close a cycle there. */
|
||||
export interface AppendGraph {
|
||||
edges?: ReadonlyMap<ts.SourceFile, readonly CycleEdge[]> | undefined;
|
||||
inlineCycleCalls?: Set<ts.Node>;
|
||||
}
|
||||
|
||||
/** An appended module's edges: its import and export declarations, then
|
||||
* the require() edges preflight resolved — a CommonJS file's only edges
|
||||
* (the compiled CommonJS package a workspace module loaded on demand
|
||||
* imports, and the sibling packages and files IT requires). */
|
||||
function appendedEdgesOf(
|
||||
program: ts.Program,
|
||||
graph: AppendGraph | undefined,
|
||||
): (sf: ts.SourceFile) => CycleEdge[] {
|
||||
const cache = new Map<ts.SourceFile, CycleEdge[]>();
|
||||
return (sf) => {
|
||||
let out = cache.get(sf);
|
||||
if (out !== undefined) return out;
|
||||
out = orderedImportsOf(program, sf).flatMap(({ stmt, dep }) =>
|
||||
dep !== null && dep !== sf
|
||||
? [{ dep, stmt: stmt as ts.ImportDeclaration | ts.ExportDeclaration }]
|
||||
: [],
|
||||
);
|
||||
for (const e of graph?.edges?.get(sf) ?? [])
|
||||
if (e.stmt === undefined && e.dep !== sf) out.push(e);
|
||||
cache.set(sf, out);
|
||||
return out;
|
||||
};
|
||||
}
|
||||
|
||||
/** Lazy inline requires run after their module's top level: their modules
|
||||
* join the order afterwards, as in preflight's walk. */
|
||||
function visitLazyEdges(
|
||||
sf: ts.SourceFile,
|
||||
edgesOf: (sf: ts.SourceFile) => CycleEdge[],
|
||||
state: ReadonlyMap<ts.SourceFile, "visiting" | "done">,
|
||||
visit: (sf: ts.SourceFile) => void,
|
||||
): void {
|
||||
for (const e of edgesOf(sf)) if (e.lazy === true && state.get(e.dep) === undefined) visit(e.dep);
|
||||
}
|
||||
|
||||
/** Program modules selected by statically resolved child_process.fork()
|
||||
* calls join the compiled graph without becoming startup dependencies of
|
||||
* the parent entry. Their guarded init functions are selected by %main only
|
||||
@@ -166,8 +209,9 @@ export function appendForkModules(
|
||||
order: ts.SourceFile[],
|
||||
targets: ts.SourceFile[],
|
||||
onCycle: (cycle: string, reason: string) => void,
|
||||
graph?: AppendGraph,
|
||||
): void {
|
||||
appendEntryModules(program, order, targets, forkTargetPaths(program, order), onCycle);
|
||||
appendEntryModules(program, order, targets, forkTargetPaths(program, order), onCycle, graph);
|
||||
}
|
||||
|
||||
export function appendWorkerModules(
|
||||
@@ -175,8 +219,9 @@ export function appendWorkerModules(
|
||||
order: ts.SourceFile[],
|
||||
targets: ts.SourceFile[],
|
||||
onCycle: (cycle: string, reason: string) => void,
|
||||
graph?: AppendGraph,
|
||||
): void {
|
||||
appendEntryModules(program, order, targets, workerTargetPaths(program, order), onCycle);
|
||||
appendEntryModules(program, order, targets, workerTargetPaths(program, order), onCycle, graph);
|
||||
}
|
||||
|
||||
function appendEntryModules(
|
||||
@@ -185,6 +230,7 @@ function appendEntryModules(
|
||||
targets: ts.SourceFile[],
|
||||
paths: readonly string[],
|
||||
onCycle: (cycle: string, reason: string) => void,
|
||||
graph?: AppendGraph,
|
||||
): void {
|
||||
if (order.length === 0) return;
|
||||
const byPath = new Map(
|
||||
@@ -194,22 +240,24 @@ function appendEntryModules(
|
||||
for (const sf of order) state.set(sf, "done");
|
||||
const added: ts.SourceFile[] = [];
|
||||
const stack: string[] = [];
|
||||
const staticEdgesOf = (sf: ts.SourceFile): CycleEdge[] =>
|
||||
orderedImportsOf(program, sf).flatMap(({ stmt, dep }) =>
|
||||
dep !== null && dep !== sf
|
||||
? [{ dep, stmt: stmt as ts.ImportDeclaration | ts.ExportDeclaration }]
|
||||
: [],
|
||||
);
|
||||
const cycleAdmissionReason = makeCycleAdmission(program, staticEdgesOf);
|
||||
const staticEdgesOf = appendedEdgesOf(program, graph);
|
||||
const cycleAdmissionReason = makeCycleAdmission(program, (sf) =>
|
||||
staticEdgesOf(sf).filter((e) => e.call === undefined),
|
||||
);
|
||||
const visit = (sf: ts.SourceFile): void => {
|
||||
const prior = state.get(sf);
|
||||
if (prior === "done") return;
|
||||
state.set(sf, "visiting");
|
||||
stack.push(sf.fileName);
|
||||
for (const edge of staticEdgesOf(sf)) {
|
||||
if (edge.lazy === true) continue; // visited after this module completes
|
||||
const edgeState = state.get(edge.dep);
|
||||
if (edgeState === "done") continue;
|
||||
if (edgeState === "visiting") {
|
||||
if (edge.call !== undefined) {
|
||||
graph?.inlineCycleCalls?.add(edge.call);
|
||||
continue;
|
||||
}
|
||||
const reason = cycleAdmissionReason(sf, edge);
|
||||
if (reason !== null) {
|
||||
const cycleStart = stack.indexOf(edge.dep.fileName);
|
||||
@@ -222,6 +270,7 @@ function appendEntryModules(
|
||||
stack.pop();
|
||||
state.set(sf, "done");
|
||||
added.push(sf);
|
||||
visitLazyEdges(sf, staticEdgesOf, state, visit);
|
||||
};
|
||||
|
||||
const knownTargets = new Set(targets);
|
||||
@@ -313,27 +362,30 @@ export function appendDynamicImportModules(
|
||||
program: ts.Program,
|
||||
order: ts.SourceFile[],
|
||||
onCycle: (cycle: string, reason: string) => void,
|
||||
graph?: AppendGraph,
|
||||
): void {
|
||||
if (order.length === 0) return; // no preflight order — sites keep their fences
|
||||
const state = new Map<ts.SourceFile, "visiting" | "done">();
|
||||
for (const sf of order) state.set(sf, "done");
|
||||
const added: ts.SourceFile[] = [];
|
||||
const stack: string[] = [];
|
||||
const staticEdgesOf = (sf: ts.SourceFile): CycleEdge[] =>
|
||||
orderedImportsOf(program, sf).flatMap(({ stmt, dep }) =>
|
||||
dep !== null && dep !== sf
|
||||
? [{ dep, stmt: stmt as ts.ImportDeclaration | ts.ExportDeclaration }]
|
||||
: [],
|
||||
);
|
||||
const cycleAdmissionReason = makeCycleAdmission(program, staticEdgesOf);
|
||||
const staticEdgesOf = appendedEdgesOf(program, graph);
|
||||
const cycleAdmissionReason = makeCycleAdmission(program, (sf) =>
|
||||
staticEdgesOf(sf).filter((e) => e.call === undefined),
|
||||
);
|
||||
const visit = (sf: ts.SourceFile): void => {
|
||||
if (state.get(sf) !== undefined) return;
|
||||
state.set(sf, "visiting");
|
||||
stack.push(sf.fileName);
|
||||
for (const e of staticEdgesOf(sf)) {
|
||||
if (e.lazy === true) continue; // visited after this module completes
|
||||
const s = state.get(e.dep);
|
||||
if (s === "done") continue;
|
||||
if (s === "visiting") {
|
||||
if (e.call !== undefined) {
|
||||
graph?.inlineCycleCalls?.add(e.call);
|
||||
continue;
|
||||
}
|
||||
// A back edge: Node answers it from the cache. Same admission
|
||||
// question as preflight's static walk, same fence when refused.
|
||||
const reason = cycleAdmissionReason(sf, e);
|
||||
@@ -348,6 +400,7 @@ export function appendDynamicImportModules(
|
||||
stack.pop();
|
||||
state.set(sf, "done");
|
||||
added.push(sf);
|
||||
visitLazyEdges(sf, staticEdgesOf, state, visit);
|
||||
};
|
||||
const scanned = new Set<ts.SourceFile>();
|
||||
for (;;) {
|
||||
|
||||
@@ -233,6 +233,7 @@ import {
|
||||
requireSpecOf,
|
||||
resolveImport,
|
||||
workspacePackageOfPath,
|
||||
type CycleEdge,
|
||||
} from "../program.js";
|
||||
import {
|
||||
containsRecord,
|
||||
@@ -784,6 +785,8 @@ export interface LowerOptions {
|
||||
startupCrash?: StartupCrash | null;
|
||||
/** LoadResult.inlineRequireCycleCalls. */
|
||||
inlineRequireCycleCalls?: ReadonlySet<ts.Node> | undefined;
|
||||
/** LoadResult.moduleEdges. */
|
||||
moduleEdges?: ReadonlyMap<ts.SourceFile, readonly CycleEdge[]> | undefined;
|
||||
/** LIBRARY mode's reachability roots: the profile-mapped exports of the
|
||||
* entry module. Executable builds root at the entry's top level alone
|
||||
* (an unreferenced export dead-strips); a library's exports are called from
|
||||
@@ -972,11 +975,15 @@ export function lowerToIr(
|
||||
),
|
||||
);
|
||||
};
|
||||
// Inline require() calls closing a cycle inside the appended subgraphs
|
||||
// join preflight's set (the same run-time refusal by name).
|
||||
const inlineRequireCycleCalls = new Set<ts.Node>(options.inlineRequireCycleCalls ?? []);
|
||||
const graph = { edges: options.moduleEdges, inlineCycleCalls: inlineRequireCycleCalls };
|
||||
for (let pass = 0; pass < 32; pass++) {
|
||||
const before = moduleOrder.length;
|
||||
appendDynamicImportModules(program, moduleOrder, onExtraCycle);
|
||||
appendForkModules(program, moduleOrder, forkTargets, onExtraCycle);
|
||||
appendWorkerModules(program, moduleOrder, workerTargets, onExtraCycle);
|
||||
appendDynamicImportModules(program, moduleOrder, onExtraCycle, graph);
|
||||
appendForkModules(program, moduleOrder, forkTargets, onExtraCycle, graph);
|
||||
appendWorkerModules(program, moduleOrder, workerTargets, onExtraCycle, graph);
|
||||
if (moduleOrder.length === before) break;
|
||||
if (pass === 31) throw new Error("dynamic import and fork module discovery did not converge");
|
||||
}
|
||||
@@ -991,7 +998,7 @@ export function lowerToIr(
|
||||
projectCheckJs: options.projectCheckJs ?? false,
|
||||
targetPlatform,
|
||||
startupCrash,
|
||||
inlineRequireCycleCalls: options.inlineRequireCycleCalls,
|
||||
inlineRequireCycleCalls,
|
||||
ffiImports,
|
||||
libraryCallbacks,
|
||||
forkTargets,
|
||||
@@ -1015,7 +1022,7 @@ export function lowerToIr(
|
||||
projectCheckJs: options.projectCheckJs ?? false,
|
||||
targetPlatform,
|
||||
startupCrash,
|
||||
inlineRequireCycleCalls: options.inlineRequireCycleCalls,
|
||||
inlineRequireCycleCalls,
|
||||
ffiImports,
|
||||
libraryCallbacks,
|
||||
forkTargets,
|
||||
@@ -1046,7 +1053,7 @@ export function lowerToIr(
|
||||
reachable,
|
||||
targetPlatform,
|
||||
startupCrash,
|
||||
inlineRequireCycleCalls: options.inlineRequireCycleCalls,
|
||||
inlineRequireCycleCalls,
|
||||
ffiImports,
|
||||
libraryCallbacks,
|
||||
forkTargets,
|
||||
@@ -1105,7 +1112,7 @@ export function lowerToIr(
|
||||
reachable,
|
||||
remainder: true,
|
||||
alreadyFlushed: resultLowerer.flushedSymbols,
|
||||
inlineRequireCycleCalls: options.inlineRequireCycleCalls,
|
||||
inlineRequireCycleCalls,
|
||||
targetPlatform,
|
||||
ffiImports,
|
||||
libraryCallbacks,
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import { nodeFrontendServices } from "./services-node.js";
|
||||
|
||||
const dirs: string[] = [];
|
||||
afterAll(() => {
|
||||
for (const dir of dirs) rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function rewrite(source: string): string | null {
|
||||
return nodeFrontendServices().enumIifes(source, "/pkg/index.js");
|
||||
}
|
||||
|
||||
function run(file: string): string {
|
||||
const result = spawnSync(process.execPath, [file], { encoding: "utf8" });
|
||||
expect(result.status).toBe(0);
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
const probe = `
|
||||
for (const name of Object.keys(module.exports)) {
|
||||
const v = module.exports[name];
|
||||
console.log(name, JSON.stringify(v), Object.keys(v ?? {}).join(","));
|
||||
}
|
||||
`;
|
||||
|
||||
/** The rewritten module exports what the original exports, keys in the
|
||||
* same order. */
|
||||
function expectSameExports(source: string): string {
|
||||
const result = rewrite(source);
|
||||
expect(typeof result).toBe("string");
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-enum-iife-"));
|
||||
dirs.push(dir);
|
||||
const original = join(dir, "original.cjs");
|
||||
const rewritten = join(dir, "rewritten.cjs");
|
||||
writeFileSync(original, source + probe);
|
||||
writeFileSync(rewritten, result! + probe);
|
||||
expect(run(rewritten)).toBe(run(original));
|
||||
// Line positions survive the respelling.
|
||||
expect(result!.split("\n").length).toBe(source.split("\n").length);
|
||||
return result!;
|
||||
}
|
||||
|
||||
test("a TypeScript 5 exported string enum becomes its object literal", () => {
|
||||
const out = expectSameExports(`"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Kind = void 0;
|
||||
var Kind;
|
||||
(function (Kind) {
|
||||
Kind["Identifier"] = "Identifier";
|
||||
Kind["Literal"] = "Literal";
|
||||
})(Kind || (exports.Kind = Kind = {}));
|
||||
`);
|
||||
expect(out).toContain(`var Kind = { "Identifier": "Identifier", "Literal": "Literal" };`);
|
||||
expect(out).toContain("exports.Kind = Kind;");
|
||||
});
|
||||
|
||||
test("numeric enums keep their reverse mapping and Node's key order", () => {
|
||||
const out = expectSameExports(`"use strict";
|
||||
var Level;
|
||||
(function (Level) {
|
||||
Level[Level["High"] = 2] = "High";
|
||||
Level[Level["Low"] = 0] = "Low";
|
||||
Level[Level["Below"] = -1] = "Below";
|
||||
Level[Level["Also"] = 2] = "Also";
|
||||
})(Level = exports.Level || (exports.Level = {}));
|
||||
`);
|
||||
expect(out).toContain(
|
||||
`var Level = { 0: "Low", 2: "Also", "High": 2, "Low": 0, "Below": -1, "-1": "Below", "Also": 2 };`,
|
||||
);
|
||||
});
|
||||
|
||||
test("an ES module's exported enum keeps its export", () => {
|
||||
const source = `export var Kind;
|
||||
(function (Kind) {
|
||||
Kind["Identifier"] = "Identifier";
|
||||
Kind[Kind["Zero"] = 0] = "Zero";
|
||||
})(Kind || (Kind = {}));
|
||||
`;
|
||||
const out = rewrite(source);
|
||||
expect(out).toContain(`export var Kind = { 0: "Zero", "Identifier": "Identifier", "Zero": 0 };`);
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-enum-iife-"));
|
||||
dirs.push(dir);
|
||||
writeFileSync(join(dir, "original.mjs"), source);
|
||||
writeFileSync(join(dir, "rewritten.mjs"), out!);
|
||||
const reader = (file: string) =>
|
||||
`import { Kind } from "./${file}"; console.log(JSON.stringify(Kind), Object.keys(Kind).join(","));`;
|
||||
writeFileSync(join(dir, "read-original.mjs"), reader("original.mjs"));
|
||||
writeFileSync(join(dir, "read-rewritten.mjs"), reader("rewritten.mjs"));
|
||||
expect(run(join(dir, "read-rewritten.mjs"))).toBe(run(join(dir, "read-original.mjs")));
|
||||
expect(out!.split("\n").length).toBe(source.split("\n").length);
|
||||
});
|
||||
|
||||
test("an enum the module does not export becomes a local object", () => {
|
||||
const out = expectSameExports(`"use strict";
|
||||
var Mode;
|
||||
(function (Mode) {
|
||||
Mode.Fast = "fast";
|
||||
})(Mode || (Mode = {}));
|
||||
exports.mode = Mode.Fast;
|
||||
`);
|
||||
expect(out).toContain(`var Mode = { "Fast": "fast" };`);
|
||||
});
|
||||
|
||||
test("merged, computed and otherwise written enums keep their text", () => {
|
||||
// A second declaration merges into the first.
|
||||
expect(
|
||||
rewrite(`var E;
|
||||
(function (E) { E["A"] = "a"; })(E || (E = {}));
|
||||
(function (E) { E["B"] = "b"; })(E || (E = {}));
|
||||
`),
|
||||
).toBeNull();
|
||||
// A member referencing another member.
|
||||
expect(
|
||||
rewrite(`var F;
|
||||
(function (F) { F[F["A"] = 1] = "A"; F[F["B"] = F.A << 1] = "B"; })(F || (exports.F = F = {}));
|
||||
`),
|
||||
).toBeNull();
|
||||
// A statement other than a member write.
|
||||
expect(
|
||||
rewrite(`var G;
|
||||
(function (G) { G["A"] = "a"; console.log("side effect"); })(G || (exports.G = G = {}));
|
||||
`),
|
||||
).toBeNull();
|
||||
// The variable is reassigned later.
|
||||
expect(
|
||||
rewrite(`var H;
|
||||
(function (H) { H["A"] = "a"; })(H || (exports.H = H = {}));
|
||||
H = null;
|
||||
`),
|
||||
).toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,284 @@
|
||||
/* TypeScript's enum emit in a compiled package: a bare `var` filled by an
|
||||
* immediately invoked function that writes each member onto its argument.
|
||||
*
|
||||
* var Kind;
|
||||
* (function (Kind) {
|
||||
* Kind["Identifier"] = "Identifier";
|
||||
* Kind[Kind["Zero"] = 0] = "Zero";
|
||||
* })(Kind || (exports.Kind = Kind = {}));
|
||||
*
|
||||
* Inference types the variable from the empty object the argument creates,
|
||||
* so every member read lands on `{}`. When every member value is a literal,
|
||||
* the enum is that object literal: the declaration becomes
|
||||
* `var Kind = { ... };` and the call becomes the export assignment it
|
||||
* performs. Keys keep Node's own-property order (integer keys ascending,
|
||||
* then the rest in insertion order, a later write replacing an earlier
|
||||
* value in place), so enumeration observes the same object.
|
||||
*
|
||||
* Accepted argument spellings: `Kind || (exports.Kind = Kind = {})`
|
||||
* (TypeScript 5), `Kind = exports.Kind || (exports.Kind = {})` (earlier
|
||||
* releases) and `Kind || (Kind = {})` (an enum the module does not
|
||||
* export). Merged declarations, computed or referencing members, and any
|
||||
* other write to the variable keep the original text. */
|
||||
import * as ts from "./ts7/syntax.js";
|
||||
import { applyEdits, unparen, type Edit } from "./npm-static-bundled-chunks.js";
|
||||
|
||||
/** True when the text may hold an enum IIFE (a cheap probe). */
|
||||
export function mayHoldEnumIife(source: string): boolean {
|
||||
return source.includes("(function (") && source.includes("|| (");
|
||||
}
|
||||
|
||||
/** The module text with each literal-valued enum IIFE respelled as an
|
||||
* object literal, or null when there is none. */
|
||||
export function rewriteEnumIifes(sf: ts.SourceFile): string | null {
|
||||
const stmts = sf.statements.nodes;
|
||||
const edits: Edit[] = [];
|
||||
for (let i = 1; i < stmts.length; i++) {
|
||||
const decl = bareVarName(stmts[i - 1]!);
|
||||
if (decl === null) continue;
|
||||
const iife = enumIife(stmts[i]!, decl);
|
||||
if (iife === null || writtenElsewhere(sf, decl, stmts[i]!)) continue;
|
||||
// Modifiers before `var` stay: `export var Kind;` (an ES module's
|
||||
// enum) keeps exporting the binding.
|
||||
const declaration = stmts[i - 1] as ts.VariableStatement;
|
||||
const modifiers = sf.text.slice(
|
||||
declaration.getStart(sf),
|
||||
declaration.declarationList.getStart(sf),
|
||||
);
|
||||
edits.push({
|
||||
start: declaration.getStart(sf),
|
||||
end: declaration.getEnd(),
|
||||
text: `${modifiers}var ${decl} = ${objectLiteral(iife.members)};`,
|
||||
});
|
||||
edits.push({
|
||||
start: stmts[i]!.getStart(sf),
|
||||
end: stmts[i]!.getEnd(),
|
||||
text: iife.exportName === null ? "" : `exports.${iife.exportName} = ${decl};`,
|
||||
});
|
||||
}
|
||||
if (edits.length === 0) return null;
|
||||
return applyEdits(sf.text, 0, sf.text.length, edits);
|
||||
}
|
||||
|
||||
/** `var Name;`: one declarator, no initializer. */
|
||||
function bareVarName(stmt: ts.Statement): string | null {
|
||||
if (!ts.isVariableStatement(stmt)) return null;
|
||||
const list = stmt.declarationList;
|
||||
if ((list.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0) return null;
|
||||
const decls = list.declarations.nodes;
|
||||
if (decls.length !== 1) return null;
|
||||
const d = decls[0]!;
|
||||
if (d.initializer !== undefined || !ts.isIdentifier(d.name)) return null;
|
||||
return d.name.text;
|
||||
}
|
||||
|
||||
type Members = Map<string, string | number>;
|
||||
|
||||
function enumIife(
|
||||
stmt: ts.Statement,
|
||||
name: string,
|
||||
): { members: Members; exportName: string | null } | null {
|
||||
if (!ts.isExpressionStatement(stmt)) return null;
|
||||
const call = unparen(stmt.expression);
|
||||
if (!ts.isCallExpression(call) || call.arguments.nodes.length !== 1) return null;
|
||||
const fn = unparen(call.expression);
|
||||
if (!ts.isFunctionExpression(fn) || fn.name !== undefined || fn.asteriskToken !== undefined)
|
||||
return null;
|
||||
if (fn.modifiers !== undefined && fn.modifiers.nodes.length > 0) return null;
|
||||
const params = fn.parameters.nodes;
|
||||
if (params.length !== 1) return null;
|
||||
const param = params[0]!;
|
||||
if (!ts.isIdentifier(param.name) || param.initializer !== undefined) return null;
|
||||
if (param.dotDotDotToken !== undefined) return null;
|
||||
const exportName = enumArgument(call.arguments.nodes[0]!, name);
|
||||
if (exportName === undefined) return null;
|
||||
const members = enumMembers(fn.body.statements.nodes, param.name.text);
|
||||
return members === null ? null : { members, exportName };
|
||||
}
|
||||
|
||||
/** The export name the argument assigns (null: not exported), or
|
||||
* undefined when it is not one of the enum spellings. */
|
||||
function enumArgument(arg: ts.Expression, name: string): string | null | undefined {
|
||||
const e = unparen(arg);
|
||||
if (!ts.isBinaryExpression(e)) return undefined;
|
||||
// `Kind = exports.Kind || (exports.Kind = {})`
|
||||
if (e.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
||||
if (!isName(e.left, name)) return undefined;
|
||||
const or = unparen(e.right);
|
||||
if (!ts.isBinaryExpression(or) || or.operatorToken.kind !== ts.SyntaxKind.BarBarToken)
|
||||
return undefined;
|
||||
const exported = exportsMember(or.left);
|
||||
if (exported === null) return undefined;
|
||||
const init = unparen(or.right);
|
||||
if (
|
||||
!ts.isBinaryExpression(init) ||
|
||||
init.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
|
||||
exportsMember(init.left) !== exported ||
|
||||
!isEmptyObject(init.right)
|
||||
)
|
||||
return undefined;
|
||||
return exported;
|
||||
}
|
||||
if (e.operatorToken.kind !== ts.SyntaxKind.BarBarToken || !isName(e.left, name)) return undefined;
|
||||
const init = unparen(e.right);
|
||||
if (!ts.isBinaryExpression(init) || init.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
||||
return undefined;
|
||||
// `Kind || (Kind = {})`
|
||||
if (isName(init.left, name)) return isEmptyObject(init.right) ? null : undefined;
|
||||
// `Kind || (exports.Kind = Kind = {})`
|
||||
const exported = exportsMember(init.left);
|
||||
const inner = unparen(init.right);
|
||||
if (
|
||||
exported === null ||
|
||||
!ts.isBinaryExpression(inner) ||
|
||||
inner.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
|
||||
!isName(inner.left, name) ||
|
||||
!isEmptyObject(inner.right)
|
||||
)
|
||||
return undefined;
|
||||
return exported;
|
||||
}
|
||||
|
||||
/** The members the body writes, or null when a statement is anything but
|
||||
* a literal member write (`E["K"] = v`, `E.K = v`, or the reverse-mapped
|
||||
* `E[E["K"] = v] = "K"`). */
|
||||
function enumMembers(body: readonly ts.Statement[], param: string): Members | null {
|
||||
const members: Members = new Map();
|
||||
for (const stmt of body) {
|
||||
if (!ts.isExpressionStatement(stmt)) return null;
|
||||
const e = stmt.expression;
|
||||
if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
||||
return null;
|
||||
const value = literalValue(e.right);
|
||||
if (value === null) return null;
|
||||
const key = memberKey(e.left, param);
|
||||
if (key !== null) {
|
||||
members.set(key, value);
|
||||
continue;
|
||||
}
|
||||
// E[E["K"] = v] = "K"
|
||||
const target = e.left;
|
||||
if (!ts.isElementAccessExpression(target) || !isName(target.expression, param)) return null;
|
||||
const inner = unparen(target.argumentExpression);
|
||||
if (!ts.isBinaryExpression(inner) || inner.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
||||
return null;
|
||||
const innerKey = memberKey(inner.left, param);
|
||||
const innerValue = literalValue(inner.right);
|
||||
if (innerKey === null || innerValue === null) return null;
|
||||
members.set(innerKey, innerValue);
|
||||
members.set(propertyKey(innerValue), value);
|
||||
}
|
||||
return members.size > 0 ? members : null;
|
||||
}
|
||||
|
||||
function memberKey(target: ts.Expression, param: string): string | null {
|
||||
if (ts.isElementAccessExpression(target) && isName(target.expression, param)) {
|
||||
const k = unparen(target.argumentExpression);
|
||||
return ts.isStringLiteral(k) || ts.isNoSubstitutionTemplateLiteral(k) ? k.text : null;
|
||||
}
|
||||
if (
|
||||
ts.isPropertyAccessExpression(target) &&
|
||||
isName(target.expression, param) &&
|
||||
ts.isIdentifier(target.name)
|
||||
)
|
||||
return target.name.text;
|
||||
return null;
|
||||
}
|
||||
|
||||
function literalValue(expr: ts.Expression): string | number | null {
|
||||
const e = unparen(expr);
|
||||
if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e)) return e.text;
|
||||
if (ts.isNumericLiteral(e)) return numberOf(e.text);
|
||||
if (
|
||||
ts.isPrefixUnaryExpression(e) &&
|
||||
e.operator === ts.SyntaxKind.MinusToken &&
|
||||
ts.isNumericLiteral(e.operand)
|
||||
) {
|
||||
const n = numberOf(e.operand.text);
|
||||
return n === null ? null : -n;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function numberOf(text: string): number | null {
|
||||
if (!/^(?:\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|0[xX][0-9a-fA-F]+)$/.test(text)) return null;
|
||||
const n = Number(text);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
/** ToPropertyKey of a member value. */
|
||||
function propertyKey(value: string | number): string {
|
||||
return typeof value === "string" ? value : String(value);
|
||||
}
|
||||
|
||||
/** The canonical array-index keys JavaScript enumerates first. */
|
||||
function isIndexKey(key: string): boolean {
|
||||
return /^(?:0|[1-9]\d*)$/.test(key) && Number(key) < 4294967295;
|
||||
}
|
||||
|
||||
function objectLiteral(members: Members): string {
|
||||
const keys = [...members.keys()];
|
||||
const ordered = [
|
||||
...keys.filter(isIndexKey).sort((a, b) => Number(a) - Number(b)),
|
||||
...keys.filter((k) => !isIndexKey(k)),
|
||||
];
|
||||
const parts = ordered.map((k) => {
|
||||
const v = members.get(k)!;
|
||||
const value = typeof v === "string" ? JSON.stringify(v) : Object.is(v, -0) ? "-0" : String(v);
|
||||
return `${isIndexKey(k) ? k : JSON.stringify(k)}: ${value}`;
|
||||
});
|
||||
return `{ ${parts.join(", ")} }`;
|
||||
}
|
||||
|
||||
/** True when the module writes `name` anywhere outside the enum call. */
|
||||
function writtenElsewhere(sf: ts.SourceFile, name: string, iife: ts.Statement): boolean {
|
||||
const start = iife.getStart(sf);
|
||||
const end = iife.getEnd();
|
||||
let written = false;
|
||||
ts.walkPreorder(sf, (n) => {
|
||||
if (written) return "stop";
|
||||
if (n.pos >= start && n.end <= end) return "skip";
|
||||
if (ts.isBinaryExpression(n) && isAssignmentOperator(n.operatorToken.kind)) {
|
||||
if (isName(n.left, name)) written = true;
|
||||
} else if (
|
||||
(ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) &&
|
||||
(n.operator === ts.SyntaxKind.PlusPlusToken ||
|
||||
n.operator === ts.SyntaxKind.MinusMinusToken) &&
|
||||
isName(n.operand, name)
|
||||
) {
|
||||
written = true;
|
||||
} else if (
|
||||
(ts.isFunctionDeclaration(n) || ts.isClassDeclaration(n)) &&
|
||||
n.name !== undefined &&
|
||||
n.name.text === name
|
||||
) {
|
||||
written = true;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return written;
|
||||
}
|
||||
|
||||
function isAssignmentOperator(kind: ts.SyntaxKind): boolean {
|
||||
return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
|
||||
}
|
||||
|
||||
function isName(e: ts.Expression, name: string): boolean {
|
||||
const u = unparen(e);
|
||||
return ts.isIdentifier(u) && u.text === name;
|
||||
}
|
||||
|
||||
function exportsMember(e: ts.Expression): string | null {
|
||||
const u = unparen(e);
|
||||
return ts.isPropertyAccessExpression(u) &&
|
||||
ts.isIdentifier(u.expression) &&
|
||||
u.expression.text === "exports" &&
|
||||
ts.isIdentifier(u.name)
|
||||
? u.name.text
|
||||
: null;
|
||||
}
|
||||
|
||||
function isEmptyObject(e: ts.Expression): boolean {
|
||||
const u = unparen(e);
|
||||
return ts.isObjectLiteralExpression(u) && u.properties.nodes.length === 0;
|
||||
}
|
||||
@@ -455,8 +455,16 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
|
||||
// through that function.
|
||||
const fnExports = original === null ? null : services.cjsFunctionExports(original, path);
|
||||
if (fnExports !== null) original = fnExports;
|
||||
// TypeScript's enum IIFEs with literal members become the object
|
||||
// literals they build.
|
||||
const enums = original === null ? null : services.enumIifes(original, path);
|
||||
if (enums !== null) original = enums;
|
||||
const respelled =
|
||||
split !== null || esm !== null || aliased !== null || fnExports !== null;
|
||||
split !== null ||
|
||||
esm !== null ||
|
||||
aliased !== null ||
|
||||
fnExports !== null ||
|
||||
enums !== null;
|
||||
if (original !== null) {
|
||||
const namepaths = services.jsDocNamepaths(path, original);
|
||||
const source = namepaths ?? original;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
checkPreflight,
|
||||
isNodeTypesPath,
|
||||
locOf,
|
||||
npmStaticDepSf7,
|
||||
npmStaticLoadableFiles,
|
||||
requiresOf,
|
||||
resolveImport,
|
||||
@@ -119,26 +120,32 @@ export type FrontendFactory = (
|
||||
* with their own static import graphs: they load at the call, outside the
|
||||
* startup module order, and their npm imports are dependencies all the
|
||||
* same (a workspace package loaded on demand, the vercel.ts config parser
|
||||
* shape). Package files are the package machinery's business. */
|
||||
* shape). An opted-in package's JavaScript reached that way joins too,
|
||||
* with every file its literal require() and import edges load: those
|
||||
* files are outside the module order as well, and the packages THEY
|
||||
* require (a compiled CommonJS dist requiring a sibling package) are
|
||||
* dependencies the selection must see. */
|
||||
function addDynamicImportGraph(load: LoadResult, scanned: Set<SourceFile>): void {
|
||||
const queue = [...scanned];
|
||||
const added = new Set<SourceFile>();
|
||||
while (queue.length > 0) {
|
||||
const sf = queue.pop()!;
|
||||
if (sf.fileName.includes("/node_modules/")) continue;
|
||||
const specs: string[] = [];
|
||||
const packageJs = sf.fileName.includes("/node_modules/");
|
||||
if (packageJs && !added.has(sf)) continue;
|
||||
const specs: { spec: string; kind: "import" | "require" }[] = [];
|
||||
// Every literal import() in the file, read from the syntax: a comment
|
||||
// can stand between the keyword and its parenthesis. A file without
|
||||
// the keyword has none.
|
||||
const calls = sf.text.includes("import") ? moduleSourceCandidates(load.program, sf).calls : [];
|
||||
// the keyword has none. Package JavaScript loads its literal require()
|
||||
// calls the same way, nested ones included.
|
||||
const calls =
|
||||
packageJs || sf.text.includes("import") ? moduleSourceCandidates(load.program, sf).calls : [];
|
||||
for (const call of calls) {
|
||||
const arg = call.arguments.nodes[0];
|
||||
if (
|
||||
call.expression.kind === SyntaxKind.ImportKeyword &&
|
||||
arg !== undefined &&
|
||||
isStringLiteralLike(arg)
|
||||
)
|
||||
specs.push(arg.text);
|
||||
if (arg === undefined || !isStringLiteralLike(arg)) continue;
|
||||
if (call.expression.kind === SyntaxKind.ImportKeyword)
|
||||
specs.push({ spec: arg.text, kind: "import" });
|
||||
else if (packageJs && isIdentifier(call.expression) && call.expression.text === "require")
|
||||
specs.push({ spec: arg.text, kind: "require" });
|
||||
}
|
||||
// The startup graph's static edges are already in the module order.
|
||||
if (added.has(sf)) {
|
||||
@@ -149,12 +156,18 @@ function addDynamicImportGraph(load: LoadResult, scanned: Set<SourceFile>): void
|
||||
isStringLiteral(stmt.moduleSpecifier) &&
|
||||
!(isImportDeclaration(stmt) ? erasedTypeOnlyImport(stmt) : stmt.isTypeOnly)
|
||||
)
|
||||
specs.push(stmt.moduleSpecifier.text);
|
||||
specs.push({ spec: stmt.moduleSpecifier.text, kind: "import" });
|
||||
}
|
||||
}
|
||||
for (const spec of specs) {
|
||||
const dep = resolveImport(load.program, sf, spec);
|
||||
if (dep === null || scanned.has(dep) || dep.fileName.includes("/node_modules/")) continue;
|
||||
for (const { spec, kind } of specs) {
|
||||
if (canonicalBuiltinModule(spec) !== null) continue;
|
||||
// Only the JavaScript of a package the selection already opted in:
|
||||
// a package's declarations load nothing.
|
||||
const dep =
|
||||
resolveImport(load.program, sf, spec, kind) ??
|
||||
npmStaticDepSf7(load.program, sf, spec, kind);
|
||||
if (dep === null || scanned.has(dep)) continue;
|
||||
if (dep.fileName.includes("/node_modules/") && !isJsSourceFileName(dep.fileName)) continue;
|
||||
scanned.add(dep);
|
||||
added.add(dep);
|
||||
queue.push(dep);
|
||||
@@ -674,6 +687,7 @@ function loadFrontend(
|
||||
frontendServices: finalLoad.services,
|
||||
startupCrash: finalLoad.startupCrash ?? null,
|
||||
inlineRequireCycleCalls: finalLoad.inlineRequireCycleCalls,
|
||||
moduleEdges: finalLoad.moduleEdges,
|
||||
projectCheckJs: finalLoad.typeWorld?.projectCheckJs ?? false,
|
||||
externalTypes: finalLoad.externalTypes,
|
||||
externalTypeSpecifiersByFile: finalLoad.externalTypeSpecifiersByFile,
|
||||
|
||||
@@ -354,6 +354,11 @@ export interface LoadResult {
|
||||
* at run time instead of refusing the whole program. Filled by
|
||||
* checkPreflight. */
|
||||
inlineRequireCycleCalls?: ReadonlySet<ts.Node>;
|
||||
/** Every program file's module edges as preflight resolved them,
|
||||
* require() edges included: the lowering orders the modules only a
|
||||
* literal import(), fork() or Worker reaches with them. Filled by
|
||||
* checkPreflight. */
|
||||
moduleEdges?: ReadonlyMap<ts.SourceFile, readonly CycleEdge[]> | undefined;
|
||||
/** Node's startup refusal, when the graph carries an import edge Node
|
||||
* rejects before ANY module evaluates — a RESOLUTION refusal (a bare
|
||||
* specifier nothing installed resolves, an exports/imports map that
|
||||
@@ -1052,10 +1057,12 @@ export function loadProgram(
|
||||
* preflight diagnostics. Preserve the caller's complete load shape when
|
||||
* mutating it, including its owned services and disposal callback. */
|
||||
export function checkPreflight<T extends LoadResult>(load: T): ScrDiagnostic[] {
|
||||
const { diags, moduleOrder, startupCrash, inlineRequireCycleCalls } = preflight7(load);
|
||||
const { diags, moduleOrder, startupCrash, inlineRequireCycleCalls, moduleEdges } =
|
||||
preflight7(load);
|
||||
load.moduleOrder = moduleOrder;
|
||||
load.startupCrash = startupCrash;
|
||||
load.inlineRequireCycleCalls = inlineRequireCycleCalls;
|
||||
load.moduleEdges = moduleEdges;
|
||||
return diags;
|
||||
}
|
||||
|
||||
@@ -4393,6 +4400,7 @@ function preflight7(load: LoadResult): {
|
||||
moduleOrder: ts.SourceFile[];
|
||||
startupCrash: StartupCrash | null;
|
||||
inlineRequireCycleCalls: ReadonlySet<ts.Node>;
|
||||
moduleEdges?: ReadonlyMap<ts.SourceFile, readonly CycleEdge[]>;
|
||||
} {
|
||||
const { program, entry } = load;
|
||||
const diags: ScrDiagnostic[] = [...load.configDiags];
|
||||
@@ -5501,6 +5509,7 @@ function preflight7(load: LoadResult): {
|
||||
moduleOrder: order,
|
||||
startupCrash: resolveCrash ?? linkCrash,
|
||||
inlineRequireCycleCalls,
|
||||
moduleEdges: edges,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
mayRequireBuiltinAlias,
|
||||
rewriteBuiltinAliasRequires,
|
||||
} from "./npm-static-builtin-aliases.js";
|
||||
import { mayHoldEnumIife, rewriteEnumIifes } from "./npm-static-enum-iife.js";
|
||||
import { builtinPackageAliasFor } from "./package-shims.js";
|
||||
import * as syntax from "./npm-static-declaration-syntax.js";
|
||||
import type { SourceFile } from "./ts7/ast-types.js";
|
||||
@@ -104,6 +105,13 @@ export class FrontendServices {
|
||||
if (!source.includes("module.exports")) return null;
|
||||
return normalizeCjsFunctionExports(this.parse(path, source, "js"));
|
||||
}
|
||||
/** A package module with its literal-valued TypeScript enum IIFEs
|
||||
* respelled as object literals (null: none). */
|
||||
enumIifes(source: string, path: string): string | null {
|
||||
this.ensureOpen();
|
||||
if (!mayHoldEnumIife(source)) return null;
|
||||
return rewriteEnumIifes(this.parse(path, source, "js"));
|
||||
}
|
||||
/** A package module with its requires of builtin-aliased polyfill
|
||||
* packages respelled as the builtins (null: none). */
|
||||
builtinAliasRequires(source: string, path: string): string | null {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/workspace-dynamic-import-deps/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// A workspace package the program loads on demand with a dynamic import.
|
||||
// Its npm dependency is compiled CommonJS that requires a sibling scoped
|
||||
// package (shipping JavaScript and declarations) from a nested file. Both
|
||||
// packages compile with the workspace package, although none of them is
|
||||
// part of the startup module graph.
|
||||
async function main(): Promise<void> {
|
||||
console.log("before");
|
||||
const m = await import("wsdynamicdeps");
|
||||
console.log(m.describe("42"));
|
||||
console.log(m.describe("name"));
|
||||
}
|
||||
main();
|
||||
Generated
Vendored
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../../workspace/wsdynamicdeps
|
||||
@@ -0,0 +1,6 @@
|
||||
import { classify, NodeKind } from "@gtdyn/estree";
|
||||
|
||||
export function describe(text: string): string {
|
||||
const kind = classify(text);
|
||||
return kind === NodeKind.Literal ? `literal ${text}` : `${kind} ${text}`;
|
||||
}
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { NodeKind } from "@gtdyn/types";
|
||||
export declare function classify(text: string): NodeKind;
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.classify = void 0;
|
||||
const types_1 = require("@gtdyn/types");
|
||||
function classify(text) {
|
||||
return /^[0-9]+$/.test(text) ? types_1.NodeKind.Literal : types_1.NodeKind.Identifier;
|
||||
}
|
||||
exports.classify = classify;
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export { classify } from "./classify";
|
||||
export { NodeKind } from "@gtdyn/types";
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.NodeKind = exports.classify = void 0;
|
||||
var classify_1 = require("./classify");
|
||||
Object.defineProperty(exports, "classify", { enumerable: true, get: function () { return classify_1.classify; } });
|
||||
var types_1 = require("@gtdyn/types");
|
||||
Object.defineProperty(exports, "NodeKind", { enumerable: true, get: function () { return types_1.NodeKind; } });
|
||||
Generated
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@gtdyn/estree",
|
||||
"version": "1.0.0",
|
||||
"description": "a compiled CommonJS package that requires its sibling scoped package from a nested file",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@gtdyn/types": "1.0.0"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
export declare enum NodeKind {
|
||||
Identifier = "Identifier",
|
||||
Literal = "Literal"
|
||||
}
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.NodeKind = void 0;
|
||||
var NodeKind;
|
||||
(function (NodeKind) {
|
||||
NodeKind["Identifier"] = "Identifier";
|
||||
NodeKind["Literal"] = "Literal";
|
||||
})(NodeKind || (exports.NodeKind = NodeKind = {}));
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@gtdyn/types",
|
||||
"version": "1.0.0",
|
||||
"description": "a scoped package shipping compiled JavaScript and declarations",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "wsdynamicdeps",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"dependencies": {
|
||||
"@gtdyn/estree": "1.0.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user