mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Type process.getBuiltinModule results as module namespaces
Under @types/node, process.getBuiltinModule("node:os") types as the
module namespace, which lowered as an unchecked dynamic value and failed.
A string-literal id naming a supported builtin now lowers to the namespace
value an `import * as` binding has, including pi's cast-and-probe spelling
`(process as P).getBuiltinModule?.("node:os") ?? null`; a nullable
namespace binding narrowed present keys the same member tables, and a
namespace is truthy. process.features read as a whole value (`"sea" in
process.features`) is an object holding none of the flags, matching the
existing member reads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5e964965f6
commit
8a02efaa91
@@ -5756,6 +5756,8 @@ export class LlEmitter {
|
||||
// Process streams use a numeric descriptor internally, including zero
|
||||
// for stdin. They remain JavaScript objects in boolean contexts.
|
||||
case "procStream":
|
||||
// A module namespace object is always present.
|
||||
case "moduleNs":
|
||||
return "true";
|
||||
case "array":
|
||||
case "record":
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
funcOf,
|
||||
} from "../../../ir/ir.js";
|
||||
import { lowerBuiltinLoaderValue } from "../lower-builtin-values.js";
|
||||
import { canonicalBuiltinModule } from "../../builtin-modules.js";
|
||||
import { lowerProcessIpcSend, ipcMessageListener, ipcDisconnectListener } from "./ipc.js";
|
||||
import { lowerDiagnosticsSubscriber, lowerTracingArguments } from "./async-context.js";
|
||||
|
||||
@@ -364,6 +365,11 @@ export function lowerProcessProperty(
|
||||
if (member === "versions") {
|
||||
return { kind: "libCall", fn: "process.versions", args: [], type: DYN, loc };
|
||||
}
|
||||
// process.features as a value (`"sea" in process.features`): an object
|
||||
// holding none of the flags, consistent with the member reads above.
|
||||
if (member === "features") {
|
||||
return { kind: "dynObjLit", type: DYN, loc };
|
||||
}
|
||||
// process.noDeprecation / process.throwDeprecation: the values the
|
||||
// program assigned (undefined before that — no Node flags reach a
|
||||
// compiled binary).
|
||||
@@ -1521,3 +1527,36 @@ function processCredentialCall(member: string, loc: SrcLoc): IrExpr {
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `process.getBuiltinModule("node:os")`: a string-literal id naming a
|
||||
* supported builtin whose call the checker types as that module's
|
||||
* namespace (@types/node's BuiltInModule map) answers the namespace value
|
||||
* `import * as ns` binds, so its members key the builtin tables at their
|
||||
* use sites. The receiver may be cast (`(process as P)`) and the call
|
||||
* optional: Node 24's process always has the member, and the cast changes
|
||||
* no runtime value. Other ids keep the dynamic loader (undefined for
|
||||
* unknown ids, Node's argument errors). */
|
||||
export function lowerProcessBuiltinModuleNamespace(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
): IrExpr | null {
|
||||
const callee = call.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "getBuiltinModule")
|
||||
return null;
|
||||
const id = call.arguments[0];
|
||||
if (call.arguments.length !== 1 || !id || !ts.isStringLiteralLike(id)) return null;
|
||||
let receiver: ts.Expression = callee.expression;
|
||||
while (
|
||||
ts.isParenthesizedExpression(receiver) ||
|
||||
ts.isAsExpression(receiver) ||
|
||||
ts.isSatisfiesExpression(receiver) ||
|
||||
ts.isNonNullExpression(receiver)
|
||||
)
|
||||
receiver = receiver.expression;
|
||||
if (!lowerer.isStdlibGlobal(receiver, "process")) return null;
|
||||
const module = canonicalBuiltinModule(id.text);
|
||||
if (module === null) return null;
|
||||
const type = lowerer.mapTypeOf(lowerer.checker.getNonNullableType(lowerer.typeOf(call)));
|
||||
if (type?.kind !== "moduleNs" || type.moduleId !== `builtin:${module}`) return null;
|
||||
return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc: locOf(call) };
|
||||
}
|
||||
|
||||
@@ -126,7 +126,10 @@ import {
|
||||
lowerPerfHooksCall,
|
||||
perfEntryTag,
|
||||
} from "./builtins/performance.js";
|
||||
import { lowerProcStreamMethodCall } from "./builtins/process.js";
|
||||
import {
|
||||
lowerProcStreamMethodCall,
|
||||
lowerProcessBuiltinModuleNamespace,
|
||||
} from "./builtins/process.js";
|
||||
import { recordMayKeepSource, throughRecordSource } from "./record-sources.js";
|
||||
import { lowerReflectCall } from "./builtins/reflection.js";
|
||||
import {
|
||||
@@ -363,6 +366,15 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
return { kind: "libCall", fn: "http2.streamNoop", args: [], type: VOID, loc };
|
||||
}
|
||||
|
||||
// `process.getBuiltinModule("node:os")` typed as the module namespace,
|
||||
// including pi's cast-and-probe spelling
|
||||
// `(process as P).getBuiltinModule?.("node:os")` (the member always
|
||||
// exists on Node 24's process object).
|
||||
{
|
||||
const namespace = lowerProcessBuiltinModuleNamespace(lowerer, expr);
|
||||
if (namespace) return namespace;
|
||||
}
|
||||
|
||||
// Optional-chain call forms: `f?.()` (the token on the call) and
|
||||
// `a?.m()` (the token on the member access). The handled markers keep
|
||||
// the chain lowering's re-entrant dispatch from looping.
|
||||
|
||||
@@ -11569,6 +11569,18 @@ export class Lowerer {
|
||||
if (mapped?.kind === "moduleNs" && mapped.moduleId.startsWith("builtin:")) {
|
||||
return mapped.moduleId.slice("builtin:".length);
|
||||
}
|
||||
// A nullable namespace binding (`const os: typeof NodeOs | null`, a
|
||||
// probed process.getBuiltinModule result) read where the checker
|
||||
// narrowed it present: its members key the same tables.
|
||||
if (stored?.kind === "union") {
|
||||
const present = this.unions.get(stored.unionId)?.arms.filter((arm) => !isUnitType(arm));
|
||||
const only = present?.length === 1 ? present[0] : undefined;
|
||||
if (only?.kind === "moduleNs" && only.moduleId.startsWith("builtin:")) {
|
||||
const narrowed = this.mapTypeOf(this.typeOf(expr));
|
||||
if (narrowed?.kind === "moduleNs" && narrowed.moduleId === only.moduleId)
|
||||
return only.moduleId.slice("builtin:".length);
|
||||
}
|
||||
}
|
||||
if (ts.isIdentifier(expr)) {
|
||||
const symbol = this.checker.getSymbolAtLocation(expr);
|
||||
const decl = symbol ? this.checker.declarationsOf(symbol)[0] : undefined;
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6232-process-builtin-module-typed/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// process.getBuiltinModule and process.features under @types/node: a
|
||||
// literal id types as the module namespace, so the result is the same
|
||||
// object `import * as os` binds (pi's user-agent probe casts process and
|
||||
// calls the member optionally), and process.features is an object a
|
||||
// feature probe can test with `in` (pi's single-executable check).
|
||||
import * as os from "node:os";
|
||||
import type * as NodeOs from "node:os";
|
||||
|
||||
const direct = process.getBuiltinModule("node:os");
|
||||
console.log("direct", direct.platform() === os.platform(), direct.EOL === os.EOL);
|
||||
console.log("bare id", process.getBuiltinModule("os").arch() === os.arch());
|
||||
|
||||
type ProcessWithOsBuiltinModule = typeof process & {
|
||||
getBuiltinModule?: (id: "node:os") => typeof NodeOs;
|
||||
};
|
||||
function loadNodeOs(): typeof NodeOs | null {
|
||||
if (typeof process === "undefined" || !(process.versions?.node || process.versions?.bun)) {
|
||||
return null;
|
||||
}
|
||||
return (process as ProcessWithOsBuiltinModule).getBuiltinModule?.("node:os") ?? null;
|
||||
}
|
||||
const nodeOs = loadNodeOs();
|
||||
const agent = nodeOs ? `pi (${nodeOs.platform()} ${nodeOs.release()}; ${nodeOs.arch()})` : "pi (browser)";
|
||||
console.log("agent", agent === `pi (${os.platform()} ${os.release()}; ${os.arch()})`);
|
||||
console.log("present", nodeOs !== null, nodeOs ? typeof nodeOs.homedir() : "none");
|
||||
|
||||
const isNodeSeaBinary = "sea" in process.features && process.features.sea === true;
|
||||
console.log("sea", isNodeSeaBinary, "sea" in process.features);
|
||||
@@ -0,0 +1 @@
|
||||
{ "type": "module" }
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user