|
1 | | -import { dirname, relative } from 'pathslash'; |
| 1 | +import { readFileSync } from 'node:fs'; |
| 2 | +import { dirname, relative, resolve } from 'pathslash'; |
2 | 3 | import type { Plugin } from 'vite'; |
3 | | -import { createPagesPlugin, generateComponentManifest } from 'void/pages'; |
| 4 | +import { |
| 5 | + createPagesPlugin, |
| 6 | + generateComponentManifest, |
| 7 | + resolveEffectiveLayoutChain, |
| 8 | +} from 'void/pages'; |
4 | 9 | import type { PageScanResult, StaticPageDataBuildOptions } from 'void/pages'; |
| 10 | +import { findIslandImports, resolveSpecifier } from 'void/pages-islands-plugin'; |
5 | 11 |
|
6 | 12 | const CLIENT_ENTRY_ID = 'virtual:void-pages-client'; |
7 | 13 | const WORKER_ENVIRONMENT_NAME = 'void_worker'; |
@@ -34,17 +40,7 @@ type PagesPluginOptions = { |
34 | 40 |
|
35 | 41 | export function pagesPlugin(pagesDir: string, options?: PagesPluginOptions): Plugin { |
36 | 42 | const root = options?.root ?? dirname(pagesDir); |
37 | | - const pagesDirRelative = relative(root, pagesDir); |
38 | | - const pagesOptimizeEntries = `${pagesDirRelative}/**/*.{js,jsx,ts,tsx}`; |
39 | | - const pagesClientOptimizeEntries = [ |
40 | | - pagesOptimizeEntries, |
41 | | - `!${pagesDirRelative}/**/*.server.{js,jsx,ts,tsx}`, |
42 | | - // Island pages render only on the server. Island components in the pages |
43 | | - // directory (`_Counter.tsx`) still match the first pattern. |
44 | | - `!${pagesDirRelative}/**/*.island.{js,jsx,ts,tsx}`, |
45 | | - // Setting entries turns off Vite's default `__tests__` ignore, so keep it. |
46 | | - `!${pagesDirRelative}/**/__tests__/**`, |
47 | | - ]; |
| 43 | + const pagesOptimizeEntries = `${relative(root, pagesDir)}/**/*.{js,jsx,ts,tsx}`; |
48 | 44 | return createPagesPlugin(pagesDir, { |
49 | 45 | name: 'void-react:pages', |
50 | 46 | root, |
@@ -74,7 +70,7 @@ export function pagesPlugin(pagesDir: string, options?: PagesPluginOptions): Plu |
74 | 70 | exclude: ['@void/react'], |
75 | 71 | // Crawl page modules before the first client request so third-party |
76 | 72 | // imports are pre-bundled instead of triggering a full reload. |
77 | | - entries: pagesClientOptimizeEntries, |
| 73 | + entries: clientOptimizeEntries(await scan(), pagesDir, root), |
78 | 74 | // Pre-include React entries that are discovered late via dynamic |
79 | 75 | // imports during SPA navigation, avoiding a full-page reload |
80 | 76 | // from Vite's dep optimizer. void/pages-client and |
@@ -124,6 +120,49 @@ export function pagesPlugin(pagesDir: string, options?: PagesPluginOptions): Plu |
124 | 120 | }); |
125 | 121 | } |
126 | 122 |
|
| 123 | +function clientOptimizeEntries( |
| 124 | + scan: PageScanResult | null, |
| 125 | + pagesDir: string, |
| 126 | + root: string, |
| 127 | +): Array<string> { |
| 128 | + if (!scan) { |
| 129 | + return []; |
| 130 | + } |
| 131 | + const browserFiles = new Set<string>(); |
| 132 | + const islandFiles = new Set<string>(); |
| 133 | + for (const page of scan.pages) { |
| 134 | + const layouts = resolveEffectiveLayoutChain(page, scan.layouts, scan.namedLayouts); |
| 135 | + if (page.island) { |
| 136 | + islandFiles.add(resolve(pagesDir, page.componentPath)); |
| 137 | + for (const layout of layouts) { |
| 138 | + if (layout.island) { |
| 139 | + islandFiles.add(resolve(pagesDir, layout.filePath)); |
| 140 | + } |
| 141 | + } |
| 142 | + } else { |
| 143 | + browserFiles.add(resolve(pagesDir, page.componentPath)); |
| 144 | + for (const layout of layouts) { |
| 145 | + browserFiles.add(resolve(pagesDir, layout.filePath)); |
| 146 | + } |
| 147 | + } |
| 148 | + } |
| 149 | + // Only hydrated components run in the browser; island pages and their |
| 150 | + // unmarked imports may depend on Worker-only packages. |
| 151 | + for (const file of islandFiles) { |
| 152 | + if (!/\.island\.(tsx|jsx)$/.test(file)) { |
| 153 | + continue; |
| 154 | + } |
| 155 | + for (const { specifier } of findIslandImports(readFileSync(file, 'utf8'), file)) { |
| 156 | + const component = resolveSpecifier(specifier, dirname(file), ['.tsx', '.jsx', '.ts', '.js']); |
| 157 | + if (component) { |
| 158 | + browserFiles.add(component); |
| 159 | + } |
| 160 | + } |
| 161 | + } |
| 162 | + // Entries are glob patterns, so route names such as [id] must be literal. |
| 163 | + return [...browserFiles].map((file) => relative(root, file).replace(/[()[\]{}*?!|]/g, '\\$&')); |
| 164 | +} |
| 165 | + |
127 | 166 | function generateRendererModule(scan: PageScanResult, pagesDir: string, isDev: boolean): string { |
128 | 167 | const preambleScript = isDev ? REACT_REFRESH_PREAMBLE : ''; |
129 | 168 |
|
|
0 commit comments