@useavalon/avalon 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/dist/mod.js +1 -0
  2. package/dist/src/build/integration-bundler-plugin.js +1 -0
  3. package/dist/src/build/integration-config.js +1 -0
  4. package/dist/src/build/integration-detection-plugin.js +1 -0
  5. package/dist/src/build/integration-resolver-plugin.js +1 -0
  6. package/dist/src/build/island-manifest.js +1 -0
  7. package/dist/src/build/island-types-generator.js +5 -0
  8. package/dist/src/build/mdx-island-transform.js +2 -0
  9. package/dist/src/build/mdx-plugin.js +1 -0
  10. package/dist/src/build/page-island-transform.js +3 -0
  11. package/dist/src/build/prop-extractors/index.js +1 -0
  12. package/dist/src/build/prop-extractors/lit.js +1 -0
  13. package/dist/src/build/prop-extractors/qwik.js +1 -0
  14. package/dist/src/build/prop-extractors/solid.js +1 -0
  15. package/dist/src/build/prop-extractors/svelte.js +1 -0
  16. package/dist/src/build/prop-extractors/vue.js +1 -0
  17. package/dist/src/build/sidecar-file-manager.js +1 -0
  18. package/dist/src/build/sidecar-renderer.js +6 -0
  19. package/dist/src/client/adapters/index.js +1 -0
  20. package/dist/src/client/components.js +1 -0
  21. package/dist/src/client/css-hmr-handler.js +1 -0
  22. package/dist/src/client/framework-adapter.js +13 -0
  23. package/dist/src/client/hmr-coordinator.js +1 -0
  24. package/dist/src/client/hmr-error-overlay.js +214 -0
  25. package/dist/src/client/main.js +39 -0
  26. package/dist/src/components/Image.js +1 -0
  27. package/dist/src/components/IslandErrorBoundary.js +1 -0
  28. package/dist/src/components/LayoutDataErrorBoundary.js +1 -0
  29. package/dist/src/components/LayoutErrorBoundary.js +1 -0
  30. package/dist/src/components/PersistentIsland.js +1 -0
  31. package/dist/src/components/StreamingErrorBoundary.js +1 -0
  32. package/dist/src/components/StreamingLayout.js +29 -0
  33. package/dist/src/core/components/component-analyzer.js +1 -0
  34. package/dist/src/core/components/component-detection.js +5 -0
  35. package/dist/src/core/components/enhanced-framework-detector.js +1 -0
  36. package/dist/src/core/components/framework-registry.js +1 -0
  37. package/dist/src/core/content/mdx-processor.js +1 -0
  38. package/dist/src/core/integrations/index.js +1 -0
  39. package/dist/src/core/integrations/loader.js +1 -0
  40. package/dist/src/core/integrations/registry.js +1 -0
  41. package/dist/src/core/islands/island-persistence.js +1 -0
  42. package/dist/src/core/islands/island-state-serializer.js +1 -0
  43. package/dist/src/core/islands/persistent-island-context.js +1 -0
  44. package/dist/src/core/islands/use-persistent-state.js +1 -0
  45. package/dist/src/core/layout/enhanced-layout-resolver.js +1 -0
  46. package/dist/src/core/layout/layout-cache-manager.js +1 -0
  47. package/dist/src/core/layout/layout-composer.js +1 -0
  48. package/dist/src/core/layout/layout-data-loader.js +1 -0
  49. package/dist/src/core/layout/layout-discovery.js +1 -0
  50. package/dist/src/core/layout/layout-matcher.js +1 -0
  51. package/dist/src/core/layout/layout-types.js +1 -0
  52. package/dist/src/core/modules/framework-module-resolver.js +1 -0
  53. package/dist/src/islands/component-analysis.js +1 -0
  54. package/dist/src/islands/css-utils.js +17 -0
  55. package/dist/src/islands/discovery/index.js +1 -0
  56. package/dist/src/islands/discovery/registry.js +1 -0
  57. package/dist/src/islands/discovery/resolver.js +2 -0
  58. package/dist/src/islands/discovery/scanner.js +1 -0
  59. package/dist/src/islands/discovery/types.js +1 -0
  60. package/dist/src/islands/discovery/validator.js +18 -0
  61. package/dist/src/islands/discovery/watcher.js +1 -0
  62. package/dist/src/islands/framework-detection.js +1 -0
  63. package/dist/src/islands/integration-loader.js +1 -0
  64. package/dist/src/islands/island.js +1 -0
  65. package/dist/src/islands/render-cache.js +1 -0
  66. package/dist/src/islands/types.js +1 -0
  67. package/dist/src/islands/universal-css-collector.js +5 -0
  68. package/dist/src/islands/universal-head-collector.js +2 -0
  69. package/dist/src/layout-system.js +1 -0
  70. package/dist/src/middleware/discovery.js +1 -0
  71. package/dist/src/middleware/executor.js +1 -0
  72. package/dist/src/middleware/index.js +1 -0
  73. package/dist/src/middleware/types.js +1 -0
  74. package/dist/src/nitro/build-config.js +1 -0
  75. package/dist/src/nitro/config.js +1 -0
  76. package/dist/src/nitro/error-handler.js +198 -0
  77. package/dist/src/nitro/index.js +1 -0
  78. package/dist/src/nitro/island-manifest.js +2 -0
  79. package/dist/src/nitro/middleware-adapter.js +1 -0
  80. package/dist/src/nitro/renderer.js +183 -0
  81. package/dist/src/nitro/route-discovery.js +1 -0
  82. package/dist/src/nitro/types.js +1 -0
  83. package/dist/src/render/collect-css.js +3 -0
  84. package/dist/src/render/error-pages.js +48 -0
  85. package/dist/src/render/isolated-ssr-renderer.js +1 -0
  86. package/dist/src/render/ssr.js +90 -0
  87. package/dist/src/schemas/api.js +1 -0
  88. package/dist/src/schemas/core.js +1 -0
  89. package/dist/src/schemas/index.js +1 -0
  90. package/dist/src/schemas/layout.js +1 -0
  91. package/dist/src/schemas/routing/index.js +1 -0
  92. package/dist/src/schemas/routing.js +1 -0
  93. package/dist/src/types/as-island.js +1 -0
  94. package/dist/src/types/layout.js +1 -0
  95. package/dist/src/types/routing.js +1 -0
  96. package/dist/src/types/types.js +1 -0
  97. package/dist/src/utils/dev-logger.js +12 -0
  98. package/dist/src/utils/fs.js +1 -0
  99. package/dist/src/vite-plugin/auto-discover.js +1 -0
  100. package/dist/src/vite-plugin/config.js +1 -0
  101. package/dist/src/vite-plugin/errors.js +1 -0
  102. package/dist/src/vite-plugin/image-optimization.js +45 -0
  103. package/dist/src/vite-plugin/integration-activator.js +1 -0
  104. package/dist/src/vite-plugin/island-sidecar-plugin.js +1 -0
  105. package/dist/src/vite-plugin/module-discovery.js +1 -0
  106. package/dist/src/vite-plugin/nitro-integration.js +42 -0
  107. package/dist/src/vite-plugin/plugin.js +1 -0
  108. package/dist/src/vite-plugin/types.js +1 -0
  109. package/dist/src/vite-plugin/validation.js +2 -0
  110. package/package.json +14 -20
  111. package/mod.ts +0 -302
  112. package/src/build/integration-bundler-plugin.ts +0 -116
  113. package/src/build/integration-config.ts +0 -168
  114. package/src/build/integration-detection-plugin.ts +0 -117
  115. package/src/build/integration-resolver-plugin.ts +0 -90
  116. package/src/build/island-manifest.ts +0 -269
  117. package/src/build/island-types-generator.ts +0 -476
  118. package/src/build/mdx-island-transform.ts +0 -464
  119. package/src/build/mdx-plugin.ts +0 -98
  120. package/src/build/page-island-transform.ts +0 -598
  121. package/src/build/prop-extractors/index.ts +0 -21
  122. package/src/build/prop-extractors/lit.ts +0 -140
  123. package/src/build/prop-extractors/qwik.ts +0 -16
  124. package/src/build/prop-extractors/solid.ts +0 -125
  125. package/src/build/prop-extractors/svelte.ts +0 -194
  126. package/src/build/prop-extractors/vue.ts +0 -111
  127. package/src/build/sidecar-file-manager.ts +0 -104
  128. package/src/build/sidecar-renderer.ts +0 -30
  129. package/src/client/adapters/index.ts +0 -21
  130. package/src/client/components.ts +0 -35
  131. package/src/client/css-hmr-handler.ts +0 -344
  132. package/src/client/framework-adapter.ts +0 -462
  133. package/src/client/hmr-coordinator.ts +0 -396
  134. package/src/client/hmr-error-overlay.js +0 -533
  135. package/src/client/main.js +0 -824
  136. package/src/components/Image.tsx +0 -123
  137. package/src/components/IslandErrorBoundary.tsx +0 -145
  138. package/src/components/LayoutDataErrorBoundary.tsx +0 -141
  139. package/src/components/LayoutErrorBoundary.tsx +0 -127
  140. package/src/components/PersistentIsland.tsx +0 -52
  141. package/src/components/StreamingErrorBoundary.tsx +0 -233
  142. package/src/components/StreamingLayout.tsx +0 -538
  143. package/src/core/components/component-analyzer.ts +0 -192
  144. package/src/core/components/component-detection.ts +0 -508
  145. package/src/core/components/enhanced-framework-detector.ts +0 -500
  146. package/src/core/components/framework-registry.ts +0 -563
  147. package/src/core/content/mdx-processor.ts +0 -46
  148. package/src/core/integrations/index.ts +0 -19
  149. package/src/core/integrations/loader.ts +0 -125
  150. package/src/core/integrations/registry.ts +0 -175
  151. package/src/core/islands/island-persistence.ts +0 -325
  152. package/src/core/islands/island-state-serializer.ts +0 -258
  153. package/src/core/islands/persistent-island-context.tsx +0 -80
  154. package/src/core/islands/use-persistent-state.ts +0 -68
  155. package/src/core/layout/enhanced-layout-resolver.ts +0 -322
  156. package/src/core/layout/layout-cache-manager.ts +0 -485
  157. package/src/core/layout/layout-composer.ts +0 -357
  158. package/src/core/layout/layout-data-loader.ts +0 -516
  159. package/src/core/layout/layout-discovery.ts +0 -243
  160. package/src/core/layout/layout-matcher.ts +0 -299
  161. package/src/core/layout/layout-types.ts +0 -110
  162. package/src/core/modules/framework-module-resolver.ts +0 -273
  163. package/src/islands/component-analysis.ts +0 -213
  164. package/src/islands/css-utils.ts +0 -565
  165. package/src/islands/discovery/index.ts +0 -80
  166. package/src/islands/discovery/registry.ts +0 -340
  167. package/src/islands/discovery/resolver.ts +0 -477
  168. package/src/islands/discovery/scanner.ts +0 -386
  169. package/src/islands/discovery/types.ts +0 -117
  170. package/src/islands/discovery/validator.ts +0 -544
  171. package/src/islands/discovery/watcher.ts +0 -368
  172. package/src/islands/framework-detection.ts +0 -428
  173. package/src/islands/integration-loader.ts +0 -490
  174. package/src/islands/island.tsx +0 -565
  175. package/src/islands/render-cache.ts +0 -550
  176. package/src/islands/types.ts +0 -80
  177. package/src/islands/universal-css-collector.ts +0 -157
  178. package/src/islands/universal-head-collector.ts +0 -137
  179. package/src/layout-system.ts +0 -218
  180. package/src/middleware/discovery.ts +0 -268
  181. package/src/middleware/executor.ts +0 -315
  182. package/src/middleware/index.ts +0 -76
  183. package/src/middleware/types.ts +0 -99
  184. package/src/nitro/build-config.ts +0 -576
  185. package/src/nitro/config.ts +0 -483
  186. package/src/nitro/error-handler.ts +0 -636
  187. package/src/nitro/index.ts +0 -173
  188. package/src/nitro/island-manifest.ts +0 -584
  189. package/src/nitro/middleware-adapter.ts +0 -260
  190. package/src/nitro/renderer.ts +0 -1471
  191. package/src/nitro/route-discovery.ts +0 -439
  192. package/src/nitro/types.ts +0 -321
  193. package/src/render/collect-css.ts +0 -198
  194. package/src/render/error-pages.ts +0 -79
  195. package/src/render/isolated-ssr-renderer.ts +0 -654
  196. package/src/render/ssr.ts +0 -1030
  197. package/src/schemas/api.ts +0 -30
  198. package/src/schemas/core.ts +0 -64
  199. package/src/schemas/index.ts +0 -212
  200. package/src/schemas/layout.ts +0 -279
  201. package/src/schemas/routing/index.ts +0 -38
  202. package/src/schemas/routing.ts +0 -376
  203. package/src/types/as-island.ts +0 -20
  204. package/src/types/layout.ts +0 -285
  205. package/src/types/routing.ts +0 -555
  206. package/src/types/types.ts +0 -5
  207. package/src/utils/dev-logger.ts +0 -299
  208. package/src/utils/fs.ts +0 -151
  209. package/src/vite-plugin/auto-discover.ts +0 -551
  210. package/src/vite-plugin/config.ts +0 -266
  211. package/src/vite-plugin/errors.ts +0 -127
  212. package/src/vite-plugin/image-optimization.ts +0 -156
  213. package/src/vite-plugin/integration-activator.ts +0 -126
  214. package/src/vite-plugin/island-sidecar-plugin.ts +0 -176
  215. package/src/vite-plugin/module-discovery.ts +0 -189
  216. package/src/vite-plugin/nitro-integration.ts +0 -1354
  217. package/src/vite-plugin/plugin.ts +0 -403
  218. package/src/vite-plugin/types.ts +0 -327
  219. package/src/vite-plugin/validation.ts +0 -228
  220. /package/{src → dist/src}/client/types/framework-runtime.d.ts +0 -0
  221. /package/{src → dist/src}/client/types/vite-hmr.d.ts +0 -0
  222. /package/{src → dist/src}/client/types/vite-virtual-modules.d.ts +0 -0
  223. /package/{src → dist/src}/layout-system.d.ts +0 -0
  224. /package/{src → dist/src}/types/image.d.ts +0 -0
  225. /package/{src → dist/src}/types/index.d.ts +0 -0
  226. /package/{src → dist/src}/types/island-jsx.d.ts +0 -0
  227. /package/{src → dist/src}/types/island-prop.d.ts +0 -0
  228. /package/{src → dist/src}/types/mdx.d.ts +0 -0
  229. /package/{src → dist/src}/types/urlpattern.d.ts +0 -0
  230. /package/{src → dist/src}/types/vite-env.d.ts +0 -0
@@ -1,1354 +0,0 @@
1
- /**
2
- * Nitro Integration Module for Avalon Vite Plugin
3
- *
4
- * Provides coordination between Avalon's Vite plugin and Nitro:
5
- * - API routes: Auto-discovered by Nitro from `api/` directory
6
- * - Page routes: Virtual module for SSR page component discovery
7
- * - Middleware: Auto-discovered by Nitro from `middleware/` directory
8
- */
9
-
10
- import type { Plugin, ViteDevServer } from 'vite';
11
- import { nitro as nitroVitePlugin } from 'nitro/vite';
12
- import { stat as fsStat } from 'node:fs/promises';
13
- import { createRequire } from 'node:module';
14
- import { dirname, join } from 'node:path';
15
- import type { ResolvedAvalonConfig } from './types.ts';
16
- import { createNitroConfig, type AvalonNitroConfig, type NitroConfigOutput } from '../nitro/config.ts';
17
- import type { PageModule } from '../nitro/types.ts';
18
- import {
19
- createNitroBuildPlugin,
20
- createIslandManifestPlugin,
21
- createSourceMapPlugin,
22
- createSourceMapConfig,
23
- } from '../nitro/index.ts';
24
- import { discoverScopedMiddleware, executeScopedMiddleware, clearMiddlewareCache } from '../middleware/index.ts';
25
- import type { MiddlewareRoute } from '../middleware/types.ts';
26
- import type { H3Event } from 'h3';
27
- import { generateErrorPage, generateFallback404 } from '../render/error-pages.ts';
28
- import { collectCssFromModuleGraph, injectSsrCss } from '../render/collect-css.ts';
29
- import { getUniversalCSSForHead } from '../islands/universal-css-collector.ts';
30
- import { getUniversalHeadForInjection } from '../islands/universal-head-collector.ts';
31
-
32
- /**
33
- * Resolves the absolute path to a file inside @useavalon/avalon's source tree.
34
- */
35
- function resolveAvalonPackagePath(relativePath: string): string {
36
- const require = createRequire(import.meta.url);
37
- const modEntry = require.resolve('@useavalon/avalon');
38
- const pkgRoot = dirname(modEntry);
39
- return join(pkgRoot, relativePath);
40
- }
41
-
42
- /**
43
- * Resolves the absolute path to a file inside an @useavalon/<name> integration package.
44
- */
45
- function resolveIntegrationPackagePath(name: string, relativePath: string): string {
46
- const require = createRequire(join(process.cwd(), 'package.json'));
47
- const modEntry = require.resolve(`@useavalon/${name}`);
48
- const pkgRoot = dirname(modEntry);
49
- return join(pkgRoot, relativePath);
50
- }
51
-
52
- export const VIRTUAL_MODULE_IDS = {
53
- PAGE_ROUTES: 'virtual:avalon/page-routes',
54
- ISLAND_MANIFEST: 'virtual:avalon/island-manifest',
55
- RUNTIME_CONFIG: 'virtual:avalon/runtime-config',
56
- CONFIG: 'virtual:avalon/config',
57
- } as const;
58
-
59
- export const RESOLVED_VIRTUAL_IDS = {
60
- PAGE_ROUTES: '\0' + VIRTUAL_MODULE_IDS.PAGE_ROUTES,
61
- ISLAND_MANIFEST: '\0' + VIRTUAL_MODULE_IDS.ISLAND_MANIFEST,
62
- RUNTIME_CONFIG: '\0' + VIRTUAL_MODULE_IDS.RUNTIME_CONFIG,
63
- CONFIG: '\0' + VIRTUAL_MODULE_IDS.CONFIG,
64
- } as const;
65
-
66
- export interface NitroIntegrationResult {
67
- nitroOptions: NitroConfigOutput;
68
- plugins: Plugin[];
69
- }
70
-
71
- export interface NitroCoordinationPluginOptions {
72
- avalonConfig: ResolvedAvalonConfig;
73
- nitroConfig: AvalonNitroConfig;
74
- verbose?: boolean;
75
- }
76
-
77
- /**
78
- * Creates the Nitro integration for Avalon — configuration, virtual modules,
79
- * build plugins, and SSR coordination.
80
- *
81
- * Uses the Nitro v3 Vite plugin from `nitro/vite` for server route discovery,
82
- * SSR rendering pipeline, and Rolldown-optimized bundling.
83
- */
84
- export function createNitroIntegration(
85
- avalonConfig: ResolvedAvalonConfig,
86
- nitroConfig: AvalonNitroConfig = {},
87
- ): NitroIntegrationResult {
88
- const nitroOptions = createNitroConfig(nitroConfig, avalonConfig);
89
-
90
- // Nitro v3 Vite plugin — only pass keys that Nitro actually accepts.
91
- // Spreading the full nitroOptions leaks Avalon-specific keys (staticAssets,
92
- // publicAssets, etc.) which Nitro forwards to Rolldown, causing
93
- // "Invalid input options" warnings (e.g. "jsx" key errors).
94
- const nitroVitePluginOptions: Record<string, unknown> = {
95
- preset: nitroOptions.preset,
96
- serverDir: nitroConfig.serverDir ?? nitroOptions.serverDir ?? './server',
97
- routeRules: nitroOptions.routeRules,
98
- runtimeConfig: nitroOptions.runtimeConfig,
99
- renderer: nitroConfig.renderer === false ? false : nitroOptions.renderer,
100
- compatibilityDate: nitroOptions.compatibilityDate,
101
- // Tell Nitro to scan the project root so it discovers routes/ and middleware/
102
- // alongside the serverDir (./server) which contains the catch-all renderer.
103
- scanDirs: ['.'],
104
- };
105
-
106
- // Only include optional keys if they're defined
107
- if (nitroOptions.publicRuntimeConfig) {
108
- nitroVitePluginOptions.publicRuntimeConfig = nitroOptions.publicRuntimeConfig;
109
- }
110
- if (nitroOptions.publicAssets) {
111
- nitroVitePluginOptions.publicAssets = nitroOptions.publicAssets;
112
- }
113
- if (nitroOptions.compressPublicAssets) {
114
- nitroVitePluginOptions.compressPublicAssets = nitroOptions.compressPublicAssets;
115
- }
116
- if (nitroOptions.serverEntry) {
117
- nitroVitePluginOptions.serverEntry = nitroOptions.serverEntry;
118
- }
119
-
120
- const nitroPlugin = nitroVitePlugin(nitroVitePluginOptions);
121
-
122
- const coordinationPlugin = createNitroCoordinationPlugin({
123
- avalonConfig,
124
- nitroConfig,
125
- verbose: avalonConfig.verbose,
126
- });
127
-
128
- const virtualModulesPlugin = createVirtualModulesPlugin({
129
- avalonConfig,
130
- nitroConfig,
131
- verbose: avalonConfig.verbose,
132
- });
133
-
134
- const buildPlugin = createNitroBuildPlugin(avalonConfig, nitroConfig);
135
-
136
- const manifestPlugin = createIslandManifestPlugin(avalonConfig, {
137
- verbose: avalonConfig.verbose,
138
- generatePreloadHints: true,
139
- });
140
-
141
- const sourceMapConfig = createSourceMapConfig(nitroConfig.preset ?? 'node_server', avalonConfig.isDev);
142
- const sourceMapPlugin = createSourceMapPlugin(sourceMapConfig);
143
-
144
- return {
145
- nitroOptions,
146
- plugins: [
147
- ...(Array.isArray(nitroPlugin) ? nitroPlugin : [nitroPlugin]),
148
- coordinationPlugin,
149
- virtualModulesPlugin,
150
- buildPlugin,
151
- manifestPlugin,
152
- sourceMapPlugin,
153
- ],
154
- };
155
- }
156
-
157
- /**
158
- * Coordination plugin: stores config/server refs, sets up SSR middleware and HMR,
159
- * and prewarms core infrastructure modules (fire-and-forget).
160
- */
161
- export function createNitroCoordinationPlugin(options: NitroCoordinationPluginOptions): Plugin {
162
- const { avalonConfig, verbose } = options;
163
-
164
- return {
165
- name: 'avalon:nitro-coordination',
166
- enforce: 'pre',
167
-
168
- configResolved(_config) {
169
- globalThis.__avalonConfig = avalonConfig;
170
- },
171
-
172
- configureServer(server: ViteDevServer) {
173
- globalThis.__viteDevServer = server;
174
-
175
- // Scoped middleware — discovered once, cached until invalidated by HMR
176
- let scopedMiddlewareRoutes: MiddlewareRoute[] | null = null;
177
-
178
- async function getScopedMiddleware(): Promise<MiddlewareRoute[]> {
179
- if (!scopedMiddlewareRoutes) {
180
- const viteRoot = server.config.root || process.cwd();
181
- scopedMiddlewareRoutes = await discoverScopedMiddleware({
182
- baseDir: `${viteRoot}/src`,
183
- devMode: false,
184
- });
185
- }
186
- return scopedMiddlewareRoutes;
187
- }
188
-
189
- function clearScopedMiddlewareRoutes(): void {
190
- scopedMiddlewareRoutes = null;
191
- }
192
-
193
- setupHMRCoordination(server, avalonConfig, verbose, clearScopedMiddlewareRoutes);
194
-
195
- // Pre-discover middleware (non-blocking)
196
- getScopedMiddleware().catch(err => {
197
- console.warn('[middleware] Failed to discover middleware:', err);
198
- });
199
-
200
- // Fire-and-forget: prewarm only core infrastructure modules.
201
- // Pages, islands, and per-route middleware are loaded on-demand.
202
- prewarmCoreModules(server, verbose).catch(err => {
203
- console.error('[prewarm] Core modules pre-warm failed:', err);
204
- });
205
-
206
- // SSR middleware — runs before Vite's SPA fallback
207
- server.middlewares.use(async (req, res, next) => {
208
- const originalUrl = req.url || '/';
209
- let url = originalUrl;
210
-
211
- if (url.endsWith('.html')) url = url.slice(0, -5) || '/';
212
- if (url === '/index') url = '/';
213
-
214
- // Skip static files, HMR, and Vite internals
215
- if (
216
- url.startsWith('/@') ||
217
- url.startsWith('/__') ||
218
- url.startsWith('/node_modules/') ||
219
- url.startsWith('/src/client/') ||
220
- url.startsWith('/packages/') ||
221
- (url.includes('.') && !url.endsWith('/'))
222
- ) {
223
- return next();
224
- }
225
-
226
- if (url.startsWith('/api/')) {
227
- return next();
228
- }
229
-
230
- try {
231
- const middlewareHandled = await handleScopedMiddleware(server, url, req, res, getScopedMiddleware, verbose);
232
- if (middlewareHandled) return;
233
-
234
- // Try streaming SSR first (streams shell before page data resolves)
235
- const streamed = await handleStreamingSSRRequest(server, url, avalonConfig, res);
236
- if (streamed) return;
237
-
238
- // Fallback to buffered SSR for non-modular pages
239
- const html = await handleSSRRequest(server, url, avalonConfig);
240
- if (html) {
241
- res.statusCode = 200;
242
- res.setHeader('Content-Type', 'text/html');
243
- res.end(html);
244
- return;
245
- }
246
-
247
- await handle404(server, url, res, avalonConfig);
248
- } catch (error) {
249
- console.error('[SSR Error]', error);
250
- res.statusCode = 500;
251
- res.setHeader('Content-Type', 'text/html');
252
- res.end(generateErrorPage(error as Error));
253
- }
254
- });
255
- },
256
-
257
- buildStart() {
258
- // no-op in production — coordination happens via other plugins
259
- },
260
- };
261
- }
262
-
263
- // ─── Dev Server Middleware Helpers ───────────────────────────────────────────
264
-
265
- import type { ServerResponse, IncomingMessage } from 'node:http';
266
-
267
- async function handleScopedMiddleware(
268
- server: ViteDevServer,
269
- url: string,
270
- req: IncomingMessage,
271
- res: ServerResponse,
272
- getScopedMiddleware: () => Promise<MiddlewareRoute[]>,
273
- verbose?: boolean,
274
- ): Promise<boolean> {
275
- const middlewareStart = performance.now();
276
- const middlewareRoutes = await getScopedMiddleware();
277
- if (middlewareRoutes.length === 0) return false;
278
-
279
- const headers: Record<string, string> = {};
280
- for (const [key, value] of Object.entries(req.headers)) {
281
- if (typeof value === 'string') headers[key] = value;
282
- else if (Array.isArray(value)) headers[key] = value.join(', ');
283
- }
284
-
285
- const fullUrl = `http://${req.headers.host || 'localhost'}${url}`;
286
- const h3Event = {
287
- url: fullUrl,
288
- method: req.method || 'GET',
289
- path: url,
290
- node: { req, res },
291
- req: new Request(fullUrl, {
292
- method: req.method || 'GET',
293
- headers,
294
- }),
295
- context: {} as Record<string, unknown>,
296
- } as unknown as H3Event;
297
-
298
- const middlewareResponse = await executeScopedMiddleware(h3Event, middlewareRoutes, { devMode: false });
299
-
300
- const middlewareTime = performance.now() - middlewareStart;
301
- if (middlewareTime > 100) {
302
- console.warn(`⚠️ Slow middleware: ${middlewareTime.toFixed(0)}ms for ${url}`);
303
- }
304
-
305
- if (middlewareResponse) {
306
- res.statusCode = middlewareResponse.status;
307
- middlewareResponse.headers.forEach((value, key) => {
308
- res.setHeader(key, value);
309
- });
310
- res.end(await middlewareResponse.text());
311
- return true;
312
- }
313
- return false;
314
- }
315
-
316
- async function handle404(
317
- server: ViteDevServer,
318
- url: string,
319
- res: ServerResponse,
320
- config: ResolvedAvalonConfig,
321
- ): Promise<void> {
322
- try {
323
- const { discoverErrorPages, getErrorPageModule, generateDefaultErrorPage } =
324
- await import('../nitro/error-handler.ts');
325
- const errorPages = await discoverErrorPages({
326
- isDev: config.isDev,
327
- pagesDir: config.pagesDir,
328
- loadPageModule: async (filePath: string): Promise<PageModule> => {
329
- return (await server.ssrLoadModule(filePath)) as PageModule;
330
- },
331
- });
332
- const errorPageModule = getErrorPageModule(404, errorPages);
333
-
334
- if (errorPageModule?.default && typeof errorPageModule.default === 'function') {
335
- const { renderToHtml } = await import('../render/ssr.ts');
336
- const ErrorPageComponent = errorPageModule.default;
337
- const errorHtml = await renderToHtml(
338
- { component: () => ErrorPageComponent({ statusCode: 404, message: `Page not found: ${url}`, url }) },
339
- {},
340
- );
341
- res.statusCode = 404;
342
- res.setHeader('Content-Type', 'text/html');
343
- res.end(errorHtml);
344
- return;
345
- }
346
-
347
- const fallbackHtml = generateDefaultErrorPage(404, `Page not found: ${url}`, config.isDev);
348
- res.statusCode = 404;
349
- res.setHeader('Content-Type', 'text/html');
350
- res.end(fallbackHtml);
351
- } catch {
352
- res.statusCode = 404;
353
- res.setHeader('Content-Type', 'text/html');
354
- res.end(generateFallback404(url));
355
- }
356
- }
357
-
358
- /**
359
- * Prewarms core infrastructure modules (fire-and-forget).
360
- * Loads SSR infrastructure and framework renderers so the first page render
361
- * doesn't pay the full module-load cost. Island components are loaded on-demand
362
- * to avoid penalizing startup with unused modules.
363
- */
364
- async function prewarmCoreModules(server: ViteDevServer, verbose?: boolean): Promise<void> {
365
- const prewarmStart = performance.now();
366
-
367
- const frameworkNames = ['react', 'vue', 'solid', 'svelte', 'lit', 'preact'] as const;
368
-
369
- const coreModules = [
370
- { path: resolveAvalonPackagePath('src/render/ssr.ts'), assignTo: 'ssr' as string | null },
371
- { path: resolveAvalonPackagePath('src/core/layout/enhanced-layout-resolver.ts'), assignTo: 'layout' as string | null },
372
- { path: resolveAvalonPackagePath('src/middleware/index.ts'), assignTo: null as string | null },
373
- ...frameworkNames.map(name => ({
374
- path: resolveIntegrationPackagePath(name, 'server/renderer.ts'),
375
- assignTo: null as string | null,
376
- })),
377
- ];
378
-
379
- const results = await Promise.allSettled(
380
- coreModules.map(async ({ path, assignTo }) => {
381
- const mod = await server.ssrLoadModule(path);
382
- if (assignTo === 'ssr') cachedSSRModule = mod;
383
- if (assignTo === 'layout') cachedLayoutModule = mod;
384
- }),
385
- );
386
-
387
- const succeeded = results.filter(r => r.status === 'fulfilled').length;
388
- const totalTime = performance.now() - prewarmStart;
389
-
390
- if (verbose && succeeded > 0) {
391
- console.log(`🔥 SSR ready in ${totalTime.toFixed(0)}ms (${succeeded}/${coreModules.length} core modules)`);
392
- }
393
- }
394
-
395
- /**
396
- * Virtual modules plugin — page routes, island manifest, runtime config.
397
- */
398
- export function createVirtualModulesPlugin(options: NitroCoordinationPluginOptions): Plugin {
399
- const { avalonConfig, nitroConfig, verbose } = options;
400
-
401
- return {
402
- name: 'avalon:nitro-virtual-modules',
403
- enforce: 'pre',
404
-
405
- resolveId(id: string) {
406
- if (id === VIRTUAL_MODULE_IDS.PAGE_ROUTES) return RESOLVED_VIRTUAL_IDS.PAGE_ROUTES;
407
- if (id === VIRTUAL_MODULE_IDS.ISLAND_MANIFEST) return RESOLVED_VIRTUAL_IDS.ISLAND_MANIFEST;
408
- if (id === VIRTUAL_MODULE_IDS.RUNTIME_CONFIG) return RESOLVED_VIRTUAL_IDS.RUNTIME_CONFIG;
409
- if (id === VIRTUAL_MODULE_IDS.CONFIG) return RESOLVED_VIRTUAL_IDS.CONFIG;
410
- return null;
411
- },
412
-
413
- async load(id: string) {
414
- if (id === RESOLVED_VIRTUAL_IDS.PAGE_ROUTES) return await generatePageRoutesModule(avalonConfig, verbose);
415
- if (id === RESOLVED_VIRTUAL_IDS.ISLAND_MANIFEST) return generateIslandManifestModule();
416
- if (id === RESOLVED_VIRTUAL_IDS.RUNTIME_CONFIG) return generateRuntimeConfigModule(avalonConfig, nitroConfig);
417
- if (id === RESOLVED_VIRTUAL_IDS.CONFIG) return generateConfigModule(avalonConfig, nitroConfig);
418
- return null;
419
- },
420
-
421
- handleHotUpdate({ file, server }) {
422
- if (file.includes(avalonConfig.pagesDir)) {
423
- const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_IDS.PAGE_ROUTES);
424
- if (mod) server.moduleGraph.invalidateModule(mod);
425
- }
426
- // Invalidate virtual:avalon/config when config-related files change
427
- if (file.includes('vite.config') || file.includes('avalon.config') || file.includes('nitro.config')) {
428
- const configMod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_IDS.CONFIG);
429
- if (configMod) server.moduleGraph.invalidateModule(configMod);
430
- }
431
- return undefined;
432
- },
433
- };
434
- }
435
-
436
- // ─── HMR Coordination ───────────────────────────────────────────────────────
437
-
438
- function setupHMRCoordination(
439
- server: ViteDevServer,
440
- _config: ResolvedAvalonConfig,
441
- _verbose?: boolean,
442
- clearScopedMiddlewareRoutes?: () => void,
443
- ): void {
444
- server.watcher.on('change', file => {
445
- if (file.includes('_middleware')) {
446
- clearMiddlewareCache();
447
- clearScopedMiddlewareRoutes?.();
448
- }
449
- if (file.includes('/render/') || file.includes('/layout/') || file.includes('/islands/')) {
450
- cachedSSRModule = null;
451
- cachedLayoutModule = null;
452
- }
453
-
454
- if (file.includes('/layouts/') || file.includes('_layout')) {
455
- const resolver = globalThis.__avalonLayoutResolver as { clearCache?: () => void } | undefined;
456
- resolver?.clearCache?.();
457
- }
458
- });
459
-
460
- server.watcher.on('add', file => {
461
- if (file.includes('_middleware')) {
462
- clearMiddlewareCache();
463
- clearScopedMiddlewareRoutes?.();
464
- }
465
- });
466
-
467
- server.watcher.on('unlink', file => {
468
- if (file.includes('_middleware')) {
469
- clearMiddlewareCache();
470
- clearScopedMiddlewareRoutes?.();
471
- }
472
- });
473
- }
474
-
475
- // ─── Virtual Module Generators ───────────────────────────────────────────────
476
-
477
- async function generatePageRoutesModule(config: ResolvedAvalonConfig, _verbose?: boolean): Promise<string> {
478
- try {
479
- const { getAllPageDirs } = await import('./module-discovery.ts');
480
- const { discoverPageRoutesFromMultipleDirs } = await import('../nitro/route-discovery.ts');
481
-
482
- // Get all page directories (traditional + modular)
483
- const pageDirs = await getAllPageDirs(
484
- config.pagesDir,
485
- config.modules,
486
- process.cwd()
487
- );
488
-
489
- const routes = await discoverPageRoutesFromMultipleDirs(pageDirs, {
490
- developmentMode: config.isDev,
491
- });
492
-
493
- const routesJson = JSON.stringify(routes, null, 2);
494
- return `export const pageRoutes = ${routesJson};\nexport default pageRoutes;\n`;
495
- } catch {
496
- return `export const pageRoutes = [];\nexport default pageRoutes;\n`;
497
- }
498
- }
499
-
500
- function generateIslandManifestModule(): string {
501
- return `export const islandManifest = { islands: {}, clientEntry: "", css: [] };\nexport default islandManifest;\n`;
502
- }
503
-
504
- function generateRuntimeConfigModule(avalonConfig: ResolvedAvalonConfig, nitroConfig: AvalonNitroConfig): string {
505
- const runtimeConfig = {
506
- avalon: {
507
- streaming: nitroConfig.streaming ?? true,
508
- pagesDir: avalonConfig.pagesDir,
509
- layoutsDir: avalonConfig.layoutsDir,
510
- isDev: avalonConfig.isDev,
511
- },
512
- ...nitroConfig.runtimeConfig,
513
- };
514
- return `export const runtimeConfig = ${JSON.stringify(runtimeConfig, null, 2)};\nexport function useRuntimeConfig() { return runtimeConfig; }\nexport default runtimeConfig;\n`;
515
- }
516
-
517
- export function generateConfigModule(avalonConfig: ResolvedAvalonConfig, nitroConfig: AvalonNitroConfig): string {
518
- const config = {
519
- streaming: nitroConfig.streaming ?? true,
520
- pagesDir: avalonConfig.pagesDir,
521
- layoutsDir: avalonConfig.layoutsDir,
522
- isDev: avalonConfig.isDev,
523
- ...nitroConfig.runtimeConfig,
524
- };
525
- return `const config = ${JSON.stringify(config, null, 2)};\nexport function useAvalonConfig() { return config; }\nexport default config;\n`;
526
- }
527
-
528
- // ─── Public Accessors ────────────────────────────────────────────────────────
529
-
530
- export function getViteDevServer(): ViteDevServer | undefined {
531
- return globalThis.__viteDevServer;
532
- }
533
-
534
- export function getAvalonConfig(): ResolvedAvalonConfig | undefined {
535
- return globalThis.__avalonConfig;
536
- }
537
-
538
- export function isDevelopmentMode(): boolean {
539
- return globalThis.__avalonConfig?.isDev ?? true;
540
- }
541
-
542
- // ─── SSR Request Handling ────────────────────────────────────────────────────
543
-
544
- const STREAM_MARKER = '<!--AVALON_STREAM_BOUNDARY-->';
545
-
546
- let cachedSSRModule: unknown = null;
547
-
548
- let cachedLayoutModule: unknown = null;
549
-
550
- /**
551
- * Streaming SSR handler — flushes the layout shell to the browser before
552
- * the page component's async data fetching resolves.
553
- *
554
- * Flow:
555
- * 1. Load page module + layout modules, collect CSS
556
- * 2. Render shell layout with a marker placeholder as children
557
- * 3. Split HTML on the marker → shellBefore / shellAfter
558
- * 4. res.write(shellBefore) — browser starts parsing <html><head>... immediately
559
- * 5. Render page content (awaits data fetches)
560
- * 6. Render wrapper layouts around page content
561
- * 7. res.write(wrappedContent + shellAfter)
562
- * 8. res.end()
563
- *
564
- * Falls back to null (caller uses buffered path) when:
565
- * - No modular layouts configured
566
- * - Page not found
567
- * - No shell layout detected
568
- * - Page provides its own complete HTML document
569
- */
570
- async function handleStreamingSSRRequest(
571
- server: ViteDevServer,
572
- url: string,
573
- config: ResolvedAvalonConfig,
574
- res: ServerResponse,
575
- ): Promise<boolean> {
576
- // Streaming only works with modular layouts (need shell + wrapper separation)
577
- if (!config.modules) return false;
578
-
579
- const pathname = url.split('?')[0];
580
- const pageFile = await findPageFile(pathname, config, server);
581
- if (!pageFile) return false;
582
-
583
- try {
584
- const pageModule = await server.ssrLoadModule(pageFile);
585
- const PageComponent = pageModule.default;
586
- if (!PageComponent) return false;
587
-
588
- // Check if page wants to skip layouts entirely (provides own HTML)
589
- const layoutConfig = pageModule.layoutConfig as { skipLayouts?: string[] } | undefined;
590
-
591
- // Collect CSS
592
- const cssContents = await collectCssFromModuleGraph(server, pageFile);
593
- const layoutFiles = await discoverLayoutFiles(pathname, server);
594
-
595
- const layoutModules: Array<{ file: string; module: Record<string, unknown> }> = [];
596
- for (const layoutFile of layoutFiles) {
597
- const layoutModule = await server.ssrLoadModule(layoutFile);
598
- layoutModules.push({ file: layoutFile, module: layoutModule });
599
- }
600
- for (const layoutFile of layoutFiles) {
601
- const layoutCss = await collectCssFromModuleGraph(server, layoutFile);
602
- cssContents.push(...layoutCss);
603
- }
604
-
605
- if (layoutModules.length === 0) return false;
606
-
607
- const { render: preactRender } = await server.ssrLoadModule('preact-render-to-string');
608
- const { h } = await server.ssrLoadModule('preact');
609
-
610
- const skipLayouts = layoutConfig?.skipLayouts || [];
611
- const activeLayouts = layoutModules.filter(({ file }) => {
612
- const layoutName = file.split('/').pop()?.replace(/\.[^.]+$/, '') || '';
613
- return !skipLayouts.includes(layoutName);
614
- });
615
-
616
- const frontmatter = pageModule.frontmatter as Record<string, unknown> | undefined;
617
- const metadata = pageModule.metadata as Record<string, unknown> | undefined;
618
- const mergedFrontmatter = { ...frontmatter, ...metadata, currentPath: pathname };
619
- const layoutProps = {
620
- children: null as unknown,
621
- frontmatter: mergedFrontmatter,
622
- params: {},
623
- url: pathname,
624
- };
625
-
626
- // Categorize layouts into shell vs wrapper
627
- const shellLayouts: Array<{ module: Record<string, unknown> }> = [];
628
- const wrapperLayouts: Array<{ module: Record<string, unknown> }> = [];
629
-
630
- for (const layout of activeLayouts) {
631
- const LayoutComponent = layout.module.default;
632
- if (!LayoutComponent || typeof LayoutComponent !== 'function') continue;
633
- try {
634
- const testProps = { ...layoutProps, children: h('div', null, 'test') };
635
- const testResult = (LayoutComponent as (props: unknown) => unknown)(testProps);
636
- const resolvedTest = testResult instanceof Promise ? await testResult : testResult;
637
- const testHtml = preactRender(resolvedTest);
638
- if (testHtml.trim().startsWith('<html') || testHtml.includes('<!DOCTYPE')) {
639
- shellLayouts.push(layout);
640
- } else {
641
- wrapperLayouts.push(layout);
642
- }
643
- } catch {
644
- wrapperLayouts.push(layout);
645
- }
646
- }
647
-
648
- // Need a shell layout to stream
649
- if (shellLayouts.length === 0) return false;
650
-
651
- // Render shell layout with stream marker as children
652
- const { module: shellModule } = shellLayouts[shellLayouts.length - 1];
653
- const ShellComponent = shellModule.default;
654
- if (!ShellComponent || typeof ShellComponent !== 'function') return false;
655
-
656
- let shellHtml: string;
657
- try {
658
- const shellProps = {
659
- ...layoutProps,
660
- children: h('div', { dangerouslySetInnerHTML: { __html: STREAM_MARKER } }),
661
- };
662
- const shellResult = (ShellComponent as (props: unknown) => unknown)(shellProps);
663
- const resolvedShell = shellResult instanceof Promise ? await shellResult : shellResult;
664
- shellHtml = preactRender(resolvedShell);
665
- } catch {
666
- return false;
667
- }
668
-
669
- // Split on marker
670
- const markerIndex = shellHtml.indexOf(STREAM_MARKER);
671
- if (markerIndex === -1) return false;
672
-
673
- let shellBefore = shellHtml.slice(0, markerIndex);
674
- const shellAfter = shellHtml.slice(markerIndex + STREAM_MARKER.length);
675
-
676
- // Inject CSS into the shell's <head>
677
- let shellBeforeWithCss = shellBefore;
678
- if (cssContents.length > 0) {
679
- const cssTag = `<style data-avalon-ssr-css>${cssContents.join('\n')}</style>`;
680
- if (shellBefore.includes('</head>')) {
681
- shellBeforeWithCss = shellBefore.replace('</head>', `${cssTag}\n</head>`);
682
- } else {
683
- shellBeforeWithCss = shellBefore + cssTag;
684
- }
685
- }
686
-
687
- // Ensure DOCTYPE
688
- if (!shellBeforeWithCss.trim().toLowerCase().startsWith('<!doctype')) {
689
- shellBeforeWithCss = '<!DOCTYPE html>\n' + shellBeforeWithCss;
690
- }
691
-
692
- // Inject universal CSS and head content
693
- const universalCSS = getUniversalCSSForHead(true);
694
- if (universalCSS && shellBeforeWithCss.includes('</head>')) {
695
- shellBeforeWithCss = shellBeforeWithCss.replace('</head>', `${universalCSS}\n</head>`);
696
- }
697
- const universalHead = getUniversalHeadForInjection(true);
698
- if (universalHead && shellBeforeWithCss.includes('</head>')) {
699
- shellBeforeWithCss = shellBeforeWithCss.replace('</head>', `${universalHead}\n</head>`);
700
- }
701
-
702
- // ── FLUSH SHELL ──
703
- res.statusCode = 200;
704
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
705
- res.setHeader('Transfer-Encoding', 'chunked');
706
- res.setHeader('X-Avalon-Streaming', '1');
707
- res.flushHeaders();
708
- res.write(shellBeforeWithCss);
709
-
710
- // ── RENDER PAGE CONTENT (this is where data fetching happens) ──
711
- let pageContent: string;
712
- try {
713
- const pageResult = typeof PageComponent === 'function'
714
- ? (PageComponent as () => unknown)()
715
- : PageComponent;
716
- const resolvedPage = pageResult instanceof Promise ? await pageResult : pageResult;
717
- pageContent = preactRender(resolvedPage);
718
- } catch (error) {
719
- console.error('[SSR Streaming] Error rendering page component:', error);
720
- pageContent = `<div>Error rendering page</div>`;
721
- }
722
-
723
- // Check if page returned a complete HTML doc (shouldn't happen with layouts, but safety check)
724
- const isCompleteDoc = pageContent.trim().startsWith('<!DOCTYPE html>') ||
725
- pageContent.trim().startsWith('<html');
726
- if (isCompleteDoc) {
727
- // Can't stream this — just send it and close
728
- res.end(pageContent);
729
- return true;
730
- }
731
-
732
- // Apply wrapper layouts around page content
733
- let content = pageContent;
734
- for (const { module: layoutModule } of wrapperLayouts) {
735
- const LayoutComponent = layoutModule.default;
736
- if (!LayoutComponent || typeof LayoutComponent !== 'function') continue;
737
- try {
738
- const props = {
739
- ...layoutProps,
740
- children: h('div', { dangerouslySetInnerHTML: { __html: content } }),
741
- };
742
- const layoutResult = (LayoutComponent as (props: unknown) => unknown)(props);
743
- const resolvedLayout = layoutResult instanceof Promise ? await layoutResult : layoutResult;
744
- content = preactRender(resolvedLayout);
745
- } catch (error) {
746
- console.error('[SSR Streaming] Error rendering wrapper layout:', error);
747
- }
748
- }
749
-
750
- // ── FLUSH PAGE CONTENT + SHELL TAIL ──
751
- // Inject client scripts before closing </body>
752
- let tail = content + shellAfter;
753
- if (!tail.includes('/src/client/main.js') && !tail.includes('/@vite/client')) {
754
- const bodyCloseIndex = tail.lastIndexOf('</body>');
755
- if (bodyCloseIndex !== -1) {
756
- tail = tail.slice(0, bodyCloseIndex) +
757
- '\n<script type="module" src="/@vite/client"></script>\n' +
758
- '<script type="module" src="/src/client/main.js"></script>\n' +
759
- tail.slice(bodyCloseIndex);
760
- }
761
- }
762
-
763
- res.end(tail);
764
- return true;
765
- } catch (error) {
766
- // If we already started writing, we can't change status code
767
- if (res.headersSent) {
768
- res.end(`<div>Streaming SSR error: ${(error as Error).message}</div></body></html>`);
769
- return true;
770
- }
771
- return false;
772
- }
773
- }
774
-
775
-
776
- async function handleSSRRequest(
777
- server: ViteDevServer,
778
- url: string,
779
- config: ResolvedAvalonConfig,
780
- ): Promise<string | null> {
781
- const pathname = url.split('?')[0];
782
- const pageFile = await findPageFile(pathname, config, server);
783
-
784
- if (!pageFile) return null;
785
-
786
- try {
787
- const pageModule = await server.ssrLoadModule(pageFile);
788
- const PageComponent = pageModule.default;
789
-
790
- if (!PageComponent) {
791
- console.warn(`[SSR] Page ${pageFile} has no default export`);
792
- return null;
793
- }
794
-
795
- // Collect CSS from the module graph after loading the page module.
796
- // This captures CSS modules, plain CSS imports, and any transitive CSS deps.
797
- const cssContents = await collectCssFromModuleGraph(server, pageFile);
798
-
799
- // Pre-load layout files via ssrLoadModule so their CSS modules enter
800
- // Vite's module graph *before* we collect CSS from them.
801
- const layoutFiles = await discoverLayoutFiles(pathname, server);
802
-
803
- // Load all layout modules
804
- const layoutModules: Array<{ file: string; module: Record<string, unknown> }> = [];
805
- for (const layoutFile of layoutFiles) {
806
- const layoutModule = await server.ssrLoadModule(layoutFile);
807
- layoutModules.push({ file: layoutFile, module: layoutModule });
808
- }
809
-
810
- // Collect CSS from layout files and merge with page CSS
811
- for (const layoutFile of layoutFiles) {
812
- const layoutCss = await collectCssFromModuleGraph(server, layoutFile);
813
- cssContents.push(...layoutCss);
814
- }
815
-
816
- let html: string;
817
-
818
- // If we have modular layouts, use manual layout composition
819
- if (config.modules && layoutModules.length > 0) {
820
- html = await renderPageWithManualLayouts(
821
- PageComponent,
822
- pageModule,
823
- layoutModules,
824
- pathname,
825
- config,
826
- server
827
- );
828
- } else {
829
- html = await renderPageToHtml(PageComponent, pageModule, pathname, config, server);
830
- }
831
-
832
- // Inject collected CSS into the HTML so styles are present on first paint
833
- if (cssContents.length > 0) {
834
- html = injectSsrCss(html, cssContents);
835
- }
836
-
837
- return html;
838
- } catch (error) {
839
- console.error(`[SSR] Error rendering ${pageFile}:`, error);
840
- throw error;
841
- }
842
- }
843
-
844
- /**
845
- * Render a page with manually composed layouts (for modular architecture)
846
- *
847
- * Layout composition order:
848
- * 1. Page content is rendered first
849
- * 2. Module-specific layouts (e.g., docs/_layout.tsx) wrap the page content
850
- * 3. Root/shell layout (shared/_layout.tsx) wraps everything last
851
- *
852
- * This ensures that layouts returning `<div>` wrappers are applied before
853
- * layouts returning complete `<html>` documents.
854
- */
855
- async function renderPageWithManualLayouts(
856
- PageComponent: unknown,
857
- pageModule: Record<string, unknown>,
858
- layoutModules: Array<{ file: string; module: Record<string, unknown> }>,
859
- pathname: string,
860
- config: ResolvedAvalonConfig,
861
- server: ViteDevServer,
862
- ): Promise<string> {
863
- const { render: preactRender } = await server.ssrLoadModule('preact-render-to-string');
864
- const { h } = await server.ssrLoadModule('preact');
865
-
866
- // Check if page wants to skip certain layouts
867
- const layoutConfig = pageModule.layoutConfig as { skipLayouts?: string[] } | undefined;
868
- const skipLayouts = layoutConfig?.skipLayouts || [];
869
-
870
- // Filter out skipped layouts
871
- const activeLayouts = layoutModules.filter(({ file }) => {
872
- const layoutName = file.split('/').pop()?.replace(/\.[^.]+$/, '') || '';
873
- return !skipLayouts.includes(layoutName);
874
- });
875
-
876
- // Render page content first
877
- let pageContent: string;
878
- try {
879
- const pageResult = typeof PageComponent === 'function'
880
- ? (PageComponent as () => unknown)()
881
- : PageComponent;
882
- const resolvedPage = pageResult instanceof Promise ? await pageResult : pageResult;
883
- pageContent = preactRender(resolvedPage);
884
- } catch (error) {
885
- console.error('[SSR] Error rendering page component:', error);
886
- pageContent = `<div>Error rendering page</div>`;
887
- }
888
-
889
- // Check if page content is a complete HTML document
890
- const isCompleteDoc = pageContent.trim().startsWith('<!DOCTYPE html>') ||
891
- pageContent.trim().startsWith('<html');
892
-
893
- if (isCompleteDoc) {
894
- // Page provides its own HTML structure, inject client script and return
895
- return injectClientScript(pageContent);
896
- }
897
-
898
- // Separate layouts into shell (returns <html>) and wrapper (returns <div>) layouts
899
- // We need to render each layout to determine its type, then apply in correct order
900
- // Merge frontmatter and metadata - metadata takes precedence for page-specific values
901
- const frontmatter = pageModule.frontmatter as Record<string, unknown> | undefined;
902
- const metadata = pageModule.metadata as Record<string, unknown> | undefined;
903
- const mergedFrontmatter = { ...frontmatter, ...metadata, currentPath: pathname };
904
-
905
- const layoutProps = {
906
- children: null as unknown, // Will be set per-layout
907
- frontmatter: mergedFrontmatter,
908
- params: {},
909
- url: pathname,
910
- };
911
-
912
- // Categorize layouts by rendering them with placeholder content
913
- const shellLayouts: Array<{ module: Record<string, unknown> }> = [];
914
- const wrapperLayouts: Array<{ module: Record<string, unknown> }> = [];
915
-
916
- for (const layout of activeLayouts) {
917
- const LayoutComponent = layout.module.default;
918
- if (!LayoutComponent || typeof LayoutComponent !== 'function') continue;
919
-
920
- try {
921
- // Render with placeholder to detect if it returns HTML shell
922
- const testProps = {
923
- ...layoutProps,
924
- children: h('div', null, 'test'),
925
- };
926
- const testResult = (LayoutComponent as (props: unknown) => unknown)(testProps);
927
- const resolvedTest = testResult instanceof Promise ? await testResult : testResult;
928
- const testHtml = preactRender(resolvedTest);
929
-
930
- if (testHtml.trim().startsWith('<html') || testHtml.includes('<!DOCTYPE')) {
931
- shellLayouts.push(layout);
932
- } else {
933
- wrapperLayouts.push(layout);
934
- }
935
- } catch {
936
- // If we can't determine, treat as wrapper
937
- wrapperLayouts.push(layout);
938
- }
939
- }
940
-
941
- // Apply wrapper layouts first (innermost to outermost)
942
- // These are module-specific layouts that return <div> wrappers
943
- let content = pageContent;
944
-
945
- for (const { module: layoutModule } of wrapperLayouts) {
946
- const LayoutComponent = layoutModule.default;
947
- if (!LayoutComponent || typeof LayoutComponent !== 'function') continue;
948
-
949
- try {
950
- const props = {
951
- ...layoutProps,
952
- children: h('div', { dangerouslySetInnerHTML: { __html: content } }),
953
- };
954
-
955
- const layoutResult = (LayoutComponent as (props: unknown) => unknown)(props);
956
- const resolvedLayout = layoutResult instanceof Promise ? await layoutResult : layoutResult;
957
- content = preactRender(resolvedLayout);
958
- } catch (error) {
959
- console.error('[SSR] Error rendering wrapper layout:', error);
960
- }
961
- }
962
-
963
- // Apply shell layout last (the one that provides <html>)
964
- // If there are multiple shell layouts, prefer the module-specific one (last in array)
965
- // since layouts are discovered in order: shared -> module-specific
966
- if (shellLayouts.length > 0) {
967
- // Use the last shell layout (module-specific takes precedence over shared)
968
- const { module: shellModule } = shellLayouts[shellLayouts.length - 1];
969
- const ShellComponent = shellModule.default;
970
-
971
- if (ShellComponent && typeof ShellComponent === 'function') {
972
- try {
973
- const props = {
974
- ...layoutProps,
975
- children: h('div', { dangerouslySetInnerHTML: { __html: content } }),
976
- };
977
-
978
- const shellResult = (ShellComponent as (props: unknown) => unknown)(props);
979
- const resolvedShell = shellResult instanceof Promise ? await shellResult : shellResult;
980
- content = preactRender(resolvedShell);
981
- } catch (error) {
982
- console.error('[SSR] Error rendering shell layout:', error);
983
- }
984
- }
985
- }
986
-
987
- // Check if final content is a complete HTML document
988
- const isFinalCompleteDoc = content.trim().startsWith('<!DOCTYPE html>') ||
989
- content.trim().startsWith('<html');
990
-
991
- if (isFinalCompleteDoc) {
992
- return injectClientScript(content);
993
- }
994
-
995
- // Wrap in basic HTML structure (fallback if no shell layout)
996
- const fallbackMetadata = (pageModule.metadata || {}) as { title?: string; description?: string };
997
- const title = fallbackMetadata.title || 'Avalon App';
998
- const description = fallbackMetadata.description || '';
999
-
1000
- return `<!DOCTYPE html>
1001
- <html lang="en">
1002
- <head>
1003
- <meta charset="utf-8">
1004
- <meta name="viewport" content="width=device-width, initial-scale=1">
1005
- <title>${escapeHtml(title)}</title>
1006
- ${description ? `<meta name="description" content="${escapeHtml(description)}">` : ''}
1007
- <script type="module" src="/@vite/client"></script>
1008
- </head>
1009
- <body>
1010
- ${content}
1011
- <script type="module" src="/src/client/main.js"></script>
1012
- </body>
1013
- </html>`;
1014
- }
1015
-
1016
- /**
1017
- * Inject client script into HTML if not already present.
1018
- * Also ensures DOCTYPE is present for valid HTML5.
1019
- */
1020
- function injectClientScript(html: string): string {
1021
- let result = html;
1022
-
1023
- // Ensure DOCTYPE is present
1024
- if (!result.trim().toLowerCase().startsWith('<!doctype')) {
1025
- result = '<!DOCTYPE html>\n' + result;
1026
- }
1027
-
1028
- // Inject universal CSS from island framework renderers (Svelte scoped, Vue scoped, Solid CSS, etc.)
1029
- if (!result.includes('data-universal-ssr="true"')) {
1030
- const universalCSS = getUniversalCSSForHead(true);
1031
- if (universalCSS && result.includes('</head>')) {
1032
- result = result.replace('</head>', `${universalCSS}\n</head>`);
1033
- }
1034
- }
1035
-
1036
- // Inject universal head content (hydration scripts from frameworks like Solid)
1037
- const universalHead = getUniversalHeadForInjection(true);
1038
- if (universalHead && result.includes('</head>')) {
1039
- result = result.replace('</head>', `${universalHead}\n</head>`);
1040
- }
1041
-
1042
- // Skip if scripts already present
1043
- if (result.includes('/src/client/main.js') || result.includes('/@vite/client')) {
1044
- return result;
1045
- }
1046
-
1047
- // Inject before </body> or at the end
1048
- const bodyCloseIndex = result.lastIndexOf('</body>');
1049
- if (bodyCloseIndex !== -1) {
1050
- return result.slice(0, bodyCloseIndex) +
1051
- '\n<script type="module" src="/@vite/client"></script>\n' +
1052
- '<script type="module" src="/src/client/main.js"></script>\n' +
1053
- result.slice(bodyCloseIndex);
1054
- }
1055
-
1056
- return result + '\n<script type="module" src="/@vite/client"></script>\n<script type="module" src="/src/client/main.js"></script>';
1057
- }
1058
-
1059
- /**
1060
- * Discover layout files that apply to a given route path.
1061
- *
1062
- * Supports both traditional layouts (src/layouts/) and modular layouts (app/modules/[module]/layouts/).
1063
- * For route "/blog/post", checks shared layouts, module layouts, and traditional layouts.
1064
- */
1065
- async function discoverLayoutFiles(pathname: string, server: ViteDevServer): Promise<string[]> {
1066
- const viteRoot = server.config.root || process.cwd();
1067
- const config = globalThis.__avalonConfig;
1068
- const layoutFileName = '_layout.tsx';
1069
- const layoutFiles: string[] = [];
1070
-
1071
- // Build path hierarchy: "/" → [''], "/blog/post" → ['', '/blog', '/blog/post']
1072
- const segments = pathname.split('/').filter(Boolean);
1073
- const paths = [''];
1074
- for (let i = 0; i < segments.length; i++) {
1075
- paths.push('/' + segments.slice(0, i + 1).join('/'));
1076
- }
1077
-
1078
- // Helper to check and add layout file
1079
- async function tryAddLayout(fullPath: string): Promise<void> {
1080
- try {
1081
- const stat = await fsStat(fullPath);
1082
- if (stat.isFile()) {
1083
- const relativePath = fullPath.slice(viteRoot.length);
1084
- if (!layoutFiles.includes(relativePath)) {
1085
- layoutFiles.push(relativePath);
1086
- }
1087
- }
1088
- } catch {
1089
- // Layout file doesn't exist — that's fine
1090
- }
1091
- }
1092
-
1093
- // 1. Check shared layouts directory (root layout)
1094
- if (config?.layoutsDir) {
1095
- const sharedLayoutsDir = `${viteRoot}/${config.layoutsDir}`;
1096
- await tryAddLayout(`${sharedLayoutsDir}/${layoutFileName}`);
1097
- }
1098
-
1099
- // 2. Check modular layouts (app/modules/*/layouts/)
1100
- if (config?.modules) {
1101
- const modulesDir = `${viteRoot}/${config.modules.dir}`;
1102
- const layoutsDirName = config.modules.layoutsDirName;
1103
-
1104
- // Determine which module this route belongs to
1105
- const firstSegment = segments[0] || '';
1106
- const rootModules = ['home', 'root', 'main', 'index'];
1107
-
1108
- // For root routes, check the home/root/main/index module
1109
- if (!firstSegment || rootModules.includes(firstSegment.toLowerCase())) {
1110
- for (const moduleName of rootModules) {
1111
- await tryAddLayout(`${modulesDir}/${moduleName}/${layoutsDirName}/${layoutFileName}`);
1112
- }
1113
- } else {
1114
- // For other routes, check the module matching the first segment
1115
- await tryAddLayout(`${modulesDir}/${firstSegment}/${layoutsDirName}/${layoutFileName}`);
1116
- }
1117
- }
1118
-
1119
- // 3. Check traditional layouts directory (src/layouts/)
1120
- const traditionalLayoutsDir = `${viteRoot}/src/layouts`;
1121
- for (const pathSegment of paths) {
1122
- const fullPath = pathSegment === ''
1123
- ? `${traditionalLayoutsDir}/${layoutFileName}`
1124
- : `${traditionalLayoutsDir}${pathSegment}/${layoutFileName}`;
1125
- await tryAddLayout(fullPath);
1126
- }
1127
-
1128
- return layoutFiles;
1129
- }
1130
-
1131
- async function findPageFile(pathname: string, config: ResolvedAvalonConfig, server: ViteDevServer): Promise<string | null> {
1132
- let normalizedPath = pathname;
1133
- if (normalizedPath.endsWith('/') && normalizedPath !== '/') {
1134
- normalizedPath = normalizedPath.slice(0, -1);
1135
- }
1136
- if (normalizedPath === '/') {
1137
- normalizedPath = '/index';
1138
- }
1139
-
1140
- const extensions = ['.tsx', '.ts', '.jsx', '.js', '.mdx', '.md'];
1141
- const viteRoot = server.config.root || process.cwd();
1142
-
1143
- // Helper to check if a file exists
1144
- async function tryFile(relativePath: string): Promise<string | null> {
1145
- try {
1146
- const fullPath = `${viteRoot}/${relativePath}`;
1147
- const stat = await fsStat(fullPath);
1148
- if (stat.isFile()) return `/${relativePath}`;
1149
- } catch {
1150
- // File doesn't exist
1151
- }
1152
- return null;
1153
- }
1154
-
1155
- // 1. Check modular page directories first
1156
- if (config.modules) {
1157
- const modulesDir = config.modules.dir;
1158
- const pagesDirName = config.modules.pagesDirName;
1159
- const segments = pathname.split('/').filter(Boolean);
1160
- const firstSegment = segments[0] || '';
1161
- const rootModules = ['home', 'root', 'main', 'index'];
1162
-
1163
- // Determine which module and what the relative path within that module is
1164
- let moduleName: string;
1165
- let moduleRelativePath: string;
1166
-
1167
- if (!firstSegment || rootModules.includes(firstSegment.toLowerCase())) {
1168
- // Root route - check home module
1169
- moduleName = 'home';
1170
- moduleRelativePath = normalizedPath;
1171
- } else {
1172
- // Check if first segment matches a module
1173
- moduleName = firstSegment;
1174
- // Remove the module prefix from the path
1175
- const remainingSegments = segments.slice(1);
1176
- moduleRelativePath = remainingSegments.length > 0
1177
- ? '/' + remainingSegments.join('/')
1178
- : '/index';
1179
- }
1180
-
1181
- // Try to find the page in the module
1182
- for (const ext of extensions) {
1183
- const result = await tryFile(`${modulesDir}/${moduleName}/${pagesDirName}${moduleRelativePath}${ext}`);
1184
- if (result) return result;
1185
- }
1186
- if (!moduleRelativePath.endsWith('/index')) {
1187
- for (const ext of extensions) {
1188
- const result = await tryFile(`${modulesDir}/${moduleName}/${pagesDirName}${moduleRelativePath}/index${ext}`);
1189
- if (result) return result;
1190
- }
1191
- }
1192
- }
1193
-
1194
- // 2. Check traditional pages directory
1195
- const pagesDir = config.pagesDir;
1196
- for (const ext of extensions) {
1197
- const result = await tryFile(`${pagesDir}${normalizedPath}${ext}`);
1198
- if (result) return result;
1199
- }
1200
- if (!normalizedPath.endsWith('/index')) {
1201
- for (const ext of extensions) {
1202
- const result = await tryFile(`${pagesDir}${normalizedPath}/index${ext}`);
1203
- if (result) return result;
1204
- }
1205
- }
1206
-
1207
- return null;
1208
- }
1209
-
1210
- async function renderPageToHtml(
1211
- PageComponent: unknown,
1212
- pageModule: Record<string, unknown>,
1213
- pathname: string,
1214
- config: ResolvedAvalonConfig,
1215
- server: ViteDevServer,
1216
- ): Promise<string> {
1217
- const metadata = (pageModule.metadata || {}) as { title?: string; description?: string };
1218
-
1219
- try {
1220
- if (!cachedSSRModule) {
1221
- cachedSSRModule = await server.ssrLoadModule(resolveAvalonPackagePath('src/render/ssr.ts'));
1222
- }
1223
- // cachedSSRModule is the dynamically-loaded render/ssr.ts module;
1224
- // expected exports: renderToHtml, renderToHtmlWithLayouts
1225
- const ssrModule = cachedSSRModule as Record<string, unknown>;
1226
-
1227
- if (!cachedLayoutModule) {
1228
- cachedLayoutModule = await server.ssrLoadModule(resolveAvalonPackagePath('src/core/layout/enhanced-layout-resolver.ts'));
1229
- }
1230
- // cachedLayoutModule is the dynamically-loaded enhanced-layout-resolver.ts module;
1231
- // expected exports: EnhancedLayoutResolver, EnhancedLayoutResolverUtils
1232
- const layoutModule = cachedLayoutModule as Record<string, unknown>;
1233
-
1234
- const routeConfig = {
1235
- component: () => (typeof PageComponent === 'function' ? (PageComponent as () => unknown)() : PageComponent),
1236
- options: { title: metadata.title || 'Avalon App' },
1237
- frontmatter: pageModule.frontmatter as Record<string, unknown> | undefined,
1238
- };
1239
-
1240
- // Try layout-aware rendering first
1241
- if (
1242
- ssrModule.renderToHtmlWithLayouts &&
1243
- layoutModule.EnhancedLayoutResolver &&
1244
- layoutModule.EnhancedLayoutResolverUtils
1245
- ) {
1246
- try {
1247
- const viteRoot = server.config.root || process.cwd();
1248
-
1249
- if (!globalThis.__avalonLayoutResolver) {
1250
- const EnhancedLayoutResolver = layoutModule.EnhancedLayoutResolver as new (
1251
- opts: Record<string, unknown>,
1252
- ) => unknown;
1253
-
1254
- // Use the shared layouts directory as the base
1255
- // The resolver will also check modular layouts via the layout composer
1256
- const layoutsDir = config.layoutsDir || 'src/layouts';
1257
-
1258
- globalThis.__avalonLayoutResolver = new EnhancedLayoutResolver({
1259
- baseDirectory: `${viteRoot}/${layoutsDir}`,
1260
- filePattern: '_layout.tsx',
1261
- excludeDirectories: ['node_modules', '.git', 'dist', 'build'],
1262
- enableWatching: true,
1263
- developmentMode: false,
1264
- enableCaching: true,
1265
- cacheTTL: 60 * 1000,
1266
- maxCacheSize: 100,
1267
- enableStreaming: true,
1268
- enableErrorBoundaries: true,
1269
- enableMetrics: false,
1270
- enableDebugInfo: false,
1271
- // Pass modules config for modular layout discovery
1272
- modulesDir: config.modules ? `${viteRoot}/${config.modules.dir}` : undefined,
1273
- modulesLayoutsDirName: config.modules?.layoutsDirName,
1274
- });
1275
- }
1276
-
1277
- const fullUrl = `http://localhost${pathname}`;
1278
- const layoutContext = {
1279
- params: {},
1280
- query: {},
1281
- url: fullUrl,
1282
- request: { method: 'GET', url: fullUrl, headers: new Headers() },
1283
- };
1284
-
1285
- return await (ssrModule.renderToHtmlWithLayouts as Function)(
1286
- routeConfig,
1287
- globalThis.__avalonLayoutResolver,
1288
- layoutContext,
1289
- pathname,
1290
- { title: metadata.title || 'Avalon App' },
1291
- undefined,
1292
- { suppressWarnings: true },
1293
- );
1294
- } catch {
1295
- // Layout rendering failed, fall back to basic rendering
1296
- }
1297
- }
1298
-
1299
- if (ssrModule.renderToHtml) {
1300
- return await (ssrModule.renderToHtml as Function)(
1301
- routeConfig,
1302
- { title: metadata.title || 'Avalon App' },
1303
- undefined,
1304
- { suppressWarnings: true },
1305
- );
1306
- }
1307
- } catch {
1308
- // SSR module not available, fall back to basic rendering
1309
- }
1310
-
1311
- // Fallback: basic HTML template
1312
- const title = metadata.title || 'Avalon App';
1313
- const description = metadata.description || '';
1314
- let content = '';
1315
- try {
1316
- const preactRenderModule = await server.ssrLoadModule('preact-render-to-string');
1317
- if (preactRenderModule.render && typeof PageComponent === 'function') {
1318
- content = preactRenderModule.render((PageComponent as () => unknown)());
1319
- }
1320
- } catch {
1321
- content = `<p>Loading page: ${escapeHtml(pathname)}</p>`;
1322
- }
1323
-
1324
- return `<!DOCTYPE html>
1325
- <html lang="en">
1326
- <head>
1327
- <meta charset="utf-8">
1328
- <meta name="viewport" content="width=device-width, initial-scale=1">
1329
- <title>${escapeHtml(title)}</title>
1330
- ${description ? `<meta name="description" content="${escapeHtml(description)}">` : ''}
1331
- <script type="module" src="/@vite/client"></script>
1332
- </head>
1333
- <body>
1334
- <div id="app">${content}</div>
1335
- <script type="module" src="/src/client/main.js"></script>
1336
- </body>
1337
- </html>`;
1338
- }
1339
-
1340
- function escapeHtml(str: string): string {
1341
- return str
1342
- .replaceAll('&', '&amp;')
1343
- .replaceAll('<', '&lt;')
1344
- .replaceAll('>', '&gt;')
1345
- .replaceAll('"', '&quot;')
1346
- .replaceAll("'", '&#039;');
1347
- }
1348
-
1349
- // ─── Global Type Declarations ────────────────────────────────────────────────
1350
-
1351
- declare global {
1352
- // deno-lint-ignore no-var
1353
- var __avalonLayoutResolver: unknown;
1354
- }