Unwrap tsc's interop helpers around builtin modules in packages

tsc wraps every CommonJS import in its helpers: `const events_1 =
__importDefault(require("events"))`, `const path =
__importStar(require("path"))`. cjs-tsc-interop.ts reads such bindings of
program modules as plain require bindings, so the helper declarations go
dead, but a builtin module is not a program module: its binding kept the
helpers live, and their probe of the module's top-level `this`
(`(this && this.__importDefault) ||`) failed at load ("property access
through top-level 'this'"). That is the shape of hundreds of tsc-compiled
files in a typical dependency tree.

A builtin carries no `__esModule`, so `events_1.default` is the module and
the namespace copy reads the module's members. When every reference has
that form, a package-file stage now respells the binding as the plain
require() of the builtin and `x.default` as `x`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-21 08:11:47 -07:00
co-authored by Claude Opus 5.5
parent 0237d70df0
commit f793009ee5
11 changed files with 266 additions and 0 deletions
@@ -0,0 +1,71 @@
import { spawnSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, expect, test } from "vitest";
import { nodeFrontendServices } from "./services-node.js";
const dirs: string[] = [];
afterAll(() => {
for (const dir of dirs) rmSync(dir, { recursive: true, force: true });
});
function rewrite(source: string): string | null {
return nodeFrontendServices().tscBuiltinImports(source, "/pkg/index.js");
}
const helpers = `"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
`;
function expectSameBehavior(source: string): string {
const result = rewrite(source);
expect(typeof result).toBe("string");
const dir = mkdtempSync(join(tmpdir(), "scriptc-tsc-builtin-imports-"));
dirs.push(dir);
writeFileSync(join(dir, "original.cjs"), source);
writeFileSync(join(dir, "rewritten.cjs"), result!);
const run = (file: string) =>
spawnSync(process.execPath, [join(dir, file)], { encoding: "utf8" });
const oracle = run("original.cjs");
expect(oracle.status).toBe(0);
expect(run("rewritten.cjs").stdout).toBe(oracle.stdout);
return result!;
}
test("builtin modules behind tsc's interop helpers become plain requires", () => {
const out = expectSameBehavior(`${helpers}const events_1 = __importDefault(require("events"));
const path = __importStar(require("node:path"));
class Bus extends events_1.default.EventEmitter {}
console.log(new Bus() instanceof events_1.default, path.basename("/a/b.txt"), typeof path.join);
`);
expect(out).toContain('const events_1 = require("events");');
expect(out).toContain("class Bus extends events_1.EventEmitter {}");
expect(out).toContain('const path = require("node:path");');
});
test("other references, program modules and default reads of a namespace keep their text", () => {
// The binding itself as a value.
expect(
rewrite(
`${helpers}const events_1 = __importDefault(require("events"));\nconsole.log(events_1);\n`,
),
).toBeNull();
// A program module (cjs-tsc-interop.ts owns it).
expect(
rewrite(`${helpers}const m = __importDefault(require("./m"));\nm.default();\n`),
).toBeNull();
// `ns.default` reads the namespace copy's own default.
expect(
rewrite(`${helpers}const p = __importStar(require("path"));\nconsole.log(p.default === p);\n`),
).toBeNull();
});
@@ -0,0 +1,106 @@
/* tsc wraps every CommonJS import in its interop helpers:
*
* const events_1 = __importDefault(require("events")); // events_1.default.X
* const path = __importStar(require("path")); // path.basename
*
* For program modules cjs-tsc-interop.ts reads such a binding as a plain
* require binding, and the helper declarations (`var __importDefault =
* (this && this.__importDefault) || …`) go dead. A builtin module is not a
* program module, so its binding kept the helpers live, and their probe of
* the module's top-level `this` has no lowering. A builtin carries no
* `__esModule`: `events_1.default` is the module and the namespace copy
* reads the module's members. When every reference has that form
* (`x.default…` for the default helper, `x.<member>` other than `default`
* for the namespace helper), the binding becomes the plain `require()` of
* the builtin and `x.default` becomes `x`. */
import * as ts from "./ts7/syntax.js";
import { applyEdits, unparen, type Edit } from "./npm-static-bundled-chunks.js";
import { canonicalBuiltinModule } from "./builtin-modules.js";
/** True when the text may hold such a binding (a cheap probe). */
export function mayHoldTscBuiltinImport(source: string): boolean {
return /__import(?:Default|Star)\(require\(/.test(source);
}
/** `__importDefault(require("<builtin>"))` / `__importStar(…)`: the helper
* and builtin spec, or null. */
function builtinInterop(init: ts.Expression): { helper: string; spec: string } | null {
const e = unparen(init);
if (!ts.isCallExpression(e) || !ts.isIdentifier(e.expression) || e.arguments.nodes.length !== 1)
return null;
const helper = e.expression.text;
if (helper !== "__importDefault" && helper !== "__importStar") return null;
const req = unparen(e.arguments.nodes[0]!);
if (
!ts.isCallExpression(req) ||
!ts.isIdentifier(req.expression) ||
req.expression.text !== "require" ||
req.arguments.nodes.length !== 1 ||
!ts.isStringLiteral(req.arguments.nodes[0]!)
)
return null;
const spec = (req.arguments.nodes[0] as ts.StringLiteral).text;
return canonicalBuiltinModule(spec) === null ? null : { helper, spec };
}
/** The `.default` accesses to remove when every reference to `name` (other
* than its declaration) has the helper's form, or null. */
function interopReferences(
sf: ts.SourceFile,
decl: ts.VariableDeclaration,
name: string,
helper: string,
): ts.PropertyAccessExpression[] | null {
const defaults: ts.PropertyAccessExpression[] = [];
let ok = true;
ts.walkPreorder(sf, (n) => {
if (!ok) return "stop";
if (!ts.isIdentifier(n) || n.text !== name || n === decl.name) return undefined;
const parent = n.parent;
if (
parent === undefined ||
!ts.isPropertyAccessExpression(parent) ||
parent.expression !== n ||
parent.questionDotToken !== undefined
) {
ok = false;
return "stop";
}
const member = parent.name.text;
if (helper === "__importDefault") {
if (member !== "default") ok = false;
else defaults.push(parent);
} else if (member === "default") {
ok = false;
}
return undefined;
});
return ok ? defaults : null;
}
/** The module text with its builtin interop bindings unwrapped, or null. */
export function unwrapTscBuiltinImports(sf: ts.SourceFile): string | null {
if (ts.isExternalModule(sf) && !sf.fileName.endsWith(".cjs")) return null;
const edits: Edit[] = [];
const stmts = sf.statements.nodes;
// Builtin modules through the interop helpers.
for (const stmt of stmts) {
if (!ts.isVariableStatement(stmt)) continue;
for (const d of stmt.declarationList.declarations.nodes) {
if (!ts.isIdentifier(d.name) || d.initializer === undefined) continue;
const interop = builtinInterop(d.initializer);
if (interop === null) continue;
const defaults = interopReferences(sf, d, d.name.text, interop.helper);
if (defaults === null) continue;
edits.push({
start: d.initializer.getStart(sf),
end: d.initializer.getEnd(),
text: `require(${JSON.stringify(interop.spec)})`,
});
for (const access of defaults)
edits.push({ start: access.getStart(sf), end: access.getEnd(), text: d.name.text });
}
}
if (edits.length === 0) return null;
return applyEdits(sf.text, 0, sf.text.length, edits);
}
@@ -455,6 +455,9 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
// Polyfill packages that ARE a Node builtin compile as the builtin.
const aliased = original === null ? null : services.builtinAliasRequires(original, path);
if (aliased !== null) original = aliased;
// tsc's interop helpers around builtin modules.
const tscHelpers = original === null ? null : services.tscBuiltinImports(original, path);
if (tscHelpers !== null) original = tscHelpers;
// A module exporting one of its functions reaches its members
// through that function.
const fnExports = original === null ? null : services.cjsFunctionExports(original, path);
@@ -469,6 +472,7 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
if (exportsVar !== null) original = exportsVar;
const respelled =
split !== null ||
tscHelpers !== null ||
esm !== null ||
aliased !== null ||
fnExports !== null ||
@@ -13,6 +13,7 @@ import {
rewriteBuiltinAliasRequires,
} from "./npm-static-builtin-aliases.js";
import { mayHoldEnumIife, rewriteEnumIifes } from "./npm-static-enum-iife.js";
import { mayHoldTscBuiltinImport, unwrapTscBuiltinImports } from "./npm-static-tsc-helpers.js";
import { builtinPackageAliasFor } from "./package-shims.js";
import * as syntax from "./npm-static-declaration-syntax.js";
import { normalizeCjsExportsVar } from "./npm-static-cjs-exports-var.js";
@@ -106,6 +107,13 @@ export class FrontendServices {
if (!source.includes("module.exports")) return null;
return normalizeCjsFunctionExports(this.parse(path, source, "js"));
}
/** A package module with tsc's interop bindings of builtin modules
* unwrapped to plain requires (null: none). */
tscBuiltinImports(source: string, path: string): string | null {
this.ensureOpen();
if (!mayHoldTscBuiltinImport(source)) return null;
return unwrapTscBuiltinImports(this.parse(path, source, "js"));
}
/** A package module with its literal-valued TypeScript enum IIFEs
* respelled as object literals (null: none). */
enumIifes(source: string, path: string): string | null {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/npm/cases/tsc-builtin-interop/main.ts"
],
"diags": []
}
+9
View File
@@ -0,0 +1,9 @@
// A tsc-compiled dependency wraps builtin modules in its interop helpers
// (`const events_1 = __importDefault(require("events"))`, then
// `class Bus extends events_1.default.EventEmitter`; `const path =
// __importStar(require("path"))`).
import { Bus, where } from "gttscinterop";
const bus = new Bus();
bus.on("ping", (name: string) => console.log("got", name));
console.log(bus.ping("a"), where("/x/y.txt"));
@@ -0,0 +1 @@
export default function suffix(): string;
@@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = suffix;
function suffix() {
return "!";
}
@@ -0,0 +1,5 @@
import events from "events";
export declare class Bus extends events.EventEmitter {
ping(name: string): string;
}
export declare function where(p: string): string;
@@ -0,0 +1,43 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Bus = void 0;
exports.where = where;
const events_1 = __importDefault(require("events"));
const path = __importStar(require("path"));
const helper_1 = __importDefault(require("./helper"));
class Bus extends events_1.default.EventEmitter {
ping(name) {
this.emit("ping", name);
return `pong ${name}${(0, helper_1.default)()}`;
}
}
exports.Bus = Bus;
function where(p) {
return path.basename(p) + (0, helper_1.default)();
}
@@ -0,0 +1,7 @@
{
"name": "gttscinterop",
"version": "1.0.0",
"description": "real tsc 5.6 output (module commonjs, esModuleInterop) of a source that default-imports `events` and namespace-imports `path`: the builtins are wrapped in tsc's __importDefault/__importStar helpers, which probe the module's top-level `this`",
"main": "lib/index.js",
"types": "lib/index.d.ts"
}