@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.
- package/CHANGELOG.md +4 -0
- package/commits.txt +1 -1
- package/dist/cli/create/index.js +22 -7
- package/dist/cli/create/index.js.map +1 -1
- package/dist/cli/create/templates/{ssr → shared}/app/pages/index.ts.tpl +1 -1
- package/dist/plugin/build-ssr.d.ts.map +1 -1
- package/dist/plugin/build-ssr.js +2 -205
- package/dist/plugin/build-ssr.js.map +1 -1
- package/dist/runtime/entry-server-template.d.ts +10 -4
- package/dist/runtime/entry-server-template.d.ts.map +1 -1
- package/dist/runtime/entry-server-template.js +54 -64
- package/dist/runtime/entry-server-template.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/plugin/build-ssr.test.ts +3 -120
- package/src/__tests__/plugin/entry-server-template.test.ts +106 -3
- package/src/cli/create/index.ts +21 -7
- package/src/cli/create/templates/{spa → shared}/app/pages/index.ts.tpl +1 -1
- package/src/plugin/build-ssr.ts +2 -205
- package/src/runtime/entry-server-template.ts +54 -64
- package/dist/cli/create/templates/spa/app/pages/index.ts.tpl +0 -8
- package/dist/cli/create/templates/ssg/app/pages/index.ts.tpl +0 -8
- package/dist/cli/create/templates/ssr/.gitignore.tpl +0 -25
- package/dist/cli/create/templates/ssr/app/layouts/default.ts.tpl +0 -15
- package/dist/cli/create/templates/ssr/index.html.tpl +0 -12
- package/dist/cli/create/templates/ssr/tsconfig.json.tpl +0 -3
- package/src/cli/create/templates/spa/.gitignore.tpl +0 -25
- package/src/cli/create/templates/spa/app/layouts/default.ts.tpl +0 -15
- package/src/cli/create/templates/spa/index.html.tpl +0 -12
- package/src/cli/create/templates/spa/tsconfig.json.tpl +0 -3
- package/src/cli/create/templates/ssg/.gitignore.tpl +0 -25
- package/src/cli/create/templates/ssg/app/layouts/default.ts.tpl +0 -15
- package/src/cli/create/templates/ssg/app/pages/index.ts.tpl +0 -8
- package/src/cli/create/templates/ssg/index.html.tpl +0 -12
- package/src/cli/create/templates/ssg/tsconfig.json.tpl +0 -3
- package/src/cli/create/templates/ssr/.gitignore.tpl +0 -25
- package/src/cli/create/templates/ssr/app/layouts/default.ts.tpl +0 -15
- package/src/cli/create/templates/ssr/app/pages/index.ts.tpl +0 -8
- package/src/cli/create/templates/ssr/index.html.tpl +0 -12
- package/src/cli/create/templates/ssr/tsconfig.json.tpl +0 -3
- /package/dist/cli/create/templates/{spa → shared}/.gitignore.tpl +0 -0
- /package/dist/cli/create/templates/{spa → shared}/app/layouts/default.ts.tpl +0 -0
- /package/dist/cli/create/templates/{spa → shared}/index.html.tpl +0 -0
- /package/dist/cli/create/templates/{spa → shared}/tsconfig.json.tpl +0 -0
- /package/{dist/cli/create/templates/ssg → src/cli/create/templates/shared}/.gitignore.tpl +0 -0
- /package/{dist/cli/create/templates/ssg → src/cli/create/templates/shared}/app/layouts/default.ts.tpl +0 -0
- /package/{dist/cli/create/templates/ssg → src/cli/create/templates/shared}/index.html.tpl +0 -0
- /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
|
|
6
|
-
//
|
|
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
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
})
|
package/src/cli/create/index.ts
CHANGED
|
@@ -111,10 +111,17 @@ async function writeTemplateFiles(
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
|
-
* Returns the path to the template directory
|
|
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
|
|
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
|
|
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(
|
|
162
|
+
if (!existsSync(sharedDir) && !existsSync(modeDir)) {
|
|
155
163
|
// Fallback: generate minimal template inline
|
|
156
|
-
console.warn(`[create-cer-app] Template directory not found at ${
|
|
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 =
|
|
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
|
|
package/src/plugin/build-ssr.ts
CHANGED
|
@@ -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
|
|
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 — decode
|
|
49
|
-
// correctly during SSR. Without this the bundled runtime falls back to a
|
|
50
|
-
// minimal set (<, >, & …) 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
|
/**
|