(Browser-first; works in TS & JS; supports ESM + CJS)
- Enable "declaration": true in tsconfig.json.
- Set "types" in package.json → e.g. "dist/index.d.ts".
- Build both ESM and CJS.
- Use Node conditional exports in package.json: { "exports": { ".": { "import": "./esm/index.js", "require": "./cjs/index.js" } } }
- Keep "main" pointing to CJS for legacy tools.
- If ESM-first: set "type": "module".
- Restrict publish using "files" or .npmignore.
- Ensure "exports", "main", "types" are correct.
- Automate dual builds using:
- Generate source maps.
- Ship unminified builds; let consumers optimize.
- Avoid forcing esModuleInterop or path aliases downstream.
- Stick to standard import/export.
- Compile to ES2018+ (or lowest you need).
- Include source maps for debug.
- Browser: avoid Node APIs unless polyfilled.
- Node ESM: no __dirname/__filename. Use import.meta.url.
- Use named exports only.
- Add "sideEffects": false if safe. Ref: webpack docs.
- Keep deps minimal.
- Use peerDependencies for frameworks (e.g. React).
- Test packaged build in:
- TS project (import + type checks)
- JS project (require + bundler import)
- CI matrix: Node + browser/bundler.
- Always ship CJS alongside ESM → works with require() + import.
- Dual package is future-proof; see Node ESM & CJS guide.
- For Node-only libs: ESM-only possible, but document and consider fallback.
- Replace __dirname with import.meta equivalents in ESM.