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

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

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

@ -1,3 +1,4 @@
import { exactRegex } from '@rolldown/pluginutils'
import path from 'node:path'
import c from 'picocolors'
import {
@ -166,11 +167,12 @@ export async function createVitePressPlugin(
: baseConfig
},
resolveId(id, importer, resolveOptions) {
if (id === SITE_DATA_ID) {
return SITE_DATA_REQUEST_PATH
}
if (startsWithThemeRE.test(id)) {
resolveId: {
filter: { id: [exactRegex(SITE_DATA_ID), startsWithThemeRE] },
handler(id, importer, resolveOptions) {
if (id === SITE_DATA_ID) {
return SITE_DATA_REQUEST_PATH
}
return this.resolve(
siteConfig.themeDir + id.slice(6),
importer,
@ -179,8 +181,9 @@ export async function createVitePressPlugin(
}
},
load(id) {
if (id === SITE_DATA_REQUEST_PATH) {
load: {
filter: { id: exactRegex(SITE_DATA_REQUEST_PATH) },
handler() {
let data = siteData
// head info is not needed by the client in production build
if (config.command === 'build') {
@ -196,48 +199,50 @@ export async function createVitePressPlugin(
}
},
// TODO: use plugin hook filters
async transform(code, id) {
if (docsearchRE.test(normalizePath(id))) {
return code
.replaceAll('[data-theme=dark]', '.dark')
.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)
})
transform: {
filter: { id: [docsearchRE, /\.vue$/, /\.md$/] },
async handler(code, id) {
if (docsearchRE.test(normalizePath(id))) {
return code
.replaceAll('[data-theme=dark]', '.dark')
.replaceAll(/\(max-width:\s*768px\)/g, '(max-width: 767px)')
.replaceAll(/\(min-width:\s*769px\)/g, '(min-width: 768px)')
}
if (
this.environment.mode === 'dev' &&
this.environment.name === 'client'
) {
logDeadLinks(deadLinks, siteConfig.logger, true)
const payload: PageDataPayload = {
path: `/${siteConfig.rewrites.map[relativePath] || relativePath}`,
pageData
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)
})
}
// notify the client to update page data
this.environment.hot.send({
type: 'custom',
event: 'vitepress:pageData',
data: payload
})
if (
this.environment.mode === 'dev' &&
this.environment.name === 'client'
) {
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',
enforce: 'pre',
resolveId(id) {
if (!id.endsWith('.md')) return
const normalizedId = id.startsWith(config.srcDir)
? id
: normalizePath(path.resolve(config.srcDir, id.replace(/^\//, '')))
const matched = config.dynamicRoutes.find(
(r) => r.fullPath === normalizedId
)
if (matched) return normalizedId
resolveId: {
filter: { id: /\.md$/ },
handler(id) {
const normalizedId = id.startsWith(config.srcDir)
? id
: normalizePath(path.resolve(config.srcDir, id.replace(/^\//, '')))
const matched = config.dynamicRoutes.find(
(r) => r.fullPath === normalizedId
)
if (matched) return normalizedId
}
},
load(id) {
const matched = config.dynamicRoutes.find((r) => r.fullPath === id)
if (matched) {
const { route, params, content } = matched
const routeFile = normalizePath(path.resolve(config.srcDir, route))
moduleGraph.add(id, [routeFile])
moduleGraph.add(routeFile, [matched.loaderPath])
let baseContent = fs.readFileSync(routeFile, 'utf-8')
// inject raw content
// this is intended for integration with CMS
// we use a special injection syntax so the content is rendered as
// static local content instead of included as runtime data.
if (content) {
baseContent = baseContent.replace(
/<!--\s*@content\s*-->/,
content.replace(/\$/g, '$$$')
)
load: {
filter: { id: /\.md$/ },
handler(id) {
const matched = config.dynamicRoutes.find((r) => r.fullPath === id)
if (matched) {
const { route, params, content } = matched
const routeFile = normalizePath(path.resolve(config.srcDir, route))
moduleGraph.add(id, [routeFile])
moduleGraph.add(routeFile, [matched.loaderPath])
let baseContent = fs.readFileSync(routeFile, 'utf-8')
// inject raw content
// this is intended for integration with CMS
// we use a special injection syntax so the content is rendered as
// static local content instead of included as runtime data.
if (content) {
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 fs from 'node:fs'
import { readFile } from 'node:fs/promises'
@ -28,13 +29,15 @@ export async function localSearchPlugin(
if (siteConfig.site.themeConfig?.search?.provider !== 'local') {
return {
name: 'vitepress:local-search',
resolveId(id) {
if (id.startsWith(LOCAL_SEARCH_INDEX_ID)) {
resolveId: {
filter: { id: prefixRegex(LOCAL_SEARCH_INDEX_ID) },
handler() {
return LOCAL_SEARCH_INDEX_REQUEST_PATH
}
},
load(id) {
if (id.startsWith(LOCAL_SEARCH_INDEX_REQUEST_PATH)) {
load: {
filter: { id: prefixRegex(LOCAL_SEARCH_INDEX_REQUEST_PATH) },
handler() {
return `export default '{}'`
}
}
@ -176,36 +179,40 @@ export async function localSearchPlugin(
pending = scanForBuild().then(onIndexUpdated)
},
resolveId(id) {
if (id.startsWith(LOCAL_SEARCH_INDEX_ID)) {
resolveId: {
filter: { id: prefixRegex(LOCAL_SEARCH_INDEX_ID) },
handler(id) {
return `/${id}`
}
},
async load(id) {
if (id === LOCAL_SEARCH_INDEX_REQUEST_PATH) {
await pending
if (process.env.NODE_ENV === 'production') {
await scanForBuild()
load: {
filter: { id: prefixRegex(LOCAL_SEARCH_INDEX_REQUEST_PATH) },
async handler(id) {
if (id === LOCAL_SEARCH_INDEX_REQUEST_PATH) {
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
},
async load(id) {
if (loaderMatch.test(id)) {
load: {
filter: { id: loaderMatch },
async handler(id) {
let _resolve: ((res: any) => void) | undefined
if (isBuild) {
if (idToPendingPromiseMap[id]) return idToPendingPromiseMap[id]

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

Loading…
Cancel
Save