Files
boos e0a5f57793 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.
2026-06-22 11:32:57 +08:00

286 lines
10 KiB
Markdown

---
description: Migrate from react-markdown to markstream-react with guidance on renderer replacement, plugin audits, and non-1:1 customization gaps.
---
# Migrate from react-markdown
If your current React Markdown renderer is `react-markdown`, moving to `markstream-react` is usually a two-step change:
1. Replace the renderer component.
2. Migrate any custom components or plugin logic that was tied to the `remark` / `rehype` pipeline.
::: warning Not Always 1:1
`markstream-react` is not a drop-in replacement for every `react-markdown` setup. The simple cases migrate quickly, but apps with a long `remarkPlugins` / `rehypePlugins` chain should review each customization before switching.
:::
::: tip See a Live Demo
Open the [React migration demo](https://markstream-react.pages.dev/migration-demo) to inspect a synthetic before/after example alongside an audit report generated in the style of the migration skill.
:::
## When to stay on `react-markdown`
- You only render completed, static Markdown.
- Your app depends heavily on the unified `remark` / `rehype` plugin ecosystem.
- You need HTML allow/deny lists as first-class component props.
## When `markstream-react` is a better fit
- You render AI/chat/SSE output while text is still arriving.
- You want progressive Mermaid, D2, KaTeX, or Monaco-backed code blocks.
- You want viewport-aware heavy-node scheduling for large documents.
- You want to move from string-in/string-out rendering to a streaming-friendly AST model.
## Smallest possible replacement
```tsx
// before
import Markdown from 'react-markdown'
export function Article({ markdown }: { markdown: string }) {
return <Markdown>{markdown}</Markdown>
}
```
```tsx
// after
import MarkdownRender from 'markstream-react'
import 'markstream-react/index.css'
export function Article({ markdown }: { markdown: string }) {
return <MarkdownRender content={markdown} />
}
```
If that works for your current content, migrate the rest incrementally.
## Mental model shift
- `react-markdown` is HTML-tag oriented: custom rendering usually happens through `components={{ h1, a, code }}`.
- `markstream-react` is AST-node oriented: custom rendering usually happens through `setCustomComponents(...)` and node types such as `heading`, `link`, `code_block`, or `inline_code`.
- `react-markdown` centers the `remark` -> `rehype` pipeline.
- `markstream-react` centers a streaming-friendly parser AST plus node renderers.
This means `components.h1` does not become `components.h1` again. It usually becomes one `heading` renderer that branches on `node.level`.
## API mapping
| `react-markdown` | `markstream-react` | Notes |
|---|---|---|
| `children` | `content` | Pass the Markdown string through `content`. |
| `components` | `setCustomComponents(id?, mapping)` | `react-markdown` keys are HTML tags; `markstream-react` keys are node types such as `heading`, `link`, `paragraph`, `image`, `code_block`, `inline_code`. |
| `remarkPlugins` | `customMarkdownIt` | Use `markdown-it` plugins instead of `remark` plugins. Many common Markdown features already work without extra plugins. |
| `remarkPlugins={[remarkGfm]}` | Often removable | Tables, task checkboxes, strikethrough, code fences, and other common constructs are already supported by the parser. Re-check edge cases before deleting plugin code. |
| `rehypePlugins` | No direct equivalent | There is no public `rehype` stage. Use custom node renderers, `customHtmlTags`, `parseOptions`, or post-process `nodes` instead. |
| `rehypeRaw` | Usually not needed | HTML-like tags are already parsed. For custom tags, prefer `customHtmlTags={['thinking']}` plus `setCustomComponents`. |
| `skipHtml` | No direct prop | HTML nodes render by default, with built-in blocking of dangerous attributes/tags and unsafe URLs in the HTML renderers. If you must remove HTML entirely, prefilter the input or parsed nodes yourself. |
| `allowedElements` / `disallowedElements` / `allowElement` | No direct prop | Filter the parsed `nodes` tree yourself, or replace specific node renderers. |
| `unwrapDisallowed` | Manual node filtering | Implement this in your node post-processing step if you need it. |
| `urlTransform` | `parseOptions.validateLink` + custom `link` renderer | `validateLink` is for allow/deny. If you need URL rewriting, do it in a custom `link` renderer or while post-processing nodes. |
## Migrating `components`
`react-markdown` custom renderers are usually written against HTML-tag names:
```tsx
import Markdown from 'react-markdown'
<Markdown
components={{
h1({ children }) {
return <h1 className="docs-heading">{children}</h1>
},
a({ href, children }) {
return <a href={href} target="_blank" rel="noreferrer">{children}</a>
},
}}
>
{markdown}
</Markdown>
```
The `markstream-react` version is node-based:
```tsx
import type { NodeComponentProps } from 'markstream-react'
import MarkdownRender, { setCustomComponents } from 'markstream-react'
import 'markstream-react/index.css'
function CustomHeading({ node, ctx, renderNode, indexKey }: NodeComponentProps<any>) {
const Tag = `h${node.level || 1}` as keyof JSX.IntrinsicElements
return (
<Tag className="docs-heading">
{node.children?.map((child: any, i: number) =>
renderNode && ctx
? renderNode(child, `${String(indexKey)}-heading-${i}`, ctx)
: null,
)}
</Tag>
)
}
function CustomLink({ node, ctx, renderNode, indexKey }: NodeComponentProps<any>) {
return (
<a href={node.href} target="_blank" rel="noreferrer">
{node.children?.map((child: any, i: number) =>
renderNode && ctx
? renderNode(child, `${String(indexKey)}-link-${i}`, ctx)
: null,
)}
</a>
)
}
setCustomComponents('docs', {
heading: CustomHeading,
link: CustomLink,
})
export function Article({ markdown }: { markdown: string }) {
return <MarkdownRender customId="docs" content={markdown} />
}
```
Useful node-type translations:
- `h1` ... `h6` -> `heading` + `node.level`
- `a` -> `link`
- `p` -> `paragraph`
- `img` -> `image`
- `code` / `pre` -> `code_block` or `inline_code`
- Custom HTML-like tags -> `customHtmlTags` + `setCustomComponents`
## Migrating code highlighting
Many `react-markdown` apps override `components.code` and wire in a third-party highlighter.
With `markstream-react`, you have three common choices:
- Keep the default `CodeBlockNode` for Monaco-powered code blocks.
- Swap `code_block` to `MarkdownCodeBlockNode` for a lighter Shiki-based renderer.
- Set `renderCodeBlocksAsPre` when you want plain `<pre><code>`.
Example: swap code blocks to Shiki:
```tsx
import MarkdownRender, { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-react'
setCustomComponents('docs', {
code_block: ({ node, isDark, ctx }: any) => (
<MarkdownCodeBlockNode
node={node}
isDark={isDark}
stream={ctx?.codeBlockStream}
{...(ctx?.codeBlockProps || {})}
/>
),
})
export function Article({ markdown }: { markdown: string }) {
return <MarkdownRender customId="docs" content={markdown} />
}
```
## Migrating plugin logic
### `remarkPlugins`
If you used `remarkPlugins` for syntax extensions, the nearest equivalent is `customMarkdownIt`.
```tsx
import type { MarkdownIt } from 'markdown-it-ts'
import { full as markdownItEmoji } from 'markdown-it-emoji'
import MarkdownRender from 'markstream-react'
function withEmoji(md: MarkdownIt) {
md.use(markdownItEmoji)
return md
}
export function Article({ markdown }: { markdown: string }) {
return <MarkdownRender content={markdown} customMarkdownIt={withEmoji} />
}
```
### `rehypePlugins`
There is no direct `rehype` step in `markstream-react`.
If your old `rehype` logic was used for:
- HTML tree rewrites: move that logic into custom node renderers or node post-processing.
- Custom HTML-like blocks: prefer `customHtmlTags`.
- Security allow/deny logic: use `parseOptions.validateLink`, custom renderers, or node filtering.
## HTML, custom tags, and security
`react-markdown` usually escapes HTML unless you add something like `rehypeRaw`.
`markstream-react` already parses HTML-like nodes, and its built-in HTML renderers strip blocked tags, dangerous event-handler attributes, and unsafe URLs.
If your old `rehypeRaw` usage was mainly there to support custom tags such as `<thinking>`, this is the recommended migration path:
```tsx
import type { NodeComponentProps } from 'markstream-react'
import MarkdownRender, { setCustomComponents } from 'markstream-react'
function ThinkingNode({ node }: NodeComponentProps<any>) {
return <aside className="thinking-box">{node.content}</aside>
}
setCustomComponents('chat', { thinking: ThinkingNode })
export function Message({ markdown }: { markdown: string }) {
return (
<MarkdownRender
customId="chat"
content={markdown}
customHtmlTags={['thinking']}
/>
)
}
```
## Streaming upgrade path
You do not need to adopt streaming on day one. A practical migration path is:
1. Replace `react-markdown` with `MarkdownRender` and keep using `content`.
2. Migrate custom renderers and plugin logic.
3. If you later render SSE/chat output, parse outside the component and pass `nodes`.
```tsx
import MarkdownRender from 'markstream-react'
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
const md = getMarkdown('chat')
const nodes = parseMarkdownToStructure(buffer, md, { final: done })
export function Message() {
return (
<MarkdownRender
nodes={nodes}
viewportPriority
deferNodesUntilVisible
/>
)
}
```
For high-frequency streaming, this `nodes` flow is usually the biggest reason to migrate.
## Migration checklist
- Replace `<Markdown>{markdown}</Markdown>` with `<MarkdownRender content={markdown} />`.
- Import `markstream-react/index.css`.
- Move `components` overrides into `setCustomComponents(customId, mapping)`.
- Remove plugins that are now redundant before re-adding custom ones.
- Re-check any `rehype`-based HTML filtering or transformation logic.
- If your app renders incremental output, move from `content` to parsed `nodes`.
## Related docs
- [React Installation](/guide/react-installation)
- [React Components & API](/guide/react-components)
- [Parser API](/guide/parser-api)