@jasonshimmy/vite-plugin-cer-app 0.4.4 → 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 (79) hide show
  1. package/.github/copilot-instructions.md +2 -0
  2. package/CHANGELOG.md +8 -0
  3. package/commits.txt +1 -1
  4. package/dist/cli/create/index.js +22 -7
  5. package/dist/cli/create/index.js.map +1 -1
  6. package/dist/cli/create/templates/{ssr → shared}/app/pages/index.ts.tpl +1 -1
  7. package/dist/cli/create/templates/ssg/cer.config.ts.tpl +0 -3
  8. package/dist/cli/create/templates/ssr/cer.config.ts.tpl +0 -3
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/plugin/build-ssr.d.ts.map +1 -1
  12. package/dist/plugin/build-ssr.js +4 -211
  13. package/dist/plugin/build-ssr.js.map +1 -1
  14. package/dist/plugin/dev-server.d.ts +0 -3
  15. package/dist/plugin/dev-server.d.ts.map +1 -1
  16. package/dist/plugin/dev-server.js.map +1 -1
  17. package/dist/plugin/index.d.ts.map +1 -1
  18. package/dist/plugin/index.js +1 -6
  19. package/dist/plugin/index.js.map +1 -1
  20. package/dist/runtime/entry-server-template.d.ts +10 -4
  21. package/dist/runtime/entry-server-template.d.ts.map +1 -1
  22. package/dist/runtime/entry-server-template.js +54 -64
  23. package/dist/runtime/entry-server-template.js.map +1 -1
  24. package/dist/types/config.d.ts +0 -4
  25. package/dist/types/config.d.ts.map +1 -1
  26. package/dist/types/config.js.map +1 -1
  27. package/dist/types/index.d.ts +1 -1
  28. package/dist/types/index.d.ts.map +1 -1
  29. package/docs/cli.md +1 -1
  30. package/docs/configuration.md +0 -25
  31. package/docs/rendering-modes.md +0 -6
  32. package/e2e/kitchen-sink/cer.config.ts +0 -1
  33. package/package.json +1 -1
  34. package/src/__tests__/plugin/build-ssg-render.test.ts +0 -1
  35. package/src/__tests__/plugin/build-ssg.test.ts +0 -1
  36. package/src/__tests__/plugin/build-ssr.test.ts +3 -121
  37. package/src/__tests__/plugin/dev-server.test.ts +0 -1
  38. package/src/__tests__/plugin/entry-server-template.test.ts +106 -3
  39. package/src/__tests__/plugin/resolve-config.test.ts +0 -10
  40. package/src/__tests__/types/config.test.ts +0 -5
  41. package/src/cli/create/index.ts +21 -7
  42. package/src/cli/create/templates/{spa → shared}/app/pages/index.ts.tpl +1 -1
  43. package/src/cli/create/templates/ssg/cer.config.ts.tpl +0 -3
  44. package/src/cli/create/templates/ssr/cer.config.ts.tpl +0 -3
  45. package/src/index.ts +1 -1
  46. package/src/plugin/build-ssr.ts +4 -211
  47. package/src/plugin/dev-server.ts +0 -1
  48. package/src/plugin/index.ts +1 -6
  49. package/src/runtime/entry-server-template.ts +54 -64
  50. package/src/types/config.ts +0 -5
  51. package/src/types/index.ts +1 -1
  52. package/dist/cli/create/templates/spa/app/pages/index.ts.tpl +0 -8
  53. package/dist/cli/create/templates/ssg/app/pages/index.ts.tpl +0 -8
  54. package/dist/cli/create/templates/ssr/.gitignore.tpl +0 -25
  55. package/dist/cli/create/templates/ssr/app/layouts/default.ts.tpl +0 -15
  56. package/dist/cli/create/templates/ssr/index.html.tpl +0 -12
  57. package/dist/cli/create/templates/ssr/tsconfig.json.tpl +0 -3
  58. package/src/cli/create/templates/spa/.gitignore.tpl +0 -25
  59. package/src/cli/create/templates/spa/app/layouts/default.ts.tpl +0 -15
  60. package/src/cli/create/templates/spa/index.html.tpl +0 -12
  61. package/src/cli/create/templates/spa/tsconfig.json.tpl +0 -3
  62. package/src/cli/create/templates/ssg/.gitignore.tpl +0 -25
  63. package/src/cli/create/templates/ssg/app/layouts/default.ts.tpl +0 -15
  64. package/src/cli/create/templates/ssg/app/pages/index.ts.tpl +0 -8
  65. package/src/cli/create/templates/ssg/index.html.tpl +0 -12
  66. package/src/cli/create/templates/ssg/tsconfig.json.tpl +0 -3
  67. package/src/cli/create/templates/ssr/.gitignore.tpl +0 -25
  68. package/src/cli/create/templates/ssr/app/layouts/default.ts.tpl +0 -15
  69. package/src/cli/create/templates/ssr/app/pages/index.ts.tpl +0 -8
  70. package/src/cli/create/templates/ssr/index.html.tpl +0 -12
  71. package/src/cli/create/templates/ssr/tsconfig.json.tpl +0 -3
  72. /package/dist/cli/create/templates/{spa → shared}/.gitignore.tpl +0 -0
  73. /package/dist/cli/create/templates/{spa → shared}/app/layouts/default.ts.tpl +0 -0
  74. /package/dist/cli/create/templates/{spa → shared}/index.html.tpl +0 -0
  75. /package/dist/cli/create/templates/{spa → shared}/tsconfig.json.tpl +0 -0
  76. /package/{dist/cli/create/templates/ssg → src/cli/create/templates/shared}/.gitignore.tpl +0 -0
  77. /package/{dist/cli/create/templates/ssg → src/cli/create/templates/shared}/app/layouts/default.ts.tpl +0 -0
  78. /package/{dist/cli/create/templates/ssg → src/cli/create/templates/shared}/index.html.tpl +0 -0
  79. /package/{dist/cli/create/templates/ssg → src/cli/create/templates/shared}/tsconfig.json.tpl +0 -0
@@ -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
  })
@@ -74,16 +74,6 @@ describe('resolveConfig', () => {
74
74
  expect(cfg.port).toBe(4000)
75
75
  })
76
76
 
77
- it('defaults ssr.dsd to true', () => {
78
- const cfg = resolveConfig({}, ROOT)
79
- expect(cfg.ssr.dsd).toBe(true)
80
- })
81
-
82
- it('respects explicit ssr.dsd=false', () => {
83
- const cfg = resolveConfig({ ssr: { dsd: false } }, ROOT)
84
- expect(cfg.ssr.dsd).toBe(false)
85
- })
86
-
87
77
  it('defaults ssg.routes to "auto"', () => {
88
78
  const cfg = resolveConfig({}, ROOT)
89
79
  expect(cfg.ssg.routes).toBe('auto')
@@ -11,11 +11,6 @@ describe('defineConfig', () => {
11
11
  expect(defineConfig({})).toEqual({})
12
12
  })
13
13
 
14
- it('preserves nested ssr config', () => {
15
- const config = { ssr: { dsd: false } }
16
- expect(defineConfig(config)).toEqual(config)
17
- })
18
-
19
14
  it('preserves nested ssg config', () => {
20
15
  const config = { ssg: { routes: ['/a', '/b'], concurrency: 2 } }
21
16
  expect(defineConfig(config)).toEqual(config)
@@ -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
  `
@@ -6,8 +6,5 @@ export default defineConfig({
6
6
  routes: 'auto',
7
7
  concurrency: 4,
8
8
  },
9
- ssr: {
10
- dsd: true,
11
- },
12
9
  autoImports: { components: true, composables: true, directives: true, runtime: true },
13
10
  })
@@ -2,8 +2,5 @@ import { defineConfig } from '@jasonshimmy/vite-plugin-cer-app'
2
2
 
3
3
  export default defineConfig({
4
4
  mode: 'ssr',
5
- ssr: {
6
- dsd: true,
7
- },
8
5
  autoImports: { components: true, composables: true, directives: true, runtime: true },
9
6
  })
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@ export { cerApp } from './plugin/index.js'
3
3
  export { defineConfig } from './types/config.js'
4
4
 
5
5
  // Re-export all types
6
- export type { CerAppConfig, SsgConfig, JitCssConfig, SsrConfig, AutoImportsConfig } from './types/config.js'
6
+ export type { CerAppConfig, SsgConfig, JitCssConfig, AutoImportsConfig } from './types/config.js'
7
7
  export type { HydrateStrategy, SsgPathsContext, PageSsgConfig, PageMeta, PageLoaderContext, PageLoader } from './types/page.js'
8
8
  export type { ApiRequest, ApiResponse, ApiHandler, ApiContext } from './types/api.js'
9
9
  export type { AppContext, AppPlugin } from './types/plugin.js'
@@ -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,216 +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
- function generateServerEntryCode(config: ResolvedCerConfig): string {
31
- const dsd = config.ssr.dsd
32
- const renderImport = dsd
33
- ? `import { registerEntityMap, renderToStringWithJITCSSDSD, DSD_POLYFILL_SCRIPT } from '@jasonshimmy/custom-elements-runtime/ssr'`
34
- : `import { registerEntityMap, renderToStringWithJITCSS } from '@jasonshimmy/custom-elements-runtime/ssr'`
35
- return `// AUTO-GENERATED server entry by @jasonshimmy/vite-plugin-cer-app
36
- import { readFileSync, existsSync } from 'node:fs'
37
- import { dirname, join } from 'node:path'
38
- import { fileURLToPath } from 'node:url'
39
- import 'virtual:cer-components'
40
- import routes from 'virtual:cer-routes'
41
- import layouts from 'virtual:cer-layouts'
42
- import plugins from 'virtual:cer-plugins'
43
- import apiRoutes from 'virtual:cer-server-api'
44
- import { registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'
45
- ${renderImport}
46
- import entitiesJson from '@jasonshimmy/custom-elements-runtime/entities.json'
47
- import { initRouter } from '@jasonshimmy/custom-elements-runtime/router'
48
- import { beginHeadCollection, endHeadCollection, serializeHeadTags } from '@jasonshimmy/vite-plugin-cer-app/composables'
49
-
50
- registerBuiltinComponents()
51
-
52
- // Pre-load the full HTML entity map so named entities like &mdash; decode
53
- // correctly during SSR. Without this the bundled runtime falls back to a
54
- // minimal set (&lt;, &gt;, &amp; …) and re-escapes everything else.
55
- registerEntityMap(entitiesJson)
56
-
57
- // Run plugins once at server startup so their provide() values are available
58
- // to useInject() during every SSR/SSG render pass.
59
- const _pluginProvides = new Map()
60
- ;(globalThis).__cerPluginProvides = _pluginProvides
61
- const _pluginsReady = (async () => {
62
- const _bootstrapRouter = initRouter({ routes })
63
- for (const plugin of plugins) {
64
- if (plugin && typeof plugin.setup === 'function') {
65
- await plugin.setup({
66
- router: _bootstrapRouter,
67
- provide: (key, value) => _pluginProvides.set(key, value),
68
- config: {},
69
- })
70
- }
71
- }
72
- })()
73
-
74
- // Load the Vite-built client index.html (dist/client/index.html) so every SSR
75
- // response includes the client-side scripts needed for hydration and routing.
76
- // The server bundle lives at dist/server/server.js, so ../client resolves correctly.
77
- const _clientTemplatePath = join(dirname(fileURLToPath(import.meta.url)), '../client/index.html')
78
- const _clientTemplate = existsSync(_clientTemplatePath)
79
- ? readFileSync(_clientTemplatePath, 'utf-8')
80
- : null
81
-
82
- // Merge the SSR rendered body with the Vite client shell so the final page
83
- // contains both pre-rendered DSD content and the client bundle scripts.
84
- function _mergeWithClientTemplate(ssrHtml, clientTemplate) {
85
- const headTag = '<head>', headCloseTag = '</head>'
86
- const bodyTag = '<body>', bodyCloseTag = '</body>'
87
- const headStart = ssrHtml.indexOf(headTag)
88
- const headEnd = ssrHtml.indexOf(headCloseTag)
89
- const bodyStart = ssrHtml.indexOf(bodyTag)
90
- const bodyEnd = ssrHtml.lastIndexOf(bodyCloseTag)
91
- const ssrHead = headStart >= 0 && headEnd > headStart
92
- ? ssrHtml.slice(headStart + headTag.length, headEnd).trim() : ''
93
- const ssrBody = bodyStart >= 0 && bodyEnd > bodyStart
94
- ? ssrHtml.slice(bodyStart + bodyTag.length, bodyEnd).trim() : ssrHtml
95
- // Hoist only top-level <style id=...> elements (cer-ssr-jit, cer-ssr-global)
96
- // from the SSR body into the document <head>. Plain <style> blocks without
97
- // an id attribute belong to shadow DOM templates and must stay in place —
98
- // hoisting them to <head> breaks shadow DOM style encapsulation (document
99
- // styles do not pierce shadow roots), which is the root cause of FOUC.
100
- const headParts = ssrHead ? [ssrHead] : []
101
- let ssrBodyContent = ssrBody
102
- let pos = 0
103
- while (pos < ssrBodyContent.length) {
104
- const styleOpen = ssrBodyContent.indexOf('<style id=', pos)
105
- if (styleOpen < 0) break
106
- const styleClose = ssrBodyContent.indexOf('</style>', styleOpen)
107
- if (styleClose < 0) break
108
- headParts.push(ssrBodyContent.slice(styleOpen, styleClose + 8))
109
- ssrBodyContent = ssrBodyContent.slice(0, styleOpen) + ssrBodyContent.slice(styleClose + 8)
110
- pos = styleOpen
111
- }
112
- ssrBodyContent = ssrBodyContent.trim()
113
- // Inject the pre-rendered layout+page as light DOM of the app mount element
114
- // so it is visible before JS boots, then the client router takes over.
115
- let merged = clientTemplate
116
- if (merged.includes('<cer-layout-view></cer-layout-view>')) {
117
- merged = merged.replace('<cer-layout-view></cer-layout-view>',
118
- '<cer-layout-view>' + ssrBodyContent + '</cer-layout-view>')
119
- } else if (merged.includes('<div id="app"></div>')) {
120
- merged = merged.replace('<div id="app"></div>',
121
- '<div id="app">' + ssrBodyContent + '</div>')
122
- }
123
- const headAdditions = headParts.filter(Boolean).join('\\n')
124
- if (headAdditions) {
125
- // If SSR provides a <title>, replace the client template's <title> so the
126
- // SSR title wins (client template title is the fallback default).
127
- if (headAdditions.includes('<title>')) {
128
- merged = merged.replace(/<title>[^<]*<\\/title>/, '')
129
- }
130
- merged = merged.replace('</head>', headAdditions + '\\n</head>')
131
- }
132
- return merged
133
- }
134
-
135
- // Per-request async setup: initialize a fresh router, resolve the matched
136
- // route and layout, pre-load the page module, and call the data loader.
137
- // Returns the vnode tree, router, head additions, and the raw loader data.
138
- //
139
- // loaderData is returned (not set on globalThis) so the handler can assign it
140
- // synchronously right before renderToStringWithJITCSS — guaranteeing that
141
- // concurrent renders (SSG concurrency > 1) never race on a shared global.
142
- const _prepareRequest = async (req) => {
143
- await _pluginsReady
144
- const router = initRouter({ routes, initialUrl: req.url ?? '/' })
145
- const current = router.getCurrent()
146
- const { route, params } = router.matchRoute(current.path)
147
- const layoutName = route?.meta?.layout ?? 'default'
148
- const layoutTag = layouts[layoutName]
149
-
150
- // Pre-load the page module so we can embed the component tag directly.
151
- // This avoids the async router-view (which injects content via script tags
152
- // and breaks Declarative Shadow DOM on initial parse).
153
- let pageVnode = { tag: 'div', props: {}, children: [] }
154
- let head
155
- let loaderData = null
156
- if (route?.load) {
157
- try {
158
- const mod = await route.load()
159
- const pageTag = mod.default
160
- if (pageTag) {
161
- pageVnode = { tag: pageTag, props: { attrs: { ...params } }, children: [] }
162
- }
163
- if (typeof mod.loader === 'function') {
164
- const query = current.query ?? {}
165
- const data = await mod.loader({ params, query, req })
166
- if (data !== undefined && data !== null) {
167
- loaderData = data
168
- head = \`<script>window.__CER_DATA__ = \${JSON.stringify(data)}</script>\`
169
- }
170
- }
171
- } catch {
172
- // Non-fatal: loader errors fall back to an empty page; client will refetch.
173
- }
174
- }
175
-
176
- const vnode = layoutTag
177
- ? { tag: layoutTag, props: {}, children: [pageVnode] }
178
- : pageVnode
179
-
180
- return { vnode, router, head, loaderData }
181
- }
182
-
183
- export const handler = async (req, res) => {
184
- const { vnode, router, head, loaderData } = await _prepareRequest(req)
185
-
186
- // Set loader data on globalThis synchronously before the render so
187
- // usePageData() can read it. Because renderToStringWithJITCSSDSD is entirely
188
- // synchronous and JavaScript is single-threaded, no concurrent request can
189
- // overwrite __CER_DATA__ between this assignment and the read inside setup().
190
- if (loaderData !== null) {
191
- ;(globalThis).__CER_DATA__ = loaderData
192
- }
193
-
194
- // Begin collecting useHead() calls made during the synchronous render pass.
195
- beginHeadCollection()
196
-
197
- // dsdPolyfill: false — we inject the polyfill manually after merging so it
198
- // lands at the end of <body>, not inside <cer-layout-view> light DOM where
199
- // scripts may not execute.
200
- const { htmlWithStyles } = ${dsd ? 'renderToStringWithJITCSSDSD' : 'renderToStringWithJITCSS'}(vnode, {
201
- dsdPolyfill: false,
202
- router,
203
- })
204
-
205
- // Collect and serialize any useHead() calls from the rendered components.
206
- const headTags = serializeHeadTags(endHeadCollection())
207
-
208
- // Clear immediately after the synchronous render so the value never leaks
209
- // to the next request on this same server process.
210
- delete (globalThis).__CER_DATA__
211
-
212
- // Merge loader data script + useHead() tags into the document head.
213
- const headContent = [head, headTags].filter(Boolean).join('\\n')
214
-
215
- // Wrap the rendered body in a full HTML document and inject the head additions
216
- // (loader data script, useHead() tags, JIT styles). No polyfill in body yet.
217
- const ssrHtml = \`<!DOCTYPE html><html><head>\${headContent}</head><body>\${htmlWithStyles}</body></html>\`
218
-
219
- let finalHtml = _clientTemplate
220
- ? _mergeWithClientTemplate(ssrHtml, _clientTemplate)
221
- : ssrHtml
222
-
223
- // Inject DSD polyfill at end of <body>, outside <cer-layout-view>, so the
224
- // browser runs it after parsing the declarative shadow roots.
225
- ${dsd ? `finalHtml = finalHtml.includes('</body>')
226
- ? finalHtml.replace('</body>', DSD_POLYFILL_SCRIPT + '</body>')
227
- : finalHtml + DSD_POLYFILL_SCRIPT` : '// dsd: false — no DSD polyfill needed'}
228
-
229
- res.setHeader('Content-Type', 'text/html; charset=utf-8')
230
- res.end(finalHtml)
231
- }
232
-
233
- export { apiRoutes, plugins, layouts }
234
- export default handler
235
- `
27
+ function generateServerEntryCode(): string {
28
+ return ENTRY_SERVER_TEMPLATE
236
29
  }
237
30
 
238
31
  /**
@@ -281,7 +74,7 @@ export async function buildSSR(
281
74
  }
282
75
 
283
76
  // Generate server entry source inline via a virtual plugin
284
- const serverEntryCode = generateServerEntryCode(config)
77
+ const serverEntryCode = generateServerEntryCode()
285
78
  const VIRTUAL_SERVER_ENTRY = 'virtual:cer-server-entry'
286
79
  const RESOLVED_SERVER_ENTRY = '\0virtual:cer-server-entry'
287
80
 
@@ -15,7 +15,6 @@ export interface ResolvedCerConfig {
15
15
  serverApiDir: string
16
16
  serverMiddlewareDir: string
17
17
  port: number
18
- ssr: { dsd: boolean }
19
18
  ssg: { routes: 'auto' | string[]; concurrency: number; fallback: boolean }
20
19
  router: { base?: string; scrollToFragment?: boolean | object }
21
20
  jitCss: { content: string[]; extendedColors: boolean }
@@ -69,9 +69,6 @@ export function resolveConfig(userConfig: CerAppConfig, root: string = process.c
69
69
  serverApiDir: join(root, 'server/api'),
70
70
  serverMiddlewareDir: join(root, 'server/middleware'),
71
71
  port: userConfig.port ?? 3000,
72
- ssr: {
73
- dsd: userConfig.ssr?.dsd ?? true,
74
- },
75
72
  ssg: {
76
73
  routes: userConfig.ssg?.routes ?? 'auto',
77
74
  concurrency: userConfig.ssg?.concurrency ?? 4,
@@ -140,7 +137,6 @@ function generateAppConfigModule(config: ResolvedCerConfig): string {
140
137
  const exportedConfig = {
141
138
  mode: config.mode,
142
139
  router: config.router,
143
- ssr: config.ssr,
144
140
  ssg: config.ssg,
145
141
  }
146
142
  return `// AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app\nexport const appConfig = ${JSON.stringify(exportedConfig, null, 2)}\nexport default appConfig\n`
@@ -371,13 +367,12 @@ export function cerApp(userConfig: CerAppConfig = {}): Plugin[] {
371
367
  // Include cerPlugin from the runtime for JIT CSS support
372
368
  // Resolve config eagerly so cerPlugin can use the final resolved values
373
369
  const resolvedForJit = resolveConfig(userConfig)
374
- const { dsd } = resolvedForJit.ssr
375
370
  const { content, ...jitOptions } = resolvedForJit.jitCss
376
371
  const jitPlugins = cerPlugin({
377
372
  content,
378
373
  ...jitOptions,
379
374
  ssr: {
380
- dsd,
375
+ dsd: true,
381
376
  jit: jitOptions,
382
377
  },
383
378
  })