UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

591 lines (557 loc) 22.2 kB
import { loadIsomorphicCodeVariant } from "./loadIsomorphicCodeVariant.mjs"; import { getFileNameFromUrl, getLanguageFromExtension } from "../loaderUtils/index.mjs"; import { performanceMeasure } from "../loadPrecomputedCodeHighlighter/performanceLogger.mjs"; // Helper function to get the source for a specific filename from a variant async function getFileSource(variant, requestedFilename, loadSource) { const filename = requestedFilename || variant.fileName; if (!filename) { // If no filename is available, return the main variant source if (variant.source !== undefined) { return { source: variant.source, filename: undefined }; } throw new Error('No filename available and no source in variant'); } // If requesting the main file and we have its source if (filename === variant.fileName && variant.source !== undefined) { return { source: variant.source, filename }; } // If requesting the main file but only have its URL, load it if (filename === variant.fileName && variant.url && loadSource) { const loadResult = await loadSource(variant.url); return { source: loadResult.source, filename, comments: loadResult.comments, externals: loadResult.externals }; } // If requesting an extra file and we have its source if (filename !== variant.fileName && variant.extraFiles) { const extraFile = variant.extraFiles[filename]; if (extraFile && typeof extraFile !== 'string' && extraFile.source !== undefined) { return { source: extraFile.source, filename }; } // If we have the URL but not the source, we need to load it if (typeof extraFile === 'string' && loadSource) { const loadResult = await loadSource(extraFile); return { source: loadResult.source, filename, comments: loadResult.comments, externals: loadResult.externals }; } if (extraFile && typeof extraFile !== 'string' && !extraFile.source && loadSource) { // This case shouldn't normally happen, but handle it anyway throw new Error(`Extra file ${filename} has no source or URL to load from`); } } throw new Error(`File ${filename} not found in variant or cannot be loaded`); } /** * Persist `comments`/`externals` produced by `loadSource` onto the cached * variant so downstream consumers (enhancers via `useFileNavigation`, export * via `useDemo`) can read them without re-fetching the file. Mirrors how * `loadIsomorphicCodeVariant` shapes these fields on the returned variant. */ function enrichVariantWithLoadedSource(base, source, comments, externals) { const enriched = { ...base, source }; // `loadSource` returns 1-indexed comments (the stored `Code` convention), so apply them // as-is — no conversion at the source-loader boundary. if (comments) { enriched.comments = comments; } if (externals && Object.keys(externals).length > 0) { enriched.externals = Object.keys(externals); } return enriched; } /** * Loads minimal data needed for fallback rendering. * Returns code, initial filename, initial source, extra files, all file names, * and processed globals code. * * @param url - File URL for the variant * @param initialVariant - Name of the initial variant to load * @param loaded - Previously loaded Code object, if any * @param options - Optional loading configuration */ export async function loadCodeFallback(url, initialVariant, loaded, options = {}) { const { shouldHighlight, fallbackUsesExtraFiles, fallbackUsesAllVariants, sourceParser, loadSource, loadVariantMeta, loadCodeMeta, sourceEnhancers, initialFilename, variants, globalsCode, output, urlPrefix } = options; loaded = { ...loaded }; // When not highlighting (deferred), pass `disableParsing: true`: the source must // stay a plain string so the client knows it still needs syntax highlighting (a // HAST source reads as "already loaded"). `loadIsomorphicCodeVariant` still frames // the loading fallback itself in that mode — line gutters + enhancers → root // fallback — only the syntax colors are deferred. When highlighting, the real // `sourceParser` runs and the source becomes a highlighted HAST as before. const functionName = 'Load Fallback Code'; let currentMark = performanceMeasure(undefined, { mark: 'Start', measure: 'Start' }, [functionName, url], true); // Step 1: Ensure we have the initial variant loaded let initial = loaded[initialVariant]; if (!initial) { if (!loadCodeMeta) { throw new Error('"loadCodeMeta" function is required when initial variant is not provided'); } try { loaded = await loadCodeMeta(url); } catch (error) { throw new Error(`Failed to load code from URL: ${url}. Error: ${JSON.stringify(error)}`); } initial = loaded[initialVariant]; if (!initial) { throw new Error(`Initial variant "${initialVariant}" not found in loaded code.`); } currentMark = performanceMeasure(currentMark, { mark: 'Loaded Code Meta', measure: 'Code Meta Loading' }, [functionName, url]); } // Check if we can return early after loadCodeMeta if (typeof initial !== 'string' && initial.allFilesListed && !fallbackUsesExtraFiles && !fallbackUsesAllVariants) { // Collect all file names from the loaded code const allFileNames = new Set(); if (initial.fileName) { allFileNames.add(initial.fileName); } if (initial.extraFiles) { Object.keys(initial.extraFiles).forEach(fileName => allFileNames.add(fileName)); } // Get the source for the requested filename (or main file if not specified) let fileSource; let actualFilename; let loadedComments; let loadedExternals; try { const result = await getFileSource(initial, initialFilename, loadSource); fileSource = result.source; actualFilename = result.filename; loadedComments = result.comments; loadedExternals = result.externals; } catch (error) { throw new Error(`Failed to get source for file ${initialFilename || initial.fileName} in variant ${initialVariant}: ${error}`); } currentMark = performanceMeasure(currentMark, { mark: 'Loaded Main File', measure: 'Main File Loading' }, [functionName, url]); // If we need highlighting and have a string source, parse it if (shouldHighlight && typeof fileSource === 'string' && sourceParser && actualFilename) { try { const parseSource = await sourceParser; fileSource = parseSource(fileSource, actualFilename); } catch (error) { throw new Error(`Failed to parse source for highlighting (variant: ${initialVariant}, file: ${actualFilename}): ${JSON.stringify(error)}`); } currentMark = performanceMeasure(currentMark, { mark: 'Parsed Main File', measure: 'Main File Parsing' }, [functionName, url]); } else if (shouldHighlight && typeof fileSource === 'string' && !actualFilename) { // Create basic HAST node when we can't parse due to missing filename // This marks that the source has passed through the parsing pipeline fileSource = { type: 'root', children: [{ type: 'text', value: fileSource }] }; } // Update the loaded code with any changes we made. if (actualFilename && actualFilename === initial.fileName || !actualFilename && !initial.fileName) { initial = enrichVariantWithLoadedSource(initial, fileSource, loadedComments, loadedExternals); loaded = { ...loaded, [initialVariant]: initial }; } // Early return - we have all the info we need return { code: loaded, initialFilename: actualFilename, initialSource: fileSource, initialExtraFiles: initial.extraFiles || {}, allFileNames: Array.from(allFileNames) }; } // Step 2: Try to get variant metadata quickly first if (typeof initial === 'string') { try { let quickVariant; if (loadVariantMeta) { // Use provided loadVariantMeta function quickVariant = await loadVariantMeta(initialVariant, initial); currentMark = performanceMeasure(currentMark, { mark: 'Loaded Initial Variant Meta', measure: 'Initial Variant Meta Loading' }, [functionName, url]); } else { // Create a basic variant using fallback logic const derivedFileName = getFileNameFromUrl(initial).fileName; const extension = derivedFileName.slice(derivedFileName.lastIndexOf('.')); quickVariant = { url: initial, fileName: derivedFileName, language: getLanguageFromExtension(extension) }; } const beforeInitialVariantMark = currentMark; loaded = { ...loaded, [initialVariant]: quickVariant }; initial = quickVariant; // If we have all files listed and don't need extra file processing, we can optimize if (quickVariant.allFilesListed && !fallbackUsesExtraFiles && !fallbackUsesAllVariants) { // Collect all file names from the quick load const allFileNames = new Set(); if (quickVariant.fileName) { allFileNames.add(quickVariant.fileName); } if (quickVariant.extraFiles) { Object.keys(quickVariant.extraFiles).forEach(fileName => allFileNames.add(fileName)); } // Get the source for the requested filename (or main file if not specified) let fileSource; let actualFilename; let loadedComments; let loadedExternals; try { const result = await getFileSource(quickVariant, initialFilename, loadSource); fileSource = result.source; actualFilename = result.filename; loadedComments = result.comments; loadedExternals = result.externals; currentMark = performanceMeasure(currentMark, { mark: 'Loaded Initial File', measure: 'Initial File Loading' }, [functionName, initialFilename || 'unknown', url]); } catch (error) { throw new Error(`Failed to get source for file ${initialFilename || quickVariant.fileName} in variant ${initialVariant}: ${error}`); } // If we need highlighting and have a string source, parse it if (shouldHighlight && typeof fileSource === 'string' && sourceParser && actualFilename) { try { const parseSource = await sourceParser; fileSource = parseSource(fileSource, actualFilename); currentMark = performanceMeasure(currentMark, { mark: 'Parsed Initial File', measure: 'Initial File Parsing' }, [functionName, initialFilename || 'unknown', url]); } catch (error) { throw new Error(`Failed to parse source for highlighting (variant: ${initialVariant}, file: ${actualFilename}): ${JSON.stringify(error)}`); } } else if (shouldHighlight && typeof fileSource === 'string' && !actualFilename) { // Create basic HAST node when we can't parse due to missing filename // This marks that the source has passed through the parsing pipeline fileSource = { type: 'root', children: [{ type: 'text', value: fileSource }] }; } // Update the loaded code with any changes we made. if (actualFilename && actualFilename === quickVariant.fileName || !actualFilename && !quickVariant.fileName) { initial = enrichVariantWithLoadedSource(quickVariant, fileSource, loadedComments, loadedExternals); loaded = { ...loaded, [initialVariant]: initial }; } currentMark = performanceMeasure(beforeInitialVariantMark, { mark: 'Loaded Initial Files', measure: 'Initial Files Loading' }, [functionName, url], true); // Early return - we have all the info we need return { code: loaded, initialFilename: actualFilename, initialSource: fileSource, initialExtraFiles: quickVariant.extraFiles || {}, allFileNames: Array.from(allFileNames) }; } } catch (error) { throw new Error(`Failed to load initial variant code (variant: ${initialVariant}, url: ${initial}): ${JSON.stringify(error)}`); } } const beforeGlobalsMark = currentMark; // Step 2b: Fall back to full loadIsomorphicCodeVariant processing // Load globalsCode - convert string URLs to Code objects, keep Code objects as-is let globalsCodeObjects; if (globalsCode && globalsCode.length > 0) { const hasStringUrls = globalsCode.some(item => typeof item === 'string'); if (hasStringUrls && !loadCodeMeta) { throw new Error('loadCodeMeta function is required when globalsCode contains string URLs'); } // Load all string URLs in parallel, keep Code objects as-is const globalsPromises = globalsCode.map(async globalItem => { if (typeof globalItem === 'string') { // String URL - load Code object via loadCodeMeta try { const codeMeta = await loadCodeMeta(globalItem); currentMark = performanceMeasure(currentMark, { mark: 'Loaded Global Code Meta', measure: 'Global Code Meta Loading' }, [functionName, globalItem, url]); return codeMeta; } catch (error) { throw new Error(`Failed to load globalsCode from URL: ${globalItem}. Error: ${JSON.stringify(error)}`); } } else { // Code object - return as-is return globalItem; } }); globalsCodeObjects = await Promise.all(globalsPromises); currentMark = performanceMeasure(beforeGlobalsMark, { mark: 'Loaded Globals Meta', measure: 'Globals Meta Loading' }, [functionName, url], true); } // Convert globalsCodeObjects to VariantCode | string for this specific variant let resolvedGlobalsCode; if (globalsCodeObjects && globalsCodeObjects.length > 0) { resolvedGlobalsCode = []; for (const codeObj of globalsCodeObjects) { // Only use the variant that matches the current initialVariant const targetVariant = codeObj[initialVariant]; if (targetVariant) { resolvedGlobalsCode.push(targetVariant); } } } try { const { code: loadedVariant } = await loadIsomorphicCodeVariant(url, initialVariant, initial, { sourceParser, loadSource, loadVariantMeta, sourceTransformers: undefined, // sourceTransformers - skip transforms for fallback sourceEnhancers, disableTransforms: true, // Don't apply transforms for fallback // Deferred highlight: keep the source a plain string, but still frame the // loading fallback (plain-text gutters + enhancers → root fallback). disableParsing: !shouldHighlight, framePlainFallback: true, globalsCode: resolvedGlobalsCode, // Pass resolved globalsCode output, urlPrefix }); currentMark = performanceMeasure(currentMark, { mark: 'Loaded Initial Variant', measure: 'Initial Variant Loading' }, [functionName, url], true); // Update the loaded code with the processed variant loaded = { ...loaded, [initialVariant]: loadedVariant }; initial = loadedVariant; } catch (error) { throw new Error(`Failed to load initial variant using loadIsomorphicCodeVariant (variant: ${initialVariant}, url: ${url}): ${JSON.stringify(error)}`); } // Step 3: Collect all file names const allFileNames = new Set(); if (initial.fileName) { allFileNames.add(initial.fileName); } // Add extra files from the initial variant if (initial.extraFiles) { Object.keys(initial.extraFiles).forEach(fileName => allFileNames.add(fileName)); } // Step 4: Handle fallbackUsesAllVariants - load all variants to get all possible files if (fallbackUsesAllVariants) { const beforeAllVariantMark = currentMark; // Determine all variants to process - use provided variants or infer from loaded code const allVariants = variants || Object.keys(loaded || {}); if (allVariants.length === 0) { console.warn('No variants found for fallbackUsesAllVariants processing'); } else { // Process all required variants, not just the ones already loaded const variantPromises = allVariants.map(async variantName => { if (variantName === initialVariant) { // Skip initial variant as it's already processed return { variantName, loadedVariant: null, fileNames: [] }; } let variant = loaded?.[variantName]; // If variant is not loaded yet, load it first using loadCodeMeta if (!variant && loadCodeMeta) { try { const allCode = await loadCodeMeta(url); variant = allCode[variantName]; // Update loaded with all variants from loadCodeMeta loaded = { ...loaded, ...allCode }; currentMark = performanceMeasure(currentMark, { mark: 'Loaded Initial Code Meta', measure: 'Initial Code Meta Loading' }, [functionName, url]); } catch (error) { console.warn(`Failed to load code meta for variant ${variantName}: ${error}`); return { variantName, loadedVariant: null, fileNames: [] }; } } if (!variant) { console.warn(`Variant ${variantName} not found after loading code meta`); return { variantName, loadedVariant: null, fileNames: [] }; } try { const { code: loadedVariant } = await loadIsomorphicCodeVariant(url, variantName, variant, { sourceParser, loadSource, loadVariantMeta, sourceTransformers: undefined, // sourceTransformers sourceEnhancers, disableTransforms: true, disableParsing: !shouldHighlight, framePlainFallback: true, output, urlPrefix, globalsCode: globalsCodeObjects && globalsCodeObjects.length > 0 ? (() => { // Convert globalsCodeObjects to VariantCode | string for this specific variant const variantGlobalsCode = []; for (const codeObj of globalsCodeObjects) { // Only use the variant that matches the current variantName const targetVariant = codeObj[variantName]; if (targetVariant) { variantGlobalsCode.push(targetVariant); } } return variantGlobalsCode; })() : undefined }); // Collect file names from this variant const fileNames = loadedVariant.fileName ? [loadedVariant.fileName] : []; if (loadedVariant.extraFiles) { fileNames.push(...Object.keys(loadedVariant.extraFiles)); } currentMark = performanceMeasure(currentMark, { mark: 'Loaded Initial Variant', measure: 'Initial Variant Loading' }, [functionName, variantName, url], true); return { variantName, loadedVariant, fileNames }; } catch (error) { // Log but don't fail - we want to get as many file names as possible console.warn(`Failed to load variant ${variantName} for file listing: ${error}`); return { variantName, loadedVariant: null, fileNames: [] }; } }); const variantResults = await Promise.all(variantPromises); // Update loaded code and collect file names variantResults.forEach(({ variantName, loadedVariant, fileNames }) => { if (loadedVariant) { loaded = { ...loaded, [variantName]: loadedVariant }; } fileNames.forEach(fileName => allFileNames.add(fileName)); }); } currentMark = performanceMeasure(beforeAllVariantMark, { mark: 'Loaded Initial Variants', measure: 'Initial Variants Loading' }, [functionName, url], true); } // Ensure we have the latest initial variant data const finalInitial = loaded[initialVariant]; if (!finalInitial || typeof finalInitial === 'string') { throw new Error(`Failed to process initial variant: ${initialVariant}`); } // Get the source for the requested filename (or main file if not specified) for the final return let finalFileSource; let finalFilename; try { const result = await getFileSource(finalInitial, initialFilename, loadSource); finalFileSource = result.source; finalFilename = result.filename; } catch (error) { // If we can't get the specific file, fall back to main file if (!finalInitial.fileName && !finalInitial.source) { throw new Error(`Cannot determine filename for initial variant "${initialVariant}". ` + `No fileName available in variant definition, no initialFilename provided, and no source available.`); } // Fall back to the main file with proper validation finalFileSource = finalInitial.source || ''; finalFilename = finalInitial.fileName; } currentMark = performanceMeasure(currentMark, { mark: 'Loaded Initial File', measure: 'Initial File Loading' }, [functionName, url]); return { code: loaded, initialFilename: finalFilename, initialSource: finalFileSource, initialExtraFiles: finalInitial.extraFiles || {}, allFileNames: Array.from(allFileNames), processedGlobalsCode: globalsCodeObjects }; }