UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

292 lines (282 loc) 14.7 kB
import * as React from 'react'; import { buildStringFallback } from "./buildStringFallback.mjs"; import { resolveFallbackCritical } from "./resolveFallbackCritical.mjs"; import { codeToFallbackProps, stripFallbackHastsFromCode } from "./codeToFallbackProps.mjs"; import { collapseRenderedFallbacks, compressResidualFallbacks, extractResidualFallbacks, mergeResidualFallbacks, residualDictionaryText } from "./fallbackCompression.mjs"; import { replaceUrlPrefix } from "../pipeline/loaderUtils/applyUrlPrefix.mjs"; import { getVariantFileLineCounts } from "../useCode/sourceLineCounts.mjs"; import { jsx as _jsx } from "react/jsx-runtime"; /** * Prepare the loading fallback and the wire `Code` from the initial source. Hoists * the rendered subset onto `ContentLoading` props, strips those fallback HASTs off * `Code`, and consolidates the rest into a compressed `residualFallbacks` blob the * client decodes against the rendered text. The render *decision* (client vs server * load, stream vs await) is the chunk's job; this is just the shared preparation * used by the content path and the server loaders. */ export function prepareInitialSource(props) { const ContentLoading = props.ContentLoading; const { slug, name, initialVariant, code: initialCode, initialFilename, fallbackUsesExtraFiles, fallbackUsesAllVariants, fallbackCollapsed } = props; const contentPropsFlags = props.contentProps; const collapseToEmpty = props.collapseToEmpty !== undefined ? props.collapseToEmpty : contentPropsFlags?.collapseToEmpty; const initialExpanded = props.initialExpanded !== undefined ? props.initialExpanded : contentPropsFlags?.initialExpanded; const collapseToEmptyEnabled = collapseToEmpty === true; const initialExpandedEnabled = initialExpanded === true; // Fold each variant's staging `fallbackCritical` into its plain `fallback` up front // (under `highlightAt: 'init'`, not `collapseToEmpty`), then strip it. The hoisted // loading fallback is therefore already highlighted-visible — so the first paint is // highlighted with no decompression — while the rest of this function (strip, hoist, // window, compress) operates on a single `fallback` field with no awareness of the // staging companion. // Normalize `'stream'` → `'init'` before resolving, mirroring `createClientProps`: stream // mode wants the loading fallback highlighted on first paint too (the client highlightAfter // type even collapses 'stream' into 'init'). const highlightAfter = props.highlightAfter === 'stream' ? 'init' : props.highlightAfter; const code = resolveFallbackCritical(initialCode, highlightAfter, collapseToEmptyEnabled) ?? initialCode; // When the block starts expanded, the loading UI needs the full content, so // the `fallbackCollapsed` window optimization (paint only the collapsed slice, // defer the rest) doesn't apply — treat it as off everywhere below. const effectiveFallbackCollapsed = fallbackCollapsed && !initialExpandedEnabled; // Strip fallbackHast entries from Code — they move to ContentLoading props // as source/extraSource instead of being serialized on Code. const { strippedCode, allFallbackHasts } = stripFallbackHastsFromCode(code, initialVariant, fallbackUsesExtraFiles, fallbackUsesAllVariants); // Compute the line counts (and, for inline strings, the windowed frames) for EVERY // file/variant passed to the fallback — the main file, extra files // (`fallbackUsesExtraFiles`), and extra variants (`fallbackUsesAllVariants`) — so // each carries its own `{ totalLines, focusedLines, collapsible }` downstream. Counts come from: // (1) the loader-stored counts on the code, else (2) reading the *original* source // here (where `hastCompressed` dictionaries are still present — they're stripped // before `codeToFallbackProps` runs), else (3) windowing an inline plain string. // Windowing also runs `sourceEnhancers` over a cheap line-guttered HAST (no syntax // highlighting) and hoists the truncated frames into `allFallbackHasts`, matching // the live render instead of `sourceToFallback`'s naive single focus frame. The raw // string stays on `codeForClient`, so the client still highlights it after hydration. const allLineCounts = {}; const { sourceEnhancers } = props; const variantsInScope = fallbackUsesAllVariants ? Object.keys(code ?? {}) : [initialVariant]; for (const variantName of variantsInScope) { const variant = code?.[variantName]; if (!variant || typeof variant === 'string') { continue; } const files = []; if (variant.fileName) { files.push({ fileName: variant.fileName, source: variant.source, comments: variant.comments }); } if ((fallbackUsesExtraFiles || fallbackUsesAllVariants) && variant.extraFiles) { for (const [fileName, fileData] of Object.entries(variant.extraFiles)) { if (typeof fileData === 'object') { files.push({ fileName, source: fileData.source, comments: fileData.comments }); } } } for (const file of files) { const storedFile = variant.fileName === file.fileName ? variant : variant.extraFiles?.[file.fileName]; let counts = storedFile && typeof storedFile !== 'string' && storedFile.totalLines !== undefined ? { totalLines: storedFile.totalLines, focusedLines: storedFile.focusedLines ?? storedFile.totalLines, collapsible: storedFile.collapsible === true } : undefined; if (!counts) { // Read off the original source; a missing dictionary throws — non-fatal here. // `totalLines === 0` ⇒ a hast with no `root.data` counts (not a real count). try { const read = getVariantFileLineCounts(variant, file.fileName); counts = read && read.totalLines > 0 ? read : undefined; } catch { counts = undefined; } } // Window an inline plain-string source (needs enhancers, and not already framed // by the loader) and override the counts with the resulting window. if (sourceEnhancers && sourceEnhancers.length > 0 && typeof file.source === 'string' && !allFallbackHasts[variantName]?.[file.fileName]) { const windowed = buildStringFallback(file.source, // `Code` comments are always 1-indexed and `buildStringFallback` passes them // straight to the enhancer (matched against the 1-indexed `dataLn` gutter). file.comments, file.fileName, sourceEnhancers); if (windowed) { (allFallbackHasts[variantName] ??= {})[file.fileName] = windowed.fallback; counts = { totalLines: windowed.totalLines, focusedLines: windowed.focusedLines, collapsible: windowed.collapsible }; } } if (counts) { (allLineCounts[variantName] ??= {})[file.fileName] = { totalLines: counts.totalLines, // Render-time collapse-to-empty empties every file's window (oversized // `'hide'` already records `focusedLines === 0`). `useCodeFallback` applies // the same rule when it demotes the source, so they stay consistent. focusedLines: collapseToEmptyEnabled ? 0 : counts.focusedLines, collapsible: collapseToEmptyEnabled ? true : counts.collapsible }; // Carry the RAW counts onto the wire `code` too, so the content component's base // render (before `hast` decodes) reads the same `collapsible`/window metadata the // loading fallback got. A windowed inline string — or a `hastCompressed` source // whose dictionary was stripped for residual compression — otherwise has no stored // counts on `codeForClient`, so `getVariantFileLineCounts` falls back to the raw // (non-collapsible) string count and `data-collapsible` flashes off until the // source highlights. `<Pre>` re-applies collapse-to-empty from the stored counts. const wireVariant = strippedCode[variantName]; if (wireVariant && typeof wireVariant === 'object') { const stored = { totalLines: counts.totalLines, focusedLines: counts.focusedLines, collapsible: counts.collapsible }; if (wireVariant.fileName === file.fileName) { strippedCode[variantName] = { ...wireVariant, ...stored }; } else { const extra = wireVariant.extraFiles?.[file.fileName]; if (extra && typeof extra === 'object') { strippedCode[variantName] = { ...wireVariant, extraFiles: { ...wireVariant.extraFiles, [file.fileName]: { ...extra, ...stored } } }; } } } } } } // Rewrite the top-level URL before it reaches the loading fallback so the // browser never sees `file://` URLs. See `createClientProps` for the same // rewrite on the regular client path. const url = props.urlPrefix && props.url ? replaceUrlPrefix(props.url, props.urlPrefix) : props.url; // `fallbackCollapsed` paints only each file's collapsed window in the loading // UI; the full fallbacks defer into the blob. Otherwise the loading UI gets // the full rendered subset, as usual. // // A file produced with `oversizedFocus: 'hide'` records `focusedLines === 0` // (collapse-to-nothing): its collapsed window is empty, so we tell // `collapseRenderedFallbacks` to emit no frames for it rather than fall back // to the first frame — matching the hydrated render. The render-time // `collapseToEmpty` flag empties the window for every file the same way. const collapsesToEmpty = (variantName, fileName) => { if (collapseToEmptyEnabled) { return true; } // A windowed inline-string file has authoritative counts here; precomputed // sources read theirs off `root.data` via `getVariantFileLineCounts`. const windowed = allLineCounts[variantName]?.[fileName]; if (windowed) { return windowed.focusedLines === 0; } const variant = code[variantName]; if (!variant || typeof variant === 'string') { return false; } // Mirror the count path's guard above: `focusedLines === 0` only means // collapse-to-empty when there's a real count. A decoded HAST with no `root.data` // reads as `{ totalLines: 0, focusedLines: 0 }` — that's "no count", not an empty // window — so don't mistake it for an intentional collapse-to-nothing. const counts = getVariantFileLineCounts(variant, fileName); return counts ? counts.totalLines > 0 && counts.focusedLines === 0 : false; }; const contentLoadingHasts = effectiveFallbackCollapsed ? collapseRenderedFallbacks(allFallbackHasts, collapsesToEmpty) : allFallbackHasts; // `allLineCounts` gives `codeToFallbackProps` a window for EVERY file/variant it // emits — main, extra files, and extra variants — so each carries its own // `totalLines`/`focusedLines`/`collapsible` (collapse-to-empty is applied per file in // `useCodeFallback`). const fallbackProps = codeToFallbackProps(initialVariant, strippedCode, initialFilename, fallbackUsesExtraFiles, fallbackUsesAllVariants, contentLoadingHasts, allLineCounts); // Consolidate every fallback the loading UI won't render into a single DEFLATE // blob, primed with the rendered (collapsed, when `fallbackCollapsed`) text so // it dedupes against what's already on the client. That's everything still on // `strippedCode` after hoisting — plus, when `fallbackCollapsed`, each // rendered file's *full* fallback (the loading UI only painted its collapsed // window, so the rest must travel here). The blob crosses once; `wireCode` // carries no inline fallbacks, and the client decompresses + scatters them back // onto the code so its consumers (render and the swap line-count classifier) // read the dictionary off `code` regardless of which variant is active. When // there's nothing worth compressing, keep the plain inline fallbacks unchanged. // Compressing the residual only shrinks the server→client wire, so skip it entirely // on the client (`compressResidual === false`): keep the fallbacks INLINE on // `codeForClient` so `CodeHighlighterClient` reads them directly — no // compress→decompress round-trip, and nothing to recompute on every re-render. This // is the same shape the `strippedCode` branch below already produces for a payload // too small to be worth compressing. const compressResidual = props.compressResidual ?? true; let residualFallbacks; let codeForClient = strippedCode; if (compressResidual) { const { wireCode, residual } = extractResidualFallbacks(strippedCode); const fullResidual = effectiveFallbackCollapsed ? mergeResidualFallbacks(residual, allFallbackHasts) : residual; residualFallbacks = compressResidualFallbacks(fullResidual, residualDictionaryText(contentLoadingHasts)); codeForClient = residualFallbacks ? wireCode : strippedCode; } // Get the component for the selected variant const component = props.components?.[initialVariant]; // Only include components (plural) if we're also including extraVariants const components = fallbackProps.extraVariants ? props.components : undefined; const contentProps = { ...props.contentProps, ...fallbackProps, name, slug, url, initialFilename, initialVariant, component, components, // Signals the ContentLoading that `source` is only the collapsed window, // so it can disable any expand control until the full content swaps in. // Off when the block starts expanded (the loading UI gets the full content). ...(effectiveFallbackCollapsed ? { fallbackCollapsed: true } : undefined), // Render-time collapse-to-empty: the loading placeholder paints an empty window // too (via `useCodeFallback`), matching the hydrated render. ...(collapseToEmpty !== undefined ? { collapseToEmpty } : undefined), // Render-time default-expanded: the loading placeholder can render expanded // so it doesn't flash collapsed before hydration. ...(initialExpanded !== undefined ? { initialExpanded } : undefined) }; const fallback = /*#__PURE__*/_jsx(ContentLoading, { ...contentProps }); return { fallback, residualFallbacks, codeForClient }; }