Skip to content

Commit 2c5b387

Browse files
authored
feat(core): expose dispose function (#707)
1 parent ff1d2f8 commit 2c5b387

File tree

4 files changed

+78
-16
lines changed

4 files changed

+78
-16
lines changed

packages/core/src/internal.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ let instancesCount = 0
2424
export async function getShikiInternal(options: HighlighterCoreOptions = {}): Promise<ShikiInternal> {
2525
instancesCount += 1
2626
if (options.warnings !== false && instancesCount >= 10 && instancesCount % 10 === 0)
27-
console.warn(`[Shiki] ${instancesCount} instances have been created. Shiki is supposed to be used as a singleton, consider refactoring your code to cache your highlighter instance.`)
27+
console.warn(`[Shiki] ${instancesCount} instances have been created. Shiki is supposed to be used as a singleton, consider refactoring your code to cache your highlighter instance; Or call \`highlighter.dispose()\` to release unused instances.`)
28+
29+
let isDisposed = false
2830

2931
async function normalizeGetter<T>(p: MaybeGetter<T>): Promise<T> {
3032
return Promise.resolve(typeof p === 'function' ? (p as any)() : p).then(r => r.default || r)
@@ -67,6 +69,7 @@ export async function getShikiInternal(options: HighlighterCoreOptions = {}): Pr
6769
let _lastTheme: string | ThemeRegistrationAny
6870

6971
function getLanguage(name: string | LanguageRegistration) {
72+
ensureNotDisposed()
7073
const _lang = _registry.getGrammar(typeof name === 'string' ? name : name.name)
7174
if (!_lang)
7275
throw new ShikiError(`Language \`${name}\` not found, you may need to load it first`)
@@ -76,13 +79,15 @@ export async function getShikiInternal(options: HighlighterCoreOptions = {}): Pr
7679
function getTheme(name: string | ThemeRegistrationAny): ThemeRegistrationResolved {
7780
if (name === 'none')
7881
return { bg: '', fg: '', name: 'none', settings: [], type: 'dark' }
82+
ensureNotDisposed()
7983
const _theme = _registry.getTheme(name)
8084
if (!_theme)
8185
throw new ShikiError(`Theme \`${name}\` not found, you may need to load it first`)
8286
return _theme
8387
}
8488

8589
function setTheme(name: string | ThemeRegistrationAny) {
90+
ensureNotDisposed()
8691
const theme = getTheme(name)
8792
if (_lastTheme !== name) {
8893
_registry.setTheme(theme)
@@ -96,18 +101,22 @@ export async function getShikiInternal(options: HighlighterCoreOptions = {}): Pr
96101
}
97102

98103
function getLoadedThemes() {
104+
ensureNotDisposed()
99105
return _registry.getLoadedThemes()
100106
}
101107

102108
function getLoadedLanguages() {
109+
ensureNotDisposed()
103110
return _registry.getLoadedLanguages()
104111
}
105112

106113
async function loadLanguage(...langs: (LanguageInput | SpecialLanguage)[]) {
114+
ensureNotDisposed()
107115
await _registry.loadLanguages(await resolveLangs(langs))
108116
}
109117

110118
async function loadTheme(...themes: (ThemeInput | SpecialTheme)[]) {
119+
ensureNotDisposed()
111120
await Promise.all(
112121
themes.map(async theme =>
113122
isSpecialTheme(theme)
@@ -117,6 +126,19 @@ export async function getShikiInternal(options: HighlighterCoreOptions = {}): Pr
117126
)
118127
}
119128

129+
function ensureNotDisposed() {
130+
if (isDisposed)
131+
throw new ShikiError('Shiki instance has been disposed')
132+
}
133+
134+
function dispose() {
135+
if (isDisposed)
136+
return
137+
isDisposed = true
138+
_registry.dispose()
139+
instancesCount -= 1
140+
}
141+
120142
return {
121143
setTheme,
122144
getTheme,
@@ -125,5 +147,7 @@ export async function getShikiInternal(options: HighlighterCoreOptions = {}): Pr
125147
getLoadedLanguages,
126148
loadLanguage,
127149
loadTheme,
150+
dispose,
151+
[Symbol.dispose]: dispose,
128152
}
129153
}

packages/core/src/registry.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { normalizeTheme } from './normalize'
66
import { ShikiError } from './error'
77

88
export class Registry extends TextMateRegistry {
9-
private _resolvedThemes: Record<string, ThemeRegistrationResolved> = {}
10-
private _resolvedGrammars: Record<string, IGrammar> = {}
11-
private _langMap: Record<string, LanguageRegistration> = {}
9+
private _resolvedThemes: Map<string, ThemeRegistrationResolved> = new Map()
10+
private _resolvedGrammars: Map<string, IGrammar> = new Map()
11+
private _langMap: Map<string, LanguageRegistration> = new Map()
1212
private _langGraph: Map<string, LanguageRegistration> = new Map()
1313

1414
private _textmateThemeCache = new WeakMap<IRawTheme, TextMateTheme>()
@@ -29,15 +29,15 @@ export class Registry extends TextMateRegistry {
2929

3030
public getTheme(theme: ThemeRegistrationAny | string) {
3131
if (typeof theme === 'string')
32-
return this._resolvedThemes[theme]
32+
return this._resolvedThemes.get(theme)
3333
else
3434
return this.loadTheme(theme)
3535
}
3636

3737
public loadTheme(theme: ThemeRegistrationAny): ThemeRegistrationResolved {
3838
const _theme = normalizeTheme(theme)
3939
if (_theme.name) {
40-
this._resolvedThemes[_theme.name] = _theme
40+
this._resolvedThemes.set(_theme.name, _theme)
4141
// Reset cache
4242
this._loadedThemesCache = null
4343
}
@@ -46,7 +46,7 @@ export class Registry extends TextMateRegistry {
4646

4747
public getLoadedThemes() {
4848
if (!this._loadedThemesCache)
49-
this._loadedThemesCache = Object.keys(this._resolvedThemes)
49+
this._loadedThemesCache = [...this._resolvedThemes.keys()]
5050
return this._loadedThemesCache
5151
}
5252

@@ -76,14 +76,17 @@ export class Registry extends TextMateRegistry {
7676
resolved.add(name)
7777
}
7878
}
79-
return this._resolvedGrammars[name]
79+
return this._resolvedGrammars.get(name)
8080
}
8181

8282
public async loadLanguage(lang: LanguageRegistration) {
8383
if (this.getGrammar(lang.name))
8484
return
8585

86-
const embeddedLazilyBy = new Set(Object.values(this._langMap).filter(i => i.embeddedLangsLazy?.includes(lang.name)))
86+
const embeddedLazilyBy = new Set(
87+
[...this._langMap.values()]
88+
.filter(i => i.embeddedLangsLazy?.includes(lang.name)),
89+
)
8790

8891
this._resolver.addLanguage(lang)
8992

@@ -95,7 +98,7 @@ export class Registry extends TextMateRegistry {
9598
// @ts-expect-error Private members, set this to override the previous grammar (that can be a stub)
9699
this._syncRegistry._rawGrammars.set(lang.scopeName, lang)
97100
const g = await this.loadGrammarWithConfiguration(lang.scopeName, 1, grammarConfig)
98-
this._resolvedGrammars[lang.name] = g!
101+
this._resolvedGrammars.set(lang.name, g!)
99102
if (lang.aliases) {
100103
lang.aliases.forEach((alias) => {
101104
this._alias[alias] = lang.name
@@ -107,14 +110,14 @@ export class Registry extends TextMateRegistry {
107110
// If there is a language that embeds this language lazily, we need to reload it
108111
if (embeddedLazilyBy.size) {
109112
for (const e of embeddedLazilyBy) {
110-
delete this._resolvedGrammars[e.name]
113+
this._resolvedGrammars.delete(e.name)
111114
// Reset cache
112115
this._loadedLanguagesCache = null
113116
// @ts-expect-error clear cache
114117
this._syncRegistry?._injectionGrammars?.delete(e.scopeName)
115118
// @ts-expect-error clear cache
116119
this._syncRegistry?._grammars?.delete(e.scopeName)
117-
await this.loadLanguage(this._langMap[e.name])
120+
await this.loadLanguage(this._langMap.get(e.name)!)
118121
}
119122
}
120123
}
@@ -124,6 +127,15 @@ export class Registry extends TextMateRegistry {
124127
await this.loadLanguages(this._langs)
125128
}
126129

130+
public override dispose(): void {
131+
super.dispose()
132+
this._resolvedThemes.clear()
133+
this._resolvedGrammars.clear()
134+
this._langMap.clear()
135+
this._langGraph.clear()
136+
this._loadedThemesCache = null
137+
}
138+
127139
public async loadLanguages(langs: LanguageRegistration[]) {
128140
for (const lang of langs)
129141
this.resolveEmbeddedLanguages(lang)
@@ -146,17 +158,20 @@ export class Registry extends TextMateRegistry {
146158
}
147159

148160
public getLoadedLanguages() {
149-
if (!this._loadedLanguagesCache)
150-
this._loadedLanguagesCache = Object.keys({ ...this._resolvedGrammars, ...this._alias })
161+
if (!this._loadedLanguagesCache) {
162+
this._loadedLanguagesCache = [
163+
...new Set([...this._resolvedGrammars.keys(), ...Object.keys(this._alias)]),
164+
]
165+
}
151166
return this._loadedLanguagesCache
152167
}
153168

154169
private resolveEmbeddedLanguages(lang: LanguageRegistration) {
155-
this._langMap[lang.name] = lang
170+
this._langMap.set(lang.name, lang)
156171
this._langGraph.set(lang.name, lang)
157172
if (lang.embeddedLangs) {
158173
for (const embeddedLang of lang.embeddedLangs)
159-
this._langGraph.set(embeddedLang, this._langMap[embeddedLang])
174+
this._langGraph.set(embeddedLang, this._langMap.get(embeddedLang)!)
160175
}
161176
}
162177
}

packages/core/src/types/highlighter.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ export interface ShikiInternal<BundledLangKeys extends string = never, BundledTh
4848
* Special-handled themes like `none` are not included.
4949
*/
5050
getLoadedThemes: () => string[]
51+
/**
52+
* Dispose the internal registry and release resources
53+
*/
54+
dispose: () => void
55+
/**
56+
* Dispose the internal registry and release resources
57+
*/
58+
[Symbol.dispose]: () => void
5159
}
5260

5361
/**

packages/shiki/test/core.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,19 @@ describe('errors', () => {
179179
await expect(() => shiki.codeToHtml('console.log("Hi")', { lang: 'mylang', theme: 'nord' }))
180180
.toThrowErrorMatchingInlineSnapshot(`[ShikiError: Circular alias \`mylang -> mylang2 -> mylang\`]`)
181181
})
182+
183+
it('throw on using disposed instance', async () => {
184+
const shiki = await getHighlighterCore({
185+
themes: [nord],
186+
langs: [js as any],
187+
})
188+
189+
expect(shiki.codeToHtml('console.log("Hi")', { lang: 'javascript', theme: 'nord' }))
190+
.toContain('console')
191+
192+
shiki.dispose()
193+
194+
expect(() => shiki.codeToHtml('console.log("Hi")', { lang: 'javascript', theme: 'nord' }))
195+
.toThrowErrorMatchingInlineSnapshot(`[ShikiError: Shiki instance has been disposed]`)
196+
})
182197
})

0 commit comments

Comments
 (0)