@knighted/duel
Version:
TypeScript dual packages.
176 lines (175 loc) • 8.39 kB
JavaScript
;
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();
}
})();