perf: use hook filters in vite plugins

Per-module hooks (resolveId / load / transform) now declare filters,
so rolldown skips the Rust-to-JS call for modules they cannot act on
instead of invoking every hook for every module in the graph.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull/5335/head
Divyansh Singh 1 month ago
parent a1f2147c0a
commit fa24c6d67b

@ -68,7 +68,7 @@ describe('node/plugins/localSearchPlugin', () => {
{ publicDir: siteConfig.publicDir } { publicDir: siteConfig.publicDir }
) )
const indexModule = (await (plugin.load as any)?.call( const indexModule = (await (plugin.load as any)?.handler.call(
{}, {},
'/@localSearchIndex' '/@localSearchIndex'
)) as string )) as string
@ -79,10 +79,16 @@ describe('node/plugins/localSearchPlugin', () => {
expect(indexModule).toContain('"zh": () => import(\'@localSearchIndexzh\')') expect(indexModule).toContain('"zh": () => import(\'@localSearchIndexzh\')')
const rootIndex = loadIndex( const rootIndex = loadIndex(
(await (plugin.load as any)?.call({}, '/@localSearchIndexroot')) as string (await (plugin.load as any)?.handler.call(
{},
'/@localSearchIndexroot'
)) as string
) )
const zhIndex = loadIndex( const zhIndex = loadIndex(
(await (plugin.load as any)?.call({}, '/@localSearchIndexzh')) as string (await (plugin.load as any)?.handler.call(
{},
'/@localSearchIndexzh'
)) as string
) )
expect(rootIndex.search('rootonlytoken')).toMatchObject([ expect(rootIndex.search('rootonlytoken')).toMatchObject([

@ -134,6 +134,7 @@
"@mdit/plugin-emoji": "^1.1.0", "@mdit/plugin-emoji": "^1.1.0",
"@mdit/plugin-tasklist": "^1.0.1", "@mdit/plugin-tasklist": "^1.0.1",
"@polka/compression": "^1.0.0-next.28", "@polka/compression": "^1.0.0-next.28",
"@rolldown/pluginutils": "^1.0.1",
"@rollup/plugin-alias": "^6.0.0", "@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-commonjs": "^29.0.3", "@rollup/plugin-commonjs": "^29.0.3",
"@rollup/plugin-json": "^6.1.0", "@rollup/plugin-json": "^6.1.0",

@ -122,6 +122,9 @@ importers:
'@polka/compression': '@polka/compression':
specifier: ^1.0.0-next.28 specifier: ^1.0.0-next.28
version: 1.0.0-next.28 version: 1.0.0-next.28
'@rolldown/pluginutils':
specifier: ^1.0.1
version: 1.0.1
'@rollup/plugin-alias': '@rollup/plugin-alias':
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0(rollup@4.62.2) version: 6.0.0(rollup@4.62.2)

@ -1,3 +1,4 @@
import { exactRegex } from '@rolldown/pluginutils'
import path from 'node:path' import path from 'node:path'
import c from 'picocolors' import c from 'picocolors'
import { import {
@ -166,11 +167,12 @@ export async function createVitePressPlugin(
: baseConfig : baseConfig
}, },
resolveId(id, importer, resolveOptions) { resolveId: {
if (id === SITE_DATA_ID) { filter: { id: [exactRegex(SITE_DATA_ID), startsWithThemeRE] },
return SITE_DATA_REQUEST_PATH handler(id, importer, resolveOptions) {
} if (id === SITE_DATA_ID) {
if (startsWithThemeRE.test(id)) { return SITE_DATA_REQUEST_PATH
}
return this.resolve( return this.resolve(
siteConfig.themeDir + id.slice(6), siteConfig.themeDir + id.slice(6),
importer, importer,
@ -179,8 +181,9 @@ export async function createVitePressPlugin(
} }
}, },
load(id) { load: {
if (id === SITE_DATA_REQUEST_PATH) { filter: { id: exactRegex(SITE_DATA_REQUEST_PATH) },
handler() {
let data = siteData let data = siteData
// head info is not needed by the client in production build // head info is not needed by the client in production build
if (config.command === 'build') { if (config.command === 'build') {
@ -196,48 +199,50 @@ export async function createVitePressPlugin(
} }
}, },
// TODO: use plugin hook filters transform: {
async transform(code, id) { filter: { id: [docsearchRE, /\.vue$/, /\.md$/] },
if (docsearchRE.test(normalizePath(id))) { async handler(code, id) {
return code if (docsearchRE.test(normalizePath(id))) {
.replaceAll('[data-theme=dark]', '.dark') return code
.replaceAll(/\(max-width:\s*768px\)/g, '(max-width: 767px)') .replaceAll('[data-theme=dark]', '.dark')
.replaceAll(/\(min-width:\s*769px\)/g, '(min-width: 768px)') .replaceAll(/\(max-width:\s*768px\)/g, '(max-width: 767px)')
} .replaceAll(/\(min-width:\s*769px\)/g, '(min-width: 768px)')
if (id.endsWith('.vue')) {
return processClientJS(code, id)
}
if (id.endsWith('.md')) {
const relativePath = path.posix.relative(srcDir, id)
// transform .md files into vueSrc so plugin-vue can handle it
const { vueSrc, deadLinks, includes, pageData } = await markdownToVue(
code,
id
)
allDeadLinks.push(...deadLinks)
if (includes.length) {
includes.forEach((i) => {
;(importerMap[slash(i)] ??= new Set()).add(relativePath)
this.addWatchFile(i)
})
} }
if ( if (id.endsWith('.vue')) {
this.environment.mode === 'dev' && return processClientJS(code, id)
this.environment.name === 'client' }
) { if (id.endsWith('.md')) {
logDeadLinks(deadLinks, siteConfig.logger, true) const relativePath = path.posix.relative(srcDir, id)
const payload: PageDataPayload = { // transform .md files into vueSrc so plugin-vue can handle it
path: `/${siteConfig.rewrites.map[relativePath] || relativePath}`, const { vueSrc, deadLinks, includes, pageData } = await markdownToVue(
pageData code,
id
)
allDeadLinks.push(...deadLinks)
if (includes.length) {
includes.forEach((i) => {
;(importerMap[slash(i)] ??= new Set()).add(relativePath)
this.addWatchFile(i)
})
} }
// notify the client to update page data if (
this.environment.hot.send({ this.environment.mode === 'dev' &&
type: 'custom', this.environment.name === 'client'
event: 'vitepress:pageData', ) {
data: payload logDeadLinks(deadLinks, siteConfig.logger, true)
}) const payload: PageDataPayload = {
path: `/${siteConfig.rewrites.map[relativePath] || relativePath}`,
pageData
}
// notify the client to update page data
this.environment.hot.send({
type: 'custom',
event: 'vitepress:pageData',
data: payload
})
}
return processClientJS(vueSrc, id)
} }
return processClientJS(vueSrc, id)
} }
}, },

@ -133,42 +133,47 @@ export const dynamicRoutesPlugin = async (
name: 'vitepress:dynamic-routes', name: 'vitepress:dynamic-routes',
enforce: 'pre', enforce: 'pre',
resolveId(id) { resolveId: {
if (!id.endsWith('.md')) return filter: { id: /\.md$/ },
const normalizedId = id.startsWith(config.srcDir) handler(id) {
? id const normalizedId = id.startsWith(config.srcDir)
: normalizePath(path.resolve(config.srcDir, id.replace(/^\//, ''))) ? id
const matched = config.dynamicRoutes.find( : normalizePath(path.resolve(config.srcDir, id.replace(/^\//, '')))
(r) => r.fullPath === normalizedId const matched = config.dynamicRoutes.find(
) (r) => r.fullPath === normalizedId
if (matched) return normalizedId )
if (matched) return normalizedId
}
}, },
load(id) { load: {
const matched = config.dynamicRoutes.find((r) => r.fullPath === id) filter: { id: /\.md$/ },
if (matched) { handler(id) {
const { route, params, content } = matched const matched = config.dynamicRoutes.find((r) => r.fullPath === id)
const routeFile = normalizePath(path.resolve(config.srcDir, route)) if (matched) {
const { route, params, content } = matched
moduleGraph.add(id, [routeFile]) const routeFile = normalizePath(path.resolve(config.srcDir, route))
moduleGraph.add(routeFile, [matched.loaderPath])
moduleGraph.add(id, [routeFile])
let baseContent = fs.readFileSync(routeFile, 'utf-8') moduleGraph.add(routeFile, [matched.loaderPath])
// inject raw content let baseContent = fs.readFileSync(routeFile, 'utf-8')
// this is intended for integration with CMS
// we use a special injection syntax so the content is rendered as // inject raw content
// static local content instead of included as runtime data. // this is intended for integration with CMS
if (content) { // we use a special injection syntax so the content is rendered as
baseContent = baseContent.replace( // static local content instead of included as runtime data.
/<!--\s*@content\s*-->/, if (content) {
content.replace(/\$/g, '$$$') baseContent = baseContent.replace(
) /<!--\s*@content\s*-->/,
content.replace(/\$/g, '$$$')
)
}
// params are injected with special markers and extracted as part of
// __pageData in ../markdownToVue.ts
return `__VP_PARAMS_START${JSON.stringify(params)}__VP_PARAMS_END__${baseContent}`
} }
// params are injected with special markers and extracted as part of
// __pageData in ../markdownToVue.ts
return `__VP_PARAMS_START${JSON.stringify(params)}__VP_PARAMS_END__${baseContent}`
} }
}, },

@ -1,3 +1,4 @@
import { prefixRegex } from '@rolldown/pluginutils'
import MiniSearch from 'minisearch' import MiniSearch from 'minisearch'
import fs from 'node:fs' import fs from 'node:fs'
import { readFile } from 'node:fs/promises' import { readFile } from 'node:fs/promises'
@ -28,13 +29,15 @@ export async function localSearchPlugin(
if (siteConfig.site.themeConfig?.search?.provider !== 'local') { if (siteConfig.site.themeConfig?.search?.provider !== 'local') {
return { return {
name: 'vitepress:local-search', name: 'vitepress:local-search',
resolveId(id) { resolveId: {
if (id.startsWith(LOCAL_SEARCH_INDEX_ID)) { filter: { id: prefixRegex(LOCAL_SEARCH_INDEX_ID) },
handler() {
return LOCAL_SEARCH_INDEX_REQUEST_PATH return LOCAL_SEARCH_INDEX_REQUEST_PATH
} }
}, },
load(id) { load: {
if (id.startsWith(LOCAL_SEARCH_INDEX_REQUEST_PATH)) { filter: { id: prefixRegex(LOCAL_SEARCH_INDEX_REQUEST_PATH) },
handler() {
return `export default '{}'` return `export default '{}'`
} }
} }
@ -176,36 +179,40 @@ export async function localSearchPlugin(
pending = scanForBuild().then(onIndexUpdated) pending = scanForBuild().then(onIndexUpdated)
}, },
resolveId(id) { resolveId: {
if (id.startsWith(LOCAL_SEARCH_INDEX_ID)) { filter: { id: prefixRegex(LOCAL_SEARCH_INDEX_ID) },
handler(id) {
return `/${id}` return `/${id}`
} }
}, },
async load(id) { load: {
if (id === LOCAL_SEARCH_INDEX_REQUEST_PATH) { filter: { id: prefixRegex(LOCAL_SEARCH_INDEX_REQUEST_PATH) },
await pending async handler(id) {
if (process.env.NODE_ENV === 'production') { if (id === LOCAL_SEARCH_INDEX_REQUEST_PATH) {
await scanForBuild() await pending
if (process.env.NODE_ENV === 'production') {
await scanForBuild()
}
let records: string[] = []
for (const [locale] of indexByLocales) {
records.push(
`${JSON.stringify(
locale
)}: () => import('${LOCAL_SEARCH_INDEX_ID}${locale}')`
)
}
return `export default {${records.join(',')}}`
} else {
await pending
return `export default ${JSON.stringify(
JSON.stringify(
indexByLocales.get(
id.replace(LOCAL_SEARCH_INDEX_REQUEST_PATH, '')
) ?? {}
)
)}`
} }
let records: string[] = []
for (const [locale] of indexByLocales) {
records.push(
`${JSON.stringify(
locale
)}: () => import('${LOCAL_SEARCH_INDEX_ID}${locale}')`
)
}
return `export default {${records.join(',')}}`
} else if (id.startsWith(LOCAL_SEARCH_INDEX_REQUEST_PATH)) {
await pending
return `export default ${JSON.stringify(
JSON.stringify(
indexByLocales.get(
id.replace(LOCAL_SEARCH_INDEX_REQUEST_PATH, '')
) ?? {}
)
)}`
} }
}, },

@ -56,8 +56,9 @@ export const staticDataPlugin: Plugin = {
server = _server server = _server
}, },
async load(id) { load: {
if (loaderMatch.test(id)) { filter: { id: loaderMatch },
async handler(id) {
let _resolve: ((res: any) => void) | undefined let _resolve: ((res: any) => void) | undefined
if (isBuild) { if (isBuild) {
if (idToPendingPromiseMap[id]) return idToPendingPromiseMap[id] if (idToPendingPromiseMap[id]) return idToPendingPromiseMap[id]

@ -7,8 +7,9 @@ export const webFontsPlugin = (enabled = false): Plugin => ({
name: 'vitepress:webfonts', name: 'vitepress:webfonts',
enforce: 'pre', enforce: 'pre',
transform(code, id) { transform: {
if (/[\\/]fonts\.s?css/.test(id)) { filter: { id: /[\\/]fonts\.s?css/ },
handler(code) {
if (enabled) { if (enabled) {
return code.match(webfontMarkerRE)?.[1] return code.match(webfontMarkerRE)?.[1]
} else { } else {

Loading…
Cancel
Save