@jasonshimmy/vite-plugin-cer-app 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/commits.txt +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +2 -0
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/preview.d.ts.map +1 -1
- package/dist/cli/commands/preview.js +21 -2
- package/dist/cli/commands/preview.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/plugin/build-ssg.d.ts.map +1 -1
- package/dist/plugin/build-ssg.js +10 -21
- package/dist/plugin/build-ssg.js.map +1 -1
- package/dist/plugin/build-ssr.d.ts.map +1 -1
- package/dist/plugin/build-ssr.js +150 -27
- package/dist/plugin/build-ssr.js.map +1 -1
- package/dist/plugin/virtual/routes.d.ts.map +1 -1
- package/dist/plugin/virtual/routes.js +11 -1
- 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 +6 -0
- package/dist/runtime/app-template.js.map +1 -1
- package/dist/runtime/composables/use-page-data.d.ts +15 -6
- package/dist/runtime/composables/use-page-data.d.ts.map +1 -1
- package/dist/runtime/composables/use-page-data.js +30 -9
- package/dist/runtime/composables/use-page-data.js.map +1 -1
- package/dist/runtime/entry-server-template.d.ts +1 -1
- package/dist/runtime/entry-server-template.d.ts.map +1 -1
- package/dist/runtime/entry-server-template.js +137 -16
- package/dist/runtime/entry-server-template.js.map +1 -1
- package/docs/data-loading.md +7 -6
- package/docs/getting-started.md +1 -1
- package/docs/rendering-modes.md +1 -1
- package/docs/server-api.md +9 -0
- package/package.json +1 -1
- package/src/__tests__/index.test.ts +21 -0
- package/src/__tests__/plugin/build-ssg.test.ts +265 -0
- package/src/__tests__/plugin/build-ssr.test.ts +180 -0
- package/src/__tests__/plugin/cer-app-plugin.test.ts +409 -0
- package/src/__tests__/plugin/dts-generator.test.ts +246 -0
- package/src/__tests__/plugin/resolve-config.test.ts +158 -0
- package/src/__tests__/plugin/virtual/error.test.ts +71 -0
- package/src/__tests__/plugin/virtual/loading.test.ts +72 -0
- package/src/__tests__/plugin/virtual/server-middleware.test.ts +102 -0
- package/src/__tests__/runtime/use-page-data.test.ts +81 -5
- package/src/__tests__/types/config.test.ts +23 -0
- package/src/cli/commands/generate.ts +2 -0
- package/src/cli/commands/preview.ts +21 -2
- package/src/cli/index.ts +1 -1
- package/src/plugin/build-ssg.ts +9 -22
- package/src/plugin/build-ssr.ts +149 -27
- package/src/plugin/virtual/routes.ts +12 -1
- package/src/runtime/app-template.ts +6 -0
- package/src/runtime/composables/use-page-data.ts +31 -9
- package/src/runtime/entry-server-template.ts +137 -16
- package/tsconfig.build.json +1 -1
- package/dist/__tests__/plugin/path-utils.test.d.ts +0 -2
- package/dist/__tests__/plugin/path-utils.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/path-utils.test.js +0 -305
- package/dist/__tests__/plugin/path-utils.test.js.map +0 -1
- package/dist/__tests__/plugin/scanner.test.d.ts +0 -2
- package/dist/__tests__/plugin/scanner.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/scanner.test.js +0 -143
- package/dist/__tests__/plugin/scanner.test.js.map +0 -1
- package/dist/__tests__/plugin/transforms/auto-import.test.d.ts +0 -2
- package/dist/__tests__/plugin/transforms/auto-import.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/transforms/auto-import.test.js +0 -151
- package/dist/__tests__/plugin/transforms/auto-import.test.js.map +0 -1
- package/dist/__tests__/plugin/transforms/head-inject.test.d.ts +0 -2
- package/dist/__tests__/plugin/transforms/head-inject.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/transforms/head-inject.test.js +0 -151
- package/dist/__tests__/plugin/transforms/head-inject.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/components.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/components.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/components.test.js +0 -47
- package/dist/__tests__/plugin/virtual/components.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/composables.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/composables.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/composables.test.js +0 -48
- package/dist/__tests__/plugin/virtual/composables.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/layouts.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/layouts.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/layouts.test.js +0 -59
- package/dist/__tests__/plugin/virtual/layouts.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/middleware.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/middleware.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/middleware.test.js +0 -58
- package/dist/__tests__/plugin/virtual/middleware.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/plugins.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/plugins.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/plugins.test.js +0 -73
- package/dist/__tests__/plugin/virtual/plugins.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/routes.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/routes.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/routes.test.js +0 -167
- package/dist/__tests__/plugin/virtual/routes.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/server-api.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/server-api.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/server-api.test.js +0 -72
- package/dist/__tests__/plugin/virtual/server-api.test.js.map +0 -1
- package/dist/__tests__/runtime/use-head.test.d.ts +0 -2
- package/dist/__tests__/runtime/use-head.test.d.ts.map +0 -1
- package/dist/__tests__/runtime/use-head.test.js +0 -202
- package/dist/__tests__/runtime/use-head.test.js.map +0 -1
- package/dist/__tests__/runtime/use-page-data.test.d.ts +0 -2
- package/dist/__tests__/runtime/use-page-data.test.d.ts.map +0 -1
- package/dist/__tests__/runtime/use-page-data.test.js +0 -41
- package/dist/__tests__/runtime/use-page-data.test.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-template.js","sourceRoot":"","sources":["../../src/runtime/app-template.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG
|
|
1
|
+
{"version":3,"file":"app-template.js","sourceRoot":"","sources":["../../src/runtime/app-template.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2I3B,CAAA"}
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* usePageData — reads SSR-injected loader data for the current page.
|
|
3
3
|
*
|
|
4
|
-
* During SSR/SSG the server calls the matched route's `loader()` function
|
|
5
|
-
*
|
|
6
|
-
* HTML `<head>`. On client hydration this composable reads that payload so
|
|
7
|
-
* the page component can skip the initial data fetch.
|
|
4
|
+
* During SSR/SSG the server calls the matched route's `loader()` function
|
|
5
|
+
* and makes the result available in two ways:
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* 1. **Server render pass** — the data is stored in a per-request
|
|
8
|
+
* `AsyncLocalStorage` context (set via `_cerDataStore.enterWith(data)`
|
|
9
|
+
* in the entry-server template). `usePageData()` reads this store so the
|
|
10
|
+
* component renders with real data in the initial SSR/SSG HTML.
|
|
11
|
+
*
|
|
12
|
+
* 2. **Client hydration** — the server also serializes the data as
|
|
13
|
+
* `window.__CER_DATA__` in the page `<head>`. The client entry captures
|
|
14
|
+
* this into `globalThis.__CER_DATA__` before the app boots. On first
|
|
15
|
+
* component instantiation `usePageData()` returns that value so the client
|
|
16
|
+
* starts with the correct state without an extra network round-trip.
|
|
17
|
+
*
|
|
18
|
+
* The client-side value is cleared after the first read so subsequent
|
|
19
|
+
* client-side navigations don't accidentally reuse stale SSR data.
|
|
11
20
|
*
|
|
12
21
|
* @returns The serialized loader result, or `null` if no SSR data is present.
|
|
13
22
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-page-data.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/use-page-data.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"use-page-data.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/use-page-data.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,WAAW,CAAC,CAAC,GAAG,OAAO,KAAK,CAAC,GAAG,IAAI,CAqBnD"}
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* usePageData — reads SSR-injected loader data for the current page.
|
|
3
3
|
*
|
|
4
|
-
* During SSR/SSG the server calls the matched route's `loader()` function
|
|
5
|
-
*
|
|
6
|
-
* HTML `<head>`. On client hydration this composable reads that payload so
|
|
7
|
-
* the page component can skip the initial data fetch.
|
|
4
|
+
* During SSR/SSG the server calls the matched route's `loader()` function
|
|
5
|
+
* and makes the result available in two ways:
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* 1. **Server render pass** — the data is stored in a per-request
|
|
8
|
+
* `AsyncLocalStorage` context (set via `_cerDataStore.enterWith(data)`
|
|
9
|
+
* in the entry-server template). `usePageData()` reads this store so the
|
|
10
|
+
* component renders with real data in the initial SSR/SSG HTML.
|
|
11
|
+
*
|
|
12
|
+
* 2. **Client hydration** — the server also serializes the data as
|
|
13
|
+
* `window.__CER_DATA__` in the page `<head>`. The client entry captures
|
|
14
|
+
* this into `globalThis.__CER_DATA__` before the app boots. On first
|
|
15
|
+
* component instantiation `usePageData()` returns that value so the client
|
|
16
|
+
* starts with the correct state without an extra network round-trip.
|
|
17
|
+
*
|
|
18
|
+
* The client-side value is cleared after the first read so subsequent
|
|
19
|
+
* client-side navigations don't accidentally reuse stale SSR data.
|
|
11
20
|
*
|
|
12
21
|
* @returns The serialized loader result, or `null` if no SSR data is present.
|
|
13
22
|
*
|
|
@@ -29,13 +38,25 @@
|
|
|
29
38
|
* ```
|
|
30
39
|
*/
|
|
31
40
|
export function usePageData() {
|
|
32
|
-
// Works in both SSR (globalThis) and browser (window) contexts.
|
|
33
41
|
const g = globalThis;
|
|
42
|
+
// Server-side: read from the per-request AsyncLocalStorage context.
|
|
43
|
+
// __CER_DATA_STORE__ is set by the entry-server template and is only present
|
|
44
|
+
// in Node.js, so this branch is tree-shaken out of the client bundle.
|
|
45
|
+
const store = g['__CER_DATA_STORE__'];
|
|
46
|
+
if (store) {
|
|
47
|
+
const ssrData = store.getStore();
|
|
48
|
+
if (ssrData !== undefined && ssrData !== null)
|
|
49
|
+
return ssrData;
|
|
50
|
+
}
|
|
51
|
+
// Client-side: read from window.__CER_DATA__ captured by the client entry.
|
|
52
|
+
// Do NOT clear here — the data is cleared by app-template.ts after the
|
|
53
|
+
// initial router.replace() completes. This ensures both the pre-rendered
|
|
54
|
+
// element (upgraded during component registration) and the new element
|
|
55
|
+
// created by router.replace() can both access the SSR data without a
|
|
56
|
+
// race where the first read consumes it before the second can use it.
|
|
34
57
|
const data = g['__CER_DATA__'];
|
|
35
58
|
if (data === undefined || data === null)
|
|
36
59
|
return null;
|
|
37
|
-
// Clear so that subsequent client navigations don't reuse stale SSR data.
|
|
38
|
-
delete g['__CER_DATA__'];
|
|
39
60
|
return data;
|
|
40
61
|
}
|
|
41
62
|
//# sourceMappingURL=use-page-data.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-page-data.js","sourceRoot":"","sources":["../../../src/runtime/composables/use-page-data.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"use-page-data.js","sourceRoot":"","sources":["../../../src/runtime/composables/use-page-data.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,UAAU,WAAW;IACzB,MAAM,CAAC,GAAG,UAAqC,CAAA;IAE/C,oEAAoE;IACpE,6EAA6E;IAC7E,sEAAsE;IACtE,MAAM,KAAK,GAAG,CAAC,CAAC,oBAAoB,CAAwC,CAAA;IAC5E,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,EAA0B,CAAA;QACxD,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO,OAAO,CAAA;IAC/D,CAAC;IAED,2EAA2E;IAC3E,uEAAuE;IACvE,yEAAyE;IACzE,uEAAuE;IACvE,qEAAqE;IACrE,sEAAsE;IACtE,MAAM,IAAI,GAAG,CAAC,CAAC,cAAc,CAAkB,CAAA;IAC/C,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IACpD,OAAO,IAAI,CAAA;AACb,CAAC"}
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* wires up the routing, and exports a handler compatible with
|
|
6
6
|
* Express/Fastify/Node http.
|
|
7
7
|
*/
|
|
8
|
-
export declare const ENTRY_SERVER_TEMPLATE = "// Server-side entry \u2014 AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app\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 {
|
|
8
|
+
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, 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 { createSSRHandler } from '@jasonshimmy/custom-elements-runtime/ssr-middleware'\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// 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 handler's full HTML document with the Vite client shell so the\n// final page 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 <style> elements from the SSR body into the document <head> so\n // JIT CSS rules are applied before the layout paints.\n const headParts = ssrHead ? [ssrHead] : []\n let ssrBodyContent = ssrBody\n let pos = 0\n while (pos < ssrBodyContent.length) {\n const styleOpen = ssrBodyContent.indexOf('<style', 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) merged = merged.replace('</head>', headAdditions + '\\n</head>')\n return merged\n}\n\n/**\n * Per-request VNode factory \u2014 initializes a fresh router at the request URL,\n * resolves the active layout from the matched route's meta, pre-loads the\n * matched page component (bypassing the async router-view so DSD renders\n * synchronously), calls the route's data loader (if any), and injects the\n * serialized result into the document head as window.__CER_DATA__ for\n * client-side hydration.\n *\n * createStreamingSSRHandler threads the router through each component's SSR\n * context so concurrent renders never share state.\n */\nconst vnodeFactory = async (req) => {\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 // Make data available to usePageData() during the SSR render pass.\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\n// Capture the raw SSR handler and wrap it to merge the response with the\n// Vite client template before sending \u2014 this injects the JS/CSS asset bundles\n// so the browser can hydrate and enable client-side routing.\nconst _rawHandler = createSSRHandler(vnodeFactory, {\n render: { dsd: true, dsdPolyfill: false },\n})\n\n/**\n * The main request handler.\n * Compatible with Express, Fastify, and Node's raw http.createServer.\n *\n * Each request is run inside a fresh _cerDataStore.run() context so that\n * concurrent renders (e.g. SSG with concurrency > 1) get isolated stores.\n * vnodeFactory calls _cerDataStore.enterWith(loaderData) from within this\n * context, making the data visible to usePageData() during SSR rendering\n * without any global-state races.\n */\nexport const handler = async (req, res) => {\n if (!_clientTemplate) {\n // No client template \u2014 run handler normally, then inject DSD polyfill.\n let _html = ''\n await _cerDataStore.run(null, async () => {\n await _rawHandler(req, { setHeader: () => {}, end: (body) => { _html = body } })\n })\n // Inject DSD polyfill at end of <body>, outside any custom element light DOM.\n const _final = _html.includes('</body>')\n ? _html.replace('</body>', DSD_POLYFILL_SCRIPT + '</body>')\n : _html + DSD_POLYFILL_SCRIPT\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n return res.end(_final)\n }\n let _capturedHtml = ''\n // Wrap _rawHandler in an isolated async-local-storage context so that\n // vnodeFactory's enterWith() call is scoped to this request only.\n await _cerDataStore.run(null, async () => {\n // Omit write() to force the non-streaming collect-then-end code path.\n await _rawHandler(req, { setHeader: () => {}, end: (body) => { _capturedHtml = body } })\n })\n let _merged = _mergeWithClientTemplate(_capturedHtml, _clientTemplate)\n // Inject DSD polyfill at end of <body>, outside <cer-layout-view> light DOM.\n _merged = _merged.includes('</body>')\n ? _merged.replace('</body>', DSD_POLYFILL_SCRIPT + '</body>')\n : _merged + DSD_POLYFILL_SCRIPT\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.end(_merged)\n}\n\nexport { apiRoutes, plugins, layouts, routes }\nexport default handler\n";
|
|
9
9
|
//# 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;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,
|
|
1
|
+
{"version":3,"file":"entry-server-template.d.ts","sourceRoot":"","sources":["../../src/runtime/entry-server-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,0oRAwLjC,CAAA"}
|
|
@@ -6,26 +6,97 @@
|
|
|
6
6
|
* Express/Fastify/Node http.
|
|
7
7
|
*/
|
|
8
8
|
export const ENTRY_SERVER_TEMPLATE = `// Server-side entry — AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app
|
|
9
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
10
|
+
import { dirname, join } from 'node:path'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
9
13
|
import 'virtual:cer-components'
|
|
10
14
|
import routes from 'virtual:cer-routes'
|
|
11
15
|
import layouts from 'virtual:cer-layouts'
|
|
12
16
|
import plugins from 'virtual:cer-plugins'
|
|
13
17
|
import apiRoutes from 'virtual:cer-server-api'
|
|
14
|
-
import {
|
|
18
|
+
import { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'
|
|
19
|
+
import { registerEntityMap, DSD_POLYFILL_SCRIPT } from '@jasonshimmy/custom-elements-runtime/ssr'
|
|
20
|
+
import entitiesJson from '@jasonshimmy/custom-elements-runtime/entities.json'
|
|
15
21
|
import { initRouter } from '@jasonshimmy/custom-elements-runtime/router'
|
|
16
|
-
import {
|
|
22
|
+
import { createSSRHandler } from '@jasonshimmy/custom-elements-runtime/ssr-middleware'
|
|
17
23
|
|
|
18
24
|
registerBuiltinComponents()
|
|
19
25
|
|
|
26
|
+
// Pre-load the full HTML entity map so named entities like — decode
|
|
27
|
+
// correctly during SSR. Without this the bundled runtime falls back to a
|
|
28
|
+
// minimal set (<, >, & …) and re-escapes everything else.
|
|
29
|
+
registerEntityMap(entitiesJson)
|
|
30
|
+
|
|
31
|
+
// Async-local storage for request-scoped SSR loader data.
|
|
32
|
+
// Using AsyncLocalStorage ensures concurrent SSR renders (e.g. SSG with
|
|
33
|
+
// concurrency > 1) never see each other's data — each request's async chain
|
|
34
|
+
// carries its own store value, so usePageData() is always race-condition-free.
|
|
35
|
+
const _cerDataStore = new AsyncLocalStorage()
|
|
36
|
+
// Expose the store so the usePageData() composable can read it server-side.
|
|
37
|
+
;(globalThis).__CER_DATA_STORE__ = _cerDataStore
|
|
38
|
+
|
|
39
|
+
// Load the Vite-built client index.html (dist/client/index.html) so every SSR
|
|
40
|
+
// response includes the client-side scripts needed for hydration and routing.
|
|
41
|
+
// The server bundle lives at dist/server/server.js, so ../client resolves correctly.
|
|
42
|
+
const _clientTemplatePath = join(dirname(fileURLToPath(import.meta.url)), '../client/index.html')
|
|
43
|
+
const _clientTemplate = existsSync(_clientTemplatePath)
|
|
44
|
+
? readFileSync(_clientTemplatePath, 'utf-8')
|
|
45
|
+
: null
|
|
46
|
+
|
|
47
|
+
// Merge the SSR handler's full HTML document with the Vite client shell so the
|
|
48
|
+
// final page contains both pre-rendered DSD content and the client bundle scripts.
|
|
49
|
+
function _mergeWithClientTemplate(ssrHtml, clientTemplate) {
|
|
50
|
+
const headTag = '<head>', headCloseTag = '</head>'
|
|
51
|
+
const bodyTag = '<body>', bodyCloseTag = '</body>'
|
|
52
|
+
const headStart = ssrHtml.indexOf(headTag)
|
|
53
|
+
const headEnd = ssrHtml.indexOf(headCloseTag)
|
|
54
|
+
const bodyStart = ssrHtml.indexOf(bodyTag)
|
|
55
|
+
const bodyEnd = ssrHtml.lastIndexOf(bodyCloseTag)
|
|
56
|
+
const ssrHead = headStart >= 0 && headEnd > headStart
|
|
57
|
+
? ssrHtml.slice(headStart + headTag.length, headEnd).trim() : ''
|
|
58
|
+
const ssrBody = bodyStart >= 0 && bodyEnd > bodyStart
|
|
59
|
+
? ssrHtml.slice(bodyStart + bodyTag.length, bodyEnd).trim() : ssrHtml
|
|
60
|
+
// Hoist <style> elements from the SSR body into the document <head> so
|
|
61
|
+
// JIT CSS rules are applied before the layout paints.
|
|
62
|
+
const headParts = ssrHead ? [ssrHead] : []
|
|
63
|
+
let ssrBodyContent = ssrBody
|
|
64
|
+
let pos = 0
|
|
65
|
+
while (pos < ssrBodyContent.length) {
|
|
66
|
+
const styleOpen = ssrBodyContent.indexOf('<style', pos)
|
|
67
|
+
if (styleOpen < 0) break
|
|
68
|
+
const styleClose = ssrBodyContent.indexOf('</style>', styleOpen)
|
|
69
|
+
if (styleClose < 0) break
|
|
70
|
+
headParts.push(ssrBodyContent.slice(styleOpen, styleClose + 8))
|
|
71
|
+
ssrBodyContent = ssrBodyContent.slice(0, styleOpen) + ssrBodyContent.slice(styleClose + 8)
|
|
72
|
+
pos = styleOpen
|
|
73
|
+
}
|
|
74
|
+
ssrBodyContent = ssrBodyContent.trim()
|
|
75
|
+
// Inject the pre-rendered layout+page as light DOM of the app mount element
|
|
76
|
+
// so it is visible before JS boots, then the client router takes over.
|
|
77
|
+
let merged = clientTemplate
|
|
78
|
+
if (merged.includes('<cer-layout-view></cer-layout-view>')) {
|
|
79
|
+
merged = merged.replace('<cer-layout-view></cer-layout-view>',
|
|
80
|
+
'<cer-layout-view>' + ssrBodyContent + '</cer-layout-view>')
|
|
81
|
+
} else if (merged.includes('<div id="app"></div>')) {
|
|
82
|
+
merged = merged.replace('<div id="app"></div>',
|
|
83
|
+
'<div id="app">' + ssrBodyContent + '</div>')
|
|
84
|
+
}
|
|
85
|
+
const headAdditions = headParts.filter(Boolean).join('\\n')
|
|
86
|
+
if (headAdditions) merged = merged.replace('</head>', headAdditions + '\\n</head>')
|
|
87
|
+
return merged
|
|
88
|
+
}
|
|
89
|
+
|
|
20
90
|
/**
|
|
21
91
|
* Per-request VNode factory — initializes a fresh router at the request URL,
|
|
22
|
-
* resolves the active layout from the matched route's meta,
|
|
23
|
-
*
|
|
24
|
-
*
|
|
92
|
+
* resolves the active layout from the matched route's meta, pre-loads the
|
|
93
|
+
* matched page component (bypassing the async router-view so DSD renders
|
|
94
|
+
* synchronously), calls the route's data loader (if any), and injects the
|
|
95
|
+
* serialized result into the document head as window.__CER_DATA__ for
|
|
96
|
+
* client-side hydration.
|
|
25
97
|
*
|
|
26
98
|
* createStreamingSSRHandler threads the router through each component's SSR
|
|
27
|
-
* context so
|
|
28
|
-
* shared activeRouterProxy singleton. This makes concurrent SSR requests safe.
|
|
99
|
+
* context so concurrent renders never share state.
|
|
29
100
|
*/
|
|
30
101
|
const vnodeFactory = async (req) => {
|
|
31
102
|
const router = initRouter({ routes, initialUrl: req.url ?? '/' })
|
|
@@ -33,38 +104,88 @@ const vnodeFactory = async (req) => {
|
|
|
33
104
|
const { route, params } = router.matchRoute(current.path)
|
|
34
105
|
const layoutName = route?.meta?.layout ?? 'default'
|
|
35
106
|
const layoutTag = layouts[layoutName]
|
|
36
|
-
const inner = html\`<router-view></router-view>\`
|
|
37
|
-
const vnode = layoutTag
|
|
38
|
-
? { tag: layoutTag, props: {}, children: [inner] }
|
|
39
|
-
: inner
|
|
40
107
|
|
|
41
|
-
//
|
|
108
|
+
// Pre-load the page module so we can embed the component tag directly.
|
|
109
|
+
// This avoids the async router-view (which injects content via script tags
|
|
110
|
+
// and breaks Declarative Shadow DOM on initial parse).
|
|
111
|
+
let pageVnode = { tag: 'div', props: {}, children: [] }
|
|
42
112
|
let head
|
|
43
113
|
if (route?.load) {
|
|
44
114
|
try {
|
|
45
115
|
const mod = await route.load()
|
|
116
|
+
const pageTag = mod.default
|
|
117
|
+
if (pageTag) {
|
|
118
|
+
pageVnode = { tag: pageTag, props: { attrs: { ...params } }, children: [] }
|
|
119
|
+
}
|
|
46
120
|
if (typeof mod.loader === 'function') {
|
|
47
121
|
const query = current.query ?? {}
|
|
48
122
|
const data = await mod.loader({ params, query, req })
|
|
49
123
|
if (data !== undefined && data !== null) {
|
|
124
|
+
// Make data available to usePageData() during the SSR render pass.
|
|
125
|
+
// enterWith() scopes the value to the current async context so
|
|
126
|
+
// concurrent renders (SSG concurrency > 1) never share data.
|
|
127
|
+
_cerDataStore.enterWith(data)
|
|
50
128
|
head = \`<script>window.__CER_DATA__ = \${JSON.stringify(data)}</script>\`
|
|
51
129
|
}
|
|
52
130
|
}
|
|
53
131
|
} catch {
|
|
54
|
-
//
|
|
132
|
+
// Non-fatal: loader errors fall back to an empty page; client will refetch.
|
|
55
133
|
}
|
|
56
134
|
}
|
|
57
135
|
|
|
136
|
+
const vnode = layoutTag
|
|
137
|
+
? { tag: layoutTag, props: {}, children: [pageVnode] }
|
|
138
|
+
: pageVnode
|
|
139
|
+
|
|
58
140
|
return { vnode, router, head }
|
|
59
141
|
}
|
|
60
142
|
|
|
143
|
+
// Capture the raw SSR handler and wrap it to merge the response with the
|
|
144
|
+
// Vite client template before sending — this injects the JS/CSS asset bundles
|
|
145
|
+
// so the browser can hydrate and enable client-side routing.
|
|
146
|
+
const _rawHandler = createSSRHandler(vnodeFactory, {
|
|
147
|
+
render: { dsd: true, dsdPolyfill: false },
|
|
148
|
+
})
|
|
149
|
+
|
|
61
150
|
/**
|
|
62
151
|
* The main request handler.
|
|
63
152
|
* Compatible with Express, Fastify, and Node's raw http.createServer.
|
|
153
|
+
*
|
|
154
|
+
* Each request is run inside a fresh _cerDataStore.run() context so that
|
|
155
|
+
* concurrent renders (e.g. SSG with concurrency > 1) get isolated stores.
|
|
156
|
+
* vnodeFactory calls _cerDataStore.enterWith(loaderData) from within this
|
|
157
|
+
* context, making the data visible to usePageData() during SSR rendering
|
|
158
|
+
* without any global-state races.
|
|
64
159
|
*/
|
|
65
|
-
export const handler =
|
|
66
|
-
|
|
67
|
-
|
|
160
|
+
export const handler = async (req, res) => {
|
|
161
|
+
if (!_clientTemplate) {
|
|
162
|
+
// No client template — run handler normally, then inject DSD polyfill.
|
|
163
|
+
let _html = ''
|
|
164
|
+
await _cerDataStore.run(null, async () => {
|
|
165
|
+
await _rawHandler(req, { setHeader: () => {}, end: (body) => { _html = body } })
|
|
166
|
+
})
|
|
167
|
+
// Inject DSD polyfill at end of <body>, outside any custom element light DOM.
|
|
168
|
+
const _final = _html.includes('</body>')
|
|
169
|
+
? _html.replace('</body>', DSD_POLYFILL_SCRIPT + '</body>')
|
|
170
|
+
: _html + DSD_POLYFILL_SCRIPT
|
|
171
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
|
172
|
+
return res.end(_final)
|
|
173
|
+
}
|
|
174
|
+
let _capturedHtml = ''
|
|
175
|
+
// Wrap _rawHandler in an isolated async-local-storage context so that
|
|
176
|
+
// vnodeFactory's enterWith() call is scoped to this request only.
|
|
177
|
+
await _cerDataStore.run(null, async () => {
|
|
178
|
+
// Omit write() to force the non-streaming collect-then-end code path.
|
|
179
|
+
await _rawHandler(req, { setHeader: () => {}, end: (body) => { _capturedHtml = body } })
|
|
180
|
+
})
|
|
181
|
+
let _merged = _mergeWithClientTemplate(_capturedHtml, _clientTemplate)
|
|
182
|
+
// Inject DSD polyfill at end of <body>, outside <cer-layout-view> light DOM.
|
|
183
|
+
_merged = _merged.includes('</body>')
|
|
184
|
+
? _merged.replace('</body>', DSD_POLYFILL_SCRIPT + '</body>')
|
|
185
|
+
: _merged + DSD_POLYFILL_SCRIPT
|
|
186
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
|
187
|
+
res.end(_merged)
|
|
188
|
+
}
|
|
68
189
|
|
|
69
190
|
export { apiRoutes, plugins, layouts, routes }
|
|
70
191
|
export default handler
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-server-template.js","sourceRoot":"","sources":["../../src/runtime/entry-server-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;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;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwLpC,CAAA"}
|
package/docs/data-loading.md
CHANGED
|
@@ -54,20 +54,21 @@ interface PageLoaderContext<P extends Record<string, string>> {
|
|
|
54
54
|
1. A request arrives for `/blog/hello-world`
|
|
55
55
|
2. The router matches `app/pages/blog/[slug].ts`
|
|
56
56
|
3. The server calls `loader({ params: { slug: 'hello-world' }, query, req })`
|
|
57
|
-
4. The returned data is serialized as `window.__CER_DATA__` in a `<script>` tag in the HTML
|
|
57
|
+
4. The returned data is serialized as `window.__CER_DATA__` in a `<script>` tag in the HTML `<head>`:
|
|
58
58
|
```html
|
|
59
59
|
<script>window.__CER_DATA__ = {"title":"Hello World","body":"..."}</script>
|
|
60
60
|
```
|
|
61
|
-
5.
|
|
62
|
-
6. The full HTML is sent to the browser
|
|
61
|
+
5. The server renders `<page-blog-slug>` directly into the layout using Declarative Shadow DOM
|
|
62
|
+
6. The full HTML (pre-rendered content + client scripts) is sent to the browser
|
|
63
63
|
|
|
64
64
|
---
|
|
65
65
|
|
|
66
66
|
## Client hydration flow
|
|
67
67
|
|
|
68
|
-
1. Browser receives the full HTML
|
|
69
|
-
2.
|
|
70
|
-
3.
|
|
68
|
+
1. Browser receives the full HTML — content is immediately visible via Declarative Shadow DOM before any JS runs
|
|
69
|
+
2. Client JS boots; `usePageData()` reads `window.__CER_DATA__` and returns the hydrated values
|
|
70
|
+
3. The value is cleared after the first read so subsequent client-side navigations trigger a fresh fetch
|
|
71
|
+
4. Components that received SSR data skip their `useOnConnected` fetch — no duplicate request
|
|
71
72
|
|
|
72
73
|
---
|
|
73
74
|
|
package/docs/getting-started.md
CHANGED
package/docs/rendering-modes.md
CHANGED
|
@@ -22,7 +22,7 @@ The simplest mode. Vite builds a standard client-only bundle. No server required
|
|
|
22
22
|
|
|
23
23
|
### How it works
|
|
24
24
|
|
|
25
|
-
- `index.html` is the entry point with `<
|
|
25
|
+
- `index.html` is the entry point with `<cer-layout-view>` as the app mount element
|
|
26
26
|
- All routing is client-side; the server returns the same `index.html` for every URL
|
|
27
27
|
- `virtual:cer-routes` injects all routes into the client-side router
|
|
28
28
|
|
package/docs/server-api.md
CHANGED
|
@@ -160,6 +160,15 @@ export default async function handler(req, res) {
|
|
|
160
160
|
| **SPA production** | Not included — deploy API routes separately or use a proxy |
|
|
161
161
|
| **SSG production** | Optionally called at build time for data; otherwise deployed separately |
|
|
162
162
|
|
|
163
|
+
### SPA mode — by design
|
|
164
|
+
|
|
165
|
+
In SPA mode (`mode: 'spa'`) the build output is a pure client bundle with no server component. API routes defined in `server/api/` are **only active during development** (Vite dev server middleware). At runtime the SPA has no server to serve them from.
|
|
166
|
+
|
|
167
|
+
Options for SPA + API:
|
|
168
|
+
- **Separate API server** — deploy a Node.js/Express server alongside the SPA that mounts the same `server/api/` handlers.
|
|
169
|
+
- **Reverse proxy** — proxy `/api/*` requests from your CDN or web server to a backend service.
|
|
170
|
+
- **Switch to SSR mode** — `mode: 'ssr'` gives you a full Node.js server that serves both the SSR pages and the API routes from a single process.
|
|
171
|
+
|
|
163
172
|
---
|
|
164
173
|
|
|
165
174
|
## Virtual module
|
package/package.json
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// Import from the main package entry to trigger coverage of src/index.ts
|
|
4
|
+
// re-exports. These are all re-exports of other modules so we just verify
|
|
5
|
+
// the named exports resolve without errors.
|
|
6
|
+
import { defineConfig, cerApp } from '../index.js'
|
|
7
|
+
|
|
8
|
+
describe('package main entry', () => {
|
|
9
|
+
it('exports defineConfig', () => {
|
|
10
|
+
expect(typeof defineConfig).toBe('function')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('defineConfig is a pass-through', () => {
|
|
14
|
+
const cfg = { mode: 'spa' as const }
|
|
15
|
+
expect(defineConfig(cfg)).toBe(cfg)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('exports cerApp', () => {
|
|
19
|
+
expect(typeof cerApp).toBe('function')
|
|
20
|
+
})
|
|
21
|
+
})
|