@jasonshimmy/vite-plugin-cer-app 0.19.1 → 0.19.2
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 +4 -0
- package/commits.txt +1 -1
- package/dist/cli/create/templates/spa/package.json.tpl +1 -1
- package/dist/cli/create/templates/ssg/package.json.tpl +1 -1
- package/dist/cli/create/templates/ssr/package.json.tpl +1 -1
- package/dist/plugin/build-ssr.d.ts.map +1 -1
- package/dist/plugin/build-ssr.js +17 -1
- package/dist/plugin/build-ssr.js.map +1 -1
- package/dist/runtime/app-template.d.ts +1 -1
- package/dist/runtime/app-template.d.ts.map +1 -1
- package/dist/runtime/app-template.js +30 -1
- package/dist/runtime/app-template.js.map +1 -1
- package/e2e/cypress/e2e/fouc.cy.ts +55 -0
- package/e2e/kitchen-sink/app/pages/plugin-dsd-test.ts +10 -0
- package/e2e/kitchen-sink/app/plugins/02.ks-plugin-components.ts +27 -0
- package/package.json +1 -1
- package/src/__tests__/plugin/build-ssr.test.ts +14 -0
- package/src/cli/create/templates/spa/package.json.tpl +1 -1
- package/src/cli/create/templates/ssg/package.json.tpl +1 -1
- package/src/cli/create/templates/ssr/package.json.tpl +1 -1
- package/src/plugin/build-ssr.ts +17 -1
- package/src/runtime/app-template.ts +30 -1
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.19.2] - 2026-03-28
|
|
5
|
+
|
|
6
|
+
- fix: add plugin DSD test cases and ensure shared component registry in SSR (ee3fffb)
|
|
7
|
+
|
|
4
8
|
## [v0.19.1] - 2026-03-28
|
|
5
9
|
|
|
6
10
|
- fix: add entry-server.ts generation for SSR support in dev server (2f102c8)
|
package/commits.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
- fix: add
|
|
1
|
+
- fix: add plugin DSD test cases and ensure shared component registry in SSR (ee3fffb)
|
|
@@ -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,
|
|
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"}
|
package/dist/plugin/build-ssr.js
CHANGED
|
@@ -100,7 +100,23 @@ export async function buildSSR(config, viteUserConfig = {}) {
|
|
|
100
100
|
},
|
|
101
101
|
},
|
|
102
102
|
ssr: {
|
|
103
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
@@ -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 ───────────────────────────────────────
|
package/src/plugin/build-ssr.ts
CHANGED
|
@@ -108,7 +108,23 @@ export async function buildSSR(
|
|
|
108
108
|
},
|
|
109
109
|
},
|
|
110
110
|
ssr: {
|
|
111
|
-
|
|
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
|
-
|
|
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.
|