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:
boos
2026-06-22 11:32:57 +08:00
parent d9c78ecc73
commit e0a5f57793
2305 changed files with 449355 additions and 0 deletions
+743
View File
@@ -0,0 +1,743 @@
# stream-monaco
[![NPM version](https://img.shields.io/npm/v/stream-monaco?color=a1b858&label=)](https://www.npmjs.com/package/stream-monaco)
[![中文版](https://img.shields.io/badge/docs-中文文档-blue)](README.zh-CN.md)
[![NPM downloads](https://img.shields.io/npm/dm/stream-monaco)](https://www.npmjs.com/package/stream-monaco)
[![Bundle size](https://img.shields.io/bundlephobia/minzip/stream-monaco)](https://bundlephobia.com/package/stream-monaco)
[![License](https://img.shields.io/npm/l/stream-monaco)](./LICENSE)
Streaming Monaco Editor integration with Shiki syntax highlighting. Framework-agnostic core for Vue, React, Svelte, Solid, Preact, and Vanilla JS.
### Introduction
stream-monaco provides a framework-agnostic core for integrating Monaco Editor with Shiki syntax highlighting, optimized for streaming updates and efficient highlighting. It works great without Vue, while also offering a Vue-friendly API and examples.
IMPORTANT: Since v0.0.32, `updateCode` is time-throttled by default (`updateThrottleMs = 50`) to reduce CPU usage under high-frequency streaming. Set `updateThrottleMs: 0` in `useMonaco()` options to restore previous RAF-only behavior.
Note: Internally, reactivity now uses a thin adapter over `alien-signals`, so Vue is no longer a hard requirement at runtime for the core logic. Vue remains supported, but is an optional peer dependency. This makes the package more portable in non-Vue environments while keeping the same API.
### Features
- 🚀 Works without Vue (framework-agnostic core)
- 🌿 Ready to use with Vue 3 Composition API
- 🔁 Use in any framework: Vue, React, Svelte, Solid, Preact, or plain JS/TS
- 🎨 Shiki highlighting with TextMate grammars and VS Code themes
- 🌓 Dark/Light theme switching
- 📝 Streaming updates (append/minimal-edit)
- 🔀 Diff editor with efficient incremental updates
- 🗑️ Auto cleanup to avoid memory leaks
- 🔧 Highly configurable (all Monaco options)
- 🎯 Full TypeScript support
### Quick API overview
The package exports helpers around theme/highlighter for advanced use:
- `registerMonacoThemes(themes, languages): Promise<Highlighter>` — create or reuse a Shiki highlighter and register themes to Monaco. Returns a Promise resolving to the highlighter for reuse (e.g., rendering snippets).
- `getOrCreateHighlighter(themes, languages): Promise<Highlighter>` — get or create a highlighter (managed by internal cache). If you need to call `codeToHtml` or `setTheme` manually, use this and handle loading/errors.
Note:
- If the target theme is already included in the `themes` you passed to `useMonaco()`, calling `monaco.editor.setTheme(themeName)` is fine.
- If you switch to a theme that was not pre-registered (e.g. dynamic theme name like `andromeeda`), prefer `await setTheme(themeName)` from `useMonaco()`. It will ensure the theme is registered, and when possible it will also try to `loadTheme` on the underlying Shiki highlighter to avoid "Theme not found, you may need to load it first" errors.
Config: `useMonaco()` does not auto-sync an external Shiki highlighter; if you need external Shiki snippets to follow theme changes, call `getOrCreateHighlighter(...)` and `highlighter.setTheme(...)` yourself.
### API Reference
#### useMonaco(options?)
##### Parameters
| Parameter | Type | Default | Description |
| --------------------------- | ------------------------------------------------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------ |
| `MAX_HEIGHT` | `number \| string` | `500` | Maximum editor height |
| `readOnly` | `boolean` | `true` | Whether the editor is read-only |
| `themes` | `MonacoTheme[]` | `['vitesse-dark', 'vitesse-light']` | Theme list; should include at least one dark and one light theme |
| `languages` | `MonacoLanguage[]` | built-in default language list | Languages to register |
| `theme` | `string` | - | Initial theme name |
| `isCleanOnBeforeCreate` | `boolean` | `true` | Whether to dispose previously registered resources before creating a new editor |
| `onBeforeCreate` | `function` | - | Hook called before editor creation |
| `autoScrollOnUpdate` | `boolean` | `true` | Auto-scroll when content updates and the viewport is already near the bottom |
| `autoScrollInitial` | `boolean` | `true` | Whether auto-scroll starts enabled |
| `autoScrollThresholdPx` | `number` | `32` | Pixel threshold for considering the viewport "near bottom" |
| `autoScrollThresholdLines` | `number` | `2` | Line-based threshold for considering the viewport "near bottom" |
| `diffAutoScroll` | `boolean` | `true` | Whether diff auto-scroll is enabled for the modified side |
| `diffHideUnchangedRegions` | `boolean \| object` | `true` | Fold unchanged regions in diff mode; accepts Monaco `hideUnchangedRegions` config too |
| `diffLineStyle` | `'background' \| 'bar'` | `'background'` | Controls whether changed lines are emphasized more as filled blocks or review-style bars |
| `diffAppearance` | `'auto' \| 'light' \| 'dark'` | `'auto'` | Controls the diff shell chrome appearance while token colors still follow the active theme |
| `diffUnchangedRegionStyle` | `'line-info' \| 'line-info-basic' \| 'metadata' \| 'simple'` | `'line-info'` | Controls how collapsed unchanged regions are rendered |
| `diffHunkActionsOnHover` | `boolean` | `false` | Enables split upper/lower hunk hover actions: `Revert` / `Stage` |
| `diffHunkHoverHideDelayMs` | `number` | `160` | Hide delay for hunk hover action widgets |
| `onDiffHunkAction` | `function` | - | Hunk action callback; return `false` to skip built-in edits |
| `revealDebounceMs` | `number` | `75` | Debounce for auto-reveal during streaming updates |
| `revealStrategy` | `'bottom' \| 'centerIfOutside' \| 'center'` | `'centerIfOutside'` | Reveal strategy used when auto-scrolling |
| `revealBatchOnIdleMs` | `number \| undefined` | - | Final idle-time reveal window for append-heavy scenarios |
| `updateThrottleMs` | `number` | `50` | Time-based throttle for `updateCode` |
| `diffUpdateThrottleMs` | `number` | `50` | Time-based throttle for diff streaming updates |
| `minimalEditMaxChars` | `number` | built-in constant | Fallback to `setValue` when documents are too large for minimal-edit diffing |
| `minimalEditMaxChangeRatio` | `number` | built-in constant | Fallback to `setValue` when the change ratio is too large |
`MonacoOptions` also includes Monaco's native editor and diff-editor construction options, so you can pass options such as `renderSideBySide`, `ignoreTrimWhitespace`, `originalEditable`, or `enableSplitViewResizing` directly.
##### Returns
| Method / property | Type | Description |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `createEditor` | `(container: HTMLElement, code: string, language: string) => Promise<MonacoEditorInstance>` | Create and mount a standalone editor |
| `createDiffEditor` | `(container: HTMLElement, original: string, modified: string, language: string) => Promise<MonacoDiffEditorInstance>` | Create and mount a diff editor |
| `cleanupEditor` | `() => void` | Dispose editor instances and cleanup resources |
| `safeClean` | `() => void` | Cancel pending update queues/RAFs without fully tearing down the integration |
| `updateCode` | `(newCode: string, codeLanguage: string) => void` | Update single-editor content and language |
| `appendCode` | `(appendText: string, codeLanguage?: string) => void` | Append to the single editor, optimized for streaming |
| `updateDiff` | `(original: string, modified: string, codeLanguage?: string) => void` | Update both sides of a diff editor |
| `updateOriginal` | `(newCode: string, codeLanguage?: string) => void` | Update only the original side |
| `updateModified` | `(newCode: string, codeLanguage?: string) => void` | Update only the modified side |
| `appendOriginal` | `(appendText: string, codeLanguage?: string) => void` | Append to the original side |
| `appendModified` | `(appendText: string, codeLanguage?: string) => void` | Append to the modified side |
| `setDiffModels` | `(models: DiffModelPair, options?: DiffModelTransitionOptions) => Promise<void>` | Swap the whole diff model pair in place; same-content swaps prewarm Monaco's diff view model and preserve view state by default |
| `setTheme` | `(theme: MonacoTheme, force?: boolean) => Promise<void>` | Switch editor theme; `force=true` re-applies even if the theme is already active |
| `refreshDiffPresentation` | `() => void` | Recompute diff chrome / unchanged overlay presentation without remounting |
| `setLanguage` | `(language: MonacoLanguage) => void` | Switch editor language |
| `getCurrentTheme` | `() => string` | Get the current theme name |
| `getEditor` | `() => typeof monaco.editor` | Get Monaco's static editor API |
| `getEditorView` | `() => MonacoEditorInstance \| null` | Get the current standalone editor instance |
| `getDiffEditorView` | `() => MonacoDiffEditorInstance \| null` | Get the current diff editor instance |
| `getDiffModels` | `() => DiffModels` | Get both diff models |
| `getMonacoInstance` | `() => typeof monaco` | Get the Monaco module instance |
| `setUpdateThrottleMs` | `(ms: number) => void` | Change `updateCode` throttling at runtime |
| `getUpdateThrottleMs` | `() => number` | Get the current `updateCode` throttle value |
| `getCode` | `() => MonacoCodeValue` | Get the latest code value from the current editor or diff editor |
##### Exported TypeScript helpers
The package exports named types for diff integration too:
- `UseMonacoReturn`
- `MonacoOptions`
- `MonacoTheme`
- `MonacoLanguage`
- `DiffHideUnchangedRegions`
- `DiffLineStyle`
- `DiffAppearance`
- `DiffUnchangedRegionStyle`
- `DiffModels`
- `DiffModelPair`
- `DiffModelTransitionOptions`
- `DiffCodeValue`
- `MonacoCodeValue`
- `DiffHunkActionContext`
For a more complete integration walkthrough, see [Diff Integration Guide](docs/diff-integration.md).
#### Diff streaming highlight tip
Monaco's diff computation is async and cancels/restarts when models change. If you stream updates too frequently (e.g. per token / every frame), the diff may only finish once streaming stops, so the difference highlights appear "at the end".
- Set `diffUpdateThrottleMs` to let the diff worker complete intermediate computations during streaming.
- The library defaults to `50ms`.
- When `diffHideUnchangedRegions` is enabled, unchanged-region folding is deferred until diff streaming goes idle to avoid collapsing and re-expanding while content is still arriving.
- Set it to `0` to restore pure RAF batching (most responsive, but may delay diff highlights under heavy streaming).
### Install
```bash
pnpm add stream-monaco
# or
npm install stream-monaco
# or
yarn add stream-monaco
```
Note: Vue is optional. If you don't use Vue, you don't need to install it.
### Example apps
- `examples/streaming-demo` — Vue 3 + Vite demo with streaming, diff, and diff UX routes
- `examples/react-demo` — React + Vite demo with in-place theme/language switching and `appendCode()` streaming
Quick start:
```bash
pnpm -C examples/streaming-demo dev
pnpm -C examples/react-demo dev
```
### Basic usage (Vue)
```vue
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import { useMonaco } from 'stream-monaco'
const props = defineProps<{
code: string
language: string
}>()
const codeEditor = ref<HTMLElement>()
const { createEditor, updateCode, cleanupEditor } = useMonaco({
themes: ['vitesse-dark', 'vitesse-light'],
languages: ['javascript', 'typescript', 'vue', 'python'],
readOnly: false,
MAX_HEIGHT: 600,
})
onMounted(async () => {
if (codeEditor.value) {
await createEditor(codeEditor.value, props.code, props.language)
}
})
watch(
() => [props.code, props.language],
([newCode, newLanguage]) => {
updateCode(newCode, newLanguage)
},
)
</script>
<template>
<div ref="codeEditor" class="monaco-editor-container" />
</template>
<style scoped>
.monaco-editor-container {
border: 1px solid #e0e0e0;
border-radius: 4px;
}
</style>
```
### Basic usage (React)
```tsx
import { useEffect, useRef } from 'react'
import { useMonaco } from 'stream-monaco'
export function MonacoEditor() {
const containerRef = useRef<HTMLDivElement>(null)
const { createEditor, cleanupEditor } = useMonaco({
themes: ['vitesse-dark', 'vitesse-light'],
languages: ['typescript', 'javascript'],
})
useEffect(() => {
if (containerRef.current) {
createEditor(
containerRef.current,
'console.log("Hello, Monaco!")',
'typescript',
)
}
return () => cleanupEditor()
}, [])
return (
<div
ref={containerRef}
style={{ height: 500, border: '1px solid #e0e0e0' }}
/>
)
}
```
Note: Svelte, Solid, and Preact integrations follow the same pattern — create a container element, call `createEditor` on mount, and `cleanupEditor` on unmount.
### Full config example (Vue)
```vue
<script setup lang="ts">
import type { MonacoLanguage, MonacoTheme } from 'stream-monaco'
import { onMounted, ref } from 'vue'
import { useMonaco } from 'stream-monaco'
const editorContainer = ref<HTMLElement>()
const {
createEditor,
updateCode,
setTheme,
setLanguage,
getCurrentTheme,
getEditor,
getEditorView,
getCode,
cleanupEditor,
} = useMonaco({
themes: ['github-dark', 'github-light'],
languages: ['javascript', 'typescript', 'python', 'vue', 'json'],
MAX_HEIGHT: 500,
readOnly: false,
isCleanOnBeforeCreate: true,
onBeforeCreate: (monaco) => {
console.log('Monaco editor is about to be created', monaco)
return []
},
fontSize: 14,
lineNumbers: 'on',
wordWrap: 'on',
minimap: { enabled: false },
scrollbar: {
verticalScrollbarSize: 10,
horizontalScrollbarSize: 10,
alwaysConsumeMouseWheel: false,
},
revealDebounceMs: 75,
})
onMounted(async () => {
if (editorContainer.value) {
const editor = await createEditor(
editorContainer.value,
'console.log("Hello, Monaco!")',
'javascript',
)
console.log('Editor created:', editor)
}
})
async function switchTheme(theme: MonacoTheme) {
await setTheme(theme)
// await setTheme(theme, true) // force re-apply even if same
}
function switchLanguage(language: MonacoLanguage) {
setLanguage(language)
}
function updateEditorCode(code: string, language: string) {
updateCode(code, language)
}
const currentTheme = getCurrentTheme()
console.log('Current theme:', currentTheme)
const monacoEditor = getEditor()
console.log('Monaco editor API:', monacoEditor)
const editorInstance = getEditorView()
console.log('Editor instance:', editorInstance)
// Get current code from editor (useful after user manually edits)
function getCurrentCode() {
const code = getCode()
if (code) {
console.log('Current code:', code)
return code
}
return null
}
</script>
<template>
<div>
<div class="controls">
<button @click="switchTheme('github-dark')">Dark</button>
<button @click="switchTheme('github-light')">Light</button>
<button @click="switchLanguage('typescript')">TypeScript</button>
<button @click="switchLanguage('python')">Python</button>
</div>
<div ref="editorContainer" class="editor" />
</div>
</template>
```
### Get current code (getCode)
After creating an editor, you can retrieve the current code content at any time using `getCode()`. This is especially useful when users manually edit the editor content:
```vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useMonaco } from 'stream-monaco'
const container = ref<HTMLElement>()
const { createEditor, updateCode, getCode, cleanupEditor } = useMonaco({
themes: ['vitesse-dark', 'vitesse-light'],
languages: ['javascript', 'typescript'],
})
onMounted(async () => {
if (container.value) {
await createEditor(container.value, 'console.log("hello")', 'javascript')
}
})
// Get current code after updates or user edits
function handleSubmit() {
const currentCode = getCode()
if (currentCode) {
console.log('Submitting code:', currentCode)
// Send to API, save to storage, etc.
}
}
// Update code programmatically
function replaceCode() {
updateCode('console.log("world")', 'javascript')
// Get the new code
setTimeout(() => {
const newCode = getCode()
console.log('Updated code:', newCode)
}, 100)
}
</script>
<template>
<div>
<div ref="container" class="editor" />
<button @click="handleSubmit">Submit Code</button>
<button @click="replaceCode">Replace Code</button>
</div>
</template>
```
For Diff editors, `getCode()` returns both sides:
```ts
const { createDiffEditor, getCode } = useMonaco()
await createDiffEditor(container, 'old code', 'new code', 'javascript')
const codes = getCode()
// codes = { original: 'old code', modified: 'new code' }
```
### Diff editor quick start (Vue)
```vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useMonaco } from 'stream-monaco'
const container = ref<HTMLElement>()
const {
createDiffEditor,
updateDiff,
updateOriginal,
updateModified,
getDiffEditorView,
cleanupEditor,
} = useMonaco({
themes: ['vitesse-dark', 'vitesse-light'],
languages: ['javascript', 'typescript'],
readOnly: true,
MAX_HEIGHT: 500,
// Collapse long unchanged regions automatically.
diffHideUnchangedRegions: true,
// Hover a changed hunk to show split upper/lower Revert/Stage actions.
diffHunkActionsOnHover: true,
})
const original = `export function add(a: number, b: number) {\n return a + b\n}`
const modified = `export function add(a: number, b: number) {\n return a + b\n}\n\nexport function sub(a: number, b: number) {\n return a - b\n}`
onMounted(async () => {
if (container.value)
await createDiffEditor(container.value, original, modified, 'typescript')
})
</script>
<template>
<div ref="container" class="diff-editor" />
</template>
```
Diff UX options:
- `diffHideUnchangedRegions` (default `true`): fold unchanged ranges (can pass Monaco `hideUnchangedRegions` object).
- `diffLineStyle` (default `background`): choose the changed-line emphasis style. Use `bar` for a subtler review-style leading bar treatment.
- `diffAppearance` (default `auto`): controls the diff shell chrome; use `light` / `dark` to force the surface style while keeping token colors theme-driven.
- `diffUnchangedRegionStyle` (default `line-info`): choose collapsed unchanged-region rendering: `line-info`, `line-info-basic`, and `metadata` use compact 32px rows, while `line-info-basic` keeps the wider legacy rail and `simple` uses a tighter 28px row.
- `diffHunkActionsOnHover` (default `false`): explicitly set `true` to enable split upper/lower `Revert` and `Stage` on hunk hover.
- `onDiffHunkAction(context)` (optional): return `false` to intercept and skip built-in model edits.
- Built-in `Revert` / `Stage` only edit Monaco models locally. They do not run `git revert`, `git add`, or `git stash` unless you intercept `onDiffHunkAction` and connect your own backend flow.
Full integration guide:
- [Diff Integration Guide](docs/diff-integration.md)
- exported TS helpers include `UseMonacoReturn`, `DiffLineStyle`, `DiffAppearance`, `DiffUnchangedRegionStyle`, `DiffModels`, and `DiffHunkActionContext`
Example 1: enable unchanged folding + hover Revert/Stage
```ts
const { createDiffEditor } = useMonaco({
themes: ['vitesse-dark', 'vitesse-light'],
languages: ['typescript'],
readOnly: true,
diffHideUnchangedRegions: {
enabled: true,
contextLineCount: 2,
minimumLineCount: 4,
revealLineCount: 2,
},
diffLineStyle: 'bar',
diffHunkActionsOnHover: true,
})
await createDiffEditor(container, original, modified, 'typescript')
```
Example 2: fully intercept Revert/Stage and handle your own Git/stash/patch flow
```ts
useMonaco({
diffHideUnchangedRegions: true,
diffHunkActionsOnHover: true,
onDiffHunkAction: async (ctx) => {
const {
action, // 'revert' | 'stage'
side, // 'upper' | 'lower'
lineChange,
originalModel,
modifiedModel,
} = ctx
await saveHunkAction({
action,
side,
range: lineChange,
original: originalModel.getValue(),
modified: modifiedModel.getValue(),
})
// false => skip built-in edit, fully controlled by user code
// true/undefined => continue built-in edit
return false
},
})
```
Example 3: backend returns refreshed file contents, then the client calls `updateDiff(...)`
```ts
const monaco = useMonaco({
diffHideUnchangedRegions: true,
diffHunkActionsOnHover: true,
onDiffHunkAction: async (ctx) => {
const response = await fetch('/api/git/hunks/apply', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
action: ctx.action,
side: ctx.side,
range: ctx.lineChange,
original: ctx.originalModel.getValue(),
modified: ctx.modifiedModel.getValue(),
}),
})
const next = await response.json()
monaco.updateDiff(next.original, next.modified, 'typescript')
return false
},
})
```
Important:
- Hover `Revert` is hunk-level undo, not `git revert <commit>`.
- Hover `Stage` is closer to `git add -p` / `git apply --cached`.
- If you want a stash flow, treat `onDiffHunkAction` as a patch-intent callback and hand the selected half-hunk to your backend stash or patch queue.
- If your backend returns refreshed file contents, `updateDiff(...)` is the most direct way to reflect the Git-applied result in the diff UI. The repo includes `pnpm run validate:diff-hunk-update-diff-flow` for that pattern.
- See [Diff Integration Guide](docs/diff-integration.md) for a concrete Git binding pattern.
Visual review helpers:
- `pnpm run shot:diff-ux -- background /tmp/stream-monaco-diff-reference.png pierre-reference`
Captures the Pierre-like reference scene with a fixed viewport, file chrome, and `-1 / +1` single-hunk diff. Pass an optional fourth argument such as `snazzy-light`.
- `pnpm run compare:diff-ux -- /tmp/stream-monaco-diff-reference.png background pierre-reference`
Re-captures the same scene and prints screenshot metrics such as `diffPixels`, `mismatchRatio`, and `exactMatch`. Pass an optional final theme argument to compare the exact same palette.
### Shiki highlighter (advanced)
If you also render Shiki snippets outside Monaco:
```ts
import { registerMonacoThemes } from 'stream-monaco'
const highlighter = await registerMonacoThemes(allThemes, allLanguages)
// later on theme switch
monaco.editor.setTheme('vitesse-dark')
await highlighter.setTheme('vitesse-dark')
// re-render snippets via highlighter.codeToHtml(...)
```
### Streaming performance tips
After 0.0.32, more fine-grained controls:
- `updateThrottleMs` (default 50): time-based throttle for `updateCode`. Set 0 for RAF-only.
- `minimalEditMaxChars`: cap for attempting minimal replace before falling back to `setValue`.
- `minimalEditMaxChangeRatio`: fallback to full replace when change ratio is high.
```ts
useMonaco({
updateThrottleMs: 50,
minimalEditMaxChars: 200000,
minimalEditMaxChangeRatio: 0.25,
})
```
Auto-reveal options for streaming append:
- `revealDebounceMs` (default 75)
- `revealBatchOnIdleMs` (optional final reveal)
- `revealStrategy`: "bottom" | "centerIfOutside" (default) | "center"
For pure tail-append, prefer explicit `appendCode` / `appendOriginal` / `appendModified`.
### Best practices
1. Performance: only load required languages
```ts
const { createEditor } = useMonaco({
languages: ['javascript', 'typescript'],
themes: ['vitesse-dark', 'vitesse-light'],
})
```
2. Memory management: dispose on unmount
```vue
<script setup>
import { onUnmounted } from 'vue'
import { useMonaco } from 'stream-monaco'
const { cleanupEditor } = useMonaco()
onUnmounted(() => {
cleanupEditor()
})
</script>
```
3. Follow system theme (via your own dark state) and call `setTheme` accordingly.
### Use without Vue (Vanilla)
You can use the core in any environment. Here's a plain TypeScript/HTML example:
```ts
import { useMonaco } from 'stream-monaco'
const container = document.getElementById('editor')!
const { createEditor, updateCode, setTheme, cleanupEditor } = useMonaco({
themes: ['vitesse-dark', 'vitesse-light'],
languages: ['javascript', 'typescript'],
MAX_HEIGHT: 500,
})
await createEditor(container, 'console.log("Hello")', 'javascript')
updateCode('console.log("World")', 'javascript')
await setTheme('vitesse-light')
// later
cleanupEditor()
```
```html
<div id="editor" style="height: 500px; border: 1px solid #e5e7eb;"></div>
<script type="module" src="/main.ts"></script>
```
### Migration notes
- v0.0.34+: Internal reactivity is implemented via a thin adapter over `alien-signals`, removing the hard dependency on Vue. Vue remains fully supported but is optional. No breaking changes to the public API.
### Troubleshooting
- Editor invisible after build: configure Monaco web workers correctly.
- Diff editor renders blank during early mount/streaming: ensure Monaco workers are configured before `createEditor`/`createDiffEditor` (e.g. call `preloadMonacoWorkers()` as early as possible).
- Theme not applied: ensure theme name is included in `themes`.
- Language highlighting missing: ensure the language is included and supported by Shiki.
#### Vue CLI 4 (Webpack 4)
Webpack 4 cannot parse `import.meta.url`. Use the `stream-monaco/legacy` entry (no `import.meta`) and configure Monaco workers in your app.
Recommended: use `monaco-editor-webpack-plugin`.
```bash
pnpm add -D monaco-editor-webpack-plugin
```
`vue.config.js`:
```js
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
module.exports = {
configureWebpack: {
plugins: [
new MonacoWebpackPlugin({
languages: ['json', 'css', 'html', 'typescript'],
}),
],
},
}
```
And call once early (e.g. `main.ts`):
```ts
import { preloadMonacoWorkers } from 'stream-monaco/legacy'
preloadMonacoWorkers()
```
The default `stream-monaco` and `stream-monaco/legacy` entry modules intentionally run a best-effort Monaco worker setup during import. The package metadata marks only those published entry files as side-effectful so tree-shaking stays correct for the rest of the library.
If you load Monaco via CDN/AMD (e.g. `<script src=".../vs/loader.js">`), `stream-monaco/legacy` also includes a best-effort auto worker setup that creates a same-origin `blob:` worker and `importScripts()` Monaco’s `vs/base/worker/workerMain.js`. If auto-detection can’t find your Monaco base URL, call:
```ts
import { ensureMonacoWorkersLegacy } from 'stream-monaco/legacy'
ensureMonacoWorkersLegacy({
baseUrl: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/',
})
```
### Development
```bash
git clone https://github.com/Simon-He95/stream-monaco.git
pnpm install
pnpm dev
pnpm build
```
### :coffee:
[buy me a cup of coffee](https://github.com/Simon-He95/sponsor)
### License
[MIT](./license)
### Sponsors
<p align="center">
<a href="https://cdn.jsdelivr.net/gh/Simon-He95/sponsor/sponsors.svg">
<img src="https://cdn.jsdelivr.net/gh/Simon-He95/sponsor/sponsors.png"/>
</a>
</p>
## Acknowledgements
### Clearing shiki highlighter cache
The library caches Shiki highlighters internally to avoid recreating them for the same theme combinations. In long-running apps that dynamically create many combinations, you can clear the cache to free memory or fully reset the shared highlighter state (e.g., in tests or on shutdown):
- `clearHighlighterCache()` — clears cached highlighters and resets the shared Monaco/Shiki highlighter state
- `getHighlighterCacheSize()` — returns number of cached entries
Call `clearHighlighterCache()` only when highlighters are no longer needed; otherwise, the cache improves performance by reusing instances.