Skip to content

Commit 45968cd

Browse files
authoredOct 29, 2024··
fix: cache markdown it instance and properly dispose shiki on config reload (#4321)
This results in over 5x speedup in build times of certain projects. But this comes at the cost of correctness. `createMarkdownRenderer` now ignores any arguments passed by user. But from our GitHub code search we didn't find any user passing options different than their siteConfig to this function. If you are doing that, please open an issue and we can discuss the best way forward. --- Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com>
1 parent 81fc148 commit 45968cd

File tree

6 files changed

+469
-423
lines changed

6 files changed

+469
-423
lines changed
 

‎package.json

+17-17
Original file line numberDiff line numberDiff line change
@@ -100,20 +100,20 @@
100100
"dependencies": {
101101
"@docsearch/css": "^3.6.2",
102102
"@docsearch/js": "^3.6.2",
103-
"@shikijs/core": "^1.22.0",
104-
"@shikijs/transformers": "^1.22.0",
105-
"@shikijs/types": "^1.22.0",
103+
"@shikijs/core": "^1.22.2",
104+
"@shikijs/transformers": "^1.22.2",
105+
"@shikijs/types": "^1.22.2",
106106
"@types/markdown-it": "^14.1.2",
107107
"@vitejs/plugin-vue": "^5.1.4",
108-
"@vue/devtools-api": "^7.4.6",
108+
"@vue/devtools-api": "^7.5.4",
109109
"@vue/shared": "^3.5.12",
110110
"@vueuse/core": "^11.1.0",
111111
"@vueuse/integrations": "^11.1.0",
112112
"focus-trap": "^7.6.0",
113113
"mark.js": "8.11.1",
114114
"minisearch": "^7.1.0",
115-
"shiki": "^1.22.0",
116-
"vite": "^5.4.8",
115+
"shiki": "^1.22.2",
116+
"vite": "^5.4.10",
117117
"vue": "^3.5.12"
118118
},
119119
"devDependencies": {
@@ -127,7 +127,7 @@
127127
"@mdit-vue/shared": "^2.1.3",
128128
"@polka/compression": "^1.0.0-next.28",
129129
"@rollup/plugin-alias": "^5.1.1",
130-
"@rollup/plugin-commonjs": "^28.0.0",
130+
"@rollup/plugin-commonjs": "^28.0.1",
131131
"@rollup/plugin-json": "^6.1.0",
132132
"@rollup/plugin-node-resolve": "^15.3.0",
133133
"@rollup/plugin-replace": "^6.0.1",
@@ -141,15 +141,15 @@
141141
"@types/markdown-it-emoji": "^3.0.1",
142142
"@types/micromatch": "^4.0.9",
143143
"@types/minimist": "^1.2.5",
144-
"@types/node": "^22.7.5",
144+
"@types/node": "^22.8.2",
145145
"@types/postcss-prefix-selector": "^1.16.3",
146146
"@types/prompts": "^2.4.9",
147147
"chokidar": "^3.6.0",
148148
"conventional-changelog-cli": "^5.0.0",
149149
"cross-spawn": "^7.0.3",
150150
"debug": "^4.3.7",
151151
"esbuild": "^0.24.0",
152-
"execa": "^9.4.0",
152+
"execa": "^9.5.1",
153153
"fs-extra": "^11.2.0",
154154
"get-port": "^7.1.0",
155155
"gray-matter": "^4.0.3",
@@ -164,31 +164,31 @@
164164
"markdown-it-mathjax3": "^4.3.2",
165165
"micromatch": "^4.0.8",
166166
"minimist": "^1.2.8",
167-
"nanoid": "^5.0.7",
167+
"nanoid": "^5.0.8",
168168
"ora": "^8.1.0",
169169
"p-map": "^7.0.2",
170170
"path-to-regexp": "^6.3.0",
171-
"picocolors": "^1.1.0",
171+
"picocolors": "^1.1.1",
172172
"pkg-dir": "^8.0.0",
173-
"playwright-chromium": "^1.48.0",
173+
"playwright-chromium": "^1.48.2",
174174
"polka": "^1.0.0-next.28",
175175
"postcss-prefix-selector": "^2.1.0",
176176
"prettier": "^3.3.3",
177177
"prompts": "^2.4.2",
178178
"punycode": "^2.3.1",
179179
"rimraf": "^6.0.1",
180-
"rollup": "^4.24.0",
180+
"rollup": "^4.24.2",
181181
"rollup-plugin-dts": "^6.1.1",
182182
"rollup-plugin-esbuild": "^6.1.1",
183183
"semver": "^7.6.3",
184184
"simple-git-hooks": "^2.11.1",
185185
"sirv": "^3.0.0",
186186
"sitemap": "^8.0.0",
187187
"supports-color": "^9.4.0",
188-
"tinyglobby": "^0.2.9",
188+
"tinyglobby": "^0.2.10",
189189
"typescript": "^5.6.3",
190-
"vitest": "^2.1.2",
191-
"vue-tsc": "^2.1.6",
190+
"vitest": "^2.1.4",
191+
"vue-tsc": "^2.1.8",
192192
"wait-on": "^8.0.1"
193193
},
194194
"peerDependencies": {
@@ -203,7 +203,7 @@
203203
"optional": true
204204
}
205205
},
206-
"packageManager": "pnpm@9.12.1",
206+
"packageManager": "pnpm@9.12.3",
207207
"pnpm": {
208208
"peerDependencyRules": {
209209
"ignoreMissing": [

‎pnpm-lock.yaml

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

‎src/node/contentLoader.ts

+8-21
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ import { glob, type GlobOptions } from 'tinyglobby'
44
import type { SiteConfig } from './config'
55
import matter from 'gray-matter'
66
import { normalizePath } from 'vite'
7-
import {
8-
createMarkdownRenderer,
9-
type MarkdownRenderer
10-
} from './markdown/markdown'
7+
import { createMarkdownRenderer } from './markdown/markdown'
118

129
export interface ContentOptions<T = ContentData[]> {
1310
/**
@@ -100,15 +97,7 @@ export function createContentLoader<T = ContentData[]>(
10097
if (typeof pattern === 'string') pattern = [pattern]
10198
pattern = pattern.map((p) => normalizePath(path.join(config.srcDir, p)))
10299

103-
let md: MarkdownRenderer
104-
105-
const cache = new Map<
106-
string,
107-
{
108-
data: any
109-
timestamp: number
110-
}
111-
>()
100+
const cache = new Map<string, { data: any; timestamp: number }>()
112101

113102
return {
114103
watch: pattern,
@@ -124,14 +113,12 @@ export function createContentLoader<T = ContentData[]>(
124113
).sort()
125114
}
126115

127-
md =
128-
md ||
129-
(await createMarkdownRenderer(
130-
config.srcDir,
131-
config.markdown,
132-
config.site.base,
133-
config.logger
134-
))
116+
const md = await createMarkdownRenderer(
117+
config.srcDir,
118+
config.markdown,
119+
config.site.base,
120+
config.logger
121+
)
135122

136123
const raw: ContentData[] = []
137124

‎src/node/markdown/markdown.ts

+26-10
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import type {
2828
import type { Logger } from 'vite'
2929
import { containerPlugin, type ContainerOptions } from './plugins/containers'
3030
import { gitHubAlertsPlugin } from './plugins/githubAlerts'
31-
import { highlight } from './plugins/highlight'
31+
import { highlight as createHighlighter } from './plugins/highlight'
3232
import { highlightLinePlugin } from './plugins/highlightLines'
3333
import { imagePlugin, type Options as ImageOptions } from './plugins/image'
3434
import { lineNumberPlugin } from './plugins/lineNumbers'
@@ -173,7 +173,7 @@ export interface MarkdownOptions extends Options {
173173
*/
174174
container?: ContainerOptions
175175
/**
176-
* Math support (experimental)
176+
* Math support
177177
*
178178
* You need to install `markdown-it-mathjax3` and set `math` to `true` to enable it.
179179
* You can also pass options to `markdown-it-mathjax3` here.
@@ -192,22 +192,38 @@ export interface MarkdownOptions extends Options {
192192

193193
export type MarkdownRenderer = MarkdownIt
194194

195-
export const createMarkdownRenderer = async (
195+
let md: MarkdownRenderer | undefined
196+
let _disposeHighlighter: (() => void) | undefined
197+
198+
export function disposeMdItInstance() {
199+
if (md) {
200+
md = undefined
201+
_disposeHighlighter?.()
202+
}
203+
}
204+
205+
/**
206+
* @experimental
207+
*/
208+
export async function createMarkdownRenderer(
196209
srcDir: string,
197210
options: MarkdownOptions = {},
198211
base = '/',
199212
logger: Pick<Logger, 'warn'> = console
200-
): Promise<MarkdownRenderer> => {
213+
): Promise<MarkdownRenderer> {
214+
if (md) return md
215+
201216
const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' }
202217
const codeCopyButtonTitle = options.codeCopyButtonTitle || 'Copy Code'
203218
const hasSingleTheme = typeof theme === 'string' || 'name' in theme
204219

205-
const md = MarkdownIt({
206-
html: true,
207-
linkify: true,
208-
highlight: options.highlight || (await highlight(theme, options, logger)),
209-
...options
210-
})
220+
let [highlight, dispose] = options.highlight
221+
? [options.highlight, () => {}]
222+
: await createHighlighter(theme, options, logger)
223+
224+
_disposeHighlighter = dispose
225+
226+
md = MarkdownIt({ html: true, linkify: true, highlight, ...options })
211227

212228
md.linkify.set({ fuzzyLink: false })
213229
md.use(restoreEntities)

‎src/node/markdown/plugins/highlight.ts

+85-82
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
2323
* 2. convert line numbers into line options:
2424
* [{ line: number, classes: string[] }]
2525
*/
26-
const attrsToLines = (attrs: string): TransformerCompactLineOption[] => {
26+
function attrsToLines(attrs: string): TransformerCompactLineOption[] {
2727
attrs = attrs.replace(/^(?:\[.*?\])?.*?([\d,-]+).*/, '$1').trim()
2828
const result: number[] = []
2929
if (!attrs) {
@@ -51,7 +51,7 @@ export async function highlight(
5151
theme: ThemeOptions,
5252
options: MarkdownOptions,
5353
logger: Pick<Logger, 'warn'> = console
54-
): Promise<(str: string, lang: string, attrs: string) => string> {
54+
): Promise<[(str: string, lang: string, attrs: string) => string, () => void]> {
5555
const {
5656
defaultHighlightLang: defaultLang = '',
5757
codeTransformers: userTransformers = []
@@ -95,93 +95,96 @@ export async function highlight(
9595
const lineNoRE = /:(no-)?line-numbers(=\d*)?$/
9696
const mustacheRE = /\{\{.*?\}\}/g
9797

98-
return (str: string, lang: string, attrs: string) => {
99-
const vPre = vueRE.test(lang) ? '' : 'v-pre'
100-
lang =
101-
lang
102-
.replace(lineNoStartRE, '')
103-
.replace(lineNoRE, '')
104-
.replace(vueRE, '')
105-
.toLowerCase() || defaultLang
98+
return [
99+
(str: string, lang: string, attrs: string) => {
100+
const vPre = vueRE.test(lang) ? '' : 'v-pre'
101+
lang =
102+
lang
103+
.replace(lineNoStartRE, '')
104+
.replace(lineNoRE, '')
105+
.replace(vueRE, '')
106+
.toLowerCase() || defaultLang
106107

107-
if (lang) {
108-
const langLoaded = highlighter.getLoadedLanguages().includes(lang as any)
109-
if (!langLoaded && !isSpecialLang(lang)) {
110-
logger.warn(
111-
c.yellow(
112-
`\nThe language '${lang}' is not loaded, falling back to '${
113-
defaultLang || 'txt'
114-
}' for syntax highlighting.`
108+
if (lang) {
109+
const langLoaded = highlighter.getLoadedLanguages().includes(lang)
110+
if (!langLoaded && !isSpecialLang(lang)) {
111+
logger.warn(
112+
c.yellow(
113+
`\nThe language '${lang}' is not loaded, falling back to '${
114+
defaultLang || 'txt'
115+
}' for syntax highlighting.`
116+
)
115117
)
116-
)
117-
lang = defaultLang
118+
lang = defaultLang
119+
}
118120
}
119-
}
120121

121-
const lineOptions = attrsToLines(attrs)
122-
const mustaches = new Map<string, string>()
122+
const lineOptions = attrsToLines(attrs)
123+
const mustaches = new Map<string, string>()
123124

124-
const removeMustache = (s: string) => {
125-
if (vPre) return s
126-
return s.replace(mustacheRE, (match) => {
127-
let marker = mustaches.get(match)
128-
if (!marker) {
129-
marker = nanoid()
130-
mustaches.set(match, marker)
131-
}
132-
return marker
133-
})
134-
}
125+
const removeMustache = (s: string) => {
126+
if (vPre) return s
127+
return s.replace(mustacheRE, (match) => {
128+
let marker = mustaches.get(match)
129+
if (!marker) {
130+
marker = nanoid()
131+
mustaches.set(match, marker)
132+
}
133+
return marker
134+
})
135+
}
135136

136-
const restoreMustache = (s: string) => {
137-
mustaches.forEach((marker, match) => {
138-
s = s.replaceAll(marker, match)
139-
})
140-
return s
141-
}
137+
const restoreMustache = (s: string) => {
138+
mustaches.forEach((marker, match) => {
139+
s = s.replaceAll(marker, match)
140+
})
141+
return s
142+
}
142143

143-
str = removeMustache(str).trimEnd()
144+
str = removeMustache(str).trimEnd()
144145

145-
const highlighted = highlighter.codeToHtml(str, {
146-
lang,
147-
transformers: [
148-
...transformers,
149-
transformerCompactLineOptions(lineOptions),
150-
{
151-
name: 'vitepress:v-pre',
152-
pre(node) {
153-
if (vPre) node.properties['v-pre'] = ''
154-
}
155-
},
156-
{
157-
name: 'vitepress:empty-line',
158-
code(hast) {
159-
hast.children.forEach((span) => {
160-
if (
161-
span.type === 'element' &&
162-
span.tagName === 'span' &&
163-
Array.isArray(span.properties.class) &&
164-
span.properties.class.includes('line') &&
165-
span.children.length === 0
166-
) {
167-
span.children.push({
168-
type: 'element',
169-
tagName: 'wbr',
170-
properties: {},
171-
children: []
172-
})
173-
}
174-
})
175-
}
176-
},
177-
...userTransformers
178-
],
179-
meta: { __raw: attrs },
180-
...(typeof theme === 'object' && 'light' in theme && 'dark' in theme
181-
? { themes: theme, defaultColor: false }
182-
: { theme })
183-
})
146+
const highlighted = highlighter.codeToHtml(str, {
147+
lang,
148+
transformers: [
149+
...transformers,
150+
transformerCompactLineOptions(lineOptions),
151+
{
152+
name: 'vitepress:v-pre',
153+
pre(node) {
154+
if (vPre) node.properties['v-pre'] = ''
155+
}
156+
},
157+
{
158+
name: 'vitepress:empty-line',
159+
code(hast) {
160+
hast.children.forEach((span) => {
161+
if (
162+
span.type === 'element' &&
163+
span.tagName === 'span' &&
164+
Array.isArray(span.properties.class) &&
165+
span.properties.class.includes('line') &&
166+
span.children.length === 0
167+
) {
168+
span.children.push({
169+
type: 'element',
170+
tagName: 'wbr',
171+
properties: {},
172+
children: []
173+
})
174+
}
175+
})
176+
}
177+
},
178+
...userTransformers
179+
],
180+
meta: { __raw: attrs },
181+
...(typeof theme === 'object' && 'light' in theme && 'dark' in theme
182+
? { themes: theme, defaultColor: false }
183+
: { theme })
184+
})
184185

185-
return restoreMustache(highlighted)
186-
}
186+
return restoreMustache(highlighted)
187+
},
188+
highlighter.dispose
189+
]
187190
}

‎src/node/plugin.ts

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
resolveAliases
1717
} from './alias'
1818
import { resolvePages, resolveUserConfig, type SiteConfig } from './config'
19+
import { disposeMdItInstance } from './markdown/markdown'
1920
import {
2021
clearCache,
2122
createMarkdownToVueRenderFn,
@@ -388,6 +389,7 @@ export async function createVitePressPlugin(
388389
return
389390
}
390391

392+
disposeMdItInstance()
391393
clearCache()
392394
await recreateServer?.()
393395
return

0 commit comments

Comments
 (0)
Please sign in to comment.