UNPKG

@knighted/duel

Version:
224 lines (223 loc) 10.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.rewriteSpecifiersAndExtensions = void 0; const promises_1 = require("node:fs/promises"); const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const magic_string_1 = __importDefault(require("magic-string")); const trace_mapping_1 = require("@jridgewell/trace-mapping"); const gen_mapping_1 = require("@jridgewell/gen-mapping"); const module_1 = require("@knighted/module"); const loadMapIfExists = async (path) => { try { const raw = await (0, promises_1.readFile)(path, 'utf8'); return JSON.parse(raw); } catch { return null; } }; const updateSourceMappingUrl = (content, mapFile) => { const comment = `//# sourceMappingURL=${mapFile}`; if (/\/\/# sourceMappingURL=/.test(content)) { return content.replace(/\/\/# sourceMappingURL=.*/g, comment); } return `${content}\n${comment}\n`; }; const composeSourceMaps = (rewriteMap, baseMap) => { const outer = new trace_mapping_1.TraceMap(rewriteMap); const inner = new trace_mapping_1.TraceMap(baseMap); const file = rewriteMap.file ?? baseMap.file; const gen = new gen_mapping_1.GenMapping({ file }); (0, trace_mapping_1.eachMapping)(outer, mapping => { if (mapping.originalLine == null || mapping.originalColumn == null) return; const traced = (0, trace_mapping_1.originalPositionFor)(inner, { line: mapping.originalLine, column: mapping.originalColumn, }); if (traced.line == null || traced.column == null || traced.source == null) return; (0, gen_mapping_1.addMapping)(gen, { generated: { line: mapping.generatedLine, column: mapping.generatedColumn }, original: { line: traced.line, column: traced.column }, source: traced.source, name: traced.name ?? mapping.name ?? null, }); }); const sourcesContent = new Map(); const baseSources = Array.isArray(baseMap.sources) ? baseMap.sources : []; const baseContents = Array.isArray(baseMap.sourcesContent) ? baseMap.sourcesContent : []; baseSources.forEach((source, idx) => { const content = baseContents[idx]; if (content != null) { sourcesContent.set(source, content); } }); for (const [source, content] of sourcesContent.entries()) { (0, gen_mapping_1.setSourceContent)(gen, source, content); } const composed = (0, gen_mapping_1.toDecodedMap)(gen); composed.file = file; return composed; }; const rewriteSpecifiersAndExtensions = async (filenames, options = {}) => { const { target, ext, syntaxMode, detectDualPackageHazard, dualPackageHazardAllowlist, dualPackageHazardScope, onDiagnostics, rewritePolicy = 'safe', validateSpecifiers, onRewrite = () => { }, onWarn = () => { }, } = options; const validateSpecifiersFinal = validateSpecifiers ?? (rewritePolicy === 'skip' ? false : true); const rewrites = []; for (const filename of filenames) { const dts = /(\.d\.ts)$/; const isDts = dts.test(filename); const outFilename = isDts ? filename.replace(dts, target === 'commonjs' ? '.d.cts' : '.d.mts') : filename.replace(/\.js$/, ext); if (isDts) { const source = await (0, promises_1.readFile)(filename, 'utf8'); const code = new magic_string_1.default(source); let mutated = false; for (const match of source.matchAll(/(?<=['"`])(\.{1,2}(?:\/[\w.-]+)*)\.js(?=['"`])/g)) { if (match.index == null) continue; const start = match.index; const end = start + match[0].length; code.overwrite(start, end, `${match[1]}${ext}`); mutated = true; } const existingMapPath = `${filename}.map`; const existingMap = await loadMapIfExists(existingMapPath); if (mutated) { rewrites.push({ file: filename, kind: 'dts' }); } const outMapPath = `${outFilename}.map`; const mapFile = (0, node_path_1.basename)(outMapPath); let output = code.toString(); let nextMap = null; /* * If an upstream map exists, carry it forward when we rename the file, * and compose when the content was mutated. If no upstream map exists, * do not emit a new one. */ if (existingMap) { if (mutated) { const rewriteMap = code.generateMap({ hires: true, includeContent: true, file: outFilename, source: filename, }); nextMap = composeSourceMaps(rewriteMap, existingMap); } else { nextMap = { ...existingMap }; } } if (nextMap) { nextMap.file = (0, node_path_1.basename)(outFilename); output = updateSourceMappingUrl(output, mapFile); await (0, promises_1.writeFile)(outMapPath, JSON.stringify(nextMap)); } await (0, promises_1.writeFile)(outFilename, output); if (outFilename !== filename) { await (0, promises_1.rm)(filename, { force: true }); if (existingMap) { await (0, promises_1.rm)(existingMapPath, { force: true }); } } continue; } const rewriteSpecifier = (value = '') => { const collapsed = value.replace(/['"`+)\s]|new String\(/g, ''); const hasTemplate = value.includes('${'); // Only consider relative specifiers (POSIX or Windows) and .js endings. if (!/^\.{1,2}[\\/]/.test(collapsed) || !/\.js$/.test(collapsed)) { return null; } if (rewritePolicy === 'skip') { return null; } // Non-greedy to avoid over-consuming on values like "./foo.js.js". const next = value.replace(/(.+?)\.js([)"'`]*)?$/, `$1${ext}$2`); if (hasTemplate) { // Dynamic/template specifiers cannot be validated statically; still rewrite the // extension to keep CJS/ESM outputs aligned without emitting noisy warnings. return next; } if (validateSpecifiersFinal) { const fileDir = (0, node_path_1.dirname)(filename); const base = collapsed.replace(/\.js$/, ''); const candidates = []; const exts = [ext, '.js', '.mjs', '.cjs']; for (const variant of exts) { candidates.push((0, node_path_1.resolve)(fileDir, `${base}${variant}`)); candidates.push((0, node_path_1.resolve)(fileDir, `${base}/index${variant}`)); } const exists = candidates.some(path => { try { (0, node_fs_1.accessSync)(path); return true; } catch { return false; } }); if (!exists) { const missingTargetMessage = `${collapsed} -> ${base}{${exts.join(',')}}`; if (rewritePolicy === 'safe') { onWarn(`Skipped rewrite for missing target: ${missingTargetMessage}`); return null; } if (rewritePolicy === 'warn') { onWarn(`Rewriting specifier with missing target: ${missingTargetMessage}`); } } } return next; }; const writeOptions = { target, rewriteSpecifier, transformSyntax: syntaxMode, sourceMap: true, diagnostics: diag => onDiagnostics?.(diag, filename), ...(detectDualPackageHazard !== undefined ? { detectDualPackageHazard } : {}), ...(dualPackageHazardAllowlist !== undefined ? { dualPackageHazardAllowlist } : {}), ...(dualPackageHazardScope !== undefined ? { dualPackageHazardScope } : {}), ...(outFilename === filename ? { inPlace: true } : { out: outFilename }), }; const result = await (0, module_1.transform)(filename, writeOptions); const nextCode = result?.code ?? result; const rewriteMap = result?.map ?? null; const existingMapPath = `${filename}.map`; const existingMap = await loadMapIfExists(existingMapPath); const outMapPath = `${outFilename}.map`; let output = typeof nextCode === 'string' ? nextCode : String(nextCode); let nextMap = null; /* * Compose the rewrite map with the upstream map when present; if the * input had no map, we do not emit a new one. */ if (rewriteMap && existingMap) { nextMap = composeSourceMaps(rewriteMap, existingMap); } if (nextMap) { const mapFile = (0, node_path_1.basename)(outMapPath); nextMap.file = (0, node_path_1.basename)(outFilename); output = updateSourceMappingUrl(output, mapFile); await (0, promises_1.writeFile)(outMapPath, JSON.stringify(nextMap)); } await (0, promises_1.writeFile)(outFilename, output); if (outFilename !== filename) { await (0, promises_1.rm)(filename, { force: true }); if (existingMap) { await (0, promises_1.rm)(existingMapPath, { force: true }); } } rewrites.push({ file: filename, kind: 'source' }); onRewrite(filename, outFilename); } return { rewrites }; }; exports.rewriteSpecifiersAndExtensions = rewriteSpecifiersAndExtensions;