@knighted/duel
Version:
TypeScript dual packages.
756 lines (755 loc) • 37.2 kB
JavaScript
#!/usr/bin/env node
import { argv } from 'node:process';
import { pathToFileURL } from 'node:url';
import { join, dirname, resolve, relative, sep } from 'node:path';
import { spawn } from 'node:child_process';
import { writeFile, rm, mkdir, cp, access, readdir, glob } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { performance } from 'node:perf_hooks';
import { findUp } from 'find-up';
import { transform, collectProjectDualPackageHazards } from '@knighted/module';
import { getTsconfig, parseTsconfig } from 'get-tsconfig';
import { init } from './init.js';
import { getRealPathAsFileUrl, getCompileFiles, log, logError, logWarn, logSuccess as logSuccessBadge, readExportsConfig, processDiagnosticsForFile, exitOnDiagnostics, hazardPackageFromMessage, filterDualPackageDiagnostics, maybeLinkNodeModules, runExportsValidationBlock, createTempCleanup, registerCleanupHandlers, } from './util.js';
import { rewriteSpecifiersAndExtensions } from './resolver.js';
const handleErrorAndExit = message => {
const parsed = parseInt(message, 10);
const exitCode = Number.isNaN(parsed) ? 1 : parsed;
logError('Compilation errors found.');
process.exit(exitCode);
};
const logDiagnostics = (diags, projectDir, hazardAllowlist = null) => {
let hasError = false;
for (const diag of diags) {
if (hazardAllowlist && diag?.code?.startsWith('dual-package') && diag?.message) {
const pkg = hazardPackageFromMessage(diag.message);
if (pkg && hazardAllowlist.has(pkg))
continue;
}
const loc = diag.loc ? ` [${diag.loc.start}-${diag.loc.end}]` : '';
const rel = diag.filePath ? `${relative(projectDir, diag.filePath)}` : '';
const location = rel ? `${rel}: ` : '';
const message = `${diag.code}: ${location}${diag.message}${loc}`;
if (diag.level === 'error') {
hasError = true;
logError(message);
}
else {
logWarn(message);
}
}
return hasError;
};
const collectGlob = async (pattern, options) => {
const files = [];
for await (const file of glob(pattern, options)) {
files.push(file);
}
return files;
};
const duel = async (args) => {
const ctx = await init(args);
if (ctx) {
const { projectDir, tsconfig, configPath, modules, dirs, transformSyntax, pkg, exports: exportsOpt, exportsConfig, exportsValidate, rewritePolicy, detectDualPackageHazard, dualPackageHazardAllowlist, dualPackageHazardScope, verbose, copyMode, } = ctx;
const logVerbose = verbose ? (...messages) => log(...messages) : () => { };
const tsc = await findUp(async (dir) => {
const candidate = join(dir, 'node_modules', 'typescript', 'bin', 'tsc');
try {
await access(candidate);
return resolve(candidate);
}
catch {
/* continue */
}
}, { cwd: projectDir });
const hasReferences = Array.isArray(tsconfig.references) && tsconfig.references.length > 0;
const runBuild = (project, outDir, tsBuildInfoFile, cwdForBuild) => {
return new Promise((fulfill, rejectBuild) => {
const useBuildMode = hasReferences;
const tsArgs = useBuildMode
? [tsc, '-b', project]
: outDir
? [tsc, '-p', project, '--outDir', outDir]
: [tsc, '-p', project];
if (!useBuildMode) {
tsArgs.push('--incremental');
if (tsBuildInfoFile) {
tsArgs.push('--tsBuildInfoFile', tsBuildInfoFile);
}
}
const build = spawn(process.execPath, tsArgs, {
stdio: 'inherit',
cwd: cwdForBuild ?? process.cwd(),
});
build.on('exit', code => {
if (code > 0) {
return rejectBuild(new Error(code));
}
fulfill(code);
});
});
};
const pkgDir = dirname(pkg.path);
const packageRoot = resolve(pkgDir);
const mainPath = pkg.packageJson.main;
const mainDefaultKind = mainPath?.endsWith('.cjs') ? 'require' : 'import';
const outDir = tsconfig.compilerOptions?.outDir ?? 'dist';
const absoluteOutDir = resolve(projectDir, outDir);
const originalType = pkg.packageJson.type ?? 'commonjs';
const isCjsBuild = originalType !== 'commonjs';
const absoluteDualOutDir = join(projectDir, isCjsBuild ? join(outDir, 'cjs') : join(outDir, 'esm'));
/*
* Workspace boundary: package root, its parent (e.g., packages/), and repo root.
* Chosen to make single-package and typical monorepo base-config extends “just work”
* even without TS references, while still excluding node_modules.
*/
const packagesRoot = dirname(packageRoot);
const repoRoot = dirname(packagesRoot);
const allowedConfigRoots = [packageRoot, packagesRoot, repoRoot].filter((root, idx, arr) => arr.indexOf(root) === idx);
const isInAllowedRoots = absPath => allowedConfigRoots.some(root => absPath === root || absPath.startsWith(`${root}${sep}`));
const shouldIncludeConfig = absPath => {
const normalized = resolve(absPath);
if (normalized.split(sep).includes('node_modules'))
return false;
return isInAllowedRoots(normalized);
};
const toWorkspaceRelative = absPath => {
const normalized = resolve(absPath);
for (const root of allowedConfigRoots) {
if (normalized === root)
return '.';
if (normalized.startsWith(`${root}${sep}`))
return relative(root, normalized);
}
return null;
};
const requireWorkspaceRelative = absPath => {
const rel = toWorkspaceRelative(absPath);
if (rel === null) {
logError(`Referenced config or source is outside the allowed workspace boundary and cannot be patched: ${absPath}. Move it inside one of: ${allowedConfigRoots.join(', ')} so Duel can create an isolated shadow build.`);
process.exit(1);
}
return rel;
};
const primaryOutDir = dirs
? isCjsBuild
? join(absoluteOutDir, 'esm')
: join(absoluteOutDir, 'cjs')
: absoluteOutDir;
const { type, exports, imports, main, module, types, typings, typesVersions, sideEffects, } = pkg.packageJson ?? {};
const pkgHashInputs = {
type,
exports,
imports,
main,
module,
types,
typings,
typesVersions,
sideEffects,
};
const hash = createHash('sha1')
.update(JSON.stringify({
configPath,
tsconfig,
packageJson: pkgHashInputs,
dualTarget: isCjsBuild ? 'cjs' : 'esm',
}))
.digest('hex')
.slice(0, 8);
const cacheDir = join(projectDir, '.duel-cache');
const primaryTsBuildInfoFile = join(cacheDir, `primary.${hash}.tsbuildinfo`);
const dualTsBuildInfoFile = join(cacheDir, `dual.${hash}.tsbuildinfo`);
const subDir = join(cacheDir, `_duel_${hash}_`);
const shadowDualOutDir = join(subDir, requireWorkspaceRelative(absoluteDualOutDir));
const hazardMode = detectDualPackageHazard ?? 'warn';
const hazardScope = dualPackageHazardScope ?? 'file';
const hazardAllowlist = new Set((dualPackageHazardAllowlist ?? []).map(entry => entry.trim()).filter(Boolean));
const logDiagnosticsWithAllowlist = diags => logDiagnostics(diags, projectDir, hazardAllowlist);
const applyHazardAllowlist = diagnostics => filterDualPackageDiagnostics(diagnostics ?? [], hazardAllowlist);
function mapReferencesToShadow(references = [], options) {
const { resolveRefPath, toShadowPathFn, fromDir } = options;
return references.map(ref => {
if (!ref?.path)
return ref;
const refAbs = resolveRefPath(ref.path);
const shadowRef = toShadowPathFn(refAbs);
return {
...ref,
path: relative(fromDir, shadowRef),
};
});
}
const getOverrideTsConfig = dualConfigDir => {
const shadowReferences = mapReferencesToShadow(tsconfig.references ?? [], {
resolveRefPath: refPath => resolve(projectDir, refPath),
toShadowPathFn: abs => join(subDir, requireWorkspaceRelative(abs)),
fromDir: dualConfigDir,
});
return {
...tsconfig,
references: shadowReferences,
compilerOptions: {
...(tsconfig.compilerOptions ?? {}),
module: 'NodeNext',
moduleResolution: 'NodeNext',
target: tsconfig.compilerOptions?.target ?? 'ES2022',
// Emit dual build into the shadow workspace, then copy to real outDir
outDir: shadowDualOutDir,
incremental: true,
tsBuildInfoFile: dualTsBuildInfoFile,
},
};
};
const runPrimaryBuild = () => {
return runBuild(configPath, hasReferences ? undefined : primaryOutDir, hasReferences ? undefined : primaryTsBuildInfoFile, projectDir);
};
const refreshDualBuildInfo = async () => {
try {
await access(shadowDualOutDir);
}
catch {
await rm(dualTsBuildInfoFile, { force: true });
}
};
const refreshPrimaryBuildInfo = async () => {
try {
await access(primaryOutDir);
}
catch {
await rm(primaryTsBuildInfoFile, { force: true });
}
};
const resolveReferenceConfigPath = (baseDir, refPath) => {
const abs = resolve(baseDir, refPath);
return /\.json$/i.test(abs) ? abs : join(abs, 'tsconfig.json');
};
const collectCompileFilesWithReferences = async ({ includeConfig }) => {
const seenConfigs = new Set();
const compileFiles = new Set();
const configFiles = new Set();
const referenceConfigFiles = new Set();
const packageJsons = new Set();
const queue = [{ configPath, tsconfig, projectDir }];
const resolveExtendsConfig = (specifier, cwdForProject) => {
try {
const resolved = getTsconfig(specifier, { cwd: cwdForProject });
if (resolved?.path) {
return {
path: resolved.path,
tsconfig: resolved.tsconfig ?? resolved,
};
}
}
catch {
/* ignore and fall back */
}
if (/^\.{1,2}[\\/]/.test(specifier)) {
const candidate = resolve(cwdForProject, specifier);
try {
const parsed = parseTsconfig(candidate);
const parsedConfig = parsed?.tsconfig ?? parsed;
if (parsedConfig) {
return { path: candidate, tsconfig: parsedConfig };
}
}
catch {
/* ignore */
}
}
return null;
};
logVerbose(`Root tsconfig references: ${JSON.stringify(tsconfig.references ?? [])}`);
/*
* Depth-first traversal (LIFO via pop) is acceptable here because results
* are collected into Sets where order is irrelevant. What matters is that
* all configs are visited, not the order in which they're processed.
*/
while (queue.length) {
const current = queue.pop();
const absConfig = resolve(current.configPath);
if (seenConfigs.has(absConfig))
continue;
seenConfigs.add(absConfig);
configFiles.add(absConfig);
const cwdForProject = dirname(absConfig);
const extendsPath = current.tsconfig.extends;
if (extendsPath) {
const resolvedExtends = resolveExtendsConfig(extendsPath, cwdForProject);
if (resolvedExtends) {
const { path: extendsConfigPath, tsconfig: nextExtendsConfig } = resolvedExtends;
const normalizedExtendsPath = resolve(extendsConfigPath);
if (includeConfig(normalizedExtendsPath)) {
configFiles.add(normalizedExtendsPath);
logVerbose(`Including extended tsconfig ${normalizedExtendsPath} in copy plan`);
queue.push({
configPath: normalizedExtendsPath,
tsconfig: nextExtendsConfig,
projectDir: dirname(normalizedExtendsPath),
});
}
else {
if (!normalizedExtendsPath.split(sep).includes('node_modules')) {
logError(`Referenced config or source is outside the allowed workspace boundary and cannot be patched: ${normalizedExtendsPath}. Move it inside one of: ${allowedConfigRoots.join(', ')} so Duel can create an isolated shadow build.`);
process.exit(1);
}
logVerbose(`Skipping external extended tsconfig ${normalizedExtendsPath}`);
}
}
}
const files = getCompileFiles(tsc, { project: absConfig, cwd: cwdForProject });
for (const file of files) {
compileFiles.add(file);
const jsSibling = file.replace(/\.(mts|cts|tsx|ts|d\.ts)$/i, '.js');
if (jsSibling !== file) {
try {
await access(jsSibling);
compileFiles.add(jsSibling);
}
catch {
/* optional */
}
}
}
const pkgPath = join(cwdForProject, 'package.json');
try {
await access(pkgPath);
packageJsons.add(pkgPath);
}
catch {
/* optional */
}
for (const ref of current.tsconfig.references ?? []) {
if (!ref?.path)
continue;
const refConfigPath = resolveReferenceConfigPath(cwdForProject, ref.path);
const refAbsPath = resolve(refConfigPath);
try {
const parsed = parseTsconfig(refAbsPath);
const nextTsconfig = parsed?.tsconfig ?? parsed;
if (nextTsconfig) {
logVerbose(`Including project reference ${refAbsPath} in copy plan`);
referenceConfigFiles.add(refAbsPath);
queue.push({
configPath: refAbsPath,
tsconfig: nextTsconfig,
projectDir: dirname(refAbsPath),
});
}
}
catch (err) {
logWarn(`Skipping missing or invalid project reference at ${refAbsPath}: ${err.message}`);
}
}
}
logVerbose(`Copy plan (mode=${copyMode}): ${compileFiles.size} compile files, ${configFiles.size} tsconfig files, ${packageJsons.size} package.json files`);
return {
compileFiles: Array.from(compileFiles),
configFiles,
referenceConfigFiles,
packageJsons,
};
};
const syntaxMode = transformSyntax ? true : 'globals-only';
const logSuccess = start => {
logSuccessBadge(`Successfully created a dual ${isCjsBuild ? 'CJS' : 'ESM'} build in ${Math.round(performance.now() - start)}ms.`);
};
log('Starting primary build...');
let success = false;
const startTime = performance.now();
try {
await refreshPrimaryBuildInfo();
await runPrimaryBuild();
success = true;
}
catch ({ message }) {
handleErrorAndExit(message);
}
if (success) {
const tsconfigRel = requireWorkspaceRelative(configPath);
const tsconfigDualRel = tsconfigRel.replace(/tsconfig\.json$/i, `tsconfig.${hash}.json`);
const dualConfigPath = join(subDir, tsconfigDualRel);
const dualConfigDir = dirname(dualConfigPath);
const tsconfigDual = getOverrideTsConfig(dualConfigDir);
const keepTemp = process.env.DUEL_KEEP_TEMP === '1';
const { cleanupTemp, cleanupTempSync } = createTempCleanup({
subDir,
keepTemp,
logWarnFn: logWarn,
});
const unregisterCleanupHandlers = registerCleanupHandlers(cleanupTempSync);
let errorMsg = '';
let exportsConfigData = null;
if (exportsConfig) {
try {
exportsConfigData = await readExportsConfig(exportsConfig, pkgDir);
}
catch (err) {
logError(err.message);
process.exit(1);
}
}
const { compileFiles, configFiles, referenceConfigFiles, packageJsons } = await collectCompileFilesWithReferences({ includeConfig: shouldIncludeConfig });
const sourceFiles = compileFiles.filter(file => {
const isSupported = /\.(?:[cm]?jsx?|[cm]?tsx?)$/i.test(file);
const isDeclaration = /\.d\.[cm]?tsx?$/i.test(file);
return isSupported && !isDeclaration;
});
const projectHazards = hazardScope === 'project' && hazardMode !== 'off'
? await collectProjectDualPackageHazards(sourceFiles, {
detectDualPackageHazard: hazardMode,
dualPackageHazardScope: 'project',
cwd: projectDir,
})
: null;
const filteredProjectHazards = projectHazards
? new Map([...projectHazards.entries()].map(([key, diags]) => [
key,
applyHazardAllowlist(diags ?? []),
]))
: null;
const projectHazardsHaveDiagnostics = filteredProjectHazards
? [...filteredProjectHazards.values()].some(diags => diags?.length)
: false;
const projectHazardsHaveLocations = filteredProjectHazards
? [...filteredProjectHazards.values()].some(diags => diags?.some(diag => diag?.filePath))
: false;
if (filteredProjectHazards) {
let hasHazardError = false;
for (const diags of filteredProjectHazards.values()) {
if (!diags?.length)
continue;
const errored = logDiagnosticsWithAllowlist(diags);
hasHazardError = hasHazardError || errored;
}
if (hasHazardError && hazardMode === 'error') {
process.exit(1);
}
}
await Promise.all([
mkdir(subDir, { recursive: true }),
mkdir(cacheDir, { recursive: true }),
]);
const linkNodeModulesPromise = maybeLinkNodeModules(projectDir, subDir);
const projectRel = requireWorkspaceRelative(projectDir);
const projectCopyDest = projectRel === '.' ? subDir : join(subDir, projectRel);
const makeCopyFilter = (rootDir, allowDist) => src => {
if (src.split(/[/\\]/).includes('.duel-cache'))
return false;
if (src.split(/[/\\]/).includes('node_modules'))
return false;
if (allowDist)
return true;
const rel = relative(rootDir, src);
if (rel.startsWith('..'))
return true;
const [segment] = rel.split(sep);
return segment !== outDir;
};
const copyFilesToTemp = async () => {
const copyDirContents = async (sourceDir, destDir, allowDist) => {
await mkdir(destDir, { recursive: true });
const filter = makeCopyFilter(sourceDir, allowDist);
const entries = await readdir(sourceDir, { withFileTypes: true });
for (const entry of entries) {
const srcPath = join(sourceDir, entry.name);
if (!filter(srcPath))
continue;
const dstPath = join(destDir, entry.name);
await cp(srcPath, dstPath, {
recursive: true,
filter,
});
}
};
if (copyMode === 'full') {
const allowDist = true;
await copyDirContents(projectDir, projectCopyDest, allowDist);
if (hasReferences) {
for (const ref of tsconfig.references ?? []) {
if (!ref.path)
continue;
const refAbs = resolve(projectDir, ref.path);
const refRel = requireWorkspaceRelative(refAbs);
const refDest = join(subDir, refRel);
await copyDirContents(refAbs, refDest, allowDist);
}
}
}
else {
const filesToCopy = new Set([...compileFiles, ...configFiles, ...packageJsons]);
for (const file of filesToCopy) {
const normalized = resolve(file);
if (!isInAllowedRoots(normalized)) {
logVerbose(`Skipping non-local file ${normalized}`);
continue;
}
const rel = toWorkspaceRelative(normalized);
const dest = join(subDir, rel);
await mkdir(dirname(dest), { recursive: true });
await cp(file, dest);
}
const missingConfigs = [];
for (const configFile of configFiles) {
const dest = join(subDir, requireWorkspaceRelative(configFile));
try {
await access(dest);
}
catch {
missingConfigs.push({ src: configFile, dest });
}
}
if (missingConfigs.length) {
logWarn(`Copying ${missingConfigs.length} missing referenced config(s) into temp workspace: ${missingConfigs
.map(entry => entry.src)
.join(', ')}`);
for (const { src, dest } of missingConfigs) {
await mkdir(dirname(dest), { recursive: true });
await cp(src, dest);
}
}
}
};
const toShadowPath = absPath => join(subDir, requireWorkspaceRelative(absPath));
// Patch referenced tsconfig files in the shadow workspace to emit dual outputs
const patchReferencedConfigs = async () => {
for (const configFile of referenceConfigFiles) {
if (configFile === configPath)
continue;
const dest = join(subDir, requireWorkspaceRelative(configFile));
const parsed = parseTsconfig(dest);
const cfg = parsed?.tsconfig ?? parsed;
if (!cfg || typeof cfg !== 'object')
continue;
const cfgDir = dirname(configFile);
const baseOut = cfg.compilerOptions?.outDir
? resolve(cfgDir, cfg.compilerOptions.outDir)
: resolve(cfgDir, 'dist');
const dualOutReal = join(baseOut, isCjsBuild ? 'cjs' : 'esm');
const dualOut = toShadowPath(dualOutReal);
const tsbuildReal = cfg.compilerOptions?.tsBuildInfoFile
? resolve(cfgDir, cfg.compilerOptions.tsBuildInfoFile)
: join(baseOut, 'tsconfig.tsbuildinfo');
const dualTsbuild = toShadowPath(join(dirname(tsbuildReal), 'tsconfig.dual.tsbuildinfo'));
const shadowReferences = mapReferencesToShadow(cfg.references ?? [], {
resolveRefPath: refPath => resolveReferenceConfigPath(cfgDir, refPath),
toShadowPathFn: toShadowPath,
fromDir: dirname(dest),
});
const patched = {
...cfg,
references: shadowReferences,
compilerOptions: {
...(cfg.compilerOptions ?? {}),
module: 'NodeNext',
moduleResolution: 'NodeNext',
outDir: dualOut,
incremental: cfg.compilerOptions?.incremental ?? true,
tsBuildInfoFile: dualTsbuild,
},
};
await writeFile(dest, JSON.stringify(patched, null, 2));
}
};
/**
* Write dual package.json and tsconfig into temp dir; avoid mutating root package.json.
*/
await copyFilesToTemp();
await patchReferencedConfigs();
const writeDualPackage = async () => {
const pkgDest = join(subDir, requireWorkspaceRelative(pkg.path));
await mkdir(dirname(pkgDest), { recursive: true });
await writeFile(pkgDest, JSON.stringify({
name: pkg.packageJson?.name,
version: pkg.packageJson?.version,
type: isCjsBuild ? 'commonjs' : 'module',
exports: pkg.packageJson?.exports,
imports: pkg.packageJson?.imports,
main: pkg.packageJson?.main,
module: pkg.packageJson?.module,
types: pkg.packageJson?.types ?? pkg.packageJson?.typings,
typesVersions: pkg.packageJson?.typesVersions,
sideEffects: pkg.packageJson?.sideEffects,
}, null, 2));
};
const writeDualConfig = async () => {
await mkdir(dualConfigDir, { recursive: true });
await writeFile(dualConfigPath, JSON.stringify({
...tsconfigDual,
compilerOptions: {
...tsconfigDual.compilerOptions,
outDir: shadowDualOutDir,
incremental: true,
tsBuildInfoFile: dualTsBuildInfoFile,
},
}, null, 2));
};
await Promise.all([linkNodeModulesPromise, writeDualPackage(), writeDualConfig()]);
if (modules) {
/**
* Transform ambiguous modules for the target dual build.
* @see https://github.com/microsoft/TypeScript/issues/58658
*/
const toTransform = await collectGlob(`${subDir.replace(/\\/g, '/')}/**/*{.js,.jsx,.ts,.tsx}`, {
ignore: `${subDir.replace(/\\/g, '/')}/**/node_modules/**`,
});
let transformDiagnosticsError = false;
/**
* If project-scope hazards didn't surface file paths, fall back to
* file-scope detection during the transform pass so we can emit
* per-file diagnostics. Otherwise, keep project scope to avoid
* duplicate warnings.
*/
const shouldFallbackToFileScope = hazardScope === 'project' &&
projectHazardsHaveDiagnostics &&
!projectHazardsHaveLocations;
const transformHazardScope = shouldFallbackToFileScope ? 'file' : hazardScope;
const transformHazardMode = hazardScope === 'project'
? shouldFallbackToFileScope
? hazardMode
: 'off'
: hazardMode;
for (const file of toTransform) {
if (file.split(/[/\\]/).includes('node_modules'))
continue;
const isTsLike = /\.[cm]?tsx?$/.test(file);
const transformSyntaxMode = syntaxMode === true && isTsLike ? 'globals-only' : syntaxMode;
const diagnostics = [];
await transform(file, {
out: file,
target: isCjsBuild ? 'commonjs' : 'module',
transformSyntax: transformSyntaxMode,
detectDualPackageHazard: transformHazardMode,
dualPackageHazardScope: transformHazardScope,
dualPackageHazardAllowlist: [...hazardAllowlist],
cwd: projectDir,
diagnostics: diag => diagnostics.push(diag),
});
const normalizedDiagnostics = diagnostics.map(diag => !diag?.filePath && transformHazardScope === 'file'
? { ...diag, filePath: file }
: diag);
const filteredDiagnostics = applyHazardAllowlist(normalizedDiagnostics);
const errored = processDiagnosticsForFile(filteredDiagnostics, projectDir, logDiagnosticsWithAllowlist);
transformDiagnosticsError = transformDiagnosticsError || errored;
}
exitOnDiagnostics(transformDiagnosticsError);
}
// Build dual
log('Starting dual build...');
try {
await refreshDualBuildInfo();
await runBuild(dualConfigPath, hasReferences ? undefined : shadowDualOutDir, hasReferences ? undefined : dualTsBuildInfoFile, subDir);
}
catch ({ message }) {
success = false;
errorMsg = message;
}
if (!success) {
await cleanupTemp();
unregisterCleanupHandlers();
if (errorMsg) {
handleErrorAndExit(errorMsg);
}
}
if (success) {
const dualTarget = isCjsBuild ? 'commonjs' : 'module';
const dualTargetExt = isCjsBuild ? '.cjs' : dirs ? '.js' : '.mjs';
await rm(absoluteDualOutDir, { force: true, recursive: true });
await mkdir(dirname(absoluteDualOutDir), { recursive: true });
// Only copy if the shadow dual outDir was produced; absent indicates a failed emit
try {
await cp(shadowDualOutDir, absoluteDualOutDir, { recursive: true });
}
catch (err) {
if (err?.code === 'ENOENT') {
throw new Error(`Dual build output not found at ${shadowDualOutDir}`, {
cause: err,
});
}
throw err;
}
const dualGlob = dualTarget === 'commonjs' ? '**/*{.js,.cjs,.d.ts}' : '**/*{.js,.mjs,.d.ts}';
const filenames = await collectGlob(`${absoluteDualOutDir.replace(/\\/g, '/')}/${dualGlob}`, {
ignore: `${absoluteDualOutDir.replace(/\\/g, '/')}/**/node_modules/**`,
});
const rewriteSyntaxMode = dualTarget === 'commonjs' ? true : syntaxMode;
let rewriteDiagnosticsError = false;
const handleRewriteDiagnostic = diag => {
const filtered = applyHazardAllowlist([diag]);
const errored = processDiagnosticsForFile(filtered, projectDir, logDiagnosticsWithAllowlist);
rewriteDiagnosticsError = rewriteDiagnosticsError || errored;
};
await rewriteSpecifiersAndExtensions(filenames, {
target: dualTarget,
ext: dualTargetExt,
syntaxMode: rewriteSyntaxMode,
detectDualPackageHazard: hazardMode,
dualPackageHazardScope: hazardScope,
dualPackageHazardAllowlist: [...hazardAllowlist],
onDiagnostics: handleRewriteDiagnostic,
rewritePolicy,
onWarn: message => logWarn(message),
onRewrite: (from, to) => logVerbose(`Rewrote specifiers in ${from} -> ${to}`),
});
if (dirs && originalType === 'commonjs') {
const primaryFiles = await collectGlob(`${primaryOutDir.replace(/\\/g, '/')}/**/*{.js,.cjs,.d.ts}`, {
ignore: `${primaryOutDir.replace(/\\/g, '/')}/**/node_modules/**`,
});
await rewriteSpecifiersAndExtensions(primaryFiles, {
target: 'commonjs',
ext: '.cjs',
// Always lower syntax for primary CJS output when dirs mode rewrites primary build.
syntaxMode: true,
detectDualPackageHazard: hazardMode,
dualPackageHazardScope: hazardScope,
dualPackageHazardAllowlist: [...hazardAllowlist],
onDiagnostics: handleRewriteDiagnostic,
rewritePolicy,
onWarn: message => logWarn(message),
onRewrite: (from, to) => logVerbose(`Rewrote specifiers in ${from} -> ${to}`),
});
}
exitOnDiagnostics(rewriteDiagnosticsError);
const esmRoot = isCjsBuild ? primaryOutDir : absoluteDualOutDir;
const cjsRoot = isCjsBuild ? absoluteDualOutDir : primaryOutDir;
await runExportsValidationBlock({
exportsOpt,
exportsConfigData,
exportsValidate,
pkg,
pkgDir,
esmRoot,
cjsRoot,
mainDefaultKind,
mainPath,
});
await cleanupTemp();
unregisterCleanupHandlers();
logSuccess(startTime);
}
}
}
};
const getCurrentHref = () => {
if (typeof import.meta !== 'undefined' && import.meta.url)
return import.meta.url;
if (typeof module !== 'undefined' && module?.filename) {
return pathToFileURL(module.filename).href;
}
return null;
};
const runIfEntry = async () => {
try {
const realFileUrlArgv1 = await getRealPathAsFileUrl(argv[1] ?? '');
const currentHref = getCurrentHref();
if (currentHref && currentHref === realFileUrlArgv1) {
await duel();
}
}
catch (err) {
logError(err?.message ?? err);
process.exit(1);
}
};
runIfEntry();
export { duel };