@mui/internal-docs-infra 0.11.1-canary.21 → 0.11.1-canary.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CodeHighlighter/CodeHighlighter.mjs +11 -2
  2. package/CodeHighlighter/CodeHighlighterClient.mjs +60 -51
  3. package/CodeHighlighter/createClientProps.mjs +14 -3
  4. package/CodeHighlighter/fallbackFormat.d.mts +38 -0
  5. package/CodeHighlighter/fallbackFormat.mjs +96 -3
  6. package/CodeHighlighter/prepareInitialSource.d.mts +11 -0
  7. package/CodeHighlighter/prepareInitialSource.mjs +67 -8
  8. package/CodeHighlighter/resolveFallbackCritical.d.mts +23 -0
  9. package/CodeHighlighter/resolveFallbackCritical.mjs +44 -0
  10. package/CodeHighlighter/types.d.mts +16 -0
  11. package/CoordinatedLazy/useChunk.mjs +10 -8
  12. package/CoordinatedLazy/useCoordinatedSwap.mjs +1 -0
  13. package/abstractCreateTypes/TypeCode.mjs +12 -11
  14. package/abstractCreateTypes/typesToJsx.mjs +13 -8
  15. package/package.json +2 -2
  16. package/pipeline/loadIsomorphicCodeVariant/loadIsomorphicCodeVariant.mjs +41 -1
  17. package/pipeline/parseSource/frameVisibility.d.mts +17 -1
  18. package/pipeline/parseSource/frameVisibility.mjs +53 -0
  19. package/useCode/EditableEngine.mjs +15 -5
  20. package/useCode/Pre.mjs +43 -48
  21. package/useCode/SourceEditingEngine.mjs +29 -8
  22. package/useCode/useCode.mjs +11 -3
  23. package/useCode/{liveEditingBugs.browser.mjs → useEditable.integration.browser.mjs} +114 -69
  24. package/useCode/useFileNavigation.mjs +20 -16
  25. package/useCode/useTransformManagement.mjs +13 -5
  26. package/useCode/useUIState.mjs +6 -6
  27. package/useCode/useVariantSelection.mjs +20 -6
  28. package/useCoordinated/coordinatePreference.mjs +4 -25
  29. package/useCoordinated/scheduleTasks.d.mts +23 -0
  30. package/useCoordinated/scheduleTasks.mjs +45 -0
  31. package/useCoordinated/useCoordinated.mjs +33 -6
  32. package/useStream/useStream.mjs +2 -4
  33. package/useStream/useStreamController.mjs +6 -1
  34. /package/useCode/{liveEditingBugs.browser.d.mts → useEditable.integration.browser.d.mts} +0 -0
@@ -106,7 +106,11 @@ export function CodeHighlighter(props) {
106
106
  };
107
107
 
108
108
  // No ContentLoading: render the content/full-load directly, with no loading
109
- // fallback (the client shows nothing until content is ready).
109
+ // fallback (the client shows nothing until content is ready). For
110
+ // `highlightAt: 'init'`, `createClientProps` folds each variant's highlighted-visible
111
+ // `fallbackCritical` over its plain `fallback`, so `<Pre>` paints the visible frames
112
+ // highlighted on the first render (no decompression) and decodes the full tree after
113
+ // paint (the `decodeAllowed` latch).
110
114
  if (process.env.NODE_ENV !== "production") renderChunk.displayName = "renderChunk";
111
115
  if (!ContentLoading) {
112
116
  if (props.highlightAfter === 'stream') {
@@ -158,7 +162,12 @@ export function CodeHighlighter(props) {
158
162
  initialFilename: initialData.initialFilename,
159
163
  initialSource: initialData.initialSource,
160
164
  initialExtraFiles: initialData.initialExtraFiles,
161
- ContentLoading
165
+ ContentLoading,
166
+ // Compressing the residual fallbacks only shrinks the server→client payload. This
167
+ // entry is isomorphic, so when it runs on the client (e.g. a Pages-Router app that
168
+ // renders everything client-side) there is no wire — skip the compress and keep the
169
+ // fallbacks inline rather than compressing them only to decompress them right back.
170
+ compressResidual: typeof window === 'undefined'
162
171
  });
163
172
  return renderChunk({
164
173
  preloaded: codeForClient,
@@ -8,6 +8,7 @@ import { hasAllVariants } from "../pipeline/loadIsomorphicCodeVariant/hasAllCode
8
8
  import { CodeHighlighterFallbackContext } from "./CodeHighlighterFallbackContext.mjs";
9
9
  import { useControlledCode } from "../CodeControllerContext/index.mjs";
10
10
  import { codeToFallbackProps, deriveFallbacksFromCode, stripFallbackHastsFromCode } from "./codeToFallbackProps.mjs";
11
+ import { resolveFallbackCritical } from "./resolveFallbackCritical.mjs";
11
12
  import { decompressResidualFallbacks, residualDictionaryText, scatterResidualFallbacks } from "./fallbackCompression.mjs";
12
13
  import { mergeCodeMetadata } from "../pipeline/loadIsomorphicCodeVariant/mergeCodeMetadata.mjs";
13
14
  import { getAvailableTransforms } from "../pipeline/loadIsomorphicCodeVariant/getAvailableTransforms.mjs";
@@ -21,6 +22,7 @@ import { useChunk } from "../CoordinatedLazy/useChunk.mjs";
21
22
  import { useCoordinatedSwap } from "../CoordinatedLazy/useCoordinatedSwap.mjs";
22
23
  import { CoordinatedFallbackContext } from "../CoordinatedLazy/CoordinatedFallbackContext.mjs";
23
24
  import { CoordinatedContentContext } from "../CoordinatedLazy/CoordinatedContentContext.mjs";
25
+ import { requestIdle } from "../useCoordinated/scheduleTasks.mjs";
24
26
  import * as Errors from "./errors.mjs";
25
27
  import { jsx as _jsx } from "react/jsx-runtime";
26
28
  const DEBUG = false; // Set to true for debugging purposes
@@ -117,11 +119,20 @@ function useInitialData({
117
119
  return code ?? {};
118
120
  }
119
121
 
122
+ // Fold each variant's highlighted-visible `fallbackCritical` over its plain
123
+ // `fallback` (under `highlightAt: 'init'`) and strip the staging field, so the
124
+ // hoisted loading fallback is already highlighted and nothing leaks to the
125
+ // content. `collapseToEmpty` isn't threaded into the client here, so the `false`
126
+ // form is assumed: under collapse-to-empty this may promote a few frames that are
127
+ // then CSS-hidden, but that is harmless — the promoted text is byte-identical
128
+ // (a valid dictionary) and the frames never paint.
129
+ const resolved = resolveFallbackCritical(loaded.code, highlightAfter, false) ?? loaded.code;
130
+
120
131
  // Strip fallbacks from code and hoist them directly
121
132
  const {
122
133
  strippedCode,
123
134
  allFallbackHasts
124
- } = stripFallbackHastsFromCode(loaded.code, variantName, fallbackUsesExtraFiles, fallbackUsesAllVariants);
135
+ } = stripFallbackHastsFromCode(resolved, variantName, fallbackUsesExtraFiles, fallbackUsesAllVariants);
125
136
  if (!signal.aborted) {
126
137
  setCode(strippedCode);
127
138
  for (const [variant, hasts] of Object.entries(allFallbackHasts)) {
@@ -279,12 +290,18 @@ function useAllVariants({
279
290
  resultCode[item.name] = item.variant.code;
280
291
  }
281
292
  }
293
+
294
+ // Strip the staging `fallbackCritical` before it enters `code` state and
295
+ // reaches the content. The full load runs with `disableParsing`, so the source
296
+ // is a raw string and no `fallbackCritical` is produced here — the strip is
297
+ // purely defensive, hence the strip-only `'idle'` (promotion is `'init'`-gated).
298
+ const resolvedResultCode = resolveFallbackCritical(resultCode, 'idle', false) ?? resultCode;
282
299
  if (errors.length > 0) {
283
300
  console.error(new Errors.ErrorCodeHighlighterClientLoadVariantsFailure(url, errors));
284
301
  } else if (!signal.aborted) {
285
- setCode(resultCode);
302
+ setCode(resolvedResultCode);
286
303
  }
287
- return resultCode;
304
+ return resolvedResultCode;
288
305
  } catch (error) {
289
306
  console.error(new Errors.ErrorCodeHighlighterClientLoadAllVariantsFailure(url, error));
290
307
  return code ?? {};
@@ -304,17 +321,6 @@ function useAllVariants({
304
321
  refresh: refreshAllVariants
305
322
  };
306
323
  }
307
- function yieldToMain() {
308
- const scheduler = globalThis.scheduler;
309
- if (scheduler?.yield) {
310
- return scheduler.yield();
311
- }
312
-
313
- // Fall back to yielding with setTimeout.
314
- return new Promise(resolve => {
315
- setTimeout(resolve, 0);
316
- });
317
- }
318
324
  function useCodeParsing({
319
325
  code,
320
326
  readyForContent,
@@ -331,22 +337,18 @@ function useCodeParsing({
331
337
  const [isHighlightAllowed, setIsHighlightAllowed] = React.useState(highlightAfter === 'init' || highlightAfter === 'hydration' && isHydrated);
332
338
  React.useEffect(() => {
333
339
  if (highlightAfter === 'idle') {
334
- const requestIdleCallback = window.requestIdleCallback ?? setTimeout;
335
- const cancelIdleCallback = window.cancelIdleCallback ?? clearTimeout;
336
- const idleRequest = requestIdleCallback(() => {
337
- setIsHighlightAllowed(true);
338
- });
339
- return () => cancelIdleCallback(idleRequest);
340
+ return requestIdle(() => setIsHighlightAllowed(true));
340
341
  }
341
342
  return undefined;
342
343
  }, [highlightAfter]);
343
344
 
344
- // Update highlight allowed state when hydration completes
345
+ // Highlight instantly once hydrated, as a non-blocking client transition,
346
+ // rather than deferring to a scheduled task. (`highlightAt: 'idle'` above is
347
+ // the mode that deliberately keeps the unhighlighted first paint and swaps in
348
+ // the highlighted tree on a later idle render.)
345
349
  React.useEffect(() => {
346
350
  if (highlightAfter === 'hydration' && isHydrated) {
347
- // we should ensure that each code highlighter is enhanced as a separate task
348
- // this should run from top to bottom
349
- yieldToMain().then(() => setIsHighlightAllowed(true));
351
+ React.startTransition(() => setIsHighlightAllowed(true));
350
352
  }
351
353
  }, [highlightAfter, isHydrated]);
352
354
 
@@ -475,13 +477,12 @@ function useCodeTransforms({
475
477
  // Get available transforms from the current variant (separate memo for efficiency)
476
478
  const availableTransforms = React.useMemo(() => getAvailableTransforms(parsedCode ?? loadedCode, variantName), [parsedCode, loadedCode, variantName]);
477
479
 
478
- // Effect to compute transformations for all variants
480
+ // Effect to compute transformations for all variants. Only runs when the
481
+ // full async pipeline is wired (`parsedCode` + worker + deltas computer);
482
+ // the no-async case is derived during render below instead of being stored,
483
+ // so this effect never publishes a synchronous pass-through state.
479
484
  React.useEffect(() => {
480
485
  if (!parsedCode || !sourceParser || !computeHastDeltasLoader) {
481
- setTransformedState({
482
- input: parsedCode,
483
- output: parsedCode
484
- });
485
486
  return;
486
487
  }
487
488
 
@@ -507,13 +508,16 @@ function useCodeTransforms({
507
508
  })();
508
509
  }, [parsedCode, sourceParser, computeHastDeltasLoader]);
509
510
 
510
- // Expose the cached output regardless of whether `parsedCode` changed since
511
- // the last computation — falling back to `undefined` here would yank the
512
- // currently-displayed HAST for a frame while the async pipeline catches up.
513
- // Staleness is signalled via `waitingForTransformedCode` so downstream
514
- // gates (e.g. `useTransformManagement` / `useVariantSelection`) hold off
515
- // committing a swap until fresh deltas land.
516
- const transformedCode = transformedState.output;
511
+ // When the full async pipeline is wired, expose the cached output regardless
512
+ // of whether `parsedCode` changed since the last computation — falling back
513
+ // to `undefined` here would yank the currently-displayed HAST for a frame
514
+ // while the async pipeline catches up. Staleness is signalled via
515
+ // `waitingForTransformedCode` so downstream gates (e.g.
516
+ // `useTransformManagement` / `useVariantSelection`) hold off committing a
517
+ // swap until fresh deltas land. Without the pipeline, `transformedCode` is a
518
+ // synchronous pass-through of `parsedCode` derived during render.
519
+ const hasAsyncPipeline = !!parsedCode && !!sourceParser && !!computeHastDeltasLoader;
520
+ const transformedCode = hasAsyncPipeline ? transformedState.output : parsedCode;
517
521
 
518
522
  // Async hast-deltas pipeline status. While true, consumers (notably
519
523
  // `useTransformManagement`'s `deferHighlight` gate) should treat
@@ -531,7 +535,7 @@ function useCodeTransforms({
531
535
  // `input` against the live `parsedCode` instead of just checking
532
536
  // `!transformedCode` so a freshly-arriving variant re-engages the wait
533
537
  // until its deltas land.
534
- const waitingForTransformedCode = !!parsedCode && !!sourceParser && !!computeHastDeltasLoader && transformedState.input !== parsedCode;
538
+ const waitingForTransformedCode = hasAsyncPipeline && transformedState.input !== parsedCode;
535
539
  return {
536
540
  transformedCode,
537
541
  availableTransforms,
@@ -835,15 +839,23 @@ export function CodeHighlighterClient(props) {
835
839
  const isControlled = Boolean(props.code || controlled?.code);
836
840
  const [code, setCode] = React.useState(typeof props.precompute === 'object' ? props.precompute : undefined);
837
841
 
838
- // Sync code state with precompute prop changes (for hot-reload)
839
- React.useEffect(() => {
842
+ // Sync code state with precompute prop changes (for hot-reload). Done with
843
+ // the store-previous-prop render-phase derivation rather than an effect:
844
+ // `code` is genuinely state (also mutated by `useInitialData` via `setCode`
845
+ // for client fallback loading) so it can't be pure derivation, but the
846
+ // re-seed on a new `precompute` is a render-time setState off the previous
847
+ // prop value. Match the original effect's branch logic: only object values
848
+ // re-seed and only an explicit `undefined` clears — any other value (e.g. a
849
+ // loader) leaves `code` untouched.
850
+ const [prevPrecompute, setPrevPrecompute] = React.useState(props.precompute);
851
+ if (props.precompute !== prevPrecompute) {
852
+ setPrevPrecompute(props.precompute);
840
853
  if (typeof props.precompute === 'object') {
841
854
  setCode(props.precompute);
842
855
  } else if (props.precompute === undefined) {
843
- // Only reset to undefined if precompute is explicitly undefined
844
856
  setCode(undefined);
845
857
  }
846
- }, [props.precompute]);
858
+ }
847
859
 
848
860
  // State to store processed globalsCode to avoid duplicate loading
849
861
  const [processedGlobalsCode, setProcessedGlobalsCode] = React.useState(undefined);
@@ -1049,22 +1061,18 @@ export function CodeHighlighterClient(props) {
1049
1061
  const [isEnhanceAllowed, setIsEnhanceAllowed] = React.useState(enhanceAfter === 'init' || enhanceAfter === 'hydration' && isHydrated);
1050
1062
  React.useEffect(() => {
1051
1063
  if (enhanceAfter === 'idle') {
1052
- const requestIdleCallback = window.requestIdleCallback ?? setTimeout;
1053
- const cancelIdleCallback = window.cancelIdleCallback ?? clearTimeout;
1054
- const idleRequest = requestIdleCallback(() => {
1055
- setIsEnhanceAllowed(true);
1056
- });
1057
- return () => cancelIdleCallback(idleRequest);
1064
+ return requestIdle(() => setIsEnhanceAllowed(true));
1058
1065
  }
1059
1066
  return undefined;
1060
1067
  }, [enhanceAfter]);
1061
1068
 
1062
- // Update enhance allowed state when hydration completes
1069
+ // Enhance instantly once hydrated, as a non-blocking client transition,
1070
+ // rather than deferring to a scheduled task. (`enhanceAfter: 'idle'` above is
1071
+ // the mode that deliberately keeps the un-enhanced first paint and swaps in
1072
+ // the enhanced tree on a later idle render.)
1063
1073
  React.useEffect(() => {
1064
1074
  if (enhanceAfter === 'hydration' && isHydrated) {
1065
- // we should ensure that each code highlighter is enhanced as a separate task
1066
- // this should run from top to bottom
1067
- yieldToMain().then(() => setIsEnhanceAllowed(true));
1075
+ React.startTransition(() => setIsEnhanceAllowed(true));
1068
1076
  }
1069
1077
  }, [enhanceAfter, isHydrated]);
1070
1078
  const readyForContent = React.useMemo(() => {
@@ -1288,6 +1296,7 @@ export function CodeHighlighterClient(props) {
1288
1296
  // ContentLoading wired the hook; the child's `useCodeFallback` effect sets it
1289
1297
  // again before that runs.
1290
1298
  if (showFallback) {
1299
+ // eslint-disable-next-line react-hooks/refs -- dev-only validation flag; reset during render so the child useCodeFallback effect (fires before this parent's validation effect) can re-set it each time the fallback shows
1291
1300
  hookCalledRef.current = false;
1292
1301
  }
1293
1302
 
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { replaceUrlPrefix } from "../pipeline/loaderUtils/applyUrlPrefix.mjs";
3
+ import { resolveFallbackCritical } from "./resolveFallbackCritical.mjs";
3
4
  import { jsx as _jsx } from "react/jsx-runtime";
4
5
  /**
5
6
  * Assemble the props for the `'use client'` `CodeHighlighterClient` from the
@@ -11,6 +12,16 @@ export function createClientProps(props) {
11
12
  const highlightAfter = props.highlightAfter === 'stream' ? 'init' : props.highlightAfter;
12
13
  const enhanceAfter = props.enhanceAfter === 'stream' ? 'init' : props.enhanceAfter;
13
14
 
15
+ // Resolve the staging `fallbackCritical` on every variant of both carriers before
16
+ // they cross to the client: under `highlightAt: 'init'` (not `collapseToEmpty`)
17
+ // promote it over the plain `fallback` so the first paint is highlighted with no
18
+ // decompression, and always strip it so it never reaches `Content`/`ContentLoading`
19
+ // or bloats the payload. `code` and `precompute` are forwarded separately (the
20
+ // client seeds its `code` state from `precompute`), so both must be resolved.
21
+ const collapseToEmpty = props.collapseToEmpty ?? props.contentProps?.collapseToEmpty ?? false;
22
+ const code = resolveFallbackCritical(props.code, highlightAfter, collapseToEmpty);
23
+ const precompute = resolveFallbackCritical(props.precompute, highlightAfter, collapseToEmpty);
24
+
14
25
  // Rewrite the top-level URL before it leaves the server. The client never
15
26
  // receives `urlPrefix` (and shouldn't deal with `file://` URLs), so any
16
27
  // local URL must be translated to its hosted form here. Variant-level URLs
@@ -20,7 +31,7 @@ export function createClientProps(props) {
20
31
  const url = props.urlPrefix && props.url ? replaceUrlPrefix(props.url, props.urlPrefix) : props.url;
21
32
  const contentProps = {
22
33
  ...props.contentProps,
23
- code: props.code || props.precompute,
34
+ code: code || precompute,
24
35
  components: props.components,
25
36
  name: props.name,
26
37
  slug: props.slug,
@@ -38,8 +49,8 @@ export function createClientProps(props) {
38
49
  };
39
50
  return {
40
51
  url,
41
- code: props.code,
42
- precompute: props.precompute,
52
+ code,
53
+ precompute,
43
54
  components: props.components,
44
55
  variants: props.variants,
45
56
  variant: props.variant,
@@ -65,6 +65,44 @@ export declare function fallbackToText(nodes: FallbackNode[]): string;
65
65
  * nodes (e.g. whitespace text between frames) are preserved in place.
66
66
  */
67
67
  export declare function buildRootFallback(root: HastRoot): FallbackNode[];
68
+ /**
69
+ * Whether a compact `fallback` already carries highlighting — i.e. at least one frame
70
+ * keeps nested token spans (array children) instead of flat plain text. True exactly for
71
+ * the promoted {@link promoteCriticalFallback} form; a plain {@link buildRootFallback} has
72
+ * a string child on every frame. `<Pre>` uses this to defer the decompressing decode ONLY
73
+ * when the first paint is already highlighted, so it never flashes plain → highlighted.
74
+ */
75
+ export declare function fallbackIsHighlighted(fallback: FallbackNode[]): boolean;
76
+ /**
77
+ * The **sparse** highlighted-visible fallback: a map from frame index to the
78
+ * highlighted `FallbackNode` for ONLY the frames visible on the initial collapsed
79
+ * render (`visibleFrames`). Off-screen frames are omitted — they flatten to exactly
80
+ * {@link buildRootFallback}'s plain output, so storing them would just duplicate
81
+ * `fallback` in the precompute. {@link promoteCriticalFallback} splices these back
82
+ * over the plain fallback for `highlightAt: 'init'` (paint highlighted on the first
83
+ * render, zero decompression). Frame indices count `span.frame` children only,
84
+ * matching `getInitialVisibleFrames`.
85
+ *
86
+ * The decoded `root` is shared/read-only; this only reads its frames (the synthetic
87
+ * visible frame reuses the frame's `children` array without mutating it, and
88
+ * `data-lined` is dropped via destructuring, not deletion).
89
+ */
90
+ export declare function buildCriticalFallback(root: HastRoot, visibleFrames: {
91
+ [key: number]: boolean;
92
+ }): {
93
+ [frameIndex: number]: FallbackNode;
94
+ };
95
+ /**
96
+ * Splice a sparse {@link buildCriticalFallback} diff back onto a plain `fallback`,
97
+ * replacing each visible frame (matched by index in document order) with its
98
+ * highlighted node. The result is the full highlighted-visible fallback; its text is
99
+ * byte-identical to `fallback` (the highlight spans only wrap the same characters), so
100
+ * it stays a valid DEFLATE dictionary — asserted by tests. Returns a new array; the
101
+ * input is not mutated.
102
+ */
103
+ export declare function promoteCriticalFallback(fallback: FallbackNode[], critical: {
104
+ [frameIndex: number]: FallbackNode;
105
+ }): FallbackNode[];
68
106
  /**
69
107
  * Redistributes a root fallback (built by `buildRootFallback`) back onto the
70
108
  * frames of a decoded HAST root, setting each frame's `data.fallback` to the
@@ -179,6 +179,17 @@ function collectFrameText(frame) {
179
179
  return out;
180
180
  }
181
181
 
182
+ /**
183
+ * The frame's precomputed plain-text fallback (`frame.data.fallback`, set by
184
+ * `addLineGutters`), falling back to walking the frame's text when absent. This
185
+ * is the exact text the highlighted children render, so it doubles as the
186
+ * DEFLATE dictionary contribution for the frame.
187
+ */
188
+ function framePlainText(frame) {
189
+ const fallbackNodes = frame.data?.fallback;
190
+ return fallbackNodes && fallbackNodes.length > 0 ? fallbackNodes.map(node => node.type === 'text' ? node.value : '').join('') : collectFrameText(frame);
191
+ }
192
+
182
193
  /**
183
194
  * Builds the variant-level root fallback from a final (post-enhancer) HAST
184
195
  * root. Each `span.frame` becomes a compact frame element whose single text
@@ -201,15 +212,13 @@ export function buildRootFallback(root) {
201
212
  dataLined,
202
213
  ...properties
203
214
  } = frame.properties || {};
204
- const fallbackNodes = frame.data?.fallback;
205
- const text = fallbackNodes && fallbackNodes.length > 0 ? fallbackNodes.map(node => node.type === 'text' ? node.value : '').join('') : collectFrameText(frame);
206
215
  syntheticChildren.push({
207
216
  type: 'element',
208
217
  tagName: 'span',
209
218
  properties,
210
219
  children: [{
211
220
  type: 'text',
212
- value: text
221
+ value: framePlainText(frame)
213
222
  }]
214
223
  });
215
224
  } else {
@@ -219,6 +228,90 @@ export function buildRootFallback(root) {
219
228
  return convertChildren(syntheticChildren);
220
229
  }
221
230
 
231
+ /** A frame node in compact `FallbackNode` form — `['span', 'frame', …]`, in any of the
232
+ * 3/4/5-element shapes (`className` is always the second element). */
233
+ function isFrameFallbackNode(node) {
234
+ return Array.isArray(node) && node.length >= 3 && node[0] === 'span' && typeof node[1] === 'string' && node[1].split(' ').includes('frame');
235
+ }
236
+
237
+ /**
238
+ * Whether a compact `fallback` already carries highlighting — i.e. at least one frame
239
+ * keeps nested token spans (array children) instead of flat plain text. True exactly for
240
+ * the promoted {@link promoteCriticalFallback} form; a plain {@link buildRootFallback} has
241
+ * a string child on every frame. `<Pre>` uses this to defer the decompressing decode ONLY
242
+ * when the first paint is already highlighted, so it never flashes plain → highlighted.
243
+ */
244
+ export function fallbackIsHighlighted(fallback) {
245
+ return fallback.some(node => {
246
+ if (!isFrameFallbackNode(node)) {
247
+ return false;
248
+ }
249
+ // `children` is the last tuple slot, except the 5-element form keeps `extra` last
250
+ // (matches `nodeText`). An array means nested token spans, i.e. highlighted.
251
+ const children = node.length === 5 ? node[3] : node[node.length - 1];
252
+ return Array.isArray(children);
253
+ });
254
+ }
255
+
256
+ /**
257
+ * The **sparse** highlighted-visible fallback: a map from frame index to the
258
+ * highlighted `FallbackNode` for ONLY the frames visible on the initial collapsed
259
+ * render (`visibleFrames`). Off-screen frames are omitted — they flatten to exactly
260
+ * {@link buildRootFallback}'s plain output, so storing them would just duplicate
261
+ * `fallback` in the precompute. {@link promoteCriticalFallback} splices these back
262
+ * over the plain fallback for `highlightAt: 'init'` (paint highlighted on the first
263
+ * render, zero decompression). Frame indices count `span.frame` children only,
264
+ * matching `getInitialVisibleFrames`.
265
+ *
266
+ * The decoded `root` is shared/read-only; this only reads its frames (the synthetic
267
+ * visible frame reuses the frame's `children` array without mutating it, and
268
+ * `data-lined` is dropped via destructuring, not deletion).
269
+ */
270
+ export function buildCriticalFallback(root, visibleFrames) {
271
+ const critical = {};
272
+ let frameIndex = 0;
273
+ for (const child of root.children) {
274
+ if (child.type === 'element' && isFrameSpan(child)) {
275
+ if (visibleFrames[frameIndex]) {
276
+ const frame = child;
277
+ const {
278
+ dataLined,
279
+ ...properties
280
+ } = frame.properties || {};
281
+ const [node] = convertChildren([{
282
+ type: 'element',
283
+ tagName: 'span',
284
+ properties,
285
+ children: frame.children
286
+ }]);
287
+ critical[frameIndex] = node;
288
+ }
289
+ frameIndex += 1;
290
+ }
291
+ }
292
+ return critical;
293
+ }
294
+
295
+ /**
296
+ * Splice a sparse {@link buildCriticalFallback} diff back onto a plain `fallback`,
297
+ * replacing each visible frame (matched by index in document order) with its
298
+ * highlighted node. The result is the full highlighted-visible fallback; its text is
299
+ * byte-identical to `fallback` (the highlight spans only wrap the same characters), so
300
+ * it stays a valid DEFLATE dictionary — asserted by tests. Returns a new array; the
301
+ * input is not mutated.
302
+ */
303
+ export function promoteCriticalFallback(fallback, critical) {
304
+ let frameIndex = 0;
305
+ return fallback.map(node => {
306
+ if (!isFrameFallbackNode(node)) {
307
+ return node;
308
+ }
309
+ const replacement = critical[frameIndex];
310
+ frameIndex += 1;
311
+ return replacement ?? node;
312
+ });
313
+ }
314
+
222
315
  /**
223
316
  * Redistributes a root fallback (built by `buildRootFallback`) back onto the
224
317
  * frames of a decoded HAST root, setting each frame's `data.fallback` to the
@@ -8,6 +8,17 @@ export interface PrepareInitialSourceOptions<T extends {}> extends CodeHighlight
8
8
  initialSource: VariantSource;
9
9
  initialExtraFiles?: VariantExtraFiles;
10
10
  ContentLoading: React.ComponentType<ContentLoadingProps<T>>;
11
+ /**
12
+ * Whether to DEFLATE-compress the consolidated residual fallbacks. This only
13
+ * shrinks the server→client serialized payload, so it pays off only when the
14
+ * result actually crosses that wire (a server render). On the client there is no
15
+ * wire — compressing here just to have `CodeHighlighterClient` decompress it right
16
+ * back is a wasted round-trip — so the isomorphic `CodeHighlighter` entry passes
17
+ * `typeof window === 'undefined'`. Defaults to `true` for the server-only loaders
18
+ * (and tests) that always produce wire output; when `false`, the fallbacks stay
19
+ * inline on `codeForClient`.
20
+ */
21
+ compressResidual?: boolean;
11
22
  }
12
23
  export interface PreparedInitialSource {
13
24
  /** The pre-rendered loading fallback (`<ContentLoading />`). */
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { buildStringFallback } from "./buildStringFallback.mjs";
3
+ import { resolveFallbackCritical } from "./resolveFallbackCritical.mjs";
3
4
  import { codeToFallbackProps, stripFallbackHastsFromCode } from "./codeToFallbackProps.mjs";
4
5
  import { collapseRenderedFallbacks, compressResidualFallbacks, extractResidualFallbacks, mergeResidualFallbacks, residualDictionaryText } from "./fallbackCompression.mjs";
5
6
  import { replaceUrlPrefix } from "../pipeline/loaderUtils/applyUrlPrefix.mjs";
@@ -19,7 +20,7 @@ export function prepareInitialSource(props) {
19
20
  slug,
20
21
  name,
21
22
  initialVariant,
22
- code,
23
+ code: initialCode,
23
24
  initialFilename,
24
25
  fallbackUsesExtraFiles,
25
26
  fallbackUsesAllVariants,
@@ -31,6 +32,18 @@ export function prepareInitialSource(props) {
31
32
  const collapseToEmptyEnabled = collapseToEmpty === true;
32
33
  const initialExpandedEnabled = initialExpanded === true;
33
34
 
35
+ // Fold each variant's staging `fallbackCritical` into its plain `fallback` up front
36
+ // (under `highlightAt: 'init'`, not `collapseToEmpty`), then strip it. The hoisted
37
+ // loading fallback is therefore already highlighted-visible — so the first paint is
38
+ // highlighted with no decompression — while the rest of this function (strip, hoist,
39
+ // window, compress) operates on a single `fallback` field with no awareness of the
40
+ // staging companion.
41
+ // Normalize `'stream'` → `'init'` before resolving, mirroring `createClientProps`: stream
42
+ // mode wants the loading fallback highlighted on first paint too (the client highlightAfter
43
+ // type even collapses 'stream' into 'init').
44
+ const highlightAfter = props.highlightAfter === 'stream' ? 'init' : props.highlightAfter;
45
+ const code = resolveFallbackCritical(initialCode, highlightAfter, collapseToEmptyEnabled) ?? initialCode;
46
+
34
47
  // When the block starts expanded, the loading UI needs the full content, so
35
48
  // the `fallbackCollapsed` window optimization (paint only the collapsed slice,
36
49
  // defer the rest) doesn't apply — treat it as off everywhere below.
@@ -125,6 +138,41 @@ export function prepareInitialSource(props) {
125
138
  focusedLines: collapseToEmptyEnabled ? 0 : counts.focusedLines,
126
139
  collapsible: collapseToEmptyEnabled ? true : counts.collapsible
127
140
  };
141
+ // Carry the RAW counts onto the wire `code` too, so the content component's base
142
+ // render (before `hast` decodes) reads the same `collapsible`/window metadata the
143
+ // loading fallback got. A windowed inline string — or a `hastCompressed` source
144
+ // whose dictionary was stripped for residual compression — otherwise has no stored
145
+ // counts on `codeForClient`, so `getVariantFileLineCounts` falls back to the raw
146
+ // (non-collapsible) string count and `data-collapsible` flashes off until the
147
+ // source highlights. `<Pre>` re-applies collapse-to-empty from the stored counts.
148
+ const wireVariant = strippedCode[variantName];
149
+ if (wireVariant && typeof wireVariant === 'object') {
150
+ const stored = {
151
+ totalLines: counts.totalLines,
152
+ focusedLines: counts.focusedLines,
153
+ collapsible: counts.collapsible
154
+ };
155
+ if (wireVariant.fileName === file.fileName) {
156
+ strippedCode[variantName] = {
157
+ ...wireVariant,
158
+ ...stored
159
+ };
160
+ } else {
161
+ const extra = wireVariant.extraFiles?.[file.fileName];
162
+ if (extra && typeof extra === 'object') {
163
+ strippedCode[variantName] = {
164
+ ...wireVariant,
165
+ extraFiles: {
166
+ ...wireVariant.extraFiles,
167
+ [file.fileName]: {
168
+ ...extra,
169
+ ...stored
170
+ }
171
+ }
172
+ };
173
+ }
174
+ }
175
+ }
128
176
  }
129
177
  }
130
178
  }
@@ -182,13 +230,24 @@ export function prepareInitialSource(props) {
182
230
  // onto the code so its consumers (render and the swap line-count classifier)
183
231
  // read the dictionary off `code` regardless of which variant is active. When
184
232
  // there's nothing worth compressing, keep the plain inline fallbacks unchanged.
185
- const {
186
- wireCode,
187
- residual
188
- } = extractResidualFallbacks(strippedCode);
189
- const fullResidual = effectiveFallbackCollapsed ? mergeResidualFallbacks(residual, allFallbackHasts) : residual;
190
- const residualFallbacks = compressResidualFallbacks(fullResidual, residualDictionaryText(contentLoadingHasts));
191
- const codeForClient = residualFallbacks ? wireCode : strippedCode;
233
+ // Compressing the residual only shrinks the server→client wire, so skip it entirely
234
+ // on the client (`compressResidual === false`): keep the fallbacks INLINE on
235
+ // `codeForClient` so `CodeHighlighterClient` reads them directly — no
236
+ // compress→decompress round-trip, and nothing to recompute on every re-render. This
237
+ // is the same shape the `strippedCode` branch below already produces for a payload
238
+ // too small to be worth compressing.
239
+ const compressResidual = props.compressResidual ?? true;
240
+ let residualFallbacks;
241
+ let codeForClient = strippedCode;
242
+ if (compressResidual) {
243
+ const {
244
+ wireCode,
245
+ residual
246
+ } = extractResidualFallbacks(strippedCode);
247
+ const fullResidual = effectiveFallbackCollapsed ? mergeResidualFallbacks(residual, allFallbackHasts) : residual;
248
+ residualFallbacks = compressResidualFallbacks(fullResidual, residualDictionaryText(contentLoadingHasts));
249
+ codeForClient = residualFallbacks ? wireCode : strippedCode;
250
+ }
192
251
 
193
252
  // Get the component for the selected variant
194
253
  const component = props.components?.[initialVariant];
@@ -0,0 +1,23 @@
1
+ import type { Code, CodeHighlighterBaseProps } from "./types.mjs";
2
+ type HighlightAfter = CodeHighlighterBaseProps<{}>['highlightAfter'];
3
+ /**
4
+ * Resolve the staging `fallbackCritical` field at a server→client (or client-load)
5
+ * boundary, returning a clone of `code` ready to cross to the client:
6
+ *
7
+ * - **Promote** — under `highlightAt: 'init'` (and not `collapseToEmpty`), each
8
+ * variant whose `fallbackCritical` *and* plain `fallback` are both present has the
9
+ * sparse `fallbackCritical` diff spliced over its `fallback` (`promoteCriticalFallback`)
10
+ * — the visible frames become highlighted, the rest stay plain — so the first paint is
11
+ * highlighted with no client-side decompression. The promoted `fallback` stays a valid
12
+ * DEFLATE dictionary because the spliced frames have byte-identical text. With
13
+ * `collapseToEmpty`, the correct critical fallback is all-plain (no frame is visible),
14
+ * which already equals `fallback` — so promotion is skipped.
15
+ * - **Strip** — `fallbackCritical` is always deleted (even when not promoting, and
16
+ * even for non-`init` modes), so it never crosses to the `Content`/`ContentLoading`
17
+ * components or bloats the serialized payload.
18
+ *
19
+ * Returns `code` unchanged (same reference) when no variant carries `fallbackCritical`,
20
+ * and clones only the variants that do.
21
+ */
22
+ export declare function resolveFallbackCritical(code: Code | undefined, highlightAfter: HighlightAfter, collapseToEmpty: boolean): Code | undefined;
23
+ export {};
@@ -0,0 +1,44 @@
1
+ import { promoteCriticalFallback } from "./fallbackFormat.mjs";
2
+ /**
3
+ * Resolve the staging `fallbackCritical` field at a server→client (or client-load)
4
+ * boundary, returning a clone of `code` ready to cross to the client:
5
+ *
6
+ * - **Promote** — under `highlightAt: 'init'` (and not `collapseToEmpty`), each
7
+ * variant whose `fallbackCritical` *and* plain `fallback` are both present has the
8
+ * sparse `fallbackCritical` diff spliced over its `fallback` (`promoteCriticalFallback`)
9
+ * — the visible frames become highlighted, the rest stay plain — so the first paint is
10
+ * highlighted with no client-side decompression. The promoted `fallback` stays a valid
11
+ * DEFLATE dictionary because the spliced frames have byte-identical text. With
12
+ * `collapseToEmpty`, the correct critical fallback is all-plain (no frame is visible),
13
+ * which already equals `fallback` — so promotion is skipped.
14
+ * - **Strip** — `fallbackCritical` is always deleted (even when not promoting, and
15
+ * even for non-`init` modes), so it never crosses to the `Content`/`ContentLoading`
16
+ * components or bloats the serialized payload.
17
+ *
18
+ * Returns `code` unchanged (same reference) when no variant carries `fallbackCritical`,
19
+ * and clones only the variants that do.
20
+ */
21
+ export function resolveFallbackCritical(code, highlightAfter, collapseToEmpty) {
22
+ if (!code) {
23
+ return code;
24
+ }
25
+ const promote = highlightAfter === 'init' && !collapseToEmpty;
26
+ let changed = false;
27
+ const resolved = {};
28
+ for (const [key, variant] of Object.entries(code)) {
29
+ if (!variant || typeof variant === 'string' || variant.fallbackCritical === undefined) {
30
+ resolved[key] = variant;
31
+ continue;
32
+ }
33
+ const {
34
+ fallbackCritical,
35
+ ...rest
36
+ } = variant;
37
+ resolved[key] = promote && rest.fallback ? {
38
+ ...rest,
39
+ fallback: promoteCriticalFallback(rest.fallback, fallbackCritical)
40
+ } : rest;
41
+ changed = true;
42
+ }
43
+ return changed ? resolved : code;
44
+ }