@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
148 lines (140 loc) • 6.36 kB
JavaScript
// webpack does not like node: imports
// eslint-disable-next-line n/prefer-node-protocol
import fs from 'fs/promises';
// eslint-disable-next-line n/prefer-node-protocol
import { fileURLToPath } from 'url';
import { resolve } from 'import-meta-resolve';
import { resolveVariantPathsWithFs } from "../loadServerCodeMeta/index.mjs";
import { fileUrlToPortablePath, portablePathToFileUrl } from "../loaderUtils/fileUrlToPortablePath.mjs";
/**
* Transforms tsconfig.json paths configuration into regex patterns for matching.
* Converts glob patterns like "**" and "*" into regex capture groups.
*
* @param tsconfigPaths - The paths object from tsconfig.json
* @returns A map of regex patterns to replacement templates
*/
function transformTsconfigPaths(tsconfigPaths) {
const paths = {};
Object.keys(tsconfigPaths).forEach(key => {
const regex = `^${key.replace('**', '(.+)').replace('*', '([^/]+)')}$`;
paths[regex] = tsconfigPaths[key].map(p => {
let index = 0;
return p.replace(/\*\*|\*/g, () => {
index = index + 1;
return `$${index}`;
});
});
});
return paths;
}
/**
* Resolves library source file paths to their actual file system locations.
*
* Handles three types of library imports:
* 1. Relative paths - files within the project root
* 2. Path-mapped imports - imports resolved through tsconfig paths
* 3. External library imports - imports resolved through node module resolution
*
* For external libraries with watchSourceDirectly enabled, follows source maps
* to find the original TypeScript source files instead of declaration files.
*
* If watchSourceDirectly is not explicitly provided, it will be automatically
* determined based on whether any variants use tsconfig path aliases.
*/
export async function resolveLibrarySourceFiles(options) {
const {
variants,
resourcePath,
rootContextDirUrl,
tsconfigPaths,
pathsBaseDir
} = options;
// Convert file URL to portable path for comparison with variant paths
const rootContextDir = fileUrlToPortablePath(rootContextDirUrl);
// Determine watchSourceDirectly if not explicitly provided
// If any variant uses a tsconfig path alias, we should watch source files directly
const watchSourceDirectly = options.watchSourceDirectly ?? (tsconfigPaths ? Object.values(variants).some(variantUrl => {
const variantPath = fileUrlToPortablePath(variantUrl);
// Skip relative paths - they don't need source watching
if (variantPath.startsWith(rootContextDir)) {
return false;
}
// Check if this variant path matches any tsconfig path pattern
return Object.keys(tsconfigPaths).some(pattern => {
const regexPattern = pattern.replace(/\*\*/g, '.+').replace(/\*/g, '[^/]+');
return new RegExp(`^${regexPattern}`).test(variantPath);
});
}) : false);
const relativeVariants = {};
const externalVariants = {};
// Transform tsconfig paths into regex patterns
const paths = tsconfigPaths && transformTsconfigPaths(tsconfigPaths);
// Categorize variants as relative, path-mapped, or external
Object.entries(variants).forEach(([variantName, variantUrl]) => {
const variantPath = fileUrlToPortablePath(variantUrl);
if (variantPath.startsWith(rootContextDir)) {
relativeVariants[variantName] = variantPath;
} else if (paths) {
const found = Object.keys(paths).find(key => {
const regex = new RegExp(key);
const pathMatch = variantPath.match(regex);
if (pathMatch && pathMatch.length > 0) {
const replacements = paths[key];
for (const replacement of replacements) {
let replacedPath = replacement;
for (let i = 1; i < pathMatch.length; i += 1) {
replacedPath = replacedPath.replace(`$${i}`, pathMatch[i]);
}
if (replacedPath.startsWith('.')) {
const baseDir = pathsBaseDir || rootContextDir;
relativeVariants[variantName] = new URL(replacedPath, portablePathToFileUrl(baseDir)).pathname;
} else {
externalVariants[variantName] = replacedPath;
}
return true;
}
}
return false;
});
if (!found) {
externalVariants[variantName] = variantPath;
}
} else {
externalVariants[variantName] = variantPath;
}
});
// Resolve relative variants using file system
const resolvedVariantMap = await resolveVariantPathsWithFs(relativeVariants);
// Resolve external variants using import resolution
const externalVariantPromises = Object.entries(externalVariants).map(async ([variantName, variantPath]) => {
// We can use this ponyfill because it behaves strangely when using native import.meta.resolve(path, parentUrl)
const resolvedUrl = resolve(variantPath, portablePathToFileUrl(resourcePath));
if (!watchSourceDirectly) {
// When not watching source directly, we want to analyze the .d.ts file, not the .js file
const dtsUrl = resolvedUrl.replace('.js', '.d.ts').replace('.mjs', '.d.mts').replace('.cjs', '.d.cts');
return [variantName, dtsUrl];
}
// Lookup the source map to find the original .ts/.tsx source file
const sourceMapUrl = resolvedUrl.replace('.js', '.d.ts.map').replace('.mjs', '.d.mts.map').replace('.cjs', '.d.cts.map');
const sourceMap = await fs.readFile(fileURLToPath(sourceMapUrl), 'utf-8').catch(() => null);
if (!sourceMap) {
throw new Error(`Missing source map for variant "${variantName}" at ${sourceMapUrl}.`);
}
const parsedSourceMap = JSON.parse(sourceMap);
if (!('sources' in parsedSourceMap) || !Array.isArray(parsedSourceMap.sources) || parsedSourceMap.sources.length === 0) {
throw new Error(`Invalid source map for variant "${variantName}" at ${sourceMapUrl}. Missing "sources" field.`);
}
const basePath = parsedSourceMap.sourceRoot ? new URL(parsedSourceMap.sourceRoot, resolvedUrl) : resolvedUrl;
const sourceUrl = new URL(parsedSourceMap.sources[0], basePath).toString();
return [variantName, sourceUrl];
});
const externalVariantResults = await Promise.all(externalVariantPromises);
externalVariantResults.forEach(result => {
if (result) {
resolvedVariantMap.set(result[0], result[1]);
}
});
return {
resolvedVariantMap
};
}