#!/usr/bin/env node /** * Post-build script: Adds .js extensions to relative import specifiers in compiled ESM output. * * TypeScript with moduleResolution:"Bundler" emits extensionless imports (e.g., from './foo'). * Node.js ESM requires explicit .js extensions. This script rewrites all relative imports * in dist/ to add .js extensions where missing. */ import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs'; import { join, extname, dirname, resolve } from 'path'; const DIST_DIR = new URL('./dist/src/', import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1'); let fixed = 0; let files = 0; function processFile(filePath) { if (!filePath.endsWith('.js') && !filePath.endsWith('.d.ts')) return; files++; const fileDir = dirname(filePath); let content = readFileSync(filePath, 'utf8'); let changed = false; // Match: from './...' or from '../...' (relative imports without .js extension) // Also match: import('./...') (dynamic imports in .d.ts files) // Also match: import './...'; (bare side-effect imports) const addExt = (match, prefix, specifier, suffix) => { const ext = extname(specifier); if (ext === '.js' || ext === '.mjs' || ext === '.cjs' || ext === '.json') { return match; } // Check if specifier points to a directory with index.js const resolved = resolve(fileDir, specifier); if (existsSync(resolved) && statSync(resolved).isDirectory()) { changed = true; return `${prefix}${specifier}/index.js${suffix}`; } changed = true; return `${prefix}${specifier}.js${suffix}`; }; const updated = content .replace(/(from\s+['"])(\.\.?\/[^'"]+)(['"])/g, addExt) .replace(/(import\(['"])(\.\.?\/[^'"]+)(['"]\))/g, addExt) .replace(/(import\s+['"])(\.\.?\/[^'"]+)(['"])/g, addExt); if (changed) { writeFileSync(filePath, updated); fixed++; } } function walk(dir) { for (const entry of readdirSync(dir)) { const full = join(dir, entry); if (statSync(full).isDirectory()) { walk(full); } else { processFile(full); } } } walk(DIST_DIR); console.log(`Fixed ${fixed}/${files} files with .js extensions`);