@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.
- package/CodeHighlighter/CodeHighlighter.mjs +11 -2
- package/CodeHighlighter/CodeHighlighterClient.mjs +60 -51
- package/CodeHighlighter/createClientProps.mjs +14 -3
- package/CodeHighlighter/fallbackFormat.d.mts +38 -0
- package/CodeHighlighter/fallbackFormat.mjs +96 -3
- package/CodeHighlighter/prepareInitialSource.d.mts +11 -0
- package/CodeHighlighter/prepareInitialSource.mjs +67 -8
- package/CodeHighlighter/resolveFallbackCritical.d.mts +23 -0
- package/CodeHighlighter/resolveFallbackCritical.mjs +44 -0
- package/CodeHighlighter/types.d.mts +16 -0
- package/CoordinatedLazy/useChunk.mjs +10 -8
- package/CoordinatedLazy/useCoordinatedSwap.mjs +1 -0
- package/abstractCreateTypes/TypeCode.mjs +12 -11
- package/abstractCreateTypes/typesToJsx.mjs +13 -8
- package/package.json +2 -2
- package/pipeline/loadIsomorphicCodeVariant/loadIsomorphicCodeVariant.mjs +41 -1
- package/pipeline/parseSource/frameVisibility.d.mts +17 -1
- package/pipeline/parseSource/frameVisibility.mjs +53 -0
- package/useCode/EditableEngine.mjs +15 -5
- package/useCode/Pre.mjs +43 -48
- package/useCode/SourceEditingEngine.mjs +29 -8
- package/useCode/useCode.mjs +11 -3
- package/useCode/{liveEditingBugs.browser.mjs → useEditable.integration.browser.mjs} +114 -69
- package/useCode/useFileNavigation.mjs +20 -16
- package/useCode/useTransformManagement.mjs +13 -5
- package/useCode/useUIState.mjs +6 -6
- package/useCode/useVariantSelection.mjs +20 -6
- package/useCoordinated/coordinatePreference.mjs +4 -25
- package/useCoordinated/scheduleTasks.d.mts +23 -0
- package/useCoordinated/scheduleTasks.mjs +45 -0
- package/useCoordinated/useCoordinated.mjs +33 -6
- package/useStream/useStream.mjs +2 -4
- package/useStream/useStreamController.mjs +6 -1
- /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(
|
|
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(
|
|
302
|
+
setCode(resolvedResultCode);
|
|
286
303
|
}
|
|
287
|
-
return
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
511
|
-
// the last computation — falling back
|
|
512
|
-
// currently-displayed HAST for a frame
|
|
513
|
-
// Staleness is signalled via
|
|
514
|
-
// gates (e.g.
|
|
515
|
-
//
|
|
516
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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:
|
|
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
|
|
42
|
-
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:
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
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
|
+
}
|