feat: Publish dual CJS and ESM builds#85
Conversation
Publish both a CommonJS and an ESM build from the same TypeScript source
(build/cjs and build/esm, each with its own package.json marker), so existing
require('asyncbox') consumers keep working unchanged while new consumers can
import it natively. Wired up via a conditional exports map (plus
main/module/types fallbacks) and a postbuild script that stamps each output
directory with its own {"type": ...}.
p-limit (a runtime dependency) is ESM-only. The CJS build's require('p-limit')
works because it relies on Node's native require(esm) support, available on
the versions already required by engines (^20.19.0 || ^22.12.0 || >=24.0.0).
tsconfig.cjs.json uses moduleResolution: "Bundler" (paired with module:
"CommonJS") so tsc can resolve p-limit's types, which only exist under its
exports field with no top-level main/types.
Also add test/cjs-interop.spec.cjs, which require()s the CJS build directly,
so a future change that breaks CJS consumption (e.g. an ESM-only dependency
bump) fails CI instead of only being caught by the ESM test suite.
Lint/format/release tooling changes are intentionally left out of this PR
per #84 (comment).
There was a problem hiding this comment.
Pull request overview
This PR updates asyncbox’s packaging and build pipeline to publish both ESM and CommonJS outputs from the same TypeScript sources, aiming to keep existing require('asyncbox') consumers working while enabling native import usage.
Changes:
- Split TypeScript build outputs into
build/esmandbuild/cjsvia dedicatedtsconfigfiles. - Add a postbuild step that stamps each build output directory with a
package.jsoncontaining the correct"type". - Add a CommonJS interop test and update docs +
package.jsonentrypoints/exports to support dual publishing.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.json | Removes shared outDir and excludes the new .cjs test from typechecking. |
| tsconfig.esm.json | Adds ESM build output configuration (build/esm). |
| tsconfig.cjs.json | Adds CJS build output configuration (build/cjs) with CJS module settings. |
| test/cjs-interop.spec.cjs | Adds a CJS-focused test to protect require() consumption. |
| scripts/postbuild.mjs | Stamps build output dirs with per-directory package.json "type" markers. |
| README.md | Documents that both ESM and CJS consumption are supported. |
| package.json | Wires up conditional exports + dual entrypoints and adjusts build/test scripts accordingly. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
npm run clean ran npm run build -- --clean, which still fires the postbuild
lifecycle hook and regenerates build/esm and build/cjs (including the stamped
package.json markers) — the repo never actually ended up clean. Just rm -rf
the whole build directory instead.
Also have test/cjs-interop.spec.cjs require('asyncbox') by package name rather
than reaching into build/cjs/lib/asyncbox.js directly, so it actually exercises
the exports/main resolution real CommonJS consumers go through.
Addresses review comments from #85
rm -rf build doesn't work in default Windows shells. Add scripts/clean.mjs (mirroring the existing scripts/postbuild.mjs pattern) using fs.rmSync instead. Addresses #85 (comment)
Fold scripts/clean.mjs into a single node -e invocation directly in package.json — same cross-platform fs.rmSync behavior, one less file.
| "extends": "@appium/tsconfig/tsconfig.json", | ||
| "compilerOptions": { | ||
| "strict": true, | ||
| "outDir": "build", |
There was a problem hiding this comment.
Should this be removed? The equivalent node-teen_process PR leaves this line untouched.
There was a problem hiding this comment.
Since outDir is set by the extended files, I guess it should be removed from the other PR
There was a problem hiding this comment.
asyncbox could drop it because it has no lint:types script — only tsc -b via the esm/cjs configs, which both set their own outDir. teen_process's lint:types needs the base config to still emit somewhere sane, so outDir: "build" here is intentional, not leftover cruft.
## [6.4.0](v6.3.5...v6.4.0) (2026-07-26) ### Features * Publish dual CJS and ESM builds ([#85](#85)) ([623bedd](623bedd))
|
🎉 This PR is included in version 6.4.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Publish both a CommonJS and an ESM build from the same TypeScript source (build/cjs and build/esm, each with its own package.json marker), so existing require('asyncbox') consumers keep working unchanged while new consumers can import it natively. Wired up via a conditional exports map (plus main/module/types fallbacks) and a postbuild script that stamps each output directory with its own {"type": ...}.
p-limit (a runtime dependency) is ESM-only. The CJS build's require('p-limit') works because it relies on Node's native require(esm) support, available on the versions already required by engines (^20.19.0 || ^22.12.0 || >=24.0.0). tsconfig.cjs.json uses moduleResolution: "Bundler" (paired with module: "CommonJS") so tsc can resolve p-limit's types, which only exist under its exports field with no top-level main/types.
Also add test/cjs-interop.spec.cjs, which require()s the CJS build directly, so a future change that breaks CJS consumption (e.g. an ESM-only dependency bump) fails CI instead of only being caught by the ESM test suite.
Lint/format/release tooling changes are intentionally left out of this PR per #84 (comment).