feat(compiler): compile CommonJS syntax passes with the native client

- Move CommonJS export detection and package rewrites onto shared native syntax with explicit dependency resolution.
- Preserve complete bundled-helper matching, significant line breaks, and deep source traversal without the TypeScript 5 parser.
- Compile optional string indexing and exercise the production CommonJS passes in native executables.
This commit is contained in:
Chris Tate
2026-09-28 11:56:16 -05:00
parent e1e5efc1ca
commit 9eb6248a04
21 changed files with 2495 additions and 2011 deletions
+7 -792
View File
@@ -1,803 +1,18 @@
/* Node's CommonJS named-export DETECTABILITY test — the compile-time
* answer to "which names can `import { x } from './cjs.js'` actually
* link?". Node never executes a CJS module to build its ESM facade: it
* lexes the source with its vendored CJS lexer (Node v24.15.0 vendors
* deps/merve, the native `cjs_lexer` binding that replaced the
* cjs-module-lexer wasm build; the detection patterns are FROZEN by
* upstream policy) and only the names that lexer detects become named
* exports. The checker sees the real export table, so it happily binds
* names the lexer cannot see — `module.exports = { a: 7 }` type-checks a
* named import of `a` that Node refuses at link time with a SyntaxError.
* This module mirrors merve's detection so the compiler can answer
* exactly what Node answers — for the program's own CJS files (program.ts
* gates the ESM instantiate graph) AND for the --dynamic island's embedded
* npm modules (npm.ts synthesizes each CJS facade from this lexer, so an
* embedded named import fails exactly where Node's would).
*
* INPUT IS SOURCE TEXT. merve is a byte lexer; this port parses the text
* with the 5.9.3 island parser and walks the AST, reading the raw source
* wherever merve's byte quirks demand it. Only strings and name sets cross
* this module's boundary — no AST object from either typescript world —
* which is what lets both the 7.0.2-world program preflight and the
* typescript5-world npm scan share the one implementation.
*
* The port is deliberately quirk-faithful — these all match probed Node
* v24.15.0 behavior, not what a clean reimplementation would choose:
* - `module.exports = { ... }` props scan LEFT TO RIGHT and the scan
* STOPS at the first prop it cannot shape — names added before the
* stop are kept, everything after is invisible. `{ vis: v, lit: 7 }`
* exports `vis` only; `{ lit: 7, vis: v }` exports nothing.
* - A prop VALUE must start with an identifier-ish token (an identifier,
* keyword, `require(...)` call — anything whose first character is
* [A-Za-z_$] or non-ASCII). Literals (`7`, `"s"`, backticks, arrows'
* `(`) stop the scan WITHOUT adding the key.
* - After an accepted value the lexer consumes ONE identifier run and
* requires the very next CHARACTER to be `,` or `}` — `a: b, c: d`
* keeps both, `a: b , c: d` keeps only `a` (the space stops the scan),
* `a: o.x` / `a: f()` keep `a` and stop.
* - A `require('spec')` VALUE adds the key, records the spec as a
* REEXPORT, and stops the scan unconditionally — `{ a: require('x'),
* b: v }` exports `a` plus x's names, never `b`. Trailing tokens on the
* require (`.foo`, `()`, `|| y`) change nothing.
* - `get name() {...}` stops the scan cold (no add); `set name(v) {}`
* adds the literal name "set" and stops; `async name() {}` adds
* "async" and stops; a plain method `name() {}` adds `name` and stops.
* - `exports.NAME = ` / `module.exports.NAME = ` / the ['string'] forms
* match ANYWHERE in the file — nested functions, dead branches — with
* no scope analysis, and nothing ever removes a detected name.
* - `module.exports = require('./x')` records a REEXPORT whenever the
* require call is the LEADING token of the right-hand side — trailers
* (`.foo`, `()`, `|| {}`, `? a : b`) are ignored, but a leading `(`
* breaks the match (`(require('x'))` records nothing). Node resolves
* each reexport and unions the target's detected names when the target
* is itself CommonJS. Every `module.exports =` assignment CLEARS the
* pending reexport list first — including star-pattern reexports
* recorded earlier — so only reexports at or after the last assignment
* count, while detected export NAMES accumulate across everything.
* - `Object.defineProperty(exports, 'n', {...})` adds `n` for the exact
* descriptor shapes merve accepts: optional leading `enumerable: true`
* then `value:` (anything), or a getter whose whole body is `return
* IDENT` / `return IDENT.IDENT` / `return IDENT['str']` and which is
* the descriptor's last property. Unlike cjs-module-lexer 2.2.0, a
* NON-matching defineProperty of the same name elsewhere does NOT
* poison the valid one (probed: Node keeps the export).
* - The transpiler star-reexport patterns ARE detected (tsc/babel output
* all over npm depends on them):
* · `__export(require('x'))` / `__exportStar(require('x'), exports)`
* — callee is the bare identifier or any member chain ending in
* one of those two exact names, at TOP LEVEL (brace depth 0 — an
* unbraced `if (c) __exportStar(...)` still counts, a braced block
* does not), and the source bytes must read `NAME(require` with NO
* whitespace or comment between the name, the `(`, and `require`
* (probed: one space kills the match; spaces anywhere inside the
* require call are fine).
* · The Babel copy loop: a top-level `var/const/let ID =
* [_interopRequireWildcard(]require('x')[)]` (ID the FIRST
* declarator; `_interopRequireWildcard` bare and byte-adjacent to
* `(require` like the star form) linked by ID to a top-level
* `Object.keys(ID).forEach(function (KEY) {...})` whose body is
* byte-for-byte one of merve's copy shapes: the `if (KEY ===
* 'default' || KEY === '__esModule') return;` filter (exact order,
* unbraced return) with optional hasOwnProperty / in-exports
* guards, or the single `if (KEY !== 'default' [&& !hasOwn]) copy`
* form; the copy either `EXPORTS[KEY] = ID[KEY]` or the
* enumerable-getter defineProperty. Named callbacks, arrow
* callbacks, braced returns, and reordered filters all miss.
* The reexport's clearing position is the LOOP's, not the var's.
* NOT modeled: cjs-module-lexer 2.2.0's phantom "get" export for an
* identifier-named getter in the table (`{ get a() {} }` exports nothing
* in Node — but `get [k]() {}` / `get "a"() {}` DO export the word "get",
* because the name token stops the lexer after it took `get` as a
* shorthand key). */
/** Node entry for CommonJS export detection. Native callers pass their
* parsed source directly to cjs-syntax and own the parser lifecycle. */
import { cjsLexedExportsOfFile, cjsVisibleNames, type CjsLexedExports } from "./cjs-syntax.js";
import { parseSourceFile } from "./ts7/source-parser-node.js";
export type { CjsLexedExports } from "./cjs-syntax.js";
import ts from "typescript5";
export interface CjsLexedExports {
/** Names Node's lexer detects in THIS file (no reexport resolution). */
exports: Set<string>;
/** Reexport specifiers surviving the last `module.exports =` clearing,
* in source order: the assignment's own require forms plus every later
* (or unclear-ed) star-pattern / Babel-loop match. */
reexports: string[];
}
/** merve's byte classification: [A-Za-z_$] plus every non-ASCII byte. */
function isIdentStartChar(ch: string): boolean {
return /[A-Za-z_$]/.test(ch) || ch.charCodeAt(0) > 127;
}
function isIdentChar(ch: string): boolean {
return /[A-Za-z0-9_$]/.test(ch) || ch.charCodeAt(0) > 127;
}
/** End of the identifier run starting at `pos` (merve consumes maximal
* identifier characters and nothing else — no member chains, no calls). */
function identRunEnd(text: string, pos: number): number {
let i = pos;
while (i < text.length && isIdentChar(text[i]!)) i++;
return i;
}
/** True when the node's SOURCE spelling starts like a lexer identifier —
* a `a`-escaped name parses to the same AST as its plain spelling,
* but merve reads bytes and stops at the backslash. */
function sourceSpellsIdentifier(node: ts.Node, sf: ts.SourceFile): boolean {
const ch = sf.text[node.getStart(sf)];
return ch !== undefined && isIdentStartChar(ch);
}
/** True when the node's WHOLE source span is one identifier run — what
* merve takes for IDENT. Keywords count (`return this.x` satisfies the
* defineProperty getter's `return IDENT.IDENT` because `this` lexes as an
* identifier run — probed), escapes and private names do not. */
function isIdentTokenNode(node: ts.Node, sf: ts.SourceFile): boolean {
const start = node.getStart(sf);
const ch = sf.text[start];
return ch !== undefined && isIdentStartChar(ch) && identRunEnd(sf.text, start) === node.getEnd();
}
/** The identifier run at the node's START — the whole name when its
* spelling is plain, the byte prefix merve consumed when a unicode escape
* interrupts the run mid-name. */
function identPrefixOf(node: ts.Node, sf: ts.SourceFile): string {
const start = node.getStart(sf);
return sf.text.slice(start, identRunEnd(sf.text, start));
}
/** The bare `require('spec')` call merve recognizes. */
export function bareRequireSpecOf(e: ts.Node): string | null {
if (!ts.isCallExpression(e) || e.questionDotToken !== undefined) return null;
if (!ts.isIdentifier(e.expression) || e.expression.text !== "require") return null;
if (e.arguments.length !== 1) return null;
const arg = e.arguments[0]!;
return ts.isStringLiteral(arg) ? arg.text : null;
}
/** True when `e` is exactly the `exports` identifier. */
export function isExportsIdent(e: ts.Expression): boolean {
return ts.isIdentifier(e) && e.text === "exports";
}
/** True when `e` is exactly `module.exports`. */
export function isModuleExports(e: ts.Expression): boolean {
return (
ts.isPropertyAccessExpression(e) &&
e.questionDotToken === undefined &&
ts.isIdentifier(e.expression) &&
e.expression.text === "module" &&
ts.isIdentifier(e.name) &&
e.name.text === "exports"
);
}
/** The `require(...)` call at the very START of `e`'s source span, if any:
* merve records the reexport as soon as it lexes `require('spec')` and any
* TRAILER (`.foo`, `()`, `|| y`, `? a : b`) merely stops whatever scan
* follows — so the recognizer descends the leftmost-child chain while it
* still starts where `e` starts. A ParenthesizedExpression breaks the
* chain naturally (its child starts after the `(`), matching merve. */
function leadingRequireOf(e: ts.Expression, sf: ts.SourceFile): { spec: string; end: number } | null {
const start = e.getStart(sf);
let cur: ts.Node = e;
for (;;) {
const spec = bareRequireSpecOf(cur);
if (spec !== null && cur.getStart(sf) === start) return { spec, end: cur.getEnd() };
let first: ts.Node | undefined;
ts.forEachChild(cur, (c) => {
first ??= c;
});
if (first === undefined || first.getStart(sf) !== start) return null;
cur = first;
}
}
/** Skips ECMA whitespace and comments forward from `pos` (merve's
* commentWhitespace) and answers the next meaningful character (or ""). */
function nextMeaningfulChar(text: string, pos: number): string {
let i = pos;
while (i < text.length) {
const ch = text[i]!;
if (ch === " " || (ch.charCodeAt(0) > 8 && ch.charCodeAt(0) < 14)) {
i++;
} else if (ch === "/" && text[i + 1] === "/") {
const nl = text.indexOf("\n", i + 2);
if (nl < 0) return "";
i = nl;
} else if (ch === "/" && text[i + 1] === "*") {
const close = text.indexOf("*/", i + 2);
if (close < 0) return "";
i = close + 2;
} else {
return ch;
}
}
return "";
}
/** The `module.exports = { ... }` table scan — merve's tryParseLiteralExports
* over the AST. Adds detected names to `out`, appends reexports (spread /
* value requires) to `reexports`, stops at the first unshapeable prop. */
function scanTableLiteral(obj: ts.ObjectLiteralExpression, sf: ts.SourceFile, out: Set<string>, reexports: string[]): void {
const text = sf.text;
for (const prop of obj.properties) {
if (ts.isGetAccessor(prop)) {
// `get name() {...}` — merve's explicit early termination... unless
// the accessor's NAME is not a plain identifier, where the lexer
// already added the word "get" as a shorthand key before the odd
// token stops it (`get [k]() {}` exports "get").
if (!ts.isIdentifier(prop.name) || !sourceSpellsIdentifier(prop.name, sf)) out.add("get");
return;
}
if (ts.isSetAccessor(prop)) {
// The word `set` lexes as a shorthand key, then the accessor name
// stops the scan — Node really does export "set" (value undefined).
out.add("set");
return;
}
if (ts.isMethodDeclaration(prop)) {
// `name() {}` adds the name, then `(` stops the scan. The modifier /
// asterisk spellings shift which token the lexer takes as the key:
// `async name()` exports "async", `*name()` exports nothing. A
// unicode escape mid-name adds only the byte prefix before it (the
// key adds BEFORE the separator check fails — see the key path).
if (prop.asteriskToken !== undefined) return;
const mods = ts.getModifiers(prop) ?? [];
if (mods.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) {
out.add("async");
return;
}
if (ts.isIdentifier(prop.name) && sourceSpellsIdentifier(prop.name, sf)) out.add(identPrefixOf(prop.name, sf));
return;
}
if (ts.isShorthandPropertyAssignment(prop)) {
if (prop.objectAssignmentInitializer !== undefined) return; // `{ a = 1 }` — cover pattern, not a table
if (!ts.isIdentifier(prop.name) || !sourceSpellsIdentifier(prop.name, sf)) return;
if (!isIdentTokenNode(prop.name, sf)) {
// an escape mid-name: the byte prefix adds, then the backslash
// fails the separator check and stops the scan
out.add(identPrefixOf(prop.name, sf));
return;
}
out.add(prop.name.text);
continue; // whitespace before `,` is skipped on the shorthand path
}
if (ts.isSpreadAssignment(prop)) {
const req = leadingRequireOf(prop.expression, sf);
let consumedEnd: number;
if (req !== null) {
reexports.push(req.spec);
consumedEnd = req.end;
} else if (sourceSpellsIdentifier(prop.expression, sf)) {
consumedEnd = identRunEnd(text, prop.expression.getStart(sf));
} else {
return; // `...{}` and friends stop the scan
}
// The spread path DOES skip whitespace/comments before the `,` check
// — `...o , a: b` continues where `a: b , c` would stop.
const ch = nextMeaningfulChar(text, consumedEnd);
if (ch === ",") continue;
return; // `...o.p`, `...f()` — the leftover token stops the scan
// (a `}` here means the spread was last; stopping is the same)
}
if (ts.isPropertyAssignment(prop)) {
const name = prop.name;
let key: string | null = null;
if (ts.isIdentifier(name) && sourceSpellsIdentifier(name, sf)) {
if (!isIdentTokenNode(name, sf)) {
// merve adds the key BEFORE validating the `:` — an escape
// mid-name adds the byte prefix, then the backslash stops the
// scan (probed: the key's prefix exports, nothing after does)
out.add(identPrefixOf(name, sf));
return;
}
key = name.text;
} else if (ts.isStringLiteral(name)) {
key = name.text;
}
if (key === null) return; // numeric / computed / template keys stop the scan
const value = prop.initializer;
const req = leadingRequireOf(value, sf);
if (req !== null) {
// A require VALUE adds the key, records the reexport, and stops
// the scan UNCONDITIONALLY — probed: `{ a: require('x'), b: v }`
// exports `a` plus x's names and never `b` (the spread path above
// genuinely continues past a `,`; the value path does not).
out.add(key);
reexports.push(req.spec);
return;
}
const start = value.getStart(sf);
const ch0 = text[start];
if (ch0 === undefined || !isIdentStartChar(ch0)) return; // literal values stop WITHOUT adding the key
const consumedEnd = identRunEnd(text, start);
out.add(key);
// NO whitespace skip here — merve requires `,` or `}` as the very
// next character after the consumed value token. `a: b , c` and
// `a: b.c, d` both keep `a` and stop.
const ch = text[consumedEnd];
if (ch === ",") continue;
return; // `}` ends the props anyway; anything else stops the scan
}
return; // any other prop kind stops the scan
}
}
/** `Object.defineProperty(exports|module.exports, 'name', {...})` — adds
* the name for merve's exact descriptor shapes (see the header). */
function scanDefineProperty(call: ts.CallExpression, sf: ts.SourceFile, out: Set<string>): void {
const callee = call.expression;
if (
!ts.isPropertyAccessExpression(callee) ||
callee.questionDotToken !== undefined ||
!ts.isIdentifier(callee.expression) ||
callee.expression.text !== "Object" ||
!ts.isIdentifier(callee.name) ||
callee.name.text !== "defineProperty"
) {
return;
}
if (call.arguments.length !== 3) return;
const [recv, nameArg, desc] = call.arguments as unknown as [ts.Expression, ts.Expression, ts.Expression];
if (!isExportsIdent(recv) && !isModuleExports(recv)) return;
if (!ts.isStringLiteral(nameArg)) return;
if (!ts.isObjectLiteralExpression(desc)) return;
let i = 0;
const props = desc.properties;
const first = props[0];
if (
first !== undefined &&
ts.isPropertyAssignment(first) &&
ts.isIdentifier(first.name) &&
first.name.text === "enumerable"
) {
// Only the exact `enumerable: true` prefix is consumed; anything else
// in the slot fails the whole match.
if (first.initializer.kind !== ts.SyntaxKind.TrueKeyword) return;
i = 1;
}
const p = props[i];
if (p === undefined) return;
if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "value") {
out.add(nameArg.text); // merve stops right at `value:` — the value and the rest of the descriptor are unchecked
return;
}
// The getter forms: `get() {...}` method, or `get: function [name]() {...}`
// (never an arrow), body EXACTLY `return IDENT` / IDENT.IDENT / IDENT['s'],
// and the getter must be the descriptor's LAST property.
const body = getterBodyOf(p);
if (body === undefined || i !== props.length - 1) return;
if (body.statements.length !== 1) return;
const ret = body.statements[0]!;
if (!ts.isReturnStatement(ret) || ret.expression === undefined) return;
const r = ret.expression;
const returnsIdentish =
isIdentTokenNode(r, sf) ||
(ts.isPropertyAccessExpression(r) && r.questionDotToken === undefined && isIdentTokenNode(r.expression, sf) && isIdentTokenNode(r.name, sf)) ||
(ts.isElementAccessExpression(r) && r.questionDotToken === undefined && isIdentTokenNode(r.expression, sf) && ts.isStringLiteral(r.argumentExpression));
if (returnsIdentish) out.add(nameArg.text);
}
/** The body of a `get() {...}` method / `get: function [name]() {...}`
* descriptor prop (merve's two getter spellings — arrows never match). */
function getterBodyOf(p: ts.ObjectLiteralElementLike): ts.Block | undefined {
if (
ts.isMethodDeclaration(p) &&
ts.isIdentifier(p.name) &&
p.name.text === "get" &&
p.asteriskToken === undefined &&
(ts.getModifiers(p) ?? []).length === 0 &&
p.parameters.length === 0
) {
return p.body;
}
if (
ts.isPropertyAssignment(p) &&
ts.isIdentifier(p.name) &&
p.name.text === "get" &&
ts.isFunctionExpression(p.initializer) &&
p.initializer.parameters.length === 0 &&
p.initializer.asteriskToken === undefined &&
(ts.getModifiers(p.initializer) ?? []).length === 0
) {
return p.initializer.body;
}
return undefined;
}
/* ── the transpiler star-reexport patterns ─────────────────────────────── */
/** The star-export call: callee is `__export` or `__exportStar` (bare, or
* the NAME of any member chain — `tslib_1.__exportStar`, `a.b.__exportStar`)
* and the first argument leads with `require('spec')`. Byte quirk: the
* source must read `NAME(require` with no trivia between the name, the
* `(`, and `require` — one space anywhere in that seam kills the match
* (probed; spaces INSIDE the require call are fine). */
function starExportSpecOf(call: ts.CallExpression, sf: ts.SourceFile): string | null {
if (call.questionDotToken !== undefined) return null;
const callee = call.expression;
let nameNode: ts.Identifier | undefined;
if (ts.isIdentifier(callee)) nameNode = callee;
else if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) nameNode = callee.name;
if (nameNode === undefined) return null;
if (nameNode.text !== "__export" && nameNode.text !== "__exportStar") return null;
if (!sourceSpellsIdentifier(nameNode, sf)) return null;
const arg0 = call.arguments[0];
if (arg0 === undefined) return null;
if (!byteAdjacentParen(sf, nameNode.getEnd(), arg0.getStart(sf))) return null;
return leadingRequireOf(arg0, sf)?.spec ?? null;
}
/** True when the bytes between a callee name ending at `nameEnd` and its
* first argument starting at `argStart` are exactly `(` — merve's anchored
* `NAME(require` seam. */
function byteAdjacentParen(sf: ts.SourceFile, nameEnd: number, argStart: number): boolean {
return argStart === nameEnd + 1 && sf.text[nameEnd] === "(";
}
/** The Babel star-copy assignment: a variable statement whose FIRST
* declarator is `ID = require('spec')` or
* `ID = _interopRequireWildcard(require('spec'))` (the wildcard helper a
* bare identifier, byte-adjacent to `(require` like the star form —
* probed: a member-qualified helper or one space after its paren misses).
* Answers [ID, spec]. */
function starAssignOf(stmt: ts.VariableStatement, sf: ts.SourceFile): [string, string] | null {
const decl = stmt.declarationList.declarations[0];
if (decl === undefined || !ts.isIdentifier(decl.name) || !sourceSpellsIdentifier(decl.name, sf)) return null;
const init = decl.initializer;
if (init === undefined) return null;
const direct = bareRequireSpecOf(init);
if (direct !== null) return [decl.name.text, direct];
if (
ts.isCallExpression(init) &&
init.questionDotToken === undefined &&
ts.isIdentifier(init.expression) &&
init.expression.text === "_interopRequireWildcard" &&
init.arguments.length >= 1
) {
const arg0 = init.arguments[0]!;
if (!byteAdjacentParen(sf, init.expression.getEnd(), arg0.getStart(sf))) return null;
const spec = bareRequireSpecOf(arg0);
if (spec !== null) return [decl.name.text, spec];
}
return null;
}
/** `exports` / `module.exports` — the copy loop accepts either spelling. */
function isExportsTarget(e: ts.Expression): boolean {
return isExportsIdent(e) || isModuleExports(e);
}
/** `X === 'str'` / `X !== 'str'` with X the loop key identifier. */
function keyStringCompare(e: ts.Expression, key: string, op: ts.SyntaxKind, str: string): boolean {
return (
ts.isBinaryExpression(e) &&
e.operatorToken.kind === op &&
ts.isIdentifier(e.left) &&
e.left.text === key &&
ts.isStringLiteral(e.right) &&
e.right.text === str
);
}
/** `Object[.prototype].hasOwnProperty.call(ANY_ID, KEY)` — the guard merve
* accepts in both loop filter forms. */
function isHasOwnCall(e: ts.Expression, key: string): boolean {
if (!ts.isCallExpression(e) || e.questionDotToken !== undefined) return false;
const callee = e.expression;
if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.name) || callee.name.text !== "call") return false;
let cur = callee.expression;
if (!ts.isPropertyAccessExpression(cur) || !ts.isIdentifier(cur.name) || cur.name.text !== "hasOwnProperty") return false;
cur = cur.expression;
if (ts.isPropertyAccessExpression(cur) && ts.isIdentifier(cur.name) && cur.name.text === "prototype") cur = cur.expression;
if (!ts.isIdentifier(cur) || cur.text !== "Object") return false;
if (e.arguments.length !== 2) return false;
const [recv, k] = e.arguments as unknown as [ts.Expression, ts.Expression];
return ts.isIdentifier(recv) && ts.isIdentifier(k) && k.text === key;
}
/** `ID.hasOwnProperty(KEY)` — form B's alternative hasOwn spelling. */
function isDirectHasOwnCall(e: ts.Expression, key: string): boolean {
return (
ts.isCallExpression(e) &&
e.questionDotToken === undefined &&
ts.isPropertyAccessExpression(e.expression) &&
ts.isIdentifier(e.expression.name) &&
e.expression.name.text === "hasOwnProperty" &&
ts.isIdentifier(e.expression.expression) &&
e.arguments.length === 1 &&
ts.isIdentifier(e.arguments[0]!) &&
(e.arguments[0] as ts.Identifier).text === key
);
}
/** The loop-body COPY statement: `EXPORTS[KEY] = ID[KEY];` or the
* enumerable-getter defineProperty (`Object.defineProperty(EXPORTS, KEY,
* { enumerable: true, get[: function[name]] () { return ID[KEY]; } })`). */
function isCopyStatement(stmt: ts.Statement, id1: string, key: string): boolean {
if (!ts.isExpressionStatement(stmt)) return false;
const e = stmt.expression;
if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
return isKeyedAccess(e.left, isExportsTarget, key) && isKeyedAccess(e.right, (x) => ts.isIdentifier(x) && x.text === id1, key);
}
if (!ts.isCallExpression(e) || e.questionDotToken !== undefined) return false;
const callee = e.expression;
if (
!ts.isPropertyAccessExpression(callee) ||
!ts.isIdentifier(callee.expression) ||
callee.expression.text !== "Object" ||
!ts.isIdentifier(callee.name) ||
callee.name.text !== "defineProperty"
) {
return false;
}
if (e.arguments.length !== 3) return false;
const [recv, nameArg, desc] = e.arguments as unknown as [ts.Expression, ts.Expression, ts.Expression];
if (!isExportsTarget(recv)) return false;
if (!ts.isIdentifier(nameArg) || nameArg.text !== key) return false;
if (!ts.isObjectLiteralExpression(desc) || desc.properties.length !== 2) return false;
const [en, get] = desc.properties as unknown as [ts.ObjectLiteralElementLike, ts.ObjectLiteralElementLike];
if (!ts.isPropertyAssignment(en) || !ts.isIdentifier(en.name) || en.name.text !== "enumerable") return false;
if (en.initializer.kind !== ts.SyntaxKind.TrueKeyword) return false;
const body = getterBodyOf(get);
if (body === undefined || body.statements.length !== 1) return false;
const ret = body.statements[0]!;
if (!ts.isReturnStatement(ret) || ret.expression === undefined) return false;
return isKeyedAccess(ret.expression, (x) => ts.isIdentifier(x) && x.text === id1, key);
}
/** `RECV[KEY]` where `recvOk` approves the receiver. */
function isKeyedAccess(e: ts.Expression, recvOk: (x: ts.Expression) => boolean, key: string): boolean {
return (
ts.isElementAccessExpression(e) &&
e.questionDotToken === undefined &&
recvOk(e.expression) &&
ts.isIdentifier(e.argumentExpression) &&
e.argumentExpression.text === key
);
}
/** An UNBRACED bare `return;` — merve reads the literal `return` token
* right after the filter's `)`; a `{ return; }` block misses (probed). */
function isBareReturn(stmt: ts.Statement): boolean {
return ts.isReturnStatement(stmt) && stmt.expression === undefined;
}
/** The Babel copy loop `Object.keys(ID1).forEach(function (KEY) {...})` —
* answers ID1 when the whole shape matches merve's EXPORT_STAR_LIB
* grammar; the caller links ID1 to a star assignment for the spec. */
function starLoopIdOf(call: ts.CallExpression, sf: ts.SourceFile): string | null {
if (call.questionDotToken !== undefined) return null;
const callee = call.expression;
if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken !== undefined) return null;
if (!ts.isIdentifier(callee.name) || callee.name.text !== "forEach") return null;
const keysCall = callee.expression;
if (!ts.isCallExpression(keysCall) || keysCall.questionDotToken !== undefined) return null;
const keysCallee = keysCall.expression;
if (
!ts.isPropertyAccessExpression(keysCallee) ||
!ts.isIdentifier(keysCallee.expression) ||
keysCallee.expression.text !== "Object" ||
!ts.isIdentifier(keysCallee.name) ||
keysCallee.name.text !== "keys"
) {
return null;
}
if (keysCall.arguments.length !== 1) return null;
const id1Node = keysCall.arguments[0]!;
if (!ts.isIdentifier(id1Node) || !sourceSpellsIdentifier(id1Node, sf)) return null;
const id1 = id1Node.text;
const fn = call.arguments[0];
if (fn === undefined || !ts.isFunctionExpression(fn)) return null; // arrows never match (probed)
if (fn.name !== undefined) return null; // a NAMED callback misses (probed)
if (fn.asteriskToken !== undefined || (ts.getModifiers(fn) ?? []).length > 0) return null;
if (fn.parameters.length !== 1) return null;
const keyParam = fn.parameters[0]!;
if (!ts.isIdentifier(keyParam.name) || keyParam.initializer !== undefined || keyParam.dotDotDotToken !== undefined) return null;
const key = keyParam.name.text;
const stmts = fn.body.statements;
// Form B: the single `if (KEY !== 'default' [&& !hasOwn]) COPY` statement.
if (stmts.length === 1 && ts.isIfStatement(stmts[0]!) && stmts[0]!.elseStatement === undefined) {
const ifStmt = stmts[0]!;
let cond = ifStmt.expression;
if (ts.isBinaryExpression(cond) && cond.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
const right = cond.right;
if (
!ts.isPrefixUnaryExpression(right) ||
right.operator !== ts.SyntaxKind.ExclamationToken ||
!(isHasOwnCall(right.operand, key) || isDirectHasOwnCall(right.operand, key))
) {
return null;
}
cond = cond.left;
}
if (!keyStringCompare(cond, key, ts.SyntaxKind.ExclamationEqualsEqualsToken, "default")) return null;
return isCopyStatement(ifStmt.thenStatement, id1, key) ? id1 : null;
}
// Form A: `if (KEY === 'default' || KEY === '__esModule') return;` (exact
// order), then optional hasOwn / in-exports guards, then the copy.
if (stmts.length < 2 || stmts.length > 4) return null;
const head = stmts[0]!;
if (!ts.isIfStatement(head) || head.elseStatement !== undefined || !isBareReturn(head.thenStatement)) return null;
const headCond = head.expression;
if (
!ts.isBinaryExpression(headCond) ||
headCond.operatorToken.kind !== ts.SyntaxKind.BarBarToken ||
!keyStringCompare(headCond.left, key, ts.SyntaxKind.EqualsEqualsEqualsToken, "default") ||
!keyStringCompare(headCond.right, key, ts.SyntaxKind.EqualsEqualsEqualsToken, "__esModule")
) {
return null;
}
let i = 1;
if (i < stmts.length - 1) {
const s = stmts[i]!;
if (ts.isIfStatement(s) && s.elseStatement === undefined && isBareReturn(s.thenStatement) && isHasOwnCall(s.expression, key)) i++;
}
if (i < stmts.length - 1) {
// `if (KEY in EXPORTS && EXPORTS[KEY] === ID1[KEY]) return;`
const s = stmts[i]!;
if (ts.isIfStatement(s) && s.elseStatement === undefined && isBareReturn(s.thenStatement)) {
const c = s.expression;
const inOk =
ts.isBinaryExpression(c) &&
c.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
ts.isBinaryExpression(c.left) &&
c.left.operatorToken.kind === ts.SyntaxKind.InKeyword &&
ts.isIdentifier(c.left.left) &&
c.left.left.text === key &&
isExportsTarget(c.left.right) &&
ts.isBinaryExpression(c.right) &&
c.right.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken &&
isKeyedAccess(c.right.left, isExportsTarget, key) &&
isKeyedAccess(c.right.right, (x) => ts.isIdentifier(x) && x.text === id1, key);
if (inOk) i++;
}
}
if (i !== stmts.length - 1) return null;
return isCopyStatement(stmts[i]!, id1, key) ? id1 : null;
}
/* ── the walk ──────────────────────────────────────────────────────────── */
/** Node kinds whose CHILDREN sit behind a `{` (or `${`) — merve's
* "top-level" for the star patterns is brace depth 0, so an unbraced
* `if (c) __exportStar(...)` still counts while any block hides it. */
function opensBraces(n: ts.Node): boolean {
switch (n.kind) {
case ts.SyntaxKind.Block:
case ts.SyntaxKind.ModuleBlock:
case ts.SyntaxKind.CaseBlock:
case ts.SyntaxKind.ObjectLiteralExpression:
case ts.SyntaxKind.ObjectBindingPattern:
case ts.SyntaxKind.ClassDeclaration:
case ts.SyntaxKind.ClassExpression:
case ts.SyntaxKind.EnumDeclaration:
case ts.SyntaxKind.TemplateSpan:
return true;
default:
return false;
}
}
/** Preorder walk with an EXPLICIT stack (minified npm bundles nest deeply
* enough to overflow recursive visits) carrying each node's brace depth. */
function walkWithBraceDepth(sf: ts.SourceFile, cb: (node: ts.Node, braceDepth: number) => void): void {
const stack: [ts.Node, number][] = [[sf, 0]];
const children: ts.Node[] = [];
while (stack.length > 0) {
const [n, depth] = stack.pop()!;
cb(n, depth);
const childDepth = depth + (opensBraces(n) ? 1 : 0);
children.length = 0;
ts.forEachChild(n, (c) => {
children.push(c);
});
for (let i = children.length - 1; i >= 0; i--) stack.push([children[i]!, childDepth]);
}
}
/** The single-file lex over SOURCE TEXT: every detection site, position-
* blind like the real lexer for the assignment forms (nested functions and
* dead branches count), brace-depth-0 for the star patterns. */
export function cjsLexedExportsOf(source: string, fileName = "module.cjs"): CjsLexedExports {
const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, false, ts.ScriptKind.JS);
const exports = new Set<string>();
/** Position-ordered reexport EVENTS: every `module.exports =` assignment
* clears the pending list before contributing its own require forms;
* star calls and Babel loops append at their own positions (a loop's
* position is the LOOP's, not its var's — probed). */
type ReexportEvent =
| { pos: number; kind: "assign"; rhs: ts.Expression }
| { pos: number; kind: "spec"; spec: string }
| { pos: number; kind: "loop"; id1: string };
const events: ReexportEvent[] = [];
/** Star-assignment declarators by identifier — the Babel loop's linkage. */
const starAssigns = new Map<string, string[]>();
walkWithBraceDepth(sf, (n, braceDepth) => {
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
const lhs = n.left;
if (
ts.isPropertyAccessExpression(lhs) &&
lhs.questionDotToken === undefined &&
ts.isIdentifier(lhs.name) &&
(isExportsIdent(lhs.expression) || isModuleExports(lhs.expression)) &&
// the whole NAME must be one identifier run — a unicode escape
// mid-name breaks merve's run before the `=`, so the pattern
// never matches (probed: Node detects nothing)
isIdentTokenNode(lhs.name, sf)
) {
exports.add(lhs.name.text);
} else if (
ts.isElementAccessExpression(lhs) &&
lhs.questionDotToken === undefined &&
ts.isStringLiteral(lhs.argumentExpression) &&
(isExportsIdent(lhs.expression) || isModuleExports(lhs.expression))
) {
exports.add(lhs.argumentExpression.text);
} else if (isModuleExports(lhs)) {
events.push({ pos: n.getStart(sf), kind: "assign", rhs: n.right });
}
} else if (ts.isCallExpression(n)) {
scanDefineProperty(n, sf, exports);
if (braceDepth === 0) {
const starSpec = starExportSpecOf(n, sf);
if (starSpec !== null) events.push({ pos: n.getStart(sf), kind: "spec", spec: starSpec });
const loopId = starLoopIdOf(n, sf);
if (loopId !== null) events.push({ pos: n.getStart(sf), kind: "loop", id1: loopId });
}
} else if (ts.isVariableStatement(n) && braceDepth === 0) {
const assign = starAssignOf(n, sf);
if (assign !== null) {
const specs = starAssigns.get(assign[0]);
if (specs === undefined) starAssigns.set(assign[0], [assign[1]]);
else specs.push(assign[1]);
}
}
});
events.sort((a, b) => a.pos - b.pos);
let reexports: string[] = [];
for (const ev of events) {
if (ev.kind === "spec") {
reexports.push(ev.spec);
} else if (ev.kind === "loop") {
for (const spec of starAssigns.get(ev.id1) ?? []) reexports.push(spec);
} else {
reexports = []; // every `module.exports =` clears — stars included (probed)
const req = leadingRequireOf(ev.rhs, sf);
if (req !== null) {
reexports.push(req.spec);
} else if (ts.isObjectLiteralExpression(ev.rhs)) {
// A PARENTHESIZED literal never matches — merve needs the `{` right
// after the `=` — so no unwrapping here (unlike the checker's view).
scanTableLiteral(ev.rhs, sf, exports, reexports);
}
}
}
return { exports, reexports };
return cjsLexedExportsOfFile(parseSourceFile(fileName, source, "js"));
}
/** The full Node-visible named-export set of a CommonJS module: its own
* lexed names plus, recursively, the names of every reexport target that
* itself resolves to a CommonJS module (Node's cjsPreparseModuleExports —
* ESM / JSON / builtin / unresolved targets contribute nothing). Generic
* over the caller's module HANDLE so both typescript worlds can drive it
* with their own file objects — only strings cross this boundary.
* Cycle-safe. */
export function cjsLexerVisibleNames<H>(
mod: H,
sourceOf: (mod: H) => string,
resolveCjsDep: (from: H, spec: string) => H | null,
memo: Map<H, Set<string>> = new Map(),
): Set<string> {
const hit = memo.get(mod);
if (hit !== undefined) return hit;
const { exports, reexports } = cjsLexedExportsOf(sourceOf(mod));
memo.set(mod, exports); // set BEFORE recursing, like Node — cycles converge
for (const spec of reexports) {
const dep = resolveCjsDep(mod, spec);
if (dep === null) continue;
for (const name of cjsLexerVisibleNames(dep, sourceOf, resolveCjsDep, memo)) exports.add(name);
}
return exports;
return cjsVisibleNames(mod, (item) => cjsLexedExportsOf(sourceOf(item)), resolveCjsDep, memo);
}
@@ -0,0 +1,798 @@
/* Node's CommonJS named-export DETECTABILITY test — the compile-time
* answer to "which names can `import { x } from './cjs.js'` actually
* link?". Node never executes a CJS module to build its ESM facade: it
* lexes the source with its vendored CJS lexer (Node v24.15.0 vendors
* deps/merve, the native `cjs_lexer` binding that replaced the
* cjs-module-lexer wasm build; the detection patterns are FROZEN by
* upstream policy) and only the names that lexer detects become named
* exports. The checker sees the real export table, so it happily binds
* names the lexer cannot see — `module.exports = { a: 7 }` type-checks a
* named import of `a` that Node refuses at link time with a SyntaxError.
* This module mirrors merve's detection so the compiler can answer
* exactly what Node answers — for the program's own CJS files (program.ts
* gates the ESM instantiate graph) AND for the --dynamic island's embedded
* npm modules (npm.ts synthesizes each CJS facade from this lexer, so an
* embedded named import fails exactly where Node's would).
*
* The syntax pass consumes an owned native TypeScript AST. Detection still
* reads raw source wherever merve's byte quirks demand it; parser semantics
* never replace Node's export-detectability rules.
*
* The port is deliberately quirk-faithful — these all match probed Node
* v24.15.0 behavior, not what a clean reimplementation would choose:
* - `module.exports = { ... }` props scan LEFT TO RIGHT and the scan
* STOPS at the first prop it cannot shape — names added before the
* stop are kept, everything after is invisible. `{ vis: v, lit: 7 }`
* exports `vis` only; `{ lit: 7, vis: v }` exports nothing.
* - A prop VALUE must start with an identifier-ish token (an identifier,
* keyword, `require(...)` call — anything whose first character is
* [A-Za-z_$] or non-ASCII). Literals (`7`, `"s"`, backticks, arrows'
* `(`) stop the scan WITHOUT adding the key.
* - After an accepted value the lexer consumes ONE identifier run and
* requires the very next CHARACTER to be `,` or `}` — `a: b, c: d`
* keeps both, `a: b , c: d` keeps only `a` (the space stops the scan),
* `a: o.x` / `a: f()` keep `a` and stop.
* - A `require('spec')` VALUE adds the key, records the spec as a
* REEXPORT, and stops the scan unconditionally — `{ a: require('x'),
* b: v }` exports `a` plus x's names, never `b`. Trailing tokens on the
* require (`.foo`, `()`, `|| y`) change nothing.
* - `get name() {...}` stops the scan cold (no add); `set name(v) {}`
* adds the literal name "set" and stops; `async name() {}` adds
* "async" and stops; a plain method `name() {}` adds `name` and stops.
* - `exports.NAME = ` / `module.exports.NAME = ` / the ['string'] forms
* match ANYWHERE in the file — nested functions, dead branches — with
* no scope analysis, and nothing ever removes a detected name.
* - `module.exports = require('./x')` records a REEXPORT whenever the
* require call is the LEADING token of the right-hand side — trailers
* (`.foo`, `()`, `|| {}`, `? a : b`) are ignored, but a leading `(`
* breaks the match (`(require('x'))` records nothing). Node resolves
* each reexport and unions the target's detected names when the target
* is itself CommonJS. Every `module.exports =` assignment CLEARS the
* pending reexport list first — including star-pattern reexports
* recorded earlier — so only reexports at or after the last assignment
* count, while detected export NAMES accumulate across everything.
* - `Object.defineProperty(exports, 'n', {...})` adds `n` for the exact
* descriptor shapes merve accepts: optional leading `enumerable: true`
* then `value:` (anything), or a getter whose whole body is `return
* IDENT` / `return IDENT.IDENT` / `return IDENT['str']` and which is
* the descriptor's last property. Unlike cjs-module-lexer 2.2.0, a
* NON-matching defineProperty of the same name elsewhere does NOT
* poison the valid one (probed: Node keeps the export).
* - The transpiler star-reexport patterns ARE detected (tsc/babel output
* all over npm depends on them):
* · `__export(require('x'))` / `__exportStar(require('x'), exports)`
* — callee is the bare identifier or any member chain ending in
* one of those two exact names, at TOP LEVEL (brace depth 0 — an
* unbraced `if (c) __exportStar(...)` still counts, a braced block
* does not), and the source bytes must read `NAME(require` with NO
* whitespace or comment between the name, the `(`, and `require`
* (probed: one space kills the match; spaces anywhere inside the
* require call are fine).
* · The Babel copy loop: a top-level `var/const/let ID =
* [_interopRequireWildcard(]require('x')[)]` (ID the FIRST
* declarator; `_interopRequireWildcard` bare and byte-adjacent to
* `(require` like the star form) linked by ID to a top-level
* `Object.keys(ID).forEach(function (KEY) {...})` whose body is
* byte-for-byte one of merve's copy shapes: the `if (KEY ===
* 'default' || KEY === '__esModule') return;` filter (exact order,
* unbraced return) with optional hasOwnProperty / in-exports
* guards, or the single `if (KEY !== 'default' [&& !hasOwn]) copy`
* form; the copy either `EXPORTS[KEY] = ID[KEY]` or the
* enumerable-getter defineProperty. Named callbacks, arrow
* callbacks, braced returns, and reordered filters all miss.
* The reexport's clearing position is the LOOP's, not the var's.
* NOT modeled: cjs-module-lexer 2.2.0's phantom "get" export for an
* identifier-named getter in the table (`{ get a() {} }` exports nothing
* in Node — but `get [k]() {}` / `get "a"() {}` DO export the word "get",
* because the name token stops the lexer after it took `get` as a
* shorthand key). */
import * as ts from "./ts7/syntax.js";
export interface CjsLexedExports {
/** Names Node's lexer detects in THIS file (no reexport resolution). */
exports: Set<string>;
/** Reexport specifiers surviving the last `module.exports =` clearing,
* in source order: the assignment's own require forms plus every later
* (or unclear-ed) star-pattern / Babel-loop match. */
reexports: string[];
}
/** merve's byte classification: [A-Za-z_$] plus every non-ASCII byte. */
function isIdentStartChar(ch: string): boolean {
return /[A-Za-z_$]/.test(ch) || ch.charCodeAt(0) > 127;
}
function isIdentChar(ch: string): boolean {
return /[A-Za-z0-9_$]/.test(ch) || ch.charCodeAt(0) > 127;
}
/** End of the identifier run starting at `pos` (merve consumes maximal
* identifier characters and nothing else — no member chains, no calls). */
function identRunEnd(text: string, pos: number): number {
let i = pos;
while (i < text.length && isIdentChar(text[i]!)) i++;
return i;
}
/** True when the node's SOURCE spelling starts like a lexer identifier —
* a `a`-escaped name parses to the same AST as its plain spelling,
* but merve reads bytes and stops at the backslash. */
function sourceSpellsIdentifier(node: ts.Node, sf: ts.SourceFile): boolean {
const ch = sf.text[node.getStart(sf)];
return ch !== undefined && isIdentStartChar(ch);
}
/** True when the node's WHOLE source span is one identifier run — what
* merve takes for IDENT. Keywords count (`return this.x` satisfies the
* defineProperty getter's `return IDENT.IDENT` because `this` lexes as an
* identifier run — probed), escapes and private names do not. */
function isIdentTokenNode(node: ts.Node, sf: ts.SourceFile): boolean {
const start = node.getStart(sf);
const ch = sf.text[start];
return ch !== undefined && isIdentStartChar(ch) && identRunEnd(sf.text, start) === node.getEnd();
}
/** The identifier run at the node's START — the whole name when its
* spelling is plain, the byte prefix merve consumed when a unicode escape
* interrupts the run mid-name. */
function identPrefixOf(node: ts.Node, sf: ts.SourceFile): string {
const start = node.getStart(sf);
return sf.text.slice(start, identRunEnd(sf.text, start));
}
/** The bare `require('spec')` call merve recognizes. */
export function bareRequireSpecOf(e: ts.Node): string | null {
if (!ts.isCallExpression(e) || e.questionDotToken !== undefined) return null;
if (!ts.isIdentifier(e.expression) || e.expression.text !== "require") return null;
if (e.arguments.length !== 1) return null;
const arg = e.arguments[0]!;
return ts.isStringLiteral(arg) ? arg.text : null;
}
/** True when `e` is exactly the `exports` identifier. */
export function isExportsIdent(e: ts.Expression): boolean {
return ts.isIdentifier(e) && e.text === "exports";
}
/** True when `e` is exactly `module.exports`. */
export function isModuleExports(e: ts.Expression): boolean {
return (
ts.isPropertyAccessExpression(e) &&
e.questionDotToken === undefined &&
ts.isIdentifier(e.expression) &&
e.expression.text === "module" &&
ts.isIdentifier(e.name) &&
e.name.text === "exports"
);
}
/** The `require(...)` call at the very START of `e`'s source span, if any:
* merve records the reexport as soon as it lexes `require('spec')` and any
* TRAILER (`.foo`, `()`, `|| y`, `? a : b`) merely stops whatever scan
* follows — so the recognizer descends the leftmost-child chain while it
* still starts where `e` starts. A ParenthesizedExpression breaks the
* chain naturally (its child starts after the `(`), matching merve. */
function leadingRequireOf(e: ts.Expression, sf: ts.SourceFile): { spec: string; end: number } | null {
const start = e.getStart(sf);
let cur: ts.Node = e;
for (;;) {
const spec = bareRequireSpecOf(cur);
if (spec !== null && cur.getStart(sf) === start) return { spec, end: cur.getEnd() };
let first: ts.Node | undefined;
ts.forEachChild(cur, (c) => {
first ??= c;
});
if (first === undefined || first.getStart(sf) !== start) return null;
cur = first;
}
}
/** Skips ECMA whitespace and comments forward from `pos` (merve's
* commentWhitespace) and answers the next meaningful character (or ""). */
function nextMeaningfulChar(text: string, pos: number): string {
let i = pos;
while (i < text.length) {
const ch = text[i]!;
if (ch === " " || (ch.charCodeAt(0) > 8 && ch.charCodeAt(0) < 14)) {
i++;
} else if (ch === "/" && text[i + 1] === "/") {
const nl = text.indexOf("\n", i + 2);
if (nl < 0) return "";
i = nl;
} else if (ch === "/" && text[i + 1] === "*") {
const close = text.indexOf("*/", i + 2);
if (close < 0) return "";
i = close + 2;
} else {
return ch;
}
}
return "";
}
/** The `module.exports = { ... }` table scan — merve's tryParseLiteralExports
* over the AST. Adds detected names to `out`, appends reexports (spread /
* value requires) to `reexports`, stops at the first unshapeable prop. */
function scanTableLiteral(obj: ts.ObjectLiteralExpression, sf: ts.SourceFile, out: Set<string>, reexports: string[]): void {
const text = sf.text;
for (const prop of obj.properties) {
if (ts.isGetAccessor(prop)) {
// `get name() {...}` — merve's explicit early termination... unless
// the accessor's NAME is not a plain identifier, where the lexer
// already added the word "get" as a shorthand key before the odd
// token stops it (`get [k]() {}` exports "get").
if (!ts.isIdentifier(prop.name) || !sourceSpellsIdentifier(prop.name, sf)) out.add("get");
return;
}
if (ts.isSetAccessor(prop)) {
// The word `set` lexes as a shorthand key, then the accessor name
// stops the scan — Node really does export "set" (value undefined).
out.add("set");
return;
}
if (ts.isMethodDeclaration(prop)) {
// `name() {}` adds the name, then `(` stops the scan. The modifier /
// asterisk spellings shift which token the lexer takes as the key:
// `async name()` exports "async", `*name()` exports nothing. A
// unicode escape mid-name adds only the byte prefix before it (the
// key adds BEFORE the separator check fails — see the key path).
if (prop.asteriskToken !== undefined) return;
const mods = ts.getModifiers(prop) ?? [];
if (mods.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) {
out.add("async");
return;
}
if (ts.isIdentifier(prop.name) && sourceSpellsIdentifier(prop.name, sf)) out.add(identPrefixOf(prop.name, sf));
return;
}
if (ts.isShorthandPropertyAssignment(prop)) {
if (prop.objectAssignmentInitializer !== undefined) return; // `{ a = 1 }` — cover pattern, not a table
if (!ts.isIdentifier(prop.name) || !sourceSpellsIdentifier(prop.name, sf)) return;
if (!isIdentTokenNode(prop.name, sf)) {
// an escape mid-name: the byte prefix adds, then the backslash
// fails the separator check and stops the scan
out.add(identPrefixOf(prop.name, sf));
return;
}
out.add(prop.name.text);
continue; // whitespace before `,` is skipped on the shorthand path
}
if (ts.isSpreadAssignment(prop)) {
const req = leadingRequireOf(prop.expression, sf);
let consumedEnd: number;
if (req !== null) {
reexports.push(req.spec);
consumedEnd = req.end;
} else if (sourceSpellsIdentifier(prop.expression, sf)) {
consumedEnd = identRunEnd(text, prop.expression.getStart(sf));
} else {
return; // `...{}` and friends stop the scan
}
// The spread path DOES skip whitespace/comments before the `,` check
// — `...o , a: b` continues where `a: b , c` would stop.
const ch = nextMeaningfulChar(text, consumedEnd);
if (ch === ",") continue;
return; // `...o.p`, `...f()` — the leftover token stops the scan
// (a `}` here means the spread was last; stopping is the same)
}
if (ts.isPropertyAssignment(prop)) {
const name = prop.name;
let key: string | null = null;
if (ts.isIdentifier(name) && sourceSpellsIdentifier(name, sf)) {
if (!isIdentTokenNode(name, sf)) {
// merve adds the key BEFORE validating the `:` — an escape
// mid-name adds the byte prefix, then the backslash stops the
// scan (probed: the key's prefix exports, nothing after does)
out.add(identPrefixOf(name, sf));
return;
}
key = name.text;
} else if (ts.isStringLiteral(name)) {
key = name.text;
}
if (key === null) return; // numeric / computed / template keys stop the scan
const value = prop.initializer;
const req = leadingRequireOf(value, sf);
if (req !== null) {
// A require VALUE adds the key, records the reexport, and stops
// the scan UNCONDITIONALLY — probed: `{ a: require('x'), b: v }`
// exports `a` plus x's names and never `b` (the spread path above
// genuinely continues past a `,`; the value path does not).
out.add(key);
reexports.push(req.spec);
return;
}
const start = value.getStart(sf);
const ch0 = text[start];
if (ch0 === undefined || !isIdentStartChar(ch0)) return; // literal values stop WITHOUT adding the key
const consumedEnd = identRunEnd(text, start);
out.add(key);
// NO whitespace skip here — merve requires `,` or `}` as the very
// next character after the consumed value token. `a: b , c` and
// `a: b.c, d` both keep `a` and stop.
const ch = text[consumedEnd];
if (ch === ",") continue;
return; // `}` ends the props anyway; anything else stops the scan
}
return; // any other prop kind stops the scan
}
}
/** `Object.defineProperty(exports|module.exports, 'name', {...})` — adds
* the name for merve's exact descriptor shapes (see the header). */
function scanDefineProperty(call: ts.CallExpression, sf: ts.SourceFile, out: Set<string>): void {
const callee = call.expression;
if (
!ts.isPropertyAccessExpression(callee) ||
callee.questionDotToken !== undefined ||
!ts.isIdentifier(callee.expression) ||
callee.expression.text !== "Object" ||
!ts.isIdentifier(callee.name) ||
callee.name.text !== "defineProperty"
) {
return;
}
if (call.arguments.length !== 3) return;
const [recv, nameArg, desc] = call.arguments as unknown as [ts.Expression, ts.Expression, ts.Expression];
if (!isExportsIdent(recv) && !isModuleExports(recv)) return;
if (!ts.isStringLiteral(nameArg)) return;
if (!ts.isObjectLiteralExpression(desc)) return;
let i = 0;
const props = desc.properties;
const first = props[0];
if (
first !== undefined &&
ts.isPropertyAssignment(first) &&
ts.isIdentifier(first.name) &&
first.name.text === "enumerable"
) {
// Only the exact `enumerable: true` prefix is consumed; anything else
// in the slot fails the whole match.
if (first.initializer.kind !== ts.SyntaxKind.TrueKeyword) return;
i = 1;
}
const p = props[i];
if (p === undefined) return;
if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "value") {
out.add(nameArg.text); // merve stops right at `value:` — the value and the rest of the descriptor are unchecked
return;
}
// The getter forms: `get() {...}` method, or `get: function [name]() {...}`
// (never an arrow), body EXACTLY `return IDENT` / IDENT.IDENT / IDENT['s'],
// and the getter must be the descriptor's LAST property.
const body = getterBodyOf(p);
if (body === undefined || i !== props.length - 1) return;
if (body.statements.length !== 1) return;
const ret = body.statements[0]!;
if (!ts.isReturnStatement(ret) || ret.expression === undefined) return;
const r = ret.expression;
const returnsIdentish =
isIdentTokenNode(r, sf) ||
(ts.isPropertyAccessExpression(r) && r.questionDotToken === undefined && isIdentTokenNode(r.expression, sf) && isIdentTokenNode(r.name, sf)) ||
(ts.isElementAccessExpression(r) && r.questionDotToken === undefined && isIdentTokenNode(r.expression, sf) && ts.isStringLiteral(r.argumentExpression));
if (returnsIdentish) out.add(nameArg.text);
}
/** The body of a `get() {...}` method / `get: function [name]() {...}`
* descriptor prop (merve's two getter spellings — arrows never match). */
function getterBodyOf(p: ts.ObjectLiteralElementLike): ts.Block | undefined {
if (
ts.isMethodDeclaration(p) &&
ts.isIdentifier(p.name) &&
p.name.text === "get" &&
p.asteriskToken === undefined &&
(ts.getModifiers(p) ?? []).length === 0 &&
p.parameters.length === 0
) {
return p.body;
}
if (
ts.isPropertyAssignment(p) &&
ts.isIdentifier(p.name) &&
p.name.text === "get" &&
ts.isFunctionExpression(p.initializer) &&
p.initializer.parameters.length === 0 &&
p.initializer.asteriskToken === undefined &&
(ts.getModifiers(p.initializer) ?? []).length === 0
) {
return p.initializer.body;
}
return undefined;
}
/* ── the transpiler star-reexport patterns ─────────────────────────────── */
/** The star-export call: callee is `__export` or `__exportStar` (bare, or
* the NAME of any member chain — `tslib_1.__exportStar`, `a.b.__exportStar`)
* and the first argument leads with `require('spec')`. Byte quirk: the
* source must read `NAME(require` with no trivia between the name, the
* `(`, and `require` — one space anywhere in that seam kills the match
* (probed; spaces INSIDE the require call are fine). */
function starExportSpecOf(call: ts.CallExpression, sf: ts.SourceFile): string | null {
if (call.questionDotToken !== undefined) return null;
const callee = call.expression;
let nameNode: ts.Identifier | undefined;
if (ts.isIdentifier(callee)) nameNode = callee;
else if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) nameNode = callee.name;
if (nameNode === undefined) return null;
if (nameNode.text !== "__export" && nameNode.text !== "__exportStar") return null;
if (!sourceSpellsIdentifier(nameNode, sf)) return null;
const arg0 = call.arguments[0];
if (arg0 === undefined) return null;
if (!byteAdjacentParen(sf, nameNode.getEnd(), arg0.getStart(sf))) return null;
return leadingRequireOf(arg0, sf)?.spec ?? null;
}
/** True when the bytes between a callee name ending at `nameEnd` and its
* first argument starting at `argStart` are exactly `(` — merve's anchored
* `NAME(require` seam. */
function byteAdjacentParen(sf: ts.SourceFile, nameEnd: number, argStart: number): boolean {
return argStart === nameEnd + 1 && sf.text[nameEnd] === "(";
}
/** The Babel star-copy assignment: a variable statement whose FIRST
* declarator is `ID = require('spec')` or
* `ID = _interopRequireWildcard(require('spec'))` (the wildcard helper a
* bare identifier, byte-adjacent to `(require` like the star form —
* probed: a member-qualified helper or one space after its paren misses).
* Answers [ID, spec]. */
function starAssignOf(stmt: ts.VariableStatement, sf: ts.SourceFile): [string, string] | null {
const decl = stmt.declarationList.declarations[0];
if (decl === undefined || !ts.isIdentifier(decl.name) || !sourceSpellsIdentifier(decl.name, sf)) return null;
const init = decl.initializer;
if (init === undefined) return null;
const direct = bareRequireSpecOf(init);
if (direct !== null) return [decl.name.text, direct];
if (
ts.isCallExpression(init) &&
init.questionDotToken === undefined &&
ts.isIdentifier(init.expression) &&
init.expression.text === "_interopRequireWildcard" &&
init.arguments.length >= 1
) {
const arg0 = init.arguments[0]!;
if (!byteAdjacentParen(sf, init.expression.getEnd(), arg0.getStart(sf))) return null;
const spec = bareRequireSpecOf(arg0);
if (spec !== null) return [decl.name.text, spec];
}
return null;
}
/** `exports` / `module.exports` — the copy loop accepts either spelling. */
function isExportsTarget(e: ts.Expression): boolean {
return isExportsIdent(e) || isModuleExports(e);
}
/** `X === 'str'` / `X !== 'str'` with X the loop key identifier. */
function keyStringCompare(e: ts.Expression, key: string, op: ts.SyntaxKind, str: string): boolean {
return (
ts.isBinaryExpression(e) &&
e.operatorToken.kind === op &&
ts.isIdentifier(e.left) &&
e.left.text === key &&
ts.isStringLiteral(e.right) &&
e.right.text === str
);
}
/** `Object[.prototype].hasOwnProperty.call(ANY_ID, KEY)` — the guard merve
* accepts in both loop filter forms. */
function isHasOwnCall(e: ts.Expression, key: string): boolean {
if (!ts.isCallExpression(e) || e.questionDotToken !== undefined) return false;
const callee = e.expression;
if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.name) || callee.name.text !== "call") return false;
let cur = callee.expression;
if (!ts.isPropertyAccessExpression(cur) || !ts.isIdentifier(cur.name) || cur.name.text !== "hasOwnProperty") return false;
cur = cur.expression;
if (ts.isPropertyAccessExpression(cur) && ts.isIdentifier(cur.name) && cur.name.text === "prototype") cur = cur.expression;
if (!ts.isIdentifier(cur) || cur.text !== "Object") return false;
if (e.arguments.length !== 2) return false;
const [recv, k] = e.arguments as unknown as [ts.Expression, ts.Expression];
return ts.isIdentifier(recv) && ts.isIdentifier(k) && k.text === key;
}
/** `ID.hasOwnProperty(KEY)` — form B's alternative hasOwn spelling. */
function isDirectHasOwnCall(e: ts.Expression, key: string): boolean {
return (
ts.isCallExpression(e) &&
e.questionDotToken === undefined &&
ts.isPropertyAccessExpression(e.expression) &&
ts.isIdentifier(e.expression.name) &&
e.expression.name.text === "hasOwnProperty" &&
ts.isIdentifier(e.expression.expression) &&
e.arguments.length === 1 &&
ts.isIdentifier(e.arguments[0]!) &&
(e.arguments[0] as ts.Identifier).text === key
);
}
/** The loop-body COPY statement: `EXPORTS[KEY] = ID[KEY];` or the
* enumerable-getter defineProperty (`Object.defineProperty(EXPORTS, KEY,
* { enumerable: true, get[: function[name]] () { return ID[KEY]; } })`). */
function isCopyStatement(stmt: ts.Statement, id1: string, key: string): boolean {
if (!ts.isExpressionStatement(stmt)) return false;
const e = stmt.expression;
if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
return isKeyedAccess(e.left, isExportsTarget, key) && isKeyedAccess(e.right, (x) => ts.isIdentifier(x) && x.text === id1, key);
}
if (!ts.isCallExpression(e) || e.questionDotToken !== undefined) return false;
const callee = e.expression;
if (
!ts.isPropertyAccessExpression(callee) ||
!ts.isIdentifier(callee.expression) ||
callee.expression.text !== "Object" ||
!ts.isIdentifier(callee.name) ||
callee.name.text !== "defineProperty"
) {
return false;
}
if (e.arguments.length !== 3) return false;
const [recv, nameArg, desc] = e.arguments as unknown as [ts.Expression, ts.Expression, ts.Expression];
if (!isExportsTarget(recv)) return false;
if (!ts.isIdentifier(nameArg) || nameArg.text !== key) return false;
if (!ts.isObjectLiteralExpression(desc) || desc.properties.length !== 2) return false;
const [en, get] = desc.properties as unknown as [ts.ObjectLiteralElementLike, ts.ObjectLiteralElementLike];
if (!ts.isPropertyAssignment(en) || !ts.isIdentifier(en.name) || en.name.text !== "enumerable") return false;
if (en.initializer.kind !== ts.SyntaxKind.TrueKeyword) return false;
const body = getterBodyOf(get);
if (body === undefined || body.statements.length !== 1) return false;
const ret = body.statements[0]!;
if (!ts.isReturnStatement(ret) || ret.expression === undefined) return false;
return isKeyedAccess(ret.expression, (x) => ts.isIdentifier(x) && x.text === id1, key);
}
/** `RECV[KEY]` where `recvOk` approves the receiver. */
function isKeyedAccess(e: ts.Expression, recvOk: (x: ts.Expression) => boolean, key: string): boolean {
return (
ts.isElementAccessExpression(e) &&
e.questionDotToken === undefined &&
recvOk(e.expression) &&
ts.isIdentifier(e.argumentExpression) &&
e.argumentExpression.text === key
);
}
/** An UNBRACED bare `return;` — merve reads the literal `return` token
* right after the filter's `)`; a `{ return; }` block misses (probed). */
function isBareReturn(stmt: ts.Statement): boolean {
return ts.isReturnStatement(stmt) && stmt.expression === undefined;
}
/** The Babel copy loop `Object.keys(ID1).forEach(function (KEY) {...})` —
* answers ID1 when the whole shape matches merve's EXPORT_STAR_LIB
* grammar; the caller links ID1 to a star assignment for the spec. */
function starLoopIdOf(call: ts.CallExpression, sf: ts.SourceFile): string | null {
if (call.questionDotToken !== undefined) return null;
const callee = call.expression;
if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken !== undefined) return null;
if (!ts.isIdentifier(callee.name) || callee.name.text !== "forEach") return null;
const keysCall = callee.expression;
if (!ts.isCallExpression(keysCall) || keysCall.questionDotToken !== undefined) return null;
const keysCallee = keysCall.expression;
if (
!ts.isPropertyAccessExpression(keysCallee) ||
!ts.isIdentifier(keysCallee.expression) ||
keysCallee.expression.text !== "Object" ||
!ts.isIdentifier(keysCallee.name) ||
keysCallee.name.text !== "keys"
) {
return null;
}
if (keysCall.arguments.length !== 1) return null;
const id1Node = keysCall.arguments[0]!;
if (!ts.isIdentifier(id1Node) || !sourceSpellsIdentifier(id1Node, sf)) return null;
const id1 = id1Node.text;
const fn = call.arguments[0];
if (fn === undefined || !ts.isFunctionExpression(fn)) return null; // arrows never match (probed)
if (fn.name !== undefined) return null; // a NAMED callback misses (probed)
if (fn.asteriskToken !== undefined || (ts.getModifiers(fn) ?? []).length > 0) return null;
if (fn.parameters.length !== 1) return null;
const keyParam = fn.parameters[0]!;
if (!ts.isIdentifier(keyParam.name) || keyParam.initializer !== undefined || keyParam.dotDotDotToken !== undefined) return null;
const key = keyParam.name.text;
const stmts = fn.body.statements;
// Form B: the single `if (KEY !== 'default' [&& !hasOwn]) COPY` statement.
if (stmts.length === 1 && ts.isIfStatement(stmts[0]!) && stmts[0]!.elseStatement === undefined) {
const ifStmt = stmts[0]!;
let cond = ifStmt.expression;
if (ts.isBinaryExpression(cond) && cond.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
const right = cond.right;
if (
!ts.isPrefixUnaryExpression(right) ||
right.operator !== ts.SyntaxKind.ExclamationToken ||
!(isHasOwnCall(right.operand, key) || isDirectHasOwnCall(right.operand, key))
) {
return null;
}
cond = cond.left;
}
if (!keyStringCompare(cond, key, ts.SyntaxKind.ExclamationEqualsEqualsToken, "default")) return null;
return isCopyStatement(ifStmt.thenStatement, id1, key) ? id1 : null;
}
// Form A: `if (KEY === 'default' || KEY === '__esModule') return;` (exact
// order), then optional hasOwn / in-exports guards, then the copy.
if (stmts.length < 2 || stmts.length > 4) return null;
const head = stmts[0]!;
if (!ts.isIfStatement(head) || head.elseStatement !== undefined || !isBareReturn(head.thenStatement)) return null;
const headCond = head.expression;
if (
!ts.isBinaryExpression(headCond) ||
headCond.operatorToken.kind !== ts.SyntaxKind.BarBarToken ||
!keyStringCompare(headCond.left, key, ts.SyntaxKind.EqualsEqualsEqualsToken, "default") ||
!keyStringCompare(headCond.right, key, ts.SyntaxKind.EqualsEqualsEqualsToken, "__esModule")
) {
return null;
}
let i = 1;
if (i < stmts.length - 1) {
const s = stmts[i]!;
if (ts.isIfStatement(s) && s.elseStatement === undefined && isBareReturn(s.thenStatement) && isHasOwnCall(s.expression, key)) i++;
}
if (i < stmts.length - 1) {
// `if (KEY in EXPORTS && EXPORTS[KEY] === ID1[KEY]) return;`
const s = stmts[i]!;
if (ts.isIfStatement(s) && s.elseStatement === undefined && isBareReturn(s.thenStatement)) {
const c = s.expression;
const inOk =
ts.isBinaryExpression(c) &&
c.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
ts.isBinaryExpression(c.left) &&
c.left.operatorToken.kind === ts.SyntaxKind.InKeyword &&
ts.isIdentifier(c.left.left) &&
c.left.left.text === key &&
isExportsTarget(c.left.right) &&
ts.isBinaryExpression(c.right) &&
c.right.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken &&
isKeyedAccess(c.right.left, isExportsTarget, key) &&
isKeyedAccess(c.right.right, (x) => ts.isIdentifier(x) && x.text === id1, key);
if (inOk) i++;
}
}
if (i !== stmts.length - 1) return null;
return isCopyStatement(stmts[i]!, id1, key) ? id1 : null;
}
/* ── the walk ──────────────────────────────────────────────────────────── */
/** Node kinds whose CHILDREN sit behind a `{` (or `${`) — merve's
* "top-level" for the star patterns is brace depth 0, so an unbraced
* `if (c) __exportStar(...)` still counts while any block hides it. */
function opensBraces(n: ts.Node): boolean {
switch (n.kind) {
case ts.SyntaxKind.Block:
case ts.SyntaxKind.ModuleBlock:
case ts.SyntaxKind.CaseBlock:
case ts.SyntaxKind.ObjectLiteralExpression:
case ts.SyntaxKind.ObjectBindingPattern:
case ts.SyntaxKind.ClassDeclaration:
case ts.SyntaxKind.ClassExpression:
case ts.SyntaxKind.EnumDeclaration:
case ts.SyntaxKind.TemplateSpan:
return true;
default:
return false;
}
}
/** Preorder walk with an EXPLICIT stack (minified npm bundles nest deeply
* enough to overflow recursive visits) carrying each node's brace depth. */
function walkWithBraceDepth(sf: ts.SourceFile, cb: (node: ts.Node, braceDepth: number) => void): void {
const stack: [ts.Node, number][] = [[sf, 0]];
const children: ts.Node[] = [];
while (stack.length > 0) {
const [n, depth] = stack.pop()!;
cb(n, depth);
const childDepth = depth + (opensBraces(n) ? 1 : 0);
children.length = 0;
ts.forEachChild(n, (c) => {
children.push(c);
});
for (let i = children.length - 1; i >= 0; i--) stack.push([children[i]!, childDepth]);
}
}
/** The single-file lex over SOURCE TEXT: every detection site, position-
* blind like the real lexer for the assignment forms (nested functions and
* dead branches count), brace-depth-0 for the star patterns. */
export function cjsLexedExportsOfFile(sf: ts.SourceFile): CjsLexedExports {
const exports = new Set<string>();
/** Position-ordered reexport EVENTS: every `module.exports =` assignment
* clears the pending list before contributing its own require forms;
* star calls and Babel loops append at their own positions (a loop's
* position is the LOOP's, not its var's — probed). */
type ReexportEvent =
| { pos: number; kind: "assign"; rhs: ts.Expression }
| { pos: number; kind: "spec"; spec: string }
| { pos: number; kind: "loop"; id1: string };
const events: ReexportEvent[] = [];
/** Star-assignment declarators by identifier — the Babel loop's linkage. */
const starAssigns = new Map<string, string[]>();
walkWithBraceDepth(sf, (n, braceDepth) => {
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
const lhs = n.left;
if (
ts.isPropertyAccessExpression(lhs) &&
lhs.questionDotToken === undefined &&
ts.isIdentifier(lhs.name) &&
(isExportsIdent(lhs.expression) || isModuleExports(lhs.expression)) &&
// the whole NAME must be one identifier run — a unicode escape
// mid-name breaks merve's run before the `=`, so the pattern
// never matches (probed: Node detects nothing)
isIdentTokenNode(lhs.name, sf)
) {
exports.add(lhs.name.text);
} else if (
ts.isElementAccessExpression(lhs) &&
lhs.questionDotToken === undefined &&
ts.isStringLiteral(lhs.argumentExpression) &&
(isExportsIdent(lhs.expression) || isModuleExports(lhs.expression))
) {
exports.add(lhs.argumentExpression.text);
} else if (isModuleExports(lhs)) {
events.push({ pos: n.getStart(sf), kind: "assign", rhs: n.right });
}
} else if (ts.isCallExpression(n)) {
scanDefineProperty(n, sf, exports);
if (braceDepth === 0) {
const starSpec = starExportSpecOf(n, sf);
if (starSpec !== null) events.push({ pos: n.getStart(sf), kind: "spec", spec: starSpec });
const loopId = starLoopIdOf(n, sf);
if (loopId !== null) events.push({ pos: n.getStart(sf), kind: "loop", id1: loopId });
}
} else if (ts.isVariableStatement(n) && braceDepth === 0) {
const assign = starAssignOf(n, sf);
if (assign !== null) {
const specs = starAssigns.get(assign[0]);
if (specs === undefined) starAssigns.set(assign[0], [assign[1]]);
else specs.push(assign[1]);
}
}
});
events.sort((a, b) => a.pos - b.pos);
let reexports: string[] = [];
for (const ev of events) {
if (ev.kind === "spec") {
reexports.push(ev.spec);
} else if (ev.kind === "loop") {
for (const spec of starAssigns.get(ev.id1) ?? []) reexports.push(spec);
} else {
reexports = []; // every `module.exports =` clears — stars included (probed)
const req = leadingRequireOf(ev.rhs, sf);
if (req !== null) {
reexports.push(req.spec);
} else if (ts.isObjectLiteralExpression(ev.rhs)) {
// A PARENTHESIZED literal never matches — merve needs the `{` right
// after the `=` — so no unwrapping here (unlike the checker's view).
scanTableLiteral(ev.rhs, sf, exports, reexports);
}
}
}
return { exports, reexports };
}
/** The full Node-visible named-export set of a CommonJS module: its own
* lexed names plus, recursively, the names of every reexport target that
* itself resolves to a CommonJS module (Node's cjsPreparseModuleExports —
* ESM / JSON / builtin / unresolved targets contribute nothing). Generic
* over the caller's module handle and parser service.
* Cycle-safe. */
export function cjsVisibleNames<H>(
mod: H,
lex: (mod: H) => CjsLexedExports,
resolveCjsDep: (from: H, spec: string) => H | null,
memo: Map<H, Set<string>> = new Map(),
): Set<string> {
const hit = memo.get(mod);
if (hit !== undefined) return hit;
const { exports, reexports } = lex(mod);
memo.set(mod, exports); // set BEFORE recursing, like Node — cycles converge
for (const spec of reexports) {
const dep = resolveCjsDep(mod, spec);
if (dep === null) continue;
for (const name of cjsVisibleNames(dep, lex, resolveCjsDep, memo)) exports.add(name);
}
return exports;
}
@@ -0,0 +1,39 @@
import { expect, test } from "vitest";
import { helperTokens } from "./helper-tokens.js";
test("matches complete helper spellings across comments, spaces and quote styles", () => {
expect(helperTokens('var x = Object.defineProperty(o, "default", {value: m});')).toBe(
helperTokens("var /*name*/ x=Object . defineProperty (o,'default',{value:m}); // end"),
);
expect(helperTokens('"default"')).toBe(helperTokens("'de\\x66ault'"));
expect(helperTokens('"default"')).toBe(helperTokens("'de\\u0066ault'"));
expect(helperTokens('"default"')).toBe(helperTokens("'de\\u{66}ault'"));
expect(helperTokens('"default"')).toBe(helperTokens("'de\\\nfault'"));
expect(helperTokens('"default"')).toBe(helperTokens("'de\\\r\nfault'"));
});
test.each([
["mod != null", "mod !== null"],
["a ??= b", "a ||= b"],
["return to;", "return other;"],
["return to;", "return; to;"],
["return to;", "return\nto;"],
["return to;", "return/*\n*/to;"],
["return to;", "return//comment\nto;"],
["return to;", "return\u2028to;"],
["return to;", "return\u2029to;"],
["(mod) => mod", "(mod)\n=> mod"],
["target.default", "target?.default"],
['"default"', '"default;s3:foo;"'],
['"default"', '"default\\0"'],
["mod", "mod other"],
])("retains a semantic difference between %s and %s", (a, b) => {
expect(helperTokens(a)).not.toBe(helperTokens(b));
});
test.each([
"/* unfinished", '"unfinished', '"line\nbreak"', "'\\xzz'", "'\\u0'", "'\\u{}'", "'\\u{110000}'",
"'\\01'", "'\\1'", "'\\8'", "`template`", "/regexp/", "0", "0x10", "a / b", "\\u0061", "café", "#name",
])("refuses unsupported or malformed helper tokens: %s", (source) => {
expect(helperTokens(source)).toBeNull();
});
@@ -0,0 +1,120 @@
/** Token fingerprints for the fixed bundler helpers we recognize. This is
* deliberately not a JavaScript parser: regexes, templates, escaped names,
* numbers, and unknown punctuation refuse the helper. None occur in the
* canonical helpers. Every accepted token is represented in the result;
* comments and whitespace alone may disappear. */
const operators = [
">>>=", "===", "!==", ">>>", "**=", "&&=", "||=", "??=", "<<=", ">>=", "...",
"=>", "==", "!=", "<=", ">=", "++", "--", "&&", "||", "??", "**", "<<", ">>",
"+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "?.",
];
function hex(text: string): number {
return /^[0-9a-fA-F]+$/.test(text) ? parseInt(text, 16) : -1;
}
interface QuotedToken { end: number; value: string }
/** Decode string literals without evaluating source. Invalid, unterminated,
* and legacy octal escapes refuse the match. Line continuations and Unicode
* escapes use JavaScript's UTF-16 string semantics. */
function quoted(text: string, start: number): QuotedToken | null {
const quote = text.charAt(start);
let value = "";
for (let i = start + 1; i < text.length;) {
const ch = text.charAt(i++);
if (ch === quote) return { end: i, value };
if (ch === "\n" || ch === "\r") return null;
if (ch !== "\\") { value += ch; continue; }
if (i === text.length) return null;
const escape = text.charAt(i++);
if (escape === "\r") { if (text.charAt(i) === "\n") i++; continue; }
if (escape === "\n" || escape === "\u2028" || escape === "\u2029") continue;
if (escape === "x" || escape === "u") {
let code: number;
if (escape === "u" && text.charAt(i) === "{") {
const end = text.indexOf("}", i + 1);
if (end < 0) return null;
code = hex(text.slice(i + 1, end));
i = end + 1;
} else {
const size = escape === "x" ? 2 : 4;
if (i + size > text.length) return null;
code = hex(text.slice(i, i + size));
i += size;
}
if (code < 0 || code > 0x10ffff) return null;
value += String.fromCodePoint(code);
} else if (escape === "0") {
if (/[0-9]/.test(text.charAt(i))) return null;
value += "\0";
} else if (/[1-9]/.test(escape)) {
return null;
} else {
value += escape === "n" ? "\n" : escape === "r" ? "\r" : escape === "t" ? "\t"
: escape === "b" ? "\b" : escape === "f" ? "\f" : escape === "v" ? "\v" : escape;
}
}
return null;
}
function fingerprint(kind: string, text: string): string {
// Length framing prevents a quoted value containing separators from
// impersonating several tokens in the canonical stream.
return kind + text.length + ":" + text + ";";
}
export function helperTokens(text: string): string | null {
let result = "";
let previous = "";
let lineBreak = false;
for (let i = 0; i < text.length;) {
const ch = text.charAt(i);
if (/\s/.test(ch)) {
if (/[\r\n\u2028\u2029]/.test(ch)) lineBreak = true;
i++;
continue;
}
if (ch === "/" && text.charAt(i + 1) === "/") {
i += 2;
while (i < text.length && !/[\r\n\u2028\u2029]/.test(text.charAt(i))) i++;
continue;
}
if (ch === "/" && text.charAt(i + 1) === "*") {
const end = text.indexOf("*/", i + 2);
if (end < 0) return null;
if (/[\r\n\u2028\u2029]/.test(text.slice(i, end))) lineBreak = true;
i = end + 2;
continue;
}
// A return followed by a line terminator returns undefined. Ignoring
// that trivia would recognize a different program as a trusted helper.
if (lineBreak && (previous === "return" || previous === "throw" || previous === "yield" || previous === "async" || text.startsWith("=>", i))) {
result += "linebreak;";
}
lineBreak = false;
if (ch === "'" || ch === '"') {
const token = quoted(text, i);
if (token === null) return null;
result += fingerprint("s", token.value);
i = token.end;
previous = "string";
} else if (/[A-Za-z_$]/.test(ch)) {
const start = i++;
while (i < text.length && /[A-Za-z0-9_$]/.test(text.charAt(i))) i++;
previous = text.slice(start, i);
result += fingerprint("t", previous);
} else {
let token = "";
for (const operator of operators) {
if (text.startsWith(operator, i)) { token = operator; break; }
}
if (token === "" && "{}()[];,.?:=+-*%!~&|^<>".includes(ch)) token = ch;
if (token === "") return null;
result += fingerprint("t", token);
previous = token;
i += token.length;
}
}
return result;
}
@@ -17,6 +17,7 @@ import { UNSUPPORTED, blockedBindingUseDiag, requiresDynamicPackageDiag, unsuppo
import { PoisonError, dynUndefinedExpr, jsFuncNameOf, neverTaintedJsType, nodeThrowExpr, own } from "./lowerer.js";
import { lowerNpmStaticSafeIndexRead, lowerSafeIndexRead, strCharsCall, tryLowerNumericIndexRead } from "./lower-containers.js";
import { arrayValueRead, arrayValueStore } from "./array-values.js";
import { lowerOptionalStringIndex } from "./string-index.js";
import { tryLowerIndexedComparison } from "./indexed-comparison.js";
import { npmStaticPackageOfPath } from "../npm-static.js";
import { unsupportedModuleFeatureOf } from "../builtin-modules.js";
@@ -4580,19 +4581,26 @@ export function lowerOptionalNumber(
// and charAt's "" is the only string-typed answer for an
// out-of-range or fractional index where JS reads `undefined` —
// SEMANTICS.md documents the divergence; in-range integer reads
// (the loop pattern) are JS-exact. Under noUncheckedIndexedAccess
// the read types `string | undefined`, which charAt cannot honor —
// fenced.
// (the loop pattern) are JS-exact. Optional results instead retain
// undefined for missing string properties.
const recv = lowerer.lowerExpr(expr.expression);
const index = lowerer.lowerExpr(expr.argumentExpression);
if (index.type.kind === "f64" && lowerer.mapTypeOf(lowerer.typeOf(expr))?.kind === "string") {
const recv = lowerer.lowerExpr(expr.expression);
return { kind: "strIntrinsic", method: "charAt", receiver: recv, args: [index], type: STRING, loc: locOf(expr) };
const resultType = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (index.type.kind === "f64" && recv.type.kind === "string") {
if (resultType?.kind === "string") {
return { kind: "strIntrinsic", method: "charAt", receiver: recv, args: [index], type: STRING, loc: locOf(expr) };
}
if (resultType?.kind === "union") {
const arms = lowerer.unions.get(resultType.unionId)?.arms;
if (arms?.length === 2 && lowerer.armTag(resultType.unionId, STRING) >= 0 && lowerer.armTag(resultType.unionId, UNDEFINED_T) >= 0) {
return lowerOptionalStringIndex(lowerer, recv, index, resultType, locOf(expr));
}
}
}
lowerer.unsupported(
"SC1090",
expr,
"string indexing with this index/result shape (a number index typed 'string' lowers to charAt; " +
"use .charAt(i) under noUncheckedIndexedAccess)",
"string indexing with this index/result shape (expected a number index and a string or string | undefined result)",
);
}
lowerer.unsupported("SC1090", expr, "element access on non-array values");
@@ -0,0 +1,46 @@
import { BOOL, F64, STRING, type IrExpr, type IrType, type SrcLoc } from "../../ir/ir.js";
import { numLit, varRef } from "../../ir/build.js";
import type { Lowerer } from "./lowerer.js";
/** A number-keyed string property read whose declared result includes
* undefined. Unlike charAt, property lookup does not truncate fractions or
* convert NaN to zero. Capture both operands before testing the index: even
* a plain receiver variable can be reassigned while evaluating the key. */
export function lowerOptionalStringIndex(
lowerer: Lowerer,
receiver: IrExpr,
index: IrExpr,
type: IrType,
loc: SrcLoc,
): IrExpr {
const textLocal = lowerer.declareHiddenLocal("%indexedString", STRING);
const indexLocal = lowerer.declareHiddenLocal("%stringIndex", F64);
const text = varRef(textLocal.id, STRING, loc);
const key = varRef(indexLocal.id, F64, loc);
const inBounds: IrExpr = {
kind: "logical", op: "&&",
left: { kind: "bin", op: ">=", left: key, right: numLit(0, loc), type: BOOL, loc },
right: { kind: "bin", op: "<", left: key,
right: { kind: "strIntrinsic", method: "length", receiver: text, args: [], type: F64, loc }, type: BOOL, loc },
type: BOOL, loc,
};
const integral: IrExpr = {
kind: "bin", op: "===", left: key,
right: { kind: "libCall", fn: "math.floor", args: [key], type: F64, loc },
type: BOOL, loc,
};
const read: IrExpr = { kind: "strIntrinsic", method: "charAt", receiver: text, args: [key], type: STRING, loc };
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: textLocal.id, init: receiver, loc },
{ kind: "varDecl", localId: indexLocal.id, init: index, loc },
],
result: {
kind: "ternary", cond: { kind: "logical", op: "&&", left: inBounds, right: integral, type: BOOL, loc },
then: lowerer.coerceToExpected(read, type),
else_: lowerer.wrappedUndefined(type, loc)!, type, loc,
},
type, loc,
};
}
@@ -36,12 +36,24 @@ test("keeps shared helper dependencies live", () => {
expect(run(result)).toEqual(run(source));
});
test("matches comments and equivalent string escapes without changing bundle behavior", () => {
const source = fixture.replaceAll('"default"', "'de\\u0066ault'")
.replace("var __create =", "var /* binding */ __create =")
+ '\nconsole.log(same(), matches("aa"));';
expect(run(rewritten(source))).toEqual(run(source));
});
test.each([
["factory effects", (s: string) => s.replace("module.exports = () =>", 'console.log("initializing"); module.exports = () =>')],
["factory capture", (s: string) => s.replace("() => /a+/g", "() => module.exports")],
["receiver", (s: string) => s.replace("() => /a+/g", "function () { return this; }")],
["arguments", (s: string) => s.replace("() => /a+/g", "() => arguments")],
["helper drift", (s: string) => s.replace("return to;", 'console.log("effect"); return to;')],
["return line terminator", (s: string) => s.replace("return to;", "return\nto;")],
["return comment terminator", (s: string) => s.replace("return to;", "return/*\n*/to;")],
["arrow line terminator", (s: string) => s.replace("(mod, isNodeMode, target) =>", "(mod, isNodeMode, target)\n=>")],
["helper operator drift", (s: string) => s.replace("mod != null", "mod !== null")],
["helper dependency drift", (s: string) => s.replace("Object.create", "Object.freeze")],
["namespace escape", (s: string) => s + "\nconsole.log(first === second);"],
["named property", (s: string) => s + "\nconsole.log(first.name);"],
["default assignment", (s: string) => s + "\n(first.default) = () => /b/;"],
@@ -1,17 +1,18 @@
import ts from "typescript5";
import * as ts from "./ts7/syntax.js";
import { helperTokens } from "./helper-tokens.js";
// Bun's cached node-mode interop helper. Match the complete token stream,
// including its dependencies: helper names alone are not a semantic contract.
const HELPERS = `
var __create = Object.create;
var __getProtoOf = Object.getPrototypeOf;
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
function __accessProp(key) { return this[key]; }
var __toESMCache_node;
var __toESMCache_esm;
var __toESM = (mod, isNodeMode, target) => {
const helperSources = new Map<string, string>([
["__create", `var __create = Object.create;`],
["__getProtoOf", `var __getProtoOf = Object.getPrototypeOf;`],
["__defProp", `var __defProp = Object.defineProperty;`],
["__getOwnPropNames", `var __getOwnPropNames = Object.getOwnPropertyNames;`],
["__hasOwnProp", `var __hasOwnProp = Object.prototype.hasOwnProperty;`],
["__accessProp", `function __accessProp(key) { return this[key]; }`],
["__toESMCache_node", `var __toESMCache_node;`],
["__toESMCache_esm", `var __toESMCache_esm;`],
["__toESM", `var __toESM = (mod, isNodeMode, target) => {
var canCache = mod != null && typeof mod === "object";
if (canCache) {
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
@@ -27,9 +28,15 @@ var __toESM = (mod, isNodeMode, target) => {
}
if (canCache) cache.set(mod, to);
return to;
};
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
`;
};`],
["__commonJS", `var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);`],
]);
const helperShapes = new Map<string, string>();
for (const [name, source] of helperSources) {
const shape = helperTokens(source);
if (shape === null) throw new Error(`invalid canonical helper ${name}`);
helperShapes.set(name, shape);
}
function declaration(stmt: ts.Statement): ts.VariableDeclaration | ts.FunctionDeclaration | null {
if (ts.isFunctionDeclaration(stmt)) return stmt;
@@ -39,27 +46,10 @@ function declaration(stmt: ts.Statement): ts.VariableDeclaration | ts.FunctionDe
return null;
}
function tokens(text: string): string {
const scanner = ts.createScanner(ts.ScriptTarget.Latest, true, ts.LanguageVariant.Standard, text);
const result: string[] = [];
for (let kind = scanner.scan(); kind !== ts.SyntaxKind.EndOfFileToken; kind = scanner.scan()) {
// Formatting and quote style are immaterial. Operators and identifiers
// must still match exactly; arbitrary block-bodied helpers stay refused.
result.push(`${kind}:${kind === ts.SyntaxKind.StringLiteral ? scanner.getTokenValue() : scanner.getTokenText()}`);
}
return result.join("|");
}
const helperShapes = new Map(ts.createSourceFile("helpers.js", HELPERS, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS).statements.map((stmt) => {
const decl = declaration(stmt)!;
return [(decl.name as ts.Identifier).text, tokens(stmt.getText())];
}));
interface Edit { start: number; end: number; text: string }
function walk(node: ts.Node, visit: (node: ts.Node) => void): void {
visit(node);
ts.forEachChild(node, (child) => walk(child, visit));
ts.walkPreorder(node, visit);
}
/** Is a reference part of an assignment, including destructuring or a loop
@@ -98,7 +88,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile)
}
for (const [name, shape] of helperShapes) {
const actual = declarations.get(name);
if (!actual || tokens(actual.stmt.getText(sf)) !== shape) return null;
if (!actual || helperTokens(actual.stmt.getText(sf)) !== shape) return null;
}
// These initializers must execute before any factory or namespace. The
// original bundle would throw if a var helper were still undefined.
@@ -0,0 +1,947 @@
/* --npm-static's bundler-emitted-CJS export surface: the getter-table
* shapes. A bundler-emitted CJS dist exports through runtime plumbing the
* checker cannot model — the `__export(target, { name: () => value, ... })`
* getter table behind `module.exports = __toCommonJS(target)`, the
* `Object.defineProperty(exports, 'n', { get })` family, and the
* transpiler star re-exports (`__exportStar(require('./x'), exports)`,
* `__reExport(target, require('./x'), module.exports)`). Node's CJS lexer
* SEES those named exports (cjs-lexer.ts mirrors it exactly), so named
* imports link under Node — but the inferred surface the opted-in
* resolution feeds the checker is an opaque object, and every import site
* reports "X has no exported member" (the SC0001 storm that used to gate
* the whole build).
*
* THE REWRITE. For an opted-in package's CJS file whose exports come from
* these shapes, the fs shadow (npm-static.ts) serves BOTH worlds — the
* checker and the lowering ride one program — a text rewrite that spells
* the same export surface in the plain CJS the whole existing machinery
* already models:
*
* - the recognized plumbing statements are SPACE-PADDED in place
* (newlines kept, so every original line/offset survives): the
* `module.exports = __toCommonJS(target)` assignment, the
* `__export(target, {...})` call, the star-pattern calls, the
* recognized defineProperty exports, and the dead esbuild annotation
* `0 && (module.exports = {...})` (left live it poisons the checker's
* alias resolution — multiple module.exports assignments make import
* aliases land on properties no lowering path resolves);
* - one canonical export table is APPENDED at the tail:
* `module.exports = { ...require("./star"), own: local, ... };`
* Each getter body the chase can follow types the export by its
* resolved VALUE (`() => localBinding` resolves to the binding's
* inferred type; member chains and scalar literals hoist to a tail
* const first), and every lexer-visible name the chase cannot follow
* falls back to a checked-dynamic `any` binding (value undefined —
* exactly what Node's import binds for a lexer-visible name the
* runtime export object never carries).
*
* LEXER PARITY IS THE CONTRACT: the rewritten text must answer the SAME
* name set to cjs-lexer.ts that the original answers to Node — named
* imports must link exactly where Node's would. The table is formatted
* for merve's byte rules (`name: IDENT,` — value ident runs directly
* followed by ','), star re-exports ride spread entries (recorded as
* reexports and resolved recursively, exactly Node's union), and the
* esbuild `__esModule: !0` marker is spelled as a deliberate SCAN STOPPER
* so it stays lexer-invisible (Node never lexes __esModule out of
* __toCommonJS) while member-access star entries after it are covered by
* the spread records.
*
* SNAPSHOT SEMANTICS (the documented divergence): the getter table reads
* its locals LIVE at every access; the canonical table captures values
* ONCE, at module tail — after every top-level statement ran, so ordinary
* modules see identical values, and only post-evaluation reassignment of
* an exported local (the lazy-cache counter idiom) can diverge. Star
* re-export targets load at module tail instead of their original
* statement positions (visible only to load-order-sensitive side
* effects). Node's __esModule marker survives on the rewritten object.
*
* THE IMPORT-SIDE TWIN: the same pass erases esbuild's __toESM interop
* wrapper around EXTERNAL dependencies (`var import_x =
* __toESM(require("x"))`, the `, 1` node-mode variant, inline
* member-accessed forms) — see the section header at planToEsmInterop.
* The wrapper pads down to the bare require it wraps and module-valued
* `.default` accesses pad down to their binding, so member accesses model
* on the required package's canonical table through the ordinary require
* machinery. Interop the recognizers cannot finish answers `{ degrade }`:
* the caller reports the package as an offender (naming the construct)
* and it serves from the island — never a failed build.
*
* Everything here is SYNTACTIC (the owned native TypeScript AST): the chase decides only what TEXT to emit — the
* checker then infers the types from the emitted spellings. A file whose
* export mechanism the recognizers cannot fully account for is left
* untouched (null): its import-site errors stay, and the frontend's
* offender attribution degrades the PACKAGE to the island with a note. */
import * as ts from "./ts7/syntax.js";
import { bareRequireSpecOf, cjsLexedExportsOfFile, isExportsIdent, isModuleExports } from "./cjs-syntax.js";
import { rewriteBundledFunctionImports } from "./npm-static-bundled-cjs.js";
/** Resolution stays with the compilation host. Syntax never reads disk or
* starts a parser implicitly, so native and Node hosts run the same pass. */
export interface CjsRewriteHost {
requireTargetEsModuleStamped(fromFile: string, specifier: string): boolean;
starTargetNames(fromFile: string, specifier: string): Set<string>;
}
export function isBundlerCjsCandidate(source: string): boolean {
return source.includes("__toCommonJS") || source.includes("__toESM")
|| source.includes("__exportStar") || source.includes("__reExport")
|| (source.includes("Object.defineProperty(exports") && source.includes("get"));
}
/** The last NAME of a callee: bare identifier or any member chain's final
* name (`tslib_1.__exportStar` → "__exportStar"). */
function calleeNameOf(call: ts.CallExpression): string | null {
const callee = call.expression;
if (ts.isIdentifier(callee)) return callee.text;
if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) return callee.name.text;
return null;
}
/** An entry the canonical table can carry. */
interface EntrySpec {
/** The export name (may need string-key spelling). */
name: string;
/** "ident": VALUE is a plain identifier spelled directly in the table.
* "hoist": VALUE is an expression text hoisted to a tail const first.
* "any": the checked-dynamic fallback (`__scriptc_any`). */
kind: "ident" | "hoist" | "any";
value?: string;
}
/** A chaseable getter/descriptor VALUE: a plain identifier, an
* identifier-rooted member/element chain (string keys only), or a scalar
* literal. Answers the classification or null (not chaseable). */
function chaseValue(e: ts.Expression): { kind: "ident" | "hoist"; text: string } | null {
let cur: ts.Expression = e;
while (ts.isParenthesizedExpression(cur)) cur = cur.expression;
if (ts.isIdentifier(cur)) return { kind: "ident", text: cur.text };
if (
ts.isNumericLiteral(cur) ||
ts.isStringLiteral(cur) ||
ts.isNoSubstitutionTemplateLiteral(cur) ||
cur.kind === ts.SyntaxKind.TrueKeyword ||
cur.kind === ts.SyntaxKind.FalseKeyword ||
cur.kind === ts.SyntaxKind.NullKeyword
) {
return { kind: "hoist", text: cur.getText() };
}
// ident-rooted member chains: this.x / a.b.c / a["k"].d — side-effect
// free reads the tail const can evaluate once.
let walk: ts.Expression = cur;
while (
(ts.isPropertyAccessExpression(walk) && walk.questionDotToken === undefined && ts.isIdentifier(walk.name)) ||
(ts.isElementAccessExpression(walk) && walk.questionDotToken === undefined && ts.isStringLiteral(walk.argumentExpression))
) {
walk = walk.expression;
}
if (ts.isIdentifier(walk)) return { kind: "hoist", text: cur.getText() };
return null;
}
/** The getter body's returned expression for the two descriptor getter
* spellings (`get() { return X }` / `get: function () { return X }`) and
* the arrow forms the esbuild table uses (`() => X`, `() => { return X }`). */
function getterReturnOf(fn: ts.Node): ts.Expression | null {
let body: ts.ConciseBody | undefined;
if (ts.isArrowFunction(fn) && fn.parameters.length === 0) body = fn.body;
else if (ts.isFunctionExpression(fn) && fn.parameters.length === 0) body = fn.body;
else if (ts.isMethodDeclaration(fn) && fn.parameters.length === 0 && fn.body !== undefined) body = fn.body;
else return null;
if (body === undefined) return null;
if (!ts.isBlock(body)) return body; // expression-bodied arrow
if (body.statements.length !== 1) return null;
const ret = body.statements[0]!;
return ts.isReturnStatement(ret) && ret.expression !== undefined ? ret.expression : null;
}
/** The `__export(IDENT, { name: () => value, ... })` call's parts, or null.
* Every property must be a `name: <function>` assignment (identifier or
* string-literal name) — anything else is not the esbuild table. */
function exportCallOf(call: ts.CallExpression): { target: string; entries: [string, ts.Expression | null][] } | null {
if (calleeNameOf(call) !== "__export") return null;
if (call.arguments.length !== 2) return null;
const [target, table] = call.arguments as unknown as [ts.Expression, ts.Expression];
if (!ts.isIdentifier(target) || !ts.isObjectLiteralExpression(table)) return null;
const entries: [string, ts.Expression | null][] = [];
for (const prop of table.properties) {
if (!ts.isPropertyAssignment(prop)) return null;
let name: string;
if (ts.isIdentifier(prop.name)) name = prop.name.text;
else if (ts.isStringLiteral(prop.name)) name = prop.name.text;
else return null;
entries.push([name, getterReturnOf(prop.initializer)]);
}
return { target: target.text, entries };
}
/** `Object.defineProperty(exports|module.exports, 'name', {...})` — the
* name plus the chased descriptor value (getter return or `value:`), or
* null when the statement is not that shape at all. `value: null` means
* recognized but unchaseable. */
function definePropertyExportOf(
call: ts.CallExpression,
): { name: string; value: ts.Expression | null; esModuleStamp: boolean } | null {
const callee = call.expression;
if (
!ts.isPropertyAccessExpression(callee) ||
!ts.isIdentifier(callee.expression) ||
callee.expression.text !== "Object" ||
!ts.isIdentifier(callee.name) ||
callee.name.text !== "defineProperty"
) {
return null;
}
if (call.arguments.length !== 3) return null;
const [recv, nameArg, desc] = call.arguments as unknown as [ts.Expression, ts.Expression, ts.Expression];
if (!isExportsIdent(recv) && !isModuleExports(recv)) return null;
if (!ts.isStringLiteral(nameArg) || !ts.isObjectLiteralExpression(desc)) return null;
const name = nameArg.text;
let value: ts.Expression | null = null;
for (const p of desc.properties) {
if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)) {
if (p.name.text === "value") value = p.initializer;
else if (p.name.text === "get") value = getterReturnOf(p.initializer);
} else if (ts.isMethodDeclaration(p) && ts.isIdentifier(p.name) && p.name.text === "get") {
value = getterReturnOf(p);
}
}
const esModuleStamp =
name === "__esModule" && value !== null && value.kind === ts.SyntaxKind.TrueKeyword;
return { name, value, esModuleStamp };
}
/** Space-pads [start, end) of `text` preserving newlines — the recognized
* statement disappears while every original offset survives. */
function padSpan(text: string, start: number, end: number): string {
let pad = "";
for (let i = start; i < end; i++) pad += text[i] === "\n" ? "\n" : " ";
return text.slice(0, start) + pad + text.slice(end);
}
/** A valid identifier-run export name (the table can spell it bare). */
function isPlainName(name: string): boolean {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
}
/* ── the __toESM(require(…)) interop import ──────────────────────────────
* esbuild's CJS output wraps every import of an EXTERNAL (unbundled)
* dependency in its __toESM helper: `var import_x = __toESM(require("x"))`
* (a trailing `, 1` in node mode). The wrapper's runtime semantics are
* static facts the required target decides: `default` binds the required
* module.exports itself (node mode, or a target whose __esModule is not
* truthy — the plain-CJS answer) or passes through to the target's own
* `default` export (the transpiled-ESM stamps), and every other member is
* a getter passthrough of the target's export. So the wrapper ERASES: the
* call pads down to the bare `require("x")` it wraps (a require binding
* the whole existing machinery models — the edge, the inline %init, the
* canonical-table member reads), and `.default` accesses on the binding
* pad down to the binding itself exactly where Node's answer is the
* module. The helper is recognized BY STRUCTURE (the cjs-lexer precedent:
* a quirk-faithful recognizer over the vendored text, never a general
* JS-semantics engine); a file whose interop deviates beyond recognition
* answers a DEGRADE reason and the package falls back to the island with
* the note — never a failed build, and never the silent alternative (an
* unrecognized-but-live helper keeps `var __create = Object.create;`
* alive, whose value declaration fences AT MODULE LOAD — the package
* would crash on its first import while the report claimed it static). */
interface ToEsmPlan {
/** Wrapper/`.default` spans to space-pad (they join `neutralize`, so the
* helper sweep sees the erased call sites as dead references). */
pads: { start: number; end: number }[];
/** Interop bindings whose `.default` IS the module (plain-CJS targets,
* the node-mode variant): pads erase their source `.default` accesses,
* and TEXT COPIED off the source (the canonical table's hoisted getter
* bodies) must drop the member spelling the same way. */
moduleBindings: Set<string>;
/** Non-null: the file's interop deviates beyond recognition — the reason
* the offender note carries (the package degrades to the island). */
degrade: string | null;
}
/** Structural recognition of esbuild's vendored __toESM helper:
* `var __toESM = (mod, isNodeMode, target) => (target = …, __copyProps(
* isNodeMode || !mod || !mod.__esModule
* ? __defProp(target, "default", { value: mod, enumerable: true })
* : target,
* mod))`
* — an arrow over (mod, isNodeMode[, target]) whose result is a
* __copyProps(…, mod) call copying onto a conditional that tests
* isNodeMode/__esModule and defines "default" with value mod. Comments and
* formatting are free; the SHAPE is the contract. */
function recognizedToEsmDecl(stmt: ts.Statement): boolean {
if (!ts.isVariableStatement(stmt)) return false;
const decls = stmt.declarationList.declarations;
if (decls.length !== 1) return false;
const d = decls[0]!;
if (!ts.isIdentifier(d.name) || d.name.text !== "__toESM" || d.initializer === undefined) return false;
let init: ts.Expression = d.initializer;
while (ts.isParenthesizedExpression(init)) init = init.expression;
if (!ts.isArrowFunction(init)) return false;
if (init.parameters.length < 2 || init.parameters.length > 3) return false;
const p0 = init.parameters[0]!;
const p1 = init.parameters[1]!;
if (!ts.isIdentifier(p0.name) || !ts.isIdentifier(p1.name)) return false;
const mod = p0.name.text;
const isNodeMode = p1.name.text;
if (ts.isBlock(init.body)) return false;
let expr: ts.Expression = init.body;
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
// the rightmost comma operand carries the result
while (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.CommaToken) {
expr = expr.right;
}
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
if (!ts.isCallExpression(expr) || !ts.isIdentifier(expr.expression) || expr.expression.text !== "__copyProps") return false;
if (expr.arguments.length < 2) return false;
const from = expr.arguments[1]!;
if (!ts.isIdentifier(from) || from.text !== mod) return false;
let to: ts.Expression = expr.arguments[0]!;
while (ts.isParenthesizedExpression(to)) to = to.expression;
if (!ts.isConditionalExpression(to)) return false;
const condText = to.condition.getText();
if (!condText.includes(isNodeMode) || !condText.includes("__esModule")) return false;
let stamp: ts.Expression = to.whenTrue;
while (ts.isParenthesizedExpression(stamp)) stamp = stamp.expression;
if (!ts.isCallExpression(stamp) || !ts.isIdentifier(stamp.expression) || stamp.expression.text !== "__defProp") return false;
if (stamp.arguments.length !== 3) return false;
const nameArg = stamp.arguments[1]!;
if (!ts.isStringLiteral(nameArg) || nameArg.text !== "default") return false;
const desc = stamp.arguments[2]!;
if (!ts.isObjectLiteralExpression(desc)) return false;
return desc.properties.some(
(p) =>
ts.isPropertyAssignment(p) &&
ts.isIdentifier(p.name) &&
p.name.text === "value" &&
ts.isIdentifier(p.initializer) &&
p.initializer.text === mod,
);
}
const TO_ESM_SHAPE_DEGRADE =
"its shipped JS calls the __toESM bundler-interop helper but spells it in a shape the " +
"recognizer cannot verify — the package serves from the island instead";
const TO_ESM_ESCAPE_DEGRADE =
"a __toESM(require(…)) bundler-interop object escapes into a use the static rewrite cannot " +
"follow (only module-scope bindings and direct member reads compile) — the package serves " +
"from the island instead";
const TO_ESM_ARG_DEGRADE =
"it calls the __toESM bundler-interop helper on something other than require(…) of a string " +
"literal — the package serves from the island instead";
const TO_ESM_MIXED_DEGRADE =
"its bundler-emitted export surface cannot be respelled around its __toESM interop imports " +
"— the package serves from the island instead";
/** The interop-erasure plan for one CJS file (see the section header). */
function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteHost): ToEsmPlan {
const pads: { start: number; end: number }[] = [];
const moduleBindings = new Set<string>();
const plan: ToEsmPlan = { pads, moduleBindings, degrade: null };
const fail = (reason: string): ToEsmPlan => ({ pads: [], moduleBindings: new Set(), degrade: reason });
const declStmts = sf.statements.filter(
(s) =>
ts.isVariableStatement(s) &&
s.declarationList.declarations.some(
(d) => ts.isIdentifier(d.name) && d.name.text === "__toESM",
),
);
const refs: ts.Identifier[] = [];
const calls: ts.CallExpression[] = [];
const collect = (n: ts.Node): void => {
if (ts.isIdentifier(n) && n.text === "__toESM") refs.push(n);
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === "__toESM") calls.push(n);
};
ts.walkPreorder(sf, collect);
if (calls.length === 0 && refs.length === 0) return plan;
// Every reference must be the declarator's own name or a call's callee —
// a helper that escapes as a VALUE is outside the recognized shape.
const callees = new Set<ts.Node>(calls.map((c) => c.expression));
const declNames = new Set<ts.Node>();
for (const s of declStmts) {
for (const d of (s as ts.VariableStatement).declarationList.declarations) {
if (ts.isIdentifier(d.name) && d.name.text === "__toESM") declNames.add(d.name);
}
}
if (refs.some((r) => !callees.has(r) && !declNames.has(r))) return fail(TO_ESM_ESCAPE_DEGRADE);
if (calls.length === 0) return plan; // declared but never called — dead helper
if (declStmts.length !== 1 || !recognizedToEsmDecl(declStmts[0]!)) return fail(TO_ESM_SHAPE_DEGRADE);
/** Module-scope interop bindings: name → whether `.default` IS the
* module (pad the access) rather than a member read of its `default`. */
const bindings = new Map<string, boolean>();
for (const call of calls) {
const arg0 = call.arguments[0];
const spec = arg0 !== undefined ? bareRequireSpecOf(arg0) : null;
if (spec === null || call.arguments.length > 2) return fail(TO_ESM_ARG_DEGRADE);
let isNodeMode = false;
if (call.arguments.length === 2) {
const modeArg = call.arguments[1]!;
if (ts.isNumericLiteral(modeArg)) isNodeMode = Number(modeArg.text) !== 0;
else if (modeArg.kind === ts.SyntaxKind.TrueKeyword) isNodeMode = true;
else if (modeArg.kind === ts.SyntaxKind.FalseKeyword) isNodeMode = false;
else return fail(TO_ESM_ARG_DEGRADE);
}
const defaultIsModule = isNodeMode || !host.requireTargetEsModuleStamped(filePath, spec);
// classify the call's use: a variable binding, or an immediate member
// read; anything else escapes.
let child: ts.Expression = call;
let parent = call.parent;
while (ts.isParenthesizedExpression(parent)) {
child = parent;
parent = parent.parent;
}
if (ts.isVariableDeclaration(parent) && parent.initializer === child && ts.isIdentifier(parent.name)) {
if (bindings.has(parent.name.text)) return fail(TO_ESM_ESCAPE_DEGRADE);
bindings.set(parent.name.text, defaultIsModule);
if (defaultIsModule) moduleBindings.add(parent.name.text);
} else if (ts.isPropertyAccessExpression(parent) && parent.expression === child && ts.isIdentifier(parent.name)) {
if (parent.name.text === "default" && defaultIsModule) {
if (!readOnlyAccess(parent)) return fail(TO_ESM_ESCAPE_DEGRADE);
pads.push({ start: child.getEnd(), end: parent.getEnd() });
}
} else {
return fail(TO_ESM_ESCAPE_DEGRADE);
}
// the wrapper itself: callee + '(' down, and everything after the
// require argument (`, 1` included) — the bare require survives at its
// original offsets.
pads.push({ start: call.getStart(sf), end: arg0!.getStart(sf) });
pads.push({ start: arg0!.getEnd(), end: call.getEnd() });
}
if (bindings.size > 0) {
// Binding safety: the erased binding must be declared exactly once and
// never written — the `.default` mapping below matches receivers by
// NAME (syntactic, like the rest of this file), so a shadow or a
// reassignment would silently change what a padded access reads.
const declCount = new Map<string, number>();
const bump = (name: string): void => {
if (bindings.has(name)) declCount.set(name, (declCount.get(name) ?? 0) + 1);
};
let violated = false;
const scan = (n: ts.Node): void => {
if (violated) return;
if (
(ts.isVariableDeclaration(n) || ts.isBindingElement(n) || ts.isParameter(n)) &&
ts.isIdentifier(n.name)
) {
bump(n.name.text);
} else if ((ts.isFunctionDeclaration(n) || ts.isClassDeclaration(n)) && n.name !== undefined) {
bump(n.name.text);
} else if (
ts.isBinaryExpression(n) &&
n.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
n.operatorToken.kind <= ts.SyntaxKind.LastAssignment &&
ts.isIdentifier(n.left) &&
bindings.has(n.left.text)
) {
violated = true;
} else if (
(ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) &&
(n.operator === ts.SyntaxKind.PlusPlusToken || n.operator === ts.SyntaxKind.MinusMinusToken) &&
ts.isIdentifier(n.operand) &&
bindings.has(n.operand.text)
) {
violated = true;
}
};
ts.walkPreorder(sf, scan);
if (violated || [...bindings.keys()].some((name) => (declCount.get(name) ?? 0) !== 1)) {
return fail(TO_ESM_ESCAPE_DEGRADE);
}
// `.default` reads on a module-valued binding pad down to the binding.
const padDefaults = (n: ts.Node): void => {
if (
ts.isPropertyAccessExpression(n) &&
ts.isIdentifier(n.expression) &&
bindings.get(n.expression.text) === true &&
ts.isIdentifier(n.name) &&
n.name.text === "default"
) {
if (!readOnlyAccess(n)) {
violated = true;
return;
}
pads.push({ start: n.expression.getEnd(), end: n.getEnd() });
}
};
ts.walkPreorder(sf, padDefaults);
if (violated) return fail(TO_ESM_ESCAPE_DEGRADE);
}
return plan;
}
/** True when a padded-away `.default` access is a plain READ — an
* assignment target or delete operand cannot lose its member spelling. */
function readOnlyAccess(access: ts.PropertyAccessExpression): boolean {
const p = access.parent;
if (p !== undefined && ts.isDeleteExpression(p)) return false;
if (
p !== undefined &&
ts.isBinaryExpression(p) &&
p.left === access &&
p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
p.operatorToken.kind <= ts.SyntaxKind.LastAssignment
) {
return false;
}
if (
p !== undefined &&
(ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p)) &&
(p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)
) {
return false;
}
return true;
}
const RESERVED_KEYS = new Set(["__proto__"]);
/** The rewrite (see the header). Null = not a recognized bundle shape (or
* nothing to fix) — serve the file untouched. A `{ degrade }` answer means
* the file carries a bundler-interop construct the recognizers cannot
* finish: the caller reports the PACKAGE as an offender with the reason
* and the fallback loop islands it — never a failed build. */
export function rewriteBundlerCjsSyntax(
sf: ts.SourceFile,
filePath: string,
host: CjsRewriteHost,
): string | { degrade: string } | null {
const source = sf.text;
if (!isBundlerCjsCandidate(source)) return null;
// ESM bundles can contain closed CommonJS function factories. Recognize
// those before refusing interop that needs a dynamic module namespace.
if (ts.isExternalModule(sf)) {
const bundled = rewriteBundledFunctionImports(source, sf);
if (bundled !== null) return bundled;
return source.includes("__toESM(")
? {
degrade:
"its ES-module dist routes an external dependency through the __toESM " +
"bundler-interop helper, which has no static story in ESM output — the " +
"package serves from the island instead",
}
: null;
}
// The __toESM interop pass (see the section header): wrapper call sites
// pad down to the bare require they wrap, module-valued `.default`
// accesses pad down to their binding, and a file whose interop deviates
// beyond recognition degrades the package.
const toEsm = planToEsmInterop(sf, filePath, host);
if (toEsm.degrade !== null) return { degrade: toEsm.degrade };
interface Neutralize {
start: number;
end: number;
}
const neutralize: Neutralize[] = [];
const exportTables: { target: string; entries: [string, ts.Expression | null][] }[] = [];
const definePropEntries: { name: string; value: ts.Expression | null }[] = [];
const memberStmts: {
names: string[];
finalRhs: ts.Expression;
viaBareExports: boolean;
pos: number;
start: number;
end: number;
}[] = [];
/** Star spec → require-load emission order (statement order). */
const starSpecsInOrder: string[] = [];
let esbuildMainTarget: string | null = null;
let tablePos: number | null = null;
let sawBundleShape = false;
const addStarSpec = (spec: string): void => {
if (!starSpecsInOrder.includes(spec)) starSpecsInOrder.push(spec);
};
for (const stmt of sf.statements) {
if (!ts.isExpressionStatement(stmt)) continue;
const e = stmt.expression;
// `module.exports = <rhs>`
if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.EqualsToken && isModuleExports(e.left)) {
const rhs = e.right;
if (ts.isCallExpression(rhs) && calleeNameOf(rhs) === "__toCommonJS" && rhs.arguments.length === 1 && ts.isIdentifier(rhs.arguments[0]!)) {
esbuildMainTarget = (rhs.arguments[0] as ts.Identifier).text;
tablePos = stmt.getStart(sf);
neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
sawBundleShape = true;
continue;
}
// Any other real module.exports assignment: the file has an export
// mechanism the checker already models (or a shape outside the
// recognizers) — not ours to rewrite.
return null;
}
// member exports — `exports.N = RHS;` / `module.exports.N = RHS;`,
// chains included (`exports.a = exports.b = void 0;` — the tsc
// void-init preamble collects every name with the chain's final RHS).
{
let cur: ts.Expression = e;
const names: string[] = [];
let viaBareExports = false;
while (
ts.isBinaryExpression(cur) &&
cur.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
ts.isPropertyAccessExpression(cur.left) &&
cur.left.questionDotToken === undefined &&
ts.isIdentifier(cur.left.name) &&
(isExportsIdent(cur.left.expression) || isModuleExports(cur.left.expression))
) {
names.push(cur.left.name.text);
if (isExportsIdent(cur.left.expression)) viaBareExports = true;
cur = cur.right;
}
if (names.length > 0) {
memberStmts.push({
names,
finalRhs: cur,
viaBareExports,
pos: stmt.getStart(sf),
start: stmt.getStart(sf),
end: stmt.getEnd(),
});
continue; // classified later (only when a rewrite actually happens)
}
}
// the dead esbuild annotation: `0 && (module.exports = { ... });`
if (
ts.isBinaryExpression(e) &&
e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
ts.isNumericLiteral(e.left) &&
e.left.text === "0"
) {
let inner: ts.Expression = e.right;
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
if (
ts.isBinaryExpression(inner) &&
inner.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
isModuleExports(inner.left) &&
ts.isObjectLiteralExpression(inner.right)
) {
neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
sawBundleShape = true;
continue;
}
}
if (!ts.isCallExpression(e)) continue;
const name = calleeNameOf(e);
// `__export(TARGET, { getter table });`
const table = exportCallOf(e);
if (table !== null) {
exportTables.push(table);
neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
sawBundleShape = true;
continue;
}
// `__exportStar(require('spec'), exports);` — the tsc/tslib spelling —
// and `__export(require('spec'))`, the legacy twin.
if ((name === "__exportStar" || name === "__export") && e.arguments.length >= 1) {
const spec = bareRequireSpecOf(e.arguments[0]!);
if (spec !== null) {
addStarSpec(spec);
neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
sawBundleShape = true;
continue;
}
}
// `__reExport(TARGET, require('spec')[, module.exports]);` — esbuild's
// `export * from` plumbing.
if (name === "__reExport" && e.arguments.length >= 2) {
const spec = bareRequireSpecOf(e.arguments[1]!);
if (spec !== null) {
addStarSpec(spec);
neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
sawBundleShape = true;
continue;
}
}
// Object.defineProperty(exports, 'n', {...})
const def = definePropertyExportOf(e);
if (def !== null) {
if (def.esModuleStamp) {
neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
// the canonical table re-stamps it (lexer-visibly — merve detects
// this defineProperty form, so parity wants a visible entry)
continue;
}
const chased = def.value !== null ? chaseValue(def.value) : null;
if (chased !== null) {
definePropEntries.push({ name: def.name, value: def.value });
neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
sawBundleShape = true;
}
// unchaseable descriptor: LEAVE the statement (its runtime fence is
// loud where an undefined-valued entry would be a silent lie); the
// name still types through the any-fallback below.
continue;
}
}
const lexed = cjsLexedExportsOfFile(sf);
let emitTable = sawBundleShape && (lexed.exports.size > 0 || lexed.reexports.length > 0);
const starSpecs: string[] = [];
const entries = new Map<string, EntrySpec>();
const starMembers: { name: string; ref: string }[] = [];
const requireConsts: string[] = [];
if (emitTable) {
/* Star specs Node actually honors are the lexer's SURVIVING reexports
* (annotation spreads for esbuild bundles, star-pattern calls for tsc
* output). The statements walked above contribute load order; specs the
* lexer dropped (cleared by a later module.exports=) drop here too. */
const lexerSpecs = new Set(lexed.reexports);
starSpecs.push(...starSpecsInOrder.filter((s) => lexerSpecs.has(s)));
for (const s of lexed.reexports) {
if (!starSpecs.includes(s)) starSpecs.push(s);
}
/* The entry list, first-wins per name: own getter-table entries, then
* defineProperty entries, then surviving top-level member exports, then
* (for names still uncovered) star enumerations in spec order, then the
* any fallback for every remaining lexer-visible name. */
const claim = (name: string, spec: EntrySpec): void => {
if (name === "__esModule" || RESERVED_KEYS.has(name)) return;
if (!entries.has(name)) entries.set(name, spec);
};
const classify = (name: string, value: ts.Expression | null): EntrySpec => {
const chased = value !== null ? chaseValue(value) : null;
if (chased === null) return { name, kind: "any" };
if (chased.kind === "ident") return { name, kind: "ident", value: chased.text };
return { name, kind: "hoist", value: chased.text };
};
// Own names: the getter table(s) — the one feeding module.exports first.
const tablesOrdered = [
...exportTables.filter((t) => t.target === esbuildMainTarget),
...exportTables.filter((t) => t.target !== esbuildMainTarget),
];
for (const t of tablesOrdered) {
for (const [name, value] of t.entries) claim(name, classify(name, value));
}
for (const d of definePropEntries) claim(d.name, classify(d.name, d.value));
/* Top-level member exports. Node's export-object identity rules decide
* each statement's fate against the ORIGINAL table (esbuildMainTarget's
* assignment): a member attached BEFORE it was discarded (the table
* replaced the object), a bare `exports.N =` AFTER it wrote the stale
* object — both keep only their lexer-visible NAME (value undefined,
* exactly Node's link answer: the any fallback below). A surviving
* member (`module.exports.N =` after the table, or any spelling when no
* table exists) re-emits its VALUE as a canonical entry — identifier
* RHS only; any other RHS would need a second evaluation, so the file
* is left alone (the offender path owns it — and when the file also
* carries erased interop wrappers, "untouched" is not on the menu:
* the live helper chain would fence at module load, so the package
* degrades with the note instead). Every member statement is then
* NEUTRALIZED: the canonical tail table replaces the object, and a
* stray earlier attach would meet the lowering's discard fence. */
for (const m of memberStmts) {
let cur: ts.Expression = m.finalRhs;
while (ts.isParenthesizedExpression(cur)) cur = cur.expression;
const isVoidInit = cur.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(cur) && cur.text === "undefined");
const discarded = tablePos !== null && (m.pos < tablePos || m.viaBareExports);
if (!isVoidInit) {
// Only side-effect-free RHS shapes may be neutralized (identifiers,
// scalar literals, ident-rooted member chains — chaseValue's set);
// anything else keeps the file untouched (the offender path owns
// it: neutralizing would drop an evaluation, keeping it meets the
// lowering's discard fence at load).
const chased = chaseValue(cur);
if (chased === null) {
return toEsm.pads.length > 0 ? { degrade: TO_ESM_MIXED_DEGRADE } : null;
}
if (!discarded) {
for (const name of m.names) claim(name, { name, kind: chased.kind, value: chased.text });
}
// discarded members keep only their lexer-visible NAME (the any
// fallback below) — value undefined, exactly Node's link answer
}
neutralize.push({ start: m.start, end: m.end });
}
/* Direct-vs-hoist for ident entries: only bindings declared by a
* top-level const/function/class may be spelled directly (alias
* plumbing needs an immutable target — a `var`/`let` in the table would
* fence the whole statement). Everything else snapshots via a tail
* const. */
const immutableTopLevel = new Set<string>();
for (const stmt of sf.statements) {
if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined) immutableTopLevel.add(stmt.name.text);
else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) immutableTopLevel.add(stmt.name.text);
else if (ts.isVariableStatement(stmt) && (stmt.declarationList.flags & ts.NodeFlags.Const) !== 0) {
for (const d of stmt.declarationList.declarations) {
if (ts.isIdentifier(d.name)) immutableTopLevel.add(d.name.text);
}
}
}
for (const spec of entries.values()) {
if (spec.kind === "ident" && !immutableTopLevel.has(spec.value!)) spec.kind = "hoist";
}
// Star enumerations for names the own entries left uncovered.
starSpecs.forEach((spec, i) => {
const names = host.starTargetNames(filePath, spec);
let refVar: string | null = null;
for (const n of names) {
if (n === "default" || n === "__esModule" || entries.has(n) || RESERVED_KEYS.has(n)) continue;
if (starMembers.some((m) => m.name === n)) continue; // earlier spec wins
if (refVar === null) {
refVar = `__scriptc_r${i}`;
requireConsts.push(`const ${refVar} = require(${JSON.stringify(spec)});`);
}
starMembers.push({ name: n, ref: refVar });
}
});
// The any fallback: every lexer-visible name nothing above covered.
for (const n of lexed.exports) claim(n, { name: n, kind: "any" });
emitTable = entries.size > 0 || starSpecs.length > 0;
}
/* A file with interop pads but no canonical table still rewrites: the
* pads erase the __toESM wrappers and the sweep below drops the dead
* helper chain — but nothing replaces the export plumbing, so none of
* it may be neutralized (the surviving text keeps exactly its
* untouched-file behavior). */
if (!emitTable) {
if (toEsm.pads.length === 0) return null;
neutralize.length = 0;
}
// Interop pads join the neutralized set BEFORE the sweep: the erased
// __toESM call sites read as dead references, so the helper declaration
// sweeps away and its __create/__getProtoOf chain follows.
neutralize.push(...toEsm.pads);
/* Bundler HELPPER declarations left unused once the plumbing statements
* are gone would still fence at module load (`var __defProp =
* Object.defineProperty;` — a stdlib generic method as a value has no
* lowering, and a top-level declaration's fence throws when init runs
* it). Mark-and-sweep the RECOGNIZED helper names: a helper whose every
* reference lies inside an already-neutralized span (the plumbing
* calls, the erased __toESM interop wrappers, or another swept helper's
* declaration) is neutralized too. Helpers live code still calls
* (__commonJS chunk factories) stay — their declarations are plain
* function values whose bodies defer per the JS function-poison rule. */
{
const HELPER_NAMES = new Set([
"__create", "__defProp", "__defProps", "__getOwnPropDesc", "__getOwnPropDescs",
"__getOwnPropNames", "__getOwnPropSymbols", "__getProtoOf", "__hasOwnProp",
"__propIsEnum", "__export", "__copyProps", "__reExport", "__toESM", "__toCommonJS",
"__exportStar", "__createBinding", "__setModuleDefault", "__importStar", "__importDefault",
]);
const helperDecls = new Map<string, { start: number; end: number }>();
for (const stmt of sf.statements) {
if (!ts.isVariableStatement(stmt)) continue;
const decls = stmt.declarationList.declarations;
if (decls.length !== 1) continue;
const d = decls[0]!;
if (ts.isIdentifier(d.name) && HELPER_NAMES.has(d.name.text)) {
helperDecls.set(d.name.text, { start: stmt.getStart(sf), end: stmt.getEnd() });
}
}
// every identifier occurrence of a helper name outside its own
// declarator name node, position-recorded for the span test
const refs = new Map<string, number[]>();
const record = (node: ts.Node): void => {
if (ts.isIdentifier(node) && helperDecls.has(node.text)) {
const parent = node.parent;
if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.name === node) return;
const list = refs.get(node.text);
if (list === undefined) refs.set(node.text, [node.getStart(sf)]);
else list.push(node.getStart(sf));
}
};
ts.walkPreorder(sf, record);
const dead = [...neutralize];
const inDead = (pos: number): boolean => dead.some((s) => pos >= s.start && pos < s.end);
const removed = new Set<string>();
for (let changed = true; changed; ) {
changed = false;
for (const [name, span] of helperDecls) {
if (removed.has(name)) continue;
// SELF-references inside the candidate's own declaration don't
// keep it alive (`var __createBinding = (this && this.__createBinding)
// || ...` — the tsc helper's UMD-style self probe).
const live = (refs.get(name) ?? []).filter((p) => p < span.start || p >= span.end);
if (live.every(inDead)) {
removed.add(name);
dead.push(span);
changed = true;
}
}
}
neutralize.length = 0;
neutralize.push(...dead);
}
/* ── emission ─────────────────────────────────────────────────────── */
let text = source;
// pad from the END so earlier spans' offsets stay valid
for (const n of [...neutralize].sort((a, b) => b.start - a.start)) {
text = padSpan(text, n.start, n.end);
}
if (!emitTable) return text; // interop pads only — nothing appends
const lines: string[] = [""];
const hasAnyFallback = [...entries.values()].some((s) => s.kind === "any");
if (hasAnyFallback) {
// checked-dynamic undefined: an implicit-any IIFE result registers a
// dyn module global the table's alias plumbing can resolve.
lines.push("const __scriptc_any = (() => { let u; return u; })();");
}
lines.push(...requireConsts);
const hoists: string[] = [];
let hoistN = 0;
const keyOf = (name: string): string => (isPlainName(name) ? name : JSON.stringify(name));
/* Hoist texts are COPIED off the original source, where a `.default`
* access on a module-valued interop binding still carries its member
* spelling (the pads erase only source spans) — drop it here exactly
* like the pads do, or the tail const would read a member the module
* never exports. */
const dropInteropDefault = (text: string): string => {
let out = text;
for (const b of toEsm.moduleBindings) {
out = out.replace(new RegExp(`\\b${b}\\s*\\.\\s*default\\b`, "g"), b);
}
return out;
};
const plainParts: string[] = [];
for (const spec of entries.values()) {
if (spec.kind === "ident") {
plainParts.push(`${keyOf(spec.name)}: ${spec.value!},`);
} else if (spec.kind === "hoist") {
const v = `__scriptc_e${hoistN++}`;
hoists.push(`const ${v} = ${dropInteropDefault(spec.value!)};`);
plainParts.push(`${keyOf(spec.name)}: ${v},`);
} else {
plainParts.push(`${keyOf(spec.name)}: __scriptc_any,`);
}
}
lines.push(...hoists);
/* The table. Spread entries first (REVERSED: Node's star copies are
* first-wins, an object literal's later-wins — reversing makes them
* agree), then the scan-visible plain entries, then — only where the
* original stamped it LEXER-DETECTABLY (the tsc defineProperty form;
* Node links `import { __esModule }` there) — the `__esModule: true`
* entry, then the member-access star entries whose names the spread
* records already cover. esbuild's marker lives inside __toCommonJS
* where Node's lexer never saw it, and spelling it here would flip the
* checker's default-import interop (an __esModule-stamped surface binds
* `import def` to exports.default) away from Node's CJS answer — so it
* is deliberately OMITTED from the canonical table. */
const spreadParts = [...starSpecs].reverse().map((s) => `...require(${JSON.stringify(s)}),`);
const esModulePart = lexed.exports.has("__esModule") ? "__esModule: true," : "";
const memberParts = starMembers.map(
(m) => `${keyOf(m.name)}: ${m.ref}${isPlainName(m.name) ? `.${m.name}` : `[${JSON.stringify(m.name)}]`},`,
);
lines.push(`module.exports = {${spreadParts.join("")}${plainParts.join("")}${esModulePart}${memberParts.join("")}};`);
return text + lines.join("\n") + "\n";
}
@@ -1,210 +1,14 @@
/* --npm-static's bundler-emitted-CJS export surface: the getter-table
* shapes. A bundler-emitted CJS dist exports through runtime plumbing the
* checker cannot model — the `__export(target, { name: () => value, ... })`
* getter table behind `module.exports = __toCommonJS(target)`, the
* `Object.defineProperty(exports, 'n', { get })` family, and the
* transpiler star re-exports (`__exportStar(require('./x'), exports)`,
* `__reExport(target, require('./x'), module.exports)`). Node's CJS lexer
* SEES those named exports (cjs-lexer.ts mirrors it exactly), so named
* imports link under Node — but the inferred surface the opted-in
* resolution feeds the checker is an opaque object, and every import site
* reports "X has no exported member" (the SC0001 storm that used to gate
* the whole build).
*
* THE REWRITE. For an opted-in package's CJS file whose exports come from
* these shapes, the fs shadow (npm-static.ts) serves BOTH worlds — the
* checker and the lowering ride one program — a text rewrite that spells
* the same export surface in the plain CJS the whole existing machinery
* already models:
*
* - the recognized plumbing statements are SPACE-PADDED in place
* (newlines kept, so every original line/offset survives): the
* `module.exports = __toCommonJS(target)` assignment, the
* `__export(target, {...})` call, the star-pattern calls, the
* recognized defineProperty exports, and the dead esbuild annotation
* `0 && (module.exports = {...})` (left live it poisons the checker's
* alias resolution — multiple module.exports assignments make import
* aliases land on properties no lowering path resolves);
* - one canonical export table is APPENDED at the tail:
* `module.exports = { ...require("./star"), own: local, ... };`
* Each getter body the chase can follow types the export by its
* resolved VALUE (`() => localBinding` resolves to the binding's
* inferred type; member chains and scalar literals hoist to a tail
* const first), and every lexer-visible name the chase cannot follow
* falls back to a checked-dynamic `any` binding (value undefined —
* exactly what Node's import binds for a lexer-visible name the
* runtime export object never carries).
*
* LEXER PARITY IS THE CONTRACT: the rewritten text must answer the SAME
* name set to cjs-lexer.ts that the original answers to Node — named
* imports must link exactly where Node's would. The table is formatted
* for merve's byte rules (`name: IDENT,` — value ident runs directly
* followed by ','), star re-exports ride spread entries (recorded as
* reexports and resolved recursively, exactly Node's union), and the
* esbuild `__esModule: !0` marker is spelled as a deliberate SCAN STOPPER
* so it stays lexer-invisible (Node never lexes __esModule out of
* __toCommonJS) while member-access star entries after it are covered by
* the spread records.
*
* SNAPSHOT SEMANTICS (the documented divergence): the getter table reads
* its locals LIVE at every access; the canonical table captures values
* ONCE, at module tail — after every top-level statement ran, so ordinary
* modules see identical values, and only post-evaluation reassignment of
* an exported local (the lazy-cache counter idiom) can diverge. Star
* re-export targets load at module tail instead of their original
* statement positions (visible only to load-order-sensitive side
* effects). Node's __esModule marker survives on the rewritten object.
*
* THE IMPORT-SIDE TWIN: the same pass erases esbuild's __toESM interop
* wrapper around EXTERNAL dependencies (`var import_x =
* __toESM(require("x"))`, the `, 1` node-mode variant, inline
* member-accessed forms) — see the section header at planToEsmInterop.
* The wrapper pads down to the bare require it wraps and module-valued
* `.default` accesses pad down to their binding, so member accesses model
* on the required package's canonical table through the ordinary require
* machinery. Interop the recognizers cannot finish answers `{ degrade }`:
* the caller reports the package as an offender (naming the construct)
* and it serves from the island — never a failed build.
*
* Everything here is SYNTACTIC (typescript5 world, the cjs-lexer's
* sanctioned island): the chase decides only what TEXT to emit — the
* checker then infers the types from the emitted spellings. A file whose
* export mechanism the recognizers cannot fully account for is left
* untouched (null): its import-site errors stay, and the frontend's
* offender attribution degrades the PACKAGE to the island with a note. */
/** Node filesystem entry for the shared native CommonJS syntax pass. */
import { dirname, join, resolve as resolvePath } from "node:path";
import ts from "typescript5";
import { bareRequireSpecOf, cjsLexedExportsOf, cjsLexerVisibleNames, isExportsIdent, isModuleExports } from "./cjs-lexer.js";
import { cjsLexedExportsOf, cjsLexerVisibleNames } from "./cjs-lexer.js";
import { trackedDirectoryExists, trackedFileExists, trackedReadFile } from "./input-tracker.js";
import { resolveExports } from "./resolve.js";
import { packageNameOfSpecifier } from "./workspace-registry.js";
import { rewriteBundledFunctionImports } from "./npm-static-bundled-cjs.js";
import { isBundlerCjsCandidate, rewriteBundlerCjsSyntax } from "./npm-static-rewrite-syntax.js";
import { parseSourceFile } from "./ts7/source-parser-node.js";
const NODE_REQUIRE_CONDITIONS = new Set(["require", "node", "default"]);
/** The last NAME of a callee: bare identifier or any member chain's final
* name (`tslib_1.__exportStar` → "__exportStar"). */
function calleeNameOf(call: ts.CallExpression): string | null {
const callee = call.expression;
if (ts.isIdentifier(callee)) return callee.text;
if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) return callee.name.text;
return null;
}
/** An entry the canonical table can carry. */
interface EntrySpec {
/** The export name (may need string-key spelling). */
name: string;
/** "ident": VALUE is a plain identifier spelled directly in the table.
* "hoist": VALUE is an expression text hoisted to a tail const first.
* "any": the checked-dynamic fallback (`__scriptc_any`). */
kind: "ident" | "hoist" | "any";
value?: string;
}
/** A chaseable getter/descriptor VALUE: a plain identifier, an
* identifier-rooted member/element chain (string keys only), or a scalar
* literal. Answers the classification or null (not chaseable). */
function chaseValue(e: ts.Expression): { kind: "ident" | "hoist"; text: string } | null {
let cur: ts.Expression = e;
while (ts.isParenthesizedExpression(cur)) cur = cur.expression;
if (ts.isIdentifier(cur)) return { kind: "ident", text: cur.text };
if (
ts.isNumericLiteral(cur) ||
ts.isStringLiteral(cur) ||
ts.isNoSubstitutionTemplateLiteral(cur) ||
cur.kind === ts.SyntaxKind.TrueKeyword ||
cur.kind === ts.SyntaxKind.FalseKeyword ||
cur.kind === ts.SyntaxKind.NullKeyword
) {
return { kind: "hoist", text: cur.getText() };
}
// ident-rooted member chains: this.x / a.b.c / a["k"].d — side-effect
// free reads the tail const can evaluate once.
let walk: ts.Expression = cur;
while (
(ts.isPropertyAccessExpression(walk) && walk.questionDotToken === undefined && ts.isIdentifier(walk.name)) ||
(ts.isElementAccessExpression(walk) && walk.questionDotToken === undefined && ts.isStringLiteral(walk.argumentExpression))
) {
walk = walk.expression;
}
if (ts.isIdentifier(walk)) return { kind: "hoist", text: cur.getText() };
return null;
}
/** The getter body's returned expression for the two descriptor getter
* spellings (`get() { return X }` / `get: function () { return X }`) and
* the arrow forms the esbuild table uses (`() => X`, `() => { return X }`). */
function getterReturnOf(fn: ts.Node): ts.Expression | null {
let body: ts.ConciseBody | undefined;
if (ts.isArrowFunction(fn) && fn.parameters.length === 0) body = fn.body;
else if (ts.isFunctionExpression(fn) && fn.parameters.length === 0) body = fn.body;
else if (ts.isMethodDeclaration(fn) && fn.parameters.length === 0 && fn.body !== undefined) body = fn.body;
else return null;
if (body === undefined) return null;
if (!ts.isBlock(body)) return body; // expression-bodied arrow
if (body.statements.length !== 1) return null;
const ret = body.statements[0]!;
return ts.isReturnStatement(ret) && ret.expression !== undefined ? ret.expression : null;
}
/** The `__export(IDENT, { name: () => value, ... })` call's parts, or null.
* Every property must be a `name: <function>` assignment (identifier or
* string-literal name) — anything else is not the esbuild table. */
function exportCallOf(call: ts.CallExpression): { target: string; entries: [string, ts.Expression | null][] } | null {
if (calleeNameOf(call) !== "__export") return null;
if (call.arguments.length !== 2) return null;
const [target, table] = call.arguments as unknown as [ts.Expression, ts.Expression];
if (!ts.isIdentifier(target) || !ts.isObjectLiteralExpression(table)) return null;
const entries: [string, ts.Expression | null][] = [];
for (const prop of table.properties) {
if (!ts.isPropertyAssignment(prop)) return null;
let name: string;
if (ts.isIdentifier(prop.name)) name = prop.name.text;
else if (ts.isStringLiteral(prop.name)) name = prop.name.text;
else return null;
entries.push([name, getterReturnOf(prop.initializer)]);
}
return { target: target.text, entries };
}
/** `Object.defineProperty(exports|module.exports, 'name', {...})` — the
* name plus the chased descriptor value (getter return or `value:`), or
* null when the statement is not that shape at all. `value: null` means
* recognized but unchaseable. */
function definePropertyExportOf(
call: ts.CallExpression,
): { name: string; value: ts.Expression | null; esModuleStamp: boolean } | null {
const callee = call.expression;
if (
!ts.isPropertyAccessExpression(callee) ||
!ts.isIdentifier(callee.expression) ||
callee.expression.text !== "Object" ||
!ts.isIdentifier(callee.name) ||
callee.name.text !== "defineProperty"
) {
return null;
}
if (call.arguments.length !== 3) return null;
const [recv, nameArg, desc] = call.arguments as unknown as [ts.Expression, ts.Expression, ts.Expression];
if (!isExportsIdent(recv) && !isModuleExports(recv)) return null;
if (!ts.isStringLiteral(nameArg) || !ts.isObjectLiteralExpression(desc)) return null;
const name = nameArg.text;
let value: ts.Expression | null = null;
for (const p of desc.properties) {
if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)) {
if (p.name.text === "value") value = p.initializer;
else if (p.name.text === "get") value = getterReturnOf(p.initializer);
} else if (ts.isMethodDeclaration(p) && ts.isIdentifier(p.name) && p.name.text === "get") {
value = getterReturnOf(p);
}
}
const esModuleStamp =
name === "__esModule" && value !== null && value.kind === ts.SyntaxKind.TrueKeyword;
return { name, value, esModuleStamp };
}
/** Node's file-first CJS probes over an ABSOLUTE base (the dist-tree
* subset: exact, .js/.cjs, /index.js, package.json "main" for
* directories). */
@@ -351,754 +155,14 @@ function starTargetNames(file: string): Set<string> {
}
}
/** Space-pads [start, end) of `text` preserving newlines — the recognized
* statement disappears while every original offset survives. */
function padSpan(text: string, start: number, end: number): string {
let pad = "";
for (let i = start; i < end; i++) pad += text[i] === "\n" ? "\n" : " ";
return text.slice(0, start) + pad + text.slice(end);
}
/** A valid identifier-run export name (the table can spell it bare). */
function isPlainName(name: string): boolean {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
}
/* ── the __toESM(require(…)) interop import ──────────────────────────────
* esbuild's CJS output wraps every import of an EXTERNAL (unbundled)
* dependency in its __toESM helper: `var import_x = __toESM(require("x"))`
* (a trailing `, 1` in node mode). The wrapper's runtime semantics are
* static facts the required target decides: `default` binds the required
* module.exports itself (node mode, or a target whose __esModule is not
* truthy — the plain-CJS answer) or passes through to the target's own
* `default` export (the transpiled-ESM stamps), and every other member is
* a getter passthrough of the target's export. So the wrapper ERASES: the
* call pads down to the bare `require("x")` it wraps (a require binding
* the whole existing machinery models — the edge, the inline %init, the
* canonical-table member reads), and `.default` accesses on the binding
* pad down to the binding itself exactly where Node's answer is the
* module. The helper is recognized BY STRUCTURE (the cjs-lexer precedent:
* a quirk-faithful recognizer over the vendored text, never a general
* JS-semantics engine); a file whose interop deviates beyond recognition
* answers a DEGRADE reason and the package falls back to the island with
* the note — never a failed build, and never the silent alternative (an
* unrecognized-but-live helper keeps `var __create = Object.create;`
* alive, whose value declaration fences AT MODULE LOAD — the package
* would crash on its first import while the report claimed it static). */
interface ToEsmPlan {
/** Wrapper/`.default` spans to space-pad (they join `neutralize`, so the
* helper sweep sees the erased call sites as dead references). */
pads: { start: number; end: number }[];
/** Interop bindings whose `.default` IS the module (plain-CJS targets,
* the node-mode variant): pads erase their source `.default` accesses,
* and TEXT COPIED off the source (the canonical table's hoisted getter
* bodies) must drop the member spelling the same way. */
moduleBindings: Set<string>;
/** Non-null: the file's interop deviates beyond recognition — the reason
* the offender note carries (the package degrades to the island). */
degrade: string | null;
}
/** Structural recognition of esbuild's vendored __toESM helper:
* `var __toESM = (mod, isNodeMode, target) => (target = …, __copyProps(
* isNodeMode || !mod || !mod.__esModule
* ? __defProp(target, "default", { value: mod, enumerable: true })
* : target,
* mod))`
* — an arrow over (mod, isNodeMode[, target]) whose result is a
* __copyProps(…, mod) call copying onto a conditional that tests
* isNodeMode/__esModule and defines "default" with value mod. Comments and
* formatting are free; the SHAPE is the contract. */
function recognizedToEsmDecl(stmt: ts.Statement): boolean {
if (!ts.isVariableStatement(stmt)) return false;
const decls = stmt.declarationList.declarations;
if (decls.length !== 1) return false;
const d = decls[0]!;
if (!ts.isIdentifier(d.name) || d.name.text !== "__toESM" || d.initializer === undefined) return false;
let init: ts.Expression = d.initializer;
while (ts.isParenthesizedExpression(init)) init = init.expression;
if (!ts.isArrowFunction(init)) return false;
if (init.parameters.length < 2 || init.parameters.length > 3) return false;
const p0 = init.parameters[0]!;
const p1 = init.parameters[1]!;
if (!ts.isIdentifier(p0.name) || !ts.isIdentifier(p1.name)) return false;
const mod = p0.name.text;
const isNodeMode = p1.name.text;
if (ts.isBlock(init.body)) return false;
let expr: ts.Expression = init.body;
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
// the rightmost comma operand carries the result
while (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.CommaToken) {
expr = expr.right;
}
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
if (!ts.isCallExpression(expr) || !ts.isIdentifier(expr.expression) || expr.expression.text !== "__copyProps") return false;
if (expr.arguments.length < 2) return false;
const from = expr.arguments[1]!;
if (!ts.isIdentifier(from) || from.text !== mod) return false;
let to: ts.Expression = expr.arguments[0]!;
while (ts.isParenthesizedExpression(to)) to = to.expression;
if (!ts.isConditionalExpression(to)) return false;
const condText = to.condition.getText();
if (!condText.includes(isNodeMode) || !condText.includes("__esModule")) return false;
let stamp: ts.Expression = to.whenTrue;
while (ts.isParenthesizedExpression(stamp)) stamp = stamp.expression;
if (!ts.isCallExpression(stamp) || !ts.isIdentifier(stamp.expression) || stamp.expression.text !== "__defProp") return false;
if (stamp.arguments.length !== 3) return false;
const nameArg = stamp.arguments[1]!;
if (!ts.isStringLiteral(nameArg) || nameArg.text !== "default") return false;
const desc = stamp.arguments[2]!;
if (!ts.isObjectLiteralExpression(desc)) return false;
return desc.properties.some(
(p) =>
ts.isPropertyAssignment(p) &&
ts.isIdentifier(p.name) &&
p.name.text === "value" &&
ts.isIdentifier(p.initializer) &&
p.initializer.text === mod,
);
}
const TO_ESM_SHAPE_DEGRADE =
"its shipped JS calls the __toESM bundler-interop helper but spells it in a shape the " +
"recognizer cannot verify — the package serves from the island instead";
const TO_ESM_ESCAPE_DEGRADE =
"a __toESM(require(…)) bundler-interop object escapes into a use the static rewrite cannot " +
"follow (only module-scope bindings and direct member reads compile) — the package serves " +
"from the island instead";
const TO_ESM_ARG_DEGRADE =
"it calls the __toESM bundler-interop helper on something other than require(…) of a string " +
"literal — the package serves from the island instead";
const TO_ESM_MIXED_DEGRADE =
"its bundler-emitted export surface cannot be respelled around its __toESM interop imports " +
"— the package serves from the island instead";
/** The interop-erasure plan for one CJS file (see the section header). */
function planToEsmInterop(sf: ts.SourceFile): ToEsmPlan {
const pads: { start: number; end: number }[] = [];
const moduleBindings = new Set<string>();
const plan: ToEsmPlan = { pads, moduleBindings, degrade: null };
const fail = (reason: string): ToEsmPlan => ({ pads: [], moduleBindings: new Set(), degrade: reason });
const declStmts = sf.statements.filter(
(s) =>
ts.isVariableStatement(s) &&
s.declarationList.declarations.some(
(d) => ts.isIdentifier(d.name) && d.name.text === "__toESM",
),
);
const refs: ts.Identifier[] = [];
const calls: ts.CallExpression[] = [];
const collect = (n: ts.Node): void => {
if (ts.isIdentifier(n) && n.text === "__toESM") refs.push(n);
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === "__toESM") calls.push(n);
ts.forEachChild(n, collect);
};
collect(sf);
if (calls.length === 0 && refs.length === 0) return plan;
// Every reference must be the declarator's own name or a call's callee —
// a helper that escapes as a VALUE is outside the recognized shape.
const callees = new Set<ts.Node>(calls.map((c) => c.expression));
const declNames = new Set<ts.Node>();
for (const s of declStmts) {
for (const d of (s as ts.VariableStatement).declarationList.declarations) {
if (ts.isIdentifier(d.name) && d.name.text === "__toESM") declNames.add(d.name);
}
}
if (refs.some((r) => !callees.has(r) && !declNames.has(r))) return fail(TO_ESM_ESCAPE_DEGRADE);
if (calls.length === 0) return plan; // declared but never called — dead helper
if (declStmts.length !== 1 || !recognizedToEsmDecl(declStmts[0]!)) return fail(TO_ESM_SHAPE_DEGRADE);
/** Module-scope interop bindings: name → whether `.default` IS the
* module (pad the access) rather than a member read of its `default`. */
const bindings = new Map<string, boolean>();
for (const call of calls) {
const arg0 = call.arguments[0];
const spec = arg0 !== undefined ? bareRequireSpecOf(arg0) : null;
if (spec === null || call.arguments.length > 2) return fail(TO_ESM_ARG_DEGRADE);
let isNodeMode = false;
if (call.arguments.length === 2) {
const modeArg = call.arguments[1]!;
if (ts.isNumericLiteral(modeArg)) isNodeMode = Number(modeArg.text) !== 0;
else if (modeArg.kind === ts.SyntaxKind.TrueKeyword) isNodeMode = true;
else if (modeArg.kind === ts.SyntaxKind.FalseKeyword) isNodeMode = false;
else return fail(TO_ESM_ARG_DEGRADE);
}
const defaultIsModule = isNodeMode || !requireTargetEsModuleStamped(sf.fileName, spec);
// classify the call's use: a variable binding, or an immediate member
// read; anything else escapes.
let child: ts.Expression = call;
let parent: ts.Node = call.parent;
while (ts.isParenthesizedExpression(parent)) {
child = parent;
parent = parent.parent;
}
if (ts.isVariableDeclaration(parent) && parent.initializer === child && ts.isIdentifier(parent.name)) {
if (bindings.has(parent.name.text)) return fail(TO_ESM_ESCAPE_DEGRADE);
bindings.set(parent.name.text, defaultIsModule);
if (defaultIsModule) moduleBindings.add(parent.name.text);
} else if (ts.isPropertyAccessExpression(parent) && parent.expression === child && ts.isIdentifier(parent.name)) {
if (parent.name.text === "default" && defaultIsModule) {
if (!readOnlyAccess(parent)) return fail(TO_ESM_ESCAPE_DEGRADE);
pads.push({ start: child.getEnd(), end: parent.getEnd() });
}
} else {
return fail(TO_ESM_ESCAPE_DEGRADE);
}
// the wrapper itself: callee + '(' down, and everything after the
// require argument (`, 1` included) — the bare require survives at its
// original offsets.
pads.push({ start: call.getStart(sf), end: arg0!.getStart(sf) });
pads.push({ start: arg0!.getEnd(), end: call.getEnd() });
}
if (bindings.size > 0) {
// Binding safety: the erased binding must be declared exactly once and
// never written — the `.default` mapping below matches receivers by
// NAME (syntactic, like the rest of this file), so a shadow or a
// reassignment would silently change what a padded access reads.
const declCount = new Map<string, number>();
const bump = (name: string): void => {
if (bindings.has(name)) declCount.set(name, (declCount.get(name) ?? 0) + 1);
};
let violated = false;
const scan = (n: ts.Node): void => {
if (violated) return;
if (
(ts.isVariableDeclaration(n) || ts.isBindingElement(n) || ts.isParameter(n)) &&
ts.isIdentifier(n.name)
) {
bump(n.name.text);
} else if ((ts.isFunctionDeclaration(n) || ts.isClassDeclaration(n)) && n.name !== undefined) {
bump(n.name.text);
} else if (
ts.isBinaryExpression(n) &&
n.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
n.operatorToken.kind <= ts.SyntaxKind.LastAssignment &&
ts.isIdentifier(n.left) &&
bindings.has(n.left.text)
) {
violated = true;
} else if (
(ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) &&
(n.operator === ts.SyntaxKind.PlusPlusToken || n.operator === ts.SyntaxKind.MinusMinusToken) &&
ts.isIdentifier(n.operand) &&
bindings.has(n.operand.text)
) {
violated = true;
}
ts.forEachChild(n, scan);
};
scan(sf);
if (violated || [...bindings.keys()].some((name) => (declCount.get(name) ?? 0) !== 1)) {
return fail(TO_ESM_ESCAPE_DEGRADE);
}
// `.default` reads on a module-valued binding pad down to the binding.
const padDefaults = (n: ts.Node): void => {
if (
ts.isPropertyAccessExpression(n) &&
ts.isIdentifier(n.expression) &&
bindings.get(n.expression.text) === true &&
ts.isIdentifier(n.name) &&
n.name.text === "default"
) {
if (!readOnlyAccess(n)) {
violated = true;
return;
}
pads.push({ start: n.expression.getEnd(), end: n.getEnd() });
}
ts.forEachChild(n, padDefaults);
};
padDefaults(sf);
if (violated) return fail(TO_ESM_ESCAPE_DEGRADE);
}
return plan;
}
/** True when a padded-away `.default` access is a plain READ — an
* assignment target or delete operand cannot lose its member spelling. */
function readOnlyAccess(access: ts.PropertyAccessExpression): boolean {
const p = access.parent;
if (p !== undefined && ts.isDeleteExpression(p)) return false;
if (
p !== undefined &&
ts.isBinaryExpression(p) &&
p.left === access &&
p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
p.operatorToken.kind <= ts.SyntaxKind.LastAssignment
) {
return false;
}
if (
p !== undefined &&
(ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p)) &&
(p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)
) {
return false;
}
return true;
}
const RESERVED_KEYS = new Set(["__proto__"]);
/** The rewrite (see the header). Null = not a recognized bundle shape (or
* nothing to fix) — serve the file untouched. A `{ degrade }` answer means
* the file carries a bundler-interop construct the recognizers cannot
* finish: the caller reports the PACKAGE as an offender with the reason
* and the fallback loop islands it — never a failed build. */
export function rewriteBundlerCjsExports(
source: string,
filePath: string,
): string | { degrade: string } | null {
// Cheap gates before any parse: CJS bundle plumbing leaves textual
// fingerprints; files without any are never candidates.
if (
!source.includes("__toCommonJS") &&
!source.includes("__toESM") &&
!source.includes("__exportStar") &&
!source.includes("__reExport") &&
!(source.includes("Object.defineProperty(exports") && source.includes("get"))
) {
return null;
}
// parents ON: the helper sweep excludes declarator-name self references
// and the chase reads expression text through the tree
const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
// ESM bundles can contain closed CommonJS function factories. Recognize
// those before refusing interop that needs a dynamic module namespace.
if (ts.isExternalModule(sf)) {
const bundled = rewriteBundledFunctionImports(source, sf);
if (bundled !== null) return bundled;
return source.includes("__toESM(")
? {
degrade:
"its ES-module dist routes an external dependency through the __toESM " +
"bundler-interop helper, which has no static story in ESM output — the " +
"package serves from the island instead",
}
: null;
}
// The __toESM interop pass (see the section header): wrapper call sites
// pad down to the bare require they wrap, module-valued `.default`
// accesses pad down to their binding, and a file whose interop deviates
// beyond recognition degrades the package.
const toEsm = planToEsmInterop(sf);
if (toEsm.degrade !== null) return { degrade: toEsm.degrade };
interface Neutralize {
start: number;
end: number;
}
const neutralize: Neutralize[] = [];
const exportTables: { target: string; entries: [string, ts.Expression | null][] }[] = [];
const definePropEntries: { name: string; value: ts.Expression | null }[] = [];
const memberStmts: {
names: string[];
finalRhs: ts.Expression;
viaBareExports: boolean;
pos: number;
start: number;
end: number;
}[] = [];
/** Star spec → require-load emission order (statement order). */
const starSpecsInOrder: string[] = [];
let esbuildMainTarget: string | null = null;
let tablePos: number | null = null;
let sawBundleShape = false;
const addStarSpec = (spec: string): void => {
if (!starSpecsInOrder.includes(spec)) starSpecsInOrder.push(spec);
};
for (const stmt of sf.statements) {
if (!ts.isExpressionStatement(stmt)) continue;
const e = stmt.expression;
// `module.exports = <rhs>`
if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.EqualsToken && isModuleExports(e.left)) {
const rhs = e.right;
if (ts.isCallExpression(rhs) && calleeNameOf(rhs) === "__toCommonJS" && rhs.arguments.length === 1 && ts.isIdentifier(rhs.arguments[0]!)) {
esbuildMainTarget = (rhs.arguments[0] as ts.Identifier).text;
tablePos = stmt.getStart(sf);
neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
sawBundleShape = true;
continue;
}
// Any other real module.exports assignment: the file has an export
// mechanism the checker already models (or a shape outside the
// recognizers) — not ours to rewrite.
return null;
}
// member exports — `exports.N = RHS;` / `module.exports.N = RHS;`,
// chains included (`exports.a = exports.b = void 0;` — the tsc
// void-init preamble collects every name with the chain's final RHS).
{
let cur: ts.Expression = e;
const names: string[] = [];
let viaBareExports = false;
while (
ts.isBinaryExpression(cur) &&
cur.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
ts.isPropertyAccessExpression(cur.left) &&
cur.left.questionDotToken === undefined &&
ts.isIdentifier(cur.left.name) &&
(isExportsIdent(cur.left.expression) || isModuleExports(cur.left.expression))
) {
names.push(cur.left.name.text);
if (isExportsIdent(cur.left.expression)) viaBareExports = true;
cur = cur.right;
}
if (names.length > 0) {
memberStmts.push({
names,
finalRhs: cur,
viaBareExports,
pos: stmt.getStart(sf),
start: stmt.getStart(sf),
end: stmt.getEnd(),
});
continue; // classified later (only when a rewrite actually happens)
}
}
// the dead esbuild annotation: `0 && (module.exports = { ... });`
if (
ts.isBinaryExpression(e) &&
e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
ts.isNumericLiteral(e.left) &&
e.left.text === "0"
) {
let inner: ts.Expression = e.right;
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
if (
ts.isBinaryExpression(inner) &&
inner.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
isModuleExports(inner.left) &&
ts.isObjectLiteralExpression(inner.right)
) {
neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
sawBundleShape = true;
continue;
}
}
if (!ts.isCallExpression(e)) continue;
const name = calleeNameOf(e);
// `__export(TARGET, { getter table });`
const table = exportCallOf(e);
if (table !== null) {
exportTables.push(table);
neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
sawBundleShape = true;
continue;
}
// `__exportStar(require('spec'), exports);` — the tsc/tslib spelling —
// and `__export(require('spec'))`, the legacy twin.
if ((name === "__exportStar" || name === "__export") && e.arguments.length >= 1) {
const spec = bareRequireSpecOf(e.arguments[0]!);
if (spec !== null) {
addStarSpec(spec);
neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
sawBundleShape = true;
continue;
}
}
// `__reExport(TARGET, require('spec')[, module.exports]);` — esbuild's
// `export * from` plumbing.
if (name === "__reExport" && e.arguments.length >= 2) {
const spec = bareRequireSpecOf(e.arguments[1]!);
if (spec !== null) {
addStarSpec(spec);
neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
sawBundleShape = true;
continue;
}
}
// Object.defineProperty(exports, 'n', {...})
const def = definePropertyExportOf(e);
if (def !== null) {
if (def.esModuleStamp) {
neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
// the canonical table re-stamps it (lexer-visibly — merve detects
// this defineProperty form, so parity wants a visible entry)
continue;
}
const chased = def.value !== null ? chaseValue(def.value) : null;
if (chased !== null) {
definePropEntries.push({ name: def.name, value: def.value });
neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
sawBundleShape = true;
}
// unchaseable descriptor: LEAVE the statement (its runtime fence is
// loud where an undefined-valued entry would be a silent lie); the
// name still types through the any-fallback below.
continue;
}
}
const lexed = cjsLexedExportsOf(source, filePath);
let emitTable = sawBundleShape && (lexed.exports.size > 0 || lexed.reexports.length > 0);
const starSpecs: string[] = [];
const entries = new Map<string, EntrySpec>();
const starMembers: { name: string; ref: string }[] = [];
const requireConsts: string[] = [];
if (emitTable) {
/* Star specs Node actually honors are the lexer's SURVIVING reexports
* (annotation spreads for esbuild bundles, star-pattern calls for tsc
* output). The statements walked above contribute load order; specs the
* lexer dropped (cleared by a later module.exports=) drop here too. */
const lexerSpecs = new Set(lexed.reexports);
starSpecs.push(...starSpecsInOrder.filter((s) => lexerSpecs.has(s)));
for (const s of lexed.reexports) {
if (!starSpecs.includes(s)) starSpecs.push(s);
}
/* The entry list, first-wins per name: own getter-table entries, then
* defineProperty entries, then surviving top-level member exports, then
* (for names still uncovered) star enumerations in spec order, then the
* any fallback for every remaining lexer-visible name. */
const claim = (name: string, spec: EntrySpec): void => {
if (name === "__esModule" || RESERVED_KEYS.has(name)) return;
if (!entries.has(name)) entries.set(name, spec);
};
const classify = (name: string, value: ts.Expression | null): EntrySpec => {
const chased = value !== null ? chaseValue(value) : null;
if (chased === null) return { name, kind: "any" };
if (chased.kind === "ident") return { name, kind: "ident", value: chased.text };
return { name, kind: "hoist", value: chased.text };
};
// Own names: the getter table(s) — the one feeding module.exports first.
const tablesOrdered = [
...exportTables.filter((t) => t.target === esbuildMainTarget),
...exportTables.filter((t) => t.target !== esbuildMainTarget),
];
for (const t of tablesOrdered) {
for (const [name, value] of t.entries) claim(name, classify(name, value));
}
for (const d of definePropEntries) claim(d.name, classify(d.name, d.value));
/* Top-level member exports. Node's export-object identity rules decide
* each statement's fate against the ORIGINAL table (esbuildMainTarget's
* assignment): a member attached BEFORE it was discarded (the table
* replaced the object), a bare `exports.N =` AFTER it wrote the stale
* object — both keep only their lexer-visible NAME (value undefined,
* exactly Node's link answer: the any fallback below). A surviving
* member (`module.exports.N =` after the table, or any spelling when no
* table exists) re-emits its VALUE as a canonical entry — identifier
* RHS only; any other RHS would need a second evaluation, so the file
* is left alone (the offender path owns it — and when the file also
* carries erased interop wrappers, "untouched" is not on the menu:
* the live helper chain would fence at module load, so the package
* degrades with the note instead). Every member statement is then
* NEUTRALIZED: the canonical tail table replaces the object, and a
* stray earlier attach would meet the lowering's discard fence. */
for (const m of memberStmts) {
let cur: ts.Expression = m.finalRhs;
while (ts.isParenthesizedExpression(cur)) cur = cur.expression;
const isVoidInit = cur.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(cur) && cur.text === "undefined");
const discarded = tablePos !== null && (m.pos < tablePos || m.viaBareExports);
if (!isVoidInit) {
// Only side-effect-free RHS shapes may be neutralized (identifiers,
// scalar literals, ident-rooted member chains — chaseValue's set);
// anything else keeps the file untouched (the offender path owns
// it: neutralizing would drop an evaluation, keeping it meets the
// lowering's discard fence at load).
const chased = chaseValue(cur);
if (chased === null) {
return toEsm.pads.length > 0 ? { degrade: TO_ESM_MIXED_DEGRADE } : null;
}
if (!discarded) {
for (const name of m.names) claim(name, { name, kind: chased.kind, value: chased.text });
}
// discarded members keep only their lexer-visible NAME (the any
// fallback below) — value undefined, exactly Node's link answer
}
neutralize.push({ start: m.start, end: m.end });
}
/* Direct-vs-hoist for ident entries: only bindings declared by a
* top-level const/function/class may be spelled directly (alias
* plumbing needs an immutable target — a `var`/`let` in the table would
* fence the whole statement). Everything else snapshots via a tail
* const. */
const immutableTopLevel = new Set<string>();
for (const stmt of sf.statements) {
if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined) immutableTopLevel.add(stmt.name.text);
else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) immutableTopLevel.add(stmt.name.text);
else if (ts.isVariableStatement(stmt) && (stmt.declarationList.flags & ts.NodeFlags.Const) !== 0) {
for (const d of stmt.declarationList.declarations) {
if (ts.isIdentifier(d.name)) immutableTopLevel.add(d.name.text);
}
}
}
for (const spec of entries.values()) {
if (spec.kind === "ident" && !immutableTopLevel.has(spec.value!)) spec.kind = "hoist";
}
// Star enumerations for names the own entries left uncovered.
starSpecs.forEach((spec, i) => {
const target = resolveRelativeCjs(filePath, spec);
if (target === null) return; // bare/unresolved: the spread entry carries it
const names = starTargetNames(target);
let refVar: string | null = null;
for (const n of names) {
if (n === "default" || n === "__esModule" || entries.has(n) || RESERVED_KEYS.has(n)) continue;
if (starMembers.some((m) => m.name === n)) continue; // earlier spec wins
if (refVar === null) {
refVar = `__scriptc_r${i}`;
requireConsts.push(`const ${refVar} = require(${JSON.stringify(spec)});`);
}
starMembers.push({ name: n, ref: refVar });
}
});
// The any fallback: every lexer-visible name nothing above covered.
for (const n of lexed.exports) claim(n, { name: n, kind: "any" });
emitTable = entries.size > 0 || starSpecs.length > 0;
}
/* A file with interop pads but no canonical table still rewrites: the
* pads erase the __toESM wrappers and the sweep below drops the dead
* helper chain — but nothing replaces the export plumbing, so none of
* it may be neutralized (the surviving text keeps exactly its
* untouched-file behavior). */
if (!emitTable) {
if (toEsm.pads.length === 0) return null;
neutralize.length = 0;
}
// Interop pads join the neutralized set BEFORE the sweep: the erased
// __toESM call sites read as dead references, so the helper declaration
// sweeps away and its __create/__getProtoOf chain follows.
neutralize.push(...toEsm.pads);
/* Bundler HELPPER declarations left unused once the plumbing statements
* are gone would still fence at module load (`var __defProp =
* Object.defineProperty;` — a stdlib generic method as a value has no
* lowering, and a top-level declaration's fence throws when init runs
* it). Mark-and-sweep the RECOGNIZED helper names: a helper whose every
* reference lies inside an already-neutralized span (the plumbing
* calls, the erased __toESM interop wrappers, or another swept helper's
* declaration) is neutralized too. Helpers live code still calls
* (__commonJS chunk factories) stay — their declarations are plain
* function values whose bodies defer per the JS function-poison rule. */
{
const HELPER_NAMES = new Set([
"__create", "__defProp", "__defProps", "__getOwnPropDesc", "__getOwnPropDescs",
"__getOwnPropNames", "__getOwnPropSymbols", "__getProtoOf", "__hasOwnProp",
"__propIsEnum", "__export", "__copyProps", "__reExport", "__toESM", "__toCommonJS",
"__exportStar", "__createBinding", "__setModuleDefault", "__importStar", "__importDefault",
]);
const helperDecls = new Map<string, { start: number; end: number }>();
for (const stmt of sf.statements) {
if (!ts.isVariableStatement(stmt)) continue;
const decls = stmt.declarationList.declarations;
if (decls.length !== 1) continue;
const d = decls[0]!;
if (ts.isIdentifier(d.name) && HELPER_NAMES.has(d.name.text)) {
helperDecls.set(d.name.text, { start: stmt.getStart(sf), end: stmt.getEnd() });
}
}
// every identifier occurrence of a helper name outside its own
// declarator name node, position-recorded for the span test
const refs = new Map<string, number[]>();
const record = (node: ts.Node): void => {
ts.forEachChild(node, record);
if (ts.isIdentifier(node) && helperDecls.has(node.text)) {
const parent = node.parent;
if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.name === node) return;
const list = refs.get(node.text);
if (list === undefined) refs.set(node.text, [node.getStart(sf)]);
else list.push(node.getStart(sf));
}
};
record(sf);
const dead = [...neutralize];
const inDead = (pos: number): boolean => dead.some((s) => pos >= s.start && pos < s.end);
const removed = new Set<string>();
for (let changed = true; changed; ) {
changed = false;
for (const [name, span] of helperDecls) {
if (removed.has(name)) continue;
// SELF-references inside the candidate's own declaration don't
// keep it alive (`var __createBinding = (this && this.__createBinding)
// || ...` — the tsc helper's UMD-style self probe).
const live = (refs.get(name) ?? []).filter((p) => p < span.start || p >= span.end);
if (live.every(inDead)) {
removed.add(name);
dead.push(span);
changed = true;
}
}
}
neutralize.length = 0;
neutralize.push(...dead);
}
/* ── emission ─────────────────────────────────────────────────────── */
let text = source;
// pad from the END so earlier spans' offsets stay valid
for (const n of [...neutralize].sort((a, b) => b.start - a.start)) {
text = padSpan(text, n.start, n.end);
}
if (!emitTable) return text; // interop pads only — nothing appends
const lines: string[] = [""];
const hasAnyFallback = [...entries.values()].some((s) => s.kind === "any");
if (hasAnyFallback) {
// checked-dynamic undefined: an implicit-any IIFE result registers a
// dyn module global the table's alias plumbing can resolve.
lines.push("const __scriptc_any = (() => { let u; return u; })();");
}
lines.push(...requireConsts);
const hoists: string[] = [];
let hoistN = 0;
const keyOf = (name: string): string => (isPlainName(name) ? name : JSON.stringify(name));
/* Hoist texts are COPIED off the original source, where a `.default`
* access on a module-valued interop binding still carries its member
* spelling (the pads erase only source spans) — drop it here exactly
* like the pads do, or the tail const would read a member the module
* never exports. */
const dropInteropDefault = (text: string): string => {
let out = text;
for (const b of toEsm.moduleBindings) {
out = out.replace(new RegExp(`\\b${b}\\s*\\.\\s*default\\b`, "g"), b);
}
return out;
};
const plainParts: string[] = [];
for (const spec of entries.values()) {
if (spec.kind === "ident") {
plainParts.push(`${keyOf(spec.name)}: ${spec.value!},`);
} else if (spec.kind === "hoist") {
const v = `__scriptc_e${hoistN++}`;
hoists.push(`const ${v} = ${dropInteropDefault(spec.value!)};`);
plainParts.push(`${keyOf(spec.name)}: ${v},`);
} else {
plainParts.push(`${keyOf(spec.name)}: __scriptc_any,`);
}
}
lines.push(...hoists);
/* The table. Spread entries first (REVERSED: Node's star copies are
* first-wins, an object literal's later-wins — reversing makes them
* agree), then the scan-visible plain entries, then — only where the
* original stamped it LEXER-DETECTABLY (the tsc defineProperty form;
* Node links `import { __esModule }` there) — the `__esModule: true`
* entry, then the member-access star entries whose names the spread
* records already cover. esbuild's marker lives inside __toCommonJS
* where Node's lexer never saw it, and spelling it here would flip the
* checker's default-import interop (an __esModule-stamped surface binds
* `import def` to exports.default) away from Node's CJS answer — so it
* is deliberately OMITTED from the canonical table. */
const spreadParts = [...starSpecs].reverse().map((s) => `...require(${JSON.stringify(s)}),`);
const esModulePart = lexed.exports.has("__esModule") ? "__esModule: true," : "";
const memberParts = starMembers.map(
(m) => `${keyOf(m.name)}: ${m.ref}${isPlainName(m.name) ? `.${m.name}` : `[${JSON.stringify(m.name)}]`},`,
);
lines.push(`module.exports = {${spreadParts.join("")}${plainParts.join("")}${esModulePart}${memberParts.join("")}};`);
return text + lines.join("\n") + "\n";
export function rewriteBundlerCjsExports(source: string, filePath: string): string | { degrade: string } | null {
if (!isBundlerCjsCandidate(source)) return null;
return rewriteBundlerCjsSyntax(parseSourceFile(filePath, source, "js"), filePath, {
requireTargetEsModuleStamped,
starTargetNames: (from, spec) => {
const target = resolveRelativeCjs(from, spec);
return target === null ? new Set<string>() : starTargetNames(target);
},
});
}
+4 -4
View File
@@ -57,7 +57,7 @@ import { isPrunedNpmReexport, planNpmStaticReexports } from "./npm-static-prune.
import { npmStaticDeclarationReexports, npmStaticRuntimeClassTargets, parseNpmStaticDeclarationOverloads, parseNpmStaticDeclarationProperties } from "./npm-static-declarations.js";
import type { NpmStaticDeclarationOverloads, NpmStaticDeclarationProperties, NpmStaticOverloadSignature } from "./npm-static-declarations.js";
import { provenanceEntryFor, provenancePaths } from "./provenance-registry.js";
import { cjsLexerVisibleNames } from "./cjs-lexer.js";
import { cjsLexedExportsOfFile, cjsVisibleNames } from "./cjs-syntax.js";
import {
ambientDtsPath,
fallbackDtsPath,
@@ -3147,9 +3147,9 @@ function cjsNamedImportLinkCheck(
};
const visible = (dep: ts.SourceFile, name: string): boolean =>
// `default` is the module.exports binding itself — always provided.
// (cjs-lexer.ts lexes SOURCE TEXT — only strings cross into the
// typescript5 island; the SourceFile is just the memo/resolve handle.)
name === "default" || cjsLexerVisibleNames(dep, (d) => d.text, resolveCjsDep, lexMemo).has(name);
// The program already owns native source ASTs; reuse them without
// starting another syntax session or reparsing their source text.
name === "default" || cjsVisibleNames(dep, cjsLexedExportsOfFile, resolveCjsDep, lexMemo).has(name);
/** The statement's resolved LOCAL CommonJS dependency, when it is an
* import/re-export from one. */
+221
View File
@@ -0,0 +1,221 @@
export interface Case {
name: string;
src: string;
/** Expected lexed export names, sorted. */
exports: string[];
/** Expected surviving reexport specifiers, in source order. */
reexports?: string[];
}
const STAR_HELPER = `var __exportStar = (m, e) => { for (var p in m) e[p] = m[p]; return e; };\n`;
const BABEL_LOOP = (id: string): string =>
`Object.keys(${id}).forEach(function (key) {\n` +
` if (key === "default" || key === "__esModule") return;\n` +
` exports[key] = ${id}[key];\n` +
`});`;
export const cjsLexerCases: Case[] = [
// ── the module.exports = { ... } table scan ──────────────────────────
// An identifier-named getter stops the scan COLD, adding nothing — the
// npm cjs-module-lexer exports a phantom "get" here (the divergence that
// opened this lane); Node exports nothing.
{ name: "table getter", src: `module.exports = { get a() { return 1; } };`, exports: [] },
{ name: "table getter then plain", src: `function v(){}\nmodule.exports = { get a() { return 1; }, b: v };`, exports: [] },
{ name: "table plain then getter", src: `const v=1;\nmodule.exports = { b: v, get a() { return 1; } };`, exports: ["b"] },
// ...but a getter whose NAME is not a plain identifier exports the word
// "get" — the lexer took `get` as a shorthand key before the odd token.
{ name: "table computed getter", src: `const k='x';\nmodule.exports = { get [k]() { return 1; } };`, exports: ["get"] },
{ name: "table string getter", src: `module.exports = { get "a"() { return 1; } };`, exports: ["get"] },
{ name: "table setter", src: `module.exports = { set a(v) {} };`, exports: ["set"] },
{ name: "table computed setter", src: `const k='x';\nmodule.exports = { set [k](v) {} };`, exports: ["set"] },
{ name: "table async method", src: `module.exports = { async a() {} };`, exports: ["async"] },
{ name: "table generator method", src: `module.exports = { *a() {} };`, exports: [] },
{ name: "table plain method", src: `module.exports = { a() {} };`, exports: ["a"] },
{ name: "table method then plain", src: `const v=1;\nmodule.exports = { a() {}, b: v };`, exports: ["a"] },
{ name: "table string-named method", src: `module.exports = { "a"() {} };`, exports: [] },
{ name: "table shorthand", src: `const a=1,b=2;\nmodule.exports = { a, b };`, exports: ["a", "b"] },
{ name: "table shorthand space before comma", src: `const a=1,b=2;\nmodule.exports = { a , b };`, exports: ["a", "b"] },
{ name: "table literal value", src: `module.exports = { a: 7 };`, exports: [] },
{ name: "table literal then ident", src: `const v=1;\nmodule.exports = { a: 7, b: v };`, exports: [] },
{ name: "table ident then literal", src: `const v=1;\nmodule.exports = { a: v, b: 7 };`, exports: ["a"] },
{ name: "table string value", src: `module.exports = { a: "s" };`, exports: [] },
{ name: "table arrow value", src: `module.exports = { a: () => 1 };`, exports: [] },
{ name: "table paren-free arrow value", src: `module.exports = { a: x => 1 };`, exports: ["a"] },
{ name: "table function value", src: `module.exports = { a: function() {} };`, exports: ["a"] },
{ name: "table space before comma", src: `const b=1,d=2;\nmodule.exports = { a: b , c: d };`, exports: ["a"] },
{ name: "table newline before comma", src: `function b(){} function d(){}\nmodule.exports = { a: b\n, c: d };`, exports: ["a"] },
{ name: "table member value", src: `const o={x:1};\nmodule.exports = { a: o.x, b: o };`, exports: ["a"] },
{ name: "table call value", src: `const f=()=>1;\nmodule.exports = { a: f(), b: f };`, exports: ["a"] },
{ name: "table string key", src: `const v=1;\nmodule.exports = { "s-key": v, b: v };`, exports: ["b", "s-key"] },
{ name: "table two string keys", src: `function v(){}\nmodule.exports = { "x-1": v, "y-2": v };`, exports: ["x-1", "y-2"] },
{ name: "table numeric key", src: `const v=1;\nmodule.exports = { 1: v, b: v };`, exports: [] },
{ name: "table computed key", src: `const v=1,k='x';\nmodule.exports = { [k]: v, b: v };`, exports: [] },
{ name: "table template value", src: "const v=1;\nmodule.exports = { a: `t`, b: v };", exports: [] },
{ name: "table keyword values", src: `module.exports = { a: this, b: void 0 };`, exports: ["a", "b"] },
{ name: "table typeof value", src: `function v(){}\nmodule.exports = { a: typeof v, b: v };`, exports: ["a"] },
{ name: "table new value", src: `class C{}\nmodule.exports = { a: new C(), b: C };`, exports: ["a"] },
{ name: "table reserved-word keys", src: `function f(){}\nmodule.exports = { ok: f, yield: f, after: f };`, exports: ["after", "ok", "yield"] },
{ name: "table escaped shorthand stops", src: `const \\u0061b=1, c=1;\nmodule.exports = { \\u0061b, c };`, exports: [] },
// an escape MID-name: merve consumed the byte prefix and added it before
// the backslash failed the separator check — the prefix exports, the
// scan stops (probed: Node answers exactly ["a"])
{ name: "table mid-escape key adds its byte prefix", src: `const v=1;\nmodule.exports = { a\\u0062c: v, d: v };`, exports: ["a"] },
{ name: "table mid-escape shorthand adds its byte prefix", src: `const ab\\u0063=1;\nmodule.exports = { ab\\u0063, d: ab\\u0063 };`, exports: ["ab"] },
{ name: "table mid-escape value keeps the key and stops", src: `const a\\u0062c=1;\nmodule.exports = { k: a\\u0062c, d: a\\u0062c };`, exports: ["k"] },
{ name: "table get as shorthand key", src: `const get=1;\nmodule.exports = { get, a: get };`, exports: ["a", "get"] },
{ name: "parenthesized table never matches", src: `const v=1;\nmodule.exports = ({ a: v });`, exports: [] },
// spreads: identifier runs continue (whitespace skipped before the `,`
// check, unlike the value path), anything else stops
{ name: "table spread ident", src: `const o={x:1},v=1;\nmodule.exports = { ...o, a: v };`, exports: ["a"] },
{ name: "table spread ident with space", src: `const o={x:1},v=1;\nmodule.exports = { ...o , a: v };`, exports: ["a"] },
{ name: "table spread call stops", src: `const f=()=>({}),v=1;\nmodule.exports = { ...f(), a: v };`, exports: [] },
{ name: "table spread member stops", src: `const o={p:{}},v=1;\nmodule.exports = { ...o.p, a: v };`, exports: [] },
// ── require(...) table values and spreads: REEXPORTS ─────────────────
// A require VALUE adds its key, records the reexport, and stops the scan
// unconditionally — `b` is never reached. cjs-module-lexer keeps the key
// but misses the reexport entirely.
{ name: "require value stops with reexport", src: `function v(){}\nmodule.exports = { a: require("./x.cjs"), b: v };`, exports: ["a"], reexports: ["./x.cjs"] },
{ name: "require value single-quoted", src: `module.exports = { a: require('./x.cjs'), b: 1 };`, exports: ["a"], reexports: ["./x.cjs"] },
{ name: "require value with member trailer", src: `const v=1;\nmodule.exports = { a: require("./x.cjs").foo, b: v };`, exports: ["a"], reexports: ["./x.cjs"] },
{ name: "require value with || trailer", src: `function y(){}\nmodule.exports = { a: require("./x.cjs") || y, c: y };`, exports: ["a"], reexports: ["./x.cjs"] },
{ name: "require value last", src: `module.exports = { a: require("./x.cjs") };`, exports: ["a"], reexports: ["./x.cjs"] },
{ name: "require value string key", src: `function v(){}\nmodule.exports = { "k": require("./x.cjs"), b: v };`, exports: ["k"], reexports: ["./x.cjs"] },
{ name: "require value with space before (", src: `function f(){}\nmodule.exports = { a: require ("./x.cjs"), b: f };`, exports: ["a"], reexports: ["./x.cjs"] },
// a template/extra-arg require is no REQUIRE: the value consumes the
// `require` identifier run, the `(` stops the scan, the key stays
{ name: "require template arg is not a require", src: "function f(){}\nmodule.exports = { a: require(`./x.cjs`), b: f };", exports: ["a"] },
{ name: "require two args is not a require", src: `function f(){}\nmodule.exports = { a: require("./x.cjs", "y"), b: f };`, exports: ["a"] },
{ name: "require after a scan stop records nothing", src: `module.exports = { lit: 7, a: require("./x.cjs") };`, exports: [] },
// the SPREAD path genuinely continues past a `,` after the require
{ name: "spread require continues", src: `const v=1;\nmodule.exports = { ...require("./x.cjs"), a: v };`, exports: ["a"], reexports: ["./x.cjs"] },
{ name: "spread require member stops", src: `function f(){}\nmodule.exports = { ...require("./x.cjs").foo, a: f };`, exports: [], reexports: ["./x.cjs"] },
{ name: "spread require call stops", src: `function f(){}\nmodule.exports = { ...require("./x.cjs")(), a: f };`, exports: [], reexports: ["./x.cjs"] },
{ name: "spread require last", src: `module.exports = { ...require("./x.cjs") };`, exports: [], reexports: ["./x.cjs"] },
{ name: "two table reexports", src: `module.exports = { ...require("./x.cjs"), b: require("./y.cjs") };`, exports: ["b"], reexports: ["./x.cjs", "./y.cjs"] },
// ── exports.NAME / ['string'] assignments: position-blind ────────────
{ name: "exports dot", src: `exports.a = 1;`, exports: ["a"] },
{ name: "module.exports dot", src: `module.exports.a = 1;`, exports: ["a"] },
{ name: "exports bracket", src: `exports["a-b"] = 1;`, exports: ["a-b"] },
{ name: "module.exports bracket", src: `module.exports["a-b"] = 1;`, exports: ["a-b"] },
{ name: "dead branch counts", src: `if (0) { exports.a = 1; }`, exports: ["a"] },
{ name: "nested function counts", src: `function f() { exports.a = 1; }`, exports: ["a"] },
{ name: "shadowed exports still counts", src: `function f(exports) { exports.a = 1; }\nf({});`, exports: ["a"] },
{ name: "shadowed module still counts", src: `function f(module) { module.exports.a = 1; }\nf({exports:{}});`, exports: ["a"] },
{ name: "template bracket misses", src: "exports[`a`] = 1;", exports: [] },
{ name: "escaped name misses", src: `exports.\\u0061b = 1;`, exports: [] },
// mid-name escapes break the run before merve ever sees the `=`, so the
// dot-assign pattern misses ENTIRELY — no prefix here (probed)
{ name: "mid-escaped dot name misses entirely", src: `exports.a\\u0062c = 1;`, exports: [] },
{ name: "compound assignment misses", src: `exports.a = 0; exports.b += 1;`, exports: ["a"] },
{ name: "logical assignment misses", src: `exports.a ||= 1;`, exports: [] },
{ name: "chained assignment takes both", src: `exports.a = exports.b = 1;`, exports: ["a", "b"] },
{ name: "reserved words are NOT filtered", src: `exports.let = 1;\nexports.static = 2;\nexports.package = 3;\nexports.await = 4;\nexports.async = 5;`, exports: ["async", "await", "let", "package", "static"] },
// ── module.exports = require(...): leading-require reexports ─────────
{ name: "bare require reexport", src: `module.exports = require("./x.cjs");`, exports: [], reexports: ["./x.cjs"] },
{ name: "require reexport without semicolon", src: `module.exports = require("./x.cjs")`, exports: [], reexports: ["./x.cjs"] },
// trailers are ignored once the require LEADS the right-hand side —
// cjs-module-lexer only takes the bare form for the member/|| shapes
{ name: "require.member reexport", src: `module.exports = require("./x.cjs").foo;`, exports: [], reexports: ["./x.cjs"] },
{ name: "require.member.member reexport", src: `module.exports = require("./x.cjs").foo.x;`, exports: [], reexports: ["./x.cjs"] },
{ name: "require newline member reexport", src: `module.exports = require("./x.cjs")\n.foo;`, exports: [], reexports: ["./x.cjs"] },
{ name: "require() call reexport", src: `module.exports = require("./x.cjs")();`, exports: [], reexports: ["./x.cjs"] },
{ name: "require || reexport", src: `module.exports = require("./x.cjs") || {};`, exports: [], reexports: ["./x.cjs"] },
{ name: "require ternary-lead reexport", src: `module.exports = require("./x.cjs") ? {} : {};`, exports: [], reexports: ["./x.cjs"] },
// ...but the require must LEAD: parens, comma operators, wrappers and
// ternary arms record nothing
{ name: "parenthesized require misses", src: `module.exports = (require("./x.cjs"));`, exports: [] },
{ name: "comma-expression require misses", src: `module.exports = (0, require("./x.cjs"));`, exports: [] },
{ name: "wrapped require misses", src: `function f(x){return x;}\nmodule.exports = f(require("./x.cjs"));`, exports: [] },
{ name: "ternary-arm require misses", src: `module.exports = 1 ? require("./x.cjs") : {};`, exports: [] },
{ name: "template require misses", src: "module.exports = require(`./x.cjs`);", exports: [] },
// ── clearing: every module.exports= resets reexports, names persist ──
{ name: "reexport cleared by empty table", src: `module.exports = require("./x.cjs");\nmodule.exports = { };`, exports: [] },
{ name: "last require wins", src: `module.exports = {};\nmodule.exports = require("./x.cjs");`, exports: [], reexports: ["./x.cjs"] },
{ name: "reexport plus dot add", src: `module.exports = require("./x.cjs");\nmodule.exports.extra = 1;`, exports: ["extra"], reexports: ["./x.cjs"] },
{ name: "table names accumulate across assignments", src: `function f(){}\nmodule.exports = { a: f };\nmodule.exports = { b: f };`, exports: ["a", "b"] },
{ name: "cleared table keeps its lexed names", src: `function f(){}\nmodule.exports = { a: require("./x.cjs") };\nmodule.exports = { b: f };`, exports: ["a", "b"] },
// ── Object.defineProperty descriptor shapes ───────────────────────────
{ name: "dp value", src: `Object.defineProperty(exports, "a", { value: 1 });`, exports: ["a"] },
{ name: "dp enumerable value", src: `Object.defineProperty(exports, "a", { enumerable: true, value: 1 });`, exports: ["a"] },
{ name: "dp enumerable:false misses", src: `Object.defineProperty(exports, "a", { enumerable: false, value: 1 });`, exports: [] },
{ name: "dp getter ident", src: `const x=1;\nObject.defineProperty(exports, "a", { get() { return x; } });`, exports: ["a"] },
{ name: "dp getter member", src: `const o={x:1};\nObject.defineProperty(exports, "a", { get() { return o.x; } });`, exports: ["a"] },
{ name: "dp getter element", src: `const o={x:1};\nObject.defineProperty(exports, "a", { get() { return o['x']; } });`, exports: ["a"] },
// `this` is an identifier RUN to the byte lexer — `return this.x` matches
{ name: "dp getter this.member", src: `Object.defineProperty(exports, "a", { get() { return this.x; } });`, exports: ["a"] },
{ name: "dp getter call misses", src: `const f=()=>1;\nObject.defineProperty(exports, "a", { get() { return f(); } });`, exports: [] },
{ name: "dp getter function expression", src: `const x=1;\nObject.defineProperty(exports, "a", { get: function() { return x; } });`, exports: ["a"] },
{ name: "dp getter arrow misses", src: `const x=1;\nObject.defineProperty(exports, "a", { get: () => x });`, exports: [] },
{ name: "dp getter not last misses", src: `const x=1;\nObject.defineProperty(exports, "a", { get() { return x; }, enumerable: true });`, exports: [] },
{ name: "dp enumerable getter", src: `const x=1;\nObject.defineProperty(exports, "a", { enumerable: true, get() { return x; } });`, exports: ["a"] },
{ name: "dp on module.exports", src: `Object.defineProperty(module.exports, "a", { value: 1 });`, exports: ["a"] },
{ name: "dp template name misses", src: "Object.defineProperty(exports, `a`, { value: 1 });", exports: [] },
{ name: "dp aliased defineProperty misses", src: `var __defProp = Object.defineProperty;\n__defProp(exports, "a", { value: 1 });`, exports: [] },
// NO POISONING: a non-matching defineProperty of the same name elsewhere
// does not remove the valid one (cjs-module-lexer removes it; Node keeps)
{ name: "dp invalid-then-valid keeps the name", src: `const x=1;\nObject.defineProperty(exports, "a", { configurable: true });\nObject.defineProperty(exports, "a", { enumerable: true, get(){ return x; } });`, exports: ["a"] },
{ name: "dp valid-then-invalid keeps the name", src: `Object.defineProperty(exports, "a", { enumerable: true, value: 1 });\nObject.defineProperty(exports, "a", { madeup: true });`, exports: ["a"] },
// ── the esbuild annotation and __export helper ────────────────────────
{ name: "esbuild dead annotation", src: `function a(){}\nfunction b(){}\n0 && (module.exports = { a, b });`, exports: ["a", "b"] },
{ name: "esbuild double-paren annotation misses", src: `function a(){}\n0 && (module.exports = ({ a }));`, exports: [] },
{ name: "esbuild __export(exports, table) detects nothing", src: `var __defProp = Object.defineProperty;\nvar __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); };\n__export(exports, { a: () => a, b: () => b });\nfunction a(){}\nfunction b(){}`, exports: [] },
// ── the transpiler star-reexport patterns ─────────────────────────────
{ name: "__exportStar records", src: STAR_HELPER + `__exportStar(require("./x.cjs"), exports);`, exports: [], reexports: ["./x.cjs"] },
{ name: "__export single-arg records", src: `var __export = (m) => { for (var p in m) exports[p] = m[p]; };\n__export(require("./x.cjs"));`, exports: [], reexports: ["./x.cjs"] },
{ name: "tslib member form records", src: `var tslib_1 = { __exportStar: (m, e) => e };\ntslib_1.__exportStar(require("./x.cjs"), exports);`, exports: [], reexports: ["./x.cjs"] },
{ name: "deep member chain records", src: `var a = { b: { __exportStar: (m, e) => e } };\na.b.__exportStar(require("./x.cjs"), exports);`, exports: [], reexports: ["./x.cjs"] },
{ name: "comma-wrapped tslib misses", src: `var tslib_1 = { __exportStar: (m, e) => e };\n(0, tslib_1.__exportStar)(require("./x.cjs"), exports);`, exports: [] },
{ name: "esbuild __reExport misses (require is 2nd arg)", src: `var __reExport = (t, m) => t;\n__reExport(exports, require("./x.cjs"));`, exports: [] },
{ name: "other helper names miss", src: `var __copyAll = (m, e) => e;\n__copyAll(require("./x.cjs"), exports);`, exports: [] },
{ name: "name boundary is exact", src: `var __exportStarX = (m, e) => e;\n__exportStarX(require("./x.cjs"), exports);`, exports: [] },
{ name: "nested star misses", src: `function f() {\n var __exportStar = (m, e) => e;\n __exportStar(require("./x.cjs"), exports);\n}\nf();`, exports: [] },
{ name: "braced-if star misses", src: STAR_HELPER + `if (true) { __exportStar(require("./x.cjs"), exports); }`, exports: [] },
{ name: "unbraced-if star records (brace depth 0)", src: STAR_HELPER + `if (true) __exportStar(require("./x.cjs"), exports);`, exports: [], reexports: ["./x.cjs"] },
// the byte seam: NAME(require with no trivia — one space kills it
{ name: "space after ( kills the star", src: STAR_HELPER + `__exportStar( require("./x.cjs"), exports);`, exports: [] },
{ name: "comment after ( kills the star", src: STAR_HELPER + `__exportStar(/*c*/require("./x.cjs"), exports);`, exports: [] },
{ name: "space before ( kills the star", src: STAR_HELPER + `__exportStar (require("./x.cjs"), exports);`, exports: [] },
{ name: "space inside the require call is fine", src: STAR_HELPER + `__exportStar(require( "./x.cjs" ), exports);`, exports: [], reexports: ["./x.cjs"] },
{ name: "space between require and its ( is fine", src: STAR_HELPER + `__exportStar(require ("./x.cjs"), exports);`, exports: [], reexports: ["./x.cjs"] },
{ name: "star arg trailer is fine", src: STAR_HELPER + `__exportStar(require("./x.cjs").constructor ? require("./x.cjs") : {}, exports);`, exports: [], reexports: ["./x.cjs"] },
{ name: "star result assigned records", src: STAR_HELPER + `var r = __exportStar(require("./x.cjs"), exports);`, exports: [], reexports: ["./x.cjs"] },
{ name: "star inside exports.dot assignment records", src: `var tslib_1 = { __exportStar: (m, e) => e };\nexports.z = tslib_1.__exportStar(require("./x.cjs"), exports);`, exports: ["z"], reexports: ["./x.cjs"] },
{ name: "two stars record both", src: STAR_HELPER + `__exportStar(require("./x.cjs"), exports);\n__exportStar(require("./y.cjs"), exports);`, exports: [], reexports: ["./x.cjs", "./y.cjs"] },
// stars join the clearing list at their own position
{ name: "star cleared by later assignment", src: STAR_HELPER + `__exportStar(require("./x.cjs"), exports);\nmodule.exports = {};`, exports: [] },
{ name: "star after clearing survives", src: `module.exports = {};\nvar __exportStar = (m, e) => e;\n__exportStar(require("./x.cjs"), module.exports);`, exports: [], reexports: ["./x.cjs"] },
{ name: "star cleared by require assignment", src: STAR_HELPER + `__exportStar(require("./x.cjs"), exports);\nmodule.exports = require("./y.cjs");`, exports: [], reexports: ["./y.cjs"] },
{ name: "star after require assignment joins it", src: STAR_HELPER + `module.exports = require("./y.cjs");\n__exportStar(require("./x.cjs"), module.exports);`, exports: [], reexports: ["./y.cjs", "./x.cjs"] },
// ── the Babel copy loop ───────────────────────────────────────────────
{ name: "babel loop records", src: `var _d = require("./x.cjs");\n` + BABEL_LOOP("_d"), exports: [], reexports: ["./x.cjs"] },
{ name: "babel const declarator records", src: `function _interopRequireWildcard(m){return m;}\nconst _d = _interopRequireWildcard(require("./x.cjs"));\n` + BABEL_LOOP("_d"), exports: [], reexports: ["./x.cjs"] },
{ name: "babel wildcard helper records", src: `function _interopRequireWildcard(m){return m;}\nvar _d = _interopRequireWildcard(require("./x.cjs"));\n` + BABEL_LOOP("_d"), exports: [], reexports: ["./x.cjs"] },
{ name: "babel hasOwnProperty guard records", src: `var _d = require("./x.cjs");\nObject.keys(_d).forEach(function (key) {\n if (key === "default" || key === "__esModule") return;\n if (Object.prototype.hasOwnProperty.call(exports, key)) return;\n exports[key] = _d[key];\n});`, exports: [], reexports: ["./x.cjs"] },
{ name: "babel !== default form records", src: `var _d = require("./x.cjs");\nObject.keys(_d).forEach(function (key) {\n if (key !== "default") exports[key] = _d[key];\n});`, exports: [], reexports: ["./x.cjs"] },
{ name: "babel defineProperty copy records", src: `var _d = require("./x.cjs");\nObject.keys(_d).forEach(function (key) {\n if (key === "default" || key === "__esModule") return;\n Object.defineProperty(exports, key, { enumerable: true, get: function () { return _d[key]; } });\n});`, exports: [], reexports: ["./x.cjs"] },
{ name: "babel var-list require-first records", src: `var _d = require("./x.cjs"), x = 1;\n` + BABEL_LOOP("_d"), exports: [], reexports: ["./x.cjs"] },
{ name: "babel var-list require-second misses", src: `var x = 1, _d = require("./x.cjs");\n` + BABEL_LOOP("_d"), exports: [] },
{ name: "babel arrow callback misses", src: `var _d = require("./x.cjs");\nObject.keys(_d).forEach((key) => {\n if (key === "default" || key === "__esModule") return;\n exports[key] = _d[key];\n});`, exports: [] },
{ name: "babel named callback misses", src: `var _d = require("./x.cjs");\nObject.keys(_d).forEach(function copy(key) {\n if (key === "default" || key === "__esModule") return;\n exports[key] = _d[key];\n});`, exports: [] },
{ name: "babel filterless loop misses", src: `var _d = require("./x.cjs");\nObject.keys(_d).forEach(function (key) {\n exports[key] = _d[key];\n});`, exports: [] },
{ name: "babel braced return misses", src: `var _d = require("./x.cjs");\nObject.keys(_d).forEach(function (key) {\n if (key === "default" || key === "__esModule") { return; }\n exports[key] = _d[key];\n});`, exports: [] },
{ name: "babel reordered filter misses", src: `var _d = require("./x.cjs");\nObject.keys(_d).forEach(function (key) {\n if (key === "__esModule" || key === "default") return;\n exports[key] = _d[key];\n});`, exports: [] },
{ name: "babel unlinked var misses", src: `var real = require("./x.cjs");\nvar _d = real;\n` + BABEL_LOOP("_d"), exports: [] },
{ name: "babel nested loop misses", src: `function go() {\n var _d = require("./x.cjs");\n ` + BABEL_LOOP("_d") + `\n}\ngo();`, exports: [] },
{ name: "babel member wildcard misses", src: `var tslib_1 = { _interopRequireWildcard: (m) => m };\nvar _d = tslib_1._interopRequireWildcard(require("./x.cjs"));\n` + BABEL_LOOP("_d"), exports: [] },
{ name: "babel wildcard-paren space misses", src: `function _interopRequireWildcard(m){return m;}\nvar _d = _interopRequireWildcard( require("./x.cjs"));\n` + BABEL_LOOP("_d"), exports: [] },
{ name: "babel loop cleared by later assignment", src: `var _d = require("./x.cjs");\n` + BABEL_LOOP("_d") + `\nmodule.exports = {};`, exports: [] },
// the reexport's clearing position is the LOOP's, not the var's
{ name: "babel var before clearing, loop after: survives", src: `var _d = require("./x.cjs");\nmodule.exports = {};\nObject.keys(_d).forEach(function (key) {\n if (key === "default" || key === "__esModule") return;\n module.exports[key] = _d[key];\n});`, exports: [], reexports: ["./x.cjs"] },
// the full tsc emits, as shipped by packages
{ name: "tsc __exportStar preamble", src: `"use strict";\nvar __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) exports[p] = m[p]; };\nObject.defineProperty(exports, "__esModule", { value: true });\n__exportStar(require("./x.cjs"), exports);`, exports: ["__esModule"], reexports: ["./x.cjs"] },
{ name: "tsc __createBinding star", src: `"use strict";\nvar __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; }));\nvar __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); };\nObject.defineProperty(exports, "__esModule", { value: true });\n__exportStar(require("./x.cjs"), exports);`, exports: ["__esModule"], reexports: ["./x.cjs"] },
];
+1 -221
View File
@@ -10,227 +10,7 @@
import { describe, expect, test } from "vitest";
import { cjsLexedExportsOf, cjsLexerVisibleNames } from "../src/frontend/cjs-lexer.js";
interface Case {
name: string;
src: string;
/** Expected lexed export names, sorted. */
exports: string[];
/** Expected surviving reexport specifiers, in source order. */
reexports?: string[];
}
const STAR_HELPER = `var __exportStar = (m, e) => { for (var p in m) e[p] = m[p]; return e; };\n`;
const BABEL_LOOP = (id: string): string =>
`Object.keys(${id}).forEach(function (key) {\n` +
` if (key === "default" || key === "__esModule") return;\n` +
` exports[key] = ${id}[key];\n` +
`});`;
const cases: Case[] = [
// ── the module.exports = { ... } table scan ──────────────────────────
// An identifier-named getter stops the scan COLD, adding nothing — the
// npm cjs-module-lexer exports a phantom "get" here (the divergence that
// opened this lane); Node exports nothing.
{ name: "table getter", src: `module.exports = { get a() { return 1; } };`, exports: [] },
{ name: "table getter then plain", src: `function v(){}\nmodule.exports = { get a() { return 1; }, b: v };`, exports: [] },
{ name: "table plain then getter", src: `const v=1;\nmodule.exports = { b: v, get a() { return 1; } };`, exports: ["b"] },
// ...but a getter whose NAME is not a plain identifier exports the word
// "get" — the lexer took `get` as a shorthand key before the odd token.
{ name: "table computed getter", src: `const k='x';\nmodule.exports = { get [k]() { return 1; } };`, exports: ["get"] },
{ name: "table string getter", src: `module.exports = { get "a"() { return 1; } };`, exports: ["get"] },
{ name: "table setter", src: `module.exports = { set a(v) {} };`, exports: ["set"] },
{ name: "table computed setter", src: `const k='x';\nmodule.exports = { set [k](v) {} };`, exports: ["set"] },
{ name: "table async method", src: `module.exports = { async a() {} };`, exports: ["async"] },
{ name: "table generator method", src: `module.exports = { *a() {} };`, exports: [] },
{ name: "table plain method", src: `module.exports = { a() {} };`, exports: ["a"] },
{ name: "table method then plain", src: `const v=1;\nmodule.exports = { a() {}, b: v };`, exports: ["a"] },
{ name: "table string-named method", src: `module.exports = { "a"() {} };`, exports: [] },
{ name: "table shorthand", src: `const a=1,b=2;\nmodule.exports = { a, b };`, exports: ["a", "b"] },
{ name: "table shorthand space before comma", src: `const a=1,b=2;\nmodule.exports = { a , b };`, exports: ["a", "b"] },
{ name: "table literal value", src: `module.exports = { a: 7 };`, exports: [] },
{ name: "table literal then ident", src: `const v=1;\nmodule.exports = { a: 7, b: v };`, exports: [] },
{ name: "table ident then literal", src: `const v=1;\nmodule.exports = { a: v, b: 7 };`, exports: ["a"] },
{ name: "table string value", src: `module.exports = { a: "s" };`, exports: [] },
{ name: "table arrow value", src: `module.exports = { a: () => 1 };`, exports: [] },
{ name: "table paren-free arrow value", src: `module.exports = { a: x => 1 };`, exports: ["a"] },
{ name: "table function value", src: `module.exports = { a: function() {} };`, exports: ["a"] },
{ name: "table space before comma", src: `const b=1,d=2;\nmodule.exports = { a: b , c: d };`, exports: ["a"] },
{ name: "table newline before comma", src: `function b(){} function d(){}\nmodule.exports = { a: b\n, c: d };`, exports: ["a"] },
{ name: "table member value", src: `const o={x:1};\nmodule.exports = { a: o.x, b: o };`, exports: ["a"] },
{ name: "table call value", src: `const f=()=>1;\nmodule.exports = { a: f(), b: f };`, exports: ["a"] },
{ name: "table string key", src: `const v=1;\nmodule.exports = { "s-key": v, b: v };`, exports: ["b", "s-key"] },
{ name: "table two string keys", src: `function v(){}\nmodule.exports = { "x-1": v, "y-2": v };`, exports: ["x-1", "y-2"] },
{ name: "table numeric key", src: `const v=1;\nmodule.exports = { 1: v, b: v };`, exports: [] },
{ name: "table computed key", src: `const v=1,k='x';\nmodule.exports = { [k]: v, b: v };`, exports: [] },
{ name: "table template value", src: "const v=1;\nmodule.exports = { a: `t`, b: v };", exports: [] },
{ name: "table keyword values", src: `module.exports = { a: this, b: void 0 };`, exports: ["a", "b"] },
{ name: "table typeof value", src: `function v(){}\nmodule.exports = { a: typeof v, b: v };`, exports: ["a"] },
{ name: "table new value", src: `class C{}\nmodule.exports = { a: new C(), b: C };`, exports: ["a"] },
{ name: "table reserved-word keys", src: `function f(){}\nmodule.exports = { ok: f, yield: f, after: f };`, exports: ["after", "ok", "yield"] },
{ name: "table escaped shorthand stops", src: `const \\u0061b=1, c=1;\nmodule.exports = { \\u0061b, c };`, exports: [] },
// an escape MID-name: merve consumed the byte prefix and added it before
// the backslash failed the separator check — the prefix exports, the
// scan stops (probed: Node answers exactly ["a"])
{ name: "table mid-escape key adds its byte prefix", src: `const v=1;\nmodule.exports = { a\\u0062c: v, d: v };`, exports: ["a"] },
{ name: "table mid-escape shorthand adds its byte prefix", src: `const ab\\u0063=1;\nmodule.exports = { ab\\u0063, d: ab\\u0063 };`, exports: ["ab"] },
{ name: "table mid-escape value keeps the key and stops", src: `const a\\u0062c=1;\nmodule.exports = { k: a\\u0062c, d: a\\u0062c };`, exports: ["k"] },
{ name: "table get as shorthand key", src: `const get=1;\nmodule.exports = { get, a: get };`, exports: ["a", "get"] },
{ name: "parenthesized table never matches", src: `const v=1;\nmodule.exports = ({ a: v });`, exports: [] },
// spreads: identifier runs continue (whitespace skipped before the `,`
// check, unlike the value path), anything else stops
{ name: "table spread ident", src: `const o={x:1},v=1;\nmodule.exports = { ...o, a: v };`, exports: ["a"] },
{ name: "table spread ident with space", src: `const o={x:1},v=1;\nmodule.exports = { ...o , a: v };`, exports: ["a"] },
{ name: "table spread call stops", src: `const f=()=>({}),v=1;\nmodule.exports = { ...f(), a: v };`, exports: [] },
{ name: "table spread member stops", src: `const o={p:{}},v=1;\nmodule.exports = { ...o.p, a: v };`, exports: [] },
// ── require(...) table values and spreads: REEXPORTS ─────────────────
// A require VALUE adds its key, records the reexport, and stops the scan
// unconditionally — `b` is never reached. cjs-module-lexer keeps the key
// but misses the reexport entirely.
{ name: "require value stops with reexport", src: `function v(){}\nmodule.exports = { a: require("./x.cjs"), b: v };`, exports: ["a"], reexports: ["./x.cjs"] },
{ name: "require value single-quoted", src: `module.exports = { a: require('./x.cjs'), b: 1 };`, exports: ["a"], reexports: ["./x.cjs"] },
{ name: "require value with member trailer", src: `const v=1;\nmodule.exports = { a: require("./x.cjs").foo, b: v };`, exports: ["a"], reexports: ["./x.cjs"] },
{ name: "require value with || trailer", src: `function y(){}\nmodule.exports = { a: require("./x.cjs") || y, c: y };`, exports: ["a"], reexports: ["./x.cjs"] },
{ name: "require value last", src: `module.exports = { a: require("./x.cjs") };`, exports: ["a"], reexports: ["./x.cjs"] },
{ name: "require value string key", src: `function v(){}\nmodule.exports = { "k": require("./x.cjs"), b: v };`, exports: ["k"], reexports: ["./x.cjs"] },
{ name: "require value with space before (", src: `function f(){}\nmodule.exports = { a: require ("./x.cjs"), b: f };`, exports: ["a"], reexports: ["./x.cjs"] },
// a template/extra-arg require is no REQUIRE: the value consumes the
// `require` identifier run, the `(` stops the scan, the key stays
{ name: "require template arg is not a require", src: "function f(){}\nmodule.exports = { a: require(`./x.cjs`), b: f };", exports: ["a"] },
{ name: "require two args is not a require", src: `function f(){}\nmodule.exports = { a: require("./x.cjs", "y"), b: f };`, exports: ["a"] },
{ name: "require after a scan stop records nothing", src: `module.exports = { lit: 7, a: require("./x.cjs") };`, exports: [] },
// the SPREAD path genuinely continues past a `,` after the require
{ name: "spread require continues", src: `const v=1;\nmodule.exports = { ...require("./x.cjs"), a: v };`, exports: ["a"], reexports: ["./x.cjs"] },
{ name: "spread require member stops", src: `function f(){}\nmodule.exports = { ...require("./x.cjs").foo, a: f };`, exports: [], reexports: ["./x.cjs"] },
{ name: "spread require call stops", src: `function f(){}\nmodule.exports = { ...require("./x.cjs")(), a: f };`, exports: [], reexports: ["./x.cjs"] },
{ name: "spread require last", src: `module.exports = { ...require("./x.cjs") };`, exports: [], reexports: ["./x.cjs"] },
{ name: "two table reexports", src: `module.exports = { ...require("./x.cjs"), b: require("./y.cjs") };`, exports: ["b"], reexports: ["./x.cjs", "./y.cjs"] },
// ── exports.NAME / ['string'] assignments: position-blind ────────────
{ name: "exports dot", src: `exports.a = 1;`, exports: ["a"] },
{ name: "module.exports dot", src: `module.exports.a = 1;`, exports: ["a"] },
{ name: "exports bracket", src: `exports["a-b"] = 1;`, exports: ["a-b"] },
{ name: "module.exports bracket", src: `module.exports["a-b"] = 1;`, exports: ["a-b"] },
{ name: "dead branch counts", src: `if (0) { exports.a = 1; }`, exports: ["a"] },
{ name: "nested function counts", src: `function f() { exports.a = 1; }`, exports: ["a"] },
{ name: "shadowed exports still counts", src: `function f(exports) { exports.a = 1; }\nf({});`, exports: ["a"] },
{ name: "shadowed module still counts", src: `function f(module) { module.exports.a = 1; }\nf({exports:{}});`, exports: ["a"] },
{ name: "template bracket misses", src: "exports[`a`] = 1;", exports: [] },
{ name: "escaped name misses", src: `exports.\\u0061b = 1;`, exports: [] },
// mid-name escapes break the run before merve ever sees the `=`, so the
// dot-assign pattern misses ENTIRELY — no prefix here (probed)
{ name: "mid-escaped dot name misses entirely", src: `exports.a\\u0062c = 1;`, exports: [] },
{ name: "compound assignment misses", src: `exports.a = 0; exports.b += 1;`, exports: ["a"] },
{ name: "logical assignment misses", src: `exports.a ||= 1;`, exports: [] },
{ name: "chained assignment takes both", src: `exports.a = exports.b = 1;`, exports: ["a", "b"] },
{ name: "reserved words are NOT filtered", src: `exports.let = 1;\nexports.static = 2;\nexports.package = 3;\nexports.await = 4;\nexports.async = 5;`, exports: ["async", "await", "let", "package", "static"] },
// ── module.exports = require(...): leading-require reexports ─────────
{ name: "bare require reexport", src: `module.exports = require("./x.cjs");`, exports: [], reexports: ["./x.cjs"] },
{ name: "require reexport without semicolon", src: `module.exports = require("./x.cjs")`, exports: [], reexports: ["./x.cjs"] },
// trailers are ignored once the require LEADS the right-hand side —
// cjs-module-lexer only takes the bare form for the member/|| shapes
{ name: "require.member reexport", src: `module.exports = require("./x.cjs").foo;`, exports: [], reexports: ["./x.cjs"] },
{ name: "require.member.member reexport", src: `module.exports = require("./x.cjs").foo.x;`, exports: [], reexports: ["./x.cjs"] },
{ name: "require newline member reexport", src: `module.exports = require("./x.cjs")\n.foo;`, exports: [], reexports: ["./x.cjs"] },
{ name: "require() call reexport", src: `module.exports = require("./x.cjs")();`, exports: [], reexports: ["./x.cjs"] },
{ name: "require || reexport", src: `module.exports = require("./x.cjs") || {};`, exports: [], reexports: ["./x.cjs"] },
{ name: "require ternary-lead reexport", src: `module.exports = require("./x.cjs") ? {} : {};`, exports: [], reexports: ["./x.cjs"] },
// ...but the require must LEAD: parens, comma operators, wrappers and
// ternary arms record nothing
{ name: "parenthesized require misses", src: `module.exports = (require("./x.cjs"));`, exports: [] },
{ name: "comma-expression require misses", src: `module.exports = (0, require("./x.cjs"));`, exports: [] },
{ name: "wrapped require misses", src: `function f(x){return x;}\nmodule.exports = f(require("./x.cjs"));`, exports: [] },
{ name: "ternary-arm require misses", src: `module.exports = 1 ? require("./x.cjs") : {};`, exports: [] },
{ name: "template require misses", src: "module.exports = require(`./x.cjs`);", exports: [] },
// ── clearing: every module.exports= resets reexports, names persist ──
{ name: "reexport cleared by empty table", src: `module.exports = require("./x.cjs");\nmodule.exports = { };`, exports: [] },
{ name: "last require wins", src: `module.exports = {};\nmodule.exports = require("./x.cjs");`, exports: [], reexports: ["./x.cjs"] },
{ name: "reexport plus dot add", src: `module.exports = require("./x.cjs");\nmodule.exports.extra = 1;`, exports: ["extra"], reexports: ["./x.cjs"] },
{ name: "table names accumulate across assignments", src: `function f(){}\nmodule.exports = { a: f };\nmodule.exports = { b: f };`, exports: ["a", "b"] },
{ name: "cleared table keeps its lexed names", src: `function f(){}\nmodule.exports = { a: require("./x.cjs") };\nmodule.exports = { b: f };`, exports: ["a", "b"] },
// ── Object.defineProperty descriptor shapes ───────────────────────────
{ name: "dp value", src: `Object.defineProperty(exports, "a", { value: 1 });`, exports: ["a"] },
{ name: "dp enumerable value", src: `Object.defineProperty(exports, "a", { enumerable: true, value: 1 });`, exports: ["a"] },
{ name: "dp enumerable:false misses", src: `Object.defineProperty(exports, "a", { enumerable: false, value: 1 });`, exports: [] },
{ name: "dp getter ident", src: `const x=1;\nObject.defineProperty(exports, "a", { get() { return x; } });`, exports: ["a"] },
{ name: "dp getter member", src: `const o={x:1};\nObject.defineProperty(exports, "a", { get() { return o.x; } });`, exports: ["a"] },
{ name: "dp getter element", src: `const o={x:1};\nObject.defineProperty(exports, "a", { get() { return o['x']; } });`, exports: ["a"] },
// `this` is an identifier RUN to the byte lexer — `return this.x` matches
{ name: "dp getter this.member", src: `Object.defineProperty(exports, "a", { get() { return this.x; } });`, exports: ["a"] },
{ name: "dp getter call misses", src: `const f=()=>1;\nObject.defineProperty(exports, "a", { get() { return f(); } });`, exports: [] },
{ name: "dp getter function expression", src: `const x=1;\nObject.defineProperty(exports, "a", { get: function() { return x; } });`, exports: ["a"] },
{ name: "dp getter arrow misses", src: `const x=1;\nObject.defineProperty(exports, "a", { get: () => x });`, exports: [] },
{ name: "dp getter not last misses", src: `const x=1;\nObject.defineProperty(exports, "a", { get() { return x; }, enumerable: true });`, exports: [] },
{ name: "dp enumerable getter", src: `const x=1;\nObject.defineProperty(exports, "a", { enumerable: true, get() { return x; } });`, exports: ["a"] },
{ name: "dp on module.exports", src: `Object.defineProperty(module.exports, "a", { value: 1 });`, exports: ["a"] },
{ name: "dp template name misses", src: "Object.defineProperty(exports, `a`, { value: 1 });", exports: [] },
{ name: "dp aliased defineProperty misses", src: `var __defProp = Object.defineProperty;\n__defProp(exports, "a", { value: 1 });`, exports: [] },
// NO POISONING: a non-matching defineProperty of the same name elsewhere
// does not remove the valid one (cjs-module-lexer removes it; Node keeps)
{ name: "dp invalid-then-valid keeps the name", src: `const x=1;\nObject.defineProperty(exports, "a", { configurable: true });\nObject.defineProperty(exports, "a", { enumerable: true, get(){ return x; } });`, exports: ["a"] },
{ name: "dp valid-then-invalid keeps the name", src: `Object.defineProperty(exports, "a", { enumerable: true, value: 1 });\nObject.defineProperty(exports, "a", { madeup: true });`, exports: ["a"] },
// ── the esbuild annotation and __export helper ────────────────────────
{ name: "esbuild dead annotation", src: `function a(){}\nfunction b(){}\n0 && (module.exports = { a, b });`, exports: ["a", "b"] },
{ name: "esbuild double-paren annotation misses", src: `function a(){}\n0 && (module.exports = ({ a }));`, exports: [] },
{ name: "esbuild __export(exports, table) detects nothing", src: `var __defProp = Object.defineProperty;\nvar __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); };\n__export(exports, { a: () => a, b: () => b });\nfunction a(){}\nfunction b(){}`, exports: [] },
// ── the transpiler star-reexport patterns ─────────────────────────────
{ name: "__exportStar records", src: STAR_HELPER + `__exportStar(require("./x.cjs"), exports);`, exports: [], reexports: ["./x.cjs"] },
{ name: "__export single-arg records", src: `var __export = (m) => { for (var p in m) exports[p] = m[p]; };\n__export(require("./x.cjs"));`, exports: [], reexports: ["./x.cjs"] },
{ name: "tslib member form records", src: `var tslib_1 = { __exportStar: (m, e) => e };\ntslib_1.__exportStar(require("./x.cjs"), exports);`, exports: [], reexports: ["./x.cjs"] },
{ name: "deep member chain records", src: `var a = { b: { __exportStar: (m, e) => e } };\na.b.__exportStar(require("./x.cjs"), exports);`, exports: [], reexports: ["./x.cjs"] },
{ name: "comma-wrapped tslib misses", src: `var tslib_1 = { __exportStar: (m, e) => e };\n(0, tslib_1.__exportStar)(require("./x.cjs"), exports);`, exports: [] },
{ name: "esbuild __reExport misses (require is 2nd arg)", src: `var __reExport = (t, m) => t;\n__reExport(exports, require("./x.cjs"));`, exports: [] },
{ name: "other helper names miss", src: `var __copyAll = (m, e) => e;\n__copyAll(require("./x.cjs"), exports);`, exports: [] },
{ name: "name boundary is exact", src: `var __exportStarX = (m, e) => e;\n__exportStarX(require("./x.cjs"), exports);`, exports: [] },
{ name: "nested star misses", src: `function f() {\n var __exportStar = (m, e) => e;\n __exportStar(require("./x.cjs"), exports);\n}\nf();`, exports: [] },
{ name: "braced-if star misses", src: STAR_HELPER + `if (true) { __exportStar(require("./x.cjs"), exports); }`, exports: [] },
{ name: "unbraced-if star records (brace depth 0)", src: STAR_HELPER + `if (true) __exportStar(require("./x.cjs"), exports);`, exports: [], reexports: ["./x.cjs"] },
// the byte seam: NAME(require with no trivia — one space kills it
{ name: "space after ( kills the star", src: STAR_HELPER + `__exportStar( require("./x.cjs"), exports);`, exports: [] },
{ name: "comment after ( kills the star", src: STAR_HELPER + `__exportStar(/*c*/require("./x.cjs"), exports);`, exports: [] },
{ name: "space before ( kills the star", src: STAR_HELPER + `__exportStar (require("./x.cjs"), exports);`, exports: [] },
{ name: "space inside the require call is fine", src: STAR_HELPER + `__exportStar(require( "./x.cjs" ), exports);`, exports: [], reexports: ["./x.cjs"] },
{ name: "space between require and its ( is fine", src: STAR_HELPER + `__exportStar(require ("./x.cjs"), exports);`, exports: [], reexports: ["./x.cjs"] },
{ name: "star arg trailer is fine", src: STAR_HELPER + `__exportStar(require("./x.cjs").constructor ? require("./x.cjs") : {}, exports);`, exports: [], reexports: ["./x.cjs"] },
{ name: "star result assigned records", src: STAR_HELPER + `var r = __exportStar(require("./x.cjs"), exports);`, exports: [], reexports: ["./x.cjs"] },
{ name: "star inside exports.dot assignment records", src: `var tslib_1 = { __exportStar: (m, e) => e };\nexports.z = tslib_1.__exportStar(require("./x.cjs"), exports);`, exports: ["z"], reexports: ["./x.cjs"] },
{ name: "two stars record both", src: STAR_HELPER + `__exportStar(require("./x.cjs"), exports);\n__exportStar(require("./y.cjs"), exports);`, exports: [], reexports: ["./x.cjs", "./y.cjs"] },
// stars join the clearing list at their own position
{ name: "star cleared by later assignment", src: STAR_HELPER + `__exportStar(require("./x.cjs"), exports);\nmodule.exports = {};`, exports: [] },
{ name: "star after clearing survives", src: `module.exports = {};\nvar __exportStar = (m, e) => e;\n__exportStar(require("./x.cjs"), module.exports);`, exports: [], reexports: ["./x.cjs"] },
{ name: "star cleared by require assignment", src: STAR_HELPER + `__exportStar(require("./x.cjs"), exports);\nmodule.exports = require("./y.cjs");`, exports: [], reexports: ["./y.cjs"] },
{ name: "star after require assignment joins it", src: STAR_HELPER + `module.exports = require("./y.cjs");\n__exportStar(require("./x.cjs"), module.exports);`, exports: [], reexports: ["./y.cjs", "./x.cjs"] },
// ── the Babel copy loop ───────────────────────────────────────────────
{ name: "babel loop records", src: `var _d = require("./x.cjs");\n` + BABEL_LOOP("_d"), exports: [], reexports: ["./x.cjs"] },
{ name: "babel const declarator records", src: `function _interopRequireWildcard(m){return m;}\nconst _d = _interopRequireWildcard(require("./x.cjs"));\n` + BABEL_LOOP("_d"), exports: [], reexports: ["./x.cjs"] },
{ name: "babel wildcard helper records", src: `function _interopRequireWildcard(m){return m;}\nvar _d = _interopRequireWildcard(require("./x.cjs"));\n` + BABEL_LOOP("_d"), exports: [], reexports: ["./x.cjs"] },
{ name: "babel hasOwnProperty guard records", src: `var _d = require("./x.cjs");\nObject.keys(_d).forEach(function (key) {\n if (key === "default" || key === "__esModule") return;\n if (Object.prototype.hasOwnProperty.call(exports, key)) return;\n exports[key] = _d[key];\n});`, exports: [], reexports: ["./x.cjs"] },
{ name: "babel !== default form records", src: `var _d = require("./x.cjs");\nObject.keys(_d).forEach(function (key) {\n if (key !== "default") exports[key] = _d[key];\n});`, exports: [], reexports: ["./x.cjs"] },
{ name: "babel defineProperty copy records", src: `var _d = require("./x.cjs");\nObject.keys(_d).forEach(function (key) {\n if (key === "default" || key === "__esModule") return;\n Object.defineProperty(exports, key, { enumerable: true, get: function () { return _d[key]; } });\n});`, exports: [], reexports: ["./x.cjs"] },
{ name: "babel var-list require-first records", src: `var _d = require("./x.cjs"), x = 1;\n` + BABEL_LOOP("_d"), exports: [], reexports: ["./x.cjs"] },
{ name: "babel var-list require-second misses", src: `var x = 1, _d = require("./x.cjs");\n` + BABEL_LOOP("_d"), exports: [] },
{ name: "babel arrow callback misses", src: `var _d = require("./x.cjs");\nObject.keys(_d).forEach((key) => {\n if (key === "default" || key === "__esModule") return;\n exports[key] = _d[key];\n});`, exports: [] },
{ name: "babel named callback misses", src: `var _d = require("./x.cjs");\nObject.keys(_d).forEach(function copy(key) {\n if (key === "default" || key === "__esModule") return;\n exports[key] = _d[key];\n});`, exports: [] },
{ name: "babel filterless loop misses", src: `var _d = require("./x.cjs");\nObject.keys(_d).forEach(function (key) {\n exports[key] = _d[key];\n});`, exports: [] },
{ name: "babel braced return misses", src: `var _d = require("./x.cjs");\nObject.keys(_d).forEach(function (key) {\n if (key === "default" || key === "__esModule") { return; }\n exports[key] = _d[key];\n});`, exports: [] },
{ name: "babel reordered filter misses", src: `var _d = require("./x.cjs");\nObject.keys(_d).forEach(function (key) {\n if (key === "__esModule" || key === "default") return;\n exports[key] = _d[key];\n});`, exports: [] },
{ name: "babel unlinked var misses", src: `var real = require("./x.cjs");\nvar _d = real;\n` + BABEL_LOOP("_d"), exports: [] },
{ name: "babel nested loop misses", src: `function go() {\n var _d = require("./x.cjs");\n ` + BABEL_LOOP("_d") + `\n}\ngo();`, exports: [] },
{ name: "babel member wildcard misses", src: `var tslib_1 = { _interopRequireWildcard: (m) => m };\nvar _d = tslib_1._interopRequireWildcard(require("./x.cjs"));\n` + BABEL_LOOP("_d"), exports: [] },
{ name: "babel wildcard-paren space misses", src: `function _interopRequireWildcard(m){return m;}\nvar _d = _interopRequireWildcard( require("./x.cjs"));\n` + BABEL_LOOP("_d"), exports: [] },
{ name: "babel loop cleared by later assignment", src: `var _d = require("./x.cjs");\n` + BABEL_LOOP("_d") + `\nmodule.exports = {};`, exports: [] },
// the reexport's clearing position is the LOOP's, not the var's
{ name: "babel var before clearing, loop after: survives", src: `var _d = require("./x.cjs");\nmodule.exports = {};\nObject.keys(_d).forEach(function (key) {\n if (key === "default" || key === "__esModule") return;\n module.exports[key] = _d[key];\n});`, exports: [], reexports: ["./x.cjs"] },
// the full tsc emits, as shipped by packages
{ name: "tsc __exportStar preamble", src: `"use strict";\nvar __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) exports[p] = m[p]; };\nObject.defineProperty(exports, "__esModule", { value: true });\n__exportStar(require("./x.cjs"), exports);`, exports: ["__esModule"], reexports: ["./x.cjs"] },
{ name: "tsc __createBinding star", src: `"use strict";\nvar __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; }));\nvar __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); };\nObject.defineProperty(exports, "__esModule", { value: true });\n__exportStar(require("./x.cjs"), exports);`, exports: ["__esModule"], reexports: ["./x.cjs"] },
];
import { cjsLexerCases as cases } from "./cjs-lexer-cases.js";
describe("cjs-lexer answers Node's vendored lexer, shape by shape", () => {
test.for(cases.map((c) => [c.name, c] as const))("%s", ([, c]) => {
@@ -8744,6 +8744,12 @@
],
"diags": []
},
"<repo>/tests/corpus/optional-string-index/main.ts": {
"order": [
"<repo>/tests/corpus/optional-string-index/main.ts"
],
"diags": []
},
"<repo>/tests/corpus/static-field-updates/main.ts": {
"order": [
"<repo>/tests/corpus/static-field-updates/counter.ts",
-4
View File
@@ -8,11 +8,7 @@ import { spawnSync } from "node:child_process";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const ALLOWED_TYPESCRIPT5_IMPORTS = new Set([
"packages/compiler/src/frontend/cjs-lexer.ts",
"packages/compiler/src/frontend/lowering/lower-comptime.ts",
// Bundled factory normalization shares npm-static-rewrite's syntactic TS5 island.
"packages/compiler/src/frontend/npm-static-bundled-cjs.ts",
"packages/compiler/src/frontend/npm-static-rewrite.ts",
"packages/compiler/src/frontend/npm.ts",
"packages/compiler/src/frontend/ts7/world-check.ts",
"packages/compiler/src/frontend/ts7/source-parser.test.ts",
@@ -0,0 +1,76 @@
function show(value: string | undefined): void {
console.log(value === undefined ? "missing" : JSON.stringify(value));
}
function at(text: string, index: number): string | undefined {
return text[index];
}
const samples = ["", "abc", "a\0b", "é工作"];
const indices = [-Infinity, -2, -1, -0, 0, 0.1, 0.9, 1, 1.1, 2, 3, 4, 100, 1e100, Infinity, NaN];
for (const text of samples) {
console.log("sample", JSON.stringify(text));
for (const index of indices) show(at(text, index));
}
// Surrogate-splitting values retain the runtime's documented U+FFFD
// boundary. These probes test UTF-16 bounds without splitting a pair.
show(at("😀x", 2));
show(at("😀x", 3));
show(at("😀x", 0.5));
let text = "before";
let events = "";
function receiver(): string {
events += "receiver;";
return text;
}
function key(index: number): number {
events += "key;";
text = "after";
return index;
}
show(receiver()[key(0)]);
console.log(events, text);
text = "before";
events = "";
show(text[key(0)]);
console.log(events, text);
events = "";
show(receiver()[key(NaN)]);
console.log(events);
function throwingReceiver(): string { events += "throw receiver;"; throw new Error("receiver"); }
function throwingKey(): number { events += "throw key;"; throw new Error("key"); }
events = "";
try { show(throwingReceiver()[key(0)]); }
catch (error) { console.log((error as Error).message, events); }
events = "";
try { show(receiver()[throwingKey()]); }
catch (error) { console.log((error as Error).message, events); }
function fallback(): string { events += "fallback;"; return "none"; }
events = "";
console.log(at("yes", 0) ?? fallback());
console.log(at("yes", -1) ?? fallback());
console.log(events);
function captured(text: string): () => string | undefined {
let index = 0;
return () => text[index++];
}
const next = captured("xy");
show(next()); show(next()); show(next());
class Source {
reads = 0;
get text(): string { this.reads++; return "get"; }
}
const source = new Source();
show(source.text[1]);
show(source.text[100]);
console.log("reads", source.reads);
const retained = [at("abc", 1), at("abc", 100), at("abc", -1)];
for (const value of retained) show(value);
console.log("done");
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}
+48
View File
@@ -0,0 +1,48 @@
import { readFileSync, writeFileSync } from "node:fs";
import { Ts7SourceParser } from "../../../packages/compiler/src/frontend/ts7/source-parser.js";
import { cjsLexedExportsOfFile, cjsVisibleNames } from "../../../packages/compiler/src/frontend/cjs-syntax.js";
import { rewriteBundlerCjsSyntax } from "../../../packages/compiler/src/frontend/npm-static-rewrite-syntax.js";
import { helperTokens } from "../../../packages/compiler/src/frontend/helper-tokens.js";
interface Request {
lex: { name: string; src: string }[];
rewrite: { name: string; source: string; stamped: string[]; stars: { specifier: string; names: string[] }[] }[];
tokens: string[];
}
export function runCjsClient(parser: Ts7SourceParser, input: string, output: string): void {
const request = JSON.parse(readFileSync(input, "utf8")) as Request;
const lex = request.lex.map((item) => {
const result = cjsLexedExportsOfFile(parser.parse("module.cjs", item.src, "js"));
return { name: item.name, exports: [...result.exports].sort(), reexports: result.reexports };
});
const rewrite = request.rewrite.map((item) => {
const stars = new Map<string, Set<string>>();
for (const entry of item.stars) stars.set(entry.specifier, new Set(entry.names));
const stamped = new Set(item.stamped);
const calls: string[] = [];
const file = parser.parse("bundle.js", item.source, "js");
const original = cjsLexedExportsOfFile(file);
const result = rewriteBundlerCjsSyntax(file, "/virtual/bundle.js", {
requireTargetEsModuleStamped: (from, spec) => { calls.push("stamp:" + from + ":" + spec); return stamped.has(spec); },
starTargetNames: (from, spec) => { calls.push("star:" + from + ":" + spec); return stars.get(spec) ?? new Set<string>(); },
});
const rewritten = typeof result === "string" ? cjsLexedExportsOfFile(parser.parse("bundle.js", result, "js")) : original;
return {
name: item.name, result, calls,
original: { exports: [...original.exports].sort(), reexports: original.reexports },
rewritten: { exports: [...rewritten.exports].sort(), reexports: rewritten.reexports },
};
});
const graph = new Map<string, string>([
["entry", 'exports.own = 1; module.exports = require("middle");'],
["middle", 'exports.middle = 1; module.exports = require("entry"); __exportStar(require("leaf"), exports);'],
["leaf", 'exports.leaf = 1; __exportStar(require("missing"), exports);'],
]);
const names = cjsVisibleNames("entry",
(name) => cjsLexedExportsOfFile(parser.parse(name + ".cjs", graph.get(name)!, "js")),
(_from, spec) => graph.has(spec) ? spec : null,
);
const tokens = request.tokens.map((source) => helperTokens(source));
writeFileSync(output, JSON.stringify({ lex, rewrite, names: [...names].sort(), tokens }));
}
+7
View File
@@ -0,0 +1,7 @@
import { Ts7Api } from "../../../packages/compiler/src/frontend/ts7/rpc-api.js";
import { Ts7SourceParser } from "../../../packages/compiler/src/frontend/ts7/source-parser.js";
import { runCjsClient } from "./cjs-client-cases.js";
const parser = new Ts7SourceParser((options) => new Ts7Api(options));
try { runCjsClient(parser, process.argv[3]!, process.argv[4]!); }
finally { parser.close(); }
+7
View File
@@ -0,0 +1,7 @@
import { createNativeTs7Api } from "../../../packages/compiler/src/frontend/ts7/native-api.js";
import { Ts7SourceParser } from "../../../packages/compiler/src/frontend/ts7/source-parser.js";
import { runCjsClient } from "./cjs-client-cases.js";
const parser = new Ts7SourceParser((options) => createNativeTs7Api({ ...options, executable: process.argv[2]! }));
try { runCjsClient(parser, process.argv[3]!, process.argv[4]!); }
finally { parser.close(); }
@@ -0,0 +1,98 @@
import { execFileSync, spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { analyze, compile } from "@scriptc/compiler";
import { expect, test } from "vitest";
import { ts7Executable } from "../../packages/compiler/src/frontend/ts7/rpc-api.js";
import { cjsLexerCases } from "../../packages/compiler/test/cjs-lexer-cases.js";
const root = join(import.meta.dirname, "../..");
const entry = join(root, "tests/fixtures/self-hosting/cjs-client.ts");
const oracle = join(root, "tests/fixtures/self-hosting/cjs-client-node.ts");
const nativeSources = join(root, "packages/compiler/native");
const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp";
const sanitize = process.env["SCRIPTC_SAN"] === "1";
function input() {
const fixture = readFileSync(join(root, "tests/fixtures/npm-static/node_modules/bundled-function/index.js"), "utf8");
const rewrite = [
{ name: "bundled function", source: fixture },
{ name: "quoted helper", source: fixture.replaceAll('"default"', "'de\\u0066ault'") },
{ name: "helper drift", source: fixture.replace("return to;", 'console.log("effect"); return to;') },
{ name: "return newline", source: fixture.replace("return to;", "return\nto;") },
{ name: "namespace escape", source: fixture + "\nconsole.log(first === second);" },
{ name: "factory capture", source: fixture.replace("() => /a+/g", "() => module.exports") },
{ name: "plain", source: "exports.value = 1;" },
{ name: "deep", source: fixture + "\nconst deep = " + "value + ".repeat(1800) + "value;" },
].map((item) => ({ ...item, stamped: [] as string[], stars: [] as {specifier: string; names: string[]}[] }));
for (const name of ["gtable", "gtstar", "gtbarrel", "gtdefine", "gtwrap"]) {
const source = readFileSync(join(root, "tests/fixtures/npm/node_modules", name, "dist/index.js"), "utf8");
rewrite.push({ name, source, stamped: ["gtable"], stars: [
{ specifier: "./leaf.js", names: ["leaf", "default", "__esModule"] },
{ specifier: "./extra.js", names: ["extra", "default", "__esModule"] },
{ specifier: "./a.js", names: ["a", "shared", "default", "__esModule"] },
{ specifier: "./b.js", names: ["b", "shared", "default", "__esModule"] },
] });
}
return {
lex: cjsLexerCases,
rewrite,
tokens: ['return to;', 'return\nto;', 'return/*\n*/to;', '"default"', "'de\\u0066ault'", "/*", "`template`", "a ??= b", "a ||= b"],
};
}
for (const backend of ["c", "llvm"] as const) {
test(`CommonJS detection and package rewrites run without Node (${backend})`, async () => {
const directory = mkdtempSync(join(tempRoot, "scriptc-commonjs-native-"));
try {
const object = join(directory, "process.o");
execFileSync("clang", ["-std=c11", "-Wall", "-Wextra", "-Werror", ...(sanitize ? ["-fsanitize=address"] : []),
"-c", join(nativeSources, "ts7-process.c"), "-o", object]);
const profile = join(directory, "ffi.json");
writeFileSync(profile, JSON.stringify({
...JSON.parse(readFileSync(join(nativeSources, "ts7-process.ffi.json"), "utf8")), libraries: [object],
}));
const { coverage } = analyze(entry, { dynamic: false, ffiProfilePath: profile });
expect(coverage.preflightFailed, JSON.stringify(coverage.diagnostics)).toBe(false);
expect(coverage.diagnostics).toEqual([]);
expect(coverage.stats.statementsFailed).toBe(0);
expect(coverage.stats.statementsIsland).toBe(0);
expect(coverage.stats.functionsSkipped).toBe(0);
const built = await compile(entry, {
backend, dynamic: false, optimization: "dev", sanitize, ffiProfilePath: profile,
outDir: directory, outPath: join(directory, process.platform === "win32" ? "parser.exe" : "parser"),
});
if (!built.ok) throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
const request = join(directory, "request.json");
writeFileSync(request, JSON.stringify(input()));
const expected = join(directory, "node.json");
const node = spawnSync(process.execPath, ["--import", "tsx", oracle, ts7Executable(), request, expected], { encoding: "utf8", timeout: 45_000 });
expect(node.error, node.stderr).toBeUndefined();
expect(node.status, node.stderr).toBe(0);
expect(node.stdout).toBe("");
expect(node.stderr).toBe("");
const report = join(directory, "native.json");
const run = spawnSync(built.binaryPath, [ts7Executable(), request, report], { encoding: "utf8", timeout: 45_000 });
expect(run.error, run.stderr).toBeUndefined();
expect(run.signal, run.stderr).toBeNull();
expect(run.status, run.stderr).toBe(0);
expect(run.stdout).toBe(node.stdout);
expect(run.stderr).toBe(node.stderr);
const result = JSON.parse(readFileSync(report, "utf8"));
expect(result).toEqual(JSON.parse(readFileSync(expected, "utf8")));
expect(result.names).toEqual(["leaf", "middle", "own"]);
for (let i = 0; i < cjsLexerCases.length; i++) {
expect(result.lex[i].exports, cjsLexerCases[i]!.name).toEqual(cjsLexerCases[i]!.exports);
expect(result.lex[i].reexports, cjsLexerCases[i]!.name).toEqual(cjsLexerCases[i]!.reexports ?? []);
}
expect(typeof result.rewrite[0].result).toBe("string");
expect(result.rewrite[0].result).not.toContain("WeakMap");
expect(result.rewrite[2].result.degrade).toContain("__toESM");
expect(result.rewrite[3].result.degrade).toContain("__toESM");
expect(result.tokens[0]).not.toBe(result.tokens[1]);
expect(result.tokens[3]).toBe(result.tokens[4]);
} finally { rmSync(directory, { recursive: true, force: true }); }
});
}