ES Modules version #1

Open
opened 2021-09-22 17:40:32 +00:00 by coolaj86 · 1 comment
Owner

2024 Update

FULL ESM support has finally landed in Node in 2024 (and Browsers in 2023), but we won't switch this package back to ESM until it's been in LTS for a while.

Original

There's an esmodule branch which has this an as ES Module using import, but there is literally NO benefit to using this in that way.

It works with other ES Modules as-is.

Transforming it to an ES Module itself just breaks backwards compatibility with 99.999% of the npm ecosystem that uses Common JS.

# 2024 Update FULL ESM support has finally landed in Node in 2024 (and Browsers in 2023), but we won't switch this package back to ESM until it's been in LTS for a while. # Original There's an [`esmodule`](https://git.rootprojects.org/root/walk.js/src/branch/esmodule) branch which has this an as ES Module using import, but there is literally NO benefit to using this in that way. It works with other ES Modules as-is. Transforming it to an ES Module itself just breaks backwards compatibility with 99.999% of the npm ecosystem that uses Common JS.
Author
Owner

The Bottom Line

As of September 2024, now it's time.

  • browser support is there
  • node support is there, but not final
    • --experimental-require-module
  • DO NOT use top-level await for published libraries (libraries don't need this anyway)
  • be kind, use single default export

HOWEVER, that's for new packages. Existing packages (like @root/walk) should wait until Node support has hit LTS before making the switch - probably when Node v25 or v27 comes out - otherwise things downstream will just break.h

(this also makes basetag obsolete for relative paths, see also https://github.com/janniks/basetag/issues/31)

3 Module Maps

It's a little annoying, but you do have to specify your module maps 3 different times in 3 slightly different (but strictly particular) formats:

  • JavaScript (WHATWG)
  • Node (package.json)
  • tsc (jsconfig.json)

JavaScript / WHATWG

<script type="importmap">
  {
    "imports": {
      "@root/passkey": "./passkey.js",
      "@root/passkey/": "./lib/",
      "localstore": "./vendor/localstore.js"
    }
  }
</script>

app.js:

import Passkey from "@root/passkey";

// ...

Node / package.json

{
  "name": "@root/passkey",
  "main": "passkey.js",
  "type": "module",
  "files": [
    "./passkey.js",
    "./lib/",
    "./vendor/"
  ],
  "exports": {
    ".": "./passkey.js",
    "./*": "./*"
  },
  "imports": {
    "@root/passkey": "./passkey.js",
    "@root/passkey/": "./lib/",
    "localstore": "./vendor/localstore.js"
  }
}

passkey.test.js:

import Passkey from "@root/passkey";

// ...

tsc / jsconfig.json

jsconfig.json:

{
  "compilerOptions": {
    "paths": {                                           /* Specify a set of entries that re-map imports to additional lookup locations. */
      "passkey": ["./passkey.js"],
      "passkey/*": ["./lib/*"],
      "localstore": ["./vendor/localstore.js"]
    }
  }
}

Note: this is the same format as tsconfig.json.

Original

I'm revisiting the idea of switching to ESM

Now that ESM has finally achieved "baseline" browser support (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap), and there's been some time for people to update their browsers and phones, there could be real benefit to switching.

I came back here in hopes of having more of an explanation. I'm disappointed in my past self for being so angsty and vague.

Will go figure it out again now and report back...

Update

Browser Support (Baseline since March 2023)

Browsers have had partial support for years, but full support is finally "Baseline" (meaning "ubiquitous", "ALL Evergreen Browsers").

See:

Node Support (Experimental since ~March of 2024)

As of v22 it's still behind the --experimental-require-module feature flag, but I'm going to bet on it being included.

// Now supported. No longer throws ERR_REQUIRE_ESM (except for top-level await)
let Foo = require('esm-foo.mjs');

It's really important to understand that this went from years of wontfix to now experimental - this means that the hangup was more political than technical. With those winds having changed, it is extremely unlikely to be killed off at this stage.

The big caveat is that modules MUST NOT use a top-level await - those must be couched in an async function init() {} or async function main() {}, or some caching mechanism that does whatever needs to be done on first use.

See:

Tedium

The tedious part is that there are 3 similar-but-different package/dependency formats: importmap, package.json, and tsconfig.json.

I'm not sure that the tooling to manage all 3 simultaneously exists - especially since this is sidestepped in most existing environments by bundlers with bespoke methods - but it's certainly doable by hand, and the tooling is fairly trivial (just get your trailing /s and *s in the right place).

Recommendation

It's time to switch to ESM for libraries with the constraint of a single default export and not using top-level await. Typically only apps need top-level await - not libraries - so this shouldn't be a big hangup.

Rationalé

Having "Baseline" browser support for over 18 months means that almost everyone should have had a power outage, computer crash, or forced update to get support by now. No doubt there are some very old Androids and iPhones that will never be updated, but they limit to 0 as time goes on.

I see "Experimental" Node support for this as a signal that they're exploring edge cases, and too much of a "no-brainer" (much like fetch) to get dropped. Bun will follow Node, and I don't think Deno supports top-level require anyway.

By definition, early adopters typically aren't on a super old Android phone, a version an old version of node.

As whatever you're building today reaches the early majority, majority, and late majority, the unsupported devices, frameworks, etc, become vanishingly few.

Time to do it.

# The Bottom Line As of September 2024, now it's time. - browser support is there - node support is there, but not final - `--experimental-require-module` - DO NOT use top-level await for published libraries (libraries don't need this anyway) - be kind, use single default export HOWEVER, that's for _new_ packages. Existing packages (like `@root/walk`) should wait until Node support has hit LTS before making the switch - probably when Node v25 or v27 comes out - otherwise things downstream will just break.h (this also makes `basetag` obsolete for relative paths, see also <https://github.com/janniks/basetag/issues/31>) ## 3 Module Maps It's a little annoying, but you do have to specify your module maps 3 different times in 3 slightly different (but strictly particular) formats: - JavaScript (WHATWG) - Node (package.json) - tsc (jsconfig.json) ### JavaScript / WHATWG ```html <script type="importmap"> { "imports": { "@root/passkey": "./passkey.js", "@root/passkey/": "./lib/", "localstore": "./vendor/localstore.js" } } </script> ``` `app.js`: ```js import Passkey from "@root/passkey"; // ... ``` ### Node / package.json ```json { "name": "@root/passkey", "main": "passkey.js", "type": "module", "files": [ "./passkey.js", "./lib/", "./vendor/" ], "exports": { ".": "./passkey.js", "./*": "./*" }, "imports": { "@root/passkey": "./passkey.js", "@root/passkey/": "./lib/", "localstore": "./vendor/localstore.js" } } ``` `passkey.test.js`: ```js import Passkey from "@root/passkey"; // ... ``` ### tsc / jsconfig.json `jsconfig.json`: ```json5 { "compilerOptions": { "paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */ "passkey": ["./passkey.js"], "passkey/*": ["./lib/*"], "localstore": ["./vendor/localstore.js"] } } } ``` Note: this is the same format as `tsconfig.json`. # Original I'm revisiting the idea of switching to ESM Now that ESM has finally achieved "baseline" browser support (<https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap>), and there's been some time for people to update their browsers and phones, there could be real benefit to switching. I came back here in hopes of having more of an explanation. I'm disappointed in my past self for being so angsty and vague. Will go figure it out again now and report back... # Update ## Browser Support (Baseline since March 2023) Browsers have had partial support for years, but full support is finally "Baseline" (meaning "ubiquitous", "*ALL* Evergreen Browsers"). See: - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap ## Node Support (Experimental since ~March of 2024) As of v22 it's still behind the `--experimental-require-module` feature flag, but I'm going to bet on it being included. ```js // Now supported. No longer throws ERR_REQUIRE_ESM (except for top-level await) let Foo = require('esm-foo.mjs'); ``` It's _really_ important to understand that this went from _years_ of `wontfix` to now `experimental` - this means that the hangup was more _political_ than _technical_. With those winds having changed, it is extremely unlikely to be killed off at this stage. The big caveat is that modules _MUST NOT_ use a `top-level` await - those must be couched in an `async function init() {}` or `async function main() {}`, or some caching mechanism that does whatever needs to be done on first use. See: - https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require - https://joyeecheung.github.io/blog/2024/03/18/require-esm-in-node-js/ ## Tedium The tedious part is that there are 3 similar-but-different package/dependency formats: `importmap`, `package.json`, and `tsconfig.json`. I'm not sure that the tooling to manage all 3 simultaneously exists - especially since this is sidestepped in most existing environments by bundlers with bespoke methods - but it's certainly doable by hand, and the tooling is fairly trivial (just get your trailing `/`s and `*s` in the right place). ## Recommendation It's time to switch to ***ESM for libraries*** with the constraint of ***a single default export*** and ***not using top-level `await`***. Typically only apps need top-level await - not libraries - so this shouldn't be a big hangup. ### Rationalé Having "Baseline" browser support for over 18 months means that almost everyone should have had a power outage, computer crash, or forced update to get support by now. No doubt there are some very old Androids and iPhones that will never be updated, but they limit to 0 as time goes on. I see "Experimental" Node support for this as a signal that they're exploring edge cases, and too much of a "no-brainer" (much like fetch) to get dropped. Bun will follow Node, and I don't think Deno supports top-level require anyway. By definition, _early adopters_ typically aren't on a super old Android phone, a version an old version of node. As whatever you're building today reaches the early majority, majority, and late majority, the unsupported devices, frameworks, etc, become vanishingly few. Time to do it.
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: root/walk.js#1
No description provided.