@knighted/duel
Version:
TypeScript dual packages.
759 lines (758 loc) • 39.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.duel = void 0;
const node_process_1 = require("node:process");
const node_url_1 = require("node:url");
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 find_up_1 = require("find-up");
const module_1 = require("@knighted/module");
const get_tsconfig_1 = require("get-tsconfig");
const init_js_1 = require("./init.cjs");
const util_js_1 = require("./util.cjs");
const resolver_js_1 = require("./resolver.cjs");
const handleErrorAndExit = message => {
const parsed = parseInt(message, 10);
const exitCode = Number.isNaN(parsed) ? 1 : parsed;
(0, util_js_1.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 = (0, util_js_1.hazardPackageFromMessage)(diag.message);
if (pkg && hazardAllowlist.has(pkg))
continue;
}
const loc = diag.loc ? ` [${diag.loc.start}-${diag.loc.end}]` : '';
const rel = diag.filePath ? `${(0, node_path_1.relative)(projectDir, diag.filePath)}` : '';
const location = rel ? `${rel}: ` : '';
const message = `${diag.code}: ${location}${diag.message}${loc}`;
if (diag.level === 'error') {
hasError = true;
(0, util_js_1.logError)(message);
}
else {
(0, util_js_1.logWarn)(message);
}
}
return hasError;
};
const collectGlob = async (pattern, options) => {
const files = [];
for await (const file of (0, promises_1.glob)(pattern, options)) {
files.push(file);
}
return files;
};
const duel = async (args) => {
const ctx = await (0, init_js_1.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) => (0, util_js_1.log)(...messages) : () => { };
const tsc = await (0, find_up_1.findUp)(async (dir) => {
const candidate = (0, node_path_1.join)(dir, 'node_modules', 'typescript', 'bin', 'tsc');
try {
await (0, promises_1.access)(candidate);
return (0, node_path_1.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 = (0, node_child_process_1.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 = (0, node_path_1.dirname)(pkg.path);
const packageRoot = (0, node_path_1.resolve)(pkgDir);
const mainPath = pkg.packageJson.main;
const mainDefaultKind = mainPath?.endsWith('.cjs') ? 'require' : 'import';
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 absoluteDualOutDir = (0, node_path_1.join)(projectDir, isCjsBuild ? (0, node_path_1.join)(outDir, 'cjs') : (0, node_path_1.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 = (0, node_path_1.dirname)(packageRoot);
const repoRoot = (0, node_path_1.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}${node_path_1.sep}`));
const shouldIncludeConfig = absPath => {
const normalized = (0, node_path_1.resolve)(absPath);
if (normalized.split(node_path_1.sep).includes('node_modules'))
return false;
return isInAllowedRoots(normalized);
};
const toWorkspaceRelative = absPath => {
const normalized = (0, node_path_1.resolve)(absPath);
for (const root of allowedConfigRoots) {
if (normalized === root)
return '.';
if (normalized.startsWith(`${root}${node_path_1.sep}`))
return (0, node_path_1.relative)(root, normalized);
}
return null;
};
const requireWorkspaceRelative = absPath => {
const rel = toWorkspaceRelative(absPath);
if (rel === null) {
(0, util_js_1.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
? (0, node_path_1.join)(absoluteOutDir, 'esm')
: (0, node_path_1.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 = (0, node_crypto_1.createHash)('sha1')
.update(JSON.stringify({
configPath,
tsconfig,
packageJson: pkgHashInputs,
dualTarget: isCjsBuild ? 'cjs' : 'esm',
}))
.digest('hex')
.slice(0, 8);
const cacheDir = (0, node_path_1.join)(projectDir, '.duel-cache');
const primaryTsBuildInfoFile = (0, node_path_1.join)(cacheDir, `primary.${hash}.tsbuildinfo`);
const dualTsBuildInfoFile = (0, node_path_1.join)(cacheDir, `dual.${hash}.tsbuildinfo`);
const subDir = (0, node_path_1.join)(cacheDir, `_duel_${hash}_`);
const shadowDualOutDir = (0, node_path_1.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 => (0, util_js_1.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: (0, node_path_1.relative)(fromDir, shadowRef),
};
});
}
const getOverrideTsConfig = dualConfigDir => {
const shadowReferences = mapReferencesToShadow(tsconfig.references ?? [], {
resolveRefPath: refPath => (0, node_path_1.resolve)(projectDir, refPath),
toShadowPathFn: abs => (0, node_path_1.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 (0, promises_1.access)(shadowDualOutDir);
}
catch {
await (0, promises_1.rm)(dualTsBuildInfoFile, { force: true });
}
};
const refreshPrimaryBuildInfo = async () => {
try {
await (0, promises_1.access)(primaryOutDir);
}
catch {
await (0, promises_1.rm)(primaryTsBuildInfoFile, { force: true });
}
};
const resolveReferenceConfigPath = (baseDir, refPath) => {
const abs = (0, node_path_1.resolve)(baseDir, refPath);
return /\.json$/i.test(abs) ? abs : (0, node_path_1.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 = (0, get_tsconfig_1.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 = (0, node_path_1.resolve)(cwdForProject, specifier);
try {
const parsed = (0, get_tsconfig_1.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 = (0, node_path_1.resolve)(current.configPath);
if (seenConfigs.has(absConfig))
continue;
seenConfigs.add(absConfig);
configFiles.add(absConfig);
const cwdForProject = (0, node_path_1.dirname)(absConfig);
const extendsPath = current.tsconfig.extends;
if (extendsPath) {
const resolvedExtends = resolveExtendsConfig(extendsPath, cwdForProject);
if (resolvedExtends) {
const { path: extendsConfigPath, tsconfig: nextExtendsConfig } = resolvedExtends;
const normalizedExtendsPath = (0, node_path_1.resolve)(extendsConfigPath);
if (includeConfig(normalizedExtendsPath)) {
configFiles.add(normalizedExtendsPath);
logVerbose(`Including extended tsconfig ${normalizedExtendsPath} in copy plan`);
queue.push({
configPath: normalizedExtendsPath,
tsconfig: nextExtendsConfig,
projectDir: (0, node_path_1.dirname)(normalizedExtendsPath),
});
}
else {
if (!normalizedExtendsPath.split(node_path_1.sep).includes('node_modules')) {
(0, util_js_1.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 = (0, util_js_1.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 (0, promises_1.access)(jsSibling);
compileFiles.add(jsSibling);
}
catch {
/* optional */
}
}
}
const pkgPath = (0, node_path_1.join)(cwdForProject, 'package.json');
try {
await (0, promises_1.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 = (0, node_path_1.resolve)(refConfigPath);
try {
const parsed = (0, get_tsconfig_1.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: (0, node_path_1.dirname)(refAbsPath),
});
}
}
catch (err) {
(0, util_js_1.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 => {
(0, util_js_1.logSuccess)(`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 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 = (0, node_path_1.join)(subDir, tsconfigDualRel);
const dualConfigDir = (0, node_path_1.dirname)(dualConfigPath);
const tsconfigDual = getOverrideTsConfig(dualConfigDir);
const keepTemp = process.env.DUEL_KEEP_TEMP === '1';
const { cleanupTemp, cleanupTempSync } = (0, util_js_1.createTempCleanup)({
subDir,
keepTemp,
logWarnFn: util_js_1.logWarn,
});
const unregisterCleanupHandlers = (0, util_js_1.registerCleanupHandlers)(cleanupTempSync);
let errorMsg = '';
let exportsConfigData = null;
if (exportsConfig) {
try {
exportsConfigData = await (0, util_js_1.readExportsConfig)(exportsConfig, pkgDir);
}
catch (err) {
(0, util_js_1.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 (0, module_1.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([
(0, promises_1.mkdir)(subDir, { recursive: true }),
(0, promises_1.mkdir)(cacheDir, { recursive: true }),
]);
const linkNodeModulesPromise = (0, util_js_1.maybeLinkNodeModules)(projectDir, subDir);
const projectRel = requireWorkspaceRelative(projectDir);
const projectCopyDest = projectRel === '.' ? subDir : (0, node_path_1.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 = (0, node_path_1.relative)(rootDir, src);
if (rel.startsWith('..'))
return true;
const [segment] = rel.split(node_path_1.sep);
return segment !== outDir;
};
const copyFilesToTemp = async () => {
const copyDirContents = async (sourceDir, destDir, allowDist) => {
await (0, promises_1.mkdir)(destDir, { recursive: true });
const filter = makeCopyFilter(sourceDir, allowDist);
const entries = await (0, promises_1.readdir)(sourceDir, { withFileTypes: true });
for (const entry of entries) {
const srcPath = (0, node_path_1.join)(sourceDir, entry.name);
if (!filter(srcPath))
continue;
const dstPath = (0, node_path_1.join)(destDir, entry.name);
await (0, promises_1.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 = (0, node_path_1.resolve)(projectDir, ref.path);
const refRel = requireWorkspaceRelative(refAbs);
const refDest = (0, node_path_1.join)(subDir, refRel);
await copyDirContents(refAbs, refDest, allowDist);
}
}
}
else {
const filesToCopy = new Set([...compileFiles, ...configFiles, ...packageJsons]);
for (const file of filesToCopy) {
const normalized = (0, node_path_1.resolve)(file);
if (!isInAllowedRoots(normalized)) {
logVerbose(`Skipping non-local file ${normalized}`);
continue;
}
const rel = toWorkspaceRelative(normalized);
const dest = (0, node_path_1.join)(subDir, rel);
await (0, promises_1.mkdir)((0, node_path_1.dirname)(dest), { recursive: true });
await (0, promises_1.cp)(file, dest);
}
const missingConfigs = [];
for (const configFile of configFiles) {
const dest = (0, node_path_1.join)(subDir, requireWorkspaceRelative(configFile));
try {
await (0, promises_1.access)(dest);
}
catch {
missingConfigs.push({ src: configFile, dest });
}
}
if (missingConfigs.length) {
(0, util_js_1.logWarn)(`Copying ${missingConfigs.length} missing referenced config(s) into temp workspace: ${missingConfigs
.map(entry => entry.src)
.join(', ')}`);
for (const { src, dest } of missingConfigs) {
await (0, promises_1.mkdir)((0, node_path_1.dirname)(dest), { recursive: true });
await (0, promises_1.cp)(src, dest);
}
}
}
};
const toShadowPath = absPath => (0, node_path_1.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 = (0, node_path_1.join)(subDir, requireWorkspaceRelative(configFile));
const parsed = (0, get_tsconfig_1.parseTsconfig)(dest);
const cfg = parsed?.tsconfig ?? parsed;
if (!cfg || typeof cfg !== 'object')
continue;
const cfgDir = (0, node_path_1.dirname)(configFile);
const baseOut = cfg.compilerOptions?.outDir
? (0, node_path_1.resolve)(cfgDir, cfg.compilerOptions.outDir)
: (0, node_path_1.resolve)(cfgDir, 'dist');
const dualOutReal = (0, node_path_1.join)(baseOut, isCjsBuild ? 'cjs' : 'esm');
const dualOut = toShadowPath(dualOutReal);
const tsbuildReal = cfg.compilerOptions?.tsBuildInfoFile
? (0, node_path_1.resolve)(cfgDir, cfg.compilerOptions.tsBuildInfoFile)
: (0, node_path_1.join)(baseOut, 'tsconfig.tsbuildinfo');
const dualTsbuild = toShadowPath((0, node_path_1.join)((0, node_path_1.dirname)(tsbuildReal), 'tsconfig.dual.tsbuildinfo'));
const shadowReferences = mapReferencesToShadow(cfg.references ?? [], {
resolveRefPath: refPath => resolveReferenceConfigPath(cfgDir, refPath),
toShadowPathFn: toShadowPath,
fromDir: (0, node_path_1.dirname)(dest),
});
const patched = {
...cfg,
references: shadowReferences,
compilerOptions: {
...(cfg.compilerOptions ?? {}),
module: 'NodeNext',
moduleResolution: 'NodeNext',
outDir: dualOut,
incremental: cfg.compilerOptions?.incremental ?? true,
tsBuildInfoFile: dualTsbuild,
},
};
await (0, promises_1.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 = (0, node_path_1.join)(subDir, requireWorkspaceRelative(pkg.path));
await (0, promises_1.mkdir)((0, node_path_1.dirname)(pkgDest), { recursive: true });
await (0, promises_1.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 (0, promises_1.mkdir)(dualConfigDir, { recursive: true });
await (0, promises_1.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 (0, module_1.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 = (0, util_js_1.processDiagnosticsForFile)(filteredDiagnostics, projectDir, logDiagnosticsWithAllowlist);
transformDiagnosticsError = transformDiagnosticsError || errored;
}
(0, util_js_1.exitOnDiagnostics)(transformDiagnosticsError);
}
// Build dual
(0, util_js_1.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 (0, promises_1.rm)(absoluteDualOutDir, { force: true, recursive: true });
await (0, promises_1.mkdir)((0, node_path_1.dirname)(absoluteDualOutDir), { recursive: true });
// Only copy if the shadow dual outDir was produced; absent indicates a failed emit
try {
await (0, promises_1.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 = (0, util_js_1.processDiagnosticsForFile)(filtered, projectDir, logDiagnosticsWithAllowlist);
rewriteDiagnosticsError = rewriteDiagnosticsError || errored;
};
await (0, resolver_js_1.rewriteSpecifiersAndExtensions)(filenames, {
target: dualTarget,
ext: dualTargetExt,
syntaxMode: rewriteSyntaxMode,
detectDualPackageHazard: hazardMode,
dualPackageHazardScope: hazardScope,
dualPackageHazardAllowlist: [...hazardAllowlist],
onDiagnostics: handleRewriteDiagnostic,
rewritePolicy,
onWarn: message => (0, util_js_1.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 (0, resolver_js_1.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 => (0, util_js_1.logWarn)(message),
onRewrite: (from, to) => logVerbose(`Rewrote specifiers in ${from} -> ${to}`),
});
}
(0, util_js_1.exitOnDiagnostics)(rewriteDiagnosticsError);
const esmRoot = isCjsBuild ? primaryOutDir : absoluteDualOutDir;
const cjsRoot = isCjsBuild ? absoluteDualOutDir : primaryOutDir;
await (0, util_js_1.runExportsValidationBlock)({
exportsOpt,
exportsConfigData,
exportsValidate,
pkg,
pkgDir,
esmRoot,
cjsRoot,
mainDefaultKind,
mainPath,
});
await cleanupTemp();
unregisterCleanupHandlers();
logSuccess(startTime);
}
}
}
};
exports.duel = duel;
const getCurrentHref = () => {
if (typeof module !== 'undefined' && require("node:url").pathToFileURL(__filename).href)
return require("node:url").pathToFileURL(__filename).href;
if (typeof module !== 'undefined' && module?.filename) {
return (0, node_url_1.pathToFileURL)(module.filename).href;
}
return null;
};
const runIfEntry = async () => {
try {
const realFileUrlArgv1 = await (0, util_js_1.getRealPathAsFileUrl)(node_process_1.argv[1] ?? '');
const currentHref = getCurrentHref();
if (currentHref && currentHref === realFileUrlArgv1) {
await duel();
}
}
catch (err) {
(0, util_js_1.logError)(err?.message ?? err);
process.exit(1);
}
};
runIfEntry();