Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/core/monaco-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@
"@kong-ui-public/i18n": "workspace:^",
"@kong/icons": "^1.41.0",
"@kong/kongponents": ">=9.42.0 <10.0.0",
"monaco-editor": ">=0.55.0 <1.0.0",
"monaco-editor": ">=0.52.0 <1.0.0",
"shiki": ">=3.0.0 <4.0.0",
"vue": "^3.5.26"
},
"devDependencies": {
Expand All @@ -75,9 +76,12 @@
"@kong/icons": "^1.41.0",
"@kong/kongponents": "9.48.7",
"monaco-editor": "^0.55.1",
"shiki": "^3.20.0",
"vue": "^3.5.26"
},
"dependencies": {
"@vueuse/core": "^14.1.0"
"@shikijs/monaco": "^3.20.0",
"@vueuse/core": "^14.1.0",
"shiki-codegen": "^3.20.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@ import { describe, it, expect, vi } from 'vitest'
import { useMonacoEditor } from './useMonacoEditor'
import { mount } from '@vue/test-utils'

const code = ref('initial code')

const dummyComponent = defineComponent({
setup() {
const target = ref<HTMLElement | null>(null)
const editorApi = useMonacoEditor(target, {
code,
language: 'javascript',
})
return { target, editorApi }
},
template: '<div ref="target" />',
})


describe('useMonacoEditor', () => {
beforeEach(() => {
vi.clearAllMocks()
Expand All @@ -12,29 +27,12 @@ describe('useMonacoEditor', () => {
const el = document.createElement('div')
document.body.appendChild(el)

const code = ref('initial code')

// define a simple dummy component using the composable
const wrapper = mount(
defineComponent({
setup() {
const target = ref<HTMLElement | null>(null)
const editorApi = useMonacoEditor(target, {
code,
language: 'javascript',
onChanged: vi.fn(),
onCreated: vi.fn(),
})
return { target, editorApi }
},
template: '<div ref="target" />',
}),
)
const wrapper = mount(dummyComponent)

// wait for next tick so lifecycle hooks run
await nextTick()

const { editorApi } = wrapper.vm as any
const { editorApi } = wrapper.vm

// ensure methods exist
expect(editorApi).toHaveProperty('setContent')
Expand Down
81 changes: 47 additions & 34 deletions packages/core/monaco-editor/src/composables/useMonacoEditor.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,50 @@
import { onActivated, onBeforeUnmount, onMounted, reactive, toValue, watch } from 'vue'
import { onActivated, onBeforeUnmount, onMounted, reactive, toValue, watch, ref } from 'vue'
import { DEFAULT_MONACO_OPTIONS } from '../constants'
import { unrefElement, useDebounceFn } from '@vueuse/core'

import * as monaco from 'monaco-editor'

import type * as monacoType from 'monaco-editor'
import { shikiToMonaco } from '@shikijs/monaco'
import { getSingletonHighlighter, bundledLanguages, bundledThemes } from 'shiki'

import type { MaybeComputedElementRef, MaybeElement } from '@vueuse/core'
import type { MonacoEditorStates, UseMonacoEditorOptions } from '../types'

// singletons
/** The Monaco instance once loaded */
let monacoInstance: typeof monacoType | undefined = undefined

// cache
const langCache = new Map<string, boolean>()
// Flag if monaco loaded
const isMonacoLoaded = ref(false)
let initPromise: Promise<void> | null = null

/**
* Lazily load Monaco and configure workers only once.
*/
function loadMonaco(language?: string): typeof monacoType {
if (!monacoInstance) {
monacoInstance = monaco
async function loadMonaco() {
if (initPromise) {
return initPromise
}

// TODO: register more languages as needed

// register language once
if (language && !langCache.get(language)) {
langCache.set(language, true)

if (!monaco.languages.getLanguages().some(lang => lang.id === language)) {
monaco.languages.register({ id: language })
initPromise = (async () => {
try {
// @ts-ignore jsonDefaults location varies across Monaco Editor versions
// v0.55.0 introduced breaking changes and issues; Konnect still uses v0.52.x.
const jsonDefaults = monaco.json?.jsonDefaults || monaco.languages.json?.jsonDefaults
// Disable JSON token provider to prevent conflicts with @shikijs/monaco
// https://github.com/shikijs/shiki/issues/865#issuecomment-3689158990
jsonDefaults?.setModeConfiguration({ tokens: false })

const highlighter = await getSingletonHighlighter(
{
themes: Object.values(bundledThemes),
langs: Object.values(bundledLanguages),
},
)
highlighter.getLoadedLanguages().forEach(lang => {
monaco.languages.register({ id: lang })
})
shikiToMonaco(highlighter, monaco)
isMonacoLoaded.value = true
} catch (error) {
initPromise = null
throw error
}
}
})()

return monacoInstance
return initPromise
}

/**
Expand All @@ -53,7 +62,7 @@ export function useMonacoEditor<T extends MaybeElement>(
* @type {monaco.editor.IStandaloneCodeEditor | undefined}
* @default undefined
*/
let editor: monacoType.editor.IStandaloneCodeEditor | undefined
let editor: monaco.editor.IStandaloneCodeEditor | undefined

// Internal flag to prevent multiple setups
let _isSetup = false
Expand Down Expand Up @@ -112,34 +121,38 @@ export function useMonacoEditor<T extends MaybeElement>(
}

/** Remeasure fonts in the editor with debouncing to optimize performance */
const remeasureFonts = useDebounceFn(() => monacoInstance?.editor.remeasureFonts(), 200)
const remeasureFonts = useDebounceFn(() => monaco.editor.remeasureFonts(), 200)


const init = (): void => {
const monaco = loadMonaco(options.language)
loadMonaco()

// we want to create our model before creating the editor so we don't end up with multiple models for the same editor (v-if toggles, etc.)
const uri = monaco.Uri.parse(`inmemory://model/${options.language}-${crypto.randomUUID()}`)
const model = monaco.editor.createModel(options.code.value, options.language, uri)
let model: monaco.editor.ITextModel | undefined

// `toValue()` safely unwraps refs, getters, or plain elements
watch(() => toValue(target), (_target) => {
watch([isMonacoLoaded, () => toValue(target)], ([_isLoaded, _target]) => {

// This ensures we skip setup if it's null, undefined, or an SVG element (as unrefElement can return SVGElement)
const el = unrefElement(_target)
if (!(el instanceof HTMLElement)) {
if (!(el instanceof HTMLElement) || !_isLoaded) {
_isSetup = false
return
}

// prevent multiple setups
if (_isSetup) return

if (!model) {
// we want to create our model before creating the editor so we don't end up with multiple models for the same editor (v-if toggles, etc.)
const uri = monaco.Uri.parse(`inmemory://model/${options.language}-${crypto.randomUUID()}`)
model = monaco.editor.createModel(options.code.value, options.language, uri)
}

editor = monaco.editor.create(el, {
...DEFAULT_MONACO_OPTIONS,
readOnly: options.readOnly || false,
language: options.language,
theme: editorStates.theme,
theme: editorStates.theme === 'light' ? 'catppuccin-latte' : 'catppuccin-mocha',
model,
editContext: false,
...options.monacoOptions,
Expand Down
10 changes: 10 additions & 0 deletions packages/core/monaco-editor/src/tests/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,27 @@ vi.mock('monaco-editor', () => {
})),
})),
remeasureFonts: vi.fn(),
defineTheme: vi.fn(),
setTheme: vi.fn(),
createModel: vi.fn(() => ({})),
}

const languages = {
getLanguages: vi.fn(() => [{ id: 'javascript' }]),
register: vi.fn(),
setTokensProvider: vi.fn(),
}

const json = {
jsonDefaults: {
setModeConfiguration: vi.fn(),
},
}

return {
Uri,
editor,
languages,
json,
}
})
16 changes: 16 additions & 0 deletions packages/core/monaco-editor/vite-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ A plugin to simplify loading the [Monaco Editor](https://github.com/microsoft/mo

This plugin also configures and loads the Monaco Editor web workers for you, so you don't need to manually set them up as described in the [official Monaco Editor ESM integration guide](https://github.com/microsoft/monaco-editor/blob/main/docs/integrate-esm.md#using-vite).

Since `@kong-public-ui/monaco-editor` uses [`@shikijs/monaco`](https://shiki.style/packages/monaco) to highlight Monaco Editor, this plugin also provides fine-grained control over the bundled languages and themes of shiki.

> [!IMPORTANT]
> `@kong-public-ui/monaco-editor` loads all the languages and features of Monaco Editor and all the corresponding languages of shiki by default, which significantly increases bundle size. Furthermore, since `@shikijs/monaco` integrates Shiki with Monaco Editor synchronously and loads all languages by default, it greatly increases Monaco Editor's initial loading time. It is strongly recommended to use this Vite plugin to reduce bundle size and improve loading performance by selectively including only the languages and features you need.

> [!TIP]
> This Vite plugin only needs to be applied in the consumer app. If you are developing a library based on Monaco Editor or `@kong-public-ui/monaco-editor`, you only need to externalize monaco-editor during the build.

## Usage

```ts
Expand All @@ -25,6 +33,10 @@ Options can be passed in to `@kong-ui-public/monaco-editor/vite-plugin`. They ca

- `customLanguages` (`{label:string; entry:string; worker:{id:string, entry:string} }[]`) - Custom languages (outside of the ones shipped with the `monaco-editor`), e.g. [monaco-yaml](https://github.com/remcohaszing/monaco-yaml).

- `shiki` (`object`) - Shiki fine-grained bundle configuration:
- `langs` (`string[]`) - Languages to include for Shiki syntax highlighting. By default, uses the same languages specified in the `languages` option above.
- `themes` (`string[]`) - Themes to include for Shiki syntax highlighting. By default, `catppuccin-latte` and `catppuccin-mocha` are included.

## Example

```ts
Expand Down Expand Up @@ -53,6 +65,10 @@ export default defineConfig({
},
},
],
shiki: {
langs: ['javascript', 'typescript', 'json', 'yaml'],
themes: ['catppuccin-latte', 'catppuccin-mocha', 'nord'],
},
}),
],
})
Expand Down
Loading
Loading