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.
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.
- Require should get renamed to nativRequire to better reflect what it realy realy does!
- .mjs should not get used as entrypoint for NodeJS Applications or Libs that get used to bootstrap Environments
- 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
- 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
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.
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!