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.
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 you only use Monaco and pass all themes
to createEditor
, typically just call monaco.editor.setTheme(themeName)
.
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.
The useMonaco()
function returns an object with the following methods:
createEditor(container, code, language)
- Create and mount Monaco editor to a containercreateDiffEditor(container, originalCode, modifiedCode, language)
- Create and mount Diff editorcleanupEditor()
- Destroy editor and cleanup resourcesgetEditorView()
- Get current editor instance (IStandaloneCodeEditor | null)getDiffEditorView()
- Get current Diff editor instance (IStandaloneDiffEditor | null)getEditor()
- Get Monaco's static editor object for calling static methodsupdateCode(newCode, language)
- Update editor content and language (incremental update when possible)appendCode(appendText, language?)
- Append text to the end of editor (optimized for streaming)getCode()
- Get current code from editorstring
for normal editor{ original: string, modified: string }
for diff editornull
if no editor existsupdateDiff(originalCode, modifiedCode, language?)
- Update both sides of diff editorupdateOriginal(newCode, language?)
- Update only the original sideupdateModified(newCode, language?)
- Update only the modified sideappendOriginal(appendText, language?)
- Append to original side (streaming)appendModified(appendText, language?)
- Append to modified side (streaming)getDiffModels()
- Get both diff models: { original, modified }
setTheme(theme)
- Switch editor theme (returns Promise)setLanguage(language)
- Switch editor languagegetCurrentTheme()
- Get current theme namesetUpdateThrottleMs(ms)
- Change update throttle at runtimegetUpdateThrottleMs()
- Get current throttle valuepnpm 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.
<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>
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.
<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>
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:
<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:
const { createDiffEditor, getCode } = useMonaco()
await createDiffEditor(container, 'old code', 'new code', 'javascript')
const codes = getCode()
// codes = { original: 'old code', modified: 'new code' }
<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,
})
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>
If you also render Shiki snippets outside Monaco:
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(...)
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.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
.
const { createEditor } = useMonaco({
languages: ['javascript', 'typescript'],
themes: ['vitesse-dark', 'vitesse-light'],
})
<script setup>
import { onUnmounted } from 'vue'
import { useMonaco } from 'stream-monaco'
const { cleanupEditor } = useMonaco()
onUnmounted(() => {
cleanupEditor()
})
</script>
setTheme
accordingly.You can use the core in any environment. Here's a plain TypeScript/HTML example:
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()
<div id="editor" style="height: 500px; border: 1px solid #e5e7eb;"></div>
<script type="module" src="/main.ts"></script>
The library also exposes isDark
(a small reactive ref) that follows <html class="dark">
or the system color-scheme. Theme switching inside the editor is handled automatically.
alien-signals
, removing the hard dependency on Vue. Vue remains fully supported but is optional. No breaking changes to the public API.themes
.git clone https://github.com/Simon-He95/stream-monaco.git
pnpm install
pnpm dev
pnpm build
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 reset state (e.g., in tests or on shutdown):
clearHighlighterCache()
— clears the internal cachegetHighlighterCacheSize()
— returns number of cached entriesCall clearHighlighterCache()
only when highlighters are no longer needed; otherwise, the cache improves performance by reusing instances.