stream-monaco 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +399 -0
- package/README.zh-CN.md +714 -0
- package/cli.mjs +2 -0
- package/dist/chunk-CHLpw0oG.js +25 -0
- package/dist/index.cjs +2079 -0
- package/dist/index.d.cts +282 -0
- package/dist/index.d.ts +281 -0
- package/dist/index.js +2043 -0
- package/license +21 -0
- package/package.json +100 -0
package/README.md
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
## stream-monaco
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/stream-monaco)
|
|
4
|
+
[](README.zh-CN.md)
|
|
5
|
+
[](https://www.npmjs.com/package/stream-monaco)
|
|
6
|
+
[](https://bundlephobia.com/package/stream-monaco)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
### Introduction
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- 🚀 Works without Vue (framework-agnostic core)
|
|
20
|
+
- 🌿 Ready to use with Vue 3 Composition API
|
|
21
|
+
- 🔁 Use in any framework: Vue, React, Svelte, Solid, Preact, or plain JS/TS
|
|
22
|
+
- 🎨 Shiki highlighting with TextMate grammars and VS Code themes
|
|
23
|
+
- 🌓 Dark/Light theme switching
|
|
24
|
+
- 📝 Streaming updates (append/minimal-edit)
|
|
25
|
+
- 🔀 Diff editor with efficient incremental updates
|
|
26
|
+
- 🗑️ Auto cleanup to avoid memory leaks
|
|
27
|
+
- 🔧 Highly configurable (all Monaco options)
|
|
28
|
+
- 🎯 Full TypeScript support
|
|
29
|
+
|
|
30
|
+
### Quick API overview
|
|
31
|
+
|
|
32
|
+
The package exports helpers around theme/highlighter for advanced use:
|
|
33
|
+
|
|
34
|
+
- `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).
|
|
35
|
+
- `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.
|
|
36
|
+
|
|
37
|
+
Note: If you only use Monaco and pass all `themes` to `createEditor`, typically just call `monaco.editor.setTheme(themeName)`.
|
|
38
|
+
|
|
39
|
+
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.
|
|
40
|
+
|
|
41
|
+
### Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pnpm add stream-monaco
|
|
45
|
+
# or
|
|
46
|
+
npm install stream-monaco
|
|
47
|
+
# or
|
|
48
|
+
yarn add stream-monaco
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Note: Vue is optional. If you don't use Vue, you don't need to install it.
|
|
52
|
+
|
|
53
|
+
### Basic usage (Vue)
|
|
54
|
+
|
|
55
|
+
```vue
|
|
56
|
+
<script setup lang="ts">
|
|
57
|
+
import { onMounted, ref, watch } from 'vue'
|
|
58
|
+
import { useMonaco } from 'stream-monaco'
|
|
59
|
+
|
|
60
|
+
const props = defineProps<{
|
|
61
|
+
code: string
|
|
62
|
+
language: string
|
|
63
|
+
}>()
|
|
64
|
+
|
|
65
|
+
const codeEditor = ref<HTMLElement>()
|
|
66
|
+
|
|
67
|
+
const { createEditor, updateCode, cleanupEditor } = useMonaco({
|
|
68
|
+
themes: ['vitesse-dark', 'vitesse-light'],
|
|
69
|
+
languages: ['javascript', 'typescript', 'vue', 'python'],
|
|
70
|
+
readOnly: false,
|
|
71
|
+
MAX_HEIGHT: 600,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
onMounted(async () => {
|
|
75
|
+
if (codeEditor.value) {
|
|
76
|
+
await createEditor(codeEditor.value, props.code, props.language)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
watch(
|
|
81
|
+
() => [props.code, props.language],
|
|
82
|
+
([newCode, newLanguage]) => {
|
|
83
|
+
updateCode(newCode, newLanguage)
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<template>
|
|
89
|
+
<div ref="codeEditor" class="monaco-editor-container" />
|
|
90
|
+
</template>
|
|
91
|
+
|
|
92
|
+
<style scoped>
|
|
93
|
+
.monaco-editor-container {
|
|
94
|
+
border: 1px solid #e0e0e0;
|
|
95
|
+
border-radius: 4px;
|
|
96
|
+
}
|
|
97
|
+
</style>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Basic usage (React)
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
import { useEffect, useRef } from 'react'
|
|
104
|
+
import { useMonaco } from 'stream-monaco'
|
|
105
|
+
|
|
106
|
+
export function MonacoEditor() {
|
|
107
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
108
|
+
const { createEditor, cleanupEditor } = useMonaco({
|
|
109
|
+
themes: ['vitesse-dark', 'vitesse-light'],
|
|
110
|
+
languages: ['typescript', 'javascript'],
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (containerRef.current)
|
|
115
|
+
createEditor(containerRef.current, 'console.log("Hello, Monaco!")', 'typescript')
|
|
116
|
+
return () => cleanupEditor()
|
|
117
|
+
}, [])
|
|
118
|
+
|
|
119
|
+
return <div ref={containerRef} style={{ height: 500, border: '1px solid #e0e0e0' }} />
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Note: Svelte, Solid, and Preact integrations follow the same pattern — create a container element, call `createEditor` on mount, and `cleanupEditor` on unmount.
|
|
124
|
+
|
|
125
|
+
### Full config example (Vue)
|
|
126
|
+
|
|
127
|
+
```vue
|
|
128
|
+
<script setup lang="ts">
|
|
129
|
+
import type { MonacoLanguage, MonacoTheme } from 'stream-monaco'
|
|
130
|
+
import { onMounted, ref } from 'vue'
|
|
131
|
+
import { useMonaco } from 'stream-monaco'
|
|
132
|
+
|
|
133
|
+
const editorContainer = ref<HTMLElement>()
|
|
134
|
+
|
|
135
|
+
const {
|
|
136
|
+
createEditor,
|
|
137
|
+
updateCode,
|
|
138
|
+
setTheme,
|
|
139
|
+
setLanguage,
|
|
140
|
+
getCurrentTheme,
|
|
141
|
+
getEditor,
|
|
142
|
+
getEditorView,
|
|
143
|
+
cleanupEditor,
|
|
144
|
+
} = useMonaco({
|
|
145
|
+
themes: ['github-dark', 'github-light'],
|
|
146
|
+
languages: ['javascript', 'typescript', 'python', 'vue', 'json'],
|
|
147
|
+
MAX_HEIGHT: 500,
|
|
148
|
+
readOnly: false,
|
|
149
|
+
isCleanOnBeforeCreate: true,
|
|
150
|
+
onBeforeCreate: (monaco) => {
|
|
151
|
+
console.log('Monaco editor is about to be created', monaco)
|
|
152
|
+
return []
|
|
153
|
+
},
|
|
154
|
+
fontSize: 14,
|
|
155
|
+
lineNumbers: 'on',
|
|
156
|
+
wordWrap: 'on',
|
|
157
|
+
minimap: { enabled: false },
|
|
158
|
+
scrollbar: {
|
|
159
|
+
verticalScrollbarSize: 10,
|
|
160
|
+
horizontalScrollbarSize: 10,
|
|
161
|
+
alwaysConsumeMouseWheel: false,
|
|
162
|
+
},
|
|
163
|
+
revealDebounceMs: 75,
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
onMounted(async () => {
|
|
167
|
+
if (editorContainer.value) {
|
|
168
|
+
const editor = await createEditor(
|
|
169
|
+
editorContainer.value,
|
|
170
|
+
'console.log("Hello, Monaco!")',
|
|
171
|
+
'javascript',
|
|
172
|
+
)
|
|
173
|
+
console.log('Editor created:', editor)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
async function switchTheme(theme: MonacoTheme) {
|
|
178
|
+
await setTheme(theme)
|
|
179
|
+
// await setTheme(theme, true) // force re-apply even if same
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function switchLanguage(language: MonacoLanguage) {
|
|
183
|
+
setLanguage(language)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function updateEditorCode(code: string, language: string) {
|
|
187
|
+
updateCode(code, language)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const currentTheme = getCurrentTheme()
|
|
191
|
+
console.log('Current theme:', currentTheme)
|
|
192
|
+
|
|
193
|
+
const monacoEditor = getEditor()
|
|
194
|
+
console.log('Monaco editor API:', monacoEditor)
|
|
195
|
+
|
|
196
|
+
const editorInstance = getEditorView()
|
|
197
|
+
console.log('Editor instance:', editorInstance)
|
|
198
|
+
</script>
|
|
199
|
+
|
|
200
|
+
<template>
|
|
201
|
+
<div>
|
|
202
|
+
<div class="controls">
|
|
203
|
+
<button @click="switchTheme('github-dark')">
|
|
204
|
+
Dark
|
|
205
|
+
</button>
|
|
206
|
+
<button @click="switchTheme('github-light')">
|
|
207
|
+
Light
|
|
208
|
+
</button>
|
|
209
|
+
<button @click="switchLanguage('typescript')">
|
|
210
|
+
TypeScript
|
|
211
|
+
</button>
|
|
212
|
+
<button @click="switchLanguage('python')">
|
|
213
|
+
Python
|
|
214
|
+
</button>
|
|
215
|
+
</div>
|
|
216
|
+
<div ref="editorContainer" class="editor" />
|
|
217
|
+
</div>
|
|
218
|
+
</template>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Diff editor quick start (Vue)
|
|
222
|
+
|
|
223
|
+
```vue
|
|
224
|
+
<script setup lang="ts">
|
|
225
|
+
import { onMounted, ref } from 'vue'
|
|
226
|
+
import { useMonaco } from 'stream-monaco'
|
|
227
|
+
|
|
228
|
+
const container = ref<HTMLElement>()
|
|
229
|
+
|
|
230
|
+
const {
|
|
231
|
+
createDiffEditor,
|
|
232
|
+
updateDiff,
|
|
233
|
+
updateOriginal,
|
|
234
|
+
updateModified,
|
|
235
|
+
getDiffEditorView,
|
|
236
|
+
cleanupEditor,
|
|
237
|
+
} = useMonaco({
|
|
238
|
+
themes: ['vitesse-dark', 'vitesse-light'],
|
|
239
|
+
languages: ['javascript', 'typescript'],
|
|
240
|
+
readOnly: true,
|
|
241
|
+
MAX_HEIGHT: 500,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
const original = `export function add(a: number, b: number) {\n return a + b\n}`
|
|
245
|
+
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}`
|
|
246
|
+
|
|
247
|
+
onMounted(async () => {
|
|
248
|
+
if (container.value)
|
|
249
|
+
await createDiffEditor(container.value, original, modified, 'typescript')
|
|
250
|
+
})
|
|
251
|
+
</script>
|
|
252
|
+
|
|
253
|
+
<template>
|
|
254
|
+
<div ref="container" class="diff-editor" />
|
|
255
|
+
</template>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Shiki highlighter (advanced)
|
|
259
|
+
|
|
260
|
+
If you also render Shiki snippets outside Monaco:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import { registerMonacoThemes } from 'stream-monaco'
|
|
264
|
+
|
|
265
|
+
const highlighter = await registerMonacoThemes(allThemes, allLanguages)
|
|
266
|
+
|
|
267
|
+
// later on theme switch
|
|
268
|
+
monaco.editor.setTheme('vitesse-dark')
|
|
269
|
+
await highlighter.setTheme('vitesse-dark')
|
|
270
|
+
// re-render snippets via highlighter.codeToHtml(...)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Streaming performance tips
|
|
274
|
+
|
|
275
|
+
After 0.0.32, more fine-grained controls:
|
|
276
|
+
|
|
277
|
+
- `updateThrottleMs` (default 50): time-based throttle for `updateCode`. Set 0 for RAF-only.
|
|
278
|
+
- `minimalEditMaxChars`: cap for attempting minimal replace before falling back to `setValue`.
|
|
279
|
+
- `minimalEditMaxChangeRatio`: fallback to full replace when change ratio is high.
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
useMonaco({
|
|
283
|
+
updateThrottleMs: 50,
|
|
284
|
+
minimalEditMaxChars: 200000,
|
|
285
|
+
minimalEditMaxChangeRatio: 0.25,
|
|
286
|
+
})
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Auto-reveal options for streaming append:
|
|
290
|
+
|
|
291
|
+
- `revealDebounceMs` (default 75)
|
|
292
|
+
- `revealBatchOnIdleMs` (optional final reveal)
|
|
293
|
+
- `revealStrategy`: "bottom" | "centerIfOutside" (default) | "center"
|
|
294
|
+
|
|
295
|
+
For pure tail-append, prefer explicit `appendCode` / `appendOriginal` / `appendModified`.
|
|
296
|
+
|
|
297
|
+
### Best practices
|
|
298
|
+
|
|
299
|
+
1) Performance: only load required languages
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
const { createEditor } = useMonaco({
|
|
303
|
+
languages: ['javascript', 'typescript'],
|
|
304
|
+
themes: ['vitesse-dark', 'vitesse-light'],
|
|
305
|
+
})
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
2) Memory management: dispose on unmount
|
|
309
|
+
|
|
310
|
+
```vue
|
|
311
|
+
<script setup>
|
|
312
|
+
import { onUnmounted } from 'vue'
|
|
313
|
+
import { useMonaco } from 'stream-monaco'
|
|
314
|
+
|
|
315
|
+
const { cleanupEditor } = useMonaco()
|
|
316
|
+
|
|
317
|
+
onUnmounted(() => {
|
|
318
|
+
cleanupEditor()
|
|
319
|
+
})
|
|
320
|
+
</script>
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
3) Follow system theme (via your own dark state) and call `setTheme` accordingly.
|
|
324
|
+
|
|
325
|
+
### Use without Vue (Vanilla)
|
|
326
|
+
|
|
327
|
+
You can use the core in any environment. Here's a plain TypeScript/HTML example:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
import { useMonaco } from 'stream-monaco'
|
|
331
|
+
|
|
332
|
+
const container = document.getElementById('editor')!
|
|
333
|
+
|
|
334
|
+
const { createEditor, updateCode, setTheme, cleanupEditor } = useMonaco({
|
|
335
|
+
themes: ['vitesse-dark', 'vitesse-light'],
|
|
336
|
+
languages: ['javascript', 'typescript'],
|
|
337
|
+
MAX_HEIGHT: 500,
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
await createEditor(container, 'console.log("Hello")', 'javascript')
|
|
341
|
+
updateCode('console.log("World")', 'javascript')
|
|
342
|
+
await setTheme('vitesse-light')
|
|
343
|
+
|
|
344
|
+
// later
|
|
345
|
+
cleanupEditor()
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
```html
|
|
349
|
+
<div id="editor" style="height: 500px; border: 1px solid #e5e7eb;"></div>
|
|
350
|
+
<script type="module" src="/main.ts"></script>
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
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.
|
|
354
|
+
|
|
355
|
+
### Migration notes
|
|
356
|
+
|
|
357
|
+
- 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.
|
|
358
|
+
|
|
359
|
+
### Troubleshooting
|
|
360
|
+
|
|
361
|
+
- Editor invisible after build: configure Monaco web workers correctly.
|
|
362
|
+
- Theme not applied: ensure theme name is included in `themes`.
|
|
363
|
+
- Language highlighting missing: ensure the language is included and supported by Shiki.
|
|
364
|
+
|
|
365
|
+
### Development
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
git clone https://github.com/Simon-He95/stream-monaco.git
|
|
369
|
+
pnpm install
|
|
370
|
+
pnpm dev
|
|
371
|
+
pnpm build
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### :coffee:
|
|
375
|
+
|
|
376
|
+
[buy me a cup of coffee](https://github.com/Simon-He95/sponsor)
|
|
377
|
+
|
|
378
|
+
### License
|
|
379
|
+
|
|
380
|
+
[MIT](./license)
|
|
381
|
+
|
|
382
|
+
### Sponsors
|
|
383
|
+
|
|
384
|
+
<p align="center">
|
|
385
|
+
<a href="https://cdn.jsdelivr.net/gh/Simon-He95/sponsor/sponsors.svg">
|
|
386
|
+
<img src="https://cdn.jsdelivr.net/gh/Simon-He95/sponsor/sponsors.png"/>
|
|
387
|
+
</a>
|
|
388
|
+
</p>
|
|
389
|
+
|
|
390
|
+
## Acknowledgements
|
|
391
|
+
|
|
392
|
+
### Clearing shiki highlighter cache
|
|
393
|
+
|
|
394
|
+
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):
|
|
395
|
+
|
|
396
|
+
- `clearHighlighterCache()` — clears the internal cache
|
|
397
|
+
- `getHighlighterCacheSize()` — returns number of cached entries
|
|
398
|
+
|
|
399
|
+
Call `clearHighlighterCache()` only when highlighters are no longer needed; otherwise, the cache improves performance by reusing instances.
|