Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions packages/compiler/src/frontend/lowering/lower-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,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,
Expand Down Expand Up @@ -162,6 +164,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
Expand Down Expand Up @@ -408,7 +412,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);
Expand Down Expand Up @@ -684,6 +689,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 (
Expand All @@ -704,6 +730,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 &&
Expand Down
72 changes: 71 additions & 1 deletion packages/compiler/src/frontend/lowering/lower-stdlib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ import {
unsupportedNativeApiDiag,
unsupportedPackageValueDiag,
} from "../../diagnostics/diagnostic.js";
import { esmNamedImportLinkCrash, isCjsJsFile, locOf, npmPackageNameOf } from "../program.js";
import {
esmNamedImportLinkCrash,
isCjsJsFile,
locOf,
npmPackageNameOf,
type StartupCrash,
} from "../program.js";
import { foldedStringKeyOf, lowerDynObjectLiteral } from "./expressions/object-literals.js";
import { noteEventListeners } from "./lower-event-target.js";
import { PoisonError, newFnCtx, own } from "./lowerer.js";
Expand Down Expand Up @@ -2169,6 +2175,7 @@ export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression
}
const res = lowerer.dynImports.get(`${call.getSourceFile().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);
}
Expand All @@ -2178,6 +2185,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 = `%fn${lowerer.lambdaCounter++}_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);
Expand Down
21 changes: 19 additions & 2 deletions packages/compiler/src/frontend/lowering/lowerer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,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
Expand Down Expand Up @@ -877,6 +879,8 @@ export interface LowererMode {
frontendServices?: FrontendServices | undefined;
/** LoadResult.inlineRequireCycleCalls. */
inlineRequireCycleCalls?: ReadonlySet<ts.Node> | undefined;
/** LoadResult.dynamicImportRejections. */
dynamicImportRejections?: ReadonlyMap<ts.SourceFile, StartupCrash> | undefined;
/** LowerOptions.libraryCallbacks (see there). */
libraryCallbacks?: boolean;
projectCheckJs?: boolean;
Expand Down Expand Up @@ -978,7 +982,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);
Expand All @@ -999,6 +1007,7 @@ export function lowerToIr(
targetPlatform,
startupCrash,
inlineRequireCycleCalls,
dynamicImportRejections: options.dynamicImportRejections,
ffiImports,
libraryCallbacks,
forkTargets,
Expand All @@ -1023,6 +1032,7 @@ export function lowerToIr(
targetPlatform,
startupCrash,
inlineRequireCycleCalls,
dynamicImportRejections: options.dynamicImportRejections,
ffiImports,
libraryCallbacks,
forkTargets,
Expand Down Expand Up @@ -1054,6 +1064,7 @@ export function lowerToIr(
targetPlatform,
startupCrash,
inlineRequireCycleCalls,
dynamicImportRejections: options.dynamicImportRejections,
ffiImports,
libraryCallbacks,
forkTargets,
Expand Down Expand Up @@ -1113,6 +1124,7 @@ export function lowerToIr(
remainder: true,
alreadyFlushed: resultLowerer.flushedSymbols,
inlineRequireCycleCalls,
dynamicImportRejections: options.dynamicImportRejections,
targetPlatform,
ffiImports,
libraryCallbacks,
Expand Down Expand Up @@ -1701,6 +1713,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. */
Expand Down Expand Up @@ -2667,7 +2681,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<
Expand Down Expand Up @@ -3020,6 +3036,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;
Expand Down
3 changes: 3 additions & 0 deletions packages/compiler/src/frontend/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,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);
Expand Down Expand Up @@ -688,6 +690,7 @@ function loadFrontend(
startupCrash: finalLoad.startupCrash ?? null,
inlineRequireCycleCalls: finalLoad.inlineRequireCycleCalls,
moduleEdges: finalLoad.moduleEdges,
dynamicImportRejections: finalLoad.dynamicImportRejections,
projectCheckJs: finalLoad.typeWorld?.projectCheckJs ?? false,
externalTypes: finalLoad.externalTypes,
externalTypeSpecifiersByFile: finalLoad.externalTypeSpecifiersByFile,
Expand Down
Loading
Loading