Skip to content

build: dual ESM+CJS output with integration test coverage#123

Merged
buffcode merged 8 commits intomasterfrom
esm-dual-build
Apr 22, 2026
Merged

build: dual ESM+CJS output with integration test coverage#123
buffcode merged 8 commits intomasterfrom
esm-dual-build

Conversation

@buffcode
Copy link
Copy Markdown
Owner

@buffcode buffcode commented Apr 22, 2026

Summary

  • Split build into dual CJS (dist/cjs/) and ESM (dist/esm/) outputs via separate tsconfigs, with subfolder package.json markers and a conditional exports map so both import and require consumers resolve correctly.
  • Added native-runtime integration tests for CJS (test/integration/cjs.cjs), ESM (test/integration/esm.mjs), and updated Deno tests to cover both dist layouts — all run in CI.
  • Wired dual build + CJS/ESM integration tests into nodejs.yml across the existing Node matrix (20, 22, 24, 25); Deno job now exercises both CJS (via createRequire) and ESM consumer paths.

Notable choices

  • moduleResolution: bundler rather than nodenext — upstream ntp-packet-parser@0.6.0 ships .d.ts files without .js extensions on relative imports, which trips nodenext during our build. bundler still honors the exports map and resolves types correctly for consumers. Can revisit if upstream fixes the extensions.
  • No new runtime or dev dependencies; dual build is pure tsc + a tiny post-build Node script.
  • Root package.json keeps "type": "commonjs" for safety; per-folder markers in dist/cjs and dist/esm do the dual-resolution work.

Test plan

  • yarn clean && yarn build produces both dist/cjs/ and dist/esm/ with type markers and declarations
  • yarn test:integration:cjs passes locally
  • yarn test:integration:esm passes locally
  • node examples/example.js continues to work against the new CJS path
  • CI green across Node 20/22/24/25 — dual build + both integration tests
  • CI Deno job green — both CJS (createRequire) and ESM (import) scenarios

🤖 Generated with Claude Code

buffcode and others added 7 commits April 22, 2026 02:51
Introduces tsconfig.base.json holding the shared compiler options, plus
tsconfig.cjs.json and tsconfig.esm.json for the respective dual-build
outputs (./dist/cjs and ./dist/esm). Root tsconfig.json now extends the
base and targets the ESM layout so editors/IDEs default to the modern
configuration while the explicit build variants drive the publishable
artifacts. Uses moduleResolution: bundler in both variants because
TypeScript 6 deprecated plain "node" and nodenext tripped on an upstream
package shipping .d.ts files without .js-extension imports.

Adds scripts/write-package-type.mjs to write the correct subfolder
package.json type marker after each tsc run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ECMAScript module resolution in Node requires explicit file extensions
on relative imports. Appending .js keeps the TypeScript source working
under both the CJS and ESM outputs and aligns with the standard modern
Node/TS pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Publishes both CJS and ESM entries via the conditional exports map so
consumers get the correct format automatically while keeping
type: commonjs at the root. Adds build:cjs / build:esm / build /
clean scripts plus test:integration variants that drive the new
runtime consumer tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Exercises the dual build outputs through native Node runtimes using
node:assert/strict. Each test imports the respective dist entry and
verifies the exported surface, singleton behavior, constructor option
merging, and that resolved options carry no __proto__ own properties —
mirroring the existing Deno scenario so regressions in either output
format are caught.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Points the createRequire-based scenario at ./dist/cjs/index.js and
extends the assertions with the __proto__ pollution checks that the
new Node integration tests share. Adds deno-esm.ts so Deno also
exercises the ESM dist natively, giving coverage of both output
formats across the Deno matrix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The build job now runs the dual CJS+ESM build and invokes the CJS and
ESM integration tests on every Node version ['20','22','24','25'].
The Deno job also runs the new CJS and ESM Deno scenarios after
building.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With the dual build output, the CJS entry lives at dist/cjs/index.
Updating the require path keeps the example runnable from CI and
local development against the published API surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@buffcode buffcode self-assigned this Apr 22, 2026
Deno's strict ESM loader rejects bare specifiers for Node built-ins.
TypeScript preserves specifiers verbatim, so the source must use the
node: prefix for both CJS and ESM output to work across Node, Deno,
and other strict ESM runtimes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@buffcode buffcode merged commit 4294f8c into master Apr 22, 2026
11 checks passed
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.

1 participant