fuse-box
Version:
Fuse-Box a bundler that does it right
207 lines (206 loc) • 8.98 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.mappingsToResolver = exports.createTsTargetResolver = exports.groupByPackage = exports.buildMappings = exports.createTsParseConfigHost = void 0;
const fs_1 = require("fs");
const path_1 = require("path");
const typescript_1 = require("typescript");
const fileLookup_1 = require("../resolver/fileLookup");
const utils_1 = require("../utils/utils");
function createTsParseConfigHost() {
return {
fileExists: typescript_1.sys.fileExists,
readDirectory: typescript_1.sys.readDirectory,
readFile: typescript_1.sys.readFile,
useCaseSensitiveFileNames: true,
};
}
exports.createTsParseConfigHost = createTsParseConfigHost;
const resolveVirtual = process.versions.pnp && require('pnpapi').resolveVirtual;
// recurse through the references to build a mapping of outputs to inputs
function buildMappings(references, tsConfigDir) {
// build the mappings
const tsHost = createTsParseConfigHost();
const inputsByOutput = new Map();
const rawReferences = references || [];
for (const rawRef of rawReferences) {
const { path: rawPath } = rawRef;
if (!rawPath)
continue;
const absPath = utils_1.ensureAbsolutePath(rawPath, tsConfigDir);
recurseTsReference(tsHost, absPath, inputsByOutput, [fs_1.realpathSync(absPath)]);
}
return inputsByOutput;
}
exports.buildMappings = buildMappings;
// group an out-to-in mapping by package (accounting for nested pacakges)
function groupByPackage(mappings) {
// find all the relevant package roots (bases)
// we need to know what node packages the typescript out/in files are in
// so that when we get an output, even if it is a PnP virtual path, we find the right out/in pair
const byPackage = new Map();
for (const [output, input] of mappings) {
for (const { base, rel: outputRel } of packageAncestorsOf(output)) {
const map = byPackage.get(base) || new Map();
if (map.size == 0) {
byPackage.set(base, map);
}
const inputRel = path_1.relative(base, input);
map.set(outputRel, inputRel);
}
}
return byPackage;
}
exports.groupByPackage = groupByPackage;
// Create a resolver that can resolve project subpaths
// mapping from outputs (e.g. dist/file.js) to their inputs (e.g. src/file.ts)
function createTsTargetResolver(references, tsConfigDir) {
const mappings = groupByPackage(buildMappings(references, tsConfigDir));
if (mappings.size === 0)
return undefined;
return mappingsToResolver(mappings);
}
exports.createTsTargetResolver = createTsTargetResolver;
function mappingsToResolver(mappings) {
// take a base and a relative target and return the input, if any, that generates it
function tsMapToInput(pkgRoot, subPath) {
const inByOutRel = mappings.get(pkgRoot);
if (inByOutRel) {
const input = inByOutRel.get(subPath);
if (input) {
return input;
}
}
return subPath;
}
// create a SubPathResolver that checks the Out->In maps
const tsResolver = (base, target, type, props) => {
const basicResult = fileLookup_1.resolveIfExists(base, tsMapToInput(base, target), type, props);
if (basicResult) {
return basicResult;
}
// Handle PnP virtual paths
if (resolveVirtual) {
const realBase = resolveVirtual(base);
if (realBase && realBase !== base) {
// if we are dealing with virtual paths
// we need to send the "real" base path to the ts mapper
// but send the original virtual base path as our result
var result = fileLookup_1.resolveIfExists(base, tsMapToInput(realBase, target), type, props);
if (result) {
return result;
}
}
}
return undefined;
};
// Construct a TargetResolver but inject our own TypeScript sub-path resolver
return (lookupArgs) => {
// same as fileLookup but with our own subpath resolver
// also, search .js first, because that's what our resolver is looking for
return fileLookup_1.fileLookup(Object.assign(Object.assign({}, lookupArgs), { javascriptFirst: true, subPathResolver: tsResolver }));
};
}
exports.mappingsToResolver = mappingsToResolver;
function parentDir(normalizedPath) {
const parent = path_1.dirname(normalizedPath);
return parent !== normalizedPath ? parent : undefined;
}
// find all package.json files in the folder ancestry
function packageAncestorsOf(path) {
const result = [];
const start = path_1.normalize(path);
let rel = path_1.basename(start);
for (let dir = parentDir(start); dir !== undefined; rel = utils_1.pathJoin(path_1.basename(dir), rel), dir = parentDir(dir)) {
const packageJsonPath = utils_1.pathJoin(dir, 'package.json');
if (utils_1.fileExists(packageJsonPath)) {
result.push({ base: dir, rel });
}
}
return result;
}
// Follow a single TypeScript reference recursively through its references
// adding output->input mappings along the way
function recurseTsReference(tsHost, reference, map, anticycle) {
if (!utils_1.fileExists(reference)) {
throw new Error(`Unable to find tsconfig reference ${reference}`);
}
const realPath = fs_1.realpathSync(reference);
const result = loadTsConfig(realPath, tsHost);
if (!result)
return;
const { files, references } = result;
for (const file of files) {
const existing = map.get(file.output);
// Ensure either there is no entry for that output, or if there is, it is the same input producing it
if (existing && existing !== file.input) {
throw new Error(`Multiple input files map to same output file (1. "${existing}", 2. "${file.input}") => ("${file.output}")`);
}
map.set(file.output, file.input);
}
for (const subref of references || []) {
const path = fs_1.realpathSync(subref.path);
if (anticycle.includes(path)) {
throw new Error(`Project references may not form a circular path. Cycle detected: ${subref.path}`);
}
recurseTsReference(tsHost, subref.path, map, [...anticycle, path]);
}
}
// Read a tsconfig file from disk and parse out the relevant parts
// and scan input files
function loadTsConfig(path, host) {
const normalized = path_1.normalize(path);
const byFolder = utils_1.pathJoin(path, 'tsconfig.json');
if (host.fileExists(byFolder))
return loadTsConfig(byFolder, host);
const raw = host.readFile(normalized);
if (!raw) {
throw new Error(`Unable to read tsconfig file at ${normalized}`);
}
const tsconfig = typescript_1.parseJsonText(normalized, raw);
// use typescript's own config file logic to get the list of input files
const files = typescript_1.parseJsonSourceFileConfigFileContent(tsconfig, host, path_1.dirname(normalized));
if (!files.options.composite) {
throw new Error(`Referenced project '${path}' must have settings "composite": true.`);
}
// since composite === true, we can determine each output from our list of inputs
// so now we know which outputs a build would create, and what they are, and what their inputs are
return {
files: files.fileNames.map(input => calculateInOutMap(files.options.rootDir, files.options.outDir, input)),
path: normalized,
references: files.projectReferences,
};
}
// The extensions that Typescript will process to the output directory
const tsOutExts = {
'.js': '.js',
'.json': '.json',
'.jsx': '.js',
'.ts': '.js',
'.tsx': '.jsx',
};
// e.g.: (.*)((\.jsx$)|(\.json$)|(\.js$)|(\.tsx$)|(\.ts$))
const tsOutPattern = new RegExp(`(.*)(${Object.keys(tsOutExts)
.map(ext => `(\\${ext})`)
.join('|')})`);
function parseTsExtension(file) {
const match = tsOutPattern.exec(file);
return match ? { ext: match[2], stem: match[1] } : { ext: '', stem: file };
}
// Calculate the output -> input map given the rootDir, outDir, and input
function calculateInOutMap(rootDir, outDir, input) {
const nroot = path_1.normalize(rootDir);
const nout = path_1.normalize(outDir);
const ninput = path_1.normalize(input);
if (!ninput.startsWith(nroot)) {
throw new Error(`File '${ninput}' is not under 'rootDir' '${rootDir}'. 'rootDir' is expected to contain all source files`);
}
// strip the root part to get the relative part
const relInput = ninput.substr(nroot.length);
const { ext, stem } = parseTsExtension(relInput);
const outExt = tsOutExts[ext] || ext;
return (outExt && {
base: rootDir,
input: ninput,
output: utils_1.pathJoin(nout, `${stem}${outExt}`),
});
}
;