Compile function export objects; name conditional export objects (#1434)

This commit is contained in:
Chris Tate
2026-10-20 03:16:27 -05:00
committed by GitHub
25 changed files with 544 additions and 5 deletions
@@ -5882,6 +5882,9 @@ export class LlEmitter {
// for stdin. They remain JavaScript objects in boolean contexts.
case "procStream":
return "true";
// A module namespace (a required builtin module object) is an object.
case "moduleNs":
return "true";
case "array":
case "record":
case "object":
@@ -30,6 +30,7 @@ import {
import {
cjsExportMemberWriteGlobalOf,
isCjsExportsIdentifier,
isCjsModuleExports,
isFreshExportsReset,
} from "./cjs-exports-object.js";
import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js";
@@ -6627,6 +6628,25 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
"require() outside the module's top level (move it to the top of the file)",
);
}
// Replacing the export object (or rebinding `exports`) anywhere but a
// top-level statement makes the module's exports depend on which path
// ran (`cond ? module.exports = a : module.exports = b`, an
// environment-dependent branch); the static export table has one shape.
if (
!topLevelJs &&
isJsSourceFile(expr.getSourceFile()) &&
ts.isBinaryExpression(expr) &&
expr.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
(isCjsModuleExports(lowerer, expr.left) || isCjsExportsIdentifier(lowerer, expr.left))
) {
lowerer.unsupported(
"SC1090",
expr.left,
isCjsModuleExports(lowerer, expr.left)
? "replacing module.exports outside a top-level statement (a conditional or environment-dependent export object)"
: "rebinding 'exports' outside a top-level statement (a conditional or environment-dependent export object)",
);
}
if (topLevelJs && ts.isExpressionStatement(stmtNode)) {
if (isFreshExportsReset(lowerer, stmtNode))
return { kind: "block", body: [], loc: locOf(expr) };
@@ -274,3 +274,21 @@ export const g = import_g.g;
degrade: expect.stringContaining("bundled CommonJS modules") as string,
});
});
test("a chunk that only re-exports a builtin module becomes that module", () => {
const answer = expectSameBehavior(`${keyedHelper}var require_stream = __commonJS({
"lib/stream.js"(exports2, module2) {
module2.exports = require("stream");
}
});
var require_user = __commonJS({
"lib/user.js"(exports2) {
var Stream = require_stream();
exports2.same = Stream === require("stream");
}
});
console.log(typeof require_stream().PassThrough, require_user().same);
`);
expect(answer.chunks.map((c) => basename(c.path))).toEqual(["index.js.require_user.cjs"]);
expect(answer.text).toContain('typeof require("stream").PassThrough');
});
@@ -23,6 +23,7 @@
* beside it). Anything else answers a degrade reason. */
import * as ts from "./ts7/syntax.js";
import { helperTokens } from "./helper-tokens.js";
import { canonicalBuiltinModule } from "./builtin-modules.js";
export interface BundledChunkSplit {
/** The bundle with chunk declarations blanked and calls respelled. */
@@ -581,10 +582,11 @@ export function splitBundledChunks(
return {
degrade: `its bundled CommonJS module factory '${r.text}' is used other than as a direct call`,
};
const forwarded = forwarderSpec(chunk);
callEdits.push({
start: call.getStart(sf),
end: call.getEnd(),
text: `require(${JSON.stringify(chunk.spec)})`,
text: `require(${JSON.stringify(forwarded ?? chunk.spec)})`,
});
}
for (const name of chunks.keys()) {
@@ -596,6 +598,11 @@ export function splitBundledChunks(
const out: BundledChunkSplit["chunks"] = [];
const bundleEdits: Edit[] = [...callEdits];
for (const chunk of chunks.values()) {
if (forwarderSpec(chunk) !== null) {
// A chunk that only re-exports a builtin module is that module.
bundleEdits.push({ start: chunk.stmt.getStart(sf), end: chunk.stmt.getEnd(), text: "" });
continue;
}
const result = chunkSource(
sf,
chunk,
@@ -656,6 +663,41 @@ export function applyEdits(source: string, from: number, to: number, edits: Edit
return text + source.slice(at, to);
}
/** The builtin module a chunk forwards, or null: a body that is exactly
* `module.exports = require("<builtin>")` (readable-stream's
* internal/streams/stream.js). Requiring the chunk is then requiring the
* builtin itself (same object, and loading a builtin runs no code). */
function forwarderSpec(chunk: Chunk): string | null {
const moduleParam = chunk.params[1];
if (moduleParam === undefined) return null;
const stmts = chunk.body.statements.nodes.filter(
(st) => !(ts.isExpressionStatement(st) && ts.isStringLiteral(st.expression)),
);
const only = stmts[0];
if (stmts.length !== 1 || only === undefined || !ts.isExpressionStatement(only)) return null;
const e = unparen(only.expression);
if (
!ts.isBinaryExpression(e) ||
e.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
!ts.isPropertyAccessExpression(e.left) ||
!ts.isIdentifier(e.left.expression) ||
e.left.expression.text !== moduleParam ||
e.left.name.text !== "exports"
)
return null;
const call = unparen(e.right);
if (
!ts.isCallExpression(call) ||
!ts.isIdentifier(call.expression) ||
call.expression.text !== "require" ||
call.arguments.nodes.length !== 1 ||
!ts.isStringLiteral(call.arguments.nodes[0]!)
)
return null;
const spec = (call.arguments.nodes[0] as ts.StringLiteral).text;
return canonicalBuiltinModule(spec) !== null ? spec : null;
}
/** How code in a factory body (a chunk or a lazy ES-module initializer)
* observes the factory's own frame, or null: its receiver, its arguments
* object, `super`, a meta property, or (when `returns`) a top-level
@@ -0,0 +1,81 @@
import { spawnSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, expect, test } from "vitest";
import { nodeFrontendServices } from "./services-node.js";
const dirs: string[] = [];
afterAll(() => {
for (const dir of dirs) rmSync(dir, { recursive: true, force: true });
});
function rewrite(source: string): string | null {
return nodeFrontendServices().cjsFunctionExports(source, "/pkg/index.js");
}
function run(file: string) {
const result = spawnSync(process.execPath, [file], { encoding: "utf8" });
return { status: result.status, stdout: result.stdout, stderr: result.stderr };
}
/** The rewritten module prints what the original prints. */
function expectSameBehavior(source: string): string {
const result = rewrite(source);
expect(typeof result).toBe("string");
const dir = mkdtempSync(join(tmpdir(), "scriptc-cjs-function-exports-"));
dirs.push(dir);
const original = join(dir, "original.cjs");
const rewritten = join(dir, "rewritten.cjs");
writeFileSync(original, source);
writeFileSync(rewritten, result as string);
const oracle = run(original);
expect(oracle.status).toBe(0);
expect(run(rewritten)).toEqual(oracle);
return result as string;
}
const report = `
console.log(typeof module.exports, module.exports === Version, Object.keys(Version).join(","));
`;
test("members of a function export object read and write the function", () => {
const rewritten = expectSameBehavior(`exports = module.exports = Version;
exports.parse = parse;
module.exports.major = (v) => parse(v)[0];
function Version(v) { return { v }; }
function parse(v) { return v.split(".").map(Number); }
console.log(exports.major("2.1.0"), module.exports.parse("1.2").length);
${report}`);
expect(rewritten).toContain("Version.parse = parse");
expect(rewritten).not.toContain("exports = module.exports");
});
test.each([
["a parameter", "function inner(Version) { return exports.parse; }"],
["a local", "function inner() { const Version = {}; return exports.parse; }"],
["a nested function", "function inner() { function Version() {} return exports.parse; }"],
[
"a catch binding",
"function inner() { try { throw 0; } catch (Version) { return exports.parse; } }",
],
])("a nested binding of the function's name (%s) keeps the module as it is", (_, inner) => {
expect(
rewrite(`exports = module.exports = Version;
exports.parse = parse;
function Version() {}
function parse() {}
${inner}
`),
).toBeNull();
});
test("an `exports` shorthand property keeps its key", () => {
const rewritten = expectSameBehavior(`exports = module.exports = Version;
exports.tag = "v";
function Version() {}
const holder = { exports };
console.log(Object.keys(holder).join(","), holder.exports === Version, holder.exports.tag);
${report}`);
expect(rewritten).toContain("{ exports: Version }");
});
@@ -0,0 +1,191 @@
/* CommonJS modules whose export object is one of their own functions:
*
* exports = module.exports = SemVer; // or module.exports = SemVer;
* exports.parse = parse; // or module.exports.parse = …
* function SemVer(version) { … }
*
* After that statement `module.exports` (and, after the rebinding form,
* `exports`) IS the function, so every member read or write through them
* is a member of the function itself. The pass respells those accesses
* through the function's name, the expando form both resolvers model
* (members on a function declaration), and drops the `exports =`
* rebinding the static export protocol has no storage for.
*
* Exact only while nothing can observe the module before the statement
* runs and nothing replaces the export object again: the statement is a
* top-level statement preceded only by inert declarations, it is the
* file's only write of `module.exports` and `exports`, the function is a
* top-level declaration nothing reassigns and the file's only binding of
* its name, and `exports`/`module` are the CommonJS globals (never
* declared in the file). Other shapes answer
* null and keep their existing treatment. */
import * as ts from "./ts7/syntax.js";
import {
applyEdits,
countDeclarations,
declaredNames,
isWritten,
references,
unparen,
type Edit,
} from "./npm-static-bundled-chunks.js";
function isModuleExports(e: ts.Expression): e is ts.PropertyAccessExpression {
const x = unparen(e);
return (
ts.isPropertyAccessExpression(x) &&
!x.questionDotToken &&
ts.isIdentifier(x.expression) &&
x.expression.text === "module" &&
x.name.text === "exports"
);
}
/** True for a statement whose evaluation runs no code that could observe
* the export object: declarations and member writes (`F.prototype.m =
* function …`) without calls, and directives. */
function inert(stmt: ts.Statement): boolean {
if (ts.isFunctionDeclaration(stmt) || ts.isEmptyStatement(stmt)) return true;
if (ts.isExpressionStatement(stmt) && ts.isStringLiteral(stmt.expression)) return true;
if (ts.isExpressionStatement(stmt)) {
const e = unparen(stmt.expression);
if (
!ts.isBinaryExpression(e) ||
e.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
!(ts.isPropertyAccessExpression(e.left) || ts.isElementAccessExpression(e.left))
)
return false;
let root: ts.Expression = e.left;
while (ts.isPropertyAccessExpression(root) || ts.isElementAccessExpression(root))
root = root.expression;
if (!ts.isIdentifier(root) || root.text === "module" || root.text === "exports") return false;
} else if (!ts.isVariableStatement(stmt)) return false;
let calls = false;
ts.walkPreorder(stmt, (n) => {
if (calls) return "stop";
if (ts.isArrowFunction(n) || ts.isFunctionExpression(n)) return "skip";
if (ts.isCallExpression(n) || ts.isNewExpression(n) || ts.isTaggedTemplateExpression(n))
calls = true;
return undefined;
});
return !calls;
}
export function normalizeCjsFunctionExports(sf: ts.SourceFile): string | null {
const source = sf.text;
if (!source.includes("module.exports")) return null;
if (!sf.fileName.endsWith(".cjs") && ts.isExternalModule(sf)) return null;
const declared = declaredNames(sf);
if (declared.has("exports") || declared.has("module")) return null;
// The whole-export statement: `[exports =] module.exports = W;`.
let whole: { stmt: ts.ExpressionStatement; fn: string; rebinds: boolean } | null = null;
const stmts = sf.statements.nodes;
for (let i = 0; i < stmts.length; i++) {
const stmt = stmts[i]!;
if (!ts.isExpressionStatement(stmt)) continue;
const top = unparen(stmt.expression);
if (!ts.isBinaryExpression(top) || top.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
continue;
let e: ts.BinaryExpression = top;
let rebinds = false;
if (ts.isIdentifier(top.left) && top.left.text === "exports") {
const inner = unparen(top.right);
if (!ts.isBinaryExpression(inner) || inner.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
continue;
rebinds = true;
e = inner;
}
if (!isModuleExports(e.left)) continue;
const value = unparen(e.right);
if (!ts.isIdentifier(value)) return null;
if (whole !== null || !stmts.slice(0, i).every(inert)) return null;
whole = { stmt, fn: value.text, rebinds };
}
if (whole === null) return null;
const fnName = whole.fn;
const fnDecls = stmts.filter(
(st) => ts.isFunctionDeclaration(st) && st.name?.text === fnName && st.body !== undefined,
);
// The respelled reads name the function from wherever they sit, so no
// other binding of its name (a parameter, a local, a nested function)
// may exist anywhere in the file.
if (fnDecls.length !== 1 || countDeclarations(sf, fnName) !== 1) return null;
const edits: Edit[] = [];
let unsafe = false;
const refs = references(sf);
for (const r of refs) {
if (r.text === fnName && isWritten(r)) unsafe = true;
if (r.text === "exports") {
if (isWritten(r)) {
// Only the whole statement's own rebinding.
const assign = r.parent;
if (
!whole.rebinds ||
assign === undefined ||
!ts.isBinaryExpression(assign) ||
assign.parent !== whole.stmt ||
assign.left !== r
)
unsafe = true;
continue;
}
// Without the rebinding, `exports` keeps the replaced object.
if (!whole.rebinds || r.getStart(sf) < whole.stmt.getEnd()) {
unsafe = true;
continue;
}
// `{ exports }` keeps its key: the shorthand spells out the value.
const shorthand =
r.parent !== undefined && ts.isShorthandPropertyAssignment(r.parent) && r.parent.name === r;
edits.push({
start: r.getStart(sf),
end: r.getEnd(),
text: shorthand ? `exports: ${fnName}` : fnName,
});
continue;
}
if (r.text === "module") {
const access = r.parent;
if (access === undefined || !isModuleExports(access as ts.Expression)) continue;
const outer = access.parent;
if (outer !== undefined && ts.isBinaryExpression(outer) && outer.left === access) {
if (
outer.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
outer.operatorToken.kind <= ts.SyntaxKind.LastAssignment &&
!(
(outer.parent === whole.stmt && !whole.rebinds) ||
(whole.rebinds && outer.parent?.parent === whole.stmt)
)
)
unsafe = true;
continue;
}
// A member access through module.exports: a member of the function.
if (
outer !== undefined &&
(ts.isPropertyAccessExpression(outer) || ts.isElementAccessExpression(outer)) &&
outer.expression === access
) {
if (access.getStart(sf) < whole.stmt.getEnd() && !insideFunction(access)) unsafe = true;
edits.push({ start: access.getStart(sf), end: access.getEnd(), text: fnName });
}
}
}
if (unsafe) return null;
if (whole.rebinds) {
// `exports = module.exports = W` → `module.exports = W`.
const e = unparen(whole.stmt.expression) as ts.BinaryExpression;
edits.push({ start: e.getStart(sf), end: unparen(e.right).getStart(sf), text: "" });
}
if (edits.length === 0) return null;
return applyEdits(source, 0, source.length, edits);
}
function insideFunction(node: ts.Node): boolean {
for (let p = node.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) {
if (ts.isFunctionLike(p)) return true;
}
return false;
}
+5 -1
View File
@@ -448,7 +448,11 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
} else if (esm !== null) {
original = esm;
}
const respelled = split !== null || esm !== null;
// A module exporting one of its functions reaches its members
// through that function.
const fnExports = original === null ? null : services.cjsFunctionExports(original, path);
if (fnExports !== null) original = fnExports;
const respelled = split !== null || esm !== null || fnExports !== null;
if (original !== null) {
const namepaths = services.jsDocNamepaths(path, original);
const source = namepaths ?? original;
@@ -2,6 +2,7 @@ import { Ts7SourceParser, type Ts7SourceKind } from "./ts7/source-parser.js";
import { Ts7Host, type Ts7ApiFactory, type Ts7HostOptions } from "./ts7/program-host.js";
import { lowerEsmInitsWithParser, rewriteBundlerCjsWithParser } from "./npm-static-rewrite-host.js";
import { isBundlerCjsCandidate } from "./npm-static-rewrite-syntax.js";
import { normalizeCjsFunctionExports } from "./npm-static-cjs-exports-alias.js";
import {
mayBundleChunks,
splitBundledChunks,
@@ -91,6 +92,13 @@ export class FrontendServices {
if (!source.includes("__esm")) return null;
return lowerEsmInitsWithParser(this.sourceParser(), source, path);
}
/** A CommonJS module whose export object is one of its functions, with
* its member accesses respelled through that function (null: none). */
cjsFunctionExports(source: string, path: string): string | null {
this.ensureOpen();
if (!source.includes("module.exports")) return null;
return normalizeCjsFunctionExports(this.parse(path, source, "js"));
}
nullableClassFields(path: string, source: string): syntax.NpmStaticOverloadRewrite | null {
return syntax.applyNpmStaticNullableClassFields(this.parse(path, source, "js"), source);
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/required-module-truthiness.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/npm/cases/2583-cjs-function-exports/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/npm/cases/2584-cjs-conditional-exports/main.ts"
],
"diags": []
}
@@ -0,0 +1,10 @@
// A required builtin module object in a boolean context is truthy, as any
// object: readable-stream's `process.env.READABLE_STREAM === "disable" &&
// Stream` and `Stream || exports` guards.
"use strict";
var Stream = require("stream");
var os = require("os");
console.log(Stream ? "stream" : "none", !!os, !os);
console.log(process.env.SCRIPTC_UNSET_FLAG === "disable" && Stream ? "disabled" : "default");
var fallback = Stream || null;
console.log(fallback === Stream);
@@ -0,0 +1,12 @@
// CommonJS modules whose export object is one of their own functions:
// semver 5/6's `exports = module.exports = Version` with members attached
// through both `exports` and `module.exports` (and read back through
// `exports` inside the module), and a constructor function exported whole
// with function and value members attached afterwards.
import Version from "gtfnexports";
import Point from "gtfnmembers";
const v = Version.parse("2.4.1");
console.log(v === null ? "null" : v.major(), String(new Version("3.0.0")));
console.log(Version.compare("5.0.0", "3.2.0"), Version.gt("1.0.0", "2.0.0"), Version.MAX_LENGTH);
console.log(Point.origin().sum(), new Point(2, 3).sum(), Point.dimensions);
@@ -0,0 +1,7 @@
// An environment-dependent export object (readable-stream 2.x's
// READABLE_STREAM=disable switch): module.exports is replaced inside an
// if/else, so the module's exports depend on which branch ran. scriptc
// names the construct; Node picks the branch at load.
import rs from "gtcondexports";
console.log(rs.Readable.name);
+2
View File
@@ -0,0 +1,2 @@
declare const exported: { Readable: Function; Stream: unknown; name: string };
export = exported;
+14
View File
@@ -0,0 +1,14 @@
var Stream = require("stream");
function Local(name) {
this.name = name;
}
if (process.env.GT_STREAMS === "native" && Stream) {
module.exports = Stream;
exports = module.exports = Stream.Readable;
exports.Readable = Stream.Readable;
exports.Stream = Stream;
} else {
exports = module.exports = Local;
exports.Stream = Stream || exports;
exports.Readable = exports;
}
+7
View File
@@ -0,0 +1,7 @@
{
"name": "gtcondexports",
"version": "1.0.0",
"description": "readable-stream 2.x's environment-dependent export object: module.exports is replaced (and exports rebound) inside an if/else on an environment variable",
"main": "index.js",
"types": "index.d.ts"
}
+13
View File
@@ -0,0 +1,13 @@
declare class Version {
constructor(raw: string);
raw: string;
major(): number;
toString(): string;
}
declare namespace Version {
const MAX_LENGTH: number;
function parse(raw: string): Version | null;
function compare(a: string, b: string): number;
function gt(a: string, b: string): boolean;
}
export = Version;
+25
View File
@@ -0,0 +1,25 @@
exports = module.exports = Version;
var label = process.env.GT_LABEL ? "[" + process.env.GT_LABEL + "]" : "";
function Version(raw) {
this.raw = raw;
this.parts = raw.split(".");
}
Version.prototype.major = function () {
return Number(this.parts[0]);
};
Version.prototype.toString = function () {
return label + this.raw;
};
exports.MAX_LENGTH = 256;
exports.parse = function parse(raw) {
return raw.length > exports.MAX_LENGTH ? null : new Version(raw);
};
exports.compare = function compare(a, b) {
return exports.parse(a).major() - exports.parse(b).major();
};
module.exports.gt = function gt(a, b) {
return exports.compare(a, b) > 0;
};
+7
View File
@@ -0,0 +1,7 @@
{
"name": "gtfnexports",
"version": "1.0.0",
"description": "semver 5/6's export idiom: exports = module.exports = Version, then members attached through exports and module.exports",
"main": "index.js",
"types": "index.d.ts"
}
+11
View File
@@ -0,0 +1,11 @@
declare class Point {
constructor(x: number, y: number);
x: number;
y: number;
sum(): number;
}
declare namespace Point {
function origin(): Point;
const dimensions: number;
}
export = Point;
+13
View File
@@ -0,0 +1,13 @@
"use strict";
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.sum = function () {
return this.x + this.y;
};
module.exports = Point;
module.exports.origin = function origin() {
return new Point(0, 0);
};
module.exports.dimensions = 2;
+7
View File
@@ -0,0 +1,7 @@
{
"name": "gtfnmembers",
"version": "1.0.0",
"description": "a constructor function as module.exports with function and value members attached through module.exports afterwards",
"main": "index.js",
"types": "index.d.ts"
}
+2 -2
View File
@@ -19,13 +19,13 @@ export function npmCases(fixturesRoot: string): NpmCase[] {
return [
...globSync(join(fixturesRoot, "npm/cases/*/main.ts"))
.sort()
// 2465-2469, 2556-2567, 2571 and 2580-2582 are the --npm-static bundler-emitted-CJS
// 2465-2469, 2556-2567, 2571 and 2580-2584 are the --npm-static bundler-emitted-CJS
// cases (npm-static.test.ts drives them with the opt-in): their
// consumers exercise the static rewrite's surface (lexer-visible
// names the shipped .d.ts never declares, the __toESM interop
// family), so they stay out of the flagless island lane by design.
.filter(
(entry) => !/\/(246[5-9]|255[6-9]|256[0-7]|2571|258[0-2])-[^/]+\/main\.ts$/.test(entry),
(entry) => !/\/(246[5-9]|255[6-9]|256[0-7]|2571|258[0-4])-[^/]+\/main\.ts$/.test(entry),
)
.map((entry) => ({
name: entry.split("/").at(-2)!,
+27 -1
View File
@@ -76,7 +76,7 @@ async function buildStatic(entry: string, npmStatic: string[] | "auto"): Promise
...globSync(
join(fixturesRoot, "npm/node_modules/cryptozoo/**/*.{js,mjs,cjs,json,d.ts}"),
).sort(),
// the bundler-emitted-CJS mini packages (cases 2465-2469, 2556-2567, 2571, 2580-2582)
// the bundler-emitted-CJS mini packages (cases 2465-2469, 2556-2567, 2571, 2580-2584)
...globSync(join(fixturesRoot, "npm/node_modules/gt*/**/*.{js,cjs,json}")).sort(),
...globSync(join(fixturesRoot, "npm/store/gt*/**/*.{js,cjs,json}")).sort(),
];
@@ -1514,6 +1514,32 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
expect(nativeRes.exitCode).toBe(nodeRes.exitCode);
}, 180_000);
// 2583: a module whose export object is one of its functions (semver
// 5/6's `exports = module.exports = Version`), under both values of the
// environment variable the module reads.
test("function export objects 2583-cjs-function-exports compile statically and byte-match Node", async () => {
const entry = join(fixturesRoot, "npm/cases/2583-cjs-function-exports/main.ts");
await expectNodeParity(entry, ["gtfnexports", "gtfnmembers"]);
const binary = await buildStatic(entry, ["gtfnexports", "gtfnmembers"]);
const env = { ...process.env, GT_LABEL: "x" };
const [nodeRes, nativeRes] = await Promise.all([
runBinary("node", [entry], env),
runBinary(binary, [], env),
]);
expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8"));
expect(nativeRes.stdout.toString("utf8")).toContain("[x]");
}, 180_000);
// 2584: an export object replaced inside an environment-dependent branch
// (readable-stream 2.x) is named precisely.
test("an environment-dependent export object is refused by name", () => {
const entry = join(fixturesRoot, "npm/cases/2584-cjs-conditional-exports/main.ts");
const { coverage } = analyze(entry, { npmStatic: ["gtcondexports"] });
expect(JSON.stringify(coverage)).toContain(
"replacing module.exports outside a top-level statement (a conditional or environment-dependent export object)",
);
}, 120_000);
// 2567: an export that reads `this`, called through its namespace, sees
// Node's module object; the lowered namespace would differ, so the
// package stays refused.