@jasonshimmy/vite-plugin-cer-app 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/commits.txt +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +2 -0
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/preview.d.ts.map +1 -1
- package/dist/cli/commands/preview.js +21 -2
- package/dist/cli/commands/preview.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/plugin/build-ssg.d.ts.map +1 -1
- package/dist/plugin/build-ssg.js +10 -21
- package/dist/plugin/build-ssg.js.map +1 -1
- package/dist/plugin/build-ssr.d.ts.map +1 -1
- package/dist/plugin/build-ssr.js +150 -27
- package/dist/plugin/build-ssr.js.map +1 -1
- package/dist/plugin/virtual/routes.d.ts.map +1 -1
- package/dist/plugin/virtual/routes.js +11 -1
- package/dist/plugin/virtual/routes.js.map +1 -1
- package/dist/runtime/app-template.d.ts +1 -1
- package/dist/runtime/app-template.d.ts.map +1 -1
- package/dist/runtime/app-template.js +6 -0
- package/dist/runtime/app-template.js.map +1 -1
- package/dist/runtime/composables/use-page-data.d.ts +15 -6
- package/dist/runtime/composables/use-page-data.d.ts.map +1 -1
- package/dist/runtime/composables/use-page-data.js +30 -9
- package/dist/runtime/composables/use-page-data.js.map +1 -1
- package/dist/runtime/entry-server-template.d.ts +1 -1
- package/dist/runtime/entry-server-template.d.ts.map +1 -1
- package/dist/runtime/entry-server-template.js +137 -16
- package/dist/runtime/entry-server-template.js.map +1 -1
- package/docs/data-loading.md +7 -6
- package/docs/getting-started.md +1 -1
- package/docs/rendering-modes.md +1 -1
- package/docs/server-api.md +9 -0
- package/package.json +1 -1
- package/src/__tests__/index.test.ts +21 -0
- package/src/__tests__/plugin/build-ssg.test.ts +265 -0
- package/src/__tests__/plugin/build-ssr.test.ts +180 -0
- package/src/__tests__/plugin/cer-app-plugin.test.ts +409 -0
- package/src/__tests__/plugin/dts-generator.test.ts +246 -0
- package/src/__tests__/plugin/resolve-config.test.ts +158 -0
- package/src/__tests__/plugin/virtual/error.test.ts +71 -0
- package/src/__tests__/plugin/virtual/loading.test.ts +72 -0
- package/src/__tests__/plugin/virtual/server-middleware.test.ts +102 -0
- package/src/__tests__/runtime/use-page-data.test.ts +81 -5
- package/src/__tests__/types/config.test.ts +23 -0
- package/src/cli/commands/generate.ts +2 -0
- package/src/cli/commands/preview.ts +21 -2
- package/src/cli/index.ts +1 -1
- package/src/plugin/build-ssg.ts +9 -22
- package/src/plugin/build-ssr.ts +149 -27
- package/src/plugin/virtual/routes.ts +12 -1
- package/src/runtime/app-template.ts +6 -0
- package/src/runtime/composables/use-page-data.ts +31 -9
- package/src/runtime/entry-server-template.ts +137 -16
- package/tsconfig.build.json +1 -1
- package/dist/__tests__/plugin/path-utils.test.d.ts +0 -2
- package/dist/__tests__/plugin/path-utils.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/path-utils.test.js +0 -305
- package/dist/__tests__/plugin/path-utils.test.js.map +0 -1
- package/dist/__tests__/plugin/scanner.test.d.ts +0 -2
- package/dist/__tests__/plugin/scanner.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/scanner.test.js +0 -143
- package/dist/__tests__/plugin/scanner.test.js.map +0 -1
- package/dist/__tests__/plugin/transforms/auto-import.test.d.ts +0 -2
- package/dist/__tests__/plugin/transforms/auto-import.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/transforms/auto-import.test.js +0 -151
- package/dist/__tests__/plugin/transforms/auto-import.test.js.map +0 -1
- package/dist/__tests__/plugin/transforms/head-inject.test.d.ts +0 -2
- package/dist/__tests__/plugin/transforms/head-inject.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/transforms/head-inject.test.js +0 -151
- package/dist/__tests__/plugin/transforms/head-inject.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/components.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/components.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/components.test.js +0 -47
- package/dist/__tests__/plugin/virtual/components.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/composables.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/composables.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/composables.test.js +0 -48
- package/dist/__tests__/plugin/virtual/composables.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/layouts.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/layouts.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/layouts.test.js +0 -59
- package/dist/__tests__/plugin/virtual/layouts.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/middleware.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/middleware.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/middleware.test.js +0 -58
- package/dist/__tests__/plugin/virtual/middleware.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/plugins.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/plugins.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/plugins.test.js +0 -73
- package/dist/__tests__/plugin/virtual/plugins.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/routes.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/routes.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/routes.test.js +0 -167
- package/dist/__tests__/plugin/virtual/routes.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/server-api.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/server-api.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/server-api.test.js +0 -72
- package/dist/__tests__/plugin/virtual/server-api.test.js.map +0 -1
- package/dist/__tests__/runtime/use-head.test.d.ts +0 -2
- package/dist/__tests__/runtime/use-head.test.d.ts.map +0 -1
- package/dist/__tests__/runtime/use-head.test.js +0 -202
- package/dist/__tests__/runtime/use-head.test.js.map +0 -1
- package/dist/__tests__/runtime/use-page-data.test.d.ts +0 -2
- package/dist/__tests__/runtime/use-page-data.test.d.ts.map +0 -1
- package/dist/__tests__/runtime/use-page-data.test.js +0 -41
- package/dist/__tests__/runtime/use-page-data.test.js.map +0 -1
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
vi.mock('node:fs', () => ({ existsSync: vi.fn().mockReturnValue(false) }))
|
|
4
|
+
vi.mock('node:fs/promises', () => ({
|
|
5
|
+
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
6
|
+
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
7
|
+
}))
|
|
8
|
+
vi.mock('fast-glob', () => ({ default: vi.fn().mockResolvedValue([]) }))
|
|
9
|
+
vi.mock('vite', () => ({
|
|
10
|
+
build: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
createServer: vi.fn(),
|
|
12
|
+
}))
|
|
13
|
+
vi.mock('../../plugin/build-ssr.js', () => ({ buildSSR: vi.fn().mockResolvedValue(undefined) }))
|
|
14
|
+
vi.mock('../../plugin/path-utils.js', () => ({ buildRouteEntry: vi.fn() }))
|
|
15
|
+
|
|
16
|
+
import { existsSync } from 'node:fs'
|
|
17
|
+
import { writeFile, mkdir } from 'node:fs/promises'
|
|
18
|
+
import fg from 'fast-glob'
|
|
19
|
+
import { createServer } from 'vite'
|
|
20
|
+
import { buildSSR } from '../../plugin/build-ssr.js'
|
|
21
|
+
import { buildRouteEntry } from '../../plugin/path-utils.js'
|
|
22
|
+
import { buildSSG } from '../../plugin/build-ssg.js'
|
|
23
|
+
import type { ResolvedCerConfig } from '../../plugin/dev-server.js'
|
|
24
|
+
|
|
25
|
+
function makeConfig(overrides: Partial<ResolvedCerConfig> = {}): ResolvedCerConfig {
|
|
26
|
+
return {
|
|
27
|
+
root: '/project',
|
|
28
|
+
srcDir: '/project/app',
|
|
29
|
+
pagesDir: '/project/app/pages',
|
|
30
|
+
mode: 'ssg',
|
|
31
|
+
ssr: { dsd: true },
|
|
32
|
+
ssg: { concurrency: 4 },
|
|
33
|
+
...overrides,
|
|
34
|
+
} as unknown as ResolvedCerConfig
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
vi.mocked(buildSSR).mockClear()
|
|
39
|
+
vi.mocked(writeFile).mockClear()
|
|
40
|
+
vi.mocked(mkdir).mockClear()
|
|
41
|
+
vi.mocked(fg).mockClear()
|
|
42
|
+
vi.mocked(existsSync).mockReturnValue(false)
|
|
43
|
+
vi.mocked(fg).mockResolvedValue([])
|
|
44
|
+
vi.mocked(buildRouteEntry).mockReset()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('buildSSG — buildSSR delegation', () => {
|
|
48
|
+
it('calls buildSSR as step 1', async () => {
|
|
49
|
+
await buildSSG(makeConfig())
|
|
50
|
+
expect(buildSSR).toHaveBeenCalledTimes(1)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('passes the config to buildSSR', async () => {
|
|
54
|
+
const config = makeConfig()
|
|
55
|
+
await buildSSG(config)
|
|
56
|
+
expect(vi.mocked(buildSSR).mock.calls[0][0]).toBe(config)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('buildSSG — ssg-manifest.json', () => {
|
|
61
|
+
it('writes ssg-manifest.json to the dist directory', async () => {
|
|
62
|
+
await buildSSG(makeConfig())
|
|
63
|
+
const manifestCall = vi.mocked(writeFile).mock.calls.find(([path]) =>
|
|
64
|
+
String(path).includes('ssg-manifest.json'),
|
|
65
|
+
)
|
|
66
|
+
expect(manifestCall).toBeDefined()
|
|
67
|
+
expect(String(manifestCall![0])).toContain('/project/dist/ssg-manifest.json')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('manifest JSON contains generatedAt field', async () => {
|
|
71
|
+
await buildSSG(makeConfig())
|
|
72
|
+
const manifestCall = vi.mocked(writeFile).mock.calls.find(([p]) =>
|
|
73
|
+
String(p).includes('ssg-manifest.json'),
|
|
74
|
+
)
|
|
75
|
+
const manifest = JSON.parse(String(manifestCall![1]))
|
|
76
|
+
expect(manifest).toHaveProperty('generatedAt')
|
|
77
|
+
expect(typeof manifest.generatedAt).toBe('string')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('manifest JSON contains paths array', async () => {
|
|
81
|
+
await buildSSG(makeConfig())
|
|
82
|
+
const manifestCall = vi.mocked(writeFile).mock.calls.find(([p]) =>
|
|
83
|
+
String(p).includes('ssg-manifest.json'),
|
|
84
|
+
)
|
|
85
|
+
const manifest = JSON.parse(String(manifestCall![1]))
|
|
86
|
+
expect(manifest).toHaveProperty('paths')
|
|
87
|
+
expect(Array.isArray(manifest.paths)).toBe(true)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('manifest JSON contains errors array', async () => {
|
|
91
|
+
await buildSSG(makeConfig())
|
|
92
|
+
const manifestCall = vi.mocked(writeFile).mock.calls.find(([p]) =>
|
|
93
|
+
String(p).includes('ssg-manifest.json'),
|
|
94
|
+
)
|
|
95
|
+
const manifest = JSON.parse(String(manifestCall![1]))
|
|
96
|
+
expect(manifest).toHaveProperty('errors')
|
|
97
|
+
expect(Array.isArray(manifest.errors)).toBe(true)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('records render errors in manifest (missing server bundle)', async () => {
|
|
101
|
+
const config = makeConfig({ ssg: { routes: ['/about'], concurrency: 1 } } as Partial<ResolvedCerConfig>)
|
|
102
|
+
await buildSSG(config)
|
|
103
|
+
const manifestCall = vi.mocked(writeFile).mock.calls.find(([p]) =>
|
|
104
|
+
String(p).includes('ssg-manifest.json'),
|
|
105
|
+
)
|
|
106
|
+
const manifest = JSON.parse(String(manifestCall![1]))
|
|
107
|
+
// paths + errors together must cover every route we attempted to render
|
|
108
|
+
expect(manifest.paths.length + manifest.errors.length).toBe(1)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('error entries have path and error fields', async () => {
|
|
112
|
+
const config = makeConfig({ ssg: { routes: ['/fail'], concurrency: 1 } } as Partial<ResolvedCerConfig>)
|
|
113
|
+
await buildSSG(config)
|
|
114
|
+
const manifestCall = vi.mocked(writeFile).mock.calls.find(([p]) =>
|
|
115
|
+
String(p).includes('ssg-manifest.json'),
|
|
116
|
+
)
|
|
117
|
+
const manifest = JSON.parse(String(manifestCall![1]))
|
|
118
|
+
if (manifest.errors.length > 0) {
|
|
119
|
+
expect(manifest.errors[0]).toHaveProperty('path')
|
|
120
|
+
expect(manifest.errors[0]).toHaveProperty('error')
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('buildSSG — path collection', () => {
|
|
126
|
+
it('uses ssg.routes when explicitly provided (skips auto-discovery)', async () => {
|
|
127
|
+
const config = makeConfig({
|
|
128
|
+
ssg: { routes: ['/a', '/b'], concurrency: 1 },
|
|
129
|
+
} as Partial<ResolvedCerConfig>)
|
|
130
|
+
await buildSSG(config)
|
|
131
|
+
// fast-glob should NOT have been called — routes are explicit
|
|
132
|
+
expect(fg).not.toHaveBeenCalled()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('calls fg when pagesDir exists and no explicit routes', async () => {
|
|
136
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
137
|
+
await buildSSG(makeConfig())
|
|
138
|
+
expect(fg).toHaveBeenCalledTimes(1)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('skips Vite dev server when all discovered pages are static', async () => {
|
|
142
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
143
|
+
vi.mocked(fg).mockResolvedValue([
|
|
144
|
+
'/project/app/pages/index.ts',
|
|
145
|
+
'/project/app/pages/about.ts',
|
|
146
|
+
])
|
|
147
|
+
vi.mocked(buildRouteEntry)
|
|
148
|
+
.mockReturnValueOnce({ routePath: '/', isDynamic: false, isCatchAll: false } as ReturnType<typeof buildRouteEntry>)
|
|
149
|
+
.mockReturnValueOnce({ routePath: '/about', isDynamic: false, isCatchAll: false } as ReturnType<typeof buildRouteEntry>)
|
|
150
|
+
|
|
151
|
+
await buildSSG(makeConfig())
|
|
152
|
+
|
|
153
|
+
expect(createServer).not.toHaveBeenCalled()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('spawns Vite dev server for dynamic pages', async () => {
|
|
157
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
158
|
+
vi.mocked(fg).mockResolvedValue(['/project/app/pages/[slug].ts'])
|
|
159
|
+
vi.mocked(buildRouteEntry).mockReturnValueOnce({
|
|
160
|
+
routePath: '/:slug',
|
|
161
|
+
isDynamic: true,
|
|
162
|
+
isCatchAll: false,
|
|
163
|
+
} as ReturnType<typeof buildRouteEntry>)
|
|
164
|
+
|
|
165
|
+
const closeFn = vi.fn().mockResolvedValue(undefined)
|
|
166
|
+
vi.mocked(createServer).mockResolvedValue({
|
|
167
|
+
ssrLoadModule: vi.fn().mockResolvedValue({}),
|
|
168
|
+
close: closeFn,
|
|
169
|
+
} as unknown as Awaited<ReturnType<typeof createServer>>)
|
|
170
|
+
|
|
171
|
+
await buildSSG(makeConfig())
|
|
172
|
+
|
|
173
|
+
expect(createServer).toHaveBeenCalledTimes(1)
|
|
174
|
+
expect(closeFn).toHaveBeenCalledTimes(1)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('closes Vite dev server even when ssrLoadModule throws', async () => {
|
|
178
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
179
|
+
vi.mocked(fg).mockResolvedValue(['/project/app/pages/[slug].ts'])
|
|
180
|
+
vi.mocked(buildRouteEntry).mockReturnValueOnce({
|
|
181
|
+
routePath: '/:slug',
|
|
182
|
+
isDynamic: true,
|
|
183
|
+
isCatchAll: false,
|
|
184
|
+
} as ReturnType<typeof buildRouteEntry>)
|
|
185
|
+
|
|
186
|
+
const closeFn = vi.fn().mockResolvedValue(undefined)
|
|
187
|
+
vi.mocked(createServer).mockResolvedValue({
|
|
188
|
+
ssrLoadModule: vi.fn().mockRejectedValue(new Error('load failed')),
|
|
189
|
+
close: closeFn,
|
|
190
|
+
} as unknown as Awaited<ReturnType<typeof createServer>>)
|
|
191
|
+
|
|
192
|
+
await buildSSG(makeConfig())
|
|
193
|
+
|
|
194
|
+
expect(closeFn).toHaveBeenCalledTimes(1)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('expands dynamic ssg.paths into concrete URL paths', async () => {
|
|
198
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
199
|
+
vi.mocked(fg).mockResolvedValue(['/project/app/pages/[id].ts'])
|
|
200
|
+
vi.mocked(buildRouteEntry).mockReturnValueOnce({
|
|
201
|
+
routePath: '/:id',
|
|
202
|
+
isDynamic: true,
|
|
203
|
+
isCatchAll: false,
|
|
204
|
+
} as ReturnType<typeof buildRouteEntry>)
|
|
205
|
+
|
|
206
|
+
const ssgPathsFn = vi.fn().mockResolvedValue([
|
|
207
|
+
{ params: { id: '1' } },
|
|
208
|
+
{ params: { id: '2' } },
|
|
209
|
+
])
|
|
210
|
+
const closeFn = vi.fn().mockResolvedValue(undefined)
|
|
211
|
+
vi.mocked(createServer).mockResolvedValue({
|
|
212
|
+
ssrLoadModule: vi.fn().mockResolvedValue({ meta: { ssg: { paths: ssgPathsFn } } }),
|
|
213
|
+
close: closeFn,
|
|
214
|
+
} as unknown as Awaited<ReturnType<typeof createServer>>)
|
|
215
|
+
|
|
216
|
+
const config = makeConfig({ ssg: { concurrency: 1 } } as Partial<ResolvedCerConfig>)
|
|
217
|
+
await buildSSG(config)
|
|
218
|
+
|
|
219
|
+
// The manifest should attempt to render '/', '/1', '/2' (3 paths total)
|
|
220
|
+
const manifestCall = vi.mocked(writeFile).mock.calls.find(([p]) =>
|
|
221
|
+
String(p).includes('ssg-manifest.json'),
|
|
222
|
+
)
|
|
223
|
+
const manifest = JSON.parse(String(manifestCall![1]))
|
|
224
|
+
expect(manifest.paths.length + manifest.errors.length).toBe(3)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('skips catch-all pages when auto-discovering paths', async () => {
|
|
228
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
229
|
+
vi.mocked(fg).mockResolvedValue(['/project/app/pages/[...all].ts'])
|
|
230
|
+
vi.mocked(buildRouteEntry).mockReturnValueOnce({
|
|
231
|
+
routePath: '/:all*',
|
|
232
|
+
isDynamic: true,
|
|
233
|
+
isCatchAll: true,
|
|
234
|
+
} as ReturnType<typeof buildRouteEntry>)
|
|
235
|
+
|
|
236
|
+
await buildSSG(makeConfig())
|
|
237
|
+
|
|
238
|
+
// Only '/' (always added) should be attempted
|
|
239
|
+
const manifestCall = vi.mocked(writeFile).mock.calls.find(([p]) =>
|
|
240
|
+
String(p).includes('ssg-manifest.json'),
|
|
241
|
+
)
|
|
242
|
+
const manifest = JSON.parse(String(manifestCall![1]))
|
|
243
|
+
expect(manifest.paths.length + manifest.errors.length).toBe(1)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('deduplicates collected paths', async () => {
|
|
247
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
248
|
+
vi.mocked(fg).mockResolvedValue([
|
|
249
|
+
'/project/app/pages/index.ts',
|
|
250
|
+
'/project/app/pages/home.ts',
|
|
251
|
+
])
|
|
252
|
+
// Both resolve to '/' — should deduplicate to a single path
|
|
253
|
+
vi.mocked(buildRouteEntry)
|
|
254
|
+
.mockReturnValueOnce({ routePath: '/', isDynamic: false, isCatchAll: false } as ReturnType<typeof buildRouteEntry>)
|
|
255
|
+
.mockReturnValueOnce({ routePath: '/', isDynamic: false, isCatchAll: false } as ReturnType<typeof buildRouteEntry>)
|
|
256
|
+
|
|
257
|
+
await buildSSG(makeConfig({ ssg: { concurrency: 1 } } as Partial<ResolvedCerConfig>))
|
|
258
|
+
|
|
259
|
+
const manifestCall = vi.mocked(writeFile).mock.calls.find(([p]) =>
|
|
260
|
+
String(p).includes('ssg-manifest.json'),
|
|
261
|
+
)
|
|
262
|
+
const manifest = JSON.parse(String(manifestCall![1]))
|
|
263
|
+
expect(manifest.paths.length + manifest.errors.length).toBe(1)
|
|
264
|
+
})
|
|
265
|
+
})
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { resolve } from 'pathe'
|
|
4
|
+
|
|
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).
|
|
8
|
+
vi.mock('vite', () => ({ build: vi.fn().mockResolvedValue(undefined) }))
|
|
9
|
+
// Partial mock: keep the real readFileSync/existsSync but allow overrides in
|
|
10
|
+
// individual describe blocks if needed.
|
|
11
|
+
vi.mock('node:fs', async (importOriginal) => {
|
|
12
|
+
const actual = await importOriginal<typeof import('node:fs')>()
|
|
13
|
+
return { ...actual, existsSync: vi.fn().mockReturnValue(true) }
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
import type { ResolvedCerConfig } from '../../plugin/dev-server.js'
|
|
17
|
+
|
|
18
|
+
// Build a minimal ResolvedCerConfig so we can call generateServerEntryCode
|
|
19
|
+
// without spinning up a real Vite build.
|
|
20
|
+
function makeConfig(overrides: Partial<ResolvedCerConfig> = {}): ResolvedCerConfig {
|
|
21
|
+
return {
|
|
22
|
+
root: '/project',
|
|
23
|
+
srcDir: '/project/app',
|
|
24
|
+
mode: 'ssr',
|
|
25
|
+
ssr: { dsd: true },
|
|
26
|
+
ssg: { paths: [], concurrency: 4 },
|
|
27
|
+
...overrides,
|
|
28
|
+
} as unknown as ResolvedCerConfig
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('build-ssr generateServerEntryCode (template content)', () => {
|
|
32
|
+
// Read the source of build-ssr.ts to assert it contains the expected
|
|
33
|
+
// generated code strings. This is intentionally coarse-grained:
|
|
34
|
+
// we check that the template emits the right imports, exports, and
|
|
35
|
+
// structural elements rather than testing every character.
|
|
36
|
+
const src = readFileSync(
|
|
37
|
+
resolve(import.meta.dirname, '../../plugin/build-ssr.ts'),
|
|
38
|
+
'utf-8',
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
it('template imports registerBuiltinComponents from custom-elements-runtime', () => {
|
|
42
|
+
expect(src).toContain('registerBuiltinComponents')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('template imports renderToStringWithJITCSS from ssr subpath', () => {
|
|
46
|
+
expect(src).toContain('renderToStringWithJITCSS')
|
|
47
|
+
expect(src).toContain('custom-elements-runtime/ssr')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('template imports initRouter from router subpath', () => {
|
|
51
|
+
expect(src).toContain('initRouter')
|
|
52
|
+
expect(src).toContain('custom-elements-runtime/router')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('template loads client index.html for merging', () => {
|
|
56
|
+
expect(src).toContain('_clientTemplate')
|
|
57
|
+
expect(src).toContain('../client/index.html')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('template defines _mergeWithClientTemplate helper', () => {
|
|
61
|
+
expect(src).toContain('_mergeWithClientTemplate')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('template defines _prepareRequest async function', () => {
|
|
65
|
+
expect(src).toContain('_prepareRequest')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('template exports handler as both named and default export', () => {
|
|
69
|
+
expect(src).toContain('export const handler')
|
|
70
|
+
expect(src).toContain('export default handler')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('template exports apiRoutes, plugins, and layouts', () => {
|
|
74
|
+
expect(src).toContain('export { apiRoutes, plugins, layouts }')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('template sets globalThis.__CER_DATA__ synchronously before render', () => {
|
|
78
|
+
expect(src).toContain('globalThis).__CER_DATA__ = loaderData')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('template deletes __CER_DATA__ after render', () => {
|
|
82
|
+
expect(src).toContain('delete (globalThis).__CER_DATA__')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('template uses renderToStringWithJITCSSDSD (dsd always on)', () => {
|
|
86
|
+
expect(src).toContain('renderToStringWithJITCSSDSD')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('template passes dsdPolyfill: false to suppress inline polyfill', () => {
|
|
90
|
+
expect(src).toContain('dsdPolyfill: false')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('template calls registerEntityMap with entities.json', () => {
|
|
94
|
+
expect(src).toContain('registerEntityMap(entitiesJson)')
|
|
95
|
+
expect(src).toContain('entities.json')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('template imports DSD_POLYFILL_SCRIPT and injects before </body>', () => {
|
|
99
|
+
expect(src).toContain('DSD_POLYFILL_SCRIPT')
|
|
100
|
+
expect(src).toContain("finalHtml.replace('</body>'")
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('template merges SSR html with client template when available', () => {
|
|
104
|
+
expect(src).toContain('_clientTemplate')
|
|
105
|
+
expect(src).toContain('_mergeWithClientTemplate(ssrHtml, _clientTemplate)')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('template reads virtual:cer-routes', () => {
|
|
109
|
+
expect(src).toContain('virtual:cer-routes')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('template reads virtual:cer-layouts', () => {
|
|
113
|
+
expect(src).toContain('virtual:cer-layouts')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('template reads virtual:cer-plugins', () => {
|
|
117
|
+
expect(src).toContain('virtual:cer-plugins')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('template reads virtual:cer-server-api', () => {
|
|
121
|
+
expect(src).toContain('virtual:cer-server-api')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('template reads virtual:cer-components', () => {
|
|
125
|
+
expect(src).toContain('virtual:cer-components')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('sets Content-Type header on response', () => {
|
|
129
|
+
expect(src).toContain('text/html; charset=utf-8')
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe('buildSSR', () => {
|
|
134
|
+
let buildMock: ReturnType<typeof vi.fn>
|
|
135
|
+
let buildSSR: (config: ResolvedCerConfig, userConfig?: Record<string, unknown>) => Promise<void>
|
|
136
|
+
|
|
137
|
+
beforeEach(async () => {
|
|
138
|
+
const { build } = await import('vite')
|
|
139
|
+
buildMock = vi.mocked(build)
|
|
140
|
+
buildMock.mockClear()
|
|
141
|
+
buildMock.mockResolvedValue(undefined as never)
|
|
142
|
+
;({ buildSSR } = await import('../../plugin/build-ssr.js'))
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('calls vite build twice (client then server)', async () => {
|
|
146
|
+
await buildSSR(makeConfig())
|
|
147
|
+
expect(buildMock).toHaveBeenCalledTimes(2)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('first build targets dist/client output dir', async () => {
|
|
151
|
+
await buildSSR(makeConfig())
|
|
152
|
+
const firstCall = buildMock.mock.calls[0][0] as Record<string, unknown>
|
|
153
|
+
expect((firstCall.build as Record<string, unknown>).outDir).toContain('dist/client')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('second build targets dist/server output dir', async () => {
|
|
157
|
+
await buildSSR(makeConfig())
|
|
158
|
+
const secondCall = buildMock.mock.calls[1][0] as Record<string, unknown>
|
|
159
|
+
expect((secondCall.build as Record<string, unknown>).outDir).toContain('dist/server')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('second build has ssr:true', async () => {
|
|
163
|
+
await buildSSR(makeConfig())
|
|
164
|
+
const secondCall = buildMock.mock.calls[1][0] as Record<string, unknown>
|
|
165
|
+
expect((secondCall.build as Record<string, unknown>).ssr).toBe(true)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('server bundle entry file is named server.js', async () => {
|
|
169
|
+
await buildSSR(makeConfig())
|
|
170
|
+
const secondCall = buildMock.mock.calls[1][0] as Record<string, unknown>
|
|
171
|
+
const rollup = (secondCall.build as Record<string, unknown>).rollupOptions as Record<string, unknown>
|
|
172
|
+
expect((rollup.output as Record<string, unknown>).entryFileNames).toBe('server.js')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('merges user viteUserConfig into client build', async () => {
|
|
176
|
+
await buildSSR(makeConfig(), { define: { MY_FLAG: 'true' } })
|
|
177
|
+
const firstCall = buildMock.mock.calls[0][0] as Record<string, unknown>
|
|
178
|
+
expect(firstCall.define).toEqual({ MY_FLAG: 'true' })
|
|
179
|
+
})
|
|
180
|
+
})
|