@jasonshimmy/vite-plugin-cer-app 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/commits.txt +1 -1
- package/dist/cli/create/templates/spa/package.json.tpl +2 -2
- package/dist/cli/create/templates/ssg/package.json.tpl +2 -2
- package/dist/cli/create/templates/ssr/package.json.tpl +2 -2
- package/dist/plugin/build-ssg.d.ts.map +1 -1
- package/dist/plugin/build-ssg.js +4 -2
- package/dist/plugin/build-ssg.js.map +1 -1
- package/dist/runtime/entry-server-template.d.ts +2 -2
- package/dist/runtime/entry-server-template.d.ts.map +1 -1
- package/dist/runtime/entry-server-template.js +36 -15
- package/dist/runtime/entry-server-template.js.map +1 -1
- package/docs/rendering-modes.md +3 -3
- package/package.json +3 -3
- package/src/__tests__/plugin/build-ssg-render.test.ts +46 -0
- package/src/__tests__/plugin/entry-server-template.test.ts +23 -5
- package/src/cli/create/templates/spa/package.json.tpl +2 -2
- package/src/cli/create/templates/ssg/package.json.tpl +2 -2
- package/src/cli/create/templates/ssr/package.json.tpl +2 -2
- package/src/plugin/build-ssg.ts +4 -2
- package/src/runtime/entry-server-template.ts +36 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
|
+
## [v0.6.0] - 2026-03-21
|
|
5
|
+
|
|
6
|
+
- feat(ssr): switch server entry from renderToStringWithJITCSSDSD to renderToStreamWithJITCSSDSD for true incremental streaming (cca67d8)
|
|
7
|
+
|
|
4
8
|
## [v0.5.0] - 2026-03-21
|
|
5
9
|
|
|
6
10
|
- feat: add runtime configuration support and ISR enhancements (847cd25)
|
package/commits.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
- feat:
|
|
1
|
+
- feat(ssr): switch server entry from renderToStringWithJITCSSDSD to renderToStreamWithJITCSSDSD for true incremental streaming (cca67d8)
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
"preview": "cer-app preview"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jasonshimmy/custom-elements-runtime": "^3.
|
|
11
|
+
"@jasonshimmy/custom-elements-runtime": "^3.4.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"vite": "^8.0.1",
|
|
15
|
-
"@jasonshimmy/vite-plugin-cer-app": "^0.
|
|
15
|
+
"@jasonshimmy/vite-plugin-cer-app": "^0.6.0",
|
|
16
16
|
"typescript": "^5.9.3"
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"preview": "cer-app preview"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@jasonshimmy/custom-elements-runtime": "^3.
|
|
12
|
+
"@jasonshimmy/custom-elements-runtime": "^3.4.0"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"vite": "^8.0.1",
|
|
16
|
-
"@jasonshimmy/vite-plugin-cer-app": "^0.
|
|
16
|
+
"@jasonshimmy/vite-plugin-cer-app": "^0.6.0",
|
|
17
17
|
"typescript": "^5.9.3"
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
"preview": "cer-app preview --ssr"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jasonshimmy/custom-elements-runtime": "^3.
|
|
11
|
+
"@jasonshimmy/custom-elements-runtime": "^3.4.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"vite": "^8.0.1",
|
|
15
|
-
"@jasonshimmy/vite-plugin-cer-app": "^0.
|
|
15
|
+
"@jasonshimmy/vite-plugin-cer-app": "^0.6.0",
|
|
16
16
|
"typescript": "^5.9.3"
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-ssg.d.ts","sourceRoot":"","sources":["../../src/plugin/build-ssg.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,MAAM,CAAA;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"build-ssg.d.ts","sourceRoot":"","sources":["../../src/plugin/build-ssg.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,MAAM,CAAA;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AA8IxD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;;;;;;GAOG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,iBAAiB,EACzB,cAAc,GAAE,UAAe,GAC9B,OAAO,CAAC,IAAI,CAAC,CAmEf"}
|
package/dist/plugin/build-ssg.js
CHANGED
|
@@ -104,12 +104,14 @@ async function renderPath(path, serverBundlePath) {
|
|
|
104
104
|
}
|
|
105
105
|
// Mock req/res for the Express-style handler.
|
|
106
106
|
// The handler internally merges with dist/client/index.html, so we just
|
|
107
|
-
// capture whatever it ends with.
|
|
107
|
+
// capture whatever it writes/ends with.
|
|
108
108
|
const mockReq = { url: path, headers: {} };
|
|
109
109
|
return new Promise((resolve, reject) => {
|
|
110
|
+
const chunks = [];
|
|
110
111
|
const mockRes = {
|
|
111
112
|
setHeader: () => { },
|
|
112
|
-
|
|
113
|
+
write: (chunk) => { chunks.push(chunk); },
|
|
114
|
+
end: (body) => resolve(chunks.join('') + (body ?? '')),
|
|
113
115
|
};
|
|
114
116
|
handlerFn(mockReq, mockRes).catch(reject);
|
|
115
117
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-ssg.js","sourceRoot":"","sources":["../../src/plugin/build-ssg.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"build-ssg.js","sourceRoot":"","sources":["../../src/plugin/build-ssg.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Key features:
|
|
9
9
|
* - AsyncLocalStorage for race-condition-free concurrent renders (SSG concurrency > 1)
|
|
10
|
-
* - Declarative Shadow DOM via
|
|
10
|
+
* - Declarative Shadow DOM via renderToStreamWithJITCSSDSD (always on, streamed)
|
|
11
11
|
* - useHead() support via beginHeadCollection / endHeadCollection
|
|
12
12
|
* - DSD polyfill injected at end of <body> after client-template merge
|
|
13
13
|
*/
|
|
14
|
-
export declare const ENTRY_SERVER_TEMPLATE = "// Server-side entry \u2014 AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app\nimport { readFileSync, existsSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { AsyncLocalStorage } from 'node:async_hooks'\nimport 'virtual:cer-components'\nimport routes from 'virtual:cer-routes'\nimport layouts from 'virtual:cer-layouts'\nimport plugins from 'virtual:cer-plugins'\nimport apiRoutes from 'virtual:cer-server-api'\nimport { runtimeConfig } from 'virtual:cer-app-config'\nimport { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'\nimport { registerEntityMap, renderToStringWithJITCSSDSD, DSD_POLYFILL_SCRIPT } from '@jasonshimmy/custom-elements-runtime/ssr'\nimport entitiesJson from '@jasonshimmy/custom-elements-runtime/entities.json'\nimport { initRouter } from '@jasonshimmy/custom-elements-runtime/router'\nimport { beginHeadCollection, endHeadCollection, serializeHeadTags, initRuntimeConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'\n\nregisterBuiltinComponents()\ninitRuntimeConfig(runtimeConfig)\n\n// Pre-load the full HTML entity map so named entities like — decode\n// correctly during SSR. Without this the bundled runtime falls back to a\n// minimal set (<, >, & \u2026) and re-escapes everything else.\nregisterEntityMap(entitiesJson)\n\n// Run plugins once at server startup so their provide() values are available\n// to useInject() during every SSR/SSG render pass. Stored on globalThis so all\n// dynamically-imported page chunks share the same reference.\nconst _pluginProvides = new Map()\n;(globalThis).__cerPluginProvides = _pluginProvides\nconst _pluginsReady = (async () => {\n const _bootstrapRouter = initRouter({ routes })\n for (const plugin of plugins) {\n if (plugin && typeof plugin.setup === 'function') {\n await plugin.setup({\n router: _bootstrapRouter,\n provide: (key, value) => _pluginProvides.set(key, value),\n config: {},\n })\n }\n }\n})()\n\n// Async-local storage for request-scoped SSR loader data.\n// Using AsyncLocalStorage ensures concurrent SSR renders (e.g. SSG with\n// concurrency > 1) never see each other's data \u2014 each request's async chain\n// carries its own store value, so usePageData() is always race-condition-free.\nconst _cerDataStore = new AsyncLocalStorage()\n// Expose the store so the usePageData() composable can read it server-side.\n;(globalThis).__CER_DATA_STORE__ = _cerDataStore\n\n// Load the Vite-built client index.html (dist/client/index.html) so every SSR\n// response includes the client-side scripts needed for hydration and routing.\n// The server bundle lives at dist/server/server.js, so ../client resolves correctly.\nconst _clientTemplatePath = join(dirname(fileURLToPath(import.meta.url)), '../client/index.html')\nconst _clientTemplate = existsSync(_clientTemplatePath)\n ? readFileSync(_clientTemplatePath, 'utf-8')\n : null\n\n// Merge the SSR rendered body with the Vite client shell so the final page\n// contains both pre-rendered DSD content and the client bundle scripts.\nfunction _mergeWithClientTemplate(ssrHtml, clientTemplate) {\n const headTag = '<head>', headCloseTag = '</head>'\n const bodyTag = '<body>', bodyCloseTag = '</body>'\n const headStart = ssrHtml.indexOf(headTag)\n const headEnd = ssrHtml.indexOf(headCloseTag)\n const bodyStart = ssrHtml.indexOf(bodyTag)\n const bodyEnd = ssrHtml.lastIndexOf(bodyCloseTag)\n const ssrHead = headStart >= 0 && headEnd > headStart\n ? ssrHtml.slice(headStart + headTag.length, headEnd).trim() : ''\n const ssrBody = bodyStart >= 0 && bodyEnd > bodyStart\n ? ssrHtml.slice(bodyStart + bodyTag.length, bodyEnd).trim() : ssrHtml\n // Hoist only top-level <style id=...> elements (cer-ssr-jit, cer-ssr-global)\n // from the SSR body into the document <head>. Plain <style> blocks without\n // an id attribute belong to shadow DOM templates and must stay in place \u2014\n // hoisting them to <head> breaks shadow DOM style encapsulation (document\n // styles do not pierce shadow roots), which is the root cause of FOUC.\n const headParts = ssrHead ? [ssrHead] : []\n let ssrBodyContent = ssrBody\n let pos = 0\n while (pos < ssrBodyContent.length) {\n const styleOpen = ssrBodyContent.indexOf('<style id=', pos)\n if (styleOpen < 0) break\n const styleClose = ssrBodyContent.indexOf('</style>', styleOpen)\n if (styleClose < 0) break\n headParts.push(ssrBodyContent.slice(styleOpen, styleClose + 8))\n ssrBodyContent = ssrBodyContent.slice(0, styleOpen) + ssrBodyContent.slice(styleClose + 8)\n pos = styleOpen\n }\n ssrBodyContent = ssrBodyContent.trim()\n // Inject the pre-rendered layout+page as light DOM of the app mount element\n // so it is visible before JS boots, then the client router takes over.\n let merged = clientTemplate\n if (merged.includes('<cer-layout-view></cer-layout-view>')) {\n merged = merged.replace('<cer-layout-view></cer-layout-view>',\n '<cer-layout-view>' + ssrBodyContent + '</cer-layout-view>')\n } else if (merged.includes('<div id=\"app\"></div>')) {\n merged = merged.replace('<div id=\"app\"></div>',\n '<div id=\"app\">' + ssrBodyContent + '</div>')\n }\n const headAdditions = headParts.filter(Boolean).join('\\n')\n if (headAdditions) {\n // If SSR provides a <title>, replace the client template's <title> so the\n // SSR title wins (client template title is the fallback default).\n if (headAdditions.includes('<title>')) {\n merged = merged.replace(/<title>[^<]*<\\/title>/, '')\n }\n merged = merged.replace('</head>', headAdditions + '\\n</head>')\n }\n return merged\n}\n\n// Per-request async setup: initialize a fresh router, resolve the matched\n// route and layout, pre-load the page module, and call the data loader.\n// Loader data is scoped to the current AsyncLocalStorage context via enterWith()\n// so concurrent renders never share state.\nconst _prepareRequest = async (req) => {\n await _pluginsReady\n const router = initRouter({ routes, initialUrl: req.url ?? '/' })\n const current = router.getCurrent()\n const { route, params } = router.matchRoute(current.path)\n\n // Pre-load the page module so we can embed the component tag directly.\n // This avoids the async router-view (which injects content via script tags\n // and breaks Declarative Shadow DOM on initial parse).\n let pageVnode = { tag: 'div', props: {}, children: [] }\n let head\n if (route?.load) {\n try {\n const mod = await route.load()\n const pageTag = mod.default\n if (pageTag) {\n pageVnode = { tag: pageTag, props: { attrs: { ...params } }, children: [] }\n }\n if (typeof mod.loader === 'function') {\n const query = current.query ?? {}\n const data = await mod.loader({ params, query, req })\n if (data !== undefined && data !== null) {\n // enterWith() scopes the value to the current async context so\n // concurrent renders (SSG concurrency > 1) never share data.\n _cerDataStore.enterWith(data)\n head = `<script>window.__CER_DATA__ = ${JSON.stringify(data)}</script>`\n }\n }\n } catch {\n // Non-fatal: loader errors fall back to an empty page; client will refetch.\n }\n }\n\n // Resolve layout chain: nested layouts (meta.layoutChain) or single layout.\n const chain = route?.meta?.layoutChain\n ? route.meta.layoutChain\n : [route?.meta?.layout ?? 'default']\n\n // Wrap pageVnode in the layout chain from innermost to outermost.\n let vnode = pageVnode\n for (let i = chain.length - 1; i >= 0; i--) {\n const tag = layouts[chain[i]]\n if (tag) vnode = { tag, props: {}, children: [vnode] }\n }\n\n return { vnode, router, head }\n}\n\nexport const handler = async (req, res) => {\n await _cerDataStore.run(null, async () => {\n const { vnode, router, head } = await _prepareRequest(req)\n\n // Begin collecting useHead() calls made during the synchronous render pass.\n beginHeadCollection()\n\n // dsdPolyfill: false \u2014 we inject the polyfill manually after merging so it\n // lands at the end of <body>, not inside <cer-layout-view> light DOM where\n // scripts may not execute.\n const { htmlWithStyles } = renderToStringWithJITCSSDSD(vnode, {\n dsdPolyfill: false,\n router,\n })\n\n // Collect and serialize any useHead() calls from the rendered components.\n const headTags = serializeHeadTags(endHeadCollection())\n\n // Merge loader data script + useHead() tags into the document head.\n const headContent = [head, headTags].filter(Boolean).join('\\n')\n\n // Wrap the rendered body in a full HTML document and inject the head additions\n // (loader data script, useHead() tags, JIT styles). No polyfill in body yet.\n const ssrHtml = `<!DOCTYPE html><html><head>${headContent}</head><body>${htmlWithStyles}</body></html>`\n\n let finalHtml = _clientTemplate\n ? _mergeWithClientTemplate(ssrHtml, _clientTemplate)\n : ssrHtml\n\n // Inject DSD polyfill at end of <body>, outside <cer-layout-view>, so the\n // browser runs it after parsing the declarative shadow roots.\n finalHtml = finalHtml.includes('</body>')\n ? finalHtml.replace('</body>', DSD_POLYFILL_SCRIPT + '</body>')\n : finalHtml + DSD_POLYFILL_SCRIPT\n\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.end(finalHtml)\n })\n}\n\nexport { apiRoutes, plugins, layouts, routes }\nexport default handler\n";
|
|
14
|
+
export declare const ENTRY_SERVER_TEMPLATE = "// Server-side entry \u2014 AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app\nimport { readFileSync, existsSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { AsyncLocalStorage } from 'node:async_hooks'\nimport 'virtual:cer-components'\nimport routes from 'virtual:cer-routes'\nimport layouts from 'virtual:cer-layouts'\nimport plugins from 'virtual:cer-plugins'\nimport apiRoutes from 'virtual:cer-server-api'\nimport { runtimeConfig } from 'virtual:cer-app-config'\nimport { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'\nimport { registerEntityMap, renderToStreamWithJITCSSDSD, DSD_POLYFILL_SCRIPT } from '@jasonshimmy/custom-elements-runtime/ssr'\nimport entitiesJson from '@jasonshimmy/custom-elements-runtime/entities.json'\nimport { initRouter } from '@jasonshimmy/custom-elements-runtime/router'\nimport { beginHeadCollection, endHeadCollection, serializeHeadTags, initRuntimeConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'\n\nregisterBuiltinComponents()\ninitRuntimeConfig(runtimeConfig)\n\n// Pre-load the full HTML entity map so named entities like — decode\n// correctly during SSR. Without this the bundled runtime falls back to a\n// minimal set (<, >, & \u2026) and re-escapes everything else.\nregisterEntityMap(entitiesJson)\n\n// Run plugins once at server startup so their provide() values are available\n// to useInject() during every SSR/SSG render pass. Stored on globalThis so all\n// dynamically-imported page chunks share the same reference.\nconst _pluginProvides = new Map()\n;(globalThis).__cerPluginProvides = _pluginProvides\nconst _pluginsReady = (async () => {\n const _bootstrapRouter = initRouter({ routes })\n for (const plugin of plugins) {\n if (plugin && typeof plugin.setup === 'function') {\n await plugin.setup({\n router: _bootstrapRouter,\n provide: (key, value) => _pluginProvides.set(key, value),\n config: {},\n })\n }\n }\n})()\n\n// Async-local storage for request-scoped SSR loader data.\n// Using AsyncLocalStorage ensures concurrent SSR renders (e.g. SSG with\n// concurrency > 1) never see each other's data \u2014 each request's async chain\n// carries its own store value, so usePageData() is always race-condition-free.\nconst _cerDataStore = new AsyncLocalStorage()\n// Expose the store so the usePageData() composable can read it server-side.\n;(globalThis).__CER_DATA_STORE__ = _cerDataStore\n\n// Load the Vite-built client index.html (dist/client/index.html) so every SSR\n// response includes the client-side scripts needed for hydration and routing.\n// The server bundle lives at dist/server/server.js, so ../client resolves correctly.\nconst _clientTemplatePath = join(dirname(fileURLToPath(import.meta.url)), '../client/index.html')\nconst _clientTemplate = existsSync(_clientTemplatePath)\n ? readFileSync(_clientTemplatePath, 'utf-8')\n : null\n\n// Merge the SSR rendered body with the Vite client shell so the final page\n// contains both pre-rendered DSD content and the client bundle scripts.\nfunction _mergeWithClientTemplate(ssrHtml, clientTemplate) {\n const headTag = '<head>', headCloseTag = '</head>'\n const bodyTag = '<body>', bodyCloseTag = '</body>'\n const headStart = ssrHtml.indexOf(headTag)\n const headEnd = ssrHtml.indexOf(headCloseTag)\n const bodyStart = ssrHtml.indexOf(bodyTag)\n const bodyEnd = ssrHtml.lastIndexOf(bodyCloseTag)\n const ssrHead = headStart >= 0 && headEnd > headStart\n ? ssrHtml.slice(headStart + headTag.length, headEnd).trim() : ''\n const ssrBody = bodyStart >= 0 && bodyEnd > bodyStart\n ? ssrHtml.slice(bodyStart + bodyTag.length, bodyEnd).trim() : ssrHtml\n // Hoist only top-level <style id=...> elements (cer-ssr-jit, cer-ssr-global)\n // from the SSR body into the document <head>. Plain <style> blocks without\n // an id attribute belong to shadow DOM templates and must stay in place \u2014\n // hoisting them to <head> breaks shadow DOM style encapsulation (document\n // styles do not pierce shadow roots), which is the root cause of FOUC.\n const headParts = ssrHead ? [ssrHead] : []\n let ssrBodyContent = ssrBody\n let pos = 0\n while (pos < ssrBodyContent.length) {\n const styleOpen = ssrBodyContent.indexOf('<style id=', pos)\n if (styleOpen < 0) break\n const styleClose = ssrBodyContent.indexOf('</style>', styleOpen)\n if (styleClose < 0) break\n headParts.push(ssrBodyContent.slice(styleOpen, styleClose + 8))\n ssrBodyContent = ssrBodyContent.slice(0, styleOpen) + ssrBodyContent.slice(styleClose + 8)\n pos = styleOpen\n }\n ssrBodyContent = ssrBodyContent.trim()\n // Inject the pre-rendered layout+page as light DOM of the app mount element\n // so it is visible before JS boots, then the client router takes over.\n let merged = clientTemplate\n if (merged.includes('<cer-layout-view></cer-layout-view>')) {\n merged = merged.replace('<cer-layout-view></cer-layout-view>',\n '<cer-layout-view>' + ssrBodyContent + '</cer-layout-view>')\n } else if (merged.includes('<div id=\"app\"></div>')) {\n merged = merged.replace('<div id=\"app\"></div>',\n '<div id=\"app\">' + ssrBodyContent + '</div>')\n }\n const headAdditions = headParts.filter(Boolean).join('\\n')\n if (headAdditions) {\n // If SSR provides a <title>, replace the client template's <title> so the\n // SSR title wins (client template title is the fallback default).\n if (headAdditions.includes('<title>')) {\n merged = merged.replace(/<title>[^<]*<\\/title>/, '')\n }\n merged = merged.replace('</head>', headAdditions + '\\n</head>')\n }\n return merged\n}\n\n// Per-request async setup: initialize a fresh router, resolve the matched\n// route and layout, pre-load the page module, and call the data loader.\n// Loader data is scoped to the current AsyncLocalStorage context via enterWith()\n// so concurrent renders never share state.\nconst _prepareRequest = async (req) => {\n await _pluginsReady\n const router = initRouter({ routes, initialUrl: req.url ?? '/' })\n const current = router.getCurrent()\n const { route, params } = router.matchRoute(current.path)\n\n // Pre-load the page module so we can embed the component tag directly.\n // This avoids the async router-view (which injects content via script tags\n // and breaks Declarative Shadow DOM on initial parse).\n let pageVnode = { tag: 'div', props: {}, children: [] }\n let head\n if (route?.load) {\n try {\n const mod = await route.load()\n const pageTag = mod.default\n if (pageTag) {\n pageVnode = { tag: pageTag, props: { attrs: { ...params } }, children: [] }\n }\n if (typeof mod.loader === 'function') {\n const query = current.query ?? {}\n const data = await mod.loader({ params, query, req })\n if (data !== undefined && data !== null) {\n // enterWith() scopes the value to the current async context so\n // concurrent renders (SSG concurrency > 1) never share data.\n _cerDataStore.enterWith(data)\n head = `<script>window.__CER_DATA__ = ${JSON.stringify(data)}</script>`\n }\n }\n } catch {\n // Non-fatal: loader errors fall back to an empty page; client will refetch.\n }\n }\n\n // Resolve layout chain: nested layouts (meta.layoutChain) or single layout.\n const chain = route?.meta?.layoutChain\n ? route.meta.layoutChain\n : [route?.meta?.layout ?? 'default']\n\n // Wrap pageVnode in the layout chain from innermost to outermost.\n let vnode = pageVnode\n for (let i = chain.length - 1; i >= 0; i--) {\n const tag = layouts[chain[i]]\n if (tag) vnode = { tag, props: {}, children: [vnode] }\n }\n\n return { vnode, router, head }\n}\n\nexport const handler = async (req, res) => {\n await _cerDataStore.run(null, async () => {\n const { vnode, router, head } = await _prepareRequest(req)\n\n // Begin collecting useHead() calls made during the synchronous render pass.\n // IMPORTANT: the stream's start() function runs synchronously on construction,\n // so ALL useHead() calls happen before the stream object is returned. We must\n // call endHeadCollection() immediately \u2014 before any await \u2014 to avoid a race\n // window where a concurrent request (e.g. SSG concurrency > 1) resets the\n // shared globalThis collector while this handler is suspended at an await.\n beginHeadCollection()\n\n // dsdPolyfill: false \u2014 we inject the polyfill manually after merging so it\n // lands at the end of <body>, not inside <cer-layout-view> light DOM where\n // scripts may not execute.\n // The first chunk from the stream is the full synchronous render. Subsequent\n // chunks are async component swap scripts streamed as they resolve.\n const stream = renderToStreamWithJITCSSDSD(vnode, { dsdPolyfill: false, router })\n\n // Collect head tags synchronously \u2014 all useHead() calls have already fired\n // inside the stream constructor's start() before it returned.\n const headTags = serializeHeadTags(endHeadCollection())\n\n const reader = stream.getReader()\n\n // Read the first (synchronous) chunk.\n const { value: firstChunk = '' } = await reader.read()\n\n // Merge loader data script + useHead() tags into the document head.\n const headContent = [head, headTags].filter(Boolean).join('\\n')\n\n // Wrap the rendered body in a full HTML document and inject the head additions\n // (loader data script, useHead() tags, JIT styles). No polyfill in body yet.\n const ssrHtml = `<!DOCTYPE html><html><head>${headContent}</head><body>${firstChunk}</body></html>`\n\n const merged = _clientTemplate\n ? _mergeWithClientTemplate(ssrHtml, _clientTemplate)\n : ssrHtml\n\n // Split at </body> so async swap scripts and the DSD polyfill can be streamed\n // in before the document is closed.\n const bodyCloseIdx = merged.lastIndexOf('</body>')\n const beforeBodyClose = bodyCloseIdx >= 0 ? merged.slice(0, bodyCloseIdx) : merged\n const fromBodyClose = bodyCloseIdx >= 0 ? merged.slice(bodyCloseIdx) : ''\n\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.setHeader('Transfer-Encoding', 'chunked')\n res.write(beforeBodyClose)\n\n // Stream async component swap scripts through as-is.\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n res.write(value)\n }\n\n // Inject DSD polyfill immediately before </body>, then close the document.\n res.end(DSD_POLYFILL_SCRIPT + fromBodyClose)\n })\n}\n\nexport { apiRoutes, plugins, layouts, routes }\nexport default handler\n";
|
|
15
15
|
//# sourceMappingURL=entry-server-template.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-server-template.d.ts","sourceRoot":"","sources":["../../src/runtime/entry-server-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,
|
|
1
|
+
{"version":3,"file":"entry-server-template.d.ts","sourceRoot":"","sources":["../../src/runtime/entry-server-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,6hVAmOjC,CAAA"}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Key features:
|
|
9
9
|
* - AsyncLocalStorage for race-condition-free concurrent renders (SSG concurrency > 1)
|
|
10
|
-
* - Declarative Shadow DOM via
|
|
10
|
+
* - Declarative Shadow DOM via renderToStreamWithJITCSSDSD (always on, streamed)
|
|
11
11
|
* - useHead() support via beginHeadCollection / endHeadCollection
|
|
12
12
|
* - DSD polyfill injected at end of <body> after client-template merge
|
|
13
13
|
*/
|
|
@@ -23,7 +23,7 @@ import plugins from 'virtual:cer-plugins'
|
|
|
23
23
|
import apiRoutes from 'virtual:cer-server-api'
|
|
24
24
|
import { runtimeConfig } from 'virtual:cer-app-config'
|
|
25
25
|
import { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'
|
|
26
|
-
import { registerEntityMap,
|
|
26
|
+
import { registerEntityMap, renderToStreamWithJITCSSDSD, DSD_POLYFILL_SCRIPT } from '@jasonshimmy/custom-elements-runtime/ssr'
|
|
27
27
|
import entitiesJson from '@jasonshimmy/custom-elements-runtime/entities.json'
|
|
28
28
|
import { initRouter } from '@jasonshimmy/custom-elements-runtime/router'
|
|
29
29
|
import { beginHeadCollection, endHeadCollection, serializeHeadTags, initRuntimeConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
@@ -180,38 +180,59 @@ export const handler = async (req, res) => {
|
|
|
180
180
|
const { vnode, router, head } = await _prepareRequest(req)
|
|
181
181
|
|
|
182
182
|
// Begin collecting useHead() calls made during the synchronous render pass.
|
|
183
|
+
// IMPORTANT: the stream's start() function runs synchronously on construction,
|
|
184
|
+
// so ALL useHead() calls happen before the stream object is returned. We must
|
|
185
|
+
// call endHeadCollection() immediately — before any await — to avoid a race
|
|
186
|
+
// window where a concurrent request (e.g. SSG concurrency > 1) resets the
|
|
187
|
+
// shared globalThis collector while this handler is suspended at an await.
|
|
183
188
|
beginHeadCollection()
|
|
184
189
|
|
|
185
190
|
// dsdPolyfill: false — we inject the polyfill manually after merging so it
|
|
186
191
|
// lands at the end of <body>, not inside <cer-layout-view> light DOM where
|
|
187
192
|
// scripts may not execute.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
})
|
|
193
|
+
// The first chunk from the stream is the full synchronous render. Subsequent
|
|
194
|
+
// chunks are async component swap scripts streamed as they resolve.
|
|
195
|
+
const stream = renderToStreamWithJITCSSDSD(vnode, { dsdPolyfill: false, router })
|
|
192
196
|
|
|
193
|
-
// Collect
|
|
197
|
+
// Collect head tags synchronously — all useHead() calls have already fired
|
|
198
|
+
// inside the stream constructor's start() before it returned.
|
|
194
199
|
const headTags = serializeHeadTags(endHeadCollection())
|
|
195
200
|
|
|
201
|
+
const reader = stream.getReader()
|
|
202
|
+
|
|
203
|
+
// Read the first (synchronous) chunk.
|
|
204
|
+
const { value: firstChunk = '' } = await reader.read()
|
|
205
|
+
|
|
196
206
|
// Merge loader data script + useHead() tags into the document head.
|
|
197
207
|
const headContent = [head, headTags].filter(Boolean).join('\\n')
|
|
198
208
|
|
|
199
209
|
// Wrap the rendered body in a full HTML document and inject the head additions
|
|
200
210
|
// (loader data script, useHead() tags, JIT styles). No polyfill in body yet.
|
|
201
|
-
const ssrHtml = \`<!DOCTYPE html><html><head>\${headContent}</head><body>\${
|
|
211
|
+
const ssrHtml = \`<!DOCTYPE html><html><head>\${headContent}</head><body>\${firstChunk}</body></html>\`
|
|
202
212
|
|
|
203
|
-
|
|
213
|
+
const merged = _clientTemplate
|
|
204
214
|
? _mergeWithClientTemplate(ssrHtml, _clientTemplate)
|
|
205
215
|
: ssrHtml
|
|
206
216
|
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
217
|
+
// Split at </body> so async swap scripts and the DSD polyfill can be streamed
|
|
218
|
+
// in before the document is closed.
|
|
219
|
+
const bodyCloseIdx = merged.lastIndexOf('</body>')
|
|
220
|
+
const beforeBodyClose = bodyCloseIdx >= 0 ? merged.slice(0, bodyCloseIdx) : merged
|
|
221
|
+
const fromBodyClose = bodyCloseIdx >= 0 ? merged.slice(bodyCloseIdx) : ''
|
|
212
222
|
|
|
213
223
|
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
|
214
|
-
res.
|
|
224
|
+
res.setHeader('Transfer-Encoding', 'chunked')
|
|
225
|
+
res.write(beforeBodyClose)
|
|
226
|
+
|
|
227
|
+
// Stream async component swap scripts through as-is.
|
|
228
|
+
while (true) {
|
|
229
|
+
const { value, done } = await reader.read()
|
|
230
|
+
if (done) break
|
|
231
|
+
res.write(value)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Inject DSD polyfill immediately before </body>, then close the document.
|
|
235
|
+
res.end(DSD_POLYFILL_SCRIPT + fromBodyClose)
|
|
215
236
|
})
|
|
216
237
|
}
|
|
217
238
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-server-template.js","sourceRoot":"","sources":["../../src/runtime/entry-server-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG
|
|
1
|
+
{"version":3,"file":"entry-server-template.js","sourceRoot":"","sources":["../../src/runtime/entry-server-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmOpC,CAAA"}
|
package/docs/rendering-modes.md
CHANGED
|
@@ -63,9 +63,9 @@ The server renders HTML for each request. Uses Declarative Shadow DOM (DSD) to e
|
|
|
63
63
|
3. API route handlers run if the URL matches `/api/`
|
|
64
64
|
4. For HTML requests, the router matches the URL to a page
|
|
65
65
|
5. The page's `loader` is called (if present)
|
|
66
|
-
6. The component tree is rendered to HTML with Declarative Shadow DOM via `
|
|
67
|
-
7. `useHead()` calls are collected and injected before `</head>`
|
|
68
|
-
8. The rendered HTML is merged with the Vite client bundle shell and
|
|
66
|
+
6. The component tree is rendered to HTML with Declarative Shadow DOM via `renderToStreamWithJITCSSDSD`; the synchronous first chunk is flushed immediately, then async component swap scripts follow as they resolve
|
|
67
|
+
7. `useHead()` calls are collected from the synchronous render and injected before `</head>`
|
|
68
|
+
8. The rendered HTML is merged with the Vite client bundle shell and streamed as a chunked response
|
|
69
69
|
|
|
70
70
|
### Build output
|
|
71
71
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jasonshimmy/vite-plugin-cer-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Nuxt-style meta-framework for @jasonshimmy/custom-elements-runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"cypress:open": "cypress open"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
|
-
"@jasonshimmy/custom-elements-runtime": ">=3.
|
|
71
|
+
"@jasonshimmy/custom-elements-runtime": ">=3.4.0",
|
|
72
72
|
"vite": ">=5.0.0"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"pathe": "^2.0.3"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
|
-
"@jasonshimmy/custom-elements-runtime": "^3.
|
|
81
|
+
"@jasonshimmy/custom-elements-runtime": "^3.4.0",
|
|
82
82
|
"@types/node": "^25.5.0",
|
|
83
83
|
"@vitest/coverage-v8": "^4.1.0",
|
|
84
84
|
"cypress": "^15.12.0",
|
|
@@ -97,6 +97,52 @@ describe('buildSSG — renderPath success (real server bundle)', () => {
|
|
|
97
97
|
expect(manifest.paths).toHaveLength(2)
|
|
98
98
|
})
|
|
99
99
|
|
|
100
|
+
it('captures HTML from a streaming handler that calls write() then end()', async () => {
|
|
101
|
+
// Simulate the renderToStreamWithJITCSSDSD-based handler which calls
|
|
102
|
+
// res.write(firstChunk) for sync content and res.end(polyfill + tail).
|
|
103
|
+
// Use a completely separate tmpdir with a unique path so Node's native ESM
|
|
104
|
+
// import cache cannot return a previously-loaded module for the same URL.
|
|
105
|
+
const streamRoot = join(tmpdir(), `cer-ssg-stream-${Date.now()}`)
|
|
106
|
+
const streamServerDir = join(streamRoot, 'dist', 'server')
|
|
107
|
+
mkdirSync(streamServerDir, { recursive: true })
|
|
108
|
+
writeFileSync(
|
|
109
|
+
join(streamServerDir, 'server.js'),
|
|
110
|
+
`export const handler = async (req, res) => {
|
|
111
|
+
res.setHeader('Content-Type', 'text/html');
|
|
112
|
+
res.setHeader('Transfer-Encoding', 'chunked');
|
|
113
|
+
res.write('<html><head></head>');
|
|
114
|
+
res.write('<body>streamed</body>');
|
|
115
|
+
res.end('</html>');
|
|
116
|
+
};
|
|
117
|
+
export const apiRoutes = [];
|
|
118
|
+
export const plugins = [];
|
|
119
|
+
export const layouts = {};
|
|
120
|
+
`,
|
|
121
|
+
'utf-8',
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
await vi.resetModules()
|
|
125
|
+
const { buildSSG } = await import('../../plugin/build-ssg.js')
|
|
126
|
+
const config = {
|
|
127
|
+
root: streamRoot,
|
|
128
|
+
srcDir: join(streamRoot, 'app'),
|
|
129
|
+
pagesDir: join(streamRoot, 'app', 'pages'),
|
|
130
|
+
mode: 'ssg',
|
|
131
|
+
ssg: { routes: ['/'], concurrency: 1 },
|
|
132
|
+
} as unknown as ResolvedCerConfig
|
|
133
|
+
await buildSSG(config)
|
|
134
|
+
|
|
135
|
+
const { readFileSync, existsSync } = await import('node:fs')
|
|
136
|
+
const outPath = join(streamRoot, 'dist', 'index.html')
|
|
137
|
+
expect(existsSync(outPath)).toBe(true)
|
|
138
|
+
const html = readFileSync(outPath, 'utf-8')
|
|
139
|
+
expect(html).toContain('<html><head></head>')
|
|
140
|
+
expect(html).toContain('<body>streamed</body>')
|
|
141
|
+
expect(html).toContain('</html>')
|
|
142
|
+
|
|
143
|
+
rmSync(streamRoot, { recursive: true, force: true })
|
|
144
|
+
})
|
|
145
|
+
|
|
100
146
|
it('uses cached _serverMod on second renderPath call (no double import)', async () => {
|
|
101
147
|
// In a fresh module instance, render two paths sequentially.
|
|
102
148
|
// The second renderPath call hits the !_serverMod === false branch (cache).
|
|
@@ -33,8 +33,8 @@ describe('entry-server-template (ENTRY_SERVER_TEMPLATE content)', () => {
|
|
|
33
33
|
expect(src).toContain('@jasonshimmy/custom-elements-runtime')
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
it('imports
|
|
37
|
-
expect(src).toContain('
|
|
36
|
+
it('imports renderToStreamWithJITCSSDSD and DSD_POLYFILL_SCRIPT from ssr subpath', () => {
|
|
37
|
+
expect(src).toContain('renderToStreamWithJITCSSDSD')
|
|
38
38
|
expect(src).toContain('DSD_POLYFILL_SCRIPT')
|
|
39
39
|
expect(src).toContain('custom-elements-runtime/ssr')
|
|
40
40
|
})
|
|
@@ -94,9 +94,17 @@ describe('entry-server-template (ENTRY_SERVER_TEMPLATE content)', () => {
|
|
|
94
94
|
expect(src).toContain('_prepareRequest')
|
|
95
95
|
})
|
|
96
96
|
|
|
97
|
-
it('
|
|
97
|
+
it('calls endHeadCollection() synchronously before any await to avoid race conditions', () => {
|
|
98
98
|
expect(src).toContain('beginHeadCollection()')
|
|
99
99
|
expect(src).toContain('endHeadCollection()')
|
|
100
|
+
// endHeadCollection must come before reader.read() so concurrent requests
|
|
101
|
+
// (SSG concurrency > 1) cannot reset the shared globalThis collector between
|
|
102
|
+
// beginHeadCollection and endHeadCollection.
|
|
103
|
+
const endIdx = src.indexOf('endHeadCollection()')
|
|
104
|
+
const readIdx = src.indexOf('reader.read()')
|
|
105
|
+
expect(endIdx).toBeGreaterThan(-1)
|
|
106
|
+
expect(readIdx).toBeGreaterThan(-1)
|
|
107
|
+
expect(endIdx).toBeLessThan(readIdx)
|
|
100
108
|
})
|
|
101
109
|
|
|
102
110
|
it('passes dsdPolyfill: false to suppress inline polyfill', () => {
|
|
@@ -104,8 +112,8 @@ describe('entry-server-template (ENTRY_SERVER_TEMPLATE content)', () => {
|
|
|
104
112
|
})
|
|
105
113
|
|
|
106
114
|
it('injects DSD_POLYFILL_SCRIPT before </body>', () => {
|
|
107
|
-
expect(src).toContain("
|
|
108
|
-
expect(src).toContain('DSD_POLYFILL_SCRIPT')
|
|
115
|
+
expect(src).toContain("lastIndexOf('</body>')")
|
|
116
|
+
expect(src).toContain('DSD_POLYFILL_SCRIPT + fromBodyClose')
|
|
109
117
|
})
|
|
110
118
|
|
|
111
119
|
it('merges SSR html with client template when available', () => {
|
|
@@ -124,4 +132,14 @@ describe('entry-server-template (ENTRY_SERVER_TEMPLATE content)', () => {
|
|
|
124
132
|
it('sets Content-Type header on response', () => {
|
|
125
133
|
expect(src).toContain('text/html; charset=utf-8')
|
|
126
134
|
})
|
|
135
|
+
|
|
136
|
+
it('sets Transfer-Encoding: chunked header for streaming', () => {
|
|
137
|
+
expect(src).toContain('Transfer-Encoding')
|
|
138
|
+
expect(src).toContain('chunked')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('reads the stream using a reader loop', () => {
|
|
142
|
+
expect(src).toContain('stream.getReader()')
|
|
143
|
+
expect(src).toContain('reader.read()')
|
|
144
|
+
})
|
|
127
145
|
})
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
"preview": "cer-app preview"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jasonshimmy/custom-elements-runtime": "^3.
|
|
11
|
+
"@jasonshimmy/custom-elements-runtime": "^3.4.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"vite": "^8.0.1",
|
|
15
|
-
"@jasonshimmy/vite-plugin-cer-app": "^0.
|
|
15
|
+
"@jasonshimmy/vite-plugin-cer-app": "^0.6.0",
|
|
16
16
|
"typescript": "^5.9.3"
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"preview": "cer-app preview"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@jasonshimmy/custom-elements-runtime": "^3.
|
|
12
|
+
"@jasonshimmy/custom-elements-runtime": "^3.4.0"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"vite": "^8.0.1",
|
|
16
|
-
"@jasonshimmy/vite-plugin-cer-app": "^0.
|
|
16
|
+
"@jasonshimmy/vite-plugin-cer-app": "^0.6.0",
|
|
17
17
|
"typescript": "^5.9.3"
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
"preview": "cer-app preview --ssr"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jasonshimmy/custom-elements-runtime": "^3.
|
|
11
|
+
"@jasonshimmy/custom-elements-runtime": "^3.4.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"vite": "^8.0.1",
|
|
15
|
-
"@jasonshimmy/vite-plugin-cer-app": "^0.
|
|
15
|
+
"@jasonshimmy/vite-plugin-cer-app": "^0.6.0",
|
|
16
16
|
"typescript": "^5.9.3"
|
|
17
17
|
}
|
|
18
18
|
}
|
package/src/plugin/build-ssg.ts
CHANGED
|
@@ -131,12 +131,14 @@ async function renderPath(
|
|
|
131
131
|
|
|
132
132
|
// Mock req/res for the Express-style handler.
|
|
133
133
|
// The handler internally merges with dist/client/index.html, so we just
|
|
134
|
-
// capture whatever it ends with.
|
|
134
|
+
// capture whatever it writes/ends with.
|
|
135
135
|
const mockReq = { url: path, headers: {} }
|
|
136
136
|
return new Promise<string>((resolve, reject) => {
|
|
137
|
+
const chunks: string[] = []
|
|
137
138
|
const mockRes = {
|
|
138
139
|
setHeader: () => {},
|
|
139
|
-
|
|
140
|
+
write: (chunk: string) => { chunks.push(chunk) },
|
|
141
|
+
end: (body?: string) => resolve(chunks.join('') + (body ?? '')),
|
|
140
142
|
}
|
|
141
143
|
;(handlerFn as (req: unknown, res: unknown) => Promise<void>)(mockReq, mockRes).catch(reject)
|
|
142
144
|
})
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Key features:
|
|
9
9
|
* - AsyncLocalStorage for race-condition-free concurrent renders (SSG concurrency > 1)
|
|
10
|
-
* - Declarative Shadow DOM via
|
|
10
|
+
* - Declarative Shadow DOM via renderToStreamWithJITCSSDSD (always on, streamed)
|
|
11
11
|
* - useHead() support via beginHeadCollection / endHeadCollection
|
|
12
12
|
* - DSD polyfill injected at end of <body> after client-template merge
|
|
13
13
|
*/
|
|
@@ -23,7 +23,7 @@ import plugins from 'virtual:cer-plugins'
|
|
|
23
23
|
import apiRoutes from 'virtual:cer-server-api'
|
|
24
24
|
import { runtimeConfig } from 'virtual:cer-app-config'
|
|
25
25
|
import { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'
|
|
26
|
-
import { registerEntityMap,
|
|
26
|
+
import { registerEntityMap, renderToStreamWithJITCSSDSD, DSD_POLYFILL_SCRIPT } from '@jasonshimmy/custom-elements-runtime/ssr'
|
|
27
27
|
import entitiesJson from '@jasonshimmy/custom-elements-runtime/entities.json'
|
|
28
28
|
import { initRouter } from '@jasonshimmy/custom-elements-runtime/router'
|
|
29
29
|
import { beginHeadCollection, endHeadCollection, serializeHeadTags, initRuntimeConfig } from '@jasonshimmy/vite-plugin-cer-app/composables'
|
|
@@ -180,38 +180,59 @@ export const handler = async (req, res) => {
|
|
|
180
180
|
const { vnode, router, head } = await _prepareRequest(req)
|
|
181
181
|
|
|
182
182
|
// Begin collecting useHead() calls made during the synchronous render pass.
|
|
183
|
+
// IMPORTANT: the stream's start() function runs synchronously on construction,
|
|
184
|
+
// so ALL useHead() calls happen before the stream object is returned. We must
|
|
185
|
+
// call endHeadCollection() immediately — before any await — to avoid a race
|
|
186
|
+
// window where a concurrent request (e.g. SSG concurrency > 1) resets the
|
|
187
|
+
// shared globalThis collector while this handler is suspended at an await.
|
|
183
188
|
beginHeadCollection()
|
|
184
189
|
|
|
185
190
|
// dsdPolyfill: false — we inject the polyfill manually after merging so it
|
|
186
191
|
// lands at the end of <body>, not inside <cer-layout-view> light DOM where
|
|
187
192
|
// scripts may not execute.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
})
|
|
193
|
+
// The first chunk from the stream is the full synchronous render. Subsequent
|
|
194
|
+
// chunks are async component swap scripts streamed as they resolve.
|
|
195
|
+
const stream = renderToStreamWithJITCSSDSD(vnode, { dsdPolyfill: false, router })
|
|
192
196
|
|
|
193
|
-
// Collect
|
|
197
|
+
// Collect head tags synchronously — all useHead() calls have already fired
|
|
198
|
+
// inside the stream constructor's start() before it returned.
|
|
194
199
|
const headTags = serializeHeadTags(endHeadCollection())
|
|
195
200
|
|
|
201
|
+
const reader = stream.getReader()
|
|
202
|
+
|
|
203
|
+
// Read the first (synchronous) chunk.
|
|
204
|
+
const { value: firstChunk = '' } = await reader.read()
|
|
205
|
+
|
|
196
206
|
// Merge loader data script + useHead() tags into the document head.
|
|
197
207
|
const headContent = [head, headTags].filter(Boolean).join('\\n')
|
|
198
208
|
|
|
199
209
|
// Wrap the rendered body in a full HTML document and inject the head additions
|
|
200
210
|
// (loader data script, useHead() tags, JIT styles). No polyfill in body yet.
|
|
201
|
-
const ssrHtml = \`<!DOCTYPE html><html><head>\${headContent}</head><body>\${
|
|
211
|
+
const ssrHtml = \`<!DOCTYPE html><html><head>\${headContent}</head><body>\${firstChunk}</body></html>\`
|
|
202
212
|
|
|
203
|
-
|
|
213
|
+
const merged = _clientTemplate
|
|
204
214
|
? _mergeWithClientTemplate(ssrHtml, _clientTemplate)
|
|
205
215
|
: ssrHtml
|
|
206
216
|
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
217
|
+
// Split at </body> so async swap scripts and the DSD polyfill can be streamed
|
|
218
|
+
// in before the document is closed.
|
|
219
|
+
const bodyCloseIdx = merged.lastIndexOf('</body>')
|
|
220
|
+
const beforeBodyClose = bodyCloseIdx >= 0 ? merged.slice(0, bodyCloseIdx) : merged
|
|
221
|
+
const fromBodyClose = bodyCloseIdx >= 0 ? merged.slice(bodyCloseIdx) : ''
|
|
212
222
|
|
|
213
223
|
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
|
214
|
-
res.
|
|
224
|
+
res.setHeader('Transfer-Encoding', 'chunked')
|
|
225
|
+
res.write(beforeBodyClose)
|
|
226
|
+
|
|
227
|
+
// Stream async component swap scripts through as-is.
|
|
228
|
+
while (true) {
|
|
229
|
+
const { value, done } = await reader.read()
|
|
230
|
+
if (done) break
|
|
231
|
+
res.write(value)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Inject DSD polyfill immediately before </body>, then close the document.
|
|
235
|
+
res.end(DSD_POLYFILL_SCRIPT + fromBodyClose)
|
|
215
236
|
})
|
|
216
237
|
}
|
|
217
238
|
|