@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
1,147 lines (1,076 loc) • 59.2 kB
JavaScript
'use client';
import * as React from 'react';
import { useCodeContext } from "../CodeProvider/CodeContext.mjs";
import { CodeHighlighterContext } from "./CodeHighlighterContext.mjs";
import { maybeCodeInitialData } from "../pipeline/loadIsomorphicCodeVariant/maybeCodeInitialData.mjs";
import { hasAllVariants } from "../pipeline/loadIsomorphicCodeVariant/hasAllCodeVariants.mjs";
import { CodeHighlighterFallbackContext } from "./CodeHighlighterFallbackContext.mjs";
import { useControlledCode } from "../CodeControllerContext/index.mjs";
import { codeToFallbackProps, deriveFallbacksFromCode, stripFallbackHastsFromCode } from "./codeToFallbackProps.mjs";
import { resolveFallbackCritical } from "./resolveFallbackCritical.mjs";
import { decompressResidualFallbacks, residualDictionaryText, scatterResidualFallbacks } from "./fallbackCompression.mjs";
import { mergeCodeMetadata } from "../pipeline/loadIsomorphicCodeVariant/mergeCodeMetadata.mjs";
import { getAvailableTransforms } from "../pipeline/loadIsomorphicCodeVariant/getAvailableTransforms.mjs";
import { useSpeculativeCodePreload } from "./useSpeculativeCodePreload.mjs";
import { useSpeculativeEditingPreload } from "./useSpeculativeEditingPreload.mjs";
import { useSpeculativeUseCodePreload } from "./useSpeculativeUseCodePreload.mjs";
import { useSpeculativeGrammarPreload } from "./useSpeculativeGrammarPreload.mjs";
import { useGrammarsReady } from "./useGrammarsReady.mjs";
import { detectGrammarScopes } from "../pipeline/parseSource/detectGrammarScopes.mjs";
import { useChunk } from "../CoordinatedLazy/useChunk.mjs";
import { useCoordinatedSwap } from "../CoordinatedLazy/useCoordinatedSwap.mjs";
import { CoordinatedFallbackContext } from "../CoordinatedLazy/CoordinatedFallbackContext.mjs";
import { CoordinatedContentContext } from "../CoordinatedLazy/CoordinatedContentContext.mjs";
import { requestIdle } from "../useCoordinated/scheduleTasks.mjs";
import * as Errors from "./errors.mjs";
import { jsx as _jsx } from "react/jsx-runtime";
const DEBUG = false; // Set to true for debugging purposes
// `useChunk` is the chunk loader/renderer, but here we use only its loading
// engine (load-when-enabled + abort + `refresh()` with stale-while-revalidate),
// so the content component is an unused placeholder.
function NoopChunkContent() {
return null;
}
function useInitialData({
variants,
variantName,
code,
setCode,
fileName,
url,
highlightAfter,
fallbackUsesExtraFiles,
fallbackUsesAllVariants,
isControlled,
globalsCode,
setProcessedGlobalsCode,
handleSetFallbackHasts
}) {
const {
sourceParser,
loadCodeMeta,
loadVariantMeta,
loadSource,
loadCodeFallbackLoader,
sourceEnhancers
} = useCodeContext();
const {
initialData,
reason
} = React.useMemo(() => maybeCodeInitialData(variants, variantName, code, fileName, highlightAfter === 'init', fallbackUsesExtraFiles, fallbackUsesAllVariants), [variants, variantName, code, fileName, highlightAfter, fallbackUsesExtraFiles, fallbackUsesAllVariants]);
const needsFallback = !initialData && !isControlled;
if (needsFallback) {
if (!url) {
// URL is required for loading fallback data
throw new Errors.ErrorCodeHighlighterClientMissingUrlForFallback();
}
// Validate against the loader accessor's presence (synchronously defined
// whenever a CodeProvider is mounted) - never against the resolved fn, so we
// don't throw merely because a lazy import is still in flight.
if (!loadCodeFallbackLoader) {
throw new Errors.ErrorCodeHighlighterClientMissingLoadFallbackCode(url);
}
}
// Signal to downstream loaders that a fallback fetch is pending. Used to gate
// `useAllVariants` so it can reuse the data populated by the fallback rather
// than racing it and re-fetching the same variant.
const fallbackPending = Boolean(needsFallback && url && loadCodeFallbackLoader);
// The fallback load runs through `useChunk` too (same loading engine as the
// full load) — the body is unchanged (it still calls `setCode` / hoists /
// `setProcessedGlobalsCode` directly; `code` stays owned by the component).
// `controlled: !needsFallback` is the gate.
// TODO: fallbackInitialRenderOnly option? this would mean we can't fetch fallback data on the client side
const fallbackSource = React.useMemo(() => ({
mode: 'data',
load: async (_options, signal) => {
if (!url || !loadCodeFallbackLoader) {
return code ?? {};
}
if (DEBUG) {
// eslint-disable-next-line no-console
console.log('Loading initial data for CodeHighlighterClient: ', reason);
}
// Lazily resolve the heavy fallback loader (instant under an eager
// CodeProvider, a deduped fetch under CodeProviderLazy) before loading.
const loadCodeFallback = await loadCodeFallbackLoader();
const loaded = await loadCodeFallback(url, variantName, code, {
shouldHighlight: highlightAfter === 'init',
fallbackUsesExtraFiles,
fallbackUsesAllVariants,
sourceParser,
loadSource,
loadVariantMeta,
loadCodeMeta,
sourceEnhancers,
initialFilename: fileName,
variants,
globalsCode // Let loadCodeFallback handle processing
}).catch(error => ({
error: error instanceof Error ? error : new Error(String(error))
}));
if ('error' in loaded) {
console.error(new Errors.ErrorCodeHighlighterClientLoadFallbackFailure(loaded.error));
return code ?? {};
}
// Fold each variant's highlighted-visible `fallbackCritical` over its plain
// `fallback` (under `highlightAt: 'init'`) and strip the staging field, so the
// hoisted loading fallback is already highlighted and nothing leaks to the
// content. `collapseToEmpty` isn't threaded into the client here, so the `false`
// form is assumed: under collapse-to-empty this may promote a few frames that are
// then CSS-hidden, but that is harmless — the promoted text is byte-identical
// (a valid dictionary) and the frames never paint.
const resolved = resolveFallbackCritical(loaded.code, highlightAfter, false) ?? loaded.code;
// Strip fallbacks from code and hoist them directly
const {
strippedCode,
allFallbackHasts
} = stripFallbackHastsFromCode(resolved, variantName, fallbackUsesExtraFiles, fallbackUsesAllVariants);
if (!signal.aborted) {
setCode(strippedCode);
for (const [variant, hasts] of Object.entries(allFallbackHasts)) {
handleSetFallbackHasts(variant, hasts);
}
// Store processed globalsCode from loadCodeFallback result
if (loaded.processedGlobalsCode) {
setProcessedGlobalsCode(loaded.processedGlobalsCode);
}
}
return strippedCode;
}
}), [reason, variantName, code, setCode, highlightAfter, url, sourceParser, loadSource, loadVariantMeta, loadCodeMeta, sourceEnhancers, fallbackUsesExtraFiles, fallbackUsesAllVariants, fileName, variants, globalsCode, setProcessedGlobalsCode, loadCodeFallbackLoader, handleSetFallbackHasts]);
const fallbackConfig = React.useMemo(() => ({
ChunkContent: NoopChunkContent,
source: fallbackSource
}), [fallbackSource]);
useChunk(fallbackConfig, {
controlled: !needsFallback
});
return {
fallbackPending
};
}
function useAllVariants({
readyForContent,
variants,
isControlled,
url,
code,
setCode,
processedGlobalsCode,
globalsCode,
setProcessedGlobalsCode,
fallbackPending
}) {
const {
loadCodeMeta,
loadVariantMeta,
loadSource,
loadIsomorphicCodeVariantLoader,
sourceEnhancers
} = useCodeContext();
const needsData = !readyForContent && !isControlled && !fallbackPending;
// validation
React.useMemo(() => {
if (needsData) {
if (!url) {
throw new Errors.ErrorCodeHighlighterClientMissingUrlForVariants();
}
if (!loadIsomorphicCodeVariantLoader) {
throw new Errors.ErrorCodeHighlighterClientMissingLoadVariant(url);
}
if (!code && !loadCodeMeta) {
throw new Errors.ErrorCodeHighlighterClientMissingLoadCodeMetaForNoCode(url);
}
if (globalsCode && globalsCode.length > 0 && globalsCode.some(item => typeof item === 'string') && !loadCodeMeta) {
throw new Errors.ErrorCodeHighlighterClientMissingLoadCodeMetaForGlobals();
}
if (!code && !loadSource) {
throw new Errors.ErrorCodeHighlighterClientMissingLoadSourceForNoCode();
}
if (code && Object.keys(code).some(variantName => {
const variant = code[variantName];
if (!variant || typeof variant === 'string' || !variant.source) {
return true;
}
const extraFiles = variant.extraFiles;
if (extraFiles && Object.keys(extraFiles).some(fileName => !extraFiles[fileName] || typeof extraFiles[fileName] === 'string' || !extraFiles[fileName].source)) {
return true;
}
return false;
}) && !loadSource) {
throw new Errors.ErrorCodeHighlighterClientMissingLoadSourceForUnloadedUrls();
}
}
}, [code, globalsCode, loadCodeMeta, loadIsomorphicCodeVariantLoader, loadSource, needsData, url]);
// The full-variant load runs through `useChunk` so it inherits the abstraction's
// load-when-enabled + abort + `refresh()` (stale-while-revalidate) engine. The
// loader body is unchanged — it still calls `setCode` / `setProcessedGlobalsCode`
// directly (the chunk's own `data`/`loading` are unused; `code` stays owned by
// this component). `controlled: !needsData` is the gate: when the data isn't
// needed the chunk treats itself as already loaded and never runs the loader.
const fullVariantSource = React.useMemo(() => ({
mode: 'data',
load: async (_options, signal) => {
if (!url || !loadIsomorphicCodeVariantLoader) {
return code ?? {};
}
try {
// Lazily resolve the heavy variant loader (instant under an eager
// CodeProvider, a deduped fetch under CodeProviderLazy) before loading.
const loadIsomorphicCodeVariant = await loadIsomorphicCodeVariantLoader();
let loadedCode = code;
if (!loadedCode) {
if (!loadCodeMeta) {
throw new Errors.ErrorCodeHighlighterClientMissingLoadCodeMeta();
}
loadedCode = await loadCodeMeta(url);
}
// Use the already-processed globalsCode from state, or process it if not available
let globalsCodeObjects = [];
if (processedGlobalsCode) {
// Use the already-processed globalsCode from state
globalsCodeObjects = processedGlobalsCode;
} else if (globalsCode && globalsCode.length > 0) {
// Process globalsCode: load any string URLs into Code objects
globalsCodeObjects = await Promise.all(globalsCode.map(async item => {
if (typeof item === 'string') {
// Load Code object from URL string
if (!loadCodeMeta) {
throw new Errors.ErrorCodeHighlighterClientMissingLoadCodeMeta();
}
return loadCodeMeta(item);
}
// Already a Code object
return item;
}));
// Store processed globalsCode in state for future use
if (!signal.aborted) {
setProcessedGlobalsCode(globalsCodeObjects);
}
}
// Load variant data without parsing or transforming
const result = await Promise.all(variants.map(name => {
// Resolve globalsCode for this specific variant
const globalsForVariant = globalsCodeObjects.map(codeObj => {
// Only include if this variant exists in the globalsCode
return codeObj[name];
}).filter(item => Boolean(item));
return loadIsomorphicCodeVariant(url, name, loadedCode[name], {
disableParsing: true,
disableTransforms: true,
loadSource,
loadVariantMeta,
sourceEnhancers,
globalsCode: globalsForVariant
}).then(variant => ({
name,
variant
})).catch(error => ({
error: error instanceof Error ? error : new Error(String(error))
}));
}));
const resultCode = {};
const errors = [];
for (const item of result) {
if ('error' in item) {
errors.push(item.error);
} else {
resultCode[item.name] = item.variant.code;
}
}
// Strip the staging `fallbackCritical` before it enters `code` state and
// reaches the content. The full load runs with `disableParsing`, so the source
// is a raw string and no `fallbackCritical` is produced here — the strip is
// purely defensive, hence the strip-only `'idle'` (promotion is `'init'`-gated).
const resolvedResultCode = resolveFallbackCritical(resultCode, 'idle', false) ?? resultCode;
if (errors.length > 0) {
console.error(new Errors.ErrorCodeHighlighterClientLoadVariantsFailure(url, errors));
} else if (!signal.aborted) {
setCode(resolvedResultCode);
}
return resolvedResultCode;
} catch (error) {
console.error(new Errors.ErrorCodeHighlighterClientLoadAllVariantsFailure(url, error));
return code ?? {};
}
}
}), [variants, url, code, setCode, loadSource, loadVariantMeta, loadCodeMeta, sourceEnhancers, processedGlobalsCode, globalsCode, setProcessedGlobalsCode, loadIsomorphicCodeVariantLoader]);
const fullVariantConfig = React.useMemo(() => ({
ChunkContent: NoopChunkContent,
source: fullVariantSource
}), [fullVariantSource]);
const {
refresh: refreshAllVariants
} = useChunk(fullVariantConfig, {
controlled: !needsData
});
return {
refresh: refreshAllVariants
};
}
function useCodeParsing({
code,
readyForContent,
highlightAfter,
isHydrated,
forceClient,
url
}) {
const {
sourceParser,
parseSource,
parseCode
} = useCodeContext();
const [isHighlightAllowed, setIsHighlightAllowed] = React.useState(highlightAfter === 'init' || highlightAfter === 'hydration' && isHydrated);
React.useEffect(() => {
if (highlightAfter === 'idle') {
return requestIdle(() => setIsHighlightAllowed(true));
}
return undefined;
}, [highlightAfter]);
// Highlight instantly once hydrated, as a non-blocking client transition,
// rather than deferring to a scheduled task. (`highlightAt: 'idle'` above is
// the mode that deliberately keeps the unhighlighted first paint and swaps in
// the highlighted tree on a later idle render.)
React.useEffect(() => {
if (highlightAfter === 'hydration' && isHydrated) {
React.startTransition(() => setIsHighlightAllowed(true));
}
}, [highlightAfter, isHydrated]);
// Determine if we should highlight based on the highlightAfter setting
const shouldHighlight = React.useMemo(() => {
if (!readyForContent) {
return false;
}
return isHighlightAllowed;
}, [readyForContent, isHighlightAllowed]);
// Memoize the "every variant is already in HAST form" check so it
// doesn't re-walk the variant + extraFiles trees on every render.
// Used both as the short-circuit inside the `parseCode` memo (fully-
// precomputed sites skip parsing entirely) and as the unmemoized
// `waitingForParsedCode` gate just below.
const allVariantsAlreadyHighlighted = React.useMemo(() => code ? hasAllVariants(Object.keys(code), code, true) : false, [code]);
// Under `CodeProviderLazy` grammars load per-language and on demand, so the
// client parse must wait until the grammars for this block's scopes are
// registered — otherwise `parseSource` falls back to plain text. Gate the
// parse memo on readiness so the block keeps its fallback until they land (no
// plain-text flash), then highlights. Synchronously ready when warm (the
// speculative preload primed them, or under an eager `CodeProvider`), so this
// adds no delay on the common path.
const grammarScopes = React.useMemo(() => code ? detectGrammarScopes(code) : [], [code]);
const grammarsReady = useGrammarsReady(grammarScopes, !!code && shouldHighlight && !allVariantsAlreadyHighlighted);
// Parse the internal code state when ready and timing conditions are met
const parsedCode = React.useMemo(() => {
if (!code || !shouldHighlight || allVariantsAlreadyHighlighted) {
return undefined;
}
if (!grammarsReady) {
// Grammars for this block's scopes are still loading; keep the fallback and
// re-run once `useGrammarsReady` flips (mirrors the `!parseSource` wait).
return undefined;
}
if (!parseSource) {
// A CodeProvider is present and its async `sourceParser` promise hasn't
// resolved yet — wait for it instead of erroring. The memo will re-run
// once `parseSource` is populated.
if (sourceParser) {
return undefined;
}
if (forceClient) {
console.error(new Errors.ErrorCodeHighlighterClientMissingParseSource(url, true));
} else {
console.error(new Errors.ErrorCodeHighlighterClientMissingParseSource(url, false));
}
return undefined;
}
if (!parseCode) {
if (forceClient) {
console.error(new Errors.ErrorCodeHighlighterClientMissingParseCode(url, true));
} else {
console.error(new Errors.ErrorCodeHighlighterClientMissingParseCode(url, false));
}
return undefined;
}
return parseCode(code, parseSource);
}, [code, shouldHighlight, allVariantsAlreadyHighlighted, grammarsReady, sourceParser, parseSource, parseCode, forceClient, url]);
// Keep highlighting deferred until parsed HAST is actually available for the
// variants that need it. `shouldHighlight` can flip true ~30ms after
// hydration, but `parseCode` only runs once the async `sourceParser` promise
// resolves. Without this wait, downstream consumers (e.g. the transform
// swap) would commit while the visible variant is still rendered from its
// raw string source, producing a structure swap on the DOM moments later.
const waitingForParsedCode = shouldHighlight && !!code && !allVariantsAlreadyHighlighted && !parsedCode;
// Only signal `deferHighlight` while a highlight pass is actively in
// flight. When `shouldHighlight` is `false` (e.g. `highlightAt: 'idle'`
// before the idle window fires, or `'view'` before the block scrolls
// into view) we render the un-highlighted source as-is — downstream
// consumers like `useTransformManagement`'s `awaitHighlight` gate must
// commit eagerly against that source instead of blocking the barrier
// indefinitely. Once the trigger fires, `shouldHighlight` flips true,
// `waitingForParsedCode` becomes true while `parseCode` runs, and
// `deferHighlight` engages for the brief window before the next
// commit paints the highlighted tree.
const deferHighlight = waitingForParsedCode;
// Render-side readiness gate. `<Pre>` (via `useCode.shouldHighlight`)
// needs to know whether the published `code` should be rendered as
// highlighted HAST *now*. That answer is false in two distinct
// windows that `deferHighlight` deliberately collapses out:
// 1. The trigger for `highlightAt: 'hydration' | 'idle' | 'visible'`
// hasn't fired yet — `shouldHighlight` is still false. The
// precomputed `codeWithGlobals` already contains HAST, so
// without a render-side gate `<Pre>` would render highlighted
// spans on the SSR pass and on first client paint, defeating
// the whole point of deferred highlighting.
// 2. The trigger has fired (`shouldHighlight = true`) but
// `parseCode` hasn't resolved yet (`waitingForParsedCode`).
// Rendering would briefly flash un-highlighted text against
// the same tree position before the highlighted HAST lands.
//
// `highlightReady` is the inverse of the pre-`e7cc08b7` wide
// `deferHighlight` semantic, exposed separately so the narrow
// `deferHighlight` (barrier consumers only block on real in-flight
// work) and the render gate can diverge without coupling.
const highlightReady = shouldHighlight && !waitingForParsedCode;
return {
parsedCode,
deferHighlight,
highlightReady
};
}
function useCodeTransforms({
parsedCode,
loadedCode,
variantName
}) {
const {
sourceParser,
computeHastDeltasLoader
} = useCodeContext();
// Track which `parsedCode` the cached `transformedCode` was computed from
// so a fresh `parsedCode` (e.g. a newly-loaded variant being added to the
// map) re-engages `waitingForTransformedCode` instead of returning the
// stale output for one render cycle. Storing input + output together lets
// callers detect staleness with reference equality.
const [transformedState, setTransformedState] = React.useState({});
// Get available transforms from the current variant (separate memo for efficiency)
const availableTransforms = React.useMemo(() => getAvailableTransforms(parsedCode ?? loadedCode, variantName), [parsedCode, loadedCode, variantName]);
// Effect to compute transformations for all variants. Only runs when the
// full async pipeline is wired (`parsedCode` + worker + deltas computer);
// the no-async case is derived during render below instead of being stored,
// so this effect never publishes a synchronous pass-through state.
React.useEffect(() => {
if (!parsedCode || !sourceParser || !computeHastDeltasLoader) {
return;
}
// Process transformations for all variants
(async () => {
try {
// Resolve the parser and the (lazy) transform-delta computer in parallel
// before computing deltas. computeHastDeltas pulls jsondiffpatch, so it's
// kept out of the initial bundle under CodeProviderLazy.
const [parseSource, computeHastDeltas] = await Promise.all([sourceParser, computeHastDeltasLoader()]);
const enhanced = await computeHastDeltas(parsedCode, parseSource);
setTransformedState({
input: parsedCode,
output: enhanced
});
} catch (error) {
console.error(new Errors.ErrorCodeHighlighterClientTransformProcessingFailure(error));
setTransformedState({
input: parsedCode,
output: parsedCode
});
}
})();
}, [parsedCode, sourceParser, computeHastDeltasLoader]);
// When the full async pipeline is wired, expose the cached output regardless
// of whether `parsedCode` changed since the last computation — falling back
// to `undefined` here would yank the currently-displayed HAST for a frame
// while the async pipeline catches up. Staleness is signalled via
// `waitingForTransformedCode` so downstream gates (e.g.
// `useTransformManagement` / `useVariantSelection`) hold off committing a
// swap until fresh deltas land. Without the pipeline, `transformedCode` is a
// synchronous pass-through of `parsedCode` derived during render.
const hasAsyncPipeline = !!parsedCode && !!sourceParser && !!computeHastDeltasLoader;
const transformedCode = hasAsyncPipeline ? transformedState.output : parsedCode;
// Async hast-deltas pipeline status. While true, consumers (notably
// `useTransformManagement`'s `deferHighlight` gate) should treat
// highlighting as not-yet-settled and hold off committing a transform
// swap. Without this, the swap can commit after `parsedCode` is ready
// but *before* `computeHastDeltas` resolves: the incoming tree first
// renders without the transform deltas, then re-renders a frame or
// two later when `transformedCode` arrives, producing a visible jump
// on top of the just-played collapse animation.
//
// Only relevant when both a worker (`sourceParser`) and a deltas
// computer (`computeHastDeltas`) are wired up — environments without
// them resolve `transformedCode` synchronously to `parsedCode` in the
// effect above, so the deltas phase is a no-op. We compare the cached
// `input` against the live `parsedCode` instead of just checking
// `!transformedCode` so a freshly-arriving variant re-engages the wait
// until its deltas land.
const waitingForTransformedCode = hasAsyncPipeline && transformedState.input !== parsedCode;
return {
transformedCode,
availableTransforms,
waitingForTransformedCode
};
}
function useControlledCodeParsing({
code,
forceClient,
url,
preParsedCache
}) {
const {
sourceParser,
parseSource,
parseControlledCode
} = useCodeContext();
// Parse the controlled code separately (no need to check readyForContent)
const parsedControlledCode = React.useMemo(() => {
if (!code) {
return undefined;
}
if (!parseSource) {
// A CodeProvider is present and its async `sourceParser` promise hasn't
// resolved yet (e.g. CodeProviderLazy dynamic-importing the engine) — wait
// for it instead of erroring. The memo re-runs once `parseSource` lands.
if (sourceParser) {
return undefined;
}
if (forceClient) {
console.error(new Errors.ErrorCodeHighlighterClientMissingParseSource(url, true));
} else {
console.error(new Errors.ErrorCodeHighlighterClientMissingParseSource(url, false));
}
return undefined;
}
if (!parseControlledCode) {
if (forceClient) {
console.error(new Errors.ErrorCodeHighlighterClientMissingParseControlledCode(url, true));
} else {
console.error(new Errors.ErrorCodeHighlighterClientMissingParseControlledCode(url, false));
}
return undefined;
}
return parseControlledCode(code, parseSource, preParsedCache);
}, [code, sourceParser, parseSource, parseControlledCode, forceClient, url, preParsedCache]);
return {
parsedControlledCode
};
}
function useGlobalsCodeMerging({
url,
code,
globalsCode,
processedGlobalsCode,
setProcessedGlobalsCode,
readyForContent,
variants
}) {
const {
loadCodeMeta,
loadSource,
loadVariantMeta,
loadIsomorphicCodeVariantLoader
} = useCodeContext();
// Set processedGlobalsCode if we have ready Code objects but haven't stored them yet
React.useEffect(() => {
if (!globalsCode || processedGlobalsCode) {
return; // No globals or already processed
}
// Check if all items are already Code objects (precomputed)
if (globalsCode.every(item => typeof item === 'object')) {
const codeObjects = globalsCode;
// Check if all Code objects have all their own variants
const allReady = codeObjects.every(codeObj => hasAllVariants(Object.keys(codeObj), codeObj));
if (allReady) {
setProcessedGlobalsCode(codeObjects);
return;
}
// If not all ready, fall through to loading logic below
}
if (!loadIsomorphicCodeVariantLoader) {
console.error(new Errors.ErrorCodeHighlighterClientMissingLoadVariantForGlobals());
return;
}
// Need to load string URLs or load missing variants
(async () => {
try {
const loadIsomorphicCodeVariant = await loadIsomorphicCodeVariantLoader();
// First, load any string URLs into Code objects
const basicCodeObjects = await Promise.all(globalsCode.map(async item => {
if (typeof item === 'string') {
if (!loadCodeMeta) {
throw new Errors.ErrorCodeHighlighterClientMissingLoadCodeMetaForStringUrls();
}
return {
codeObj: await loadCodeMeta(item),
originalUrl: item
};
}
return {
codeObj: item,
originalUrl: undefined
};
}));
// Now check if we need to load variants for any of the Code objects
const fullyLoadedCodeObjects = await Promise.all(basicCodeObjects.map(async ({
codeObj,
originalUrl
}) => {
// Check if this Code object has all required variants
if (hasAllVariants(variants, codeObj)) {
return codeObj; // Already has all variants
}
// Need to load missing variants
const loadedVariants = {
...codeObj
};
await Promise.all(variants.map(async variantName => {
if (codeObj[variantName] && typeof codeObj[variantName] === 'object') {
return; // Variant already loaded
}
// Need to load this variant
try {
const result = await loadIsomorphicCodeVariant(originalUrl || '',
// Use the original URL if available
variantName, codeObj[variantName],
// May be undefined or string
{
disableParsing: true,
disableTransforms: true,
loadSource,
loadVariantMeta
});
loadedVariants[variantName] = result.code;
} catch (error) {
console.error(new Errors.ErrorCodeHighlighterClientLoadVariantFailureForGlobals(variantName, originalUrl, error));
// Keep the original variant data (may be undefined)
}
}));
return loadedVariants;
}));
setProcessedGlobalsCode(fullyLoadedCodeObjects);
} catch (error) {
console.error(new Errors.ErrorCodeHighlighterClientLoadGlobalsCodeFailure(url || 'No URL', error));
}
})();
}, [url, globalsCode, processedGlobalsCode, setProcessedGlobalsCode, loadCodeMeta, loadSource, loadVariantMeta, variants, loadIsomorphicCodeVariantLoader]);
// Determine globalsCodeObjects to use (prefer processed, fallback to direct if ready)
const globalsCodeObjects = React.useMemo(() => {
if (processedGlobalsCode) {
return processedGlobalsCode;
}
if (globalsCode && globalsCode.every(item => typeof item === 'object')) {
const codeObjects = globalsCode;
const allGlobalsReady = codeObjects.every(codeObj => hasAllVariants(Object.keys(codeObj), codeObj));
if (allGlobalsReady) {
return codeObjects;
}
}
return undefined;
}, [processedGlobalsCode, globalsCode]);
// Merge globalsCode with code when ready
return React.useMemo(() => {
// If no globalsCode or code not ready, return as-is
if (!globalsCode || !code || !readyForContent) {
return code;
}
// If globalsCodeObjects isn't ready yet, return unmerged code for now
if (!globalsCodeObjects) {
return code;
}
// For precomputed code, do simple synchronous merging of extraFiles
const mergedCode = {
...code
};
let hasChanges = false;
variants.forEach(variant => {
const variantData = code[variant];
if (!variantData || typeof variantData === 'string') {
return;
}
// Get globalsCode for this variant (only exact matches, no fallback)
const globalsForVariant = globalsCodeObjects.map(codeObj => codeObj[variant]).filter(item => Boolean(item) && typeof item === 'object');
if (globalsForVariant.length > 0) {
// Use mergeCodeMetadata for sophisticated globals merging with proper positioning
let currentVariant = variantData;
globalsForVariant.forEach(globalVariant => {
if (globalVariant.extraFiles) {
// Convert globals extraFiles to metadata format for mergeCodeMetadata
const globalsMetadata = {};
for (const [key, value] of Object.entries(globalVariant.extraFiles)) {
if (typeof value === 'string') {
globalsMetadata[key] = {
source: value
};
} else {
globalsMetadata[key] = {
...value
};
}
}
// Use mergeCodeMetadata to properly position and merge the globals
currentVariant = mergeCodeMetadata(currentVariant, globalsMetadata);
}
});
// Only update if the variant actually changed
if (currentVariant !== variantData) {
mergedCode[variant] = currentVariant;
hasChanges = true;
}
}
});
// Return merged code if we made changes, otherwise return original code
return hasChanges ? mergedCode : code;
}, [code, globalsCode, globalsCodeObjects, readyForContent, variants]);
}
function usePropsCodeGlobalsMerging({
code,
globalsCode,
processedGlobalsCode,
variants
}) {
// For props.code, always do synchronous merging if possible
// We don't want to cache this in state since props.code can change frequently
return React.useMemo(() => {
if (!code || !globalsCode || !processedGlobalsCode) {
return code; // No merge needed or not ready
}
// Use processedGlobalsCode for synchronous merging
const globalsCodeObjects = processedGlobalsCode;
// For props.code (controlled), do simple synchronous merging
const mergedCode = {
...code
};
let hasChanges = false;
variants.forEach(variant => {
const variantData = code[variant];
if (!variantData || typeof variantData === 'string') {
return;
}
// Get globalsCode for this variant (only exact matches, no fallback)
const globalsForVariant = globalsCodeObjects.map(codeObj => codeObj[variant]).filter(item => Boolean(item) && typeof item === 'object');
if (globalsForVariant.length > 0) {
// Use mergeCodeMetadata for sophisticated globals merging with proper positioning
let currentVariant = variantData;
globalsForVariant.forEach(globalVariant => {
if (globalVariant.extraFiles) {
// Convert globals extraFiles to metadata format for mergeCodeMetadata
const globalsMetadata = {};
for (const [key, value] of Object.entries(globalVariant.extraFiles)) {
if (typeof value === 'string') {
globalsMetadata[key] = {
source: value
};
} else {
globalsMetadata[key] = {
...value
};
}
}
// Use mergeCodeMetadata to properly position and merge the globals
currentVariant = mergeCodeMetadata(currentVariant, globalsMetadata);
}
});
// Only update if the variant actually changed
if (currentVariant !== variantData) {
mergedCode[variant] = currentVariant;
hasChanges = true;
}
}
});
// Return merged code if we made changes, otherwise return original code
return hasChanges ? mergedCode : code;
}, [code, globalsCode, processedGlobalsCode, variants]);
}
export function CodeHighlighterClient(props) {
const controlled = useControlledCode();
const isControlled = Boolean(props.code || controlled?.code);
const [code, setCode] = React.useState(typeof props.precompute === 'object' ? props.precompute : undefined);
// Sync code state with precompute prop changes (for hot-reload). Done with
// the store-previous-prop render-phase derivation rather than an effect:
// `code` is genuinely state (also mutated by `useInitialData` via `setCode`
// for client fallback loading) so it can't be pure derivation, but the
// re-seed on a new `precompute` is a render-time setState off the previous
// prop value. Match the original effect's branch logic: only object values
// re-seed and only an explicit `undefined` clears — any other value (e.g. a
// loader) leaves `code` untouched.
const [prevPrecompute, setPrevPrecompute] = React.useState(props.precompute);
if (props.precompute !== prevPrecompute) {
setPrevPrecompute(props.precompute);
if (typeof props.precompute === 'object') {
setCode(props.precompute);
} else if (props.precompute === undefined) {
setCode(undefined);
}
}
// State to store processed globalsCode to avoid duplicate loading
const [processedGlobalsCode, setProcessedGlobalsCode] = React.useState(undefined);
const activeCode = controlled?.code || props.code || code;
const variants = React.useMemo(() => props.variants || Object.keys(props.components || activeCode || {}), [props.variants, props.components, activeCode]);
// TODO: if using props.variant, then the variant is controlled and we can't use our own state
// does props.variant make any sense instead of controlledSelection?.variant?
const [selection, setSelection] = React.useState({
variant: props.initialVariant || props.defaultVariant || variants[0]
});
const variantName = controlled?.selection?.variant || props.variant || selection.variant;
let initialFilename;
if (typeof activeCode?.[variantName] === 'object') {
const variant = activeCode[variantName];
initialFilename = variant?.filesOrder ? variant.filesOrder[0] : variant?.fileName;
}
const fileName = controlled?.selection?.fileName || props.fileName || initialFilename;
const {
url,
highlightAfter,
enhanceAfter,
fallbackUsesExtraFiles,
fallbackUsesAllVariants,
editActivation
} = props;
// Speculative preload: on first render, start fetching the heavy loaders this
// block is about to need (under CodeProviderLazy) so they're in flight before
// the content mounts and awaits them. Signals are cheap + accurate, so a
// precomputed or code-free block preloads nothing.
// Only the precomputed/loaded (non-controlled) code drives speculative loading.
const speculativeCode = isControlled ? undefined : code;
const speculativeGrammarScopes = React.useMemo(() => speculativeCode ? detectGrammarScopes(speculativeCode) : [], [speculativeCode]);
const speculativeAllPresent = React.useMemo(() => speculativeCode ? hasAllVariants(variants, speculativeCode) : false, [variants, speculativeCode]);
const speculativeHasTransforms = React.useMemo(() => !!speculativeCode && !hasAllVariants(variants, speculativeCode, true) && getAvailableTransforms(speculativeCode, variantName).length > 0, [variants, speculativeCode, variantName]);
useSpeculativeCodePreload({
needsData: !isControlled && !!url && !speculativeAllPresent,
hasTransforms: speculativeHasTransforms
});
// Per-block editing activation: flipped once when the block first engages for
// editing — threaded down to `useEditable.onActivate` via `CodeHighlighterContext`
// (immediately in `'eager'`, on hover/focus/click in `'interaction'`). Drives
// the editable speculative preload below and notifies the CodeControllerContext.
const [editingActivated, setEditingActivated] = React.useState(false);
const controllerOnActivate = controlled?.onActivate;
const handleEditingActivated = React.useCallback(() => {
setEditingActivated(true);
controllerOnActivate?.();
}, [controllerOnActivate]);
// Grammar scopes the editable files need for live re-highlighting. Unlike the
// speculative highlight/transform preloads — which intentionally skip
// controlled blocks (`speculativeCode` is cleared above) — an editable block
// DOES re-highlight its edits on the client, so its grammars must load or the
// edited source falls back to plain text. The editable file set (and thus the
// scopes) comes from `props.code`: editing changes source *content*, never
// which files exist, so this stays stable across keystrokes.
const editableGrammarScopes = React.useMemo(() => {
const editableCode = props.code ?? code;
return editableCode ? detectGrammarScopes(editableCode) : [];
}, [props.code, code]);
// When the block is editable (a CodeControllerContext with `setCode` is in
// scope), warm the live-editing engine, the per-language grammars, and the
// worker so they're in flight before the user edits. Deduped page-wide. In
// `editActivation: 'interaction'` mode the warming waits until the block is
// `activated` (engaged) — that mode defers loading until the reader engages.
useSpeculativeEditingPreload({
enabled: Boolean(controlled?.setCode),
editActivation,
activated: editingActivated,
scopes: editableGrammarScopes
});
// Preload the client-side transform applier (the `jsondiffpatch` chunk) when
// the code declares transforms — so it is warm before the reader switches a
// transform, in parallel with the (lazy) content. Broader than the
// `speculativeHasTransforms` highlight signal above: even a fully-precomputed
// (already-highlighted) block needs the applier to switch transforms
// client-side, so this drops the not-yet-highlighted gate. A block with no
// transforms never pulls the chunk.
const speculativeHasAnyTransforms = React.useMemo(() => speculativeCode ? getAvailableTransforms(speculativeCode, variantName).length > 0 : false, [speculativeCode, variantName]);
useSpeculativeUseCodePreload({
hasTransforms: speculativeHasAnyTransforms
});
// Preload the per-language grammar chunks this block needs, before `useCode`
// mounts and parses — in parallel with the (lazy) content. Only when the block
// will actually highlight client-side: it is forced client-side, not yet
// fully precomputed (so the client must parse), or eagerly editable (live
// re-highlight). A fully-precomputed read-only block renders its highlighted
// HAST and never parses, so it loads no grammar at all.
const willClientHighlight = !!speculativeCode && (Boolean(props.forceClient) || !hasAllVariants(variants, speculativeCode, true) || (editActivation ?? 'eager') !== 'interaction' && Boolean(controlled?.setCode));
useSpeculativeGrammarPreload({
scopes: speculativeGrammarScopes,
enabled: willClientHighlight
});
// ── Fallback hoisting ──
// State for fallbacks hoisted from ContentLoading via useCodeFallback.
// Content is stripped from Code on the server and passed to ContentLoading
// as source/extraSource props. ContentLoading hoists them back here so
// CodeHighlighterClient can derive text dictionaries for decompression.
const [hoistedFallbackHasts, setHoistedFallbackHasts] = React.useState({});
// Track whether ContentLoading called useCodeFallback via callback. The
// force-mount-once behavior (mounting the fallback even when the code is
// already ready, so `useCodeFallback` can hoist the DEFLATE dictionary) is now
// owned by `useCoordinatedSwap` below; this ref only drives the dev-time
// validation that ContentLoading wired its hoist hook.
const hookCalledRef = React.useRef(false);
const handleHookCalled = React.useCallback(() => {
hookCalledRef.current = true;
}, []);
// Stable callback for ContentLoading to hoist its fallbacks.
const handleSetFallbackHasts = React.useCallback((variant, hasts) => {
setHoistedFallbackHasts(prev => {
if (prev[variant] === hasts) {
return prev;
}
return {
...prev,
[variant]: hasts
};
});
}, []);
const {
fallbackPending
} = useInitialData({
variants,
variantName,
code,
setCode,
fileName,
url,
highlightAfter,
fallbackUsesExtraFiles,
fallbackUsesAllVariants,
isControlled,
globalsCode: props.globalsCode,
setProcessedGlobalsCode,
handleSetFallbackHasts
});
// Reverse the server-side residual consolidation, scattering the decompressed
// fallbacks back onto the code so every variant carries its own dictionary
// (the swap line-count classifier reads `code.fallback`, not the active-only
// hoist). The blob is primed with the RENDERED subset's text, which reaches
// the client only via the hoist — so wait for that subset to hoist before
// decompressing. WHICH variants are rendered depends on `fallbackUsesAllVariants`
// (every variant, or just the initial one); gate on THAT subset, never on the
// *current* `variantName`, or swapping to a non-rendered variant drops the
// scatter and strands the other variants without their dictionary.
const residualFallbacks = props.residualFallbacks;
const renderedVariant = props.initialVariant || props.defaultVariant || variants[0];
const renderedHoisted = fallbackUsesAllVariants ? variants.every(variant => Boolean(hoistedFallbackHasts[variant])) : Boolean(hoistedFallbackHasts[renderedVariant]);
const residualMap = React.useMemo(() => {
if (!residualFallbacks || !renderedHoisted) {
return undefined;
}
return decompressResidualFallbacks(residualFallbacks, residualDictionaryText(hoistedFallbackHasts));
}, [residualFallbacks, renderedHoisted, hoistedFallbackHasts]);
// Scatter the dictionaries back onto whichever code carries it, so consumers
// (the render and the swap line-count classifier) read `code.fallback` for any
// variant. Two sources: the decompressed residual blob (`residualMap` — the
// non-rendered variants, and under `fallbackUsesAllVariants` the blob is empty)
// and the hoist (`hoistedFallbackHasts` — the rendered subset, which is the ONLY
// place every variant's dictionary lives under `fallbackUsesAllVariants`). Skip
// the hoist under `fallbackCollapsed`, where it is only each file's collapsed
// window; the full dictionary comes from the blob there. Memoized so the
// freshly-cloned code keeps a stable identity until its inputs change.
const restoreFallbacks = React.useCallback(base => {
if (!base) {
return base;
}
let restored = residualMap ? scatterResidualFallbacks(base, residualMap) : base;
if (!props.fallbackCollapsed) {
// `preserveExisting`: never let the hoist overwrite a `fallback` already
// on the variant. A fully-loaded `hastCompressed` source carries its own
// source-paired (structured) `fallback`, which is the only valid DEFLATE
// dictionary. The hoist can be an un-highlighted *raw-string* fallback
// whose text keeps a trailing newline `buildRootFallback` drops, so
// overwriting the structured one makes `decodeHastSource` throw a
// dictionary mismatch. The hoist is the dictionary only when the variant's
// own was stripped, so apply it solely where one isn't already present.
restored = scatterResidualFallbacks(restored, hoistedFallbackHasts, true);
}
return restored;
}, [residualMap, hoistedFallbackHasts, props.fallbackCollapsed]);
const resolvedPropsCode = React.useMemo(() => restoreFallbacks(props.code), [props.code, restoreFallbacks]);
const resolvedStateCode = React.useMemo(() => restoreFallbacks(code), [code, restoreFallbacks]);
// Use useSyncExternalStore to detect hydration
const subscribe = React.useCallback(() => () => {}, []);
const getSnapshot = React.useCallback(() => true, []);
const getServerSnapshot = React.useCallback(() => false, []);
const useIsHydrated = () => React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
const isHydrated = useIsHydrated();
const [isEnhanceAllowed, setIsEnhanceAllowed] = React.useState(enhanceAfter === 'init' || enhanceAfter === 'hydration' && isHydrated);
React.useEffect(() => {
if (enhanceAfter === 'idle') {
return requestIdle(() => setIsEnhanceAllowed(true));
}
return undefined;
}, [enhanceAfter]);
// Enhance instantly once hydrated, as a non-blocking client transition,
// rather than deferring to a scheduled task. (`enhanceAfter: 'idle'` above is
// the mode that deliberately keeps the un-enhanced first paint and swaps in
// the enhanced tree on a later idle render.)
React.useEffect(() => {
if (enhanceAfter === 'hydration' && isHydrated) {
React.startTransition(() => setIsEnhanceAllowed(true));
}
}, [enhanceAfter, isHydrated]);
const readyForContent = React.useMemo(() => {
if (!code) {
return false;
}
return hasAllVariants(variants, code);
}, [code, variants]);
// Separate check for activeCode to determine when to show fallback
const activeCodeReady = React.useMemo(() => {
if (!activeCode || !isEnhanceAllowed) {
return false;
}
// Controlled code is always ready since it comes from editing already-ready code
if (controlled?.code) {
return true;
}
// For regular code, use the existing hasAllVariants function
const regularCode = props.code || code;
return regularCode ? hasAllVariants(variants, regularCode) : false;
}, [activeCode, isEnhanceAllowed, controlled?.code, variants, props.code, code]);
const {
refresh: refreshAllVariants
} = useAllVariants({
readyForContent,
variants,
isControlled,
url,
code,
setCode,
processedGlobalsCode,
globalsCode: props.globalsCode,
setProcessedGlobalsCode,
fallbackPending
});
// Merge globalsCode with internal state code (fetched data) - this should be stable once ready
const stateCodeWithGlobals = useGlobalsCodeMerging({
url,
code: resolvedStateCode,
// Only use internal state, not props.code
globalsCode: props.globalsCode,
processedGlobalsCode,
setProcessedGlobalsCode,
readyForContent,
variants
});
// For props.code (controlled), always re-merge when it changes (don't cache in state)
const propsCodeWithGlobals = usePropsCodeGlobalsMerging({
code: resolvedPropsCode,
globalsCode: props.globalsCode,
processedGlobalsCode,
variants
});
// Use props.code result if available, otherwise use state code result
const codeWithGlobals = propsCodeWithGlobals || stateCodeWithGlobals;
const {
parsedCode,
deferHighlight: deferHighlightForParsing,
highlightReady
} = useCodeParsing({
code: codeWithGlobals,
readyForContent: readyForContent || Boolean(props.code),
highlightAfter,
isHydrated,
forceClient: props.forceClient,