@jasonshimmy/vite-plugin-cer-app 0.4.5 → 0.4.6

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 (47) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/commits.txt +1 -1
  3. package/dist/cli/create/index.js +22 -7
  4. package/dist/cli/create/index.js.map +1 -1
  5. package/dist/cli/create/templates/{ssr → shared}/app/pages/index.ts.tpl +1 -1
  6. package/dist/plugin/build-ssr.d.ts.map +1 -1
  7. package/dist/plugin/build-ssr.js +2 -205
  8. package/dist/plugin/build-ssr.js.map +1 -1
  9. package/dist/runtime/entry-server-template.d.ts +10 -4
  10. package/dist/runtime/entry-server-template.d.ts.map +1 -1
  11. package/dist/runtime/entry-server-template.js +54 -64
  12. package/dist/runtime/entry-server-template.js.map +1 -1
  13. package/package.json +1 -1
  14. package/src/__tests__/plugin/build-ssr.test.ts +3 -120
  15. package/src/__tests__/plugin/entry-server-template.test.ts +106 -3
  16. package/src/cli/create/index.ts +21 -7
  17. package/src/cli/create/templates/{spa → shared}/app/pages/index.ts.tpl +1 -1
  18. package/src/plugin/build-ssr.ts +2 -205
  19. package/src/runtime/entry-server-template.ts +54 -64
  20. package/dist/cli/create/templates/spa/app/pages/index.ts.tpl +0 -8
  21. package/dist/cli/create/templates/ssg/app/pages/index.ts.tpl +0 -8
  22. package/dist/cli/create/templates/ssr/.gitignore.tpl +0 -25
  23. package/dist/cli/create/templates/ssr/app/layouts/default.ts.tpl +0 -15
  24. package/dist/cli/create/templates/ssr/index.html.tpl +0 -12
  25. package/dist/cli/create/templates/ssr/tsconfig.json.tpl +0 -3
  26. package/src/cli/create/templates/spa/.gitignore.tpl +0 -25
  27. package/src/cli/create/templates/spa/app/layouts/default.ts.tpl +0 -15
  28. package/src/cli/create/templates/spa/index.html.tpl +0 -12
  29. package/src/cli/create/templates/spa/tsconfig.json.tpl +0 -3
  30. package/src/cli/create/templates/ssg/.gitignore.tpl +0 -25
  31. package/src/cli/create/templates/ssg/app/layouts/default.ts.tpl +0 -15
  32. package/src/cli/create/templates/ssg/app/pages/index.ts.tpl +0 -8
  33. package/src/cli/create/templates/ssg/index.html.tpl +0 -12
  34. package/src/cli/create/templates/ssg/tsconfig.json.tpl +0 -3
  35. package/src/cli/create/templates/ssr/.gitignore.tpl +0 -25
  36. package/src/cli/create/templates/ssr/app/layouts/default.ts.tpl +0 -15
  37. package/src/cli/create/templates/ssr/app/pages/index.ts.tpl +0 -8
  38. package/src/cli/create/templates/ssr/index.html.tpl +0 -12
  39. package/src/cli/create/templates/ssr/tsconfig.json.tpl +0 -3
  40. /package/dist/cli/create/templates/{spa → shared}/.gitignore.tpl +0 -0
  41. /package/dist/cli/create/templates/{spa → shared}/app/layouts/default.ts.tpl +0 -0
  42. /package/dist/cli/create/templates/{spa → shared}/index.html.tpl +0 -0
  43. /package/dist/cli/create/templates/{spa → shared}/tsconfig.json.tpl +0 -0
  44. /package/{dist/cli/create/templates/ssg → src/cli/create/templates/shared}/.gitignore.tpl +0 -0
  45. /package/{dist/cli/create/templates/ssg → src/cli/create/templates/shared}/app/layouts/default.ts.tpl +0 -0
  46. /package/{dist/cli/create/templates/ssg → src/cli/create/templates/shared}/index.html.tpl +0 -0
  47. /package/{dist/cli/create/templates/ssg → src/cli/create/templates/shared}/tsconfig.json.tpl +0 -0
@@ -1,10 +1,7 @@
1
1
  import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
2
- import { readFileSync } from 'node:fs'
3
- import { resolve } from 'pathe'
4
2
 
5
- // We test the server entry code generation by importing just that function.
6
- // The `buildSSR` function itself invokes Vite's `build` API which we don't
7
- // need to exercise in unit tests (it's an integration concern).
3
+ // We test the SSR build pipeline by exercising buildSSR and its helpers.
4
+ // Template content is tested in entry-server-template.test.ts.
8
5
  vi.mock('vite', () => ({ build: vi.fn().mockResolvedValue(undefined) }))
9
6
  vi.mock('../../plugin/generated-dir.js', () => ({
10
7
  writeGeneratedDir: vi.fn(),
@@ -20,8 +17,6 @@ vi.mock('node:fs', async (importOriginal) => {
20
17
 
21
18
  import type { ResolvedCerConfig } from '../../plugin/dev-server.js'
22
19
 
23
- // Build a minimal ResolvedCerConfig so we can call generateServerEntryCode
24
- // without spinning up a real Vite build.
25
20
  function makeConfig(overrides: Partial<ResolvedCerConfig> = {}): ResolvedCerConfig {
26
21
  return {
27
22
  root: '/project',
@@ -32,118 +27,6 @@ function makeConfig(overrides: Partial<ResolvedCerConfig> = {}): ResolvedCerConf
32
27
  } as unknown as ResolvedCerConfig
33
28
  }
34
29
 
35
- describe('build-ssr generateServerEntryCode (template content)', () => {
36
- // Read the source of build-ssr.ts to assert it contains the expected
37
- // generated code strings. This is intentionally coarse-grained:
38
- // we check that the template emits the right imports, exports, and
39
- // structural elements rather than testing every character.
40
- const src = readFileSync(
41
- resolve(import.meta.dirname, '../../plugin/build-ssr.ts'),
42
- 'utf-8',
43
- )
44
-
45
- it('template imports registerBuiltinComponents from custom-elements-runtime', () => {
46
- expect(src).toContain('registerBuiltinComponents')
47
- })
48
-
49
- it('template imports renderToStringWithJITCSS from ssr subpath', () => {
50
- expect(src).toContain('renderToStringWithJITCSS')
51
- expect(src).toContain('custom-elements-runtime/ssr')
52
- })
53
-
54
- it('template imports initRouter from router subpath', () => {
55
- expect(src).toContain('initRouter')
56
- expect(src).toContain('custom-elements-runtime/router')
57
- })
58
-
59
- it('template loads client index.html for merging', () => {
60
- expect(src).toContain('_clientTemplate')
61
- expect(src).toContain('../client/index.html')
62
- })
63
-
64
- it('template defines _mergeWithClientTemplate helper', () => {
65
- expect(src).toContain('_mergeWithClientTemplate')
66
- })
67
-
68
- it('template defines _prepareRequest async function', () => {
69
- expect(src).toContain('_prepareRequest')
70
- })
71
-
72
- it('template exports handler as both named and default export', () => {
73
- expect(src).toContain('export const handler')
74
- expect(src).toContain('export default handler')
75
- })
76
-
77
- it('template exports apiRoutes, plugins, and layouts', () => {
78
- expect(src).toContain('export { apiRoutes, plugins, layouts }')
79
- })
80
-
81
- it('template sets globalThis.__CER_DATA__ synchronously before render', () => {
82
- expect(src).toContain('globalThis).__CER_DATA__ = loaderData')
83
- })
84
-
85
- it('template deletes __CER_DATA__ after render', () => {
86
- expect(src).toContain('delete (globalThis).__CER_DATA__')
87
- })
88
-
89
- it('template uses renderToStringWithJITCSSDSD (dsd always on)', () => {
90
- expect(src).toContain('renderToStringWithJITCSSDSD')
91
- })
92
-
93
- it('template passes dsdPolyfill: false to suppress inline polyfill', () => {
94
- expect(src).toContain('dsdPolyfill: false')
95
- })
96
-
97
- it('template calls registerEntityMap with entities.json', () => {
98
- expect(src).toContain('registerEntityMap(entitiesJson)')
99
- expect(src).toContain('entities.json')
100
- })
101
-
102
- it('template imports DSD_POLYFILL_SCRIPT and injects before </body>', () => {
103
- expect(src).toContain('DSD_POLYFILL_SCRIPT')
104
- expect(src).toContain("finalHtml.replace('</body>'")
105
- })
106
-
107
- it('template merges SSR html with client template when available', () => {
108
- expect(src).toContain('_clientTemplate')
109
- expect(src).toContain('_mergeWithClientTemplate(ssrHtml, _clientTemplate)')
110
- })
111
-
112
- it('template reads virtual:cer-routes', () => {
113
- expect(src).toContain('virtual:cer-routes')
114
- })
115
-
116
- it('template reads virtual:cer-layouts', () => {
117
- expect(src).toContain('virtual:cer-layouts')
118
- })
119
-
120
- it('template reads virtual:cer-plugins', () => {
121
- expect(src).toContain('virtual:cer-plugins')
122
- })
123
-
124
- it('template reads virtual:cer-server-api', () => {
125
- expect(src).toContain('virtual:cer-server-api')
126
- })
127
-
128
- it('template reads virtual:cer-components', () => {
129
- expect(src).toContain('virtual:cer-components')
130
- })
131
-
132
- it('sets Content-Type header on response', () => {
133
- expect(src).toContain('text/html; charset=utf-8')
134
- })
135
-
136
- it('template initializes plugins and sets globalThis.__cerPluginProvides', () => {
137
- expect(src).toContain('__cerPluginProvides')
138
- expect(src).toContain('_pluginProvides')
139
- expect(src).toContain('_pluginsReady')
140
- })
141
-
142
- it('template awaits _pluginsReady before handling each request', () => {
143
- expect(src).toContain('await _pluginsReady')
144
- })
145
- })
146
-
147
30
  describe('buildSSR', () => {
148
31
  let buildMock: ReturnType<typeof vi.fn>
149
32
  let buildSSR: (config: ResolvedCerConfig, userConfig?: Record<string, unknown>) => Promise<void>
@@ -285,7 +168,7 @@ describe('buildSSR — virtual server-entry plugin', () => {
285
168
  const plugin = await getServerPlugin()
286
169
  const source = plugin.load('\0virtual:cer-server-entry')
287
170
  expect(typeof source).toBe('string')
288
- expect(source).toContain('AUTO-GENERATED server entry')
171
+ expect(source).toContain('AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app')
289
172
  })
290
173
 
291
174
  it('load returns undefined for other ids', async () => {
@@ -8,17 +8,120 @@ const src = readFileSync(
8
8
  )
9
9
 
10
10
  describe('entry-server-template (ENTRY_SERVER_TEMPLATE content)', () => {
11
- it('template imports plugins from virtual:cer-plugins', () => {
11
+ it('imports virtual:cer-components', () => {
12
+ expect(src).toContain('virtual:cer-components')
13
+ })
14
+
15
+ it('imports virtual:cer-routes', () => {
16
+ expect(src).toContain('virtual:cer-routes')
17
+ })
18
+
19
+ it('imports virtual:cer-layouts', () => {
20
+ expect(src).toContain('virtual:cer-layouts')
21
+ })
22
+
23
+ it('imports virtual:cer-plugins', () => {
12
24
  expect(src).toContain('virtual:cer-plugins')
13
25
  })
14
26
 
15
- it('template initializes plugins and sets globalThis.__cerPluginProvides', () => {
27
+ it('imports virtual:cer-server-api', () => {
28
+ expect(src).toContain('virtual:cer-server-api')
29
+ })
30
+
31
+ it('imports registerBuiltinComponents from custom-elements-runtime', () => {
32
+ expect(src).toContain('registerBuiltinComponents')
33
+ expect(src).toContain('@jasonshimmy/custom-elements-runtime')
34
+ })
35
+
36
+ it('imports renderToStringWithJITCSSDSD and DSD_POLYFILL_SCRIPT from ssr subpath', () => {
37
+ expect(src).toContain('renderToStringWithJITCSSDSD')
38
+ expect(src).toContain('DSD_POLYFILL_SCRIPT')
39
+ expect(src).toContain('custom-elements-runtime/ssr')
40
+ })
41
+
42
+ it('imports initRouter from router subpath', () => {
43
+ expect(src).toContain('initRouter')
44
+ expect(src).toContain('custom-elements-runtime/router')
45
+ })
46
+
47
+ it('imports beginHeadCollection, endHeadCollection, serializeHeadTags from composables', () => {
48
+ expect(src).toContain('beginHeadCollection')
49
+ expect(src).toContain('endHeadCollection')
50
+ expect(src).toContain('serializeHeadTags')
51
+ expect(src).toContain('vite-plugin-cer-app/composables')
52
+ })
53
+
54
+ it('uses AsyncLocalStorage for request-scoped data isolation', () => {
55
+ expect(src).toContain('AsyncLocalStorage')
56
+ expect(src).toContain('node:async_hooks')
57
+ expect(src).toContain('_cerDataStore')
58
+ expect(src).toContain('__CER_DATA_STORE__')
59
+ })
60
+
61
+ it('scopes each request in _cerDataStore.run()', () => {
62
+ expect(src).toContain('_cerDataStore.run(')
63
+ })
64
+
65
+ it('uses _cerDataStore.enterWith() to scope loader data', () => {
66
+ expect(src).toContain('_cerDataStore.enterWith(data)')
67
+ })
68
+
69
+ it('initializes plugins and sets globalThis.__cerPluginProvides', () => {
16
70
  expect(src).toContain('__cerPluginProvides')
17
71
  expect(src).toContain('_pluginProvides')
18
72
  expect(src).toContain('_pluginsReady')
19
73
  })
20
74
 
21
- it('template awaits _pluginsReady before handling each request', () => {
75
+ it('awaits _pluginsReady before handling each request', () => {
22
76
  expect(src).toContain('await _pluginsReady')
23
77
  })
78
+
79
+ it('calls registerEntityMap with entities.json', () => {
80
+ expect(src).toContain('registerEntityMap(entitiesJson)')
81
+ expect(src).toContain('entities.json')
82
+ })
83
+
84
+ it('loads client index.html for merging', () => {
85
+ expect(src).toContain('_clientTemplate')
86
+ expect(src).toContain('../client/index.html')
87
+ })
88
+
89
+ it('defines _mergeWithClientTemplate helper', () => {
90
+ expect(src).toContain('_mergeWithClientTemplate')
91
+ })
92
+
93
+ it('defines _prepareRequest async function', () => {
94
+ expect(src).toContain('_prepareRequest')
95
+ })
96
+
97
+ it('uses beginHeadCollection / endHeadCollection around the render', () => {
98
+ expect(src).toContain('beginHeadCollection()')
99
+ expect(src).toContain('endHeadCollection()')
100
+ })
101
+
102
+ it('passes dsdPolyfill: false to suppress inline polyfill', () => {
103
+ expect(src).toContain('dsdPolyfill: false')
104
+ })
105
+
106
+ it('injects DSD_POLYFILL_SCRIPT before </body>', () => {
107
+ expect(src).toContain("finalHtml.replace('</body>'")
108
+ expect(src).toContain('DSD_POLYFILL_SCRIPT')
109
+ })
110
+
111
+ it('merges SSR html with client template when available', () => {
112
+ expect(src).toContain('_mergeWithClientTemplate(ssrHtml, _clientTemplate)')
113
+ })
114
+
115
+ it('exports handler as both named and default export', () => {
116
+ expect(src).toContain('export const handler')
117
+ expect(src).toContain('export default handler')
118
+ })
119
+
120
+ it('exports apiRoutes, plugins, layouts, and routes', () => {
121
+ expect(src).toContain('export { apiRoutes, plugins, layouts, routes }')
122
+ })
123
+
124
+ it('sets Content-Type header on response', () => {
125
+ expect(src).toContain('text/html; charset=utf-8')
126
+ })
24
127
  })
@@ -111,10 +111,17 @@ async function writeTemplateFiles(
111
111
  }
112
112
 
113
113
  /**
114
- * Returns the path to the template directory for the given mode.
114
+ * Returns the path to the shared template directory (files common to all modes).
115
+ */
116
+ function getSharedTemplateDir(): string {
117
+ return join(__dirname, 'templates', 'shared')
118
+ }
119
+
120
+ /**
121
+ * Returns the path to the mode-specific template directory.
115
122
  * Resolves relative to the compiled dist output.
116
123
  */
117
- function getTemplateDir(mode: AppMode): string {
124
+ function getModeTemplateDir(mode: AppMode): string {
118
125
  // When running from compiled dist/, templates are in create/templates/
119
126
  // This file is at dist/cli/create/index.js, so templates are at dist/cli/create/templates/
120
127
  return join(__dirname, 'templates', mode)
@@ -148,15 +155,22 @@ async function main(): Promise<void> {
148
155
  }
149
156
  }
150
157
 
151
- // Load template files
152
- const templateDir = getTemplateDir(mode)
158
+ // Load template files: shared first, then mode-specific (mode overrides shared)
159
+ const sharedDir = getSharedTemplateDir()
160
+ const modeDir = getModeTemplateDir(mode)
153
161
 
154
- if (!existsSync(templateDir)) {
162
+ if (!existsSync(sharedDir) && !existsSync(modeDir)) {
155
163
  // Fallback: generate minimal template inline
156
- console.warn(`[create-cer-app] Template directory not found at ${templateDir}, using inline template.`)
164
+ console.warn(`[create-cer-app] Template directory not found at ${modeDir}, using inline template.`)
157
165
  await generateInlineTemplate(targetDir, projectName, mode)
158
166
  } else {
159
- const files = await readTemplateFiles(templateDir)
167
+ const files = new Map<string, string>()
168
+ if (existsSync(sharedDir)) {
169
+ for (const [k, v] of await readTemplateFiles(sharedDir)) files.set(k, v)
170
+ }
171
+ if (existsSync(modeDir)) {
172
+ for (const [k, v] of await readTemplateFiles(modeDir)) files.set(k, v)
173
+ }
160
174
  await writeTemplateFiles(files, targetDir, { projectName })
161
175
  }
162
176
 
@@ -1,7 +1,7 @@
1
1
  component('page-index', () => {
2
2
  return html`
3
3
  <div>
4
- <h1>Welcome to {{projectName}}</h1>
4
+ <h1 class="text-2xl">Welcome to {{projectName}}</h1>
5
5
  <p>Edit <code>app/pages/index.ts</code> to get started.</p>
6
6
  </div>
7
7
  `
@@ -3,6 +3,7 @@ import { join, resolve } from 'pathe'
3
3
  import { existsSync, renameSync } from 'node:fs'
4
4
  import type { ResolvedCerConfig } from './dev-server.js'
5
5
  import { getGeneratedDir, writeGeneratedDir } from './generated-dir.js'
6
+ import { ENTRY_SERVER_TEMPLATE } from '../runtime/entry-server-template.js'
6
7
 
7
8
  /**
8
9
  * Resolves the client build entry point for an SSR/SSG build.
@@ -23,212 +24,8 @@ export function resolveClientEntry(config: ResolvedCerConfig): string {
23
24
  return resolve(config.srcDir, 'app.ts')
24
25
  }
25
26
 
26
- /**
27
- * The server entry template that wires all virtual modules together and
28
- * exports a request handler for Node.js (Express-compatible).
29
- */
30
27
  function generateServerEntryCode(): string {
31
- return `// AUTO-GENERATED server entry by @jasonshimmy/vite-plugin-cer-app
32
- import { readFileSync, existsSync } from 'node:fs'
33
- import { dirname, join } from 'node:path'
34
- import { fileURLToPath } from 'node:url'
35
- import 'virtual:cer-components'
36
- import routes from 'virtual:cer-routes'
37
- import layouts from 'virtual:cer-layouts'
38
- import plugins from 'virtual:cer-plugins'
39
- import apiRoutes from 'virtual:cer-server-api'
40
- import { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'
41
- import { registerEntityMap, renderToStringWithJITCSSDSD, DSD_POLYFILL_SCRIPT } from '@jasonshimmy/custom-elements-runtime/ssr'
42
- import entitiesJson from '@jasonshimmy/custom-elements-runtime/entities.json'
43
- import { initRouter } from '@jasonshimmy/custom-elements-runtime/router'
44
- import { beginHeadCollection, endHeadCollection, serializeHeadTags } from '@jasonshimmy/vite-plugin-cer-app/composables'
45
-
46
- registerBuiltinComponents()
47
-
48
- // Pre-load the full HTML entity map so named entities like &mdash; decode
49
- // correctly during SSR. Without this the bundled runtime falls back to a
50
- // minimal set (&lt;, &gt;, &amp; …) and re-escapes everything else.
51
- registerEntityMap(entitiesJson)
52
-
53
- // Run plugins once at server startup so their provide() values are available
54
- // to useInject() during every SSR/SSG render pass.
55
- const _pluginProvides = new Map()
56
- ;(globalThis).__cerPluginProvides = _pluginProvides
57
- const _pluginsReady = (async () => {
58
- const _bootstrapRouter = initRouter({ routes })
59
- for (const plugin of plugins) {
60
- if (plugin && typeof plugin.setup === 'function') {
61
- await plugin.setup({
62
- router: _bootstrapRouter,
63
- provide: (key, value) => _pluginProvides.set(key, value),
64
- config: {},
65
- })
66
- }
67
- }
68
- })()
69
-
70
- // Load the Vite-built client index.html (dist/client/index.html) so every SSR
71
- // response includes the client-side scripts needed for hydration and routing.
72
- // The server bundle lives at dist/server/server.js, so ../client resolves correctly.
73
- const _clientTemplatePath = join(dirname(fileURLToPath(import.meta.url)), '../client/index.html')
74
- const _clientTemplate = existsSync(_clientTemplatePath)
75
- ? readFileSync(_clientTemplatePath, 'utf-8')
76
- : null
77
-
78
- // Merge the SSR rendered body with the Vite client shell so the final page
79
- // contains both pre-rendered DSD content and the client bundle scripts.
80
- function _mergeWithClientTemplate(ssrHtml, clientTemplate) {
81
- const headTag = '<head>', headCloseTag = '</head>'
82
- const bodyTag = '<body>', bodyCloseTag = '</body>'
83
- const headStart = ssrHtml.indexOf(headTag)
84
- const headEnd = ssrHtml.indexOf(headCloseTag)
85
- const bodyStart = ssrHtml.indexOf(bodyTag)
86
- const bodyEnd = ssrHtml.lastIndexOf(bodyCloseTag)
87
- const ssrHead = headStart >= 0 && headEnd > headStart
88
- ? ssrHtml.slice(headStart + headTag.length, headEnd).trim() : ''
89
- const ssrBody = bodyStart >= 0 && bodyEnd > bodyStart
90
- ? ssrHtml.slice(bodyStart + bodyTag.length, bodyEnd).trim() : ssrHtml
91
- // Hoist only top-level <style id=...> elements (cer-ssr-jit, cer-ssr-global)
92
- // from the SSR body into the document <head>. Plain <style> blocks without
93
- // an id attribute belong to shadow DOM templates and must stay in place —
94
- // hoisting them to <head> breaks shadow DOM style encapsulation (document
95
- // styles do not pierce shadow roots), which is the root cause of FOUC.
96
- const headParts = ssrHead ? [ssrHead] : []
97
- let ssrBodyContent = ssrBody
98
- let pos = 0
99
- while (pos < ssrBodyContent.length) {
100
- const styleOpen = ssrBodyContent.indexOf('<style id=', pos)
101
- if (styleOpen < 0) break
102
- const styleClose = ssrBodyContent.indexOf('</style>', styleOpen)
103
- if (styleClose < 0) break
104
- headParts.push(ssrBodyContent.slice(styleOpen, styleClose + 8))
105
- ssrBodyContent = ssrBodyContent.slice(0, styleOpen) + ssrBodyContent.slice(styleClose + 8)
106
- pos = styleOpen
107
- }
108
- ssrBodyContent = ssrBodyContent.trim()
109
- // Inject the pre-rendered layout+page as light DOM of the app mount element
110
- // so it is visible before JS boots, then the client router takes over.
111
- let merged = clientTemplate
112
- if (merged.includes('<cer-layout-view></cer-layout-view>')) {
113
- merged = merged.replace('<cer-layout-view></cer-layout-view>',
114
- '<cer-layout-view>' + ssrBodyContent + '</cer-layout-view>')
115
- } else if (merged.includes('<div id="app"></div>')) {
116
- merged = merged.replace('<div id="app"></div>',
117
- '<div id="app">' + ssrBodyContent + '</div>')
118
- }
119
- const headAdditions = headParts.filter(Boolean).join('\\n')
120
- if (headAdditions) {
121
- // If SSR provides a <title>, replace the client template's <title> so the
122
- // SSR title wins (client template title is the fallback default).
123
- if (headAdditions.includes('<title>')) {
124
- merged = merged.replace(/<title>[^<]*<\\/title>/, '')
125
- }
126
- merged = merged.replace('</head>', headAdditions + '\\n</head>')
127
- }
128
- return merged
129
- }
130
-
131
- // Per-request async setup: initialize a fresh router, resolve the matched
132
- // route and layout, pre-load the page module, and call the data loader.
133
- // Returns the vnode tree, router, head additions, and the raw loader data.
134
- //
135
- // loaderData is returned (not set on globalThis) so the handler can assign it
136
- // synchronously right before renderToStringWithJITCSS — guaranteeing that
137
- // concurrent renders (SSG concurrency > 1) never race on a shared global.
138
- const _prepareRequest = async (req) => {
139
- await _pluginsReady
140
- const router = initRouter({ routes, initialUrl: req.url ?? '/' })
141
- const current = router.getCurrent()
142
- const { route, params } = router.matchRoute(current.path)
143
- const layoutName = route?.meta?.layout ?? 'default'
144
- const layoutTag = layouts[layoutName]
145
-
146
- // Pre-load the page module so we can embed the component tag directly.
147
- // This avoids the async router-view (which injects content via script tags
148
- // and breaks Declarative Shadow DOM on initial parse).
149
- let pageVnode = { tag: 'div', props: {}, children: [] }
150
- let head
151
- let loaderData = null
152
- if (route?.load) {
153
- try {
154
- const mod = await route.load()
155
- const pageTag = mod.default
156
- if (pageTag) {
157
- pageVnode = { tag: pageTag, props: { attrs: { ...params } }, children: [] }
158
- }
159
- if (typeof mod.loader === 'function') {
160
- const query = current.query ?? {}
161
- const data = await mod.loader({ params, query, req })
162
- if (data !== undefined && data !== null) {
163
- loaderData = data
164
- head = \`<script>window.__CER_DATA__ = \${JSON.stringify(data)}</script>\`
165
- }
166
- }
167
- } catch {
168
- // Non-fatal: loader errors fall back to an empty page; client will refetch.
169
- }
170
- }
171
-
172
- const vnode = layoutTag
173
- ? { tag: layoutTag, props: {}, children: [pageVnode] }
174
- : pageVnode
175
-
176
- return { vnode, router, head, loaderData }
177
- }
178
-
179
- export const handler = async (req, res) => {
180
- const { vnode, router, head, loaderData } = await _prepareRequest(req)
181
-
182
- // Set loader data on globalThis synchronously before the render so
183
- // usePageData() can read it. Because renderToStringWithJITCSSDSD is entirely
184
- // synchronous and JavaScript is single-threaded, no concurrent request can
185
- // overwrite __CER_DATA__ between this assignment and the read inside setup().
186
- if (loaderData !== null) {
187
- ;(globalThis).__CER_DATA__ = loaderData
188
- }
189
-
190
- // Begin collecting useHead() calls made during the synchronous render pass.
191
- beginHeadCollection()
192
-
193
- // dsdPolyfill: false — we inject the polyfill manually after merging so it
194
- // lands at the end of <body>, not inside <cer-layout-view> light DOM where
195
- // scripts may not execute.
196
- const { htmlWithStyles } = renderToStringWithJITCSSDSD(vnode, {
197
- dsdPolyfill: false,
198
- router,
199
- })
200
-
201
- // Collect and serialize any useHead() calls from the rendered components.
202
- const headTags = serializeHeadTags(endHeadCollection())
203
-
204
- // Clear immediately after the synchronous render so the value never leaks
205
- // to the next request on this same server process.
206
- delete (globalThis).__CER_DATA__
207
-
208
- // Merge loader data script + useHead() tags into the document head.
209
- const headContent = [head, headTags].filter(Boolean).join('\\n')
210
-
211
- // Wrap the rendered body in a full HTML document and inject the head additions
212
- // (loader data script, useHead() tags, JIT styles). No polyfill in body yet.
213
- const ssrHtml = \`<!DOCTYPE html><html><head>\${headContent}</head><body>\${htmlWithStyles}</body></html>\`
214
-
215
- let finalHtml = _clientTemplate
216
- ? _mergeWithClientTemplate(ssrHtml, _clientTemplate)
217
- : ssrHtml
218
-
219
- // Inject DSD polyfill at end of <body>, outside <cer-layout-view>, so the
220
- // browser runs it after parsing the declarative shadow roots.
221
- finalHtml = finalHtml.includes('</body>')
222
- ? finalHtml.replace('</body>', DSD_POLYFILL_SCRIPT + '</body>')
223
- : finalHtml + DSD_POLYFILL_SCRIPT
224
-
225
- res.setHeader('Content-Type', 'text/html; charset=utf-8')
226
- res.end(finalHtml)
227
- }
228
-
229
- export { apiRoutes, plugins, layouts }
230
- export default handler
231
- `
28
+ return ENTRY_SERVER_TEMPLATE
232
29
  }
233
30
 
234
31
  /**