Monaco Editor Integration
Monaco integration is provided by stream-monaco and is optional. It supports fast, incremental updates for large code blocks.
Install:
pnpm add stream-monacoUse CodeBlockNode (default) to render Monaco-powered code blocks. For read-only usage, use MarkdownCodeBlockNode.
Tips:
- Defer Monaco initialization for offscreen code blocks
- Use
codeBlockStream: falseto avoid partial updates if desired - Preload via
preloadCodeBlockRuntime()during app idle or route setup when code blocks are likely - No additional CSS import is required
Vue CLI (Webpack 4) limitation
stream-monaco relies on import.meta.url to locate Monaco worker assets, which Webpack 4 cannot compile reliably. For Vue CLI 4 (Webpack 4):
- Prefer upgrading to Webpack 5 or migrating to Vite for the best Monaco experience.
- If you must stay on Webpack 4, prefer switching code blocks to the Shiki-based renderer (
stream-markdown) by overridingcode_block(see /guide/code-blocks). You can also explicitly ignore Monaco deps invue.config.js:
// vue.config.js
const webpack = require('webpack')
module.exports = {
configureWebpack: {
plugins: [
new webpack.IgnorePlugin({ resourceRegExp: /^stream-monaco$/ }),
new webpack.IgnorePlugin({ resourceRegExp: /^monaco-editor$/ }),
],
},
}Vite & worker setup
Monaco requires worker packaging for production builds. Use vite-plugin-monaco-editor-esm to ensure workers are bundled into your app's build output. Example config:
import path from 'node:path'
import { defineConfig } from 'vite'
import monacoEditorPlugin from 'vite-plugin-monaco-editor-esm'
export default defineConfig({
plugins: [
monacoEditorPlugin({
languageWorkers: [
'editorWorkerService',
'typescript',
'css',
'html',
'json',
],
customDistPath(root, buildOutDir, base) {
return path.resolve(buildOutDir, 'monacoeditorwork')
},
}),
],
})Preloading Monaco
To avoid a cold-start fallback flash when the first code block mounts, preload the code block runtime during app initialization, app idle time, or on first route mount:
import { preloadCodeBlockRuntime } from 'markstream-vue'
void preloadCodeBlockRuntime()preloadCodeBlockRuntime() dynamically imports stream-monaco, registers Monaco workers through markstream-vue's runtime path, and marks the code block runtime as ready so later CodeBlockNode remounts can skip the <pre> loading fallback.
Existing code that calls getUseMonaco() can stay as-is; it now has the same runtime-ready side effect and still returns the loaded stream-monaco module or null.
Webpack & MonacoWebpackPlugin
If your app bundles Monaco with monaco-editor-webpack-plugin, let the plugin own worker resolution via globalThis.MonacoEnvironment. markstream-vue will not override MonacoEnvironment.getWorker/getWorkerUrl when they already exist.
Quick try — Render a monaco-enabled code block (after installing stream-monaco):
<script setup>
import { CodeBlockNode } from 'markstream-vue'
const node = { type: 'code_block', language: 'js', raw: 'console.log(123)', code: 'console.log(123)' }
</script>
<template>
<CodeBlockNode :node="node" />
</template>Add extra languages & themes
Only a minimal set of Monaco languages ships with the default integration to keep the first render fast. When you need additional grammars (Rust, Go, Bash, etc.) or want to ship your own VS Code themes, pass them through monacoOptions — either directly on CodeBlockNode or globally via MarkdownRender's codeBlockMonacoOptions prop. The object is forwarded to useMonaco() unchanged.
languagesis not appended to the built-in defaults fromstream-monaco; providing this array replaces the internaldefaultLanguages. Include every language you need (even the original ones) whenever you override it.
Only ts twoslash and vue twoslash fences in this docs site enable hoverable type details. Hover languages, themes, theme, MAX_HEIGHT, or onDiffHunkAction below for the useful payloads.
<script setup lang="ts">
import type { CodeBlockMonacoOptions, CodeBlockMonacoTheme } from 'markstream-vue'
import MarkdownRender from 'markstream-vue'
const docsDark: CodeBlockMonacoTheme = {
name: 'docs-dark',
base: 'vs-dark',
inherit: true,
colors: {
'editor.background': '#05060a',
},
rules: [],
}
const docsLight: CodeBlockMonacoTheme = {
name: 'docs-light',
base: 'vs',
inherit: true,
colors: {
'editor.background': '#ffffff',
},
rules: [],
}
const monacoOptions = {
languages: ['javascript', 'python', 'rust', 'shell'],
themes: [docsDark, docsLight],
theme: 'docs-dark',
MAX_HEIGHT: 640,
} satisfies CodeBlockMonacoOptions
const markdown = `
\`\`\`python
print("extra languages go here")
\`\`\`
\`\`\`rust
fn main() {}
\`\`\`
`
</script>
<template>
<MarkdownRender
custom-id="docs"
:content="markdown"
:code-block-monaco-options="monacoOptions"
/>
</template>Each entry in
languagescan be a Monaco language id string or the loader signature thatstream-monacodocuments (for lazy language bundles). When not usingMarkdownRender, pass the samemonacoOptionsobject directly toCodeBlockNodevia:monaco-options.
Diff hover actions
Diff code blocks can show hover action buttons for each hunk split (Revert / Stage). These options are also passed through monacoOptions / codeBlockMonacoOptions:
import type { CodeBlockDiffHunkActionContext, CodeBlockMonacoOptions } from 'markstream-vue'
const monacoOptions = {
diffHunkActionsOnHover: true,
diffHunkHoverHideDelayMs: 240,
onDiffHunkAction(context: CodeBlockDiffHunkActionContext) {
console.log(context.action, context.side, context.lineChange)
// Return false to prevent stream-monaco's built-in model edits.
return false
},
} satisfies CodeBlockMonacoOptionsdiffHunkActionsOnHover: enable the hover buttonsdiffHunkHoverHideDelayMs: control how long the hover widget stays visible after mouse leaveonDiffHunkAction: interceptrevert/stagebefore the default edit runs
Full example
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
import { ref } from 'vue'
const actionLogs = ref<string[]>([])
const monacoOptions = {
diffHunkActionsOnHover: true,
diffHunkHoverHideDelayMs: 240,
onDiffHunkAction(context) {
actionLogs.value = [
`${context.action}:${context.side}`,
...actionLogs.value,
].slice(0, 6)
// Prevent built-in edits so the demo stays stable while you inspect events.
return false
},
}
const markdown = [
'```diff json:package.json',
'{',
' "name": "markstream-vue",',
'- "version": "0.0.49",',
'+ "version": "0.0.54-beta.1",',
' "packageManager": "pnpm@10.16.1"',
'}',
'```',
].join('\\n')
</script>
<template>
<MarkdownRender
:content="markdown"
:code-block-monaco-options="monacoOptions"
/>
<ul>
<li v-for="(item, index) in actionLogs" :key="`${item}-${index}`">
{{ item }}
</li>
</ul>
</template>Hover the changed red/green hunk area to reveal the Revert / Stage buttons. Clicking either button will call onDiffHunkAction.
The current action labels are
RevertandStage(notStash).In
markstream-vue, these options should be provided in the initialmonacoOptionsobject used to create the editor. If you need to toggle them at runtime, remount the code block so the Monaco diff editor is recreated with the new options.