diff --git a/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts b/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts index c5b5568214..fe2cc9f7d8 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts @@ -365,3 +365,103 @@ test("a minified initializer that ends with its class still hoists it", () => { ); expect(lowered).toContain("class Counter{"); }); + +test("an empty namespace and names a module does not export read undefined", () => { + const lowered = expectSameBehavior(`"use strict"; +${helpers}${lazyModule} +var manifest_exports = {}; +var init_manifest = __esm({ "src/manifest.ts"() { console.log("manifest init"); } }); +function read() { + const manifest = (init_manifest(), __toCommonJS(manifest_exports)); + const lazy = (init_lazy(), __toCommonJS(lazy_exports)); + return [manifest.version, lazy.missing, lazy.__esModule, lazy.VALUE].join(" "); +} +console.log(read()); +`); + expect(lowered).not.toContain("manifest_exports"); +}); + +test("a star re-export of a types-only module into the entry runs its initializer", () => { + const lowered = expectSameBehavior(`"use strict"; +${helpers} +var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default")); +var index_exports = {}; +__export(index_exports, { run: () => run }); +module.exports = __toCommonJS(index_exports); +var manifest_exports = {}; +var init_manifest = __esm({ "src/manifest.ts"() { console.log("manifest init"); } }); +__reExport(index_exports, (init_manifest(), __toCommonJS(manifest_exports)), module.exports); +function run() { return "ran"; } +console.log(Object.keys(module.exports).join(","), module.exports.run()); +`); + expect(lowered).not.toContain("module.exports);"); +}); + +test("the bundle's own entry namespace answers member reads", () => { + expectSameBehavior(`"use strict"; +${helpers} +var index_exports = {}; +__export(index_exports, { KIND: () => KIND, late: () => late }); +module.exports = __toCommonJS(index_exports); +var KIND; +var init_index = __esm({ "src/index.ts"() { KIND = "lambda"; } }); +function late() { + const index = (init_index(), __toCommonJS(index_exports)); + return index.KIND + " " + index.missing; +} +init_index(); +console.log(late()); +`); +}); + +const reExportingEntry = (use: string): string => `"use strict"; +${helpers} +var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default")); +var other_exports = {}; +__export(other_exports, { EXTRA: () => EXTRA }); +var EXTRA; +var init_other = __esm({ "src/other.ts"() { EXTRA = "extra"; } }); +var index_exports = {}; +__export(index_exports, { KIND: () => KIND, late: () => late }); +module.exports = __toCommonJS(index_exports); +__reExport(index_exports, (init_other(), __toCommonJS(other_exports)), module.exports); +var KIND; +var init_index = __esm({ "src/index.ts"() { KIND = "lambda"; } }); +function late() { + const index = (init_index(), __toCommonJS(index_exports)); + return ${use}; +} +init_index(); +console.log(late()); +`; + +test("an entry that star re-exports a module still answers its own names", () => { + expectSameBehavior(reExportingEntry("index.KIND")); +}); + +test("a read through the entry namespace of a name a star re-export adds keeps the package refused", () => { + expect(lower(reExportingEntry("index.KIND + ' ' + index.EXTRA"))).toEqual({ + degrade: expect.stringContaining( + "reads the namespace of a bundled ES module ('index_exports')", + ) as string, + }); +}); + +test.each([ + ["constructor", "lazy.constructor === Object"], + ["toString", "typeof lazy.toString"], + ["__proto__", "lazy.__proto__ === Object.prototype"], + ["hasOwnProperty", "typeof lazy.hasOwnProperty"], +])("a read of the inherited %s through a namespace keeps the package refused", (_, use) => { + expect( + lower(`"use strict"; +${helpers}${lazyModule} +const lazy = (init_lazy(), __toCommonJS(lazy_exports)); +console.log(${use}); +`), + ).toEqual({ + degrade: expect.stringContaining( + "reads the namespace of a bundled ES module ('lazy_exports')", + ) as string, + }); +}); diff --git a/packages/compiler/src/frontend/npm-static-bundled-esm.ts b/packages/compiler/src/frontend/npm-static-bundled-esm.ts index d74b41a1d5..d509d1945d 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-esm.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-esm.ts @@ -503,15 +503,64 @@ export function lowerBundledEsmInits( ) mainTarget = (rhs.arguments.nodes[0] as ts.Identifier).text; } - const tables = new Map(); + const tables = new Map< + string, + { stmt: ts.Statement | null; entries: [string, ts.Expression][] } + >(); + // The bundle's own entry namespace keeps its statements (the CommonJS + // rewrite builds the export table from them); its table only answers + // member reads through `__toCommonJS(main)` in the bundle's own code. + let mainTable: { stmt: ts.Statement | null; entries: [string, ts.Expression][] } | null = null; for (const stmt of sf.statements.nodes) { if (!ts.isExpressionStatement(stmt) || !ts.isCallExpression(stmt.expression)) continue; const table = exportTableOf(stmt.expression); - if (table !== null && table.target !== mainTarget) { + if (table !== null && table.target === mainTarget) { + mainTable = mainTable === null ? { stmt, entries: table.entries } : null; + } else if (table !== null) { if (tables.has(table.target)) return { degrade: ESM_SHAPE_DEGRADE }; tables.set(table.target, { stmt, entries: table.entries }); } } + // A module that exports only types keeps an empty namespace object + // (`var x_exports = {}`) with no __export call: an empty table, read + // only through `__toCommonJS(x_exports)`. + for (const [name, t] of top) { + if (tables.has(name) || name === mainTarget || writes.has(name)) continue; + const d = t.node; + if (!ts.isVariableDeclaration(d) || d.initializer === undefined) continue; + const init = unparen(d.initializer); + if (!ts.isObjectLiteralExpression(init) || init.properties.nodes.length !== 0) continue; + const uses = (refsByName.get(name) ?? []).filter((x) => x !== d.name); + if ( + uses.length > 0 && + uses.every((x) => { + const p = x.parent; + return ( + p !== undefined && + ts.isCallExpression(p) && + ts.isIdentifier(p.expression) && + helperBase(p.expression.text) === "__toCommonJS" && + p.arguments.nodes.length === 1 + ); + }) + ) + tables.set(name, { stmt: null, entries: [] }); + } + // A star re-export into the entry (`__reExport(main, ns, module.exports)`) + // adds names its export table does not list, so the entry namespace's + // missing names are unknown. + const entryReExports = + mainTarget !== null && + (refsByName.get(mainTarget) ?? []).some((x) => { + const p = x.parent; + return ( + p !== undefined && + ts.isCallExpression(p) && + ts.isIdentifier(p.expression) && + helperBase(p.expression.text) === "__reExport" && + p.arguments.nodes[0] === x + ); + }); const lowered = new Set(); /** Some binding between the use site and the top level rebinds a name * the table's getters read. */ @@ -673,11 +722,25 @@ export function lowerBundledEsmInits( } for (const r of allRefs) { - const table = tables.get(r.text); + const p = r.parent; + const isMain = + r.text === mainTarget && + mainTable !== null && + p !== undefined && + ts.isCallExpression(p) && + ts.isIdentifier(p.expression) && + helperBase(p.expression.text) === "__toCommonJS" && + p.arguments.nodes.length === 1 && + !( + p.parent !== undefined && + ts.isBinaryExpression(p.parent) && + p.parent.right === p && + ts.isPropertyAccessExpression(p.parent.left) + ); + const table = isMain ? mainTable! : tables.get(r.text); if (table === undefined) continue; if (loaderSpans.some((span) => r.getStart(sf) >= span.start && r.getEnd() <= span.end)) continue; - const p = r.parent; // The table's own declaration and __export call are handled below. if (p !== undefined && ts.isVariableDeclaration(p) && p.name === r) continue; if ( @@ -712,13 +775,33 @@ export function lowerBundledEsmInits( else break; } const values = new Map(table.entries); - const memberRead = (access: ts.Node | undefined, of: ts.Node): boolean => - access !== undefined && - ts.isPropertyAccessExpression(access) && - access.expression === of && - values.has(access.name.text) && - plainRead(access) && - (!calledWithReceiver(access) || receiverHidden(values, access.name.text)); + // A name the module does not export reads undefined on the namespace + // object (`__esModule` reads true); plain reads only, never calls. The + // namespace is an ordinary object, so a name Object.prototype answers + // (constructor, toString, __proto__) is no missing name, and neither + // is any name of an entry that star re-exports other modules. + const missing = new Set(); + const readNames = new Set(); + const memberRead = (access: ts.Node | undefined, of: ts.Node): boolean => { + if ( + access === undefined || + !ts.isPropertyAccessExpression(access) || + access.expression !== of || + !plainRead(access) + ) + return false; + const name = access.name.text; + if (!values.has(name)) { + const parent = access.parent; + if (parent !== undefined && ts.isCallExpression(parent) && parent.expression === access) + return false; + if (name in Object.prototype || (isMain && entryReExports)) return false; + missing.add(name); + return true; + } + readNames.add(name); + return !calledWithReceiver(access) || receiverHidden(values, name); + }; const holder = ns.parent; // `__reExport(main, ns, module.exports)`: a star re-export of the // module into the bundle's own exports (`export * from` in its entry). @@ -752,7 +835,7 @@ export function lowerBundledEsmInits( end: holder.parent.getEnd(), text: `${init}__export(${mainTarget}, { ${entries} });`, }); - lowered.add(r.text); + if (!isMain) lowered.add(r.text); continue; } let readable = memberRead(holder, ns); @@ -793,17 +876,22 @@ export function lowerBundledEsmInits( }; return { degrade: `its shipped JS ${why}` }; } + // The entry namespace answers only the names read here. + const entries = isMain ? table.entries.filter(([n]) => readNames.has(n)) : table.entries; // The getters read top-level bindings: nothing between the use site // and the top level may rebind one of their names. - if (shadowed(p, table.entries)) return { degrade: `its shipped JS ${why}` }; - const getters = table.entries - .map( + if (shadowed(p, entries)) return { degrade: `its shipped JS ${why}` }; + const getters = [ + ...entries.map( ([n, value]) => `get ${/^[A-Za-z_$][\w$]*$/.test(n) ? n : JSON.stringify(n)}() { return ${getterText(value)}; }`, - ) - .join(", "); + ), + ...[...missing].map( + (n) => `get ${n}() { return ${n === "__esModule" ? "true" : "void 0"}; }`, + ), + ].join(", "); edits.push({ start: p.getStart(sf), end: p.getEnd(), text: `{ ${getters} }` }); - lowered.add(r.text); + if (!isMain) lowered.add(r.text); } // A table nothing reads (its module is never required) goes too: its // names are no exports of the bundle. @@ -812,7 +900,10 @@ export function lowerBundledEsmInits( const live = (refsByName.get(target) ?? []).some((x) => { const p = x.parent; if (p !== undefined && ts.isVariableDeclaration(p) && p.name === x) return false; - return !(x.getStart(sf) >= table.stmt.getStart(sf) && x.getEnd() <= table.stmt.getEnd()); + return ( + table.stmt === null || + !(x.getStart(sf) >= table.stmt.getStart(sf) && x.getEnd() <= table.stmt.getEnd()) + ); }); if (!live) lowered.add(target); } @@ -828,7 +919,8 @@ export function lowerBundledEsmInits( writes.has(target) ) return { degrade: ESM_SHAPE_DEGRADE }; - edits.push({ start: table.stmt.getStart(sf), end: table.stmt.getEnd(), text: "" }); + if (table.stmt !== null) + edits.push({ start: table.stmt.getStart(sf), end: table.stmt.getEnd(), text: "" }); deadDeclarators.add(decl); } // Every removed declarator in one pass: declarators of one statement diff --git a/packages/compiler/src/frontend/npm-static-tsc-helpers.test.ts b/packages/compiler/src/frontend/npm-static-tsc-helpers.test.ts new file mode 100644 index 0000000000..735c9d298b --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-tsc-helpers.test.ts @@ -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(); +}); diff --git a/packages/compiler/src/frontend/npm-static-tsc-helpers.ts b/packages/compiler/src/frontend/npm-static-tsc-helpers.ts new file mode 100644 index 0000000000..fcb73be5b6 --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-tsc-helpers.ts @@ -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.` 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(""))` / `__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); +} diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index 9087c00a37..2454b35469 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -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; // Minified merged declarations of requires and the bindings read // from them become one declaration per binding. const declSplit = @@ -475,6 +478,7 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow const respelled = split !== null || declSplit !== null || + tscHelpers !== null || esm !== null || aliased !== null || fnExports !== null || diff --git a/packages/compiler/src/frontend/services.ts b/packages/compiler/src/frontend/services.ts index 689caddf89..5fe85f48c9 100644 --- a/packages/compiler/src/frontend/services.ts +++ b/packages/compiler/src/frontend/services.ts @@ -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 { mayMergeRequireDeclarations, splitRequireDeclarations, @@ -117,6 +118,13 @@ export class FrontendServices { if (!mayMergeRequireDeclarations(source)) return null; return splitRequireDeclarations(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 { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2595-bundled-entry-namespace/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2595-bundled-entry-namespace/main.ts.json new file mode 100644 index 0000000000..141e4c6385 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2595-bundled-entry-namespace/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/2595-bundled-entry-namespace/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/tsc-builtin-interop/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/tsc-builtin-interop/main.ts.json new file mode 100644 index 0000000000..cdb86be049 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/tsc-builtin-interop/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/tsc-builtin-interop/main.ts" + ], + "diags": [] +} diff --git a/tests/fixtures/npm/cases/2595-bundled-entry-namespace/main.ts b/tests/fixtures/npm/cases/2595-bundled-entry-namespace/main.ts new file mode 100644 index 0000000000..e79c9844c2 --- /dev/null +++ b/tests/fixtures/npm/cases/2595-bundled-entry-namespace/main.ts @@ -0,0 +1,7 @@ +// A bundled module requires the bundle's own entry and a module that exports +// only types: esbuild reads them as `(init_index(), __toCommonJS(index_exports))` +// and `(init_manifest(), __toCommonJS(manifest_exports))`, the second an +// empty namespace whose names all read undefined. +import { KIND, late } from "gtemptyns"; + +console.log(KIND, late()); diff --git a/tests/fixtures/npm/cases/tsc-builtin-interop/main.ts b/tests/fixtures/npm/cases/tsc-builtin-interop/main.ts new file mode 100644 index 0000000000..99bbe020a1 --- /dev/null +++ b/tests/fixtures/npm/cases/tsc-builtin-interop/main.ts @@ -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")); diff --git a/tests/fixtures/npm/cases/tsc-builtin-interop/node_modules/gttscinterop/lib/helper.d.ts b/tests/fixtures/npm/cases/tsc-builtin-interop/node_modules/gttscinterop/lib/helper.d.ts new file mode 100644 index 0000000000..cebef91199 --- /dev/null +++ b/tests/fixtures/npm/cases/tsc-builtin-interop/node_modules/gttscinterop/lib/helper.d.ts @@ -0,0 +1 @@ +export default function suffix(): string; diff --git a/tests/fixtures/npm/cases/tsc-builtin-interop/node_modules/gttscinterop/lib/helper.js b/tests/fixtures/npm/cases/tsc-builtin-interop/node_modules/gttscinterop/lib/helper.js new file mode 100644 index 0000000000..9c847c6ac5 --- /dev/null +++ b/tests/fixtures/npm/cases/tsc-builtin-interop/node_modules/gttscinterop/lib/helper.js @@ -0,0 +1,6 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = suffix; +function suffix() { + return "!"; +} diff --git a/tests/fixtures/npm/cases/tsc-builtin-interop/node_modules/gttscinterop/lib/index.d.ts b/tests/fixtures/npm/cases/tsc-builtin-interop/node_modules/gttscinterop/lib/index.d.ts new file mode 100644 index 0000000000..f185ed2580 --- /dev/null +++ b/tests/fixtures/npm/cases/tsc-builtin-interop/node_modules/gttscinterop/lib/index.d.ts @@ -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; diff --git a/tests/fixtures/npm/cases/tsc-builtin-interop/node_modules/gttscinterop/lib/index.js b/tests/fixtures/npm/cases/tsc-builtin-interop/node_modules/gttscinterop/lib/index.js new file mode 100644 index 0000000000..96450e0e54 --- /dev/null +++ b/tests/fixtures/npm/cases/tsc-builtin-interop/node_modules/gttscinterop/lib/index.js @@ -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)(); +} diff --git a/tests/fixtures/npm/cases/tsc-builtin-interop/node_modules/gttscinterop/package.json b/tests/fixtures/npm/cases/tsc-builtin-interop/node_modules/gttscinterop/package.json new file mode 100644 index 0000000000..bc83710c82 --- /dev/null +++ b/tests/fixtures/npm/cases/tsc-builtin-interop/node_modules/gttscinterop/package.json @@ -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" +} diff --git a/tests/fixtures/npm/node_modules/gtemptyns/dist/index.js b/tests/fixtures/npm/node_modules/gtemptyns/dist/index.js new file mode 100644 index 0000000000..41a9c9a401 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtemptyns/dist/index.js @@ -0,0 +1 @@ +var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __esm=(fn,res)=>function(){return fn&&(res=(0,fn[__getOwnPropNames(fn)[0]])(fn=0)),res};var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})},__copyProps=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames(from))!__hasOwnProp.call(to,key)&&key!==except&&__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:!0}),mod);var manifest_exports={};var init_manifest=__esm({"src/manifest.ts"(){}});var KIND,init_types=__esm({"src/types.ts"(){KIND="lambda"}});function late(){let index=(init_index(),__toCommonJS(index_exports)),manifest=(init_manifest(),__toCommonJS(manifest_exports));return`${index.KIND} ${manifest.version}`}var init_lazy=__esm({"src/lazy.ts"(){}});var index_exports={};__export(index_exports,{KIND:()=>KIND,late:()=>late});module.exports=__toCommonJS(index_exports);var init_index=__esm({"src/index.ts"(){init_manifest();init_types();init_lazy()}});init_index();0&&(module.exports={KIND,late}); diff --git a/tests/fixtures/npm/node_modules/gtemptyns/index.d.ts b/tests/fixtures/npm/node_modules/gtemptyns/index.d.ts new file mode 100644 index 0000000000..4ba5318a42 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtemptyns/index.d.ts @@ -0,0 +1,2 @@ +export declare const KIND: string; +export declare function late(): string; diff --git a/tests/fixtures/npm/node_modules/gtemptyns/package.json b/tests/fixtures/npm/node_modules/gtemptyns/package.json new file mode 100644 index 0000000000..383b7a0767 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtemptyns/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtemptyns", + "version": "1.0.0", + "description": "real esbuild 0.27 output (--bundle --platform=node --format=cjs --minify-whitespace --minify-syntax) of a TypeScript entry whose module requires the entry itself and a types-only module: `(init_index(), __toCommonJS(index_exports)).KIND` reads the bundle's own entry namespace, and `(init_manifest(), __toCommonJS(manifest_exports))` an empty one", + "main": "dist/index.js", + "types": "index.d.ts" +} diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 3a1942b096..1c12d184c8 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -1657,6 +1657,14 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { ]); }, 180_000); + // 2595: a bundle's own entry namespace and an empty (types-only) one, + // read through `(init_x(), __toCommonJS(x_exports))`. + test("bundled namespace reads 2595-bundled-entry-namespace byte-match Node", async () => { + await expectNodeParity(join(fixturesRoot, "npm/cases/2595-bundled-entry-namespace/main.ts"), [ + "gtemptyns", + ]); + }, 180_000); + // 2596: cross-spawn 7's function export with typed function members // behind require bindings. test("function export members 2596-function-export-members byte-match Node", async () => {