@jasonshimmy/vite-plugin-cer-app 0.19.1 → 0.19.3

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,14 @@
1
1
  # Changelog
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
+ ## [v0.19.3] - 2026-03-29
5
+
6
+ - fix: bump versions in generated package.json files and update the cer dependency for development (bf1014d)
7
+
8
+ ## [v0.19.2] - 2026-03-28
9
+
10
+ - fix: add plugin DSD test cases and ensure shared component registry in SSR (ee3fffb)
11
+
4
12
  ## [v0.19.1] - 2026-03-28
5
13
 
6
14
  - fix: add entry-server.ts generation for SSR support in dev server (2f102c8)
package/commits.txt CHANGED
@@ -1 +1 @@
1
- - fix: add entry-server.ts generation for SSR support in dev server (2f102c8)
1
+ - fix: bump versions in generated package.json files and update the cer dependency for development (bf1014d)
@@ -8,10 +8,10 @@
8
8
  "preview": "cer-app preview"
9
9
  },
10
10
  "dependencies": {
11
- "@jasonshimmy/custom-elements-runtime": "^3.7.1"
11
+ "@jasonshimmy/custom-elements-runtime": "^3.7.2"
12
12
  },
13
13
  "devDependencies": {
14
- "@jasonshimmy/vite-plugin-cer-app": "^0.19.1",
14
+ "@jasonshimmy/vite-plugin-cer-app": "^0.19.3",
15
15
  "typescript": "^5.9.3",
16
16
  "vite": "^8.0.3"
17
17
  }
@@ -9,10 +9,10 @@
9
9
  "preview": "cer-app preview"
10
10
  },
11
11
  "dependencies": {
12
- "@jasonshimmy/custom-elements-runtime": "^3.7.1"
12
+ "@jasonshimmy/custom-elements-runtime": "^3.7.2"
13
13
  },
14
14
  "devDependencies": {
15
- "@jasonshimmy/vite-plugin-cer-app": "^0.19.1",
15
+ "@jasonshimmy/vite-plugin-cer-app": "^0.19.3",
16
16
  "typescript": "^5.9.3",
17
17
  "vite": "^8.0.3"
18
18
  }
@@ -8,10 +8,10 @@
8
8
  "preview": "cer-app preview --ssr"
9
9
  },
10
10
  "dependencies": {
11
- "@jasonshimmy/custom-elements-runtime": "^3.7.1"
11
+ "@jasonshimmy/custom-elements-runtime": "^3.7.2"
12
12
  },
13
13
  "devDependencies": {
14
- "@jasonshimmy/vite-plugin-cer-app": "^0.19.1",
14
+ "@jasonshimmy/vite-plugin-cer-app": "^0.19.3",
15
15
  "typescript": "^5.9.3",
16
16
  "vite": "^8.0.3"
17
17
  }
@@ -1 +1 @@
1
- {"version":3,"file":"build-ssr.d.ts","sourceRoot":"","sources":["../../src/plugin/build-ssr.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,UAAU,EAAE,MAAM,MAAM,CAAA;AAG7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAIxD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAQpE;AAMD;;;;;GAKG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,iBAAiB,EACzB,cAAc,GAAE,UAAe,GAC9B,OAAO,CAAC,IAAI,CAAC,CA8Ef"}
1
+ {"version":3,"file":"build-ssr.d.ts","sourceRoot":"","sources":["../../src/plugin/build-ssr.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,UAAU,EAAE,MAAM,MAAM,CAAA;AAG7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAIxD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAQpE;AAMD;;;;;GAKG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,iBAAiB,EACzB,cAAc,GAAE,UAAe,GAC9B,OAAO,CAAC,IAAI,CAAC,CA8Ff"}
@@ -100,7 +100,23 @@ export async function buildSSR(config, viteUserConfig = {}) {
100
100
  },
101
101
  },
102
102
  ssr: {
103
- noExternal: ['@jasonshimmy/custom-elements-runtime', '@jasonshimmy/vite-plugin-cer-app'],
103
+ // Keep vite-plugin-cer-app inlined so its virtual-module composables
104
+ // (useRoute, useState, useFetch, etc.) are available in the server bundle.
105
+ //
106
+ // Do NOT add @jasonshimmy/custom-elements-runtime here. Inlining it
107
+ // creates a second, isolated copy of the component registry (the module-
108
+ // level Map in namespace-helpers). Third-party CER component libraries
109
+ // (e.g. @jasonshimmy/cer-material) are external and resolve the runtime
110
+ // from node_modules at runtime, giving them a *different* Map instance.
111
+ // That means components registered by plugins never appear in the
112
+ // renderer's registry → renderToStreamWithJITCSSDSD emits bare stubs
113
+ // with no DSD → FOUC when the browser upgrades those elements.
114
+ //
115
+ // By keeping the runtime external both the server bundle and all
116
+ // third-party plugins resolve it from node_modules at runtime, sharing
117
+ // one Map and one registry, so DSD is generated for all registered
118
+ // components regardless of which package called component().
119
+ noExternal: ['@jasonshimmy/vite-plugin-cer-app'],
104
120
  },
105
121
  });
106
122
  console.log('[cer-app] SSR build complete.');
@@ -1 +1 @@
1
- {"version":3,"file":"build-ssr.js","sourceRoot":"","sources":["../../src/plugin/build-ssr.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"build-ssr.js","sourceRoot":"","sources":["../../src/plugin/build-ssr.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 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// Expose the router globally so useRoute() and navigateTo() can access it\n// from any composable without circular imports.\n;(globalThis).__cerRouter = router\n\n// \u2500\u2500\u2500 Page loader 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\n\n// Holds the pre-loaded page tag and loader-derived attrs for the current route.\n// Set by _loadPageForPath before every navigation so cer-layout-view can render\n// the page element directly with the correct attributes (enabling useProps()).\nlet _currentPageTag = null\nlet _currentPageAttrs = {}\n// The pathname this tag was loaded for \u2014 used to detect router-internal\n// redirects (e.g. middleware returning '/login') so cer-layout-view falls\n// back to <router-view> when the current route differs from what was pre-loaded.\nlet _currentPagePath = null\n\n// Pre-loads the page module for `path`, calls its loader if present, and\n// stores the results so cer-layout-view can pass them as element attributes.\nasync function _loadPageForPath(path) {\n try {\n const url = new URL(path, 'http://x')\n const query = Object.fromEntries(url.searchParams)\n const matched = router.matchRoute(url.pathname)\n const route = matched?.route\n if (!route?.load) {\n _currentPageTag = null\n _currentPageAttrs = {}\n _currentPagePath = url.pathname\n return\n }\n const mod = await route.load()\n _currentPageTag = mod.default ?? null\n _currentPagePath = url.pathname\n const params = matched.params ?? {}\n let loaderAttrs = { ...params }\n if (typeof mod.loader === 'function') {\n try {\n const data = await mod.loader({ params, query })\n if (data !== undefined && data !== null) {\n // Make loader data available via usePageData() for this navigation.\n ;(globalThis).__CER_DATA__ = data\n // Merge primitive values as element attributes so useProps() works.\n const primitives = Object.fromEntries(\n Object.entries(data).filter(([, v]) => v !== null && v !== undefined && typeof v !== 'object' && typeof v !== 'function')\n )\n loaderAttrs = { ...loaderAttrs, ...primitives }\n }\n } catch (err) {\n // Loader errors are surfaced through currentError so the error boundary\n // (app/error.ts) can display a meaningful message \u2014 consistent with how\n // the server-side handler behaves when a loader throws.\n currentError.value = err instanceof Error ? err.message : String(err)\n }\n }\n _currentPageAttrs = loaderAttrs\n } catch {\n _currentPageTag = null\n _currentPageAttrs = {}\n _currentPagePath = null\n }\n}\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 // Clear stale loader data from the previous route before loading the new one.\n // If the new route has no loader, __CER_DATA__ stays undefined so usePageData()\n // correctly returns null instead of leaking the previous page's data.\n delete (globalThis).__CER_DATA__\n await _loadPageForPath(path)\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 // Clear stale loader data from the previous route before loading the new one.\n delete (globalThis).__CER_DATA__\n await _loadPageForPath(path)\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 const matched = router.matchRoute(current.value.path)\n const routeMeta = matched?.route?.meta\n\n if (currentError.value !== null) {\n const routeErrorTag = routeMeta?.errorTag ?? null\n const effectiveErrorTag = routeErrorTag ?? (hasError ? errorTag : null)\n if (effectiveErrorTag) {\n return { tag: effectiveErrorTag, 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 // Render the page component directly when a pre-loaded tag is available AND\n // the loaded path matches the current route. The path guard detects\n // router-internal redirects (e.g. middleware returning '/login'): when the\n // router redirects to a different path, _currentPagePath won't match\n // current.value.path and we fall back to <router-view> so the correct page\n // is rendered without stale pre-loaded state.\n const _useDirectRender = _currentPageTag && _currentPagePath === current.value.path\n const pageVnode = _useDirectRender\n ? { tag: _currentPageTag, props: { attrs: _currentPageAttrs }, children: [] }\n : { tag: 'router-view', props: {}, children: [] }\n\n // Support nested layout chains: meta.layoutChain = ['default', 'admin']\n // renders <layout-default><layout-admin><page/></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 = pageVnode\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 const _initPath = window.location.pathname + window.location.search + window.location.hash\n // Pre-load the page module and run the loader so cer-layout-view renders\n // the page tag directly with loader attrs (enables useProps() on hydration).\n await _loadPageForPath(_initPath)\n // Only activate the initial route if the URL hasn't changed while we were\n // loading the module asynchronously (e.g. a test or plugin navigated away\n // before _doHydrate finished). Calling _replace with a stale path would\n // override any navigation that happened during the async gap.\n const _currentPath = window.location.pathname + window.location.search + window.location.hash\n if (_currentPath === _initPath) {\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(_initPath)\n }\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";
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 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// Expose the router globally so useRoute() and navigateTo() can access it\n// from any composable without circular imports.\n;(globalThis).__cerRouter = router\n\n// \u2500\u2500\u2500 Page loader 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\n\n// Holds the pre-loaded page tag and loader-derived attrs for the current route.\n// Set by _loadPageForPath before every navigation so cer-layout-view can render\n// the page element directly with the correct attributes (enabling useProps()).\nlet _currentPageTag = null\nlet _currentPageAttrs = {}\n// The pathname this tag was loaded for \u2014 used to detect router-internal\n// redirects (e.g. middleware returning '/login') so cer-layout-view falls\n// back to <router-view> when the current route differs from what was pre-loaded.\nlet _currentPagePath = null\n\n// Pre-loads the page module for `path`, calls its loader if present, and\n// stores the results so cer-layout-view can pass them as element attributes.\nasync function _loadPageForPath(path) {\n try {\n const url = new URL(path, 'http://x')\n const query = Object.fromEntries(url.searchParams)\n const matched = router.matchRoute(url.pathname)\n const route = matched?.route\n if (!route?.load) {\n _currentPageTag = null\n _currentPageAttrs = {}\n _currentPagePath = url.pathname\n return\n }\n const mod = await route.load()\n _currentPageTag = mod.default ?? null\n _currentPagePath = url.pathname\n const params = matched.params ?? {}\n let loaderAttrs = { ...params }\n if (typeof mod.loader === 'function') {\n try {\n const data = await mod.loader({ params, query })\n if (data !== undefined && data !== null) {\n // Make loader data available via usePageData() for this navigation.\n ;(globalThis).__CER_DATA__ = data\n // Merge primitive values as element attributes so useProps() works.\n const primitives = Object.fromEntries(\n Object.entries(data).filter(([, v]) => v !== null && v !== undefined && typeof v !== 'object' && typeof v !== 'function')\n )\n loaderAttrs = { ...loaderAttrs, ...primitives }\n }\n } catch (err) {\n // Loader errors are surfaced through currentError so the error boundary\n // (app/error.ts) can display a meaningful message \u2014 consistent with how\n // the server-side handler behaves when a loader throws.\n currentError.value = err instanceof Error ? err.message : String(err)\n }\n }\n _currentPageAttrs = loaderAttrs\n } catch {\n _currentPageTag = null\n _currentPageAttrs = {}\n _currentPagePath = null\n }\n}\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 // Clear stale loader data from the previous route before loading the new one.\n // If the new route has no loader, __CER_DATA__ stays undefined so usePageData()\n // correctly returns null instead of leaking the previous page's data.\n delete (globalThis).__CER_DATA__\n await _loadPageForPath(path)\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 // Clear stale loader data from the previous route before loading the new one.\n delete (globalThis).__CER_DATA__\n await _loadPageForPath(path)\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\n// True until the initial page module has loaded and _replace has been called.\n// While true, cer-layout-view renders a <slot> so the SSR/SSG pre-rendered\n// light-DOM content stays visible and no FOUC occurs during the hydration gap.\n// Declared here (before component()) because customElements.define() upgrades\n// existing DOM elements synchronously, so the render function reads this ref\n// on the very first call \u2014 before _doHydrate has a chance to run.\nconst _cerHydrating = ref(true)\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 // Any router navigation (including the initial _replace in _doHydrate) will\n // arrive here. If we are still in the hydration gap when a navigation fires\n // (e.g. a redirect or user interaction before _doHydrate finishes), drop the\n // slot immediately so the live render takes over.\n unsub = router.subscribe((s) => {\n if (_cerHydrating.value) _cerHydrating.value = false\n current.value = s\n })\n })\n useOnDisconnected(() => { unsub?.(); unsub = undefined })\n\n // While the initial page module is being loaded, keep the SSR/SSG\n // pre-rendered light-DOM content visible by forwarding it through a <slot>.\n // This prevents the intermediate blank state that occurs when the shadow DOM\n // renders layout-default with an empty <router-view> before the page chunk\n // has arrived. In SPA mode (no pre-rendered content) the slot is empty, which\n // is no worse than the current router-view behaviour.\n if (_cerHydrating.value) {\n return { tag: 'slot', props: {}, children: [] }\n }\n\n const matched = router.matchRoute(current.value.path)\n const routeMeta = matched?.route?.meta\n\n if (currentError.value !== null) {\n const routeErrorTag = routeMeta?.errorTag ?? null\n const effectiveErrorTag = routeErrorTag ?? (hasError ? errorTag : null)\n if (effectiveErrorTag) {\n return { tag: effectiveErrorTag, 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 // Render the page component directly when a pre-loaded tag is available AND\n // the loaded path matches the current route. The path guard detects\n // router-internal redirects (e.g. middleware returning '/login'): when the\n // router redirects to a different path, _currentPagePath won't match\n // current.value.path and we fall back to <router-view> so the correct page\n // is rendered without stale pre-loaded state.\n const _useDirectRender = _currentPageTag && _currentPagePath === current.value.path\n const pageVnode = _useDirectRender\n ? { tag: _currentPageTag, props: { attrs: _currentPageAttrs }, children: [] }\n : { tag: 'router-view', props: {}, children: [] }\n\n // Support nested layout chains: meta.layoutChain = ['default', 'admin']\n // renders <layout-default><layout-admin><page/></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 = pageVnode\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 const _initPath = window.location.pathname + window.location.search + window.location.hash\n // Pre-load the page module and run the loader so cer-layout-view renders\n // the page tag directly with loader attrs (enables useProps() on hydration).\n await _loadPageForPath(_initPath)\n // Only activate the initial route if the URL hasn't changed while we were\n // loading the module asynchronously (e.g. a test or plugin navigated away\n // before _doHydrate finished). Calling _replace with a stale path would\n // override any navigation that happened during the async gap.\n const _currentPath = window.location.pathname + window.location.search + window.location.hash\n // Drop the hydration slot cover unconditionally: the page module is loaded\n // and cer-layout-view must switch from the <slot> to live rendering whether\n // or not a redirect changed the path during loading.\n _cerHydrating.value = false\n if (_currentPath === _initPath) {\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(_initPath)\n }\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,wvZAyR9B,CAAA"}
1
+ {"version":3,"file":"app-template.d.ts","sourceRoot":"","sources":["../../src/runtime/app-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,24cAsT9B,CAAA"}
@@ -156,6 +156,14 @@ const _pluginProvides = new Map()
156
156
 
157
157
  // ─── <cer-layout-view> ───────────────────────────────────────────────────────
158
158
 
159
+ // True until the initial page module has loaded and _replace has been called.
160
+ // While true, cer-layout-view renders a <slot> so the SSR/SSG pre-rendered
161
+ // light-DOM content stays visible and no FOUC occurs during the hydration gap.
162
+ // Declared here (before component()) because customElements.define() upgrades
163
+ // existing DOM elements synchronously, so the render function reads this ref
164
+ // on the very first call — before _doHydrate has a chance to run.
165
+ const _cerHydrating = ref(true)
166
+
159
167
  component('cer-layout-view', () => {
160
168
  // Forward plugin-provided values into the component context so inject() in
161
169
  // any descendant component can resolve them by walking up the DOM tree.
@@ -167,10 +175,27 @@ component('cer-layout-view', () => {
167
175
  let unsub
168
176
 
169
177
  useOnConnected(() => {
170
- unsub = router.subscribe((s) => { current.value = s })
178
+ // Any router navigation (including the initial _replace in _doHydrate) will
179
+ // arrive here. If we are still in the hydration gap when a navigation fires
180
+ // (e.g. a redirect or user interaction before _doHydrate finishes), drop the
181
+ // slot immediately so the live render takes over.
182
+ unsub = router.subscribe((s) => {
183
+ if (_cerHydrating.value) _cerHydrating.value = false
184
+ current.value = s
185
+ })
171
186
  })
172
187
  useOnDisconnected(() => { unsub?.(); unsub = undefined })
173
188
 
189
+ // While the initial page module is being loaded, keep the SSR/SSG
190
+ // pre-rendered light-DOM content visible by forwarding it through a <slot>.
191
+ // This prevents the intermediate blank state that occurs when the shadow DOM
192
+ // renders layout-default with an empty <router-view> before the page chunk
193
+ // has arrived. In SPA mode (no pre-rendered content) the slot is empty, which
194
+ // is no worse than the current router-view behaviour.
195
+ if (_cerHydrating.value) {
196
+ return { tag: 'slot', props: {}, children: [] }
197
+ }
198
+
174
199
  const matched = router.matchRoute(current.value.path)
175
200
  const routeMeta = matched?.route?.meta
176
201
 
@@ -253,6 +278,10 @@ if (typeof window !== 'undefined') {
253
278
  // before _doHydrate finished). Calling _replace with a stale path would
254
279
  // override any navigation that happened during the async gap.
255
280
  const _currentPath = window.location.pathname + window.location.search + window.location.hash
281
+ // Drop the hydration slot cover unconditionally: the page module is loaded
282
+ // and cer-layout-view must switch from the <slot> to live rendering whether
283
+ // or not a redirect changed the path during loading.
284
+ _cerHydrating.value = false
256
285
  if (_currentPath === _initPath) {
257
286
  // Use the original (unwrapped) replace so isNavigating stays false during
258
287
  // the initial paint — the loading component must not flash over pre-rendered content.
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyRjC,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsTjC,CAAA"}
@@ -27,6 +27,7 @@ const ALL_ROUTES = [
27
27
  '/items/1',
28
28
  '/items/2',
29
29
  '/login',
30
+ '/plugin-dsd-test',
30
31
  ]
31
32
 
32
33
  if (mode === 'spa') {
@@ -111,6 +112,60 @@ if (mode === 'spa') {
111
112
  })
112
113
  })
113
114
 
115
+ // ─── Plugin-registered components get DSD ────────────────────────────────
116
+ //
117
+ // Regression test for the "dual-instance registry" bug:
118
+ // When @jasonshimmy/custom-elements-runtime was in ssr.noExternal, the bundler
119
+ // inlined a separate copy of the runtime's component Map. Plugin files (treated
120
+ // as external) resolved the CER runtime from node_modules at runtime — a
121
+ // different Map instance. Components registered by plugins never appeared in
122
+ // the SSR renderer's registry, so renderToStreamWithJITCSSDSD emitted bare
123
+ // <custom-element> stubs with no DSD → FOUC when the browser upgraded them.
124
+ // The fix: keep @jasonshimmy/custom-elements-runtime external so all code
125
+ // (server bundle + plugin packages) shares one node_modules instance.
126
+
127
+ describe('Plugin-registered components — DSD in SSR/SSG', () => {
128
+ it('ks-plugin-card (registered via plugin, not auto-scanned) has a shadow template', () => {
129
+ cy.request('/plugin-dsd-test').then((resp) => {
130
+ const html: string = resp.body
131
+ expect(html, 'ks-plugin-card must appear in rendered HTML').to.include('ks-plugin-card')
132
+ // The component must have a shadow root template — not a bare stub
133
+ const pluginCardIdx = html.indexOf('ks-plugin-card')
134
+ const templateAfterCard = html.indexOf('<template shadowrootmode="open"', pluginCardIdx)
135
+ const nextTagClose = html.indexOf('</ks-plugin-card>', pluginCardIdx)
136
+ expect(templateAfterCard, 'ks-plugin-card must contain a <template shadowrootmode> (DSD)').to.be.greaterThan(-1)
137
+ expect(templateAfterCard, 'shadow template must appear before </ks-plugin-card>').to.be.lessThan(nextTagClose)
138
+ })
139
+ })
140
+
141
+ it('ks-plugin-card DSD contains the card CSS style', () => {
142
+ cy.request('/plugin-dsd-test').then((resp) => {
143
+ cy.assertDSDContains('/plugin-dsd-test', '.card')
144
+ })
145
+ })
146
+
147
+ it('ks-plugin-card has a live shadow root after hydration', () => {
148
+ // ks-plugin-card lives inside page-plugin-dsd-test's shadow root.
149
+ // page-plugin-dsd-test is light DOM of layout-default (slotted), so
150
+ // document.querySelector() can find it directly.
151
+ cy.visit('/plugin-dsd-test')
152
+ cy.get('[data-cy="plugin-dsd-test-page"]').should('exist')
153
+ cy.window().then((win) => {
154
+ const page = win.document.querySelector('page-plugin-dsd-test')
155
+ expect(page, 'page-plugin-dsd-test must exist').to.not.be.null
156
+ const card = page?.shadowRoot?.querySelector('ks-plugin-card')
157
+ expect(card, 'ks-plugin-card must exist inside page-plugin-dsd-test shadow root').to.not.be.null
158
+ expect((card as Element & { shadowRoot: ShadowRoot | null }).shadowRoot,
159
+ 'ks-plugin-card must have a live shadow root after hydration').to.not.be.null
160
+ })
161
+ })
162
+
163
+ it('ks-plugin-card slot content is accessible after hydration', () => {
164
+ cy.visit('/plugin-dsd-test')
165
+ cy.get('[data-cy="plugin-card-content"]').should('have.text', 'Card content from plugin component')
166
+ })
167
+ })
168
+
114
169
  // ─── DSD polyfill ─────────────────────────────────────────────────────────
115
170
 
116
171
  describe('DSD polyfill', () => {
@@ -0,0 +1,10 @@
1
+ export default component('page-plugin-dsd-test', () => {
2
+ return html`
3
+ <div data-cy="plugin-dsd-test-page">
4
+ <h1 data-cy="plugin-dsd-heading">Plugin DSD Test</h1>
5
+ <ks-plugin-card>
6
+ <span data-cy="plugin-card-content">Card content from plugin component</span>
7
+ </ks-plugin-card>
8
+ </div>
9
+ `
10
+ })
@@ -0,0 +1,27 @@
1
+ // Simulates a third-party component library imported as a plugin side-effect.
2
+ // This component is NOT in app/components/ (auto-scanned), so it can only be
3
+ // registered by this plugin import — exactly like @jasonshimmy/cer-material or
4
+ // any other npm CER component package.
5
+ //
6
+ // In SSR/SSG the server bundle must share one CER runtime registry with this
7
+ // plugin so that renderToStreamWithJITCSSDSD can emit DSD for ks-plugin-card.
8
+ import { component, html, css, useStyle } from '@jasonshimmy/custom-elements-runtime'
9
+
10
+ component('ks-plugin-card', () => {
11
+ useStyle(() => css`
12
+ :host { display: block; }
13
+ .card {
14
+ background: #f0fdf4;
15
+ border: 1px solid #bbf7d0;
16
+ border-radius: 8px;
17
+ padding: 16px;
18
+ }
19
+ `)
20
+ return html`
21
+ <div class="card" data-cy="ks-plugin-card">
22
+ <slot></slot>
23
+ </div>
24
+ `
25
+ })
26
+
27
+ export default { name: 'ks-plugin-components' }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jasonshimmy/vite-plugin-cer-app",
3
- "version": "0.19.1",
3
+ "version": "0.19.3",
4
4
  "description": "Nuxt-style meta-framework for @jasonshimmy/custom-elements-runtime",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -92,7 +92,7 @@
92
92
  "pathe": "^2.0.3"
93
93
  },
94
94
  "devDependencies": {
95
- "@jasonshimmy/custom-elements-runtime": "^3.7.1",
95
+ "@jasonshimmy/custom-elements-runtime": "^3.7.2",
96
96
  "@types/node": "^25.5.0",
97
97
  "@vitest/coverage-v8": "^4.1.2",
98
98
  "cypress": "^15.13.0",
@@ -74,6 +74,20 @@ describe('buildSSR', () => {
74
74
  const firstCall = buildMock.mock.calls[0][0] as Record<string, unknown>
75
75
  expect(firstCall.define).toEqual({ MY_FLAG: 'true' })
76
76
  })
77
+
78
+ it('server build keeps @jasonshimmy/vite-plugin-cer-app in ssr.noExternal', async () => {
79
+ await buildSSR(makeConfig())
80
+ const secondCall = buildMock.mock.calls[1][0] as Record<string, unknown>
81
+ const noExternal = (secondCall.ssr as Record<string, unknown>).noExternal as string[]
82
+ expect(noExternal).toContain('@jasonshimmy/vite-plugin-cer-app')
83
+ })
84
+
85
+ it('server build does NOT bundle @jasonshimmy/custom-elements-runtime so third-party component libraries share the same registry', async () => {
86
+ await buildSSR(makeConfig())
87
+ const secondCall = buildMock.mock.calls[1][0] as Record<string, unknown>
88
+ const noExternal = (secondCall.ssr as Record<string, unknown>).noExternal as string[]
89
+ expect(noExternal).not.toContain('@jasonshimmy/custom-elements-runtime')
90
+ })
77
91
  })
78
92
 
79
93
  // ─── resolveClientEntry fallback paths ───────────────────────────────────────
@@ -8,10 +8,10 @@
8
8
  "preview": "cer-app preview"
9
9
  },
10
10
  "dependencies": {
11
- "@jasonshimmy/custom-elements-runtime": "^3.7.1"
11
+ "@jasonshimmy/custom-elements-runtime": "^3.7.2"
12
12
  },
13
13
  "devDependencies": {
14
- "@jasonshimmy/vite-plugin-cer-app": "^0.19.1",
14
+ "@jasonshimmy/vite-plugin-cer-app": "^0.19.3",
15
15
  "typescript": "^5.9.3",
16
16
  "vite": "^8.0.3"
17
17
  }
@@ -9,10 +9,10 @@
9
9
  "preview": "cer-app preview"
10
10
  },
11
11
  "dependencies": {
12
- "@jasonshimmy/custom-elements-runtime": "^3.7.1"
12
+ "@jasonshimmy/custom-elements-runtime": "^3.7.2"
13
13
  },
14
14
  "devDependencies": {
15
- "@jasonshimmy/vite-plugin-cer-app": "^0.19.1",
15
+ "@jasonshimmy/vite-plugin-cer-app": "^0.19.3",
16
16
  "typescript": "^5.9.3",
17
17
  "vite": "^8.0.3"
18
18
  }
@@ -8,10 +8,10 @@
8
8
  "preview": "cer-app preview --ssr"
9
9
  },
10
10
  "dependencies": {
11
- "@jasonshimmy/custom-elements-runtime": "^3.7.1"
11
+ "@jasonshimmy/custom-elements-runtime": "^3.7.2"
12
12
  },
13
13
  "devDependencies": {
14
- "@jasonshimmy/vite-plugin-cer-app": "^0.19.1",
14
+ "@jasonshimmy/vite-plugin-cer-app": "^0.19.3",
15
15
  "typescript": "^5.9.3",
16
16
  "vite": "^8.0.3"
17
17
  }
@@ -108,7 +108,23 @@ export async function buildSSR(
108
108
  },
109
109
  },
110
110
  ssr: {
111
- noExternal: ['@jasonshimmy/custom-elements-runtime', '@jasonshimmy/vite-plugin-cer-app'],
111
+ // Keep vite-plugin-cer-app inlined so its virtual-module composables
112
+ // (useRoute, useState, useFetch, etc.) are available in the server bundle.
113
+ //
114
+ // Do NOT add @jasonshimmy/custom-elements-runtime here. Inlining it
115
+ // creates a second, isolated copy of the component registry (the module-
116
+ // level Map in namespace-helpers). Third-party CER component libraries
117
+ // (e.g. @jasonshimmy/cer-material) are external and resolve the runtime
118
+ // from node_modules at runtime, giving them a *different* Map instance.
119
+ // That means components registered by plugins never appear in the
120
+ // renderer's registry → renderToStreamWithJITCSSDSD emits bare stubs
121
+ // with no DSD → FOUC when the browser upgrades those elements.
122
+ //
123
+ // By keeping the runtime external both the server bundle and all
124
+ // third-party plugins resolve it from node_modules at runtime, sharing
125
+ // one Map and one registry, so DSD is generated for all registered
126
+ // components regardless of which package called component().
127
+ noExternal: ['@jasonshimmy/vite-plugin-cer-app'],
112
128
  },
113
129
  })
114
130
 
@@ -156,6 +156,14 @@ const _pluginProvides = new Map()
156
156
 
157
157
  // ─── <cer-layout-view> ───────────────────────────────────────────────────────
158
158
 
159
+ // True until the initial page module has loaded and _replace has been called.
160
+ // While true, cer-layout-view renders a <slot> so the SSR/SSG pre-rendered
161
+ // light-DOM content stays visible and no FOUC occurs during the hydration gap.
162
+ // Declared here (before component()) because customElements.define() upgrades
163
+ // existing DOM elements synchronously, so the render function reads this ref
164
+ // on the very first call — before _doHydrate has a chance to run.
165
+ const _cerHydrating = ref(true)
166
+
159
167
  component('cer-layout-view', () => {
160
168
  // Forward plugin-provided values into the component context so inject() in
161
169
  // any descendant component can resolve them by walking up the DOM tree.
@@ -167,10 +175,27 @@ component('cer-layout-view', () => {
167
175
  let unsub
168
176
 
169
177
  useOnConnected(() => {
170
- unsub = router.subscribe((s) => { current.value = s })
178
+ // Any router navigation (including the initial _replace in _doHydrate) will
179
+ // arrive here. If we are still in the hydration gap when a navigation fires
180
+ // (e.g. a redirect or user interaction before _doHydrate finishes), drop the
181
+ // slot immediately so the live render takes over.
182
+ unsub = router.subscribe((s) => {
183
+ if (_cerHydrating.value) _cerHydrating.value = false
184
+ current.value = s
185
+ })
171
186
  })
172
187
  useOnDisconnected(() => { unsub?.(); unsub = undefined })
173
188
 
189
+ // While the initial page module is being loaded, keep the SSR/SSG
190
+ // pre-rendered light-DOM content visible by forwarding it through a <slot>.
191
+ // This prevents the intermediate blank state that occurs when the shadow DOM
192
+ // renders layout-default with an empty <router-view> before the page chunk
193
+ // has arrived. In SPA mode (no pre-rendered content) the slot is empty, which
194
+ // is no worse than the current router-view behaviour.
195
+ if (_cerHydrating.value) {
196
+ return { tag: 'slot', props: {}, children: [] }
197
+ }
198
+
174
199
  const matched = router.matchRoute(current.value.path)
175
200
  const routeMeta = matched?.route?.meta
176
201
 
@@ -253,6 +278,10 @@ if (typeof window !== 'undefined') {
253
278
  // before _doHydrate finished). Calling _replace with a stale path would
254
279
  // override any navigation that happened during the async gap.
255
280
  const _currentPath = window.location.pathname + window.location.search + window.location.hash
281
+ // Drop the hydration slot cover unconditionally: the page module is loaded
282
+ // and cer-layout-view must switch from the <slot> to live rendering whether
283
+ // or not a redirect changed the path during loading.
284
+ _cerHydrating.value = false
256
285
  if (_currentPath === _initPath) {
257
286
  // Use the original (unwrapped) replace so isNavigating stays false during
258
287
  // the initial paint — the loading component must not flash over pre-rendered content.