forked from boos/BoosAPI
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,285 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user