@knighted/duel
Version:
TypeScript dual packages.
224 lines (223 loc) • 10.3 kB
JavaScript
;
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;