Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions website/raw-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ module.exports = function (context, options) {
configureWebpack(config, isServer, utils) {
return {
ignoreWarnings: [
// Suppress warning about dynamic import expressions in monaco-init.ts
// This is expected behavior for dynamic TypeScript definition loading
// Suppress warning about dynamic import expressions in
// Playground/monaco/typeLibs.ts. This is expected behavior for
// dynamic TypeScript definition loading
{
module: /monaco-init\.ts/,
module: /Playground[\\/]monaco[\\/]typeLibs\.ts/,
message:
/Critical dependency: the request of a dependency is an expression/,
},
Expand Down
2 changes: 1 addition & 1 deletion website/scripts/generateMonacoPreloads.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const VS_ROOT = path.dirname(
const MONACO_ROOT = path.resolve(VS_ROOT, '../..');
const MANIFEST_PATH = path.join(
WEBSITE_ROOT,
'src/components/Playground/monacoPreloadManifest.ts',
'src/components/Playground/monaco/preloadManifest.ts',
);

/** @returns {{ monacoVersion: string, preloadPaths: string[], prefetchPaths: string[] }} */
Expand Down
13 changes: 7 additions & 6 deletions website/src/components/DiffEditorMonaco.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { type editor } from 'monaco-editor';
import { useMemo } from 'react';

import type { CodeDocument } from './Playground/editor/codeModel';
import { extensionToMonacoLanguage } from './Playground/extensionToMonacoLanguage';
import './Playground/monaco-init';
import { isMobileOrBot } from './Playground/isMobileOrBot';
import { options } from './Playground/monacoOptions';
import { extensionToMonacoLanguage } from './Playground/monaco/language';
import { options } from './Playground/monaco/options';
import './Playground/monaco/setup';
import { MONACO_THEME } from './Playground/monaco/theme';
import useAutoHeight from './Playground/monaco/useAutoHeight';
import styles from './Playground/styles.module.css';
import useAutoHeight from './Playground/useAutoHeight';
import { isMobileOrBot } from './Playground/userAgent';

export default function DiffEditor({ documents, fallback }: DiffMonacoProps) {
const [original, modified] = useMemo(
Expand Down Expand Up @@ -48,7 +49,7 @@ export default function DiffEditor({ documents, fallback }: DiffMonacoProps) {
options={DIFF_OPTIONS}
onMount={handleMount}
height={height}
theme="prism"
theme={MONACO_THEME}
loading={fallback}
// Prevent "TextModel got disposed before DiffEditorWidget model got reset" error
// by not letting @monaco-editor/react manage model disposal
Expand Down
17 changes: 3 additions & 14 deletions website/src/components/HooksPlayground.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Fixture, Interceptor } from '@data-client/test';
import React, { memo } from 'react';

import Playground from './Playground';
import Playground, { type PlaygroundProps } from './Playground';

/** MDX entry point: wraps one or more code fences in a live Playground. */
const HooksPlayground = ({
children,
groupId,
Expand All @@ -24,6 +24,7 @@ const HooksPlayground = ({
defaultTab={defaultTab}
headerControls={headerControls}
>
{/* A single fence arrives as one <pre> element; unwrap it to its <code> */}
{typeof children === 'string' ?
children
: Array.isArray(children) ?
Expand All @@ -34,15 +35,3 @@ const HooksPlayground = ({
</Playground>
);
export default memo(HooksPlayground);

interface PlaygroundProps<T = any> {
groupId: string;
defaultOpen?: 'y' | 'n';
row: boolean;
hidden?: boolean;
fixtures?: (Fixture | Interceptor<T>)[];
getInitialInterceptorData?: () => T;
children: React.ReactNode;
defaultTab?: string;
headerControls?: React.ReactNode;
}
4 changes: 2 additions & 2 deletions website/src/components/Playground/PlaygroundEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import InteractiveEditor from './editor/InteractiveEditor';
import PlaygroundLiveEditor from './PlaygroundLiveEditor';
import PlaygroundMonacoEditor from './PlaygroundMonacoEditor';

// Always use Monaco editor wrapper - mobile/bot check happens inside
// This avoids hydration mismatch from module-level navigator checks
const PlaygroundEditor = PlaygroundMonacoEditor;
const PlaygroundEditor = InteractiveEditor;
export default PlaygroundEditor;

// Re-export for use as fallback inside Monaco editor
Expand Down
116 changes: 0 additions & 116 deletions website/src/components/Playground/PlaygroundMonacoEditor.tsx

This file was deleted.

131 changes: 131 additions & 0 deletions website/src/components/Playground/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Playground

Live, editable code examples for the docs, blog and homepage. Read this before
changing anything here: most of the complexity exists to keep pages fast,
indexable and hydration-safe, and those behaviors are easy to drop silently.

## Entry points

| Component | Used by | What it renders |
| ------------------------------------------------------------------------------------------- | ------------------------------ | --------------------------------------- |
| `HooksPlayground` (`../HooksPlayground.tsx`) → `Playground` (`index.tsx`) | MDX docs/blog, homepage `Demo` | Editor + live preview + Store inspector |
| `TypeScriptEditor` (`../TypeScriptEditor.tsx`) | MDX | Editor only (`variant="standalone"`) |
| `EndpointPlayground` (`../HTTP/EndpointPlayground.tsx`) | MDX | Editor + static HTTP request/response |
| `DiffEditor` (`../DiffEditor.tsx` → `../DiffEditorChooser.tsx` → `../DiffEditorMonaco.tsx`) | MDX | Read-only Monaco diff of two fences |
| `Demo/CodeEditor` (`../Demo/CodeEditor.tsx`) | Homepage | One `HooksPlayground` per protocol tab |

## Layout

```
index.tsx Playground: code model, editor/preview ordering, hidden handling
userAgent.ts isBot / isMobileOrBot gates
PlaygroundEditor.tsx editor entry used by EditorSurface (aliases editor/InteractiveEditor)
PlaygroundLiveEditor.tsx editable react-live editor (mobile/bot fallback)
resources/ sample resources (currently unreferenced; kept for future demos)
Boundary.tsx BrowserOnly + Suspense with one fallback for SSR and loading
Header.tsx, TabList.tsx, styles.module.css shared chrome (also used by HTTP/, Demo/)
editor/ code model + editor UI
codeModel.ts parse MDX fences/strings into documents; edits
EditorShell.tsx react-live LiveProvider supplying the Prism theme
EditorSurface.tsx headers, file tabs, collapse state, per-file TextEditTab
StaticEditor.tsx SSR/crawler/loading markup (Prism, not editable)
InteractiveEditor.tsx client-only: Monaco, or editable react-live on mobile/bots
monaco/ everything Monaco-specific
setup.ts side-effect init: CDN loader, hints, compiler options, theme, types
typeLibs.ts raw .d.ts chunks from editor-types/ → addExtraLib
navigation.ts cross-tab go-to-definition + import completions
modelPath.ts `/<id>/<file>` model URI scheme
preloadManifest.ts GENERATED by website/scripts/generateMonacoPreloads.cjs
resourceHints.ts, theme.ts, options.ts, language.ts, useAutoHeight.ts, highlightSelections.ts
preview/ live execution (loaded lazily, never on the server)
LivePreview.tsx react-live LiveProvider with scope + transformCode
Preview.tsx DataProvider + MockResolver(fixtures) + Store inspector
...
editor-types/ .d.ts bundles fed to Monaco (raw-loader)
DesignSystem/ components injected into preview scope
```

## Invariants

### SSR / SSG and crawlers

- Open files' source is in the static HTML: `TextEditTab` renders
`StaticEditor` on the server and during hydration (BrowserOnly fallback).
Collapsed/unselected files render nothing until opened.
- The preview renders `previewLoading` (empty frame + Store toggle) on the
server, while loading, for bots, and while `hidden`.
- Fixtures render server-side as JSON `CodeBlock`s; function responses are
BrowserOnly (a stringified function differs between server and client
bundles).
- Never branch on `navigator`/user agent during render outside `BrowserOnly`
(hydration mismatch). `isBot` is module-level and only read in the
`lazy()` factory (and in StackBlitz after intersection); `isMobileOrBot()`
is only called inside BrowserOnly or in `monaco/setup.ts`.

### Incremental JS delivery

- `monaco/setup.ts` runs once when the editor chunk evaluates: injects
preload/prefetch `<link>`s for the version-matched CDN files, starts
`loader.init()` and the type-lib chunk downloads in parallel.
`preloadManifest.ts` is regenerated on every docusaurus config load.
- Type libs: one webpack chunk per third-party `.d.ts` (`reactDTS`, …) and a
single `lazy-once` chunk for `editor-types/@data-client/*`. Failed fetches
degrade to empty libs. `website/raw-plugin.js` silences the expected
"expression in import" warning for `monaco/typeLibs.ts` by path.
- Preview code is a lazy chunk (`PreviewWithScope`, prefetched) and
`PreviewBlock` a nested lazy chunk (preloaded with its parent).
- Mobile and bots download none of Monaco or the type libs.

### Mobile and bots

- Mobile: editable react-live editor instead of Monaco; live preview still runs.
- Bots: same editor fallback; preview stays `previewLoading` (never loads
`LivePreview`); StackBlitz embeds never load.
- `DiffEditor` shows a two-`CodeBlock` grid instead of Monaco.

### Hidden playgrounds (homepage Demo protocol tabs)

- All protocols stay mounted so every protocol's source is in the HTML.
- `hidden` keeps `StaticEditor` (no Monaco) until first shown, then latches
interactive so undo history and go-to-definition survive tab switches.
- The live preview unmounts while hidden (its store resets).

### Editor

- Fence metastring: `title="…"`, `path="…"` (quotes optional), `collapsed`, `column`, `{1-3}`
highlight ranges (pre-selected in Monaco); `language-*` class. Element props
override metastring values (Demo passes them directly). `defaultTab`
overrides `collapsed`.
- `row` layout: non-`column` files are tabs (focus switches tab); `column`
files stack with collapsible headers. Stacked layout: every titled file gets a header,
collapsible when there are several files or fixtures.
- Collapsed/unselected files stay mounted (`display: none`), never unmounted:
their Monaco models must exist for cross-file types, go-to-definition and
to keep edits. Monaco is uncontrolled (`defaultValue`), so re-renders never
reset the buffer.
- Fixtures are listed above the editor in stacked layout only.
- Go-to-definition across files: focusing the target editor reveals its tab.
Relies on the `/<numeric id>/<path>` model URIs (`monaco/modelPath.ts`).
- Relative import completions list only the other files of the same
playground (same model id).
- Handlers passed to `InteractiveEditor` are referentially stable so unedited
tabs skip re-rendering on keystrokes.
- Height follows Monaco content size and re-measures when a tab is shown.

### Preview

- All documents are concatenated (deferred with `useDeferredValue`),
stripped of imports/exports by `transformCode`, and run `noInline`
(`render(<App />)`) with `preview/scope.ts` as globals. Keep
`monaco/typeLibs.ts` declarations in sync with the scope.
- Each playground gets its own `DataProvider` store (`MockResolver` serves
`fixtures`); `memo(Preview)` keeps it from re-rendering on code edits.
- Store inspector open state persists per `groupId` via tab storage and
avoids scroll jumps; in `row` layout it replaces the result while open.
- Third-party console noise is demoted only while previews are mounted
(`usePlaygroundConsoleDemotion`); never add first-party matchers.

## Tests

`yarn test --selectProjects ReactDOM --testPathPatterns website/src/components/Playground`
(CI persists only this directory of the website for these tests.)
2 changes: 1 addition & 1 deletion website/src/components/Playground/editor/EditorShell.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { LiveProvider } from 'react-live';

import { useReactLiveTheme } from '../useReactLiveTheme';
import { useReactLiveTheme } from './useReactLiveTheme';

export default function EditorShell({
children,
Expand Down
Loading
Loading