Skip to content

Commit d926209

Browse files
authoredJan 24, 2025··
perf(plugin-shiki): lazy load languages (#347)
1 parent 44f1e72 commit d926209

File tree

13 files changed

+138
-45
lines changed

13 files changed

+138
-45
lines changed
 

‎docs/plugins/markdown/shiki.md

+3-5
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,11 @@ export default {
4141

4242
- Details:
4343

44-
Languages of code blocks to be parsed by Shiki.
44+
Additional languages to be parsed by Shiki.
4545

46-
This option will be forwarded to `createHighlighter()` method of Shiki.
46+
::: tip
4747

48-
::: warning
49-
50-
We recommend you to provide the languages list you are using explicitly, otherwise Shiki will load all languages and can affect performance.
48+
The plugin now automatically loads the languages used in your markdown files, so you don't need to specify them manually.
5149

5250
:::
5351

‎docs/zh/plugins/markdown/shiki.md

+3-5
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,11 @@ export default {
4141

4242
- 详情:
4343

44-
Shiki 要解析的代码块的语言
44+
Shiki 解析的额外语言
4545

46-
该配置项会被传递到 Shiki 的 `createHighlighter()` 方法中。
46+
::: tip
4747

48-
::: warning
49-
50-
我们建议明确传入所有你使用的语言列表,否则 Shiki 会加载所有语言,并可能影响性能。
48+
插件现在会自动加载你的 markdown 文件中使用的语言,所以你不需要手动指定它们。
5149

5250
:::
5351

‎plugins/markdown/plugin-shiki/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"type": "module",
2424
"exports": {
2525
".": "./lib/node/index.js",
26+
"./resolveLang": "./lib/node/resolveLang.js",
2627
"./styles/*": "./lib/client/styles/*",
2728
"./package.json": "./package.json"
2829
},
@@ -42,7 +43,8 @@
4243
"@vuepress/helper": "workspace:*",
4344
"@vuepress/highlighter-helper": "workspace:*",
4445
"nanoid": "^5.0.9",
45-
"shiki": "^2.1.0"
46+
"shiki": "^2.1.0",
47+
"synckit": "^0.9.2"
4648
},
4749
"peerDependencies": {
4850
"vuepress": "2.0.0-rc.19"
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { rollupBundle } from '../../../scripts/rollup.js'
22

3-
export default rollupBundle('node/index', {
4-
external: ['@shikijs/transformers', 'nanoid', 'shiki'],
5-
})
3+
export default rollupBundle(
4+
{ base: 'node', files: ['index', 'resolveLang'] },
5+
{
6+
external: ['@shikijs/transformers', 'nanoid', 'shiki', 'synckit'],
7+
},
8+
)
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
export type * from './options.js'
2-
export * from './shiki.js'
32
export * from './shikiPlugin.js'
43
export type * from './types.js'
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,66 @@
1+
import { createRequire } from 'node:module'
12
import type { BundledLanguage, BundledTheme, HighlighterGeneric } from 'shiki'
2-
import { createHighlighter } from 'shiki'
3-
import { bundledLanguageNames } from '../../shiki.js'
3+
import { createHighlighter, isSpecialLang } from 'shiki'
4+
import { createSyncFn } from 'synckit'
5+
import type { ShikiResolveLang } from '../../resolveLang.js'
46
import type { ShikiHighlightOptions } from '../../types.js'
7+
import { resolveLanguage } from '../../utils.js'
8+
9+
const require = createRequire(import.meta.url)
10+
11+
const resolveLangSync = createSyncFn<ShikiResolveLang>(
12+
require.resolve('@vuepress/plugin-shiki/resolveLang'),
13+
)
14+
15+
export type ShikiLoadLang = (lang: string) => boolean
516

617
export const createShikiHighlighter = async ({
7-
langs = bundledLanguageNames,
18+
langs = [],
819
langAlias = {},
920
defaultLang,
1021
shikiSetup,
1122
...options
12-
}: ShikiHighlightOptions = {}): Promise<
13-
HighlighterGeneric<BundledLanguage, BundledTheme>
14-
> => {
15-
const shikiHighlighter = await createHighlighter({
16-
langs,
23+
}: ShikiHighlightOptions = {}): Promise<{
24+
highlighter: HighlighterGeneric<BundledLanguage, BundledTheme>
25+
loadLang: ShikiLoadLang
26+
}> => {
27+
const highlighter = await createHighlighter({
28+
langs: [...langs, ...Object.values(langAlias)],
1729
langAlias,
1830
themes:
1931
'themes' in options
2032
? Object.values(options.themes)
2133
: [options.theme ?? 'nord'],
2234
})
2335

24-
await shikiSetup?.(shikiHighlighter)
36+
const loadLang = (lang: string): boolean => {
37+
if (isSpecialLang(lang)) return true
38+
39+
const loadedLangs = highlighter.getLoadedLanguages()
40+
41+
if (!loadedLangs.includes(lang)) {
42+
const resolvedLang = resolveLangSync(lang)
43+
44+
if (!resolvedLang.length) return false
45+
46+
highlighter.loadLanguageSync(resolvedLang)
47+
}
48+
49+
return true
50+
}
51+
52+
// patch for twoslash - https://github.com/vuejs/vitepress/issues/4334
53+
const rawGetLanguage = highlighter.getLanguage
54+
55+
highlighter.getLanguage = (name) => {
56+
const lang = typeof name === 'string' ? name : name.name
57+
58+
loadLang(resolveLanguage(lang))
59+
60+
return rawGetLanguage.call(highlighter, name)
61+
}
62+
63+
await shikiSetup?.(highlighter)
2564

26-
return shikiHighlighter
65+
return { highlighter, loadLang }
2766
}

‎plugins/markdown/plugin-shiki/src/node/markdown/highlighter/getHighLightFunction.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import type { ShikiHighlightOptions } from '../../types.js'
88
import { attrsToLines } from '../../utils.js'
99
import type { MarkdownFilePathGetter } from './createMarkdownFilePathGetter.js'
10+
import type { ShikiLoadLang } from './createShikiHighlighter.js'
1011
import { getLanguage } from './getLanguage.js'
1112
import { handleMustache } from './handleMustache.js'
1213

@@ -19,20 +20,15 @@ type MarkdownItHighlight = (
1920
export const getHighLightFunction = (
2021
highlighter: HighlighterGeneric<BundledLanguage, BundledTheme>,
2122
options: ShikiHighlightOptions,
23+
loadLang: ShikiLoadLang,
2224
markdownFilePathGetter: MarkdownFilePathGetter,
2325
): MarkdownItHighlight => {
2426
const transformers = getTransformers(options)
25-
const loadedLanguages = highlighter.getLoadedLanguages()
2627

2728
return (content, language, attrs) =>
2829
handleMustache(content, (str) =>
2930
highlighter.codeToHtml(str, {
30-
lang: getLanguage(
31-
language,
32-
loadedLanguages,
33-
options,
34-
markdownFilePathGetter,
35-
),
31+
lang: getLanguage(language, options, loadLang, markdownFilePathGetter),
3632
meta: {
3733
/**
3834
* Custom `transformers` passed by users may require `attrs`.

‎plugins/markdown/plugin-shiki/src/node/markdown/highlighter/getLanguage.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
import { isSpecialLang } from 'shiki'
21
import { colors } from 'vuepress/utils'
32
import type { ShikiHighlightOptions } from '../../types.js'
43
import { logger, resolveLanguage } from '../../utils.js'
54
import type { MarkdownFilePathGetter } from './createMarkdownFilePathGetter.js'
5+
import type { ShikiLoadLang } from './createShikiHighlighter.js'
66

77
const WARNED_LANGS = new Set<string>()
88

99
export const getLanguage = (
1010
lang: string,
11-
loadedLanguages: string[],
1211
{ defaultLang, logLevel }: ShikiHighlightOptions,
12+
loadLang: ShikiLoadLang,
1313
markdownFilePathGetter: MarkdownFilePathGetter,
1414
): string => {
1515
let result = resolveLanguage(lang)
1616

17-
if (result && !loadedLanguages.includes(result) && !isSpecialLang(result)) {
17+
if (result && !loadLang(result)) {
1818
// warn for unknown languages only once
1919
if (logLevel !== 'silent' && !WARNED_LANGS.has(result)) {
2020
logger.warn(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type {
2+
DynamicImportLanguageRegistration,
3+
LanguageRegistration,
4+
} from 'shiki'
5+
import { bundledLanguages } from 'shiki'
6+
import { runAsWorker } from 'synckit'
7+
8+
async function resolveLang(lang: string): Promise<LanguageRegistration[]> {
9+
return (
10+
(
11+
bundledLanguages as Record<
12+
string,
13+
DynamicImportLanguageRegistration | undefined
14+
>
15+
)
16+
[lang]?.()
17+
.then((m) => m.default) ?? []
18+
)
19+
}
20+
21+
runAsWorker(resolveLang)
22+
23+
export type ShikiResolveLang = typeof resolveLang

‎plugins/markdown/plugin-shiki/src/node/shiki.ts

-5
This file was deleted.

‎plugins/markdown/plugin-shiki/src/node/shikiPlugin.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,12 @@ export const shikiPlugin = (_options: ShikiPluginOptions = {}): Plugin => {
4141
const { preWrapper, lineNumbers, collapsedLines } = options
4242

4343
const markdownFilePathGetter = createMarkdownFilePathGetter(md)
44-
const shikiHighlighter = await createShikiHighlighter(options)
44+
const { highlighter, loadLang } = await createShikiHighlighter(options)
4545

4646
md.options.highlight = getHighLightFunction(
47-
shikiHighlighter,
47+
highlighter,
4848
options,
49+
loadLang,
4950
markdownFilePathGetter,
5051
)
5152

‎plugins/markdown/plugin-shiki/tests/shiki-preWrapper.spec.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,22 @@ import {
1818
} from '../src/node/markdown/index.js'
1919
import type { ShikiPluginOptions } from '../src/node/options.js'
2020

21-
const shikiHighlighter = await createShikiHighlighter()
21+
const { highlighter, loadLang } = await createShikiHighlighter()
2222

2323
const createMarkdown = ({
2424
preWrapper = true,
2525
lineNumbers = true,
2626
collapsedLines = false,
2727
...options
2828
}: ShikiPluginOptions = {}): MarkdownIt => {
29-
const md = MarkdownIt()
29+
const md = new MarkdownIt()
3030

3131
const markdownFilePathGetter = createMarkdownFilePathGetter(md)
3232

3333
md.options.highlight = getHighLightFunction(
34-
shikiHighlighter,
34+
highlighter,
3535
options,
36+
loadLang,
3637
markdownFilePathGetter,
3738
)
3839

‎pnpm-lock.yaml

+38
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.