|
#! /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]; |
|
} |
@developit I have a "PR":
On line 56 your regex is missing
class