feat(compiler): add Node module resolution introspection (#322)

This commit is contained in:
Chris Tate
2026-09-14 18:15:44 -05:00
committed by GitHub
parent 2bfc37b138
commit 2ac8743dbb
36 changed files with 820 additions and 147 deletions
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.15.0",
"nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119",
"artifactVersion": "66b252bc9bee6f91f667",
"artifactVersion": "039dcd2c69e09f0e38f2",
"rowCount": 3662
}
+26 -26
View File
@@ -602,18 +602,18 @@
"apiEntries": 27,
"static": {
"supported": 0,
"partial": 13,
"partial": 15,
"refused": 0,
"not-implemented": 11,
"not-implemented": 9,
"by-design": 0,
"unreviewed": 3,
"not-applicable": 0
},
"dynamic": {
"supported": 0,
"partial": 18,
"partial": 20,
"refused": 0,
"not-implemented": 9,
"not-implemented": 7,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -626,18 +626,18 @@
"apiEntries": 22,
"static": {
"supported": 0,
"partial": 11,
"partial": 12,
"refused": 0,
"not-implemented": 11,
"not-implemented": 10,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
},
"dynamic": {
"supported": 0,
"partial": 8,
"partial": 9,
"refused": 0,
"not-implemented": 14,
"not-implemented": 13,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -56489,14 +56489,14 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "Not implemented in scriptc's static module-loader subset yet.",
"verification": "declared-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
"detail": "Not implemented in the embedded module-loader subset yet.",
"verification": "declared-gap"
"status": "partial",
"detail": "Implemented for the documented embedded module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -56517,14 +56517,14 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "Not implemented in scriptc's static module-loader subset yet.",
"verification": "declared-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
"detail": "Not implemented in the embedded module-loader subset yet.",
"verification": "declared-gap"
"status": "partial",
"detail": "Implemented for the documented embedded module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -57105,14 +57105,14 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"detail": "Not implemented in scriptc's static module-loader subset yet.",
"verification": "declared-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
},
"dynamic": {
"status": "not-implemented",
"detail": "Not implemented in the embedded module-loader subset yet.",
"verification": "declared-gap"
"status": "partial",
"detail": "Implemented for the documented embedded module-loader subset.",
"verification": "test-backed"
}
},
{
+4 -2
View File
@@ -22,11 +22,13 @@
{ "chapter": "environment_variables", "symbols": ["process.env"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
{ "chapter": "environment_variables", "symbols": ["export"], "status": "not-applicable" },
{ "chapter": "modules", "symbols": ["__dirname", "__filename", "exports", "module", "require", "cache", "filename", "id", "loaded", "parent", "path"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/create-require/main.ts", "tests/fixtures/npm/cases/dynamic-import/main.ts"] },
{ "chapter": "modules", "symbols": ["require.resolve", "require.resolve.paths", "extensions", "main", "module.require", "children", "isPreloading", "paths", "Module"], "status": "not-implemented" },
{ "chapter": "modules", "symbols": ["require.resolve", "require.resolve.paths"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/create-require/main.ts"] },
{ "chapter": "modules", "symbols": ["extensions", "main", "module.require", "children", "isPreloading", "paths", "Module"], "status": "not-implemented" },
{ "chapter": "modules", "symbols": ["node:", "node_modules"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/builtin-shims/main.ts", "tests/fixtures/npm/cases/create-require/main.ts"] },
{ "chapter": "esm", "symbols": ["import", "file:", "node:", "await", "url"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/dynamic-import/main.ts", "tests/corpus/2646-top-level-await.ts"] },
{ "chapter": "esm", "signatures": ["`import()` expressions"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/dynamic-import/main.ts", "tests/corpus/2050-dynamic-import-own-module/main.ts", "tests/corpus/2790-dynamic-import-self-name/main.ts"] },
{ "chapter": "esm", "symbols": ["meta", "import.meta.resolve", "dirname", "filename", "main", "data:", "require", "__filename", "require.main", "require.resolve", "NODE_PATH", "require.extensions", "require.cache"], "status": "not-implemented" },
{ "chapter": "esm", "symbols": ["import.meta.resolve"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/dual-entry/main.ts"] },
{ "chapter": "esm", "symbols": ["meta", "dirname", "filename", "main", "data:", "require", "__filename", "require.main", "require.resolve", "NODE_PATH", "require.extensions", "require.cache"], "status": "not-implemented" },
{ "chapter": "packages", "symbols": ["package.json", "\"name\"", "\"main\"", "\"type\"", "\"exports\"", "\"imports\""], "status": "partial", "evidence": ["tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", "tests/fixtures/npm/cases/scoped-nested/main.ts", "tests/corpus/2790-dynamic-import-self-name/main.ts"] },
{ "chapter": "packages", "symbols": ["--input-type"], "status": "not-applicable" },
{ "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2705-type-import-link-dynamic/main.ts", "tests/corpus/2706-type-import-link-dynamic-success/main.ts", "tests/corpus/2708-type-import-link-dynamic/main.ts", "tests/harness/errors.test.ts"] }
@@ -9,15 +9,15 @@
"static": {
"replace-refusal": 73,
"verify-gap": 2759,
"audit-partial": 376,
"audit-partial": 379,
"classify": 3,
"implement": 92
"implement": 89
},
"dynamic": {
"audit-partial": 745,
"audit-partial": 748,
"verify-gap": 2302,
"replace-refusal": 101,
"implement": 220
"implement": 217
}
},
"priorities": {
@@ -59564,20 +59564,25 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "implement",
"verification": "declared-gap",
"confidence": "medium",
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:modules.require.resolve",
"tests": []
"tests": [
"tests/corpus/2858-import-meta-resolve/main.mjs",
"tests/corpus/2860-require-resolve/main.cjs"
]
},
"dynamic": {
"status": "not-implemented",
"action": "implement",
"verification": "declared-gap",
"confidence": "medium",
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "island-feature:modules.require.resolve",
"tests": []
"tests": [
"tests/fixtures/npm/cases/create-require/main.ts"
]
}
}
},
@@ -59597,20 +59602,25 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "implement",
"verification": "declared-gap",
"confidence": "medium",
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:modules.require.resolve.paths",
"tests": []
"tests": [
"tests/corpus/2858-import-meta-resolve/main.mjs",
"tests/corpus/2860-require-resolve/main.cjs"
]
},
"dynamic": {
"status": "not-implemented",
"action": "implement",
"verification": "declared-gap",
"confidence": "medium",
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "island-feature:modules.require.resolve.paths",
"tests": []
"tests": [
"tests/fixtures/npm/cases/create-require/main.ts"
]
}
}
},
@@ -60321,20 +60331,24 @@
"priority": "normal",
"tiers": {
"static": {
"status": "not-implemented",
"action": "implement",
"verification": "declared-gap",
"confidence": "medium",
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:esm.import.meta.resolve",
"tests": []
"tests": [
"tests/corpus/2858-import-meta-resolve/main.mjs"
]
},
"dynamic": {
"status": "not-implemented",
"action": "implement",
"verification": "declared-gap",
"confidence": "medium",
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "island-feature:esm.import.meta.resolve",
"tests": []
"tests": [
"tests/fixtures/npm/cases/dual-entry/main.ts"
]
}
}
},
@@ -675,18 +675,18 @@
"apiEntries": 27,
"static": {
"supported": 0,
"partial": 13,
"partial": 15,
"refused": 0,
"not-implemented": 11,
"not-implemented": 9,
"by-design": 0,
"unreviewed": 3,
"not-applicable": 0
},
"dynamic": {
"supported": 0,
"partial": 18,
"partial": 20,
"refused": 0,
"not-implemented": 9,
"not-implemented": 7,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -699,18 +699,18 @@
"apiEntries": 22,
"static": {
"supported": 0,
"partial": 11,
"partial": 12,
"refused": 0,
"not-implemented": 11,
"not-implemented": 10,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
},
"dynamic": {
"supported": 0,
"partial": 8,
"partial": 9,
"refused": 0,
"not-implemented": 14,
"not-implemented": 13,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -73697,7 +73697,9 @@
"tests/corpus/1598-cjs-builtin-require/main.js",
"tests/corpus/1612-cjs-module-globals.cjs",
"tests/corpus/1629-require-main-filename.cjs",
"tests/corpus/2830-cjs-require-self-name/main.cjs"
"tests/corpus/2830-cjs-require-self-name/main.cjs",
"tests/corpus/2858-import-meta-resolve/main.mjs",
"tests/corpus/2860-require-resolve/main.cjs"
]
},
"dynamic": {
@@ -73971,12 +73973,19 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-feature:modules.require.resolve"
"status": "partial",
"evidence": "compiler-feature:modules.require.resolve",
"tests": [
"tests/corpus/2858-import-meta-resolve/main.mjs",
"tests/corpus/2860-require-resolve/main.cjs"
]
},
"dynamic": {
"status": "not-implemented",
"evidence": "island-feature:modules.require.resolve"
"status": "partial",
"evidence": "island-feature:modules.require.resolve",
"tests": [
"tests/fixtures/npm/cases/create-require/main.ts"
]
},
"anchorSource": "exact"
},
@@ -73998,12 +74007,19 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-feature:modules.require.resolve.paths"
"status": "partial",
"evidence": "compiler-feature:modules.require.resolve.paths",
"tests": [
"tests/corpus/2858-import-meta-resolve/main.mjs",
"tests/corpus/2860-require-resolve/main.cjs"
]
},
"dynamic": {
"status": "not-implemented",
"evidence": "island-feature:modules.require.resolve.paths"
"status": "partial",
"evidence": "island-feature:modules.require.resolve.paths",
"tests": [
"tests/fixtures/npm/cases/create-require/main.ts"
]
},
"anchorSource": "exact"
},
@@ -74947,6 +74963,7 @@
"tests": [
"tests/corpus/2646-top-level-await.ts",
"tests/corpus/2701-import-meta/main.mjs",
"tests/corpus/2858-import-meta-resolve/main.mjs",
"tests/corpus/950-modules-basic/main.ts"
]
},
@@ -74955,6 +74972,7 @@
"evidence": "derived:descendants",
"tests": [
"tests/corpus/2646-top-level-await.ts",
"tests/fixtures/npm/cases/dual-entry/main.ts",
"tests/fixtures/npm/cases/dynamic-import/main.ts"
]
},
@@ -75005,12 +75023,18 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"evidence": "compiler-feature:esm.import.meta.resolve"
"status": "partial",
"evidence": "compiler-feature:esm.import.meta.resolve",
"tests": [
"tests/corpus/2858-import-meta-resolve/main.mjs"
]
},
"dynamic": {
"status": "not-implemented",
"evidence": "island-feature:esm.import.meta.resolve"
"status": "partial",
"evidence": "island-feature:esm.import.meta.resolve",
"tests": [
"tests/fixtures/npm/cases/dual-entry/main.ts"
]
},
"anchorSource": "exact"
},
+4 -2
View File
@@ -75,11 +75,13 @@
{ "chapter": "environment_variables", "symbols": ["export"], "status": "not-applicable" },
{ "chapter": "globals", "symbols": ["fetch"], "status": "partial", "evidence": ["tests/harness/fetch-conformance.test.ts"] },
{ "chapter": "modules", "symbols": ["__dirname", "__filename", "exports", "require", "module.require", "main", "filename"], "status": "partial", "evidence": ["tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs"] },
{ "chapter": "modules", "symbols": ["require.resolve", "require.resolve.paths", "cache", "extensions", "children", "id", "isPreloading", "loaded", "parent", "paths", "Module"], "status": "not-implemented" },
{ "chapter": "modules", "symbols": ["require.resolve", "require.resolve.paths"], "status": "partial", "evidence": ["tests/corpus/2858-import-meta-resolve/main.mjs", "tests/corpus/2860-require-resolve/main.cjs"] },
{ "chapter": "modules", "symbols": ["cache", "extensions", "children", "id", "isPreloading", "loaded", "parent", "paths", "Module"], "status": "not-implemented" },
{ "chapter": "modules", "symbols": ["node:", "node_modules"], "status": "partial", "evidence": ["tests/corpus/1598-cjs-builtin-require/main.js", "tests/corpus/1596-cjs-modules/main.js"] },
{ "chapter": "esm", "symbols": ["import", "file:", "node:", "await"], "status": "partial", "evidence": ["tests/corpus/950-modules-basic/main.ts", "tests/corpus/2646-top-level-await.ts"] },
{ "chapter": "esm", "symbols": ["url", "dirname", "filename", "main"], "status": "partial", "evidence": ["tests/corpus/2701-import-meta/main.mjs"] },
{ "chapter": "esm", "symbols": ["meta", "import.meta.resolve", "data:", "require", "__filename", "require.main", "require.resolve", "NODE_PATH", "require.extensions", "require.cache"], "status": "not-implemented" },
{ "chapter": "esm", "symbols": ["import.meta.resolve"], "status": "partial", "evidence": ["tests/corpus/2858-import-meta-resolve/main.mjs"] },
{ "chapter": "esm", "symbols": ["meta", "data:", "require", "__filename", "require.main", "require.resolve", "NODE_PATH", "require.extensions", "require.cache"], "status": "not-implemented" },
{ "chapter": "packages", "symbols": ["package.json", "\"name\"", "\"main\"", "\"type\"", "\"exports\"", "\"imports\""], "status": "partial", "evidence": ["tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", "tests/corpus/2390-dot-requires/main.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs"] },
{ "chapter": "packages", "symbols": ["--input-type"], "status": "not-applicable" },
{ "chapter": "child_process", "signatures": ["`subprocess[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
+12 -4
View File
@@ -307,7 +307,14 @@ declare var global: typeof globalThis;
* to the ENTRY module's record ({ filename } — a compiled binary's main
* module is always the entry, Node's answer for a directly-run script);
* the rest fences per site. */
declare var require: {
interface ScriptcRequireResolveOptions {
paths?: string[];
}
interface ScriptcRequireResolve {
(id: string, options?: ScriptcRequireResolveOptions): string;
paths(id: string): string[] | null;
}
interface ScriptcRequire {
/* The call signature types NON-IMPORT-SHAPED require expressions (the
* checker models import-shaped requires as module aliases regardless,
* exactly like under @types/node's NodeRequire). `any`, like
@@ -315,9 +322,10 @@ declare var require: {
* value model — reached uses fence per site. */
(id: string): any;
main: { filename: string } | undefined;
resolve(id: string): string;
resolve: ScriptcRequireResolve;
cache: { [id: string]: unknown };
};
}
declare var require: ScriptcRequire;
/* setImmediate/clearImmediate — Node's macrotask pair (fires after I/O
* events of the current loop turn, before timers due later). The handle
@@ -914,7 +922,7 @@ declare var performance: import("node:perf_hooks").Performance;
* name the builtin, like in Node (the builtin wins over the npm package
* named "module" for the bare specifier there too). */
declare module "node:module" {
export function createRequire(filename: string | URL): (id: string) => unknown;
export function createRequire(filename: string | URL): ScriptcRequire;
export const builtinModules: string[];
export function isBuiltin(moduleName: string): boolean;
export function syncBuiltinESMExports(): void;
@@ -8,6 +8,7 @@ import {
trackedAccessibleEntries,
trackedDirectoryExists,
trackedFileExists,
markFrontendInputsUnstable,
trackedReadFile,
validFrontendInputSnapshot,
} from "./input-tracker.js";
@@ -86,6 +87,14 @@ test("a candidate appearing during the frontend prevents cache publication", asy
expect(frontendInputsStillMatch(snapshot)).toBe(false);
});
test("an opaque host query can decline frontend cache publication", () => {
const tracker = new FrontendInputTracker();
tracker.run(() => markFrontendInputsUnstable());
const snapshot = tracker.snapshot();
expect(snapshot.stable).toBe(false);
expect(validFrontendInputSnapshot(snapshot)).toBe(false);
});
test("directory enumeration invalidates workspace discovery", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-"));
scratch.push(dir);
@@ -118,6 +118,16 @@ export class FrontendInputTracker {
}
}
/** Prevent this frontend result from entering a persistent cache.
*
* Some host APIs perform semantic filesystem discovery internally and do
* not expose the candidates they inspected. Callers may still use their
* exact answer for the current build, but must decline cache publication
* rather than pretend the observation set is complete. */
markUnstable(): void {
this.stable = false;
}
snapshot(): FrontendInputSnapshot {
return {
version: 1,
@@ -133,6 +143,12 @@ function record(probe: FrontendInputProbe): void {
activeTracker.getStore()?.record(probe);
}
/** Mark the active frontend as unsafe to persist because an exact host query
* bypassed the tracked filesystem wrappers. No-op outside a frontend run. */
export function markFrontendInputsUnstable(): void {
activeTracker.getStore()?.markUnstable();
}
export function trackedReadFile(path: string): string | null {
path = resolve(path);
try {
@@ -8,11 +8,12 @@ import { dirname, resolve } from "node:path";
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import { PoisonError, dynUndefinedExpr, ladderFenceExpr, nodeThrowExpr, own } from "./lowerer.js";
import { canonicalBuiltinModule, isJsSourceFile, locOf, requireSpecOf } from "../program.js";
import { canonicalBuiltinModule, isJsSourceFile, isNodeEsmFile, locOf, requireSpecOf } from "../program.js";
import { isRelativeSpecifier } from "../workspace-registry.js";
import { probeNodeRequireRefusal } from "../npm.js";
import { isNpmStaticPackage } from "../npm-static.js";
import { trackedReadFile } from "../input-tracker.js";
import { requireResolvePathsRuntime, resolveImportMetaRuntime, resolveRequireRuntime, type RuntimeResolveError, type RuntimeResolveResult } from "../runtime-resolve.js";
import { invalidJsonModuleDiag, requiresDynamicImportDiag } from "../../diagnostics/diagnostic.js";
import {
BuiltinModuleFn,
@@ -623,6 +624,169 @@ function lowerBuiltinOptionalDefault(
};
}
function staticString(node: ts.Expression | undefined): string | null {
if (node === undefined) return null;
const value = stripTypeCasts(node);
return ts.isStringLiteralLike(value) ? value.text : null;
}
function runtimeResolveThrow(error: RuntimeResolveError, type: IrType, loc: SrcLoc): IrExpr {
return nodeThrowExpr(error.name === "TypeError" ? 1 : 0, error.code, error.message, type, loc);
}
/** import.meta.resolve("literal") folds through the runtime-module
* resolver, never TypeScript's declaration-file resolver. Relative and
* URL-like names remain valid even when no file exists; bare packages use
* Node's import-condition exports path. */
export function lowerImportMetaResolveCall(
lowerer: Lowerer,
call: ts.CallExpression,
): IrExpr | null {
const callee = call.expression;
if (
!ts.isPropertyAccessExpression(callee) || callee.questionDotToken !== undefined ||
callee.name.text !== "resolve" || !ts.isMetaProperty(callee.expression) ||
callee.expression.keywordToken !== ts.SyntaxKind.ImportKeyword ||
callee.expression.name.text !== "meta"
) {
return null;
}
if (call.questionDotToken !== undefined || call.arguments.length !== 1) {
lowerer.noLowering(
"import.meta.resolve with this argument shape",
call,
"the lowered form is import.meta.resolve(\"<static specifier>\") using the containing module as its parent",
);
}
const specifier = staticString(call.arguments[0]);
if (specifier === null) {
lowerer.noLowering(
"import.meta.resolve with a runtime-computed specifier",
call.arguments[0]!,
"a compiled binary has a fixed module graph — pass a string literal",
);
}
const result = resolveImportMetaRuntime(
call.getSourceFile().fileName,
specifier,
lowerer.targetPlatform,
);
if (result === null) {
lowerer.noLowering(
`import.meta.resolve of '${specifier}'`,
call,
"relative paths, URL-like names, builtins, and installed package names are supported; package-import aliases remain unsupported",
);
}
const loc = locOf(call);
return result.ok
? { kind: "strLit", value: result.value, type: STRING, loc }
: runtimeResolveThrow(result.error, STRING, loc);
}
function requireResolverBaseFile(lowerer: Lowerer, receiver: ts.Expression): ts.SourceFile | null {
const created = createRequireCalleeFileOf(lowerer, receiver);
if (created !== null) return created;
return lowerer.isStdlibGlobal(receiver, "require") && !isNodeEsmFile(receiver.getSourceFile())
? receiver.getSourceFile()
: null;
}
function staticResolveOptionPaths(node: ts.Expression | undefined): readonly string[] | undefined | null {
if (node === undefined) return undefined;
const value = stripTypeCasts(node);
if (ts.isIdentifier(value) && value.text === "undefined") return undefined;
if (!ts.isObjectLiteralExpression(value)) return null;
let paths: readonly string[] | undefined;
for (const prop of value.properties) {
if (!ts.isPropertyAssignment(prop)) return null;
const name = ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name) ? prop.name.text : null;
if (name !== "paths" || paths !== undefined) return null;
const init = stripTypeCasts(prop.initializer);
if (!ts.isArrayLiteralExpression(init)) return null;
const entries: string[] = [];
for (const item of init.elements) {
if (!ts.isStringLiteralLike(item)) return null;
entries.push(item.text);
}
paths = entries;
}
return paths;
}
/** CommonJS require.resolve and require.resolve.paths for the ambient
* wrapper or a supported createRequire binding. Results are build-time
* constants; failures lower to Node's catchable error object. */
export function lowerRequireResolveCall(
lowerer: Lowerer,
call: ts.CallExpression,
): IrExpr | null {
const callee = call.expression;
if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken !== undefined) return null;
let receiver: ts.Expression;
let pathsCall = false;
if (callee.name.text === "resolve") {
receiver = callee.expression;
} else if (
callee.name.text === "paths" && ts.isPropertyAccessExpression(callee.expression) &&
callee.expression.questionDotToken === undefined && callee.expression.name.text === "resolve"
) {
receiver = callee.expression.expression;
pathsCall = true;
} else {
return null;
}
const baseFile = requireResolverBaseFile(lowerer, receiver);
if (baseFile === null) return null;
if (call.questionDotToken !== undefined || call.arguments.length < 1 || call.arguments.length > (pathsCall ? 1 : 2)) {
lowerer.noLowering(
pathsCall ? "require.resolve.paths with this argument shape" : "require.resolve with this argument shape",
call,
);
}
const specifier = staticString(call.arguments[0]);
if (specifier === null) {
lowerer.noLowering(
`${pathsCall ? "require.resolve.paths" : "require.resolve"} with a runtime-computed request`,
call.arguments[0]!,
"a compiled binary has a fixed module graph — pass a string literal",
);
}
const loc = locOf(call);
if (pathsCall) {
const result = requireResolvePathsRuntime(baseFile.fileName, specifier, lowerer.targetPlatform);
if (result !== null && !Array.isArray(result)) {
return runtimeResolveThrow(result as RuntimeResolveError, lowerer.irTypeOf(call), loc);
}
const raw: IrExpr = result === null
? { kind: "unitLit", unit: "null", type: NULL_T, loc }
: {
kind: "arrayLit",
elems: result.map((value) => ({ kind: "strLit", value, type: STRING, loc })),
type: arrayOf(STRING),
loc,
};
return lowerer.coerceInto(call, raw, lowerer.irTypeOf(call));
}
const paths = staticResolveOptionPaths(call.arguments[1]);
if (paths === null) {
lowerer.noLowering(
"require.resolve with runtime-computed options",
call.arguments[1]!,
"omit options or pass { paths: [\"<static directory>\", ...] }",
);
}
const result: RuntimeResolveResult = resolveRequireRuntime(
baseFile.fileName,
specifier,
lowerer.targetPlatform,
paths,
);
return result.ok
? { kind: "strLit", value: result.value, type: STRING, loc }
: runtimeResolveThrow(result.error, STRING, loc);
}
/** The builtin modules whose `constants` object bakes as literals at
* every access site (the fs.constants precedent, scaled up): http2's
* full Node v24 table, and crypto's OpenSSL-constant table. The object
@@ -17,7 +17,7 @@ import { ffiBindingDiag, ffiSignatureDiag, libCallbackDiag, requiresDynamicDiag
import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js";
import { mixinFnShapeOf } from "./lower-mixins.js";
import { bufEncoding, dynStringReceiver, lowerArrayFromCall, lowerDynArrayFilterCall, lowerDynArrayFlatMapCall, lowerGroupByStaticCall, lowerIteratorHelperCall, lowerObjectAssignIndexShape, lowerObjectFromEntriesCall, lowerObjectIterOverIndexShape, lowerRegexMethodCall, lowerStringMethodCall, lowerTupleReadMethodCall } from "./lower-containers.js";
import { lowerChildStreamMethodCall, lowerCreateRequireCall, lowerDirentMethodCall, lowerFileHandleMethodCall, lowerPerfHooksCall, lowerProcStreamMethodCall, lowerReflectApplyCall, lowerWatcherMethodCall } from "./lower-builtins.js";
import { lowerChildStreamMethodCall, lowerCreateRequireCall, lowerDirentMethodCall, lowerFileHandleMethodCall, lowerImportMetaResolveCall, lowerPerfHooksCall, lowerProcStreamMethodCall, lowerReflectApplyCall, lowerRequireResolveCall, lowerWatcherMethodCall } from "./lower-builtins.js";
import { droppableStatic, lowerAbsenceProbe, lowerPromiseAllTupleCall, lowerPromiseRejectCall, probeLower, templateRawTextOf } from "./lower-exprs.js";
import { httpClientFnBindingOf, isStreamUndefCallExpr, lowerCompatReqStreamOptionalCall, lowerHttpClientFnCall } from "./lower-server.js";
import { EMITTER_API_MEMBERS, exactInstanceClassOf, findGenericMethodOn, lowerClassGenericMethodCall, lowerStaticMethodCall, type ClassInfo } from "./lower-classes.js";
@@ -3050,6 +3050,11 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
if (crServed) return crServed;
}
{
const resolved = lowerImportMetaResolveCall(lowerer, expr) ?? lowerRequireResolveCall(lowerer, expr);
if (resolved) return resolved;
}
// `process.getuid?.()` — intercepted BEFORE the optional-chain
// machinery (the member always exists on a POSIX target, so the
// optional call IS the call; `process.getuid` itself has no value
@@ -2005,13 +2005,6 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
const sf = expr.getSourceFile();
const fileName = moduleFileName(lowerer, sf);
if (lowerer.dynamic) {
lowerer.unsupported(
"SC1090",
expr,
`'import.meta.${expr.name.text}' in a dynamic island (module-loader metadata is supported only in static ESM)`,
);
}
switch (expr.name.text) {
case "url":
return {
@@ -4088,7 +4081,7 @@ export function lowerOptionalChain(lowerer: Lowerer, expr: ts.CallExpression | t
}
lowerer.badType(expr, tsType);
}
let type = mapped as IrType & { kind: "array" };
const type = mapped as IrType & { kind: "array" };
// Keep the array payload type fixed. Optional reads are stored through
// arrayValueStore, which records UNDEFINED in the state byte while the
// payload remains number/string/etc.; widening the array element here
@@ -0,0 +1,55 @@
import { expect, test } from "vitest";
import { moduleSpecifiersOf } from "./npm.js";
import { NpmGraphBuilder } from "./npm.js";
test("collects import.meta.resolve literals as resolution-only edges", () => {
const result = moduleSpecifiersOf(
'export const relative = import.meta.resolve("./asset.js");\n' +
'export const packageUrl = import.meta.resolve("example-package");\n',
"/project/index.mjs",
);
expect(result.uses).toEqual([
{
specifier: "./asset.js",
static: false,
require: false,
requireLocal: false,
requireViaHelper: false,
dynamicImport: false,
importMetaResolve: true,
},
{
specifier: "example-package",
static: false,
require: false,
requireLocal: false,
requireViaHelper: false,
dynamicImport: false,
importMetaResolve: true,
},
]);
});
test("emits an import-condition edge for an embedded bare import.meta.resolve", () => {
const root = new URL("../../../../tests/fixtures/npm/", import.meta.url).pathname;
const entry = `${root}cases/dual-entry/main.ts`;
const builder = new NpmGraphBuilder();
builder.addImport(entry, "dual");
const graph = builder.finish();
const dual = graph.modules.find((module) => module.key.endsWith("/dual/index.mjs"));
expect(dual).toBeDefined();
expect(graph.edges).toContainEqual({
from: dual!.key,
specifier: "cjszoo",
to: expect.stringMatching(/\/cjszoo\/index\.js$/),
kind: "import",
});
});
test("runtime introspection keeps import and require export conditions separate", () => {
const root = new URL("../../../../tests/fixtures/npm/", import.meta.url).pathname;
const entry = `${root}cases/dual-entry/main.ts`;
const builder = new NpmGraphBuilder();
expect(builder.resolveForIntrospection(entry, "dual", "import")).toMatch(/\/dual\/index\.mjs$/);
expect(builder.resolveForIntrospection(entry, "dual", "require")).toMatch(/\/dual\/index\.cjs$/);
});
+55 -7
View File
@@ -596,7 +596,7 @@ const KNOWN_BUILTINS = new Set([
* several forms in one file; any STATIC occurrence makes the edge eager
* (Node refuses the whole static graph at link time regardless of what the
* lazy sites would have done). */
interface SpecifierUse {
export interface SpecifierUse {
specifier: string;
/** import/export declaration — eager, link-time. */
static: boolean;
@@ -616,12 +616,16 @@ interface SpecifierUse {
requireViaHelper: boolean;
/** import("x") — evaluation-time. */
dynamicImport: boolean;
/** import.meta.resolve("x") — resolves synchronously without loading.
* It still needs an emitted edge for bare package names so the island can
* answer from its fixed graph; relative and URL-like names need no edge. */
importMetaResolve: boolean;
}
/** moduleSpecifiersOf's full answer: the per-specifier call-site kinds
* plus where the file's `__require` binding comes from, when it is not
* its own. */
interface ModuleSpecifiers {
export interface ModuleSpecifiers {
uses: SpecifierUse[];
/** The specifier `__require` is IMPORTED from (`import { __require }
* from "./chunk-X.js"` — esbuild's shared-helper chunk shape), else
@@ -641,7 +645,7 @@ interface ModuleSpecifiers {
* __require("literal") (esbuild's external-require helper — collecting its
* literal call sites gives bundled dists an honest build-time inventory).
* A real parse, never a regex. */
function moduleSpecifiersOf(source: string, fileName: string): ModuleSpecifiers {
export function moduleSpecifiersOf(source: string, fileName: string): ModuleSpecifiers {
const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, false, ts.ScriptKind.JS);
const uses: SpecifierUse[] = [];
const bySpec = new Map<string, SpecifierUse>();
@@ -660,6 +664,7 @@ function moduleSpecifiersOf(source: string, fileName: string): ModuleSpecifiers
requireLocal: false,
requireViaHelper: false,
dynamicImport: false,
importMetaResolve: false,
};
bySpec.set(spec, use);
uses.push(use);
@@ -698,6 +703,14 @@ function moduleSpecifiersOf(source: string, fileName: string): ModuleSpecifiers
ts.isStringLiteralLike(arg)
) {
push(arg.text, "dynamicImport");
} else if (
ts.isPropertyAccessExpression(n.expression) &&
n.expression.name.text === "resolve" &&
ts.isMetaProperty(n.expression.expression) &&
n.expression.expression.keywordToken === ts.SyntaxKind.ImportKeyword &&
n.arguments.length >= 1 && arg !== undefined && ts.isStringLiteralLike(arg)
) {
push(arg.text, null).importMetaResolve = true;
} else if (
ts.isIdentifier(n.expression) &&
(n.expression.text === "require" || n.expression.text === "__require") &&
@@ -945,6 +958,27 @@ export class NpmGraphBuilder {
constructor(private readonly host: Host = realHost) {}
/** Resolve one runtime module without embedding it. This is the package
* half of import.meta.resolve/require.resolve: the answer is executable
* source under Node's import/require conditions, never the declaration-file
* answer from resolve.ts. Diagnostics produced by the graph-oriented
* resolver are discarded here; the introspection lowering supplies Node's
* public error shape separately. */
resolveForIntrospection(
fromFile: string,
specifier: string,
mode: "import" | "require",
): string | null {
const before = this.errors.length;
const key = this.resolvePackage(dirname(resolve(fromFile)), specifier, mode, {
importer: resolve(fromFile),
chain: [],
preferModuleField: false,
});
this.errors.length = before;
return key;
}
/** Registers one user-level npm import and walks everything it reaches.
* Idempotent per (importing dir, specifier). */
addImport(fromFile: string, specifier: string): void {
@@ -1349,7 +1383,7 @@ export class NpmGraphBuilder {
fromDir: string,
specifier: string,
mode: "import" | "require",
ctx: { importer: string; chain: readonly string[] },
ctx: { importer: string; chain: readonly string[]; preferModuleField?: boolean },
): string | null {
const name = packageNameOf(specifier);
const parts = specifier.split("/");
@@ -1414,7 +1448,10 @@ export class NpmGraphBuilder {
target = fromExports;
} else if (subpath !== ".") {
target = subpath;
} else if (mode === "import" && typeof pkg.module === "string" && pkg.module !== "") {
} else if (
mode === "import" && ctx.preferModuleField !== false &&
typeof pkg.module === "string" && pkg.module !== ""
) {
// No "exports": the "module" field names the ESM build (a
// bundler convention Node itself ignores; embedding prefers the
// real ES module over require-shimming the CJS "main").
@@ -1575,6 +1612,9 @@ export class NpmGraphBuilder {
const spec = use.specifier;
const eager = !lazy && use.static;
if (spec.startsWith("./") || spec.startsWith("../")) {
if (use.importMetaResolve && !use.static && !use.dynamicImport && !use.require) {
continue;
}
// A relative file: no conditions apply, so every call form shares
// one "any" edge. A blocked lazy one embeds the import trap only
// for import()/static-in-lazy sites — require-reached specs embed
@@ -1605,6 +1645,12 @@ export class NpmGraphBuilder {
this.walk(to, chain, lazy || !use.static);
continue;
}
if (
use.importMetaResolve && !use.static && !use.dynamicImport && !use.require &&
(/^[A-Za-z][A-Za-z\d+.-]*:/.test(spec) || spec.startsWith("/"))
) {
continue;
}
const builtin = builtinKeyOf(spec);
if (builtin) {
let seen = this.builtinsSeen.get(builtin);
@@ -1691,7 +1737,7 @@ export class NpmGraphBuilder {
this.walk(to, nextChain, true);
}
}
if (use.static || use.dynamicImport) {
if (use.static || use.dynamicImport || use.importMetaResolve) {
const errorsBefore = this.errors.length;
const to = this.resolvePackage(dirname(key), spec, "import", { importer: key, chain });
if (to === null) {
@@ -1724,7 +1770,9 @@ export class NpmGraphBuilder {
);
}
pushEdge(key, spec, to, "import");
this.walk(to, nextChain, lazy || !use.static);
if (!use.importMetaResolve || use.static || use.dynamicImport) {
this.walk(to, nextChain, lazy || !use.static);
}
}
}
}
@@ -0,0 +1,19 @@
import { join } from "node:path";
import { expect, test } from "vitest";
import { resolveImportMetaRuntime, runtimePathForTarget } from "./runtime-resolve.js";
test("WASI resolution metadata uses guest paths", () => {
const source = join(process.cwd(), "tests", "program", "main.mjs");
expect(runtimePathForTarget(source, "wasi")).toBe("/tests/program/main.mjs");
expect(resolveImportMetaRuntime(source, "./asset with space.js", "wasi")).toEqual({
ok: true,
value: "file:///tests/program/asset%20with%20space.js",
});
});
test("Windows resolution metadata uses target URL semantics on any host", () => {
expect(resolveImportMetaRuntime("C:\\work\\main.mjs", "./asset.js", "win32")).toEqual({
ok: true,
value: "file:///C:/work/asset.js",
});
});
@@ -0,0 +1,128 @@
/* Node runtime resolution used by compile-time-folded module introspection.
*
* This is deliberately separate from resolve.ts: that resolver answers the
* TypeScript program's TYPE surface and may correctly select a .d.ts file,
* while import.meta.resolve / require.resolve must answer Node's executable
* file or URL. CJS resolution delegates to the pinned Node 24 host for exact
* exports/main/extension/error behavior. The host API does not expose its
* failed lookup candidates, so those builds decline persistent frontend-cache
* publication rather than retaining an incomplete dependency snapshot. ESM
* package success uses the tracked embedded-graph resolver under Node's import
* conditions; relative and URL-like specifiers are pure URL resolution and do
* not require the target to exist, matching Node. */
import { createRequire, isBuiltin } from "node:module";
import { isAbsolute } from "node:path";
import { pathToFileURL } from "node:url";
import { markFrontendInputsUnstable } from "./input-tracker.js";
import { NpmGraphBuilder, probeNodeImportRefusal } from "./npm.js";
import { wasiGuestPath } from "../wasi-paths.js";
export interface RuntimeResolveError {
name: string;
code: string;
message: string;
}
export type RuntimeResolveResult =
| { ok: true; value: string }
| { ok: false; error: RuntimeResolveError };
function errorShape(error: unknown): RuntimeResolveError {
const e = error as { name?: unknown; code?: unknown; message?: unknown };
return {
name: typeof e?.name === "string" ? e.name : "Error",
code: typeof e?.code === "string" ? e.code : "",
message: typeof e?.message === "string" ? e.message : String(error),
};
}
export function runtimePathForTarget(path: string, targetPlatform: string): string {
if (targetPlatform !== "wasi") return path;
return wasiGuestPath(path) ?? path.replaceAll("\\", "/");
}
function targetFileUrl(path: string, targetPlatform: string): string {
return pathToFileURL(runtimePathForTarget(path, targetPlatform), {
windows: targetPlatform === "win32",
}).href;
}
function urlLike(specifier: string): boolean {
return /^[A-Za-z][A-Za-z\d+.-]*:/.test(specifier);
}
/** import.meta.resolve for a static string and the containing source file.
* Package-import (`#name`) resolution remains a named lowering fence until
* its Node error/success path can share the package-import resolver without
* crossing back into TypeScript's declaration-file answer. */
export function resolveImportMetaRuntime(
fromFile: string,
specifier: string,
targetPlatform: string,
): RuntimeResolveResult | null {
const base = targetFileUrl(fromFile, targetPlatform);
if (
specifier.startsWith("./") || specifier.startsWith("../") ||
isAbsolute(specifier) || urlLike(specifier)
) {
try {
return { ok: true, value: new URL(specifier, base).href };
} catch (error) {
return { ok: false, error: errorShape(error) };
}
}
if (isBuiltin(specifier)) {
return { ok: true, value: specifier.startsWith("node:") ? specifier : `node:${specifier}` };
}
if (specifier.startsWith("#")) return null;
const builder = new NpmGraphBuilder();
const key = builder.resolveForIntrospection(fromFile, specifier, "import");
if (key !== null) return { ok: true, value: targetFileUrl(key, targetPlatform) };
const refusal = probeNodeImportRefusal(fromFile, specifier);
if (refusal !== null) {
return {
ok: false,
error: { name: "Error", code: refusal.code, message: refusal.message },
};
}
return null;
}
/** require.resolve under the containing file's CommonJS resolver. */
export function resolveRequireRuntime(
fromFile: string,
specifier: string,
targetPlatform: string,
paths?: readonly string[],
): RuntimeResolveResult {
markFrontendInputsUnstable();
try {
const require = createRequire(pathToFileURL(fromFile));
const value = paths === undefined
? require.resolve(specifier)
: require.resolve(specifier, { paths: [...paths] });
return {
ok: true,
value: isAbsolute(value) ? runtimePathForTarget(value, targetPlatform) : value,
};
} catch (error) {
return { ok: false, error: errorShape(error) };
}
}
/** require.resolve.paths under the containing file's CommonJS resolver. */
export function requireResolvePathsRuntime(
fromFile: string,
specifier: string,
targetPlatform: string,
): readonly string[] | null | RuntimeResolveError {
markFrontendInputsUnstable();
try {
const paths = createRequire(pathToFileURL(fromFile)).resolve.paths(specifier);
return paths?.map((path) => runtimePathForTarget(path, targetPlatform)) ?? null;
} catch (error) {
return errorShape(error);
}
}
@@ -6064,6 +6064,24 @@
],
"diags": []
},
"<repo>/tests/corpus/2858-import-meta-resolve/main.mjs": {
"order": [
"<repo>/tests/corpus/2858-import-meta-resolve/main.mjs"
],
"diags": []
},
"<repo>/tests/corpus/2859-import-meta-resolve-dynamic/main.mjs": {
"order": [
"<repo>/tests/corpus/2859-import-meta-resolve-dynamic/main.mjs"
],
"diags": []
},
"<repo>/tests/corpus/2860-require-resolve/main.cjs": {
"order": [
"<repo>/tests/corpus/2860-require-resolve/main.cjs"
],
"diags": []
},
"<repo>/tests/corpus/300-if-else.ts": {
"order": [
"<repo>/tests/corpus/300-if-else.ts"
@@ -7648,18 +7666,6 @@
],
"diags": []
},
"<repo>/tests/diagnostics/import-meta-dynamic.ts": {
"order": [
"<repo>/tests/diagnostics/import-meta-dynamic.ts"
],
"diags": []
},
"<repo>/tests/diagnostics/import-meta-resolve.ts": {
"order": [
"<repo>/tests/diagnostics/import-meta-resolve.ts"
],
"diags": []
},
"<repo>/tests/diagnostics/import-meta-write.ts": {
"order": [
"<repo>/tests/diagnostics/import-meta-write.ts"
@@ -8007,6 +8013,12 @@
}
]
},
"<repo>/tests/diagnostics/module-resolution-introspection.ts": {
"order": [
"<repo>/tests/diagnostics/module-resolution-introspection.ts"
],
"diags": []
},
"<repo>/tests/diagnostics/multiple-errors.ts": {
"order": [
"<repo>/tests/diagnostics/multiple-errors.ts"
+83 -8
View File
@@ -2378,6 +2378,27 @@ static char *isl_module_normalize(JSContext *ctx, const char *base,
return js_strdup(ctx, target);
}
/* import.meta.resolve is module-relative, so each compiled module receives a
* tiny closure carrying its emitted key. The bootstrap owns the JavaScript
* resolution policy (builtins, URL-like names, relative URLs, and known
* embedded edges); this C shim only supplies the lexical module identity. */
static JSValue isl_import_meta_resolve(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv, int magic,
JSValueConst *func_data) {
(void)this_val;
(void)magic;
JSValue global = JS_GetGlobalObject(ctx);
JSValue fn = JS_GetPropertyStr(ctx, global, "__scr_import_meta_resolve");
JS_FreeValue(ctx, global);
if (JS_IsException(fn)) return fn;
JSValueConst args[2] = {
func_data[0], argc > 0 ? argv[0] : JS_UNDEFINED,
};
JSValue out = JS_Call(ctx, fn, JS_UNDEFINED, 2, args);
JS_FreeValue(ctx, fn);
return out;
}
/* Named export lists for the builtin ESM wrappers. The wrapper source is
* `const m = __scr_require("node:x"); export default m; export const
* {…} = m;` — the shims themselves live in the bootstrap below. The
@@ -2469,6 +2490,12 @@ static JSModuleDef *isl_module_load(JSContext *ctx, const char *name, void *opaq
url = JS_NewString(ctx, name);
}
JS_SetPropertyStr(ctx, meta, "url", url); /* consumed */
JSValue key = JS_NewString(ctx, name);
JSValueConst data[1] = {key};
JSValue resolve_fn = JS_NewCFunctionData(ctx, isl_import_meta_resolve, 1,
0, 1, data);
JS_FreeValue(ctx, key); /* resolve_fn's func_data retains it */
JS_SetPropertyStr(ctx, meta, "resolve", resolve_fn); /* consumed */
JS_FreeValue(ctx, meta);
}
JS_FreeValue(ctx, v);
@@ -2513,8 +2540,10 @@ static JSValue isl_host_resolve(JSContext *ctx, JSValueConst this_val, int argc,
if (from) JS_FreeCString(ctx, from);
return JS_EXCEPTION;
}
/* host.resolve serves the require shim exclusively — require kind. */
const char *to = strncmp(spec, "node:", 5) == 0 ? spec : isl_edge_find(from, spec, 2);
/* The require shim asks for kind 2; import.meta.resolve passes a true third
* argument and asks for kind 1, preserving dual-package conditions. */
int want = argc > 2 && JS_ToBool(ctx, argv[2]) > 0 ? 1 : 2;
const char *to = strncmp(spec, "node:", 5) == 0 ? spec : isl_edge_find(from, spec, want);
JSValue r = to ? JS_NewString(ctx, to) : JS_UNDEFINED;
JS_FreeCString(ctx, from);
JS_FreeCString(ctx, spec);
@@ -3442,6 +3471,7 @@ static const char isl_modules_bootstrap[] =
" }\n"
" const cache = Object.create(null);\n"
" const builtins = Object.create(null);\n"
" const builtinModules = ['assert','assert/strict','async_hooks','buffer','child_process','cluster','console','constants','crypto','dgram','diagnostics_channel','dns','dns/promises','domain','events','fs','fs/promises','http','http2','https','inspector','inspector/promises','module','net','os','path','path/posix','path/win32','perf_hooks','process','punycode','querystring','readline','readline/promises','repl','stream','stream/consumers','stream/promises','stream/web','string_decoder','sys','timers','timers/promises','tls','trace_events','tty','url','util','util/types','v8','vm','wasi','worker_threads','zlib'];\n"
/* Node's require stack: each CJS module remembers its FIRST requirer
* (Node's module.parent / moduleParentCache — the chain is static,
* captured at first load, not the dynamic call stack), and a failing
@@ -3478,6 +3508,39 @@ static const char isl_modules_bootstrap[] =
" }\n"
" return to;\n"
" };\n"
" const requirePathsFrom = (from, spec) => {\n"
" if (typeof spec !== 'string') {\n"
" const err = new TypeError('The \"request\" argument must be of type string.');\n"
" err.code = 'ERR_INVALID_ARG_TYPE';\n"
" throw err;\n"
" }\n"
" const bare = spec.startsWith('node:') ? spec.slice(5) : spec;\n"
" if (builtinModules.includes(bare) && (spec.startsWith('node:') || bare !== 'test')) return null;\n"
" const slash = from.lastIndexOf('/');\n"
" const base = slash < 0 ? '/' : (from.slice(0, slash) || '/');\n"
" if (spec.startsWith('./') || spec.startsWith('../') || spec.startsWith('/')) return [base];\n"
" const out = [];\n"
" for (let dir = base;;) {\n"
" out.push((dir === '/' ? '' : dir) + '/node_modules');\n"
" const at = dir.lastIndexOf('/');\n"
" const parent = at <= 0 ? '/' : dir.slice(0, at);\n"
" if (parent === dir) break;\n"
" dir = parent;\n"
" }\n"
" return out;\n"
" };\n"
" const decorateRequire = (req, from) => {\n"
" const resolve = (spec) => {\n"
" if (typeof spec !== 'string') return requirePathsFrom(from, spec);\n"
" const bare = spec.startsWith('node:') ? spec.slice(5) : spec;\n"
" if (builtinModules.includes(bare) && (spec.startsWith('node:') || bare !== 'test')) return spec;\n"
" return resolveFrom(from, spec);\n"
" };\n"
" resolve.paths = (spec) => requirePathsFrom(from, spec);\n"
" req.resolve = resolve;\n"
" req.cache = cache;\n"
" return req;\n"
" };\n"
" const requireKey = (key, parent) => {\n"
" if (key.startsWith('node:')) {\n"
" const b = builtins[key.slice(5)];\n"
@@ -3495,8 +3558,7 @@ static const char isl_modules_bootstrap[] =
" if (format === 2) { mod.exports = JSON.parse(src); return mod.exports; }\n"
" if (format === 0) { delete cache[key]; throw new Error('require() of ES module ' + key); }\n"
" const fn = new Function('exports', 'require', 'module', '__filename', '__dirname', src);\n"
" const req = (spec) => requireKey(resolveFrom(key, spec), key);\n"
" req.cache = cache;\n"
" const req = decorateRequire((spec) => requireKey(resolveFrom(key, spec), key), key);\n"
" const dir = key.slice(0, key.lastIndexOf('/')) || '/';\n"
/* A module whose evaluation THROWS leaves no cache entry — Node
* deletes it so a later require re-evaluates (and a lazy require
@@ -4244,15 +4306,12 @@ static const char isl_modules_bootstrap[] =
" if (key.startsWith('file://')) key = decodeURIComponent(key.slice(7));\n"
/* The base file is the created require's parent, like Node: modules
* it loads report it in their require stacks. */
" const req = (spec) => requireKey(spec.startsWith('node:') ? spec : resolveFrom(key, spec), key);\n"
" req.cache = cache;\n"
" return req;\n"
" return decorateRequire((spec) => requireKey(spec.startsWith('node:') ? spec : resolveFrom(key, spec), key), key);\n"
" };\n"
/* builtinModules/isBuiltin answer Node's QUESTION ("is this name a
* Node builtin?") with Node's full list — resolution of unshimmed
* ones still fails lazily at the call, the island's documented
* shape. */
" const builtinModules = ['assert','assert/strict','async_hooks','buffer','child_process','cluster','console','constants','crypto','dgram','diagnostics_channel','dns','dns/promises','domain','events','fs','fs/promises','http','http2','https','inspector','inspector/promises','module','net','os','path','path/posix','path/win32','perf_hooks','process','punycode','querystring','readline','readline/promises','repl','stream','stream/consumers','stream/promises','stream/web','string_decoder','sys','timers','timers/promises','tls','trace_events','tty','url','util','util/types','v8','vm','wasi','worker_threads','zlib'];\n"
" const isBuiltin = (name) => {\n"
" const n = String(name);\n"
" return n.startsWith('node:') ? builtinModules.includes(n.slice(5)) : builtinModules.includes(n);\n"
@@ -9205,6 +9264,22 @@ static const char isl_modules_bootstrap[] =
" }\n"
" if (globalThis.global === undefined) globalThis.global = globalThis;\n"
" globalThis.__scr_require = requireKey;\n"
" globalThis.__scr_import_meta_resolve = (from, raw) => {\n"
" const spec = String(raw);\n"
" const bare = spec.startsWith('node:') ? spec.slice(5) : spec;\n"
" if (builtinModules.includes(bare) && (spec.startsWith('node:') || bare !== 'test')) return 'node:' + bare;\n"
" const base = from.startsWith('/') ? new URL('file://' + from).href : from;\n"
" if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(spec) || spec.startsWith('./') || spec.startsWith('../') || spec.startsWith('/')) {\n"
" return new URL(spec, base).href;\n"
" }\n"
" const to = host.resolve(from, spec, true);\n"
" if (to === undefined) {\n"
" const err = new Error(\"Cannot find package '\" + spec + \"' imported from \" + from);\n"
" err.code = 'ERR_MODULE_NOT_FOUND';\n"
" throw err;\n"
" }\n"
" return to.startsWith('/') ? new URL('file://' + to).href : to;\n"
" };\n"
" return (key, name) => {\n"
" const exports = requireKey(key);\n"
" if (name === 'default') return exports;\n"
@@ -0,0 +1,21 @@
import { createRequire } from "node:module";
// import.meta.resolve is scoped to each compiled module. Relative names are
// URLs even when the target does not exist; builtins canonicalize to node:;
// URL-like inputs normalize without touching the filesystem; installed bare
// packages resolve under Node's ESM import conditions.
console.log("relative", import.meta.resolve("./module with space.mjs"));
console.log("missing", import.meta.resolve("./missing.mjs"));
console.log("builtins", import.meta.resolve("fs"), import.meta.resolve("node:path"));
console.log("url", import.meta.resolve("https://example.com/a/../b"));
console.log("data", import.meta.resolve("data:text/javascript,export default 1"));
console.log("package", import.meta.resolve("typescript").endsWith("/typescript/lib/typescript.js"));
const require = createRequire(import.meta.url);
console.log("createRequire", require.resolve("./module with space.mjs"));
console.log("createRequire core paths", require.resolve.paths("fs"));
try {
import.meta.resolve("definitely-not-installed-here");
console.log("unexpected resolution");
} catch (error) {
console.log("missing package", error.code, error.message.split("\n")[0]);
}
@@ -0,0 +1 @@
export const marker = "resolved";
@@ -0,0 +1,9 @@
// @dynamic
// Embedding the package engine does not move the program's own ESM modules
// into it: their metadata remains per-file compile-time identity.
console.log(import.meta.url);
console.log(import.meta.filename);
console.log(import.meta.dirname);
console.log(import.meta.main);
console.log(import.meta.resolve("./missing package module.mjs"));
console.log(import.meta.resolve("fs"));
@@ -0,0 +1 @@
{ "resolved": true }
@@ -0,0 +1,19 @@
// CommonJS resolution introspection returns filenames without evaluating the
// target. The options.paths form searches from its supplied directories, and
// missing requests throw Node's catchable MODULE_NOT_FOUND shape.
console.log("relative", require.resolve("./cfg.json"));
console.log("builtins", require.resolve("fs"), require.resolve("node:path"));
console.log("core paths", require.resolve.paths("fs"));
console.log("relative paths", JSON.stringify(require.resolve.paths("./cfg.json")));
console.log(
"custom paths",
require.resolve("fs", {
paths: ["tests/corpus/2860-require-resolve/vendor"],
}),
);
try {
require.resolve("./missing.cjs");
console.log("unexpected resolution");
} catch (error) {
console.log("missing", error.code, error.message.split("\n")[0]);
}
+1 -1
View File
@@ -12,7 +12,7 @@ import { createRequire } from "node:module";
// The base names some OTHER file: unrecognized — the member fence.
const req = createRequire("/tmp/parent.js");
console.log(String(req("./config.cjs")));
// The supported base still fences dynamic require calls and program-module loads.
const require = createRequire(import.meta.url);
// A computed specifier: fenced by name.
-3
View File
@@ -1,3 +0,0 @@
// @dynamic
const url = import.meta.url;
console.log(url);
-2
View File
@@ -1,2 +0,0 @@
const resolved = import.meta.resolve("./module.mjs");
console.log(resolved);
@@ -0,0 +1,9 @@
import { createRequire } from "node:module";
const specifier = process.env.MODULE_NAME ?? "./fallback.mjs";
console.log(import.meta.resolve(specifier));
// createRequire uses the same static-literal boundary.
const require = createRequire(import.meta.url);
console.log(require.resolve(specifier));
console.log(require.resolve("./fallback.mjs", { paths: process.argv }));
// Dynamic requests and dynamic search paths remain explicit refusals.
+5
View File
@@ -12,6 +12,11 @@ const require = createRequire(import.meta.url);
// A plain CJS package: module.exports members through the island.
const zoo = require("cjszoo") as any;
console.log("zoo:", zoo.alpha as string, zoo.extra as string, zoo.gamma() as string);
console.log(
"zoo resolve:",
(zoo.resolved as string).endsWith("/cjszoo/core.js"),
(zoo.resolvePaths as string[])[0]?.endsWith("/cjszoo"),
);
// The dual package: "exports" splits import/require — require() loads
// the CJS arm, exactly Node's createRequire.
+3 -1
View File
@@ -1,7 +1,9 @@
// @dynamic
// Dual-published package: the ESM ("import" condition) entry wins, exactly
// like Node resolving an import statement.
import { flavor } from "dual";
import { cjsUrl, flavor, zooUrl } from "dual";
const f: string = flavor;
console.log(f);
console.log(cjsUrl.endsWith("/dual/index.cjs"));
console.log(zooUrl.endsWith("/cjszoo/index.js"));
+2
View File
@@ -3,3 +3,5 @@
"use strict";
module.exports = require("./core.js");
module.exports.extra = "extra";
module.exports.resolved = require.resolve("./core.js");
module.exports.resolvePaths = require.resolve.paths("./core.js");
+2
View File
@@ -1 +1,3 @@
export declare const flavor: string;
export declare const cjsUrl: string;
export declare const zooUrl: string;
+2
View File
@@ -1 +1,3 @@
export const flavor = "esm";
export const cjsUrl = import.meta.resolve("./index.cjs");
export const zooUrl = import.meta.resolve("cjszoo");
@@ -1,3 +1,12 @@
create-require.ts:13:7 - error SC2011: values of type 'ScriptcRequire' have no static representation but run in the embedded dynamic engine, which this build does not include
12 | // The base names some OTHER file: unrecognized — the member fence.
13 | const req = createRequire("/tmp/parent.js");
| ^~~
14 | console.log(String(req("./config.cjs")));
hint: build with --dynamic to run these values in the embedded engine (adds ~620KB to the binary), or restate the type inside the static surface (https://scriptc.dev/limitations describes the boundary)
create-require.ts:13:13 - error SC2020: 'module.createRequire' is part of the standard library types but has no scriptc lowering yet
12 | // The base names some OTHER file: unrecognized — the member fence.
@@ -7,6 +16,15 @@ create-require.ts:13:13 - error SC2020: 'module.createRequire' is part of the st
hint: the lowered shape is a const binding over createRequire(import.meta.url) (or __filename) whose require calls take STATIC string literals — builtins, relative .json documents, and installed npm packages (under --dynamic) resolve at build time; dynamic specifiers cannot exist in a compiled binary's fixed module graph
create-require.ts:14:20 - error SC2004: uses of 'req' inherit the blocker on its declaration
13 | const req = createRequire("/tmp/parent.js");
14 | console.log(String(req("./config.cjs")));
| ^~~
15 | // The supported base still fences dynamic require calls and program-module loads.
hint: the declaration of 'req' did not compile — fix the diagnostic reported there and these sites clear with it
create-require.ts:20:1 - error SC2020: 'createRequire's require with this argument shape' is part of the standard library types but has no scriptc lowering yet
19 | const which = "left" + "-pad";
@@ -1,6 +0,0 @@
import-meta-dynamic.ts:2:13 - error SC1090: 'import.meta.url' in a dynamic island (module-loader metadata is supported only in static ESM) is not supported yet
1 | // @dynamic
2 | const url = import.meta.url;
| ^~~~~~~~~~~~~~~
3 | console.log(url);
@@ -1,7 +0,0 @@
import-meta-resolve.ts:1:18 - error SC2020: 'ImportMeta.resolve' is part of the standard library types but has no scriptc lowering yet
1 | const resolved = import.meta.resolve("./module.mjs");
| ^~~~~~~~~~~~~~~~~~~
2 | console.log(resolved);
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
@@ -0,0 +1,26 @@
module-resolution-introspection.ts:4:33 - error SC2020: 'import.meta.resolve with a runtime-computed specifier' is part of the standard library types but has no scriptc lowering yet
3 | const specifier = process.env.MODULE_NAME ?? "./fallback.mjs";
4 | console.log(import.meta.resolve(specifier));
| ^~~~~~~~~
5 | // createRequire uses the same static-literal boundary.
hint: a compiled binary has a fixed module graph — pass a string literal
module-resolution-introspection.ts:7:29 - error SC2020: 'require.resolve with a runtime-computed request' is part of the standard library types but has no scriptc lowering yet
6 | const require = createRequire(import.meta.url);
7 | console.log(require.resolve(specifier));
| ^~~~~~~~~
8 | console.log(require.resolve("./fallback.mjs", { paths: process.argv }));
hint: a compiled binary has a fixed module graph — pass a string literal
module-resolution-introspection.ts:8:47 - error SC2020: 'require.resolve with runtime-computed options' is part of the standard library types but has no scriptc lowering yet
7 | console.log(require.resolve(specifier));
8 | console.log(require.resolve("./fallback.mjs", { paths: process.argv }));
| ^~~~~~~~~~~~~~~~~~~~~~~
9 | // Dynamic requests and dynamic search paths remain explicit refusals.
hint: omit options or pass { paths: ["<static directory>", ...] }