From 021074c0ee2f46de09261a264fe897790004639f Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 6 Aug 2026 15:48:10 +0800 Subject: [PATCH] build(web): split the shell dist into index and vendor chunks with langs/ and fonts/ dirs --- apps/web/vite.config.ts | 91 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 842317911e..d2d717f0dc 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -18,10 +18,101 @@ function rejectStandaloneServe(): Plugin { } } +/** + * Vendor-chunk membership, by exact npm package name — the heavy render + * families (math, highlight, markdown) that change only on dependency bumps. + * Only packages workspace code imports DIRECTLY need listing: their private + * transitive dependencies (unified/hast/oniguruma machinery, ~50 packages) + * are imported solely by these and rollup's chunk coloring pulls them into + * vendor automatically. A dependency shared with index-side code falls back + * to index — a few kB of dilution, never a correctness problem. Anything not + * listed (react family, the vendored cordis workspace, tiny helpers like + * anser/clsx, all workspace code) stays in the default `index` chunk, so + * editing shell code re-hashes only index and returning clients keep the + * cached vendor chunk. + */ +const VENDOR_PACKAGES: ReadonlySet = new Set([ + // math + 'katex', + 'rehype-katex', + // syntax highlight (@shikijs/langs is handled separately below — + // lazy grammars must not land here) + 'shiki', + // markdown pipeline + 'react-markdown', + 'remark-gfm', + 'remark-math', + 'mdast-util-from-markdown', + 'mdast-util-gfm', + 'micromark-extension-gfm', + 'micromark-extension-math', + 'micromark-factory-space', + 'micromark-util-character', + 'micromark-util-symbol', + 'micromark-util-types', +]) + +/** + * Boot grammars statically imported by ui-primitives' highlight.ts + * (`@shikijs/langs/typescript` → `dist/typescript.mjs`, etc.). They live in + * the same package as the lazy read-card grammars, but unlike those they are + * part of the initial load and belong in the vendor chunk; the lazy ones must + * stay unassigned so each keeps its own on-demand chunk. + */ +const BOOT_GRAMMAR_FILES: readonly string[] = [ + 'dist/typescript.mjs', + 'dist/shellscript.mjs', + 'dist/json.mjs', +] + +/** Font asset extensions routed to assets/fonts/ (KaTeX's woff2/woff/ttf faces today). */ +const FONT_EXTENSIONS: readonly string[] = ['.woff2', '.woff', '.ttf'] + +/** npm package name of a resolved module id (the segment after the LAST `node_modules/` — pnpm nests the real package under an inner node_modules). */ +function npmPackageOf(id: string): string | undefined { + const parts = id.split('/node_modules/') + if (parts.length === 1) return undefined + const [first, second] = parts[parts.length - 1].split('/') + if (first.startsWith('.')) return undefined // .pnpm store segment, not a package + return first.startsWith('@') ? `${first}/${second}` : first +} + export default defineConfig({ plugins: [rejectStandaloneServe(), react()], build: { sourcemap: true, + rollupOptions: { + output: { + // Output layout: the two main chunks stay at assets/ root; lazy + // @shikijs/langs grammar chunks group under assets/langs/; fonts + // (today all KaTeX faces referenced by vendor.css) group under + // assets/fonts/. Sourcemaps need no arrangement: rollup writes each + // .map next to its js and references it by bare relative filename. + chunkFileNames(chunk): string { + // Grammar chunks are recognized by their member modules, not the + // facade: shared embedded-grammar chunks (e.g. html+javascript, + // split out because php/ruby/mdx embed them) have no facade at all. + // index and vendor are excluded by name — vendor legitimately + // carries the three boot grammars. + if (chunk.name === 'index' || chunk.name === 'vendor') return 'assets/[name]-[hash].js' + const isLangChunk = chunk.moduleIds.some(id => id.includes('/node_modules/@shikijs/langs/')) + return isLangChunk ? 'assets/langs/[name]-[hash].js' : 'assets/[name]-[hash].js' + }, + assetFileNames(asset): string { + const fileName = asset.names[0] ?? '' + const isFont = FONT_EXTENSIONS.some(ext => fileName.endsWith(ext)) + return isFont ? 'assets/fonts/[name]-[hash][extname]' : 'assets/[name]-[hash][extname]' + }, + manualChunks(id: string): string | undefined { + const pkg = npmPackageOf(id) + if (pkg === undefined) return undefined // workspace + vendored cordis: index + if (pkg === '@shikijs/langs') { + return BOOT_GRAMMAR_FILES.some(file => id.endsWith(`/${file}`)) ? 'vendor' : undefined + } + return VENDOR_PACKAGES.has(pkg) ? 'vendor' : undefined + }, + }, + }, }, resolve: { // Workspace packages resolve to SOURCE: package.json exports point at lib