Skip to content

Commit 0a81ffc

Browse files
authored
v2025.08.07
August 2025 update to add cjs requirement to make esm work in edge cases
1 parent 201f0f6 commit 0a81ffc

File tree

1 file changed

+230
-7
lines changed

1 file changed

+230
-7
lines changed

README.md

Lines changed: 230 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
1+
# Fundementals of ESM/CJS modules and their Heresies
2+
3+
<!-- TOC start (generated with https://github.com/derlin/bitdowntoc) -->
4+
15
- [`std.module.format` ](#stdmoduleformat)
2-
+ [ES6 exports / imports cheat sheet](#es6-exports--imports-cheat-sheet)
6+
+ [Different Types of Imports and Exports](#different-types-of-imports-and-exports)
7+
* [The Special Case of `export default`](#the-special-case-of-export-default)
8+
+ [Why the Difference?](#why-the-difference)
9+
* [The Third Way: `export { thing as default }`](#the-third-way-export-thing-as-default-)
10+
+ [Special Case: `export default function`](#special-case-export-default-function)
11+
* [Circular Dependencies and Hoisting](#circular-dependencies-and-hoisting)
12+
+ [Function Hoisting Behavior](#function-hoisting-behavior)
13+
+ [Circular Dependencies Example](#circular-dependencies-example)
14+
* [Summary](#summary)
15+
* [Why you may need to publish CJS to have ESM work](#why-you-may-need-to-publish-cjs-to-have-esm-work)
16+
* [Usage](#usage)
17+
+ [Multiple Entries](#multiple-entries)
18+
+ [Custom Files](#custom-files)
19+
+ [Best Practices](#best-practices)
20+
- [Named exports (blue boxes) create live references when imported through any import syntax](#named-exports-blue-boxes-create-live-references-when-imported-through-any-import-syntax)
21+
- [Default exports (orange boxes) behave differently:](#default-exports-orange-boxes-behave-differently)
22+
- [Circular dependencies behavior:](#circular-dependencies-behavior)
23+
+ [ES6 exports / imports cheat sheet](#es6-exports-imports-cheat-sheet)
324
* [`package.json` TLDR](#packagejson-tldr)
425
* [Problematic Settings](#problematic-settings)
5-
+ [~~Dont use `browser` field~~ Use `browser` specifically for CDN's](#dont-use-browser-field-use-browser-specifically-for-cdns)
26+
+ [~~Dont use `browser` field~~ Use `browser` specifically for CDN's, YMMV per module usage](#dont-use-browser-field-use-browser-specifically-for-cdns-ymmv-per-module-usage)
627
+ [Restrictions on Import Assertions Under ](#restrictions-on-import-assertions-under)
728
+ [dont use `preserveModules` ](#dont-use-preservemodules)
829
+ [`moduleResolution` `NodeNext` only allows defined import paths ](#moduleresolution-nodenext-only-allows-defined-import-paths)
@@ -21,10 +42,10 @@
2142
+ [Cheatsheet](#cheatsheet)
2243
* [Avoid Default Exports and Prefer Named Exports](#avoid-default-exports-and-prefer-named-exports)
2344
+ [Context](#context)
24-
+ [Summary](#summary)
45+
+ [Summary](#summary-1)
2546
* [Decision](#decision)
2647
+ [ECMAScript Module Support in Node.js](#ecmascript-module-support-in-nodejs)
27-
+ [`.mjs`, `.cjs`, == `.mts`, `.cts` && `.d.mts` and `.d.cts`.](#mjs-cjs--mts-cts--dmts-and-dcts)
48+
+ [`.mjs`, `.cjs`, == `.mts`, `.cts` && `.d.mts` and `.d.cts`.](#mjs-cjs-mts-cts-dmts-and-dcts)
2849
* [Avoid Export Default](#avoid-export-default)
2950
+ [Poor Discoverability](#poor-discoverability)
3051
+ [Autocomplete](#autocomplete)
@@ -35,11 +56,11 @@
3556
+ [Dynamic Imports](#dynamic-imports)
3657
+ [ES Module Interop](#es-module-interop)
3758
* [Note](#note)
38-
+ [Needs two lines for non-class / non-function](#needs-two-lines-for-non-class--non-function)
39-
+ [React.js - Named Exports](#reactjs---named-exports)
59+
+ [Needs two lines for non-class / non-function](#needs-two-lines-for-non-class-non-function)
60+
+ [React.js - Named Exports](#reactjs-named-exports)
4061
* [{} type](#-type)
4162
+ [If you want a type that means "empty object"](#if-you-want-a-type-that-means-empty-object)
42-
+ [If you are using React, and you want to define `type Props = {}`.](#if-you-are-using-react-and-you-want-to-define-type-props--)
63+
+ [If you are using React, and you want to define `type Props = {}`.](#if-you-are-using-react-and-you-want-to-define-type-props-)
4364
+ [`GenericObject`](#genericobject)
4465
+ [Module-related host hooks](#module-related-host-hooks)
4566
* [Customizing module resolution](#customizing-module-resolution)
@@ -50,6 +71,8 @@
5071
+ [Use a compatibility layer](#use-a-compatibility-layer)
5172
* [License](#license)
5273

74+
<!-- TOC end -->
75+
5376
<a name="stdmoduleformat"></a>
5477
# `std.module.format`
5578

@@ -219,6 +242,206 @@ export default thing;
219242
export default 'hello!';
220243
```
221244

245+
## Why you may need to publish CJS to have ESM work
246+
247+
> `cjyes` [![npm version](https://img.shields.io/npm/v/cjyes.svg)](https://www.npmjs.org/package/cjyes)
248+
249+
> source: <https://gist.github.com/developit/96de429483bb98927c7cd27c773b0fff>
250+
251+
If you're publishing ES Modules, you need to also publish CommonJS versions of those modules.
252+
253+
This isn't to support old browsers or Node versions: even in Node 14, using `require()` to load a module won't work if it's only available as ESM.
254+
255+
`cjyes` is the bare minimum fix for this problem. You write ES Modules and fill out a valid `package.json`, and it'll generate the corresponding CommonJS files pretty much instantly. `cjyes` takes up 500kb of disk space including its two dependencies.
256+
257+
## Usage
258+
259+
The easiest way to use `cjyes` is to define [package exports](https://nodejs.org/api/esm.html#esm_conditional_exports) the way Node 13+ requires:
260+
261+
```json
262+
{
263+
"main": "index.mjs",
264+
"exports": {
265+
"import": "./index.mjs",
266+
"require": "./dist/index.cjs"
267+
},
268+
"scripts": {
269+
"prepare": "cjyes"
270+
},
271+
"devDependencies": {
272+
"cjyes": "^0.3.0"
273+
}
274+
}
275+
```
276+
277+
`cjyes` will create CommonJS versions of all modules listed in the `"exports"` field and place them at the specified locations.
278+
279+
> You can also use `.js` file extensions and the `{"type":"module"}` field - cjyes will detect this and generate the required `.cjs` output files.
280+
281+
### Multiple Entries
282+
283+
Multiple entry points are supported automatically. Simply define them in your export map:
284+
285+
```json
286+
{
287+
"main": "index.mjs",
288+
"exports": {
289+
".": {
290+
"import": "./index.mjs",
291+
"require": "./index.cjs"
292+
},
293+
"./jsx": {
294+
"import": "./jsx.mjs",
295+
"require": "./jsx.cjs"
296+
},
297+
"./hooks": {
298+
"import": "./hooks/index.mjs",
299+
"require": "./hooks/index.cjs"
300+
}
301+
},
302+
"scripts": { "prepare": "cjyes" },
303+
"devDependencies": { "cjyes": "^0.3.0" }
304+
}
305+
```
306+
307+
### Custom Files
308+
309+
It is also possible to pass a list of input modules to `cjyes` directly:
310+
311+
```sh
312+
cjyes src/index.js src/other.mjs
313+
# generates the following:
314+
# dist/
315+
# index.cjs
316+
# other.cjs
317+
```
318+
319+
<details>
320+
<summary> cjyes.js code
321+
322+
</summary>
323+
324+
~~~javascript
325+
#! /usr/bin/env node
326+
327+
const path = require('path');
328+
const fs = require('fs').promises;
329+
const MagicString = require('magic-string').default;
330+
const { parse } = require('es-module-lexer');
331+
332+
const ALIASES = {
333+
'-v': 'verbose',
334+
'-s': 'silent',
335+
'-d': 'dry'
336+
};
337+
const FLAGS = {
338+
default: 'Force `exports.default=` instead of `module.exports=`',
339+
flat: 'Force merging named exports into default (module.exports=A;exports.B=B)',
340+
dry: `Don't write anything to disk [-d]`,
341+
verbose: 'Verbose output logging [-v]',
342+
silent: 'No output logging [-s]'
343+
};
344+
run(process.argv.slice(2))
345+
.then(() => process.exit(0))
346+
.catch(err => (console.error(err), process.exit(1)));
347+
348+
async function run(argv) {
349+
const flags = {};
350+
const files = argv.filter(file => {
351+
return !((file in ALIASES || file.startsWith('--')) && (flags[ALIASES[file] || file.substring(2)] = true));
352+
});
353+
if (flags.help) return console.log(`cjyes [...files]\nOptions:\n ${Object.keys(FLAGS).map(k=>`--${k.padEnd(7)} ${FLAGS[k]}`).join('\n ')}`);
354+
let pkg;
355+
try {
356+
pkg = JSON.parse(await fs.readFile('package.json','utf-8'));
357+
} catch (e) {}
358+
if (files.length === 0 && pkg && pkg.exports) {
359+
crawl(pkg.exports, files);
360+
if (flags.verbose) {
361+
console.log(`[cjyes] Using files listing from Export Map:\n ${files.join('\n ')}`);
362+
}
363+
}
364+
const ctx = {};
365+
return Promise.all(files.map(f => cjs(f, { flags, pkg, ctx })))
366+
}
367+
368+
async function cjs(file, { flags, pkg, ctx }) {
369+
const code = await fs.readFile(file, 'utf-8');
370+
const out = new MagicString(code);
371+
const [imports, exports] = await parse(code, file);
372+
for (const imp of imports) {
373+
const spec = JSON.stringify(code.substring(imp.s, imp.e));
374+
const s = code.substring(imp.ss + 6, imp.s - 1).replace(/\s*from\s*/g, '');
375+
const r = `const ${s.replace(/\sas\s/g, ':')} = require(${spec})`;
376+
out.overwrite(imp.ss, imp.se, r);
377+
}
378+
const nonDefaultExports = exports.filter(p => p!=='default');
379+
const defaultExport = !flags.flat && (flags.default || nonDefaultExports.length) ? 'exports.default=' : 'module.exports=';
380+
const t = /(^|[;\s(])export(\s*default)?(?:\s*{[^}]+}|\s+(function|const|let|var))/g;
381+
let token;
382+
while ((token = t.exec(code))) {
383+
const r = `${token[2] ? defaultExport : ''}${token[3] || ''}`;
384+
out.overwrite(token.index + token[1].length, t.lastIndex, r);
385+
}
386+
for (const exp of nonDefaultExports) out.append(`\nexports.${exp}=${exp};`);
387+
388+
let outFile;
389+
// use the export map if one exists:
390+
const entry = './' + file.replace(/\.m?js$/, '').split(path.sep).join('/');
391+
const def = pkg && pkg.exports && resolve(pkg.exports, entry);
392+
if (def) {
393+
if (flags.verbose) {
394+
console.log(`[cjyes] using Export Map entry for ${entry}`);
395+
}
396+
outFile = def.replace(/^\.\//,'').split('/').join(path.sep);
397+
}
398+
else {
399+
// fall back to a dist directory
400+
const ext = pkg && pkg.type === 'module' ? '.cjs' : '.js';
401+
const parts = file.replace(/\.m?js$/, ext).split(path.sep);
402+
const index = parts.lastIndexOf('src');
403+
if (index === -1) parts.unshift('dist');
404+
else parts[index] = 'dist';
405+
outFile = parts.join(path.sep);
406+
if (!flags.silent && !ctx.warned) {
407+
ctx.warned = true;
408+
console.log(`[cjyes] no Export Map found, generating filenames:`);
409+
if (ext=='.cjs') console.log(` - Using .cjs due to {"type":"module"}`);
410+
if (index===-1) console.log(` - Replacing src/ with dist/`);
411+
else console.log(` - Prepending dist/ directory`);
412+
}
413+
}
414+
if (!flags.dry) {
415+
try {
416+
await fs.mkdir(path.dirname(outFile), { recursive: true });
417+
} catch (e) {}
418+
await fs.writeFile(outFile, out.toString());
419+
}
420+
if (!flags.silent) {
421+
console.log(`${file} --> ${outFile}`);
422+
}
423+
}
424+
425+
function crawl(exp, files) {
426+
if (typeof exp==='string') files.push(exp.replace(/^\.\//,''));
427+
else if (exp.import || exp.default) crawl(exp.import || exp.default, files);
428+
else for (let i in exp) {
429+
if (i[0]=='.' && !i.endsWith('/')) crawl(exp[i], files);
430+
}
431+
}
432+
433+
function resolve(exp, entry) {
434+
if (!exp || typeof exp=='string') return exp;
435+
return exp.require || exp.default || resolve(select(exp, entry) || exp['.'], entry);
436+
}
437+
438+
function select(exp, entry) {
439+
for (let i in exp) if (i==entry) return exp[i];
440+
}
441+
~~~
442+
443+
</details>
444+
222445
_Note: The original article was written by Jake Archibald, with contributions from the V8 team members Toon Verwaest, Marja Hölttä, and Mathias Bynens, as well as Dave Herman and Daniel Ehrenberg._
223446

224447
### Best Practices

0 commit comments

Comments
 (0)