All of the above gets much easier with an ES Modules-only approach. Right now not enough people are blogging about it so it’s still overlooked but in 2024 it is a viable and good strategy, especially for Typescript projects. The basic nutshell: use “type”: “module” in package.json and all .js files are expected to be ES Modules. If you need a CommonJS file for some internal package reason (staring daggers at eslint config files) you can use the .cjs extension. (Don’t export .cjs files; Node-like internals only.) All your internal relative imports in Typescript should include the .js file extension. After a while Typescript picks up on that for you and starts to do it too in auto-imports.
At that point you only need “exports” in package.json and with only one type of file to export and a single “front-door” you can boil “exports” all the way down until it looks like the old “main”; “exports”: “./index.js” or something like that.
You don’t need to prebundle your library in this case. Node imports work in every currently supported node version. (Bonus: there’s a built-in unit test harness in Node now, which is a reason to need Node imports now.) All major bundlers if they understand “exports” they understand “type”: “module”. If you want to try working without a bundler, if your dependencies are ES Modules and your package follows this format especially internal relative imports have .js extensions you can test shipping a stripped node_modules and use an importmap to map Node bare package names and use native browser module loading script type=“module” (which can be great).
ES Module-only is really nice and the time for it was yesterday and definitely is now. Simpler package.json, simpler builds, simpler or no bundles (importmap and script type=“module”). I very much recommend it.
All of the above gets much easier with an ES Modules-only approach. Right now not enough people are blogging about it so it’s still overlooked but in 2024 it is a viable and good strategy, especially for Typescript projects. The basic nutshell: use “type”: “module” in package.json and all .js files are expected to be ES Modules. If you need a CommonJS file for some internal package reason (staring daggers at eslint config files) you can use the .cjs extension. (Don’t export .cjs files; Node-like internals only.) All your internal relative imports in Typescript should include the .js file extension. After a while Typescript picks up on that for you and starts to do it too in auto-imports.
At that point you only need “exports” in package.json and with only one type of file to export and a single “front-door” you can boil “exports” all the way down until it looks like the old “main”; “exports”: “./index.js” or something like that.
You don’t need to prebundle your library in this case. Node imports work in every currently supported node version. (Bonus: there’s a built-in unit test harness in Node now, which is a reason to need Node imports now.) All major bundlers if they understand “exports” they understand “type”: “module”. If you want to try working without a bundler, if your dependencies are ES Modules and your package follows this format especially internal relative imports have .js extensions you can test shipping a stripped node_modules and use an importmap to map Node bare package names and use native browser module loading script type=“module” (which can be great).
ES Module-only is really nice and the time for it was yesterday and definitely is now. Simpler package.json, simpler builds, simpler or no bundles (importmap and script type=“module”). I very much recommend it.