@jasonshimmy/vite-plugin-cer-app 0.18.2 → 0.19.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/commits.txt +2 -1
  3. package/dist/cli/create/templates/spa/package.json.tpl +1 -1
  4. package/dist/cli/create/templates/ssg/package.json.tpl +1 -1
  5. package/dist/cli/create/templates/ssr/package.json.tpl +1 -1
  6. package/dist/plugin/index.d.ts.map +1 -1
  7. package/dist/plugin/index.js +14 -0
  8. package/dist/plugin/index.js.map +1 -1
  9. package/dist/runtime/composables/use-auth.d.ts +6 -0
  10. package/dist/runtime/composables/use-auth.d.ts.map +1 -1
  11. package/dist/runtime/composables/use-auth.js.map +1 -1
  12. package/dist/runtime/composables/use-cookie.d.ts +2 -0
  13. package/dist/runtime/composables/use-cookie.d.ts.map +1 -1
  14. package/dist/runtime/composables/use-cookie.js.map +1 -1
  15. package/dist/runtime/composables/use-fetch.d.ts +1 -0
  16. package/dist/runtime/composables/use-fetch.d.ts.map +1 -1
  17. package/dist/runtime/composables/use-fetch.js.map +1 -1
  18. package/dist/runtime/composables/use-head.d.ts +4 -0
  19. package/dist/runtime/composables/use-head.d.ts.map +1 -1
  20. package/dist/runtime/composables/use-head.js.map +1 -1
  21. package/dist/runtime/composables/use-locale.d.ts +1 -0
  22. package/dist/runtime/composables/use-locale.d.ts.map +1 -1
  23. package/dist/runtime/composables/use-locale.js.map +1 -1
  24. package/dist/runtime/composables/use-runtime-config.d.ts +3 -0
  25. package/dist/runtime/composables/use-runtime-config.d.ts.map +1 -1
  26. package/dist/runtime/composables/use-runtime-config.js +17 -4
  27. package/dist/runtime/composables/use-runtime-config.js.map +1 -1
  28. package/dist/runtime/composables/use-seo-meta.d.ts +5 -0
  29. package/dist/runtime/composables/use-seo-meta.d.ts.map +1 -1
  30. package/dist/runtime/composables/use-seo-meta.js.map +1 -1
  31. package/dist/runtime/composables/use-session.d.ts +5 -0
  32. package/dist/runtime/composables/use-session.d.ts.map +1 -1
  33. package/dist/runtime/composables/use-session.js.map +1 -1
  34. package/dist/runtime/entry-server-template.d.ts +1 -1
  35. package/dist/runtime/entry-server-template.d.ts.map +1 -1
  36. package/dist/runtime/entry-server-template.js +21 -1
  37. package/dist/runtime/entry-server-template.js.map +1 -1
  38. package/dist/runtime/isr-handler.d.ts +2 -0
  39. package/dist/runtime/isr-handler.d.ts.map +1 -1
  40. package/dist/runtime/isr-handler.js.map +1 -1
  41. package/dist/types/api.d.ts +21 -0
  42. package/dist/types/api.d.ts.map +1 -1
  43. package/dist/types/config.d.ts +120 -0
  44. package/dist/types/config.d.ts.map +1 -1
  45. package/dist/types/config.js +19 -0
  46. package/dist/types/config.js.map +1 -1
  47. package/dist/types/index.d.ts +1 -1
  48. package/dist/types/index.d.ts.map +1 -1
  49. package/dist/types/middleware.d.ts +16 -0
  50. package/dist/types/middleware.d.ts.map +1 -1
  51. package/dist/types/page.d.ts +56 -0
  52. package/dist/types/page.d.ts.map +1 -1
  53. package/dist/types/plugin.d.ts +21 -0
  54. package/dist/types/plugin.d.ts.map +1 -1
  55. package/docs/authentication.md +18 -0
  56. package/docs/configuration.md +126 -1
  57. package/e2e/cypress/e2e/observability.cy.ts +77 -0
  58. package/e2e/kitchen-sink/app/pages/observability-test.ts +25 -0
  59. package/e2e/kitchen-sink/cer.config.ts +14 -0
  60. package/package.json +1 -1
  61. package/src/__tests__/plugin/entry-server-template.test.ts +50 -0
  62. package/src/__tests__/runtime/use-runtime-config.test.ts +40 -1
  63. package/src/cli/create/templates/spa/package.json.tpl +1 -1
  64. package/src/cli/create/templates/ssg/package.json.tpl +1 -1
  65. package/src/cli/create/templates/ssr/package.json.tpl +1 -1
  66. package/src/plugin/index.ts +13 -0
  67. package/src/runtime/composables/use-auth.ts +6 -0
  68. package/src/runtime/composables/use-cookie.ts +2 -0
  69. package/src/runtime/composables/use-fetch.ts +1 -0
  70. package/src/runtime/composables/use-head.ts +4 -0
  71. package/src/runtime/composables/use-locale.ts +1 -0
  72. package/src/runtime/composables/use-runtime-config.ts +23 -3
  73. package/src/runtime/composables/use-seo-meta.ts +5 -0
  74. package/src/runtime/composables/use-session.ts +5 -0
  75. package/src/runtime/entry-server-template.ts +21 -1
  76. package/src/runtime/isr-handler.ts +2 -0
  77. package/src/types/api.ts +21 -0
  78. package/src/types/config.ts +126 -1
  79. package/src/types/index.ts +1 -1
  80. package/src/types/middleware.ts +16 -0
  81. package/src/types/page.ts +58 -2
  82. package/src/types/plugin.ts +21 -0
  83. package/docs/plan-production-hardening.md +0 -1010
@@ -11,5 +11,5 @@
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 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 serverMiddleware from 'virtual:cer-server-middleware'\nimport { runtimeConfig, _runtimePrivateDefaults, _authSessionKey } 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, resolvePrivateConfig, useSession } from '@jasonshimmy/vite-plugin-cer-app/composables'\nimport { errorTag } from 'virtual:cer-error'\nimport { createIsrHandler } from '@jasonshimmy/vite-plugin-cer-app/isr'\n\nregisterBuiltinComponents()\n\n// Resolve private config from environment variables at server startup.\n// Each key declared in runtimeConfig.private is looked up in process.env,\n// first as-is, then as ALL_CAPS. The declared default is used as fallback.\ninitRuntimeConfig({ ...runtimeConfig, private: resolvePrivateConfig(_runtimePrivateDefaults ?? {}) })\n\n// Pre-load the full HTML entity map so named entities like &mdash; decode\n// correctly during SSR. Without this the bundled runtime falls back to a\n// minimal set (&lt;, &gt;, &amp; \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// Async-local storage for request-scoped req/res access.\n// Allows isomorphic composables (e.g. useCookie) to read/write HTTP headers\n// without prop-drilling the request context through the component tree.\nconst _cerReqStore = new AsyncLocalStorage()\n;(globalThis).__CER_REQ_STORE__ = _cerReqStore\n\n// Async-local storage for the authenticated user resolved before each render.\n// useAuth() reads this store server-side to return the current user synchronously.\nconst _cerAuthStore = new AsyncLocalStorage()\n;(globalThis).__CER_AUTH_STORE__ = _cerAuthStore\n\n// Async-local storage for per-request useFetch() data.\n// useFetch() writes fetched results here; the handler serialises the map into\n// window.__CER_FETCH_DATA__ for client-side hydration.\nconst _cerFetchStore = new AsyncLocalStorage()\n;(globalThis).__CER_FETCH_STORE__ = _cerFetchStore\n\n// Async-local storage for the current route info (path, params, query, meta).\n// useRoute() reads this on the server so layouts and components can access\n// route metadata without prop-drilling.\nconst _cerRouteStore = new AsyncLocalStorage()\n;(globalThis).__CER_ROUTE_STORE__ = _cerRouteStore\n\n// Async-local storage for per-request useState() reactive state.\n// Each request gets a fresh Map so concurrent SSR/SSG renders never share\n// reactive state set by different pages or loaders.\nconst _cerStateStore = new AsyncLocalStorage()\n;(globalThis).__CER_STATE_STORE__ = _cerStateStore\n\n// Runs fn inside the per-request AsyncLocalStorage context so that isomorphic\n// composables (useCookie, useSession, etc.) can access req/res without prop-drilling.\n// Call this for every API handler invocation \u2014 not just SSR renders.\nexport function runWithRequestContext(req, res, fn) {\n return _cerReqStore.run({ req, res }, fn)\n}\n\n// Runs the server/middleware/ chain for a request.\n// Returns false if a middleware short-circuited the response; true to continue.\n// Exported so Netlify / Vercel / Cloudflare bridges can also call it for API routes.\n// P1-2: Middleware may throw { status: 401 } (or any numeric .status) to produce\n// a non-500 response \u2014 the status is extracted and forwarded to res.statusCode.\nexport async function runServerMiddleware(req, res) {\n for (const { handler: mw } of (serverMiddleware ?? [])) {\n if (typeof mw !== 'function') continue\n let calledNext = false\n try {\n await new Promise((resolve, reject) => {\n Promise.resolve(mw(req, res, (err) => {\n if (err) reject(err)\n else { calledNext = true; resolve() }\n })).catch(reject)\n })\n } catch (err) {\n if (!res.writableEnded) {\n const statusCode = (typeof err === 'object' && err !== null && 'status' in err && typeof err.status === 'number')\n ? (isNaN(err.status) ? 500 : err.status)\n : 500\n res.statusCode = statusCode\n res.end('Internal Server Error')\n }\n return false\n }\n if (res.writableEnded || !calledNext) return false\n }\n return true\n}\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.\n//\n// Cloudflare Workers (and other runtimes without node:fs) can inject the\n// template before this module loads by setting globalThis.__CER_CLIENT_TEMPLATE__.\n// The Cloudflare adapter inlines dist/client/index.html as a string constant in\n// _worker.js and sets the global before dynamically importing the server bundle.\nlet _clientTemplate = (globalThis).__CER_CLIENT_TEMPLATE__ ?? null\nif (!_clientTemplate) {\n try {\n const _clientTemplatePath = join(dirname(fileURLToPath(import.meta.url)), '../client/index.html')\n _clientTemplate = existsSync(_clientTemplatePath)\n ? readFileSync(_clientTemplatePath, 'utf-8')\n : null\n } catch {\n // node:fs not available in this runtime \u2014 Cloudflare adapter must set\n // globalThis.__CER_CLIENT_TEMPLATE__ before importing this bundle.\n }\n}\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 // Store the current route info so useRoute() can read it synchronously\n // from any layout or component during this render pass.\n _cerRouteStore.enterWith({\n path: current.path,\n params,\n query: current.query ?? {},\n meta: route?.meta ?? null,\n })\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 // P2-2: Route-level error tag (from co-located .error.ts or _error.ts).\n // Preferred over the global errorTag when rendering loader errors for this route.\n const routeErrorTag = mod.errorTag ?? null\n\n // P1-1: Synthetic 404 catch-all \u2014 no page component registered.\n if (!pageTag) {\n const notFoundErrorTag = routeErrorTag ?? errorTag\n const notFoundVnode = notFoundErrorTag\n ? { tag: notFoundErrorTag, props: { attrs: { error: 'Not Found', status: '404' } }, children: [] }\n : { tag: 'div', props: {}, children: [] }\n return { vnode: notFoundVnode, router, head: undefined, status: 404 }\n }\n\n // Run the loader before creating the page vnode so we can pass its\n // primitive return values as HTML attributes. useProps() in the page\n // component reads element attributes, so merging loader data here makes\n // both useProps() and usePageData() work in SSR / SSG.\n let loaderAttrs = {}\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 // Expose primitive loader values as element attributes so useProps()\n // can read them. Complex objects are only accessible via usePageData().\n loaderAttrs = Object.fromEntries(\n Object.entries(data).filter(([, v]) => v !== null && v !== undefined && typeof v !== 'object' && typeof v !== 'function')\n )\n }\n }\n\n pageVnode = { tag: pageTag, props: { attrs: { ...params, ...loaderAttrs } }, children: [] }\n } catch (err) {\n // Loader threw \u2014 render the error page server-side if app/error.ts exists.\n const status = (err && typeof err === 'object' && 'status' in err && typeof err.status === 'number')\n ? err.status : 500\n const message = (err instanceof Error) ? err.message : String(err)\n // P2-2: Prefer the route-level errorTag over the global one.\n // routeErrorTag is not in scope here; use route?.meta?.errorTag from the matched route.\n const effectiveErrorTag = route?.meta?.errorTag ?? errorTag\n if (!effectiveErrorTag) {\n console.error('[cer-app] Loader error (no app/error.ts defined):', err)\n }\n const errVnode = effectiveErrorTag\n ? { tag: effectiveErrorTag, props: { attrs: { error: message, status: String(status) } }, children: [] }\n : { tag: 'div', props: {}, children: [] }\n return { vnode: errVnode, router, head: undefined, status }\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 // If the request matched a catch-all route (user-defined 404.ts or [...all].ts),\n // return HTTP 404 so browsers and crawlers treat it as a not-found response.\n const isCatchAll = route?.path === '/:all*'\n return { vnode, router, head, status: isCatchAll ? 404 : null }\n}\n\nexport const handler = async (req, res) => {\n await _cerStateStore.run(new Map(), async () => {\n await _cerReqStore.run({ req, res }, async () => {\n await _cerDataStore.run(null, async () => {\n // Fresh per-request fetch map \u2014 populated by useFetch() calls inside loaders.\n const _fetchMap = new Map()\n await _cerFetchStore.run(_fetchMap, async () => {\n // Pre-resolve the authenticated user so useAuth() works synchronously during rendering.\n let _authUser = null\n if (_authSessionKey) {\n try { _authUser = await useSession({ name: _authSessionKey }).get() } catch { /* no session secret */ }\n }\n await _cerAuthStore.run(_authUser, async () => {\n const { vnode, router, head, status } = await _prepareRequest(req)\n if (status != null) res.statusCode = status\n\n let _headCollectionOpen = false\n try {\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 _headCollectionOpen = true\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 _headCollectionOpen = false\n\n const reader = stream.getReader()\n\n // Read the first (synchronous) chunk \u2014 rejects if the sync render failed.\n const { value: firstChunk = '' } = await reader.read()\n\n // Serialise useFetch() results collected during loader execution.\n const _fetchObj = Object.fromEntries(_fetchMap)\n const _fetchScript = Object.keys(_fetchObj).length > 0\n ? `<script>window.__CER_FETCH_DATA__ = ${JSON.stringify(_fetchObj)}</script>`\n : ''\n\n // Serialise the auth user for client-side hydration via useAuth().\n const _authScript = _authUser\n ? `<script>window.__CER_AUTH_USER__ = ${JSON.stringify(_authUser)}</script>`\n : ''\n\n // Serialise useState() values for client-side hydration.\n // The state Map is populated by loader calls (in _prepareRequest) and by component\n // render functions (during renderToStreamWithJITCSSDSD above). Both run before this\n // point. Injected as window.__CER_STATE_INIT__ so the client useState() can\n // pre-populate its singleton Map on first use \u2014 no flash to default values.\n const _stateMap = _cerStateStore.getStore()\n let _stateScript = ''\n if (_stateMap && _stateMap.size > 0) {\n const _stateObj = {}\n for (const [k, v] of _stateMap) { _stateObj[k] = v.value }\n _stateScript = `<script>window.__CER_STATE_INIT__ = ${JSON.stringify(_stateObj)}</script>`\n }\n\n // Merge loader data script + useHead() tags + fetch/auth/state hydration scripts.\n const headContent = [head, headTags, _fetchScript, _authScript, _stateScript].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 } catch (_renderErr) {\n // Ensure the head collector is never left open on error.\n if (_headCollectionOpen) { try { endHeadCollection() } catch { /* ignore */ } }\n // If headers have not been flushed yet we can still send a proper 500 page.\n // If writing has already started we can only close the connection cleanly.\n if (!res.headersSent) {\n res.statusCode = 500\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.end('<!DOCTYPE html><html><head></head><body><h1>500 Internal Server Error</h1><p>An unexpected error occurred while rendering this page.</p></body></html>')\n } else {\n res.end()\n }\n }\n }) // _cerAuthStore.run\n }) // _cerFetchStore.run\n }) // _cerDataStore.run\n }) // _cerReqStore.run\n }) // _cerStateStore.run\n}\n\n// ISR-wrapped handler for production integrations (Express, Hono, Fastify).\n// Routes with meta.ssg.revalidate are served stale-while-revalidate.\nexport const isrHandler = createIsrHandler(routes, handler)\n\nexport { apiRoutes, plugins, layouts, routes, serverMiddleware }\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 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 serverMiddleware from 'virtual:cer-server-middleware'\nimport { runtimeConfig, _runtimePrivateDefaults, _authSessionKey, _hooks } 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, resolvePrivateConfig, useSession } from '@jasonshimmy/vite-plugin-cer-app/composables'\nimport { errorTag } from 'virtual:cer-error'\nimport { createIsrHandler } from '@jasonshimmy/vite-plugin-cer-app/isr'\n\nregisterBuiltinComponents()\n\n// Resolve private config from environment variables at server startup.\n// Each key declared in runtimeConfig.private is looked up in process.env,\n// first as-is, then as ALL_CAPS. The declared default is used as fallback.\ninitRuntimeConfig({ ...runtimeConfig, private: resolvePrivateConfig(_runtimePrivateDefaults ?? {}) })\n\n// Pre-load the full HTML entity map so named entities like &mdash; decode\n// correctly during SSR. Without this the bundled runtime falls back to a\n// minimal set (&lt;, &gt;, &amp; \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// Async-local storage for request-scoped req/res access.\n// Allows isomorphic composables (e.g. useCookie) to read/write HTTP headers\n// without prop-drilling the request context through the component tree.\nconst _cerReqStore = new AsyncLocalStorage()\n;(globalThis).__CER_REQ_STORE__ = _cerReqStore\n\n// Async-local storage for the authenticated user resolved before each render.\n// useAuth() reads this store server-side to return the current user synchronously.\nconst _cerAuthStore = new AsyncLocalStorage()\n;(globalThis).__CER_AUTH_STORE__ = _cerAuthStore\n\n// Async-local storage for per-request useFetch() data.\n// useFetch() writes fetched results here; the handler serialises the map into\n// window.__CER_FETCH_DATA__ for client-side hydration.\nconst _cerFetchStore = new AsyncLocalStorage()\n;(globalThis).__CER_FETCH_STORE__ = _cerFetchStore\n\n// Async-local storage for the current route info (path, params, query, meta).\n// useRoute() reads this on the server so layouts and components can access\n// route metadata without prop-drilling.\nconst _cerRouteStore = new AsyncLocalStorage()\n;(globalThis).__CER_ROUTE_STORE__ = _cerRouteStore\n\n// Async-local storage for per-request useState() reactive state.\n// Each request gets a fresh Map so concurrent SSR/SSG renders never share\n// reactive state set by different pages or loaders.\nconst _cerStateStore = new AsyncLocalStorage()\n;(globalThis).__CER_STATE_STORE__ = _cerStateStore\n\n// Runs fn inside the per-request AsyncLocalStorage context so that isomorphic\n// composables (useCookie, useSession, etc.) can access req/res without prop-drilling.\n// Call this for every API handler invocation \u2014 not just SSR renders.\nexport function runWithRequestContext(req, res, fn) {\n return _cerReqStore.run({ req, res }, fn)\n}\n\n// Runs the server/middleware/ chain for a request.\n// Returns false if a middleware short-circuited the response; true to continue.\n// Exported so Netlify / Vercel / Cloudflare bridges can also call it for API routes.\n// P1-2: Middleware may throw { status: 401 } (or any numeric .status) to produce\n// a non-500 response \u2014 the status is extracted and forwarded to res.statusCode.\nexport async function runServerMiddleware(req, res) {\n for (const { handler: mw } of (serverMiddleware ?? [])) {\n if (typeof mw !== 'function') continue\n let calledNext = false\n try {\n await new Promise((resolve, reject) => {\n Promise.resolve(mw(req, res, (err) => {\n if (err) reject(err)\n else { calledNext = true; resolve() }\n })).catch(reject)\n })\n } catch (err) {\n if (_hooks?.onError) {\n try { await _hooks.onError(err, { type: 'middleware', path: new URL(req.url ?? '/', 'http://x').pathname, req }) } catch { /* hooks must not crash the handler */ }\n }\n if (!res.writableEnded) {\n const statusCode = (typeof err === 'object' && err !== null && 'status' in err && typeof err.status === 'number')\n ? (isNaN(err.status) ? 500 : err.status)\n : 500\n res.statusCode = statusCode\n res.end('Internal Server Error')\n }\n return false\n }\n if (res.writableEnded || !calledNext) return false\n }\n return true\n}\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.\n//\n// Cloudflare Workers (and other runtimes without node:fs) can inject the\n// template before this module loads by setting globalThis.__CER_CLIENT_TEMPLATE__.\n// The Cloudflare adapter inlines dist/client/index.html as a string constant in\n// _worker.js and sets the global before dynamically importing the server bundle.\nlet _clientTemplate = (globalThis).__CER_CLIENT_TEMPLATE__ ?? null\nif (!_clientTemplate) {\n try {\n const _clientTemplatePath = join(dirname(fileURLToPath(import.meta.url)), '../client/index.html')\n _clientTemplate = existsSync(_clientTemplatePath)\n ? readFileSync(_clientTemplatePath, 'utf-8')\n : null\n } catch {\n // node:fs not available in this runtime \u2014 Cloudflare adapter must set\n // globalThis.__CER_CLIENT_TEMPLATE__ before importing this bundle.\n }\n}\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 // Store the current route info so useRoute() can read it synchronously\n // from any layout or component during this render pass.\n _cerRouteStore.enterWith({\n path: current.path,\n params,\n query: current.query ?? {},\n meta: route?.meta ?? null,\n })\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 // P2-2: Route-level error tag (from co-located .error.ts or _error.ts).\n // Preferred over the global errorTag when rendering loader errors for this route.\n const routeErrorTag = mod.errorTag ?? null\n\n // P1-1: Synthetic 404 catch-all \u2014 no page component registered.\n if (!pageTag) {\n const notFoundErrorTag = routeErrorTag ?? errorTag\n const notFoundVnode = notFoundErrorTag\n ? { tag: notFoundErrorTag, props: { attrs: { error: 'Not Found', status: '404' } }, children: [] }\n : { tag: 'div', props: {}, children: [] }\n return { vnode: notFoundVnode, router, head: undefined, status: 404 }\n }\n\n // Run the loader before creating the page vnode so we can pass its\n // primitive return values as HTML attributes. useProps() in the page\n // component reads element attributes, so merging loader data here makes\n // both useProps() and usePageData() work in SSR / SSG.\n let loaderAttrs = {}\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 // Expose primitive loader values as element attributes so useProps()\n // can read them. Complex objects are only accessible via usePageData().\n loaderAttrs = Object.fromEntries(\n Object.entries(data).filter(([, v]) => v !== null && v !== undefined && typeof v !== 'object' && typeof v !== 'function')\n )\n }\n }\n\n pageVnode = { tag: pageTag, props: { attrs: { ...params, ...loaderAttrs } }, children: [] }\n } catch (err) {\n // Loader threw \u2014 render the error page server-side if app/error.ts exists.\n const status = (err && typeof err === 'object' && 'status' in err && typeof err.status === 'number')\n ? err.status : 500\n const message = (err instanceof Error) ? err.message : String(err)\n if (_hooks?.onError) {\n try { await _hooks.onError(err, { type: 'loader', path: new URL(req.url ?? '/', 'http://x').pathname, req }) } catch { /* hooks must not crash the handler */ }\n }\n // P2-2: Prefer the route-level errorTag over the global one.\n // routeErrorTag is not in scope here; use route?.meta?.errorTag from the matched route.\n const effectiveErrorTag = route?.meta?.errorTag ?? errorTag\n if (!effectiveErrorTag) {\n console.error('[cer-app] Loader error (no app/error.ts defined):', err)\n }\n const errVnode = effectiveErrorTag\n ? { tag: effectiveErrorTag, props: { attrs: { error: message, status: String(status) } }, children: [] }\n : { tag: 'div', props: {}, children: [] }\n return { vnode: errVnode, router, head: undefined, status }\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 // If the request matched a catch-all route (user-defined 404.ts or [...all].ts),\n // return HTTP 404 so browsers and crawlers treat it as a not-found response.\n const isCatchAll = route?.path === '/:all*'\n return { vnode, router, head, status: isCatchAll ? 404 : null }\n}\n\nexport const handler = async (req, res) => {\n const _requestPath = new URL(req.url ?? '/', 'http://x').pathname\n const _requestStart = Date.now()\n if (_hooks?.onRequest) {\n try { await _hooks.onRequest({ path: _requestPath, method: req.method ?? 'GET', req }) } catch { /* hooks must not crash the handler */ }\n }\n await _cerStateStore.run(new Map(), async () => {\n await _cerReqStore.run({ req, res }, async () => {\n await _cerDataStore.run(null, async () => {\n // Fresh per-request fetch map \u2014 populated by useFetch() calls inside loaders.\n const _fetchMap = new Map()\n await _cerFetchStore.run(_fetchMap, async () => {\n // Pre-resolve the authenticated user so useAuth() works synchronously during rendering.\n let _authUser = null\n if (_authSessionKey) {\n try { _authUser = await useSession({ name: _authSessionKey }).get() } catch { /* no session secret */ }\n }\n await _cerAuthStore.run(_authUser, async () => {\n const { vnode, router, head, status } = await _prepareRequest(req)\n if (status != null) res.statusCode = status\n\n let _headCollectionOpen = false\n try {\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 _headCollectionOpen = true\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 _headCollectionOpen = false\n\n const reader = stream.getReader()\n\n // Read the first (synchronous) chunk \u2014 rejects if the sync render failed.\n const { value: firstChunk = '' } = await reader.read()\n\n // Serialise useFetch() results collected during loader execution.\n const _fetchObj = Object.fromEntries(_fetchMap)\n const _fetchScript = Object.keys(_fetchObj).length > 0\n ? `<script>window.__CER_FETCH_DATA__ = ${JSON.stringify(_fetchObj)}</script>`\n : ''\n\n // Serialise the auth user for client-side hydration via useAuth().\n const _authScript = _authUser\n ? `<script>window.__CER_AUTH_USER__ = ${JSON.stringify(_authUser)}</script>`\n : ''\n\n // Serialise useState() values for client-side hydration.\n // The state Map is populated by loader calls (in _prepareRequest) and by component\n // render functions (during renderToStreamWithJITCSSDSD above). Both run before this\n // point. Injected as window.__CER_STATE_INIT__ so the client useState() can\n // pre-populate its singleton Map on first use \u2014 no flash to default values.\n const _stateMap = _cerStateStore.getStore()\n let _stateScript = ''\n if (_stateMap && _stateMap.size > 0) {\n const _stateObj = {}\n for (const [k, v] of _stateMap) { _stateObj[k] = v.value }\n _stateScript = `<script>window.__CER_STATE_INIT__ = ${JSON.stringify(_stateObj)}</script>`\n }\n\n // Merge loader data script + useHead() tags + fetch/auth/state hydration scripts.\n const headContent = [head, headTags, _fetchScript, _authScript, _stateScript].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 if (_hooks?.onResponse) {\n try { void _hooks.onResponse({ path: _requestPath, method: req.method ?? 'GET', statusCode: res.statusCode, duration: Date.now() - _requestStart, req }) } catch { /* ignore */ }\n }\n } catch (_renderErr) {\n if (_hooks?.onError) {\n try { await _hooks.onError(_renderErr, { type: 'render', path: _requestPath, req }) } catch { /* hooks must not crash the handler */ }\n }\n // Ensure the head collector is never left open on error.\n if (_headCollectionOpen) { try { endHeadCollection() } catch { /* ignore */ } }\n // If headers have not been flushed yet we can still send a proper 500 page.\n // If writing has already started we can only close the connection cleanly.\n if (!res.headersSent) {\n res.statusCode = 500\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.end('<!DOCTYPE html><html><head></head><body><h1>500 Internal Server Error</h1><p>An unexpected error occurred while rendering this page.</p></body></html>')\n } else {\n res.end()\n }\n if (_hooks?.onResponse) {\n try { void _hooks.onResponse({ path: _requestPath, method: req.method ?? 'GET', statusCode: res.statusCode, duration: Date.now() - _requestStart, req }) } catch { /* ignore */ }\n }\n }\n }) // _cerAuthStore.run\n }) // _cerFetchStore.run\n }) // _cerDataStore.run\n }) // _cerReqStore.run\n }) // _cerStateStore.run\n}\n\n// ISR-wrapped handler for production integrations (Express, Hono, Fastify).\n// Routes with meta.ssg.revalidate are served stale-while-revalidate.\nexport const isrHandler = createIsrHandler(routes, handler)\n\nexport { apiRoutes, plugins, layouts, routes, serverMiddleware }\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,4+oBAqajC,CAAA"}
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,kzrBAybjC,CAAA"}
@@ -21,7 +21,7 @@ import layouts from 'virtual:cer-layouts'
21
21
  import plugins from 'virtual:cer-plugins'
22
22
  import apiRoutes from 'virtual:cer-server-api'
23
23
  import serverMiddleware from 'virtual:cer-server-middleware'
24
- import { runtimeConfig, _runtimePrivateDefaults, _authSessionKey } from 'virtual:cer-app-config'
24
+ import { runtimeConfig, _runtimePrivateDefaults, _authSessionKey, _hooks } from 'virtual:cer-app-config'
25
25
  import { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'
26
26
  import { registerEntityMap, renderToStreamWithJITCSSDSD, DSD_POLYFILL_SCRIPT } from '@jasonshimmy/custom-elements-runtime/ssr'
27
27
  import entitiesJson from '@jasonshimmy/custom-elements-runtime/entities.json'
@@ -121,6 +121,9 @@ export async function runServerMiddleware(req, res) {
121
121
  })).catch(reject)
122
122
  })
123
123
  } catch (err) {
124
+ if (_hooks?.onError) {
125
+ try { await _hooks.onError(err, { type: 'middleware', path: new URL(req.url ?? '/', 'http://x').pathname, req }) } catch { /* hooks must not crash the handler */ }
126
+ }
124
127
  if (!res.writableEnded) {
125
128
  const statusCode = (typeof err === 'object' && err !== null && 'status' in err && typeof err.status === 'number')
126
129
  ? (isNaN(err.status) ? 500 : err.status)
@@ -277,6 +280,9 @@ const _prepareRequest = async (req) => {
277
280
  const status = (err && typeof err === 'object' && 'status' in err && typeof err.status === 'number')
278
281
  ? err.status : 500
279
282
  const message = (err instanceof Error) ? err.message : String(err)
283
+ if (_hooks?.onError) {
284
+ try { await _hooks.onError(err, { type: 'loader', path: new URL(req.url ?? '/', 'http://x').pathname, req }) } catch { /* hooks must not crash the handler */ }
285
+ }
280
286
  // P2-2: Prefer the route-level errorTag over the global one.
281
287
  // routeErrorTag is not in scope here; use route?.meta?.errorTag from the matched route.
282
288
  const effectiveErrorTag = route?.meta?.errorTag ?? errorTag
@@ -309,6 +315,11 @@ const _prepareRequest = async (req) => {
309
315
  }
310
316
 
311
317
  export const handler = async (req, res) => {
318
+ const _requestPath = new URL(req.url ?? '/', 'http://x').pathname
319
+ const _requestStart = Date.now()
320
+ if (_hooks?.onRequest) {
321
+ try { await _hooks.onRequest({ path: _requestPath, method: req.method ?? 'GET', req }) } catch { /* hooks must not crash the handler */ }
322
+ }
312
323
  await _cerStateStore.run(new Map(), async () => {
313
324
  await _cerReqStore.run({ req, res }, async () => {
314
325
  await _cerDataStore.run(null, async () => {
@@ -406,7 +417,13 @@ export const handler = async (req, res) => {
406
417
 
407
418
  // Inject DSD polyfill immediately before </body>, then close the document.
408
419
  res.end(DSD_POLYFILL_SCRIPT + fromBodyClose)
420
+ if (_hooks?.onResponse) {
421
+ try { void _hooks.onResponse({ path: _requestPath, method: req.method ?? 'GET', statusCode: res.statusCode, duration: Date.now() - _requestStart, req }) } catch { /* ignore */ }
422
+ }
409
423
  } catch (_renderErr) {
424
+ if (_hooks?.onError) {
425
+ try { await _hooks.onError(_renderErr, { type: 'render', path: _requestPath, req }) } catch { /* hooks must not crash the handler */ }
426
+ }
410
427
  // Ensure the head collector is never left open on error.
411
428
  if (_headCollectionOpen) { try { endHeadCollection() } catch { /* ignore */ } }
412
429
  // If headers have not been flushed yet we can still send a proper 500 page.
@@ -418,6 +435,9 @@ export const handler = async (req, res) => {
418
435
  } else {
419
436
  res.end()
420
437
  }
438
+ if (_hooks?.onResponse) {
439
+ try { void _hooks.onResponse({ path: _requestPath, method: req.method ?? 'GET', statusCode: res.statusCode, duration: Date.now() - _requestStart, req }) } catch { /* ignore */ }
440
+ }
421
441
  }
422
442
  }) // _cerAuthStore.run
423
443
  }) // _cerFetchStore.run
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqapC,CAAA"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAybpC,CAAA"}
@@ -15,6 +15,7 @@
15
15
  * app.use('*', createIsrHandler(routes, handler))
16
16
  */
17
17
  import type { IncomingMessage, ServerResponse } from 'node:http';
18
+ /** A single cached SSR response stored by `createIsrHandler`. Includes the full rendered HTML, response headers, status code, and revalidation metadata. */
18
19
  export interface IsrCacheEntry {
19
20
  html: string;
20
21
  headers: Record<string, string>;
@@ -22,6 +23,7 @@ export interface IsrCacheEntry {
22
23
  builtAt: number;
23
24
  revalidate: number;
24
25
  }
26
+ /** The SSR request handler signature produced by the server entry bundle (exported as `handler`). Compatible with Express, Hono, and any Node.js HTTP server. */
25
27
  export type SsrHandlerFn = (req: IncomingMessage, res: ServerResponse) => unknown;
26
28
  /**
27
29
  * Wraps an SSR handler with stale-while-revalidate ISR caching.
@@ -1 +1 @@
1
- {"version":3,"file":"isr-handler.d.ts","sourceRoot":"","sources":["../../src/runtime/isr-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAA;AA8FjF;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,EAC/D,OAAO,EAAE,YAAY,GACpB,YAAY,CAgDd"}
1
+ {"version":3,"file":"isr-handler.d.ts","sourceRoot":"","sources":["../../src/runtime/isr-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE,4JAA4J;AAC5J,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,iKAAiK;AACjK,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAA;AA8FjF;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,EAC/D,OAAO,EAAE,YAAY,GACpB,YAAY,CAgDd"}
@@ -1 +1 @@
1
- {"version":3,"file":"isr-handler.js","sourceRoot":"","sources":["../../src/runtime/isr-handler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"isr-handler.js","sourceRoot":"","sources":["../../src/runtime/isr-handler.ts"],"names":[],"mappings":"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"}
@@ -1,14 +1,35 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ /**
3
+ * Augmented Node.js `IncomingMessage` passed to API route handlers.
4
+ * Route params and query string are pre-parsed; `body` is parsed from JSON automatically.
5
+ */
2
6
  export interface ApiRequest extends IncomingMessage {
3
7
  params: Record<string, string>;
4
8
  query: Record<string, string>;
5
9
  body: unknown;
6
10
  }
11
+ /**
12
+ * Augmented Node.js `ServerResponse` passed to API route handlers.
13
+ * Adds convenience methods for JSON responses and fluent status setting.
14
+ */
7
15
  export interface ApiResponse extends ServerResponse {
8
16
  json(data: unknown): void;
9
17
  status(code: number): ApiResponse;
10
18
  }
19
+ /**
20
+ * Handler function for a file-based API route (`app/api/**\/*.ts`).
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * // app/api/posts/[id].ts
25
+ * export const GET: ApiHandler = async (req, res) => {
26
+ * const post = await fetchPost(req.params.id)
27
+ * res.json(post)
28
+ * }
29
+ * ```
30
+ */
11
31
  export type ApiHandler = (req: ApiRequest, res: ApiResponse) => Promise<void> | void;
32
+ /** Combined request/response context passed to API utility functions and composables inside API handlers. */
12
33
  export interface ApiContext {
13
34
  req: ApiRequest;
14
35
  res: ApiResponse;
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/types/api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE,MAAM,WAAW,UAAW,SAAQ,eAAe;IACjD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,IAAI,EAAE,OAAO,CAAA;CACd;AAED,MAAM,WAAW,WAAY,SAAQ,cAAc;IACjD,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAA;IACzB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;CAClC;AAED,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;AAEpF,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,UAAU,CAAA;IACf,GAAG,EAAE,WAAW,CAAA;CACjB"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/types/api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE;;;GAGG;AACH,MAAM,WAAW,UAAW,SAAQ,eAAe;IACjD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,IAAI,EAAE,OAAO,CAAA;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,WAAY,SAAQ,cAAc;IACjD,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAA;IACzB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;CAClC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;AAEpF,6GAA6G;AAC7G,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,UAAU,CAAA;IACf,GAAG,EAAE,WAAW,CAAA;CACjB"}
@@ -1,10 +1,56 @@
1
1
  import type { RouterConfig } from '@jasonshimmy/custom-elements-runtime/router';
2
+ import type { IncomingMessage } from 'node:http';
3
+ /**
4
+ * Context passed to the `onError` hook.
5
+ * `type` indicates which layer the error originated from so reporters can
6
+ * tag or route errors appropriately.
7
+ */
8
+ export interface ErrorHookContext {
9
+ /** The layer that threw the error. */
10
+ type: 'loader' | 'render' | 'middleware';
11
+ /** The request URL pathname (e.g. `/about`). */
12
+ path: string;
13
+ /** The raw Node.js incoming request. */
14
+ req: IncomingMessage;
15
+ }
16
+ /**
17
+ * Context passed to the `onRequest` hook.
18
+ * Fires at the start of every SSR request, before the route is matched
19
+ * or the loader runs.
20
+ */
21
+ export interface RequestHookContext {
22
+ /** The request URL pathname (e.g. `/about`). */
23
+ path: string;
24
+ /** HTTP method in upper-case (e.g. `'GET'`). */
25
+ method: string;
26
+ /** The raw Node.js incoming request. */
27
+ req: IncomingMessage;
28
+ }
29
+ /**
30
+ * Context passed to the `onResponse` hook.
31
+ * Fires after the response has been sent (both success and error paths).
32
+ * Use this for latency tracking and request logging.
33
+ */
34
+ export interface ResponseHookContext {
35
+ /** The request URL pathname (e.g. `/about`). */
36
+ path: string;
37
+ /** HTTP method in upper-case (e.g. `'GET'`). */
38
+ method: string;
39
+ /** Final HTTP status code written to the response. */
40
+ statusCode: number;
41
+ /** Request duration in milliseconds from the first byte received to `res.end()`. */
42
+ duration: number;
43
+ /** The raw Node.js incoming request. */
44
+ req: IncomingMessage;
45
+ }
46
+ /** Token payload returned by an OAuth provider after a successful authorization code exchange. */
2
47
  export interface OAuthTokens {
3
48
  accessToken: string;
4
49
  refreshToken?: string;
5
50
  expiresIn?: number;
6
51
  tokenType: string;
7
52
  }
53
+ /** Configuration for a single OAuth 2.0 provider (Google, GitHub, Discord, or a custom provider). */
8
54
  export interface OAuthProviderConfig {
9
55
  /** OAuth application client ID. */
10
56
  clientId: string;
@@ -25,6 +71,7 @@ export interface OAuthProviderConfig {
25
71
  */
26
72
  mapUser?: (profile: Record<string, unknown>, tokens: OAuthTokens) => Record<string, unknown>;
27
73
  }
74
+ /** Authentication configuration. Enables OAuth login flows and auto-generates `/api/auth/*` routes. */
28
75
  export interface AuthConfig {
29
76
  /**
30
77
  * OAuth provider configurations keyed by provider name.
@@ -47,6 +94,7 @@ export interface AuthConfig {
47
94
  */
48
95
  sessionKey?: string;
49
96
  }
97
+ /** Internationalisation routing configuration. When set, locale-aware URL routes are generated and `useLocale()` is enabled. */
50
98
  export interface I18nConfig {
51
99
  /**
52
100
  * All supported locale codes. e.g. `['en', 'fr', 'de']`.
@@ -64,24 +112,42 @@ export interface I18nConfig {
64
112
  */
65
113
  strategy?: 'prefix' | 'prefix_except_default' | 'no_prefix';
66
114
  }
115
+ /** Global Static Site Generation (SSG) configuration. Controls which routes are pre-rendered and at what concurrency. */
67
116
  export interface SsgConfig {
117
+ /**
118
+ * Routes to pre-render.
119
+ * - `'auto'` (default) — pre-render every static route discovered in `app/pages/`.
120
+ * - `string[]` — explicit list of paths (e.g. `['/about', '/contact']`).
121
+ */
68
122
  routes?: 'auto' | string[];
123
+ /** Maximum number of pages rendered in parallel. Defaults to `1`. Increase for faster SSG builds at the cost of higher memory usage. */
69
124
  concurrency?: number;
125
+ /** When `true`, unenumerated routes fall back to SSR at runtime instead of returning 404. */
70
126
  fallback?: boolean;
71
127
  }
128
+ /** JIT (Just-In-Time) CSS configuration for shadow-DOM style injection. */
72
129
  export interface JitCssConfig {
130
+ /** Additional glob patterns for content files scanned by the JIT CSS engine. */
73
131
  content?: string[];
132
+ /** Enable the extended color palette. Defaults to `false`. */
74
133
  extendedColors?: boolean;
75
134
  }
135
+ /** Fine-grained control over which auto-import categories are injected into page/layout/component files. All categories are enabled by default. */
76
136
  export interface AutoImportsConfig {
137
+ /** Auto-import components defined in `app/components/`. Defaults to `true`. */
77
138
  components?: boolean;
139
+ /** Auto-import framework composables (`useHead`, `useState`, etc.). Defaults to `true`. */
78
140
  composables?: boolean;
141
+ /** Auto-import template directives (`when`, `each`, `bind`). Defaults to `true`. */
79
142
  directives?: boolean;
143
+ /** Auto-import runtime primitives (`component`, `html`, `ref`, etc.). Defaults to `true`. */
80
144
  runtime?: boolean;
81
145
  }
146
+ /** Arbitrary public runtime values serialized into the virtual module at build time. Available on both server and client via `useRuntimeConfig().public`. */
82
147
  export interface RuntimePublicConfig {
83
148
  [key: string]: unknown;
84
149
  }
150
+ /** Server-only secrets resolved from `process.env` at startup. Never serialized into the client bundle. Available via `useRuntimeConfig().private` in loaders and server middleware only. */
85
151
  export interface RuntimePrivateConfig {
86
152
  /**
87
153
  * HMAC-SHA-256 signing secret(s) for `useSession()`.
@@ -107,6 +173,7 @@ export interface RuntimePrivateConfig {
107
173
  sessionSecret?: string | string[];
108
174
  [key: string]: string | string[] | undefined;
109
175
  }
176
+ /** Runtime configuration split into `public` (client + server) and `private` (server-only) sections. */
110
177
  export interface RuntimeConfig {
111
178
  /**
112
179
  * Public runtime config — available on both server and client via
@@ -133,6 +200,7 @@ export interface RuntimeConfig {
133
200
  */
134
201
  private?: RuntimePrivateConfig;
135
202
  }
203
+ /** Root configuration object for `cer.config.ts`. Pass to `defineConfig()` for type-safe configuration. */
136
204
  export interface CerAppConfig {
137
205
  mode?: 'spa' | 'ssr' | 'ssg';
138
206
  srcDir?: string;
@@ -205,6 +273,58 @@ export interface CerAppConfig {
205
273
  * ```
206
274
  */
207
275
  auth?: AuthConfig;
276
+ /**
277
+ * Called when an error is caught by the framework's SSR error boundaries
278
+ * (loader throws, render crash, or server middleware throws). Use this to
279
+ * forward errors to Sentry, Datadog, or any other error-reporting service.
280
+ *
281
+ * Errors in this hook are silently swallowed so they cannot crash the request handler.
282
+ *
283
+ * @example
284
+ * ```ts
285
+ * import * as Sentry from '@sentry/node'
286
+ * export default defineConfig({
287
+ * onError(err, ctx) {
288
+ * Sentry.captureException(err, { tags: { type: ctx.type, path: ctx.path } })
289
+ * },
290
+ * })
291
+ * ```
292
+ */
293
+ onError?: (err: unknown, ctx: ErrorHookContext) => void | Promise<void>;
294
+ /**
295
+ * Called at the start of every SSR request, before route matching and the loader.
296
+ * Use this for request logging or to initialise per-request APM transactions.
297
+ *
298
+ * Errors in this hook are silently swallowed.
299
+ */
300
+ onRequest?: (ctx: RequestHookContext) => void | Promise<void>;
301
+ /**
302
+ * Called after every SSR response is sent (both success and error paths).
303
+ * `ctx.duration` contains the elapsed milliseconds from first byte to `res.end()`.
304
+ * Use this for latency tracking and access logging.
305
+ *
306
+ * Errors in this hook are silently swallowed.
307
+ */
308
+ onResponse?: (ctx: ResponseHookContext) => void | Promise<void>;
208
309
  }
310
+ /**
311
+ * Define the framework configuration with full TypeScript intellisense.
312
+ * This is a pass-through helper — it returns `config` unchanged and exists
313
+ * solely to provide type checking and IDE autocompletion in `cer.config.ts`.
314
+ *
315
+ * @example
316
+ * ```ts
317
+ * // cer.config.ts
318
+ * import { defineConfig } from '@jasonshimmy/vite-plugin-cer-app'
319
+ *
320
+ * export default defineConfig({
321
+ * mode: 'ssr',
322
+ * runtimeConfig: {
323
+ * public: { apiBase: 'https://api.example.com' },
324
+ * private: { dbUrl: '' },
325
+ * },
326
+ * })
327
+ * ```
328
+ */
209
329
  export declare function defineConfig(config: CerAppConfig): CerAppConfig;
210
330
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,22 @@
1
+ /**
2
+ * Define the framework configuration with full TypeScript intellisense.
3
+ * This is a pass-through helper — it returns `config` unchanged and exists
4
+ * solely to provide type checking and IDE autocompletion in `cer.config.ts`.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * // cer.config.ts
9
+ * import { defineConfig } from '@jasonshimmy/vite-plugin-cer-app'
10
+ *
11
+ * export default defineConfig({
12
+ * mode: 'ssr',
13
+ * runtimeConfig: {
14
+ * public: { apiBase: 'https://api.example.com' },
15
+ * private: { dbUrl: '' },
16
+ * },
17
+ * })
18
+ * ```
19
+ */
1
20
  export function defineConfig(config) {
2
21
  return config;
3
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AA8NA,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO,MAAM,CAAA;AACf,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAwUA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO,MAAM,CAAA;AACf,CAAC"}
@@ -1,4 +1,4 @@
1
- export type { CerAppConfig, SsgConfig, JitCssConfig, AutoImportsConfig, RuntimeConfig, RuntimePublicConfig, RuntimePrivateConfig, I18nConfig } from './config.js';
1
+ export type { CerAppConfig, SsgConfig, JitCssConfig, AutoImportsConfig, RuntimeConfig, RuntimePublicConfig, RuntimePrivateConfig, I18nConfig, ErrorHookContext, RequestHookContext, ResponseHookContext } 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,aAAa,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACjK,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,WAAW,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAClF,YAAY,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,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,oBAAoB,EAAE,UAAU,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAC5N,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,WAAW,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAClF,YAAY,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAA"}
@@ -33,5 +33,21 @@ export type GuardResult = boolean | string | Promise<boolean | string>;
33
33
  * ```
34
34
  */
35
35
  export type MiddlewareFn = (to: RouteState, from: RouteState | null, next: () => Promise<void>) => GuardResult | void;
36
+ /**
37
+ * Server middleware function. Receives the raw Node.js `req`/`res` pair and a `next`
38
+ * callback. Call `next()` to pass control to the next middleware in the chain,
39
+ * or call `next(err)` to signal an error (sets the response status from `err.status`,
40
+ * defaulting to 500).
41
+ *
42
+ * Defined with `defineServerMiddleware()` and placed in `app/middleware/`.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * export default defineServerMiddleware((req, res, next) => {
47
+ * res.setHeader('X-Request-Id', crypto.randomUUID())
48
+ * next()
49
+ * })
50
+ * ```
51
+ */
36
52
  export type ServerMiddleware = (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => void | Promise<void>;
37
53
  //# sourceMappingURL=middleware.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/types/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6CAA6C,CAAA;AAC7E,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC,CAAA;AAEtE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,MAAM,YAAY,GAAG,CACzB,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,UAAU,GAAG,IAAI,EACvB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KACtB,WAAW,GAAG,IAAI,CAAA;AAEvB,MAAM,MAAM,gBAAgB,GAAG,CAC7B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,OAAO,KAAK,IAAI,KAC1B,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/types/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6CAA6C,CAAA;AAC7E,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC,CAAA;AAEtE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,MAAM,YAAY,GAAG,CACzB,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,UAAU,GAAG,IAAI,EACvB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KACtB,WAAW,GAAG,IAAI,CAAA;AAEvB;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAC7B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,OAAO,KAAK,IAAI,KAC1B,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA"}