UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

402 lines (373 loc) 17.8 kB
import { rewriteImports } from "./rewriteImports.mjs"; import { isJavaScriptModule } from "./resolveModulePath.mjs"; import { getFileNameFromUrl } from "./getFileNameFromUrl.mjs"; import { fileUrlToPortablePath, portablePathToFileUrl } from "./fileUrlToPortablePath.mjs"; /** * Processes flat mode with intelligent conflict resolution */ function processFlatMode(importResult, resolvedPathsMap) { const extraFiles = {}; const fileMapping = []; // First pass: collect all files and their path segments Object.entries(importResult).forEach(([relativePath, importInfo]) => { const resolvedUrl = resolvedPathsMap.get(importInfo.url); if (resolvedUrl) { // Convert file URL to portable path for path manipulation const resolvedPath = resolvedUrl.startsWith('file://') ? fileUrlToPortablePath(resolvedUrl) : resolvedUrl; const fileExtension = getFileNameFromUrl(resolvedUrl).extension; const pathSegments = resolvedPath.split('/').filter(Boolean); fileMapping.push({ resolvedPath, extension: fileExtension, segments: pathSegments, originalImportPath: relativePath }); } }); // Second pass: determine candidate names and group by conflicts const candidateNames = new Map(); const nameGroups = new Map(); for (const file of fileMapping) { const fileName = file.segments[file.segments.length - 1]; const isIndexFile = fileName.startsWith('index.'); const isUnderscoreIndexFile = fileName.startsWith('_index.'); let candidateName; if (isUnderscoreIndexFile) { // Files starting with "_index." should be treated as direct index imports // e.g., "../../dir/_index.module.css" -> "index.module.css" candidateName = `index${file.extension}`; } else if (isIndexFile) { // Check if the original import was a direct index file (e.g., "./index.ext") const originalImportParts = file.originalImportPath.split('/'); const isDirectIndexImport = originalImportParts.length === 2 && originalImportParts[0] === '.' && originalImportParts[1].startsWith('index.'); if (isDirectIndexImport) { // For direct index imports like "./index.ext", keep the original name candidateName = `index${file.extension}`; } else { // For nested index files like "./test/index.ext", use parent directory + extension const parentDir = file.segments[file.segments.length - 2]; candidateName = `${parentDir}${file.extension}`; } } else { candidateName = fileName; } candidateNames.set(file.resolvedPath, candidateName); if (!nameGroups.has(candidateName)) { nameGroups.set(candidateName, []); } nameGroups.get(candidateName).push(file.resolvedPath); } // Third pass: resolve conflicts for all files in conflicting groups const finalNames = new Map(); nameGroups.forEach((paths, candidateName) => { if (paths.length === 1) { // No conflict, use the candidate name finalNames.set(paths[0], candidateName); } else { // Conflict detected, find optimal minimal distinguishing paths for all files const conflictingFiles = paths.map(resolvedPath => fileMapping.find(f => f.resolvedPath === resolvedPath)); // Check if we can resolve conflicts by treating some files differently // This specifically handles cases like: // - /path/to/a/Component.js and /path/to/Component.js (parent-child relationship) // Find files that are "shorter" (parent level) compared to others const minLength = Math.min(...conflictingFiles.map(f => f.segments.length)); const maxLengthForSmart = Math.max(...conflictingFiles.map(f => f.segments.length)); if (maxLengthForSmart > minLength) { // We have files at different depths, check if it's a parent-child scenario const shorterFiles = conflictingFiles.filter(file => file.segments.length === minLength); const longerFiles = conflictingFiles.filter(file => file.segments.length > minLength); if (shorterFiles.length === 1 && longerFiles.length >= 1) { // Check if the shorter file is truly a "parent" of the longer files const shorterFile = shorterFiles[0]; const shorterPath = shorterFile.segments.slice(0, -1).join('/'); // Remove filename // Check if all longer files share the same prefix as the shorter file const allLongerFilesAreChildren = longerFiles.every(longerFile => { const longerPath = longerFile.segments.slice(0, shorterFile.segments.length - 1).join('/'); return longerPath === shorterPath; }); if (allLongerFilesAreChildren) { // This is a true parent-child scenario, apply smart resolution // For longer files, find distinguishing index let distinguishingIndex = -1; const maxLongerLength = Math.max(...longerFiles.map(f => f.segments.length)); for (let i = 0; i < maxLongerLength; i += 1) { const segmentsAtIndex = new Set(longerFiles.map(f => f.segments[i]).filter(Boolean)); if (segmentsAtIndex.size === longerFiles.length) { distinguishingIndex = i; break; } } if (distinguishingIndex !== -1) { // Generate names for longer files using distinguishing segment for (const file of longerFiles) { const fileName = file.segments[file.segments.length - 1]; const isIndexFile = fileName.startsWith('index.'); const isUnderscoreIndexFile = fileName.startsWith('_index.'); const distinguishingSegment = file.segments[distinguishingIndex]; let finalName; if (isUnderscoreIndexFile) { // Files starting with "_index." should always use "index" as the base name finalName = `${distinguishingSegment}/index${file.extension}`; } else if (isIndexFile) { // Check if this was a direct index import const originalImportParts = file.originalImportPath.split('/'); const isDirectIndexImport = originalImportParts.length === 2 && originalImportParts[0] === '.' && originalImportParts[1].startsWith('index.'); if (isDirectIndexImport) { finalName = `${distinguishingSegment}/index${file.extension}`; } else { const parentDir = file.segments[file.segments.length - 2]; finalName = `${distinguishingSegment}/${parentDir}${file.extension}`; } } else { finalName = `${distinguishingSegment}/${fileName}`; } finalNames.set(file.resolvedPath, finalName); } // For shorter files, use the candidate name as-is (no conflicts after disambiguation) for (const shortFile of shorterFiles) { finalNames.set(shortFile.resolvedPath, candidateName); } return; // Successfully resolved } } } } // Fallback to original algorithm if smart resolution fails let distinguishingIndex = -1; const maxLength = Math.max(...conflictingFiles.map(f => f.segments.length)); for (let i = 0; i < maxLength; i += 1) { const segmentsAtIndex = new Set(conflictingFiles.map(f => f.segments[i]).filter(Boolean)); if (segmentsAtIndex.size === conflictingFiles.length) { distinguishingIndex = i; break; } } if (distinguishingIndex === -1) { throw new Error(`Cannot find distinguishing segment for files: ${paths.join(', ')}`); } // Generate names using the distinguishing segment for (const file of conflictingFiles) { const fileName = file.segments[file.segments.length - 1]; const isIndexFile = fileName.startsWith('index.'); const isUnderscoreIndexFile = fileName.startsWith('_index.'); const distinguishingSegment = file.segments[distinguishingIndex]; let finalName; if (isUnderscoreIndexFile) { // Files starting with "_index." should always use "index" as the base name finalName = `${distinguishingSegment}/index${file.extension}`; } else if (isIndexFile) { // Check if this was a direct index import const originalImportParts = file.originalImportPath.split('/'); const isDirectIndexImport = originalImportParts.length === 2 && originalImportParts[0] === '.' && originalImportParts[1].startsWith('index.'); if (isDirectIndexImport) { finalName = `${distinguishingSegment}/index${file.extension}`; } else { const parentDir = file.segments[file.segments.length - 2]; finalName = `${distinguishingSegment}/${parentDir}${file.extension}`; } } else { finalName = `${distinguishingSegment}/${fileName}`; } finalNames.set(file.resolvedPath, finalName); } } }); // Fourth pass: build the extraFiles mapping finalNames.forEach((finalName, resolvedPath) => { extraFiles[`./${finalName}`] = portablePathToFileUrl(resolvedPath); }); return { processedSource: '', // Will be set by caller after rewriting extraFiles }; } /** * Processes imports with basic conflict resolution based on filenames. * Used for CSS, JSON, and other simple file types that don't require complex module resolution. */ function processBasicImports(source, importResult, storeAt) { const extraFiles = {}; if (storeAt === 'flat') { const finalNames = new Map(); const usedNames = new Set(); // Process each import to determine final names with simple conflict resolution Object.entries(importResult).forEach(([_relativePath, importInfo]) => { const resolvedPath = importInfo.url; // For CSS, this is already resolved by parseImports const fileUrl = portablePathToFileUrl(resolvedPath); const { fileName, extension } = getFileNameFromUrl(fileUrl); let finalName = fileName; let counter = 1; // Handle naming conflicts by appending numbers while (usedNames.has(finalName)) { const baseName = fileName.replace(extension, ''); finalName = `${baseName}-${counter}${extension}`; counter += 1; } usedNames.add(finalName); finalNames.set(resolvedPath, finalName); }); // Create the import path mapping for rewriting const importPathMapping = new Map(); Object.entries(importResult).forEach(([relativePath, importInfo]) => { const resolvedPath = importInfo.url; const finalName = finalNames.get(resolvedPath); if (finalName) { importPathMapping.set(relativePath, finalName); } }); // Create extraFiles entries finalNames.forEach((finalName, resolvedPath) => { extraFiles[`./${finalName}`] = portablePathToFileUrl(resolvedPath); }); // Apply import path replacements using position-based rewriting // Only rewrite if positions are available const hasPositions = Object.values(importResult).some(imp => imp.positions && imp.positions.length > 0); const processedSource = hasPositions ? rewriteImports(source, importPathMapping, importResult) : source; return { processedSource, extraFiles }; } if (storeAt === 'import') { // Create a mapping for import mode: remove ./ prefix from relative paths const importModeMapping = new Map(); Object.keys(importResult).forEach(relativePath => { if (relativePath.startsWith('./')) { importModeMapping.set(relativePath, relativePath.slice(2)); } // Keep other paths (../, absolute URLs) unchanged }); // Process each import for extraFiles Object.entries(importResult).forEach(([relativePath, importInfo]) => { const resolvedPath = importInfo.url; extraFiles[relativePath] = portablePathToFileUrl(resolvedPath); // Always use original path for extraFiles }); // Apply import path replacements using position-based rewriting // Only rewrite if positions are available const hasPositions = Object.values(importResult).some(imp => imp.positions && imp.positions.length > 0); const processedSource = hasPositions ? rewriteImports(source, importModeMapping, importResult) : source; return { processedSource, extraFiles }; } // Canonical mode - no rewriting needed Object.entries(importResult).forEach(([relativePath, importInfo]) => { const resolvedPath = importInfo.url; extraFiles[relativePath] = portablePathToFileUrl(resolvedPath); // Use original import path }); return { processedSource: source, // No rewriting needed for canonical mode extraFiles }; } /** * Processes JavaScript imports with complex conflict resolution and module handling */ function processJsImports(source, importResult, storeAt, resolvedPathsMap) { const extraFiles = {}; if (storeAt === 'flat') { const result = processFlatMode(importResult, resolvedPathsMap); // Build a reverse mapping from resolved paths to extraFiles keys const resolvedToExtraFile = new Map(); Object.entries(result.extraFiles).forEach(([extraFileKey, fileUrl]) => { // Convert file URL to portable path for lookup const resolvedPath = fileUrl.startsWith('file://') ? fileUrlToPortablePath(fileUrl) : fileUrl; resolvedToExtraFile.set(resolvedPath, extraFileKey); }); // For each import, find its resolved path and map to the corresponding extraFile key const importPathMapping = new Map(); Object.entries(importResult).forEach(([relativePath, importInfo]) => { const resolvedUrl = resolvedPathsMap.get(importInfo.url); if (resolvedUrl) { // Convert file URL to portable path for lookup const resolvedPath = resolvedUrl.startsWith('file://') ? fileUrlToPortablePath(resolvedUrl) : resolvedUrl; const extraFileKey = resolvedToExtraFile.get(resolvedPath); if (extraFileKey) { // For JavaScript modules, remove the extension; for other files (CSS, JSON, etc.), keep it const isJavascriptModule = isJavaScriptModule(relativePath); let newPath = extraFileKey; if (isJavascriptModule) { // Handle TypeScript declaration files (.d.ts) properly if (newPath.endsWith('.d.ts')) { newPath = newPath.replace(/\.d\.ts$/, ''); } else { newPath = newPath.replace(/\.[^/.]+$/, ''); } } // For non-JS modules (CSS, JSON, etc.), keep the full path with extension importPathMapping.set(relativePath, newPath); } } }); // Apply import path replacements using position-based rewriting const processedSource = rewriteImports(source, importPathMapping, importResult); return { processedSource, extraFiles: result.extraFiles }; } // Non-flat modes (canonical and import) Object.entries(importResult).forEach(([relativePath, importInfo]) => { const resolvedUrl = resolvedPathsMap.get(importInfo.url); if (!resolvedUrl) { return; } // Convert file URL to portable path for path manipulation const resolvedPath = resolvedUrl.startsWith('file://') ? fileUrlToPortablePath(resolvedUrl) : resolvedUrl; const fileExtension = getFileNameFromUrl(resolvedUrl).extension; const isJavascriptModule = isJavaScriptModule(relativePath); let keyPath; if (!isJavascriptModule) { // For static assets (CSS, JSON, etc.), use the original import path as-is keyPath = relativePath; } else { // For JS/TS modules, apply the existing logic switch (storeAt) { case 'canonical': // Show the full resolved path including index files when they exist keyPath = `${relativePath}${resolvedPath.endsWith(`/index${fileExtension}`) ? `/index${fileExtension}` : fileExtension}`; break; case 'import': // Use the original import path with the actual file extension keyPath = `${relativePath}${fileExtension}`; break; default: keyPath = `${relativePath}${fileExtension}`; } } extraFiles[keyPath] = portablePathToFileUrl(resolvedPath); }); return { processedSource: source, // No rewriting needed for non-flat modes extraFiles }; } /** * Processes imports based on the specified storage mode, automatically handling * source rewriting when needed (e.g., for 'flat' mode). Works for both JavaScript and simple file types. * * @param source - The original source code * @param importResult - The result from parseImports * @param storeAt - How to process the imports * @param isJsFile - Whether this is a JavaScript file (false = basic processing for CSS/JSON/etc.) * @param resolvedPathsMap - Map from import paths to resolved file paths (only needed for JavaScript files) * @returns Object with processed source and extraFiles mapping */ export function processRelativeImports(source, importResult, storeAt, isJsFile = false, resolvedPathsMap) { if (!isJsFile) { // Use basic processing mode for CSS, JSON, and other simple file types return processBasicImports(source, importResult, storeAt); } // Use complex JavaScript processing mode if (!resolvedPathsMap) { throw new Error('resolvedPathsMap is required for JavaScript files'); } return processJsImports(source, importResult, storeAt, resolvedPathsMap); }