Skip to content

Instantly share code, notes, and snippets.

@developit
Last active July 25, 2023 12:54
Show Gist options
  • Save developit/96de429483bb98927c7cd27c773b0fff to your computer and use it in GitHub Desktop.
Save developit/96de429483bb98927c7cd27c773b0fff to your computer and use it in GitHub Desktop.
more-or-less instant command-line ESM to CJS transform. Copies from src to dist. `cjyes src/*.js`

cjyes npm version

🔍 see jay, yes! 🎉 / 👨🏻‍💻 see, JS! 👾 / ⚓️ sea JS ⛴

If you're publishing ES Modules, you need to also publish CommonJS versions of those modules.

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.

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.

Usage

The easiest way to use cjyes is to define package exports the way Node 13+ requires:

{
  "main": "index.mjs",
  "exports": {
    "import": "./index.mjs",
    "require": "./dist/index.cjs"
  },
  "scripts": {
    "prepare": "cjyes"
  },
  "devDependencies": {
	"cjyes": "^0.3.0"
  }
}

cjyes will create CommonJS versions of all modules listed in the "exports" field and place them at the specified locations.

You can also use .js file extensions and the {"type":"module"} field - cjyes will detect this and generate the required .cjs output files.

Multiple Entries

Multiple entry points are supported automatically. Simply define them in your export map:

{
  "main": "index.mjs",
  "exports": {
    ".": {
      "import": "./index.mjs",
      "require": "./index.cjs"
	},
    "./jsx": {
      "import": "./jsx.mjs",
      "require": "./jsx.cjs"
	},
    "./hooks": {
      "import": "./hooks/index.mjs",
      "require": "./hooks/index.cjs"
	}
  },
  "scripts": { "prepare": "cjyes" },
  "devDependencies": { "cjyes": "^0.3.0" }
}

Custom Files

It is also possible to pass a list of input modules to cjyes directly:

cjyes src/index.js src/other.mjs
# generates the following:
# dist/
#    index.cjs
#    other.cjs
#! /usr/bin/env node
const path = require('path');
const fs = require('fs').promises;
const MagicString = require('magic-string').default;
const { parse } = require('es-module-lexer');
const ALIASES = {
'-v': 'verbose',
'-s': 'silent',
'-d': 'dry'
};
const FLAGS = {
default: 'Force `exports.default=` instead of `module.exports=`',
flat: 'Force merging named exports into default (module.exports=A;exports.B=B)',
dry: `Don't write anything to disk [-d]`,
verbose: 'Verbose output logging [-v]',
silent: 'No output logging [-s]'
};
run(process.argv.slice(2))
.then(() => process.exit(0))
.catch(err => (console.error(err), process.exit(1)));
async function run(argv) {
const flags = {};
const files = argv.filter(file => {
return !((file in ALIASES || file.startsWith('--')) && (flags[ALIASES[file] || file.substring(2)] = true));
});
if (flags.help) return console.log(`cjyes [...files]\nOptions:\n ${Object.keys(FLAGS).map(k=>`--${k.padEnd(7)} ${FLAGS[k]}`).join('\n ')}`);
let pkg;
try {
pkg = JSON.parse(await fs.readFile('package.json','utf-8'));
} catch (e) {}
if (files.length === 0 && pkg && pkg.exports) {
crawl(pkg.exports, files);
if (flags.verbose) {
console.log(`[cjyes] Using files listing from Export Map:\n ${files.join('\n ')}`);
}
}
const ctx = {};
return Promise.all(files.map(f => cjs(f, { flags, pkg, ctx })))
}
async function cjs(file, { flags, pkg, ctx }) {
const code = await fs.readFile(file, 'utf-8');
const out = new MagicString(code);
const [imports, exports] = await parse(code, file);
for (const imp of imports) {
const spec = JSON.stringify(code.substring(imp.s, imp.e));
const s = code.substring(imp.ss + 6, imp.s - 1).replace(/\s*from\s*/g, '');
const r = `const ${s.replace(/\sas\s/g, ':')} = require(${spec})`;
out.overwrite(imp.ss, imp.se, r);
}
const nonDefaultExports = exports.filter(p => p!=='default');
const defaultExport = !flags.flat && (flags.default || nonDefaultExports.length) ? 'exports.default=' : 'module.exports=';
const t = /(^|[;\s(])export(\s*default)?(?:\s*{[^}]+}|\s+(function|const|let|var))/g;
let token;
while ((token = t.exec(code))) {
const r = `${token[2] ? defaultExport : ''}${token[3] || ''}`;
out.overwrite(token.index + token[1].length, t.lastIndex, r);
}
for (const exp of nonDefaultExports) out.append(`\nexports.${exp}=${exp};`);
let outFile;
// use the export map if one exists:
const entry = './' + file.replace(/\.m?js$/, '').split(path.sep).join('/');
const def = pkg && pkg.exports && resolve(pkg.exports, entry);
if (def) {
if (flags.verbose) {
console.log(`[cjyes] using Export Map entry for ${entry}`);
}
outFile = def.replace(/^\.\//,'').split('/').join(path.sep);
}
else {
// fall back to a dist directory
const ext = pkg && pkg.type === 'module' ? '.cjs' : '.js';
const parts = file.replace(/\.m?js$/, ext).split(path.sep);
const index = parts.lastIndexOf('src');
if (index === -1) parts.unshift('dist');
else parts[index] = 'dist';
outFile = parts.join(path.sep);
if (!flags.silent && !ctx.warned) {
ctx.warned = true;
console.log(`[cjyes] no Export Map found, generating filenames:`);
if (ext=='.cjs') console.log(` - Using .cjs due to {"type":"module"}`);
if (index===-1) console.log(` - Replacing src/ with dist/`);
else console.log(` - Prepending dist/ directory`);
}
}
if (!flags.dry) {
try {
await fs.mkdir(path.dirname(outFile), { recursive: true });
} catch (e) {}
await fs.writeFile(outFile, out.toString());
}
if (!flags.silent) {
console.log(`${file} --> ${outFile}`);
}
}
function crawl(exp, files) {
if (typeof exp==='string') files.push(exp.replace(/^\.\//,''));
else if (exp.import || exp.default) crawl(exp.import || exp.default, files);
else for (let i in exp) {
if (i[0]=='.' && !i.endsWith('/')) crawl(exp[i], files);
}
}
function resolve(exp, entry) {
if (!exp || typeof exp=='string') return exp;
return exp.require || exp.default || resolve(select(exp, entry) || exp['.'], entry);
}
function select(exp, entry) {
for (let i in exp) if (i==entry) return exp[i];
}
{
"name": "cjyes",
"bin": "cjyes.js",
"version": "0.3.1",
"license": "Apache-2.0",
"dependencies": {
"es-module-lexer": "^0.3.18",
"magic-string": "^0.25.7"
},
"scripts": {
"prepack": "mv *cjyes.md README.md",
"postpack": "mv README.md *cjyes.md"
}
}
@calebeby
Copy link

calebeby commented Jun 4, 2020

@developit I have a "PR":

On line 56 your regex is missing class

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