diff --git a/test/bundler/bundler_cjs.test.ts b/test/bundler/bundler_cjs.test.ts index 1579d1dad4..39f847d170 100644 --- a/test/bundler/bundler_cjs.test.ts +++ b/test/bundler/bundler_cjs.test.ts @@ -4,27 +4,25 @@ import { itBundled } from "./expectBundled"; // Tests for CommonJS <> ESM interop, specifically the __toESM helper behavior. // // The key insight from the code change: -// - `input_module_type` is set based on the AST's exports_kind (whether the importing -// file uses ESM syntax like import/export or CJS syntax like require/module.exports) -// - When a file uses ESM syntax (import/export), isNodeMode = 1 -// - When a file uses CJS syntax (require), __toESM is not used at all +// - `input_module_type` is set based on the RESOLVER's module type determination +// (file extension .mjs/.mts and package.json "type" field), NOT on syntax detection. +// - When the importing file is in Node ESM mode (.mjs/.mts or "type": "module"), isNodeMode = 1 +// - When the importing file is NOT in Node ESM mode (regular .js), isNodeMode = 0 // // This means: -// - Any file using `import` will always get isNodeMode=1, which IGNORES __esModule -// and always wraps the CJS module as the default export -// - This matches Node.js ESM behavior where importing CJS from .mjs always wraps -// the entire exports object as the default +// - Regular .js files RESPECT __esModule markers and extract exports.default +// - .mjs files IGNORE __esModule markers and wrap the entire module as default // -// The __esModule marker is only respected in non-bundled scenarios or when using -// actual CommonJS require() syntax. +// This matches the correct Node.js behavior where __esModule is a Babel/TypeScript +// interop convention that only applies in non-Node-ESM contexts. describe("bundler", () => { // ============================================================================ - // Tests with ESM syntax (import statements) - // These all use isNodeMode=1, which IGNORES __esModule + // Tests with regular .js entry file (NOT Node ESM mode) + // These use isNodeMode=0, which RESPECTS __esModule // ============================================================================ - // Test 1: import with __esModule marker - IGNORED + // Test 1: import with __esModule marker - RESPECTED in .js files itBundled("cjs/__toESM_import_syntax_with_esModule", { files: { "/entry.js": /* js */ ` @@ -38,9 +36,9 @@ describe("bundler", () => { `, }, run: { - // With import syntax, isNodeMode=1, so __esModule is IGNORED - // The entire CJS exports object is wrapped as default - stdout: '{"__esModule":true,"default":{"value":"default export"},"named":"named export"}', + // With .js file (NOT Node ESM), isNodeMode=0, so __esModule IS RESPECTED + // The default import gets exports.default + stdout: '{"value":"default export"}', }, }); @@ -57,7 +55,7 @@ describe("bundler", () => { `, }, run: { - // Same behavior - entire module wrapped as default + // No __esModule, so entire module wrapped as default stdout: '{"foo":"foo","bar":"bar"}', }, }); @@ -94,8 +92,28 @@ describe("bundler", () => { }, }); - // Test 5: import with named + default + // Test 5: import with named + default (with __esModule) itBundled("cjs/__toESM_import_syntax_named_and_default", { + files: { + "/entry.js": /* js */ ` + import lib, { foo } from './lib.cjs'; + console.log(JSON.stringify({ default: lib, named: foo })); + `, + "/lib.cjs": /* js */ ` + exports.__esModule = true; + exports.default = 'the default'; + exports.foo = 'foo value'; + exports.bar = 'bar value'; + `, + }, + run: { + // __esModule is respected: default gets exports.default, named gets exports.foo + stdout: '{"default":"the default","named":"foo value"}', + }, + }); + + // Test 5b: import with named + default (without __esModule) + itBundled("cjs/__toESM_import_syntax_named_and_default_no_esModule", { files: { "/entry.js": /* js */ ` import lib, { foo } from './lib.cjs'; @@ -107,6 +125,7 @@ describe("bundler", () => { `, }, run: { + // No __esModule: default gets entire module, named gets exports.foo stdout: '{"default":{"foo":"foo value","bar":"bar value"},"named":"foo value"}', }, }); @@ -131,7 +150,7 @@ describe("bundler", () => { // ============================================================================ // Tests with different targets - // Target doesn't affect isNodeMode - it's based on syntax + // Target doesn't affect isNodeMode - it's based on resolver module type // ============================================================================ // Test 7: target=node @@ -193,7 +212,7 @@ describe("bundler", () => { // Output format doesn't affect isNodeMode either // ============================================================================ - // Test 10: format=esm + // Test 10: format=esm with __esModule (should be respected in .js entry) itBundled("cjs/__toESM_format_esm", { files: { "/entry.js": /* js */ ` @@ -208,8 +227,8 @@ describe("bundler", () => { }, format: "esm", run: { - // __esModule ignored because we're using import syntax - stdout: '{"__esModule":true,"default":"the default","other":"other"}', + // __esModule respected because entry is .js (not Node ESM) + stdout: '"the default"', }, }); @@ -228,13 +247,14 @@ describe("bundler", () => { }, format: "cjs", run: { - // Still ignores __esModule because entry uses import syntax - stdout: '{"__esModule":true,"default":"the default","other":"other"}', + // __esModule respected because entry is .js (not Node ESM) + stdout: '"the default"', }, }); // ============================================================================ // Tests for .mjs files re-exporting from .cjs + // .mjs files ARE in Node ESM mode, so __esModule is IGNORED // ============================================================================ // Test 12: .mjs re-exporting default from CJS @@ -253,11 +273,12 @@ describe("bundler", () => { `, }, run: { + // .mjs wraps entire module as default stdout: '{"foo":"foo","bar":"bar"}', }, }); - // Test 13: .mjs re-exporting with __esModule (still ignored) + // Test 13: .mjs re-exporting with __esModule (IGNORED in .mjs) itBundled("cjs/__toESM_mjs_reexport_with_esModule", { files: { "/entry.js": /* js */ ` @@ -274,12 +295,12 @@ describe("bundler", () => { `, }, run: { - // __esModule ignored - entire module wrapped as default + // __esModule IGNORED in .mjs - entire module wrapped as default stdout: '{"__esModule":true,"default":{"value":"from cjs"},"other":"other"}', }, }); - // Test 14: Deep re-export chain + // Test 14: Deep re-export chain through .mjs files itBundled("cjs/__toESM_deep_reexport_chain", { files: { "/entry.js": /* js */ ` @@ -301,7 +322,7 @@ describe("bundler", () => { }, }); - // Test 15: Re-export with rename + // Test 15: Re-export with rename from .mjs itBundled("cjs/__toESM_reexport_with_rename", { files: { "/entry.js": /* js */ ` @@ -337,13 +358,41 @@ describe("bundler", () => { `, }, run: { - // Entire module wrapped, including the .default property + // No __esModule, so entire module wrapped as default stdout: '{"default":"I am a prop named default","other":"other"}', }, }); - // Test 17: Mixed import styles + // Test 17: Mixed import styles (with __esModule) itBundled("cjs/__toESM_mixed_import_styles", { + files: { + "/entry.js": /* js */ ` + import defaultExport from './lib.cjs'; + import { foo } from './lib.cjs'; + import * as namespace from './lib.cjs'; + console.log(JSON.stringify({ + default: defaultExport, + named: foo, + namespace: namespace + })); + `, + "/lib.cjs": /* js */ ` + exports.__esModule = true; + exports.default = 'the default'; + exports.foo = 'foo'; + exports.bar = 'bar'; + `, + }, + run: { + // __esModule respected: default gets exports.default, named gets exports.foo + // namespace gets all exports + stdout: + '{"default":"the default","named":"foo","namespace":{"default":"the default","foo":"foo","bar":"bar","__esModule":true}}', + }, + }); + + // Test 17b: Mixed import styles (without __esModule) + itBundled("cjs/__toESM_mixed_import_styles_no_esModule", { files: { "/entry.js": /* js */ ` import defaultExport from './lib.cjs'; @@ -361,6 +410,7 @@ describe("bundler", () => { `, }, run: { + // No __esModule: default gets entire module, named gets exports.foo stdout: '{"default":{"foo":"foo","bar":"bar"},"named":"foo","namespace":{"default":{"foo":"foo","bar":"bar"},"foo":"foo","bar":"bar"}}', }, @@ -380,8 +430,8 @@ describe("bundler", () => { `, }, run: { - // Even if __esModule were respected, only `true` would work - // But it's ignored anyway due to import syntax + // __esModule must be strictly `true` to be respected + // 'truthy' is not `true`, so entire module wrapped as default stdout: '{"__esModule":"truthy","default":{"value":"default"},"other":"other"}', }, }); @@ -400,7 +450,7 @@ describe("bundler", () => { `, }, run: { - // Entire module wrapped as default (since we use import syntax) + // __esModule = false, so entire module wrapped as default stdout: '{"__esModule":false,"default":{"value":"ignored"},"foo":"foo"}', }, }); @@ -421,14 +471,12 @@ describe("bundler", () => { `, }, run: { - // __esModule is in the object but ignored due to import syntax - stdout: '{"__esModule":true,"default":{"value":"nested"},"other":"prop"}', + // __esModule respected in .js entry, default gets the default property + stdout: '{"value":"nested"}', }, }); // Test 21: Input=ESM, output=CJS, importing CJS with __esModule and named imports - // This test covers the specific fix for printing __toESM when output format is CJS - // and input uses ESM syntax to import both default and named exports from CJS with __esModule itBundled("cjs/__toESM_input_esm_output_cjs_wrapper_print", { files: { "/entry.js": /* js */ ` @@ -443,10 +491,8 @@ describe("bundler", () => { }, format: "cjs", run: { - // With the fix: ignores __esModule, wraps entire module as default - // So default gets the whole exports object, named gets the named property - stdout: - '{"default":{"__esModule":true,"default":{"value":"default"},"named":"named export"},"named":"named export"}', + // __esModule respected: default gets exports.default, named gets exports.named + stdout: '{"default":{"value":"default"},"named":"named export"}', }, }); @@ -464,7 +510,7 @@ describe("bundler", () => { `, }, run: { - // Star import gets the exports as-is, no wrapper + // Star import gets exports as-is (though may have synthetic default added) stdout: '{"named":"named","default":"default","__esModule":true}', }, }); @@ -486,9 +532,71 @@ describe("bundler", () => { `, }, run: { - // Default gets entire module, named import gets specific function + // No __esModule: Default gets entire module, named import gets specific function // Both reference the same function stdout: '{"hasMap":true,"same":true}', }, }); + + // ============================================================================ + // Tests for the original issue #26901 - .js file importing CJS with __esModule + // ============================================================================ + + // Test 24: Original issue case - .js importing from CJS with __esModule + itBundled("cjs/__toESM_issue_26901_js_file", { + files: { + "/entry.js": /* js */ ` + import lib from './lib.cjs'; + console.log(JSON.stringify(lib)); + `, + "/lib.cjs": /* js */ ` + exports.__esModule = true; + exports.default = { msg: 'hello' }; + `, + }, + run: { + // .js file: __esModule is RESPECTED, so default import gets exports.default + stdout: '{"msg":"hello"}', + }, + }); + + // Test 25: Same case but with .mjs entry - __esModule is IGNORED + itBundled("cjs/__toESM_issue_26901_mjs_file", { + files: { + "/entry.mjs": /* js */ ` + import lib from './lib.cjs'; + console.log(JSON.stringify(lib)); + `, + "/lib.cjs": /* js */ ` + exports.__esModule = true; + exports.default = { msg: 'hello' }; + `, + }, + run: { + // .mjs file: __esModule is IGNORED, so default import gets entire module + stdout: '{"__esModule":true,"default":{"msg":"hello"}}', + }, + }); + + // Test 26: package.json "type": "module" - __esModule is IGNORED + itBundled("cjs/__toESM_issue_26901_type_module", { + files: { + "/entry.js": /* js */ ` + import lib from './lib.cjs'; + console.log(JSON.stringify(lib)); + `, + "/package.json": /* json */ ` + { "type": "module" } + `, + "/lib.cjs": /* js */ ` + exports.__esModule = true; + exports.default = { msg: 'hello' }; + `, + }, + run: { + // "type": "module" makes .js files Node ESM mode + // __esModule is IGNORED, so default import gets entire module + stdout: '{"__esModule":true,"default":{"msg":"hello"}}', + }, + }); }); diff --git a/test/bundler/bundler_npm.test.ts b/test/bundler/bundler_npm.test.ts index 827081a0e8..a758cc583f 100644 --- a/test/bundler/bundler_npm.test.ts +++ b/test/bundler/bundler_npm.test.ts @@ -66,9 +66,6 @@ describe("bundler", () => { ], }, }, - expectExactFilesize: { - "out/entry.js": 221720, - }, run: { stdout: "
This is an example.
", },