Route more packages and values through Node islands (NI-4c–4g) (#1904)

This commit is contained in:
Chris Tate
2026-10-28 21:53:40 -05:00
committed by GitHub
62 changed files with 1307 additions and 59 deletions
@@ -1056,7 +1056,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"nodeIsland.defer": "scr_node_island_defer",
"nodeIsland.import": "scr_ni_import",
"nodeIsland.get": "scr_node_island_get",
"nodeIsland.getKey": "scr_ni_get",
"nodeIsland.getKey": "scr_node_island_get_key",
"nodeIsland.set": "scr_node_island_set",
"nodeIsland.setKey": "scr_node_island_set_key",
"nodeIsland.invoke": "scr_node_island_invoke",
@@ -1064,6 +1064,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"nodeIsland.applyTo": "scr_node_island_apply_to",
"nodeIsland.construct": "scr_node_island_construct",
"nodeIsland.instanceof": "scr_node_island_instanceof",
"nodeIsland.isArray": "scr_ni_is_array",
"nodeIsland.classMethods": "scr_node_island_class_methods",
"nodeIsland.subclass": "scr_node_island_subclass",
"nodeIsland.superConstruct": "scr_node_island_super_construct",
@@ -186,7 +186,10 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
),
unit((u) => u.opts.assert || u.regex || u.opts.symbol, "scr_assert.c"),
unit((u) => u.opts.inspect, "scr_inspect.c", "scr_console_native.c"),
unit((u) => u.opts.dynInvoke || u.opts.workers || u.nativeFetch, "scr_dyn_invoke.c"),
unit(
(u) => u.opts.dynInvoke || u.opts.workers || u.nativeFetch || u.opts.nodeIsland === true,
"scr_dyn_invoke.c",
),
// Buffer.prototype methods called on checked-dynamic values.
unit((u) => u.opts.dynBuffer === true && u.opts.dynInvoke, "scr_bytes_dyn.c"),
// Builtin functions read as values in JavaScript dependencies.
+2 -2
View File
@@ -117,7 +117,9 @@ export const DIAGNOSTIC_CODES = {
SC8010: "value crossing a Node island boundary in a form without an exact copy",
SC8011: "native class extending a Node island class outside the supported forms",
SC8020: "stateful module duplicated across the Node island boundary",
SC8021: "Node island evaluation notes (deferred or at startup)",
SC8022: "island code extends a class of a compiled module",
SC8023: "Node island build failed; built natively with runtime fences",
SC8030: "type patch: no range covers the installed copy",
SC8031: "type patch: selector matches nothing",
SC8032: "type patch: declaration already has the type",
@@ -156,8 +158,6 @@ export const RETIRED_DIAGNOSTIC_CODES = {
* abandoned, delete them here. */
export const RESERVED_DIAGNOSTIC_CODES = {
SC8012: "Node islands (ni/* branches)",
SC8021: "Node islands (ni/* branches)",
SC8023: "Node islands (ni/* branches)",
} as const satisfies Record<`SC${number}`, string>;
export type DiagnosticCode = keyof typeof DIAGNOSTIC_CODES;
@@ -442,6 +442,23 @@ export function nodeIslandBoundaryDiag(what: string, loc: SrcLoc): ScrDiagnostic
};
}
/** SC8023 — the build that ran fenced packages as Node islands failed, so
* the program was built natively with those runtime fences (each throws
* when reached). A warning on the native build, with the island build's
* first error. */
export function nodeIslandRetryFailedDiag(
packages: readonly string[],
first: ScrDiagnostic,
loc: SrcLoc,
): ScrDiagnostic {
return {
code: "SC8023",
message: `running ${packages.map((p) => `'${p}'`).join(", ")} in Node islands failed to compile (${first.code} at ${first.loc.file}:${first.loc.start}: ${first.message}); built natively instead, where their runtime fences throw when reached`,
loc,
hint: "fix the island build's error, or mark the packages with --node-island=<pkg> to see every error",
};
}
/** SC8030–SC8033 — a type patch (`scriptc.typePatches`) that did not
* apply as written: no range covers the installed copy (SC8030), its
* selector matches nothing (SC8031), the declaration already has the type
@@ -234,6 +234,19 @@ export interface NodeIslandsEnvelope {
unknownPackages: string[];
}
/** Node island notices of a build that succeeds (scope-B): SC8001 and
* SC8021 notes (a package runs in a Node island, an island evaluates at
* boot) and the SC8020 warning (a stateful module duplicated across the
* boundary). The envelope reports all three as warnings. */
export function isNodeIslandNoticeCode(code: string): boolean {
return code === "SC8001" || code === "SC8020" || code === "SC8021" || code === "SC8023";
}
/** SC8001 and SC8021 are notes, not warnings. */
export function isNodeIslandNoteCode(code: string): boolean {
return code === "SC8001" || code === "SC8021";
}
/** The divergence band: compiles, but the program can behave differently
* from Node. Warnings, never build failures. */
export function isDivergenceCode(code: string): boolean {
@@ -370,6 +383,7 @@ export function locateDiagnostic(
severity:
isDivergenceCode(diag.code) ||
isPerformanceCode(diag.code) ||
isNodeIslandNoticeCode(diag.code) ||
diag.code === "SC8004" ||
isTypePatchCode(diag.code)
? "warning"
+15 -5
View File
@@ -4,6 +4,7 @@
* longer than FRAME_MAX_WIDTH (minified sources) shows a window of that
* width around the diagnostic's column, with "…" where text is cut.
*/
import { isNodeIslandNoteCode, isNodeIslandNoticeCode } from "./envelope.js";
import { isTypePatchCode, type ScrDiagnostic } from "./diagnostic.js";
export interface RenderOptions {
@@ -102,14 +103,21 @@ export function renderDiagnostic(
frame.push(
c(DIM, ` ${" ".repeat(gutterWidth)} | `) +
" ".repeat(caretCol) +
c(isWarningCode(diag.code) ? YELLOW : RED, "^" + "~".repeat(spanOnLine - 1)),
c(
isWarningCode(diag.code) || isNodeIslandNoticeCode(diag.code) ? YELLOW : RED,
"^" + "~".repeat(spanOnLine - 1),
),
);
emitLine(lineNum + 1);
}
const warning = isWarningCode(diag.code);
// Node island notices of a successful build: SC8020 warns, SC8001 and
// SC8021 are notes.
const note = isNodeIslandNoteCode(diag.code);
const warning = isWarningCode(diag.code) || diag.code === "SC8020" || diag.code === "SC8023";
const severity = note ? c(CYAN, "note") : warning ? c(YELLOW, "warning") : c(RED, "error");
out.push(
`${c(BOLD, `${diag.loc.file}:${lineNum}:${colNum}`)} - ${warning ? c(YELLOW, "warning") : c(RED, "error")} ${c(BOLD, diag.code)}: ${diag.message}`,
`${c(BOLD, `${diag.loc.file}:${lineNum}:${colNum}`)} - ${severity} ${c(BOLD, diag.code)}: ${diag.message}`,
);
if (frame.length) {
out.push("", ...frame);
@@ -191,9 +199,11 @@ export function renderWarnings(
sourceTextByFile: Map<string, string>,
opts: RenderOptions = {},
): string {
const n = diags.length;
const n = diags.filter((d) => !isNodeIslandNoteCode(d.code)).length;
const rendered = renderDiagnostics([...diags], sourceTextByFile, opts);
if (n === 0) return rendered;
return (
renderDiagnostics([...diags], sourceTextByFile, opts) +
rendered +
`\n\n${n} warning${n === 1 ? "" : "s"}: the program compiled, but ${n === 1 ? "this site" : "these sites"} can behave differently from Node.`
);
}
+159 -23
View File
@@ -4,6 +4,7 @@ import { frontendDisposed } from "../frontend/dispose-hook.js";
import {
checkerPanicDiag,
nodeIslandNoteDiag,
nodeIslandRetryFailedDiag,
npmStaticRefusalDiag,
iceDiag,
isCheckerPanic,
@@ -70,9 +71,10 @@ export function prepareExecutableModule(
timing("frontend-island-reload");
}
let lowered: LowerResult;
let fenceEdges: readonly PackageEdge[] | undefined;
let sourceTexts: Map<string, string>;
let disposed = false;
let fenceGrown: Map<string, IslandPackage> | null = null;
const fenceSure = new Set<string>();
// The frontend (and its tsgo server) is released as soon as lowering
// ends — clang and the link never hold it open.
try {
@@ -165,6 +167,31 @@ export function prepareExecutableModule(
retryFences,
);
}
// A failed program whose compiled packages need the runtime or a
// mutable object model (NI-2's report): try them as islands. If that
// build fails too, its failure is the one reported. This build typed
// those packages from their JavaScript, where the types they only
// declare (interfaces, aliases such as `Static<T>`) are `any`, so its
// diagnostics describe a program the island build does not compile:
// there the packages' declarations type the user code.
const marked = retryFences
? fenceIslands(entryPath, opts, fe, lowered, lowerIslands, false)
: null;
if (marked !== null) {
fe.dispose();
frontendDisposed();
disposed = true;
return prepareExecutableModule(
entryPath,
opts,
ffi,
buildPlatform,
createFrontend,
timing,
marked,
false,
);
}
return fail(lowered.diagnostics);
}
@@ -187,10 +214,6 @@ export function prepareExecutableModule(
return fail([targetRefusalDiag("wasm32-wasi", unavailable.surface, unavailable.loc)]);
}
}
// The package import edges island selection reads after lowering, taken
// while the frontend is open.
if (retryFences && lowered.runtimeFences.some(sureFence))
fenceEdges = fe.nodeIslandScan?.({ userJs: false }).edges;
sourceTexts = fe.sourceTexts();
if (lowered.contracts) {
const checked = checkContracts(lowered.module, lowered.contracts, sourceTexts);
@@ -198,6 +221,8 @@ export function prepareExecutableModule(
if (checked.diagnostics.length > 0) return fail(checked.diagnostics);
if (checked.contracts) lowered.module.contracts = checked.contracts;
}
if (retryFences)
fenceGrown = fenceIslands(entryPath, opts, fe, lowered, lowerIslands, true, fenceSure);
} finally {
if (!disposed) {
fe.dispose();
@@ -218,17 +243,21 @@ export function prepareExecutableModule(
...(lowered.performance ?? []),
],
};
// Runtime fences in a compiled package that surely fire: fences that need
// the JavaScript runtime itself (R1: code from strings, computed
// import()/require(), addons, loader hooks), and fences in top-level
// module code, which throw when the module evaluates. Build again with
// those packages as islands, and keep this build when that one cannot
// compile. A gap fence inside a function body may never run, so it stays
// a deferred refusal.
const runtimeFences = lowered.runtimeFences.filter(sureFence);
if (retryFences && runtimeFences.length > 0) {
const grown = loweringIslands(entryPath, opts, fe, runtimeFences, lowerIslands, fenceEdges);
if (grown !== null) {
// Packages compiled with runtime fences that need the JavaScript
// runtime or a mutable object model (fenceIslands): build again with
// them as islands, and keep this build when that one cannot compile.
// If that build fails, retry with only the packages whose fences fire
// for sure (module-init or runtime fences); if that fails too, the native
// build carries an SC8023 warning with the island build's first error,
// never a silent fallback.
if (fenceGrown !== null) {
const attempts = [fenceGrown];
const sure = new Map(
[...fenceGrown].filter(([name]) => lowerIslands.has(name) || fenceSure.has(name)),
);
if (sure.size > lowerIslands.size && sure.size < fenceGrown.size) attempts.push(sure);
let firstError: ScrDiagnostic | null = null;
for (const islands of attempts) {
const islanded = prepareExecutableModule(
entryPath,
opts,
@@ -236,21 +265,22 @@ export function prepareExecutableModule(
buildPlatform,
createFrontend,
timing,
grown,
islands,
false,
);
if (islanded.ok) return islanded;
firstError ??= islanded.diagnostics[0] ?? null;
}
if (firstError !== null) {
const added = [...fenceGrown.keys()].filter((name) => !lowerIslands.has(name));
prepared.warnings.push(
nodeIslandRetryFailedDiag(added, firstError, { file: entryPath, start: 0, end: 0 }),
);
}
}
return prepared;
}
/** A run-time fence that surely fires: it needs the JavaScript runtime
* itself, or it is in top-level module code. */
function sureFence(d: ScrDiagnostic): boolean {
return d.moduleInit === true || classifyDiagnostic(d, "fence").class === "runtime";
}
/** The packages a build runs in Node islands, or null for none: under
* `--node-islands=auto` (the default) every npm package the native
* pipeline refused, plus packages marked `node`; under `explicit` only the
@@ -473,3 +503,109 @@ function preflightIslands(
}
return grown.size === known.size ? null : grown;
}
/** Packages compiled natively with runtime fences (refusals deferred to the
* operation that reaches them) that should run in islands instead: those
* with a fence that surely fires (one in top-level module code, which runs
* when the module evaluates) or that itself needs the runtime (R1). Other
* fences stay deferred refusals until decision 8 (D6) retires them, so
* typed packages like jose stay native. With `requireFence` false (after a
* failed build) the package's island class by NI-2's report decides
* instead: runtime (R1) or object-model (R2). `--node-islands=auto` only;
* null for none. */
function fenceIslands(
entryPath: string,
opts: CompileRequestOptions,
fe: ReturnType<FrontendFactory>,
lowered: LowerResult,
known: ReadonlyMap<string, IslandPackage>,
requireFence = true,
sure?: Set<string>,
): Map<string, IslandPackage> | null {
if (fe.nodeIslandScan === undefined) return null;
if (requireFence && lowered.runtimeFences.length === 0) return null;
const policy = nodeIslandPolicy(readProjectConfig(entryPath), opts.nodeIslands);
if (policy.mode !== "auto" || known.size > 32) return null;
// An explicit --npm-static request keeps its package native.
const explicit = new Set(
Array.isArray(opts.npmStatic) ? opts.npmStatic.map((n) => n.replace(/\.\.\.$/, "")) : [],
);
const compiled = new Set(
fe.npmStatic
.filter((e) => e.status === "static" && !explicit.has(e.package))
.map((e) => e.package),
);
const fenced = new Map<string, boolean>();
for (const d of lowered.runtimeFences) {
const at = packageDirOf(d.loc.file);
if (at === null || !compiled.has(at.name) || known.has(at.name)) continue;
if (policy.forcedNative.has(at.name) || policy.unloaded.has(at.name)) continue;
// A fence in top-level module code throws when the module evaluates:
// it counts like one that needs the runtime.
const runtime = d.moduleInit === true || classifyDiagnostic(d, "fence").class === "runtime";
fenced.set(at.name, (fenced.get(at.name) ?? false) || runtime);
if (runtime) sure?.add(at.name);
}
if (requireFence && fenced.size === 0) return null;
const scan = fe.nodeIslandScan({ userJs: false });
const report = buildNodeIslandReport({
entry: entryPath,
scan,
npm: fe.npmStatic,
diagnostics: lowered.diagnostics,
fences: lowered.runtimeFences,
policy,
});
const grown = new Map(known);
for (const island of report.islands) {
const runtimeFence = requireFence
? fenced.get(island.package)
: (fenced.get(island.package) ?? false);
if (runtimeFence === undefined || island.path === null) continue;
if (!compiled.has(island.package) || known.has(island.package)) continue;
if (policy.forcedNative.has(island.package) || policy.unloaded.has(island.package)) continue;
// With fences (requireFence), only fences that surely fire count: a
// gap fence inside a function body may never run (jose's typed paths
// stay native, DECISIONS.md 2026-10-21). After a failed build
// (!requireFence), the package's report class decides.
if (requireFence && !runtimeFence) continue;
if (!runtimeFence && island.class !== "runtime" && island.class !== "object-model") continue;
grown.set(island.package, {
name: island.package,
version: island.version,
dir: island.path,
class: island.class,
reason: island.reason,
});
}
// A native ES module that imports an island CommonJS module would read
// its bindings from the module's exports object, where Node gives it the
// namespace its CommonJS facade builds (the lexed names, module.exports
// as the default): the importing package runs in the island too.
for (let added = true; added;) {
added = false;
for (const edge of scan.edges) {
const name = edge.to;
if (!grown.has(name) || edge.fromDir === null || edge.shim) continue;
const at = packageDirOf(edge.fromFile);
if (at === null || grown.has(at.name) || !compiled.has(at.name)) continue;
if (policy.forcedNative.has(at.name) || policy.unloaded.has(at.name)) continue;
if (!isEsmFile(edge.fromFile)) continue;
const target =
resolveBareModule(edge.fromFile, name, "js-only") ??
resolveBareModule(edge.fromFile, name, "js-only", "require");
if (target === null || isEsmFile(target.typesFile)) continue;
grown.set(at.name, {
name: at.name,
version: packageVersionOf(at.dir),
dir: at.dir,
class: "packaging",
reason: `an ES module of the package imports the CommonJS island package '${name}'`,
});
// It goes wherever that island goes, the sure-fence retry included.
if (known.has(name) || sure?.has(name) === true) sure?.add(at.name);
added = true;
}
}
return grown.size === known.size ? null : grown;
}
@@ -4919,6 +4919,9 @@ function lowerArrayIsArrayCall(
// itself instead of folding on the extracted type (typeof's rule).
if (arg.kind === "dynCheck" && arg.value.type.kind === "dyn") arg = arg.value;
if (arg.type.kind === "dyn") {
// With Node islands the value may be an island array (scope-B NI-4).
if (lowerer.nodeIslands !== null)
return { kind: "libCall", fn: "nodeIsland.isArray", args: [arg], type: BOOL, loc };
return { kind: "dynTest", test: "array", value: arg, type: BOOL, loc };
}
if (arg.type.kind === "union") {
@@ -5142,11 +5145,22 @@ function lowerUnionToStringCall(
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (call.questionDotToken || access.questionDotToken) return null;
if (access.name.text !== "toString" || call.arguments.nodes.length !== 0) return null;
if (access.name.text !== "toString") return null;
// `.toString("utf8")` on `string | Buffer` (a Buffer's default decode;
// String.prototype.toString ignores its arguments): the same helper.
const args = call.arguments.nodes;
const utf8Arg =
args.length === 1 && ts.isStringLiteralLike(args[0]!) && /^utf-?8$/i.test(args[0].text);
if (args.length !== 0 && !utf8Arg) return null;
const recvT = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
if (recvT?.kind !== "union") return null;
if (!lowerer.isStdlibMember(access)) return null;
const def = lowerer.unions.get(recvT.unionId);
if (
utf8Arg &&
!def?.arms.every((a) => a.kind === "string" || (a.kind === "bytes" && a.elem === "u8"))
)
return null;
const stringable = def?.arms.every(
(a) =>
a.kind === "string" ||
@@ -3,7 +3,7 @@ import { isIslandSubclassDecl } from "../../node-islands/boundary.js";
import { computedGlobalReadOf, globalValuesInstall } from "./global-values.js";
import type { TypeFailureSlot } from "../../diagnostics/diagnostic.js";
import { THREADS_MODULE } from "../threads-import.js";
import { collectIslandImport } from "./lower-node-island.js";
import { collectIslandImport, collectIslandRequires } from "./lower-node-island.js";
import { NODE_API_MODULE } from "../node-api-import.js";
import { positionTag } from "./structural-positions.js";
import { EXPERIMENTAL_MODULE } from "../experimental-import.js";
@@ -732,7 +732,10 @@ export function collectNpmImports(lowerer: Lowerer, parts: FileParts[]): void {
}
}
}
for (const fp of parts) collectDynamicImports(lowerer, fp.sf);
for (const fp of parts) {
collectDynamicImports(lowerer, fp.sf);
collectIslandRequires(lowerer, fp.sf);
}
}
/** Literal dynamic-import collection: every
@@ -413,10 +413,32 @@ export function collectIslandImport(
// module evaluated.
const clause = ts.isImportDeclaration(stmt) ? stmt.importClause : undefined;
const named = clause?.namedBindings;
const snapshotNames: string[] = [];
if (!binding.esm && named !== undefined && ts.isNamedImports(named)) {
for (const el of named.elements.nodes) {
if (el.isTypeOnly) continue;
const name = el.propertyName?.text ?? el.name.text;
snapshotNames.push(el.propertyName?.text ?? el.name.text);
}
}
// A CommonJS namespace import's members are the same snapshot: the
// members the file reads (`ns.name`).
if (!binding.esm && named !== undefined && ts.isNamespaceImport(named)) {
const nsSymbol = lowerer.checker.getSymbolAtLocation(named.name);
ts.walkPreorder(sf, (node) => {
if (
ts.isPropertyAccessExpression(node) &&
ts.isIdentifier(node.expression) &&
ts.isIdentifier(node.name) &&
node.expression.text === named.name.text &&
lowerer.checker.getSymbolAtLocation(node.expression) === nsSymbol &&
!snapshotNames.includes(node.name.text)
)
snapshotNames.push(node.name.text);
return undefined;
});
}
{
for (const name of snapshotNames) {
if (name === "default") continue;
let g = binding.snapshots.get(name);
if (g === undefined) {
@@ -451,6 +473,7 @@ function islandGet(target: IrExpr, key: string, loc: SrcLoc): IrExpr {
}
type IslandBinding =
| { kind: "required"; module: IslandModuleBinding }
| { kind: "namespace"; module: IslandModuleBinding }
| { kind: "default"; module: IslandModuleBinding }
| { kind: "named"; module: IslandModuleBinding; name: string };
@@ -487,7 +510,13 @@ function islandBindingOf(lowerer: Lowerer, ident: ts.Identifier): IslandBinding
const name = decl.propertyName?.text ?? decl.name.text;
binding = name === "default" ? "default" : { name };
} else {
return null;
// `const x = require("pkg")` (a CommonJS module of a native package
// requiring an island package): x is module.exports.
const spec = requireSpecOf(lowerer, decl);
if (spec === null) return null;
const path = islandModuleFile(islands, ts.getSourceFileOfNode(decl).fileName, spec, "require");
const module = path === null ? undefined : lowerer.islandModules.get(path);
return module === undefined ? null : { kind: "required", module };
}
const spec = moduleSpecifierOf(importDecl);
if (spec === null || importDecl === undefined) return null;
@@ -514,6 +543,8 @@ function islandBindingValue(
): IrExpr {
const ns = varRef(b.module.global.id, DYN, loc);
switch (b.kind) {
case "required":
return ns;
case "namespace":
if (!b.module.esm)
boundaryFail(lowerer, "the namespace object of a CommonJS island module as a value", node);
@@ -624,6 +655,8 @@ function copiedType(lowerer: Lowerer, t: IrType): boolean {
case "bigint":
case "nullT":
case "undefinedT":
// Buffers and typed arrays share their memory with the V8 view.
case "bytes":
return true;
case "union": {
const def = lowerer.unions.get(t.unionId);
@@ -844,11 +877,20 @@ function toIsland(lowerer: Lowerer, node: ts.Expression): IrExpr {
// NI-5: the runtime exports dyn objects by reference and copies only
// primitives, strings and (for now) arrays.
return lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN);
case "refused":
case "refused": {
const type = lowerer.typeOf(node);
// A type parameter the instance binds to dyn (a generic function
// over island types, `<T extends TSchema>`): its values are dyn
// values, and they cross the way `any` values do.
if (
(type.flags & ts.TypeFlags.TypeParameter) !== 0 &&
lowerer.typeParamResolver(type)?.kind === "dyn"
)
return lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN);
// NI-5: closures, promises, records and class instances cross by
// reference (scr_node_island_export.c): their dyn boxes keep the
// native referent, and the closure's thunk checks its arguments.
if (exportedByReference(lowerer, lowerer.typeOf(node))) {
if (exportedByReference(lowerer, type)) {
const lowered = lowerer.lowerExpr(node);
const value = lowerer.coerceToExpected(lowered, DYN);
return lowered.type.kind === "object"
@@ -857,9 +899,10 @@ function toIsland(lowerer: Lowerer, node: ts.Expression): IrExpr {
}
return boundaryFail(
lowerer,
`a '${lowerer.checker.typeToString(lowerer.typeOf(node))}' value (a native object would cross as a copy and lose its identity)`,
`a '${lowerer.checker.typeToString(type)}' value (a native object would cross as a copy and lose its identity)`,
node,
);
}
}
}
@@ -1041,6 +1084,11 @@ function freshLiteral(
node: ts.ObjectLiteralExpression | ts.ArrayLiteralExpression,
): IrExpr {
const loc = locOf(node);
// Spreads, methods, accessors and computed keys: the literal lowers as
// the program's own object, which crosses by reference (NI-5 exports);
// nothing else holds it, so that is exact.
if (!plainLiteral(node))
return lowerer.coerceToExpected(lowerer.lowerExprExpecting(node, DYN), DYN);
if (ts.isArrayLiteralExpression(node)) {
const elems = node.elements.nodes.map((el) => {
if (ts.isSpreadElement(el) || ts.isOmittedExpression(el))
@@ -1324,7 +1372,7 @@ export function lowerIslandExpr(lowerer: Lowerer, expr: ts.Expression): IrExpr |
if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) {
if (!isForeignExpr(lowerer, callee.expression)) return null;
lowerer.usesNodeIsland = true;
const target = islandReceiver(lowerer, callee.expression, callee.name.text, loc);
const target = islandReceiver(lowerer, callee.expression, callee.name.text, loc, true);
if (target.direct !== undefined) {
return fromIsland(
lowerer,
@@ -1603,12 +1651,14 @@ function islandCompoundAssign(
/** The receiver of a member operation on an island value. A CommonJS
* namespace binding's members are module.exports' (and `default` is
* module.exports itself), answered directly. */
* module.exports itself), answered directly; a call of a member keeps
* module.exports as its receiver, so it goes through the method call. */
function islandReceiver(
lowerer: Lowerer,
receiver: ts.Expression,
member: string,
loc: SrcLoc,
call = false,
): { value: IrExpr; direct?: IrExpr } {
const inner = stripTypeWrappers(receiver);
if (ts.isIdentifier(inner)) {
@@ -1616,6 +1666,9 @@ function islandReceiver(
if (b !== null && b.kind === "namespace" && !b.module.esm) {
const exports = varRef(b.module.global.id, DYN, loc);
if (member === "default") return { value: exports, direct: exports };
const snapshot = b.module.snapshots.get(member);
if (snapshot !== undefined && !call)
return { value: exports, direct: varRef(snapshot.id, DYN, loc) };
return { value: exports };
}
}
@@ -1943,6 +1996,82 @@ export function lowerIslandSuperMethodCall(
return fromIsland(lowerer, raw, call);
}
/** A literal freshLiteral copies member by member: identifier or string
* keys with values, shorthand properties, array elements without spreads
* or holes. */
function plainLiteral(node: ts.ObjectLiteralExpression | ts.ArrayLiteralExpression): boolean {
if (ts.isArrayLiteralExpression(node))
return node.elements.nodes.every(
(el) => !ts.isSpreadElement(el) && !ts.isOmittedExpression(el),
);
return node.properties.nodes.every(
(p) =>
(ts.isPropertyAssignment(p) && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))) ||
ts.isShorthandPropertyAssignment(p),
);
}
/** The specifier of `x = require("spec")` (a variable declaration binding
* an identifier to a literal require), or null. */
function requireSpecOf(lowerer: Lowerer, decl: ts.Node): string | null {
if (
!ts.isVariableDeclaration(decl) ||
!ts.isIdentifier(decl.name) ||
decl.initializer === undefined
)
return null;
// Only a const keeps the module: a let or var can be reassigned.
const list = decl.parent;
if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) return null;
let init: ts.Expression = decl.initializer;
while (ts.isParenthesizedExpression(init)) init = init.expression;
if (
!ts.isCallExpression(init) ||
!ts.isIdentifier(init.expression) ||
init.expression.text !== "require" ||
init.arguments.nodes.length !== 1
)
return null;
// The global require, not a function of the program named require.
if (
lowerer.checker
.getSymbolAtLocation(init.expression)
?.declarations.some((d) => !ts.getSourceFileOfNode(d).isDeclarationFile)
)
return null;
const arg = init.arguments.nodes[0]!;
return ts.isStringLiteralLike(arg) ? arg.text : null;
}
/** Top-level `const x = require("pkg")` statements of island packages in a
* compiled module (a native package requiring an island package): load the
* island module in the module's %init prelude (shortcut: before the body,
* not at the statement). */
export function collectIslandRequires(lowerer: Lowerer, sf: ts.SourceFile): void {
const islands = lowerer.nodeIslands;
if (islands === null) return;
for (const stmt of sf.statements.nodes) {
if (!ts.isVariableStatement(stmt)) continue;
for (const decl of stmt.declarationList.declarations.nodes) {
const spec = requireSpecOf(lowerer, decl);
if (spec === null) continue;
const path = islandModuleFile(islands, sf.fileName, spec, "require");
if (path === null) continue;
lowerer.usesNodeIsland = true;
const loc = locOf(decl);
const binding = islandModuleGlobal(lowerer, path);
const actions = lowerer.jsonInitActions.get(sf) ?? [];
lowerer.jsonInitActions.set(sf, actions);
actions.push({
kind: "assign",
localId: binding.global.id,
value: loadIslandModule(lowerer, path, loc),
loc,
});
}
}
}
/* `x instanceof Error` for values that may be island objects (NI-5c). */
const INTRINSIC_ERRORS = new Set([
"Error",
@@ -341,6 +341,11 @@ export function statefulReason(sf: ts.SourceFile): string | null {
)
continue;
if (ts.isExpressionStatement(stmt) && ts.isStringLiteral(stmt.expression)) continue; // directives
// CommonJS export statements (`module.exports = f`, `exports.x = v`,
// `module.exports.x = v`) and expandos on the module's own functions
// (`f.sync = g`) declare the module's surface; they are not effects.
if (ts.isExpressionStatement(stmt) && isCjsDeclaringAssignment(stmt.expression, functions(sf)))
continue;
return "its evaluation has effects beyond declarations";
}
// Writes from function bodies to module-level bindings.
@@ -600,3 +605,39 @@ export function sharedModuleRegistrations(lowerer: Lowerer): IrStmt[] {
});
return out;
}
/** The names of a module's top-level function declarations. */
function functions(sf: ts.SourceFile): Set<string> {
const names = new Set<string>();
for (const stmt of sf.statements.nodes)
if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined) names.add(stmt.name.text);
return names;
}
/** `module.exports = …`, `exports.x = …`, `module.exports.x = …` (also
* chained: `exports = module.exports = …`), or `f.x = …` for a top-level
* function `f`. */
function isCjsDeclaringAssignment(e: ts.Expression, fns: Set<string>): boolean {
while (ts.isParenthesizedExpression(e)) e = e.expression;
if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return false;
const left = e.left;
const isModuleExports = (n: ts.Expression): boolean =>
ts.isPropertyAccessExpression(n) &&
ts.isIdentifier(n.expression) &&
n.expression.text === "module" &&
ts.isIdentifier(n.name) &&
n.name.text === "exports";
const target =
(ts.isIdentifier(left) && left.text === "exports") ||
isModuleExports(left) ||
(ts.isPropertyAccessExpression(left) &&
((ts.isIdentifier(left.expression) &&
(left.expression.text === "exports" || fns.has(left.expression.text))) ||
isModuleExports(left.expression)));
if (!target) return false;
// A chained assignment declares too.
const right = e.right;
if (ts.isBinaryExpression(right) && right.operatorToken.kind === ts.SyntaxKind.EqualsToken)
return isCjsDeclaringAssignment(right, fns);
return true;
}
+9 -2
View File
@@ -6410,7 +6410,11 @@ function preflight7(load: LoadResult): {
// optional-dependency try/require pattern), so no fence.
// Binding forms keep the fence: their downstream reads
// would need the module that never loads.
if (req.decl !== null || probeNodeRequireRefusal(sf.fileName, req.spec) === null) {
if (
(req.decl !== null || probeNodeRequireRefusal(sf.fileName, req.spec) === null) &&
// A Node island package: loaded in Node at the require (NI-4).
!isFrontendIsland(packageNameOfSpecifier(req.spec))
) {
diags.push(unsupportedDiag("SC1010", loc, unsupportedModuleFeatureOf(req.spec)));
}
}
@@ -6467,7 +6471,10 @@ function preflight7(load: LoadResult): {
if (canonicalBuiltinModule(spec) === null && !processModuleAliasRequire7(spec, null)) {
// Binding-less by construction — same require-site throw
// channel as the statement-level form above.
if (probeNodeRequireRefusal(sf.fileName, spec) === null) {
if (
probeNodeRequireRefusal(sf.fileName, spec) === null &&
!isFrontendIsland(packageNameOfSpecifier(spec))
) {
diags.push(unsupportedDiag("SC1010", loc, unsupportedModuleFeatureOf(spec)));
}
}
@@ -204,6 +204,9 @@ function buildLibFnSignatures() {
"nodeIsland.applyTo": { argTypes: [DYN, DYN, DYN], result: DYN },
"nodeIsland.construct": { argTypes: [DYN, DYN], result: DYN },
"nodeIsland.instanceof": { argTypes: [DYN, DYN], result: BOOL },
/** Array.isArray of a dyn value in a program with Node islands: island
* arrays answer true too. */
"nodeIsland.isArray": { argTypes: [DYN], result: BOOL },
/** NI-5: a class instance crossing into an island, with the builder of
* its class's method table (run once per class). Answers the value. */
"nodeIsland.classMethods": { argTypes: [DYN, DYN], result: DYN },
@@ -41,7 +41,7 @@ import {
export const NODE_ISLAND_MIN_NODE = "24.13.1";
/** The bridge ABI this compiler's runtime speaks (scr_node_bridge.h). */
export const NODE_BRIDGE_ABI = 8;
export const NODE_BRIDGE_ABI = 9;
/** The prefix of a recorded path relative to the executable's directory. */
export const ORIGIN = "$ORIGIN/";
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6818-node-island-mixed/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6819-node-island-fenced/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6825-node-island-init-fence/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6826-node-island-undefined-args/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6829-node-island-declared-types/main.ts"
],
"diags": []
}
@@ -1441,6 +1441,11 @@ Local<Value> ExportValue(Isolate *isolate, Local<Context> context, const ScrNode
} else {
obj = ExportTemplate(isolate)->NewInstance(context).ToLocalChecked();
obj->SetInternalField(0, data);
// A template instance inherits from its implicit constructor's own
// prototype object; a native plain object is a plain object to island
// code (`Object.getPrototypeOf(o) === Object.prototype`, as jose's
// isObject checks), so it inherits Object.prototype directly.
obj->SetPrototypeV2(context, v8::Object::New(isolate)->GetPrototypeV2()).FromMaybe(false);
}
if (!obj->IsProxy()) obj->SetPrivate(context, ExportKey(isolate), data).Check();
Global<Object> &slot = g_exports[id];
@@ -1608,6 +1613,41 @@ Local<Value> ToV8(Isolate *isolate, Local<Context> context, const ScrNodeValue *
}
}
// Node's arrow header for an exception's throw site, recorded once on the
// error object (as Node decorates an error the first time it is thrown):
// "<resource>:<line>\n<source line>\n<caret>", the caret under the
// message's columns with tabs kept. Printed by the runtime when the error
// ends the program uncaught (ErrorInfo's `arrow`).
Local<v8::Private> ArrowKey(Isolate *isolate) {
return v8::Private::ForApi(isolate, Utf8(isolate, "scriptc.node-island.arrow"));
}
void RecordArrow(Isolate *isolate, Local<Context> context, const TryCatch &try_catch,
Local<Value> exception) {
if (!exception->IsObject()) return;
Local<Object> error = exception.As<Object>();
Local<v8::Private> key = ArrowKey(isolate);
if (error->HasPrivate(context, key).FromMaybe(true)) return;
// Node decorates from the error's own message (its construction site,
// as Exception::CreateMessage answers for an Error with a stack), not
// from where the throw left the call.
Local<v8::Message> message = v8::Exception::CreateMessage(isolate, exception);
if (message.IsEmpty()) message = try_catch.Message();
if (message.IsEmpty()) return;
Local<String> source;
if (!message->GetSourceLine(context).ToLocal(&source)) return;
std::string line = *String::Utf8Value(isolate, source);
std::string resource = *String::Utf8Value(isolate, message->GetScriptResourceName());
int linenum = message->GetLineNumber(context).FromMaybe(0);
int start = message->GetStartColumn(context).FromMaybe(0);
int end = message->GetEndColumn(context).FromMaybe(start + 1);
std::string arrow = resource + ":" + std::to_string(linenum) + "\n" + line + "\n";
for (int i = 0; i < start; i++)
arrow += (i < static_cast<int>(line.size()) && line[i] == '\t') ? '\t' : ' ';
for (int i = start; i < end || i == start; i++) arrow += '^';
(void)error->SetPrivate(context, key, Utf8(isolate, arrow.data(), arrow.size()));
}
// One entry from the runtime: the isolate scope, V8's stack limit for the
// caller's stack, and the out-value scratch (reset by outermost entries).
struct Op {
@@ -1625,6 +1665,7 @@ struct Op {
}
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
Local<Value> exception = try_catch.Exception();
RecordArrow(e.isolate, e.context, try_catch, exception);
try_catch.Reset();
FromV8(e.isolate, e.context, exception, out);
} else {
@@ -1886,6 +1927,9 @@ void ErrorInfo(uint32_t h, ScrNodeErrorInfo *out) {
read("message", &out->message, &out->message_len, false);
read("stack", &out->stack, &out->stack_len, true);
read("code", &out->code, &out->code_len, true);
Local<Value> arrow;
if (error->GetPrivate(op.e.context, ArrowKey(op.e.isolate)).ToLocal(&arrow) && arrow->IsString())
out->arrow = Scratch(Wtf8(op.e.isolate, arrow.As<String>()), &out->arrow_len);
}
void DropHandle(uint32_t h) {
@@ -68,7 +68,9 @@ export const OPTIONAL_RUNTIME_SOURCES = [
["scr_assert.c", any("assert", "regex", "symbol")],
["scr_inspect.c", "inspect"],
["scr_console_native.c", "inspect"],
["scr_dyn_invoke.c", any("dynInvoke", "nativeFetch")],
// Island libCalls on native values typed by island declarations invoke
// through the checked-dynamic path (scr_node_island_handle.c).
["scr_dyn_invoke.c", any("dynInvoke", "nativeFetch", "nodeIsland")],
// Buffer.prototype methods called on checked-dynamic values.
["scr_bytes_dyn.c", all("dynBuffer", "dynInvoke")],
// Builtin functions read as values in JavaScript dependencies.
@@ -248,8 +250,13 @@ export function variantsFor(source) {
defines: ["SCR_JS_TIER", "SCR_HANDLE_CLASSES", "SCR_NODE_ISLANDS"],
});
}
// util.inspect and instanceof of Node island handles.
if (source === "scr_inspect.c" || source === "scr_dyn_builtin_class.c") {
// util.inspect and instanceof of Node island handles; Node's report for
// an uncaught island error.
if (
source === "scr_inspect.c" ||
source === "scr_dyn_builtin_class.c" ||
source === "scr_exception.c"
) {
variants.push({
id: "node-island",
when: { nodeIsland: true },
+13
View File
@@ -9,6 +9,9 @@
* dummy returns); this file only stores, renders, and releases the value.
*/
#include "scr_runtime.h"
#ifdef SCR_NODE_ISLANDS
#include "scr_node_island.h"
#endif
#include <stdatomic.h>
#include <stdio.h>
@@ -700,6 +703,16 @@ void scr_exc_print_uncaught(void) {
/* Settle any runtime-internal stdout fragment before the stderr line when
* both share an fd. JavaScript-visible writes already flush themselves. */
fflush(stdout);
#ifdef SCR_NODE_ISLANDS
/* An error thrown by Node island code: Node's own report (the throw
* site's source line, the stack, the Node.js version). */
if (scr_exc_kind == SCR_EXC_OBJ && scr_error_is(scr_exc_payload) &&
scr_ni_report_uncaught((ScrError *)scr_exc_payload)) {
scr_exc_reset(); /* the payload counts as live until released */
scr_exc_cell_changed();
return;
}
#endif
fputs("Uncaught ", stderr);
switch (scr_exc_kind) {
case SCR_EXC_F64: {
+3
View File
@@ -5060,6 +5060,9 @@ static bool scr_native_iterator_over(const ScrDyn *value, unsigned kind);
/* Node util.types reads internal brands, never user properties or prototypes. */
bool scr_dyn_util_type_is(const ScrDyn *value, const ScrStr *probe) {
if (!value) return false;
#ifdef SCR_NODE_ISLANDS
if (scr_ni_is_handle(value)) return scr_ni_util_type_is(value, probe);
#endif
#define PROBE(name) (probe->len == sizeof(name) - 1 && memcmp(probe->data, name, sizeof(name) - 1) == 0)
if (PROBE("isAnyArrayBuffer")) return scr_buffer_storage_is(value);
+6 -1
View File
@@ -17,7 +17,7 @@
#include <stddef.h>
#include <stdint.h>
#define SCR_NODE_BRIDGE_ABI 8u
#define SCR_NODE_BRIDGE_ABI 9u
/* The symbol a bridge library exports: `const ScrNodeBridge *(void)`. */
#define SCR_NODE_BRIDGE_ENTRY "scr_node_bridge_v1"
@@ -149,6 +149,11 @@ typedef struct ScrNodeErrorInfo {
uint32_t kind;
const char *name, *message, *stack, *code;
size_t name_len, message_len, stack_len, code_len;
/* Node's arrow header for the throw site ("<file>:<line>\n<source
* line>\n<caret>"), recorded when the error was thrown out of island
* code; NULL when none. */
const char *arrow;
size_t arrow_len;
} ScrNodeErrorInfo;
/* What the runtime provides to the bridge. */
+8
View File
@@ -87,6 +87,13 @@ ScrDyn *scr_ni_call_method(ScrDyn *obj, const char *key, size_t len, ScrDyn *con
size_t argc);
ScrDyn *scr_ni_construct(ScrDyn *ctor, ScrDyn *const *args, size_t argc);
ScrStr *scr_ni_typeof(ScrDyn *value);
/* util.types.<probe>(value) answered by Node for an island handle. */
bool scr_ni_util_type_is(const ScrDyn *value, const ScrStr *probe);
/* Array.isArray of a dyn value, island arrays included. */
bool scr_ni_is_array(ScrDyn *value);
/* Prints Node's uncaught-exception report for an error thrown by island
* code (true), or nothing (false: not an island error). */
bool scr_ni_report_uncaught(ScrError *error);
int scr_ni_instanceof(ScrDyn *value, ScrDyn *ctor);
bool scr_ni_is_handle(const ScrDyn *value);
/* ToPrimitive of an island object (hint SCR_DYN_HINT_*), run by V8:
@@ -195,6 +202,7 @@ ScrDyn *scr_node_island_apply_to(ScrDyn *fn, ScrDyn *this_, ScrDyn *args);
ScrDyn *scr_node_island_construct(ScrDyn *ctor, ScrDyn *args);
void scr_node_island_define_field(ScrDyn *self, ScrStr *key, ScrDyn *value);
ScrDyn *scr_node_island_get(ScrDyn *obj, ScrStr *key);
ScrDyn *scr_node_island_get_key(ScrDyn *obj, ScrDyn *key);
bool scr_node_island_instanceof(ScrDyn *value, ScrDyn *ctor);
bool scr_node_island_instanceof_global(ScrDyn *value, ScrStr *name);
ScrDyn *scr_node_island_invoke(ScrDyn *obj, ScrStr *key, ScrDyn *args);
+96 -5
View File
@@ -1457,6 +1457,58 @@ void scr_ni_handles_install(void) {
scr_atexit(nh_cleanup);
}
/* ── NI-4d: util.types, Array.isArray and uncaught reports ─────────────── */
static ScrDyn *nh_util_types = NULL; /* Node's util.types (held to exit) */
static void nh_util_types_cleanup(void) {
scr_dyn_release(nh_util_types);
nh_util_types = NULL;
}
bool scr_ni_util_type_is(const ScrDyn *value, const ScrStr *probe) {
if (nh_util_types == NULL) {
ScrStr *path = scr_str_new("node:util", 9);
ScrDyn *util = scr_ni_require(path);
scr_str_release(path);
if (util == NULL) return false;
nh_util_types = scr_ni_get_str(util, "types", 5);
if (nh_util_types != NULL) scr_atexit(nh_util_types_cleanup);
scr_dyn_release(util);
if (nh_util_types == NULL) return false;
}
ScrDyn *args[1] = {(ScrDyn *)value};
ScrDyn *r = scr_ni_call_method(nh_util_types, probe->data, probe->len, args, 1);
if (r == NULL) return false;
bool answer = r->kind == SCR_DYN_BOOL && r->v.b;
scr_dyn_release(r);
return answer;
}
bool scr_ni_is_array(ScrDyn *value) {
if (value == NULL) return false;
if (value->kind == SCR_DYN_ARR) return true;
if (!scr_ni_is_handle(value)) return value->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(value);
return (((ScrNodeRef *)value->v.handle.ptr)->flags & SCR_NVF_ARRAY) != 0;
}
bool scr_ni_report_uncaught(ScrError *error) {
ScrNodeRef *ref = nh_pinned(error, NH_PIN_ERROR);
const ScrNodeBridge *bridge = scr_ni_bridge_loaded();
if (ref == NULL || ref->id == 0 || bridge == NULL) return false;
ScrNodeErrorInfo info;
bridge->error_info(ref->id, &info);
if (info.stack == NULL) return false;
if (info.arrow != NULL && info.arrow_len > 0) {
fwrite(info.arrow, 1, info.arrow_len, stderr);
fputs("\n\n", stderr);
}
fwrite(info.stack, 1, info.stack_len, stderr);
fprintf(stderr, "\n\nNode.js v%s\n", bridge->node_version);
fflush(stderr);
return true;
}
/* ── NI-4a: libCall entry points of compiled island code ─────────────────
* The compiler (frontend/lowering/lower-node-island.ts) lowers member
* operations on island values to `nodeIsland.*` libCalls, which pass
@@ -1473,28 +1525,61 @@ static size_t ni_libcall_args(ScrDyn *args, ScrDyn *const **items) {
return args->v.arr.len;
}
/* Routing follows the value: a value typed by an island's declarations
* may still be a native object (an island interface annotating a native
* literal, a native error handed to an island callback). Those take the
* ordinary checked-dynamic operations instead of the bridge. */
ScrDyn *scr_dyn_reflect_get(ScrDyn *target, ScrDyn *raw_key, ScrDyn *receiver);
static bool ni_lc_island(const ScrDyn *v) { return v != NULL && scr_ni_is_handle(v); }
/* A NUL-terminated copy of `key` for the dyn invoke path (heap only for long names). */
static ScrDyn *ni_lc_native_invoke(ScrDyn *obj, ScrStr *key, ScrDyn *const *items, size_t argc) {
char small[128];
char *name = key->len < sizeof small ? small : malloc(key->len + 1);
if (name == NULL) scr_trap("scriptc: out of memory\n");
memcpy(name, key->data, key->len);
name[key->len] = '\0';
ScrDyn *result = scr_dyn_invoke(obj, name, items, argc, name);
if (name != small) free(name);
return result;
}
ScrDyn *scr_node_island_get(ScrDyn *obj, ScrStr *key) {
return scr_ni_get_str(obj, key->data, key->len);
if (ni_lc_island(obj)) return scr_ni_get_str(obj, key->data, key->len);
ScrDyn *k = scr_dyn_new_str(key);
ScrDyn *value = scr_dyn_reflect_get(obj, k, obj);
scr_dyn_release(k);
return value;
}
ScrDyn *scr_node_island_get_key(ScrDyn *obj, ScrDyn *key) {
if (ni_lc_island(obj)) return scr_ni_get(obj, key);
return scr_dyn_reflect_get(obj, key, obj);
}
void scr_node_island_set(ScrDyn *obj, ScrStr *key, ScrDyn *value) {
(void)scr_ni_set_str(obj, key->data, key->len, value);
if (ni_lc_island(obj)) (void)scr_ni_set_str(obj, key->data, key->len, value);
else scr_dyn_key_set(obj, key, value);
}
void scr_node_island_set_key(ScrDyn *obj, ScrDyn *key, ScrDyn *value) {
(void)scr_ni_set(obj, key, value);
if (ni_lc_island(obj)) (void)scr_ni_set(obj, key, value);
else scr_dyn_key_set_computed(obj, key, value);
}
ScrDyn *scr_node_island_invoke(ScrDyn *obj, ScrStr *key, ScrDyn *args) {
ScrDyn *const *items;
size_t argc = ni_libcall_args(args, &items);
return scr_ni_call_method(obj, key->data, key->len, items, argc);
if (ni_lc_island(obj)) return scr_ni_call_method(obj, key->data, key->len, items, argc);
return ni_lc_native_invoke(obj, key, items, argc);
}
ScrDyn *scr_node_island_apply(ScrDyn *fn, ScrDyn *args) {
ScrDyn *const *items;
size_t argc = ni_libcall_args(args, &items);
return scr_ni_call(fn, NULL, items, argc);
if (ni_lc_island(fn)) return scr_ni_call(fn, NULL, items, argc);
return scr_dyn_call(fn, items, argc, "value");
}
ScrDyn *scr_node_island_apply_to(ScrDyn *fn, ScrDyn *this_, ScrDyn *args) {
@@ -1505,6 +1590,12 @@ ScrDyn *scr_node_island_apply_to(ScrDyn *fn, ScrDyn *this_, ScrDyn *args) {
}
ScrDyn *scr_node_island_construct(ScrDyn *ctor, ScrDyn *args) {
if (!ni_lc_island(ctor)) {
ScrStr *what = scr_str_new("value", 5);
ScrDyn *result = scr_dyn_construct(ctor, args, what);
scr_str_release(what);
return result;
}
ScrDyn *const *items;
size_t argc = ni_libcall_args(args, &items);
return scr_ni_construct(ctor, items, argc);
@@ -0,0 +1,51 @@
// @requires-libnode
// A Node island package used from both sides: a natively compiled package
// requires it; island-declared interfaces type native objects too (member
// operations follow the value, not the declared type); options built with
// spread and method members cross in by reference; a `string | Buffer`
// member reads back from an island object; a rejection's error is read in
// a native callback.
import { localName, swapped, twice } from "wraplib";
import * as lib from "islandlib";
import {
countFiles,
describeFile,
blob,
text,
render,
failLater,
type Files,
type File,
type FailError,
} from "islandlib";
console.log("twice", twice(5));
console.log("swapped", swapped(), localName(), "ns call", lib.getValue());
const files: Files = {};
files["a.txt"] = { mode: 420, data: "alpha" };
files["b.txt"] = blob("beta");
console.log("files", countFiles(files), files["a.txt"]!.mode, Object.keys(files).join(","));
const local: File = files["a.txt"]!;
console.log("local", local.mode, describeFile(local), describeFile(files["b.txt"]!));
const fromIsland = blob("gamma");
const data = fromIsland.data;
console.log("buffer", typeof data === "string" ? data : data.toString("utf8"), describeFile(text("delta")));
const base = { name: "job", retries: 1 };
console.log(
"render",
render({
...base,
retries: 3,
format(name: string): string {
return name.toUpperCase();
},
}),
);
failLater("nope").catch((e: unknown) => {
const err = e as FailError;
console.log("caught", err.message, err.code, (e as { message?: string }).message);
});
+29
View File
@@ -0,0 +1,29 @@
export declare class Counter {
constructor(start: number);
value: number;
inc(): number;
}
export interface File {
mode: number;
data: string | Buffer;
}
export interface Files {
[path: string]: File;
}
export interface RenderOptions {
name: string;
retries: number;
format(name: string): string;
}
export interface FailError extends Error {
code: string;
}
export declare function makeCounter(start: number): Counter;
export declare function countFiles(files: Files): number;
export declare function describeFile(file: File): string;
export declare function blob(text: string): File;
export declare function text(text: string): File;
export declare function render(opts: RenderOptions): string;
export declare function failLater(message: string): Promise<never>;
export declare const value: number;
export declare function getValue(): number;
+22
View File
@@ -0,0 +1,22 @@
"use strict";
// Loads its parts with a computed specifier (R1.2): a Node island.
const { createRequire } = require("node:module");
const load = createRequire(__filename);
const { Counter } = load(["./lib", "counter"].join("/"));
exports.makeCounter = (start) => new Counter(start);
// A method that reads its receiver.
exports.value = 7;
exports.getValue = function () {
return this.value;
};
exports.Counter = Counter;
// A files map: native callers build their own with the same interface.
exports.countFiles = (files) => Object.keys(files).length;
exports.describeFile = (file) => `${file.mode}:${file.data.toString("utf8")}`;
exports.blob = (text) => ({ mode: 420, data: Buffer.from(text) });
exports.text = (text) => ({ mode: 384, data: text });
// Takes options built from spread and method members.
exports.render = (opts) => `${opts.name}/${opts.retries}/${opts.format(opts.name)}`;
// Settles a promise with an Error the caller inspects in .catch.
exports.failLater = (message) => Promise.reject(Object.assign(new Error(message), { code: "EFAIL" }));
@@ -0,0 +1,10 @@
"use strict";
class Counter {
constructor(start) {
this.value = start;
}
inc() {
return ++this.value;
}
}
exports.Counter = Counter;
@@ -0,0 +1 @@
{ "name": "islandlib", "version": "2.0.0", "main": "index.js", "types": "index.d.ts" }
+3
View File
@@ -0,0 +1,3 @@
export declare function twice(start: number): number;
export declare function swapped(): number;
export declare function localName(): string;
+15
View File
@@ -0,0 +1,15 @@
"use strict";
// A plain package scriptc compiles natively that requires an island package.
const islandlib = require("islandlib");
// A binding the module reassigns after requiring: reads follow the binding.
let swappable = require("islandlib");
swappable = { makeCounter: () => ({ inc: () => 100 }) };
exports.swapped = () => swappable.makeCounter(0).inc();
exports.localName = require("./local").localName;
exports.twice = function twice(start) {
const c = islandlib.makeCounter(start);
c.inc();
return c.inc();
};
+7
View File
@@ -0,0 +1,7 @@
"use strict";
// A module function named require: calls of it are its own.
function require(name) {
return { name: `local ${name}` };
}
const fake = require("islandlib");
exports.localName = () => fake.name;
@@ -0,0 +1 @@
{ "name": "wraplib", "version": "1.0.0", "main": "index.js", "types": "index.d.ts", "dependencies": { "islandlib": "2.0.0" } }
@@ -0,0 +1,10 @@
// @requires-libnode
// A package scriptc compiles natively only with runtime fences that need
// a mutable object model (getters over live bindings, a constructor
// function with prototype methods) runs in a Node island automatically.
import { bump, current, Tally } from "fencedkit";
import kit from "fencedkit";
console.log(current, bump(), bump(), kit.current);
const t = new Tally(1);
console.log(t.add(2) === t, t.add(3).total, t instanceof Tally);
@@ -0,0 +1,7 @@
export declare const current: number;
export declare function bump(): number;
export declare class Tally {
constructor(start: number);
total: number;
add(n: number): Tally;
}
+29
View File
@@ -0,0 +1,29 @@
"use strict";
// The esbuild CommonJS bundle prologue: getters over live `let` bindings,
// plus a constructor function with prototype methods (R2.1). scriptc
// compiles it natively only with runtime fences, so it runs in a Node
// island.
var __defProp = Object.defineProperty;
var __export = (target, all) => {
for (var name in all) __defProp(target, name, { get: all[name], enumerable: true });
};
var src_exports = {};
__export(src_exports, {
Tally: () => Tally,
current: () => current,
bump: () => bump,
});
module.exports = src_exports;
let current = 0;
function bump() {
current += 1;
return current;
}
function Tally(start) {
this.total = start;
}
Tally.prototype.add = function (n) {
this.total += n;
return this;
};
0 && (module.exports = { Tally, bump, current });
@@ -0,0 +1 @@
{ "name": "fencedkit", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1,16 @@
// @requires-libnode
// A package whose top-level code always hits a runtime fence (pump's
// `var` function exported by reference), reached through a natively
// compiled package, runs in a Node island automatically; a `string |
// Buffer` member read from an island object decodes with toString("utf8");
// a CommonJS helper required on both sides is duplicated quietly (its
// exports are declarations).
import { pack } from "tarlike";
import { fromText, fromBytes, onceTotal } from "bloblike";
console.log("pack", pack([1, 2, 3]));
for (const blob of [fromText("plain"), fromBytes("bytes")]) {
const data = blob.data;
console.log("blob", blob.mode.toString(8), data.toString("utf8"), typeof data === "string");
}
console.log("once", onceTotal([4, 5]));
@@ -0,0 +1,8 @@
export declare class FileBlob {
constructor(data: string | Buffer);
data: string | Buffer;
mode: number;
}
export declare function fromText(text: string): FileBlob;
export declare function fromBytes(text: string): FileBlob;
export declare function onceTotal(values: number[]): number;
@@ -0,0 +1,19 @@
"use strict";
// Loads its parts with a computed specifier (R1.2): a Node island. It
// also requires the CommonJS helper the native side uses.
const { createRequire } = require("node:module");
const once = createRequire(__filename)(["once", "like"].join(""));
class FileBlob {
constructor(data) {
this.data = data;
this.mode = 0o644;
}
}
exports.FileBlob = FileBlob;
exports.fromText = (text) => new FileBlob(text);
exports.fromBytes = (text) => new FileBlob(Buffer.from(text));
exports.onceTotal = (values) => {
const sum = once(() => values.reduce((a, b) => a + b, 0));
return sum() + sum();
};
@@ -0,0 +1 @@
{ "name": "bloblike", "version": "1.0.0", "main": "index.js", "types": "index.d.ts", "dependencies": { "oncelike": "1.4.0" } }
+17
View File
@@ -0,0 +1,17 @@
// A CommonJS helper both native code and an island require: its exports
// are declarations, so each side's copy is unobservable.
module.exports = once;
once.strict = onceStrict;
function once(fn) {
var f = function () {
if (f.called) return f.value;
f.called = true;
return (f.value = fn.apply(this, arguments));
};
f.called = false;
return f;
}
function onceStrict(fn) {
var f = once(fn);
return f;
}
@@ -0,0 +1 @@
{ "name": "oncelike", "version": "1.4.0", "main": "once.js" }
@@ -0,0 +1,17 @@
// pump's shape: a function stored in a module-level `var` and exported by
// reference after it is defined.
var once = require("oncelike");
var noop = function () {};
var pumpish = function () {
var steps = Array.prototype.slice.call(arguments);
var done = once(typeof steps[steps.length - 1] === "function" ? steps.pop() : noop);
var total = steps.reduce(function (acc, n) {
return acc + n;
}, 0);
done(total);
return total;
};
module.exports = pumpish;
@@ -0,0 +1 @@
{ "name": "pumplike", "version": "3.0.3", "main": "index.js", "dependencies": { "oncelike": "1.4.0" } }
@@ -0,0 +1 @@
export declare function pack(sizes: number[]): string;
+13
View File
@@ -0,0 +1,13 @@
"use strict";
// A typed package scriptc compiles natively that requires pump's shape.
const pump = require("pumplike");
const once = require("oncelike");
exports.pack = function pack(sizes) {
let reported = 0;
const report = once((n) => {
reported = n;
});
const total = pump(...sizes, report);
return `${total}/${reported}`;
};
@@ -0,0 +1 @@
{ "name": "tarlike", "version": "2.1.5", "main": "index.js", "types": "index.d.ts", "dependencies": { "pumplike": "3.0.3", "oncelike": "1.4.0" } }
@@ -0,0 +1,85 @@
// @requires-libnode
// Arguments crossing into a Node island keep JavaScript's exact shape:
// explicit `undefined` arguments count (arguments.length, default
// parameters), an omitted optional field is absent while one holding
// `undefined` is present (`in`, hasOwnProperty, Object.keys), and native
// number arrays arrive as arrays.
import {
probe,
defaults,
version,
inspect,
nodeVersionFor,
cwd,
envValue,
plain,
type Config,
} from "argprobe";
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
interface Settings {
nodeVersion?: string;
}
function settingsFor(flag: boolean): Settings | undefined {
return flag ? { nodeVersion: "20.x" } : undefined;
}
console.log(probe());
console.log(probe(undefined));
console.log(probe("a", undefined));
console.log(probe("a", {}, undefined));
console.log(probe("a", { nodeVersion: undefined }, undefined, [18, 20, 22]));
const settings = settingsFor(false);
console.log(probe("a", { nodeVersion: settings?.nodeVersion }, undefined, [22]));
console.log(probe("a", { label: "x", nodeVersion: undefined }));
console.log(defaults());
console.log(defaults(undefined));
console.log(defaults(undefined, "y"));
console.log(defaults("x", undefined, "r"));
const available: number[] = [18, 20, 22];
console.log(version("/work", "unused", { nodeVersion: settings?.nodeVersion }, undefined, available));
console.log(version("/work", "unused", { nodeVersion: settingsFor(true)?.nodeVersion }, undefined, available));
console.log(version("/work", undefined, undefined, undefined, available));
console.log(inspect({ nodeVersion: undefined, list: [1, undefined, 3] }));
// A native object typed by the island's own interface, read with `?.`.
function projectSettings(flag: boolean): Config | undefined {
return flag ? { nodeVersion: "20.x", label: "proj" } : undefined;
}
const project = projectSettings(true);
const none = projectSettings(false);
console.log(version("/work", "unused", { nodeVersion: project?.nodeVersion }, undefined, available));
console.log(version("/work", "unused", { nodeVersion: none?.nodeVersion }, undefined, available));
console.log(probe("p", { nodeVersion: project?.nodeVersion }), probe("n", { nodeVersion: none?.nodeVersion }));
const claims = { role: "admin" };
console.log("plain", plain({ sub: "alice" }), plain(claims));
async function main(): Promise<void> {
// A package.json the program writes just before the island reads it.
const dir = mkdtempSync(join(tmpdir(), "argprobe-"));
try {
writeFileSync(join(dir, "package.json"), JSON.stringify({ engines: { node: "22.x" } }));
console.log(await nodeVersionFor(dir, "unused", { nodeVersion: settings?.nodeVersion }, undefined, available));
rmSync(join(dir, "package.json"));
console.log(await nodeVersionFor(dir, "unused", { nodeVersion: "18.x" }, { isDev: true }));
try {
await nodeVersionFor(dir, "unused", { nodeVersion: settings?.nodeVersion }, undefined, available);
} catch (e) {
console.log("rejected", (e as Error).message);
}
// Process state the program changes after Node loaded is Node's too.
const before = process.cwd();
process.chdir(dir);
console.log("cwd", cwd() === process.cwd(), cwd() === before);
process.chdir(before);
process.env["ARGPROBE_SETTING"] = "set-natively";
console.log("env", envValue("ARGPROBE_SETTING"));
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
void main();
@@ -0,0 +1,24 @@
export interface Config {
nodeVersion?: string;
label?: string;
}
export declare function probe(a?: unknown, b?: Config, c?: unknown, d?: number[]): string;
export declare function defaults(x?: string, y?: string, ...rest: string[]): string;
export declare function version(
path: string,
fallback?: string,
config?: Config,
meta?: { nodeVersion?: string },
available?: number[],
): string;
export declare function inspect(v: unknown): string;
export declare function nodeVersionFor(
dir: string,
fallback?: string,
config?: Config,
meta?: { isDev?: boolean },
available?: number[],
): Promise<string>;
export declare function cwd(): string;
export declare function envValue(name: string): string | undefined;
export declare function plain(v: object): string;
@@ -0,0 +1,78 @@
"use strict";
// Loads its parts with a computed specifier (R1.2): a Node island. Its
// functions report exactly what arrives: argument counts, explicit
// undefined, key presence of undefined-valued fields, arrays.
const { createRequire } = require("node:module");
const util = createRequire(__filename)(["node", "util"].join(":"));
function describeValue(v) {
if (v === undefined) return "undefined";
if (Array.isArray(v)) return `array(${v.length}:${v.map(describeValue).join(",")})`;
if (v !== null && typeof v === "object")
return `{${Object.keys(v)
.map((k) => `${k}=${describeValue(v[k])}`)
.join(",")}}`;
return JSON.stringify(v);
}
exports.probe = function probe(a, b, c, d) {
return [
`len=${arguments.length}`,
`a=${describeValue(a)}`,
`b=${describeValue(b)}`,
`in=${b !== undefined && b !== null && "nodeVersion" in b}`,
`own=${b !== undefined && b !== null && Object.prototype.hasOwnProperty.call(b, "nodeVersion")}`,
`c=${describeValue(c)}`,
`d=${describeValue(d)}`,
].join(" ");
};
exports.defaults = function defaults(x = "dx", y = "dy", ...rest) {
return `${x}|${y}|rest=${rest.length}|len=${arguments.length}`;
};
exports.version = function version(path, fallback = "unused", config = {}, meta, available) {
const wanted = config.nodeVersion ?? (meta && meta.nodeVersion) ?? null;
const list = Array.isArray(available) ? available : [];
if (wanted === null) {
const latest = Math.max(...list);
return `${path}: ${list.includes(latest) ? `${latest}.x` : fallback}`;
}
return `${path}: ${wanted}`;
};
exports.inspect = (v) => util.inspect(v);
// A version-lookup helper's shape: async, defaults for omitted
// arguments, engines.node from the package.json in `dir` first.
exports.nodeVersionFor = async function nodeVersionFor(
dir,
fallback = "unused",
config = {},
meta = {},
available = [16],
) {
const fs = require("node:fs/promises");
const path = require("node:path");
let engines = null;
try {
engines = JSON.parse(await fs.readFile(path.join(dir, "package.json"), "utf8")).engines ?? null;
} catch {
engines = null;
}
if (engines && engines.node) return `engines:${engines.node}:${Math.max(...available)}`;
const configured = config.nodeVersion || fallback;
if (configured === "unused") throw new Error(`Found invalid or discontinued Node.js Version: "${configured}"`);
return `configured:${configured}:${meta.isDev === true}`;
};
exports.cwd = () => process.cwd();
exports.envValue = (name) => process.env[name];
// A native plain object is a plain object here (jose's isObject).
exports.plain = (v) =>
[
Object.getPrototypeOf(v) === Object.prototype,
Object.prototype.toString.call(v),
Object.getPrototypeOf(Object.getPrototypeOf(v)) === null,
].join(" ");
@@ -0,0 +1 @@
{ "name": "argprobe", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -0,0 +1,39 @@
// @requires-libnode
// Typed code names an island package's declared interfaces and generic
// aliases: the package's declarations type it, while its values cross the
// boundary as handles.
import { Arr, Num, Str, check, type Static, type TArray, type TNumber, type TSchema, type TString } from "schemakit";
interface Tool<T extends TSchema = TSchema> {
name: string;
parameters: T;
execute(id: string, params: Static<T>): string;
}
const sum: Tool<TArray<TNumber>> = {
name: "sum",
parameters: Arr(Num()),
execute: (id, params) => `${id}: ${params.reduce((a, b) => a + b, 0)}`,
};
const shout: Tool<TString> = {
name: "shout",
parameters: Str(),
execute: (id, params) => `${id}: ${params.toUpperCase()}`,
};
function run<T extends TSchema>(tool: Tool<T>, input: unknown): string {
if (!check(tool.parameters, input)) return `${tool.name}: rejected ${JSON.stringify(input)}`;
return tool.execute(tool.name, input);
}
function describeAll(schemas: TSchema[]): string[] {
return schemas.map((s) => `${s.kind} ${s.describe()}`);
}
console.log(run(sum, [1, 2, 3]));
console.log(run(sum, [1, "2"]));
console.log(run(shout, "quiet"));
console.log(run(shout, 7));
console.log(describeAll([sum.parameters, shout.parameters, Arr(Arr(Str()))]).join(" | "));
const tools: Tool[] = [sum, shout];
console.log(tools.map((t) => t.name).join(","));
@@ -0,0 +1,27 @@
// Types the package only declares: its JavaScript has no interfaces and no
// conditional aliases, so code typed from the JavaScript sees `any` here.
export interface TSchema {
readonly kind: string;
describe(): string;
}
export interface TNumber extends TSchema {
readonly kind: "number";
}
export interface TString extends TSchema {
readonly kind: "string";
}
export interface TArray<T extends TSchema = TSchema> extends TSchema {
readonly kind: "array";
readonly items: T;
}
export type Static<T extends TSchema> = T extends TNumber
? number
: T extends TString
? string
: T extends TArray<infer I>
? Static<I>[]
: unknown;
export declare function Num(): TNumber;
export declare function Str(): TString;
export declare function Arr<T extends TSchema>(items: T): TArray<T>;
export declare function check<T extends TSchema>(schema: T, value: unknown): value is Static<T>;
@@ -0,0 +1,37 @@
"use strict";
// The esbuild CommonJS bundle prologue: getters over live `let` bindings
// and a constructor function with prototype methods. scriptc compiles it
// natively only with runtime fences, so it runs in a Node island.
var __defProp = Object.defineProperty;
var __export = (target, all) => {
for (var name in all) __defProp(target, name, { get: all[name], enumerable: true });
};
var src_exports = {};
__export(src_exports, {
Num: () => Num,
Str: () => Str,
Arr: () => Arr,
check: () => check,
});
module.exports = src_exports;
function Schema(kind, items) {
this.kind = kind;
if (items !== undefined) this.items = items;
}
Schema.prototype.describe = function () {
return this.items === undefined ? this.kind : this.kind + "<" + this.items.describe() + ">";
};
function Num() {
return new Schema("number");
}
function Str() {
return new Schema("string");
}
function Arr(items) {
return new Schema("array", items);
}
function check(schema, value) {
if (schema.kind === "array") return Array.isArray(value) && value.every((v) => check(schema.items, v));
return typeof value === schema.kind;
}
0 && (module.exports = { Num, Str, Arr, check });
@@ -0,0 +1 @@
{ "name": "schemakit", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
+27
View File
@@ -0,0 +1,27 @@
// A failed build whose package runs in a Node island reports the island
// build's errors: there the package's declarations type the program. The
// test builds it beside corpus 6829's schemakit package.
import { Num, check, type Static, type TNumber, type TSchema } from "schemakit";
interface Tool<T extends TSchema = TSchema> {
name: string;
parameters: T;
execute(id: string, params: Static<T>): string;
}
const double: Tool<TNumber> = {
name: "double",
parameters: Num(),
execute: (id, params) => `${id}: ${params * 2}`,
};
const input: unknown = 4;
if (check(double.parameters, input)) console.log(double.execute(double.name, input));
// The build's own error: member decorators have no native lowering.
function logged(method: () => number, context: ClassMethodDecoratorContext): void {}
class Counter {
@logged
next(): number {
return 1;
}
}
console.log(new Counter().next());
@@ -0,0 +1 @@
{ "plain": 3400, "san": 62000 }
+26 -1
View File
@@ -308,7 +308,7 @@ describe.skipIf(skip)("Node islands in-process", () => {
});
expect(result.code).toBe(1);
expect(result.stderr).toBe(
`scriptc: the libnode pack at ${dir} has an incompatible bridge (ABI 999; this program needs 8)\n`,
`scriptc: the libnode pack at ${dir} has an incompatible bridge (ABI 999; this program needs 9)\n`,
);
});
@@ -355,6 +355,31 @@ test("recursive JSON types cross into Node", async () => {
expect(ir).toContain("nodeIsland.callSync");
}, 300_000);
// When a build fails and the packages it compiled with runtime fences then
// fail as islands too, the island build's errors are reported. That build
// types the user code from the packages' declarations; the native one
// typed it from their JavaScript, where declared-only types (`Static<T>`)
// are `any`. Needs no libnode pack: both builds fail before linking.
test("a failed build reports the island build's errors", async () => {
const dir = join(outDir, "declared-types");
rmSync(dir, { recursive: true, force: true });
cpSync(
join(repoRoot, "tests/corpus/6829-node-island-declared-types/node_modules"),
join(dir, "node_modules"),
{ recursive: true },
);
copyFileSync(
join(repoRoot, "tests/fixtures/node-island/declared-types.ts"),
join(dir, "main.ts"),
);
const result = await compile(join(dir, "main.ts"), { outPath: join(dir, "main"), outDir: dir });
expect(result.ok).toBe(false);
if (result.ok) return;
const messages = result.diagnostics.map((d) => `${d.code}: ${d.message}`);
expect(messages.some((m) => m.startsWith("SC1090: method decorators"))).toBe(true);
expect(messages.filter((m) => /Tool|Static|'check'/.test(m))).toEqual([]);
}, 300_000);
// Needs no libnode pack: the build fails before linking.
test("re-exporting a path-mapped island package reports the island boundary", async () => {
const dir = join(outDir, "path-mapped-reexport");
+47 -4
View File
@@ -47,7 +47,7 @@ type Known =
const KNOWN: Record<string, Known> = {
"assert-shims": { run: "SC1043" },
"buffer-shims": { run: "SC2020" },
"bytes-exit": { build: "SC2004" },
"bytes-exit": { island: true },
"cjs-esm-named": { island: true },
"cjs-lexer-aligned": { island: true },
"commander-calc": { run: "SC1090", argvs: [["--help"]] },
@@ -214,6 +214,43 @@ async function build(entry: string, shapeOverlay: boolean | undefined): Promise<
};
}
/** Node's uncaught-error report ends with `Node.js v<version>`: the island
* prints libnode's release, which may differ from the reference Node's
* patch release. */
function normalizeNodeVersion([a, b]: [string, string]): [string, string] {
const re = /^Node\.js v\d+\.\d+\.\d+$/m;
return [a.replace(re, "Node.js v<version>"), b.replace(re, "Node.js v<version>")];
}
/** The two stderr texts with their stack frame runs cut to the frames
* they share: an uncaught error from a Node island prints Node's report,
* whose frames match Node's from the throw site up to the first native
* frame, which has no Node-identical spelling (DESIGN.md §2.10). Every
* other line, and each run's common leading frames, compare exactly. */
function sharedFrames(native: Buffer, node: Buffer): [string, string] {
const frame = (line: string): boolean => /^ {4}at /.test(line);
const a = native.toString("utf8").split("\n");
const b = node.toString("utf8").split("\n");
const outA: string[] = [];
const outB: string[] = [];
let i = 0;
let j = 0;
while (i < a.length || j < b.length) {
if (i < a.length && j < b.length && frame(a[i]!) && frame(b[j]!)) {
while (i < a.length && j < b.length && frame(a[i]!) && a[i] === b[j]) {
outA.push(a[i++]!);
outB.push(b[j++]!);
}
while (i < a.length && frame(a[i]!)) i++;
while (j < b.length && frame(b[j]!)) j++;
continue;
}
if (i < a.length) outA.push(a[i++]!);
if (j < b.length) outB.push(b[j++]!);
}
return [outA.join("\n"), outB.join("\n")];
}
/** A Node island's modules run from the module tree the program ships
* (node-islands/modules.ts), extracted under the user's cache directory, so
* a path they print names the tree's copy. The tree's root is the fixtures'
@@ -223,14 +260,14 @@ function fixturePaths(out: Buffer): Buffer {
return Buffer.from(out.toString("utf8").replace(tree, join(fixturesRoot, "npm")), "utf8");
}
async function expectMatchesNode(c: NpmCase, binary: string, island = false): Promise<void> {
async function expectMatchesNode(c: NpmCase, binary: string, islandFrames = false): Promise<void> {
const cwd = dirname(c.entry);
for (const argv of c.argvs ?? [[]]) {
const [nodeRes, ran] = await Promise.all([
runBinary(process.execPath, [c.entry, ...argv], cwd),
runBinary(binary, argv, cwd),
]);
const nativeRes = island
const nativeRes = islandFrames
? { ...ran, stdout: fixturePaths(ran.stdout), stderr: fixturePaths(ran.stderr) }
: ran;
const label = argv.join(" ");
@@ -239,7 +276,13 @@ async function expectMatchesNode(c: NpmCase, binary: string, island = false): Pr
expect.unreachable("stdout differed at byte level but not after utf8 decode");
}
expect(nativeRes.exitCode, label).toBe(nodeRes.exitCode);
if (c.compareStderr) expect(programStderr(nativeRes.stderr), label).toEqual(nodeRes.stderr);
if (c.compareStderr && islandFrames) {
const [native, node] = normalizeNodeVersion(
sharedFrames(programStderr(nativeRes.stderr), nodeRes.stderr),
);
expect(native, label).toBe(node);
} else if (c.compareStderr)
expect(programStderr(nativeRes.stderr), label).toEqual(nodeRes.stderr);
}
}