UNPKG

@knighted/duel

Version:
493 lines (492 loc) 19.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.runExportsValidationBlock = exports.maybeLinkNodeModules = exports.exitOnDiagnostics = exports.processDiagnosticsForFile = exports.filterDualPackageDiagnostics = exports.hazardPackageFromMessage = exports.registerCleanupHandlers = exports.createTempCleanup = exports.ensureDotSlash = exports.stripKnownExt = exports.generateExports = exports.getSubpath = exports.readExportsConfig = exports.getCompileFiles = exports.getRealPathAsFileUrl = exports.logWarn = exports.logSuccess = exports.logError = exports.log = void 0; const node_url_1 = require("node:url"); const promises_1 = require("node:fs/promises"); const node_fs_1 = require("node:fs"); const node_child_process_1 = require("node:child_process"); const node_process_1 = require("node:process"); const node_path_1 = require("node:path"); const find_up_1 = require("find-up"); const COLORS = { reset: '\x1b[0m', info: '\x1b[36m', success: '\x1b[32m', warn: '\x1b[33m', error: '\x1b[31m', }; const log = (msg = '', level = 'info', opts = {}) => { const { bare = false } = opts; const palette = { info: COLORS.info, success: COLORS.success, warn: COLORS.warn, error: COLORS.error, }; const badge = { success: '[✓]', warn: '[!]', error: '[x]', info: '[i]', }[level]; const color = palette[level] ?? COLORS.info; const prefix = !bare && badge ? `${badge} ` : ''; // eslint-disable-next-line no-console console.log(`${color}${prefix}%s${COLORS.reset}`, msg); }; exports.log = log; const logSuccess = msg => log(msg, 'success'); exports.logSuccess = logSuccess; const logWarn = msg => log(msg, 'warn'); exports.logWarn = logWarn; const logError = msg => log(msg, 'error'); exports.logError = logError; const createTempCleanup = ({ subDir, keepTemp = false, logWarnFn = logWarn }) => { let cleaned = false; let cleanupPromise = null; // Marks cleanup as started; returns false when already running or completed. const beginCleanup = () => { if (cleaned) return false; cleaned = true; return true; }; const cleanupTempSync = () => { if (!beginCleanup()) return; if (keepTemp) { logWarnFn(`DUEL_KEEP_TEMP=1 set; temp workspace preserved at ${subDir}`); return; } try { (0, node_fs_1.rmSync)(subDir, { force: true, recursive: true }); } catch { /* ignore */ } }; const cleanupTemp = async () => { if (cleanupPromise) return cleanupPromise; const runCleanup = async () => { if (!beginCleanup()) return; if (keepTemp) { logWarnFn(`DUEL_KEEP_TEMP=1 set; temp workspace preserved at ${subDir}`); return; } try { await (0, promises_1.rm)(subDir, { force: true, recursive: true }); } catch { /* ignore */ } }; cleanupPromise = runCleanup(); return cleanupPromise; }; return { cleanupTempSync, cleanupTemp }; }; exports.createTempCleanup = createTempCleanup; const registerCleanupHandlers = cleanupTempSync => { const onExit = () => cleanupTempSync(); const onSigint = () => { cleanupTempSync(); process.exit(1); }; const onSigterm = () => { cleanupTempSync(); process.exit(1); }; const onUncaught = err => { cleanupTempSync(); throw err; }; const onUnhandled = reason => { cleanupTempSync(); throw reason instanceof Error ? reason : new Error(String(reason)); }; process.once('exit', onExit); process.once('SIGINT', onSigint); process.once('SIGTERM', onSigterm); process.once('uncaughtException', onUncaught); process.once('unhandledRejection', onUnhandled); return () => { process.removeListener('exit', onExit); process.removeListener('SIGINT', onSigint); process.removeListener('SIGTERM', onSigterm); process.removeListener('uncaughtException', onUncaught); process.removeListener('unhandledRejection', onUnhandled); }; }; exports.registerCleanupHandlers = registerCleanupHandlers; const getRealPathAsFileUrl = async (path) => { const realPath = await (0, promises_1.realpath)(path); const asFileUrl = (0, node_url_1.pathToFileURL)(realPath).href; return asFileUrl; }; exports.getRealPathAsFileUrl = getRealPathAsFileUrl; // Backward compatibility: `options` may be a string (cwd) or an object with { cwd, project }. const getCompileFiles = (tscPath, options = {}) => { const { cwd: workingDir = (0, node_process_1.cwd)(), project = null } = typeof options === 'string' ? { cwd: options, project: null } : options; const args = [tscPath]; if (project) { const projectPath = (0, node_path_1.isAbsolute)(project) ? project : (0, node_path_1.resolve)(workingDir, project); if (!(0, node_fs_1.existsSync)(projectPath)) { throw new Error(`Project path not found: ${project}`); } args.push('-p', project); } args.push('--listFilesOnly'); const { stdout } = (0, node_child_process_1.spawnSync)(process.execPath, args, { cwd: workingDir, }); const root = (0, node_path_1.normalize)((0, node_path_1.resolve)(workingDir)); const normalize = candidate => (0, node_path_1.normalize)((0, node_path_1.isAbsolute)(candidate) ? candidate : (0, node_path_1.resolve)(workingDir, candidate)); // Normalize casing only on Windows; POSIX stays case-sensitive to match fs semantics. const toComparable = path => (node_process_1.platform === 'win32' ? path.toLowerCase() : path); const rootComparable = toComparable(root); const isInsideRoot = candidate => { const comparable = toComparable(candidate); return (comparable === rootComparable || comparable.startsWith(`${rootComparable}${node_path_1.sep}`)); }; const isNodeModules = candidate => candidate.split(node_path_1.sep).includes('node_modules'); const allPaths = stdout .toString() // tsc may emit LF or CRLF depending on shell/platform; accept both. .split(/\r?\n/) .map(line => line.trim()) .filter(Boolean) .map(normalize) .filter(path => !isNodeModules(path)); const insideRoot = allPaths.filter(isInsideRoot); // Prefer paths within the project root. On Windows, edge cases (UNC paths, junctions, etc.) // can cause all paths to be filtered out. Fall back to unbounded list. See docs/faq.md. return insideRoot.length ? insideRoot : allPaths; }; exports.getCompileFiles = getCompileFiles; const stripKnownExt = path => { return path.replace(/(\.d\.(?:ts|mts|cts)|\.(?:mjs|cjs|js))$/, ''); }; exports.stripKnownExt = stripKnownExt; const ensureDotSlash = path => { return path.startsWith('./') ? path : `./${path}`; }; exports.ensureDotSlash = ensureDotSlash; const readExportsConfig = async (configPath, pkgDir) => { const abs = (0, node_path_1.isAbsolute)(configPath) ? configPath : configPath.startsWith('.') ? (0, node_path_1.resolve)(pkgDir, configPath) : (0, node_path_1.resolve)((0, node_process_1.cwd)(), configPath); const raw = await (0, promises_1.readFile)(abs, 'utf8'); let parsed; try { parsed = JSON.parse(raw); } catch (err) { throw new Error(`Invalid JSON in --exports-config (${configPath}): ${err.message}`, { cause: err, }); } const { entries, main } = parsed; if (!entries || !Array.isArray(entries) || entries.some(item => typeof item !== 'string')) { throw new Error('--exports-config expects an object with an "entries" array of strings'); } if (main && typeof main !== 'string') { throw new Error('--exports-config "main" must be a string when provided'); } const normalize = value => ensureDotSlash(value.replace(/\\/g, '/')); const normalizedEntries = [...new Set(entries.map(normalize))]; const normalizedMain = main ? normalize(main) : null; return { entries: normalizedEntries, main: normalizedMain }; }; exports.readExportsConfig = readExportsConfig; const getSubpath = (mode, relFromRoot) => { const parsed = (0, node_path_1.parse)(relFromRoot); const segments = parsed.dir.split('/').filter(Boolean); if (mode === 'name') { return parsed.name ? `./${parsed.name}` : null; } if (mode === 'dir') { const last = segments.at(-1); return last ? `./${last}/*` : null; } if (mode === 'wildcard') { const first = segments[0]; return first ? `./${first}/*` : null; } return null; }; exports.getSubpath = getSubpath; const generateExports = async (options) => { const { mode, pkg, pkgDir, esmRoot, cjsRoot, mainDefaultKind, mainPath, entries, dryRun, } = options; const toPosix = path => path.replace(/\\/g, '/'); const esmRootPosix = toPosix(esmRoot); const cjsRootPosix = toPosix(cjsRoot); const esmPrefix = toPosix((0, node_path_1.relative)(pkgDir, esmRoot)); const cjsPrefix = toPosix((0, node_path_1.relative)(pkgDir, cjsRoot)); const esmIgnore = ['node_modules/**']; const cjsIgnore = ['node_modules/**']; const baseMap = new Map(); const subpathMap = new Map(); const baseToSubpath = new Map(); if (cjsRootPosix.startsWith(`${esmRootPosix}/`)) { esmIgnore.push(`${cjsRootPosix}/**`); } if (esmRootPosix.startsWith(`${cjsRootPosix}/`)) { cjsIgnore.push(`${esmRootPosix}/**`); } const toWildcardValue = value => { const dir = node_path_1.posix.dirname(value); const file = node_path_1.posix.basename(value); const dtsMatch = file.match(/(\.d\.(?:ts|mts|cts))$/i); if (dtsMatch) { const ext = dtsMatch[1]; return dir === '.' ? `./*${ext}` : `${dir}/*${ext}`; } const ext = node_path_1.posix.extname(file); return dir === '.' ? `./*${ext}` : `${dir}/*${ext}`; }; const expandEntriesBase = base => { const variants = [base]; if (esmPrefix && cjsPrefix && esmPrefix !== cjsPrefix) { const esmPrefixWithSlash = `${esmPrefix}/`; const cjsPrefixWithSlash = `${cjsPrefix}/`; if (base.startsWith(esmPrefixWithSlash)) { variants.push(base.replace(esmPrefixWithSlash, cjsPrefixWithSlash)); } if (base.startsWith(cjsPrefixWithSlash)) { variants.push(base.replace(cjsPrefixWithSlash, esmPrefixWithSlash)); } } return variants; }; const entriesBase = entries?.length ? new Set(entries.flatMap(entry => { const normalized = stripKnownExt(entry.replace(/^\.\//, '')); return expandEntriesBase(normalized); })) : null; const recordPath = (kind, filePath, root) => { const relPkg = toPosix((0, node_path_1.relative)(pkgDir, filePath)); const relFromRoot = toPosix((0, node_path_1.relative)(root, filePath)); const withDot = ensureDotSlash(relPkg); const baseKey = stripKnownExt(relPkg); const useEntriesSubpaths = Boolean(entriesBase); if (entriesBase && !entriesBase.has(baseKey)) { return; } const baseEntry = baseMap.get(baseKey) ?? {}; if (kind === 'types') { baseEntry.types = baseEntry.types ?? withDot; } else { baseEntry[kind] = withDot; } baseMap.set(baseKey, baseEntry); const subpath = useEntriesSubpaths ? ensureDotSlash(stripKnownExt(relFromRoot)) : getSubpath(mode, relFromRoot); const useWildcard = subpath?.includes('*'); if (kind === 'types') { const mappedSubpath = baseToSubpath.get(baseKey); if (mappedSubpath) { const subEntry = subpathMap.get(mappedSubpath) ?? {}; const nextType = useWildcard ? toWildcardValue(withDot) : withDot; subEntry.types = subEntry.types ?? nextType; subpathMap.set(mappedSubpath, subEntry); } return; } if (subpath && subpath !== '.') { const subEntry = subpathMap.get(subpath) ?? {}; subEntry[kind] = useWildcard ? toWildcardValue(withDot) : withDot; subpathMap.set(subpath, subEntry); baseToSubpath.set(baseKey, subpath); } }; for await (const file of (0, promises_1.glob)(`${esmRootPosix}/**/*.{js,mjs,d.ts,d.mts}`, { ignore: esmIgnore, })) { if (/\.d\.(ts|mts)$/.test(file)) { recordPath('types', file, esmRoot); } else { recordPath('import', file, esmRoot); } } for await (const file of (0, promises_1.glob)(`${cjsRootPosix}/**/*.{js,cjs,d.ts,d.cts}`, { ignore: cjsIgnore, })) { if (/\.d\.(ts|cts)$/.test(file)) { recordPath('types', file, cjsRoot); } else { recordPath('require', file, cjsRoot); } } const exportsMap = {}; const mainBase = mainPath ? stripKnownExt(mainPath.replace(/^\.\//, '')) : null; const mainEntry = mainBase ? (baseMap.get(mainBase) ?? {}) : {}; if (mainPath) { const rootEntry = {}; if (mainEntry.types) { rootEntry.types = mainEntry.types; } if (mainDefaultKind === 'import') { rootEntry.import = mainEntry.import ?? ensureDotSlash(mainPath); if (mainEntry.require) { rootEntry.require = mainEntry.require; } } else { rootEntry.require = mainEntry.require ?? ensureDotSlash(mainPath); if (mainEntry.import) { rootEntry.import = mainEntry.import; } } rootEntry.default = ensureDotSlash(mainPath); exportsMap['.'] = rootEntry; } const defaultKind = mainDefaultKind ?? 'import'; for (const [subpath, entry] of subpathMap.entries()) { const out = {}; if (entry.types) { out.types = entry.types; } if (entry.import) { out.import = entry.import; } if (entry.require) { out.require = entry.require; } const def = defaultKind === 'import' ? (entry.import ?? entry.require) : (entry.require ?? entry.import); if (def) { out.default = def; } if (Object.keys(out).length) { exportsMap[subpath] = out; } } if (!exportsMap['.']) { const firstNonWildcard = [...subpathMap.entries()].find(([key]) => !key.includes('*')); if (firstNonWildcard) { const [, entry] = firstNonWildcard; const out = {}; if (entry.types) { out.types = entry.types; } if (entry.import) { out.import = entry.import; } if (entry.require) { out.require = entry.require; } const def = defaultKind === 'import' ? (entry.import ?? entry.require) : (entry.require ?? entry.import); if (def) { out.default = def; } if (Object.keys(out).length) { exportsMap['.'] = out; } } } if (Object.keys(exportsMap).length) { if (dryRun) { return { exportsMap }; } const pkgJson = { ...pkg.packageJson, exports: exportsMap, }; await (0, promises_1.writeFile)(pkg.path, `${JSON.stringify(pkgJson, null, 2)}\n`); } return { exportsMap }; }; exports.generateExports = generateExports; const hazardPackageFromMessage = message => { if (!message) return null; const match = /Package '([^']+)'/.exec(message); return match?.[1] ?? null; }; exports.hazardPackageFromMessage = hazardPackageFromMessage; const filterDualPackageDiagnostics = (diagnostics, allowlist = new Set()) => { if (!diagnostics?.length) return []; const allowlistSet = allowlist instanceof Set ? allowlist : new Set(allowlist); if (!allowlistSet.size) return diagnostics; return diagnostics.filter(diag => { if (!diag) return false; if (!diag.code?.startsWith('dual-package')) return true; const pkg = hazardPackageFromMessage(diag.message ?? ''); if (!pkg) return true; return !allowlistSet.has(pkg); }); }; exports.filterDualPackageDiagnostics = filterDualPackageDiagnostics; const processDiagnosticsForFile = (diagnostics, projectDir, logDiagnosticsFn) => { if (!diagnostics.length) return false; return logDiagnosticsFn(diagnostics, projectDir); }; exports.processDiagnosticsForFile = processDiagnosticsForFile; const exitOnDiagnostics = (hasError, exitFn = process.exit) => { if (hasError) { exitFn(1); } }; exports.exitOnDiagnostics = exitOnDiagnostics; const maybeLinkNodeModules = async (projectRoot, subDir, symlinkFn = promises_1.symlink, findUpFn = find_up_1.findUp) => { const nodeModules = await findUpFn('node_modules', { cwd: projectRoot, type: 'directory', }); if (nodeModules) { try { await symlinkFn(nodeModules, (0, node_path_1.join)(subDir, 'node_modules'), 'junction'); } catch (err) { if (err?.code === 'EEXIST') return; logWarn(`Failed to link node_modules into temp workspace (falling back to existing resolution): ${err.message}`); } } }; exports.maybeLinkNodeModules = maybeLinkNodeModules; const runExportsValidationBlock = async (options) => { const { exportsOpt, exportsConfigData, exportsValidate, pkg, pkgDir, esmRoot, cjsRoot, mainDefaultKind, mainPath, logWarnFn = logWarn, logFn = log, generateExportsFn = generateExports, } = options; if (!exportsOpt && !exportsConfigData && !exportsValidate) { return { exportsMap: null }; } if (exportsValidate && !exportsOpt && !exportsConfigData) { logWarnFn('--exports-validate has no effect without --exports or --exports-config; No exports were written.'); } const result = await generateExportsFn({ mode: exportsOpt, pkg, pkgDir, esmRoot, cjsRoot, mainDefaultKind, mainPath: exportsConfigData?.main ?? mainPath, entries: exportsConfigData?.entries, dryRun: exportsValidate, }); if (exportsValidate) { logFn('Exports validation successful.'); } return result; }; exports.runExportsValidationBlock = runExportsValidationBlock;