UNPKG

@knighted/duel

Version:
759 lines (758 loc) 39.3 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_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();