mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Scope-B milestone NI-1, API side: the explicit call API that replaces the build container's hand-written node subprocess (api patch 09). - The new package @scriptc/node exports callSync(specifier, name, ...args) and call(...). The module loads with require() from the calling module; arguments and results cross as JSON copies; a thrown Error crosses as its nearest intrinsic class with its message and string code, other thrown values as JSON copies; callSync refuses a function that returns a promise. Its Node implementation runs the calls in-process with the same copies, so a program prints the same under Node and compiled. - The compiler lowers calls to the named or namespace imports (node-api-import.ts, lower-node-island.ts) to the nodeIsland.callSync and nodeIsland.call libCalls: the calling module's real path, the specifier, the export name and the boxed argument array, with the result checked against the declared type like `JSON.parse(s) as T`. Arguments and results whose types are not JSON data are refused at build time, and the functions and the namespace refuse as values. - Corpus programs 6800-6803 (sync and async results, errors, process.exit from Node with the program's exit listeners, Node's pending timers keeping the program alive) run under Node and as binaries that load libnode; `// @requires-libnode` skips them where SCRIPTC_LIBNODE names no pack (until the pack CI matrix publishes packs). The node-island harness covers loading Node only on the first call, the beside packaging, a missing pack, a pack of another Node version or bridge ABI, and the executable's symbols. - Release tooling publishes @scriptc/node with the other packages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
138 lines
4.0 KiB
JavaScript
138 lines
4.0 KiB
JavaScript
// @scriptc/node — the Node.js implementation.
|
|
//
|
|
// scriptc builds lower call() and callSync() to calls into Node.js loaded
|
|
// into the program (packages/runtime/src/scr_node_island.c and its
|
|
// bootstrap, scr_node_island_host.cjs). Under Node.js they run directly,
|
|
// with the same copies at the boundary, so a program prints the same
|
|
// either way. Keep the two in step: every message and copy rule here has
|
|
// its twin in the bootstrap.
|
|
|
|
import { createRequire } from "node:module";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
// Error classes that cross as copies, at the host's kind index.
|
|
const ERROR_CLASSES = [
|
|
Error,
|
|
TypeError,
|
|
RangeError,
|
|
SyntaxError,
|
|
undefined,
|
|
ReferenceError,
|
|
EvalError,
|
|
URIError,
|
|
];
|
|
const ERROR_KINDS = [
|
|
[TypeError, 1],
|
|
[RangeError, 2],
|
|
[SyntaxError, 3],
|
|
[ReferenceError, 5],
|
|
[EvalError, 6],
|
|
[URIError, 7],
|
|
];
|
|
|
|
/** The file of the code that called `api`. */
|
|
function callerFile(api) {
|
|
const savedPrepare = Error.prepareStackTrace;
|
|
const savedLimit = Error.stackTraceLimit;
|
|
let sites;
|
|
try {
|
|
Error.stackTraceLimit = 1;
|
|
Error.prepareStackTrace = (_error, callSites) => callSites;
|
|
const holder = {};
|
|
Error.captureStackTrace(holder, api);
|
|
sites = holder.stack;
|
|
} finally {
|
|
Error.prepareStackTrace = savedPrepare;
|
|
Error.stackTraceLimit = savedLimit;
|
|
}
|
|
const name = Array.isArray(sites) && sites.length > 0 ? sites[0].getFileName() : undefined;
|
|
if (typeof name !== "string")
|
|
throw new Error("@scriptc/node: cannot determine the calling module");
|
|
return name.startsWith("file:") ? fileURLToPath(name) : name;
|
|
}
|
|
|
|
const requires = new Map();
|
|
|
|
function exportedFunction(file, specifier, name) {
|
|
let load = requires.get(file);
|
|
if (load === undefined) {
|
|
load = createRequire(file);
|
|
requires.set(file, load);
|
|
}
|
|
const namespace = load(specifier);
|
|
const fn = namespace === null || namespace === undefined ? undefined : namespace[name];
|
|
if (typeof fn !== "function")
|
|
throw new TypeError(`'${name}' exported by '${specifier}' is not a function`);
|
|
return { fn, namespace };
|
|
}
|
|
|
|
function isThenable(value) {
|
|
return (
|
|
value !== null &&
|
|
(typeof value === "object" || typeof value === "function") &&
|
|
typeof value.then === "function"
|
|
);
|
|
}
|
|
|
|
function errorKind(error) {
|
|
for (const [constructor, kind] of ERROR_KINDS) if (error instanceof constructor) return kind;
|
|
return 0;
|
|
}
|
|
|
|
/** The copy of a thrown value that the caller sees. */
|
|
function copyThrown(error) {
|
|
if (error instanceof Error) {
|
|
const copy = new ERROR_CLASSES[errorKind(error)](String(error.message));
|
|
if (typeof error.code === "string") copy.code = error.code;
|
|
return copy;
|
|
}
|
|
let text;
|
|
try {
|
|
text = JSON.stringify(error);
|
|
} catch {
|
|
text = undefined;
|
|
}
|
|
return text === undefined ? undefined : JSON.parse(text);
|
|
}
|
|
|
|
/** The JSON copy of a result; throws what JSON.stringify throws. */
|
|
function copyResult(value) {
|
|
const text = JSON.stringify(value);
|
|
return text === undefined ? undefined : JSON.parse(text);
|
|
}
|
|
|
|
function copyArgs(args) {
|
|
return JSON.parse(JSON.stringify(args));
|
|
}
|
|
|
|
export function callSync(specifier, name, ...args) {
|
|
const file = callerFile(callSync);
|
|
const copied = copyArgs(args);
|
|
let result;
|
|
try {
|
|
const { fn, namespace } = exportedFunction(file, specifier, name);
|
|
result = Reflect.apply(fn, namespace, copied);
|
|
if (isThenable(result)) {
|
|
// The promise is dropped: its rejection must not end the process.
|
|
Promise.resolve(result).catch(() => {});
|
|
throw new TypeError(
|
|
`callSync: '${name}' exported by '${specifier}' returned a promise; use call() for asynchronous functions`,
|
|
);
|
|
}
|
|
return copyResult(result);
|
|
} catch (error) {
|
|
throw copyThrown(error);
|
|
}
|
|
}
|
|
|
|
export async function call(specifier, name, ...args) {
|
|
const file = callerFile(call);
|
|
const copied = copyArgs(args);
|
|
try {
|
|
const { fn, namespace } = exportedFunction(file, specifier, name);
|
|
return copyResult(await Reflect.apply(fn, namespace, copied));
|
|
} catch (error) {
|
|
throw copyThrown(error);
|
|
}
|
|
}
|