@jasonshimmy/vite-plugin-cer-app 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +56 -5
- package/CHANGELOG.md +4 -0
- package/commits.txt +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +19 -5
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/dev.js +1 -1
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/preview.d.ts.map +1 -1
- package/dist/cli/commands/preview.js +0 -1
- package/dist/cli/commands/preview.js.map +1 -1
- package/dist/plugin/build-ssg.d.ts.map +1 -1
- package/dist/plugin/build-ssg.js.map +1 -1
- package/dist/plugin/build-ssr.d.ts +10 -0
- package/dist/plugin/build-ssr.d.ts.map +1 -1
- package/dist/plugin/build-ssr.js +21 -8
- package/dist/plugin/build-ssr.js.map +1 -1
- package/dist/plugin/dev-server.d.ts.map +1 -1
- package/dist/plugin/dev-server.js +0 -2
- package/dist/plugin/dev-server.js.map +1 -1
- package/dist/plugin/dts-generator.d.ts +4 -4
- package/dist/plugin/dts-generator.d.ts.map +1 -1
- package/dist/plugin/dts-generator.js +39 -19
- package/dist/plugin/dts-generator.js.map +1 -1
- package/dist/plugin/generated-dir.d.ts +34 -0
- package/dist/plugin/generated-dir.d.ts.map +1 -0
- package/dist/plugin/generated-dir.js +94 -0
- package/dist/plugin/generated-dir.js.map +1 -0
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +27 -0
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/path-utils.js.map +1 -1
- package/dist/plugin/virtual/loading.d.ts.map +1 -1
- package/dist/plugin/virtual/loading.js.map +1 -1
- package/dist/runtime/app-template.d.ts +8 -0
- package/dist/runtime/app-template.d.ts.map +1 -0
- package/dist/runtime/app-template.js +158 -0
- package/dist/runtime/app-template.js.map +1 -0
- package/docs/configuration.md +2 -2
- package/docs/rendering-modes.md +2 -2
- package/docs/routing.md +1 -1
- package/e2e/kitchen-sink/tsconfig.json +3 -0
- package/eslint.config.ts +22 -0
- package/package.json +6 -1
- package/src/__tests__/plugin/build-ssr.test.ts +24 -10
- package/src/__tests__/plugin/cer-app-plugin.test.ts +35 -0
- package/src/__tests__/plugin/dts-generator.test.ts +15 -6
- package/src/__tests__/plugin/generated-dir.test.ts +168 -0
- package/src/cli/commands/build.ts +19 -5
- package/src/cli/commands/dev.ts +2 -2
- package/src/cli/commands/preview.ts +7 -5
- package/src/plugin/build-ssg.ts +2 -2
- package/src/plugin/build-ssr.ts +22 -8
- package/src/plugin/dev-server.ts +4 -3
- package/src/plugin/dts-generator.ts +43 -19
- package/src/plugin/generated-dir.ts +102 -0
- package/src/plugin/index.ts +32 -1
- package/src/plugin/path-utils.ts +1 -1
- package/src/plugin/virtual/loading.ts +0 -1
- package/{e2e/kitchen-sink/app/app.ts → src/runtime/app-template.ts} +23 -7
- package/e2e/kitchen-sink/index.html +0 -12
|
@@ -6,11 +6,16 @@ import { resolve } from 'pathe'
|
|
|
6
6
|
// The `buildSSR` function itself invokes Vite's `build` API which we don't
|
|
7
7
|
// need to exercise in unit tests (it's an integration concern).
|
|
8
8
|
vi.mock('vite', () => ({ build: vi.fn().mockResolvedValue(undefined) }))
|
|
9
|
+
vi.mock('../../plugin/generated-dir.js', () => ({
|
|
10
|
+
writeGeneratedDir: vi.fn(),
|
|
11
|
+
getGeneratedDir: vi.fn().mockReturnValue('/project/.cer'),
|
|
12
|
+
GENERATED_DIR_NAME: '.cer',
|
|
13
|
+
}))
|
|
9
14
|
// Partial mock: keep the real readFileSync/existsSync but allow overrides in
|
|
10
15
|
// individual describe blocks if needed.
|
|
11
16
|
vi.mock('node:fs', async (importOriginal) => {
|
|
12
17
|
const actual = await importOriginal<typeof import('node:fs')>()
|
|
13
|
-
return { ...actual, existsSync: vi.fn().mockReturnValue(true) }
|
|
18
|
+
return { ...actual, existsSync: vi.fn().mockReturnValue(true), renameSync: vi.fn() }
|
|
14
19
|
})
|
|
15
20
|
|
|
16
21
|
import type { ResolvedCerConfig } from '../../plugin/dev-server.js'
|
|
@@ -202,24 +207,33 @@ describe('buildSSR — resolveClientEntry fallbacks', () => {
|
|
|
202
207
|
})
|
|
203
208
|
|
|
204
209
|
it('uses index.html when it exists', async () => {
|
|
205
|
-
existsSyncMock.
|
|
210
|
+
existsSyncMock.mockImplementation((p: unknown) => String(p).endsWith('index.html'))
|
|
206
211
|
await buildSSR(makeConfig())
|
|
207
|
-
const clientInput = (buildMock.mock.calls[0][0] as
|
|
208
|
-
expect(clientInput).toMatch(/index\.html$/)
|
|
212
|
+
const clientInput = (buildMock.mock.calls[0][0] as Record<string, unknown>)
|
|
213
|
+
expect(((clientInput.build as Record<string, unknown>).rollupOptions as Record<string, unknown>).input).toMatch(/(?<!\.cer\/)index\.html$/)
|
|
209
214
|
})
|
|
210
215
|
|
|
211
|
-
it('falls back to
|
|
216
|
+
it('falls back to .cer/index.html when root index.html is absent', async () => {
|
|
217
|
+
existsSyncMock.mockImplementation((p: unknown) =>
|
|
218
|
+
String(p).endsWith('.cer/index.html'),
|
|
219
|
+
)
|
|
220
|
+
await buildSSR(makeConfig())
|
|
221
|
+
const clientInput = (buildMock.mock.calls[0][0] as Record<string, unknown>)
|
|
222
|
+
expect(((clientInput.build as Record<string, unknown>).rollupOptions as Record<string, unknown>).input).toMatch(/\.cer\/index\.html$/)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('falls back to entry-client.ts when no index.html exists', async () => {
|
|
212
226
|
existsSyncMock.mockImplementation((p: unknown) => String(p).endsWith('entry-client.ts'))
|
|
213
227
|
await buildSSR(makeConfig())
|
|
214
|
-
const clientInput = (buildMock.mock.calls[0][0] as
|
|
215
|
-
expect(clientInput).toMatch(/entry-client\.ts$/)
|
|
228
|
+
const clientInput = (buildMock.mock.calls[0][0] as Record<string, unknown>)
|
|
229
|
+
expect(((clientInput.build as Record<string, unknown>).rollupOptions as Record<string, unknown>).input).toMatch(/entry-client\.ts$/)
|
|
216
230
|
})
|
|
217
231
|
|
|
218
|
-
it('falls back to app.ts when
|
|
232
|
+
it('falls back to app.ts when nothing else exists', async () => {
|
|
219
233
|
existsSyncMock.mockReturnValue(false)
|
|
220
234
|
await buildSSR(makeConfig())
|
|
221
|
-
const clientInput = (buildMock.mock.calls[0][0] as
|
|
222
|
-
expect(clientInput).toMatch(/app\.ts$/)
|
|
235
|
+
const clientInput = (buildMock.mock.calls[0][0] as Record<string, unknown>)
|
|
236
|
+
expect(((clientInput.build as Record<string, unknown>).rollupOptions as Record<string, unknown>).input).toMatch(/app\.ts$/)
|
|
223
237
|
})
|
|
224
238
|
})
|
|
225
239
|
|
|
@@ -3,6 +3,10 @@ import { vi, describe, it, expect, beforeEach } from 'vitest'
|
|
|
3
3
|
vi.mock('@jasonshimmy/custom-elements-runtime/vite-plugin', () => ({
|
|
4
4
|
cerPlugin: vi.fn().mockReturnValue([{ name: 'cer-runtime-plugin' }]),
|
|
5
5
|
}))
|
|
6
|
+
vi.mock('node:fs', async (importOriginal) => {
|
|
7
|
+
const actual = await importOriginal<typeof import('node:fs')>()
|
|
8
|
+
return { ...actual, existsSync: vi.fn().mockReturnValue(true), readFileSync: vi.fn().mockReturnValue('') }
|
|
9
|
+
})
|
|
6
10
|
vi.mock('../../plugin/dev-server.js', () => ({
|
|
7
11
|
configureCerDevServer: vi.fn().mockResolvedValue(undefined),
|
|
8
12
|
}))
|
|
@@ -15,6 +19,11 @@ vi.mock('../../plugin/dts-generator.js', () => ({
|
|
|
15
19
|
writeAutoImportDts: vi.fn().mockResolvedValue(undefined),
|
|
16
20
|
writeTsconfigPaths: vi.fn(),
|
|
17
21
|
}))
|
|
22
|
+
vi.mock('../../plugin/generated-dir.js', () => ({
|
|
23
|
+
writeGeneratedDir: vi.fn(),
|
|
24
|
+
getGeneratedDir: vi.fn().mockReturnValue('/project/.cer'),
|
|
25
|
+
GENERATED_DIR_NAME: '.cer',
|
|
26
|
+
}))
|
|
18
27
|
vi.mock('../../plugin/virtual/routes.js', () => ({ generateRoutesCode: vi.fn().mockResolvedValue('// routes') }))
|
|
19
28
|
vi.mock('../../plugin/virtual/layouts.js', () => ({ generateLayoutsCode: vi.fn().mockResolvedValue('// layouts') }))
|
|
20
29
|
vi.mock('../../plugin/virtual/components.js', () => ({ generateComponentsCode: vi.fn().mockResolvedValue('// components') }))
|
|
@@ -263,6 +272,16 @@ describe('cerApp plugin — transform hook', () => {
|
|
|
263
272
|
})
|
|
264
273
|
|
|
265
274
|
describe('cerApp plugin — buildStart hook', () => {
|
|
275
|
+
it('calls writeGeneratedDir on build start', async () => {
|
|
276
|
+
const { writeGeneratedDir } = await import('../../plugin/generated-dir.js')
|
|
277
|
+
vi.mocked(writeGeneratedDir).mockClear()
|
|
278
|
+
const plugin = getCerPlugin()
|
|
279
|
+
plugin.config({ root: '/project' }, { command: 'build', mode: 'production' })
|
|
280
|
+
plugin.configResolved(FAKE_RESOLVED)
|
|
281
|
+
await plugin.buildStart()
|
|
282
|
+
expect(writeGeneratedDir).toHaveBeenCalledTimes(1)
|
|
283
|
+
})
|
|
284
|
+
|
|
266
285
|
it('calls scanComposableExports on build start', async () => {
|
|
267
286
|
const { scanComposableExports } = await import('../../plugin/dts-generator.js')
|
|
268
287
|
vi.mocked(scanComposableExports).mockClear()
|
|
@@ -295,6 +314,22 @@ describe('cerApp plugin — buildStart hook', () => {
|
|
|
295
314
|
})
|
|
296
315
|
|
|
297
316
|
describe('cerApp plugin — configureServer hook', () => {
|
|
317
|
+
it('calls writeGeneratedDir on server configure', async () => {
|
|
318
|
+
const { writeGeneratedDir } = await import('../../plugin/generated-dir.js')
|
|
319
|
+
vi.mocked(writeGeneratedDir).mockClear()
|
|
320
|
+
const plugin = getCerPlugin()
|
|
321
|
+
plugin.config({ root: '/project' }, { command: 'serve', mode: 'development' })
|
|
322
|
+
plugin.configResolved(FAKE_RESOLVED)
|
|
323
|
+
const mockServer = {
|
|
324
|
+
watcher: { on: vi.fn() },
|
|
325
|
+
moduleGraph: { getModuleById: vi.fn().mockReturnValue(null), invalidateModule: vi.fn() },
|
|
326
|
+
ws: { send: vi.fn() },
|
|
327
|
+
middlewares: { use: vi.fn() },
|
|
328
|
+
}
|
|
329
|
+
await plugin.configureServer(mockServer)
|
|
330
|
+
expect(writeGeneratedDir).toHaveBeenCalledTimes(1)
|
|
331
|
+
})
|
|
332
|
+
|
|
298
333
|
it('calls scanComposableExports on server configure', async () => {
|
|
299
334
|
const { scanComposableExports } = await import('../../plugin/dts-generator.js')
|
|
300
335
|
vi.mocked(scanComposableExports).mockClear()
|
|
@@ -6,10 +6,12 @@ vi.mock('node:fs', async (importOriginal) => {
|
|
|
6
6
|
...actual,
|
|
7
7
|
existsSync: vi.fn().mockReturnValue(false),
|
|
8
8
|
writeFileSync: vi.fn(),
|
|
9
|
+
mkdirSync: vi.fn(),
|
|
9
10
|
readFileSync: vi.fn().mockReturnValue(''),
|
|
10
11
|
}
|
|
11
12
|
})
|
|
12
13
|
vi.mock('../../plugin/scanner.js', () => ({ scanDirectory: vi.fn().mockResolvedValue([]) }))
|
|
14
|
+
vi.mock('../../plugin/generated-dir.js', () => ({ GENERATED_DIR_NAME: '.cer' }))
|
|
13
15
|
|
|
14
16
|
import { existsSync, writeFileSync, readFileSync } from 'node:fs'
|
|
15
17
|
import { scanDirectory } from '../../plugin/scanner.js'
|
|
@@ -32,10 +34,10 @@ beforeEach(() => {
|
|
|
32
34
|
})
|
|
33
35
|
|
|
34
36
|
describe('writeTsconfigPaths', () => {
|
|
35
|
-
it('writes
|
|
37
|
+
it('writes tsconfig.json to the .cer directory', () => {
|
|
36
38
|
writeTsconfigPaths(ROOT, `${ROOT}/app`)
|
|
37
39
|
expect(writeFileSync).toHaveBeenCalledWith(
|
|
38
|
-
`${ROOT}/
|
|
40
|
+
`${ROOT}/.cer/tsconfig.json`,
|
|
39
41
|
expect.any(String),
|
|
40
42
|
'utf-8',
|
|
41
43
|
)
|
|
@@ -65,6 +67,13 @@ describe('writeTsconfigPaths', () => {
|
|
|
65
67
|
const json = JSON.parse(content)
|
|
66
68
|
expect(json).toHaveProperty('compilerOptions.paths')
|
|
67
69
|
})
|
|
70
|
+
|
|
71
|
+
it('includes project source directories in include array', () => {
|
|
72
|
+
writeTsconfigPaths(ROOT, `${ROOT}/app`)
|
|
73
|
+
const content = vi.mocked(writeFileSync).mock.calls[0][1] as string
|
|
74
|
+
const json = JSON.parse(content) as { include?: string[] }
|
|
75
|
+
expect(Array.isArray(json.include)).toBe(true)
|
|
76
|
+
})
|
|
68
77
|
})
|
|
69
78
|
|
|
70
79
|
describe('scanComposableExports', () => {
|
|
@@ -227,16 +236,16 @@ describe('generateVirtualModuleDts', () => {
|
|
|
227
236
|
})
|
|
228
237
|
|
|
229
238
|
describe('writeAutoImportDts', () => {
|
|
230
|
-
it('writes
|
|
239
|
+
it('writes auto-imports.d.ts to .cer/', async () => {
|
|
231
240
|
await writeAutoImportDts(ROOT, COMPOSABLES_DIR)
|
|
232
241
|
const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
|
|
233
|
-
expect(paths.some(p => p.includes('cer
|
|
242
|
+
expect(paths.some(p => p.includes('.cer/auto-imports.d.ts'))).toBe(true)
|
|
234
243
|
})
|
|
235
244
|
|
|
236
|
-
it('writes
|
|
245
|
+
it('writes env.d.ts to .cer/', async () => {
|
|
237
246
|
await writeAutoImportDts(ROOT, COMPOSABLES_DIR)
|
|
238
247
|
const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
|
|
239
|
-
expect(paths.some(p => p.includes('cer
|
|
248
|
+
expect(paths.some(p => p.includes('.cer/env.d.ts'))).toBe(true)
|
|
240
249
|
})
|
|
241
250
|
|
|
242
251
|
it('writes exactly two files', async () => {
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
vi.mock('node:fs', async (importOriginal) => {
|
|
4
|
+
const actual = await importOriginal<typeof import('node:fs')>()
|
|
5
|
+
return {
|
|
6
|
+
...actual,
|
|
7
|
+
existsSync: vi.fn().mockReturnValue(false),
|
|
8
|
+
writeFileSync: vi.fn(),
|
|
9
|
+
mkdirSync: vi.fn(),
|
|
10
|
+
readFileSync: vi.fn().mockReturnValue(''),
|
|
11
|
+
appendFileSync: vi.fn(),
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
vi.mock('../../runtime/app-template.js', () => ({ APP_ENTRY_TEMPLATE: '// app template' }))
|
|
15
|
+
|
|
16
|
+
import { existsSync, writeFileSync, mkdirSync, readFileSync, appendFileSync } from 'node:fs'
|
|
17
|
+
import {
|
|
18
|
+
GENERATED_DIR_NAME,
|
|
19
|
+
getGeneratedDir,
|
|
20
|
+
resolveAppEntry,
|
|
21
|
+
resolveHtmlEntry,
|
|
22
|
+
generateDefaultHtml,
|
|
23
|
+
writeGeneratedDir,
|
|
24
|
+
} from '../../plugin/generated-dir.js'
|
|
25
|
+
|
|
26
|
+
const ROOT = '/project'
|
|
27
|
+
const mockConfig = {
|
|
28
|
+
root: ROOT,
|
|
29
|
+
srcDir: `${ROOT}/app`,
|
|
30
|
+
} as Parameters<typeof writeGeneratedDir>[0]
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.mocked(existsSync).mockReturnValue(false)
|
|
34
|
+
vi.mocked(writeFileSync).mockClear()
|
|
35
|
+
vi.mocked(mkdirSync).mockClear()
|
|
36
|
+
vi.mocked(readFileSync).mockReturnValue('')
|
|
37
|
+
vi.mocked(appendFileSync).mockClear()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('GENERATED_DIR_NAME', () => {
|
|
41
|
+
it('is .cer', () => {
|
|
42
|
+
expect(GENERATED_DIR_NAME).toBe('.cer')
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('getGeneratedDir', () => {
|
|
47
|
+
it('returns <root>/.cer', () => {
|
|
48
|
+
expect(getGeneratedDir(ROOT)).toBe(`${ROOT}/.cer`)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('resolveAppEntry', () => {
|
|
53
|
+
it('returns user app/app.ts when it exists', () => {
|
|
54
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
55
|
+
expect(resolveAppEntry(mockConfig)).toBe(`${ROOT}/app/app.ts`)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('returns .cer/app.ts when user app/app.ts is absent', () => {
|
|
59
|
+
vi.mocked(existsSync).mockReturnValue(false)
|
|
60
|
+
expect(resolveAppEntry(mockConfig)).toBe(`${ROOT}/.cer/app.ts`)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('resolveHtmlEntry', () => {
|
|
65
|
+
it('returns user index.html when it exists', () => {
|
|
66
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
67
|
+
expect(resolveHtmlEntry(mockConfig)).toBe(`${ROOT}/index.html`)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('returns .cer/index.html when user index.html is absent', () => {
|
|
71
|
+
vi.mocked(existsSync).mockReturnValue(false)
|
|
72
|
+
expect(resolveHtmlEntry(mockConfig)).toBe(`${ROOT}/.cer/index.html`)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('generateDefaultHtml', () => {
|
|
77
|
+
it('references /app/app.ts when user entry exists', () => {
|
|
78
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
79
|
+
const html = generateDefaultHtml(mockConfig)
|
|
80
|
+
expect(html).toContain('/app/app.ts')
|
|
81
|
+
expect(html).not.toContain('/.cer/app.ts')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('references /.cer/app.ts when user entry is absent', () => {
|
|
85
|
+
vi.mocked(existsSync).mockReturnValue(false)
|
|
86
|
+
const html = generateDefaultHtml(mockConfig)
|
|
87
|
+
expect(html).toContain('/.cer/app.ts')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('includes <cer-layout-view> mount point', () => {
|
|
91
|
+
const html = generateDefaultHtml(mockConfig)
|
|
92
|
+
expect(html).toContain('<cer-layout-view>')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('is valid HTML with doctype', () => {
|
|
96
|
+
const html = generateDefaultHtml(mockConfig)
|
|
97
|
+
expect(html).toContain('<!DOCTYPE html>')
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('writeGeneratedDir', () => {
|
|
102
|
+
it('creates the .cer directory when absent', () => {
|
|
103
|
+
// existsSync returns false for everything → dir is created
|
|
104
|
+
writeGeneratedDir(mockConfig)
|
|
105
|
+
expect(mkdirSync).toHaveBeenCalledWith(`${ROOT}/.cer`, { recursive: true })
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('does not re-create the directory when it already exists', () => {
|
|
109
|
+
// existsSync returns true → dir already present
|
|
110
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
111
|
+
writeGeneratedDir(mockConfig)
|
|
112
|
+
expect(mkdirSync).not.toHaveBeenCalled()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('writes .cer/app.ts when app/app.ts does not exist', () => {
|
|
116
|
+
// Only the .cer dir check needs to return true to skip mkdirSync — but we
|
|
117
|
+
// want the user entry check to return false. Use a counter.
|
|
118
|
+
let callCount = 0
|
|
119
|
+
vi.mocked(existsSync).mockImplementation(() => {
|
|
120
|
+
callCount++
|
|
121
|
+
// First call: .cer/ dir → true (already exists, skip mkdir)
|
|
122
|
+
// Second call: app/app.ts → false (absent, write template)
|
|
123
|
+
// Subsequent calls: false (no .gitignore)
|
|
124
|
+
return callCount === 1
|
|
125
|
+
})
|
|
126
|
+
writeGeneratedDir(mockConfig)
|
|
127
|
+
const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
|
|
128
|
+
expect(paths.some(p => p.endsWith('/.cer/app.ts'))).toBe(true)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('skips writing .cer/app.ts when app/app.ts exists', () => {
|
|
132
|
+
// existsSync always returns true — dir exists, user entry exists
|
|
133
|
+
vi.mocked(existsSync).mockReturnValue(true)
|
|
134
|
+
writeGeneratedDir(mockConfig)
|
|
135
|
+
const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
|
|
136
|
+
expect(paths.some(p => p.endsWith('/.cer/app.ts'))).toBe(false)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('always writes .cer/index.html', () => {
|
|
140
|
+
writeGeneratedDir(mockConfig)
|
|
141
|
+
const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
|
|
142
|
+
expect(paths.some(p => p.endsWith('/.cer/index.html'))).toBe(true)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('creates .gitignore when absent', () => {
|
|
146
|
+
// existsSync returns false → .cer/ dir created, app.ts written, .gitignore created
|
|
147
|
+
writeGeneratedDir(mockConfig)
|
|
148
|
+
const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
|
|
149
|
+
expect(paths.some(p => p.endsWith('/.gitignore'))).toBe(true)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('appends .cer/ to existing .gitignore that does not contain it', () => {
|
|
153
|
+
// .gitignore exists (readFileSync returns '' — no .cer/ entry)
|
|
154
|
+
vi.mocked(existsSync).mockImplementation((p) => String(p).endsWith('.gitignore'))
|
|
155
|
+
vi.mocked(readFileSync).mockReturnValue('node_modules/\ndist/\n')
|
|
156
|
+
writeGeneratedDir(mockConfig)
|
|
157
|
+
expect(appendFileSync).toHaveBeenCalled()
|
|
158
|
+
const appendArg = vi.mocked(appendFileSync).mock.calls[0][1] as string
|
|
159
|
+
expect(appendArg).toContain('.cer/')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('does not append to .gitignore when .cer/ is already present', () => {
|
|
163
|
+
vi.mocked(existsSync).mockImplementation((p) => String(p).endsWith('.gitignore'))
|
|
164
|
+
vi.mocked(readFileSync).mockReturnValue('node_modules/\n.cer/\ndist/\n')
|
|
165
|
+
writeGeneratedDir(mockConfig)
|
|
166
|
+
expect(appendFileSync).not.toHaveBeenCalled()
|
|
167
|
+
})
|
|
168
|
+
})
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { build } from 'vite'
|
|
3
|
-
import { resolve } from 'pathe'
|
|
3
|
+
import { resolve, join } from 'pathe'
|
|
4
4
|
import { pathToFileURL } from 'node:url'
|
|
5
|
-
import { existsSync } from 'node:fs'
|
|
5
|
+
import { existsSync, renameSync } from 'node:fs'
|
|
6
6
|
import { cerApp, resolveConfig } from '../../plugin/index.js'
|
|
7
|
-
import { buildSSR } from '../../plugin/build-ssr.js'
|
|
7
|
+
import { buildSSR, resolveClientEntry } from '../../plugin/build-ssr.js'
|
|
8
8
|
import { buildSSG } from '../../plugin/build-ssg.js'
|
|
9
|
+
import { writeGeneratedDir } from '../../plugin/generated-dir.js'
|
|
9
10
|
import type { CerAppConfig } from '../../types/config.js'
|
|
10
11
|
|
|
11
12
|
async function loadCerConfig(root: string): Promise<CerAppConfig> {
|
|
@@ -73,15 +74,28 @@ export function buildCommand(): Command {
|
|
|
73
74
|
|
|
74
75
|
switch (config.mode) {
|
|
75
76
|
case 'spa': {
|
|
76
|
-
//
|
|
77
|
+
// Write .cer/ files BEFORE resolveClientEntry checks for .cer/index.html.
|
|
78
|
+
writeGeneratedDir(config)
|
|
79
|
+
const spaEntry = resolveClientEntry(config)
|
|
80
|
+
const spaOutDir = resolve(root, 'dist')
|
|
77
81
|
await build({
|
|
78
82
|
root,
|
|
79
83
|
plugins: cerApp(userConfig),
|
|
80
84
|
build: {
|
|
81
|
-
outDir:
|
|
85
|
+
outDir: spaOutDir,
|
|
86
|
+
rollupOptions: { input: spaEntry },
|
|
82
87
|
},
|
|
83
88
|
})
|
|
89
|
+
// If the entry was .cer/index.html, Vite outputs it as dist/.cer/index.html.
|
|
90
|
+
// Rename it to dist/index.html so the preview server can find it.
|
|
91
|
+
const generatedHtmlOut = join(spaOutDir, '.cer/index.html')
|
|
92
|
+
const rootHtmlOut = join(spaOutDir, 'index.html')
|
|
93
|
+
if (existsSync(generatedHtmlOut) && !existsSync(rootHtmlOut)) {
|
|
94
|
+
renameSync(generatedHtmlOut, rootHtmlOut)
|
|
95
|
+
}
|
|
84
96
|
console.log('[cer-app] SPA build complete.')
|
|
97
|
+
// Force exit: Vite HTML builds may keep Node timers alive.
|
|
98
|
+
process.exit(0)
|
|
85
99
|
break
|
|
86
100
|
}
|
|
87
101
|
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { createServer } from 'vite'
|
|
|
3
3
|
import { resolve } from 'pathe'
|
|
4
4
|
import { pathToFileURL } from 'node:url'
|
|
5
5
|
import { existsSync } from 'node:fs'
|
|
6
|
-
import { cerApp
|
|
6
|
+
import { cerApp } from '../../plugin/index.js'
|
|
7
7
|
import type { CerAppConfig } from '../../types/config.js'
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -28,7 +28,7 @@ async function loadCerConfig(root: string): Promise<CerAppConfig> {
|
|
|
28
28
|
try {
|
|
29
29
|
// Use Vite's build to transpile TS config at runtime
|
|
30
30
|
const { build } = await import('vite')
|
|
31
|
-
|
|
31
|
+
await build({
|
|
32
32
|
build: {
|
|
33
33
|
lib: {
|
|
34
34
|
entry: filePath,
|
|
@@ -107,9 +107,10 @@ export function previewCommand(): Command {
|
|
|
107
107
|
console.log('[cer-app] Starting SSR preview server...')
|
|
108
108
|
|
|
109
109
|
// Load the server bundle
|
|
110
|
+
type SsrHandlerFn = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>
|
|
110
111
|
let serverMod: {
|
|
111
|
-
handler?:
|
|
112
|
-
default?:
|
|
112
|
+
handler?: SsrHandlerFn
|
|
113
|
+
default?: SsrHandlerFn
|
|
113
114
|
apiRoutes?: Array<{ path: string; handlers: Record<string, unknown> }>
|
|
114
115
|
}
|
|
115
116
|
try {
|
|
@@ -151,10 +152,11 @@ export function previewCommand(): Command {
|
|
|
151
152
|
}
|
|
152
153
|
augRes.status = function (code) { this.statusCode = code; return this }
|
|
153
154
|
|
|
155
|
+
type ApiHandlerFn = (req: typeof augReq, res: typeof augRes) => void | Promise<void>
|
|
154
156
|
const handlerFn =
|
|
155
|
-
(route.handlers[method.toLowerCase()] as
|
|
156
|
-
(route.handlers[method.toUpperCase()] as
|
|
157
|
-
(route.handlers['default'] as
|
|
157
|
+
(route.handlers[method.toLowerCase()] as ApiHandlerFn | undefined) ??
|
|
158
|
+
(route.handlers[method.toUpperCase()] as ApiHandlerFn | undefined) ??
|
|
159
|
+
(route.handlers['default'] as ApiHandlerFn | undefined)
|
|
158
160
|
|
|
159
161
|
if (typeof handlerFn === 'function') {
|
|
160
162
|
try {
|
package/src/plugin/build-ssg.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { writeFile, mkdir } from 'node:fs/promises'
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
3
|
import { join } from 'pathe'
|
|
4
|
-
import { createServer,
|
|
4
|
+
import { createServer, type UserConfig } from 'vite'
|
|
5
5
|
import type { ResolvedCerConfig } from './dev-server.js'
|
|
6
6
|
import { buildSSR } from './build-ssr.js'
|
|
7
7
|
import { buildRouteEntry } from './path-utils.js'
|
|
@@ -138,7 +138,7 @@ async function renderPath(
|
|
|
138
138
|
setHeader: () => {},
|
|
139
139
|
end: (body: string) => resolve(body),
|
|
140
140
|
}
|
|
141
|
-
;(handlerFn as
|
|
141
|
+
;(handlerFn as (req: unknown, res: unknown) => Promise<void>)(mockReq, mockRes).catch(reject)
|
|
142
142
|
})
|
|
143
143
|
}
|
|
144
144
|
|
package/src/plugin/build-ssr.ts
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { build, type UserConfig } from 'vite'
|
|
2
2
|
import { join, resolve } from 'pathe'
|
|
3
|
-
import { existsSync } from 'node:fs'
|
|
3
|
+
import { existsSync, renameSync } from 'node:fs'
|
|
4
4
|
import type { ResolvedCerConfig } from './dev-server.js'
|
|
5
|
+
import { getGeneratedDir, writeGeneratedDir } from './generated-dir.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Resolves the client build entry point for an SSR/SSG build.
|
|
8
9
|
*
|
|
9
10
|
* Priority order:
|
|
10
|
-
* 1. `index.html` at the project root —
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* themselves (e.g. a custom Express server).
|
|
15
|
-
* 3. `app/app.ts` — last resort (same bundle, no DSD hydration preamble).
|
|
11
|
+
* 1. `index.html` at the project root — consumer-provided HTML shell.
|
|
12
|
+
* 2. `.cer/index.html` — auto-generated HTML shell (Nuxt-style magic).
|
|
13
|
+
* 3. `app/entry-client.ts` — fallback for projects that manage HTML externally.
|
|
14
|
+
* 4. `app/app.ts` — last resort (same bundle, no DSD hydration preamble).
|
|
16
15
|
*/
|
|
17
|
-
function resolveClientEntry(config: ResolvedCerConfig): string {
|
|
16
|
+
export function resolveClientEntry(config: ResolvedCerConfig): string {
|
|
18
17
|
const indexHtml = resolve(config.root, 'index.html')
|
|
19
18
|
if (existsSync(indexHtml)) return indexHtml
|
|
19
|
+
const cerIndexHtml = join(getGeneratedDir(config.root), 'index.html')
|
|
20
|
+
if (existsSync(cerIndexHtml)) return cerIndexHtml
|
|
20
21
|
const entryClient = resolve(config.srcDir, 'entry-client.ts')
|
|
21
22
|
if (existsSync(entryClient)) return entryClient
|
|
22
23
|
return resolve(config.srcDir, 'app.ts')
|
|
@@ -225,6 +226,10 @@ export async function buildSSR(
|
|
|
225
226
|
const clientOutDir = join(config.root, 'dist/client')
|
|
226
227
|
const serverOutDir = join(config.root, 'dist/server')
|
|
227
228
|
|
|
229
|
+
// Write .cer/ generated files BEFORE resolving the client entry so that
|
|
230
|
+
// .cer/index.html is on disk when resolveClientEntry checks for it.
|
|
231
|
+
writeGeneratedDir(config)
|
|
232
|
+
|
|
228
233
|
// Resolve the client entry — index.html is preferred so Vite writes a
|
|
229
234
|
// processed index.html to dist/client/ for use as the SSG shell template.
|
|
230
235
|
const clientEntry = resolveClientEntry(config)
|
|
@@ -244,6 +249,15 @@ export async function buildSSR(
|
|
|
244
249
|
},
|
|
245
250
|
})
|
|
246
251
|
|
|
252
|
+
// If the client entry was .cer/index.html, Vite outputs it as
|
|
253
|
+
// dist/client/.cer/index.html (preserving relative path). The SSR server
|
|
254
|
+
// template expects dist/client/index.html, so rename it into place.
|
|
255
|
+
const generatedHtmlOut = join(clientOutDir, '.cer/index.html')
|
|
256
|
+
const rootHtmlOut = join(clientOutDir, 'index.html')
|
|
257
|
+
if (existsSync(generatedHtmlOut) && !existsSync(rootHtmlOut)) {
|
|
258
|
+
renameSync(generatedHtmlOut, rootHtmlOut)
|
|
259
|
+
}
|
|
260
|
+
|
|
247
261
|
// Generate server entry source inline via a virtual plugin
|
|
248
262
|
const serverEntryCode = generateServerEntryCode()
|
|
249
263
|
const VIRTUAL_SERVER_ENTRY = 'virtual:cer-server-entry'
|
package/src/plugin/dev-server.ts
CHANGED
|
@@ -186,11 +186,12 @@ export function configureCerDevServer(
|
|
|
186
186
|
|
|
187
187
|
// Try to find a handler for the HTTP method. Exports may be GET/POST (uppercase)
|
|
188
188
|
// or get/post (lowercase); try both plus a 'default' fallback.
|
|
189
|
+
type RouteHandlerFn = (req: typeof augmentedReq, res: typeof augmentedRes) => void | Promise<void>
|
|
189
190
|
const handlerKey = method.toLowerCase()
|
|
190
191
|
const handler =
|
|
191
|
-
(route.handlers[handlerKey] as
|
|
192
|
-
(route.handlers[method.toUpperCase()] as
|
|
193
|
-
(route.handlers['default'] as
|
|
192
|
+
(route.handlers[handlerKey] as RouteHandlerFn | undefined) ??
|
|
193
|
+
(route.handlers[method.toUpperCase()] as RouteHandlerFn | undefined) ??
|
|
194
|
+
(route.handlers['default'] as RouteHandlerFn | undefined)
|
|
194
195
|
|
|
195
196
|
if (typeof handler === 'function') {
|
|
196
197
|
try {
|
|
@@ -1,27 +1,46 @@
|
|
|
1
|
-
import { writeFileSync } from 'node:fs'
|
|
2
|
-
import { readFileSync
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync } from 'node:fs'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
3
|
import { join, relative } from 'pathe'
|
|
4
4
|
import { scanDirectory } from './scanner.js'
|
|
5
|
+
import { GENERATED_DIR_NAME } from './generated-dir.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* Writes
|
|
8
|
-
*
|
|
9
|
-
* { "extends": "
|
|
8
|
+
* Writes `.cer/tsconfig.json` containing path aliases for the `~/` prefix
|
|
9
|
+
* plus include/exclude entries so the consumer's `tsconfig.json` only needs:
|
|
10
|
+
* { "extends": "./.cer/tsconfig.json" }
|
|
10
11
|
*/
|
|
11
12
|
export function writeTsconfigPaths(root: string, srcDir: string): void {
|
|
12
|
-
const
|
|
13
|
+
const cerDir = join(root, GENERATED_DIR_NAME)
|
|
14
|
+
if (!existsSync(cerDir)) {
|
|
15
|
+
mkdirSync(cerDir, { recursive: true })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Paths are relative to .cer/ inside the project root, so prefix with ../
|
|
19
|
+
const srcRel = '../' + relative(root, srcDir).replace(/\\/g, '/')
|
|
13
20
|
const paths: Record<string, string[]> = {
|
|
14
|
-
'~/*': [`${
|
|
15
|
-
'~/pages/*': [`${
|
|
16
|
-
'~/layouts/*': [`${
|
|
17
|
-
'~/components/*': [`${
|
|
18
|
-
'~/composables/*': [`${
|
|
19
|
-
'~/plugins/*': [`${
|
|
20
|
-
'~/middleware/*': [`${
|
|
21
|
-
'~/assets/*': [`${
|
|
21
|
+
'~/*': [`${srcRel}/*`],
|
|
22
|
+
'~/pages/*': [`${srcRel}/pages/*`],
|
|
23
|
+
'~/layouts/*': [`${srcRel}/layouts/*`],
|
|
24
|
+
'~/components/*': [`${srcRel}/components/*`],
|
|
25
|
+
'~/composables/*': [`${srcRel}/composables/*`],
|
|
26
|
+
'~/plugins/*': [`${srcRel}/plugins/*`],
|
|
27
|
+
'~/middleware/*': [`${srcRel}/middleware/*`],
|
|
28
|
+
'~/assets/*': [`${srcRel}/assets/*`],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const tsconfig = {
|
|
32
|
+
compilerOptions: { paths },
|
|
33
|
+
include: [
|
|
34
|
+
'../app/**/*.ts',
|
|
35
|
+
'../server/**/*.ts',
|
|
36
|
+
'./**/*.ts',
|
|
37
|
+
'./**/*.d.ts',
|
|
38
|
+
],
|
|
39
|
+
exclude: ['../node_modules', '../dist'],
|
|
22
40
|
}
|
|
23
|
-
|
|
24
|
-
|
|
41
|
+
|
|
42
|
+
const content = JSON.stringify(tsconfig, null, 2) + '\n'
|
|
43
|
+
writeFileSync(join(cerDir, 'tsconfig.json'), content, 'utf-8')
|
|
25
44
|
}
|
|
26
45
|
|
|
27
46
|
const RUNTIME_GLOBALS = [
|
|
@@ -199,16 +218,21 @@ export async function generateVirtualModuleDts(
|
|
|
199
218
|
}
|
|
200
219
|
|
|
201
220
|
/**
|
|
202
|
-
* Writes
|
|
221
|
+
* Writes `auto-imports.d.ts` and `env.d.ts` to `.cer/` inside the project root.
|
|
203
222
|
*/
|
|
204
223
|
export async function writeAutoImportDts(
|
|
205
224
|
root: string,
|
|
206
225
|
composablesDir: string,
|
|
207
226
|
composableExports?: Map<string, string>,
|
|
208
227
|
): Promise<void> {
|
|
228
|
+
const cerDir = join(root, GENERATED_DIR_NAME)
|
|
229
|
+
if (!existsSync(cerDir)) {
|
|
230
|
+
mkdirSync(cerDir, { recursive: true })
|
|
231
|
+
}
|
|
232
|
+
|
|
209
233
|
const scanned = composableExports ?? await scanComposableExports(composablesDir)
|
|
210
234
|
const autoImportsContent = await generateAutoImportDts(root, composablesDir, scanned)
|
|
211
235
|
const envContent = await generateVirtualModuleDts(root, composablesDir, scanned)
|
|
212
|
-
writeFileSync(join(
|
|
213
|
-
writeFileSync(join(
|
|
236
|
+
writeFileSync(join(cerDir, 'auto-imports.d.ts'), autoImportsContent, 'utf-8')
|
|
237
|
+
writeFileSync(join(cerDir, 'env.d.ts'), envContent, 'utf-8')
|
|
214
238
|
}
|