UNPKG

@knighted/duel

Version:
176 lines (175 loc) 8.39 kB
#!/usr/bin/env node "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.duel = void 0; const node_process_1 = require("node:process"); const node_path_1 = require("node:path"); const node_child_process_1 = require("node:child_process"); const promises_1 = require("node:fs/promises"); const node_crypto_1 = require("node:crypto"); const node_perf_hooks_1 = require("node:perf_hooks"); const glob_1 = require("glob"); const find_up_1 = require("find-up"); const specifier_1 = require("@knighted/specifier"); const module_1 = require("@knighted/module"); const init_js_1 = require("./init.cjs"); const util_js_1 = require("./util.cjs"); const handleErrorAndExit = message => { const exitCode = Number(message); (0, util_js_1.logError)('Compilation errors found.'); process.exit(exitCode); }; const duel = async (args) => { const ctx = await (0, init_js_1.init)(args); if (ctx) { const { projectDir, tsconfig, configPath, modules, dirs, pkg } = ctx; const tsc = await (0, find_up_1.findUp)(async (dir) => { const tscBin = (0, node_path_1.join)(dir, 'node_modules', '.bin', 'tsc'); if (await (0, find_up_1.pathExists)(tscBin)) { return tscBin; } }, { cwd: projectDir }); const runBuild = (project, outDir) => { return new Promise((resolve, reject) => { const args = outDir ? ['-p', project, '--outDir', outDir] : ['-p', project]; const build = (0, node_child_process_1.spawn)(tsc, args, { stdio: 'inherit', shell: node_process_1.platform === 'win32' }); build.on('exit', code => { if (code > 0) { return reject(new Error(code)); } resolve(code); }); }); }; const pkgDir = (0, node_path_1.dirname)(pkg.path); const outDir = tsconfig.compilerOptions?.outDir ?? 'dist'; const absoluteOutDir = (0, node_path_1.resolve)(projectDir, outDir); const originalType = pkg.packageJson.type ?? 'commonjs'; const isCjsBuild = originalType !== 'commonjs'; const targetExt = isCjsBuild ? '.cjs' : '.mjs'; const hex = (0, node_crypto_1.randomBytes)(4).toString('hex'); const getOverrideTsConfig = () => { return { ...tsconfig, compilerOptions: { ...tsconfig.compilerOptions, module: 'NodeNext', moduleResolution: 'NodeNext', }, }; }; const runPrimaryBuild = () => { return runBuild(configPath, dirs ? isCjsBuild ? (0, node_path_1.join)(absoluteOutDir, 'esm') : (0, node_path_1.join)(absoluteOutDir, 'cjs') : absoluteOutDir); }; const updateSpecifiersAndFileExtensions = async (filenames) => { for (const filename of filenames) { const dts = /(\.d\.ts)$/; const outFilename = dts.test(filename) ? filename.replace(dts, isCjsBuild ? '.d.cts' : '.d.mts') : filename.replace(/\.js$/, targetExt); const update = await specifier_1.specifier.update(filename, ({ value }) => { // Collapse any BinaryExpression or NewExpression to test for a relative specifier const collapsed = value.replace(/['"`+)\s]|new String\(/g, ''); const relative = /^(?:\.|\.\.)\//; if (relative.test(collapsed)) { // $2 is for any closing quotation/parens around BE or NE return value.replace(/(.+)\.js([)'"`]*)?$/, `$1${targetExt}$2`); } }); await (0, promises_1.writeFile)(outFilename, update); await (0, promises_1.rm)(filename, { force: true }); } }; const logSuccess = start => { (0, util_js_1.log)(`Successfully created a dual ${isCjsBuild ? 'CJS' : 'ESM'} build in ${Math.round(node_perf_hooks_1.performance.now() - start)}ms.`); }; (0, util_js_1.log)('Starting primary build...'); let success = false; const startTime = node_perf_hooks_1.performance.now(); try { await runPrimaryBuild(); success = true; } catch ({ message }) { handleErrorAndExit(message); } if (success) { const subDir = (0, node_path_1.join)(projectDir, `_${hex}_`); const absoluteDualOutDir = (0, node_path_1.join)(projectDir, isCjsBuild ? (0, node_path_1.join)(outDir, 'cjs') : (0, node_path_1.join)(outDir, 'esm')); const tsconfigDual = getOverrideTsConfig(); const pkgRename = 'package.json.bak'; let dualConfigPath = (0, node_path_1.join)(projectDir, `tsconfig.${hex}.json`); let errorMsg = ''; if (modules) { const compileFiles = (0, util_js_1.getCompileFiles)(tsc, projectDir); dualConfigPath = (0, node_path_1.join)(subDir, `tsconfig.${hex}.json`); await (0, promises_1.mkdir)(subDir); await Promise.all(compileFiles.map(file => (0, promises_1.cp)(file, (0, node_path_1.join)(subDir, (0, node_path_1.relative)(projectDir, file).replace(/^(\.\.\/)*/, ''))))); /** * Transform ambiguous modules for the target dual build. * @see https://github.com/microsoft/TypeScript/issues/58658 */ const toTransform = await (0, glob_1.glob)(`${subDir}/**/*{.js,.jsx,.ts,.tsx}`, { ignore: 'node_modules/**', }); for (const file of toTransform) { /** * Maybe include the option to transform modules implicitly * (modules: true) so that `exports` are correctly converted * when targeting a CJS dual build. Depends on @knighted/module * supporting he `modules` option. * * @see https://github.com/microsoft/TypeScript/issues/58658 */ await (0, module_1.transform)(file, { out: file, type: isCjsBuild ? 'commonjs' : 'module' }); } } /** * Create a new package.json with updated `type` field. * Create a new tsconfig.json. */ await (0, promises_1.rename)(pkg.path, (0, node_path_1.join)(pkgDir, pkgRename)); await (0, promises_1.writeFile)(pkg.path, JSON.stringify({ type: isCjsBuild ? 'commonjs' : 'module', })); await (0, promises_1.writeFile)(dualConfigPath, JSON.stringify(tsconfigDual)); // Build dual (0, util_js_1.log)('Starting dual build...'); try { await runBuild(dualConfigPath, absoluteDualOutDir); } catch ({ message }) { success = false; errorMsg = message; } finally { // Cleanup and restore await (0, promises_1.rm)(dualConfigPath, { force: true }); await (0, promises_1.rm)(pkg.path, { force: true }); await (0, promises_1.rm)(subDir, { force: true, recursive: true }); await (0, promises_1.rename)((0, node_path_1.join)(pkgDir, pkgRename), pkg.path); if (errorMsg) { handleErrorAndExit(errorMsg); } } if (success) { const filenames = await (0, glob_1.glob)(`${absoluteDualOutDir}/**/*{.js,.d.ts}`, { ignore: 'node_modules/**', }); await updateSpecifiersAndFileExtensions(filenames); logSuccess(startTime); } } } }; exports.duel = duel; (async () => { const realFileUrlArgv1 = await (0, util_js_1.getRealPathAsFileUrl)(node_process_1.argv[1] ?? ''); if (require("node:url").pathToFileURL(__filename).toString() === realFileUrlArgv1) { await duel(); } })();