I am not sure yet if it is realistic, I'm not even sure if this all makes sense, but I want to find a way to build and publish a JavaScript package that works both in the Browser and in Node with the minimal possible effort. With commonjs and esm. With support for treeshaking. Without going crazy on configuration or build steps and without changing the way you're working right now (or at least without changing it too much).
The main reason for this investigation is a (still unresolved) issue I got in an open source project of mine. At the time of writing this, I have absolutely no idea how an ideal solution to that issue could look like.
- browser support by directly embedding a transpiled/bundled UMD version via
<script src></script>
- browser support for
<script type="module" src></script>
- full backwards-compatibilty with commonjs and
require()
in Node - tree-shaking support for bundlers like Parcel.js, Rollup or Webpack
- work effortlessly with the current state of Node's experimental support for ECMAScript Modules (
import x from 'x'
, without additional build step required) - stick with Babel + Webpack for transpiling and bundling the library that is published to npm
- do not require users to make changes to their existing build tools
- Do browsers understand
.cjs
file endings without explicitly sending a application/javascript header (assumption: they do) - Does webpack and other bundlers find
.cjs
files without explicit configuration (assumption: as long as themain
property in package.json points to a.cjs
file, they do) - How do source files need to be built/transpiled so that they can be tree-shaken by webpack et al.?
- What must happen so that Node can
import
particular ESModules without complaining
- Node's experimental implementation of ESModules had breaking changes/different behavior between versions
12.11.0
→12.11.1
,12.12.0
,12.13.0
→12.13.1
whentype
is set tomodule
in package.json. - Native ESModules can only be imported in Node if the file that is importing the ESModule is ending on
.mjs
or if it is inside of a project that has"type":"module"
defined in its package.json. In this case.js
also works. - When importing from a relative ESModule the
import pathimport identifier must contain the file suffix (import add from './add.js'
works whileimport add from './add'
does not). This is according to the current spec and matches Browser behavior as far as I can tell. - On the other side, if you do have
"type":"module"
in your package.json, you can use.cjs
for commonjs modules. - LOL: when importing
./example/es/index.js
with ESLint witheslint-plugin-unicorn
and autofix enabled, theunicorn/import-index
rule automatically shortens the path to./example/es
making the script fail. - Unless
"type":"module"
is set in package.json, files need to be named.mjs
to tell Node it's an ESModule. Babel, however, does not yet support writing file extentions other than.js
. There's been an open PR to add a new--out-file-extension
option tobabel-cli
but it hasn't been merged yet. Update: will probably be released with Babel 7.8! - That also means that at the time of writing there seems to be no way to safely generate ES Modules in Babel without setting
"type":"modules"
for the complete package due to the lack of support for.mjs
as output file extension. - There is an open issue in TypeScript to support writing compiled files with a
.mjs
file-extension. Until this is done, there's also no safe support for.mjs
as indicator for ES Modules. - @karlhorky pointed me to babel-esm-plugin which looks helpful until #9144 is merged. Will investigate.
- VSCode 1.40.2 (and probably other editors and IDEs) do not treat
.cjs
files as JavaScript but useplaintext
instead. This can be configured by setting:"files.associations": { "*.cjs": "javascript" }
.mjs
however, is correctly detected as JavaScript. - Once
"type":"module"
is set, you can't use.babelrc.js
orwebpack.config.js
anymore but you must stricly use.cjs
and rename them.babelrc.cjs
andwebpack.config.cjs
. That is because@babel/core
is still usingrequire()
to load config files. However, Babel looks for the existence of a.babelrc.cjs
file automatically (source). Webpack does not. You have to add--config webppack.config.cjs
explicitly. - There is a new
exports
property inpackage.json
for Node. It is a map containing aliases to tell Node where to look for imports. See Node docs on ECMAScript Modules for more info.
- https://twitter.com/MylesBorins/status/1202686414896300033
- https://twitter.com/mjackson/status/1202650812159184896
- graphql/graphql-js#2277
- https://medium.com/@nodejs/announcing-a-new-experimental-modules-1be8d2d6c2ff
- https://nodejs.org/api/esm.html
- https://github.com/rollup/rollup/wiki/pkg.module
- parcel-bundler/parcel#3545
- https://twitter.com/wesbos/status/1205159427491414017
- https://github.com/vanillaes
If you wanna try it out yourself:
yarn
npx lerna bootstrap --force-local
More docs on that coming soon
tbd.