Skip to content

Commit

Permalink
perf(plugin-shiki): lazy load languages
Browse files Browse the repository at this point in the history
  • Loading branch information
Mister-Hope committed Jan 24, 2025
1 parent 108c888 commit af8c80c
Show file tree
Hide file tree
Showing 14 changed files with 164 additions and 50 deletions.
8 changes: 3 additions & 5 deletions docs/plugins/markdown/shiki.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,11 @@ export default {

- Details:

Languages of code blocks to be parsed by Shiki.
Additional languages to be parsed by Shiki.

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

::: warning

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

:::

Expand Down
8 changes: 3 additions & 5 deletions docs/zh/plugins/markdown/shiki.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,11 @@ export default {

- 详情:

Shiki 要解析的代码块的语言
Shiki 解析的额外语言

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

::: warning

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

:::

Expand Down
4 changes: 3 additions & 1 deletion plugins/markdown/plugin-shiki/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"type": "module",
"exports": {
".": "./lib/node/index.js",
"./resolveLang": "./lib/node/resolveLang.js",
"./styles/*": "./lib/client/styles/*",
"./package.json": "./package.json"
},
Expand All @@ -42,7 +43,8 @@
"@vuepress/helper": "workspace:*",
"@vuepress/highlighter-helper": "workspace:*",
"nanoid": "^5.0.9",
"shiki": "^2.1.0"
"shiki": "^2.1.0",
"synckit": "^0.9.2"
},
"peerDependencies": {
"vuepress": "2.0.0-rc.19"
Expand Down
9 changes: 6 additions & 3 deletions plugins/markdown/plugin-shiki/rollup.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { rollupBundle } from '../../../scripts/rollup.js'

export default rollupBundle('node/index', {
external: ['@shikijs/transformers', 'nanoid', 'shiki'],
})
export default rollupBundle(
{ base: 'node', files: ['index', 'resolveLang'] },
{
external: ['@shikijs/transformers', 'nanoid', 'shiki', 'synckit'],
},
)
1 change: 0 additions & 1 deletion plugins/markdown/plugin-shiki/src/node/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export type * from './options.js'
export * from './shiki.js'
export * from './shikiPlugin.js'
export type * from './types.js'
Original file line number Diff line number Diff line change
@@ -1,27 +1,70 @@
import type { BundledLanguage, BundledTheme, HighlighterGeneric } from 'shiki'
import { createHighlighter } from 'shiki'
import { bundledLanguageNames } from '../../shiki.js'
import { createRequire } from 'node:module'
import type {
BundledLanguage,
BundledTheme,
HighlighterGeneric,
LanguageRegistration,
} from 'shiki'
import { createHighlighter, isSpecialLang } from 'shiki'
import { createSyncFn } from 'synckit'
import type { ShikiResolveLang } from '../../resolveLang.js'
import type { ShikiHighlightOptions } from '../../types.js'

const require = createRequire(import.meta.url)

const resolveLangSync = createSyncFn<ShikiResolveLang>(
require.resolve('@vuepress/plugin-shiki/resolveLang'),
)

export type ShikiLoadLang = (lang: LanguageRegistration | string) => boolean

export const createShikiHighlighter = async ({
langs = bundledLanguageNames,
langs = [],
langAlias = {},
defaultLang,
shikiSetup,
...options
}: ShikiHighlightOptions = {}): Promise<
HighlighterGeneric<BundledLanguage, BundledTheme>
> => {
const shikiHighlighter = await createHighlighter({
langs,
}: ShikiHighlightOptions = {}): Promise<{
highlighter: HighlighterGeneric<BundledLanguage, BundledTheme>
loadLang: (lang: LanguageRegistration | string) => boolean
}> => {
const highlighter = await createHighlighter({
langs: [...langs, ...Object.values(langAlias)],
langAlias,
themes:
'themes' in options
? Object.values(options.themes)
: [options.theme ?? 'nord'],
})

await shikiSetup?.(shikiHighlighter)
const loadLang = (langConfig: LanguageRegistration | string): boolean => {
const lang = typeof langConfig === 'string' ? langConfig : langConfig.name

if (
!isSpecialLang(lang) &&
!highlighter.getLoadedLanguages().includes(lang)
) {
const resolvedLang = resolveLangSync(lang)

if (!resolvedLang.length) return false

console.log('loading lang', lang)

Check warning on line 51 in plugins/markdown/plugin-shiki/src/node/markdown/highlighter/createShikiHighlighter.ts

View workflow job for this annotation

GitHub Actions / build-check

Unexpected console statement

Check warning on line 51 in plugins/markdown/plugin-shiki/src/node/markdown/highlighter/createShikiHighlighter.ts

View workflow job for this annotation

GitHub Actions / build-check

Unexpected console statement

Check warning on line 51 in plugins/markdown/plugin-shiki/src/node/markdown/highlighter/createShikiHighlighter.ts

View workflow job for this annotation

GitHub Actions / bundle-check

Unexpected console statement

Check warning on line 51 in plugins/markdown/plugin-shiki/src/node/markdown/highlighter/createShikiHighlighter.ts

View workflow job for this annotation

GitHub Actions / bundle-check

Unexpected console statement

highlighter.loadLanguageSync(resolvedLang)
}
return true
}

// patch for twoslash - https://github.com/vuejs/vitepress/issues/4334
const rawGetLanguage = highlighter.getLanguage

highlighter.getLanguage = (name) => {
loadLang(name)

return rawGetLanguage.call(highlighter, name)
}

await shikiSetup?.(highlighter)

return shikiHighlighter
return { highlighter, loadLang }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import type { ShikiHighlightOptions } from '../../types.js'
import { attrsToLines } from '../../utils.js'
import type { MarkdownFilePathGetter } from './createMarkdownFilePathGetter.js'
import type { ShikiLoadLang } from './createShikiHighlighter.js'
import { getLanguage } from './getLanguage.js'
import { handleMustache } from './handleMustache.js'

Expand All @@ -19,20 +20,15 @@ type MarkdownItHighlight = (
export const getHighLightFunction = (
highlighter: HighlighterGeneric<BundledLanguage, BundledTheme>,
options: ShikiHighlightOptions,
loadLang: ShikiLoadLang,
markdownFilePathGetter: MarkdownFilePathGetter,
): MarkdownItHighlight => {
const transformers = getTransformers(options)
const loadedLanguages = highlighter.getLoadedLanguages()

return (content, language, attrs) =>
handleMustache(content, (str) =>
highlighter.codeToHtml(str, {
lang: getLanguage(
language,
loadedLanguages,
options,
markdownFilePathGetter,
),
lang: getLanguage(language, options, loadLang, markdownFilePathGetter),
meta: {
/**
* Custom `transformers` passed by users may require `attrs`.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { isSpecialLang } from 'shiki'
import { colors } from 'vuepress/utils'
import type { ShikiHighlightOptions } from '../../types.js'
import { logger, resolveLanguage } from '../../utils.js'
import type { MarkdownFilePathGetter } from './createMarkdownFilePathGetter.js'
import type { ShikiLoadLang } from './createShikiHighlighter.js'

const WARNED_LANGS = new Set<string>()

export const getLanguage = (
lang: string,
loadedLanguages: string[],
{ defaultLang, logLevel }: ShikiHighlightOptions,
loadLang: ShikiLoadLang,
markdownFilePathGetter: MarkdownFilePathGetter,
): string => {
let result = resolveLanguage(lang)

if (result && !loadedLanguages.includes(result) && !isSpecialLang(result)) {
if (result && !loadLang(lang)) {
// warn for unknown languages only once
if (logLevel !== 'silent' && !WARNED_LANGS.has(result)) {
logger.warn(
Expand Down
23 changes: 23 additions & 0 deletions plugins/markdown/plugin-shiki/src/node/resolveLang.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type {
DynamicImportLanguageRegistration,
LanguageRegistration,
} from 'shiki'
import { bundledLanguages } from 'shiki'
import { runAsWorker } from 'synckit'

async function resolveLang(lang: string): Promise<LanguageRegistration[]> {
return (
(
bundledLanguages as Record<
string,
DynamicImportLanguageRegistration | undefined
>
)
[lang]?.()
.then((m) => m.default) ?? []
)
}

runAsWorker(resolveLang)

export type ShikiResolveLang = typeof resolveLang
5 changes: 0 additions & 5 deletions plugins/markdown/plugin-shiki/src/node/shiki.ts

This file was deleted.

5 changes: 3 additions & 2 deletions plugins/markdown/plugin-shiki/src/node/shikiPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,12 @@ export const shikiPlugin = (_options: ShikiPluginOptions = {}): Plugin => {
const { preWrapper, lineNumbers, collapsedLines } = options

const markdownFilePathGetter = createMarkdownFilePathGetter(md)
const shikiHighlighter = await createShikiHighlighter(options)
const { highlighter, loadLang } = await createShikiHighlighter(options)

md.options.highlight = getHighLightFunction(
shikiHighlighter,
highlighter,
options,
loadLang,
markdownFilePathGetter,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,45 @@ export const getTransformers = (
const transformers: ShikiTransformer[] = []

if (options.notationDiff) {
transformers.push(transformerNotationDiff())
transformers.push(
transformerNotationDiff({
matchAlgorithm: 'v3',
}),
)
}

if (options.notationFocus) {
transformers.push(
transformerNotationFocus({
classActiveLine: 'has-focus',
classActivePre: 'has-focused-lines',
matchAlgorithm: 'v3',
}),
)
}

if (options.notationHighlight) {
transformers.push(transformerNotationHighlight())
transformers.push(
transformerNotationHighlight({
matchAlgorithm: 'v3',
}),
)
}

if (options.notationErrorLevel) {
transformers.push(transformerNotationErrorLevel())
transformers.push(
transformerNotationErrorLevel({
matchAlgorithm: 'v3',
}),
)
}

if (options.notationWordHighlight) {
transformers.push(transformerNotationWordHighlight())
transformers.push(
transformerNotationWordHighlight({
matchAlgorithm: 'v3',
}),
)
transformers.push(transformerMetaWordHighlight())
}

Expand Down
7 changes: 4 additions & 3 deletions plugins/markdown/plugin-shiki/tests/shiki-preWrapper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,22 @@ import {
} from '../src/node/markdown/index.js'
import type { ShikiPluginOptions } from '../src/node/options.js'

const shikiHighlighter = await createShikiHighlighter()
const { highlighter, loadLang } = await createShikiHighlighter()

const createMarkdown = ({
preWrapper = true,
lineNumbers = true,
collapsedLines = false,
...options
}: ShikiPluginOptions = {}): MarkdownIt => {
const md = MarkdownIt()
const md = new MarkdownIt()

const markdownFilePathGetter = createMarkdownFilePathGetter(md)

md.options.highlight = getHighLightFunction(
shikiHighlighter,
highlighter,
options,
loadLang,
markdownFilePathGetter,
)

Expand Down
Loading

0 comments on commit af8c80c

Please sign in to comment.