mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
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:
co-authored by
Claude Opus 5.5
parent
0237d70df0
commit
f793009ee5
@@ -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 {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/tsc-builtin-interop/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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"));
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export default function suffix(): string;
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.default = suffix;
|
||||
function suffix() {
|
||||
return "!";
|
||||
}
|
||||
Generated
Vendored
+5
@@ -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;
|
||||
Generated
Vendored
+43
@@ -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)();
|
||||
}
|
||||
Generated
Vendored
+7
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user