@jasonshimmy/vite-plugin-cer-app 0.12.0 → 0.12.1

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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Changelog
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
+ ## [v0.12.1] - 2026-03-22
5
+
6
+ - fix: add meta.hydrate support for route hydration strategies and implement related tests (0513743)
7
+
4
8
  ## [v0.12.0] - 2026-03-22
5
9
 
6
10
  - feat: implement ISR support with isrHandler for SSR fallback in Cloudflare, Netlify, and Vercel adapters (ddcc4d4)
package/commits.txt CHANGED
@@ -1 +1 @@
1
- - feat: implement ISR support with isrHandler for SSR fallback in Cloudflare, Netlify, and Vercel adapters (ddcc4d4)
1
+ - fix: add meta.hydrate support for route hydration strategies and implement related tests (0513743)
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/plugin/virtual/routes.ts"],"names":[],"mappings":"AA6HA;;;;;;;;;;;;;GAaG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA+I1E"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/plugin/virtual/routes.ts"],"names":[],"mappings":"AA+IA;;;;;;;;;;;;;GAaG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAqJ1E"}
@@ -63,6 +63,25 @@ function extractTransition(source) {
63
63
  return boolMatch[1] === 'true';
64
64
  return null;
65
65
  }
66
+ /**
67
+ * Extracts the per-route `hydrate` strategy from a page file's source.
68
+ * Returns 'load', 'idle', 'visible', 'none', or null if absent.
69
+ * 'load' is the default — callers skip emitting it to keep the bundle lean.
70
+ *
71
+ * Matches patterns like:
72
+ * hydrate: 'idle'
73
+ * hydrate: 'visible'
74
+ * hydrate: 'none'
75
+ */
76
+ function extractHydrate(source) {
77
+ const match = source.match(/hydrate\s*:\s*['"]([^'"]+)['"]/);
78
+ if (!match)
79
+ return null;
80
+ const val = match[1];
81
+ if (val === 'load' || val === 'idle' || val === 'visible' || val === 'none')
82
+ return val;
83
+ return null;
84
+ }
66
85
  /**
67
86
  * Extracts the per-route `render` strategy from a page file's source.
68
87
  * Returns 'static', 'server', 'spa', or null if absent.
@@ -176,16 +195,17 @@ export async function generateRoutesCode(pagesDir) {
176
195
  revalidate: extractRevalidate(src),
177
196
  transition: extractTransition(src),
178
197
  render: extractRender(src),
198
+ hydrate: extractHydrate(src),
179
199
  };
180
200
  }
181
201
  catch {
182
- return { middleware: [], layout: null, layoutChain: null, revalidate: null, transition: null, render: null };
202
+ return { middleware: [], layout: null, layoutChain: null, revalidate: null, transition: null, render: null, hydrate: null };
183
203
  }
184
204
  }));
185
205
  const lines = ['// AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app', ''];
186
206
  // Build routes array with lazy load() functions for code splitting.
187
207
  const routeItems = sorted.map((entry, i) => {
188
- const { middleware: mw, layout, layoutChain, revalidate, transition, render } = metaPerEntry[i];
208
+ const { middleware: mw, layout, layoutChain, revalidate, transition, render, hydrate } = metaPerEntry[i];
189
209
  const filePath = JSON.stringify(entry.filePath);
190
210
  const tagName = JSON.stringify(entry.tagName);
191
211
  const routePath = JSON.stringify(entry.routePath);
@@ -211,6 +231,10 @@ export async function generateRoutesCode(pagesDir) {
211
231
  if (render !== null) {
212
232
  metaFields.push(`render: ${JSON.stringify(render)}`);
213
233
  }
234
+ // 'load' is the default — only emit non-default values to keep bundle lean.
235
+ if (hydrate !== null && hydrate !== 'load') {
236
+ metaFields.push(`hydrate: ${JSON.stringify(hydrate)}`);
237
+ }
214
238
  const metaStr = metaFields.length > 0 ? ` meta: { ${metaFields.join(', ')} },\n` : '';
215
239
  if (mw.length === 0) {
216
240
  return (` {\n` +
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../../src/plugin/virtual/routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../../src/plugin/virtual/routes.ts"],"names":[],"mappings":"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"}
@@ -5,5 +5,5 @@
5
5
  * automatically receive the latest bootstrap code on plugin update.
6
6
  * This file is gitignored and should never be edited directly.
7
7
  */
8
- export declare const APP_ENTRY_TEMPLATE = "// AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app \u2014 do not edit.\n// Regenerated automatically on every dev server start and build.\n\nimport '@jasonshimmy/custom-elements-runtime/css'\nimport 'virtual:cer-jit-css'\nimport 'virtual:cer-components'\nimport routes from 'virtual:cer-routes'\nimport layouts from 'virtual:cer-layouts'\nimport plugins from 'virtual:cer-plugins'\nimport { hasLoading, loadingTag } from 'virtual:cer-loading'\nimport { hasError, errorTag } from 'virtual:cer-error'\nimport { runtimeConfig } from 'virtual:cer-app-config'\nimport {\n component,\n ref,\n provide,\n useOnConnected,\n useOnDisconnected,\n registerBuiltinComponents,\n} from '@jasonshimmy/custom-elements-runtime'\nimport { initRouter } from '@jasonshimmy/custom-elements-runtime/router'\nimport { enableJITCSS } from '@jasonshimmy/custom-elements-runtime/jit-css'\nimport { initRuntimeConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'\n\nregisterBuiltinComponents()\nenableJITCSS()\ninitRuntimeConfig(runtimeConfig)\n\nconst router = initRouter({ routes })\n\n// \u2500\u2500\u2500 Navigation state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst isNavigating = ref(false)\nconst currentError = ref(null)\n\nconst resetError = () => {\n currentError.value = null\n void router.replace(router.getCurrent().path)\n}\n;(globalThis).resetError = resetError\n\nconst _push = router.push.bind(router)\nconst _replace = router.replace.bind(router)\n\nrouter.push = async (path) => {\n isNavigating.value = true\n currentError.value = null\n try {\n await _push(path)\n } catch (err) {\n currentError.value = err instanceof Error ? err.message : String(err)\n } finally {\n isNavigating.value = false\n }\n}\n\nrouter.replace = async (path) => {\n isNavigating.value = true\n currentError.value = null\n try {\n await _replace(path)\n } catch (err) {\n currentError.value = err instanceof Error ? err.message : String(err)\n } finally {\n isNavigating.value = false\n }\n}\n\n// \u2500\u2500\u2500 Plugins \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// Collect plugin-provided values so cer-layout-view can forward them into\n// the component context tree via the real provide() hook (which inject() walks).\n// Declared BEFORE component('cer-layout-view') to avoid a temporal dead zone\n// ReferenceError: customElements.define() upgrades existing DOM elements\n// synchronously, calling the render function immediately.\nconst _pluginProvides = new Map()\n// Expose plugin provides globally so page components can read them synchronously\n// regardless of render order.\n;(globalThis).__cerPluginProvides = _pluginProvides\n\n// \u2500\u2500\u2500 <cer-layout-view> \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ncomponent('cer-layout-view', () => {\n // Forward plugin-provided values into the component context so inject() in\n // any descendant component can resolve them by walking up the DOM tree.\n for (const [key, value] of _pluginProvides) {\n provide(key, value)\n }\n\n const current = ref(router.getCurrent())\n let unsub\n\n useOnConnected(() => {\n unsub = router.subscribe((s) => { current.value = s })\n })\n useOnDisconnected(() => { unsub?.(); unsub = undefined })\n\n if (currentError.value !== null) {\n if (hasError && errorTag) {\n return { tag: errorTag, props: { attrs: { error: String(currentError.value) } }, children: [] }\n }\n return { tag: 'div', props: { attrs: { style: 'padding:2rem;font-family:monospace' } }, children: String(currentError.value) }\n }\n\n if (isNavigating.value && hasLoading && loadingTag) {\n return { tag: loadingTag, props: {}, children: [] }\n }\n\n const matched = router.matchRoute(current.value.path)\n const routeMeta = matched?.route?.meta\n const routerView = { tag: 'router-view', props: {}, children: [] }\n\n // Support nested layout chains: meta.layoutChain = ['default', 'admin']\n // renders <layout-default><layout-admin><router-view/></layout-admin></layout-default>\n const chain = routeMeta?.layoutChain\n ? routeMeta.layoutChain\n : [routeMeta?.layout ?? 'default']\n\n // Build nested vnodes from innermost to outermost.\n let vnode = routerView\n for (let i = chain.length - 1; i >= 0; i--) {\n const tag = layouts[chain[i]]\n if (tag) vnode = { tag, props: {}, children: [vnode] }\n }\n return vnode\n})\n\nfor (const plugin of plugins) {\n if (plugin && typeof plugin.setup === 'function') {\n await plugin.setup({\n router,\n provide: (key, value) => { _pluginProvides.set(key, value) },\n config: {},\n })\n }\n}\n\n// \u2500\u2500\u2500 Pre-load initial route \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Download the current page's route chunk AFTER plugins run so that\n// cer-layout-view's first render (which calls provide()) completes before\n// page components are defined and their renders are scheduled. This ensures\n// inject() in child components can find values stored by provide().\n\nif (typeof window !== 'undefined') {\n const _initMatch = router.matchRoute(window.location.pathname)\n if (_initMatch?.route?.load) {\n try { await _initMatch.route.load() } catch { /* non-fatal */ }\n }\n}\n\n// \u2500\u2500\u2500 Initial navigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nif (typeof window !== 'undefined') {\n // Use the original (unwrapped) replace so isNavigating stays false during\n // the initial paint \u2014 the loading component must not flash over pre-rendered content.\n await _replace(window.location.pathname + window.location.search + window.location.hash)\n // Clear SSR loader data after initial navigation so subsequent client-side\n // navigations don't accidentally reuse stale server data.\n delete (globalThis).__CER_DATA__\n}\n\nexport { router }\n";
8
+ export declare const APP_ENTRY_TEMPLATE = "// AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app \u2014 do not edit.\n// Regenerated automatically on every dev server start and build.\n\nimport '@jasonshimmy/custom-elements-runtime/css'\nimport 'virtual:cer-jit-css'\nimport 'virtual:cer-components'\nimport routes from 'virtual:cer-routes'\nimport layouts from 'virtual:cer-layouts'\nimport plugins from 'virtual:cer-plugins'\nimport { hasLoading, loadingTag } from 'virtual:cer-loading'\nimport { hasError, errorTag } from 'virtual:cer-error'\nimport { runtimeConfig } from 'virtual:cer-app-config'\nimport {\n component,\n ref,\n provide,\n useOnConnected,\n useOnDisconnected,\n registerBuiltinComponents,\n} from '@jasonshimmy/custom-elements-runtime'\nimport { initRouter } from '@jasonshimmy/custom-elements-runtime/router'\nimport { enableJITCSS } from '@jasonshimmy/custom-elements-runtime/jit-css'\nimport { initRuntimeConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'\n\nregisterBuiltinComponents()\nenableJITCSS()\ninitRuntimeConfig(runtimeConfig)\n\nconst router = initRouter({ routes })\n\n// \u2500\u2500\u2500 Navigation state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst isNavigating = ref(false)\nconst currentError = ref(null)\n\nconst resetError = () => {\n currentError.value = null\n void router.replace(router.getCurrent().path)\n}\n;(globalThis).resetError = resetError\n\nconst _push = router.push.bind(router)\nconst _replace = router.replace.bind(router)\n\nrouter.push = async (path) => {\n isNavigating.value = true\n currentError.value = null\n try {\n await _push(path)\n } catch (err) {\n currentError.value = err instanceof Error ? err.message : String(err)\n } finally {\n isNavigating.value = false\n }\n}\n\nrouter.replace = async (path) => {\n isNavigating.value = true\n currentError.value = null\n try {\n await _replace(path)\n } catch (err) {\n currentError.value = err instanceof Error ? err.message : String(err)\n } finally {\n isNavigating.value = false\n }\n}\n\n// \u2500\u2500\u2500 Plugins \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// Collect plugin-provided values so cer-layout-view can forward them into\n// the component context tree via the real provide() hook (which inject() walks).\n// Declared BEFORE component('cer-layout-view') to avoid a temporal dead zone\n// ReferenceError: customElements.define() upgrades existing DOM elements\n// synchronously, calling the render function immediately.\nconst _pluginProvides = new Map()\n// Expose plugin provides globally so page components can read them synchronously\n// regardless of render order.\n;(globalThis).__cerPluginProvides = _pluginProvides\n\n// \u2500\u2500\u2500 <cer-layout-view> \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ncomponent('cer-layout-view', () => {\n // Forward plugin-provided values into the component context so inject() in\n // any descendant component can resolve them by walking up the DOM tree.\n for (const [key, value] of _pluginProvides) {\n provide(key, value)\n }\n\n const current = ref(router.getCurrent())\n let unsub\n\n useOnConnected(() => {\n unsub = router.subscribe((s) => { current.value = s })\n })\n useOnDisconnected(() => { unsub?.(); unsub = undefined })\n\n if (currentError.value !== null) {\n if (hasError && errorTag) {\n return { tag: errorTag, props: { attrs: { error: String(currentError.value) } }, children: [] }\n }\n return { tag: 'div', props: { attrs: { style: 'padding:2rem;font-family:monospace' } }, children: String(currentError.value) }\n }\n\n if (isNavigating.value && hasLoading && loadingTag) {\n return { tag: loadingTag, props: {}, children: [] }\n }\n\n const matched = router.matchRoute(current.value.path)\n const routeMeta = matched?.route?.meta\n const routerView = { tag: 'router-view', props: {}, children: [] }\n\n // Support nested layout chains: meta.layoutChain = ['default', 'admin']\n // renders <layout-default><layout-admin><router-view/></layout-admin></layout-default>\n const chain = routeMeta?.layoutChain\n ? routeMeta.layoutChain\n : [routeMeta?.layout ?? 'default']\n\n // Build nested vnodes from innermost to outermost.\n let vnode = routerView\n for (let i = chain.length - 1; i >= 0; i--) {\n const tag = layouts[chain[i]]\n if (tag) vnode = { tag, props: {}, children: [vnode] }\n }\n return vnode\n})\n\nfor (const plugin of plugins) {\n if (plugin && typeof plugin.setup === 'function') {\n await plugin.setup({\n router,\n provide: (key, value) => { _pluginProvides.set(key, value) },\n config: {},\n })\n }\n}\n\n// \u2500\u2500\u2500 Pre-load initial route + hydration strategy \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Download the current page's route chunk AFTER plugins run so that\n// cer-layout-view's first render (which calls provide()) completes before\n// page components are defined and their renders are scheduled. This ensures\n// inject() in child components can find values stored by provide().\n//\n// meta.hydrate controls WHEN the initial page activates:\n// 'load' \u2014 immediately (default)\n// 'idle' \u2014 deferred until requestIdleCallback (browser idle)\n// 'visible' \u2014 deferred until cer-layout-view enters the viewport\n// 'none' \u2014 never: SSR HTML stays as-is, no JS activation\n\nif (typeof window !== 'undefined') {\n const _initMatch = router.matchRoute(window.location.pathname)\n const _hydrateStrategy = _initMatch?.route?.meta?.hydrate ?? 'load'\n\n if (_hydrateStrategy === 'none') {\n // Static HTML only \u2014 leave SSR output untouched, clean up data immediately.\n delete (globalThis).__CER_DATA__\n } else {\n const _doHydrate = async () => {\n if (_initMatch?.route?.load) {\n try { await _initMatch.route.load() } catch { /* non-fatal */ }\n }\n // Use the original (unwrapped) replace so isNavigating stays false during\n // the initial paint \u2014 the loading component must not flash over pre-rendered content.\n await _replace(window.location.pathname + window.location.search + window.location.hash)\n // Clear SSR loader data after initial navigation so subsequent client-side\n // navigations don't accidentally reuse stale server data.\n delete (globalThis).__CER_DATA__\n }\n\n if (_hydrateStrategy === 'idle') {\n // Defer until the browser has finished higher-priority work.\n if (typeof requestIdleCallback !== 'undefined') {\n requestIdleCallback(() => { void _doHydrate() })\n } else {\n // Safari / older environments fallback.\n setTimeout(() => { void _doHydrate() }, 1)\n }\n } else if (_hydrateStrategy === 'visible') {\n // Defer until cer-layout-view (or body as fallback) enters the viewport.\n const _el = document.querySelector('cer-layout-view') ?? document.body\n const _io = new IntersectionObserver(([entry]) => {\n if (entry.isIntersecting) { _io.disconnect(); void _doHydrate() }\n })\n _io.observe(_el)\n } else {\n // 'load' \u2014 hydrate immediately (default behaviour).\n await _doHydrate()\n }\n }\n}\n\nexport { router }\n";
9
9
  //# sourceMappingURL=app-template.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"app-template.d.ts","sourceRoot":"","sources":["../../src/runtime/app-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,qoOAkK9B,CAAA"}
1
+ {"version":3,"file":"app-template.d.ts","sourceRoot":"","sources":["../../src/runtime/app-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,slQAgM9B,CAAA"}
@@ -142,28 +142,58 @@ for (const plugin of plugins) {
142
142
  }
143
143
  }
144
144
 
145
- // ─── Pre-load initial route ───────────────────────────────────────────────────
145
+ // ─── Pre-load initial route + hydration strategy ─────────────────────────────
146
146
  // Download the current page's route chunk AFTER plugins run so that
147
147
  // cer-layout-view's first render (which calls provide()) completes before
148
148
  // page components are defined and their renders are scheduled. This ensures
149
149
  // inject() in child components can find values stored by provide().
150
+ //
151
+ // meta.hydrate controls WHEN the initial page activates:
152
+ // 'load' — immediately (default)
153
+ // 'idle' — deferred until requestIdleCallback (browser idle)
154
+ // 'visible' — deferred until cer-layout-view enters the viewport
155
+ // 'none' — never: SSR HTML stays as-is, no JS activation
150
156
 
151
157
  if (typeof window !== 'undefined') {
152
158
  const _initMatch = router.matchRoute(window.location.pathname)
153
- if (_initMatch?.route?.load) {
154
- try { await _initMatch.route.load() } catch { /* non-fatal */ }
155
- }
156
- }
157
-
158
- // ─── Initial navigation ──────────────────────────────────────────────────────
159
+ const _hydrateStrategy = _initMatch?.route?.meta?.hydrate ?? 'load'
160
+
161
+ if (_hydrateStrategy === 'none') {
162
+ // Static HTML only — leave SSR output untouched, clean up data immediately.
163
+ delete (globalThis).__CER_DATA__
164
+ } else {
165
+ const _doHydrate = async () => {
166
+ if (_initMatch?.route?.load) {
167
+ try { await _initMatch.route.load() } catch { /* non-fatal */ }
168
+ }
169
+ // Use the original (unwrapped) replace so isNavigating stays false during
170
+ // the initial paint — the loading component must not flash over pre-rendered content.
171
+ await _replace(window.location.pathname + window.location.search + window.location.hash)
172
+ // Clear SSR loader data after initial navigation so subsequent client-side
173
+ // navigations don't accidentally reuse stale server data.
174
+ delete (globalThis).__CER_DATA__
175
+ }
159
176
 
160
- if (typeof window !== 'undefined') {
161
- // Use the original (unwrapped) replace so isNavigating stays false during
162
- // the initial paint the loading component must not flash over pre-rendered content.
163
- await _replace(window.location.pathname + window.location.search + window.location.hash)
164
- // Clear SSR loader data after initial navigation so subsequent client-side
165
- // navigations don't accidentally reuse stale server data.
166
- delete (globalThis).__CER_DATA__
177
+ if (_hydrateStrategy === 'idle') {
178
+ // Defer until the browser has finished higher-priority work.
179
+ if (typeof requestIdleCallback !== 'undefined') {
180
+ requestIdleCallback(() => { void _doHydrate() })
181
+ } else {
182
+ // Safari / older environments fallback.
183
+ setTimeout(() => { void _doHydrate() }, 1)
184
+ }
185
+ } else if (_hydrateStrategy === 'visible') {
186
+ // Defer until cer-layout-view (or body as fallback) enters the viewport.
187
+ const _el = document.querySelector('cer-layout-view') ?? document.body
188
+ const _io = new IntersectionObserver(([entry]) => {
189
+ if (entry.isIntersecting) { _io.disconnect(); void _doHydrate() }
190
+ })
191
+ _io.observe(_el)
192
+ } else {
193
+ // 'load' — hydrate immediately (default behaviour).
194
+ await _doHydrate()
195
+ }
196
+ }
167
197
  }
168
198
 
169
199
  export { router }
@@ -1 +1 @@
1
- {"version":3,"file":"app-template.js","sourceRoot":"","sources":["../../src/runtime/app-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkKjC,CAAA"}
1
+ {"version":3,"file":"app-template.js","sourceRoot":"","sources":["../../src/runtime/app-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgMjC,CAAA"}
package/docs/routing.md CHANGED
@@ -148,14 +148,30 @@ Array of named middleware to run before navigating to this page. Names must matc
148
148
 
149
149
  ### `meta.hydrate`
150
150
 
151
- Controls when the component hydrates on the client after SSR:
151
+ Controls when the page component activates (hydrates) on the client after SSR. The server always renders full HTML regardless of this setting — it only affects client-side JS activation timing.
152
152
 
153
- | Value | Behavior |
154
- |---|---|
155
- | `'load'` | Hydrates immediately on page load |
156
- | `'idle'` | Hydrates when the browser is idle |
157
- | `'visible'` | Hydrates when the element enters the viewport |
158
- | `'none'` | Never hydrates (static HTML only) |
153
+ | Value | Behavior | Browser API |
154
+ |---|---|---|
155
+ | `'load'` | Hydrates immediately on page load (default) | — |
156
+ | `'idle'` | Defers until the browser has finished higher-priority work | `requestIdleCallback` (falls back to `setTimeout` in Safari) |
157
+ | `'visible'` | Defers until `<cer-layout-view>` enters the viewport | `IntersectionObserver` |
158
+ | `'none'` | Never hydrates SSR HTML stays as-is, no JS activation | — |
159
+
160
+ ```ts
161
+ // app/pages/marketing.ts — defer activation until idle
162
+ export const meta = {
163
+ hydrate: 'idle',
164
+ }
165
+
166
+ // app/pages/legal/terms.ts — fully static, no JS needed
167
+ export const meta = {
168
+ hydrate: 'none',
169
+ }
170
+ ```
171
+
172
+ **`'none'` and `usePageData`:** When `hydrate: 'none'` is set, the page component never activates and `usePageData()` loader data is not consumed. Avoid using `loader` on pages with `hydrate: 'none'` — the serialized `window.__CER_DATA__` will be present in the HTML but never read or cleaned up by the client.
173
+
174
+ **SPA mode:** `meta.hydrate` has no effect in SPA mode — there is no SSR output to preserve, so the component always activates immediately.
159
175
 
160
176
  ### `meta.ssg.paths`
161
177
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jasonshimmy/vite-plugin-cer-app",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Nuxt-style meta-framework for @jasonshimmy/custom-elements-runtime",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -432,6 +432,81 @@ describe('generateRoutesCode — meta.render', () => {
432
432
  })
433
433
  })
434
434
 
435
+ // ─── meta.hydrate ─────────────────────────────────────────────────────────────
436
+
437
+ describe('generateRoutesCode — meta.hydrate', () => {
438
+ beforeEach(() => {
439
+ vi.mocked(existsSync).mockReturnValue(true)
440
+ vi.mocked(scanDirectory).mockResolvedValue([])
441
+ vi.mocked(readFile).mockResolvedValue('' as never)
442
+ })
443
+
444
+ it('emits meta.hydrate "idle" when declared', async () => {
445
+ vi.mocked(scanDirectory).mockResolvedValue([`${PAGES}/about.ts`])
446
+ vi.mocked(readFile).mockResolvedValue(
447
+ `export const meta = { hydrate: 'idle' }` as never,
448
+ )
449
+ const code = await generateRoutesCode(PAGES)
450
+ expect(code).toContain('hydrate: "idle"')
451
+ })
452
+
453
+ it('emits meta.hydrate "visible" when declared', async () => {
454
+ vi.mocked(scanDirectory).mockResolvedValue([`${PAGES}/hero.ts`])
455
+ vi.mocked(readFile).mockResolvedValue(
456
+ `export const meta = { hydrate: 'visible' }` as never,
457
+ )
458
+ const code = await generateRoutesCode(PAGES)
459
+ expect(code).toContain('hydrate: "visible"')
460
+ })
461
+
462
+ it('emits meta.hydrate "none" when declared', async () => {
463
+ vi.mocked(scanDirectory).mockResolvedValue([`${PAGES}/static-page.ts`])
464
+ vi.mocked(readFile).mockResolvedValue(
465
+ `export const meta = { hydrate: 'none' }` as never,
466
+ )
467
+ const code = await generateRoutesCode(PAGES)
468
+ expect(code).toContain('hydrate: "none"')
469
+ })
470
+
471
+ it('does NOT emit meta.hydrate when "load" (default — no-op)', async () => {
472
+ vi.mocked(scanDirectory).mockResolvedValue([`${PAGES}/about.ts`])
473
+ vi.mocked(readFile).mockResolvedValue(
474
+ `export const meta = { hydrate: 'load' }` as never,
475
+ )
476
+ const code = await generateRoutesCode(PAGES)
477
+ expect(code).not.toContain('hydrate:')
478
+ })
479
+
480
+ it('omits hydrate meta when not declared', async () => {
481
+ vi.mocked(scanDirectory).mockResolvedValue([`${PAGES}/about.ts`])
482
+ vi.mocked(readFile).mockResolvedValue(
483
+ `component('page-about', () => html\`<h1>About</h1>\`)` as never,
484
+ )
485
+ const code = await generateRoutesCode(PAGES)
486
+ expect(code).not.toContain('hydrate:')
487
+ })
488
+
489
+ it('ignores unknown hydrate values', async () => {
490
+ vi.mocked(scanDirectory).mockResolvedValue([`${PAGES}/about.ts`])
491
+ vi.mocked(readFile).mockResolvedValue(
492
+ `export const meta = { hydrate: 'lazy' }` as never,
493
+ )
494
+ const code = await generateRoutesCode(PAGES)
495
+ expect(code).not.toContain('hydrate:')
496
+ })
497
+
498
+ it('can combine hydrate with layout and render', async () => {
499
+ vi.mocked(scanDirectory).mockResolvedValue([`${PAGES}/dashboard.ts`])
500
+ vi.mocked(readFile).mockResolvedValue(
501
+ `export const meta = { hydrate: 'idle', layout: 'minimal', render: 'server' }` as never,
502
+ )
503
+ const code = await generateRoutesCode(PAGES)
504
+ expect(code).toContain('hydrate: "idle"')
505
+ expect(code).toContain('layout: "minimal"')
506
+ expect(code).toContain('render: "server"')
507
+ })
508
+ })
509
+
435
510
  // ─── nested layouts (layoutChain) ────────────────────────────────────────────
436
511
 
437
512
  describe('generateRoutesCode — layoutChain (nested layouts)', () => {
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { APP_ENTRY_TEMPLATE } from '../../runtime/app-template.js'
3
+
4
+ // ─── Hydration strategy ───────────────────────────────────────────────────────
5
+
6
+ describe('APP_ENTRY_TEMPLATE — meta.hydrate', () => {
7
+ it('reads meta.hydrate from the matched route', () => {
8
+ expect(APP_ENTRY_TEMPLATE).toContain('_initMatch?.route?.meta?.hydrate')
9
+ })
10
+
11
+ it('defaults to "load" when hydrate is not set', () => {
12
+ expect(APP_ENTRY_TEMPLATE).toContain(`?? 'load'`)
13
+ })
14
+
15
+ it('skips hydration entirely for strategy "none"', () => {
16
+ expect(APP_ENTRY_TEMPLATE).toContain(`_hydrateStrategy === 'none'`)
17
+ // Should delete __CER_DATA__ but NOT call _replace
18
+ const noneBlock = APP_ENTRY_TEMPLATE.slice(
19
+ APP_ENTRY_TEMPLATE.indexOf(`_hydrateStrategy === 'none'`),
20
+ )
21
+ // The delete must appear before _doHydrate is defined (i.e. in the none branch)
22
+ expect(APP_ENTRY_TEMPLATE).toContain(`delete (globalThis).__CER_DATA__`)
23
+ })
24
+
25
+ it('defers hydration with requestIdleCallback for strategy "idle"', () => {
26
+ expect(APP_ENTRY_TEMPLATE).toContain(`_hydrateStrategy === 'idle'`)
27
+ expect(APP_ENTRY_TEMPLATE).toContain('requestIdleCallback')
28
+ })
29
+
30
+ it('includes a setTimeout fallback for environments without requestIdleCallback', () => {
31
+ expect(APP_ENTRY_TEMPLATE).toContain('typeof requestIdleCallback')
32
+ expect(APP_ENTRY_TEMPLATE).toContain('setTimeout')
33
+ })
34
+
35
+ it('defers hydration with IntersectionObserver for strategy "visible"', () => {
36
+ expect(APP_ENTRY_TEMPLATE).toContain(`_hydrateStrategy === 'visible'`)
37
+ expect(APP_ENTRY_TEMPLATE).toContain('IntersectionObserver')
38
+ })
39
+
40
+ it('observes cer-layout-view element for "visible" strategy', () => {
41
+ expect(APP_ENTRY_TEMPLATE).toContain(`querySelector('cer-layout-view')`)
42
+ })
43
+
44
+ it('falls back to document.body when cer-layout-view is not found', () => {
45
+ expect(APP_ENTRY_TEMPLATE).toContain('document.body')
46
+ })
47
+
48
+ it('disconnects the IntersectionObserver after first intersection', () => {
49
+ expect(APP_ENTRY_TEMPLATE).toContain('_io.disconnect()')
50
+ })
51
+
52
+ it('calls _doHydrate immediately for strategy "load"', () => {
53
+ // The else branch (default load) calls _doHydrate directly with await
54
+ expect(APP_ENTRY_TEMPLATE).toContain('await _doHydrate()')
55
+ })
56
+
57
+ it('_doHydrate loads the route chunk and calls _replace', () => {
58
+ expect(APP_ENTRY_TEMPLATE).toContain('_initMatch?.route?.load')
59
+ expect(APP_ENTRY_TEMPLATE).toContain('await _replace(window.location.pathname')
60
+ })
61
+
62
+ it('_doHydrate clears __CER_DATA__ after navigation', () => {
63
+ // delete must appear inside _doHydrate, i.e. after _replace and before the closing }
64
+ const doHydrateStart = APP_ENTRY_TEMPLATE.indexOf('const _doHydrate')
65
+ const doHydrateEnd = APP_ENTRY_TEMPLATE.indexOf('\n }', doHydrateStart)
66
+ const doHydrateBlock = APP_ENTRY_TEMPLATE.slice(doHydrateStart, doHydrateEnd)
67
+ expect(doHydrateBlock).toContain('delete (globalThis).__CER_DATA__')
68
+ })
69
+ })
@@ -64,6 +64,24 @@ function extractTransition(source: string): string | boolean | null {
64
64
  return null
65
65
  }
66
66
 
67
+ /**
68
+ * Extracts the per-route `hydrate` strategy from a page file's source.
69
+ * Returns 'load', 'idle', 'visible', 'none', or null if absent.
70
+ * 'load' is the default — callers skip emitting it to keep the bundle lean.
71
+ *
72
+ * Matches patterns like:
73
+ * hydrate: 'idle'
74
+ * hydrate: 'visible'
75
+ * hydrate: 'none'
76
+ */
77
+ function extractHydrate(source: string): 'load' | 'idle' | 'visible' | 'none' | null {
78
+ const match = source.match(/hydrate\s*:\s*['"]([^'"]+)['"]/)
79
+ if (!match) return null
80
+ const val = match[1]
81
+ if (val === 'load' || val === 'idle' || val === 'visible' || val === 'none') return val
82
+ return null
83
+ }
84
+
67
85
  /**
68
86
  * Extracts the per-route `render` strategy from a page file's source.
69
87
  * Returns 'static', 'server', 'spa', or null if absent.
@@ -180,6 +198,7 @@ export async function generateRoutesCode(pagesDir: string): Promise<string> {
180
198
  revalidate: number | null
181
199
  transition: string | boolean | null
182
200
  render: 'static' | 'server' | 'spa' | null
201
+ hydrate: 'load' | 'idle' | 'visible' | 'none' | null
183
202
  }> = await Promise.all(
184
203
  sorted.map(async (entry) => {
185
204
  try {
@@ -193,9 +212,10 @@ export async function generateRoutesCode(pagesDir: string): Promise<string> {
193
212
  revalidate: extractRevalidate(src),
194
213
  transition: extractTransition(src),
195
214
  render: extractRender(src),
215
+ hydrate: extractHydrate(src),
196
216
  }
197
217
  } catch {
198
- return { middleware: [], layout: null, layoutChain: null, revalidate: null, transition: null, render: null }
218
+ return { middleware: [], layout: null, layoutChain: null, revalidate: null, transition: null, render: null, hydrate: null }
199
219
  }
200
220
  }),
201
221
  )
@@ -204,7 +224,7 @@ export async function generateRoutesCode(pagesDir: string): Promise<string> {
204
224
 
205
225
  // Build routes array with lazy load() functions for code splitting.
206
226
  const routeItems = sorted.map((entry, i) => {
207
- const { middleware: mw, layout, layoutChain, revalidate, transition, render } = metaPerEntry[i]
227
+ const { middleware: mw, layout, layoutChain, revalidate, transition, render, hydrate } = metaPerEntry[i]
208
228
  const filePath = JSON.stringify(entry.filePath)
209
229
  const tagName = JSON.stringify(entry.tagName)
210
230
  const routePath = JSON.stringify(entry.routePath)
@@ -231,6 +251,10 @@ export async function generateRoutesCode(pagesDir: string): Promise<string> {
231
251
  if (render !== null) {
232
252
  metaFields.push(`render: ${JSON.stringify(render)}`)
233
253
  }
254
+ // 'load' is the default — only emit non-default values to keep bundle lean.
255
+ if (hydrate !== null && hydrate !== 'load') {
256
+ metaFields.push(`hydrate: ${JSON.stringify(hydrate)}`)
257
+ }
234
258
  const metaStr = metaFields.length > 0 ? ` meta: { ${metaFields.join(', ')} },\n` : ''
235
259
 
236
260
  if (mw.length === 0) {
@@ -142,28 +142,58 @@ for (const plugin of plugins) {
142
142
  }
143
143
  }
144
144
 
145
- // ─── Pre-load initial route ───────────────────────────────────────────────────
145
+ // ─── Pre-load initial route + hydration strategy ─────────────────────────────
146
146
  // Download the current page's route chunk AFTER plugins run so that
147
147
  // cer-layout-view's first render (which calls provide()) completes before
148
148
  // page components are defined and their renders are scheduled. This ensures
149
149
  // inject() in child components can find values stored by provide().
150
+ //
151
+ // meta.hydrate controls WHEN the initial page activates:
152
+ // 'load' — immediately (default)
153
+ // 'idle' — deferred until requestIdleCallback (browser idle)
154
+ // 'visible' — deferred until cer-layout-view enters the viewport
155
+ // 'none' — never: SSR HTML stays as-is, no JS activation
150
156
 
151
157
  if (typeof window !== 'undefined') {
152
158
  const _initMatch = router.matchRoute(window.location.pathname)
153
- if (_initMatch?.route?.load) {
154
- try { await _initMatch.route.load() } catch { /* non-fatal */ }
155
- }
156
- }
157
-
158
- // ─── Initial navigation ──────────────────────────────────────────────────────
159
+ const _hydrateStrategy = _initMatch?.route?.meta?.hydrate ?? 'load'
160
+
161
+ if (_hydrateStrategy === 'none') {
162
+ // Static HTML only — leave SSR output untouched, clean up data immediately.
163
+ delete (globalThis).__CER_DATA__
164
+ } else {
165
+ const _doHydrate = async () => {
166
+ if (_initMatch?.route?.load) {
167
+ try { await _initMatch.route.load() } catch { /* non-fatal */ }
168
+ }
169
+ // Use the original (unwrapped) replace so isNavigating stays false during
170
+ // the initial paint — the loading component must not flash over pre-rendered content.
171
+ await _replace(window.location.pathname + window.location.search + window.location.hash)
172
+ // Clear SSR loader data after initial navigation so subsequent client-side
173
+ // navigations don't accidentally reuse stale server data.
174
+ delete (globalThis).__CER_DATA__
175
+ }
159
176
 
160
- if (typeof window !== 'undefined') {
161
- // Use the original (unwrapped) replace so isNavigating stays false during
162
- // the initial paint the loading component must not flash over pre-rendered content.
163
- await _replace(window.location.pathname + window.location.search + window.location.hash)
164
- // Clear SSR loader data after initial navigation so subsequent client-side
165
- // navigations don't accidentally reuse stale server data.
166
- delete (globalThis).__CER_DATA__
177
+ if (_hydrateStrategy === 'idle') {
178
+ // Defer until the browser has finished higher-priority work.
179
+ if (typeof requestIdleCallback !== 'undefined') {
180
+ requestIdleCallback(() => { void _doHydrate() })
181
+ } else {
182
+ // Safari / older environments fallback.
183
+ setTimeout(() => { void _doHydrate() }, 1)
184
+ }
185
+ } else if (_hydrateStrategy === 'visible') {
186
+ // Defer until cer-layout-view (or body as fallback) enters the viewport.
187
+ const _el = document.querySelector('cer-layout-view') ?? document.body
188
+ const _io = new IntersectionObserver(([entry]) => {
189
+ if (entry.isIntersecting) { _io.disconnect(); void _doHydrate() }
190
+ })
191
+ _io.observe(_el)
192
+ } else {
193
+ // 'load' — hydrate immediately (default behaviour).
194
+ await _doHydrate()
195
+ }
196
+ }
167
197
  }
168
198
 
169
199
  export { router }