Skip to content

fix: preserve TypeScript imports for ESM builds#1319

Draft
Jaksenc wants to merge 4 commits into
vercel:mainfrom
Jaksenc:codex/preserve-esm-ts-imports
Draft

fix: preserve TypeScript imports for ESM builds#1319
Jaksenc wants to merge 4 commits into
vercel:mainfrom
Jaksenc:codex/preserve-esm-ts-imports

Conversation

@Jaksenc
Copy link
Copy Markdown

@Jaksenc Jaksenc commented May 14, 2026

Summary

  • preserve TypeScript ES imports when ncc has selected ESM output but tsconfig.json asks TypeScript to emit a known CommonJS-style module format
  • leave user TypeScript module settings alone for non-ESM builds and other module modes
  • add a regression test for an ESM package that exposes only an import condition

Fixes #1311.

Why

@actions/core@3.0.0 is ESM-only: its package root has type: "module" and an exports["."].import entry, but no require condition. In a TypeScript project with type: "module" and compilerOptions.module set to CommonJS, ncc correctly selects ESM output, but ts-loader lowers the entry import to require(). Webpack then resolves the dependency through CommonJS conditions and fails with Package path . is not exported.

The fixture sets esm: true explicitly so the test focuses on ncc's ESM output path while keeping the TypeScript module: "commonjs" setting that triggers the regression.

This patch keeps TypeScript imports intact only for that ESM-output/CommonJS-emit mismatch, so webpack can resolve import-only packages through their import condition without broadening the behavior for CJS ncc builds.

Research / Repro

  • Confirmed @actions/core@3.0.0 exposes package root through import only via npm view @actions/core@3.0.0 version type exports --json.
  • Confirmed TypeScript parses compilerOptions.module: "CommonJS" to ModuleKind.CommonJS, which is the known CommonJS-style emit mode covered here.
  • Ran the new repro against origin/main in a detached worktree: it fails with Package path . is not exported for an import-only package.
  • Ran the same repro against this branch: it passes and the bundled code contains the import-only package value.
  • Checked open PR overlap for github/core package fails build because of exports map #1311; only this draft PR currently targets it.

Testing

  • ./node_modules/.bin/tsc --noEmit -p test/unit/ts-esm-import-condition/tsconfig.json
  • node --expose-gc --max_old_space_size=4096 node_modules/jest/bin/jest.js test/unit.test.js --runInBand --testNamePattern ts-esm-import-condition
  • node --expose-gc --max_old_space_size=4096 node_modules/jest/bin/jest.js test/unit.test.js --runInBand --coverage --globals '{"coverage":true}' --testNamePattern ts-esm-import-condition

@Jaksenc Jaksenc marked this pull request as ready for review May 18, 2026 13:43
@Jaksenc Jaksenc requested review from Timer and styfle as code owners May 18, 2026 13:43
Comment thread src/index.js Outdated
return ['commonjs', 'amd', 'umd', 'system'].includes(module.toLowerCase());
}

return module >= 1 && module <= 4;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are these integers defined? Lets link to the docs so its clear.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah, that’s fair. I added a link to the TypeScript enum here and renamed the helper so it’s clearer what those numbers are doing.

Comment thread test/unit.test.js Outdated
}

async function expectEsmOnlyTypeScriptBuild(compilerOptions) {
const testDir = fs.mkdtempSync(path.join(__dirname, "tmp-esm-ts-cjs-"));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a temp directory? Looks like you're building a test fixture so lets just create a new test fixture and commit it since this doesn't need any dynamic behavior

Copy link
Copy Markdown
Author

@Jaksenc Jaksenc May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Youre right that makes more sense, just changed it. Thanks!!

@Jaksenc Jaksenc changed the title Preserve TypeScript imports for ESM builds fix: preserve TypeScript imports for ESM builds May 18, 2026
@Jaksenc Jaksenc force-pushed the codex/preserve-esm-ts-imports branch from df63d25 to 7b6fccb Compare May 18, 2026 23:43
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is super strange behavior since you have module=commonjs in tsconfig.json here but type=module in package.json

Does this code even compile and run using tsc + node?

I dont understand why ncc should support this 🤔

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No you’re so right, the fixture makes the case look a bit weird.

I was trying to test ncc’s ESM output path instaed of saying that this setup should be run directly with tsc + Node. Lemme clean it up by moving the import-only package into node_modules and making esm: true explicit in the fixture options so the test is clearer.

@Jaksenc Jaksenc marked this pull request as draft May 19, 2026 13:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

github/core package fails build because of exports map

2 participants