@mui/internal-docs-infra 0.12.1-canary.5 → 0.12.1-canary.7

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 (162) hide show
  1. package/CodeControllerContext/CodeControllerContext.d.mts +41 -6
  2. package/CodeControllerContext/CodeControllerContext.mjs +14 -0
  3. package/CodeHighlighter/CodeHighlighterClient.mjs +112 -15
  4. package/CodeHighlighter/CodeHighlighterContext.d.mts +6 -0
  5. package/CodeHighlighter/createClientProps.mjs +3 -0
  6. package/CodeHighlighter/parseControlledCode.d.mts +13 -4
  7. package/CodeHighlighter/parseControlledCode.mjs +41 -49
  8. package/CodeHighlighter/types.d.mts +21 -0
  9. package/CodeHighlighter/useGrammarsReady.mjs +44 -9
  10. package/CodeProvider/createParseSourceWorkerClient.d.mts +6 -0
  11. package/CodeProvider/createParseSourceWorkerClient.mjs +83 -15
  12. package/CodeProvider/useCodeProviderValue.mjs +50 -4
  13. package/abstractCreateDemo/abstractCreateDemo.d.mts +14 -0
  14. package/abstractCreateDemo/abstractCreateDemo.mjs +8 -0
  15. package/abstractCreateDemoClient/abstractCreateDemoClient.mjs +4 -0
  16. package/cli/loadNextConfig.d.mts +9 -0
  17. package/cli/loadNextConfig.mjs +11 -2
  18. package/cli/runValidate.mjs +15 -7
  19. package/cli/validateWorker.d.mts +1 -0
  20. package/cli/validateWorker.mjs +2 -1
  21. package/package.json +32 -3
  22. package/pipeline/cacheUtils/constants.d.mts +5 -0
  23. package/pipeline/cacheUtils/constants.mjs +5 -0
  24. package/pipeline/cacheUtils/hashCacheContent.d.mts +5 -0
  25. package/pipeline/cacheUtils/hashCacheContent.mjs +9 -0
  26. package/pipeline/cacheUtils/index.d.mts +9 -0
  27. package/pipeline/cacheUtils/index.mjs +7 -0
  28. package/pipeline/cacheUtils/loadFileCache.d.mts +13 -0
  29. package/pipeline/cacheUtils/loadFileCache.mjs +20 -0
  30. package/pipeline/cacheUtils/loadFileCacheEntry.d.mts +13 -0
  31. package/pipeline/cacheUtils/loadFileCacheEntry.mjs +47 -0
  32. package/pipeline/cacheUtils/resolveCachePath.d.mts +16 -0
  33. package/pipeline/cacheUtils/resolveCachePath.mjs +24 -0
  34. package/pipeline/cacheUtils/saveFileCache.d.mts +17 -0
  35. package/pipeline/cacheUtils/saveFileCache.mjs +54 -0
  36. package/pipeline/cacheUtils/types.d.mts +23 -0
  37. package/pipeline/cacheUtils/types.mjs +1 -0
  38. package/pipeline/cacheUtils/withFileCache.d.mts +40 -0
  39. package/pipeline/cacheUtils/withFileCache.mjs +81 -0
  40. package/pipeline/loadIsomorphicCodeSource/loadIsomorphicCodeSource.mjs +1 -1
  41. package/pipeline/loadPrecomputedSitemap/loadPrecomputedSitemap.d.mts +6 -0
  42. package/pipeline/loadPrecomputedSitemap/loadPrecomputedSitemap.mjs +2 -1
  43. package/pipeline/loadPrecomputedTypes/loadPrecomputedTypes.d.mts +6 -0
  44. package/pipeline/loadPrecomputedTypes/loadPrecomputedTypes.mjs +5 -1
  45. package/pipeline/loadServerPageIndex/enrichPageIndex.d.mts +15 -0
  46. package/pipeline/loadServerPageIndex/enrichPageIndex.mjs +50 -0
  47. package/pipeline/loadServerPageIndex/extractPrefixAndTitle.d.mts +19 -0
  48. package/pipeline/loadServerPageIndex/extractPrefixAndTitle.mjs +80 -0
  49. package/pipeline/loadServerPageIndex/index.d.mts +5 -2
  50. package/pipeline/loadServerPageIndex/index.mjs +4 -1
  51. package/pipeline/loadServerPageIndex/loadServerPageIndex.d.mts +13 -22
  52. package/pipeline/loadServerPageIndex/loadServerPageIndex.mjs +32 -124
  53. package/pipeline/loadServerPageIndex/resolvePageIndexCacheKey.d.mts +13 -0
  54. package/pipeline/loadServerPageIndex/resolvePageIndexCacheKey.mjs +23 -0
  55. package/pipeline/loadServerTypes/loadServerTypes.mjs +67 -7
  56. package/pipeline/loadServerTypesMeta/loadServerTypesMeta.mjs +1 -1
  57. package/pipeline/loadServerTypesText/loadServerTypesText.d.mts +15 -1
  58. package/pipeline/loadServerTypesText/loadServerTypesText.mjs +30 -8
  59. package/pipeline/loadServerTypesText/normalizeTypesSourceDataForCache.d.mts +2 -0
  60. package/pipeline/loadServerTypesText/normalizeTypesSourceDataForCache.mjs +144 -0
  61. package/pipeline/loadServerTypesText/resolveTypesCacheKey.d.mts +19 -0
  62. package/pipeline/loadServerTypesText/resolveTypesCacheKey.mjs +29 -0
  63. package/pipeline/loaderUtils/parseImportsAndComments.d.mts +5 -3
  64. package/pipeline/loaderUtils/parseImportsAndComments.mjs +284 -40
  65. package/pipeline/parseCreateFactoryCall/parseCreateFactoryCall.mjs +2 -3
  66. package/pipeline/parseSource/detectFileTypes.d.mts +16 -0
  67. package/pipeline/parseSource/detectFileTypes.mjs +44 -0
  68. package/pipeline/syncPageIndex/mergeMetadataMarkdown.d.mts +24 -0
  69. package/pipeline/syncPageIndex/mergeMetadataMarkdown.mjs +79 -49
  70. package/pipeline/syncPageIndex/metadataToMarkdown.d.mts +14 -0
  71. package/pipeline/syncPageIndex/metadataToMarkdown.mjs +32 -10
  72. package/pipeline/syncPageIndex/syncPageIndex.d.mts +12 -0
  73. package/pipeline/syncPageIndex/syncPageIndex.mjs +72 -12
  74. package/pipeline/syncTypes/syncTypes.d.mts +7 -0
  75. package/pipeline/syncTypes/syncTypes.mjs +31 -1
  76. package/pipeline/transformHtmlCodeBlock/transformHtmlCodeBlock.mjs +1 -1
  77. package/pipeline/transformMarkdownCode/transformMarkdownCode.mjs +25 -15
  78. package/pipeline/transformMarkdownMetadata/transformMarkdownMetadata.mjs +3 -0
  79. package/pipeline/transformMarkdownMetadata/types.d.mts +7 -0
  80. package/useCode/Pre.d.mts +8 -1
  81. package/useCode/Pre.mjs +4 -3
  82. package/useCode/useCode.d.mts +16 -0
  83. package/useCode/useCode.mjs +10 -0
  84. package/useCode/useFileNavigation.d.mts +3 -0
  85. package/useCode/useFileNavigation.mjs +3 -1
  86. package/useCode/useSourceEditing.mjs +44 -9
  87. package/useCode/useSourceEnhancing.mjs +41 -9
  88. package/useCode/useUIState.d.mts +4 -0
  89. package/useCode/useUIState.mjs +8 -1
  90. package/useCrossTabState/index.d.mts +2 -0
  91. package/useCrossTabState/index.mjs +2 -0
  92. package/useCrossTabState/useCrossTabMirror.d.mts +26 -0
  93. package/useCrossTabState/useCrossTabMirror.mjs +108 -0
  94. package/useCrossTabState/useCrossTabState.d.mts +21 -0
  95. package/useCrossTabState/useCrossTabState.mjs +31 -0
  96. package/useDemo/useDemo.d.mts +3 -0
  97. package/useDemo/useDemo.mjs +5 -0
  98. package/useDemoController/BuildEngine.d.mts +2 -0
  99. package/useDemoController/BuildEngine.mjs +10 -0
  100. package/useDemoController/DemoRunner.d.mts +34 -0
  101. package/useDemoController/DemoRunner.mjs +83 -0
  102. package/useDemoController/ErrorBoundary.d.mts +34 -0
  103. package/useDemoController/ErrorBoundary.mjs +49 -0
  104. package/useDemoController/Runner.d.mts +44 -0
  105. package/useDemoController/Runner.mjs +71 -0
  106. package/useDemoController/absolutizeImports.d.mts +13 -0
  107. package/useDemoController/absolutizeImports.mjs +73 -0
  108. package/useDemoController/buildScope.d.mts +48 -0
  109. package/useDemoController/buildScope.mjs +268 -0
  110. package/useDemoController/collectSources.d.mts +67 -0
  111. package/useDemoController/collectSources.mjs +92 -0
  112. package/useDemoController/compileCssModule.d.mts +64 -0
  113. package/useDemoController/compileCssModule.mjs +40 -0
  114. package/useDemoController/compileCssWithPostcss.d.mts +17 -0
  115. package/useDemoController/compileCssWithPostcss.mjs +89 -0
  116. package/useDemoController/compileModule.d.mts +14 -0
  117. package/useDemoController/compileModule.mjs +17 -0
  118. package/useDemoController/constants.d.mts +13 -0
  119. package/useDemoController/constants.mjs +13 -0
  120. package/useDemoController/createRequire.d.mts +10 -0
  121. package/useDemoController/createRequire.mjs +17 -0
  122. package/useDemoController/createTranspileWorkerClient.d.mts +27 -0
  123. package/useDemoController/createTranspileWorkerClient.mjs +139 -0
  124. package/useDemoController/cssModules.d.mts +47 -0
  125. package/useDemoController/currentBrowserTarget.d.mts +16 -0
  126. package/useDemoController/currentBrowserTarget.mjs +36 -0
  127. package/useDemoController/evalCode.d.mts +12 -0
  128. package/useDemoController/evalCode.mjs +35 -0
  129. package/useDemoController/generateCssModuleSource.d.mts +11 -0
  130. package/useDemoController/generateCssModuleSource.mjs +55 -0
  131. package/useDemoController/generateElement.d.mts +21 -0
  132. package/useDemoController/generateElement.mjs +26 -0
  133. package/useDemoController/hashString.d.mts +11 -0
  134. package/useDemoController/hashString.mjs +20 -0
  135. package/useDemoController/importCode.d.mts +11 -0
  136. package/useDemoController/importCode.mjs +15 -0
  137. package/useDemoController/index.d.mts +1 -0
  138. package/useDemoController/index.mjs +1 -0
  139. package/useDemoController/instantiateElement.d.mts +24 -0
  140. package/useDemoController/instantiateElement.mjs +57 -0
  141. package/useDemoController/instantiateModule.d.mts +17 -0
  142. package/useDemoController/instantiateModule.mjs +24 -0
  143. package/useDemoController/resolveCssImports.d.mts +27 -0
  144. package/useDemoController/resolveCssImports.mjs +79 -0
  145. package/useDemoController/transformCode.d.mts +14 -0
  146. package/useDemoController/transformCode.mjs +42 -0
  147. package/useDemoController/transpileClientSingleton.d.mts +10 -0
  148. package/useDemoController/transpileClientSingleton.mjs +83 -0
  149. package/useDemoController/transpileSource.d.mts +29 -0
  150. package/useDemoController/transpileSource.mjs +36 -0
  151. package/useDemoController/transpileWorker.d.mts +14 -0
  152. package/useDemoController/transpileWorker.mjs +41 -0
  153. package/useDemoController/types.d.mts +30 -0
  154. package/useDemoController/types.mjs +1 -0
  155. package/useDemoController/useDemoController.d.mts +65 -0
  156. package/useDemoController/useDemoController.mjs +184 -0
  157. package/useDemoController/useRunner.d.mts +53 -0
  158. package/useDemoController/useRunner.mjs +118 -0
  159. package/useDemoController/useVariantBuilds.d.mts +31 -0
  160. package/useDemoController/useVariantBuilds.mjs +188 -0
  161. package/withDocsInfra/withDocsInfra.d.mts +14 -0
  162. package/withDocsInfra/withDocsInfra.mjs +25 -11
@@ -43,6 +43,12 @@ export interface CodeControllerContext {
43
43
  * e.g. `{ variantA: {}, variantB: {} }`.
44
44
  */
45
45
  components?: Record<string, React.ReactNode> | undefined;
46
+ /**
47
+ * Current runtime error message per variant key (or `null` when the variant
48
+ * rendered cleanly), as reported by the preview components. Demos surface the
49
+ * selected variant's error via `useDemo().error`.
50
+ */
51
+ errors?: Record<string, string | null> | undefined;
46
52
  /**
47
53
  * Additional source enhancers to apply to parsed HAST sources.
48
54
  * These are merged with enhancers from CodeProvider and useCode opts.
@@ -52,13 +58,38 @@ export interface CodeControllerContext {
52
58
  * Called once when a block in this controller's scope first activates for
53
59
  * editing — immediately for `editActivation: 'eager'`, or on first engagement
54
60
  * (hover / focus / click) for `'interaction'`. Lets the host react to "editing
55
- * has begun" (e.g. fetch the editable source, light up UI). `CodeHighlighter`
56
- * separately warms its own live-editing dependencies (engine, grammars, worker)
57
- * at the same moment, so a host that only wants the default behavior can leave
58
- * this unset.
61
+ * has begun" (e.g. fetch the editable source, light up UI, or preload its live
62
+ * runtime). `deps` reports which file kinds the block spans — `js` when it has any
63
+ * JS/TS/JSX/TSX/MJS file, `css` when it has any CSS file so the host can warm only
64
+ * the engine chunks it will need. `CodeHighlighter` separately warms its own
65
+ * live-editing dependencies (engine, grammars, worker) at the same moment, so a host
66
+ * that only wants the default behavior can leave this unset.
59
67
  */
60
- onActivate?: () => void;
68
+ onActivate?: (deps: {
69
+ js: boolean;
70
+ css: boolean;
71
+ }) => void;
61
72
  }
73
+ /**
74
+ * Props a code-controller component receives: `children` to wrap and the optional
75
+ * `url` identifying the demo, plus any custom props `T` (typically a specific
76
+ * controller's options). Mirrors `ContentProps` — a small base extended by `T`.
77
+ */
78
+ export type CodeControllerProps<T extends {} = {}> = {
79
+ children: React.ReactNode;
80
+ /**
81
+ * The demo's url — identifies this controller, e.g. as the per-demo key for
82
+ * cross-tab sync. Supplied by the demo factory, or passed explicitly.
83
+ */
84
+ url?: string;
85
+ } & T;
86
+ /**
87
+ * A code-controller component — the counterpart to `DemoContent`. It wraps `children`
88
+ * in a {@link CodeControllerContext} provider, typically sourcing the value from a hook
89
+ * such as `useDemoController`. Parameterize with `T` for the controller's own props
90
+ * (e.g. `CodeController<UseDemoControllerOptions>`).
91
+ */
92
+ export type CodeController<T extends {} = {}> = React.ComponentType<CodeControllerProps<T>>;
62
93
  export declare const CodeControllerContext: React.Context<CodeControllerContext | undefined>;
63
94
  /**
64
95
  * Hook to access controlled code state and setters. This is useful for custom
@@ -81,6 +112,10 @@ export declare function useControlledCode(): {
81
112
  setCode: React.Dispatch<React.SetStateAction<ControlledCode | undefined>> | undefined;
82
113
  setSelection: React.Dispatch<React.SetStateAction<Selection>> | undefined;
83
114
  components: Record<string, React.ReactNode> | undefined;
115
+ errors: Record<string, string | null> | undefined;
84
116
  sourceEnhancers: SourceEnhancers | undefined;
85
- onActivate: (() => void) | undefined;
117
+ onActivate: ((deps: {
118
+ js: boolean;
119
+ css: boolean;
120
+ }) => void) | undefined;
86
121
  };
@@ -10,6 +10,19 @@ import * as React from 'react';
10
10
  * event fires.
11
11
  */
12
12
 
13
+ /**
14
+ * Props a code-controller component receives: `children` to wrap and the optional
15
+ * `url` identifying the demo, plus any custom props `T` (typically a specific
16
+ * controller's options). Mirrors `ContentProps` — a small base extended by `T`.
17
+ */
18
+
19
+ /**
20
+ * A code-controller component — the counterpart to `DemoContent`. It wraps `children`
21
+ * in a {@link CodeControllerContext} provider, typically sourcing the value from a hook
22
+ * such as `useDemoController`. Parameterize with `T` for the controller's own props
23
+ * (e.g. `CodeController<UseDemoControllerOptions>`).
24
+ */
25
+
13
26
  export const CodeControllerContext = /*#__PURE__*/React.createContext(undefined);
14
27
 
15
28
  /**
@@ -36,6 +49,7 @@ export function useControlledCode() {
36
49
  setCode: context?.setCode,
37
50
  setSelection: context?.setSelection,
38
51
  components: context?.components,
52
+ errors: context?.errors,
39
53
  sourceEnhancers: context?.sourceEnhancers,
40
54
  onActivate: context?.onActivate
41
55
  };
@@ -18,6 +18,7 @@ import { useSpeculativeUseCodePreload } from "./useSpeculativeUseCodePreload.mjs
18
18
  import { useSpeculativeGrammarPreload } from "./useSpeculativeGrammarPreload.mjs";
19
19
  import { useGrammarsReady } from "./useGrammarsReady.mjs";
20
20
  import { detectGrammarScopes } from "../pipeline/parseSource/detectGrammarScopes.mjs";
21
+ import { detectFileTypes } from "../pipeline/parseSource/detectFileTypes.mjs";
21
22
  import { useChunk } from "../CoordinatedLazy/useChunk.mjs";
22
23
  import { useCoordinatedSwap } from "../CoordinatedLazy/useCoordinatedSwap.mjs";
23
24
  import { CoordinatedFallbackContext } from "../CoordinatedLazy/CoordinatedFallbackContext.mjs";
@@ -27,12 +28,40 @@ import * as Errors from "./errors.mjs";
27
28
  import { jsx as _jsx } from "react/jsx-runtime";
28
29
  const DEBUG = false; // Set to true for debugging purposes
29
30
 
31
+ // Safety-net deadline (ms) for the default `'idle'` highlight/enhance swaps. They
32
+ // defer the swap to `requestIdleCallback`, which a busy main thread can starve
33
+ // INDEFINITELY — leaving code stuck at its un-highlighted first paint until a full
34
+ // reload. The `timeout` guarantees the swap still runs even if the browser never
35
+ // goes idle (idle normally wins long before this deadline).
36
+ const IDLE_SWAP_TIMEOUT_MS = 10_000;
37
+
38
+ // Safety-net deadline (ms) for the async transform-deltas pipeline. If it neither
39
+ // resolves nor rejects (a stalled worker / hung delta computation), the block would
40
+ // otherwise wait on `waitingForTransformedCode` forever — code stuck un-highlighted
41
+ // until a reload. On timeout we publish the un-delta'd parsed HAST so the gate drops.
42
+ const TRANSFORM_PIPELINE_TIMEOUT_MS = 10_000;
43
+
30
44
  // `useChunk` is the chunk loader/renderer, but here we use only its loading
31
45
  // engine (load-when-enabled + abort + `refresh()` with stale-while-revalidate),
32
46
  // so the content component is an unused placeholder.
33
47
  function NoopChunkContent() {
34
48
  return null;
35
49
  }
50
+
51
+ /**
52
+ * Inject a build-time `fallback` into a bridged live-preview node. The controller's
53
+ * components are erased to `ReactNode` across the context, so the type argument on
54
+ * `isValidElement` re-narrows the node to one that accepts `fallback` — no cast. A
55
+ * non-element node (nothing to clone) is returned untouched.
56
+ */
57
+ function injectFallback(node, fallback) {
58
+ if (! /*#__PURE__*/React.isValidElement(node)) {
59
+ return node;
60
+ }
61
+ return /*#__PURE__*/React.cloneElement(node, {
62
+ fallback
63
+ });
64
+ }
36
65
  function useInitialData({
37
66
  variants,
38
67
  variantName,
@@ -337,7 +366,9 @@ function useCodeParsing({
337
366
  const [isHighlightAllowed, setIsHighlightAllowed] = React.useState(highlightAfter === 'init' || highlightAfter === 'hydration' && isHydrated);
338
367
  React.useEffect(() => {
339
368
  if (highlightAfter === 'idle') {
340
- return requestIdle(() => setIsHighlightAllowed(true));
369
+ return requestIdle(() => setIsHighlightAllowed(true), {
370
+ timeout: IDLE_SWAP_TIMEOUT_MS
371
+ });
341
372
  }
342
373
  return undefined;
343
374
  }, [highlightAfter]);
@@ -483,9 +514,30 @@ function useCodeTransforms({
483
514
  // so this effect never publishes a synchronous pass-through state.
484
515
  React.useEffect(() => {
485
516
  if (!parsedCode || !sourceParser || !computeHastDeltasLoader) {
486
- return;
517
+ return undefined;
487
518
  }
488
519
 
520
+ // `settled` guards against two wedges: (1) a late write from a SUPERSEDED run
521
+ // (the cleanup flips it so an in-flight pipeline for an older `parsedCode` can't
522
+ // clobber the current one), and (2) the timeout below committing twice. The first
523
+ // commit wins.
524
+ let settled = false;
525
+ const commit = output => {
526
+ if (!settled) {
527
+ settled = true;
528
+ setTransformedState({
529
+ input: parsedCode,
530
+ output
531
+ });
532
+ }
533
+ };
534
+
535
+ // Safety net: if the pipeline neither resolves NOR rejects (a stalled worker / hung
536
+ // delta computation), the `catch` never fires and `waitingForTransformedCode` would
537
+ // stay true forever. Fall back to the un-delta'd parsed HAST so the gate drops and
538
+ // the block shows highlighted (un-transformed) code rather than plain text forever.
539
+ const timer = setTimeout(() => commit(parsedCode), TRANSFORM_PIPELINE_TIMEOUT_MS);
540
+
489
541
  // Process transformations for all variants
490
542
  (async () => {
491
543
  try {
@@ -494,18 +546,18 @@ function useCodeTransforms({
494
546
  // kept out of the initial bundle under CodeProviderLazy.
495
547
  const [parseSource, computeHastDeltas] = await Promise.all([sourceParser, computeHastDeltasLoader()]);
496
548
  const enhanced = await computeHastDeltas(parsedCode, parseSource);
497
- setTransformedState({
498
- input: parsedCode,
499
- output: enhanced
500
- });
549
+ commit(enhanced);
501
550
  } catch (error) {
502
551
  console.error(new Errors.ErrorCodeHighlighterClientTransformProcessingFailure(error));
503
- setTransformedState({
504
- input: parsedCode,
505
- output: parsedCode
506
- });
552
+ commit(parsedCode);
553
+ } finally {
554
+ clearTimeout(timer);
507
555
  }
508
556
  })();
557
+ return () => {
558
+ settled = true; // a newer run (or unmount) supersedes this one; ignore late writes
559
+ clearTimeout(timer);
560
+ };
509
561
  }, [parsedCode, sourceParser, computeHastDeltasLoader]);
510
562
 
511
563
  // When the full async pipeline is wired, expose the cached output regardless
@@ -903,10 +955,21 @@ export function CodeHighlighterClient(props) {
903
955
  // the editable speculative preload below and notifies the CodeControllerContext.
904
956
  const [editingActivated, setEditingActivated] = React.useState(false);
905
957
  const controllerOnActivate = controlled?.onActivate;
958
+ // Which live-editing engine chunks the controller should preload on activation: `js`
959
+ // if the demo has any JS/TS file, `css` if any CSS file. Stable across keystrokes
960
+ // (editing changes source content, never which files exist), like the grammar scopes
961
+ // below.
962
+ const editableFileTypes = React.useMemo(() => {
963
+ const editableCode = props.code ?? code;
964
+ return editableCode ? detectFileTypes(editableCode) : {
965
+ js: false,
966
+ css: false
967
+ };
968
+ }, [props.code, code]);
906
969
  const handleEditingActivated = React.useCallback(() => {
907
970
  setEditingActivated(true);
908
- controllerOnActivate?.();
909
- }, [controllerOnActivate]);
971
+ controllerOnActivate?.(editableFileTypes);
972
+ }, [controllerOnActivate, editableFileTypes]);
910
973
 
911
974
  // Grammar scopes the editable files need for live re-highlighting. Unlike the
912
975
  // speculative highlight/transform preloads — which intentionally skip
@@ -1061,7 +1124,9 @@ export function CodeHighlighterClient(props) {
1061
1124
  const [isEnhanceAllowed, setIsEnhanceAllowed] = React.useState(enhanceAfter === 'init' || enhanceAfter === 'hydration' && isHydrated);
1062
1125
  React.useEffect(() => {
1063
1126
  if (enhanceAfter === 'idle') {
1064
- return requestIdle(() => setIsEnhanceAllowed(true));
1127
+ return requestIdle(() => setIsEnhanceAllowed(true), {
1128
+ timeout: IDLE_SWAP_TIMEOUT_MS
1129
+ });
1065
1130
  }
1066
1131
  return undefined;
1067
1132
  }, [enhanceAfter]);
@@ -1268,13 +1333,45 @@ export function CodeHighlighterClient(props) {
1268
1333
  setFallbackHasts: handleSetFallbackHasts,
1269
1334
  onHookCalled: handleHookCalled
1270
1335
  }), [variantName, codeForFallback, fileName, props.fallbackUsesExtraFiles, props.fallbackUsesAllVariants, handleSetFallbackHasts, handleHookCalled]);
1336
+
1337
+ // Keep the build-time render for any variant the controller hasn't produced a live
1338
+ // preview for yet — an in-flight build, a build error, or a variant with no live build
1339
+ // at all — by merging PER VARIANT (live entries override the build-time ones; the rest
1340
+ // stay visible). A whole-object swap (`controlled?.components || props.components`) would
1341
+ // drop the build-time render for ALL variants the instant ANY one went live, flashing
1342
+ // empty previews; the merge keeps "no live entry ⇒ show the build-time render".
1343
+ //
1344
+ // Each live entry is the lazy `DemoRunner`. Render it under a `Suspense` whose fallback is
1345
+ // that variant's build-time render (shown while the chunk resolves), AND inject the same
1346
+ // build-time render as the runner's `fallback` prop — so a live preview that throws on its
1347
+ // FIRST render, before any successful one (e.g. a render-error first edit, where the
1348
+ // baseline never got to paint), shows the original instead of blanking.
1349
+ const bridgedComponents = React.useMemo(() => {
1350
+ const live = controlled?.components;
1351
+ if (!live) {
1352
+ return props.components;
1353
+ }
1354
+ const merged = {
1355
+ ...(props.components || {})
1356
+ };
1357
+ for (const variant of Object.keys(live)) {
1358
+ const buildTime = props.components?.[variant];
1359
+ const liveNode = injectFallback(live[variant], buildTime);
1360
+ merged[variant] = /*#__PURE__*/React.createElement(React.Suspense, {
1361
+ key: variant,
1362
+ fallback: buildTime ?? null
1363
+ }, liveNode);
1364
+ }
1365
+ return merged;
1366
+ }, [controlled?.components, props.components]);
1271
1367
  const context = React.useMemo(() => ({
1272
1368
  code: overlaidCode,
1273
1369
  // Use processed/transformed code
1274
1370
  setCode: controlled?.setCode,
1275
1371
  selection: controlled?.selection || selection,
1276
1372
  setSelection: controlled?.setSelection || setSelection,
1277
- components: controlled?.components || props.components,
1373
+ components: bridgedComponents,
1374
+ errors: controlled?.errors,
1278
1375
  // Only suppress when an external CodeController owns the code; static
1279
1376
  // `props.code` still needs the locally-computed list.
1280
1377
  availableTransforms: controlled?.code ? [] : availableTransforms,
@@ -1287,7 +1384,7 @@ export function CodeHighlighterClient(props) {
1287
1384
  onEditingActivated: handleEditingActivated,
1288
1385
  refresh,
1289
1386
  preParsedCache
1290
- }), [overlaidCode, controlled?.setCode, selection, controlled?.selection, controlled?.setSelection, controlled?.components, props.components, controlled?.code, availableTransforms, props.url, deferHighlight, activeFallbacks, highlightReady, highlightAfter, editActivation, handleEditingActivated, refresh, preParsedCache]);
1387
+ }), [overlaidCode, controlled?.setCode, selection, controlled?.selection, controlled?.setSelection, bridgedComponents, controlled?.errors, controlled?.code, availableTransforms, props.url, deferHighlight, activeFallbacks, highlightReady, highlightAfter, editActivation, handleEditingActivated, refresh, preParsedCache]);
1291
1388
  if (!props.variants && !props.components && !activeCode) {
1292
1389
  throw new Errors.ErrorCodeHighlighterClientMissingData();
1293
1390
  }
@@ -18,6 +18,12 @@ export interface CodeHighlighterContextType {
18
18
  selection?: Selection;
19
19
  setSelection?: React.Dispatch<React.SetStateAction<Selection>>;
20
20
  components?: Record<string, React.ReactNode>;
21
+ /**
22
+ * Current runtime error message per variant key (or `null` when the variant
23
+ * rendered cleanly), bridged from the controller context. Surfaced through
24
+ * `useDemo().error`.
25
+ */
26
+ errors?: Record<string, string | null>;
21
27
  availableTransforms?: string[];
22
28
  url?: string;
23
29
  deferHighlight?: boolean;
@@ -45,6 +45,9 @@ export function createClientProps(props) {
45
45
  } : undefined),
46
46
  ...(props.initialExpanded !== undefined ? {
47
47
  initialExpanded: props.initialExpanded
48
+ } : undefined),
49
+ ...(props.initialDisabled !== undefined ? {
50
+ initialDisabled: props.initialDisabled
48
51
  } : undefined)
49
52
  };
50
53
  return {
@@ -1,13 +1,22 @@
1
1
  import type { Code, ControlledCode, ParseSource } from "./types.mjs";
2
2
  import type { PreParsedCacheEntry } from "./CodeHighlighterContext.mjs";
3
+ /**
4
+ * Cache key for a parsed file. Qualified by variant so two variants that share a
5
+ * file name (e.g. both have `Demo.tsx`) get independent entries instead of
6
+ * evicting each other. The `\u0000` separator cannot appear in a variant key or
7
+ * file name, so the two parts can never run together ambiguously.
8
+ */
9
+ export declare function preParsedCacheKey(variant: string, fileName: string): string;
3
10
  /**
4
11
  * Pure function to parse controlled code and convert it to regular Code format.
5
12
  * Handles the conversion from ControlledCode (string|null sources) to Code (HAST nodes).
6
13
  *
7
14
  * When `preParsedCache` is supplied, each file's source is first looked up in
8
- * the cache. If the cached entry's source string matches byte-for-byte, the
9
- * cached HAST is reused and `parseSource` is skipped for that file. On a
10
- * mismatch the stale entry is evicted before re-parsing so the cache cannot
11
- * grow stale across rapid edits.
15
+ * the cache (keyed by variant + file name). If the cached entry's source string
16
+ * matches byte-for-byte, the cached HAST is reused and `parseSource` is skipped.
17
+ * A fresh parse is WRITTEN THROUGH to the cache, so on the next controlled-code
18
+ * change (a keystroke updates one file) every unchanged file is reused instead
19
+ * of re-parsed — only the edited file's source differs. A stale entry is evicted
20
+ * and replaced; a parse failure keeps the raw string and is not cached.
12
21
  */
13
22
  export declare function parseControlledCode(controlledCode: ControlledCode, parseSource: ParseSource, preParsedCache?: Map<string, PreParsedCacheEntry>): Code;
@@ -1,31 +1,50 @@
1
+ /**
2
+ * Cache key for a parsed file. Qualified by variant so two variants that share a
3
+ * file name (e.g. both have `Demo.tsx`) get independent entries instead of
4
+ * evicting each other. The `\u0000` separator cannot appear in a variant key or
5
+ * file name, so the two parts can never run together ambiguously.
6
+ */
7
+ export function preParsedCacheKey(variant, fileName) {
8
+ return `${variant}\u0000${fileName}`;
9
+ }
10
+
1
11
  /**
2
12
  * Pure function to parse controlled code and convert it to regular Code format.
3
13
  * Handles the conversion from ControlledCode (string|null sources) to Code (HAST nodes).
4
14
  *
5
15
  * When `preParsedCache` is supplied, each file's source is first looked up in
6
- * the cache. If the cached entry's source string matches byte-for-byte, the
7
- * cached HAST is reused and `parseSource` is skipped for that file. On a
8
- * mismatch the stale entry is evicted before re-parsing so the cache cannot
9
- * grow stale across rapid edits.
16
+ * the cache (keyed by variant + file name). If the cached entry's source string
17
+ * matches byte-for-byte, the cached HAST is reused and `parseSource` is skipped.
18
+ * A fresh parse is WRITTEN THROUGH to the cache, so on the next controlled-code
19
+ * change (a keystroke updates one file) every unchanged file is reused instead
20
+ * of re-parsed — only the edited file's source differs. A stale entry is evicted
21
+ * and replaced; a parse failure keeps the raw string and is not cached.
10
22
  */
11
23
  export function parseControlledCode(controlledCode, parseSource, preParsedCache) {
12
24
  /**
13
- * Try the cache for `fileName`/`source`. Returns the cached HAST on an
14
- * exact match; evicts and returns `undefined` on a mismatch.
25
+ * Resolves one file's string source to HAST, reusing a cached parse on an exact
26
+ * source match and writing a fresh parse through to the cache. Returns the raw
27
+ * string (uncached) if `parseSource` throws.
15
28
  */
16
- const tryCache = (fileName, source) => {
17
- if (!preParsedCache) {
18
- return undefined;
19
- }
20
- const entry = preParsedCache.get(fileName);
21
- if (!entry) {
22
- return undefined;
29
+ const resolveSource = (variant, fileName, source) => {
30
+ const key = preParsedCacheKey(variant, fileName);
31
+ const entry = preParsedCache?.get(key);
32
+ if (entry) {
33
+ if (entry.source === source) {
34
+ return entry.hast;
35
+ }
36
+ preParsedCache?.delete(key);
23
37
  }
24
- if (entry.source === source) {
25
- return entry.hast;
38
+ try {
39
+ const hast = parseSource(source, fileName);
40
+ preParsedCache?.set(key, {
41
+ source,
42
+ hast
43
+ });
44
+ return hast;
45
+ } catch {
46
+ return source;
26
47
  }
27
- preParsedCache.delete(fileName);
28
- return undefined;
29
48
  };
30
49
  const parsed = {};
31
50
  for (const [variant, variantCode] of Object.entries(controlledCode)) {
@@ -39,17 +58,7 @@ export function parseControlledCode(controlledCode, parseSource, preParsedCache)
39
58
  // Convert null to empty string, then parse
40
59
  const sourceToProcess = variantCode.source === null ? '' : variantCode.source;
41
60
  if (typeof sourceToProcess === 'string' && variantCode.fileName) {
42
- const cached = tryCache(variantCode.fileName, sourceToProcess);
43
- if (cached) {
44
- mainSource = cached;
45
- } else {
46
- try {
47
- mainSource = parseSource(sourceToProcess, variantCode.fileName);
48
- } catch (error) {
49
- // Keep original string if parsing fails
50
- mainSource = sourceToProcess;
51
- }
52
- }
61
+ mainSource = resolveSource(variant, variantCode.fileName, sourceToProcess);
53
62
  } else if (typeof sourceToProcess === 'string' && !variantCode.fileName) {
54
63
  // Return a basic HAST root node with the source text for unsupported file types
55
64
  // This indicates that the source has at least passed through the parsing pipeline
@@ -74,27 +83,10 @@ export function parseControlledCode(controlledCode, parseSource, preParsedCache)
74
83
  // Convert null to empty string, then parse
75
84
  const fileSourceToProcess = fileData.source === null ? '' : fileData.source;
76
85
  if (typeof fileSourceToProcess === 'string') {
77
- const cached = tryCache(fileName, fileSourceToProcess);
78
- if (cached) {
79
- parsedExtraFiles[fileName] = {
80
- source: cached,
81
- comments: fileData.comments
82
- };
83
- } else {
84
- try {
85
- const parsedSource = parseSource(fileSourceToProcess, fileName);
86
- parsedExtraFiles[fileName] = {
87
- source: parsedSource,
88
- comments: fileData.comments
89
- };
90
- } catch (error) {
91
- // Keep original if parsing fails
92
- parsedExtraFiles[fileName] = {
93
- source: fileSourceToProcess,
94
- comments: fileData.comments
95
- };
96
- }
97
- }
86
+ parsedExtraFiles[fileName] = {
87
+ source: resolveSource(variant, fileName, fileSourceToProcess),
88
+ comments: fileData.comments
89
+ };
98
90
  } else {
99
91
  // Keep other values as-is
100
92
  parsedExtraFiles[fileName] = {
@@ -240,6 +240,14 @@ export type ControlledVariantCode = CodeMeta & {
240
240
  collapseMap?: CollapseMap;
241
241
  totalLines?: number;
242
242
  emptyLines?: number[];
243
+ /**
244
+ * The pre-edit build inputs, carried ONLY on the first edit of a variant (the
245
+ * transition from precomputed to controlled). The live runner builds this as a
246
+ * baseline first — so a broken first edit has a good last-good render to fall
247
+ * back to — then swaps to the edited `source`. Read by `useVariantBuilds`;
248
+ * stripped on the next edit so it does not linger in the controlled state.
249
+ */
250
+ original?: Pick<ControlledVariantCode, 'source' | 'extraFiles'>;
243
251
  };
244
252
  export type ControlledCode = {
245
253
  [key: string]: undefined | null | ControlledVariantCode;
@@ -261,6 +269,14 @@ type BaseContentProps = CodeIdentityProps & Pick<CodeContentProps, 'code' | 'com
261
269
  * see it too.
262
270
  */
263
271
  initialExpanded?: boolean;
272
+ /**
273
+ * Start an editable block read-only (a truthy value), leaving it to the
274
+ * `editable` / `setEditable` toggle `useCode` returns to turn editing on.
275
+ * Runtime-only; lives on `contentProps` — the demo factory resolves a demo's
276
+ * `initialDisabled` (instance prop → `meta` → `createDemoFactory` default) into it.
277
+ * Distinct from the hard `disabled` opt, which forbids editing outright.
278
+ */
279
+ initialDisabled?: boolean;
264
280
  };
265
281
  export type ContentProps<T extends {}> = BaseContentProps & T;
266
282
  /**
@@ -622,6 +638,11 @@ export interface CodeHighlighterBaseProps<T extends {}> extends CodeIdentityProp
622
638
  * `contentProps` so both `useCode` and the loading fallback honor it.
623
639
  */
624
640
  initialExpanded?: boolean;
641
+ /**
642
+ * Start the editable code read-only. Threaded into `contentProps`; `useCode` seeds its
643
+ * `editable` toggle from it (a reader flips it back with `setEditable`). Runtime-only.
644
+ */
645
+ initialDisabled?: boolean;
625
646
  }
626
647
  /**
627
648
  * Props for the client-side CodeHighlighter component.
@@ -3,6 +3,12 @@
3
3
  import * as React from 'react';
4
4
  import { areGrammarsRegistered, ensureGrammars } from "../pipeline/parseSource/grammarCache.mjs";
5
5
 
6
+ // Safety-net deadline (ms) for the grammar load. A load that HANGS (a stalled dynamic
7
+ // import that never settles) — rather than rejecting — would otherwise leave `ready`
8
+ // false forever, wedging the block un-highlighted until a reload. After the deadline we
9
+ // fail open (plain text) just like a hard rejection.
10
+ const GRAMMAR_LOAD_TIMEOUT_MS = 10_000;
11
+
6
12
  /**
7
13
  * Gates client-side highlighting on grammar readiness. Under `CodeProviderLazy`,
8
14
  * grammars load per-language and on demand, so a block must wait until the
@@ -22,24 +28,53 @@ import { areGrammarsRegistered, ensureGrammars } from "../pipeline/parseSource/g
22
28
  */
23
29
  export function useGrammarsReady(scopes, enabled) {
24
30
  const [, forceUpdate] = React.useReducer(count => count + 1, 0);
31
+ // The scopes whose grammar load has definitively FAILED (a hard chunk-fetch error,
32
+ // not a partial register). Tracked so the block can fail OPEN — render plain text for
33
+ // those scopes — instead of wedging on `ready === false` forever (which would leave
34
+ // the code permanently un-highlighted until a reload). Stored as a CONTENT key (the
35
+ // joined scopes), not a reference, so it stays matched if the caller passes a fresh
36
+ // array — and a different set of scopes (a new language) still retries.
37
+ const [failedScopesKey, setFailedScopesKey] = React.useState(null);
25
38
 
26
- // Nothing to wait for when disabled or when every scope is already registered.
27
- const ready = !enabled || areGrammarsRegistered(scopes);
39
+ // Nothing to wait for when disabled or when every scope is already registered. The
40
+ // content key (joined scopes) drives both the fail-open match AND the effect dep, so
41
+ // the load runs once per scope-CONTENT set — not on every render when a caller passes
42
+ // a fresh `scopes` array (which would otherwise re-trigger `ensureGrammars`/timeouts).
43
+ const registered = areGrammarsRegistered(scopes);
44
+ const scopesKey = scopes.join('\n');
45
+ const ready = !enabled || registered || failedScopesKey === scopesKey;
28
46
  React.useEffect(() => {
29
- if (ready) {
47
+ if (!enabled || registered) {
30
48
  return undefined;
31
49
  }
32
50
  let cancelled = false;
33
- // Fails open: a load error still flips ready so the block highlights what it
34
- // can (and renders plain text for any scope that failed to register).
35
- ensureGrammars(scopes).finally(() => {
51
+ // Fail open on a hard rejection (catch) OR a hang (the timer): a grammar load that
52
+ // never settles would otherwise leave `ready` false forever the same wedge.
53
+ const failOpen = () => {
36
54
  if (!cancelled) {
37
- forceUpdate();
55
+ setFailedScopesKey(scopesKey);
56
+ }
57
+ };
58
+ const timer = setTimeout(failOpen, GRAMMAR_LOAD_TIMEOUT_MS);
59
+ (async () => {
60
+ try {
61
+ // Reconstruct from the content key so this effect depends only on the joined
62
+ // content (`scopesKey`), never the array reference.
63
+ await ensureGrammars(scopesKey.split('\n'));
64
+ // Registered now — re-render so `ready` recomputes to true.
65
+ if (!cancelled) {
66
+ forceUpdate();
67
+ }
68
+ } catch {
69
+ failOpen();
70
+ } finally {
71
+ clearTimeout(timer);
38
72
  }
39
- });
73
+ })();
40
74
  return () => {
41
75
  cancelled = true;
76
+ clearTimeout(timer);
42
77
  };
43
- }, [ready, scopes]);
78
+ }, [enabled, registered, scopesKey]);
44
79
  return ready;
45
80
  }
@@ -42,6 +42,12 @@ export interface ParseSourceWorkerClient {
42
42
  *
43
43
  * Each client owns exactly one underlying `Worker`. Concurrent in-flight
44
44
  * requests are demuxed by a monotonically increasing `id`.
45
+ *
46
+ * If the worker dies unexpectedly (a load/parse failure, or a message that fails
47
+ * to deserialize), every in-flight parse request and pending `init`/`register`
48
+ * handshake is rejected and the client latches `dead`, so a crash surfaces as a
49
+ * rejected promise (which the consumer already handles like an abort, falling back
50
+ * to the synchronous highlighter) rather than a request that hangs forever.
45
51
  */
46
52
  export declare function createParseSourceWorkerClient(): ParseSourceWorkerClient;
47
53
  export {};