import * as Fs from 'node:fs' import * as Path from 'node:path' import { execSync } from 'node:child_process' const TARGET = 'target/build' const SUBMODULES = ['compiler', 'errors', 'parser', 'syntax', 'system', 'type', 'value'] function shell(cmd) { console.log(`> ${cmd}`) execSync(cmd, { stdio: 'inherit' }) } function rmrf(dir) { Fs.rmSync(dir, { recursive: true, force: true }) } function mkdirp(dir) { Fs.mkdirSync(dir, { recursive: true }) } function cp(src, dest) { mkdirp(Path.dirname(dest)) Fs.copyFileSync(src, dest) } // --- Clean --- function clean() { rmrf('target') } // --- Remove MIT Notices from dist --- function removeNotices(dir) { function escape(content) { return content.split('').map((c) => `\\${c}`).join('') } function removeNotice(content) { const open = escape('/*--------------------------------------------------------------------------') const close = escape('---------------------------------------------------------------------------*/') const criteria = 'Permission is hereby granted, free of charge' const pattern = new RegExp(`${open}[\\s\\S]*?${close}`, 'gm') while (true) { const match = pattern.exec(content) if (match === null) return content.trimStart() if (!match[0].includes(criteria)) continue content = content.replace(match[0], '') } } function processFile(sourcePath) { const sourceContent = Fs.readFileSync(sourcePath, 'utf-8') const targetContent = removeNotice(sourceContent) Fs.writeFileSync(sourcePath, targetContent) } function walk(sourcePath) { const stat = Fs.statSync(sourcePath) if (stat.isDirectory()) { for (const entry of Fs.readdirSync(sourcePath)) { walk(Path.join(sourcePath, entry)) } } else if (stat.isFile()) { processFile(sourcePath) } } walk(dir) } // --- ESM Specifier Rewrite --- function convertToEsm(dir) { function replaceInlineImportSpecifiers(content) { const pattern = /import\((.*?)\)/g while (true) { const match = pattern.exec(content) if (match === null) return content const captured = match[1] if (captured.includes('.mjs')) continue const specifier = captured.slice(1, captured.length - 1) content = content.replace(captured, `"${specifier}.mjs"`) } } function replaceExportSpecifiers(content) { const pattern = /(export|import)(.*) from ('(.*)');/g while (true) { const match = pattern.exec(content) if (match === null) return content const captured = match[3] const specifier = captured.slice(1, captured.length - 1) content = content.replace(captured, `'${specifier}.mjs'`) } } function replaceSpecifiers(content) { return replaceInlineImportSpecifiers(replaceExportSpecifiers(content)) } function newExtension(ext) { return ext === '.js' ? '.mjs' : ext === '.ts' ? '.mts' : ext } function processFile(sourcePath) { const ext = Path.extname(sourcePath) if (!['.js', '.ts'].includes(ext)) return const dirname = Path.dirname(sourcePath) const basename = Path.basename(sourcePath, ext) const newExt = newExtension(ext) const sourceContent = Fs.readFileSync(sourcePath, 'utf-8') const targetContent = replaceSpecifiers(sourceContent) const targetPath = Path.join(dirname, `${basename}${newExt}`) Fs.writeFileSync(sourcePath, targetContent) Fs.renameSync(sourcePath, targetPath) } function walk(sourcePath) { const stat = Fs.statSync(sourcePath) if (stat.isDirectory()) { for (const entry of Fs.readdirSync(sourcePath)) { walk(Path.join(sourcePath, entry)) } } else if (stat.isFile()) { processFile(sourcePath) } } walk(dir) } // --- Build CJS --- function buildCjs() { console.log('building...cjs') const target = `${TARGET}/build/cjs` shell(`tsc -p ./src/tsconfig.json --outDir ${target} --target ES2020 --module Node16 --moduleResolution Node16 --declaration`) removeNotices(target) } // --- Build ESM --- function buildEsm() { console.log('building...esm') const target = `${TARGET}/build/esm` shell(`tsc -p ./src/tsconfig.json --outDir ${target} --target ES2020 --module ESNext --moduleResolution Bundler --declaration`) convertToEsm(target) removeNotices(target) } // --- Build package.json --- function buildPackageJson() { console.log('building...package.json') const rootPkg = JSON.parse(Fs.readFileSync('package.json', 'utf-8')) const exports = { '.': { require: { types: './build/cjs/index.d.ts', default: './build/cjs/index.js' }, import: { types: './build/esm/index.d.mts', default: './build/esm/index.mjs' }, }, ...SUBMODULES.reduce((acc, sub) => { acc[`./${sub}`] = { require: { types: `./build/cjs/${sub}/index.d.ts`, default: `./build/cjs/${sub}/index.js` }, import: { types: `./build/esm/${sub}/index.d.mts`, default: `./build/esm/${sub}/index.mjs` }, } return acc }, {}), } const pkg = { name: rootPkg.name, version: rootPkg.version, description: rootPkg.description, keywords: rootPkg.keywords, author: rootPkg.author, license: rootPkg.license, repository: rootPkg.repository, scripts: { test: 'echo test' }, types: './build/cjs/index.d.ts', main: './build/cjs/index.js', module: './build/esm/index.mjs', 'esm.sh': { bundle: false }, sideEffects: [ './build/esm/type/registry/format.mjs', './build/esm/type/registry/type.mjs', './build/esm/type/system/policy.mjs', './build/cjs/type/registry/format.js', './build/cjs/type/registry/type.js', './build/cjs/type/system/policy.js', ], exports, } Fs.writeFileSync(`${TARGET}/package.json`, JSON.stringify(pkg, null, 2)) // redirect package.json for older Node resolution for (const sub of SUBMODULES) { mkdirp(`${TARGET}/${sub}`) Fs.writeFileSync( `${TARGET}/${sub}/package.json`, JSON.stringify({ main: `../build/cjs/${sub}/index.js`, types: `../build/cjs/${sub}/index.d.ts` }, null, 2) ) } } // --- Main --- const command = process.argv[2] || 'build' if (command === 'clean') { clean() } else if (command === 'build') { clean() buildCjs() buildEsm() buildPackageJson() cp('readme.md', `${TARGET}/readme.md`) cp('license', `${TARGET}/license`) shell(`cd ${TARGET} && npm pack`) console.log('done!') } else { console.error(`unknown command: ${command}`) process.exit(1) }