mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Route more packages and values through Node islands (NI-4c–4g) (#1904)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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/";
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6818-node-island-mixed/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6819-node-island-fenced/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6825-node-island-init-fence/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6826-node-island-undefined-args/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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 },
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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" }));
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
"use strict";
|
||||
class Counter {
|
||||
constructor(start) {
|
||||
this.value = start;
|
||||
}
|
||||
inc() {
|
||||
return ++this.value;
|
||||
}
|
||||
}
|
||||
exports.Counter = Counter;
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "islandlib", "version": "2.0.0", "main": "index.js", "types": "index.d.ts" }
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export declare function twice(start: number): number;
|
||||
export declare function swapped(): number;
|
||||
export declare function localName(): string;
|
||||
+15
@@ -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
@@ -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;
|
||||
+1
@@ -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);
|
||||
+7
@@ -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
@@ -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 });
|
||||
+1
@@ -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]));
|
||||
+8
@@ -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;
|
||||
+19
@@ -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();
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "bloblike", "version": "1.0.0", "main": "index.js", "types": "index.d.ts", "dependencies": { "oncelike": "1.4.0" } }
|
||||
+17
@@ -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;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "oncelike", "version": "1.4.0", "main": "once.js" }
|
||||
+17
@@ -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;
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "pumplike", "version": "3.0.3", "main": "index.js", "dependencies": { "oncelike": "1.4.0" } }
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function pack(sizes: number[]): string;
|
||||
+13
@@ -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}`;
|
||||
};
|
||||
+1
@@ -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();
|
||||
+24
@@ -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;
|
||||
+78
@@ -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(" ");
|
||||
+1
@@ -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(","));
|
||||
+27
@@ -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>;
|
||||
+37
@@ -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 });
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "schemakit", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
|
||||
+27
@@ -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 }
|
||||
@@ -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,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user