@jasonshimmy/vite-plugin-cer-app 0.4.6 → 0.6.0
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/.github/copilot-instructions.md +4 -2
- package/CHANGELOG.md +8 -0
- package/IMPLEMENTATION_PLAN.md +52 -10
- package/commits.txt +1 -1
- package/dist/cli/commands/preview-isr.d.ts +51 -0
- package/dist/cli/commands/preview-isr.d.ts.map +1 -0
- package/dist/cli/commands/preview-isr.js +104 -0
- package/dist/cli/commands/preview-isr.js.map +1 -0
- package/dist/cli/commands/preview.d.ts.map +1 -1
- package/dist/cli/commands/preview.js +65 -1
- package/dist/cli/commands/preview.js.map +1 -1
- package/dist/cli/create/templates/spa/package.json.tpl +2 -2
- package/dist/cli/create/templates/ssg/package.json.tpl +2 -2
- package/dist/cli/create/templates/ssr/package.json.tpl +2 -2
- package/dist/plugin/build-ssg.d.ts.map +1 -1
- package/dist/plugin/build-ssg.js +4 -2
- package/dist/plugin/build-ssg.js.map +1 -1
- package/dist/plugin/dev-server.d.ts +3 -0
- package/dist/plugin/dev-server.d.ts.map +1 -1
- package/dist/plugin/dev-server.js.map +1 -1
- package/dist/plugin/dts-generator.d.ts.map +1 -1
- package/dist/plugin/dts-generator.js +8 -1
- package/dist/plugin/dts-generator.js.map +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +9 -1
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/transforms/auto-import.js +2 -2
- package/dist/plugin/transforms/auto-import.js.map +1 -1
- package/dist/plugin/virtual/routes.d.ts.map +1 -1
- package/dist/plugin/virtual/routes.js +95 -8
- package/dist/plugin/virtual/routes.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 +16 -4
- package/dist/runtime/app-template.js.map +1 -1
- package/dist/runtime/composables/index.d.ts +1 -0
- package/dist/runtime/composables/index.d.ts.map +1 -1
- package/dist/runtime/composables/index.js +1 -0
- package/dist/runtime/composables/index.js.map +1 -1
- package/dist/runtime/composables/use-runtime-config.d.ts +32 -0
- package/dist/runtime/composables/use-runtime-config.d.ts.map +1 -0
- package/dist/runtime/composables/use-runtime-config.js +41 -0
- package/dist/runtime/composables/use-runtime-config.js.map +1 -0
- package/dist/runtime/entry-server-template.d.ts +2 -2
- package/dist/runtime/entry-server-template.d.ts.map +1 -1
- package/dist/runtime/entry-server-template.js +50 -21
- package/dist/runtime/entry-server-template.js.map +1 -1
- package/dist/types/config.d.ts +24 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/page.d.ts +17 -0
- package/dist/types/page.d.ts.map +1 -1
- package/docs/composables.md +36 -0
- package/docs/configuration.md +52 -0
- package/docs/layouts.md +82 -0
- package/docs/rendering-modes.md +55 -14
- package/docs/routing.md +66 -0
- package/e2e/cypress/e2e/isr-nested-runtime.cy.ts +100 -0
- package/e2e/kitchen-sink/app/layouts/admin.ts +13 -0
- package/e2e/kitchen-sink/app/pages/admin/_layout.ts +1 -0
- package/e2e/kitchen-sink/app/pages/admin/dashboard.ts +11 -0
- package/e2e/kitchen-sink/app/pages/blog/[slug].ts +1 -0
- package/e2e/kitchen-sink/app/pages/isr-test.ts +17 -0
- package/e2e/kitchen-sink/cer.config.ts +5 -0
- package/package.json +3 -3
- package/src/__tests__/cli/preview-isr.test.ts +246 -0
- package/src/__tests__/plugin/build-ssg-render.test.ts +46 -0
- package/src/__tests__/plugin/dts-generator.test.ts +20 -0
- package/src/__tests__/plugin/entry-server-template.test.ts +23 -5
- package/src/__tests__/plugin/resolve-config.test.ts +15 -0
- package/src/__tests__/plugin/transforms/auto-import.test.ts +16 -0
- package/src/__tests__/plugin/virtual/routes.test.ts +195 -0
- package/src/__tests__/runtime/use-runtime-config.test.ts +59 -0
- package/src/cli/commands/preview-isr.ts +139 -0
- package/src/cli/commands/preview.ts +71 -2
- package/src/cli/create/templates/spa/package.json.tpl +2 -2
- package/src/cli/create/templates/ssg/package.json.tpl +2 -2
- package/src/cli/create/templates/ssr/package.json.tpl +2 -2
- package/src/plugin/build-ssg.ts +4 -2
- package/src/plugin/dev-server.ts +1 -0
- package/src/plugin/dts-generator.ts +8 -1
- package/src/plugin/index.ts +11 -1
- package/src/plugin/transforms/auto-import.ts +2 -2
- package/src/plugin/virtual/routes.ts +106 -9
- package/src/runtime/app-template.ts +16 -4
- package/src/runtime/composables/index.ts +1 -0
- package/src/runtime/composables/use-runtime-config.ts +40 -0
- package/src/runtime/entry-server-template.ts +50 -21
- package/src/types/config.ts +26 -0
- package/src/types/index.ts +1 -1
- package/src/types/page.ts +17 -0
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Key features:
|
|
9
9
|
* - AsyncLocalStorage for race-condition-free concurrent renders (SSG concurrency > 1)
|
|
10
|
-
* - Declarative Shadow DOM via
|
|
10
|
+
* - Declarative Shadow DOM via renderToStreamWithJITCSSDSD (always on, streamed)
|
|
11
11
|
* - useHead() support via beginHeadCollection / endHeadCollection
|
|
12
12
|
* - DSD polyfill injected at end of <body> after client-template merge
|
|
13
13
|
*/
|
|
14
|
-
export declare const ENTRY_SERVER_TEMPLATE = "// Server-side entry \u2014 AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app\nimport { readFileSync, existsSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { AsyncLocalStorage } from 'node:async_hooks'\nimport 'virtual:cer-components'\nimport routes from 'virtual:cer-routes'\nimport layouts from 'virtual:cer-layouts'\nimport plugins from 'virtual:cer-plugins'\nimport apiRoutes from 'virtual:cer-server-api'\nimport { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'\nimport { registerEntityMap, renderToStringWithJITCSSDSD, DSD_POLYFILL_SCRIPT } from '@jasonshimmy/custom-elements-runtime/ssr'\nimport entitiesJson from '@jasonshimmy/custom-elements-runtime/entities.json'\nimport { initRouter } from '@jasonshimmy/custom-elements-runtime/router'\nimport { beginHeadCollection, endHeadCollection, serializeHeadTags } from '@jasonshimmy/vite-plugin-cer-app/composables'\n\nregisterBuiltinComponents()\n\n// Pre-load the full HTML entity map so named entities like — decode\n// correctly during SSR. Without this the bundled runtime falls back to a\n// minimal set (<, >, & \u2026) and re-escapes everything else.\nregisterEntityMap(entitiesJson)\n\n// Run plugins once at server startup so their provide() values are available\n// to useInject() during every SSR/SSG render pass. Stored on globalThis so all\n// dynamically-imported page chunks share the same reference.\nconst _pluginProvides = new Map()\n;(globalThis).__cerPluginProvides = _pluginProvides\nconst _pluginsReady = (async () => {\n const _bootstrapRouter = initRouter({ routes })\n for (const plugin of plugins) {\n if (plugin && typeof plugin.setup === 'function') {\n await plugin.setup({\n router: _bootstrapRouter,\n provide: (key, value) => _pluginProvides.set(key, value),\n config: {},\n })\n }\n }\n})()\n\n// Async-local storage for request-scoped SSR loader data.\n// Using AsyncLocalStorage ensures concurrent SSR renders (e.g. SSG with\n// concurrency > 1) never see each other's data \u2014 each request's async chain\n// carries its own store value, so usePageData() is always race-condition-free.\nconst _cerDataStore = new AsyncLocalStorage()\n// Expose the store so the usePageData() composable can read it server-side.\n;(globalThis).__CER_DATA_STORE__ = _cerDataStore\n\n// Load the Vite-built client index.html (dist/client/index.html) so every SSR\n// response includes the client-side scripts needed for hydration and routing.\n// The server bundle lives at dist/server/server.js, so ../client resolves correctly.\nconst _clientTemplatePath = join(dirname(fileURLToPath(import.meta.url)), '../client/index.html')\nconst _clientTemplate = existsSync(_clientTemplatePath)\n ? readFileSync(_clientTemplatePath, 'utf-8')\n : null\n\n// Merge the SSR rendered body with the Vite client shell so the final page\n// contains both pre-rendered DSD content and the client bundle scripts.\nfunction _mergeWithClientTemplate(ssrHtml, clientTemplate) {\n const headTag = '<head>', headCloseTag = '</head>'\n const bodyTag = '<body>', bodyCloseTag = '</body>'\n const headStart = ssrHtml.indexOf(headTag)\n const headEnd = ssrHtml.indexOf(headCloseTag)\n const bodyStart = ssrHtml.indexOf(bodyTag)\n const bodyEnd = ssrHtml.lastIndexOf(bodyCloseTag)\n const ssrHead = headStart >= 0 && headEnd > headStart\n ? ssrHtml.slice(headStart + headTag.length, headEnd).trim() : ''\n const ssrBody = bodyStart >= 0 && bodyEnd > bodyStart\n ? ssrHtml.slice(bodyStart + bodyTag.length, bodyEnd).trim() : ssrHtml\n // Hoist only top-level <style id=...> elements (cer-ssr-jit, cer-ssr-global)\n // from the SSR body into the document <head>. Plain <style> blocks without\n // an id attribute belong to shadow DOM templates and must stay in place \u2014\n // hoisting them to <head> breaks shadow DOM style encapsulation (document\n // styles do not pierce shadow roots), which is the root cause of FOUC.\n const headParts = ssrHead ? [ssrHead] : []\n let ssrBodyContent = ssrBody\n let pos = 0\n while (pos < ssrBodyContent.length) {\n const styleOpen = ssrBodyContent.indexOf('<style id=', pos)\n if (styleOpen < 0) break\n const styleClose = ssrBodyContent.indexOf('</style>', styleOpen)\n if (styleClose < 0) break\n headParts.push(ssrBodyContent.slice(styleOpen, styleClose + 8))\n ssrBodyContent = ssrBodyContent.slice(0, styleOpen) + ssrBodyContent.slice(styleClose + 8)\n pos = styleOpen\n }\n ssrBodyContent = ssrBodyContent.trim()\n // Inject the pre-rendered layout+page as light DOM of the app mount element\n // so it is visible before JS boots, then the client router takes over.\n let merged = clientTemplate\n if (merged.includes('<cer-layout-view></cer-layout-view>')) {\n merged = merged.replace('<cer-layout-view></cer-layout-view>',\n '<cer-layout-view>' + ssrBodyContent + '</cer-layout-view>')\n } else if (merged.includes('<div id=\"app\"></div>')) {\n merged = merged.replace('<div id=\"app\"></div>',\n '<div id=\"app\">' + ssrBodyContent + '</div>')\n }\n const headAdditions = headParts.filter(Boolean).join('\\n')\n if (headAdditions) {\n // If SSR provides a <title>, replace the client template's <title> so the\n // SSR title wins (client template title is the fallback default).\n if (headAdditions.includes('<title>')) {\n merged = merged.replace(/<title>[^<]*<\\/title>/, '')\n }\n merged = merged.replace('</head>', headAdditions + '\\n</head>')\n }\n return merged\n}\n\n// Per-request async setup: initialize a fresh router, resolve the matched\n// route and layout, pre-load the page module, and call the data loader.\n// Loader data is scoped to the current AsyncLocalStorage context via enterWith()\n// so concurrent renders never share state.\nconst _prepareRequest = async (req) => {\n await _pluginsReady\n const router = initRouter({ routes, initialUrl: req.url ?? '/' })\n const current = router.getCurrent()\n const { route, params } = router.matchRoute(current.path)\n const layoutName = route?.meta?.layout ?? 'default'\n const layoutTag = layouts[layoutName]\n\n // Pre-load the page module so we can embed the component tag directly.\n // This avoids the async router-view (which injects content via script tags\n // and breaks Declarative Shadow DOM on initial parse).\n let pageVnode = { tag: 'div', props: {}, children: [] }\n let head\n if (route?.load) {\n try {\n const mod = await route.load()\n const pageTag = mod.default\n if (pageTag) {\n pageVnode = { tag: pageTag, props: { attrs: { ...params } }, children: [] }\n }\n if (typeof mod.loader === 'function') {\n const query = current.query ?? {}\n const data = await mod.loader({ params, query, req })\n if (data !== undefined && data !== null) {\n // enterWith() scopes the value to the current async context so\n // concurrent renders (SSG concurrency > 1) never share data.\n _cerDataStore.enterWith(data)\n head = `<script>window.__CER_DATA__ = ${JSON.stringify(data)}</script>`\n }\n }\n } catch {\n // Non-fatal: loader errors fall back to an empty page; client will refetch.\n }\n }\n\n const vnode = layoutTag\n ? { tag: layoutTag, props: {}, children: [pageVnode] }\n : pageVnode\n\n return { vnode, router, head }\n}\n\nexport const handler = async (req, res) => {\n await _cerDataStore.run(null, async () => {\n const { vnode, router, head } = await _prepareRequest(req)\n\n // Begin collecting useHead() calls made during the synchronous render pass.\n beginHeadCollection()\n\n // dsdPolyfill: false \u2014 we inject the polyfill manually after merging so it\n // lands at the end of <body>, not inside <cer-layout-view> light DOM where\n // scripts may not execute.\n const { htmlWithStyles } = renderToStringWithJITCSSDSD(vnode, {\n dsdPolyfill: false,\n router,\n })\n\n // Collect and serialize any useHead() calls from the rendered components.\n const headTags = serializeHeadTags(endHeadCollection())\n\n // Merge loader data script + useHead() tags into the document head.\n const headContent = [head, headTags].filter(Boolean).join('\\n')\n\n // Wrap the rendered body in a full HTML document and inject the head additions\n // (loader data script, useHead() tags, JIT styles). No polyfill in body yet.\n const ssrHtml = `<!DOCTYPE html><html><head>${headContent}</head><body>${htmlWithStyles}</body></html>`\n\n let finalHtml = _clientTemplate\n ? _mergeWithClientTemplate(ssrHtml, _clientTemplate)\n : ssrHtml\n\n // Inject DSD polyfill at end of <body>, outside <cer-layout-view>, so the\n // browser runs it after parsing the declarative shadow roots.\n finalHtml = finalHtml.includes('</body>')\n ? finalHtml.replace('</body>', DSD_POLYFILL_SCRIPT + '</body>')\n : finalHtml + DSD_POLYFILL_SCRIPT\n\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.end(finalHtml)\n })\n}\n\nexport { apiRoutes, plugins, layouts, routes }\nexport default handler\n";
|
|
14
|
+
export declare const ENTRY_SERVER_TEMPLATE = "// Server-side entry \u2014 AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app\nimport { readFileSync, existsSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { AsyncLocalStorage } from 'node:async_hooks'\nimport 'virtual:cer-components'\nimport routes from 'virtual:cer-routes'\nimport layouts from 'virtual:cer-layouts'\nimport plugins from 'virtual:cer-plugins'\nimport apiRoutes from 'virtual:cer-server-api'\nimport { runtimeConfig } from 'virtual:cer-app-config'\nimport { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'\nimport { registerEntityMap, renderToStreamWithJITCSSDSD, DSD_POLYFILL_SCRIPT } from '@jasonshimmy/custom-elements-runtime/ssr'\nimport entitiesJson from '@jasonshimmy/custom-elements-runtime/entities.json'\nimport { initRouter } from '@jasonshimmy/custom-elements-runtime/router'\nimport { beginHeadCollection, endHeadCollection, serializeHeadTags, initRuntimeConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'\n\nregisterBuiltinComponents()\ninitRuntimeConfig(runtimeConfig)\n\n// Pre-load the full HTML entity map so named entities like — decode\n// correctly during SSR. Without this the bundled runtime falls back to a\n// minimal set (<, >, & \u2026) and re-escapes everything else.\nregisterEntityMap(entitiesJson)\n\n// Run plugins once at server startup so their provide() values are available\n// to useInject() during every SSR/SSG render pass. Stored on globalThis so all\n// dynamically-imported page chunks share the same reference.\nconst _pluginProvides = new Map()\n;(globalThis).__cerPluginProvides = _pluginProvides\nconst _pluginsReady = (async () => {\n const _bootstrapRouter = initRouter({ routes })\n for (const plugin of plugins) {\n if (plugin && typeof plugin.setup === 'function') {\n await plugin.setup({\n router: _bootstrapRouter,\n provide: (key, value) => _pluginProvides.set(key, value),\n config: {},\n })\n }\n }\n})()\n\n// Async-local storage for request-scoped SSR loader data.\n// Using AsyncLocalStorage ensures concurrent SSR renders (e.g. SSG with\n// concurrency > 1) never see each other's data \u2014 each request's async chain\n// carries its own store value, so usePageData() is always race-condition-free.\nconst _cerDataStore = new AsyncLocalStorage()\n// Expose the store so the usePageData() composable can read it server-side.\n;(globalThis).__CER_DATA_STORE__ = _cerDataStore\n\n// Load the Vite-built client index.html (dist/client/index.html) so every SSR\n// response includes the client-side scripts needed for hydration and routing.\n// The server bundle lives at dist/server/server.js, so ../client resolves correctly.\nconst _clientTemplatePath = join(dirname(fileURLToPath(import.meta.url)), '../client/index.html')\nconst _clientTemplate = existsSync(_clientTemplatePath)\n ? readFileSync(_clientTemplatePath, 'utf-8')\n : null\n\n// Merge the SSR rendered body with the Vite client shell so the final page\n// contains both pre-rendered DSD content and the client bundle scripts.\nfunction _mergeWithClientTemplate(ssrHtml, clientTemplate) {\n const headTag = '<head>', headCloseTag = '</head>'\n const bodyTag = '<body>', bodyCloseTag = '</body>'\n const headStart = ssrHtml.indexOf(headTag)\n const headEnd = ssrHtml.indexOf(headCloseTag)\n const bodyStart = ssrHtml.indexOf(bodyTag)\n const bodyEnd = ssrHtml.lastIndexOf(bodyCloseTag)\n const ssrHead = headStart >= 0 && headEnd > headStart\n ? ssrHtml.slice(headStart + headTag.length, headEnd).trim() : ''\n const ssrBody = bodyStart >= 0 && bodyEnd > bodyStart\n ? ssrHtml.slice(bodyStart + bodyTag.length, bodyEnd).trim() : ssrHtml\n // Hoist only top-level <style id=...> elements (cer-ssr-jit, cer-ssr-global)\n // from the SSR body into the document <head>. Plain <style> blocks without\n // an id attribute belong to shadow DOM templates and must stay in place \u2014\n // hoisting them to <head> breaks shadow DOM style encapsulation (document\n // styles do not pierce shadow roots), which is the root cause of FOUC.\n const headParts = ssrHead ? [ssrHead] : []\n let ssrBodyContent = ssrBody\n let pos = 0\n while (pos < ssrBodyContent.length) {\n const styleOpen = ssrBodyContent.indexOf('<style id=', pos)\n if (styleOpen < 0) break\n const styleClose = ssrBodyContent.indexOf('</style>', styleOpen)\n if (styleClose < 0) break\n headParts.push(ssrBodyContent.slice(styleOpen, styleClose + 8))\n ssrBodyContent = ssrBodyContent.slice(0, styleOpen) + ssrBodyContent.slice(styleClose + 8)\n pos = styleOpen\n }\n ssrBodyContent = ssrBodyContent.trim()\n // Inject the pre-rendered layout+page as light DOM of the app mount element\n // so it is visible before JS boots, then the client router takes over.\n let merged = clientTemplate\n if (merged.includes('<cer-layout-view></cer-layout-view>')) {\n merged = merged.replace('<cer-layout-view></cer-layout-view>',\n '<cer-layout-view>' + ssrBodyContent + '</cer-layout-view>')\n } else if (merged.includes('<div id=\"app\"></div>')) {\n merged = merged.replace('<div id=\"app\"></div>',\n '<div id=\"app\">' + ssrBodyContent + '</div>')\n }\n const headAdditions = headParts.filter(Boolean).join('\\n')\n if (headAdditions) {\n // If SSR provides a <title>, replace the client template's <title> so the\n // SSR title wins (client template title is the fallback default).\n if (headAdditions.includes('<title>')) {\n merged = merged.replace(/<title>[^<]*<\\/title>/, '')\n }\n merged = merged.replace('</head>', headAdditions + '\\n</head>')\n }\n return merged\n}\n\n// Per-request async setup: initialize a fresh router, resolve the matched\n// route and layout, pre-load the page module, and call the data loader.\n// Loader data is scoped to the current AsyncLocalStorage context via enterWith()\n// so concurrent renders never share state.\nconst _prepareRequest = async (req) => {\n await _pluginsReady\n const router = initRouter({ routes, initialUrl: req.url ?? '/' })\n const current = router.getCurrent()\n const { route, params } = router.matchRoute(current.path)\n\n // Pre-load the page module so we can embed the component tag directly.\n // This avoids the async router-view (which injects content via script tags\n // and breaks Declarative Shadow DOM on initial parse).\n let pageVnode = { tag: 'div', props: {}, children: [] }\n let head\n if (route?.load) {\n try {\n const mod = await route.load()\n const pageTag = mod.default\n if (pageTag) {\n pageVnode = { tag: pageTag, props: { attrs: { ...params } }, children: [] }\n }\n if (typeof mod.loader === 'function') {\n const query = current.query ?? {}\n const data = await mod.loader({ params, query, req })\n if (data !== undefined && data !== null) {\n // enterWith() scopes the value to the current async context so\n // concurrent renders (SSG concurrency > 1) never share data.\n _cerDataStore.enterWith(data)\n head = `<script>window.__CER_DATA__ = ${JSON.stringify(data)}</script>`\n }\n }\n } catch {\n // Non-fatal: loader errors fall back to an empty page; client will refetch.\n }\n }\n\n // Resolve layout chain: nested layouts (meta.layoutChain) or single layout.\n const chain = route?.meta?.layoutChain\n ? route.meta.layoutChain\n : [route?.meta?.layout ?? 'default']\n\n // Wrap pageVnode in the layout chain 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\n return { vnode, router, head }\n}\n\nexport const handler = async (req, res) => {\n await _cerDataStore.run(null, async () => {\n const { vnode, router, head } = await _prepareRequest(req)\n\n // Begin collecting useHead() calls made during the synchronous render pass.\n // IMPORTANT: the stream's start() function runs synchronously on construction,\n // so ALL useHead() calls happen before the stream object is returned. We must\n // call endHeadCollection() immediately \u2014 before any await \u2014 to avoid a race\n // window where a concurrent request (e.g. SSG concurrency > 1) resets the\n // shared globalThis collector while this handler is suspended at an await.\n beginHeadCollection()\n\n // dsdPolyfill: false \u2014 we inject the polyfill manually after merging so it\n // lands at the end of <body>, not inside <cer-layout-view> light DOM where\n // scripts may not execute.\n // The first chunk from the stream is the full synchronous render. Subsequent\n // chunks are async component swap scripts streamed as they resolve.\n const stream = renderToStreamWithJITCSSDSD(vnode, { dsdPolyfill: false, router })\n\n // Collect head tags synchronously \u2014 all useHead() calls have already fired\n // inside the stream constructor's start() before it returned.\n const headTags = serializeHeadTags(endHeadCollection())\n\n const reader = stream.getReader()\n\n // Read the first (synchronous) chunk.\n const { value: firstChunk = '' } = await reader.read()\n\n // Merge loader data script + useHead() tags into the document head.\n const headContent = [head, headTags].filter(Boolean).join('\\n')\n\n // Wrap the rendered body in a full HTML document and inject the head additions\n // (loader data script, useHead() tags, JIT styles). No polyfill in body yet.\n const ssrHtml = `<!DOCTYPE html><html><head>${headContent}</head><body>${firstChunk}</body></html>`\n\n const merged = _clientTemplate\n ? _mergeWithClientTemplate(ssrHtml, _clientTemplate)\n : ssrHtml\n\n // Split at </body> so async swap scripts and the DSD polyfill can be streamed\n // in before the document is closed.\n const bodyCloseIdx = merged.lastIndexOf('</body>')\n const beforeBodyClose = bodyCloseIdx >= 0 ? merged.slice(0, bodyCloseIdx) : merged\n const fromBodyClose = bodyCloseIdx >= 0 ? merged.slice(bodyCloseIdx) : ''\n\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.setHeader('Transfer-Encoding', 'chunked')\n res.write(beforeBodyClose)\n\n // Stream async component swap scripts through as-is.\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n res.write(value)\n }\n\n // Inject DSD polyfill immediately before </body>, then close the document.\n res.end(DSD_POLYFILL_SCRIPT + fromBodyClose)\n })\n}\n\nexport { apiRoutes, plugins, layouts, routes }\nexport default handler\n";
|
|
15
15
|
//# sourceMappingURL=entry-server-template.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-server-template.d.ts","sourceRoot":"","sources":["../../src/runtime/entry-server-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,
|
|
1
|
+
{"version":3,"file":"entry-server-template.d.ts","sourceRoot":"","sources":["../../src/runtime/entry-server-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,6hVAmOjC,CAAA"}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Key features:
|
|
9
9
|
* - AsyncLocalStorage for race-condition-free concurrent renders (SSG concurrency > 1)
|
|
10
|
-
* - Declarative Shadow DOM via
|
|
10
|
+
* - Declarative Shadow DOM via renderToStreamWithJITCSSDSD (always on, streamed)
|
|
11
11
|
* - useHead() support via beginHeadCollection / endHeadCollection
|
|
12
12
|
* - DSD polyfill injected at end of <body> after client-template merge
|
|
13
13
|
*/
|
|
@@ -21,13 +21,15 @@ import routes from 'virtual:cer-routes'
|
|
|
21
21
|
import layouts from 'virtual:cer-layouts'
|
|
22
22
|
import plugins from 'virtual:cer-plugins'
|
|
23
23
|
import apiRoutes from 'virtual:cer-server-api'
|
|
24
|
+
import { runtimeConfig } from 'virtual:cer-app-config'
|
|
24
25
|
import { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'
|
|
25
|
-
import { registerEntityMap,
|
|
26
|
+
import { registerEntityMap, renderToStreamWithJITCSSDSD, DSD_POLYFILL_SCRIPT } from '@jasonshimmy/custom-elements-runtime/ssr'
|
|
26
27
|
import entitiesJson from '@jasonshimmy/custom-elements-runtime/entities.json'
|
|
27
28
|
import { initRouter } from '@jasonshimmy/custom-elements-runtime/router'
|
|
28
|
-
import { beginHeadCollection, endHeadCollection, serializeHeadTags } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
29
|
+
import { beginHeadCollection, endHeadCollection, serializeHeadTags, initRuntimeConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
29
30
|
|
|
30
31
|
registerBuiltinComponents()
|
|
32
|
+
initRuntimeConfig(runtimeConfig)
|
|
31
33
|
|
|
32
34
|
// Pre-load the full HTML entity map so named entities like — decode
|
|
33
35
|
// correctly during SSR. Without this the bundled runtime falls back to a
|
|
@@ -130,8 +132,6 @@ const _prepareRequest = async (req) => {
|
|
|
130
132
|
const router = initRouter({ routes, initialUrl: req.url ?? '/' })
|
|
131
133
|
const current = router.getCurrent()
|
|
132
134
|
const { route, params } = router.matchRoute(current.path)
|
|
133
|
-
const layoutName = route?.meta?.layout ?? 'default'
|
|
134
|
-
const layoutTag = layouts[layoutName]
|
|
135
135
|
|
|
136
136
|
// Pre-load the page module so we can embed the component tag directly.
|
|
137
137
|
// This avoids the async router-view (which injects content via script tags
|
|
@@ -160,9 +160,17 @@ const _prepareRequest = async (req) => {
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
// Resolve layout chain: nested layouts (meta.layoutChain) or single layout.
|
|
164
|
+
const chain = route?.meta?.layoutChain
|
|
165
|
+
? route.meta.layoutChain
|
|
166
|
+
: [route?.meta?.layout ?? 'default']
|
|
167
|
+
|
|
168
|
+
// Wrap pageVnode in the layout chain from innermost to outermost.
|
|
169
|
+
let vnode = pageVnode
|
|
170
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
171
|
+
const tag = layouts[chain[i]]
|
|
172
|
+
if (tag) vnode = { tag, props: {}, children: [vnode] }
|
|
173
|
+
}
|
|
166
174
|
|
|
167
175
|
return { vnode, router, head }
|
|
168
176
|
}
|
|
@@ -172,38 +180,59 @@ export const handler = async (req, res) => {
|
|
|
172
180
|
const { vnode, router, head } = await _prepareRequest(req)
|
|
173
181
|
|
|
174
182
|
// Begin collecting useHead() calls made during the synchronous render pass.
|
|
183
|
+
// IMPORTANT: the stream's start() function runs synchronously on construction,
|
|
184
|
+
// so ALL useHead() calls happen before the stream object is returned. We must
|
|
185
|
+
// call endHeadCollection() immediately — before any await — to avoid a race
|
|
186
|
+
// window where a concurrent request (e.g. SSG concurrency > 1) resets the
|
|
187
|
+
// shared globalThis collector while this handler is suspended at an await.
|
|
175
188
|
beginHeadCollection()
|
|
176
189
|
|
|
177
190
|
// dsdPolyfill: false — we inject the polyfill manually after merging so it
|
|
178
191
|
// lands at the end of <body>, not inside <cer-layout-view> light DOM where
|
|
179
192
|
// scripts may not execute.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
})
|
|
193
|
+
// The first chunk from the stream is the full synchronous render. Subsequent
|
|
194
|
+
// chunks are async component swap scripts streamed as they resolve.
|
|
195
|
+
const stream = renderToStreamWithJITCSSDSD(vnode, { dsdPolyfill: false, router })
|
|
184
196
|
|
|
185
|
-
// Collect
|
|
197
|
+
// Collect head tags synchronously — all useHead() calls have already fired
|
|
198
|
+
// inside the stream constructor's start() before it returned.
|
|
186
199
|
const headTags = serializeHeadTags(endHeadCollection())
|
|
187
200
|
|
|
201
|
+
const reader = stream.getReader()
|
|
202
|
+
|
|
203
|
+
// Read the first (synchronous) chunk.
|
|
204
|
+
const { value: firstChunk = '' } = await reader.read()
|
|
205
|
+
|
|
188
206
|
// Merge loader data script + useHead() tags into the document head.
|
|
189
207
|
const headContent = [head, headTags].filter(Boolean).join('\\n')
|
|
190
208
|
|
|
191
209
|
// Wrap the rendered body in a full HTML document and inject the head additions
|
|
192
210
|
// (loader data script, useHead() tags, JIT styles). No polyfill in body yet.
|
|
193
|
-
const ssrHtml = \`<!DOCTYPE html><html><head>\${headContent}</head><body>\${
|
|
211
|
+
const ssrHtml = \`<!DOCTYPE html><html><head>\${headContent}</head><body>\${firstChunk}</body></html>\`
|
|
194
212
|
|
|
195
|
-
|
|
213
|
+
const merged = _clientTemplate
|
|
196
214
|
? _mergeWithClientTemplate(ssrHtml, _clientTemplate)
|
|
197
215
|
: ssrHtml
|
|
198
216
|
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
217
|
+
// Split at </body> so async swap scripts and the DSD polyfill can be streamed
|
|
218
|
+
// in before the document is closed.
|
|
219
|
+
const bodyCloseIdx = merged.lastIndexOf('</body>')
|
|
220
|
+
const beforeBodyClose = bodyCloseIdx >= 0 ? merged.slice(0, bodyCloseIdx) : merged
|
|
221
|
+
const fromBodyClose = bodyCloseIdx >= 0 ? merged.slice(bodyCloseIdx) : ''
|
|
204
222
|
|
|
205
223
|
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
|
206
|
-
res.
|
|
224
|
+
res.setHeader('Transfer-Encoding', 'chunked')
|
|
225
|
+
res.write(beforeBodyClose)
|
|
226
|
+
|
|
227
|
+
// Stream async component swap scripts through as-is.
|
|
228
|
+
while (true) {
|
|
229
|
+
const { value, done } = await reader.read()
|
|
230
|
+
if (done) break
|
|
231
|
+
res.write(value)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Inject DSD polyfill immediately before </body>, then close the document.
|
|
235
|
+
res.end(DSD_POLYFILL_SCRIPT + fromBodyClose)
|
|
207
236
|
})
|
|
208
237
|
}
|
|
209
238
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-server-template.js","sourceRoot":"","sources":["../../src/runtime/entry-server-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG
|
|
1
|
+
{"version":3,"file":"entry-server-template.js","sourceRoot":"","sources":["../../src/runtime/entry-server-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmOpC,CAAA"}
|
package/dist/types/config.d.ts
CHANGED
|
@@ -14,6 +14,24 @@ export interface AutoImportsConfig {
|
|
|
14
14
|
directives?: boolean;
|
|
15
15
|
runtime?: boolean;
|
|
16
16
|
}
|
|
17
|
+
export interface RuntimePublicConfig {
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface RuntimeConfig {
|
|
21
|
+
/**
|
|
22
|
+
* Public runtime config — available on both server and client via
|
|
23
|
+
* `useRuntimeConfig().public`. Values are serialized into the virtual module
|
|
24
|
+
* at build time, so only use static/env-var values here.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* runtimeConfig: {
|
|
28
|
+
* public: {
|
|
29
|
+
* apiBase: process.env.VITE_API_BASE ?? 'https://api.example.com',
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
*/
|
|
33
|
+
public?: RuntimePublicConfig;
|
|
34
|
+
}
|
|
17
35
|
export interface CerAppConfig {
|
|
18
36
|
mode?: 'spa' | 'ssr' | 'ssg';
|
|
19
37
|
srcDir?: string;
|
|
@@ -22,6 +40,12 @@ export interface CerAppConfig {
|
|
|
22
40
|
jitCss?: JitCssConfig;
|
|
23
41
|
autoImports?: AutoImportsConfig;
|
|
24
42
|
port?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Runtime configuration accessible via `useRuntimeConfig()`.
|
|
45
|
+
* Only `public` values are exposed to the client; keep secrets
|
|
46
|
+
* out of `public`.
|
|
47
|
+
*/
|
|
48
|
+
runtimeConfig?: RuntimeConfig;
|
|
25
49
|
}
|
|
26
50
|
export declare function defineConfig(config: CerAppConfig): CerAppConfig;
|
|
27
51
|
//# sourceMappingURL=config.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6CAA6C,CAAA;AAE/E,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAA;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,GAAG,CAAC,EAAE,SAAS,CAAA;IACf,MAAM,CAAC,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,kBAAkB,CAAC,CAAA;IACxD,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,WAAW,CAAC,EAAE,iBAAiB,CAAA;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6CAA6C,CAAA;AAE/E,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,mBAAmB,CAAA;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAA;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,GAAG,CAAC,EAAE,SAAS,CAAA;IACf,MAAM,CAAC,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,kBAAkB,CAAC,CAAA;IACxD,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,WAAW,CAAC,EAAE,iBAAiB,CAAA;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAA;CAC9B;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAE/D"}
|
package/dist/types/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAwDA,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO,MAAM,CAAA;AACf,CAAC"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { CerAppConfig, SsgConfig, JitCssConfig, AutoImportsConfig } from './config.js';
|
|
1
|
+
export type { CerAppConfig, SsgConfig, JitCssConfig, AutoImportsConfig, RuntimeConfig, RuntimePublicConfig } from './config.js';
|
|
2
2
|
export { defineConfig } from './config.js';
|
|
3
3
|
export type { HydrateStrategy, SsgPathsContext, PageSsgConfig, PageMeta, PageLoaderContext, PageLoader } from './page.js';
|
|
4
4
|
export type { ApiRequest, ApiResponse, ApiHandler, ApiContext } from './api.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAC/H,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAAE,QAAQ,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AACzH,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAC/E,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACxD,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA"}
|
package/dist/types/page.d.ts
CHANGED
|
@@ -5,12 +5,29 @@ export interface SsgPathsContext {
|
|
|
5
5
|
}
|
|
6
6
|
export interface PageSsgConfig {
|
|
7
7
|
paths?: () => Promise<SsgPathsContext[]> | SsgPathsContext[];
|
|
8
|
+
/**
|
|
9
|
+
* Seconds before a cached SSR response is stale and should be re-rendered.
|
|
10
|
+
* Enables Incremental Static Regeneration (ISR) in the preview server and
|
|
11
|
+
* any production adapter that reads `meta.ssg.revalidate`.
|
|
12
|
+
*
|
|
13
|
+
* @example export const meta = { ssg: { revalidate: 60 } }
|
|
14
|
+
*/
|
|
15
|
+
revalidate?: number;
|
|
8
16
|
}
|
|
9
17
|
export interface PageMeta {
|
|
10
18
|
layout?: string;
|
|
11
19
|
middleware?: string[];
|
|
12
20
|
hydrate?: HydrateStrategy;
|
|
13
21
|
ssg?: PageSsgConfig;
|
|
22
|
+
/**
|
|
23
|
+
* CSS transition name applied to the page during route changes.
|
|
24
|
+
* Set to `true` to use the default 'page' transition name.
|
|
25
|
+
* The framework adds/removes `[data-transition="<name>"]` on the root element
|
|
26
|
+
* so you can target it with CSS animations.
|
|
27
|
+
*
|
|
28
|
+
* @example export const meta = { transition: 'fade' }
|
|
29
|
+
*/
|
|
30
|
+
transition?: string | boolean;
|
|
14
31
|
}
|
|
15
32
|
export interface PageLoaderContext<P extends Record<string, string> = Record<string, string>> {
|
|
16
33
|
params: P;
|
package/dist/types/page.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/types/page.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAEhD,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAA;AAElE,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,GAAG,eAAe,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/types/page.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAEhD,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAA;AAElE,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,GAAG,eAAe,EAAE,CAAA;IAC5D;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,OAAO,CAAC,EAAE,eAAe,CAAA;IACzB,GAAG,CAAC,EAAE,aAAa,CAAA;IACnB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAC9B;AAED,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAC1F,MAAM,EAAE,CAAC,CAAA;IACT,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,GAAG,EAAE,eAAe,CAAA;CACrB;AAED,MAAM,MAAM,UAAU,CACpB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACzD,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IACzB,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA"}
|
package/docs/composables.md
CHANGED
|
@@ -141,3 +141,39 @@ import { useInject } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
> **Note:** Prefer `useInject` over the raw `inject()` primitive whenever reading plugin-provided values. Raw `inject()` works in SPA mode but returns `undefined` in SSR and SSG because the server renders components without `<cer-layout-view>`'s provide context.
|
|
144
|
+
|
|
145
|
+
### `useRuntimeConfig()`
|
|
146
|
+
|
|
147
|
+
Returns the `public` runtime configuration object set in `cer.config.ts` under `runtimeConfig.public`. Available in all rendering modes (SPA, SSR, SSG) and on both server and client.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// cer.config.ts
|
|
151
|
+
export default defineConfig({
|
|
152
|
+
runtimeConfig: {
|
|
153
|
+
public: {
|
|
154
|
+
apiBase: process.env.VITE_API_BASE ?? '/api',
|
|
155
|
+
featureFlags: { darkMode: true },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// app/pages/index.ts — auto-imported, no import statement needed
|
|
163
|
+
component('page-index', () => {
|
|
164
|
+
const { public: cfg } = useRuntimeConfig()
|
|
165
|
+
// cfg.apiBase → '/api'
|
|
166
|
+
|
|
167
|
+
return html`<p>API base: ${cfg.apiBase}</p>`
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The config is initialized at app boot (both client and server) by calling `initRuntimeConfig(runtimeConfig)` with the value from `virtual:cer-app-config`. You only need `useRuntimeConfig()` to read it.
|
|
172
|
+
|
|
173
|
+
**Only use `runtimeConfig.public` for values safe to expose to the browser.** Secrets, tokens, and private keys must stay in server-only code (loaders, API handlers, server middleware).
|
|
174
|
+
|
|
175
|
+
If you need it outside auto-imported directories:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { useRuntimeConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
179
|
+
```
|
package/docs/configuration.md
CHANGED
|
@@ -198,10 +198,60 @@ When `directives: true`, the following are injected if used and not already impo
|
|
|
198
198
|
import { when, each, match, anchorBlock } from '@jasonshimmy/custom-elements-runtime/directives'
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
+
The following framework composables are **always** auto-imported when used, regardless of the `runtime` flag — they come from the plugin package:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { useHead, usePageData, useInject, useRuntimeConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
205
|
+
```
|
|
206
|
+
|
|
201
207
|
Set any flag to `false` to opt out and manage imports manually.
|
|
202
208
|
|
|
203
209
|
---
|
|
204
210
|
|
|
211
|
+
## `runtimeConfig` options
|
|
212
|
+
|
|
213
|
+
Expose typed, centralized public configuration to both server and client code via `useRuntimeConfig()`.
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
export default defineConfig({
|
|
217
|
+
runtimeConfig: {
|
|
218
|
+
public: {
|
|
219
|
+
apiBase: process.env.VITE_API_BASE ?? 'https://api.example.com',
|
|
220
|
+
appVersion: '1.0.0',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### `runtimeConfig.public`
|
|
227
|
+
|
|
228
|
+
**Type:** `Record<string, unknown>`
|
|
229
|
+
**Default:** `{}`
|
|
230
|
+
|
|
231
|
+
Values placed here are serialized into `virtual:cer-app-config` at build time and accessible on both server and client via `useRuntimeConfig().public`.
|
|
232
|
+
|
|
233
|
+
> **Security:** Only put values here that are safe to expose to the browser. Do not put secrets, tokens, or private keys in `public`. Those should be read directly from `process.env` inside server-only code (loaders, server middleware, API handlers).
|
|
234
|
+
|
|
235
|
+
> **Serialization:** Values must be JSON-serializable (strings, numbers, booleans, plain objects, arrays). Functions, class instances, `undefined`, and circular references are not supported and will be lost or throw during the build.
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
// Any page, layout, component, or composable
|
|
239
|
+
component('page-index', () => {
|
|
240
|
+
const config = useRuntimeConfig()
|
|
241
|
+
// config.public.apiBase → 'https://api.example.com'
|
|
242
|
+
|
|
243
|
+
return html`<p>API: ${config.public.apiBase}</p>`
|
|
244
|
+
})
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**TypeScript:** Import `RuntimePublicConfig` to type your public config if needed:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import type { RuntimePublicConfig } from '@jasonshimmy/vite-plugin-cer-app/types'
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
205
255
|
## Passing config to the Vite plugin directly
|
|
206
256
|
|
|
207
257
|
When using `vite.config.ts` instead of (or alongside) `cer.config.ts`:
|
|
@@ -234,5 +284,7 @@ import type {
|
|
|
234
284
|
SsgConfig,
|
|
235
285
|
JitCssConfig,
|
|
236
286
|
AutoImportsConfig,
|
|
287
|
+
RuntimeConfig,
|
|
288
|
+
RuntimePublicConfig,
|
|
237
289
|
} from '@jasonshimmy/vite-plugin-cer-app/types'
|
|
238
290
|
```
|
package/docs/layouts.md
CHANGED
|
@@ -110,3 +110,85 @@ import layouts from 'virtual:cer-layouts'
|
|
|
110
110
|
## Layout switching and DOM preservation
|
|
111
111
|
|
|
112
112
|
When navigating between pages with different layouts, the framework uses `<cer-keep-alive>` to preserve the layout DOM and avoid unnecessary teardown/remount cycles. This means transitions between pages sharing the same layout are smooth with no layout flash.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 🪆 Nested layouts
|
|
117
|
+
|
|
118
|
+
Place a `_layout.ts` file inside any page subdirectory to add an inner layout that wraps pages in that subtree. The outer layout (from `meta.layout` or the default) wraps the inner layout, which wraps `<router-view>`.
|
|
119
|
+
|
|
120
|
+
### File convention
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
app/
|
|
124
|
+
layouts/
|
|
125
|
+
default.ts # outer layout — full shell (header, footer)
|
|
126
|
+
minimal.ts # outer layout — bare minimum
|
|
127
|
+
sidebar.ts # inner layout — adds a sidebar panel
|
|
128
|
+
pages/
|
|
129
|
+
index.ts # uses 'default' layout only
|
|
130
|
+
admin/
|
|
131
|
+
_layout.ts # ← inner layout override for all /admin/* pages
|
|
132
|
+
index.ts # layout chain: ['default', 'sidebar']
|
|
133
|
+
users.ts # layout chain: ['default', 'sidebar']
|
|
134
|
+
settings.ts # layout chain: ['default', 'sidebar']
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `_layout.ts` syntax
|
|
138
|
+
|
|
139
|
+
Export the inner layout name as a default string:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// app/pages/admin/_layout.ts
|
|
143
|
+
export default 'sidebar'
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The value must match a filename (without extension) in `app/layouts/`.
|
|
147
|
+
|
|
148
|
+
### Rendered structure
|
|
149
|
+
|
|
150
|
+
For a page at `app/pages/admin/users.ts` with the above setup:
|
|
151
|
+
|
|
152
|
+
```html
|
|
153
|
+
<layout-default>
|
|
154
|
+
<layout-sidebar>
|
|
155
|
+
<router-view></router-view>
|
|
156
|
+
</layout-sidebar>
|
|
157
|
+
</layout-default>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Each layout receives the inner content via `<slot>`.
|
|
161
|
+
|
|
162
|
+
### Overriding the outer layout
|
|
163
|
+
|
|
164
|
+
If a page in a nested subtree needs a different outer layout, declare it in `meta.layout` as usual:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
// app/pages/admin/login.ts
|
|
168
|
+
export const meta = {
|
|
169
|
+
layout: 'minimal', // overrides outer; chain = ['minimal', 'sidebar']
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Multiple nesting levels
|
|
174
|
+
|
|
175
|
+
Nesting is resolved recursively. Each ancestor directory that contains a `_layout.ts` contributes one level to the chain:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
app/pages/
|
|
179
|
+
admin/
|
|
180
|
+
_layout.ts → 'sidebar'
|
|
181
|
+
settings/
|
|
182
|
+
_layout.ts → 'settings-tabs'
|
|
183
|
+
profile.ts # chain: ['default', 'sidebar', 'settings-tabs']
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### `meta.layoutChain` in routes
|
|
187
|
+
|
|
188
|
+
The framework emits the resolved chain as `meta.layoutChain` on the route object at build time. You can read it at runtime:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import routes from 'virtual:cer-routes'
|
|
192
|
+
const adminUsers = routes.find(r => r.path === '/admin/users')
|
|
193
|
+
// adminUsers.meta.layoutChain → ['default', 'sidebar']
|
|
194
|
+
```
|
package/docs/rendering-modes.md
CHANGED
|
@@ -63,9 +63,9 @@ The server renders HTML for each request. Uses Declarative Shadow DOM (DSD) to e
|
|
|
63
63
|
3. API route handlers run if the URL matches `/api/`
|
|
64
64
|
4. For HTML requests, the router matches the URL to a page
|
|
65
65
|
5. The page's `loader` is called (if present)
|
|
66
|
-
6. The component tree is rendered to HTML with Declarative Shadow DOM via `
|
|
67
|
-
7. `useHead()` calls are collected and injected before `</head>`
|
|
68
|
-
8. The rendered HTML is merged with the Vite client bundle shell and
|
|
66
|
+
6. The component tree is rendered to HTML with Declarative Shadow DOM via `renderToStreamWithJITCSSDSD`; the synchronous first chunk is flushed immediately, then async component swap scripts follow as they resolve
|
|
67
|
+
7. `useHead()` calls are collected from the synchronous render and injected before `</head>`
|
|
68
|
+
8. The rendered HTML is merged with the Vite client bundle shell and streamed as a chunked response
|
|
69
69
|
|
|
70
70
|
### Build output
|
|
71
71
|
|
|
@@ -223,19 +223,60 @@ Any CDN or static host. Upload the entire `dist/` directory (excluding `dist/ser
|
|
|
223
223
|
|
|
224
224
|
---
|
|
225
225
|
|
|
226
|
+
## ISR — Incremental Static Regeneration
|
|
227
|
+
|
|
228
|
+
ISR is a per-route cache layer in the SSR preview server. Pages with `meta.ssg.revalidate` set are rendered once, cached, and re-rendered in the background when the TTL expires (stale-while-revalidate).
|
|
229
|
+
|
|
230
|
+
### How it works
|
|
231
|
+
|
|
232
|
+
1. **First request (HIT after fresh render):** Cache miss — render via SSR, store in memory cache with TTL, then serve from the newly-populated cache. `X-Cache: HIT` is set.
|
|
233
|
+
2. **Within TTL (HIT):** Serve directly from cache. `X-Cache: HIT` header is set.
|
|
234
|
+
3. **After TTL expires (STALE):** Serve the stale cached HTML immediately with `X-Cache: STALE`. Kick off a background re-render. When the re-render completes, update the cache.
|
|
235
|
+
4. **While revalidating:** Continue serving stale HTML to new requests.
|
|
236
|
+
|
|
237
|
+
### Configuration
|
|
238
|
+
|
|
239
|
+
Add `revalidate` (seconds) to `meta.ssg` in any page:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
// app/pages/blog/[slug].ts
|
|
243
|
+
export const meta = {
|
|
244
|
+
ssg: {
|
|
245
|
+
revalidate: 60, // cache for 60 s; re-render in background after expiry
|
|
246
|
+
paths: async () => {
|
|
247
|
+
const posts = await fetchPosts()
|
|
248
|
+
return posts.map(p => ({ params: { slug: p.slug } }))
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Use cases
|
|
255
|
+
|
|
256
|
+
| TTL | Use case |
|
|
257
|
+
|---|---|
|
|
258
|
+
| `revalidate: 0` | TTL expires immediately — first request is HIT; every subsequent request is STALE with a background re-render |
|
|
259
|
+
| `revalidate: 60` | News articles, dashboards |
|
|
260
|
+
| `revalidate: 3600` | Product pages, documentation |
|
|
261
|
+
| `revalidate: 86400` | Marketing pages, rarely-changing content |
|
|
262
|
+
|
|
263
|
+
### Availability
|
|
264
|
+
|
|
265
|
+
ISR is currently active in the built-in **preview server** (`cer-app preview`). When integrating the server bundle into Express / Hono / Fastify in production, implement the same stale-while-revalidate pattern using `route.meta?.ssg?.revalidate` from the exported `routes` array.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
226
269
|
## Comparing modes
|
|
227
270
|
|
|
228
|
-
| Feature | SPA | SSR | SSG |
|
|
229
|
-
|
|
230
|
-
| Initial HTML | Empty shell | Full HTML | Full HTML |
|
|
231
|
-
| SEO | Poor | Excellent | Excellent |
|
|
232
|
-
| TTFB | Fast | Depends on server | Very fast (CDN) |
|
|
233
|
-
| Server required | No | Yes | No |
|
|
234
|
-
| Data freshness | Real-time | Real-time | Build-time |
|
|
235
|
-
| Dynamic routes | Yes | Yes | Requires `ssg.paths` |
|
|
236
|
-
| API routes | Separate deploy | Same process | Separate deploy |
|
|
237
|
-
| `useHead()` SSR injection | No | Yes | Yes |
|
|
238
|
-
| Streaming | No | No | No |
|
|
271
|
+
| Feature | SPA | SSR | SSG | ISR |
|
|
272
|
+
|---|---|---|---|---|
|
|
273
|
+
| Initial HTML | Empty shell | Full HTML | Full HTML | Full HTML |
|
|
274
|
+
| SEO | Poor | Excellent | Excellent | Excellent |
|
|
275
|
+
| TTFB | Fast | Depends on server | Very fast (CDN) | Very fast after first render |
|
|
276
|
+
| Server required | No | Yes | No | Yes |
|
|
277
|
+
| Data freshness | Real-time | Real-time | Build-time | Configurable TTL |
|
|
278
|
+
| Dynamic routes | Yes | Yes | Requires `ssg.paths` | Yes |
|
|
279
|
+
| API routes | Separate deploy | Same process | Separate deploy | Same process |
|
|
239
280
|
|
|
240
281
|
---
|
|
241
282
|
|