Goal is to collect the list of ESM-CJS interoperability issues known to Vite's team.
In order to make Vite's native SSR API rock solid.
TypeScript transpiles ESM to CJS like this:
// ESM
export default 'hi'
export const msg = 'hello'
// CJS
"use strict";
exports.__esModule = true;
exports.msg = void 0;
exports["default"] = 'hi';
exports.msg = 'hello';
TODO: I don't know the purpose of the line exports.msg = void 0;
; seems superfluous?
In CJS, exports
always denotes the default export.
// hi.js (CJS)
exports["default"] = 'hi';
exports.msg = 'hello';
// CJS
const moduleDefaut = require('./hi.js')
const { msg } = require('./hi.js')
console.log(moduleDefaut)
console.log(msg)
Prints:
{ default: 'hi', msg: 'hello' }
hello
This means that the ESM default lives at moduleDefaut.default
. (See previous section about how TypeScript transpiles ESM to CJS.)
Node.js doesn't support the __esModule
compatibility layer.
// hi.mjs (ESM)
export default 'hi'
export const msg = 'hello'
const imported = await import('./hi.mjs')
console.log(imported)
prints:
{ default: 'hi', msg: 'hello' }
Whereas
// hi.ts (ESM, TypeScript)
export default 'hi'
export const msg = 'hello'
which TypeScript transpiles to
// hi.js (CJS)
"use strict";
exports.__esModule = true;
exports.msg = void 0;
exports["default"] = 'hi';
exports.msg = 'hello';
const imported = await import('./hi.js')
console.log(imported)
Prints:
{
__esModule: true,
default: { __esModule: true, msg: 'hello', default: 'hi' },
msg: 'hello'
}
Node.js is working on supporting __esModule
, see nodejs/node#40891 and nodejs/node#40902.
But this will never be Node.js's default behavior as it would break existings app.
Also note how exports["default"]
is overwritten with exports
, leading to our next observation.
When loading CJS from ESM, the default export is overwritten.
exports["default"] = 'hi';
exports.msg = 'hello';
// CJS
const moduleExports = require('./hi.js')
console.log(moduleExports)
Prints:
{ default: 'hi', msg: 'hello' }
While
// ESM
const moduleExports = await import('./hi.js')
console.log(moduleExports)
prints:
{
default: { default: 'hi', msg: 'hello' },
msg: 'hello'
}
Which makes sense considering our previous observations.
When loading CJS from ESM, non-statically-analysable exports are missing.
// CJS
exports.msg = 'hello';
const key = 'msg2';
exports[key] = 'bonjour';
// ESM
const moduleExports = await import('./hi.js')
console.log(moduleExports)
Prints:
{
default: { msg: 'hello', msg2: 'bonjour' },
msg: 'hello'
}
Note how msg
is present at the root while msg2
is missing.
The only way to access msg2
is over default.msg2
,
whereas msg
can be accessed directly.