Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save frank-dspeed/485efddda0c8a32913bde22794f185de to your computer and use it in GitHub Desktop.
Save frank-dspeed/485efddda0c8a32913bde22794f185de to your computer and use it in GitHub Desktop.
The Myth about ESM Module System as Answer to everything. in the ECMAScript World

The Myth about ESM Module System as Answer to everything. in the ECMAScript World

The Problem is easy to Explain the ECMAScript Engine Boots Sync for good reason and ESModules are designed to be Async by default as they have to come from a external source.

So in some development strategies you need Sync behavior from start till end this is not archive able with ESModules again by design!

In general everything that interacts with the nativeCode of the engine needs to use so called NativeRequire a function that exposes modules that are loaded inside the Engine it self not the JS Scope then it exposes that loaded NativModule via Bindings in the JS Scope. NativModule Loading is a Sync Prozess even if the lib is dynamic linked. Always remember the Core JS ES Principle that it is Strict Sync and then returns to the Async stack and again processes fully sync.

so the flow looks like sync => registers ASYNC Listneres => returns to sync cycling all listners so repeats until programm exit.

in JS ES we call that the Stack we produce the Environment the JS Context sync which will then start prozessing sync and will run till it exits. when you start your prozess for example a nodejs application with a .mjs file you do not get a Async Engine! You get the same Sync Engine executing import('asyncImportedEsm.mjs') inside a fresh new Sync CJS Context that directly finishes its first iteration after registering the import.

What did we do wrong

We should not have Implemented top level ESM Support for NodeJS as it confuses People the browser also has no top level ESM Entrypoint support the DOM is always generated with the Global Sync JS Scope.

What needs to get corrected

  1. Require should get renamed to nativRequire to better reflect what it realy realy does!
  2. .mjs should not get used as entrypoint for NodeJS Applications or Libs that get used to bootstrap Environments
  3. Authoring with the ES6+ Module Syntax is good if you plan to release it in 2 versions anyway else if you want to go single version simply always produce .cjs code avoid the ESM Syntax for exports but follow all rules of the so called namedExports pattern only use exports and assign string propertys to that avoid module.exports avoid the default property on exports and also as export default in esm
  4. when default is not avoid able for any reason you need to export a object that has the same propertys as the one that your exporting already. cjs
exports.first = 'first'
exports.secund = ()=>'secund'
module.exports = exports;  // If you do not supply a default or module.exports that is also the default behavior of every engine that it does auto set that 

esm

export const first = 'first'
export const secund = ()=>'secund'
export default = { first, secund };
export const first = 'first' // Enables import { first } from './self.mjs'
export const secund = () => 'secund'
export default = { first, secund }; // Enables import myMod from './self.mjs' usage myMod.first
// If you do not supply a default that is also the default behavior of every engine that it does auto set that 

Example make Express Compatible

import express from 'express' // This works because it is CJS only and this syntax is the shorthand for { default: express }
export const expressApp = express
export const expressRouter = express.Router
export const expressStatic = express.static
export default { expressApp, expressRouter, expressStatic } // If you do not supply a default that is also the default behavior of every engine that it does auto set that 

express is a good example as it module.exports a so called class which is not a namedProperty on Exports its a Constructor function with the name express on this class are some methods that are also class constructors the Router and static middleware that they bundle are assigned as propertys on the module.exports exported express class.

we now split that up and turn the names into camel Case Names to indicate that this are constructors methods that do not need the new keyword.

this introduces the following usage changes

const { expressApp, expressRouter, expressStatic } = require('compatible-express')
const app = expressApp();
const router = expressRouter();
app.use(expressStatic('./static'))
const express = require('compatible-express')
const { expressApp, expressRouter, expressStatic } = express;
const app = expressApp();
const router = expressRouter();
app.use(expressStatic('./static'))
const express = require('compatible-express')
const app = express.expressApp();
const router = express.expressRouter();
app.use(express.expressStatic('./static'))

but the good is that works with ESM Also

import { expressApp, expressRouter, expressStatic } from 'compatible-express'
const app = expressApp();
const router = expressRouter();
app.use(expressStatic('./static'))
import express from 'compatible-express'
const app = express.expressApp();
const router = express.expressRouter();
app.use(express.expressStatic('./static'))

this way you can distribute it with a single type definition file for example that always works even if transpiled to esm.

Extra bonus

When you bundle that you get with most bundlers if they work correct nice objects that are named like the namedExports before so the types even stay lets rebundle

export * from 'compatible-express' // exports expressStatic, expressApp and so on
export * from './myapp.mjs' // exports myApp
import { expressStatic, myApp } from 'rebundled';
// This is the final goal that you wanted to archive consistent code!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment