Skip to content

Commit d7c4294

Browse files
fix: add .js extensions to declaration file imports to resolve type re-export issue
Fixes type re-export issue where types exported from this package could not be re-exported by consuming packages when package.json has "type": "module". ## Root Cause When `package.json` has `"type": "module"`, TypeScript treats `.d.ts` files as ESM modules, which require explicit file extensions (`.js`) for relative imports. The generated declaration files had imports like `from './twist-api'` instead of `from './twist-api.js'`, causing module resolution to fail when other packages tried to re-export types. ## Changes - **Added `scripts/fix-dts-imports.cjs`**: Post-build script that adds `.js` extensions to all relative imports in generated `.d.ts` files - Handles `import` statements - Handles `export` statements - Handles dynamic `import()` type expressions - **Updated `package.json`**: - Added `build:fix-dts` script to run the DTS fixer - Updated `build` script to include the fix-dts step - Added `attw` script for type validation using `@arethetypeswrong/cli` - Updated `integrity-checks` to include `attw` validation ## Verification **Before fix** (using `@arethetypeswrong/cli --pack`): - ❌ **InternalResolutionError** - Module resolution failed (breaking bug) **After fix**: - ✅ **No problems found** - All module resolution working correctly ⭐ - ✅ Declaration files now have `.js` extensions on all relative imports - ✅ Type validation passes with `@arethetypeswrong/cli` ## Test Plan - [x] Build completes successfully with new fix-dts step - [x] Declaration files now have `.js` extensions on all relative imports - [x] `@arethetypeswrong/cli` validation passes (no problems found) - [x] All 166 tests pass without modification - [x] TypeScript compilation succeeds without errors Applies same fix as: - Doist/todoist-api-typescript#410 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent eddce29 commit d7c4294

File tree

2 files changed

+129
-2
lines changed

2 files changed

+129
-2
lines changed

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@
2929
"build:cjs": "npx tsc -p tsconfig.cjs.json",
3030
"build:esm": "npx tsc -p tsconfig.esm.json",
3131
"build:fix-esm": "node scripts/fix-esm-imports.cjs",
32+
"build:fix-dts": "node scripts/fix-dts-imports.cjs",
3233
"build:post": "echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
33-
"build": "npm-run-all clean build:cjs build:esm build:fix-esm build:post",
34+
"build": "npm-run-all clean build:cjs build:esm build:fix-esm build:fix-dts build:post",
3435
"type-check": "npx tsc --noEmit",
3536
"biome:sort-imports": "biome check --formatter-enabled=false --linter-enabled=false --organize-imports-enabled=true --write .",
3637
"lint:check": "biome lint",
@@ -39,8 +40,9 @@
3940
"format:write": "biome format --write",
4041
"check": "biome check",
4142
"check:fix": "biome check --fix --unsafe",
43+
"attw": "npx @arethetypeswrong/cli --pack --ignore-rules fallback-condition false-esm",
4244
"audit": "npm audit --audit-level=moderate",
43-
"integrity-checks": "npm-run-all clean check test build",
45+
"integrity-checks": "npm-run-all clean check test build attw",
4446
"prepublishOnly": "npm run integrity-checks",
4547
"prepare": "npm run build"
4648
},

scripts/fix-dts-imports.cjs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('node:fs')
4+
const path = require('node:path')
5+
6+
/**
7+
* Post-build script to fix TypeScript declaration file imports by adding .js extensions.
8+
* This is required because when package.json has "type": "module", TypeScript treats
9+
* .d.ts files as ESM modules, which require explicit file extensions for relative imports.
10+
*/
11+
12+
const TYPES_DIR = path.join(__dirname, '../dist/types')
13+
14+
function fixImportsInFile(filePath) {
15+
let content = fs.readFileSync(filePath, 'utf8')
16+
let modified = false
17+
18+
// Fix relative imports - add .js extension if missing
19+
content = content.replace(
20+
/from\s+['"](\.[^'"]*?)['"];?/g,
21+
(match, importPath) => {
22+
// Skip if already has extension or is directory import
23+
if (path.extname(importPath) || importPath.endsWith('/')) {
24+
return match
25+
}
26+
27+
// Check if this is a directory import (look for index file)
28+
const fullPath = path.resolve(path.dirname(filePath), importPath)
29+
const indexPath = path.join(fullPath, 'index.d.ts')
30+
31+
if (fs.existsSync(indexPath)) {
32+
// Directory import - add /index.js
33+
modified = true
34+
return match.replace(importPath, importPath + '/index.js')
35+
} else {
36+
// File import - add .js
37+
modified = true
38+
return match.replace(importPath, importPath + '.js')
39+
}
40+
}
41+
)
42+
43+
// Fix export statements as well
44+
content = content.replace(
45+
/export\s+(?:\*|\{[^}]*\})\s+from\s+['"](\.[^'"]*?)['"];?/g,
46+
(match, importPath) => {
47+
// Skip if already has extension or is directory import
48+
if (path.extname(importPath) || importPath.endsWith('/')) {
49+
return match
50+
}
51+
52+
// Check if this is a directory import (look for index file)
53+
const fullPath = path.resolve(path.dirname(filePath), importPath)
54+
const indexPath = path.join(fullPath, 'index.d.ts')
55+
56+
if (fs.existsSync(indexPath)) {
57+
// Directory import - add /index.js
58+
modified = true
59+
return match.replace(importPath, importPath + '/index.js')
60+
} else {
61+
// File import - add .js
62+
modified = true
63+
return match.replace(importPath, importPath + '.js')
64+
}
65+
}
66+
)
67+
68+
// Fix dynamic import() type expressions
69+
content = content.replace(
70+
/import\(['"](\.[^'"]*?)['"]\)/g,
71+
(match, importPath) => {
72+
// Skip if already has extension or is directory import
73+
if (path.extname(importPath) || importPath.endsWith('/')) {
74+
return match
75+
}
76+
77+
// Check if this is a directory import (look for index file)
78+
const fullPath = path.resolve(path.dirname(filePath), importPath)
79+
const indexPath = path.join(fullPath, 'index.d.ts')
80+
81+
if (fs.existsSync(indexPath)) {
82+
// Directory import - add /index.js
83+
modified = true
84+
return match.replace(importPath, importPath + '/index.js')
85+
} else {
86+
// File import - add .js
87+
modified = true
88+
return match.replace(importPath, importPath + '.js')
89+
}
90+
}
91+
)
92+
93+
if (modified) {
94+
fs.writeFileSync(filePath, content, 'utf8')
95+
console.log(`Fixed imports in: ${path.relative(process.cwd(), filePath)}`)
96+
}
97+
}
98+
99+
function walkDirectory(dir) {
100+
const files = fs.readdirSync(dir)
101+
102+
for (const file of files) {
103+
const filePath = path.join(dir, file)
104+
const stat = fs.statSync(filePath)
105+
106+
if (stat.isDirectory()) {
107+
walkDirectory(filePath)
108+
} else if (file.endsWith('.d.ts')) {
109+
fixImportsInFile(filePath)
110+
}
111+
}
112+
}
113+
114+
function main() {
115+
if (!fs.existsSync(TYPES_DIR)) {
116+
console.log('Types directory does not exist, skipping declaration file fixing')
117+
return
118+
}
119+
120+
console.log('Fixing TypeScript declaration file imports by adding .js extensions...')
121+
walkDirectory(TYPES_DIR)
122+
console.log('Declaration file import fixing completed')
123+
}
124+
125+
main()

0 commit comments

Comments
 (0)