mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Run npm packages in Node islands: object handles and compiler routing (NI-4) (#1899)
This commit is contained in:
@@ -1045,6 +1045,17 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"json.stringifyValue": "scr_json_stringify_value",
|
||||
"nodeIsland.callSync": "scr_node_island_call_sync",
|
||||
"nodeIsland.call": "scr_node_island_call",
|
||||
"nodeIsland.require": "scr_ni_require",
|
||||
"nodeIsland.import": "scr_ni_import",
|
||||
"nodeIsland.get": "scr_node_island_get",
|
||||
"nodeIsland.getKey": "scr_ni_get",
|
||||
"nodeIsland.set": "scr_node_island_set",
|
||||
"nodeIsland.setKey": "scr_node_island_set_key",
|
||||
"nodeIsland.invoke": "scr_node_island_invoke",
|
||||
"nodeIsland.apply": "scr_node_island_apply",
|
||||
"nodeIsland.applyTo": "scr_node_island_apply_to",
|
||||
"nodeIsland.construct": "scr_node_island_construct",
|
||||
"nodeIsland.instanceof": "scr_node_island_instanceof",
|
||||
"dyn.keySet": "scr_dyn_key_set",
|
||||
"dyn.keySetComputed": "scr_dyn_key_set_computed",
|
||||
"dyn.keyDelete": "scr_dyn_key_delete",
|
||||
@@ -2022,4 +2033,15 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
// drives (scr_node_island.c) before the program exits.
|
||||
"nodeIsland.call",
|
||||
"nodeIsland.callSync",
|
||||
"nodeIsland.require",
|
||||
"nodeIsland.import",
|
||||
"nodeIsland.get",
|
||||
"nodeIsland.getKey",
|
||||
"nodeIsland.set",
|
||||
"nodeIsland.setKey",
|
||||
"nodeIsland.invoke",
|
||||
"nodeIsland.apply",
|
||||
"nodeIsland.applyTo",
|
||||
"nodeIsland.construct",
|
||||
"nodeIsland.instanceof",
|
||||
]);
|
||||
|
||||
@@ -209,7 +209,9 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
u.opts.workers ||
|
||||
u.opts.dc ||
|
||||
u.opts.fileHandle ||
|
||||
u.nativeFetch,
|
||||
u.nativeFetch ||
|
||||
// Island promises cross as dyn promise boxes (scr_node_island_handle.c).
|
||||
u.opts.nodeIsland === true,
|
||||
"scr_async_dyn.c",
|
||||
),
|
||||
// The performance timeline (every perf_hooks user-timing call selects
|
||||
@@ -369,7 +371,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unitArgs((u) => (u.tlsCa && targetPlatform(u.driver) === "win32" ? ["-lcrypt32"] : [])),
|
||||
// Fetch uses the native network, TLS, and HTTP runtime.
|
||||
unit((u) => u.nativeFetch, "scr_fetch.c"),
|
||||
unit((u) => u.opts.nodeIsland === true, "scr_node_island.c"),
|
||||
unit((u) => u.opts.nodeIsland === true, "scr_node_island.c", "scr_node_island_handle.c"),
|
||||
// Readable.fromWeb reads a web ReadableStream through its reader.
|
||||
unit((u) => u.nativeFetch && u.opts.stream, "scr_stream_from_web.c"),
|
||||
];
|
||||
|
||||
@@ -665,6 +665,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
? {}
|
||||
: { profileUse: resolve(values["profile-use"]) }),
|
||||
...(npmStatic !== undefined ? { npmStatic } : {}),
|
||||
...(nodeIslands !== undefined ? { nodeIslands } : {}),
|
||||
...(values["shape-overlay"] === false ? { shapeOverlay: false } : {}),
|
||||
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
|
||||
...(printNativeLinkInfo ? { nativeLinkInfo: true } : {}),
|
||||
|
||||
@@ -11,6 +11,9 @@ import type { NodeIslandOptions } from "./node-islands/policy.js";
|
||||
export type CompileOutputKind = "ir" | "llvm" | "asm" | "obj" | "exe";
|
||||
|
||||
export interface CompileBaseOptions {
|
||||
/** The Node island policy's command-line half (`--node-islands`,
|
||||
* `--node-island`, `--native`): which npm packages run in Node islands. */
|
||||
nodeIslands?: NodeIslandOptions;
|
||||
/** Primary artifact path. The CLI supplies the output-kind default. */
|
||||
outPath: string;
|
||||
/** Where generated intermediates and compatibility side artifacts land. */
|
||||
|
||||
@@ -107,11 +107,13 @@ export const DIAGNOSTIC_CODES = {
|
||||
SC7006: "Acyclic contract in an unchecked position",
|
||||
SC7007: "Acyclic type can lie on a reference cycle",
|
||||
SC7008: "ProgramLifetime class extends another class",
|
||||
SC8001: "package or module runs in a Node island (note)",
|
||||
SC8002: "Node islands off for a package or module outside the native subset",
|
||||
SC8003: "package marked native is outside the native subset",
|
||||
SC8004: "unknown scriptc.packages or scriptc.nodeIslands configuration",
|
||||
SC8005: "no usable libnode pack for a program that calls into Node.js",
|
||||
SC8006: "Node island modules the build cannot collect",
|
||||
SC8010: "value crossing a Node island boundary in a form without an exact copy",
|
||||
SC8030: "type patch: no range covers the installed copy",
|
||||
SC8031: "type patch: selector matches nothing",
|
||||
SC8032: "type patch: declaration already has the type",
|
||||
@@ -149,8 +151,6 @@ export const RETIRED_DIAGNOSTIC_CODES = {
|
||||
* that work lands it moves its codes into DIAGNOSTIC_CODES; when it is
|
||||
* abandoned, delete them here. */
|
||||
export const RESERVED_DIAGNOSTIC_CODES = {
|
||||
SC8001: "Node islands (ni/* branches)",
|
||||
SC8010: "Node islands (ni/* branches)",
|
||||
SC8011: "Node islands (ni/* branches)",
|
||||
SC8012: "Node islands (ni/* branches)",
|
||||
SC8020: "Node islands (ni/* branches)",
|
||||
|
||||
@@ -116,6 +116,11 @@ export interface ScrDiagnostic {
|
||||
* alone does not say it (a computed require(), a native addon, `with`).
|
||||
* Report-only: never part of the rendered message. */
|
||||
reason?: string;
|
||||
/** Runtime fences only: the fenced site is in a module's top-level code
|
||||
* (outside every function body), so it runs, and throws, when the module
|
||||
* evaluates. Island selection treats such a fence like one that needs the
|
||||
* runtime (scope-B NI-4). */
|
||||
moduleInit?: true;
|
||||
/** A runtime fence in a module's top-level code, outside every function
|
||||
* and class member body: it throws when the module initializes, which is
|
||||
* program startup for a module the entry imports. */
|
||||
@@ -357,6 +362,34 @@ export function nodeIslandsOffModuleDiag(
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8001 — a package runs in a Node island (a note, once per island in
|
||||
* the build output). */
|
||||
export function nodeIslandNoteDiag(
|
||||
pkg: string,
|
||||
version: string | null,
|
||||
klass: string,
|
||||
reason: string,
|
||||
loc: SrcLoc,
|
||||
): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC8001",
|
||||
message: `package '${version === null ? pkg : `${pkg}@${version}`}' runs in a Node island: ${klass}: ${reason}`,
|
||||
loc,
|
||||
hint: "it runs in Node.js loaded into the program on first use; --node-islands=off makes this an error",
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8010 — a value crosses an island boundary in a way NI-4 cannot keep
|
||||
* exact yet (callbacks, native objects, remote promises). */
|
||||
export function nodeIslandBoundaryDiag(what: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC8010",
|
||||
message: `${what} cannot cross into or out of a Node island yet`,
|
||||
loc,
|
||||
hint: "pass primitives, strings, island objects or fresh object and array literals of those; callbacks, native objects and promises cross from NI-5 on",
|
||||
};
|
||||
}
|
||||
|
||||
/** 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
|
||||
|
||||
@@ -185,6 +185,8 @@ export interface EarlyExecutableCacheOptions {
|
||||
/** Omitted unless SCRIPTC_HEAP_CENSUS is set (backend/llvm/heap-census.ts). */
|
||||
heapCensus?: true;
|
||||
npmStatic: readonly string[] | "auto" | null;
|
||||
/** Omitted for the default Node island policy; else nodeIslandOptionsKey. */
|
||||
nodeIslands?: string;
|
||||
/** Omitted unless shape inference's overlay compiles the packages
|
||||
* (frontend/shape-inference). */
|
||||
shapeOverlay?: true;
|
||||
@@ -341,6 +343,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string {
|
||||
...(options.functionLayout !== undefined ? [`layout:${options.functionLayout}`] : []),
|
||||
...(options.checker !== undefined ? [`checker:${options.checker}`] : []),
|
||||
...(options.youngNursery ? ["young-nursery"] : []),
|
||||
...(options.nodeIslands !== undefined ? [`node-islands:${options.nodeIslands}`] : []),
|
||||
...(options.heapCensus ? ["heap-census"] : []),
|
||||
options.npmStatic === null
|
||||
? "<npm-static-off>"
|
||||
@@ -389,6 +392,7 @@ function routeKey(options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment"
|
||||
if (options.functionLayout !== undefined) hash.update(`layout:${options.functionLayout}\0`);
|
||||
if (options.checker !== undefined) hash.update(`checker:${options.checker}\0`);
|
||||
if (options.youngNursery) hash.update("young-nursery\0");
|
||||
if (options.nodeIslands !== undefined) hash.update(`node-islands:${options.nodeIslands}\0`);
|
||||
if (options.heapCensus) hash.update("heap-census\0");
|
||||
hash
|
||||
.update(
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { FfiProfile } from "../ffi/ffi-manifest.js";
|
||||
import { frontendDisposed } from "../frontend/dispose-hook.js";
|
||||
import {
|
||||
checkerPanicDiag,
|
||||
nodeIslandNoteDiag,
|
||||
npmStaticRefusalDiag,
|
||||
iceDiag,
|
||||
isCheckerPanic,
|
||||
@@ -19,6 +20,16 @@ import {
|
||||
import type { LowerResult, LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { FrontendFactory } from "../frontend/pipeline.js";
|
||||
import type { CompilationTiming } from "../timing.js";
|
||||
import { readProjectConfig } from "../project-config.js";
|
||||
import { nodeIslandPolicy } from "../node-islands/policy.js";
|
||||
import { buildNodeIslandReport } from "../node-islands/report.js";
|
||||
import type { IslandPackage, NodeIslandSet } from "../node-islands/boundary.js";
|
||||
import { classifyDiagnostic, isIslandReasonCode } from "../node-islands/classify.js";
|
||||
import { packageDirOf, packageVersionOf } from "../node-islands/origin.js";
|
||||
import { describe } from "../node-islands/summary.js";
|
||||
import { resolveBareModule } from "../frontend/resolve.js";
|
||||
import { isEsmFile } from "../frontend/lowering/lower-node-island.js";
|
||||
import type { PackageEdge } from "../node-islands/scan.js";
|
||||
|
||||
export interface PreparedExecutableModule {
|
||||
ok: true;
|
||||
@@ -38,11 +49,30 @@ export function prepareExecutableModule(
|
||||
buildPlatform: string,
|
||||
createFrontend: FrontendFactory,
|
||||
timing: CompilationTiming = () => {},
|
||||
lowerIslands: ReadonlyMap<string, IslandPackage> = new Map(),
|
||||
retryFences = true,
|
||||
): PreparedExecutableModule | CompileFailure {
|
||||
const fe = createFrontend(entryPath, opts.npmStatic);
|
||||
// The island decision reads the detector's scan unless islands are off.
|
||||
const islandScan =
|
||||
nodeIslandPolicy(readProjectConfig(entryPath), opts.nodeIslands).mode !== "off";
|
||||
let fe = createFrontend(entryPath, opts.npmStatic, undefined, undefined, { islandScan });
|
||||
timing("frontend-load");
|
||||
// Node islands (scope-B NI-4): packages refused natively run in Node,
|
||||
// unless the policy says otherwise. The frontend reloads without them,
|
||||
// so their declarations type the program.
|
||||
const islandDecision = decideNodeIslands(entryPath, opts, fe, lowerIslands);
|
||||
if (islandDecision !== null) {
|
||||
fe.dispose();
|
||||
fe = createFrontend(entryPath, opts.npmStatic, undefined, undefined, {
|
||||
islandPackages: new Set(islandDecision.packages.keys()),
|
||||
islandScan,
|
||||
});
|
||||
timing("frontend-island-reload");
|
||||
}
|
||||
let lowered: LowerResult;
|
||||
let fenceEdges: readonly PackageEdge[] | undefined;
|
||||
let sourceTexts: Map<string, string>;
|
||||
let disposed = false;
|
||||
// The frontend (and its tsgo server) is released as soon as lowering
|
||||
// ends — clang and the link never hold it open.
|
||||
try {
|
||||
@@ -65,10 +95,32 @@ export function prepareExecutableModule(
|
||||
),
|
||||
);
|
||||
}
|
||||
if (fe.preflight.length > 0) return fail(fe.preflight);
|
||||
if (fe.preflight.length > 0) {
|
||||
// An import of a natively compiled package that preflight refuses
|
||||
// in the importer (a CommonJS namespace import) is that package's
|
||||
// refusal: it runs in a Node island instead.
|
||||
const grown = preflightIslands(entryPath, opts, fe, lowerIslands);
|
||||
if (grown !== null) {
|
||||
fe.dispose();
|
||||
frontendDisposed();
|
||||
disposed = true;
|
||||
return prepareExecutableModule(
|
||||
entryPath,
|
||||
opts,
|
||||
ffi,
|
||||
buildPlatform,
|
||||
createFrontend,
|
||||
timing,
|
||||
grown,
|
||||
retryFences,
|
||||
);
|
||||
}
|
||||
return fail(fe.preflight);
|
||||
}
|
||||
|
||||
try {
|
||||
lowered = fe.lower({
|
||||
...(islandDecision !== null ? { nodeIslands: islandDecision } : {}),
|
||||
targetPlatform: buildPlatform,
|
||||
...(ffi !== null ? { ffiImports: ffi.functions } : {}),
|
||||
...(opts.tier !== undefined ? { tierMode: opts.tier } : {}),
|
||||
@@ -83,7 +135,35 @@ export function prepareExecutableModule(
|
||||
checkerPanicDiag(e.message.split("\n", 1)[0]!, { file: entryPath, start: 0, end: 0 }),
|
||||
]);
|
||||
}
|
||||
if (lowered.module === null) return fail(lowered.diagnostics);
|
||||
if (lowered.module === null) {
|
||||
// Packages whose own modules refused to lower run in Node islands
|
||||
// too (the authoritative lowering, DESIGN.md §1.4): build again with
|
||||
// them as islands.
|
||||
const grown = loweringIslands(
|
||||
entryPath,
|
||||
opts,
|
||||
fe,
|
||||
lowered.diagnostics,
|
||||
lowerIslands,
|
||||
fe.nodeIslandScan?.({ userJs: false }).edges,
|
||||
);
|
||||
if (grown !== null) {
|
||||
fe.dispose();
|
||||
frontendDisposed();
|
||||
disposed = true;
|
||||
return prepareExecutableModule(
|
||||
entryPath,
|
||||
opts,
|
||||
ffi,
|
||||
buildPlatform,
|
||||
createFrontend,
|
||||
timing,
|
||||
grown,
|
||||
retryFences,
|
||||
);
|
||||
}
|
||||
return fail(lowered.diagnostics);
|
||||
}
|
||||
|
||||
const validation = validateModule(lowered.module);
|
||||
timing("ir-validate");
|
||||
@@ -104,6 +184,10 @@ 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);
|
||||
@@ -112,20 +196,277 @@ export function prepareExecutableModule(
|
||||
if (checked.contracts) lowered.module.contracts = checked.contracts;
|
||||
}
|
||||
} finally {
|
||||
fe.dispose();
|
||||
frontendDisposed();
|
||||
timing("frontend-dispose");
|
||||
if (!disposed) {
|
||||
fe.dispose();
|
||||
frontendDisposed();
|
||||
timing("frontend-dispose");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
const prepared: PreparedExecutableModule = {
|
||||
ok: true,
|
||||
mod: lowered.module!,
|
||||
sourceTexts,
|
||||
stats: lowered.stats,
|
||||
warnings: [
|
||||
...(islandDecision?.notes ?? []),
|
||||
...(fe.warnings ?? []),
|
||||
...(lowered.divergences ?? []),
|
||||
...(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) {
|
||||
const islanded = prepareExecutableModule(
|
||||
entryPath,
|
||||
opts,
|
||||
ffi,
|
||||
buildPlatform,
|
||||
createFrontend,
|
||||
timing,
|
||||
grown,
|
||||
false,
|
||||
);
|
||||
if (islanded.ok) return islanded;
|
||||
}
|
||||
}
|
||||
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
|
||||
* marked ones; under `off`, none (refusals stay SC2013). Each island is
|
||||
* reported once as an SC8001 note. */
|
||||
function decideNodeIslands(
|
||||
entryPath: string,
|
||||
opts: CompileRequestOptions,
|
||||
fe: ReturnType<FrontendFactory>,
|
||||
lowerIslands: ReadonlyMap<string, IslandPackage>,
|
||||
): (NodeIslandSet & { notes: ScrDiagnostic[] }) | null {
|
||||
const policy = nodeIslandPolicy(readProjectConfig(entryPath), opts.nodeIslands);
|
||||
if (policy.mode === "off" || fe.nodeIslandScan === undefined) return null;
|
||||
// A package marked native stays refused: the build reports it rather
|
||||
// than running it in Node.
|
||||
const refused = new Set(
|
||||
policy.mode === "auto"
|
||||
? fe.npmStatic
|
||||
.filter((e) => e.status === "refused" && !policy.forcedNative.has(e.package))
|
||||
.map((e) => e.package)
|
||||
: [],
|
||||
);
|
||||
for (const name of lowerIslands.keys()) refused.add(name);
|
||||
if (refused.size === 0 && policy.forcedNode.size === 0) return null;
|
||||
const report = buildNodeIslandReport({
|
||||
entry: entryPath,
|
||||
scan: fe.nodeIslandScan({ userJs: false }),
|
||||
npm: fe.npmStatic,
|
||||
diagnostics: [],
|
||||
fences: [],
|
||||
policy,
|
||||
});
|
||||
const packages = new Map<string, IslandPackage>();
|
||||
const notes: ScrDiagnostic[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const island of report.islands) {
|
||||
if (island.path === null) continue;
|
||||
seen.add(island.package);
|
||||
if (!refused.has(island.package) && !policy.forcedNode.has(island.package)) continue;
|
||||
packages.set(island.package, {
|
||||
name: island.package,
|
||||
version: island.version,
|
||||
dir: island.path,
|
||||
class: island.class,
|
||||
reason: island.reason,
|
||||
});
|
||||
notes.push(
|
||||
nodeIslandNoteDiag(
|
||||
island.package,
|
||||
island.version,
|
||||
island.class,
|
||||
island.reason,
|
||||
island.importedFrom[0] ??
|
||||
fe.npmImportSites.get(island.package) ?? { file: entryPath, start: 0, end: 0 },
|
||||
),
|
||||
);
|
||||
}
|
||||
// Refused packages the report has no installed directory for (refused
|
||||
// before they joined the program): their directory from the runtime
|
||||
// entry Node resolves.
|
||||
for (const name of refused) {
|
||||
if (packages.has(name) || lowerIslands.has(name)) continue;
|
||||
const site = fe.npmImportSites.get(name) ?? { file: entryPath, start: 0, end: 0 };
|
||||
const js =
|
||||
resolveBareModule(site.file, name, "js-only") ??
|
||||
resolveBareModule(site.file, name, "js-only", "require");
|
||||
const at = js === null ? null : packageDirOf(js.typesFile);
|
||||
if (at === null) continue;
|
||||
const status = fe.npmStatic.find((e) => e.package === name);
|
||||
packages.set(name, {
|
||||
name,
|
||||
version: packageVersionOf(at.dir),
|
||||
dir: at.dir,
|
||||
class: "packaging",
|
||||
reason: status?.detail ?? "its static compilation was refused",
|
||||
});
|
||||
notes.push(
|
||||
nodeIslandNoteDiag(name, packageVersionOf(at.dir), "packaging", status?.detail ?? "", site),
|
||||
);
|
||||
}
|
||||
// Packages islanded by their lowering refusals keep their first reason
|
||||
// (the reload no longer compiles them, so the scan has no reason).
|
||||
for (const [name, island] of lowerIslands) {
|
||||
if (seen.has(name) && packages.has(name)) {
|
||||
const kept = packages.get(name)!;
|
||||
if (kept.reason === "" || kept.class === "") packages.set(name, island);
|
||||
continue;
|
||||
}
|
||||
packages.set(name, island);
|
||||
notes.push(
|
||||
nodeIslandNoteDiag(
|
||||
island.name,
|
||||
island.version,
|
||||
island.class,
|
||||
island.reason,
|
||||
fe.npmImportSites.get(name) ?? { file: entryPath, start: 0, end: 0 },
|
||||
),
|
||||
);
|
||||
}
|
||||
return packages.size === 0 ? null : { packages, notes };
|
||||
}
|
||||
|
||||
/** The island set grown by the npm packages whose own modules refused to
|
||||
* lower (`--node-islands=auto` only), or null when there are none. */
|
||||
function loweringIslands(
|
||||
entryPath: string,
|
||||
opts: CompileRequestOptions,
|
||||
fe: ReturnType<FrontendFactory>,
|
||||
diagnostics: readonly ScrDiagnostic[],
|
||||
known: ReadonlyMap<string, IslandPackage>,
|
||||
edges: readonly PackageEdge[] | undefined,
|
||||
): Map<string, IslandPackage> | null {
|
||||
const policy = nodeIslandPolicy(readProjectConfig(entryPath), opts.nodeIslands);
|
||||
if (policy.mode !== "auto" || known.size > 16) return null;
|
||||
const compiled = new Set(fe.npmStatic.filter((e) => e.status === "static").map((e) => e.package));
|
||||
const grown = new Map(known);
|
||||
for (const d of diagnostics) {
|
||||
// A use of a package outside the compiled module graph (an import()
|
||||
// the frontend did not compile): that package runs in an island.
|
||||
const named = d.code === "SC2013" ? /npm package '([^']+)'/.exec(d.message) : null;
|
||||
if (named !== null) {
|
||||
const name = named[1]!;
|
||||
if (grown.has(name) || policy.forcedNative.has(name) || policy.unloaded.has(name)) continue;
|
||||
const js =
|
||||
resolveBareModule(d.loc.file, name, "js-only") ??
|
||||
resolveBareModule(d.loc.file, name, "js-only", "require");
|
||||
const at = js === null ? null : packageDirOf(js.typesFile);
|
||||
if (at === null || at.name !== name) continue;
|
||||
grown.set(name, {
|
||||
name,
|
||||
version: packageVersionOf(at.dir),
|
||||
dir: at.dir,
|
||||
class: "packaging",
|
||||
reason: `${d.code}: ${d.message}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!isIslandReasonCode(d.code)) continue;
|
||||
const at = packageDirOf(d.loc.file);
|
||||
if (at === null || !compiled.has(at.name) || grown.has(at.name)) continue;
|
||||
if (policy.forcedNative.has(at.name) || policy.unloaded.has(at.name)) continue;
|
||||
const reason = classifyDiagnostic(d, "refusal");
|
||||
grown.set(at.name, {
|
||||
name: at.name,
|
||||
version: packageVersionOf(at.dir),
|
||||
dir: at.dir,
|
||||
class: reason.class,
|
||||
reason: describe(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 = edges !== undefined; added;) {
|
||||
added = false;
|
||||
for (const edge of 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}'`,
|
||||
});
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
return grown.size === known.size ? null : grown;
|
||||
}
|
||||
|
||||
/** Preflight refusals at import statements of natively compiled npm
|
||||
* packages (`--node-islands=auto` only): those packages become islands. */
|
||||
function preflightIslands(
|
||||
entryPath: string,
|
||||
opts: CompileRequestOptions,
|
||||
fe: ReturnType<FrontendFactory>,
|
||||
known: ReadonlyMap<string, IslandPackage>,
|
||||
): Map<string, IslandPackage> | null {
|
||||
const policy = nodeIslandPolicy(readProjectConfig(entryPath), opts.nodeIslands);
|
||||
if (policy.mode !== "auto" || known.size > 16) return null;
|
||||
const compiled = new Set(fe.npmStatic.filter((e) => e.status === "static").map((e) => e.package));
|
||||
if (compiled.size === 0) return null;
|
||||
const texts = fe.sourceTexts();
|
||||
const grown = new Map(known);
|
||||
for (const d of fe.preflight) {
|
||||
if (d.code !== "SC1013") continue;
|
||||
const text = texts.get(d.loc.file);
|
||||
if (text === undefined) continue;
|
||||
// The import statement around the diagnostic: its module specifier.
|
||||
const start = text.lastIndexOf("import", d.loc.start);
|
||||
const end = text.indexOf(";", d.loc.end);
|
||||
const stmt = text.slice(start < 0 ? d.loc.start : start, end < 0 ? text.length : end);
|
||||
const m = /\bfrom\s*["']([^"']+)["']/.exec(stmt);
|
||||
if (m === null) continue;
|
||||
const spec = m[1]!;
|
||||
const name = spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0]!;
|
||||
if (!compiled.has(name) || grown.has(name)) continue;
|
||||
if (policy.forcedNative.has(name) || policy.unloaded.has(name)) continue;
|
||||
const site = fe.npmImportSites.get(name);
|
||||
const jsEntry = site === undefined ? null : resolveBareModule(site.file, spec, "js-only");
|
||||
const at = jsEntry === null ? null : packageDirOf(jsEntry.typesFile);
|
||||
if (at === null) continue;
|
||||
grown.set(name, {
|
||||
name,
|
||||
version: packageVersionOf(at.dir),
|
||||
dir: at.dir,
|
||||
class: "gap",
|
||||
reason: `${d.code}: ${d.message}`,
|
||||
});
|
||||
}
|
||||
return grown.size === known.size ? null : grown;
|
||||
}
|
||||
|
||||
@@ -194,9 +194,13 @@ function createRequireFence(
|
||||
surface: string,
|
||||
node: ts.Node,
|
||||
hint: string,
|
||||
reason?: string,
|
||||
): IrStmt & { kind: "runtimeFence" } {
|
||||
const loc = locOf(node);
|
||||
const d = noLoweringDiag(surface, loc, hint);
|
||||
// The Node island reason the refusal would carry (a computed require is
|
||||
// dynamic module loading), so island selection classifies the fence.
|
||||
if (reason !== undefined) d.reason = reason;
|
||||
lowerer.runtimeFences.push(d);
|
||||
const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(node), loc.start).line + 1;
|
||||
return {
|
||||
@@ -299,6 +303,7 @@ function deferredCreateRequireThrow(
|
||||
surface: string,
|
||||
hint: string,
|
||||
loc: SrcLoc,
|
||||
reason?: string,
|
||||
): IrExpr {
|
||||
const stmts: IrStmt[] = call.arguments.nodes.map((arg) => ({
|
||||
kind: "exprStmt",
|
||||
@@ -310,7 +315,7 @@ function deferredCreateRequireThrow(
|
||||
slot.mutable = true;
|
||||
stmts.push(
|
||||
{ kind: "varDecl", localId: slot.id, init: null, loc },
|
||||
createRequireFence(lowerer, surface, call, hint),
|
||||
createRequireFence(lowerer, surface, call, hint, reason),
|
||||
);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
@@ -339,7 +344,7 @@ export function lowerCreateRequireCall(
|
||||
// Spread arguments have no run-time resolution: they run, then the
|
||||
// call throws the fence.
|
||||
if (args.some((a) => ts.isSpreadElement(a)))
|
||||
return deferredCreateRequireThrow(lowerer, call, surface, hint, loc);
|
||||
return deferredCreateRequireThrow(lowerer, call, surface, hint, loc, "dynamic-require");
|
||||
// Node evaluates every argument, then requires the first.
|
||||
const stmts: IrStmt[] = [];
|
||||
let id = args.length > 0 ? lowerer.lowerExprExpecting(args[0]!, DYN) : dynUndefinedExpr(loc);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
lowerNewTargetName,
|
||||
} from "./new-target.js";
|
||||
import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js";
|
||||
import { lowerIslandExpr } from "./lower-node-island.js";
|
||||
import { lowerAwaitedUniformPromiseAll } from "./builtins/promises.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
@@ -289,6 +290,13 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
const chainRecv = lowerer.chainRecvByNode.get(expr);
|
||||
if (chainRecv) return { ...chainRecv, loc };
|
||||
|
||||
// Node islands (scope-B NI-4): member operations on island values and
|
||||
// island import bindings. Programs without islands skip it.
|
||||
if (lowerer.nodeIslands !== null) {
|
||||
const island = lowerIslandExpr(lowerer, expr);
|
||||
if (island !== null) return island;
|
||||
}
|
||||
|
||||
// --external-types supplies CHECKER truth only. A value rooted in one
|
||||
// of those declarations has no runtime implementation in scriptc, so
|
||||
// every use fences at its expression instead of accidentally lowering
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 { NODE_API_MODULE } from "../node-api-import.js";
|
||||
import { positionTag } from "./structural-positions.js";
|
||||
import { EXPERIMENTAL_MODULE } from "../experimental-import.js";
|
||||
@@ -653,7 +654,13 @@ export function collectNpmImports(lowerer: Lowerer, parts: FileParts[]): void {
|
||||
// graph: resolveImport follows its node_modules link to the loaded
|
||||
// TypeScript entry. Its internal `.js` spellings have already been
|
||||
// resolved to `.ts` by that graph.
|
||||
if (!isRelativeSpecifier(spec) && resolveImport(lowerer.program, fp.sf, spec) !== null) {
|
||||
const projectDep = isRelativeSpecifier(spec)
|
||||
? null
|
||||
: resolveImport(lowerer.program, fp.sf, spec);
|
||||
if (projectDep !== null) {
|
||||
// A Node island package that tsconfig `paths` maps to its installed
|
||||
// directory resolves to its declarations: it loads in Node.
|
||||
if (projectDep.isDeclarationFile) collectIslandImport(lowerer, fp.sf, stmt, spec);
|
||||
continue;
|
||||
}
|
||||
const npm = resolveNpmImport(fp.sf.fileName, spec);
|
||||
@@ -705,6 +712,9 @@ export function collectNpmImports(lowerer: Lowerer, parts: FileParts[]): void {
|
||||
// it, %main throws before any init runs, and the bindings never
|
||||
|
||||
if (npm !== null && probeNodeImportRefusal(fp.sf.fileName, spec) !== null) continue;
|
||||
// A Node island package (scope-B NI-4): loaded in Node at this
|
||||
// module's position, its bindings are island values.
|
||||
if (collectIslandImport(lowerer, fp.sf, stmt, spec)) continue;
|
||||
{
|
||||
lowerer.pushDiag(unsupportedPackageImportDiag(npm?.packageName ?? relPkg!, locOf(stmt)));
|
||||
continue;
|
||||
|
||||
@@ -41,6 +41,7 @@ import * as ts from "../ts7/adapter.js";
|
||||
import { isPackageShimFile, isShimIntrinsicName, packageShimFor } from "../package-shims.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { boundIdentifiersOf } from "./lowerer.js";
|
||||
import { isIslandImportBinding } from "./lower-node-island.js";
|
||||
import type { FileParts } from "./lower-modules.js";
|
||||
import { isCjsJsFile, isJsSourceFile, locOf, resolveImport } from "../program.js";
|
||||
import { F64, type IrExpr, type IrStmt, type IrType, STRING } from "../../ir/ir.js";
|
||||
@@ -686,6 +687,9 @@ export function ambientUndefinedVarSymbolOf(
|
||||
lowerer: Lowerer,
|
||||
ident: ts.Identifier,
|
||||
): ts.Symbol | null {
|
||||
// An island package's `declare const` is defined by its JavaScript, which
|
||||
// runs in the island.
|
||||
if (isIslandImportBinding(lowerer, ident)) return null;
|
||||
const sym = lowerer.resolveValueSymbol(ident);
|
||||
const decl = sym ? sym.declarations[0] : undefined;
|
||||
if (
|
||||
|
||||
@@ -16,14 +16,35 @@
|
||||
* checked against the declared result type, like `JSON.parse(s) as T`; a
|
||||
* thrown Error crosses as its nearest intrinsic class with its message and
|
||||
* string `code`. */
|
||||
import { realpathSync } from "node:fs";
|
||||
import { isAbsolute } from "node:path";
|
||||
import { readFileSync, realpathSync } from "node:fs";
|
||||
import { resolveFrom } from "../../node-islands/resolve.js";
|
||||
import { dirname, isAbsolute, join } from "node:path";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { locOf } from "../program.js";
|
||||
import type { NodeApiMember } from "../node-api-import.js";
|
||||
import { PoisonError, type Lowerer } from "./lowerer.js";
|
||||
import { nodeIslandModuleDiag } from "../../diagnostics/diagnostic.js";
|
||||
import { DYN, STRING, type IrExpr, type IrType } from "../../ir/ir.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
FOREIGN,
|
||||
STRING,
|
||||
type IrExpr,
|
||||
type IrGlobal,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { resolveBareModule } from "../resolve.js";
|
||||
import { nodeIslandBoundaryDiag, nodeIslandModuleDiag } from "../../diagnostics/diagnostic.js";
|
||||
import {
|
||||
crossingOf,
|
||||
isForeignType,
|
||||
islandPackageOfFile,
|
||||
type NodeIslandSet,
|
||||
} from "../../node-islands/boundary.js";
|
||||
import { stripTypeWrappers } from "./typed-arguments.js";
|
||||
import { thenReader } from "./checked-promise-all.js";
|
||||
|
||||
const JSON_HINT =
|
||||
"only JSON data (numbers, strings, booleans, null, arrays, plain objects of those, and unknown) crosses into Node.js in this version";
|
||||
@@ -88,6 +109,7 @@ export function lowerNodeApiCall(
|
||||
const loc = locOf(call);
|
||||
const surface = `${member} (@scriptc/node)`;
|
||||
const argNodes = call.arguments.nodes;
|
||||
if (member === "requireModule") return lowerRequireModule(lowerer, call, surface);
|
||||
if (call.questionDotToken || argNodes.length < 2 || argNodes.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(
|
||||
`${surface} without a specifier and an export name, or with spread arguments`,
|
||||
@@ -155,3 +177,766 @@ export function lowerNodeApiCall(
|
||||
// `JSON.parse(s) as T`: types don't lie, a mismatch throws.
|
||||
return lowerer.coerceToExpected(raw, declared);
|
||||
}
|
||||
|
||||
/** requireModule(specifier): Node's require() from the calling module, run
|
||||
* in the island; the exports cross by reference (scope-B NI-4: an island
|
||||
* handle the program uses as an `any` value). The specifier must be a
|
||||
* string literal: it resolves at build time exactly as require.resolve
|
||||
* resolves it from the caller, and the island loads that file, shipped
|
||||
* with the program (islandLoadPath). */
|
||||
function lowerRequireModule(lowerer: Lowerer, call: ts.CallExpression, surface: string): IrExpr {
|
||||
const loc = locOf(call);
|
||||
const argNodes = call.arguments.nodes;
|
||||
const arg = argNodes[0];
|
||||
if (
|
||||
call.questionDotToken ||
|
||||
argNodes.length !== 1 ||
|
||||
arg === undefined ||
|
||||
!ts.isStringLiteral(arg)
|
||||
) {
|
||||
lowerer.noLowering(
|
||||
`${surface} without exactly one string-literal specifier`,
|
||||
call,
|
||||
"pass the module specifier as a string literal",
|
||||
);
|
||||
}
|
||||
const from = callerPath(call);
|
||||
let path: string;
|
||||
try {
|
||||
path = resolveFrom(from, arg.text);
|
||||
} catch {
|
||||
lowerer.noLowering(
|
||||
`${surface} of '${arg.text}', which does not resolve from ${from}`,
|
||||
call,
|
||||
"install the module or fix the specifier",
|
||||
);
|
||||
}
|
||||
lowerer.usesNodeIsland = true;
|
||||
const raw: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "nodeIsland.require",
|
||||
args: [islandLoadPath(lowerer, path, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const declared = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (declared === null || declared === undefined || declared.kind === "dyn") return raw;
|
||||
return lowerer.coerceToExpected(raw, declared);
|
||||
}
|
||||
|
||||
/* ── compiler-managed Node islands (scope-B NI-4) ─────────────────────────
|
||||
*
|
||||
* A package the build runs in a Node island is never compiled. Its static
|
||||
* imports load the island module at the importing module's position
|
||||
* (eager evaluation: `nodeIsland.require` in the module's %init prelude,
|
||||
* before the body), and their bindings read island values: a named import
|
||||
* of an ES module reads the live namespace member at each use; a CommonJS
|
||||
* module's named imports are read once after it evaluates, as Node's
|
||||
* namespace for CommonJS snapshots them; a default import of CommonJS is
|
||||
* module.exports. `require()` of an island package anywhere and `import()`
|
||||
* load the same way at the call.
|
||||
*
|
||||
* Island values are typed by the package's declarations: values of
|
||||
* island-declared object types are island handles (FOREIGN, see
|
||||
* node-islands/boundary.ts), and member reads, writes, calls and `new` on
|
||||
* them become bridge calls whose results meet the declared type through
|
||||
* the checked dyn conversion (types don't lie: a mismatch throws). */
|
||||
|
||||
/** One island module load: the module global that holds what the import
|
||||
* form sees (module.exports for CommonJS, the namespace for ESM). */
|
||||
export interface IslandModuleBinding {
|
||||
global: IrGlobal;
|
||||
/** The JavaScript file Node loads. */
|
||||
path: string;
|
||||
esm: boolean;
|
||||
/** CommonJS named imports, snapshotted after evaluation: name -> global. */
|
||||
snapshots: Map<string, IrGlobal>;
|
||||
}
|
||||
|
||||
function boundaryFail(lowerer: Lowerer, what: string, node: ts.Node): never {
|
||||
lowerer.pushDiag(nodeIslandBoundaryDiag(what, locOf(node)));
|
||||
throw new PoisonError();
|
||||
}
|
||||
|
||||
/** Whether the JavaScript file is an ES module by Node's rules. */
|
||||
export function isEsmFile(file: string): boolean {
|
||||
if (file.endsWith(".mjs")) return true;
|
||||
if (file.endsWith(".cjs")) return false;
|
||||
for (let dir = dirname(file); ;) {
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
|
||||
type?: unknown;
|
||||
};
|
||||
return pkg.type === "module";
|
||||
} catch {
|
||||
/* no package.json here */
|
||||
}
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) return false;
|
||||
dir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
/** The island module file `spec` names from `fromFile`, or null when the
|
||||
* specifier does not resolve into an island package. */
|
||||
export function islandModuleFile(
|
||||
islands: NodeIslandSet,
|
||||
fromFile: string,
|
||||
spec: string,
|
||||
kind: "import" | "require",
|
||||
): string | null {
|
||||
const js = resolveBareModule(fromFile, spec, "js-only", kind);
|
||||
if (js === null) return null;
|
||||
if (islandPackageOfFile(islands, js.typesFile) === null) return null;
|
||||
try {
|
||||
return realpathSync(js.typesFile);
|
||||
} catch {
|
||||
return js.typesFile;
|
||||
}
|
||||
}
|
||||
|
||||
/** The module global of the island module at `path`, created on first use. */
|
||||
function islandModuleGlobal(lowerer: Lowerer, path: string): IslandModuleBinding {
|
||||
let binding = lowerer.islandModules.get(path);
|
||||
if (binding === undefined) {
|
||||
const global: IrGlobal = {
|
||||
id: `%g.island.${lowerer.globalsList.length}`,
|
||||
name: path.split("/").at(-1) ?? "island",
|
||||
type: DYN,
|
||||
mutable: true,
|
||||
};
|
||||
lowerer.globalsList.push(global);
|
||||
binding = { global, path, esm: isEsmFile(path), snapshots: new Map() };
|
||||
lowerer.islandModules.set(path, binding);
|
||||
}
|
||||
return binding;
|
||||
}
|
||||
|
||||
/** The path literal of a load of the file at `path`. The build ships the
|
||||
* file with the program (node-islands/modules.ts): the literal names it by
|
||||
* its path in the shipped tree once lowering ends. A built-in module keeps
|
||||
* its name. */
|
||||
function islandLoadPath(lowerer: Lowerer, path: string, loc: SrcLoc): IrExpr {
|
||||
const literal: IrExpr & { kind: "strLit" } = { kind: "strLit", value: path, type: STRING, loc };
|
||||
if (!isAbsolute(path)) return literal;
|
||||
let real = path;
|
||||
try {
|
||||
real = realpathSync(path);
|
||||
} catch {
|
||||
/* the resolved path */
|
||||
}
|
||||
const problem = lowerer.nodeIslandModules.addLoaded(real);
|
||||
if (problem !== null) {
|
||||
lowerer.pushDiag(nodeIslandModuleDiag(problem, loc));
|
||||
throw new PoisonError();
|
||||
}
|
||||
lowerer.nodeIslandLoads.push({ literal, path: real });
|
||||
return literal;
|
||||
}
|
||||
|
||||
function loadIslandModule(lowerer: Lowerer, path: string, loc: SrcLoc): IrExpr {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "nodeIsland.require",
|
||||
args: [islandLoadPath(lowerer, path, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A static import (or re-export) of an island package: loads the module
|
||||
* in the importer's %init prelude. Returns false when `spec` is not an
|
||||
* island module. */
|
||||
export function collectIslandImport(
|
||||
lowerer: Lowerer,
|
||||
sf: ts.SourceFile,
|
||||
stmt: ts.Statement,
|
||||
spec: string,
|
||||
): boolean {
|
||||
const islands = lowerer.nodeIslands;
|
||||
if (islands === null) return false;
|
||||
const path = islandModuleFile(islands, sf.fileName, spec, "import");
|
||||
if (path === null) return false;
|
||||
if (ts.isExportDeclaration(stmt)) {
|
||||
lowerer.pushDiag(
|
||||
nodeIslandBoundaryDiag(`re-exporting the Node island module '${spec}'`, locOf(stmt)),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
lowerer.usesNodeIsland = true;
|
||||
const loc = locOf(stmt);
|
||||
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,
|
||||
});
|
||||
// CommonJS named imports: Node's namespace snapshots them once the
|
||||
// module evaluated.
|
||||
const clause = ts.isImportDeclaration(stmt) ? stmt.importClause : undefined;
|
||||
const named = clause?.namedBindings;
|
||||
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;
|
||||
if (name === "default") continue;
|
||||
let g = binding.snapshots.get(name);
|
||||
if (g === undefined) {
|
||||
g = {
|
||||
id: `%g.island.${lowerer.globalsList.length}`,
|
||||
name,
|
||||
type: DYN,
|
||||
mutable: true,
|
||||
};
|
||||
lowerer.globalsList.push(g);
|
||||
binding.snapshots.set(name, g);
|
||||
}
|
||||
actions.push({
|
||||
kind: "assign",
|
||||
localId: g.id,
|
||||
value: islandGet(varRef(binding.global.id, DYN, loc), name, loc),
|
||||
loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function islandGet(target: IrExpr, key: string, loc: SrcLoc): IrExpr {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "nodeIsland.get",
|
||||
args: [target, { kind: "strLit", value: key, type: STRING, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
type IslandBinding =
|
||||
| { kind: "namespace"; module: IslandModuleBinding }
|
||||
| { kind: "default"; module: IslandModuleBinding }
|
||||
| { kind: "named"; module: IslandModuleBinding; name: string };
|
||||
|
||||
function moduleSpecifierOf(decl: ts.Node | undefined): string | null {
|
||||
return ts.isImportDeclaration(decl) && ts.isStringLiteral(decl.moduleSpecifier)
|
||||
? decl.moduleSpecifier.text
|
||||
: null;
|
||||
}
|
||||
|
||||
/** What an identifier binds from an island module, or null. */
|
||||
/** Whether `ident` names an import of a Node island module: its value
|
||||
* comes from the island, whatever the package's declarations say. */
|
||||
export function isIslandImportBinding(lowerer: Lowerer, ident: ts.Identifier): boolean {
|
||||
return islandBindingOf(lowerer, ident) !== null;
|
||||
}
|
||||
|
||||
function islandBindingOf(lowerer: Lowerer, ident: ts.Identifier): IslandBinding | null {
|
||||
const islands = lowerer.nodeIslands;
|
||||
if (islands === null) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(ident);
|
||||
const decl = symbol ? symbol.declarations[0] : undefined;
|
||||
if (decl === undefined) return null;
|
||||
let importDecl: ts.Node | undefined;
|
||||
let binding: "namespace" | "default" | { name: string };
|
||||
if (ts.isNamespaceImport(decl)) {
|
||||
importDecl = decl.parent?.parent;
|
||||
binding = "namespace";
|
||||
} else if (ts.isImportClause(decl)) {
|
||||
importDecl = decl.parent;
|
||||
binding = "default";
|
||||
} else if (ts.isImportSpecifier(decl)) {
|
||||
importDecl = decl.parent?.parent?.parent;
|
||||
const name = decl.propertyName?.text ?? decl.name.text;
|
||||
binding = name === "default" ? "default" : { name };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
const spec = moduleSpecifierOf(importDecl);
|
||||
if (spec === null || importDecl === undefined) return null;
|
||||
const path = islandModuleFile(
|
||||
islands,
|
||||
ts.getSourceFileOfNode(importDecl).fileName,
|
||||
spec,
|
||||
"import",
|
||||
);
|
||||
if (path === null) return null;
|
||||
const module = lowerer.islandModules.get(path);
|
||||
if (module === undefined) return null;
|
||||
if (binding === "namespace") return { kind: "namespace", module };
|
||||
if (binding === "default") return { kind: "default", module };
|
||||
return { kind: "named", module, name: binding.name };
|
||||
}
|
||||
|
||||
/** The raw (dyn) value an island import binding reads. */
|
||||
function islandBindingValue(
|
||||
lowerer: Lowerer,
|
||||
b: IslandBinding,
|
||||
node: ts.Node,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const ns = varRef(b.module.global.id, DYN, loc);
|
||||
switch (b.kind) {
|
||||
case "namespace":
|
||||
if (!b.module.esm)
|
||||
boundaryFail(lowerer, "the namespace object of a CommonJS island module as a value", node);
|
||||
return ns;
|
||||
case "default":
|
||||
return b.module.esm ? islandGet(ns, "default", loc) : ns;
|
||||
case "named": {
|
||||
if (b.module.esm) return islandGet(ns, b.name, loc);
|
||||
const g = b.module.snapshots.get(b.name);
|
||||
return g === undefined ? islandGet(ns, b.name, loc) : varRef(g.id, DYN, loc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** An island value (dyn) at the declared type of `node`: island handles
|
||||
* stay dyn (FOREIGN), primitives and strings meet their declared type
|
||||
* through the checked conversion. */
|
||||
function fromIsland(lowerer: Lowerer, raw: IrExpr, node: ts.Expression): IrExpr {
|
||||
const tsType = lowerer.typeOf(node);
|
||||
const islands = lowerer.nodeIslands!;
|
||||
if (isForeignType(islands, tsType, lowerer.checker)) return raw;
|
||||
if ((tsType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return raw;
|
||||
if ((tsType.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined)) !== 0 && isDiscarded(node))
|
||||
return raw;
|
||||
const mapped = lowerer.mapTypeOf(tsType);
|
||||
if (mapped === null)
|
||||
boundaryFail(
|
||||
lowerer,
|
||||
`an island value of type '${lowerer.checker.typeToString(tsType)}'`,
|
||||
node,
|
||||
);
|
||||
if (mapped.kind === "dyn") return raw;
|
||||
if (mapped.kind === "void") return raw;
|
||||
// An island promise is the same promise on both sides (the runtime keeps
|
||||
// one ScrPromise per V8 promise): PromiseResolve of the dyn value, whose
|
||||
// settlement value meets the declared type when it is awaited.
|
||||
if (
|
||||
mapped.kind === "promise" &&
|
||||
(mapped.inner.kind === "dyn" || copiedType(lowerer, mapped.inner))
|
||||
) {
|
||||
const promise: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "dyn.promiseResolve",
|
||||
args: [raw, thenReader(lowerer, raw.loc)],
|
||||
type: { kind: "promise", inner: DYN },
|
||||
loc: raw.loc,
|
||||
};
|
||||
return mapped.inner.kind === "dyn" ? promise : lowerer.coerceToExpected(promise, mapped);
|
||||
}
|
||||
if (!copiedType(lowerer, mapped))
|
||||
boundaryFail(
|
||||
lowerer,
|
||||
`an island value of type '${lowerer.checker.typeToString(tsType)}' (only primitives and strings are copied; objects of types the island does not declare stay handles from NI-5 on)`,
|
||||
node,
|
||||
);
|
||||
return lowerer.coerceToExpected(raw, mapped);
|
||||
}
|
||||
|
||||
/** Whether a value of IR type `t` is copied at the boundary: primitives,
|
||||
* strings, null/undefined, and unions of those. */
|
||||
function copiedType(lowerer: Lowerer, t: IrType): boolean {
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
case "string":
|
||||
case "bool":
|
||||
case "bigint":
|
||||
case "nullT":
|
||||
case "undefinedT":
|
||||
return true;
|
||||
case "union": {
|
||||
const def = lowerer.unions.get(t.unionId);
|
||||
return def !== undefined && def.arms.every((a) => copiedType(lowerer, a));
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isDiscarded(node: ts.Node): boolean {
|
||||
let n: ts.Node = node;
|
||||
while (n.parent && ts.isParenthesizedExpression(n.parent)) n = n.parent;
|
||||
return (
|
||||
n.parent !== undefined && (ts.isExpressionStatement(n.parent) || ts.isVoidExpression(n.parent))
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether `node` evaluates to an island handle (by its declared type) or
|
||||
* is an island namespace binding. */
|
||||
function isForeignExpr(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
const islands = lowerer.nodeIslands;
|
||||
if (islands === null) return false;
|
||||
const inner = stripTypeWrappers(node);
|
||||
if (ts.isIdentifier(inner) && islandBindingOf(lowerer, inner) !== null) return true;
|
||||
return isForeignType(islands, lowerer.typeOf(inner), lowerer.checker);
|
||||
}
|
||||
|
||||
/** A value crossing INTO the island (an argument or an assigned value), as
|
||||
* dyn. */
|
||||
function toIsland(lowerer: Lowerer, node: ts.Expression): IrExpr {
|
||||
const islands = lowerer.nodeIslands!;
|
||||
const inner = stripTypeWrappers(node);
|
||||
if (ts.isObjectLiteralExpression(inner) || ts.isArrayLiteralExpression(inner)) {
|
||||
return freshLiteral(lowerer, inner);
|
||||
}
|
||||
const crossing = crossingOf(islands, lowerer.typeOf(node), lowerer.checker);
|
||||
switch (crossing) {
|
||||
case "copy":
|
||||
case "handle":
|
||||
return lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN);
|
||||
case "dyn":
|
||||
case "refused":
|
||||
return boundaryFail(
|
||||
lowerer,
|
||||
`a '${lowerer.checker.typeToString(lowerer.typeOf(node))}' value (a native object would cross as a copy and lose its identity)`,
|
||||
node,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** A fresh object or array literal crosses as a copy (a plain dyn object
|
||||
* or array the runtime copies into V8): nothing native can alias it. Its
|
||||
* members cross by the same rules (toIsland). */
|
||||
function freshLiteral(
|
||||
lowerer: Lowerer,
|
||||
node: ts.ObjectLiteralExpression | ts.ArrayLiteralExpression,
|
||||
): IrExpr {
|
||||
const loc = locOf(node);
|
||||
if (ts.isArrayLiteralExpression(node)) {
|
||||
const elems = node.elements.nodes.map((el) => {
|
||||
if (ts.isSpreadElement(el) || ts.isOmittedExpression(el))
|
||||
boundaryFail(lowerer, "a spread or hole in an array literal", el);
|
||||
return toIsland(lowerer, el);
|
||||
});
|
||||
return { kind: "dynArrLit", elems, type: DYN, loc };
|
||||
}
|
||||
const fields = node.properties.nodes.map((p) => {
|
||||
let key: string;
|
||||
let value: IrExpr;
|
||||
if (ts.isPropertyAssignment(p) && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))) {
|
||||
key = p.name.text;
|
||||
value = toIsland(lowerer, p.initializer);
|
||||
} else if (ts.isShorthandPropertyAssignment(p)) {
|
||||
const name = p.name as ts.Identifier;
|
||||
key = name.text;
|
||||
value = toIsland(lowerer, name);
|
||||
} else {
|
||||
return boundaryFail(
|
||||
lowerer,
|
||||
"a computed key, method, accessor or spread in an object literal",
|
||||
p,
|
||||
);
|
||||
}
|
||||
return { key: { kind: "strLit", value: key, type: STRING, loc } as IrExpr, value };
|
||||
});
|
||||
return { kind: "dynObjLit", fields, type: DYN, loc };
|
||||
}
|
||||
|
||||
/** Whether evaluating an argument runs no code that could change the
|
||||
* receiver: literals, names, `this`, functions it creates, object and
|
||||
* array literals of those, and operators over those. A unary operator
|
||||
* (`+`, `-`, `~`) converts its operand with ToNumber, which calls a
|
||||
* user-defined `valueOf`: only a numeric literal operand is inert. */
|
||||
function inertArgument(node: ts.Expression): boolean {
|
||||
const e = stripTypeWrappers(node);
|
||||
if (
|
||||
ts.isIdentifier(e) ||
|
||||
ts.isLiteralExpression(e) ||
|
||||
ts.isNoSubstitutionTemplateLiteral(e) ||
|
||||
ts.isArrowFunction(e) ||
|
||||
ts.isFunctionExpression(e) ||
|
||||
e.kind === ts.SyntaxKind.ThisKeyword ||
|
||||
e.kind === ts.SyntaxKind.TrueKeyword ||
|
||||
e.kind === ts.SyntaxKind.FalseKeyword ||
|
||||
e.kind === ts.SyntaxKind.NullKeyword
|
||||
)
|
||||
return true;
|
||||
if (ts.isParenthesizedExpression(e)) return inertArgument(e.expression);
|
||||
if (ts.isArrayLiteralExpression(e))
|
||||
return e.elements.nodes.every((el) => !ts.isSpreadElement(el) && inertArgument(el));
|
||||
if (ts.isObjectLiteralExpression(e))
|
||||
return e.properties.nodes.every(
|
||||
(p) =>
|
||||
ts.isShorthandPropertyAssignment(p) ||
|
||||
ts.isMethodDeclaration(p) ||
|
||||
(ts.isPropertyAssignment(p) &&
|
||||
!ts.isComputedPropertyName(p.name) &&
|
||||
inertArgument(p.initializer)),
|
||||
);
|
||||
if (ts.isPrefixUnaryExpression(e)) {
|
||||
// `!x` runs no conversion on its operand. `+`, `-` and `~` convert it
|
||||
// with ToNumber, which calls a user-defined `valueOf` on an object:
|
||||
// that can replace the method being called, so only a numeric literal
|
||||
// is inert.
|
||||
if (e.operator === ts.SyntaxKind.ExclamationToken) return inertArgument(e.operand);
|
||||
return e.operand.kind === ts.SyntaxKind.NumericLiteral;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function argsOf(lowerer: Lowerer, args: readonly ts.Expression[], loc: SrcLoc): IrExpr {
|
||||
for (const a of args) if (ts.isSpreadElement(a)) boundaryFail(lowerer, "spread arguments", a);
|
||||
return { kind: "dynArrLit", elems: args.map((a) => toIsland(lowerer, a)), type: DYN, loc };
|
||||
}
|
||||
|
||||
/** Expressions on island values (the lowerExpr hook; null: not an island
|
||||
* expression, the ordinary lowering applies). */
|
||||
export function lowerIslandExpr(lowerer: Lowerer, expr: ts.Expression): IrExpr | null {
|
||||
if (lowerer.nodeIslands === null) return null;
|
||||
const loc = locOf(expr);
|
||||
if (ts.isIdentifier(expr)) {
|
||||
const b = islandBindingOf(lowerer, expr);
|
||||
if (b === null) return null;
|
||||
lowerer.usesNodeIsland = true;
|
||||
return fromIsland(lowerer, islandBindingValue(lowerer, b, expr, loc), expr);
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) {
|
||||
if (!isForeignExpr(lowerer, expr.expression)) return null;
|
||||
if (expr.questionDotToken)
|
||||
boundaryFail(lowerer, "an optional member read on an island value", expr);
|
||||
lowerer.usesNodeIsland = true;
|
||||
const target = islandReceiver(lowerer, expr.expression, expr.name.text, loc);
|
||||
if (target.direct !== undefined) return fromIsland(lowerer, target.direct, expr);
|
||||
return fromIsland(lowerer, islandGet(target.value, expr.name.text, loc), expr);
|
||||
}
|
||||
if (ts.isElementAccessExpression(expr)) {
|
||||
if (!isForeignExpr(lowerer, expr.expression)) return null;
|
||||
if (expr.questionDotToken)
|
||||
boundaryFail(lowerer, "an optional member read on an island value", expr);
|
||||
lowerer.usesNodeIsland = true;
|
||||
const target = lowerer.lowerExpr(expr.expression);
|
||||
const key = toIsland(lowerer, expr.argumentExpression);
|
||||
return fromIsland(
|
||||
lowerer,
|
||||
{ kind: "libCall", fn: "nodeIsland.getKey", args: [target, key], type: DYN, loc },
|
||||
expr,
|
||||
);
|
||||
}
|
||||
if (ts.isCallExpression(expr)) {
|
||||
const dynamicLoad = lowerIslandLoadCall(lowerer, expr, loc);
|
||||
if (dynamicLoad !== undefined) return dynamicLoad;
|
||||
const callee = stripTypeWrappers(expr.expression);
|
||||
if (expr.questionDotToken) return null;
|
||||
const args = expr.arguments.nodes;
|
||||
if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) {
|
||||
if (!isForeignExpr(lowerer, callee.expression)) return null;
|
||||
if (callee.questionDotToken)
|
||||
boundaryFail(lowerer, "an optional call on an island value", expr);
|
||||
lowerer.usesNodeIsland = true;
|
||||
const target = islandReceiver(lowerer, callee.expression, callee.name.text, loc);
|
||||
if (target.direct !== undefined) {
|
||||
return fromIsland(
|
||||
lowerer,
|
||||
{
|
||||
kind: "libCall",
|
||||
fn: "nodeIsland.apply",
|
||||
args: [target.direct, argsOf(lowerer, args, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
expr,
|
||||
);
|
||||
}
|
||||
const name: IrExpr = { kind: "strLit", value: callee.name.text, type: STRING, loc };
|
||||
if (args.every((a) => inertArgument(a))) {
|
||||
return fromIsland(
|
||||
lowerer,
|
||||
{
|
||||
kind: "libCall",
|
||||
fn: "nodeIsland.invoke",
|
||||
args: [target.value, name, argsOf(lowerer, args, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
expr,
|
||||
);
|
||||
}
|
||||
// An argument that runs code could replace the method: JavaScript
|
||||
// reads the method first, then evaluates the arguments.
|
||||
const stmts: IrStmt[] = [];
|
||||
const receiver = lowerer.declareHiddenLocal("%islandRecv", target.value.type);
|
||||
stmts.push({ kind: "varDecl", localId: receiver.id, init: target.value, loc });
|
||||
const recv = varRef(receiver.id, target.value.type, loc);
|
||||
const method = lowerer.declareHiddenLocal("%islandMethod", DYN);
|
||||
stmts.push({
|
||||
kind: "varDecl",
|
||||
localId: method.id,
|
||||
init: { kind: "libCall", fn: "nodeIsland.get", args: [recv, name], type: DYN, loc },
|
||||
loc,
|
||||
});
|
||||
const call: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "nodeIsland.applyTo",
|
||||
args: [varRef(method.id, DYN, loc), recv, argsOf(lowerer, args, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const result = fromIsland(lowerer, call, expr);
|
||||
return { kind: "seqExpr", stmts, result, type: result.type, loc };
|
||||
}
|
||||
if (!isForeignExpr(lowerer, callee)) return null;
|
||||
lowerer.usesNodeIsland = true;
|
||||
const fn = lowerer.lowerExpr(callee);
|
||||
return fromIsland(
|
||||
lowerer,
|
||||
{
|
||||
kind: "libCall",
|
||||
fn: "nodeIsland.apply",
|
||||
args: [fn, argsOf(lowerer, args, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
expr,
|
||||
);
|
||||
}
|
||||
if (ts.isNewExpression(expr)) {
|
||||
if (!isForeignExpr(lowerer, expr.expression)) return null;
|
||||
lowerer.usesNodeIsland = true;
|
||||
const ctor = lowerer.lowerExpr(expr.expression);
|
||||
return fromIsland(
|
||||
lowerer,
|
||||
{
|
||||
kind: "libCall",
|
||||
fn: "nodeIsland.construct",
|
||||
args: [ctor, argsOf(lowerer, expr.arguments?.nodes ?? [], loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
expr,
|
||||
);
|
||||
}
|
||||
if (ts.isBinaryExpression(expr)) {
|
||||
const op = expr.operatorToken.kind;
|
||||
if (op === ts.SyntaxKind.InstanceOfKeyword && isForeignExpr(lowerer, expr.right)) {
|
||||
lowerer.usesNodeIsland = true;
|
||||
const value = lowerer.coerceToExpected(lowerer.lowerExpr(expr.left), DYN);
|
||||
const ctor = lowerer.lowerExpr(expr.right);
|
||||
return { kind: "libCall", fn: "nodeIsland.instanceof", args: [value, ctor], type: BOOL, loc };
|
||||
}
|
||||
if (op === ts.SyntaxKind.EqualsToken) {
|
||||
const left = stripTypeWrappers(expr.left);
|
||||
if (
|
||||
(ts.isPropertyAccessExpression(left) && ts.isIdentifier(left.name)) ||
|
||||
ts.isElementAccessExpression(left)
|
||||
) {
|
||||
if (!isForeignExpr(lowerer, left.expression)) return null;
|
||||
lowerer.usesNodeIsland = true;
|
||||
// JavaScript's order: the receiver, then the key, then the value.
|
||||
const stmts: IrStmt[] = [];
|
||||
const hold = (name: string, init: IrExpr, type: IrType): IrExpr => {
|
||||
const local = lowerer.declareHiddenLocal(name, type);
|
||||
stmts.push({ kind: "varDecl", localId: local.id, init, loc });
|
||||
return varRef(local.id, type, loc);
|
||||
};
|
||||
const target = lowerer.lowerExpr(left.expression);
|
||||
const receiver = hold("%islandSetTarget", target, target.type);
|
||||
const key = ts.isPropertyAccessExpression(left)
|
||||
? null
|
||||
: hold("%islandSetKey", toIsland(lowerer, left.argumentExpression), DYN);
|
||||
const v = hold("%islandSet", toIsland(lowerer, expr.right), DYN);
|
||||
stmts.push({
|
||||
kind: "exprStmt",
|
||||
expr:
|
||||
key === null
|
||||
? {
|
||||
kind: "libCall",
|
||||
fn: "nodeIsland.set",
|
||||
args: [
|
||||
receiver,
|
||||
{
|
||||
kind: "strLit",
|
||||
value: (left as ts.PropertyAccessExpression).name.text,
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
v,
|
||||
],
|
||||
type: { kind: "void" },
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "libCall",
|
||||
fn: "nodeIsland.setKey",
|
||||
args: [receiver, key, v],
|
||||
type: { kind: "void" },
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
const result = fromIsland(lowerer, v, expr);
|
||||
return { kind: "seqExpr", stmts, result, type: result.type, loc };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const left = stripTypeWrappers(expr.left);
|
||||
if (
|
||||
op >= ts.SyntaxKind.FirstCompoundAssignment &&
|
||||
op <= ts.SyntaxKind.LastCompoundAssignment &&
|
||||
(ts.isPropertyAccessExpression(left) || ts.isElementAccessExpression(left)) &&
|
||||
isForeignExpr(lowerer, left.expression)
|
||||
) {
|
||||
boundaryFail(lowerer, "a compound assignment to an island object's member", expr);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
function islandReceiver(
|
||||
lowerer: Lowerer,
|
||||
receiver: ts.Expression,
|
||||
member: string,
|
||||
loc: SrcLoc,
|
||||
): { value: IrExpr; direct?: IrExpr } {
|
||||
const inner = stripTypeWrappers(receiver);
|
||||
if (ts.isIdentifier(inner)) {
|
||||
const b = islandBindingOf(lowerer, inner);
|
||||
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 };
|
||||
return { value: exports };
|
||||
}
|
||||
}
|
||||
return { value: lowerer.coerceToExpected(lowerer.lowerExpr(receiver), FOREIGN) };
|
||||
}
|
||||
|
||||
/** `require("pkg")` (the global require, anywhere) and `import("pkg")` of
|
||||
* an island module: load it at the call. Undefined: not such a call. */
|
||||
function lowerIslandLoadCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | undefined {
|
||||
const islands = lowerer.nodeIslands!;
|
||||
const arg = expr.arguments.nodes[0];
|
||||
if (expr.arguments.nodes.length !== 1 || arg === undefined || !ts.isStringLiteralLike(arg))
|
||||
return undefined;
|
||||
const isImport = expr.expression.kind === ts.SyntaxKind.ImportKeyword;
|
||||
const isRequire =
|
||||
ts.isIdentifier(expr.expression) &&
|
||||
expr.expression.text === "require" &&
|
||||
!lowerer.checker
|
||||
.getSymbolAtLocation(expr.expression)
|
||||
?.declarations.some((d) => !ts.getSourceFileOfNode(d).isDeclarationFile);
|
||||
if (!isImport && !isRequire) return undefined;
|
||||
const file = ts.getSourceFileOfNode(expr).fileName;
|
||||
const path = islandModuleFile(islands, file, arg.text, isImport ? "import" : "require");
|
||||
if (path === null) return undefined;
|
||||
lowerer.usesNodeIsland = true;
|
||||
if (isImport) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "nodeIsland.import",
|
||||
args: [islandLoadPath(lowerer, path, loc)],
|
||||
type: { kind: "promise", inner: DYN },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return fromIsland(lowerer, loadIslandModule(lowerer, path, loc), expr);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { DiagnosticCode } from "../../diagnostics/codes.js";
|
||||
import { keepsInitializerLayout, notePreservedLayout } from "./structural-instances.js";
|
||||
import { jsGlobalValueByName } from "./global-values.js";
|
||||
import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js";
|
||||
import { lowerIslandExpr } from "./lower-node-island.js";
|
||||
import {
|
||||
allBytesArms,
|
||||
indexedUnionArms,
|
||||
@@ -8032,6 +8033,11 @@ function lowerDereferencedElementWrite(lowerer: Lowerer, expr: ts.BinaryExpressi
|
||||
export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStmt {
|
||||
const stmtNode = expr.parent;
|
||||
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
|
||||
// Node islands (scope-B NI-4): writes and calls on island values.
|
||||
if (lowerer.nodeIslands !== null) {
|
||||
const island = lowerIslandExpr(lowerer, expr);
|
||||
if (island !== null) return { kind: "exprStmt", expr: island, loc: locOf(expr) };
|
||||
}
|
||||
// A discarded conditional has no value to merge: represent its lazy
|
||||
// branch selection directly as statement control flow. In particular,
|
||||
// both arms may be void calls (`flag ? emitA() : emitB()`), for which a
|
||||
|
||||
@@ -44,6 +44,13 @@ import {
|
||||
import { moduleNamespaceObject, moduleNamespaceValue } from "./module-namespace-elements.js";
|
||||
import { keyedHelperName, moduleTagNumber, portableRelativePath } from "./stable-names.js";
|
||||
import { positionTag, StructuralPositions } from "./structural-positions.js";
|
||||
import {
|
||||
activeNodeIslands,
|
||||
isForeignType,
|
||||
setActiveNodeIslands,
|
||||
type NodeIslandSet,
|
||||
} from "../../node-islands/boundary.js";
|
||||
import type { IslandModuleBinding } from "./lower-node-island.js";
|
||||
import { bindingSymbolOfRead, onlyForwarded, stripTypeWrappers } from "./typed-arguments.js";
|
||||
import {
|
||||
needsGeneralNodeStreams,
|
||||
@@ -938,6 +945,9 @@ export interface LowerResult {
|
||||
}
|
||||
|
||||
export interface LowerOptions {
|
||||
/** Packages that run in Node islands (scope-B NI-4): their imports lower
|
||||
* to island loads and their values to island handles. */
|
||||
nodeIslands?: NodeIslandSet | null;
|
||||
/** Parser/checker services owned by the load; required for npm graph analysis. */
|
||||
frontendServices?: FrontendServices | undefined;
|
||||
/** The project's tsconfig sets `checkJs` (see isCheckedJsSourceFile). */
|
||||
@@ -1179,6 +1189,60 @@ export function lowerToIr(
|
||||
entry: ts.SourceFile,
|
||||
moduleOrder: ts.SourceFile[],
|
||||
options: LowerOptions = {},
|
||||
): LowerResult {
|
||||
setActiveNodeIslands(options.nodeIslands ?? null);
|
||||
try {
|
||||
const result = lowerToIrWithIslands(program, entry, moduleOrder, options);
|
||||
markModuleInitFences(program, result.runtimeFences);
|
||||
return result;
|
||||
} finally {
|
||||
setActiveNodeIslands(null);
|
||||
}
|
||||
}
|
||||
|
||||
/** Marks the runtime fences in npm package code whose site is top-level
|
||||
* module code (outside every function body): they throw when the module
|
||||
* evaluates (ScrDiagnostic.moduleInit). */
|
||||
function markModuleInitFences(program: ts.Program, fences: readonly ScrDiagnostic[]): void {
|
||||
for (const d of fences) {
|
||||
if (!d.loc.file.includes("/node_modules/")) continue;
|
||||
// A fence without a source position belongs to synthesized code, such
|
||||
// as the global object's lookup (global-values.ts), which runs when the
|
||||
// program calls it, not when the module evaluates.
|
||||
if (d.loc.start < 0) continue;
|
||||
const sf = program.getSourceFile(d.loc.file);
|
||||
if (sf === undefined) continue;
|
||||
let inFunction = false;
|
||||
let node: ts.Node | undefined = sf;
|
||||
while (node !== undefined && !inFunction) {
|
||||
let next: ts.Node | undefined;
|
||||
ts.forEachChild(node, (child) => {
|
||||
if (
|
||||
next === undefined &&
|
||||
ts.tokenStartOfNode(child) <= d.loc.start &&
|
||||
d.loc.start < child.end
|
||||
)
|
||||
next = child;
|
||||
return undefined;
|
||||
});
|
||||
if (next === undefined) break;
|
||||
if (
|
||||
ts.isFunctionLike(next) ||
|
||||
(ts.isPropertyDeclaration(next) &&
|
||||
!next.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword))
|
||||
)
|
||||
inFunction = true;
|
||||
node = next;
|
||||
}
|
||||
if (!inFunction) d.moduleInit = true;
|
||||
}
|
||||
}
|
||||
|
||||
function lowerToIrWithIslands(
|
||||
program: ts.Program,
|
||||
entry: ts.SourceFile,
|
||||
moduleOrder: ts.SourceFile[],
|
||||
options: LowerOptions,
|
||||
): LowerResult {
|
||||
const phaseTiming = process.env["SCRIPTC_TIMING"] === "1";
|
||||
const phaseStarted = performance.now();
|
||||
@@ -3550,6 +3614,11 @@ export class Lowerer {
|
||||
/** The program calls into Node.js (@scriptc/node): the module carries
|
||||
* the nodeIsland record, which the executable build fills in. */
|
||||
usesNodeIsland = false;
|
||||
/** The program's Node island packages (scope-B NI-4), or null. */
|
||||
readonly nodeIslands: NodeIslandSet | null = activeNodeIslands();
|
||||
/** Island module loads, by the importing file and specifier: the module
|
||||
* global holding what the import form sees (lower-node-island.ts). */
|
||||
readonly islandModules = new Map<string, IslandModuleBinding>();
|
||||
/** The modules Node island calls load (lower-node-island.ts). */
|
||||
readonly nodeIslandModules = new NodeIslandModuleCollector();
|
||||
/** Each call's caller literal, set to its tree path when lowering ends. */
|
||||
@@ -3560,6 +3629,8 @@ export class Lowerer {
|
||||
caller: string;
|
||||
target: string;
|
||||
}[] = [];
|
||||
/** Each island module load's path literal, set to its tree path when lowering ends. */
|
||||
readonly nodeIslandLoads: { literal: IrExpr & { kind: "strLit" }; path: string }[] = [];
|
||||
/** Per-directory answers of experimental-import.ts's declaration-file test. */
|
||||
readonly experimentalFiles = new Map<string, boolean>();
|
||||
/** Interned LifetimeRegion.run helpers, by callback type key. */
|
||||
@@ -4044,6 +4115,11 @@ export class Lowerer {
|
||||
// one lowers again rather than replaying a record.
|
||||
checkedTable("nodeIslandCallers", () => this.nodeIslandCallers.length),
|
||||
checkedTable("nodeIslandSpecifiers", () => this.nodeIslandSpecifiers.length),
|
||||
// Island module loads hold literals of the unit's IR too, and their
|
||||
// module globals (with each CommonJS named import's snapshot global)
|
||||
// are created by the first load: a unit that adds either lowers again.
|
||||
checkedTable("islandModules", () => this.islandModules.size),
|
||||
checkedTable("nodeIslandLoads", () => this.nodeIslandLoads.length),
|
||||
checkedTable("fnSigsBySymbol", () => this.fnSigsBySymbol.size),
|
||||
checkedTable("cjsExportsAssignments", () => this.cjsExportsAssignments.size),
|
||||
checkedTable("inlineRequireCycleCalls", () => this.inlineRequireCycleCalls.size),
|
||||
@@ -4267,6 +4343,7 @@ export class Lowerer {
|
||||
static readonly unjournaled: readonly string[] = [
|
||||
// settings
|
||||
"frontendServices",
|
||||
"nodeIslands",
|
||||
"ambient",
|
||||
"overridesAmbient",
|
||||
"remainder",
|
||||
@@ -4820,8 +4897,12 @@ export class Lowerer {
|
||||
this.externalTypeSpecifiersByFile =
|
||||
mode.externalTypeSpecifiersByFile ?? directExternalTypeSpecifiersByFile(this.externalTypes);
|
||||
this.checker = program.getTypeChecker();
|
||||
const islands = this.nodeIslands;
|
||||
const shadow = canonShadowHook(this.checker);
|
||||
this.typeCtx = {
|
||||
...(islands !== null
|
||||
? { foreignType: (t: ts.Type) => isForeignType(islands, t, this.checker) }
|
||||
: {}),
|
||||
checker: this.checker,
|
||||
positionOf: (node) => this.positions.of(node),
|
||||
shapes: this.shapes,
|
||||
@@ -10568,14 +10649,16 @@ export class Lowerer {
|
||||
}
|
||||
|
||||
/** The module's Node island record: the collected module tree, with each
|
||||
* call's caller literal set to its path in that tree. The executable
|
||||
* build fills in the libnode pack (index.ts). */
|
||||
* call's caller literal and each load's path literal set to its path in
|
||||
* that tree. The executable build fills in the libnode pack (index.ts). */
|
||||
private nodeIslandRecord(): NonNullable<IrModule["nodeIsland"]> {
|
||||
const modules = this.nodeIslandModules.tree();
|
||||
for (const { literal, caller } of this.nodeIslandCallers)
|
||||
literal.value = NodeIslandModuleCollector.callerPath(modules, caller);
|
||||
for (const { literal, caller, target } of this.nodeIslandSpecifiers)
|
||||
literal.value = NodeIslandModuleCollector.relativeSpecifier(caller, target);
|
||||
for (const { literal, path } of this.nodeIslandLoads)
|
||||
literal.value = NodeIslandModuleCollector.loadPath(modules, path);
|
||||
return { libnode: "", nodeVersion: "", ...(modules === null ? {} : { modules }) };
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ import * as ts from "./ts7/adapter.js";
|
||||
|
||||
export const NODE_API_MODULE = "@scriptc/node";
|
||||
|
||||
export type NodeApiMember = "call" | "callSync";
|
||||
export type NodeApiMember = "call" | "callSync" | "requireModule";
|
||||
|
||||
function isNodeApiMember(name: string): name is NodeApiMember {
|
||||
return name === "call" || name === "callSync" || name === "requireModule";
|
||||
}
|
||||
|
||||
function moduleSpecifierText(decl: ts.Node | undefined): string | null {
|
||||
return ts.isImportDeclaration(decl) && ts.isStringLiteral(decl.moduleSpecifier)
|
||||
@@ -28,7 +32,7 @@ export function nodeApiBindingOf(
|
||||
if (!ts.isImportSpecifier(decl)) return null;
|
||||
if (moduleSpecifierText(decl.parent?.parent?.parent) !== NODE_API_MODULE) return null;
|
||||
const member = decl.propertyName?.text ?? decl.name.text;
|
||||
return member === "call" || member === "callSync" ? member : null;
|
||||
return isNodeApiMember(member) ? member : null;
|
||||
}
|
||||
|
||||
/** The @scriptc/node export an identifier is a named-import binding of. */
|
||||
@@ -55,6 +59,6 @@ export function nodeApiCalleeOf(program: ts.Program, callee: ts.Expression): Nod
|
||||
)
|
||||
return null;
|
||||
const member = callee.name.text;
|
||||
if (member !== "call" && member !== "callSync") return null;
|
||||
if (!isNodeApiMember(member)) return null;
|
||||
return isNodeApiNamespace(program, callee.expression) ? member : null;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ import { NODE_API_MODULE } from "./node-api-import.js";
|
||||
import { erasedTypeOnlyImport } from "./type-only-imports.js";
|
||||
import { EXPERIMENTAL_MODULE } from "./experimental-import.js";
|
||||
import { readProjectConfig } from "../project-config.js";
|
||||
import { setFrontendIslands } from "../node-islands/boundary.js";
|
||||
import { typePatchDiagnostics } from "./type-patches/layer.js";
|
||||
import { packageDirOf } from "../node-islands/origin.js";
|
||||
import {
|
||||
@@ -128,12 +129,18 @@ export interface Frontend {
|
||||
}
|
||||
|
||||
/** Hosts own transport lifetimes; shared compiler stages own the frontend. */
|
||||
/** What a frontend computes beyond the build's own needs. */
|
||||
/** How a frontend loads the program, beyond its npm package selection. */
|
||||
export interface FrontendOptions {
|
||||
/** Packages that run in Node islands (scope-B NI-4): never compiled,
|
||||
* typed by their declarations. */
|
||||
islandPackages?: ReadonlySet<string>;
|
||||
/** The Node island report (coverage): the project's scriptc settings and
|
||||
* the detector's scan of the program and its npm packages. Builds do not
|
||||
* read either. */
|
||||
nodeIslands?: boolean;
|
||||
/** The detector's scan alone, for a build that may run packages in Node
|
||||
* islands (executable/prepare.ts decides which). */
|
||||
islandScan?: boolean;
|
||||
}
|
||||
|
||||
export type FrontendFactory = (
|
||||
@@ -482,15 +489,21 @@ export function runFrontend(
|
||||
transitive.add(base);
|
||||
return base;
|
||||
});
|
||||
return loadFrontend(
|
||||
entryPath,
|
||||
trackedLoader,
|
||||
"lib",
|
||||
externalTypes,
|
||||
[...new Set(names)],
|
||||
transitive,
|
||||
options,
|
||||
);
|
||||
const islands = options.islandPackages ?? new Set<string>();
|
||||
setFrontendIslands(islands);
|
||||
try {
|
||||
return loadFrontend(
|
||||
entryPath,
|
||||
trackedLoader,
|
||||
"lib",
|
||||
externalTypes,
|
||||
[...new Set(names)].filter((name) => !islands.has(name)),
|
||||
transitive,
|
||||
options,
|
||||
);
|
||||
} finally {
|
||||
setFrontendIslands(new Set());
|
||||
}
|
||||
} catch (error) {
|
||||
for (const load of active) {
|
||||
// Preserve the original failure and attempt every remaining cleanup.
|
||||
@@ -520,8 +533,10 @@ function loadFrontend(
|
||||
clearResolveCaches();
|
||||
const statuses: NpmStaticStatus[] = [];
|
||||
const npmSites = new Map<string, SrcLoc>();
|
||||
// Node island detection (report-only): how each package was refused, and
|
||||
// the modules of packages removed from the program.
|
||||
// Node island detection (the report, or a build that may run islands):
|
||||
// how each package was refused, and the modules of packages removed from
|
||||
// the program.
|
||||
const scanIslands = options.nodeIslands === true || options.islandScan === true;
|
||||
const refusals: RefusalRecord[] = [];
|
||||
const droppedScan: { modules: NodeIslandScan["modules"]; edges: NodeIslandScan["edges"] } = {
|
||||
modules: [],
|
||||
@@ -532,7 +547,11 @@ function loadFrontend(
|
||||
};
|
||||
// Explicit library attempts bypass only the auto-selection heuristics.
|
||||
// The shared preflight/lowering refusal and dependency-closure checks remain.
|
||||
// Node island packages are judged already: they stay out of the
|
||||
// program, typed by their declarations (scope-B NI-4).
|
||||
const judged = new Set<string>(libraryNpmStatic);
|
||||
if (options.islandPackages !== undefined)
|
||||
for (const name of options.islandPackages) judged.add(name);
|
||||
let requested: string[] = [];
|
||||
let reusableScout: ReturnType<typeof loadProgram> | null = null;
|
||||
let reusablePreflight: ScrDiagnostic[] | null = null;
|
||||
@@ -658,7 +677,7 @@ function loadFrontend(
|
||||
}
|
||||
const dropping = [...reasons.keys()].filter((p) => effective.has(p));
|
||||
if (dropping.length === 0) break;
|
||||
if (options.nodeIslands === true) scanDroppedPackages(load, dropping, droppedScan);
|
||||
if (scanIslands) scanDroppedPackages(load, dropping, droppedScan);
|
||||
for (const p of dropping) {
|
||||
effective.delete(p);
|
||||
statuses.push({ package: p, status: "refused", detail: reasons.get(p)! });
|
||||
@@ -710,15 +729,13 @@ function loadFrontend(
|
||||
probe.dispose();
|
||||
if (probeDiags.some((d) => d.code === "SC0001")) dropWithNote(p);
|
||||
}
|
||||
if (options.nodeIslands === true)
|
||||
scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan);
|
||||
if (scanIslands) scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan);
|
||||
load.dispose();
|
||||
load = loadProgram(entryPath, { npmStatic: [...effective], externalTypes });
|
||||
preflight = checkPreflight(load);
|
||||
if (preflight.some((d) => d.code === "SC0001") && effective.size > 0) {
|
||||
for (const p of [...effective]) dropWithNote(p);
|
||||
if (options.nodeIslands === true)
|
||||
scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan);
|
||||
if (scanIslands) scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan);
|
||||
load.dispose();
|
||||
load = loadProgram(entryPath, { npmStatic: [...effective], externalTypes });
|
||||
preflight = checkPreflight(load);
|
||||
@@ -820,29 +837,28 @@ function loadFrontend(
|
||||
},
|
||||
npmStatic: statuses,
|
||||
npmImportSites: npmSites,
|
||||
nodeIslandScan:
|
||||
options.nodeIslands !== true
|
||||
? undefined
|
||||
: ({ userJs }) => {
|
||||
const files = new Set<SourceFile>([finalLoad.entry, ...finalLoad.moduleOrder]);
|
||||
addDynamicImportGraph(finalLoad, files);
|
||||
// Package edges matter only when the program has npm packages: one
|
||||
// that loads, one that was refused, or one judged and dropped.
|
||||
const npm =
|
||||
statuses.length > 0 ||
|
||||
droppedScan.modules.length > 0 ||
|
||||
[...files].some((sf) => sf.fileName.includes("/node_modules/"));
|
||||
const scanned = scanFiles(finalLoad.program, [...files], {
|
||||
userJs,
|
||||
dropped: false,
|
||||
edges: npm,
|
||||
});
|
||||
return {
|
||||
modules: [...droppedScan.modules, ...scanned.modules],
|
||||
edges: [...droppedScan.edges, ...scanned.edges],
|
||||
refusals,
|
||||
};
|
||||
},
|
||||
nodeIslandScan: !scanIslands
|
||||
? undefined
|
||||
: ({ userJs }) => {
|
||||
const files = new Set<SourceFile>([finalLoad.entry, ...finalLoad.moduleOrder]);
|
||||
addDynamicImportGraph(finalLoad, files);
|
||||
// Package edges matter only when the program has npm packages: one
|
||||
// that loads, one that was refused, or one judged and dropped.
|
||||
const npm =
|
||||
statuses.length > 0 ||
|
||||
droppedScan.modules.length > 0 ||
|
||||
[...files].some((sf) => sf.fileName.includes("/node_modules/"));
|
||||
const scanned = scanFiles(finalLoad.program, [...files], {
|
||||
userJs,
|
||||
dropped: false,
|
||||
edges: npm,
|
||||
});
|
||||
return {
|
||||
modules: [...droppedScan.modules, ...scanned.modules],
|
||||
edges: [...droppedScan.edges, ...scanned.edges],
|
||||
refusals,
|
||||
};
|
||||
},
|
||||
dispose: finalLoad.dispose,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
* JSON.parse's wording rather than tsc's, the one message-text delta on
|
||||
* that path (no snapshot pins it). */
|
||||
|
||||
import { isFrontendIsland } from "../node-islands/boundary.js";
|
||||
import { builtinModules } from "node:module";
|
||||
import { dirname, isAbsolute, resolve } from "node:path";
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
@@ -124,6 +125,7 @@ import {
|
||||
isRelativeSpecifier,
|
||||
isWorkspacePackageName,
|
||||
npmPackageNameOf,
|
||||
packageNameOfSpecifier,
|
||||
registerWorkspacePackage,
|
||||
workspacePackageOfPath,
|
||||
} from "./workspace-registry.js";
|
||||
@@ -5860,6 +5862,16 @@ function preflight7(load: LoadResult): {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// A re-export from a Node island package that tsconfig `paths` maps
|
||||
// to its installed directory: its declarations are no program
|
||||
// module (the lowering reports the island boundary).
|
||||
if (
|
||||
projectReexport !== null &&
|
||||
projectReexport.isDeclarationFile &&
|
||||
!isRelativeSpecifier(fromSpec) &&
|
||||
isFrontendIsland(packageNameOfSpecifier(fromSpec))
|
||||
)
|
||||
continue;
|
||||
const reDep = projectReexport;
|
||||
// `export * as ns from "./m"` re-exports the module NAMESPACE
|
||||
// object under a name: importers' `x.ns.member` reads resolve
|
||||
@@ -5984,6 +5996,16 @@ function preflight7(load: LoadResult): {
|
||||
// Node's startup crash with Node's exact message; what keeps a compile
|
||||
// fence is unsupported builtin or types-only surface.
|
||||
if (isBare && npmStaticDep === null) {
|
||||
// A Node island package that tsconfig `paths` maps to its
|
||||
// installed directory resolves to its declarations here: it loads
|
||||
// in Node, typed by them, like an island package resolved through
|
||||
// node_modules (scope-B NI-4).
|
||||
if (
|
||||
projDep !== null &&
|
||||
projDep.isDeclarationFile &&
|
||||
isFrontendIsland(packageNameOfSpecifier(spec))
|
||||
)
|
||||
continue;
|
||||
if (projDep !== null && projDep.isDeclarationFile) {
|
||||
diags.push(
|
||||
unsupportedDiag(
|
||||
@@ -6145,7 +6167,12 @@ function preflight7(load: LoadResult): {
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (dep === null && !(!isRelative && ambientModules.has(spec))) {
|
||||
} else if (
|
||||
dep === null &&
|
||||
!(!isRelative && ambientModules.has(spec)) &&
|
||||
// A Node island module's namespace (scope-B NI-4).
|
||||
!(!isRelative && isFrontendIsland(packageNameOfSpecifier(spec)))
|
||||
) {
|
||||
diags.push(unsupportedDiag("SC1013", locOf7(clause.namedBindings)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
CRYPTOHMAC_T,
|
||||
DATE_T,
|
||||
DYN,
|
||||
FOREIGN,
|
||||
F64,
|
||||
funcOf,
|
||||
isSupportedArrayElem,
|
||||
@@ -1366,6 +1367,10 @@ export type TypeParamTsResolver = (t: ts.Type) => ts.Type | null;
|
||||
* once per Lowerer and threaded through the recursion — adding a hook means
|
||||
* one new field here, not a new parameter at every call site. */
|
||||
export interface TypeMapperCtx {
|
||||
/** Node islands (scope-B NI-4): whether a type's values are island
|
||||
* handles, which map to the `foreign` type (a dyn value). Absent in
|
||||
* programs without islands. */
|
||||
foreignType?: (type: ts.Type) => boolean;
|
||||
/** Set only while a union maps an arm that is the type inferred from an
|
||||
* empty object literal, beside another non-nullish arm: that arm holds
|
||||
* only the literal's objects, so it maps to the empty record (see the
|
||||
@@ -2003,6 +2008,7 @@ function mapTypeEntry(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
return STRING;
|
||||
if (flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral)) return BOOL;
|
||||
}
|
||||
if (ctx.foreignType !== undefined && ctx.foreignType(type)) return FOREIGN;
|
||||
const localValue = ctx.localClassValue?.(type);
|
||||
if (localValue) {
|
||||
contextResolutions++;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createRequire } from "node:module";
|
||||
import { shapeOverlayEnabled } from "./frontend/shape-inference/index.js";
|
||||
import { nodeFrontendServices } from "./frontend/services-node.js";
|
||||
import {
|
||||
@@ -9,6 +10,8 @@ import { optimizationField, type NativeOptimization } from "./backend/optimizati
|
||||
import { targetCpuField, targetCpuTargetRefusal } from "./backend/target-cpu.js";
|
||||
import { compilationTiming, type CompilationTiming, type TimingClock } from "./timing.js";
|
||||
import { prepareExecutableModule } from "./executable/prepare.js";
|
||||
import { nodeIslandOptionsKey } from "./node-islands/policy.js";
|
||||
import { setIslandResolver } from "./node-islands/resolve.js";
|
||||
import {
|
||||
prepareLibrary,
|
||||
libraryLocalizeSymbols,
|
||||
@@ -458,6 +461,10 @@ export async function analyzeCached(
|
||||
return result;
|
||||
}
|
||||
|
||||
// Island code resolves through Node (node-islands/resolve.ts; the
|
||||
// self-hosted compiler builds no islands).
|
||||
setIslandResolver((fromFile, spec) => createRequire(fromFile).resolve(spec));
|
||||
|
||||
const nodeFrontend: FrontendFactory = (
|
||||
entry,
|
||||
npmStatic,
|
||||
@@ -1770,6 +1777,9 @@ async function compileTracked(
|
||||
...(functionLayoutDigest === undefined ? {} : { functionLayout: functionLayoutDigest }),
|
||||
...checkerField(),
|
||||
...(youngNurseryRequested() ? { youngNursery: true as const } : {}),
|
||||
...(nodeIslandOptionsKey(opts.nodeIslands) !== ""
|
||||
? { nodeIslands: nodeIslandOptionsKey(opts.nodeIslands) }
|
||||
: {}),
|
||||
...(heapCensusRequested() ? { heapCensus: true as const } : {}),
|
||||
npmStatic: opts.npmStatic ?? null,
|
||||
...(shapeOverlayEnabled() ? { shapeOverlay: true as const } : {}),
|
||||
|
||||
@@ -10,6 +10,17 @@ import type { IrLibFn } from "./builtin-signatures.js";
|
||||
export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
// The copied error of a synchronous call into Node (@scriptc/node).
|
||||
"nodeIsland.callSync",
|
||||
// Island module evaluation and member operations run JavaScript.
|
||||
"nodeIsland.require",
|
||||
"nodeIsland.get",
|
||||
"nodeIsland.getKey",
|
||||
"nodeIsland.set",
|
||||
"nodeIsland.setKey",
|
||||
"nodeIsland.invoke",
|
||||
"nodeIsland.apply",
|
||||
"nodeIsland.applyTo",
|
||||
"nodeIsland.construct",
|
||||
"nodeIsland.instanceof",
|
||||
// Node's argument, cipher-name, IV, tag-length and key-length errors.
|
||||
"crypto.cipherNew",
|
||||
// Node's curve, key, point, encoding and format errors.
|
||||
|
||||
@@ -185,6 +185,23 @@ function buildLibFnSignatures() {
|
||||
argTypes: [STRING, STRING, STRING, DYN],
|
||||
result: { kind: "promise", inner: DYN },
|
||||
},
|
||||
/** Node islands (scope-B NI-4, scr_node_island_handle.c): island values
|
||||
* are dyn values (`foreign` lowers as dyn), objects are handle boxes
|
||||
* whose identity is the V8 object's. require/import load an island
|
||||
* module by its absolute (or `$ORIGIN/`) path (also @scriptc/node
|
||||
* requireModule); the rest are member reads,
|
||||
* writes and calls on island values, argument lists as one dyn array. */
|
||||
"nodeIsland.require": { argTypes: [STRING], result: DYN },
|
||||
"nodeIsland.import": { argTypes: [STRING], result: { kind: "promise", inner: DYN } },
|
||||
"nodeIsland.get": { argTypes: [DYN, STRING], result: DYN },
|
||||
"nodeIsland.getKey": { argTypes: [DYN, DYN], result: DYN },
|
||||
"nodeIsland.set": { argTypes: [DYN, STRING, DYN], result: VOID },
|
||||
"nodeIsland.setKey": { argTypes: [DYN, DYN, DYN], result: VOID },
|
||||
"nodeIsland.invoke": { argTypes: [DYN, STRING, DYN], result: DYN },
|
||||
"nodeIsland.apply": { argTypes: [DYN, DYN], result: DYN },
|
||||
"nodeIsland.applyTo": { argTypes: [DYN, DYN, DYN], result: DYN },
|
||||
"nodeIsland.construct": { argTypes: [DYN, DYN], result: DYN },
|
||||
"nodeIsland.instanceof": { argTypes: [DYN, DYN], result: BOOL },
|
||||
/** Keyed WRITE on a dyn value — `h.onDone = cb` / `h["k"] = v` on a
|
||||
* checked-dynamic object (args: receiver, key string, value — all
|
||||
* borrowed; the runtime copies the key and retains the value in). An
|
||||
|
||||
@@ -618,6 +618,14 @@ export const CHILDWRITER_T: IrType = { kind: "childWriter" };
|
||||
export const PROCSTREAM_T: IrType = { kind: "procStream" };
|
||||
export const VOID: IrType = { kind: "void" };
|
||||
export const DYN: IrType = { kind: "dyn" };
|
||||
/** The `foreign` type (scope-B NI-4): a value owned by a Node island,
|
||||
* typed by the island package's declarations. Objects stay in V8 and cross
|
||||
* as handles (identity is the V8 object's); member operations compile from
|
||||
* the declarations into `nodeIsland.*` bridge calls. NI-4 represents it
|
||||
* exactly as `dyn` (a handle box with the island tag, primitives as plain
|
||||
* dyn values), so foreign -> any is the identity and the generic dyn paths
|
||||
* (equality, inspection, any slots) apply unchanged. */
|
||||
export const FOREIGN: IrType = DYN;
|
||||
export const JSVAL: IrType = { kind: "jsval" };
|
||||
export const CAUGHT: IrType = { kind: "caught" };
|
||||
export const UNDEFINED_T: IrType = { kind: "undefinedT" };
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/* The Node island boundary (DESIGN.md §2.4–§2.5, scope-B NI-4): which
|
||||
* values of a compiled program are island values, and what crosses by
|
||||
* copy. Rule (decision 2): a value crosses by reference unless copying it
|
||||
* is unobservable. Primitives and strings are copied; every object stays a
|
||||
* handle whose identity is the V8 object's.
|
||||
*
|
||||
* Island values are typed by the island package's own declarations. A
|
||||
* checker type is FOREIGN when it is an object type declared in an island
|
||||
* package (its .d.ts, or its @types twin): a class, interface, type alias
|
||||
* or type literal there, the type of an exported function or class value,
|
||||
* or an island module's namespace. Values of foreign types are held as
|
||||
* island handles (the `foreign` IR type, represented as a dyn value), and
|
||||
* member operations on them compile from the declarations into bridge
|
||||
* calls. A union is foreign when every arm that is not null or undefined
|
||||
* is foreign (the dyn representation holds null and undefined). */
|
||||
import * as ts from "../frontend/ts7/adapter.js";
|
||||
import { packageDirOf } from "./origin.js";
|
||||
|
||||
/** One package the build runs in a Node island. */
|
||||
export interface IslandPackage {
|
||||
name: string;
|
||||
version: string | null;
|
||||
/** The package directory (…/node_modules/<name>). */
|
||||
dir: string;
|
||||
/** The island class (runtime, object-model, gap, packaging) and the first
|
||||
* reason, for the SC8001 note. */
|
||||
class: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/** The program's island packages, by package name. */
|
||||
export interface NodeIslandSet {
|
||||
packages: ReadonlyMap<string, IslandPackage>;
|
||||
}
|
||||
|
||||
/** The island package that owns a declaration or JavaScript file, or null.
|
||||
* `@types/<name>` declarations type the package `<name>`. */
|
||||
export function islandPackageOfFile(islands: NodeIslandSet, file: string): IslandPackage | null {
|
||||
const at = packageDirOf(file);
|
||||
if (at === null) return null;
|
||||
let name = at.name;
|
||||
if (name.startsWith("@types/")) {
|
||||
const bare = name.slice("@types/".length);
|
||||
name = bare.includes("__") ? `@${bare.replace("__", "/")}` : bare;
|
||||
}
|
||||
return islands.packages.get(name) ?? null;
|
||||
}
|
||||
|
||||
function declaredInIsland(islands: NodeIslandSet, symbol: ts.Symbol | null | undefined): boolean {
|
||||
if (!symbol) return false;
|
||||
const decls = symbol.declarations;
|
||||
if (decls.length === 0) return false;
|
||||
return decls.every(
|
||||
(d) => islandPackageOfFile(islands, ts.getSourceFileOfNode(d).fileName) !== null,
|
||||
);
|
||||
}
|
||||
|
||||
const NULLISH = ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void;
|
||||
|
||||
/** Whether values of `type` are island handles (see the header). An
|
||||
* anonymous object type every property of which the island declares (the
|
||||
* namespace TypeScript synthesizes for `import()` of a CommonJS island
|
||||
* module) is foreign too. */
|
||||
export function isForeignType(
|
||||
islands: NodeIslandSet,
|
||||
type: ts.Type,
|
||||
checker?: ts.TypeChecker,
|
||||
): boolean {
|
||||
if (type.isUnionType()) {
|
||||
const arms = ts.constituentTypes(type).filter((t) => (t.flags & NULLISH) === 0);
|
||||
return arms.length > 0 && arms.every((t) => isForeignType(islands, t, checker));
|
||||
}
|
||||
// An intersection the island declares (execa's `ChildProcess &
|
||||
// ExecaChildPromise & Promise<…>`), or one with an island-declared arm,
|
||||
// is made by island code: its values are island objects.
|
||||
if ((type.flags & ts.TypeFlags.Intersection) !== 0) {
|
||||
if (declaredInIsland(islands, type.getAliasSymbol())) return true;
|
||||
return ts.constituentTypes(type).some((t) => isForeignType(islands, t, checker));
|
||||
}
|
||||
if ((type.flags & ts.TypeFlags.Object) === 0) return false;
|
||||
if (
|
||||
declaredInIsland(islands, type.getAliasSymbol()) ||
|
||||
declaredInIsland(islands, type.getSymbol())
|
||||
)
|
||||
return true;
|
||||
if (checker === undefined || type.getAliasSymbol()) return false;
|
||||
const symbol = type.getSymbol();
|
||||
if (symbol && symbol.declarations.length > 0) return false;
|
||||
// Synthetic members (the interop `default`) have no declarations.
|
||||
let declared = 0;
|
||||
for (const p of checker.getPropertiesOfType(type)) {
|
||||
if (p.declarations.length === 0) continue;
|
||||
if (!declaredInIsland(islands, p)) return false;
|
||||
declared++;
|
||||
}
|
||||
return declared > 0;
|
||||
}
|
||||
|
||||
/** How a value of a checker type crosses INTO an island as an argument or
|
||||
* an assigned value: copied (primitives, strings, null/undefined), passed
|
||||
* as its handle (foreign), passed as a dyn value the runtime converts
|
||||
* (any/unknown: a handle crosses as its object, a primitive is copied, a
|
||||
* fresh literal is copied), or not representable yet. */
|
||||
export type BoundaryCrossing = "copy" | "handle" | "dyn" | "refused";
|
||||
|
||||
const COPIED =
|
||||
ts.TypeFlags.NumberLike |
|
||||
ts.TypeFlags.StringLike |
|
||||
ts.TypeFlags.BooleanLike |
|
||||
ts.TypeFlags.BigIntLike |
|
||||
ts.TypeFlags.Null |
|
||||
ts.TypeFlags.Undefined |
|
||||
ts.TypeFlags.Void;
|
||||
|
||||
export function crossingOf(
|
||||
islands: NodeIslandSet,
|
||||
type: ts.Type,
|
||||
checker?: ts.TypeChecker,
|
||||
): BoundaryCrossing {
|
||||
if (type.isUnionType()) {
|
||||
const arms = ts.constituentTypes(type);
|
||||
if (arms.every((t) => (t.flags & COPIED) !== 0)) return "copy";
|
||||
if (isForeignType(islands, type, checker)) return "handle";
|
||||
return "refused";
|
||||
}
|
||||
if ((type.flags & COPIED) !== 0) return "copy";
|
||||
if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return "dyn";
|
||||
if (isForeignType(islands, type, checker)) return "handle";
|
||||
return "refused";
|
||||
}
|
||||
|
||||
/* The island set of the lowering in progress. lowerToIr sets it from its
|
||||
* options for the duration of one synchronous lowering; every Lowerer it
|
||||
* creates reads it at construction. Null in programs without islands, so
|
||||
* their lowering is unchanged. */
|
||||
let active: NodeIslandSet | null = null;
|
||||
|
||||
export function setActiveNodeIslands(islands: NodeIslandSet | null): void {
|
||||
active = islands !== null && islands.packages.size > 0 ? islands : null;
|
||||
}
|
||||
|
||||
export function activeNodeIslands(): NodeIslandSet | null {
|
||||
return active;
|
||||
}
|
||||
|
||||
/* The island package names of the frontend load in progress (runFrontend
|
||||
* sets them for one synchronous load): preflight lets their imports
|
||||
* through, typed by their declarations. */
|
||||
let frontendIslands: ReadonlySet<string> = new Set();
|
||||
|
||||
export function setFrontendIslands(names: ReadonlySet<string>): void {
|
||||
frontendIslands = names;
|
||||
}
|
||||
|
||||
export function isFrontendIsland(pkg: string): boolean {
|
||||
return frontendIslands.size > 0 && frontendIslands.has(pkg);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ const MESSAGE_REASONS: { code: string; pattern: RegExp; reason: string }[] = [
|
||||
pattern: /createRequire's require with this argument shape/,
|
||||
reason: "dynamic-require",
|
||||
},
|
||||
{ code: "SC2020", pattern: /native addon/, reason: "native-addon" },
|
||||
{ code: "SC2020", pattern: /^native addon /, reason: "native-addon" },
|
||||
{
|
||||
code: "SC2020",
|
||||
pattern: /through require\.cache|require\.extensions|Module\._|module\.register/,
|
||||
|
||||
@@ -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 = 4;
|
||||
export const NODE_BRIDGE_ABI = 5;
|
||||
|
||||
/** The prefix of a recorded path relative to the executable's directory. */
|
||||
export const ORIGIN = "$ORIGIN/";
|
||||
|
||||
@@ -30,6 +30,10 @@ describe("module specifiers in JavaScript", () => {
|
||||
export * from "./f";
|
||||
export { g } from "./g";
|
||||
const h = await import("./h.js");
|
||||
const require2 = createRequire(import.meta.url);
|
||||
const j = require2("./j.cjs");
|
||||
const k = require2.resolve("k");
|
||||
const i = import.meta.resolve("i");
|
||||
import.meta.url;
|
||||
obj.require("x");
|
||||
const t = \`\${require("./t")}\`;
|
||||
@@ -44,6 +48,9 @@ describe("module specifiers in JavaScript", () => {
|
||||
"./f",
|
||||
"./g",
|
||||
"./h.js",
|
||||
"./j.cjs",
|
||||
"k",
|
||||
"i",
|
||||
"./t",
|
||||
]);
|
||||
expect(scan.computed).toEqual([]);
|
||||
@@ -67,10 +74,19 @@ describe("module specifiers in JavaScript", () => {
|
||||
expect(forms("require(name)")).toEqual(["require() with a computed specifier"]);
|
||||
expect(forms("require('./a' + b)")).toEqual(["require() with a computed specifier"]);
|
||||
expect(forms("import(`./x/${y}`)")).toEqual(["import() with a computed specifier"]);
|
||||
expect(forms("import.meta.resolve(name)")).toEqual([
|
||||
"import.meta.resolve() with a computed specifier",
|
||||
]);
|
||||
expect(forms("const r = require; r('./a')")).toEqual(["require as a value"]);
|
||||
expect(forms("createRequire(import.meta.url)('./a')")).toEqual([
|
||||
"a require() from createRequire()",
|
||||
]);
|
||||
expect(forms("const req = createRequire(import.meta.url); req(name)")).toEqual([
|
||||
"require() with a computed specifier",
|
||||
]);
|
||||
expect(forms("const req = createRequire(import.meta.url); use(req)")).toEqual([
|
||||
"require as a value",
|
||||
]);
|
||||
expect(forms("function require(x) {} typeof require")).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -138,6 +154,36 @@ describe("the collected module tree", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("packages a package loads without declaring them ship too", () => {
|
||||
const root = project({
|
||||
"main.ts": "",
|
||||
"node_modules/bridge/package.json": JSON.stringify({
|
||||
type: "module",
|
||||
imports: { "#mapped": { node: "mapped", default: "./local.js" } },
|
||||
}),
|
||||
"node_modules/bridge/index.js":
|
||||
'import { a } from "zoo";\nexport const b = () => import("zoo/extra.js");',
|
||||
"node_modules/bridge/index.d.ts": 'import type { T } from "typesonly";',
|
||||
"node_modules/zoo/package.json": "{}",
|
||||
"node_modules/zoo/index.js": "exports.a = 1;",
|
||||
"node_modules/zoo/extra.js": "",
|
||||
"node_modules/typesonly/index.d.ts": "",
|
||||
"node_modules/mapped/index.js": "",
|
||||
});
|
||||
const collector = new NodeIslandModuleCollector();
|
||||
expect(collector.addLoaded(join(root, "node_modules/bridge/index.js"))).toBeNull();
|
||||
// The root stays above node_modules, where Node looks for packages.
|
||||
expect(collector.tree()!.files.map((f) => f.path)).toEqual([
|
||||
"node_modules/bridge/index.d.ts",
|
||||
"node_modules/bridge/index.js",
|
||||
"node_modules/bridge/package.json",
|
||||
"node_modules/mapped/index.js",
|
||||
"node_modules/zoo/extra.js",
|
||||
"node_modules/zoo/index.js",
|
||||
"node_modules/zoo/package.json",
|
||||
]);
|
||||
});
|
||||
|
||||
test("pnpm's links between package directories stay links inside the tree", () => {
|
||||
const root = project({
|
||||
"app/main.ts": "",
|
||||
|
||||
@@ -5,18 +5,25 @@
|
||||
* Each `call`/`callSync` names its module with a string literal. The build
|
||||
* resolves it from the calling module as Node's require() does:
|
||||
* - a relative or absolute specifier names a module of the program: the
|
||||
* module and every module its string-literal `require()`, `import` and
|
||||
* `export ... from` reach are collected; a computed specifier in such a
|
||||
* module is refused, since the build cannot know what it loads;
|
||||
* module and every module its string-literal `require()` (also through a
|
||||
* `createRequire()` function), `import` and `export ... from` reach are
|
||||
* collected; a computed specifier in such a module is refused, since the
|
||||
* build cannot know what it loads;
|
||||
* - a package specifier names a package: its whole directory is collected
|
||||
* (computed requires inside packages keep working), and so are its
|
||||
* `dependencies`, `optionalDependencies` and `peerDependencies`, found as
|
||||
* Node finds them;
|
||||
* `dependencies`, `optionalDependencies` and `peerDependencies`, the
|
||||
* packages its "imports" map to and the packages its JavaScript names
|
||||
* with string literals, all found as Node finds them;
|
||||
* - a Node built-in module needs nothing, and neither does a module the
|
||||
* build cannot find: loading it throws MODULE_NOT_FOUND when the call
|
||||
* runs, as under Node.
|
||||
* A load of a resolved file (`requireModule` and the compiler's own Node
|
||||
* islands, lower-node-island.ts) collects the package that holds the file
|
||||
* the same way, or, for a module of the program, the module and what it
|
||||
* loads; the load then names the file by its path in the tree.
|
||||
* Paths keep their layout relative to one root directory (the deepest
|
||||
* directory that holds every collected path), with symbolic links between
|
||||
* directory that holds every collected path, above any node_modules
|
||||
* directory, where Node looks for packages), with symbolic links between
|
||||
* package directories (pnpm's layout) kept as links. Only these relative
|
||||
* paths enter the executable. Every read goes through the frontend input
|
||||
* tracker, so an edit to an island module invalidates cached builds. */
|
||||
@@ -33,6 +40,7 @@ import {
|
||||
trackedRealpath,
|
||||
} from "../frontend/input-tracker.js";
|
||||
import type { IrNodeIslandModules } from "../ir/ir.js";
|
||||
import { packageDirOf } from "./origin.js";
|
||||
|
||||
export type NodeIslandModuleTree = IrNodeIslandModules;
|
||||
type NodeIslandModuleFile = IrNodeIslandModules["files"][number];
|
||||
@@ -46,6 +54,9 @@ const FILE_EXTENSIONS = [".js", ".json", ".node"];
|
||||
/** Module files whose imports are traced. */
|
||||
const TRACED = /\.(?:[cm]?js|[cm]?ts)$/;
|
||||
|
||||
/** A package's JavaScript files, whose imports of other packages are followed. */
|
||||
const SCRIPT = /\.[cm]?js$/;
|
||||
|
||||
function isBuiltin(specifier: string): boolean {
|
||||
return specifier.startsWith("node:") || BUILTINS.has(specifier);
|
||||
}
|
||||
@@ -138,6 +149,16 @@ function commonDirectory(paths: readonly string[]): string {
|
||||
return joined === "" ? sep : joined;
|
||||
}
|
||||
|
||||
/** The string targets of a package.json "imports" map, through its
|
||||
* conditions and fallback arrays. */
|
||||
function importTargets(value: unknown): string[] {
|
||||
if (typeof value === "string") return [value];
|
||||
if (Array.isArray(value)) return value.flatMap(importTargets);
|
||||
if (value !== null && typeof value === "object")
|
||||
return Object.values(value).flatMap(importTargets);
|
||||
return [];
|
||||
}
|
||||
|
||||
function portable(path: string): string {
|
||||
return sep === "/" ? path : path.split(sep).join("/");
|
||||
}
|
||||
@@ -151,6 +172,8 @@ export class NodeIslandModuleCollector {
|
||||
private readonly traced = new Set<string>();
|
||||
/** Real paths of the calling modules. */
|
||||
private readonly callers = new Set<string>();
|
||||
/** Real paths of the files loads name (addLoaded). */
|
||||
private readonly loaded = new Set<string>();
|
||||
|
||||
/** Collects the module `specifier` names from `caller` (a real path).
|
||||
* Returns null, or the reason the build cannot ship it. */
|
||||
@@ -159,6 +182,19 @@ export class NodeIslandModuleCollector {
|
||||
return this.resolveFrom(caller, specifier);
|
||||
}
|
||||
|
||||
/** Collects the module a load names by its resolved path (a real path):
|
||||
* the package that holds it, or a module of the program with what it
|
||||
* loads. Returns null, or the reason the build cannot ship it. */
|
||||
addLoaded(file: string): string | null {
|
||||
this.loaded.add(file);
|
||||
const at = packageDirOf(file);
|
||||
if (at !== null) {
|
||||
this.addPackage(at.dir);
|
||||
return null;
|
||||
}
|
||||
return this.addModule(this.placed(file));
|
||||
}
|
||||
|
||||
/** The collected tree, or null when no call loads a file. */
|
||||
tree(): NodeIslandModuleTree | null {
|
||||
if (this.files.size === 0 && this.links.size === 0) return null;
|
||||
@@ -167,8 +203,14 @@ export class NodeIslandModuleCollector {
|
||||
...this.links.keys(),
|
||||
...this.links.values(),
|
||||
...[...this.callers].map((caller) => dirname(caller)),
|
||||
...[...this.loaded].map((file) => dirname(file)),
|
||||
];
|
||||
const root = commonDirectory(anchors);
|
||||
// Node finds a package through the node_modules directory that holds
|
||||
// it, so a root inside one moves above it.
|
||||
let root = commonDirectory(anchors);
|
||||
const parts = root.split(sep);
|
||||
const outer = parts.indexOf("node_modules");
|
||||
if (outer >= 0) root = parts.slice(0, outer).join(sep) || sep;
|
||||
const files: NodeIslandModuleFile[] = [];
|
||||
for (const file of [...this.files].sort()) {
|
||||
const bytes = trackedReadBytes(file);
|
||||
@@ -193,6 +235,12 @@ export class NodeIslandModuleCollector {
|
||||
return tree === null ? basename(caller) : portable(relative(tree.root, caller));
|
||||
}
|
||||
|
||||
/** The name a load gives a collected file: `./` and its path relative to
|
||||
* the tree root (the runtime resolves it under the root). */
|
||||
static loadPath(tree: NodeIslandModuleTree | null, file: string): string {
|
||||
return tree === null ? file : `./${portable(relative(tree.root, file))}`;
|
||||
}
|
||||
|
||||
private resolveFrom(fromFile: string, specifier: string): string | null {
|
||||
if (isBuiltin(specifier)) return null;
|
||||
if (specifier.startsWith("#"))
|
||||
@@ -270,7 +318,21 @@ export class NodeIslandModuleCollector {
|
||||
const dir = this.placed(found);
|
||||
if (this.packages.has(dir)) return;
|
||||
this.packages.add(dir);
|
||||
this.addDirectory(dir, new Set());
|
||||
const scripts: string[] = [];
|
||||
this.addDirectory(dir, new Set(), scripts);
|
||||
// A package may load another it does not declare (one installed beside
|
||||
// it): Node finds that one by walking up node_modules, and so does the
|
||||
// build, from each string-literal specifier of the package's JavaScript.
|
||||
for (const file of scripts) {
|
||||
const text = trackedReadFile(file);
|
||||
if (text === null) continue;
|
||||
for (const specifier of scanModuleSpecifiers(text).specifiers) {
|
||||
if (isBuiltin(specifier) || isPathSpecifier(specifier)) continue;
|
||||
if (specifier.startsWith("#") || /^[a-z][a-z0-9+.-]*:/i.test(specifier)) continue;
|
||||
const other = packageDirectory(dirname(file), packageName(specifier));
|
||||
if (other !== null) this.addPackage(other);
|
||||
}
|
||||
}
|
||||
const manifest = trackedReadFile(join(dir, "package.json"));
|
||||
if (manifest === null) return;
|
||||
let parsed: Record<string, unknown>;
|
||||
@@ -285,6 +347,9 @@ export class NodeIslandModuleCollector {
|
||||
if (deps !== null && typeof deps === "object")
|
||||
for (const name of Object.keys(deps)) names.add(name);
|
||||
}
|
||||
// A package import ("imports") may map to another package.
|
||||
for (const target of importTargets(parsed["imports"]))
|
||||
if (!isPathSpecifier(target) && !isBuiltin(target)) names.add(packageName(target));
|
||||
for (const name of [...names].sort()) {
|
||||
// A dependency that is not installed (optional, or a peer the program
|
||||
// does not use) is not loaded either.
|
||||
@@ -294,17 +359,21 @@ export class NodeIslandModuleCollector {
|
||||
}
|
||||
|
||||
/** Every file below `dir`, except nested node_modules (dependencies come
|
||||
* through package.json) and .git. */
|
||||
private addDirectory(dir: string, visited: Set<string>): void {
|
||||
* through package.json and the package's imports) and .git; the
|
||||
* JavaScript files among them also go to `scripts`. */
|
||||
private addDirectory(dir: string, visited: Set<string>, scripts: string[]): void {
|
||||
const real = trackedRealpath(dir) ?? dir;
|
||||
if (visited.has(real)) return;
|
||||
visited.add(real);
|
||||
const entries = trackedAccessibleEntries(dir);
|
||||
if (entries === null) return;
|
||||
for (const file of entries.files) this.files.add(join(dir, file));
|
||||
for (const file of entries.files) {
|
||||
this.files.add(join(dir, file));
|
||||
if (SCRIPT.test(file)) scripts.push(join(dir, file));
|
||||
}
|
||||
for (const sub of entries.directories) {
|
||||
if (sub === "node_modules" || sub === ".git") continue;
|
||||
this.addDirectory(join(dir, sub), visited);
|
||||
this.addDirectory(join(dir, sub), visited, scripts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -490,8 +559,8 @@ export function moduleTokens(text: string): Token[] {
|
||||
}
|
||||
|
||||
export interface ModuleSpecifierScan {
|
||||
/** String-literal specifiers of require(), require.resolve(), import()
|
||||
* and import/export declarations, in source order. */
|
||||
/** String-literal specifiers of require(), require.resolve(), import(),
|
||||
* import.meta.resolve() and import/export declarations, in source order. */
|
||||
specifiers: string[];
|
||||
/** Loads whose module the source does not name with a string literal. */
|
||||
computed: { at: number; form: string }[];
|
||||
@@ -513,11 +582,27 @@ export function scanModuleSpecifiers(text: string): ModuleSpecifierScan {
|
||||
(is(k + 2, "punct", ")") || is(k + 2, "punct", ","))
|
||||
? at(k + 1)!.value
|
||||
: null;
|
||||
// `const r = createRequire(...)` declares a require function of the
|
||||
// module: calls of `r` load modules as require() calls do.
|
||||
const aliases = new Set<string>();
|
||||
const declarations = new Set<number>();
|
||||
for (let k = 0; k + 4 < tokens.length; k++) {
|
||||
if (
|
||||
(is(k, "name", "const") || is(k, "name", "let") || is(k, "name", "var")) &&
|
||||
is(k + 1, "name") &&
|
||||
is(k + 2, "punct", "=") &&
|
||||
is(k + 3, "name", "createRequire") &&
|
||||
is(k + 4, "punct", "(")
|
||||
) {
|
||||
aliases.add(at(k + 1)!.value);
|
||||
declarations.add(k + 1).add(k + 3);
|
||||
}
|
||||
}
|
||||
for (let k = 0; k < tokens.length; k++) {
|
||||
const t = tokens[k]!;
|
||||
if (t.kind !== "name") continue;
|
||||
if (t.kind !== "name" || declarations.has(k)) continue;
|
||||
const member = is(k - 1, "punct", ".") || is(k - 1, "punct", "?.");
|
||||
if (t.value === "require" && !member && !is(k + 1, "punct", ":")) {
|
||||
if ((t.value === "require" || aliases.has(t.value)) && !member && !is(k + 1, "punct", ":")) {
|
||||
if (is(k - 1, "name", "function") || is(k - 1, "name", "typeof")) continue;
|
||||
let call = k + 1;
|
||||
let form = "require()";
|
||||
@@ -541,7 +626,16 @@ export function scanModuleSpecifiers(text: string): ModuleSpecifierScan {
|
||||
continue;
|
||||
}
|
||||
if (t.value === "import" && !member) {
|
||||
if (is(k + 1, "punct", ".")) continue; // import.meta
|
||||
if (is(k + 1, "punct", ".")) {
|
||||
// import.meta.resolve() names a module, as require.resolve() does.
|
||||
if (is(k + 2, "name", "meta") && is(k + 3, "punct", ".") && is(k + 4, "name", "resolve")) {
|
||||
const literal = literalCall(k + 5);
|
||||
if (literal !== null) specifiers.push(literal);
|
||||
else if (is(k + 5, "punct", "("))
|
||||
computed.push({ at: t.at, form: "import.meta.resolve() with a computed specifier" });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (is(k + 1, "punct", "(")) {
|
||||
const literal = literalCall(k + 1);
|
||||
if (literal === null)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/** `require.resolve(spec)` from `fromFile` for island code (scope-B NI-4).
|
||||
* The Node-hosted compiler installs the resolver (index.ts, Node's own
|
||||
* createRequire); the self-hosted compiler builds no islands and never
|
||||
* installs one. Throws when the specifier does not resolve. */
|
||||
type IslandResolver = (fromFile: string, spec: string) => string;
|
||||
|
||||
let islandResolver: IslandResolver | null = null;
|
||||
|
||||
export function setIslandResolver(resolver: IslandResolver): void {
|
||||
islandResolver = resolver;
|
||||
}
|
||||
|
||||
export function resolveFrom(fromFile: string, spec: string): string {
|
||||
if (islandResolver === null) throw new Error(`no module resolver for '${spec}'`);
|
||||
return islandResolver(fromFile, spec);
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6812-node-island-handles/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6813-node-island-handle-errors/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6814-node-island-handle-bytes/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6815-node-island-handle-promises/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6816-node-island-objects/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6817-node-island-esm/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6839-node-island-path-mapped/main.ts"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC1010",
|
||||
"message": "the 'pathkit' import (it resolves only to type declarations — there is no runtime implementation to compile) is not supported yet",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/corpus/6839-node-island-path-mapped/main.ts",
|
||||
"start": 234,
|
||||
"end": 294
|
||||
},
|
||||
"milestone": "M4",
|
||||
"hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <node.h>
|
||||
#include <node_buffer.h>
|
||||
#include <node_api.h>
|
||||
#include <node_version.h>
|
||||
#include <uv.h>
|
||||
@@ -32,6 +33,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -55,6 +57,7 @@ using v8::TryCatch;
|
||||
using v8::Value;
|
||||
|
||||
const ScrNodeBridgeHost *g_host = nullptr;
|
||||
void DropAllHandles();
|
||||
std::unique_ptr<node::MultiIsolatePlatform> g_platform;
|
||||
node::EmbedderSnapshotData::Pointer g_snapshot;
|
||||
std::unique_ptr<node::CommonEnvironmentSetup> g_setup;
|
||||
@@ -64,6 +67,7 @@ Global<Function> g_call;
|
||||
Global<Function> g_set_timer, g_clear_timer, g_ref_timer, g_refresh_timer;
|
||||
Global<Function> g_set_immediate, g_clear_immediate, g_ref_immediate;
|
||||
Global<Function> g_next_tick, g_emit;
|
||||
Global<Function> g_load; // the bootstrap's module loader (NI-4)
|
||||
|
||||
char *Copy(const std::string &text, size_t *len) {
|
||||
char *out = static_cast<char *>(std::malloc(text.size() + 1));
|
||||
@@ -282,7 +286,8 @@ char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *ar
|
||||
!TakeFunction(isolate, context, d, "clearImmediate", &g_clear_immediate) ||
|
||||
!TakeFunction(isolate, context, d, "refImmediate", &g_ref_immediate) ||
|
||||
!TakeFunction(isolate, context, d, "nextTick", &g_next_tick) ||
|
||||
!TakeFunction(isolate, context, d, "emit", &g_emit))
|
||||
!TakeFunction(isolate, context, d, "emit", &g_emit) ||
|
||||
!TakeFunction(isolate, context, d, "load", &g_load))
|
||||
return Copy("the Node island bootstrap left an incomplete dispatcher", nullptr);
|
||||
global->Delete(context, Utf8(isolate, "__scriptcNodeIsland")).FromMaybe(false);
|
||||
global->Delete(context, Utf8(isolate, "__scriptcNodeIslandHost")).FromMaybe(false);
|
||||
@@ -614,6 +619,8 @@ void Stop() {
|
||||
Isolate *isolate = g_setup->isolate();
|
||||
node::Stop(g_setup->env());
|
||||
g_call.Reset();
|
||||
g_load.Reset();
|
||||
DropAllHandles();
|
||||
for (Global<Function> *fn :
|
||||
{&g_set_timer, &g_clear_timer, &g_ref_timer, &g_refresh_timer, &g_set_immediate,
|
||||
&g_clear_immediate, &g_ref_immediate, &g_next_tick, &g_emit})
|
||||
@@ -627,6 +634,594 @@ void Stop() {
|
||||
node::TearDownOncePerProcess();
|
||||
}
|
||||
|
||||
|
||||
// ── object handles (scope-B NI-4) ───────────────────────────────────────
|
||||
// The import table: V8 values the runtime holds, one v8::Global per value
|
||||
// and one id per value while held (found again through the value's
|
||||
// identity hash), so a V8 object crossing twice is the same runtime
|
||||
// handle. A typed array's slot also keeps its backing store, so memory the
|
||||
// runtime aliases stays valid even if JavaScript detaches the buffer.
|
||||
|
||||
struct Slot {
|
||||
Global<Value> value;
|
||||
std::shared_ptr<v8::BackingStore> store;
|
||||
int hash = 0;
|
||||
bool live = false;
|
||||
};
|
||||
|
||||
std::vector<Slot> g_slots(1); // id 0 is never used
|
||||
std::vector<uint32_t> g_free_ids;
|
||||
std::unordered_multimap<int, uint32_t> g_ids;
|
||||
uint32_t g_live_handles = 0;
|
||||
// Strings of out-values: valid until the next outermost entry.
|
||||
std::deque<std::string> g_scratch;
|
||||
int g_entry_depth = 0;
|
||||
|
||||
int IdentityHash(Local<Value> v) {
|
||||
if (v->IsObject()) return v.As<Object>()->GetIdentityHash();
|
||||
if (v->IsSymbol()) return v.As<v8::Name>()->GetIdentityHash();
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t Import(Isolate *isolate, Local<Value> v, std::shared_ptr<v8::BackingStore> store = nullptr) {
|
||||
int hash = IdentityHash(v);
|
||||
auto range = g_ids.equal_range(hash);
|
||||
for (auto it = range.first; it != range.second; ++it) {
|
||||
Slot &slot = g_slots[it->second];
|
||||
if (slot.value.Get(isolate)->StrictEquals(v)) return it->second;
|
||||
}
|
||||
uint32_t id;
|
||||
if (!g_free_ids.empty()) {
|
||||
id = g_free_ids.back();
|
||||
g_free_ids.pop_back();
|
||||
} else {
|
||||
id = static_cast<uint32_t>(g_slots.size());
|
||||
g_slots.emplace_back();
|
||||
}
|
||||
Slot &slot = g_slots[id];
|
||||
slot.value.Reset(isolate, v);
|
||||
slot.store = std::move(store);
|
||||
slot.hash = hash;
|
||||
slot.live = true;
|
||||
g_ids.emplace(hash, id);
|
||||
g_live_handles++;
|
||||
return id;
|
||||
}
|
||||
|
||||
Local<Value> HandleValue(Isolate *isolate, uint32_t id) {
|
||||
if (id == 0 || id >= g_slots.size() || !g_slots[id].live) return v8::Undefined(isolate);
|
||||
return g_slots[id].value.Get(isolate);
|
||||
}
|
||||
|
||||
void Drop(uint32_t id) {
|
||||
if (id == 0 || id >= g_slots.size() || !g_slots[id].live) return;
|
||||
Slot &slot = g_slots[id];
|
||||
auto range = g_ids.equal_range(slot.hash);
|
||||
for (auto it = range.first; it != range.second; ++it) {
|
||||
if (it->second == id) {
|
||||
g_ids.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
slot.value.Reset();
|
||||
slot.store.reset();
|
||||
slot.live = false;
|
||||
g_free_ids.push_back(id);
|
||||
g_live_handles--;
|
||||
}
|
||||
|
||||
void DropAllHandles() {
|
||||
for (uint32_t id = 1; id < g_slots.size(); id++) Drop(id);
|
||||
}
|
||||
|
||||
uint32_t LiveHandles() { return g_live_handles; }
|
||||
|
||||
const char *Scratch(std::string text, size_t *len) {
|
||||
g_scratch.push_back(std::move(text));
|
||||
*len = g_scratch.back().size();
|
||||
return g_scratch.back().data();
|
||||
}
|
||||
|
||||
// WTF-8: V8's UTF-8 writer encodes a lone surrogate as its 3-byte form.
|
||||
std::string Wtf8(Isolate *isolate, Local<String> s) {
|
||||
size_t n = s->Utf8LengthV2(isolate);
|
||||
std::string out(n, '\0');
|
||||
if (n > 0) s->WriteUtf8V2(isolate, out.data(), n);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool HasLoneSurrogate(const char *p, size_t n) {
|
||||
for (size_t i = 0; i + 1 < n; i++)
|
||||
if (static_cast<unsigned char>(p[i]) == 0xED && static_cast<unsigned char>(p[i + 1]) >= 0xA0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// A runtime string in V8: NewFromUtf8 would replace lone surrogates, so a
|
||||
// string that holds one decodes WTF-8 to UTF-16 here.
|
||||
Local<String> FromWtf8(Isolate *isolate, const char *p, size_t n) {
|
||||
if (!HasLoneSurrogate(p, n)) return Utf8(isolate, p, n);
|
||||
std::vector<uint16_t> units;
|
||||
units.reserve(n);
|
||||
for (size_t i = 0; i < n;) {
|
||||
unsigned char c = static_cast<unsigned char>(p[i]);
|
||||
uint32_t cp;
|
||||
size_t k;
|
||||
if (c < 0x80) { cp = c; k = 1; }
|
||||
else if ((c & 0xE0) == 0xC0 && i + 1 < n) { cp = ((c & 0x1F) << 6) | (p[i + 1] & 0x3F); k = 2; }
|
||||
else if ((c & 0xF0) == 0xE0 && i + 2 < n) {
|
||||
cp = ((c & 0x0F) << 12) | ((p[i + 1] & 0x3F) << 6) | (p[i + 2] & 0x3F); k = 3;
|
||||
} else if ((c & 0xF8) == 0xF0 && i + 3 < n) {
|
||||
cp = ((c & 0x07) << 18) | ((p[i + 1] & 0x3F) << 12) | ((p[i + 2] & 0x3F) << 6) | (p[i + 3] & 0x3F);
|
||||
k = 4;
|
||||
} else { cp = 0xFFFD; k = 1; }
|
||||
if (cp >= 0x10000) {
|
||||
cp -= 0x10000;
|
||||
units.push_back(static_cast<uint16_t>(0xD800 + (cp >> 10)));
|
||||
units.push_back(static_cast<uint16_t>(0xDC00 + (cp & 0x3FF)));
|
||||
} else {
|
||||
units.push_back(static_cast<uint16_t>(cp));
|
||||
}
|
||||
i += k;
|
||||
}
|
||||
return String::NewFromTwoByte(isolate, units.data(), NewStringType::kNormal,
|
||||
static_cast<int>(units.size()))
|
||||
.ToLocalChecked();
|
||||
}
|
||||
|
||||
// A decimal BigInt text in V8, through 64-bit words.
|
||||
Local<Value> BigIntFromText(Local<Context> context, const char *p, size_t n) {
|
||||
int sign = 0;
|
||||
size_t i = 0;
|
||||
if (i < n && p[i] == '-') { sign = 1; i++; }
|
||||
std::vector<uint64_t> words(1, 0);
|
||||
for (; i < n; i++) {
|
||||
unsigned __int128 carry = static_cast<unsigned>(p[i] - '0');
|
||||
for (uint64_t &w : words) {
|
||||
unsigned __int128 v = static_cast<unsigned __int128>(w) * 10 + carry;
|
||||
w = static_cast<uint64_t>(v);
|
||||
carry = v >> 64;
|
||||
}
|
||||
if (carry != 0) words.push_back(static_cast<uint64_t>(carry));
|
||||
}
|
||||
return v8::BigInt::NewFromWords(context, sign, static_cast<int>(words.size()), words.data())
|
||||
.ToLocalChecked();
|
||||
}
|
||||
|
||||
uint32_t ElemOf(Local<v8::ArrayBufferView> view) {
|
||||
// ScrBytesElem numbering (scr_bytes.h).
|
||||
if (view->IsUint8Array()) return 0;
|
||||
if (view->IsUint32Array()) return 1;
|
||||
if (view->IsFloat32Array()) return 2;
|
||||
if (view->IsInt32Array()) return 3;
|
||||
if (view->IsFloat64Array()) return 4;
|
||||
if (view->IsInt8Array()) return 5;
|
||||
if (view->IsUint16Array()) return 6;
|
||||
if (view->IsInt16Array()) return 7;
|
||||
if (view->IsUint8ClampedArray()) return 8;
|
||||
if (view->IsFloat16Array()) return 9;
|
||||
return 0; // DataView: bytes
|
||||
}
|
||||
|
||||
size_t ElemSize(uint32_t elem) {
|
||||
static const size_t sizes[] = {1, 4, 4, 4, 8, 1, 2, 2, 1, 2};
|
||||
return elem < 10 ? sizes[elem] : 1;
|
||||
}
|
||||
|
||||
void FromV8(Isolate *isolate, Local<Context> context, Local<Value> v, ScrNodeValue *out) {
|
||||
std::memset(out, 0, sizeof *out);
|
||||
if (v.IsEmpty() || v->IsUndefined()) { out->tag = SCR_NV_UNDEFINED; return; }
|
||||
if (v->IsNull()) { out->tag = SCR_NV_NULL; return; }
|
||||
if (v->IsBoolean()) { out->tag = SCR_NV_BOOL; out->u.boolean = v->IsTrue() ? 1 : 0; return; }
|
||||
if (v->IsNumber()) { out->tag = SCR_NV_NUMBER; out->u.num = v.As<v8::Number>()->Value(); return; }
|
||||
if (v->IsString()) {
|
||||
out->tag = SCR_NV_STRING;
|
||||
out->u.str.data = Scratch(Wtf8(isolate, v.As<String>()), &out->u.str.len);
|
||||
return;
|
||||
}
|
||||
if (v->IsBigInt()) {
|
||||
out->tag = SCR_NV_BIGINT;
|
||||
Local<String> text = v->ToString(context).ToLocalChecked();
|
||||
out->u.str.data = Scratch(Wtf8(isolate, text), &out->u.str.len);
|
||||
return;
|
||||
}
|
||||
if (v->IsArrayBufferView()) {
|
||||
Local<v8::ArrayBufferView> view = v.As<v8::ArrayBufferView>();
|
||||
bool dataview = view->IsDataView();
|
||||
// Buffer() moves on-heap elements off the heap: the data stays put.
|
||||
Local<v8::ArrayBuffer> buffer = view->Buffer();
|
||||
std::shared_ptr<v8::BackingStore> store = buffer->GetBackingStore();
|
||||
if (!store->IsShared() && !store->IsResizableByUserJavaScript()) {
|
||||
uint32_t elem = dataview ? 0 : ElemOf(view);
|
||||
out->tag = SCR_NV_BYTES;
|
||||
out->u.bytes.elem = elem;
|
||||
out->u.bytes.data = static_cast<uint8_t *>(store->Data()) + view->ByteOffset();
|
||||
out->u.bytes.len = view->ByteLength() / ElemSize(elem);
|
||||
if (node::Buffer::HasInstance(v)) out->flags |= SCR_NVF_BUFFER;
|
||||
if (dataview) out->flags |= SCR_NVF_DATAVIEW;
|
||||
out->u.bytes.handle = Import(isolate, v, store);
|
||||
return;
|
||||
}
|
||||
}
|
||||
out->tag = SCR_NV_HANDLE;
|
||||
uint32_t flags = 0;
|
||||
if (v->IsSymbol()) flags |= SCR_NVF_SYMBOL;
|
||||
if (v->IsFunction()) {
|
||||
flags |= SCR_NVF_FUNCTION;
|
||||
if (v.As<Function>()->IsConstructor()) flags |= SCR_NVF_CONSTRUCTOR;
|
||||
}
|
||||
if (v->IsNativeError()) flags |= SCR_NVF_ERROR;
|
||||
if (v->IsArray()) flags |= SCR_NVF_ARRAY;
|
||||
if (v->IsPromise()) flags |= SCR_NVF_PROMISE;
|
||||
out->flags = flags;
|
||||
out->u.handle = Import(isolate, v);
|
||||
}
|
||||
|
||||
void FreeRuntimeBytes(void *, size_t, void *owner) {
|
||||
if (owner != nullptr) g_host->bytes_free(owner);
|
||||
}
|
||||
|
||||
Local<Value> ErrorConstructor(Isolate *isolate, Local<Context> context, uint32_t kind) {
|
||||
static const char *const names[] = {"Error", "TypeError", "RangeError", "SyntaxError",
|
||||
"Error", "ReferenceError", "EvalError",
|
||||
"URIError"};
|
||||
const char *name = kind < 8 ? names[kind] : "Error";
|
||||
return context->Global()->Get(context, Utf8(isolate, name)).ToLocalChecked();
|
||||
}
|
||||
|
||||
Local<Value> ToV8(Isolate *isolate, Local<Context> context, const ScrNodeValue *in) {
|
||||
if (in == nullptr) return v8::Undefined(isolate);
|
||||
switch (in->tag) {
|
||||
case SCR_NV_NULL: return v8::Null(isolate);
|
||||
case SCR_NV_BOOL: return v8::Boolean::New(isolate, in->u.boolean != 0);
|
||||
case SCR_NV_NUMBER: return v8::Number::New(isolate, in->u.num);
|
||||
case SCR_NV_STRING: return FromWtf8(isolate, in->u.str.data, in->u.str.len);
|
||||
case SCR_NV_BIGINT: return BigIntFromText(context, in->u.str.data, in->u.str.len);
|
||||
case SCR_NV_HANDLE: return HandleValue(isolate, in->u.handle);
|
||||
case SCR_NV_BYTES: {
|
||||
if (in->u.bytes.handle != 0) return HandleValue(isolate, in->u.bytes.handle);
|
||||
size_t elem_size = ElemSize(in->u.bytes.elem);
|
||||
size_t byte_len = in->u.bytes.len * elem_size;
|
||||
Local<v8::ArrayBuffer> buffer;
|
||||
if (byte_len == 0 || in->u.bytes.data == nullptr) {
|
||||
buffer = v8::ArrayBuffer::New(isolate, 0);
|
||||
if (in->u.bytes.owner != nullptr) g_host->bytes_free(in->u.bytes.owner);
|
||||
} else {
|
||||
std::unique_ptr<v8::BackingStore> store = v8::ArrayBuffer::NewBackingStore(
|
||||
in->u.bytes.data, byte_len, FreeRuntimeBytes, in->u.bytes.owner);
|
||||
buffer = v8::ArrayBuffer::New(isolate, std::move(store));
|
||||
}
|
||||
size_t n = in->u.bytes.len;
|
||||
if (in->flags & SCR_NVF_DATAVIEW) return v8::DataView::New(buffer, 0, byte_len);
|
||||
if (in->flags & SCR_NVF_BUFFER)
|
||||
return node::Buffer::New(isolate, buffer, 0, byte_len).ToLocalChecked();
|
||||
switch (in->u.bytes.elem) {
|
||||
case 1: return v8::Uint32Array::New(buffer, 0, n);
|
||||
case 2: return v8::Float32Array::New(buffer, 0, n);
|
||||
case 3: return v8::Int32Array::New(buffer, 0, n);
|
||||
case 4: return v8::Float64Array::New(buffer, 0, n);
|
||||
case 5: return v8::Int8Array::New(buffer, 0, n);
|
||||
case 6: return v8::Uint16Array::New(buffer, 0, n);
|
||||
case 7: return v8::Int16Array::New(buffer, 0, n);
|
||||
case 8: return v8::Uint8ClampedArray::New(buffer, 0, n);
|
||||
case 9: return v8::Float16Array::New(buffer, 0, n);
|
||||
default: return v8::Uint8Array::New(buffer, 0, n);
|
||||
}
|
||||
}
|
||||
case SCR_NV_ERROR: {
|
||||
Local<Value> ctor = ErrorConstructor(isolate, context, in->u.error.kind);
|
||||
Local<Value> argv[] = {FromWtf8(isolate, in->u.error.message, in->u.error.message_len)};
|
||||
Local<Object> error = ctor.As<Function>()->NewInstance(context, 1, argv).ToLocalChecked();
|
||||
std::string name(in->u.error.name, in->u.error.name_len);
|
||||
Local<Value> default_name;
|
||||
if (!error->Get(context, Utf8(isolate, "name")).ToLocal(&default_name) ||
|
||||
ToStdString(isolate, default_name) != name)
|
||||
error->Set(context, Utf8(isolate, "name"), FromWtf8(isolate, name.data(), name.size())).Check();
|
||||
if (in->u.error.stack != nullptr)
|
||||
error->Set(context, Utf8(isolate, "stack"),
|
||||
FromWtf8(isolate, in->u.error.stack, in->u.error.stack_len)).Check();
|
||||
if (in->u.error.code != nullptr)
|
||||
error->Set(context, Utf8(isolate, "code"),
|
||||
FromWtf8(isolate, in->u.error.code, in->u.error.code_len)).Check();
|
||||
return error;
|
||||
}
|
||||
case SCR_NV_JSON: {
|
||||
Local<Value> parsed;
|
||||
if (v8::JSON::Parse(context, FromWtf8(isolate, in->u.str.data, in->u.str.len)).ToLocal(&parsed))
|
||||
return parsed;
|
||||
return v8::Undefined(isolate);
|
||||
}
|
||||
default: return v8::Undefined(isolate);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
Enter e;
|
||||
TryCatch try_catch;
|
||||
Op() : try_catch(e.isolate) {
|
||||
if (g_entry_depth++ == 0) g_scratch.clear();
|
||||
}
|
||||
~Op() { g_entry_depth--; }
|
||||
// The operation's outcome: 0 and the result, or 1 and the thrown value.
|
||||
int Finish(Local<Value> result, bool ok, ScrNodeValue *out) {
|
||||
if (ok) {
|
||||
FromV8(e.isolate, e.context, result, out);
|
||||
return 0;
|
||||
}
|
||||
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
|
||||
Local<Value> exception = try_catch.Exception();
|
||||
try_catch.Reset();
|
||||
FromV8(e.isolate, e.context, exception, out);
|
||||
} else {
|
||||
std::memset(out, 0, sizeof *out);
|
||||
out->tag = SCR_NV_UNDEFINED;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
Local<Value> V(const ScrNodeValue *in) { return ToV8(e.isolate, e.context, in); }
|
||||
};
|
||||
|
||||
int Load(const char *path, int esm, ScrNodeValue *out) {
|
||||
Op op;
|
||||
Local<Value> argv[] = {Utf8(op.e.isolate, path), v8::Boolean::New(op.e.isolate, esm != 0)};
|
||||
Local<Value> result;
|
||||
bool ok = g_load.Get(op.e.isolate)
|
||||
->Call(op.e.context, v8::Undefined(op.e.isolate), 2, argv)
|
||||
.ToLocal(&result);
|
||||
return op.Finish(result, ok, out);
|
||||
}
|
||||
|
||||
// The target of a property operation: an object, or a primitive whose
|
||||
// wrapper answers (ToObject throws for undefined and null).
|
||||
bool Target(Op &op, uint32_t h, Local<Object> *target) {
|
||||
return HandleValue(op.e.isolate, h)->ToObject(op.e.context).ToLocal(target);
|
||||
}
|
||||
|
||||
bool Key(Op &op, const ScrNodeValue *key, Local<Value> *out) {
|
||||
Local<Value> k = op.V(key);
|
||||
if (k->IsString() || k->IsSymbol() || k->IsNumber()) {
|
||||
*out = k;
|
||||
return true;
|
||||
}
|
||||
Local<String> name;
|
||||
if (!k->ToString(op.e.context).ToLocal(&name)) return false;
|
||||
*out = name;
|
||||
return true;
|
||||
}
|
||||
|
||||
int Get(uint32_t h, const ScrNodeValue *key, ScrNodeValue *out) {
|
||||
Op op;
|
||||
Local<Object> target;
|
||||
Local<Value> k, result;
|
||||
bool ok = Target(op, h, &target) && Key(op, key, &k) && target->Get(op.e.context, k).ToLocal(&result);
|
||||
return op.Finish(result, ok, out);
|
||||
}
|
||||
|
||||
int SetProp(uint32_t h, const ScrNodeValue *key, const ScrNodeValue *value, ScrNodeValue *out) {
|
||||
Op op;
|
||||
Local<Object> target;
|
||||
Local<Value> k;
|
||||
bool ok = Target(op, h, &target) && Key(op, key, &k);
|
||||
if (ok) {
|
||||
v8::Maybe<bool> done = target->Set(op.e.context, k, op.V(value));
|
||||
ok = done.IsJust();
|
||||
// Strict-mode semantics: a refused assignment throws, as in a module.
|
||||
if (ok && !done.FromJust()) {
|
||||
op.e.isolate->ThrowException(v8::Exception::TypeError(
|
||||
Utf8(op.e.isolate, "Cannot assign to read only property")));
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
return op.Finish(v8::Undefined(op.e.isolate), ok, out);
|
||||
}
|
||||
|
||||
int Has(uint32_t h, const ScrNodeValue *key, ScrNodeValue *out) {
|
||||
Op op;
|
||||
Local<Value> target = HandleValue(op.e.isolate, h);
|
||||
if (!target->IsObject()) {
|
||||
op.e.isolate->ThrowException(v8::Exception::TypeError(
|
||||
Utf8(op.e.isolate, "Cannot use 'in' operator to search for a key in a primitive")));
|
||||
return op.Finish(Local<Value>(), false, out);
|
||||
}
|
||||
Local<Value> k;
|
||||
bool ok = Key(op, key, &k);
|
||||
v8::Maybe<bool> found = ok ? target.As<Object>()->Has(op.e.context, k) : v8::Nothing<bool>();
|
||||
ok = found.IsJust();
|
||||
return op.Finish(v8::Boolean::New(op.e.isolate, ok && found.FromJust()), ok, out);
|
||||
}
|
||||
|
||||
int Del(uint32_t h, const ScrNodeValue *key, ScrNodeValue *out) {
|
||||
Op op;
|
||||
Local<Object> target;
|
||||
Local<Value> k;
|
||||
bool ok = Target(op, h, &target) && Key(op, key, &k);
|
||||
v8::Maybe<bool> done = ok ? target->Delete(op.e.context, k) : v8::Nothing<bool>();
|
||||
ok = done.IsJust();
|
||||
if (ok && !done.FromJust()) {
|
||||
op.e.isolate->ThrowException(v8::Exception::TypeError(
|
||||
Utf8(op.e.isolate, "Cannot delete property")));
|
||||
ok = false;
|
||||
}
|
||||
return op.Finish(v8::Boolean::New(op.e.isolate, ok), ok, out);
|
||||
}
|
||||
|
||||
std::vector<Local<Value>> Args(Op &op, const ScrNodeValue *args, size_t argc) {
|
||||
std::vector<Local<Value>> argv;
|
||||
argv.reserve(argc);
|
||||
for (size_t i = 0; i < argc; i++) argv.push_back(op.V(&args[i]));
|
||||
return argv;
|
||||
}
|
||||
|
||||
void NotAFunction(Op &op, const char *what) {
|
||||
op.e.isolate->ThrowException(v8::Exception::TypeError(
|
||||
Utf8(op.e.isolate, (std::string(what) + " is not a function").c_str())));
|
||||
}
|
||||
|
||||
int Invoke(uint32_t fn, const ScrNodeValue *this_, const ScrNodeValue *args, size_t argc,
|
||||
ScrNodeValue *out) {
|
||||
Op op;
|
||||
Local<Value> callee = HandleValue(op.e.isolate, fn);
|
||||
if (!callee->IsFunction()) {
|
||||
NotAFunction(op, "value");
|
||||
return op.Finish(Local<Value>(), false, out);
|
||||
}
|
||||
std::vector<Local<Value>> argv = Args(op, args, argc);
|
||||
Local<Value> result;
|
||||
bool ok = callee.As<Function>()
|
||||
->Call(op.e.context, op.V(this_), static_cast<int>(argv.size()), argv.data())
|
||||
.ToLocal(&result);
|
||||
return op.Finish(result, ok, out);
|
||||
}
|
||||
|
||||
int InvokeMethod(uint32_t h, const ScrNodeValue *key, const ScrNodeValue *args, size_t argc,
|
||||
ScrNodeValue *out) {
|
||||
Op op;
|
||||
Local<Value> receiver = HandleValue(op.e.isolate, h);
|
||||
Local<Object> target;
|
||||
Local<Value> k, callee;
|
||||
if (!receiver->ToObject(op.e.context).ToLocal(&target) || !Key(op, key, &k) ||
|
||||
!target->Get(op.e.context, k).ToLocal(&callee))
|
||||
return op.Finish(Local<Value>(), false, out);
|
||||
if (!callee->IsFunction()) {
|
||||
std::string what = "property";
|
||||
if (k->IsString()) what = ToStdString(op.e.isolate, k);
|
||||
NotAFunction(op, what.c_str());
|
||||
return op.Finish(Local<Value>(), false, out);
|
||||
}
|
||||
std::vector<Local<Value>> argv = Args(op, args, argc);
|
||||
Local<Value> result;
|
||||
bool ok = callee.As<Function>()
|
||||
->Call(op.e.context, receiver, static_cast<int>(argv.size()), argv.data())
|
||||
.ToLocal(&result);
|
||||
return op.Finish(result, ok, out);
|
||||
}
|
||||
|
||||
int Construct(uint32_t h, const ScrNodeValue *args, size_t argc, ScrNodeValue *out) {
|
||||
Op op;
|
||||
Local<Value> callee = HandleValue(op.e.isolate, h);
|
||||
if (!callee->IsFunction() || !callee.As<Function>()->IsConstructor()) {
|
||||
op.e.isolate->ThrowException(v8::Exception::TypeError(
|
||||
Utf8(op.e.isolate, "value is not a constructor")));
|
||||
return op.Finish(Local<Value>(), false, out);
|
||||
}
|
||||
std::vector<Local<Value>> argv = Args(op, args, argc);
|
||||
Local<Object> result;
|
||||
bool ok = callee.As<Function>()
|
||||
->NewInstance(op.e.context, static_cast<int>(argv.size()), argv.data())
|
||||
.ToLocal(&result);
|
||||
return op.Finish(result, ok, out);
|
||||
}
|
||||
|
||||
int InstanceOf(const ScrNodeValue *value, uint32_t ctor, ScrNodeValue *out) {
|
||||
Op op;
|
||||
Local<Value> v = op.V(value);
|
||||
Local<Value> c = HandleValue(op.e.isolate, ctor);
|
||||
if (!c->IsObject()) {
|
||||
op.e.isolate->ThrowException(v8::Exception::TypeError(
|
||||
Utf8(op.e.isolate, "Right-hand side of 'instanceof' is not callable")));
|
||||
return op.Finish(Local<Value>(), false, out);
|
||||
}
|
||||
v8::Maybe<bool> is = v->InstanceOf(op.e.context, c.As<Object>());
|
||||
bool ok = is.IsJust();
|
||||
return op.Finish(v8::Boolean::New(op.e.isolate, ok && is.FromJust()), ok, out);
|
||||
}
|
||||
|
||||
int OwnKeys(uint32_t h, int mode, ScrNodeValue *out) {
|
||||
Op op;
|
||||
Local<Object> target;
|
||||
Local<v8::Array> keys;
|
||||
bool ok = Target(op, h, &target) &&
|
||||
target
|
||||
->GetOwnPropertyNames(
|
||||
op.e.context,
|
||||
mode == 0 ? static_cast<v8::PropertyFilter>(v8::ONLY_ENUMERABLE | v8::SKIP_SYMBOLS)
|
||||
: v8::ALL_PROPERTIES,
|
||||
v8::KeyConversionMode::kConvertToString)
|
||||
.ToLocal(&keys);
|
||||
return op.Finish(keys, ok, out);
|
||||
}
|
||||
|
||||
void OnSettled(const FunctionCallbackInfo<Value> &info, int rejected) {
|
||||
Isolate *isolate = info.GetIsolate();
|
||||
Local<Context> context = isolate->GetCurrentContext();
|
||||
uint32_t token = info.Data()->Uint32Value(context).FromMaybe(0);
|
||||
ScrNodeValue value;
|
||||
FromV8(isolate, context, info[0], &value);
|
||||
g_host->settle_value(token, rejected, &value);
|
||||
}
|
||||
|
||||
void OnFulfilled(const FunctionCallbackInfo<Value> &info) { OnSettled(info, 0); }
|
||||
void OnRejected(const FunctionCallbackInfo<Value> &info) { OnSettled(info, 1); }
|
||||
|
||||
// await of a value: PromiseResolve (a native promise is itself) and one
|
||||
// reaction, as `await` does in JavaScript.
|
||||
int Then(const ScrNodeValue *value, uint32_t token, ScrNodeValue *out) {
|
||||
Op op;
|
||||
Local<Value> v = op.V(value);
|
||||
Local<v8::Promise> promise;
|
||||
if (v->IsPromise()) {
|
||||
promise = v.As<v8::Promise>();
|
||||
} else {
|
||||
Local<v8::Promise::Resolver> resolver;
|
||||
if (!v8::Promise::Resolver::New(op.e.context).ToLocal(&resolver) ||
|
||||
resolver->Resolve(op.e.context, v).IsNothing())
|
||||
return op.Finish(Local<Value>(), false, out);
|
||||
promise = resolver->GetPromise();
|
||||
}
|
||||
Local<Value> data = Integer::NewFromUnsigned(op.e.isolate, token);
|
||||
Local<Function> fulfilled, rejected;
|
||||
Local<v8::Promise> chained;
|
||||
bool ok = Function::New(op.e.context, OnFulfilled, data).ToLocal(&fulfilled) &&
|
||||
Function::New(op.e.context, OnRejected, data).ToLocal(&rejected) &&
|
||||
promise->Then(op.e.context, fulfilled, rejected).ToLocal(&chained);
|
||||
return op.Finish(v8::Undefined(op.e.isolate), ok, out);
|
||||
}
|
||||
|
||||
void ErrorInfo(uint32_t h, ScrNodeErrorInfo *out) {
|
||||
Op op;
|
||||
std::memset(out, 0, sizeof *out);
|
||||
Local<Value> v = HandleValue(op.e.isolate, h);
|
||||
if (!v->IsObject()) return;
|
||||
Local<Object> error = v.As<Object>();
|
||||
static const struct { const char *name; uint32_t kind; } kinds[] = {
|
||||
{"TypeError", 1}, {"RangeError", 2}, {"SyntaxError", 3},
|
||||
{"ReferenceError", 5}, {"EvalError", 6}, {"URIError", 7}};
|
||||
for (const auto &k : kinds) {
|
||||
Local<Value> ctor = op.e.context->Global()->Get(op.e.context, Utf8(op.e.isolate, k.name)).ToLocalChecked();
|
||||
if (error->InstanceOf(op.e.context, ctor.As<Object>()).FromMaybe(false)) {
|
||||
out->kind = k.kind;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto read = [&](const char *key, const char **data, size_t *len, bool optional) {
|
||||
Local<Value> value;
|
||||
if (!error->Get(op.e.context, Utf8(op.e.isolate, key)).ToLocal(&value)) {
|
||||
op.try_catch.Reset();
|
||||
return;
|
||||
}
|
||||
if (optional && !value->IsString()) return;
|
||||
Local<String> text;
|
||||
if (!value->ToString(op.e.context).ToLocal(&text)) {
|
||||
op.try_catch.Reset();
|
||||
return;
|
||||
}
|
||||
*data = Scratch(Wtf8(op.e.isolate, text), len);
|
||||
};
|
||||
read("name", &out->name, &out->name_len, false);
|
||||
read("message", &out->message, &out->message_len, false);
|
||||
read("stack", &out->stack, &out->stack_len, true);
|
||||
read("code", &out->code, &out->code_len, true);
|
||||
}
|
||||
|
||||
void DropHandle(uint32_t h) {
|
||||
if (!g_setup) return;
|
||||
Isolate *isolate = g_setup->isolate();
|
||||
HandleScope handles(isolate);
|
||||
Drop(h);
|
||||
}
|
||||
|
||||
const ScrNodeBridge kBridge = {
|
||||
SCR_NODE_BRIDGE_ABI,
|
||||
NODE_VERSION_STRING,
|
||||
@@ -648,6 +1243,20 @@ const ScrNodeBridge kBridge = {
|
||||
RunLoop,
|
||||
Emit,
|
||||
Stop,
|
||||
Load,
|
||||
Get,
|
||||
SetProp,
|
||||
Has,
|
||||
Del,
|
||||
Invoke,
|
||||
InvokeMethod,
|
||||
Construct,
|
||||
InstanceOf,
|
||||
OwnKeys,
|
||||
Then,
|
||||
ErrorInfo,
|
||||
DropHandle,
|
||||
LiveHandles,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -46,7 +46,7 @@ static const char kBootstrap[] =
|
||||
" },\n"
|
||||
" setTimer() { return 0; }, clearTimer() {}, refTimer() {}, refreshTimer() {},\n"
|
||||
" setImmediate() { return 0; }, clearImmediate() {}, refImmediate() {},\n"
|
||||
" nextTick() {}, emit() {},\n"
|
||||
" nextTick() {}, emit() {}, load() {},\n"
|
||||
"};\n";
|
||||
|
||||
static double now_ms(void) {
|
||||
|
||||
Vendored
+13
@@ -36,3 +36,16 @@ export declare function call<T = unknown>(
|
||||
name: string,
|
||||
...args: unknown[]
|
||||
): Promise<T>;
|
||||
|
||||
/** Loads the module `specifier` with require() from the calling module and
|
||||
* returns its exports **by reference** (scope-B Node islands, NI-4).
|
||||
*
|
||||
* In scriptc builds the module runs in Node.js loaded into the program on
|
||||
* first use; the specifier must be a string literal, resolved at build time
|
||||
* as `require.resolve` resolves it from the caller. The exports and every
|
||||
* object reached through them stay Node objects: the program holds handles
|
||||
* to them, so identity, aliasing and mutation are shared with Node.
|
||||
* Primitives, strings and BigInt are copied; promises become promises;
|
||||
* Buffers and typed arrays share their memory. Under Node.js this is
|
||||
* `createRequire(callerFile)(specifier)`. */
|
||||
export declare function requireModule<T = any>(specifier: string): T;
|
||||
|
||||
@@ -135,3 +135,8 @@ export async function call(specifier, name, ...args) {
|
||||
throw copyThrown(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** requireModule(specifier): the module's exports, by reference. */
|
||||
export function requireModule(specifier) {
|
||||
return createRequire(callerFile(requireModule))(specifier);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,10 @@ export const OPTIONAL_RUNTIME_SOURCES = [
|
||||
["scr_js_value.c", "dynTier"],
|
||||
["scr_js_object.c", "dynTier"],
|
||||
["scr_event_target.c", any("eventTarget", "nativeFetch", "abortSignal")],
|
||||
["scr_async_dyn.c", any("dynAsync", "dynInvoke", "dc", "fileHandle", "nativeFetch")],
|
||||
[
|
||||
"scr_async_dyn.c",
|
||||
any("dynAsync", "dynInvoke", "dc", "fileHandle", "nativeFetch", "nodeIsland"),
|
||||
],
|
||||
// The crypto global's object extends scr_async_dyn.c's crypto object.
|
||||
["scr_webcrypto_object.c", all("webcrypto", "dynAsync")],
|
||||
// The performance timeline emits its buffer warning through scr_async_dyn.c.
|
||||
@@ -191,6 +194,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [
|
||||
// Node islands: libnode loaded on first use (explicit @scriptc/node calls),
|
||||
// over the SCR_NODE_ISLANDS build of scr_async.c.
|
||||
["scr_node_island.c", "nodeIsland"],
|
||||
["scr_node_island_handle.c", "nodeIsland"],
|
||||
["scr_stream_from_web.c", all("stream", "nativeFetch")],
|
||||
];
|
||||
|
||||
@@ -201,6 +205,18 @@ export function variantsFor(source) {
|
||||
// other program links the ordinary object, byte for byte.
|
||||
if (source === "scr_json.c") {
|
||||
variants.push({ id: "dyn-tier", when: { dynTier: true }, defines: ["SCR_JS_TIER"] });
|
||||
// Dynamic operations on Node island handles (typeof, calls, `in`,
|
||||
// delete, Object.keys) route to scr_node_island_handle.c.
|
||||
variants.push({
|
||||
id: "node-island",
|
||||
when: { nodeIsland: true },
|
||||
defines: ["SCR_NODE_ISLANDS"],
|
||||
});
|
||||
variants.push({
|
||||
id: "dyn-tier-node-island",
|
||||
when: { dynTier: true, nodeIsland: true },
|
||||
defines: ["SCR_JS_TIER", "SCR_NODE_ISLANDS"],
|
||||
});
|
||||
// Programs with web streams: instanceof over their classes' handles.
|
||||
variants.push({
|
||||
id: "handle-classes",
|
||||
@@ -212,6 +228,26 @@ export function variantsFor(source) {
|
||||
when: { dynTier: true, webStreams: true },
|
||||
defines: ["SCR_JS_TIER", "SCR_HANDLE_CLASSES"],
|
||||
});
|
||||
// Both: the variant with the most conditions is chosen, so programs
|
||||
// with web streams and Node islands need the builds that define both.
|
||||
variants.push({
|
||||
id: "handle-classes-node-island",
|
||||
when: { webStreams: true, nodeIsland: true },
|
||||
defines: ["SCR_HANDLE_CLASSES", "SCR_NODE_ISLANDS"],
|
||||
});
|
||||
variants.push({
|
||||
id: "dyn-tier-handle-classes-node-island",
|
||||
when: { dynTier: true, webStreams: true, nodeIsland: true },
|
||||
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") {
|
||||
variants.push({
|
||||
id: "node-island",
|
||||
when: { nodeIsland: true },
|
||||
defines: ["SCR_NODE_ISLANDS"],
|
||||
});
|
||||
}
|
||||
if (source === "scr_bytes.c") {
|
||||
variants.push({
|
||||
@@ -239,6 +275,14 @@ export function variantsFor(source) {
|
||||
when: { webStreams: true },
|
||||
defines: ["SCR_NATIVE_REACTIONS"],
|
||||
});
|
||||
// A program with both web streams and Node islands needs both builds:
|
||||
// the island slot carries the hosted loop, and the variant with the
|
||||
// most conditions is the one selected.
|
||||
variants.push({
|
||||
id: "native-reactions-node-island",
|
||||
when: { webStreams: true, nodeIsland: true },
|
||||
defines: ["SCR_NATIVE_REACTIONS", "SCR_NODE_ISLANDS"],
|
||||
});
|
||||
}
|
||||
if (source === "scr_dyn_builtin_class.c" || source === "scr_fetch.c") {
|
||||
variants.push({
|
||||
@@ -247,6 +291,14 @@ export function variantsFor(source) {
|
||||
defines: ["SCR_HANDLE_CLASSES"],
|
||||
});
|
||||
}
|
||||
// instanceof over both web stream and Node island handles.
|
||||
if (source === "scr_dyn_builtin_class.c") {
|
||||
variants.push({
|
||||
id: "handle-classes-node-island",
|
||||
when: { webStreams: true, nodeIsland: true },
|
||||
defines: ["SCR_HANDLE_CLASSES", "SCR_NODE_ISLANDS"],
|
||||
});
|
||||
}
|
||||
// stdin setEncoding, string data listeners and stdio 'resize' events.
|
||||
if (source === "scr_events.c") {
|
||||
variants.push({
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { createRuntimePackMatrix } from "../runtime-pack-matrix.mjs";
|
||||
|
||||
const matrix = createRuntimePackMatrix({
|
||||
target: { object_format: "elf" },
|
||||
compileFlags: [],
|
||||
systemLibraries: [],
|
||||
});
|
||||
|
||||
/** The runtime pack's variant selection: the matching variant with the
|
||||
* most conditions, ties by id. */
|
||||
function selected(
|
||||
source: string,
|
||||
features: Record<string, boolean>,
|
||||
): { id: string; defines: string[] } {
|
||||
const unit = matrix.runtime_units.find((u: { source: string }) => u.source === source);
|
||||
const matches = unit.variants.filter((variant: { when: Record<string, boolean> }) =>
|
||||
Object.entries(variant.when).every(([name, expected]) => features[name] === expected),
|
||||
);
|
||||
matches.sort(
|
||||
(a: { when: object; id: string }, b: { when: object; id: string }) =>
|
||||
Object.keys(b.when).length - Object.keys(a.when).length || a.id.localeCompare(b.id),
|
||||
);
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
test("web streams and Node islands select an async build that defines both", () => {
|
||||
const variant = selected("scr_async.c", { webStreams: true, nodeIsland: true, workers: false });
|
||||
expect(variant.defines).toEqual(
|
||||
expect.arrayContaining(["SCR_NATIVE_REACTIONS", "SCR_NODE_ISLANDS"]),
|
||||
);
|
||||
// Each feature alone still selects its own build, byte for byte.
|
||||
expect(
|
||||
selected("scr_async.c", { webStreams: true, nodeIsland: false, workers: false }).defines,
|
||||
).toEqual(["SCR_NATIVE_REACTIONS"]);
|
||||
expect(
|
||||
selected("scr_async.c", { webStreams: false, nodeIsland: true, workers: false }).defines,
|
||||
).toEqual(["SCR_NODE_ISLANDS"]);
|
||||
expect(
|
||||
selected("scr_async.c", { webStreams: false, nodeIsland: false, workers: false }).defines,
|
||||
).toEqual([]);
|
||||
});
|
||||
@@ -240,6 +240,7 @@ typedef enum {
|
||||
|
||||
/* Tags added since the last pin, by name only (see above). */
|
||||
#define SCR_DYNH_NEW_TAGS(Y) \
|
||||
Y(NODE_ISLAND) /* a Node island value (scr_node_island_handle.c) */ \
|
||||
Y(WS_BYTE_STRATEGY) /* ByteLengthQueuingStrategy (scr_webstreams.c) */ \
|
||||
Y(WS_COUNT_STRATEGY) /* CountQueuingStrategy (scr_webstreams.c) */ \
|
||||
Y(WS_RCONTROLLER) /* ReadableStreamDefaultController (scr_webstreams.c) */ \
|
||||
|
||||
@@ -1587,6 +1587,9 @@ bool scr_dyn_builtin_instanceof(ScrDyn *value, double kind) {
|
||||
|
||||
/* `value instanceof callee` for an untyped constructor function. */
|
||||
bool scr_dyn_function_instanceof(ScrDyn *value, ScrDyn *callee) {
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
if (scr_ni_is_handle(callee)) return scr_ni_instanceof(value, callee) == 1;
|
||||
#endif
|
||||
if (!scr_dyn_is_callable(callee)) {
|
||||
static const char message[] = "Right-hand side of 'instanceof' is not callable";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
|
||||
@@ -2653,6 +2653,10 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
|
||||
ScrStr *iterator = scr_insp_native_iterator(d, recurse, depth);
|
||||
if (iterator) return iterator;
|
||||
}
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
/* An island object prints as Node's util.inspect prints it. */
|
||||
if (d->v.handle.tag == SCR_DYNH_NODE_ISLAND) return scr_ni_inspect(d, recurse, depth);
|
||||
#endif
|
||||
const ScrDynHandleOps *ops = scr_dyn_handle_ops_of(d);
|
||||
if (ops->inspect_view) {
|
||||
/* Node's custom inspectors answer the bare class name beyond the
|
||||
|
||||
@@ -5604,6 +5604,9 @@ static bool scr_dyn_instanceof_function(const ScrDyn *value) {
|
||||
}
|
||||
|
||||
bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const ScrStr *refusal) {
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
if (scr_ni_is_handle(callee)) return scr_ni_instanceof((ScrDyn *)value, (ScrDyn *)callee) == 1;
|
||||
#endif
|
||||
if (!callee || callee->kind != SCR_DYN_FUNC) {
|
||||
static const char message[] = "Right-hand side of 'instanceof' is not callable";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
@@ -6459,6 +6462,15 @@ ScrDyn *scr_dyn_class_prototype(ScrDyn *value, ScrDyn *base) {
|
||||
* compiled per signature). Args borrowed; result owned (+1) or NULL with
|
||||
* the exception pending. */
|
||||
ScrDyn *scr_dyn_call(const ScrDyn *d, ScrDyn *const *args, size_t argc, const char *what) {
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
/* An island function: called in Node with the ambient receiver. */
|
||||
if (scr_ni_is_handle(d) && scr_ni_is_function(d)) {
|
||||
ScrDyn *receiver = scr_dyn_this_get();
|
||||
ScrDyn *result = scr_ni_call((ScrDyn *)d, receiver, args, argc);
|
||||
scr_dyn_release(receiver);
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
if (d->kind == SCR_DYN_PROXY && scr_dyn_is_callable(d->v.proxy.target)) {
|
||||
ScrDyn *trap = scr_dyn_proxy_trap(d, "apply");
|
||||
if (scr_exc_pending()) return NULL;
|
||||
@@ -7590,6 +7602,10 @@ void scr_dyn_name_class_prototype(ScrDyn *prototype, const ScrStr *name) {
|
||||
/* Ordinary native function constructors retain their live prototype table.
|
||||
* A constructor may replace the fresh receiver with another object. */
|
||||
ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) {
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
if (scr_ni_is_handle(callee))
|
||||
return scr_ni_construct((ScrDyn *)callee, args->v.arr.items, args->v.arr.len);
|
||||
#endif
|
||||
if (callee->kind != SCR_DYN_FUNC || callee->v.fn.class_obj ||
|
||||
(scr_closure_identity(callee->v.fn.clo)->function_kind & SCR_CLOSURE_KIND_MASK) != SCR_CLOSURE_KIND_OWN_PROTOTYPE)
|
||||
return scr_bytes_construct(callee, args, what);
|
||||
@@ -9685,6 +9701,9 @@ bool scr_dyn_truthy(const ScrDyn *d) {
|
||||
* null answers "object" — JS's oldest wart, preserved. */
|
||||
ScrStr *scr_dyn_typeof(const ScrDyn *d) {
|
||||
const char *s;
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
if (scr_ni_is_handle(d)) return scr_ni_typeof((ScrDyn *)d);
|
||||
#endif
|
||||
if (d->kind == SCR_DYN_PROXY) return scr_dyn_typeof(d->v.proxy.target);
|
||||
if (d->kind == SCR_DYN_TYPED_REF) {
|
||||
if ((d->v.typed_ref.type_key_len >= 7 &&
|
||||
@@ -9721,6 +9740,9 @@ ScrStr *scr_dyn_typeof(const ScrDyn *d) {
|
||||
|
||||
bool scr_dyn_is_callable(const ScrDyn *value) {
|
||||
while (value->kind == SCR_DYN_PROXY) value = value->v.proxy.target;
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
if (scr_ni_is_handle(value)) return scr_ni_is_function(value);
|
||||
#endif
|
||||
return value->kind == SCR_DYN_FUNC || scr_dyn_ref_typeof_is(value, "function");
|
||||
}
|
||||
|
||||
@@ -11828,6 +11850,14 @@ bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key) {
|
||||
* Array.prototype before it for arrays) and, for a class instance seen as
|
||||
* `unknown`, its class's compiled methods and accessors. */
|
||||
bool scr_dyn_in(const ScrDyn *v, const ScrStr *key) {
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
if (scr_ni_is_handle(v)) {
|
||||
ScrDyn *k = scr_dyn_new_str((ScrStr *)key);
|
||||
int found = scr_ni_has((ScrDyn *)v, k);
|
||||
scr_dyn_release(k);
|
||||
return found == 1;
|
||||
}
|
||||
#endif
|
||||
/* Generator objects have no checked view; scr_dyn_has_key answers their
|
||||
* prototype members. */
|
||||
if (v->kind == SCR_DYN_TYPED_REF && !scr_dyn_generator(v)) {
|
||||
@@ -14205,6 +14235,14 @@ void scr_dyn_obj_delete_field(ScrDyn *obj, const ScrStr *key) {
|
||||
* arrays, typed references and handles keep their explicit boundary;
|
||||
* deleting from a materialized snapshot would lose the mutation. */
|
||||
void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) {
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
if (scr_ni_is_handle(recv)) {
|
||||
ScrDyn *k = scr_dyn_new_str((ScrStr *)key);
|
||||
(void)scr_ni_delete(recv, k);
|
||||
scr_dyn_release(k);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (scr_dyn_class_reflection_fence(recv) || scr_dyn_error_prototype_fence(recv)) return;
|
||||
if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_delete(recv, key); return; }
|
||||
if (recv->kind == SCR_DYN_TYPED_REF && recv->v.typed_ref.commit &&
|
||||
@@ -16246,6 +16284,34 @@ static ScrDyn *scr_dyn_obj_data_walk(const ScrDyn *v, ScrObjWalk mode) {
|
||||
|
||||
static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) {
|
||||
if (scr_dyn_class_reflection_fence(v)) return NULL;
|
||||
#ifdef SCR_NODE_ISLANDS
|
||||
/* Object.keys/values/entries of an island object: its own enumerable
|
||||
* string keys, read in Node. */
|
||||
if (scr_ni_is_handle(v)) {
|
||||
ScrDyn *keys = scr_ni_own_keys((ScrDyn *)v, 0);
|
||||
if (keys == NULL || mode == SCR_OBJWALK_KEYS) return keys;
|
||||
ScrDyn *out = scr_dyn_new_arr();
|
||||
for (size_t i = 0; i < keys->v.arr.len; i++) {
|
||||
ScrDyn *key = keys->v.arr.items[i];
|
||||
ScrDyn *value = scr_ni_get((ScrDyn *)v, key);
|
||||
if (value == NULL) {
|
||||
scr_dyn_release(out);
|
||||
scr_dyn_release(keys);
|
||||
return NULL;
|
||||
}
|
||||
if (mode == SCR_OBJWALK_VALUES) {
|
||||
scr_dyn_arr_push(out, value);
|
||||
} else {
|
||||
ScrDyn *pair = scr_dyn_new_arr();
|
||||
scr_dyn_arr_push(pair, scr_dyn_retain(key));
|
||||
scr_dyn_arr_push(pair, value);
|
||||
scr_dyn_arr_push(out, pair);
|
||||
}
|
||||
}
|
||||
scr_dyn_release(keys);
|
||||
return out;
|
||||
}
|
||||
#endif
|
||||
if (v->kind == SCR_DYN_PROXY) {
|
||||
ScrDyn *trap = scr_dyn_proxy_trap(v, "ownKeys");
|
||||
if (scr_exc_pending()) return NULL;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define SCR_NODE_BRIDGE_ABI 4u
|
||||
#define SCR_NODE_BRIDGE_ABI 5u
|
||||
/* The symbol a bridge library exports: `const ScrNodeBridge *(void)`. */
|
||||
#define SCR_NODE_BRIDGE_ENTRY "scr_node_bridge_v1"
|
||||
|
||||
@@ -59,6 +59,78 @@ typedef struct ScrNodeIoState {
|
||||
int cap_ms;
|
||||
} ScrNodeIoState;
|
||||
|
||||
|
||||
/* ── object handles (scope-B NI-4) ────────────────────────────────────────
|
||||
* A value crossing between the runtime and V8. Primitives, strings and
|
||||
* BigInt are copies; objects are handles: ids into the bridge's import
|
||||
* table of v8::Global references, one id per V8 object while the runtime
|
||||
* holds it (the bridge answers the same id for the same object), released
|
||||
* with `drop`. Typed arrays and Buffers coming out of V8 also carry their
|
||||
* backing store's memory (BYTES), which the runtime aliases.
|
||||
*
|
||||
* Out-values the bridge writes hold bridge-owned string data valid until
|
||||
* the next bridge entry; the runtime copies it at once. In-values are
|
||||
* borrowed for the call. */
|
||||
enum {
|
||||
SCR_NV_UNDEFINED = 0,
|
||||
SCR_NV_NULL = 1,
|
||||
SCR_NV_BOOL = 2,
|
||||
SCR_NV_NUMBER = 3,
|
||||
SCR_NV_STRING = 4, /* WTF-8: lone surrogates as their 3-byte forms */
|
||||
SCR_NV_BIGINT = 5, /* str: decimal text with an optional leading '-' */
|
||||
SCR_NV_HANDLE = 6, /* handle: an import-table id; flags: SCR_NVF_* */
|
||||
SCR_NV_BYTES = 7, /* bytes: a typed array / Buffer / DataView over shared memory */
|
||||
SCR_NV_ERROR = 8, /* into V8 only: a new Error of an intrinsic class */
|
||||
SCR_NV_JSON = 9, /* into V8 only: str is JSON text, parsed there (a copy) */
|
||||
SCR_NV_EXPORT = 10, /* reserved for NI-5: a runtime object exported by reference */
|
||||
};
|
||||
/* What a handle is, reported with every handle the bridge answers. */
|
||||
enum {
|
||||
SCR_NVF_FUNCTION = 1,
|
||||
SCR_NVF_SYMBOL = 2,
|
||||
SCR_NVF_ERROR = 4,
|
||||
SCR_NVF_ARRAY = 8,
|
||||
SCR_NVF_PROMISE = 16,
|
||||
SCR_NVF_CONSTRUCTOR = 32,
|
||||
SCR_NVF_BUFFER = 64, /* BYTES: a Buffer; DATAVIEW: a DataView */
|
||||
SCR_NVF_DATAVIEW = 128,
|
||||
};
|
||||
/* BYTES element kinds: ScrBytesElem's numbering (scr_bytes.h). */
|
||||
typedef struct ScrNodeValue {
|
||||
uint32_t tag;
|
||||
uint32_t flags;
|
||||
union {
|
||||
int boolean;
|
||||
double num;
|
||||
struct {
|
||||
const char *data;
|
||||
size_t len;
|
||||
} str;
|
||||
uint32_t handle;
|
||||
struct {
|
||||
uint8_t *data; /* first element */
|
||||
size_t len; /* elements */
|
||||
uint32_t elem; /* ScrBytesElem */
|
||||
uint32_t handle; /* out of V8: the view's handle (keeps the store alive) */
|
||||
void *owner; /* into V8: the runtime ScrBytes root that owns data */
|
||||
} bytes;
|
||||
struct {
|
||||
uint32_t kind; /* scr_error.h SCR_ERR_* (intrinsic class) */
|
||||
const char *name, *message, *stack, *code; /* code NULL: none */
|
||||
size_t name_len, message_len, stack_len, code_len;
|
||||
} error;
|
||||
} u;
|
||||
} ScrNodeValue;
|
||||
|
||||
/* An island error's own properties, read by `error_info` (bridge-owned
|
||||
* strings, valid until the next entry; code NULL when it has no string
|
||||
* code). kind is the nearest intrinsic class (SCR_ERR_*). */
|
||||
typedef struct ScrNodeErrorInfo {
|
||||
uint32_t kind;
|
||||
const char *name, *message, *stack, *code;
|
||||
size_t name_len, message_len, stack_len, code_len;
|
||||
} ScrNodeErrorInfo;
|
||||
|
||||
/* What the runtime provides to the bridge. */
|
||||
typedef struct ScrNodeBridgeHost {
|
||||
/* An asynchronous call settled with this outcome. */
|
||||
@@ -93,6 +165,13 @@ typedef struct ScrNodeBridgeHost {
|
||||
* start it records for the thread, so it must not record the thread's
|
||||
* when JavaScript runs on the runtime's stack. */
|
||||
uintptr_t (*stack_start)(void);
|
||||
/* NI-4: a thenable the runtime awaits (`then`) settled; `value` is the
|
||||
* fulfillment or (rejected=1) the reason. Runs as a V8 reaction job. */
|
||||
void (*settle_value)(uint32_t token, int rejected, const ScrNodeValue *value);
|
||||
/* NI-4: V8 freed a backing store over runtime bytes (`owner` of a BYTES
|
||||
* value that went into V8). May run on any thread: the runtime queues
|
||||
* the release for its own thread. */
|
||||
void (*bytes_free)(void *owner);
|
||||
} ScrNodeBridgeHost;
|
||||
|
||||
typedef struct ScrNodeBridge {
|
||||
@@ -151,6 +230,36 @@ typedef struct ScrNodeBridge {
|
||||
void (*emit)(uint32_t event, uint32_t listener, const char *name, int arg);
|
||||
/* Tears Node down at process exit. */
|
||||
void (*stop)(void);
|
||||
/* ── object handles (NI-4). Each operation answers 0 (returned: *out is
|
||||
* the result) or 1 (threw: *out is the thrown value). ── */
|
||||
/* Loads a module by absolute path: require(path) (esm=0, the exports),
|
||||
* or import(path) (esm=1, a promise of the namespace). */
|
||||
int (*load)(const char *path, int esm, ScrNodeValue *out);
|
||||
/* Property access on handle h; key goes through ToPropertyKey. */
|
||||
int (*get)(uint32_t h, const ScrNodeValue *key, ScrNodeValue *out);
|
||||
int (*set)(uint32_t h, const ScrNodeValue *key, const ScrNodeValue *value, ScrNodeValue *out);
|
||||
int (*has)(uint32_t h, const ScrNodeValue *key, ScrNodeValue *out); /* out: BOOL */
|
||||
int (*del)(uint32_t h, const ScrNodeValue *key, ScrNodeValue *out); /* out: BOOL */
|
||||
/* fn(...args) with receiver this_ (NULL: undefined). */
|
||||
int (*invoke)(uint32_t fn, const ScrNodeValue *this_, const ScrNodeValue *args, size_t argc,
|
||||
ScrNodeValue *out);
|
||||
/* h[key](...args) with h as the receiver. */
|
||||
int (*invoke_method)(uint32_t h, const ScrNodeValue *key, const ScrNodeValue *args, size_t argc,
|
||||
ScrNodeValue *out);
|
||||
int (*construct)(uint32_t h, const ScrNodeValue *args, size_t argc, ScrNodeValue *out);
|
||||
int (*instance_of)(const ScrNodeValue *value, uint32_t ctor, ScrNodeValue *out); /* BOOL */
|
||||
/* Own keys of h: mode 0 Object.keys, mode 1 Reflect.ownKeys. out: a
|
||||
* handle to a fresh array, which the runtime reads and drops. */
|
||||
int (*own_keys)(uint32_t h, int mode, ScrNodeValue *out);
|
||||
/* Promise.resolve(value).then(...): settles through host->settle_value
|
||||
* with `token`. Answers 1 with *out thrown if `then` could not attach. */
|
||||
int (*then)(const ScrNodeValue *value, uint32_t token, ScrNodeValue *out);
|
||||
/* The own name, message, stack and string code of an error handle. */
|
||||
void (*error_info)(uint32_t h, ScrNodeErrorInfo *out);
|
||||
/* Releases handle h's Global. */
|
||||
void (*drop)(uint32_t h);
|
||||
/* The number of live import-table entries (leak tests). */
|
||||
uint32_t (*live_handles)(void);
|
||||
} ScrNodeBridge;
|
||||
|
||||
#endif /* SCR_NODE_BRIDGE_H */
|
||||
|
||||
@@ -689,19 +689,24 @@ static void ni_open_modules(void) {
|
||||
ni_fatal("scriptc: cannot write this program's Node island modules to a private directory\n");
|
||||
}
|
||||
|
||||
/* The calling module's path inside the tree: `<root>/<relative path>`. */
|
||||
static char *ni_caller_path(const ScrStr *relative) {
|
||||
/* A path inside the tree: `<root>/<relative path>`. */
|
||||
static char *ni_tree_path(const char *relative, size_t len) {
|
||||
size_t root = strlen(ni_root);
|
||||
bool slash = root > 0 && ni_root[root - 1] == '/';
|
||||
char *path = malloc(root + 1 + relative->len + 1);
|
||||
char *path = malloc(root + 1 + len + 1);
|
||||
if (path == NULL) ni_oom();
|
||||
memcpy(path, ni_root, root);
|
||||
if (!slash) path[root++] = '/';
|
||||
memcpy(path + root, relative->data, relative->len);
|
||||
path[root + relative->len] = '\0';
|
||||
memcpy(path + root, relative, len);
|
||||
path[root + len] = '\0';
|
||||
return path;
|
||||
}
|
||||
|
||||
/* The calling module's path inside the tree. */
|
||||
static char *ni_caller_path(const ScrStr *relative) {
|
||||
return ni_tree_path(relative->data, relative->len);
|
||||
}
|
||||
|
||||
/* The bootstrap: the tree root for the host's resolution guard, then the
|
||||
* host script. +1 malloc'd. */
|
||||
static char *ni_bootstrap(void) {
|
||||
@@ -1158,6 +1163,8 @@ static const ScrNodeBridgeHost ni_host = {
|
||||
&ni_host_listen,
|
||||
&ni_stack_limit,
|
||||
&ni_stack_start,
|
||||
&scr_ni_settle_value,
|
||||
&scr_ni_bytes_free,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
@@ -1186,6 +1193,7 @@ void scr_node_island_emit(uint32_t event, uint32_t listener, ScrStr *name, int a
|
||||
|
||||
static void ni_stop_job(void *p) {
|
||||
(void)p;
|
||||
scr_ni_stop();
|
||||
ni_bridge->stop();
|
||||
}
|
||||
|
||||
@@ -1194,6 +1202,13 @@ static void ni_stop_job(void *p) {
|
||||
* the events unit's own, which then finds them done), then Node stops. */
|
||||
static void ni_stop_atexit(void) {
|
||||
scr_run_exit_listeners((double)scr_exit_code_hint_get());
|
||||
/* SCRIPTC_NODE_ISLANDS_TRACE=handles: the import table at exit (leak
|
||||
* tests: acyclic island objects the program dropped are released). */
|
||||
const char *trace = getenv("SCRIPTC_NODE_ISLANDS_TRACE");
|
||||
if (trace != NULL && strcmp(trace, "handles") == 0) {
|
||||
fflush(stdout);
|
||||
fprintf(stderr, "[node-islands] live handles %u\n", scr_ni_live_handles());
|
||||
}
|
||||
ni_on_node_stack(ni_stop_job, NULL);
|
||||
}
|
||||
|
||||
@@ -1349,6 +1364,7 @@ static void ni_start(void) {
|
||||
* delivery; installed before the stop handler, whose 'exit' listeners
|
||||
* run first. */
|
||||
scr_events_install();
|
||||
scr_ni_handles_install();
|
||||
scr_atexit(ni_stop_atexit);
|
||||
ni_hosted = *bridge;
|
||||
ni_hosted.run_loop = &ni_run_loop;
|
||||
@@ -1365,6 +1381,21 @@ static void ni_start(void) {
|
||||
scr_loop_host(&ni_hosted, since);
|
||||
}
|
||||
|
||||
const ScrNodeBridge *scr_ni_bridge(void) {
|
||||
ni_start();
|
||||
return ni_bridge;
|
||||
}
|
||||
|
||||
const ScrNodeBridge *scr_ni_bridge_loaded(void) { return ni_bridge; }
|
||||
|
||||
char *scr_ni_module_path(const ScrStr *path) {
|
||||
if (path->len >= 2 && path->data[0] == '.' && path->data[1] == '/')
|
||||
return ni_tree_path(path->data + 2, path->len - 2);
|
||||
return ni_cstr_of(path->data, path->len);
|
||||
}
|
||||
|
||||
void scr_ni_on_node_stack(void (*job)(void *), void *arg) { ni_on_node_stack(job, arg); }
|
||||
|
||||
/* ── calls ────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* The JSON text of the argument array (+1 dyn string), or NULL with the
|
||||
|
||||
@@ -66,4 +66,60 @@ void scr_signal_off_island(uint32_t listener);
|
||||
void scr_process_on_exit_island(uint32_t listener);
|
||||
void scr_process_off_exit_island(uint32_t listener);
|
||||
|
||||
/* ── object handles (scope-B NI-4, scr_node_island_handle.c) ─────────────
|
||||
* Island values the program holds are SCR_DYN_HANDLE boxes with tag
|
||||
* SCR_DYNH_NODE_ISLAND: one handle per V8 value, so `===` is identity.
|
||||
* Primitives, strings and BigInt are copies; island promises are scriptc
|
||||
* promises; typed arrays and Buffers are ScrBytes over the same memory.
|
||||
* All arguments are borrowed; ScrDyn results are +1, NULL (or false, or
|
||||
* -1 for the int forms) with the exception pending. The first call loads
|
||||
* Node. See scriptc-perf/results/ni-4/ABI.md. */
|
||||
ScrDyn *scr_ni_require(ScrStr *path); /* require(absolute path): the exports */
|
||||
ScrPromise *scr_ni_import(ScrStr *path); /* import(absolute path): the namespace */
|
||||
ScrDyn *scr_ni_get(ScrDyn *obj, ScrDyn *key);
|
||||
ScrDyn *scr_ni_get_str(ScrDyn *obj, const char *key, size_t len);
|
||||
bool scr_ni_set(ScrDyn *obj, ScrDyn *key, ScrDyn *value);
|
||||
bool scr_ni_set_str(ScrDyn *obj, const char *key, size_t len, ScrDyn *value);
|
||||
int scr_ni_has(ScrDyn *obj, ScrDyn *key);
|
||||
int scr_ni_delete(ScrDyn *obj, ScrDyn *key);
|
||||
ScrDyn *scr_ni_call(ScrDyn *fn, ScrDyn *this_, ScrDyn *const *args, size_t argc);
|
||||
ScrDyn *scr_ni_call_method(ScrDyn *obj, const char *key, size_t len, ScrDyn *const *args,
|
||||
size_t argc);
|
||||
ScrDyn *scr_ni_construct(ScrDyn *ctor, ScrDyn *const *args, size_t argc);
|
||||
ScrStr *scr_ni_typeof(ScrDyn *value);
|
||||
int scr_ni_instanceof(ScrDyn *value, ScrDyn *ctor);
|
||||
bool scr_ni_is_handle(const ScrDyn *value);
|
||||
bool scr_ni_is_function(const ScrDyn *value); /* an island handle that is callable */
|
||||
/* util.inspect of an island value at nesting `recurse` with limit `depth`,
|
||||
* by Node's own util.inspect (+1; "" with the exception pending). */
|
||||
ScrStr *scr_ni_inspect(const ScrDyn *value, double recurse, double depth);
|
||||
ScrDyn *scr_ni_own_keys(ScrDyn *obj, int mode); /* 0 Object.keys, 1 Reflect.ownKeys */
|
||||
ScrPromise *scr_ni_await(ScrDyn *value);
|
||||
uint32_t scr_ni_live_handles(void); /* live import-table entries (leak tests) */
|
||||
|
||||
/* Between the two island units. */
|
||||
struct ScrNodeValue;
|
||||
const struct ScrNodeBridge *scr_ni_bridge(void); /* loads Node */
|
||||
const struct ScrNodeBridge *scr_ni_bridge_loaded(void); /* NULL until loaded */
|
||||
/* The file a module load names, +1 malloc'd: `./<path>` is a path in the
|
||||
* program's module tree, under its root once Node is loaded; anything else
|
||||
* (a built-in module) as it is. */
|
||||
char *scr_ni_module_path(const ScrStr *path);
|
||||
void scr_ni_on_node_stack(void (*job)(void *), void *arg);
|
||||
void scr_ni_settle_value(uint32_t token, int rejected, const struct ScrNodeValue *value);
|
||||
void scr_ni_bytes_free(void *owner);
|
||||
void scr_ni_handles_install(void);
|
||||
void scr_ni_stop(void);
|
||||
|
||||
/* Island operations the compiler emits (lib-shared.ts adapters). */
|
||||
ScrDyn *scr_node_island_apply(ScrDyn *fn, ScrDyn *args);
|
||||
/* fn called with `this` (a method read before its arguments ran). */
|
||||
ScrDyn *scr_node_island_apply_to(ScrDyn *fn, ScrDyn *this_, ScrDyn *args);
|
||||
ScrDyn *scr_node_island_construct(ScrDyn *ctor, ScrDyn *args);
|
||||
ScrDyn *scr_node_island_get(ScrDyn *obj, ScrStr *key);
|
||||
bool scr_node_island_instanceof(ScrDyn *value, ScrDyn *ctor);
|
||||
ScrDyn *scr_node_island_invoke(ScrDyn *obj, ScrStr *key, ScrDyn *args);
|
||||
void scr_node_island_set(ScrDyn *obj, ScrStr *key, ScrDyn *value);
|
||||
void scr_node_island_set_key(ScrDyn *obj, ScrDyn *key, ScrDyn *value);
|
||||
|
||||
#endif /* SCR_NODE_ISLAND_H */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@
|
||||
* them in-process under Node: both must produce the same results, errors
|
||||
* and messages. Outcomes are "R<json>" (returned; "R" alone is undefined)
|
||||
* or "T<json>" (threw), the encoding of scr_node_bridge.h. */
|
||||
const { createRequire, registerHooks } = require("node:module");
|
||||
const { createRequire, isBuiltin, registerHooks } = require("node:module");
|
||||
const { constants: osConstants } = require("node:os");
|
||||
const timers = require("node:timers");
|
||||
|
||||
@@ -157,6 +157,19 @@ function call(id, sync, file, specifier, name, argsJson) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ── island modules (scope-B NI-4) ──────────────────────────────────────
|
||||
// Island code loaded by absolute path: require (CommonJS, and require(esm)
|
||||
// for ES modules without top-level await) answers the exports; import
|
||||
// answers a promise of the namespace.
|
||||
const { pathToFileURL } = require("node:url");
|
||||
|
||||
function load(path, esm) {
|
||||
// A built-in module's name, with or without the node: prefix.
|
||||
if (isBuiltin(path)) return require(path);
|
||||
if (esm) return import(pathToFileURL(path).href);
|
||||
return createRequire(path)(path);
|
||||
}
|
||||
|
||||
// ── the hosted loop (scope-B NI-3) ─────────────────────────────────────
|
||||
// The runtime's ticks, timers and immediates are Node's, created here with
|
||||
// Node's own timers (captured now, so island code replacing the globals
|
||||
@@ -388,4 +401,5 @@ globalThis.__scriptcNodeIsland = {
|
||||
refImmediate,
|
||||
nextTick,
|
||||
emit,
|
||||
load,
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ static const char scr_node_island_host_js[] =
|
||||
" * them in-process under Node: both must produce the same results, errors\012"
|
||||
" * and messages. Outcomes are \"R<json>\" (returned; \"R\" alone is undefined)\012"
|
||||
" * or \"T<json>\" (threw), the encoding of scr_node_bridge.h. */\012"
|
||||
"const { createRequire, registerHooks } = require(\"node:module\");\012"
|
||||
"const { createRequire, isBuiltin, registerHooks } = require(\"node:module\");\012"
|
||||
"const { constants: osConstants } = require(\"node:os\");\012"
|
||||
"const timers = require(\"node:timers\");\012"
|
||||
"\012"
|
||||
@@ -163,6 +163,19 @@ static const char scr_node_island_host_js[] =
|
||||
" return undefined;\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"// \342\224\200\342\224\200 island modules (scope-B NI-4) \342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\012"
|
||||
"// Island code loaded by absolute path: require (CommonJS, and require(esm)\012"
|
||||
"// for ES modules without top-level await) answers the exports; import\012"
|
||||
"// answers a promise of the namespace.\012"
|
||||
"const { pathToFileURL } = require(\"node:url\");\012"
|
||||
"\012"
|
||||
"function load(path, esm) {\012"
|
||||
" // A built-in module's name, with or without the node: prefix.\012"
|
||||
" if (isBuiltin(path)) return require(path);\012"
|
||||
" if (esm) return import(pathToFileURL(path).href);\012"
|
||||
" return createRequire(path)(path);\012"
|
||||
"}\012"
|
||||
"\012"
|
||||
"// \342\224\200\342\224\200 the hosted loop (scope-B NI-3) \342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\012"
|
||||
"// The runtime's ticks, timers and immediates are Node's, created here with\012"
|
||||
"// Node's own timers (captured now, so island code replacing the globals\012"
|
||||
@@ -394,6 +407,7 @@ static const char scr_node_island_host_js[] =
|
||||
" refImmediate,\012"
|
||||
" nextTick,\012"
|
||||
" emit,\012"
|
||||
" load,\012"
|
||||
"};\012";
|
||||
|
||||
#endif /* SCR_NODE_ISLAND_HOST_H */
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
// fs.realpath 1.0.0 (ISC) and value-forms read builtin functions as values:
|
||||
// fs.realpathSync stored and compared, Buffer.alloc aliased, and a String
|
||||
// method read for a feature probe.
|
||||
// package.json keeps bound-builtins native: a fence in its module code
|
||||
// would run it in a Node island, and this program iterates the array its
|
||||
// report() returns, which native code cannot iterate across the island
|
||||
// boundary yet.
|
||||
import truncate from 'truncate-utf8-bytes';
|
||||
import { report, badLength } from 'bound-builtins';
|
||||
import realpath from 'fs.realpath';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "type": "module", "scriptc": { "packages": { "bound-builtins": "native" } } }
|
||||
@@ -0,0 +1,6 @@
|
||||
export const kind = "esm";
|
||||
export let counter = 0;
|
||||
export function bump() {
|
||||
return ++counter;
|
||||
}
|
||||
export default { name: "default export" };
|
||||
@@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
const shared = { count: 0, items: [] };
|
||||
|
||||
class Point {
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
norm() {
|
||||
return Math.hypot(this.x, this.y);
|
||||
}
|
||||
}
|
||||
|
||||
exports.shared = shared;
|
||||
exports.Point = Point;
|
||||
exports.getShared = () => shared;
|
||||
exports.bump = () => ++shared.count;
|
||||
exports.read = (o, k) => o[k];
|
||||
exports.same = (a, b) => a === b;
|
||||
exports.makeCounter = () => {
|
||||
let n = 0;
|
||||
return { inc: () => ++n, get value() { return n; } };
|
||||
};
|
||||
exports.add = (a, b) => a + b;
|
||||
exports.numbers = () => [-0, NaN, Infinity, 2 ** 53 + 2, 10n ** 30n, -7n];
|
||||
exports.isNegZero = (x) => Object.is(x, -0);
|
||||
exports.strings = () => ["plain", "wörld ☃ 🌍", "lone \ud800 high", "\udc00 lone low"];
|
||||
exports.codeUnits = (s) => Array.from(s, (c) => c.charCodeAt(0).toString(16)).join(" ");
|
||||
exports.describe = (v) => `${typeof v} ${Object.prototype.toString.call(v)}`;
|
||||
exports.keysOf = (o) => Object.keys(o).join(",");
|
||||
@@ -0,0 +1,62 @@
|
||||
// @requires-libnode
|
||||
// Island objects held by reference (scope-B NI-4): one handle per Node
|
||||
// object, so identity, aliasing and mutation are shared with Node, while
|
||||
// primitives, strings and BigInt cross as exact copies.
|
||||
import { requireModule } from "@scriptc/node";
|
||||
|
||||
const lib = requireModule("./lib.cjs");
|
||||
|
||||
// Identity: the same Node object is the same value on both sides.
|
||||
const a = lib.getShared();
|
||||
const b = lib.shared;
|
||||
console.log("same handle", a === b, lib.same(a, b), lib.same(a, lib.getShared()));
|
||||
console.log("distinct", lib.makeCounter() === lib.makeCounter());
|
||||
|
||||
// Mutation is visible on both sides.
|
||||
a.count = 41;
|
||||
console.log("island sees", lib.read(b, "count"), "bump", lib.bump(), "native sees", a.count);
|
||||
a.items.push("x");
|
||||
lib.shared.items.push("y");
|
||||
console.log("items", a.items.length, b.items.join("+"), lib.read(lib.shared.items, 1));
|
||||
a.extra = { nested: true };
|
||||
console.log("keys", lib.keysOf(a), Object.keys(b).join(","));
|
||||
console.log("in", "extra" in a, "missing" in a);
|
||||
delete a.extra;
|
||||
console.log("deleted", "extra" in b, lib.keysOf(a));
|
||||
|
||||
// Closures and getters live in Node.
|
||||
const counter = lib.makeCounter();
|
||||
counter.inc();
|
||||
counter.inc();
|
||||
console.log("counter", counter.inc(), counter.value);
|
||||
|
||||
// Functions as values, typeof, classes, instanceof.
|
||||
const add = lib.add;
|
||||
console.log("call", add(2, 3), typeof add, typeof a, typeof lib.Point);
|
||||
const p = new lib.Point(3, 4);
|
||||
console.log("point", p.x, p.y, p.norm(), p instanceof lib.Point, a instanceof lib.Point);
|
||||
console.log("describe", lib.describe(p), lib.describe(add));
|
||||
|
||||
// Exact numbers and BigInt.
|
||||
const nums = lib.numbers();
|
||||
const negZero = nums[0];
|
||||
console.log("numbers", Object.is(negZero, -0), lib.isNegZero(negZero), Number.isNaN(nums[1]), nums[2], nums[3]);
|
||||
console.log("bigint", nums[4], typeof nums[4], nums[4] + 1n, lib.add(nums[5], 2n));
|
||||
console.log("negzero in", lib.isNegZero(-0), lib.isNegZero(0));
|
||||
|
||||
// Strings, lone surrogates included.
|
||||
const strs = lib.strings();
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const s = strs[i];
|
||||
console.log("string", s.length, lib.codeUnits(s) === lib.codeUnits(strs[i]), lib.same(s, strs[i]));
|
||||
}
|
||||
console.log("native string", lib.codeUnits("a\ud800b"), lib.add("x", "é"));
|
||||
|
||||
// util.inspect of island objects is Node's own.
|
||||
console.log(p);
|
||||
console.log(counter, [1, 2]);
|
||||
console.log("inspect", lib.makeCounter());
|
||||
|
||||
// ES modules load through require(esm): the namespace is a live handle.
|
||||
const esm = requireModule("./esm.mjs");
|
||||
console.log("esm", esm.kind, esm.default.name, esm.bump(), esm.bump(), esm.counter);
|
||||
@@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
class NotFound extends Error {
|
||||
constructor(what) {
|
||||
super(`${what} not found`);
|
||||
this.name = "NotFound";
|
||||
this.code = "E_NOT_FOUND";
|
||||
}
|
||||
}
|
||||
let last;
|
||||
exports.NotFound = NotFound;
|
||||
exports.fail = (kind) => {
|
||||
if (kind === "type") {
|
||||
const e = new TypeError("bad type");
|
||||
e.code = "ERR_BAD";
|
||||
last = e;
|
||||
throw e;
|
||||
}
|
||||
if (kind === "range") throw (last = new RangeError("out of range"));
|
||||
if (kind === "custom") throw (last = new NotFound("thing"));
|
||||
if (kind === "string") throw "just a string";
|
||||
if (kind === "object") throw (last = { reason: "plain object" });
|
||||
return "no throw";
|
||||
};
|
||||
exports.isLast = (e) => e === last;
|
||||
exports.inspectError = (e) =>
|
||||
`${e instanceof Error} ${e.constructor.name} ${e.name} ${JSON.stringify(e.message)} ${e.code} ${typeof e.stack}`;
|
||||
exports.rethrow = (e) => {
|
||||
throw e;
|
||||
};
|
||||
exports.call = (o, m) => o[m]();
|
||||
@@ -0,0 +1,61 @@
|
||||
// @requires-libnode
|
||||
// Errors across the island boundary (scope-B NI-4): an island Error caught
|
||||
// in the program keeps its class, name, message, code and stack, and is the
|
||||
// same Node object when it crosses back; a program error passed into Node
|
||||
// becomes a Node error with the same name, message and code.
|
||||
import { requireModule } from "@scriptc/node";
|
||||
|
||||
const lib = requireModule("./lib.cjs");
|
||||
|
||||
for (const kind of ["type", "range", "custom"]) {
|
||||
try {
|
||||
lib.fail(kind);
|
||||
} catch (e) {
|
||||
console.log(kind, e instanceof Error, e instanceof TypeError, e instanceof RangeError);
|
||||
console.log(" ", e.name, e.message, e.code);
|
||||
console.log(" stack head", String(e.stack).split("\n")[0]);
|
||||
console.log(" stack has island frame", String(e.stack).includes("lib.cjs"));
|
||||
console.log(" same object in Node", lib.isLast(e), lib.inspectError(e));
|
||||
try {
|
||||
lib.rethrow(e);
|
||||
} catch (again) {
|
||||
console.log(" rethrown", again === e, again.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
lib.fail("string");
|
||||
} catch (e) {
|
||||
console.log("string", typeof e, e);
|
||||
}
|
||||
try {
|
||||
lib.fail("object");
|
||||
} catch (e) {
|
||||
console.log("object", typeof e, e.reason, lib.isLast(e));
|
||||
}
|
||||
console.log(lib.fail("none"));
|
||||
|
||||
// Program errors crossing into Node.
|
||||
const mine = new RangeError("from the program");
|
||||
console.log("native", lib.inspectError(mine));
|
||||
const coded = new Error("with code");
|
||||
coded.code = "E_MINE";
|
||||
console.log("native coded", lib.inspectError(coded));
|
||||
try {
|
||||
lib.rethrow(new TypeError("round trip"));
|
||||
} catch (e) {
|
||||
console.log("round trip", e instanceof TypeError, e.message);
|
||||
}
|
||||
|
||||
// Errors from calling missing members.
|
||||
try {
|
||||
lib.call({}, "nope");
|
||||
} catch (e) {
|
||||
console.log("missing method", e instanceof TypeError, e.message);
|
||||
}
|
||||
try {
|
||||
lib.nope();
|
||||
} catch (e) {
|
||||
console.log("missing export", e instanceof TypeError, e.message);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
"use strict";
|
||||
let kept;
|
||||
exports.makeBuffer = () => (kept = Buffer.from("hello"));
|
||||
exports.kept = () => kept;
|
||||
exports.read = (b) => `${b.constructor.name} ${Buffer.isBuffer(b)} [${Array.from(b).join(",")}]`;
|
||||
exports.fill = (b, v) => {
|
||||
b.fill(v);
|
||||
return b.length;
|
||||
};
|
||||
exports.same = (a, b) => a === b;
|
||||
exports.floats = () => new Float64Array([1.5, -0, NaN]);
|
||||
exports.sum = (arr) => arr.reduce((a, b) => a + b, 0);
|
||||
@@ -0,0 +1,31 @@
|
||||
// @requires-libnode
|
||||
// Byte stores across the island boundary (scope-B NI-4): a Buffer or typed
|
||||
// array crossing either way shares its memory, so writes on one side are
|
||||
// visible on the other without copies.
|
||||
import { requireModule } from "@scriptc/node";
|
||||
|
||||
const lib = requireModule("./lib.cjs");
|
||||
|
||||
const buf = lib.makeBuffer();
|
||||
console.log("from island", buf.length, buf.toString(), Buffer.isBuffer(buf));
|
||||
buf[0] = 72 - 32 + 32; // 'H'
|
||||
buf[1] = 69;
|
||||
console.log("island sees native write", lib.read(lib.kept()));
|
||||
lib.fill(lib.kept(), 120);
|
||||
console.log("native sees island write", buf.toString(), buf[4]);
|
||||
console.log("same buffer back", lib.same(buf, lib.kept()), buf === lib.kept());
|
||||
|
||||
const mine = new Uint8Array([1, 2, 3]);
|
||||
console.log("into island", lib.read(mine));
|
||||
lib.fill(mine, 9);
|
||||
console.log("native sees fill", mine.join(","));
|
||||
|
||||
const nb = Buffer.from("abc");
|
||||
lib.fill(nb, 122);
|
||||
console.log("native buffer", nb.toString(), lib.read(nb));
|
||||
|
||||
const floats = lib.floats();
|
||||
console.log("floats", floats.length, floats[0], Object.is(floats[1], -0), Number.isNaN(floats[2]));
|
||||
floats[0] = 2.5;
|
||||
floats[2] = 4;
|
||||
console.log("float sum in island", lib.sum(floats));
|
||||
@@ -0,0 +1,10 @@
|
||||
"use strict";
|
||||
let pending;
|
||||
exports.later = (v, ms) => new Promise((resolve) => setTimeout(() => resolve(v), ms));
|
||||
exports.fails = async (m) => {
|
||||
await null;
|
||||
throw new RangeError(m);
|
||||
};
|
||||
exports.object = async () => ({ answer: 42 });
|
||||
exports.keep = () => (pending = Promise.resolve("kept"));
|
||||
exports.isKept = (p) => p === pending;
|
||||
@@ -0,0 +1,24 @@
|
||||
// @requires-libnode
|
||||
// Island promises as program promises (scope-B NI-4): awaiting one settles
|
||||
// through a V8 reaction on the one hosted loop; rejections keep their
|
||||
// error; the same Node promise is the same program promise.
|
||||
import { requireModule } from "@scriptc/node";
|
||||
|
||||
const lib = requireModule("./lib.cjs");
|
||||
|
||||
async function main() {
|
||||
console.log("later", await lib.later("value", 5));
|
||||
try {
|
||||
await lib.fails("nope");
|
||||
} catch (e) {
|
||||
console.log("rejected", e instanceof RangeError, e.message);
|
||||
}
|
||||
const o = await lib.object();
|
||||
console.log("object", o.answer, typeof o);
|
||||
const p = lib.keep();
|
||||
console.log("promise identity", lib.isKept(p), await p);
|
||||
const all = await Promise.all([lib.later(1, 3), lib.later(2, 1)]);
|
||||
console.log("all", all.join(","));
|
||||
}
|
||||
|
||||
main().then(() => console.log("done"));
|
||||
@@ -0,0 +1,84 @@
|
||||
// @requires-libnode
|
||||
// An npm package scriptc refuses natively (its loader uses a computed
|
||||
// require() specifier) runs in a Node island: its objects cross as
|
||||
// handles, so the same object returned twice is ===, and writes through
|
||||
// one reference are visible through every other, on both sides.
|
||||
import { Store, createStore, describe, VERSION, makePoint, addPoints, ping, makeTricky, type Tricky } from "islandkit";
|
||||
import * as kit from "islandkit";
|
||||
|
||||
console.log(VERSION);
|
||||
const s = createStore("fruits");
|
||||
const apple = s.add("apple", 3);
|
||||
const banana = s.add("banana", 5);
|
||||
console.log(s.name, s.size, s.total());
|
||||
|
||||
const again = s.get("apple");
|
||||
console.log("identity", apple === again, apple !== banana, again === s.first());
|
||||
console.log("owner", apple.owner === s, banana.owner === s);
|
||||
|
||||
apple.value = 10;
|
||||
console.log("alias", s.get("apple")!.value, s.total());
|
||||
|
||||
s.rename(apple, "apricot");
|
||||
console.log(apple.name, describe(apple), s.get("apricot") === apple);
|
||||
|
||||
const missing = s.get("cherry");
|
||||
console.log("missing", missing === undefined);
|
||||
|
||||
const veg = new Store("veg");
|
||||
veg.add("kale", 1);
|
||||
console.log(veg.name, veg.size, veg instanceof Store, s instanceof kit.Store);
|
||||
console.log(veg.first() !== null, new Store("empty").first() === null);
|
||||
|
||||
const p = addPoints(makePoint(1, 2), { x: 3, y: 4 });
|
||||
console.log(p.x, p.y);
|
||||
p.x = 100;
|
||||
console.log(p.x + p.y);
|
||||
|
||||
// JavaScript reads `tricky.method` before it evaluates `+swapper`, whose
|
||||
// valueOf replaces the method: the call must reach the original.
|
||||
class Swapper {
|
||||
target: Tricky;
|
||||
constructor(target: Tricky) {
|
||||
this.target = target;
|
||||
}
|
||||
valueOf(): number {
|
||||
this.target.method = this.target.other;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
const tricky = makeTricky();
|
||||
const swapper = new Swapper(tricky);
|
||||
console.log("unary", tricky.method(+swapper), tricky.method(1));
|
||||
|
||||
console.log(ping(), ping(), kit.ping());
|
||||
|
||||
try {
|
||||
s.add("", 1);
|
||||
} catch (e) {
|
||||
console.log("caught", (e as Error).message);
|
||||
}
|
||||
|
||||
function heaviest(store: Store, names: string[]): string {
|
||||
let best = "";
|
||||
let max = -Infinity;
|
||||
for (const n of names) {
|
||||
const e = store.get(n);
|
||||
if (e !== undefined && e.value > max) {
|
||||
max = e.value;
|
||||
best = e.name;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
console.log("heaviest", heaviest(s, ["apricot", "banana", "cherry"]));
|
||||
|
||||
// Native web streams and Node islands in one program select the async
|
||||
// runtime build that defines both (SCR_NATIVE_REACTIONS and
|
||||
// SCR_NODE_ISLANDS); without it the hosted-loop symbols are undefined.
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
console.log("stream", stream instanceof ReadableStream, stream.locked);
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
export interface Entry {
|
||||
name: string;
|
||||
value: number;
|
||||
readonly owner: Store;
|
||||
}
|
||||
export interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
export declare class Store {
|
||||
constructor(name: string);
|
||||
readonly name: string;
|
||||
readonly size: number;
|
||||
add(name: string, value: number): Entry;
|
||||
get(name: string): Entry | undefined;
|
||||
total(): number;
|
||||
rename(entry: Entry, name: string): void;
|
||||
first(): Entry | null;
|
||||
}
|
||||
export declare const VERSION: string;
|
||||
export declare function createStore(name: string): Store;
|
||||
export declare function describe(entry: Entry): string;
|
||||
export declare function makePoint(x: number, y: number): Point;
|
||||
export declare function addPoints(a: Point, b: Point): Point;
|
||||
export declare let loads: number;
|
||||
export declare function ping(): number;
|
||||
export interface Tricky {
|
||||
method(value: number): string;
|
||||
other(): string;
|
||||
}
|
||||
export declare function makeTricky(): Tricky;
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
"use strict";
|
||||
// A small object API whose loader picks its parts with a computed
|
||||
// require() specifier: scriptc cannot compile that natively (R1.2), so the
|
||||
// package runs in a Node island. Its objects keep their identity across
|
||||
// the boundary.
|
||||
const { createRequire } = require("node:module");
|
||||
const load = createRequire(__filename);
|
||||
const parts = ["./lib/format", "./lib/store"].map((p) => load(p));
|
||||
const [format, store] = parts;
|
||||
|
||||
exports.VERSION = "islandkit " + require("./package.json").version;
|
||||
exports.Store = store.Store;
|
||||
exports.createStore = (name) => new store.Store(name);
|
||||
exports.describe = format.describe;
|
||||
exports.makePoint = (x, y) => ({ x, y });
|
||||
exports.addPoints = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
|
||||
exports.loads = 0;
|
||||
exports.ping = () => ++exports.loads;
|
||||
exports.makeTricky = () => ({
|
||||
method() {
|
||||
return "original";
|
||||
},
|
||||
other() {
|
||||
return "replaced";
|
||||
},
|
||||
});
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
exports.describe = (entry) => `${entry.name}=${entry.value} in ${entry.owner.name}`;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
"use strict";
|
||||
class Store {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.entries = new Map();
|
||||
}
|
||||
get size() {
|
||||
return this.entries.size;
|
||||
}
|
||||
add(name, value) {
|
||||
if (name === "") throw new TypeError("entry names must not be empty");
|
||||
const entry = { name, value, owner: this };
|
||||
this.entries.set(name, entry);
|
||||
return entry;
|
||||
}
|
||||
get(name) {
|
||||
return this.entries.get(name);
|
||||
}
|
||||
total() {
|
||||
let sum = 0;
|
||||
for (const e of this.entries.values()) sum += e.value;
|
||||
return sum;
|
||||
}
|
||||
rename(entry, name) {
|
||||
this.entries.delete(entry.name);
|
||||
entry.name = name;
|
||||
this.entries.set(name, entry);
|
||||
}
|
||||
first() {
|
||||
return this.entries.values().next().value ?? null;
|
||||
}
|
||||
}
|
||||
exports.Store = Store;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "islandkit",
|
||||
"version": "1.2.0",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// @requires-libnode
|
||||
// An ES module package scriptc refuses natively (it imports its parts
|
||||
// with a computed specifier) runs in a Node island: named imports are live
|
||||
// bindings, the default export and the namespace are the module's own, and
|
||||
// a later import() answers the same module instance.
|
||||
import greet, { count, bump, Counter, loadPart, shared, hit } from "esmisland";
|
||||
import * as ns from "esmisland";
|
||||
|
||||
console.log(greet("island"), count);
|
||||
bump();
|
||||
bump();
|
||||
console.log("live", count, ns.count);
|
||||
|
||||
const c = new Counter(5);
|
||||
c.inc();
|
||||
console.log(c.value, c instanceof Counter, c instanceof ns.Counter);
|
||||
console.log(hit() === shared, shared.hits, ns.shared === shared);
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const label = await loadPart("a");
|
||||
console.log(label);
|
||||
const again = await import("esmisland");
|
||||
console.log(again.count, again.shared === shared, again.Counter === Counter);
|
||||
again.bump();
|
||||
console.log("after", count);
|
||||
}
|
||||
void main();
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
export declare let count: number;
|
||||
export declare function bump(): number;
|
||||
export declare class Counter {
|
||||
constructor(start: number);
|
||||
value: number;
|
||||
inc(): number;
|
||||
}
|
||||
export default function greet(name: string): string;
|
||||
export declare function loadPart(name: string): Promise<string>;
|
||||
export interface Shared {
|
||||
hits: number;
|
||||
}
|
||||
export declare const shared: Shared;
|
||||
export declare function hit(): Shared;
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// An ES module whose parts load through import() with a computed
|
||||
// specifier: scriptc cannot compile that natively (R1.2), so it runs in a
|
||||
// Node island. Its live bindings, classes and shared objects keep their
|
||||
// meaning across the boundary.
|
||||
export let count = 0;
|
||||
export function bump() {
|
||||
count++;
|
||||
return count;
|
||||
}
|
||||
export class Counter {
|
||||
constructor(start) {
|
||||
this.value = start;
|
||||
}
|
||||
inc() {
|
||||
return ++this.value;
|
||||
}
|
||||
}
|
||||
export default function greet(name) {
|
||||
return `hello ${name}`;
|
||||
}
|
||||
export async function loadPart(name) {
|
||||
const part = await import(`./parts/${name}.js`);
|
||||
return part.label;
|
||||
}
|
||||
export const shared = { hits: 0 };
|
||||
export function hit() {
|
||||
shared.hits++;
|
||||
return shared;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "esmisland",
|
||||
"version": "0.3.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./index.d.ts",
|
||||
"default": "./index.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const label = "part a";
|
||||
@@ -0,0 +1,21 @@
|
||||
// @requires-libnode
|
||||
// tsconfig `paths` maps an island package to its installed directory (a
|
||||
// monorepo pinning one copy): its import resolves to the package's
|
||||
// declarations, which type the program, and the package loads in Node.
|
||||
import { Paths, heavier, route, type Route } from "pathkit";
|
||||
|
||||
interface Table<T extends Route> {
|
||||
title: string;
|
||||
rows: T[];
|
||||
}
|
||||
|
||||
function render<T extends Route>(table: Table<T>): string {
|
||||
return `${table.title}: ${table.rows.map((r) => r.label()).join(", ")}`;
|
||||
}
|
||||
|
||||
const routes: Route[] = [route("/a", 1), route("/b", 3), route("/c", 2)];
|
||||
const table: Table<Route> = { title: "routes", rows: routes };
|
||||
console.log(render(table));
|
||||
const top = heavier(routes[0]!, routes[1]!);
|
||||
console.log(top.rank, top.label(), top === routes[1], routes.length);
|
||||
console.log(Paths.join("api", "v1"));
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
export interface Route {
|
||||
readonly path: string;
|
||||
readonly weight: number;
|
||||
label(): string;
|
||||
}
|
||||
export type Weighted<T extends Route> = T & { readonly rank: number };
|
||||
export declare function route(path: string, weight: number): Route;
|
||||
export declare function heavier<T extends Route>(a: T, b: T): Weighted<T>;
|
||||
import * as Paths from "./paths.mjs";
|
||||
export { Paths };
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// The loader requires its part with spread arguments, which scriptc does
|
||||
// not compile natively: the package runs in a Node island.
|
||||
import { createRequire } from "node:module";
|
||||
const load = createRequire(import.meta.url);
|
||||
const { Route } = load(...["./route.cjs"]);
|
||||
export function route(path, weight) {
|
||||
return new Route(path, weight);
|
||||
}
|
||||
export function heavier(a, b) {
|
||||
return Object.assign(b.weight > a.weight ? b : a, { rank: 1 });
|
||||
}
|
||||
import * as Paths from "./paths.mjs";
|
||||
export { Paths };
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function join(...parts: string[]): string;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export function join(...parts) {
|
||||
return "/" + parts.join("/");
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
"use strict";
|
||||
class Route {
|
||||
constructor(path, weight) {
|
||||
this.path = path;
|
||||
this.weight = weight;
|
||||
}
|
||||
label() {
|
||||
return this.path + "@" + this.weight;
|
||||
}
|
||||
}
|
||||
exports.Route = Route;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "pathkit",
|
||||
"version": "2.0.0",
|
||||
"type": "module",
|
||||
"types": "./build/index.d.mts",
|
||||
"exports": { ".": { "import": "./build/index.mjs", "default": "./build/index.mjs" } }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"pathkit": ["./node_modules/pathkit"]
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// requireModule() of Node's built-in modules, named with and without the
|
||||
// node: prefix.
|
||||
import { requireModule } from "@scriptc/node";
|
||||
|
||||
const path = requireModule("path");
|
||||
const os = requireModule("node:os");
|
||||
console.log(path.join("a", "b"), typeof os.platform());
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
"use strict";
|
||||
exports.make = (i) => ({ i, payload: new Array(16).fill(i), tag: `object ${i}` });
|
||||
exports.buffer = (i) => Buffer.alloc(64, i & 0xff);
|
||||
exports.fail = (i) => {
|
||||
throw new Error(`failure ${i}`);
|
||||
};
|
||||
exports.later = async (i) => i;
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// Island objects the program drops are released (scope-B NI-4, acyclic
|
||||
// garbage): after many iterations that each take handles to fresh Node
|
||||
// objects, buffers, errors and promises, the import table holds only what
|
||||
// the program still references. node-island-handles.test.ts reads the
|
||||
// count SCRIPTC_NODE_ISLANDS_TRACE=handles prints at exit.
|
||||
import { requireModule } from "@scriptc/node";
|
||||
|
||||
const lib = requireModule("./handles-leak.cjs");
|
||||
const n = Number(process.argv[2] ?? "20000");
|
||||
let sum = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const o = lib.make(i);
|
||||
sum += o.i + o.payload.length;
|
||||
const b = lib.buffer(i);
|
||||
sum += b[0];
|
||||
try {
|
||||
lib.fail(i);
|
||||
} catch (e) {
|
||||
sum += e.message.length;
|
||||
}
|
||||
}
|
||||
let awaited = 0;
|
||||
for (let i = 0; i < 1000; i++) awaited += await lib.later(i);
|
||||
const kept = lib.make(-1);
|
||||
console.log("sum", sum, awaited, kept.tag);
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// JavaScript's evaluation order on island objects: a method call reads the
|
||||
// method before its arguments run, and an assignment evaluates the receiver
|
||||
// and the key before the value.
|
||||
import { make, type Thing } from "orderkit";
|
||||
|
||||
const o = make();
|
||||
// The argument replaces the method; the call still runs the one it read.
|
||||
console.log((o.m as (...args: unknown[]) => string)((o.m = 0), 1));
|
||||
|
||||
const trace: string[] = [];
|
||||
const target = make();
|
||||
function receiver(): Thing {
|
||||
trace.push("receiver");
|
||||
return target;
|
||||
}
|
||||
function key(): string {
|
||||
trace.push("key");
|
||||
return "k";
|
||||
}
|
||||
function value(): number {
|
||||
trace.push("value");
|
||||
return 1;
|
||||
}
|
||||
receiver()[key()] = value();
|
||||
receiver().p = value();
|
||||
console.log(trace.join(","), target["k"], target["p"]);
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export declare class Thing {
|
||||
m: ((...args: unknown[]) => string) | number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
export declare function make(): Thing;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
"use strict";
|
||||
// Loads its part through a computed specifier when it loads, so it runs in
|
||||
// a Node island.
|
||||
const { createRequire } = require("node:module");
|
||||
const parts = [createRequire(__filename)(["./thing"][0])];
|
||||
exports.make = () => new parts[0].Thing();
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "orderkit", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
"use strict";
|
||||
class Thing {
|
||||
m() {
|
||||
return "original";
|
||||
}
|
||||
}
|
||||
exports.Thing = Thing;
|
||||
@@ -0,0 +1,7 @@
|
||||
// Re-exports from an island package that tsconfig `paths` maps to its
|
||||
// installed directory: the type-only one is erased, the value one reports
|
||||
// the island boundary, and the package's declarations never compile as a
|
||||
// program module. The test builds it beside corpus 6839's pathkit package.
|
||||
export type { Route } from "pathkit";
|
||||
export { route } from "pathkit";
|
||||
console.log("loaded");
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2000, "san": 30000 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2000, "san": 30000 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2000, "san": 30000 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2000, "san": 30000 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3300, "san": 30000 }
|
||||
@@ -0,0 +1,217 @@
|
||||
/* Island object handles (scope-B NI-4) where a differential corpus program
|
||||
* cannot show it: handles to Node objects the program dropped are released,
|
||||
* so the import table does not grow with the work a program does (acyclic
|
||||
* garbage; cycles across both heaps wait for NI-8). The corpus (6812-6815)
|
||||
* pins handle semantics against Node. An island package ships with the
|
||||
* program, so the executable runs without the tree it was built from.
|
||||
* Needs a libnode pack in SCRIPTC_LIBNODE (libnode.ts). */
|
||||
import { execFile } from "node:child_process";
|
||||
import { cpSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { compile } from "@scriptc/compiler";
|
||||
import { libnodePack } from "./libnode.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = join(import.meta.dirname, "../..");
|
||||
const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/node-island-handles");
|
||||
const sanitize = process.env["SCRIPTC_SAN"] === "1";
|
||||
const skip = libnodePack() === null;
|
||||
|
||||
describe.skipIf(skip)("Node island handles", () => {
|
||||
test("handles the program dropped are released", async () => {
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const result = await compile(join(repoRoot, "tests/fixtures/node-island/handles-leak.mjs"), {
|
||||
outPath: join(outDir, "handles-leak"),
|
||||
outDir,
|
||||
sanitize,
|
||||
});
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const env = { ...process.env, SCRIPTC_NODE_ISLANDS_TRACE: "handles" };
|
||||
const counts: number[] = [];
|
||||
for (const n of ["2000", "20000"]) {
|
||||
const { stdout, stderr } = await execFileAsync(result.binaryPath, [n], {
|
||||
env,
|
||||
encoding: "utf8",
|
||||
});
|
||||
expect(stdout).toMatch(/^sum \d+ 499500 object -1\n$/);
|
||||
const live = /\[node-islands\] live handles (\d+)/.exec(stderr);
|
||||
expect(live).not.toBeNull();
|
||||
counts.push(Number(live![1]));
|
||||
}
|
||||
// The module and the kept object (with util.inspect, at most a few
|
||||
// more), independent of the number of iterations.
|
||||
expect(counts[0]).toBe(counts[1]);
|
||||
expect(counts[1]).toBeLessThan(8);
|
||||
}, 300_000);
|
||||
|
||||
async function runFixture(name: string, entry: string): Promise<string> {
|
||||
const dir = join(outDir, name);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const result = await compile(join(repoRoot, "tests/fixtures/node-island", entry), {
|
||||
outPath: join(dir, name),
|
||||
outDir: dir,
|
||||
sanitize,
|
||||
});
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const env = { ...process.env, XDG_CACHE_HOME: join(dir, "cache") };
|
||||
return (await execFileAsync(result.binaryPath, [], { env, encoding: "utf8" })).stdout;
|
||||
}
|
||||
|
||||
test("island member calls and assignments evaluate in JavaScript's order", async () => {
|
||||
expect(await runFixture("order", "order/main.ts")).toBe(
|
||||
"original\nreceiver,key,value,receiver,value 1 1\n",
|
||||
);
|
||||
}, 300_000);
|
||||
|
||||
test("requireModule() loads Node's built-in modules", async () => {
|
||||
expect(await runFixture("builtins", "builtins.ts")).toBe("a/b string\n");
|
||||
}, 300_000);
|
||||
|
||||
/** Runs a binary that refuses at run time: its stderr. */
|
||||
async function refusal(binary: string): Promise<string> {
|
||||
const run = await execFileAsync(binary, [], { encoding: "utf8" }).catch(
|
||||
(e: { stderr?: string }) => e,
|
||||
);
|
||||
return String((run as { stderr?: string }).stderr ?? "");
|
||||
}
|
||||
|
||||
test("a package marked native never runs in a Node island", async () => {
|
||||
// esmbridge's native build has deferred refusals (its CommonJS exports
|
||||
// object read as a value, an Object.keys form), so by default it runs in
|
||||
// a Node island; marked native, it compiles natively and refuses at run
|
||||
// time.
|
||||
const dir = join(outDir, "native-policy");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const entry = join(repoRoot, "tests/fixtures/npm/cases/cjs-esm-named/main.ts");
|
||||
const islanded = await compile(entry, { outPath: join(dir, "main"), outDir: dir, sanitize });
|
||||
// The SC8001 notes that name esmbridge.
|
||||
const islandNotes = (r: typeof islanded): string[] =>
|
||||
r.ok
|
||||
? (r.warnings ?? [])
|
||||
.filter((d) => d.code === "SC8001" && d.message.includes("'esmbridge@"))
|
||||
.map((d) => d.message)
|
||||
: [];
|
||||
expect(islanded.ok).toBe(true);
|
||||
expect(islandNotes(islanded)).toHaveLength(1);
|
||||
const marked = await compile(entry, {
|
||||
outPath: join(dir, "marked"),
|
||||
outDir: dir,
|
||||
sanitize,
|
||||
nodeIslands: { native: ["esmbridge"] },
|
||||
});
|
||||
expect(marked.ok).toBe(true);
|
||||
expect(islandNotes(marked)).toEqual([]);
|
||||
if (marked.ok)
|
||||
expect(await refusal(marked.binaryPath)).toMatch(/\[SC\d{4} at [^\]]*\/esmbridge\//);
|
||||
}, 300_000);
|
||||
|
||||
/** The SC8001 notes of a build that name `pkg`. */
|
||||
async function islandNotesOf(entry: string, name: string, pkg: string): Promise<string[]> {
|
||||
const dir = join(outDir, name);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const result = await compile(join(repoRoot, entry), {
|
||||
outPath: join(dir, name),
|
||||
outDir: dir,
|
||||
sanitize,
|
||||
});
|
||||
expect(result.ok, name).toBe(true);
|
||||
if (!result.ok) return [];
|
||||
return (result.warnings ?? [])
|
||||
.filter((d) => d.code === "SC8001" && d.message.includes(`'${pkg}@`))
|
||||
.map((d) => d.message);
|
||||
}
|
||||
|
||||
test("a package whose fences may never run stays native", async () => {
|
||||
// jose's run-time fences are inside function bodies, on paths a program
|
||||
// may never call: only fences that surely fire make a Node island.
|
||||
expect(
|
||||
await islandNotesOf("tests/corpus/6331-jose-root-entry/main.ts", "jose-native", "jose"),
|
||||
).toEqual([]);
|
||||
}, 300_000);
|
||||
|
||||
test("a fence in module code makes a Node island, with the ES modules importing it", async () => {
|
||||
// cjszoo's module code sets its exports object's prototype (R2.2), so
|
||||
// its fence fires when it loads; esmbridge's ES module imports names
|
||||
// from it, which only Node's CommonJS facade gives as Node does.
|
||||
const entry = "tests/fixtures/npm/cases/cjs-esm-named/main.ts";
|
||||
const zoo = await islandNotesOf(entry, "fence-sure", "cjszoo");
|
||||
expect(zoo).toHaveLength(1);
|
||||
expect(zoo[0]).toContain("R2.2");
|
||||
const bridge = await islandNotesOf(entry, "fence-sure", "esmbridge");
|
||||
expect(bridge).toEqual([
|
||||
"package 'esmbridge@1.0.0' runs in a Node island: packaging: an ES module of the package imports the CommonJS island package 'cjszoo'",
|
||||
]);
|
||||
}, 300_000);
|
||||
|
||||
test("without islands, a package's deferred refusals refuse at run time", async () => {
|
||||
// With --node-islands=off, packages whose native builds have deferred
|
||||
// refusals compile natively and report them when they run: orderkit's
|
||||
// createRequire call with a computed specifier (SC2020), and the first
|
||||
// one cjs-esm-named reaches in its packages (esmbridge, cjszoo).
|
||||
const cases = [
|
||||
["order-off", "tests/fixtures/node-island/order/main.ts", /\[SC2020 at [^\]]*\/orderkit\//],
|
||||
[
|
||||
"esm-named-off",
|
||||
"tests/fixtures/npm/cases/cjs-esm-named/main.ts",
|
||||
/\[SC\d{4} at [^\]]*\/node_modules\/(?:esmbridge|cjszoo)\//,
|
||||
],
|
||||
] as const;
|
||||
for (const [name, entry, code] of cases) {
|
||||
const dir = join(outDir, name);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const result = await compile(join(repoRoot, entry), {
|
||||
outPath: join(dir, name),
|
||||
outDir: dir,
|
||||
sanitize,
|
||||
nodeIslands: { mode: "off" },
|
||||
});
|
||||
expect(result.ok, name).toBe(true);
|
||||
if (!result.ok) continue;
|
||||
expect((result.warnings ?? []).map((d) => d.code)).not.toContain("SC8001");
|
||||
expect(await refusal(result.binaryPath), name).toMatch(code);
|
||||
}
|
||||
}, 300_000);
|
||||
|
||||
test("an island package ships with the program", async () => {
|
||||
const root = join(outDir, "ships");
|
||||
const src = join(root, "src");
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
cpSync(join(repoRoot, "tests/corpus/6816-node-island-objects"), src, { recursive: true });
|
||||
const env = { ...process.env, XDG_CACHE_HOME: join(root, "cache") };
|
||||
const built: Record<string, string> = {};
|
||||
for (const libnode of ["inside", "beside"]) {
|
||||
const dir = join(root, libnode);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const result = await compile(join(src, "main.ts"), {
|
||||
outPath: join(dir, "main"),
|
||||
outDir: dir,
|
||||
sanitize,
|
||||
...(libnode === "beside" ? { libnode } : {}),
|
||||
});
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
built[libnode] = result.binaryPath;
|
||||
}
|
||||
const before = await execFileAsync(built["inside"]!, [], { env, encoding: "utf8" });
|
||||
expect(before.stdout).toMatch(/^islandkit \d/);
|
||||
// The executable names its island package by its path in the shipped
|
||||
// tree, never by the directory it was built from.
|
||||
expect(readFileSync(built["inside"]!).toString("latin1").includes(src)).toBe(false);
|
||||
// Beside the executable, the tree holds the package's files.
|
||||
const index = readFileSync(join(src, "node_modules/islandkit/index.js"), "utf8");
|
||||
const modules = join(root, "beside/main.libnode/modules");
|
||||
const files = readdirSync(modules, { recursive: true, encoding: "utf8" });
|
||||
expect(
|
||||
files.some((f) => f.endsWith("index.js") && readFileSync(join(modules, f), "utf8") === index),
|
||||
).toBe(true);
|
||||
renameSync(src, join(root, "moved"));
|
||||
for (const binary of Object.values(built)) {
|
||||
const after = await execFileAsync(binary, [], { env, encoding: "utf8" });
|
||||
expect(after.stdout).toBe(before.stdout);
|
||||
}
|
||||
}, 300_000);
|
||||
});
|
||||
@@ -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 4)\n`,
|
||||
`scriptc: the libnode pack at ${dir} has an incompatible bridge (ABI 999; this program needs 5)\n`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -354,3 +354,22 @@ test("recursive JSON types cross into Node", async () => {
|
||||
const ir = JSON.stringify(deserializeModule(readFileSync(outPath, "utf8")));
|
||||
expect(ir).toContain("nodeIsland.callSync");
|
||||
}, 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");
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
const corpus = join(repoRoot, "tests/corpus/6839-node-island-path-mapped");
|
||||
cpSync(join(corpus, "node_modules"), join(dir, "node_modules"), { recursive: true });
|
||||
copyFileSync(join(corpus, "tsconfig.json"), join(dir, "tsconfig.json"));
|
||||
copyFileSync(
|
||||
join(repoRoot, "tests/fixtures/node-island/path-mapped-reexport.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;
|
||||
expect(result.diagnostics.map((d) => `${d.code} ${d.loc.file.slice(dir.length + 1)}`)).toEqual([
|
||||
"SC8010 main.ts",
|
||||
]);
|
||||
}, 300_000);
|
||||
|
||||
@@ -15,6 +15,7 @@ import { promisify } from "node:util";
|
||||
import { beforeEach, describe, expect, test } from "vitest";
|
||||
import { analyze, compile } from "@scriptc/compiler";
|
||||
import { skipUnownedTest } from "./shard.js";
|
||||
import { libnodePack } from "./libnode.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = join(import.meta.dirname, "../..");
|
||||
@@ -632,7 +633,10 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("a word segmenter in a package constructs, and its segment() throws SC2020", async () => {
|
||||
// The package builds its segmenter at module load, a run-time fence that
|
||||
// surely fires, so the package runs in its Node island and segments as
|
||||
// Node does.
|
||||
test("a word segmenter a package builds at load runs in its Node island", async () => {
|
||||
const entry = join(pilotRoot, "word-segments-cli.ts");
|
||||
const { coverage } = analyze(entry, { npmStatic: "auto" });
|
||||
expect(coverage.npmStatic).toEqual([{ package: "word-segments", status: "static" }]);
|
||||
@@ -645,12 +649,15 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
|
||||
outDir,
|
||||
outPath: join(outDir, "program"),
|
||||
});
|
||||
if (libnodePack() === null) {
|
||||
expect(result.ok, "no libnode pack: the compile stops").toBe(false);
|
||||
if (!result.ok) expect(result.diagnostics.map((d) => d.code)).toContain("SC8005");
|
||||
return;
|
||||
}
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const nativeRes = await runBinary(result.binaryPath, []);
|
||||
expect(nativeRes.stdout.toString()).toMatch(
|
||||
/^SC2020: Intl\.Segmenter segmentation with granularity 'word' is not supported yet \[SC2020 at .*index\.js:1\]\n$/,
|
||||
);
|
||||
expect(nativeRes.stdout.toString()).toBe("two| |words\n");
|
||||
expect(nativeRes.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
|
||||
+43
-16
@@ -9,7 +9,10 @@
|
||||
* (a refusal deferred to the operation that reached it), for every argv
|
||||
* list or for the listed `argvs`, while the other lists match Node;
|
||||
* - divergence: it runs but does not match Node yet. These run as expected
|
||||
* failures, so a fix that makes one match fails here until KNOWN drops it.
|
||||
* failures, so a fix that makes one match fails here until KNOWN drops it;
|
||||
* - island: a package it imports runs in a Node island (scope-B NI-4): with
|
||||
* a libnode pack (SCRIPTC_LIBNODE) it matches Node; without one the build
|
||||
* stops with SC8005 (no pack to load Node from).
|
||||
*
|
||||
* The fixture node_modules are committed test data. Binaries compile the
|
||||
* package sources at build time and read nothing from node_modules at run
|
||||
@@ -24,6 +27,7 @@ import { describe, expect, test } from "vitest";
|
||||
import { compile } from "@scriptc/compiler";
|
||||
import { npmCases, type NpmCase } from "./npm-cases.js";
|
||||
import { shardSelect, shardSuffix } from "./shard.js";
|
||||
import { libnodePack } from "./libnode.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = join(import.meta.dirname, "../..");
|
||||
@@ -31,22 +35,26 @@ const fixturesRoot = join(repoRoot, "tests/fixtures");
|
||||
const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests");
|
||||
const sanitize = process.env["SCRIPTC_SAN"] === "1";
|
||||
|
||||
type Known = { build: string } | { run: string; argvs?: string[][] } | { divergence: string };
|
||||
type Known =
|
||||
| { build: string }
|
||||
| { run: string; argvs?: string[][] }
|
||||
| { divergence: string }
|
||||
| { island: true };
|
||||
|
||||
const KNOWN: Record<string, Known> = {
|
||||
"assert-shims": { run: "SC1043" },
|
||||
"buffer-shims": { run: "SC2020" },
|
||||
"bytes-exit": { build: "SC2004" },
|
||||
"cjs-esm-named": { run: "SC1090" },
|
||||
"cjs-lexer-aligned": { run: "SC1090" },
|
||||
"cjs-esm-named": { island: true },
|
||||
"cjs-lexer-aligned": { island: true },
|
||||
"commander-calc": { run: "SC1090", argvs: [["--help"]] },
|
||||
"commander-calc-async": { run: "SC1090", argvs: [["--help"]] },
|
||||
"create-require": { run: "SC1090" },
|
||||
"dotfolder": { run: "SC1090" },
|
||||
"dual-package-kinds": { build: "SC2013" },
|
||||
"dynamic-import": { build: "SC2012" },
|
||||
"dotfolder": { island: true },
|
||||
"dual-package-kinds": { island: true },
|
||||
"dynamic-import": { island: true },
|
||||
"dynamic-import-local": { build: "SC2012" },
|
||||
"esbuild-require": { build: "SC2013" },
|
||||
"esbuild-require": { island: true },
|
||||
"esm-missing-default": { build: "SC1090" },
|
||||
"esm-missing-export": { build: "SC1090" },
|
||||
"fs-shims": { run: "SC1101" },
|
||||
@@ -58,19 +66,25 @@ const KNOWN: Record<string, Known> = {
|
||||
},
|
||||
// require() inside a function.
|
||||
"island-web-plumbing": { run: "SC1090" },
|
||||
"lazy-traps": { build: "SC2013" },
|
||||
"lazy-traps": {
|
||||
divergence:
|
||||
"an island module loaded by path lists itself twice in MODULE_NOT_FOUND require stacks",
|
||||
},
|
||||
"misc-shims": { run: "SC1090" },
|
||||
"namespace": { build: "SC1013" },
|
||||
namespace: { island: true },
|
||||
"node24-async-events": { run: "SC2020" },
|
||||
"node24-commonjs-metadata": { run: "SC2020" },
|
||||
"node24-commonjs-metadata": {
|
||||
divergence:
|
||||
"a Node island (its require.cache use needs the runtime) loads its entry with itself as the CJS parent, so require stacks list it twice",
|
||||
},
|
||||
"node24-crypto-kdf": { run: "SC2020" },
|
||||
"node24-deep-equality": { run: "SC2020" },
|
||||
"node24-diagnostics-async": { run: "SC1090" },
|
||||
"node24-diagnostics-tracing": { run: "SC1090" },
|
||||
"node24-env": { run: "SC1090" },
|
||||
"node24-esm-metadata": { run: "SC1090" },
|
||||
"node24-esm-metadata": { island: true },
|
||||
"node24-event-emitter": { run: "SC2020" },
|
||||
"node24-file-url-conversion": { build: "SC1090" },
|
||||
"node24-file-url-conversion": { island: true },
|
||||
"node24-fs-descriptors": { run: "SC2020" },
|
||||
"node24-mime": { run: "SC2020" },
|
||||
"node24-mime-errors": { run: "SC2020" },
|
||||
@@ -87,16 +101,16 @@ const KNOWN: Record<string, Known> = {
|
||||
"node24-url-mutation": { run: "SC1090" },
|
||||
"node24-util-adapters": { run: "Object.create with this value is not supported yet" },
|
||||
"node24-util-types": { run: "SC1090" },
|
||||
"package-imports": { build: "SC2013" },
|
||||
"package-imports": { island: true },
|
||||
"promise-bridge": { build: "SC1090" },
|
||||
"self-name": { build: "SC2013" },
|
||||
"stream-shims": { run: "SC2020" },
|
||||
"util-shims": { run: "SC2020" },
|
||||
// The package redeclares the var it exported: Node copies the first value.
|
||||
"var-export-redeclared": { run: "SC1090" },
|
||||
"var-export-redeclared": { island: true },
|
||||
// `class Upper extends TransformStream`: extending a builtin class from JavaScript.
|
||||
"web-streams": { run: "SC1090" },
|
||||
"workspace-copied": { build: "SC2013" },
|
||||
"workspace-copied": { island: true },
|
||||
"workspace-unclean": { build: "SC2013" },
|
||||
};
|
||||
|
||||
@@ -222,6 +236,19 @@ describe(`npm native (${cases.length} programs${sanitize ? ", sanitized" : ""}${
|
||||
}, 120_000);
|
||||
continue;
|
||||
}
|
||||
if (known !== undefined && "island" in known) {
|
||||
test(`${c.name} (Node island)`, async () => {
|
||||
const built = await build(c.entry);
|
||||
if (libnodePack() === null) {
|
||||
expect(built.ok, "no libnode pack: the compile stops").toBe(false);
|
||||
if (!built.ok) expect(built.codes, built.text).toContain("SC8005");
|
||||
return;
|
||||
}
|
||||
if (!built.ok) throw new Error(`npm fixture failed to compile:\n${built.text}`);
|
||||
await expectMatchesNode(c, built.binary);
|
||||
}, 120_000);
|
||||
continue;
|
||||
}
|
||||
test(
|
||||
c.name,
|
||||
async () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user