diff --git a/src/client/theme-default/components/VPLocalSearchBox.vue b/src/client/theme-default/components/VPLocalSearchBox.vue index 2f49250cae6b..965ab1df985f 100644 --- a/src/client/theme-default/components/VPLocalSearchBox.vue +++ b/src/client/theme-default/components/VPLocalSearchBox.vue @@ -28,6 +28,7 @@ import { } from 'vue' import type { ModalTranslations } from '../../../../types/local-search' import { pathToFile } from '../../app/utils' +import { escapeRegExp } from '../../shared' import { useData } from '../composables/data' import { LRUCache } from '../support/lru' import { createSearchTranslate } from '../support/translation' @@ -146,8 +147,8 @@ const cache = new LRUCache>(16) // 16 files debouncedWatch( () => [searchIndex.value, filterText.value, showDetailedList.value] as const, async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => { - - if (old?.[0] !== index) { // in case of hmr + if (old?.[0] !== index) { + // in case of hmr cache.clear() } @@ -396,11 +397,7 @@ function formMarkRegex(terms: Set) { return new RegExp( [...terms] .sort((a, b) => b.length - a.length) - .map((term) => { - return `(${term - .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') - .replace(/-/g, '\\x2d')})` - }) + .map((term) => `(${escapeRegExp(term)})`) .join('|'), 'gi' ) diff --git a/src/client/theme-default/without-fonts.ts b/src/client/theme-default/without-fonts.ts index f53fb679dff1..35532c38b7aa 100644 --- a/src/client/theme-default/without-fonts.ts +++ b/src/client/theme-default/without-fonts.ts @@ -11,9 +11,6 @@ import type { Theme } from 'vitepress' import VPBadge from './components/VPBadge.vue' import Layout from './Layout.vue' -// Note: if we add more optional components here, i.e. components that are not -// used in the theme by default unless the user imports them, make sure to update -// the `lazyDefaultThemeComponentsRE` regex in src/node/build/bundle.ts. export { default as VPImage } from './components/VPImage.vue' export { default as VPButton } from './components/VPButton.vue' export { default as VPHomeHero } from './components/VPHomeHero.vue' diff --git a/src/node/build/bundle.ts b/src/node/build/bundle.ts index 159bb43ce874..6eee911dedbc 100644 --- a/src/node/build/bundle.ts +++ b/src/node/build/bundle.ts @@ -11,18 +11,29 @@ import { import { APP_PATH } from '../alias' import type { SiteConfig } from '../config' import { createVitePressPlugin } from '../plugin' -import { sanitizeFileName, slash } from '../shared' +import { escapeRegExp, sanitizeFileName, slash } from '../shared' import { task } from '../utils/task' import { buildMPAClient } from './buildMPAClient' -// A list of default theme components that should only be loaded on demand. -const lazyDefaultThemeComponentsRE = - /VP(HomeSponsors|DocAsideSponsors|TeamPage|TeamMembers|LocalSearchBox|AlgoliaSearchBox|CarbonAds|DocAsideCarbonAds|Sponsors)/ +// https://github.com/vitejs/vite/blob/d2aa0969ee316000d3b957d7e879f001e85e369e/packages/vite/src/node/plugins/splitVendorChunk.ts#L14 +const CSS_LANGS_RE = + /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/ const clientDir = normalizePath( path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../client') ) +// these deps are also being used in the client code (outside of the theme) +// exclude them from the theme chunk so there is no circular dependency +const excludedModules = [ + '/@siteData', + 'node_modules/@vueuse/core/', + 'node_modules/@vueuse/shared/', + 'node_modules/vue/', + 'node_modules/vue-demi/', + clientDir +] + // bundles the VitePress app for both client AND server. export async function bundle( config: SiteConfig, @@ -47,6 +58,12 @@ export async function bundle( input[slash(alias).replace(/\//g, '_')] = path.resolve(config.srcDir, file) }) + const themeEntryRE = new RegExp( + `^${escapeRegExp( + path.resolve(config.themeDir, 'index.js').replace(/\\/g, '/') + ).slice(0, -2)}m?(j|t)s` + ) + // resolve options to pass to vite const { rollupOptions } = options @@ -109,12 +126,6 @@ export async function bundle( : `${config.assetsDir}/chunks/[name].[hash].js` }, manualChunks(id, ctx) { - if (lazyDefaultThemeComponentsRE.test(id)) { - return - } - if (id.startsWith(`${clientDir}/theme-default`)) { - return 'theme' - } // move known framework code into a stable chunk so that // custom theme changes do not invalidate hash for all pages if (id.startsWith('\0vite')) { @@ -135,6 +146,19 @@ export async function bundle( ) { return 'framework' } + + if ( + (id.startsWith(`${clientDir}/theme-default`) || + !excludedModules.some((i) => id.includes(i))) && + staticImportedByEntry( + id, + ctx.getModuleInfo, + cacheTheme, + themeEntryRE + ) + ) { + return 'theme' + } } }) } @@ -182,6 +206,7 @@ export async function bundle( } const cache = new Map() +const cacheTheme = new Map() /** * Check if a module is statically imported by at least one entry. @@ -189,7 +214,7 @@ const cache = new Map() function isEagerChunk(id: string, getModuleInfo: Rollup.GetModuleInfo) { if ( id.includes('node_modules') && - !/\.css($|\\?)/.test(id) && + !CSS_LANGS_RE.test(id) && staticImportedByEntry(id, getModuleInfo, cache) ) { return true @@ -200,6 +225,7 @@ function staticImportedByEntry( id: string, getModuleInfo: Rollup.GetModuleInfo, cache: Map, + entryRE: RegExp | null = null, importStack: string[] = [] ): boolean { if (cache.has(id)) { @@ -216,7 +242,7 @@ function staticImportedByEntry( return false } - if (mod.isEntry) { + if (entryRE ? entryRE.test(id) : mod.isEntry) { cache.set(id, true) return true } @@ -225,6 +251,7 @@ function staticImportedByEntry( importer, getModuleInfo, cache, + entryRE, importStack.concat(id) ) ) diff --git a/src/shared/shared.ts b/src/shared/shared.ts index 44f562a97c3f..8c8e48848c7c 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -195,3 +195,8 @@ export function treatAsHtml(filename: string): boolean { return ext == null || !KNOWN_EXTENSIONS.has(ext.toLowerCase()) } + +// https://github.com/sindresorhus/escape-string-regexp/blob/ba9a4473850cb367936417e97f1f2191b7cc67dd/index.js +export function escapeRegExp(str: string) { + return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d') +}