diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index b08c95e29..427c7101e 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -59405,7 +59405,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -59447,7 +59448,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -59489,7 +59491,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -59531,7 +59534,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -59609,7 +59613,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -59798,7 +59803,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -59873,7 +59879,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -59947,7 +59954,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -59989,7 +59997,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -60031,7 +60040,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 23959d71a..4e94da274 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -73916,7 +73916,8 @@ "tests/corpus/2830-cjs-require-self-name/main.cjs", "tests/corpus/2858-import-meta-resolve/main.mjs", "tests/corpus/2860-require-resolve/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -73955,7 +73956,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -74020,7 +74022,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -74058,7 +74061,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -74096,7 +74100,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -74164,7 +74169,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -74331,7 +74337,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -74395,7 +74402,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -74459,7 +74467,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -74497,7 +74506,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { @@ -74535,7 +74545,8 @@ "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", - "tests/corpus/2880-cjs-module-graph/main.cjs" + "tests/corpus/2880-cjs-module-graph/main.cjs", + "tests/corpus/2923-cjs-factory-export/main.cjs" ] }, "dynamic": { diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index e259bfe18..295b67aca 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -77,7 +77,7 @@ { "chapter": "buffer", "signatures": ["`buf.readBigInt64BE([offset])`", "`buf.readBigInt64LE([offset])`", "`buf.readBigUInt64BE([offset])`", "`buf.readBigUInt64LE([offset])`", "`buf.writeBigInt64BE(value[, offset])`", "`buf.writeBigInt64LE(value[, offset])`", "`buf.writeBigUInt64BE(value[, offset])`", "`buf.writeBigUInt64LE(value[, offset])`"], "status": "supported", "evidence": ["tests/corpus/2902-bigint-bytes.ts", "tests/corpus/2904-bigint-errors.ts"] }, { "chapter": "environment_variables", "symbols": ["export"], "status": "not-applicable" }, { "chapter": "globals", "symbols": ["fetch"], "status": "partial", "evidence": ["tests/harness/fetch-conformance.test.ts"] }, - { "chapter": "modules", "symbols": ["__dirname", "__filename", "exports", "require", "module.require", "main", "filename"], "status": "partial", "evidence": ["tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", "tests/corpus/2880-cjs-module-graph/main.cjs"] }, + { "chapter": "modules", "symbols": ["__dirname", "__filename", "exports", "require", "module.require", "main", "filename"], "status": "partial", "evidence": ["tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", "tests/corpus/2880-cjs-module-graph/main.cjs", "tests/corpus/2923-cjs-factory-export/main.cjs"] }, { "chapter": "modules", "symbols": ["require.resolve", "require.resolve.paths"], "status": "partial", "evidence": ["tests/corpus/2858-import-meta-resolve/main.mjs", "tests/corpus/2860-require-resolve/main.cjs"] }, { "chapter": "modules", "symbols": ["cache", "children", "id", "isPreloading", "loaded", "parent", "path", "paths", "Module"], "status": "partial", "evidence": ["tests/corpus/2880-cjs-module-graph/main.cjs"] }, { "chapter": "modules", "symbols": ["extensions"], "status": "not-implemented" }, diff --git a/packages/compiler/src/backend/c/exprs.ts b/packages/compiler/src/backend/c/exprs.ts index 3dd4ec0b0..b793107ce 100644 --- a/packages/compiler/src/backend/c/exprs.ts +++ b/packages/compiler/src/backend/c/exprs.ts @@ -4351,6 +4351,11 @@ function emitDynamicLibCall(state: LibCallState): Temp { // +1 string or NULL with the exception pending (user // toString/valueOf throws propagate). Borrows the dyn. return finish(`scr_dyn_string_coerce_js(${arg(0)})`); + case "dyn.toNumberCoerce": + // JS ToNumber (number-hint valueOf/toString protocol). A + // thrown hook leaves the exception pending; the may-throw + // epilogue abandons the NaN dummy. + return finish(`scr_dyn_number_coerce(${arg(0)})`); case "global.undefRead": // A declare-d const nothing defines: Node's catchable // ReferenceError at the access (always throws — the typed diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index bfb285dbf..95365b054 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -221,6 +221,7 @@ export const LIB_FN_SYMS: Record = { // The checked-dynamic compare/equals validators (scr_bytes_io.c): // Node's argument ladders throw catchably (MAY_THROW_LIB_FNS). "dyn.toStringCoerce": "scr_dyn_string_coerce_js", + "dyn.toNumberCoerce": "scr_dyn_number_coerce", "buffer.compareChk": "scr_buffer_compare_chk", "bytes.equalsChk": "scr_bytes_equals_chk", "bytes.compareChk": "scr_bytes_compare_chk", diff --git a/packages/compiler/src/frontend/lowering/lower-builtins.ts b/packages/compiler/src/frontend/lowering/lower-builtins.ts index 6102a2b8f..955f60b47 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtins.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtins.ts @@ -6219,6 +6219,19 @@ function lowerOptionalStringSearchParams(lowerer: Lowerer, init: IrExpr, loc: Sr if (member !== "isTTY" && member !== "columns") return null; let recv: ts.Expression = expr.expression; while (ts.isParenthesizedExpression(recv) || ts.isAsExpression(recv) || ts.isTypeAssertion(recv)) recv = recv.expression; + // Browser-compatible packages commonly write + // `(process.stdout || {}).isTTY`. Node's stream object is always + // present and truthy, so the fallback cannot run; match the left + // receiver exactly as the value-level always-truthy `||` lowering + // does, preserving the native isatty/columns answer. + if ( + ts.isBinaryExpression(recv) && + recv.operatorToken.kind === ts.SyntaxKind.BarBarToken + ) { + let left = recv.left; + while (ts.isParenthesizedExpression(left) || ts.isAsExpression(left) || ts.isTypeAssertion(left)) left = left.expression; + if (ts.isPropertyAccessExpression(left)) recv = left; + } if (!ts.isPropertyAccessExpression(recv)) return null; const stream = lowerer.stdlibGlobalMember(recv, "process"); if (stream !== "stdin" && stream !== "stdout" && stream !== "stderr") return null; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index a58e07414..42a70a9e1 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -840,6 +840,22 @@ function completeFuncValueArgs( ) { return; } + // An uncontextualized npm-static implementation function can still + // have a declaration-backed completed ABI: the package's .d.ts + // projection supplies the body-facing parameter/return types while + // the shipped JavaScript's own inferred type remains wider (`any` + // callback fields are the common case). lambdaSignature has already + // constructed and validated that concrete ABI, and every eventual + // storage/call site independently checks it. Admit the value at birth; + // do not make the unprojected JS inference veto its authored surface. + if ( + target === undefined && + contextual !== null && + (ts.isArrowFunction(contextual) || ts.isFunctionExpression(contextual)) && + npmStaticPackageOfPath(contextual.getSourceFile().fileName) !== null + ) { + return; + } // An 'any'-typed slot is the ISLAND boundary: the host-function // trampoline already implements JS call semantics over the completed // signature — a missing engine argument arrives as undefined and takes @@ -5188,8 +5204,10 @@ function lowerOptionalStringNumber( arrayReceiver && ((lowerer.checker.getTypeArguments(recvTs as ts.TypeReference)[0]?.flags ?? 0) & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0; + const checkerUntyped = + (recvTs.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 || anyArray; let recv: IrExpr; - if (recvTs.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown) || anyArray) { + if (checkerUntyped) { recv = lowerer.lowerExpr(access.expression); } else { // A checker-TYPED spelling whose VALUE is still checked-dynamic (an @@ -5272,7 +5290,24 @@ function lowerOptionalStringNumber( // member models. Order note: JS reads the callee before evaluating // arguments — dynKeyGet's undefined-receiver TypeError fires first, // exactly Node. - if (DYN_PROTO_METHOD_NAMES.has(access.name.text)) return null; + // A checker-typed object whose represented value remains dyn still + // has an authored own-member contract. Let that member win even when + // its name overlaps a primitive prototype (`colors.bold(...)`): the + // source value was compiled from that object shape, while genuinely + // untyped/any receivers retain the runtime-kind ambiguity and fence. + const ownCallable = !checkerUntyped && (() => { + const prop = lowerer.checker.getPropertyOfType(recvTs, access.name.text); + if (prop === undefined) return false; + // getPropertyOfType includes inherited Object.prototype members. + // Those are not stored members of a checked-dynamic object, so + // dynKeyGet cannot stand in for JavaScript's prototype lookup. Only + // an authored declaration can justify the own-member path. + const authored = lowerer.checker.declarationsOf(prop).some( + (decl) => !lowerer.isStdlibFile(decl.getSourceFile()), + ); + return authored && lowerer.checker.getCallSignatures(lowerer.checker.getTypeOfSymbol(prop)).length > 0; + })(); + if (DYN_PROTO_METHOD_NAMES.has(access.name.text) && !ownCallable) return null; // Optional forms (`obj.cb?.()`, `obj?.cb()`) belong to the chain // machinery's short-circuit semantics — not modeled here yet. if (call.questionDotToken || access.questionDotToken) return null; diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index da65e5b49..b4bc2605b 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1032,13 +1032,17 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // type-mapper.ts pins the one concrete signature `(value: string) => // primitive`; direct calls `String(x)` never reach here — the call // lowering intercepts them with the wider static coercions). - // JavaScript sources keep the identity-token path below. + const jsPrimitiveCtorSelector = + isJsSourceFile(expr.getSourceFile()) && + expr.parent !== undefined && + ts.isArrowFunction(expr.parent) && + expr.parent.body === expr; if ( (expr.text === "String" || expr.text === "Number" || expr.text === "Boolean") && - !isJsSourceFile(expr.getSourceFile()) && + (!isJsSourceFile(expr.getSourceFile()) || jsPrimitiveCtorSelector) && lowerer.isStdlibSymbol(lowerer.checker.getSymbolAtLocation(expr)) ) { - return primitiveCtorClosure(lowerer, expr.text, loc); + return primitiveCtorClosure(lowerer, expr.text, loc, jsPrimitiveCtorSelector); } // The lib fence's IDENTIFIER chokepoint: the real standard library // resolves names the old minimal ambient world never declared @@ -5620,7 +5624,14 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio if (raw.type.kind === "bigint") { return { kind: "libCall", fn: "bigint.not", args: [raw], type: BIGINT_T, loc }; } - const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand); + // A checked-dynamic value retains enough JavaScript structure to + // run exact ToNumber, including the object valueOf/toString + // protocol. This is the static `any`/JS-lane answer for idioms + // such as `while (~index)`, where inference may have widened an + // otherwise numeric slot to the checked-dynamic tree. + const operand = raw.type.kind === "dyn" + ? { kind: "libCall" as const, fn: "dyn.toNumberCoerce" as const, args: [raw], type: F64, loc } + : lowerOptionalNumber(lowerer, raw, loc, expr.operand); if (operand.type.kind !== "f64") lowerer.unsupported("SC1043", expr); return { kind: "unary", op: "~", operand, type: F64, loc }; } @@ -5981,6 +5992,15 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr if (left.kind === "unitLit") { return op === ts.SyntaxKind.BarBarToken ? right : left; } + // JS objects and symbols are always truthy. In `ref || fallback` + // the fallback is therefore unreachable and the result is the + // already-evaluated left value — the browser-fallback idiom used by + // packages for `process.argv || []` and `process.env || {}`. The + // operand itself remains in the IR, preserving reads/calls that + // produce the reference; only the unreachable right lowering drops. + if (op === ts.SyntaxKind.BarBarToken && REF_TRUTHY_KINDS.has(left.type.kind)) { + return left; + } if (left.type.kind === "dyn" || right.type.kind === "dyn") { // A checked-dynamic operand (`fn.name || ''` — // test/common's _mustCallInner): both sides live in the checked-dynamic tree and @@ -9527,37 +9547,45 @@ function lowerStreamObjectProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpr } /** The primitive-constructor closure (`String`/`Number`/`Boolean` as a - * VALUE): interns one synthesized module function per constructor — - * `%builtin.String` et al. — and returns the zero-capture closure over it. - * The body IS the string-coercion the type mapping promised - * (`(value: string) => primitive`): String is identity, Number the ECMA - * StringToNumber (num.fromString — the same runtime call the direct - * `Number(s)` lowering emits), Boolean the emptiness test. Interning makes - * every reference the SAME immortal closure, so `opt.type === String` - * compares like JS function identity. */ + * VALUE): interns one synthesized module function per constructor/ABI and + * returns the zero-capture closure over it. Typed sources use the mapped + * `(value: string) => primitive` signature; a recognized JavaScript + * concise-arrow selector (`() => String`, the picocolors fallback) takes + * dyn so its ordinary coercion signature survives inference residue. The + * dyn bodies run exact ToString/ToNumber/ToBoolean, including object hooks. + * Interning makes every reference under one ABI the SAME immortal closure, + * so `opt.type === String` compares like JS function identity. */ function primitiveCtorClosure( lowerer: Lowerer, name: "String" | "Number" | "Boolean", loc: SrcLoc, + dynamicInput: boolean, ): IrExpr { const ret = name === "String" ? STRING : name === "Number" ? F64 : BOOL; - const fnT = funcOf([STRING], ret); - let fnName = lowerer.primitiveCtorFns.get(name); + const input = dynamicInput ? DYN : STRING; + const fnT = funcOf([input], ret); + const key = `${name}:${dynamicInput ? "dyn" : "string"}`; + let fnName = lowerer.primitiveCtorFns.get(key); if (!fnName) { - fnName = `%builtin.${name}`; - lowerer.primitiveCtorFns.set(name, fnName); - const s: IrExpr = { kind: "varRef", localId: "v.0", type: STRING, loc }; - const value: IrExpr = - name === "String" - ? s + fnName = `%builtin.${name}${dynamicInput ? ".dyn" : ""}`; + lowerer.primitiveCtorFns.set(key, fnName); + const v: IrExpr = { kind: "varRef", localId: "v.0", type: input, loc }; + const value: IrExpr = dynamicInput + ? name === "String" + ? { kind: "libCall", fn: "dyn.toStringCoerce", args: [v], type: STRING, loc } + : name === "Number" + ? { kind: "libCall", fn: "dyn.toNumberCoerce", args: [v], type: F64, loc } + : { kind: "dynTest", test: "truthy", value: v, type: BOOL, loc } + : name === "String" + ? v : name === "Number" - ? { kind: "libCall", fn: "num.fromString", args: [s], type: F64, loc } - : { kind: "strEq", negated: true, left: s, right: { kind: "strLit", value: "", type: STRING, loc }, type: BOOL, loc }; + ? { kind: "libCall", fn: "num.fromString", args: [v], type: F64, loc } + : { kind: "strEq", negated: true, left: v, right: { kind: "strLit", value: "", type: STRING, loc }, type: BOOL, loc }; const fn: IrFunction = { name: fnName, - params: [{ localId: "v.0", name: "value", type: STRING }], + params: [{ localId: "v.0", name: "value", type: input }], returnType: ret, - locals: [{ id: "v.0", name: "value", type: STRING, mutable: false }], + locals: [{ id: "v.0", name: "value", type: input, mutable: false }], body: [{ kind: "return", value, loc }], loc, }; diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 0f046cd07..ca3b6e272 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -15,10 +15,10 @@ import { invalidJsonModuleDiag, npmEmbedFailedDiag, requiresDynamicImportDiag } import { BOOL, DYN, IrClassDef, IrExpr, IrFunction, IrGlobal, IrRecordShape, IrStmt, IrType, IrUnionDef, JSVAL, RUNTIME_ERROR_CLASSES, STRING, SrcLoc, VOID, arrayOf, canConvertToDyn, isUnitType } from "../../ir/ir.js"; import { ENTRY_NAME, PoisonError, boundIdentifiersOf, dynFallbackType, dynUndefinedExpr, importCallHandleType, newFnCtx, uncheckedOverloadHandleCall } from "./lowerer.js"; import { builtinMemberRequireDecl, builtinNamespaceDestructureModuleOf, createRequireBindingDecl, createRequireNamespaceDecl, createRequireSpecOf, isPromisifyCall, registerBuiltinCallableAlias, textCodecBindingDecl } from "./lower-builtins.js"; -import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, bindingGenericFnInfoOf, bindingGenericFnNodeOf, deadUnmappableBinding, implicitLocalFnInfoOf, implicitLocalFnNodeOf, nullishGenericBindingUnitOf, registerOverloadedCallableAlias } from "./lower-calls.js"; +import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, bindingGenericFnInfoOf, bindingGenericFnNodeOf, bindingNeverReassigned, deadUnmappableBinding, implicitLocalFnInfoOf, implicitLocalFnNodeOf, nullishGenericBindingUnitOf, registerOverloadedCallableAlias } from "./lower-calls.js"; import { isVarDeclared, numericIteratorSourceOf, provenanceElidedConstDecl } from "./lower-stmts.js"; import { streamClassAliasDecl } from "./lower-stream.js"; -import { stdlibGlobalAliasDecl } from "./surfaces.js"; +import { stdlibGlobalAliasDecl, stdlibGlobalAliasNameOf } from "./surfaces.js"; import { collectNamespaceStmt, nsPathPrefix, trapDeclRootOf } from "./lower-namespaces.js"; import { collectExpandoMembers } from "./lower-expando.js"; import { isUnitOnlyTsType, unitOnlyUnion } from "../type-mapper.js"; @@ -961,11 +961,20 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts // included); unmappable pieces take the JS checked-dynamic // fallback like every JS binding. const fnValued = ts.isFunctionExpression(rhs) || ts.isArrowFunction(rhs); - if (cjsScalarLiteral(rhs) || fnValued) { + // A factory-returned CommonJS root (`module.exports = make()`) + // is a single value just like the scalar/function forms above + // when its result has a static or checked-dynamic representation. Register one + // snapshot slot keyed by the export-assignment declaration; the + // statement lowering evaluates the call exactly once at its + // source position and assigns that slot. Unmappable calls retain + // the existing statement-level fence. + const factoryValued = ts.isCallExpression(rhs); + if (cjsScalarLiteral(rhs) || fnValued || factoryValued) { const diagsBefore = lowerer.diags.length; try { const strict = lowerer.typeOf(rhs); - const t = lowerer.mapTypeOf(strict) ?? (fnValued ? dynFallbackType(lowerer, rhs, strict) : null); + const t = lowerer.mapTypeOf(strict) ?? + (fnValued || factoryValued ? dynFallbackType(lowerer, rhs, strict) : null); if (t && t.kind !== "void") { const g: IrGlobal = { id: `%g.${tag}exports`, name: "exports", type: t, mutable: false }; lowerer.globalsByDeclNode.set(cjs.expr, g); @@ -1283,7 +1292,18 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts // global snapshot — alias plumbing, no global storage (see // stdlibGlobalAliasDecl; the statement lowering skips it by the // same test). - if (isConst && stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer)) continue; + const stableStdlibAlias = + isConst || + ( + (list.flags & ts.NodeFlags.Let) !== 0 && + ts.isIdentifier(decl.name) && + stdlibGlobalAliasNameOf(lowerer, decl.initializer) !== null && + (() => { + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + return symbol !== undefined && bindingNeverReassigned(lowerer, symbol, decl); + })() + ); + if (stableStdlibAlias && stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer)) continue; // Stored default TextEncoder/TextDecoder instances are the same // compile-time alias plumbing as their statement lowering: calls // trace this const initializer, so no module global exists. diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index c0220ad98..85c39c20e 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -12,12 +12,12 @@ import { BOOL, BYTES_U8, CAUGHT, DYN, F64, IrExpr, IrGlobal, IrJsOp, IrLocal, Ir import { PoisonError, boundIdentifiersOf, dynFallbackType, dynUndefinedExpr, importCallHandleType, neverTaintedJsType, stmtUsesIsland, uncheckedOverloadHandleCall } from "./lowerer.js"; import { enforceLibBoundary } from "./lib-boundary.js"; import { cjsExportAssignmentOf, cjsExportDiscardReason, cjsExportTargetLiteral, isCjsJsFile, isJsSourceFile, locOf, requireSpecOf } from "../program.js"; -import { COMPOUND_ASSIGN_OPS, CompoundOp, STR_METHODS, UNSUPPORTED_STMT, isStdlibMember, sideEffectFreeOptionValue, stdlibGlobalAliasDecl, stdlibGlobalNameOf } from "./surfaces.js"; +import { COMPOUND_ASSIGN_OPS, CompoundOp, STR_METHODS, UNSUPPORTED_STMT, isStdlibMember, sideEffectFreeOptionValue, stdlibGlobalAliasDecl, stdlibGlobalAliasNameOf, stdlibGlobalNameOf } from "./surfaces.js"; import { isProvenanceSourceFile } from "../provenance-registry.js"; import { ambientUndefVarRootOf, lowerImportEquals, nsUndefRead, nsWritableTarget, trapDeclRootOf } from "./lower-namespaces.js"; import { expandoWritableTarget, lowerExpandoAssignStmt } from "./lower-expando.js"; import { ForOfIterProjection, lowerForOfArrayIter, lowerForOfMap, lowerForOfSearchParams, lowerForOfSet, lowerSafeIndexRead, objectIterOverIndexShape, strCharsCall } from "./lower-containers.js"; -import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, bindingGenericFnInfoOf, bindingGenericFnNodeOf, deadUnmappableBinding, implicitLocalFnInfoOf, implicitLocalFnNodeOf, implicitMethodCallInfersReturn, nullishExprUnitOf, nullishGenericBindingUnitOf, recordKeysArrayCall, registerOverloadedCallableAlias } from "./lower-calls.js"; +import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, bindingGenericFnInfoOf, bindingGenericFnNodeOf, bindingNeverReassigned, deadUnmappableBinding, implicitLocalFnInfoOf, implicitLocalFnNodeOf, implicitMethodCallInfersReturn, nullishExprUnitOf, nullishGenericBindingUnitOf, recordKeysArrayCall, registerOverloadedCallableAlias } from "./lower-calls.js"; import { isMixinFnBinding, mixinResultBindingClassOf } from "./lower-mixins.js"; import type { ClassInfo, ClassIteratorInfo } from "./lower-classes.js"; import { genericIfaceBindingKeepsClass } from "./lower-classes.js"; @@ -3504,7 +3504,19 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL // `const process = globalThis.process` — a stdlib-global snapshot: // alias plumbing (see stdlibGlobalAliasDecl), no storage, no code. - if (!isLet && stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer)) return null; + const stableStdlibAlias = + !isLet || + ( + ts.isVariableDeclarationList(decl.parent) && + (decl.parent.flags & ts.NodeFlags.Let) !== 0 && + ts.isIdentifier(decl.name) && + stdlibGlobalAliasNameOf(lowerer, decl.initializer) !== null && + (() => { + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + return symbol !== undefined && bindingNeverReassigned(lowerer, symbol, decl); + })() + ); + if (stableStdlibAlias && stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer)) return null; // `const encoder = new TextEncoder()` and a statically-labelled // TextDecoder twin: the codec has no general value representation, but calls @@ -4497,7 +4509,7 @@ function isEsModuleStamp(expr: ts.Expression): boolean { const discarded = ts.isExpressionStatement(stmt) ? cjsExportDiscardReason(stmt) : null; if (discarded !== null) lowerer.unsupported("SC1090", cjs.expr, discarded); } - const assignExport = (nameNode: ts.Node, value: ts.Expression): IrStmt => { + const assignExport = (nameNode: ts.Node, value: ts.Expression, root?: IrGlobal): IrStmt => { const symbol = lowerer.checker.getSymbolAtLocation(nameNode) ?? (ts.isIdentifier(nameNode) || ts.isPrivateIdentifier(nameNode) @@ -4510,7 +4522,42 @@ function isEsModuleStamp(expr: ts.Expression): boolean { lowerer.lowerExpr(value); lowerer.badType(value, lowerer.typeOf(value)); } - return { kind: "assign", localId: g.id, value: lowerer.lowerExprExpecting(value, g.type), loc: locOf(value) }; + const lowered = lowerer.lowerExprExpecting(value, g.type); + if (root?.type.kind !== "dyn" || !ts.isIdentifier(nameNode)) { + return { kind: "assign", localId: g.id, value: lowered, loc: locOf(value) }; + } + // A factory whole export is represented by its checked-dynamic root. + // A later `module.exports.name = value` mutates THAT same object in + // Node; the separate export global exists only so named-import and + // lexer-visible member paths have a static snapshot. Evaluate the + // RHS once, perform the observable object write first (it may throw + // if a factory returned a primitive), then publish the snapshot. + const saved = lowerer.declareHiddenLocal("%cjsMember", g.type); + const savedRef = (): IrExpr => ({ kind: "varRef", localId: saved.id, type: g.type, loc: locOf(value) }); + const dynValue = lowerer.coerceInto(value, savedRef(), DYN); + return { + kind: "block", + body: [ + { kind: "varDecl", localId: saved.id, init: lowered, loc: locOf(value) }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.keySet", + args: [ + { kind: "varRef", localId: root.id, type: DYN, loc }, + { kind: "strLit", value: nameNode.text, type: STRING, loc: locOf(nameNode) }, + dynValue, + ], + type: VOID, + loc, + }, + loc, + }, + { kind: "assign", localId: g.id, value: savedRef(), loc: locOf(value) }, + ], + loc, + }; }; if (cjs.kind === "member") { if (!ts.isIdentifier(cjs.name)) { @@ -4571,7 +4618,14 @@ function isEsModuleStamp(expr: ts.Expression): boolean { if (names.every(laterRealOf)) return { kind: "block", body: [], loc }; } } - return assignExport(cjs.name, cjs.value); + const sf = cjs.expr.getSourceFile(); + let root: IrGlobal | undefined; + for (const stmt of sf.statements) { + const whole = cjsExportAssignmentOf(stmt); + if (whole?.kind !== "table" || cjsExportDiscardReason(stmt) !== null) continue; + root = lowerer.globalsByDeclNode.get(whole.expr); + } + return assignExport(cjs.name, cjs.value, root); } let tableObj = cjs.obj; let resolvedTarget = false; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 45fd78ef9..4f061571e 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -854,7 +854,27 @@ function jsFallbackFunctionType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir if (!sig) return null; const params = sig.getParameters().map((p): IrType => { const pt = lowerer.checker.getTypeOfSymbolAtLocation(p, node); - return lowerer.mapTypeOf(pt) ?? DYN; + let mapped = lowerer.mapTypeOf(pt) ?? DYN; + // The fallback must preserve the same completed ABI as mapType and + // lambdaSignature: a spelled optional/defaulted parameter accepts an + // omitted call, represented by an undefined arm in its fixed native + // slot. JS inference reports the body type (`boolean` for + // `enabled = true`) rather than that call-site type, so recover the + // syntactic optionality from the parameter declaration. + const decl = lowerer.checker.valueDeclarationOf(p); + const optional = + decl !== undefined && + ts.isParameter(decl) && + (decl.questionToken !== undefined || decl.initializer !== undefined); + if ( + optional && + mapped.kind !== "dyn" && + mapped.kind !== "jsval" && + !lowerer.bareUndefinedArmedUnion(mapped) + ) { + mapped = lowerer.withUndefinedArm(mapped); + } + return mapped; }); const retT = lowerer.checker.getReturnTypeOfSignature(sig); const ret: IrType = retT.flags & ts.TypeFlags.Void ? VOID : lowerer.mapTypeOf(retT) ?? DYN; @@ -1536,9 +1556,9 @@ export class Lowerer { readonly spHofHelpers = new Map(); /** The primitive-constructor VALUES (`String`/`Number`/`Boolean` as * bare identifiers — CLI option tables store and compare them): one - * synthesized coercion function per constructor per program, interned - * here by name so every reference is the SAME zero-capture closure and - * `opt.type === String` is JS identity (see primitiveCtorClosure). */ + * synthesized coercion function per constructor and typed/dyn ABI, + * interned here so references under one source-world ABI are the SAME + * zero-capture closure (see primitiveCtorClosure). */ readonly primitiveCtorFns = new Map(); /** Builtin-module FUNCTIONS admitted as values by their surface-table * entries. The resolved runtime function plus ABI is the identity key, @@ -2380,10 +2400,14 @@ export class Lowerer { while (init && ts.isParenthesizedExpression(init)) init = init.expression; if (!init || !ts.isObjectLiteralExpression(init)) return false; } - // The scalar-literal export= symbol declares AT the `module.exports - // =` statement itself; table/Proxy replacements share that - // declaration node, so only scalar RHS reads as a single value. + // A represented whole export declares AT the `module.exports =` + // statement itself. Scalar/function/factory-call roots register one + // snapshot global on that declaration; table/Proxy replacements do + // not. A binding whose declaration owns such storage is therefore a + // single VALUE, not a namespace whose members must appear in Node's + // CJS lexer export table. if (d && ts.isBinaryExpression(d)) { + if (this.globalsByDeclNode.has(d)) return false; let r: ts.Expression = d.right; while (ts.isParenthesizedExpression(r)) r = r.expression; const scalar = diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 42eca3351..3082ca67a 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1711,26 +1711,50 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record is not defined"). args[0] is the @@ -7390,6 +7395,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "error.nodeThrow", // USVString coercion runs user toString/valueOf — throws propagate. "dyn.toStringCoerce", + // Numeric coercion runs user valueOf/toString — throws propagate. + "dyn.toNumberCoerce", "child.kill", // The caller's lookup runs synchronously inside the connect call — a // throw there propagates like Node's. diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index bd8d29337..27c05ec9d 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -912,6 +912,7 @@ export const LIB_FN_SIGS: Record/tests/corpus/2922-dyn-bitwise-not.cjs": { + "order": [ + "/tests/corpus/2922-dyn-bitwise-not.cjs" + ], + "diags": [] + }, + "/tests/corpus/2923-cjs-factory-export/main.cjs": { + "order": [ + "/tests/corpus/2923-cjs-factory-export/factory.cjs", + "/tests/corpus/2923-cjs-factory-export/main.cjs" + ], + "diags": [] + }, "/tests/corpus/2930-union-receiver-calls.ts": { "order": [ "/tests/corpus/2930-union-receiver-calls.ts" diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 37a75b9c4..31b08fc32 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1905,6 +1905,12 @@ bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out) { } } +double scr_dyn_number_coerce(const ScrDyn *d) { + double out = NAN; + (void)scr_dyn_number_coerce_js(d, &out); + return out; +} + /* The checked-dynamic keyed WRITE (`h.k = v` on a dyn receiver): OBJ sets * the member (later writes win, insertion order — JS); undefined/null * throws Node's "Cannot set properties of ..."; every other kind throws diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 24069c446..9e58dee71 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -3496,6 +3496,9 @@ ScrStr *scr_dyn_string_coerce(const ScrDyn *d); * Borrows; +1 or NULL with the exception pending. */ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d); bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out); +/* Direct-return ABI wrapper for compiler libCalls: JS ToNumber, or NaN + * with the exception pending when an object hook throws/refuses. */ +double scr_dyn_number_coerce(const ScrDyn *d); /* `d instanceof TypeError` (and the other builtin error classes) on a * checked-dynamic value: the from_error cache resolves the dyn encoding diff --git a/tests/corpus/2922-dyn-bitwise-not.cjs b/tests/corpus/2922-dyn-bitwise-not.cjs new file mode 100644 index 000000000..5f3e3c2ce --- /dev/null +++ b/tests/corpus/2922-dyn-bitwise-not.cjs @@ -0,0 +1,6 @@ +// Checked-dynamic unary bitwise-not: JS ToNumber across JSON kinds, then +// ToInt32/complement. The object and array rows pin ordinary-to-primitive +// behavior; this stays engine-free. +const values = JSON.parse('[0,1,-1,1.5,"2","x",true,false,null,[],[1],{}]'); + +for (const value of values) console.log(~value); diff --git a/tests/corpus/2923-cjs-factory-export/factory.cjs b/tests/corpus/2923-cjs-factory-export/factory.cjs new file mode 100644 index 000000000..6db40cd96 --- /dev/null +++ b/tests/corpus/2923-cjs-factory-export/factory.cjs @@ -0,0 +1,12 @@ +let builds = 0; + +function make(prefix) { + builds++; + return { + prefix, + count: builds, + }; +} + +module.exports = make("root:"); +module.exports.extra = "member"; diff --git a/tests/corpus/2923-cjs-factory-export/main.cjs b/tests/corpus/2923-cjs-factory-export/main.cjs new file mode 100644 index 000000000..88b7614ab --- /dev/null +++ b/tests/corpus/2923-cjs-factory-export/main.cjs @@ -0,0 +1,6 @@ +// A factory-returned CommonJS root is evaluated once, cached by identity, +// and remains the object later `module.exports.member =` statements mutate. +const api = require("./factory.cjs"); +const again = require("./factory.cjs"); + +console.log(api === again, api.prefix, api.count, api.extra); diff --git a/tests/fixtures/npm-static/colors-prototype-cli.ts b/tests/fixtures/npm-static/colors-prototype-cli.ts new file mode 100644 index 000000000..f228c581f --- /dev/null +++ b/tests/fixtures/npm-static/colors-prototype-cli.ts @@ -0,0 +1,4 @@ +import pc from "picocolors"; + +console.log(pc.hasOwnProperty("bold")); +console.log(pc.valueOf() === pc); diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 44d5b6520..57f2783b5 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -2,7 +2,8 @@ * program modules (no island) — the slice-2 pilot. Three tiers pinned * here: * - * 1. DRIVEN DIFFERENTIALS (escape-string-regexp, slash, ms, commander): + * 1. DRIVEN DIFFERENTIALS (escape-string-regexp, slash, ms, picocolors, + * commander): * fully static builds whose stdout, stderr, and exit status byte-match * Node for the exercised programs. * 2. COVERAGE FRONTIERS: dormant package paths may retain runtime fences; @@ -35,9 +36,9 @@ interface RunResult { exitCode: number; } -async function runBinary(cmd: string, args: string[]): Promise { +async function runBinary(cmd: string, args: string[], env: NodeJS.ProcessEnv = process.env): Promise { try { - const { stdout, stderr } = await execFileAsync(cmd, args, { encoding: "buffer" }); + const { stdout, stderr } = await execFileAsync(cmd, args, { encoding: "buffer", env }); return { stdout, stderr, exitCode: 0 }; } catch (err) { const e = err as { code?: unknown; stdout?: Buffer; stderr?: Buffer }; @@ -121,6 +122,45 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { expect(nativeRes.exitCode).toBe(nodeRes.exitCode); }, 120_000); + test("picocolors compiles fully statically and byte-matches both color branches", async () => { + const entry = join(pilotRoot, "colors-cli.ts"); + const { coverage } = analyze(entry, { npmStatic: ["picocolors"] }); + expect(coverage.npmStatic).toEqual([{ package: "picocolors", status: "static" }]); + expect(coverage.preflightFailed).toBe(false); + expect(coverage.diagnostics).toHaveLength(0); + expect(coverage.runtimeFences ?? []).toHaveLength(0); + expect(coverage.stats.statementsFailed).toBe(0); + + const binary = await buildStatic(entry, ["picocolors"]); + const plainEnv: NodeJS.ProcessEnv = { ...process.env, NO_COLOR: "1" }; + delete plainEnv["FORCE_COLOR"]; + const colorEnv: NodeJS.ProcessEnv = { ...process.env, FORCE_COLOR: "1" }; + delete colorEnv["NO_COLOR"]; + + for (const env of [plainEnv, colorEnv]) { + const [nodeRes, nativeRes] = await Promise.all([ + runBinary("node", [entry], env), + runBinary(binary, [], env), + ]); + expect(nativeRes.stdout).toEqual(nodeRes.stdout); + expect(comparableStderr(nativeRes.stderr)).toEqual(nodeRes.stderr); + expect(nativeRes.exitCode).toBe(nodeRes.exitCode); + } + }, 180_000); + + test("picocolors inherited Object methods retain the prototype-method fence", () => { + const entry = join(pilotRoot, "colors-prototype-cli.ts"); + const { coverage } = analyze(entry, { npmStatic: ["picocolors"] }); + expect(coverage.npmStatic).toEqual([{ package: "picocolors", status: "static" }]); + expect(coverage.preflightFailed).toBe(false); + expect(coverage.runtimeFences ?? []).toHaveLength(0); + expect(coverage.diagnostics.map((diagnostic) => diagnostic.code)).toEqual(["SC2020", "SC2020"]); + expect(coverage.diagnostics.map((diagnostic) => diagnostic.message)).toEqual([ + expect.stringContaining(".hasOwnProperty' is part of the standard library types"), + expect.stringContaining(".valueOf' is part of the standard library types"), + ]); + }, 120_000); + // Tier 1, auto mode: the eligibility heuristics pick escape-string-regexp // (own .d.ts, unminified, no transform markers) without naming it. test("--npm-static=auto opts the eligible pilot in", () => {