Skip to content

Declare package as ESM so import works on older Node - #160

Merged
botandrose merged 2 commits into
bigskysoftware:mainfrom
myabc:fix/declare-esm-entry-as-module
Aug 27, 2026
Merged

Declare package as ESM so import works on older Node#160
botandrose merged 2 commits into
bigskysoftware:mainfrom
myabc:fix/declare-esm-entry-as-module

Conversation

@myabc

@myabc myabc commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Fixes #158.

exports["."].import resolves to dist/idiomorph.esm.js, but the package has no "type" field and the file has a .js extension. Node calls that an ambiguous file and treats it as CommonJS unless module syntax detection rescues it. Detection was unflagged in 22.7.0 and backported to 20.19.0, so on Node 18.x, 20.0-20.18 and 22.0-22.6 import { Idiomorph } from "idiomorph" fails to link with SyntaxError: Named export 'Idiomorph' not found.

A repro with no flags and no bundler:

npm init -y && npm pkg set type=module
npm install idiomorph
echo 'import { Idiomorph } from "idiomorph"' > index.js

npx -y node@20.18.0 index.js   # SyntaxError: Named export 'Idiomorph' not found
npx -y node@20.19.0 index.js   # works

Same package, same file. The two Nodes disagree about its format because the package doesn't declare one. 825388e changed this: dropping the require condition and main leaves the ESM file as the only entry point, so those versions have no CommonJS path left to fall back to.

The fix

"type": "module" in package.json is what Node recommends over relying on detection, which costs an extra parse. It also disambiguates dist/idiomorph.js and dist/idiomorph-ext.js. There are no dist renames, and no change to files, module, unpkg, types or exports.

The field applies to every .js file in the package, so the two in-tree CommonJS scripts had to go. Rather than rename them to .cjs, they're converted to ESM: perf/runner.js resolves its two paths against import.meta.url instead of __dirname, and test/lib/ensure-full-coverage.js takes lcov-parse's CommonJS export as a default import. Both are dev-only and ship in neither the published tarball nor the browser tests, which are served as <script> tags and never see package.json. <script src> and unpkg consumers are unaffected because browsers pick format by MIME type rather than by package.json.

The `import` condition resolves to dist/idiomorph.esm.js, but the
package had no `type` field and the file has a .js extension. Node
calls that an ambiguous file and treats it as CommonJS unless module
syntax detection rescues it. Detection was unflagged in 22.7.0 and
backported to 20.19.0, so on node 18.x, 20.0-20.18 and 22.0-22.6
`import { Idiomorph } from "idiomorph"` fails to link with:

  SyntaxError: Named export 'Idiomorph' not found.

825388e removed the `require` condition and `main`, which leaves the
mis-typed ESM file as the only entry point, so there is no longer a
CommonJS path to fall back to on those versions.

Declaring "type": "module" is what node recommends, since detection
costs an extra parse. It also disambiguates dist/idiomorph.js. The
field applies to every .js file in the package, so the two in-tree
CommonJS scripts move to .cjs. Renames only, contents untouched.

Closes bigskysoftware#158
@myabc
myabc force-pushed the fix/declare-esm-entry-as-module branch from 75bfeb0 to f493232 Compare August 26, 2026 03:22
@myabc
myabc marked this pull request as ready for review August 26, 2026 03:24
Copilot AI lite review requested due to automatic review settings August 26, 2026 03:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Declares the package as ESM to ensure import { Idiomorph } from "idiomorph" works reliably on Node versions that don’t have ESM syntax detection, and updates internal Node-run scripts accordingly. It also adds a CI smoke test that verifies the packed tarball imports correctly on Node 20.18.0 (and a modern Node).

Changes:

  • Add "type": "module" to package.json so .js entrypoints are unambiguously ESM for Node.
  • Rename the two in-repo CommonJS Node scripts to .cjs and update npm scripts to call them.
  • Add a GitHub Actions job that packs and imports the tarball in a type: module project on Node 20.18.0 and 22.

Reviewed changes

Copilot reviewed 2 out of 4 changed files in this pull request and generated no comments.

File Description
package.json Declares ESM package type and updates script paths to .cjs.
test/lib/ensure-full-coverage.cjs CommonJS coverage-enforcement helper moved to .cjs.
perf/runner.cjs CommonJS perf runner moved to .cjs.
.github/workflows/ci.yml Adds an ESM entry smoke-test job using npm pack + import verification.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@botandrose

Copy link
Copy Markdown
Collaborator

@myabc Thank you for explaining this, and for the PR.

On one hand, those node versions are EOL, but on the other hand, its so simple to add "type": "module" that we might as well just do it, if there aren't any drawbacks.

Seems like the only drawback is needing to rename those two remaining CJS files to .cjs so that they still work. And really, I want idiomorph to only use ESM and <script src> so really I think the fix there would be to convert those files to ESM, thus finally closing the door on CJS. Is that reasonable to do here?

I would prefer to not have the extra github action. So drop that and I'll merge, and if you feel like taking a crack at the CJS removal, go for it. Either here in this PR, or in a follow up, or I'll just merge the CJS rename and do it myself.

@myabc

myabc commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

@botandrose thanks for the feedback!

I want idiomorph to only use ESM and <script src> so really I think the fix there would be to convert those files to ESM, thus finally closing the door on CJS. Is that reasonable to do here?

tbh I prefer that approach too. I just wanted to keep this PR tightly-scoped.

I'll drop the GitHub Action. I think we can trust that ESM imports "just work" on all recent node versions once "type": "module" is in place.

@myabc
myabc force-pushed the fix/declare-esm-entry-as-module branch from f493232 to d5567bb Compare August 27, 2026 04:11
The package declares "type": "module", so the two node-run scripts had
to be renamed to .cjs to keep working. Convert them to ESM instead and
rename them back to .js, so no CommonJS is left in the repo.

perf/runner.js drops __dirname in favour of resolving its two paths
against import.meta.url directly. ensure-full-coverage.js takes
lcov-parse's CommonJS export as a default import.

Both are dev-only scripts; nothing here ships in the published tarball.
@botandrose
botandrose merged commit dfbd372 into bigskysoftware:main Aug 27, 2026
6 checks passed
@botandrose

Copy link
Copy Markdown
Collaborator

@myabc Looks great! Thank you!

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.

ESM entry is not declared as a module, so import { Idiomorph } fails on Node without syntax detection

3 participants