From 981ed9da9c0ed2bf525d0480237bddf7b881cc3f Mon Sep 17 00:00:00 2001 From: HusnainMaroof Date: Sun, 1 Nov 2026 17:39:47 +0500 Subject: [PATCH] fix(themes): move chunk rules to Rolldown codeSplitting groups so shared-home holds only home code Rolldown runs output.manualChunks as one group with includeDependenciesRecursively on, so shared-home captured the product components, the shared libraries, Swiper and the engine, and shared-product / shared-common were never emitted. The shared-common matchers also never matched the engine's prebuilt flat files. Move the rules to build.rolldownOptions.output.codeSplitting.groups with priorities: vendor-core, vendor-swiper and vendor-salla for libraries, and shared-common / shared-product / shared-home for theme code. shared-home drops from 160.4 kB to 8.5 kB gzip and total client JS from 425.4 kB to 415.6 kB. Part of #96 Co-Authored-By: Claude Opus 5.5 --- vite.config.ts | 104 +++++++++++++++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 38 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 70d3d90..187662a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,6 +3,29 @@ import { twilightReact } from '@salla.sa/twilight-theme-engine/vite'; import { visualizer } from 'rollup-plugin-visualizer'; import path from 'path'; +type ModuleInfo = { isEntry: boolean; importers: string[] } | null; + +// Group name callback that names a matching module only when it is statically +// reachable from an entry or from app/hydrate.tsx (which bootClient always loads), +// i.e. code that every page runs anyway. +function everyPage(test: RegExp, name: string) { + return (id: string, ctx: { getModuleInfo(id: string): ModuleInfo }): string | null => { + if (!test.test(id)) return null; + const seen = new Set(); + const queue = [id]; + while (queue.length) { + const cur = queue.pop()!; + if (seen.has(cur)) continue; + seen.add(cur); + const info = ctx.getModuleInfo(cur); + if (!info) continue; + if (info.isEntry || cur.endsWith('/app/hydrate.tsx')) return name; + queue.push(...info.importers); + } + return null; + }; +} + export default defineConfig(async () => ({ plugins: [ // twilightReact() registers and configures the SSR runtime, so a theme @@ -84,44 +107,49 @@ export default defineConfig(async () => ({ external: ['@salla.sa/twilight-components', '@salla.sa/twilight-components/dist/*'], output: { chunkFileNames: 'assets/[name]-[hash].js', - manualChunks(id: string): string | undefined { - // Kept apart so the entry, which only waits for the LCP image, imports nothing heavy. - if (id.includes('vite/preload-helper')) { - return 'preload-helper'; - } - if (id.includes('node_modules/react/') || id.includes('node_modules/react-dom/')) { - return 'vendor-react'; - } - if (id.includes('node_modules/@tanstack/react-router/')) { - return 'vendor-router'; - } - if (id.includes('node_modules/@uidotdev/usehooks/')) { - return 'vendor-hooks'; - } - if ( - id.includes('/components/common/Breadcrumb') || - id.includes('/components/common/NoContent') || - id.includes('/components/common/Image') || - id.includes('/components/common/Link') || - id.includes('/components/common/CurrencySymbol') - ) { - return 'shared-common'; - } - if ( - id.includes('/components/product/ProductCard') || - id.includes('/components/product/ProductDetails') || - id.includes('/components/product/ProductGallery') || - id.includes('/components/product/AddToCartForm') - ) { - return 'shared-product'; - } - if ( - id.includes('/components/home/ProductsSlider') || - id.includes('/components/home/Testimonials') || - id.includes('/components/home/HomeComponentRenderer') - ) { - return 'shared-home'; - } + // Rolldown turns `manualChunks` into a single group that also captures every + // dependency of a matched module, whatever name the function returns for it. + // Matching the home components therefore dragged i18next, ky, sonner, + // query-core, swiper, the engine, etc. into one 530 kB `shared-home`. + // Prioritised groups claim the libraries first, so each chunk keeps its own code. + codeSplitting: { + groups: [ + // Kept apart so the entry, which only waits for the LCP image, imports nothing heavy. + { name: 'preload-helper', test: /vite\/preload-helper/, priority: 100 }, + { name: 'vendor-react', test: /node_modules\/(react|react-dom)\//, priority: 90 }, + { + name: 'vendor-router', + test: /node_modules\/@tanstack\/react-router\//, + priority: 80, + }, + { + name: 'vendor-core', + test: /node_modules\/(i18next|react-i18next|ky|sonner|use-sync-external-store|@tanstack\/(query-core|react-query|virtual-core|react-virtual))\//, + priority: 70, + }, + { name: 'vendor-swiper', test: /node_modules\/swiper\//, priority: 60 }, + { name: 'vendor-hooks', test: /node_modules\/@uidotdev\/usehooks\//, priority: 50 }, + // The engine ships prebuilt flat files (dist/Breadcrumb-*.js, dist/chunk-*.js), + // so match by package and keep only what every page loads; screens the engine + // imports lazily stay in their own route chunks. + { + name: everyPage( + /node_modules\/(@salla\.sa\/twilight-(theme-engine|components-react)|@lit\/react|@stencil\/react-output-target|react-intersection-observer)\//, + 'vendor-salla' + ), + priority: 40, + }, + { + name: everyPage(/\/app\/(components\/common\/|components\/icons|hooks\/use|lib\/)/, 'shared-common'), + priority: 30, + }, + // The product barrel and ProductPage import ../home, so they are left to shared-home. + { + name: everyPage(/\/app\/components\/product\/(?!index|ProductPage)/, 'shared-product'), + priority: 20, + }, + { name: 'shared-home', test: /\/app\/components\/home\//, priority: 10 }, + ], }, }, },