Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .attw.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
{
"profile": "node16",
"ignoreRules": ["cjs-resolves-to-esm"]
"profile": "node16"
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1074,7 +1074,7 @@ const { data: webhooks } = await getV2Webhooks({ client });
- **[Hey API](https://heyapi.dev/)**: OpenAPI client and Zod schema generation
- **Biome**: lint and format with a single tool
- **Vitest**: fast tests with coverage and thresholds
- **tsdown**: ESM builds for Node
- **tsdown**: dual ESM/CJS builds for Node
- **CI**: lint, typecheck, test, coverage, and size comments/badges
- **Deno-friendly**: `.ts` source imports for direct consumption
- **OIDC + Provenance**: publish to npm and JSR via manual CI release
Expand Down
13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs"
}
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
Comment on lines +38 to +39
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"
      }

},
"./package.json": "./package.json"
},
"types": "./dist/index.d.mts",
"types": "./dist/index.d.cts",
Comment on lines 36 to +43
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.

"sideEffects": false,
"files": [
"dist"
Expand Down Expand Up @@ -113,5 +114,7 @@
"**/*.yml"
],
"absolute": false
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs"
}
7 changes: 7 additions & 0 deletions tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import { defineConfig } from "tsdown";

export default defineConfig({
entry: { index: "./src/index.ts" },
format: ["esm", "cjs"],
platform: "node",
dts: true,
sourcemap: true,
exports: true,
publint: true,
attw: {
profile: "node16",
level: "error",
},
});