mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
Merge main
This commit is contained in:
@@ -34,11 +34,13 @@ import {
|
||||
locOf,
|
||||
makeCycleAdmission,
|
||||
orderedImportsOf,
|
||||
npmStaticDepSf7,
|
||||
requireSpecOf,
|
||||
resolveImport,
|
||||
resolveNpmImport,
|
||||
} from "../program.js";
|
||||
import type { CycleEdge } from "../program.js";
|
||||
import type { CycleEdge, StartupCrash } from "../program.js";
|
||||
import { isUnloadedPackage } from "../package-shims.js";
|
||||
import {
|
||||
markCycleEarlyBindings,
|
||||
withClassInitFlag,
|
||||
@@ -175,6 +177,8 @@ export interface FileParts {
|
||||
export interface AppendGraph {
|
||||
edges?: ReadonlyMap<ts.SourceFile, readonly CycleEdge[]> | undefined;
|
||||
inlineCycleCalls?: Set<ts.Node>;
|
||||
/** LoadResult.dynamicImportRejections: import() roots that never load. */
|
||||
rejected?: ReadonlyMap<ts.SourceFile, StartupCrash> | undefined;
|
||||
}
|
||||
|
||||
/** An appended module's edges: its import and export declarations, then
|
||||
@@ -440,7 +444,8 @@ export function appendDynamicImportModules(
|
||||
if (scan.length === 0) break;
|
||||
for (const sf of scan) {
|
||||
scanned.add(sf);
|
||||
for (const dep of dynamicProgramImportsOf(program, sf)) visit(dep);
|
||||
for (const dep of dynamicProgramImportsOf(program, sf))
|
||||
if (graph?.rejected?.has(dep) !== true) visit(dep);
|
||||
}
|
||||
}
|
||||
if (added.length > 0) order.splice(order.length - 1, 0, ...added);
|
||||
@@ -722,6 +727,27 @@ export function collectNpmImports(lowerer: Lowerer, parts: FileParts[]): void {
|
||||
* specifiers are skipped — the lowering owns that fence (the module
|
||||
* graph is a build-time artifact; there is nothing to embed for a
|
||||
* runtime-computed name). */
|
||||
/** The refusal a literal import() rejects with, or null when it loads:
|
||||
* Node's ERR_MODULE_NOT_FOUND for a package the project marks never loaded
|
||||
* (package-shims.ts), or the first refusal in the static graph of a module
|
||||
* only import() loads (LoadResult.dynamicImportRejections). */
|
||||
function dynamicImportRejectionOf(
|
||||
lowerer: Lowerer,
|
||||
sf: ts.SourceFile,
|
||||
spec: string,
|
||||
call: ts.CallExpression,
|
||||
): StartupCrash | null {
|
||||
if (isUnloadedPackage(spec)) {
|
||||
const refusal = probeNodeImportRefusal(sf.fileName, spec);
|
||||
if (refusal === null) return null;
|
||||
return { message: refusal.message, className: "%Error", code: refusal.code, loc: locOf(call) };
|
||||
}
|
||||
if (lowerer.dynamicImportRejections.size === 0) return null;
|
||||
const root =
|
||||
resolveImport(lowerer.program, sf, spec) ?? npmStaticDepSf7(lowerer.program, sf, spec);
|
||||
return root === null ? null : (lowerer.dynamicImportRejections.get(root) ?? null);
|
||||
}
|
||||
|
||||
function collectDynamicImports(lowerer: Lowerer, sf: ts.SourceFile): void {
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
@@ -742,6 +768,13 @@ function collectDynamicImports(lowerer: Lowerer, sf: ts.SourceFile): void {
|
||||
// module has no runtime namespace object — the per-site fence
|
||||
// says so. Only non-declaration source files count; a sibling
|
||||
// .d.mts typing shipped JS is the EMBED case, not this one.
|
||||
// A package the project marks never loaded, or a module whose
|
||||
// static graph Node's resolution refuses: the import() rejects.
|
||||
const rejection = dynamicImportRejectionOf(lowerer, sf, spec, node);
|
||||
if (rejection !== null) {
|
||||
lowerer.dynImports.set(mapKey, { kind: "rejected", crash: rejection });
|
||||
return undefined;
|
||||
}
|
||||
const modSym = lowerer.checker.getSymbolAtLocation(node.arguments.nodes[0]);
|
||||
const ownModule =
|
||||
modSym !== undefined &&
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
isJsSourceFile,
|
||||
locOf,
|
||||
npmPackageNameOf,
|
||||
type StartupCrash,
|
||||
} from "../program.js";
|
||||
import { foldedStringKeyOf, lowerDynObjectLiteral } from "./expressions/object-literals.js";
|
||||
import { noteEventListeners } from "./lower-event-target.js";
|
||||
@@ -2243,6 +2244,7 @@ export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression
|
||||
}
|
||||
const res = lowerer.dynImports.get(`${ts.getSourceFileOfNode(call).fileName}\u0000${arg.text}`);
|
||||
if (res === undefined) lowerer.refuseUnsupportedApi("'import()'", call);
|
||||
if (res.kind === "rejected") return lowerRejectedImport(lowerer, call, res.crash);
|
||||
if (res.kind === "program-module") {
|
||||
return lowerOwnStaticModuleImport(lowerer, call, arg);
|
||||
}
|
||||
@@ -2252,6 +2254,69 @@ export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression
|
||||
});
|
||||
}
|
||||
|
||||
/** An import() Node rejects before any module of its graph evaluates
|
||||
* (a package marked never loaded, a refused edge in the module's static
|
||||
* graph): the promise rejects on a microtask with Node's error, and the
|
||||
* module never joins the compiled graph. Its namespace is never produced,
|
||||
* so the result is typed dynamically. */
|
||||
function lowerRejectedImport(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
crash: StartupCrash,
|
||||
): IrExpr {
|
||||
const loc = locOf(call);
|
||||
const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: DYN };
|
||||
const fnName = lowerer.closureName(loc, "_rejectedImport");
|
||||
const fnType: IrType & { kind: "func" } = { kind: "func", params: [], ret: resultT };
|
||||
const fnCtx = newFnCtx(true, null, fnType, DYN);
|
||||
fnCtx.isAsync = true;
|
||||
lowerer.fnStack.push(fnCtx);
|
||||
try {
|
||||
const hop: IrExpr = {
|
||||
kind: "awaitExpr",
|
||||
value: {
|
||||
kind: "intrinsic",
|
||||
name: "promise.resolve",
|
||||
args: [],
|
||||
type: { kind: "promise", inner: VOID },
|
||||
loc,
|
||||
},
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
const errorKind = crash.className === "%TypeError" ? 1 : 0;
|
||||
const body: IrStmt[] = [
|
||||
{ kind: "exprStmt", expr: hop, loc },
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: nodeThrowExpr(errorKind, crash.code ?? "", crash.message, VOID, loc),
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: dynUndefinedExpr(loc), loc },
|
||||
];
|
||||
const ctx = lowerer.ctx;
|
||||
lowerer.liftedFns.push({
|
||||
name: fnName,
|
||||
params: [],
|
||||
returnType: DYN,
|
||||
locals: ctx.locals,
|
||||
captures: ctx.captures ?? [],
|
||||
body,
|
||||
async: true,
|
||||
loc,
|
||||
});
|
||||
} finally {
|
||||
lowerer.fnStack.pop();
|
||||
}
|
||||
return {
|
||||
kind: "callValue",
|
||||
callee: { kind: "closure", fnName, captures: [], type: fnType, loc },
|
||||
args: [],
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The checker-resolved program source behind one literal import(). */
|
||||
function programImportTarget(lowerer: Lowerer, arg: ts.StringLiteralLike): ts.SourceFile | null {
|
||||
const modSym = lowerer.checker.getSymbolAtLocation(arg);
|
||||
|
||||
@@ -965,6 +965,8 @@ export interface LowerOptions {
|
||||
inlineRequireCycleCalls?: ReadonlySet<ts.Node> | undefined;
|
||||
/** LoadResult.moduleEdges. */
|
||||
moduleEdges?: ReadonlyMap<ts.SourceFile, readonly CycleEdge[]> | undefined;
|
||||
/** LoadResult.dynamicImportRejections. */
|
||||
dynamicImportRejections?: ReadonlyMap<ts.SourceFile, StartupCrash> | 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
|
||||
@@ -1086,6 +1088,8 @@ export interface LowererMode {
|
||||
dynNodeStreams?: boolean;
|
||||
/** LoadResult.inlineRequireCycleCalls. */
|
||||
inlineRequireCycleCalls?: ReadonlySet<ts.Node> | undefined;
|
||||
/** LoadResult.dynamicImportRejections. */
|
||||
dynamicImportRejections?: ReadonlyMap<ts.SourceFile, StartupCrash> | undefined;
|
||||
/** LowerOptions.libraryCallbacks (see there). */
|
||||
libraryCallbacks?: boolean;
|
||||
projectCheckJs?: boolean;
|
||||
@@ -1219,7 +1223,11 @@ 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 };
|
||||
const graph = {
|
||||
edges: options.moduleEdges,
|
||||
inlineCycleCalls: inlineRequireCycleCalls,
|
||||
rejected: options.dynamicImportRejections,
|
||||
};
|
||||
for (let pass = 0; pass < 32; pass++) {
|
||||
const before = moduleOrder.length;
|
||||
appendDynamicImportModules(program, moduleOrder, onExtraCycle, graph);
|
||||
@@ -1240,6 +1248,7 @@ export function lowerToIr(
|
||||
targetPlatform,
|
||||
startupCrash,
|
||||
inlineRequireCycleCalls,
|
||||
dynamicImportRejections: options.dynamicImportRejections,
|
||||
ffiImports,
|
||||
libraryCallbacks,
|
||||
forkTargets,
|
||||
@@ -1264,6 +1273,7 @@ export function lowerToIr(
|
||||
targetPlatform,
|
||||
startupCrash,
|
||||
inlineRequireCycleCalls,
|
||||
dynamicImportRejections: options.dynamicImportRejections,
|
||||
ffiImports,
|
||||
libraryCallbacks,
|
||||
forkTargets,
|
||||
@@ -1329,6 +1339,7 @@ export function lowerToIr(
|
||||
targetPlatform,
|
||||
startupCrash,
|
||||
inlineRequireCycleCalls,
|
||||
dynamicImportRejections: options.dynamicImportRejections,
|
||||
ffiImports,
|
||||
libraryCallbacks,
|
||||
forkTargets,
|
||||
@@ -1374,6 +1385,7 @@ export function lowerToIr(
|
||||
targetPlatform,
|
||||
startupCrash,
|
||||
inlineRequireCycleCalls,
|
||||
dynamicImportRejections: options.dynamicImportRejections,
|
||||
ffiImports,
|
||||
libraryCallbacks,
|
||||
forkTargets,
|
||||
@@ -1515,6 +1527,7 @@ export function lowerToIr(
|
||||
degradedTypes: resultLowerer.degradedTypes,
|
||||
retriedSymbols: resultLowerer.retriedSymbols,
|
||||
inlineRequireCycleCalls,
|
||||
dynamicImportRejections: options.dynamicImportRejections,
|
||||
targetPlatform,
|
||||
ffiImports,
|
||||
libraryCallbacks,
|
||||
@@ -2135,6 +2148,8 @@ export class Lowerer {
|
||||
/** Inline require() calls whose module edge closes a require cycle
|
||||
* (LoadResult.inlineRequireCycleCalls). */
|
||||
readonly inlineRequireCycleCalls: ReadonlySet<ts.Node>;
|
||||
/** LoadResult.dynamicImportRejections. */
|
||||
readonly dynamicImportRejections: ReadonlyMap<ts.SourceFile, StartupCrash>;
|
||||
/** Immutable callable aliases and compiler-generated adapters. The map
|
||||
* is symbol-keyed, so shadowing and cross-module imports keep the same
|
||||
* exact binding discipline as globals/functions. */
|
||||
@@ -3472,7 +3487,9 @@ export class Lowerer {
|
||||
* collection; lowerDynamicImportCall reads it per site). */
|
||||
readonly dynImports = new Map<
|
||||
string,
|
||||
{ kind: "program-module" } | { kind: "static-builtin"; module: string }
|
||||
| { kind: "program-module" }
|
||||
| { kind: "static-builtin"; module: string }
|
||||
| { kind: "rejected"; crash: StartupCrash }
|
||||
>();
|
||||
|
||||
readonly createRequireImports = new Map<
|
||||
@@ -4030,6 +4047,7 @@ export class Lowerer {
|
||||
checkedTable("fnSigsBySymbol", () => this.fnSigsBySymbol.size),
|
||||
checkedTable("cjsExportsAssignments", () => this.cjsExportsAssignments.size),
|
||||
checkedTable("inlineRequireCycleCalls", () => this.inlineRequireCycleCalls.size),
|
||||
checkedTable("dynamicImportRejections", () => this.dynamicImportRejections.size),
|
||||
checkedTable("staticCallables", () => this.staticCallables.size),
|
||||
checkedTable("genericFnsBySymbol", () => this.genericFnsBySymbol.size),
|
||||
checkedTable("objLitGenericFns", () => this.objLitGenericFns.size),
|
||||
@@ -4781,6 +4799,7 @@ export class Lowerer {
|
||||
this.forkTargets = mode.forkTargets ?? [];
|
||||
this.frontendServices = mode.frontendServices;
|
||||
this.inlineRequireCycleCalls = mode.inlineRequireCycleCalls ?? new Set();
|
||||
this.dynamicImportRejections = mode.dynamicImportRejections ?? new Map();
|
||||
this.libraryCallbacks = mode.libraryCallbacks ?? false;
|
||||
this.projectCheckJs = mode.projectCheckJs ?? false;
|
||||
this.reachable = mode.reachable ?? null;
|
||||
|
||||
@@ -208,6 +208,8 @@ function addDynamicImportGraph(load: LoadResult, scanned: Set<SourceFile>): void
|
||||
npmStaticDepSf7(load.program, sf, spec, kind);
|
||||
if (dep === null || scanned.has(dep)) continue;
|
||||
if (dep.fileName.includes("/node_modules/") && !isJsSourceFileName(dep.fileName)) continue;
|
||||
// An import() whose graph Node refuses rejects: nothing in it loads.
|
||||
if (kind === "import" && load.dynamicImportRejections?.has(dep)) continue;
|
||||
scanned.add(dep);
|
||||
added.add(dep);
|
||||
queue.push(dep);
|
||||
@@ -849,6 +851,7 @@ function loadFrontend(
|
||||
startupCrash: l.startupCrash ?? null,
|
||||
inlineRequireCycleCalls: l.inlineRequireCycleCalls,
|
||||
moduleEdges: l.moduleEdges,
|
||||
dynamicImportRejections: l.dynamicImportRejections,
|
||||
projectCheckJs: l.typeWorld?.projectCheckJs ?? false,
|
||||
externalTypes: l.externalTypes,
|
||||
externalTypeSpecifiersByFile: l.externalTypeSpecifiersByFile,
|
||||
|
||||
@@ -97,6 +97,7 @@ import {
|
||||
configurePackageShims,
|
||||
isPackageShimFile,
|
||||
isShimmedImport,
|
||||
isUnloadedPackage,
|
||||
npmStaticOmitsPackage,
|
||||
} from "./package-shims.js";
|
||||
import { provenanceEntryFor, provenancePaths } from "./provenance-registry.js";
|
||||
@@ -393,6 +394,11 @@ export interface LoadResult {
|
||||
* literal import(), fork() or Worker reaches with them. Filled by
|
||||
* checkPreflight. */
|
||||
moduleEdges?: ReadonlyMap<ts.SourceFile, readonly CycleEdge[]> | undefined;
|
||||
/** Program modules only a literal import() loads whose static graph
|
||||
* Node's resolution refuses: each such import() rejects with the refusal
|
||||
* instead of failing the build, and the module never joins the compiled
|
||||
* graph. Filled by checkPreflight. */
|
||||
dynamicImportRejections?: ReadonlyMap<ts.SourceFile, StartupCrash> | 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
|
||||
@@ -545,6 +551,8 @@ export interface StartupCrash {
|
||||
message: string;
|
||||
className: "%Error" | "%TypeError" | "%SyntaxError";
|
||||
loc: { file: string; start: number; end: number };
|
||||
/** Node's error code, for the refusals that carry one. */
|
||||
code?: string;
|
||||
}
|
||||
|
||||
/** True when a coverage mapping names one exact bare host module. TypeScript
|
||||
@@ -1467,12 +1475,19 @@ function declBoundaryDroppedDiag(file: string): ScrDiagnostic {
|
||||
* 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, moduleEdges } =
|
||||
preflight7(load);
|
||||
const {
|
||||
diags,
|
||||
moduleOrder,
|
||||
startupCrash,
|
||||
inlineRequireCycleCalls,
|
||||
moduleEdges,
|
||||
dynamicImportRejections,
|
||||
} = preflight7(load);
|
||||
load.moduleOrder = moduleOrder;
|
||||
load.startupCrash = startupCrash;
|
||||
load.inlineRequireCycleCalls = inlineRequireCycleCalls;
|
||||
load.moduleEdges = moduleEdges;
|
||||
load.dynamicImportRejections = dynamicImportRejections;
|
||||
return diags;
|
||||
}
|
||||
|
||||
@@ -5411,6 +5426,7 @@ function preflight7(load: LoadResult): {
|
||||
startupCrash: StartupCrash | null;
|
||||
inlineRequireCycleCalls: ReadonlySet<ts.Node>;
|
||||
moduleEdges?: ReadonlyMap<ts.SourceFile, readonly CycleEdge[]>;
|
||||
dynamicImportRejections?: ReadonlyMap<ts.SourceFile, StartupCrash>;
|
||||
} {
|
||||
const { program, entry } = load;
|
||||
const diags: ScrDiagnostic[] = [...load.configDiags];
|
||||
@@ -5968,9 +5984,10 @@ function preflight7(load: LoadResult): {
|
||||
message: string,
|
||||
className: StartupCrash["className"],
|
||||
fallbackFeature: string,
|
||||
code?: string,
|
||||
): void => {
|
||||
refusals.get(sf)!.push({
|
||||
crash: { message, className, loc: locOf7(stmt) },
|
||||
crash: { message, className, loc: locOf7(stmt), ...(code !== undefined ? { code } : {}) },
|
||||
fallback: unsupportedDiag("SC1010", locOf7(stmt), fallbackFeature),
|
||||
pos: ts.tokenStartOfNode(stmt),
|
||||
});
|
||||
@@ -6013,6 +6030,7 @@ function preflight7(load: LoadResult): {
|
||||
refusal.message,
|
||||
"%Error",
|
||||
`the '${spec}' package (its runtime resolution fails: ${refusal.message})`,
|
||||
refusal.code,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
@@ -6050,6 +6068,7 @@ function preflight7(load: LoadResult): {
|
||||
`Invalid module "#" is not a valid internal imports specifier name imported from ${sf.fileName}`,
|
||||
"%TypeError",
|
||||
`the '#' import (Node rejects the bare '#' specifier as invalid)`,
|
||||
"ERR_INVALID_MODULE_SPECIFIER",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -6070,6 +6089,7 @@ function preflight7(load: LoadResult): {
|
||||
`Package import specifier "${spec}" is not defined${pkgJson !== null ? ` in package ${pkgJson}` : ""} imported from ${sf.fileName}`,
|
||||
"%TypeError",
|
||||
`the '${spec}' import (no package.json imports-field entry maps it)`,
|
||||
"ERR_PACKAGE_IMPORT_NOT_DEFINED",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -6078,7 +6098,7 @@ function preflight7(load: LoadResult): {
|
||||
const ambientNote = ambientDeclared(spec)
|
||||
? `the '${spec}' module (it exists only as an ambient 'declare module' type surface — there is no runtime module to import)`
|
||||
: `the '${spec}' package (nothing installed resolves it)`;
|
||||
refuse(refusal.message, "%Error", ambientNote);
|
||||
refuse(refusal.message, "%Error", ambientNote, refusal.code);
|
||||
continue;
|
||||
}
|
||||
// Runtime-resolvable (or a shape the probe stays conservative
|
||||
@@ -6544,6 +6564,7 @@ function preflight7(load: LoadResult): {
|
||||
// them). A found resolution crash wins over the CJS link check below:
|
||||
// Node's fetch/resolve phase runs before instantiate.
|
||||
let resolveCrash: StartupCrash | null = null;
|
||||
const dynamicImportRejections = new Map<ts.SourceFile, StartupCrash>();
|
||||
{
|
||||
const seen = new Set<ts.SourceFile>();
|
||||
const walk = (sf: ts.SourceFile): StartupCrash | null => {
|
||||
@@ -6563,6 +6584,45 @@ function preflight7(load: LoadResult): {
|
||||
return null;
|
||||
};
|
||||
resolveCrash = walk(entry);
|
||||
// A module only a literal import() loads resolves its static graph when
|
||||
// that import runs: Node rejects the import's promise with the first
|
||||
// refusal it meets, before any module of that graph evaluates. Such a
|
||||
// module never joins the compiled graph, and the refusals inside its
|
||||
// graph are that rejection, not build failures.
|
||||
const reach = (from: ts.SourceFile, into: Set<ts.SourceFile>): void => {
|
||||
const stack = [from];
|
||||
while (stack.length > 0) {
|
||||
const sf = stack.pop()!;
|
||||
if (into.has(sf)) continue;
|
||||
into.add(sf);
|
||||
for (const { dep } of depPositions.get(sf) ?? []) stack.push(dep);
|
||||
}
|
||||
};
|
||||
const startup = new Set<ts.SourceFile>();
|
||||
reach(entry, startup);
|
||||
const covered = new Set<ts.SourceFile>();
|
||||
for (const sf of userFiles) {
|
||||
if (!sf.text.includes("import")) continue;
|
||||
for (const call of moduleSourceCandidates(program, sf).calls) {
|
||||
const arg = call.arguments.nodes[0];
|
||||
if (call.expression.kind !== ts.SyntaxKind.ImportKeyword || arg === undefined) continue;
|
||||
if (!ts.isStringLiteralLike(arg) || isUnloadedPackage(arg.text)) continue;
|
||||
const root =
|
||||
resolveImport7(program, sf, arg.text) ?? npmStaticDepSf7(program, sf, arg.text);
|
||||
if (root === null || root.isDeclarationFile || startup.has(root)) continue;
|
||||
// A module inside an already-rejected graph is `covered`, but a
|
||||
// literal import() of it needs its OWN rejection entry: its graph
|
||||
// rejects when that import runs, so it must not join the compiled
|
||||
// graph (where lowering would fail) merely because another rejected
|
||||
// import also reaches it. Walk each direct root independently.
|
||||
if (dynamicImportRejections.has(root)) continue;
|
||||
seen.clear();
|
||||
const crash = walk(root);
|
||||
if (crash === null) continue;
|
||||
dynamicImportRejections.set(root, crash);
|
||||
reach(root, covered);
|
||||
}
|
||||
}
|
||||
// The crash channel engages only for programs that OTHERWISE pass
|
||||
// preflight: when any other diagnostic already fails the build, every
|
||||
// refusal keeps its compile-fence fallback instead — the report names
|
||||
@@ -6570,7 +6630,8 @@ function preflight7(load: LoadResult): {
|
||||
// get a diagnostic. (No reachable refusal → same: fallbacks only.)
|
||||
if (resolveCrash === null || diags.length > 0) {
|
||||
resolveCrash = null;
|
||||
for (const cands of refusals.values()) {
|
||||
for (const [sf, cands] of refusals) {
|
||||
if (covered.has(sf) && !startup.has(sf)) continue;
|
||||
for (const c of cands) diags.push(c.fallback);
|
||||
}
|
||||
}
|
||||
@@ -6591,6 +6652,7 @@ function preflight7(load: LoadResult): {
|
||||
startupCrash: resolveCrash ?? linkCrash,
|
||||
inlineRequireCycleCalls,
|
||||
moduleEdges: edges,
|
||||
dynamicImportRejections,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "./npm-static.js";
|
||||
import {
|
||||
clearPackageShimCaches,
|
||||
isUnloadedPackage,
|
||||
packageShimFor,
|
||||
throughBundledShimForwarder,
|
||||
} from "./package-shims.js";
|
||||
@@ -1026,6 +1027,9 @@ export function resolveWorkspaceSourceModule(
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
// A workspace package the project marks never loaded (package-shims.ts)
|
||||
// resolves like a missing one.
|
||||
if (isUnloadedPackage(specifier)) return null;
|
||||
const source = resolveBareModule(fromFile, specifier, "runtime-source");
|
||||
if (
|
||||
source?.workspaceDir === undefined ||
|
||||
|
||||
@@ -2287,6 +2287,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
return viaConditional;
|
||||
}
|
||||
}
|
||||
const viaDistribution = mapDistributedConditional(type, ctx);
|
||||
if (viaDistribution !== null) {
|
||||
contextResolutions++;
|
||||
return viaDistribution;
|
||||
}
|
||||
}
|
||||
}
|
||||
const widened = checker.getBaseTypeOfLiteralType(type);
|
||||
@@ -5299,11 +5304,19 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
!checker.isNeverType(part) &&
|
||||
!isEmptyObjectLiteralType(part, ctx),
|
||||
);
|
||||
let neverConditionalPart = false;
|
||||
let undefinedPart: ts.Type | undefined;
|
||||
for (const part of parts) {
|
||||
if (hasArrayPart && emptyTuple(part)) continue;
|
||||
// TypeScript's client can retain impossible intersections in a
|
||||
// distributed union. They have no inhabitants and no runtime tag.
|
||||
if (checker.isNeverType(part)) continue;
|
||||
// A conditional over a bound parameter whose every member selects
|
||||
// `never` contributes no arm either.
|
||||
if (distributedConditionalParts(part, ctx)?.length === 0) {
|
||||
neverConditionalPart = true;
|
||||
continue;
|
||||
}
|
||||
// A `void` PART is inhabited only by undefined (`Promise<void> |
|
||||
// void` return types, `string | void`): it becomes the undefinedT
|
||||
// unit arm, exactly like an undefined part — the value either
|
||||
@@ -5311,6 +5324,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// void stays VOID (the return-position mapping above).
|
||||
if (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) {
|
||||
byKey.set(typeKey(UNDEFINED_T), UNDEFINED_T);
|
||||
undefinedPart ??= part;
|
||||
continue;
|
||||
}
|
||||
if (part.flags & ts.TypeFlags.Null) {
|
||||
@@ -5353,6 +5367,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
}
|
||||
const arms = [...byKey.values()];
|
||||
if (arms.length === 0) return F64; // same unreachable placeholder as standalone never
|
||||
// `Cond<T> | undefined` whose conditional selects never for the
|
||||
// instance's binding is the plain `undefined` the concrete
|
||||
// instantiation's optional member has.
|
||||
if (neverConditionalPart && arms.length === 1 && undefinedPart !== undefined)
|
||||
return mapType(undefinedPart, ctx);
|
||||
// A single surviving UNIT arm cannot stand alone (degenerate — the
|
||||
// checker collapsed everything else away); anything else single is
|
||||
// just that type. A single arm UNDER A MINTED PLACEHOLDER cannot
|
||||
@@ -6439,6 +6458,104 @@ function mapGenericIndexedAccess(type: ts.Type, ctx: TypeMapperCtx): IrType | nu
|
||||
return { kind: "union", unionId: ctx.unions.intern(arms) };
|
||||
}
|
||||
|
||||
/** A distributive conditional (`T extends "a" ? A : T extends "b" ? B :
|
||||
* never`, its check type a naked type parameter) over a parameter the
|
||||
* instance binds: evaluated once per member of the binding (`"a" | "b"`,
|
||||
* or the open `"a" | (string & {})` spelling), as the checker distributes
|
||||
* it. Answers the non-never results (empty when every member selects
|
||||
* never, so an optional member of that type holds only undefined, as in
|
||||
* the concrete instantiation), or null when the conditional is another
|
||||
* shape or a member does not evaluate. */
|
||||
function distributedConditionalParts(type: ts.Type, ctx: TypeMapperCtx): ts.Type[] | null {
|
||||
const { checker, resolveTypeParamTs } = ctx;
|
||||
if (!resolveTypeParamTs || !type.isConditionalType()) return null;
|
||||
const checkType = type.getCheckType();
|
||||
if (!checkType || !(checkType.flags & ts.TypeFlags.TypeParameter)) return null;
|
||||
const symbol = checkType.getSymbol();
|
||||
const bound = resolveTypeParamTs(checkType);
|
||||
if (!symbol || !bound) return null;
|
||||
// The answer depends on the instance's binding: a mapping that consults
|
||||
// it, even one that drops an all-never part, is not context-free.
|
||||
contextResolutions++;
|
||||
const parts: ts.Type[] = [];
|
||||
for (const member of bound.isUnionType() ? ts.constituentTypes(bound) : [bound]) {
|
||||
// `string & {}` (the open-union spelling) extends exactly what `string`
|
||||
// extends: the empty object adds nothing to a primitive.
|
||||
const emptyObject = (part: ts.Type): boolean =>
|
||||
(part.flags & ts.TypeFlags.Object) !== 0 &&
|
||||
checker.getPropertiesOfType(part).length === 0 &&
|
||||
checker.getIndexInfosOfType(part).length === 0 &&
|
||||
checker.getCallSignatures(part).length === 0 &&
|
||||
checker.getConstructSignatures(part).length === 0;
|
||||
const primitive = member.isIntersectionType()
|
||||
? ts.constituentTypes(member).filter((part) => !emptyObject(part))
|
||||
: [member];
|
||||
if (primitive.length !== 1) return null;
|
||||
const evaluated = evalMappedTemplate(type, new Map(), new Map([[symbol, primitive[0]!]]), ctx);
|
||||
if (!evaluated) return null;
|
||||
if (checker.isNeverType(evaluated)) continue;
|
||||
if (!parts.includes(evaluated)) parts.push(evaluated);
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
/** A unit-only type once its conditionals over bound parameters are
|
||||
* distributed: `Cond<T> | undefined` where every member of T's binding
|
||||
* selects never (the optional member's type in a generic body). */
|
||||
function neverConditionalUnitOnly(t: ts.Type, ctx: TypeMapperCtx): boolean {
|
||||
const UNIT = ts.TypeFlags.Undefined | ts.TypeFlags.Void | ts.TypeFlags.Null;
|
||||
if (!t.isUnionType()) return false;
|
||||
let conditional = false;
|
||||
for (const part of ts.constituentTypes(t)) {
|
||||
if ((part.flags & UNIT) !== 0) continue;
|
||||
if (distributedConditionalParts(part, ctx)?.length !== 0) return false;
|
||||
conditional = true;
|
||||
}
|
||||
return conditional;
|
||||
}
|
||||
|
||||
/** mapType of a distributed conditional (distributedConditionalParts): the
|
||||
* union of its members' results, as the concrete conditional's union maps
|
||||
* them. An `unknown` or `any` result absorbs the rest (the checker's union
|
||||
* is that type), as does a result that maps to the checked-dynamic value
|
||||
* (`{}`, `object`, `Object`: the checker's union of one with a concrete
|
||||
* result is the top type), and `undefined` and `null` results are the unit
|
||||
* arms. Null when it does not distribute, when a result does not map, when
|
||||
* every member selects never (the enclosing union drops such a part), or
|
||||
* when only units remain. */
|
||||
function mapDistributedConditional(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
const parts = distributedConditionalParts(type, ctx);
|
||||
if (!parts || parts.length === 0) return null;
|
||||
const top = parts.find((part) => (part.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0);
|
||||
if (top !== undefined) return mapType(top, ctx);
|
||||
const arms: IrType[] = [];
|
||||
for (const part of parts) {
|
||||
if (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) {
|
||||
if (!arms.some((a) => typeEquals(a, UNDEFINED_T))) arms.push(UNDEFINED_T);
|
||||
continue;
|
||||
}
|
||||
if (part.flags & ts.TypeFlags.Null) {
|
||||
if (!arms.some((a) => typeEquals(a, NULL_T))) arms.push(NULL_T);
|
||||
continue;
|
||||
}
|
||||
const mapped = mapType(part, ctx);
|
||||
if (!mapped) return null;
|
||||
// A top-type result (`{}`, `object`, `Object`): the checker's union with
|
||||
// a concrete result is the top type itself, so the member is
|
||||
// checked-dynamic. A `dyn` arm would be invalid union IR (SC9001).
|
||||
if (mapped.kind === "dyn") return DYN;
|
||||
const members =
|
||||
mapped.kind === "union" ? (ctx.unions.get(mapped.unionId)?.arms ?? null) : [mapped];
|
||||
if (!members) return null;
|
||||
for (const arm of members) if (!arms.some((a) => typeEquals(a, arm))) arms.push(arm);
|
||||
}
|
||||
if (arms.every(isUnitType)) return null;
|
||||
if (arms.length === 1) return arms[0]!;
|
||||
if (!unionContainerArmsOk(arms)) return null;
|
||||
arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
|
||||
return { kind: "union", unionId: ctx.unions.intern(arms) };
|
||||
}
|
||||
|
||||
/** `keyof T extends never ? A : B` over a bound record T (the lib's
|
||||
* Object.entries element spelling): decided by whether the binding has any
|
||||
* key. */
|
||||
@@ -8569,7 +8686,10 @@ function mapRecordTypeInner(
|
||||
// absent-field idiom — and `{ p: undefined }` spellings): the
|
||||
// unit-only union. The runtime value is the interned unit, JSON
|
||||
// omits the undefined arm exactly like an omitted optional.
|
||||
if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam))
|
||||
if (
|
||||
pt?.kind === "void" &&
|
||||
(isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam) || neverConditionalUnitOnly(fieldTs, ctx))
|
||||
)
|
||||
pt = unitOnlyUnion(ctx.unions);
|
||||
if (pt !== null && isUnitOnlyIr(pt, ctx) && jsObjectLiteralProperty(p)) pt = DYN;
|
||||
if (pt?.kind === "array" && jsNeverArrayProp(p, fieldTs, checker)) pt = DYN;
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/generic-conditional-member-bindings.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/generic-conditional-member-units.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/unloaded-dynamic-import/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Members typed by a distributive conditional over a generic parameter
|
||||
// (`opts?: T extends "alpha" ? AlphaOpts : ...`), read inside the generic
|
||||
// body: the instance's binding decides the layout, member by member of a
|
||||
// literal union, and an all-never binding leaves an optional member that
|
||||
// holds only undefined, as the concrete instantiation's member does.
|
||||
|
||||
type KnownApi = "alpha" | "beta" | "gamma";
|
||||
type Api = KnownApi | (string & {});
|
||||
|
||||
interface AlphaOpts {
|
||||
fast?: boolean;
|
||||
}
|
||||
interface BetaOpts {
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
interface Base<TApi extends string> {
|
||||
id: string;
|
||||
api: TApi;
|
||||
}
|
||||
|
||||
interface Item<TApi extends Api> extends Base<TApi> {
|
||||
kind?: "item";
|
||||
opts?: TApi extends "alpha" ? AlphaOpts : TApi extends "beta" ? BetaOpts : never;
|
||||
}
|
||||
|
||||
interface Other extends Base<string> {
|
||||
kind: "other";
|
||||
size: number;
|
||||
}
|
||||
|
||||
type AnyItem<TApi extends Api = Api> = Item<TApi> | Other;
|
||||
|
||||
interface Options<TApi extends Api> {
|
||||
name: string;
|
||||
items: readonly AnyItem<TApi>[];
|
||||
}
|
||||
|
||||
interface Built<TApi extends Api> {
|
||||
name: string;
|
||||
all: () => readonly AnyItem<TApi>[];
|
||||
chat: () => Item<TApi>[];
|
||||
add: (item: AnyItem<TApi>) => void;
|
||||
}
|
||||
|
||||
function describe(item: Item<Api>): string {
|
||||
return `${item.id}:${item.api}:${item.opts === undefined ? "-" : JSON.stringify(item.opts)}`;
|
||||
}
|
||||
|
||||
function build<TApi extends Api = Api>(input: Options<TApi>): Built<TApi> {
|
||||
const baseline = input.items;
|
||||
let dynamic: readonly AnyItem<TApi>[] = [];
|
||||
const current = (): readonly AnyItem<TApi>[] => {
|
||||
const merged = [...baseline];
|
||||
for (const item of dynamic) merged.push(item);
|
||||
return merged;
|
||||
};
|
||||
return {
|
||||
name: input.name,
|
||||
all: current,
|
||||
chat: () => current().filter((item): item is Item<TApi> => item.kind !== "other"),
|
||||
add: (item) => {
|
||||
dynamic = [...dynamic, item];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function report<TApi extends Api>(built: Built<TApi>): void {
|
||||
console.log(`${built.name}: ${built.all().length} items, ${built.chat().length} chat`);
|
||||
for (const item of built.chat()) console.log(` ${describe(item)}`);
|
||||
}
|
||||
|
||||
const mixed: Built<"alpha" | "gamma"> = build({
|
||||
name: "mixed",
|
||||
items: [
|
||||
{ id: "a1", api: "alpha", opts: { fast: true } },
|
||||
{ id: "g1", api: "gamma" },
|
||||
],
|
||||
});
|
||||
mixed.add({ id: "a2", api: "alpha" });
|
||||
report(mixed);
|
||||
|
||||
const beta: Built<"beta"> = build({
|
||||
name: "beta",
|
||||
items: [
|
||||
{ id: "b1", api: "beta", opts: { depth: 2 } },
|
||||
{ id: "o1", api: "other", kind: "other", size: 1 },
|
||||
],
|
||||
});
|
||||
beta.add({ id: "o2", api: "other", kind: "other", size: 3 });
|
||||
report(beta);
|
||||
|
||||
const custom = build<string>({ name: "custom", items: [{ id: "c1", api: "custom" }] });
|
||||
custom.add({ id: "c2", api: "custom" });
|
||||
report(custom);
|
||||
|
||||
const open = build({ name: "open", items: [{ id: "x1", api: "external" }] });
|
||||
report(open);
|
||||
const first = open.chat()[0];
|
||||
console.log(first === undefined ? "none" : first.opts === undefined ? "no opts" : "opts");
|
||||
@@ -0,0 +1,75 @@
|
||||
// Members typed by a distributive conditional over a generic parameter whose
|
||||
// binding selects `unknown`, `{}`, `undefined` or `null` for some members,
|
||||
// read inside the generic body. The checker's union of the results decides the
|
||||
// layout: `unknown` and `{}` absorb the other results, and `undefined` and
|
||||
// `null` are the union's unit arms, as in the concrete instantiation.
|
||||
|
||||
type Kind = "alpha" | "beta" | "gamma" | (string & {});
|
||||
|
||||
interface BetaOpts {
|
||||
depth: number;
|
||||
}
|
||||
|
||||
interface Loose<K extends Kind> {
|
||||
kind: K;
|
||||
meta: K extends "alpha" ? unknown : K extends "beta" ? BetaOpts : never;
|
||||
}
|
||||
|
||||
interface Empty<K extends Kind> {
|
||||
kind: K;
|
||||
meta: K extends "alpha" ? {} : K extends "beta" ? BetaOpts : never;
|
||||
}
|
||||
|
||||
interface Units<K extends Kind> {
|
||||
kind: K;
|
||||
meta: K extends "alpha" ? undefined : K extends "beta" ? BetaOpts : K extends "gamma" ? null : never;
|
||||
}
|
||||
|
||||
function looseMetas<K extends Kind>(items: readonly Loose<K>[]): string {
|
||||
let out = "";
|
||||
for (const item of items) out += `${item.kind}:${JSON.stringify(item.meta)} `;
|
||||
return out;
|
||||
}
|
||||
|
||||
function emptyMetas<K extends Kind>(items: readonly Empty<K>[]): string {
|
||||
let out = "";
|
||||
for (const item of items) {
|
||||
// The local holds the member's type, so the `{}` arm's mapping is
|
||||
// emitted (a bare read would convert to dyn at the use site).
|
||||
const meta = item.meta;
|
||||
out += `${item.kind}:${JSON.stringify(meta)} `;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function unitMetas<K extends Kind>(items: readonly Units<K>[]): string {
|
||||
let out = "";
|
||||
for (const item of items) {
|
||||
const meta = item.meta;
|
||||
out += `${item.kind}:${meta === undefined ? "u" : meta === null ? "n" : meta.depth} `;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const loose: Loose<"alpha" | "beta">[] = [
|
||||
{ kind: "alpha", meta: "free" },
|
||||
{ kind: "alpha", meta: [1, 2] },
|
||||
{ kind: "beta", meta: { depth: 2 } },
|
||||
];
|
||||
console.log(looseMetas(loose));
|
||||
|
||||
const empty: Empty<"alpha" | "beta">[] = [
|
||||
{ kind: "alpha", meta: { free: true } },
|
||||
{ kind: "alpha", meta: "text" },
|
||||
{ kind: "beta", meta: { depth: 4 } },
|
||||
];
|
||||
console.log(emptyMetas(empty));
|
||||
console.log(JSON.stringify(empty));
|
||||
|
||||
const units: Units<"alpha" | "beta" | "gamma">[] = [
|
||||
{ kind: "alpha", meta: undefined },
|
||||
{ kind: "beta", meta: { depth: 3 } },
|
||||
{ kind: "gamma", meta: null },
|
||||
];
|
||||
console.log(unitMetas(units));
|
||||
console.log(JSON.stringify(units));
|
||||
@@ -0,0 +1,9 @@
|
||||
// A dependency of the import()-only `lazy.ts` graph below. Its own static
|
||||
// import names a package the project marks unloaded, so the graph rejects
|
||||
// when `./lazy.ts` is imported — and equally when `./dep.ts` is imported
|
||||
// directly.
|
||||
import { heavy } from "gtunloadedheavy";
|
||||
|
||||
export function helper(text: string): string {
|
||||
return heavy(text);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { heavy } from "gtunloadedheavy";
|
||||
import { helper } from "./dep.ts";
|
||||
|
||||
console.log("lazy evaluated");
|
||||
|
||||
export function run(text: string): string {
|
||||
return helper(text) + heavy(text);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/// <reference path="./types.d.ts" />
|
||||
// Packages the project marks never loaded
|
||||
// (package.json: "scriptc": { "packages": { ...: "unloaded" } }) behind
|
||||
// import(): a workspace package imported on demand, and a module only
|
||||
// import() loads whose static import names such a package. Each import()
|
||||
// rejects with Node's ERR_MODULE_NOT_FOUND, exactly as when the package is
|
||||
// not installed, and no module of the refused graph evaluates.
|
||||
// tests/harness/package-shims.test.ts compiles this program with both
|
||||
// packages installed and checks the output is unchanged.
|
||||
function report(what: string, err: unknown): void {
|
||||
const e = err as { code?: string; message: string };
|
||||
const [reason, importer] = e.message.split(" imported from ");
|
||||
console.log(what, e.code, reason, importer?.slice(importer.lastIndexOf("/") + 1));
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log("start");
|
||||
try {
|
||||
const { parse } = await import("wsunloadedparser");
|
||||
console.log("parsed", parse("a"));
|
||||
} catch (err) {
|
||||
report("parser:", err);
|
||||
}
|
||||
try {
|
||||
const lazy = await import("./lazy.ts");
|
||||
console.log(lazy.run("b"));
|
||||
} catch (err) {
|
||||
report("lazy:", err);
|
||||
}
|
||||
// `dep.ts` is a dependency of the already-rejected `./lazy.ts` graph, but
|
||||
// this literal import() targets it directly: its own graph rejects too, so
|
||||
// it must reject at run time instead of joining the compiled graph.
|
||||
try {
|
||||
const dep = await import("./dep.ts");
|
||||
console.log(dep.helper("c"));
|
||||
} catch (err) {
|
||||
report("dep:", err);
|
||||
}
|
||||
console.log("end");
|
||||
}
|
||||
main();
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scriptc": {
|
||||
"packages": {
|
||||
"wsunloadedparser": "unloaded",
|
||||
"gtunloadedheavy": "unloaded"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Types for the two packages the project marks never loaded. Neither is
|
||||
// installed here; tests/harness/package-shims.test.ts installs both.
|
||||
declare module "wsunloadedparser" {
|
||||
export function parse(text: string): string;
|
||||
}
|
||||
declare module "gtunloadedheavy" {
|
||||
export function heavy(text: string): string;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 14775, "san": 16991 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3600, "san": 5500 }
|
||||
@@ -4,6 +4,9 @@
|
||||
* - a package the project marks unloaded stays unloaded when it IS
|
||||
* installed: the native program prints what Node prints without the
|
||||
* package, and the package's code never runs;
|
||||
* - the same behind import(): an unloaded workspace package, and a module
|
||||
* only import() loads that imports an unloaded package, reject with
|
||||
* Node's ERR_MODULE_NOT_FOUND;
|
||||
* - a polyfill package with a builtin alias that the project marks unloaded
|
||||
* stays unloaded: its require throws MODULE_NOT_FOUND;
|
||||
* - a shimmed package that is not installed, or whose major version has no
|
||||
@@ -22,7 +25,15 @@
|
||||
* Node running the real package.
|
||||
*/
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import { copyFileSync, cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import {
|
||||
copyFileSync,
|
||||
cpSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
@@ -116,6 +127,57 @@ describe("package shims", () => {
|
||||
expect(await run(result.binaryPath, [], dir)).toBe(expected);
|
||||
}, 180_000);
|
||||
|
||||
test("unloaded packages behind import() stay unloaded, even when installed", async () => {
|
||||
const original = join(casesRoot, "unloaded-dynamic-import");
|
||||
const expected = await run(process.execPath, [join(original, "main.ts")], original);
|
||||
expect(expected).toContain("parser: ERR_MODULE_NOT_FOUND");
|
||||
expect(expected).toContain("lazy: ERR_MODULE_NOT_FOUND");
|
||||
expect(expected).toContain("dep: ERR_MODULE_NOT_FOUND");
|
||||
|
||||
const dir = copyCase("unloaded-dynamic-import", "unloaded-dynamic-installed");
|
||||
const write = (path: string, text: string) => {
|
||||
mkdirSync(join(dir, path, ".."), { recursive: true });
|
||||
writeFileSync(join(dir, path), text);
|
||||
};
|
||||
// A workspace package (linked into node_modules from outside it) and an
|
||||
// installed ES module package.
|
||||
write(
|
||||
"workspace/parser/package.json",
|
||||
'{ "name": "wsunloadedparser", "version": "1.0.0", "type": "module", "main": "./index.ts" }\n',
|
||||
);
|
||||
write(
|
||||
"workspace/parser/index.ts",
|
||||
'console.log("parser loaded");\nexport function parse(text: string): string {\n return `parsed:${text}`;\n}\n',
|
||||
);
|
||||
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
||||
symlinkSync("../workspace/parser", join(dir, "node_modules/wsunloadedparser"));
|
||||
write(
|
||||
"node_modules/gtunloadedheavy/package.json",
|
||||
'{ "name": "gtunloadedheavy", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }\n',
|
||||
);
|
||||
write(
|
||||
"node_modules/gtunloadedheavy/index.js",
|
||||
'console.log("heavy loaded");\nexport function heavy(text) {\n return `heavy:${text}`;\n}\n',
|
||||
);
|
||||
write(
|
||||
"node_modules/gtunloadedheavy/index.d.ts",
|
||||
"export declare function heavy(text: string): string;\n",
|
||||
);
|
||||
// Control: Node loads both.
|
||||
const loaded = await run(process.execPath, [join(dir, "main.ts")], dir);
|
||||
expect(loaded).toContain("parsed parsed:a");
|
||||
expect(loaded).toContain("heavy:b");
|
||||
|
||||
const out = join(dir, ".scriptc");
|
||||
const result = await compile(join(dir, "main.ts"), {
|
||||
outPath: join(out, "program"),
|
||||
outDir: out,
|
||||
sanitize,
|
||||
});
|
||||
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
|
||||
expect(await run(result.binaryPath, [], dir)).toBe(expected);
|
||||
}, 180_000);
|
||||
|
||||
test("a CommonJS package requiring fs-extra compiles the shim", async () => {
|
||||
const dir = join(scratch, "fs-extra-require");
|
||||
cpSync(fsExtraModules, join(dir, "node_modules"), { recursive: true });
|
||||
|
||||
Reference in New Issue
Block a user