From 86a6d11d7c7a4481bbebe92f3183d63fe5e85689 Mon Sep 17 00:00:00 2001 From: Jaksenc Date: Thu, 14 May 2026 10:14:33 -0400 Subject: [PATCH 1/4] Preserve TS imports for ESM builds --- src/index.js | 32 +++++++++++++++++++------- test/unit.test.js | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 8 deletions(-) diff --git a/src/index.js b/src/index.js index 78a9d9db..3de0c819 100644 --- a/src/index.js +++ b/src/index.js @@ -258,6 +258,21 @@ function ncc ( })); } + const tsCompilerOptions = { + module: 'esnext', + target: 'esnext', + ...fullTsconfig.compilerOptions, + allowSyntheticDefaultImports: true, + noEmit: false, + outDir: '//' + }; + + if (esm && isCommonJsModule(tsCompilerOptions.module)) { + // ESM builds need TypeScript to preserve imports so webpack can resolve + // dependencies with import conditions instead of CommonJS conditions. + tsCompilerOptions.module = 'esnext'; + } + const compiler = webpack({ entry, cache: cache === false ? undefined : { @@ -365,14 +380,7 @@ function ncc ( options: { transpileOnly, compiler: eval('__dirname + "/typescript.js"'), - compilerOptions: { - module: 'esnext', - target: 'esnext', - ...fullTsconfig.compilerOptions, - allowSyntheticDefaultImports: true, - noEmit: false, - outDir: '//' - } + compilerOptions: tsCompilerOptions } }] }, @@ -635,6 +643,14 @@ function ncc ( } } +function isCommonJsModule(module) { + if (typeof module === 'string') { + return ['commonjs', 'amd', 'umd', 'system'].includes(module.toLowerCase()); + } + + return module >= 1 && module <= 4; +} + // this could be rewritten with actual FS apis / globs, but this is simpler function getFlatFiles(mfsData, output, getAssetMeta, tsconfig, curBase = "") { for (const path of Object.keys(mfsData)) { diff --git a/test/unit.test.js b/test/unit.test.js index e01ca242..5048aeea 100644 --- a/test/unit.test.js +++ b/test/unit.test.js @@ -1,4 +1,5 @@ const fs = require("fs"); +const path = require("path"); const coverage = global.coverage; const ncc = coverage ? require("../src/index") : require("../"); @@ -79,3 +80,60 @@ for (const unitTest of fs.readdirSync(`${__dirname}/unit`)) { ) }); } + +async function expectEsmOnlyTypeScriptBuild(compilerOptions) { + const testDir = fs.mkdtempSync(path.join(__dirname, "tmp-esm-ts-cjs-")); + const prevTsNodeProject = process.env.TS_NODE_PROJECT; + + try { + fs.writeFileSync(`${testDir}/package.json`, JSON.stringify({ + type: "module" + })); + fs.writeFileSync(`${testDir}/tsconfig.json`, JSON.stringify({ + compilerOptions + })); + + const packageDir = `${testDir}/node_modules/esm-only-condition-package`; + fs.mkdirSync(packageDir, { recursive: true }); + fs.writeFileSync(`${packageDir}/package.json`, JSON.stringify({ + name: "esm-only-condition-package", + type: "module", + exports: { + ".": { + types: "./index.d.ts", + import: "./index.js" + } + } + })); + fs.writeFileSync(`${packageDir}/index.js`, "export const value = 'from-esm-only-package';\n"); + fs.writeFileSync(`${packageDir}/index.d.ts`, "export declare const value: string;\n"); + fs.writeFileSync( + `${testDir}/input.ts`, + "import { value } from 'esm-only-condition-package';\nconsole.log(value);\n" + ); + + process.env.TS_NODE_PROJECT = `${testDir}/tsconfig.json`; + + const { code } = await ncc(`${testDir}/input.ts`, { + cache: false, + quiet: true, + transpileOnly: true + }); + + expect(code).toContain("from-esm-only-package"); + } finally { + if (prevTsNodeProject === undefined) + delete process.env.TS_NODE_PROJECT; + else + process.env.TS_NODE_PROJECT = prevTsNodeProject; + + fs.rmSync(testDir, { recursive: true, force: true }); + } +} + +it("preserves TypeScript imports for ESM builds when tsconfig emits CommonJS", async () => { + await expectEsmOnlyTypeScriptBuild({ + module: "CommonJS", + target: "ES2020" + }); +}); From 2fdbf358c4a2ccff3493a2dbc294c704b4ae3468 Mon Sep 17 00:00:00 2001 From: Jaksen Charles <46287095+Jaksenc@users.noreply.github.com> Date: Mon, 18 May 2026 18:13:56 -0400 Subject: [PATCH 2/4] Address PR feedback --- src/index.js | 15 +++-- test/unit.test.js | 58 ------------------- test/unit/ts-esm-import-condition/index.d.ts | 1 + test/unit/ts-esm-import-condition/index.js | 1 + test/unit/ts-esm-import-condition/input.ts | 3 + .../output-coverage.js | 13 +++++ test/unit/ts-esm-import-condition/output.js | 13 +++++ .../unit/ts-esm-import-condition/package.json | 10 ++++ .../ts-esm-import-condition/tsconfig.json | 7 +++ 9 files changed, 57 insertions(+), 64 deletions(-) create mode 100644 test/unit/ts-esm-import-condition/index.d.ts create mode 100644 test/unit/ts-esm-import-condition/index.js create mode 100644 test/unit/ts-esm-import-condition/input.ts create mode 100644 test/unit/ts-esm-import-condition/output-coverage.js create mode 100644 test/unit/ts-esm-import-condition/output.js create mode 100644 test/unit/ts-esm-import-condition/package.json create mode 100644 test/unit/ts-esm-import-condition/tsconfig.json diff --git a/src/index.js b/src/index.js index 3de0c819..ecb1b56a 100644 --- a/src/index.js +++ b/src/index.js @@ -267,9 +267,9 @@ function ncc ( outDir: '//' }; - if (esm && isCommonJsModule(tsCompilerOptions.module)) { + if (esm && emitsNonEsmModule(tsCompilerOptions.module)) { // ESM builds need TypeScript to preserve imports so webpack can resolve - // dependencies with import conditions instead of CommonJS conditions. + // dependencies with "import" conditions instead of "require" conditions. tsCompilerOptions.module = 'esnext'; } @@ -643,12 +643,15 @@ function ncc ( } } -function isCommonJsModule(module) { - if (typeof module === 'string') { - return ['commonjs', 'amd', 'umd', 'system'].includes(module.toLowerCase()); +function emitsNonEsmModule(moduleKind) { + if (typeof moduleKind === 'string') { + return ['commonjs', 'amd', 'umd', 'system'].includes(moduleKind.toLowerCase()); } - return module >= 1 && module <= 4; + // Values match TypeScript's ModuleKind enum: + // CommonJS=1, AMD=2, UMD=3, System=4. + // https://github.com/microsoft/TypeScript/blob/v5.2.2/src/compiler/types.ts#L213 + return moduleKind >= 1 && moduleKind <= 4; } // this could be rewritten with actual FS apis / globs, but this is simpler diff --git a/test/unit.test.js b/test/unit.test.js index 5048aeea..e01ca242 100644 --- a/test/unit.test.js +++ b/test/unit.test.js @@ -1,5 +1,4 @@ const fs = require("fs"); -const path = require("path"); const coverage = global.coverage; const ncc = coverage ? require("../src/index") : require("../"); @@ -80,60 +79,3 @@ for (const unitTest of fs.readdirSync(`${__dirname}/unit`)) { ) }); } - -async function expectEsmOnlyTypeScriptBuild(compilerOptions) { - const testDir = fs.mkdtempSync(path.join(__dirname, "tmp-esm-ts-cjs-")); - const prevTsNodeProject = process.env.TS_NODE_PROJECT; - - try { - fs.writeFileSync(`${testDir}/package.json`, JSON.stringify({ - type: "module" - })); - fs.writeFileSync(`${testDir}/tsconfig.json`, JSON.stringify({ - compilerOptions - })); - - const packageDir = `${testDir}/node_modules/esm-only-condition-package`; - fs.mkdirSync(packageDir, { recursive: true }); - fs.writeFileSync(`${packageDir}/package.json`, JSON.stringify({ - name: "esm-only-condition-package", - type: "module", - exports: { - ".": { - types: "./index.d.ts", - import: "./index.js" - } - } - })); - fs.writeFileSync(`${packageDir}/index.js`, "export const value = 'from-esm-only-package';\n"); - fs.writeFileSync(`${packageDir}/index.d.ts`, "export declare const value: string;\n"); - fs.writeFileSync( - `${testDir}/input.ts`, - "import { value } from 'esm-only-condition-package';\nconsole.log(value);\n" - ); - - process.env.TS_NODE_PROJECT = `${testDir}/tsconfig.json`; - - const { code } = await ncc(`${testDir}/input.ts`, { - cache: false, - quiet: true, - transpileOnly: true - }); - - expect(code).toContain("from-esm-only-package"); - } finally { - if (prevTsNodeProject === undefined) - delete process.env.TS_NODE_PROJECT; - else - process.env.TS_NODE_PROJECT = prevTsNodeProject; - - fs.rmSync(testDir, { recursive: true, force: true }); - } -} - -it("preserves TypeScript imports for ESM builds when tsconfig emits CommonJS", async () => { - await expectEsmOnlyTypeScriptBuild({ - module: "CommonJS", - target: "ES2020" - }); -}); diff --git a/test/unit/ts-esm-import-condition/index.d.ts b/test/unit/ts-esm-import-condition/index.d.ts new file mode 100644 index 00000000..a5987fbd --- /dev/null +++ b/test/unit/ts-esm-import-condition/index.d.ts @@ -0,0 +1 @@ +export declare const value: string; diff --git a/test/unit/ts-esm-import-condition/index.js b/test/unit/ts-esm-import-condition/index.js new file mode 100644 index 00000000..f02f7dbc --- /dev/null +++ b/test/unit/ts-esm-import-condition/index.js @@ -0,0 +1 @@ +export const value = 'from-esm-only-package'; diff --git a/test/unit/ts-esm-import-condition/input.ts b/test/unit/ts-esm-import-condition/input.ts new file mode 100644 index 00000000..e7753ffb --- /dev/null +++ b/test/unit/ts-esm-import-condition/input.ts @@ -0,0 +1,3 @@ +import { value } from "esm-only-condition-package"; + +console.log(value); diff --git a/test/unit/ts-esm-import-condition/output-coverage.js b/test/unit/ts-esm-import-condition/output-coverage.js new file mode 100644 index 00000000..36ae0469 --- /dev/null +++ b/test/unit/ts-esm-import-condition/output-coverage.js @@ -0,0 +1,13 @@ +/******/ /* webpack/runtime/compat */ +/******/ +/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = new URL('.', import.meta.url).pathname.slice(import.meta.url.match(/^file:\/\/\/\w:/) ? 1 : 0, -1) + "/"; +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; + +;// CONCATENATED MODULE: ./test/unit/ts-esm-import-condition/index.js +const value = 'from-esm-only-package'; + +;// CONCATENATED MODULE: ./test/unit/ts-esm-import-condition/input.ts + +console.log(value); \ No newline at end of file diff --git a/test/unit/ts-esm-import-condition/output.js b/test/unit/ts-esm-import-condition/output.js new file mode 100644 index 00000000..36ae0469 --- /dev/null +++ b/test/unit/ts-esm-import-condition/output.js @@ -0,0 +1,13 @@ +/******/ /* webpack/runtime/compat */ +/******/ +/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = new URL('.', import.meta.url).pathname.slice(import.meta.url.match(/^file:\/\/\/\w:/) ? 1 : 0, -1) + "/"; +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; + +;// CONCATENATED MODULE: ./test/unit/ts-esm-import-condition/index.js +const value = 'from-esm-only-package'; + +;// CONCATENATED MODULE: ./test/unit/ts-esm-import-condition/input.ts + +console.log(value); \ No newline at end of file diff --git a/test/unit/ts-esm-import-condition/package.json b/test/unit/ts-esm-import-condition/package.json new file mode 100644 index 00000000..89551fa2 --- /dev/null +++ b/test/unit/ts-esm-import-condition/package.json @@ -0,0 +1,10 @@ +{ + "name": "esm-only-condition-package", + "type": "module", + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.js" + } + } +} diff --git a/test/unit/ts-esm-import-condition/tsconfig.json b/test/unit/ts-esm-import-condition/tsconfig.json new file mode 100644 index 00000000..e8073242 --- /dev/null +++ b/test/unit/ts-esm-import-condition/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "commonjs", + "moduleResolution": "node", + "target": "es2020" + } +} From 7b6fccbd1e1d68ee93d8f76ebe40c63a1e7b496e Mon Sep 17 00:00:00 2001 From: Jaksen Charles <46287095+Jaksenc@users.noreply.github.com> Date: Mon, 18 May 2026 18:21:48 -0400 Subject: [PATCH 3/4] Fix TypeScript ModuleKind link --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index ecb1b56a..636d03a1 100644 --- a/src/index.js +++ b/src/index.js @@ -650,7 +650,7 @@ function emitsNonEsmModule(moduleKind) { // Values match TypeScript's ModuleKind enum: // CommonJS=1, AMD=2, UMD=3, System=4. - // https://github.com/microsoft/TypeScript/blob/v5.2.2/src/compiler/types.ts#L213 + // https://github.com/microsoft/TypeScript/blob/v5.2.2/src/compiler/types.ts#L7255-L7260 return moduleKind >= 1 && moduleKind <= 4; } From b3476b62dab3dd7c59bc429aebbe8d12b7e5ff87 Mon Sep 17 00:00:00 2001 From: Jaksen Charles <46287095+Jaksenc@users.noreply.github.com> Date: Tue, 19 May 2026 09:30:37 -0400 Subject: [PATCH 4/4] Clarify ESM import condition fixture --- test/unit/ts-esm-import-condition/input.ts | 2 +- .../{ => node_modules/esm-only-condition-package}/index.d.ts | 0 .../{ => node_modules/esm-only-condition-package}/index.js | 0 .../esm-only-condition-package}/package.json | 2 ++ test/unit/ts-esm-import-condition/opt.json | 3 +++ test/unit/ts-esm-import-condition/output-coverage.js | 4 ++-- test/unit/ts-esm-import-condition/output.js | 4 ++-- 7 files changed, 10 insertions(+), 5 deletions(-) rename test/unit/ts-esm-import-condition/{ => node_modules/esm-only-condition-package}/index.d.ts (100%) rename test/unit/ts-esm-import-condition/{ => node_modules/esm-only-condition-package}/index.js (100%) rename test/unit/ts-esm-import-condition/{ => node_modules/esm-only-condition-package}/package.json (76%) create mode 100644 test/unit/ts-esm-import-condition/opt.json diff --git a/test/unit/ts-esm-import-condition/input.ts b/test/unit/ts-esm-import-condition/input.ts index e7753ffb..0ccbf64c 100644 --- a/test/unit/ts-esm-import-condition/input.ts +++ b/test/unit/ts-esm-import-condition/input.ts @@ -1,3 +1,3 @@ -import { value } from "esm-only-condition-package"; +import { value } from 'esm-only-condition-package'; console.log(value); diff --git a/test/unit/ts-esm-import-condition/index.d.ts b/test/unit/ts-esm-import-condition/node_modules/esm-only-condition-package/index.d.ts similarity index 100% rename from test/unit/ts-esm-import-condition/index.d.ts rename to test/unit/ts-esm-import-condition/node_modules/esm-only-condition-package/index.d.ts diff --git a/test/unit/ts-esm-import-condition/index.js b/test/unit/ts-esm-import-condition/node_modules/esm-only-condition-package/index.js similarity index 100% rename from test/unit/ts-esm-import-condition/index.js rename to test/unit/ts-esm-import-condition/node_modules/esm-only-condition-package/index.js diff --git a/test/unit/ts-esm-import-condition/package.json b/test/unit/ts-esm-import-condition/node_modules/esm-only-condition-package/package.json similarity index 76% rename from test/unit/ts-esm-import-condition/package.json rename to test/unit/ts-esm-import-condition/node_modules/esm-only-condition-package/package.json index 89551fa2..95c48dc2 100644 --- a/test/unit/ts-esm-import-condition/package.json +++ b/test/unit/ts-esm-import-condition/node_modules/esm-only-condition-package/package.json @@ -1,6 +1,8 @@ { "name": "esm-only-condition-package", + "version": "1.0.0", "type": "module", + "types": "./index.d.ts", "exports": { ".": { "types": "./index.d.ts", diff --git a/test/unit/ts-esm-import-condition/opt.json b/test/unit/ts-esm-import-condition/opt.json new file mode 100644 index 00000000..a63f21dd --- /dev/null +++ b/test/unit/ts-esm-import-condition/opt.json @@ -0,0 +1,3 @@ +{ + "esm": true +} diff --git a/test/unit/ts-esm-import-condition/output-coverage.js b/test/unit/ts-esm-import-condition/output-coverage.js index 36ae0469..ac1139e1 100644 --- a/test/unit/ts-esm-import-condition/output-coverage.js +++ b/test/unit/ts-esm-import-condition/output-coverage.js @@ -5,9 +5,9 @@ /************************************************************************/ var __webpack_exports__ = {}; -;// CONCATENATED MODULE: ./test/unit/ts-esm-import-condition/index.js +;// CONCATENATED MODULE: ./test/unit/ts-esm-import-condition/node_modules/esm-only-condition-package/index.js const value = 'from-esm-only-package'; ;// CONCATENATED MODULE: ./test/unit/ts-esm-import-condition/input.ts -console.log(value); \ No newline at end of file +console.log(value); diff --git a/test/unit/ts-esm-import-condition/output.js b/test/unit/ts-esm-import-condition/output.js index 36ae0469..ac1139e1 100644 --- a/test/unit/ts-esm-import-condition/output.js +++ b/test/unit/ts-esm-import-condition/output.js @@ -5,9 +5,9 @@ /************************************************************************/ var __webpack_exports__ = {}; -;// CONCATENATED MODULE: ./test/unit/ts-esm-import-condition/index.js +;// CONCATENATED MODULE: ./test/unit/ts-esm-import-condition/node_modules/esm-only-condition-package/index.js const value = 'from-esm-only-package'; ;// CONCATENATED MODULE: ./test/unit/ts-esm-import-condition/input.ts -console.log(value); \ No newline at end of file +console.log(value);