Skip to content

fix: publish dual esm cjs package#168

Merged
hbmartin merged 1 commit into
mainfrom
publish-dual-esm-cjs-package
May 15, 2026
Merged

fix: publish dual esm cjs package#168
hbmartin merged 1 commit into
mainfrom
publish-dual-esm-cjs-package

Conversation

@hbmartin
Copy link
Copy Markdown
Owner

@hbmartin hbmartin commented May 15, 2026

Publish dual ESM/CJS package output

  • Updates tsdown.config.ts to emit both ESM (./dist/index.mjs) and CJS (./dist/index.cjs) formats, and enables exports, publint, and attw checks during build.
  • Updates package.json exports map to include a require field for CJS consumers, adds a ./package.json subpath export, and switches the top-level types to ./dist/index.d.cts.
  • Removes the cjs-resolves-to-esm ignore rule from .attw.json now that a proper CJS build exists.
  • Behavioral Change: the top-level types entry now points to a .d.cts file instead of .d.mts, which may affect TypeScript consumers using moduleResolution: node.

Macroscope summarized d24bf4a.

Summary by CodeRabbit

  • Chores

    • Enhanced build configuration to generate dual ESM/CJS outputs for Node environments.
    • Updated package entry points to properly resolve both ES module and CommonJS imports.
  • Documentation

    • Updated development tools documentation to reflect dual format build support.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

📝 Walkthrough

Walkthrough

This PR enables dual ESM and CommonJS output generation for the package. The build configuration in tsdown.config.ts is updated to specify both esm and cjs formats, alongside new validation options for exports and type analysis. The package manifest in package.json is updated to expose both formats through conditional exports (require → CJS, import → ESM) and top-level main and module entry points. The .attw.json configuration is simplified by removing a workaround rule that is no longer needed now that CJS is properly generated. Documentation is updated to reflect the dual-format capability.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: publish dual esm cjs package' directly and accurately summarizes the main change across all modified files, which collectively implement dual ESM/CJS build output and package exports.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch publish-dual-esm-cjs-package

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

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'.

Comment thread package.json
Comment on lines +38 to +39
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

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
Copy link
Copy Markdown

codecov Bot commented May 15, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.23%. Comparing base (514eded) to head (d24bf4a).
⚠️ Report is 2 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 4 files

Re-trigger cubic

@hbmartin hbmartin merged commit 7ba3756 into main May 15, 2026
12 checks passed
@hbmartin hbmartin deleted the publish-dual-esm-cjs-package branch May 15, 2026 18:36
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 514eded and d24bf4a.

📒 Files selected for processing (4)
  • .attw.json
  • README.md
  • package.json
  • tsdown.config.ts

Comment thread package.json
Comment on lines 36 to +43
"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",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 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 -50

Repository: 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 -20

Repository: 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:


🏁 Script executed:

cat tsdown.config.ts

Repository: hbmartin/attio-ts-sdk

Length of output: 347


🏁 Script executed:

cat tsconfig.json

Repository: 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 json

Repository: 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 -10

Repository: 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 2

Repository: 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.

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