feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
import * as nextEntry from 'markstream-react/next'
|
||||
import * as serverEntry from 'markstream-react/server'
|
||||
import { SsrLabPage } from '../../src/ssr-lab-page'
|
||||
|
||||
export default function AppSsrLabPage() {
|
||||
return (
|
||||
<SsrLabPage
|
||||
version="next15"
|
||||
router="app"
|
||||
nextEntry={nextEntry}
|
||||
serverEntry={serverEntry}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import '../styles/globals.css'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Next 15 SSR Lab',
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: { children: any }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main className="home-shell">
|
||||
<h1>Next 15 Playground</h1>
|
||||
<p>Use the dedicated SSR lab routes for formal acceptance.</p>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><Link href="/app-ssr-lab">/app-ssr-lab</Link></li>
|
||||
<li><Link href="/pages-ssr-lab">/pages-ssr-lab</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -0,0 +1,26 @@
|
||||
import { resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const rootDir = fileURLToPath(new URL('.', import.meta.url))
|
||||
const reactAliases = {
|
||||
'react': resolve(rootDir, 'node_modules/react'),
|
||||
'react-dom': resolve(rootDir, 'node_modules/react-dom'),
|
||||
'react/jsx-runtime': resolve(rootDir, 'node_modules/react/jsx-runtime.js'),
|
||||
'react/jsx-dev-runtime': resolve(rootDir, 'node_modules/react/jsx-dev-runtime.js'),
|
||||
}
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
transpilePackages: ['markstream-react', 'stream-markdown-parser'],
|
||||
webpack(config, { isServer }) {
|
||||
if (!isServer) {
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
...reactAliases,
|
||||
}
|
||||
}
|
||||
return config
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "playground-next15",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@antv/infographic": "^0.2.16",
|
||||
"@terrastruct/d2": "^0.1.33",
|
||||
"katex": "^0.16.44",
|
||||
"markstream-react": "workspace:*",
|
||||
"mermaid": "^11.13.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"next": "15.5.14",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"stream-monaco": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.12.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { AppProps } from 'next/app'
|
||||
import '../styles/globals.css'
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return <Component {...pageProps} />
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import * as nextEntry from 'markstream-react/next'
|
||||
import * as serverEntry from 'markstream-react/server'
|
||||
import React from 'react'
|
||||
import {
|
||||
createMatrixCases,
|
||||
FALLBACK_DEMOS,
|
||||
SERVER_BASIC_NODES,
|
||||
SERVER_CUSTOM_ALT_ID,
|
||||
SERVER_CUSTOM_ALT_NODES,
|
||||
SERVER_CUSTOM_ID,
|
||||
SERVER_CUSTOM_NODES,
|
||||
} from '../src/ssr-lab-data'
|
||||
import { SsrLabPage } from '../src/ssr-lab-page'
|
||||
|
||||
function readAttr(node: any, name: string) {
|
||||
if (!node?.attrs)
|
||||
return undefined
|
||||
if (Array.isArray(node.attrs))
|
||||
return node.attrs.find((item: [string, string | null]) => item[0] === name)?.[1] ?? undefined
|
||||
return node.attrs[name]
|
||||
}
|
||||
|
||||
function renderCustomChildren(props: any) {
|
||||
if (!Array.isArray(props.node?.children) || !props.renderNode || !props.ctx)
|
||||
return null
|
||||
|
||||
return props.node.children.map((child: any, idx: number) => (
|
||||
<React.Fragment key={`${String(props.indexKey ?? 'custom')}-${idx}`}>
|
||||
{props.renderNode(child, `${String(props.indexKey ?? 'custom')}-${idx}`, props.ctx)}
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
|
||||
function ServerParagraphOverride({ node }: any) {
|
||||
return (
|
||||
<div data-ssr-status="server-paragraph-override">
|
||||
{node.raw}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ServerInsightNode(props: any) {
|
||||
return (
|
||||
<section data-ssr-status="server-custom-node">
|
||||
<strong>{props.node.label}</strong>
|
||||
<div data-ssr-status="server-custom-node-children">
|
||||
{renderCustomChildren(props)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function ServerThinkingTag({ node }: any) {
|
||||
return (
|
||||
<aside
|
||||
data-ssr-status="server-thinking-tag"
|
||||
data-ssr-tone={readAttr(node, 'data-tone')}
|
||||
data-ssr-scope={readAttr(node, 'data-scope')}
|
||||
>
|
||||
{node.content}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
function ServerParagraphAlt({ node }: any) {
|
||||
return (
|
||||
<div data-ssr-status="server-paragraph-alt">
|
||||
{node.raw}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ServerInsightAlt(props: any) {
|
||||
return (
|
||||
<section data-ssr-status="server-custom-node-alt">
|
||||
<strong>{props.node.label}</strong>
|
||||
<div data-ssr-status="server-custom-node-alt-children">
|
||||
{renderCustomChildren(props)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function ServerThinkingTagAlt({ node }: any) {
|
||||
return (
|
||||
<aside
|
||||
data-ssr-status="server-thinking-tag-alt"
|
||||
data-ssr-scope={readAttr(node, 'data-scope')}
|
||||
>
|
||||
{node.content}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
function renderServerFallbackHtml() {
|
||||
return (
|
||||
<>
|
||||
<div data-ssr-demo="mermaid" className="ssr-card">
|
||||
<serverEntry.MermaidBlockNode node={FALLBACK_DEMOS.mermaid as any} />
|
||||
</div>
|
||||
<div data-ssr-demo="d2" className="ssr-card">
|
||||
<serverEntry.D2BlockNode node={FALLBACK_DEMOS.d2 as any} />
|
||||
</div>
|
||||
<div data-ssr-demo="infographic" className="ssr-card">
|
||||
<serverEntry.InfographicBlockNode node={FALLBACK_DEMOS.infographic as any} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function renderServerMatrixHtml() {
|
||||
const items = createMatrixCases(serverEntry, serverEntry)
|
||||
return (
|
||||
<div className="ssr-matrix">
|
||||
{items.map(item => (
|
||||
<section key={item.name} data-ssr-export={item.name} className="ssr-card">
|
||||
<h3>{item.name}</h3>
|
||||
<div className="markstream-react ssr-matrix-renderer">{item.element}</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function renderServerCustomHtml() {
|
||||
return (
|
||||
<>
|
||||
<section data-ssr-custom-id="server-primary" className="ssr-card">
|
||||
<serverEntry.NodeRenderer
|
||||
customId={SERVER_CUSTOM_ID}
|
||||
customHtmlTags={['thinking']}
|
||||
nodes={SERVER_CUSTOM_NODES}
|
||||
final
|
||||
showTooltips={false}
|
||||
/>
|
||||
</section>
|
||||
<section data-ssr-custom-id="server-alt" className="ssr-card">
|
||||
<serverEntry.NodeRenderer
|
||||
customId={SERVER_CUSTOM_ALT_ID}
|
||||
customHtmlTags={['thinking']}
|
||||
nodes={SERVER_CUSTOM_ALT_NODES}
|
||||
final
|
||||
showTooltips={false}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface PagesSsrLabProps {
|
||||
serverBasicHtml: string
|
||||
serverCustomHtml: string
|
||||
serverFallbackHtml: string
|
||||
serverMatrixHtml: string
|
||||
}
|
||||
|
||||
export default function PagesSsrLabPage({
|
||||
serverBasicHtml,
|
||||
serverCustomHtml,
|
||||
serverFallbackHtml,
|
||||
serverMatrixHtml,
|
||||
}: PagesSsrLabProps) {
|
||||
return (
|
||||
<SsrLabPage
|
||||
version="next15"
|
||||
router="pages"
|
||||
nextEntry={nextEntry}
|
||||
serverEntry={serverEntry}
|
||||
serverBasicHtml={serverBasicHtml}
|
||||
serverCustomHtml={serverCustomHtml}
|
||||
serverFallbackHtml={serverFallbackHtml}
|
||||
serverMatrixHtml={serverMatrixHtml}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerSideProps() {
|
||||
const { renderToStaticMarkup } = await import('react-dom/server')
|
||||
|
||||
serverEntry.setCustomComponents(SERVER_CUSTOM_ID, {
|
||||
paragraph: ServerParagraphOverride,
|
||||
insight: ServerInsightNode,
|
||||
thinking: ServerThinkingTag,
|
||||
})
|
||||
serverEntry.setCustomComponents(SERVER_CUSTOM_ALT_ID, {
|
||||
paragraph: ServerParagraphAlt,
|
||||
insight: ServerInsightAlt,
|
||||
thinking: ServerThinkingTagAlt,
|
||||
})
|
||||
|
||||
return {
|
||||
props: {
|
||||
serverBasicHtml: renderToStaticMarkup(
|
||||
<serverEntry.NodeRenderer
|
||||
customId={SERVER_CUSTOM_ID}
|
||||
nodes={SERVER_BASIC_NODES}
|
||||
final
|
||||
showTooltips={false}
|
||||
/>,
|
||||
),
|
||||
serverCustomHtml: renderToStaticMarkup(renderServerCustomHtml()),
|
||||
serverFallbackHtml: renderToStaticMarkup(renderServerFallbackHtml()),
|
||||
serverMatrixHtml: renderToStaticMarkup(renderServerMatrixHtml()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
markstream-next15
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="40" viewBox="0 0 80 40" role="img" aria-labelledby="title desc">
|
||||
<title id="title">SSR image</title>
|
||||
<desc id="desc">Static test image for Next SSR acceptance.</desc>
|
||||
<rect width="80" height="40" fill="#dbeafe" rx="6" />
|
||||
<text x="40" y="25" text-anchor="middle" font-size="12" fill="#1e3a8a" font-family="system-ui, sans-serif">SSR</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 413 B |
@@ -0,0 +1,249 @@
|
||||
'use client'
|
||||
|
||||
import * as nextEntry from 'markstream-react/next'
|
||||
import { NodeRenderer, setCustomComponents } from 'markstream-react/next'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { createMatrixCases, NEXT_CUSTOM_ALT_ID, NEXT_CUSTOM_ID } from './ssr-lab-data'
|
||||
|
||||
function readAttr(node: any, name: string) {
|
||||
if (!node?.attrs)
|
||||
return undefined
|
||||
if (Array.isArray(node.attrs))
|
||||
return node.attrs.find((item: [string, string | null]) => item[0] === name)?.[1] ?? undefined
|
||||
return node.attrs[name]
|
||||
}
|
||||
|
||||
function renderCustomChildren(props: any) {
|
||||
if (!Array.isArray(props.node?.children) || !props.renderNode || !props.ctx)
|
||||
return null
|
||||
|
||||
return props.node.children.map((child: any, idx: number) => (
|
||||
<React.Fragment key={`${String(props.indexKey ?? 'custom')}-${idx}`}>
|
||||
{props.renderNode(child, `${String(props.indexKey ?? 'custom')}-${idx}`, props.ctx)}
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
|
||||
setCustomComponents(NEXT_CUSTOM_ID, {
|
||||
paragraph: ({ node }: any) => (
|
||||
<div data-ssr-status="next-paragraph-override">
|
||||
{node.raw}
|
||||
</div>
|
||||
),
|
||||
insight: (props: any) => (
|
||||
<section data-ssr-status="next-custom-node">
|
||||
<strong>{props.node.label}</strong>
|
||||
<div data-ssr-status="next-custom-node-children">
|
||||
{renderCustomChildren(props)}
|
||||
</div>
|
||||
</section>
|
||||
),
|
||||
thinking: ({ node }: any) => (
|
||||
<aside
|
||||
data-ssr-status="next-thinking-tag"
|
||||
data-ssr-tone={readAttr(node, 'data-tone')}
|
||||
data-ssr-scope={readAttr(node, 'data-scope')}
|
||||
>
|
||||
{node.content}
|
||||
</aside>
|
||||
),
|
||||
})
|
||||
|
||||
setCustomComponents(NEXT_CUSTOM_ALT_ID, {
|
||||
paragraph: ({ node }: any) => (
|
||||
<div data-ssr-status="next-paragraph-alt">
|
||||
{node.raw}
|
||||
</div>
|
||||
),
|
||||
insight: (props: any) => (
|
||||
<section data-ssr-status="next-custom-node-alt">
|
||||
<strong>{props.node.label}</strong>
|
||||
<div data-ssr-status="next-custom-node-alt-children">
|
||||
{renderCustomChildren(props)}
|
||||
</div>
|
||||
</section>
|
||||
),
|
||||
thinking: ({ node }: any) => (
|
||||
<aside
|
||||
data-ssr-status="next-thinking-tag-alt"
|
||||
data-ssr-scope={readAttr(node, 'data-scope')}
|
||||
>
|
||||
{node.content}
|
||||
</aside>
|
||||
),
|
||||
})
|
||||
|
||||
export function NextCustomRenderer({ customId, nodes }: { customId: string, nodes: any[] }) {
|
||||
return (
|
||||
<NodeRenderer
|
||||
customId={customId}
|
||||
customHtmlTags={['thinking']}
|
||||
nodes={nodes}
|
||||
final
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipExportDemo() {
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-stone-300 bg-white px-4 py-2 text-xs font-semibold tracking-[0.04em] text-stone-900 shadow-sm transition hover:bg-stone-50 focus:outline-none focus:ring-2 focus:ring-stone-300"
|
||||
onBlur={() => setVisible(false)}
|
||||
onFocus={(event) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
setVisible(true)
|
||||
}}
|
||||
onMouseEnter={(event) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
setVisible(true)
|
||||
}}
|
||||
onMouseLeave={() => setVisible(false)}
|
||||
>
|
||||
Hover for tooltip
|
||||
</button>
|
||||
<span className="text-xs font-medium text-stone-500">
|
||||
hover / focus overlay export
|
||||
</span>
|
||||
<nextEntry.Tooltip
|
||||
visible={visible}
|
||||
anchorEl={anchorEl}
|
||||
content="Tooltip body"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function HtmlPreviewFrameExportDemo() {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-stone-300 bg-white px-4 py-2 text-xs font-semibold tracking-[0.04em] text-stone-900 shadow-sm transition hover:bg-stone-50 focus:outline-none focus:ring-2 focus:ring-stone-300"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
Open preview
|
||||
</button>
|
||||
<span className="text-xs font-medium text-stone-500">
|
||||
interactive modal export
|
||||
</span>
|
||||
{open
|
||||
? (
|
||||
<nextEntry.HtmlPreviewFrame
|
||||
code="<p>Preview</p>"
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function NextExportMatrix() {
|
||||
const items = createMatrixCases(nextEntry, nextEntry)
|
||||
|
||||
return (
|
||||
<article data-ssr-case="export-matrix" data-ssr-entry="next">
|
||||
<h2>Next Export Matrix</h2>
|
||||
<div className="ssr-matrix">
|
||||
{items.map(item => (
|
||||
<section key={item.name} data-ssr-export={item.name} className="ssr-card">
|
||||
<h3>{item.name}</h3>
|
||||
<div className="markstream-react ssr-matrix-renderer">
|
||||
{item.name === 'Tooltip'
|
||||
? <TooltipExportDemo />
|
||||
: item.name === 'HtmlPreviewFrame'
|
||||
? <HtmlPreviewFrameExportDemo />
|
||||
: item.element}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function readEnhancementState() {
|
||||
const nextEnhanced = document.querySelector('[data-ssr-case="enhanced"][data-ssr-entry="next"]')
|
||||
const serverFallback = document.querySelector('[data-ssr-case="fallback"][data-ssr-entry="server"]')
|
||||
|
||||
const codeReady = Boolean(
|
||||
nextEnhanced?.querySelector('[data-ssr-demo="code"] .monaco-editor')
|
||||
|| nextEnhanced?.querySelector('[data-ssr-demo="code"] .code-editor-fallback-surface'),
|
||||
)
|
||||
const mathReady = Boolean(nextEnhanced?.querySelector('[data-ssr-demo="math"] .katex'))
|
||||
const mermaidReady = Boolean(
|
||||
nextEnhanced?.querySelector('[data-ssr-demo="mermaid"] .mermaid-block')
|
||||
&& !nextEnhanced?.querySelector('[data-ssr-demo="mermaid"] [data-ssr-fallback="mermaid"]'),
|
||||
)
|
||||
const d2Ready = Boolean(nextEnhanced?.querySelector('[data-ssr-demo="d2"] .d2-svg svg'))
|
||||
const infographicReady = Boolean(nextEnhanced?.querySelector('[data-ssr-demo="infographic"] svg'))
|
||||
const fallbackStable = (serverFallback?.querySelectorAll('[data-ssr-fallback]').length ?? 0) >= 3
|
||||
|
||||
return {
|
||||
codeReady,
|
||||
mathReady,
|
||||
mermaidReady,
|
||||
d2Ready,
|
||||
infographicReady,
|
||||
fallbackStable,
|
||||
}
|
||||
}
|
||||
|
||||
export function HydrationProbe() {
|
||||
const [nextStatus, setNextStatus] = useState('server')
|
||||
const [serverStatus, setServerStatus] = useState('server')
|
||||
const [summary, setSummary] = useState('pending')
|
||||
|
||||
useEffect(() => {
|
||||
let attempts = 0
|
||||
const timer = window.setInterval(() => {
|
||||
attempts += 1
|
||||
const state = readEnhancementState()
|
||||
setSummary([
|
||||
`code:${state.codeReady ? 'ready' : 'pending'}`,
|
||||
`math:${state.mathReady ? 'ready' : 'pending'}`,
|
||||
`mermaid:${state.mermaidReady ? 'ready' : 'pending'}`,
|
||||
`d2:${state.d2Ready ? 'ready' : 'fallback'}`,
|
||||
`infographic:${state.infographicReady ? 'ready' : 'fallback'}`,
|
||||
].join(' '))
|
||||
|
||||
if (state.codeReady && state.mathReady && state.mermaidReady)
|
||||
setNextStatus('enhanced')
|
||||
else
|
||||
setNextStatus(attempts > 4 ? 'hydrating' : 'server')
|
||||
|
||||
if (state.fallbackStable)
|
||||
setServerStatus('fallback-stable')
|
||||
else if (attempts > 4)
|
||||
setServerStatus('fallback-missing')
|
||||
|
||||
if ((state.codeReady && state.mathReady && state.mermaidReady) && state.fallbackStable)
|
||||
window.clearInterval(timer)
|
||||
|
||||
if (attempts >= 40)
|
||||
window.clearInterval(timer)
|
||||
}, 250)
|
||||
|
||||
return () => window.clearInterval(timer)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<article data-ssr-case="hydration" data-ssr-entry="next" data-ssr-status={nextStatus}>
|
||||
<h2>Hydration / Next</h2>
|
||||
<p>{summary}</p>
|
||||
</article>
|
||||
<article data-ssr-case="hydration" data-ssr-entry="server" data-ssr-status={serverStatus}>
|
||||
<h2>Hydration / Server</h2>
|
||||
<p>Server fallback should stay stable after hydration.</p>
|
||||
</article>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,618 @@
|
||||
import React from 'react'
|
||||
|
||||
const DEMO_IMAGE_SRC = '/ssr-image.svg'
|
||||
|
||||
function textNode(content: string) {
|
||||
return {
|
||||
type: 'text',
|
||||
content,
|
||||
raw: content,
|
||||
}
|
||||
}
|
||||
|
||||
function paragraphNode(content: string) {
|
||||
return {
|
||||
type: 'paragraph',
|
||||
raw: content,
|
||||
children: [textNode(content)],
|
||||
}
|
||||
}
|
||||
|
||||
function headingNode(content: string, level = 2) {
|
||||
return {
|
||||
type: 'heading',
|
||||
level,
|
||||
raw: `${'#'.repeat(level)} ${content}`,
|
||||
children: [textNode(content)],
|
||||
}
|
||||
}
|
||||
|
||||
function listItemNode(content: string) {
|
||||
return {
|
||||
type: 'list_item',
|
||||
raw: content,
|
||||
children: [paragraphNode(content)],
|
||||
}
|
||||
}
|
||||
|
||||
function definitionListNode() {
|
||||
return {
|
||||
type: 'definition_list',
|
||||
items: [
|
||||
{
|
||||
type: 'definition_item',
|
||||
term: [textNode('Term')],
|
||||
definition: [paragraphNode('Definition')],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function tableNode() {
|
||||
return {
|
||||
type: 'table',
|
||||
header: {
|
||||
cells: [
|
||||
{ children: [textNode('Name')] },
|
||||
{ children: [textNode('Value')] },
|
||||
],
|
||||
},
|
||||
rows: [
|
||||
{
|
||||
cells: [
|
||||
{ children: [textNode('Alpha')] },
|
||||
{ children: [textNode('One')] },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function vmrContainerNode() {
|
||||
return {
|
||||
type: 'vmr_container',
|
||||
name: 'tip',
|
||||
attrs: [['data-kind', 'tip']],
|
||||
children: [paragraphNode('Container body')],
|
||||
}
|
||||
}
|
||||
|
||||
function createRenderCtx(serverEntry: any) {
|
||||
return {
|
||||
customComponents: {},
|
||||
events: {},
|
||||
typewriter: false,
|
||||
codeBlockStream: true,
|
||||
showTooltips: false,
|
||||
renderCodeBlocksAsPre: false,
|
||||
codeBlockThemes: {},
|
||||
customId: 'matrix',
|
||||
renderNode: serverEntry.renderNode,
|
||||
} as any
|
||||
}
|
||||
|
||||
function insightNode(label: string, body: string) {
|
||||
return {
|
||||
type: 'insight',
|
||||
raw: label,
|
||||
label,
|
||||
children: [paragraphNode(body)],
|
||||
}
|
||||
}
|
||||
|
||||
function customThinkingNode(scope: string, body: string) {
|
||||
return {
|
||||
type: 'html_block',
|
||||
tag: 'thinking',
|
||||
attrs: [
|
||||
['data-tone', 'calm'],
|
||||
['data-scope', scope],
|
||||
],
|
||||
content: `<thinking data-tone="calm" data-scope="${scope}">${body}</thinking>`,
|
||||
raw: `<thinking data-tone="calm" data-scope="${scope}">${body}</thinking>`,
|
||||
}
|
||||
}
|
||||
|
||||
export const BASIC_NEXT_MARKDOWN = `
|
||||
# Next Entry Basic
|
||||
|
||||
Server-first markdown content with <strong>inline HTML</strong>, a [stable link](https://example.com), and a footnote.[^1]
|
||||
|
||||
<div data-ssr-status="next-basic-html">Next HTML block</div>
|
||||
|
||||
| Name | Value |
|
||||
| --- | --- |
|
||||
| Next | SSR |
|
||||
|
||||

|
||||
|
||||
[^1]: Footnote text from the Next entry.
|
||||
`.trim()
|
||||
|
||||
export const NEXT_CUSTOM_ID = 'next-ssr-lab'
|
||||
export const NEXT_CUSTOM_ALT_ID = 'next-ssr-lab-alt'
|
||||
export const SERVER_CUSTOM_ID = 'server-ssr-lab'
|
||||
export const SERVER_CUSTOM_ALT_ID = 'server-ssr-lab-alt'
|
||||
|
||||
export const NEXT_CUSTOM_NODES = [
|
||||
paragraphNode('Next scoped paragraph'),
|
||||
insightNode('Next custom node', 'Next custom child body'),
|
||||
customThinkingNode('next-primary', 'Next scoped thinking body'),
|
||||
] as any[]
|
||||
|
||||
export const NEXT_CUSTOM_ALT_NODES = [
|
||||
paragraphNode('Next isolated paragraph'),
|
||||
insightNode('Next isolated custom node', 'Next isolated child body'),
|
||||
customThinkingNode('next-alt', 'Next isolated thinking body'),
|
||||
] as any[]
|
||||
|
||||
export const SERVER_BASIC_NODES = [
|
||||
headingNode('Server Entry Basic'),
|
||||
paragraphNode('Server scoped paragraph'),
|
||||
{
|
||||
type: 'link',
|
||||
href: 'https://example.com/server',
|
||||
title: 'Server link',
|
||||
text: 'Server link',
|
||||
raw: '[Server link](https://example.com/server)',
|
||||
children: [textNode('Server link')],
|
||||
},
|
||||
tableNode(),
|
||||
{
|
||||
type: 'html_block',
|
||||
content: '<div data-ssr-status="server-basic-html">Server HTML block</div>',
|
||||
raw: '<div data-ssr-status="server-basic-html">Server HTML block</div>',
|
||||
},
|
||||
] as any[]
|
||||
|
||||
export const SERVER_CUSTOM_NODES = [
|
||||
paragraphNode('Server scoped paragraph'),
|
||||
insightNode('Server custom node', 'Server custom child body'),
|
||||
customThinkingNode('server-primary', 'Server scoped thinking body'),
|
||||
] as any[]
|
||||
|
||||
export const SERVER_CUSTOM_ALT_NODES = [
|
||||
paragraphNode('Server isolated paragraph'),
|
||||
insightNode('Server isolated custom node', 'Server isolated child body'),
|
||||
customThinkingNode('server-alt', 'Server isolated thinking body'),
|
||||
] as any[]
|
||||
|
||||
export const ENHANCED_DEMOS = {
|
||||
code: {
|
||||
type: 'code_block',
|
||||
language: 'ts',
|
||||
code: 'export const answer = 42\nconsole.log(answer)',
|
||||
raw: 'export const answer = 42\nconsole.log(answer)',
|
||||
loading: false,
|
||||
},
|
||||
markdown: {
|
||||
type: 'code_block',
|
||||
language: 'md',
|
||||
code: '# Nested markdown\n\n- item one\n- item two',
|
||||
raw: '# Nested markdown\n\n- item one\n- item two',
|
||||
loading: false,
|
||||
},
|
||||
mathBlock: {
|
||||
type: 'math_block',
|
||||
content: 'x^2 + y^2 = z^2',
|
||||
raw: '$$x^2 + y^2 = z^2$$',
|
||||
loading: false,
|
||||
},
|
||||
mathInline: {
|
||||
type: 'math_inline',
|
||||
content: 'a+b',
|
||||
raw: '$a+b$',
|
||||
markup: '$',
|
||||
loading: false,
|
||||
},
|
||||
mermaid: {
|
||||
type: 'code_block',
|
||||
language: 'mermaid',
|
||||
code: 'graph TD;A[Start]-->B[Done];',
|
||||
raw: 'graph TD;A[Start]-->B[Done];',
|
||||
loading: false,
|
||||
},
|
||||
d2: {
|
||||
type: 'code_block',
|
||||
language: 'd2',
|
||||
code: 'api -> db',
|
||||
raw: 'api -> db',
|
||||
loading: false,
|
||||
},
|
||||
infographic: {
|
||||
type: 'code_block',
|
||||
language: 'infographic',
|
||||
code: '{"title":"SSR Lab","children":[{"title":"Node A"},{"title":"Node B"}]}',
|
||||
raw: '{"title":"SSR Lab","children":[{"title":"Node A"},{"title":"Node B"}]}',
|
||||
loading: false,
|
||||
},
|
||||
} as const
|
||||
|
||||
export const FALLBACK_DEMOS = {
|
||||
nextPre: {
|
||||
type: 'code_block',
|
||||
language: 'ts',
|
||||
code: 'const stableFallback = true',
|
||||
raw: 'const stableFallback = true',
|
||||
loading: false,
|
||||
},
|
||||
mermaid: {
|
||||
type: 'code_block',
|
||||
language: 'mermaid',
|
||||
code: 'graph TD;Fallback-->Stable;',
|
||||
raw: 'graph TD;Fallback-->Stable;',
|
||||
loading: false,
|
||||
},
|
||||
d2: {
|
||||
type: 'code_block',
|
||||
language: 'd2',
|
||||
code: 'fallback -> stable',
|
||||
raw: 'fallback -> stable',
|
||||
loading: false,
|
||||
},
|
||||
infographic: {
|
||||
type: 'code_block',
|
||||
language: 'infographic',
|
||||
code: '{"title":"Fallback"}',
|
||||
raw: '{"title":"Fallback"}',
|
||||
loading: false,
|
||||
},
|
||||
} as const
|
||||
|
||||
export function createMatrixCases(entry: any, serverEntry: any) {
|
||||
const ctx = createRenderCtx(serverEntry)
|
||||
const withCtx = (props: Record<string, any>) => ({
|
||||
...props,
|
||||
ctx,
|
||||
renderNode: serverEntry.renderNode,
|
||||
indexKey: 'matrix',
|
||||
customId: 'matrix',
|
||||
})
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'NodeRenderer',
|
||||
element: React.createElement(entry.NodeRenderer, {
|
||||
content: '# Matrix Heading\n\nParagraph text',
|
||||
final: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'AdmonitionNode',
|
||||
element: React.createElement(entry.AdmonitionNode, withCtx({
|
||||
node: {
|
||||
type: 'admonition',
|
||||
kind: 'note',
|
||||
children: [paragraphNode('Alert body')],
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: 'BlockquoteNode',
|
||||
element: React.createElement(entry.BlockquoteNode, withCtx({
|
||||
node: {
|
||||
type: 'blockquote',
|
||||
children: [paragraphNode('Quoted body')],
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: 'CheckboxNode',
|
||||
element: React.createElement(entry.CheckboxNode, {
|
||||
node: { type: 'checkbox', checked: true },
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'CodeBlockNode',
|
||||
element: React.createElement(entry.CodeBlockNode, {
|
||||
node: {
|
||||
type: 'code_block',
|
||||
language: 'ts',
|
||||
code: 'const answer = 42',
|
||||
raw: 'const answer = 42',
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'ReactCodeBlockNode',
|
||||
element: React.createElement(entry.ReactCodeBlockNode, {
|
||||
node: {
|
||||
type: 'code_block',
|
||||
language: 'ts',
|
||||
code: 'const answer = 42',
|
||||
raw: 'const answer = 42',
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'MarkdownCodeBlockNode',
|
||||
element: React.createElement(entry.MarkdownCodeBlockNode, {
|
||||
node: {
|
||||
type: 'code_block',
|
||||
language: 'md',
|
||||
code: '# nested markdown',
|
||||
raw: '# nested markdown',
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'PreCodeNode',
|
||||
element: React.createElement(entry.PreCodeNode, {
|
||||
node: {
|
||||
type: 'code_block',
|
||||
language: 'js',
|
||||
code: 'const fallback = true',
|
||||
raw: 'const fallback = true',
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'D2BlockNode',
|
||||
element: React.createElement(entry.D2BlockNode, {
|
||||
node: {
|
||||
type: 'code_block',
|
||||
language: 'd2',
|
||||
code: 'x -> y',
|
||||
raw: 'x -> y',
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'MermaidBlockNode',
|
||||
element: React.createElement(entry.MermaidBlockNode, {
|
||||
node: {
|
||||
type: 'code_block',
|
||||
language: 'mermaid',
|
||||
code: 'graph TD;A-->B;',
|
||||
raw: 'graph TD;A-->B;',
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'InfographicBlockNode',
|
||||
element: React.createElement(entry.InfographicBlockNode, {
|
||||
node: {
|
||||
type: 'code_block',
|
||||
language: 'infographic',
|
||||
code: '{"title":"Demo"}',
|
||||
raw: '{"title":"Demo"}',
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'DefinitionListNode',
|
||||
element: React.createElement(entry.DefinitionListNode, withCtx({
|
||||
node: definitionListNode(),
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: 'EmojiNode',
|
||||
element: React.createElement(entry.EmojiNode, {
|
||||
node: { type: 'emoji', name: 'sparkles' },
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'EmphasisNode',
|
||||
element: React.createElement(entry.EmphasisNode, {
|
||||
node: { type: 'emphasis' },
|
||||
children: 'Emphasis',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'StrongNode',
|
||||
element: React.createElement(entry.StrongNode, {
|
||||
node: { type: 'strong' },
|
||||
children: 'Strong',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'StrikethroughNode',
|
||||
element: React.createElement(entry.StrikethroughNode, {
|
||||
node: { type: 'strikethrough' },
|
||||
children: 'Strike',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'HighlightNode',
|
||||
element: React.createElement(entry.HighlightNode, {
|
||||
node: { type: 'highlight' },
|
||||
children: 'Highlight',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'InsertNode',
|
||||
element: React.createElement(entry.InsertNode, {
|
||||
node: { type: 'insert' },
|
||||
children: 'Insert',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'SubscriptNode',
|
||||
element: React.createElement(entry.SubscriptNode, {
|
||||
node: { type: 'subscript' },
|
||||
children: 'Sub',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'SuperscriptNode',
|
||||
element: React.createElement(entry.SuperscriptNode, {
|
||||
node: { type: 'superscript' },
|
||||
children: 'Sup',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'FootnoteNode',
|
||||
element: React.createElement(entry.FootnoteNode, withCtx({
|
||||
node: {
|
||||
type: 'footnote',
|
||||
id: '1',
|
||||
children: [paragraphNode('Footnote body')],
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: 'FootnoteReferenceNode',
|
||||
element: React.createElement(entry.FootnoteReferenceNode, {
|
||||
node: { type: 'footnote_reference', id: '1' },
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'FootnoteAnchorNode',
|
||||
element: React.createElement(entry.FootnoteAnchorNode, {
|
||||
node: { type: 'footnote_anchor', id: '1' },
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'HardBreakNode',
|
||||
element: React.createElement(entry.HardBreakNode, {
|
||||
node: { type: 'hardbreak' },
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'HeadingNode',
|
||||
element: React.createElement(entry.HeadingNode, {
|
||||
node: { type: 'heading', level: 2 },
|
||||
children: 'Heading',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'ParagraphNode',
|
||||
element: React.createElement(entry.ParagraphNode, {
|
||||
node: { type: 'paragraph' },
|
||||
children: 'Paragraph',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'HtmlBlockNode',
|
||||
element: React.createElement(entry.HtmlBlockNode, {
|
||||
node: { type: 'html_block', content: '<div><strong>Block HTML</strong></div>' },
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'HtmlInlineNode',
|
||||
element: React.createElement(entry.HtmlInlineNode, {
|
||||
node: { type: 'html_inline', content: '<span>Inline HTML</span>' },
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'ImageNode',
|
||||
element: React.createElement(entry.ImageNode, {
|
||||
node: {
|
||||
type: 'image',
|
||||
src: DEMO_IMAGE_SRC,
|
||||
alt: 'Demo image',
|
||||
title: 'Demo image',
|
||||
raw: '',
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'InlineCodeNode',
|
||||
element: React.createElement(entry.InlineCodeNode, {
|
||||
node: { type: 'inline_code', code: 'inline-demo' },
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'LinkNode',
|
||||
element: React.createElement(entry.LinkNode, {
|
||||
node: {
|
||||
type: 'link',
|
||||
href: 'https://example.com',
|
||||
title: 'Example',
|
||||
text: 'Example',
|
||||
raw: '[Example](https://example.com)',
|
||||
children: [textNode('Example')],
|
||||
},
|
||||
children: 'Example',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'ListItemNode',
|
||||
element: React.createElement(entry.ListItemNode, {
|
||||
node: { type: 'list_item' },
|
||||
children: 'List item',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'ListNode',
|
||||
element: React.createElement(entry.ListNode, withCtx({
|
||||
node: {
|
||||
type: 'list',
|
||||
ordered: false,
|
||||
items: [listItemNode('List body')],
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: 'MathBlockNode',
|
||||
element: React.createElement(entry.MathBlockNode, {
|
||||
node: {
|
||||
type: 'math_block',
|
||||
content: 'x^2',
|
||||
raw: '$$x^2$$',
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'MathInlineNode',
|
||||
element: React.createElement(entry.MathInlineNode, {
|
||||
node: {
|
||||
type: 'math_inline',
|
||||
content: 'a+b',
|
||||
raw: '$a+b$',
|
||||
markup: '$',
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'ReferenceNode',
|
||||
element: React.createElement(entry.ReferenceNode, {
|
||||
node: { type: 'reference', id: 'ref-1' },
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'TableNode',
|
||||
element: React.createElement(entry.TableNode, withCtx({
|
||||
node: tableNode(),
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: 'TextNode',
|
||||
element: React.createElement(entry.TextNode, {
|
||||
node: { type: 'text', content: 'Text body' },
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'ThematicBreakNode',
|
||||
element: React.createElement(entry.ThematicBreakNode, {
|
||||
node: { type: 'thematic_break' },
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'Tooltip',
|
||||
element: React.createElement(entry.Tooltip, {
|
||||
visible: true,
|
||||
anchorEl: null,
|
||||
content: 'Tooltip body',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'HtmlPreviewFrame',
|
||||
element: React.createElement(entry.HtmlPreviewFrame, {
|
||||
code: '<p>Preview</p>',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'VmrContainerNode',
|
||||
element: React.createElement(entry.VmrContainerNode, withCtx({
|
||||
node: vmrContainerNode(),
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: 'FallbackComponent',
|
||||
element: React.createElement(entry.FallbackComponent, {
|
||||
node: { type: 'mystery' },
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import React from 'react'
|
||||
import { HydrationProbe, NextCustomRenderer, NextExportMatrix } from './ssr-lab-client'
|
||||
import {
|
||||
BASIC_NEXT_MARKDOWN,
|
||||
createMatrixCases,
|
||||
ENHANCED_DEMOS,
|
||||
FALLBACK_DEMOS,
|
||||
NEXT_CUSTOM_ALT_ID,
|
||||
NEXT_CUSTOM_ALT_NODES,
|
||||
NEXT_CUSTOM_ID,
|
||||
NEXT_CUSTOM_NODES,
|
||||
SERVER_BASIC_NODES,
|
||||
SERVER_CUSTOM_ALT_ID,
|
||||
SERVER_CUSTOM_ALT_NODES,
|
||||
SERVER_CUSTOM_ID,
|
||||
SERVER_CUSTOM_NODES,
|
||||
} from './ssr-lab-data'
|
||||
|
||||
function readAttr(node: any, name: string) {
|
||||
if (!node?.attrs)
|
||||
return undefined
|
||||
if (Array.isArray(node.attrs))
|
||||
return node.attrs.find((item: [string, string | null]) => item[0] === name)?.[1] ?? undefined
|
||||
return node.attrs[name]
|
||||
}
|
||||
|
||||
function renderCustomChildren(props: any) {
|
||||
if (!Array.isArray(props.node?.children) || !props.renderNode || !props.ctx)
|
||||
return null
|
||||
|
||||
return props.node.children.map((child: any, idx: number) => (
|
||||
<React.Fragment key={`${String(props.indexKey ?? 'custom')}-${idx}`}>
|
||||
{props.renderNode(child, `${String(props.indexKey ?? 'custom')}-${idx}`, props.ctx)}
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
|
||||
function ServerParagraphOverride({ node }: any) {
|
||||
return (
|
||||
<div data-ssr-status="server-paragraph-override">
|
||||
{node.raw}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ServerInsightNode(props: any) {
|
||||
return (
|
||||
<section data-ssr-status="server-custom-node">
|
||||
<strong>{props.node.label}</strong>
|
||||
<div data-ssr-status="server-custom-node-children">
|
||||
{renderCustomChildren(props)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function ServerThinkingTag({ node }: any) {
|
||||
return (
|
||||
<aside
|
||||
data-ssr-status="server-thinking-tag"
|
||||
data-ssr-tone={readAttr(node, 'data-tone')}
|
||||
data-ssr-scope={readAttr(node, 'data-scope')}
|
||||
>
|
||||
{node.content}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
function ServerParagraphAlt({ node }: any) {
|
||||
return (
|
||||
<div data-ssr-status="server-paragraph-alt">
|
||||
{node.raw}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ServerInsightAlt(props: any) {
|
||||
return (
|
||||
<section data-ssr-status="server-custom-node-alt">
|
||||
<strong>{props.node.label}</strong>
|
||||
<div data-ssr-status="server-custom-node-alt-children">
|
||||
{renderCustomChildren(props)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function ServerThinkingTagAlt({ node }: any) {
|
||||
return (
|
||||
<aside
|
||||
data-ssr-status="server-thinking-tag-alt"
|
||||
data-ssr-scope={readAttr(node, 'data-scope')}
|
||||
>
|
||||
{node.content}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
function MatrixSection({ caseName, entryName, items }: { caseName: string, entryName: string, items: Array<{ name: string, element: React.ReactElement }> }) {
|
||||
return (
|
||||
<article data-ssr-case={caseName} data-ssr-entry={entryName}>
|
||||
<h2>{entryName === 'next' ? 'Next Export Matrix' : 'Server Export Matrix'}</h2>
|
||||
<div className="ssr-matrix">
|
||||
{items.map(item => (
|
||||
<section key={item.name} data-ssr-export={item.name} className="ssr-card">
|
||||
<h3>{item.name}</h3>
|
||||
<div className="markstream-react ssr-matrix-renderer">{item.element}</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export function SsrLabPage({
|
||||
version,
|
||||
router,
|
||||
nextEntry,
|
||||
serverEntry,
|
||||
serverBasicHtml,
|
||||
serverCustomHtml,
|
||||
serverFallbackHtml,
|
||||
serverMatrixHtml,
|
||||
}: {
|
||||
version: string
|
||||
router: 'app' | 'pages'
|
||||
nextEntry: any
|
||||
serverEntry: any
|
||||
serverBasicHtml?: string
|
||||
serverCustomHtml?: string
|
||||
serverFallbackHtml?: string
|
||||
serverMatrixHtml?: string
|
||||
}) {
|
||||
serverEntry.setCustomComponents(SERVER_CUSTOM_ID, {
|
||||
paragraph: ServerParagraphOverride,
|
||||
insight: ServerInsightNode,
|
||||
thinking: ServerThinkingTag,
|
||||
})
|
||||
serverEntry.setCustomComponents(SERVER_CUSTOM_ALT_ID, {
|
||||
paragraph: ServerParagraphAlt,
|
||||
insight: ServerInsightAlt,
|
||||
thinking: ServerThinkingTagAlt,
|
||||
})
|
||||
|
||||
const serverMatrix = createMatrixCases(serverEntry, serverEntry)
|
||||
|
||||
return (
|
||||
<main data-ssr-version={version} data-ssr-router={router} className="ssr-lab-shell markstream-react">
|
||||
<header className="ssr-hero">
|
||||
<p className="ssr-eyebrow">
|
||||
{version}
|
||||
{' / '}
|
||||
{router}
|
||||
</p>
|
||||
<h1>Markstream React Next SSR Lab</h1>
|
||||
<p>Dual entry, dual router, raw HTML first, hydration afterwards.</p>
|
||||
</header>
|
||||
|
||||
<div className="ssr-grid">
|
||||
<article data-ssr-case="basic" data-ssr-entry="next">
|
||||
<h2>Basic / Next</h2>
|
||||
<nextEntry.NodeRenderer
|
||||
content={BASIC_NEXT_MARKDOWN}
|
||||
final
|
||||
showTooltips={false}
|
||||
/>
|
||||
</article>
|
||||
|
||||
<article data-ssr-case="basic" data-ssr-entry="server">
|
||||
<h2>Basic / Server</h2>
|
||||
{serverBasicHtml
|
||||
? <div dangerouslySetInnerHTML={{ __html: serverBasicHtml }} />
|
||||
: (
|
||||
<serverEntry.NodeRenderer
|
||||
customId={SERVER_CUSTOM_ID}
|
||||
customHtmlTags={['thinking']}
|
||||
nodes={SERVER_BASIC_NODES}
|
||||
final
|
||||
showTooltips={false}
|
||||
/>
|
||||
)}
|
||||
</article>
|
||||
|
||||
<article data-ssr-case="custom" data-ssr-entry="next">
|
||||
<h2>Custom / Next</h2>
|
||||
<section data-ssr-custom-id="next-primary" className="ssr-card">
|
||||
<NextCustomRenderer customId={NEXT_CUSTOM_ID} nodes={NEXT_CUSTOM_NODES} />
|
||||
</section>
|
||||
<section data-ssr-custom-id="next-alt" className="ssr-card">
|
||||
<NextCustomRenderer customId={NEXT_CUSTOM_ALT_ID} nodes={NEXT_CUSTOM_ALT_NODES} />
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<article data-ssr-case="custom" data-ssr-entry="server">
|
||||
<h2>Custom / Server</h2>
|
||||
{serverCustomHtml
|
||||
? <div dangerouslySetInnerHTML={{ __html: serverCustomHtml }} />
|
||||
: (
|
||||
<>
|
||||
<section data-ssr-custom-id="server-primary" className="ssr-card">
|
||||
<serverEntry.NodeRenderer
|
||||
customId={SERVER_CUSTOM_ID}
|
||||
customHtmlTags={['thinking']}
|
||||
nodes={SERVER_CUSTOM_NODES}
|
||||
final
|
||||
showTooltips={false}
|
||||
/>
|
||||
</section>
|
||||
<section data-ssr-custom-id="server-alt" className="ssr-card">
|
||||
<serverEntry.NodeRenderer
|
||||
customId={SERVER_CUSTOM_ALT_ID}
|
||||
customHtmlTags={['thinking']}
|
||||
nodes={SERVER_CUSTOM_ALT_NODES}
|
||||
final
|
||||
showTooltips={false}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
|
||||
<article data-ssr-case="enhanced" data-ssr-entry="next">
|
||||
<h2>Enhanced / Next</h2>
|
||||
<div data-ssr-demo="code" className="ssr-card">
|
||||
<nextEntry.CodeBlockNode node={ENHANCED_DEMOS.code as any} />
|
||||
</div>
|
||||
<div data-ssr-demo="markdown" className="ssr-card">
|
||||
<nextEntry.MarkdownCodeBlockNode node={ENHANCED_DEMOS.markdown as any} />
|
||||
</div>
|
||||
<div data-ssr-demo="math" className="ssr-card">
|
||||
<nextEntry.MathBlockNode node={ENHANCED_DEMOS.mathBlock as any} />
|
||||
<p>
|
||||
Inline math:
|
||||
{' '}
|
||||
<nextEntry.MathInlineNode node={ENHANCED_DEMOS.mathInline as any} />
|
||||
</p>
|
||||
</div>
|
||||
<div data-ssr-demo="mermaid" className="ssr-card">
|
||||
<nextEntry.MermaidBlockNode node={ENHANCED_DEMOS.mermaid as any} />
|
||||
</div>
|
||||
<div data-ssr-demo="d2" className="ssr-card">
|
||||
<nextEntry.D2BlockNode node={ENHANCED_DEMOS.d2 as any} />
|
||||
</div>
|
||||
<div data-ssr-demo="infographic" className="ssr-card">
|
||||
<nextEntry.InfographicBlockNode node={ENHANCED_DEMOS.infographic as any} />
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article data-ssr-case="fallback" data-ssr-entry="next">
|
||||
<h2>Fallback / Next</h2>
|
||||
<div data-ssr-demo="pre" className="ssr-card">
|
||||
<nextEntry.PreCodeNode node={FALLBACK_DEMOS.nextPre as any} />
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article data-ssr-case="fallback" data-ssr-entry="server">
|
||||
<h2>Fallback / Server</h2>
|
||||
{serverFallbackHtml
|
||||
? <div dangerouslySetInnerHTML={{ __html: serverFallbackHtml }} />
|
||||
: (
|
||||
<>
|
||||
<div data-ssr-demo="mermaid" className="ssr-card">
|
||||
<serverEntry.MermaidBlockNode node={FALLBACK_DEMOS.mermaid as any} />
|
||||
</div>
|
||||
<div data-ssr-demo="d2" className="ssr-card">
|
||||
<serverEntry.D2BlockNode node={FALLBACK_DEMOS.d2 as any} />
|
||||
</div>
|
||||
<div data-ssr-demo="infographic" className="ssr-card">
|
||||
<serverEntry.InfographicBlockNode node={FALLBACK_DEMOS.infographic as any} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
|
||||
<NextExportMatrix />
|
||||
{serverMatrixHtml
|
||||
? (
|
||||
<article data-ssr-case="server-export-matrix" data-ssr-entry="server">
|
||||
<h2>Server Export Matrix</h2>
|
||||
<div dangerouslySetInnerHTML={{ __html: serverMatrixHtml }} />
|
||||
</article>
|
||||
)
|
||||
: <MatrixSection caseName="server-export-matrix" entryName="server" items={serverMatrix} />}
|
||||
<HydrationProbe />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
@import 'katex/dist/katex.min.css';
|
||||
@import 'markstream-react/index.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html {
|
||||
background: #f4f1e8;
|
||||
color: #201a17;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.home-shell,
|
||||
.ssr-lab-shell {
|
||||
width: min(1200px, calc(100vw - 32px));
|
||||
margin: 0 auto;
|
||||
padding: 32px 0 64px;
|
||||
}
|
||||
|
||||
.ssr-hero {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.ssr-eyebrow {
|
||||
margin: 0 0 8px;
|
||||
font: 600 12px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ssr-grid {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
[data-ssr-case] {
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
border: 1px solid rgba(32, 26, 23, 0.14);
|
||||
border-radius: 18px;
|
||||
padding: 18px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ssr-card {
|
||||
border: 1px solid rgba(32, 26, 23, 0.12);
|
||||
border-radius: 14px;
|
||||
padding: 14px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ssr-card + .ssr-card {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.ssr-matrix {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.ssr-matrix-renderer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ssr-divider {
|
||||
height: 1px;
|
||||
margin: 16px 0;
|
||||
background: rgba(32, 26, 23, 0.12);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.home-shell,
|
||||
.ssr-lab-shell {
|
||||
width: min(100vw - 20px, 1200px);
|
||||
padding-top: 20px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: {
|
||||
relative: true,
|
||||
files: [
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./src/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
},
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"target": "ES2022",
|
||||
"jsx": "preserve",
|
||||
"lib": ["dom", "dom.iterable", "es2022"],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": false,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"skipLibCheck": true,
|
||||
"plugins": [{ "name": "next" }]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user