Skip to content

Comments

[heft-typescript-plugin] Add emitModulePackageJson option for ESM output folders#5665

Open
iclanton wants to merge 3 commits intomicrosoft:mainfrom
iclanton:lib-type-file
Open

[heft-typescript-plugin] Add emitModulePackageJson option for ESM output folders#5665
iclanton wants to merge 3 commits intomicrosoft:mainfrom
iclanton:lib-type-file

Conversation

@iclanton
Copy link
Member

Summary

When a project uses additionalModuleKindsToEmit to emit ESM output to a lib-esm/ folder, Node.js 18 fails to load the .js files as ES modules because there is no "type": "module" in the nearest package.json. This adds a new emitModulePackageJson option that automatically writes a package.json with the correct "type" field into the output folder after compilation.

Fixes the esm-node-import-test build failure on Node 18.

Details

On Node 18, when a package's exports map points "import" to ./lib-esm/index.js but the package lacks "type": "module", Node treats .js files as CommonJS. This causes SyntaxError: Named export 'Path' not found errors. Node 20.17+ handles this via automatic module detection, but Node 18 (which is still in the supported nodeSupportedVersionRange) does not.

The fix adds emitModulePackageJson as an optional boolean on additionalModuleKindsToEmit entries in typescript.json. When set to true, after TypeScript compilation completes, the plugin writes a package.json containing { "type": "module" } (for ESNext/ES2015 module kinds) or { "type": "commonjs" } (for CommonJS) into the output folder. This is emitted in all three code paths: normal build, solution build, and watch mode.

An alternative considered was using .mjs file extensions via emitMjsExtensionForESModule, but that requires updating all exports maps and import specifiers. Another alternative was a per-project copy-files-plugin task to copy a marker file, but that doesn't scale across the monorepo.

This change is fully backwards compatible — emitModulePackageJson defaults to false.

The option is enabled in local-node-rig and decoupled-local-node-rig for their lib-esm output.

How it was tested

  • Built @rushstack/heft-typescript-plugin and verified the new option is compiled into the schema and builder
  • Built @rushstack/node-core-library and confirmed package.json with { "type": "module" } is emitted
  • Ran esm-node-import-test (heft test --clean) on Node 18 (v18.20.8), Node 20 (v20.20.0), and Node 24 (v24.13.1) — all pass
  • Verified @rushstack/heft-typescript-plugin's own build succeeds with no errors
  • Verified @rushstack/node-core-library's 241 tests pass with no regressions

…put folders

Add a new 'emitModulePackageJson' option for 'additionalModuleKindsToEmit'
entries in typescript.json. When enabled, the TypeScript plugin writes a
package.json with the appropriate "type" field ("module" for ESNext/ES2015,
"commonjs" for CommonJS) to the output folder after compilation.

This ensures Node.js correctly interprets .js files in ESM output folders
like lib-esm/, fixing named import failures on Node 18 where .js files
without a nearest "type": "module" package.json are treated as CommonJS.

Enabled by default in local-node-rig and decoupled-local-node-rig for
their lib-esm output.
}

const isEsm: boolean =
moduleKindToEmit.moduleKind === ts.ModuleKind.ES2015 ||
Copy link
Contributor

Choose a reason for hiding this comment

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

There are a lot more module kinds that are ESM, e.g. ES2020. Probably should be doing a range comparison.

Or, more likely, select module types that aren't ESM, and make the rest be ESM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Needs triage

Development

Successfully merging this pull request may close these issues.

2 participants