fix: publish dual esm cjs package#168
Conversation
📝 WalkthroughWalkthroughThis PR enables dual ESM and CommonJS output generation for the package. The build configuration in 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Comment |
There was a problem hiding this comment.
Code Review
This pull request transitions the project to a dual ESM/CJS build system. Key changes include updating package.json to include both import and require exports, configuring tsdown for dual-format output, and adding linting/validation tools like publint and attw. Feedback highlights a missing types condition in the exports map, which is critical for proper TypeScript resolution in dual-format packages to avoid the 'Dual Package Hazard'.
| "import": "./dist/index.mjs", | ||
| "require": "./dist/index.cjs" |
There was a problem hiding this comment.
The types condition is missing from the exports map. For a dual ESM/CJS package, it is essential to provide specific type definitions for each format to ensure correct resolution by TypeScript and to avoid the 'Dual Package Hazard'. By using nested conditions for import and require, you can explicitly point to the appropriate .d.mts and .d.cts files. Note that the types condition must always be the first entry within each block to be correctly recognized.
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #168 +/- ##
=======================================
Coverage 94.23% 94.23%
=======================================
Files 31 31
Lines 2099 2099
Branches 424 424
=======================================
Hits 1978 1978
Misses 67 67
Partials 54 54 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@package.json`:
- Around line 36-43: The package.json currently exposes a top-level "types":
"./dist/index.d.cts" which is non-idiomatic for dual ESM/CJS packages; update
the configuration by either enabling tsdown's dts.cjsReexport option (set
dts.cjsReexport: true in your tsdown config so it generates a shared .d.mts plus
a .d.cts re-export stub) or move the types entry into the "exports" map (add a
"types" key alongside "import" and "require" under the "." export pointing to
"./dist/index.d.ts") and remove the top-level "types" field so type resolution
follows modern dual-package best practices.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 081cf8a8-f9be-4578-9196-83192b2d63fc
📒 Files selected for processing (4)
.attw.jsonREADME.mdpackage.jsontsdown.config.ts
| "exports": { | ||
| ".": { | ||
| "types": "./dist/index.d.mts", | ||
| "import": "./dist/index.mjs" | ||
| } | ||
| "import": "./dist/index.mjs", | ||
| "require": "./dist/index.cjs" | ||
| }, | ||
| "./package.json": "./package.json" | ||
| }, | ||
| "types": "./dist/index.d.mts", | ||
| "types": "./dist/index.d.cts", |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify tsdown output and type resolution
# Check what declaration files are generated
echo "=== Generated declaration files ==="
fd -e d.ts -e d.mts -e d.cts . dist/ 2>/dev/null || echo "dist/ directory not found"
# Verify attw can validate the package structure
echo -e "\n=== `@arethetypeswrong/cli` analysis ==="
if command -v attw &> /dev/null; then
attw --pack . 2>&1 || echo "attw validation failed or not installed"
else
echo "attw not available in sandbox"
fi
# Check package.json exports resolution
echo -e "\n=== Package exports structure ==="
cat package.json | jq '.exports, .types, .main, .module'Repository: hbmartin/attio-ts-sdk
Length of output: 414
🏁 Script executed:
# Check for build configuration and tsdown setup
find . -maxdepth 2 -type f \( -name "tsconfig*.json" -o -name "tsdown.config.*" -o -name "build.config.*" \)Repository: hbmartin/attio-ts-sdk
Length of output: 100
🏁 Script executed:
# Check package.json more thoroughly for build/type setup
cat package.json | head -50Repository: hbmartin/attio-ts-sdk
Length of output: 1070
🏁 Script executed:
# Look at the actual source structure to understand the project layout
fd -t f -e ts -e tsx -e js . src/ --max-depth 2 2>/dev/null | head -20Repository: hbmartin/attio-ts-sdk
Length of output: 506
🌐 Web query:
tsdown type declarations dual package ESM CJS typescript resolution
💡 Result:
When building dual ESM and CommonJS (CJS) packages with tsdown, you may encounter the "dual module type hazard," where TypeScript treats types from the ESM and CJS builds as distinct, incompatible entities [1]. This often leads to errors like TS2352 when consumers mix resolution paths [1]. To resolve this and ensure correct type resolution, follow these practices: 1. Use the cjsReexport Option: Recent updates to tsdown (as of March 2026) introduced the dts.cjsReexport: true option [1]. When enabled, tsdown skips the separate CJS declaration compilation pass and instead generates a.d.cts stub that re-exports types from the.d.mts file [1]. This ensures that both ESM and CJS consumers resolve to the same underlying type definitions, effectively eliminating the hazard [1]. 2. Configure package.json Exports: Properly define your package.json to guide consumers to the correct files [2][3]. Use the exports field to map import and require conditions to their respective types and entry points [2][3]: { "exports": { ".": { "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" } } } } 3. Validate with Tooling: Use the @arethetypeswrong/cli tool to verify your package configuration [2][3]. It checks your package.json exports and type resolution against various Node.js and bundler environments to ensure no resolution problems exist [2][4]. 4. Configuration Strategy: You can define dual builds in your tsdown.config.ts by passing an array of configurations or using the outputOptions hook to dynamically set output directories based on the format [5][2]. // Example tsdown.config.ts import { defineConfig } from "tsdown"; export default defineConfig([ { entry: "src/index.ts", format: ["esm", "cjs"], dts: true, // Enable to fix dual module type hazard dts: { cjsReexport: true } } ]); By using the cjsReexport feature and verifying with @arethetypeswrong/cli, you can maintain a robust dual-package setup that avoids common TypeScript resolution pitfalls [1][2].
Citations:
- 1: feat(dts): add
cjsReexportoption to eliminate dual module type hazard rolldown/tsdown#856 - 2: https://dev.to/hacksore/dual-publish-esm-and-cjs-with-tsdown-2l75
- 3: https://johnnyreilly.com/dual-publishing-esm-cjs-modules-with-tsup-and-are-the-types-wrong
- 4: https://github.com/Hacksore/tsdown-dual-publish-esm-cjs
- 5: Combining ESM/CJS configs rolldown/tsdown#223
🏁 Script executed:
cat tsdown.config.tsRepository: hbmartin/attio-ts-sdk
Length of output: 347
🏁 Script executed:
cat tsconfig.jsonRepository: hbmartin/attio-ts-sdk
Length of output: 591
🏁 Script executed:
# Search for any tsdown or type resolution related discussions/config
rg "cjsReexport|dual module|type.*hazard" --type ts --type jsonRepository: hbmartin/attio-ts-sdk
Length of output: 47
🏁 Script executed:
# Check if there are any build or type-related issues documented
fd -e md . -maxdepth 3 -type f | xargs grep -l "tsdown\|type.*resolution\|types.*field" 2>/dev/null | head -10Repository: hbmartin/attio-ts-sdk
Length of output: 296
🏁 Script executed:
# Check the actual exports being auto-generated - is types in the exports conditions?
rg "types.*:" package.json -A 2 -B 2Repository: hbmartin/attio-ts-sdk
Length of output: 744
Consider modernizing type declaration configuration for dual packages.
The top-level types field pointing to ./dist/index.d.cts works because your project uses attw: { level: "error" } in tsdown.config.ts, which validates and confirms type resolution is correct. However, this pattern is not aligned with current best practices (as of March 2026).
Modern tsdown supports the dts.cjsReexport: true option, which generates a .d.cts re-export stub that points to a shared .d.mts file. This is the recommended approach for dual ESM/CJS packages:
{
"dts": {
"cjsReexport": true
}
}Alternatively, move the types field into the exports map:
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./package.json": "./package.json"
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@package.json` around lines 36 - 43, The package.json currently exposes a
top-level "types": "./dist/index.d.cts" which is non-idiomatic for dual ESM/CJS
packages; update the configuration by either enabling tsdown's dts.cjsReexport
option (set dts.cjsReexport: true in your tsdown config so it generates a shared
.d.mts plus a .d.cts re-export stub) or move the types entry into the "exports"
map (add a "types" key alongside "import" and "require" under the "." export
pointing to "./dist/index.d.ts") and remove the top-level "types" field so type
resolution follows modern dual-package best practices.
Publish dual ESM/CJS package output
./dist/index.mjs) and CJS (./dist/index.cjs) formats, and enablesexports,publint, andattwchecks during build.requirefield for CJS consumers, adds a./package.jsonsubpath export, and switches the top-leveltypesto./dist/index.d.cts.cjs-resolves-to-esmignore rule from .attw.json now that a proper CJS build exists.typesentry now points to a.d.ctsfile instead of.d.mts, which may affect TypeScript consumers usingmoduleResolution: node.Macroscope summarized d24bf4a.
Summary by CodeRabbit
Chores
Documentation