@jasonshimmy/vite-plugin-cer-app 0.1.6 → 0.3.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.
Files changed (95) hide show
  1. package/.github/workflows/publish.yml +56 -5
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +2 -0
  4. package/commits.txt +1 -1
  5. package/dist/cli/commands/build.d.ts.map +1 -1
  6. package/dist/cli/commands/build.js +19 -5
  7. package/dist/cli/commands/build.js.map +1 -1
  8. package/dist/cli/commands/dev.js +1 -1
  9. package/dist/cli/commands/dev.js.map +1 -1
  10. package/dist/cli/commands/preview.d.ts.map +1 -1
  11. package/dist/cli/commands/preview.js +0 -1
  12. package/dist/cli/commands/preview.js.map +1 -1
  13. package/dist/cli/create/index.js +7 -3
  14. package/dist/cli/create/index.js.map +1 -1
  15. package/dist/cli/create/templates/spa/.gitignore.tpl +25 -0
  16. package/dist/cli/create/templates/spa/index.html.tpl +1 -1
  17. package/dist/cli/create/templates/ssg/.gitignore.tpl +25 -0
  18. package/dist/cli/create/templates/ssg/index.html.tpl +1 -1
  19. package/dist/cli/create/templates/ssr/.gitignore.tpl +25 -0
  20. package/dist/cli/create/templates/ssr/cer.config.ts.tpl +0 -1
  21. package/dist/cli/create/templates/ssr/index.html.tpl +1 -1
  22. package/dist/plugin/build-ssg.d.ts.map +1 -1
  23. package/dist/plugin/build-ssg.js.map +1 -1
  24. package/dist/plugin/build-ssr.d.ts +10 -0
  25. package/dist/plugin/build-ssr.d.ts.map +1 -1
  26. package/dist/plugin/build-ssr.js +21 -8
  27. package/dist/plugin/build-ssr.js.map +1 -1
  28. package/dist/plugin/dev-server.d.ts +0 -1
  29. package/dist/plugin/dev-server.d.ts.map +1 -1
  30. package/dist/plugin/dev-server.js +0 -2
  31. package/dist/plugin/dev-server.js.map +1 -1
  32. package/dist/plugin/dts-generator.d.ts +4 -4
  33. package/dist/plugin/dts-generator.d.ts.map +1 -1
  34. package/dist/plugin/dts-generator.js +39 -19
  35. package/dist/plugin/dts-generator.js.map +1 -1
  36. package/dist/plugin/generated-dir.d.ts +28 -0
  37. package/dist/plugin/generated-dir.d.ts.map +1 -0
  38. package/dist/plugin/generated-dir.js +106 -0
  39. package/dist/plugin/generated-dir.js.map +1 -0
  40. package/dist/plugin/index.d.ts.map +1 -1
  41. package/dist/plugin/index.js +27 -1
  42. package/dist/plugin/index.js.map +1 -1
  43. package/dist/plugin/path-utils.js.map +1 -1
  44. package/dist/plugin/virtual/loading.d.ts.map +1 -1
  45. package/dist/plugin/virtual/loading.js.map +1 -1
  46. package/dist/runtime/app-template.d.ts +9 -0
  47. package/dist/runtime/app-template.d.ts.map +1 -0
  48. package/dist/runtime/app-template.js +159 -0
  49. package/dist/runtime/app-template.js.map +1 -0
  50. package/dist/types/config.d.ts +0 -1
  51. package/dist/types/config.d.ts.map +1 -1
  52. package/dist/types/config.js.map +1 -1
  53. package/docs/cli.md +1 -1
  54. package/docs/configuration.md +2 -11
  55. package/docs/getting-started.md +2 -100
  56. package/docs/rendering-modes.md +4 -5
  57. package/docs/routing.md +1 -1
  58. package/e2e/kitchen-sink/tsconfig.json +3 -0
  59. package/eslint.config.ts +22 -0
  60. package/package.json +6 -1
  61. package/src/__tests__/plugin/build-ssr.test.ts +24 -10
  62. package/src/__tests__/plugin/cer-app-plugin.test.ts +35 -0
  63. package/src/__tests__/plugin/dev-server.test.ts +1 -1
  64. package/src/__tests__/plugin/dts-generator.test.ts +15 -6
  65. package/src/__tests__/plugin/generated-dir.test.ts +137 -0
  66. package/src/__tests__/plugin/resolve-config.test.ts +0 -5
  67. package/src/__tests__/types/config.test.ts +1 -1
  68. package/src/cli/commands/build.ts +19 -5
  69. package/src/cli/commands/dev.ts +2 -2
  70. package/src/cli/commands/preview.ts +7 -5
  71. package/src/cli/create/index.ts +12 -8
  72. package/src/cli/create/templates/spa/.gitignore.tpl +25 -0
  73. package/src/cli/create/templates/spa/index.html.tpl +1 -1
  74. package/src/cli/create/templates/ssg/.gitignore.tpl +25 -0
  75. package/src/cli/create/templates/ssg/index.html.tpl +1 -1
  76. package/src/cli/create/templates/ssr/.gitignore.tpl +25 -0
  77. package/src/cli/create/templates/ssr/cer.config.ts.tpl +0 -1
  78. package/src/cli/create/templates/ssr/index.html.tpl +1 -1
  79. package/src/plugin/build-ssg.ts +2 -2
  80. package/src/plugin/build-ssr.ts +22 -8
  81. package/src/plugin/dev-server.ts +5 -4
  82. package/src/plugin/dts-generator.ts +43 -19
  83. package/src/plugin/generated-dir.ts +115 -0
  84. package/src/plugin/index.ts +32 -2
  85. package/src/plugin/path-utils.ts +1 -1
  86. package/src/plugin/virtual/loading.ts +0 -1
  87. package/{e2e/kitchen-sink/app/app.ts → src/runtime/app-template.ts} +24 -7
  88. package/src/types/config.ts +0 -1
  89. package/dist/cli/create/templates/spa/app/app.ts.tpl +0 -93
  90. package/dist/cli/create/templates/ssg/app/app.ts.tpl +0 -97
  91. package/dist/cli/create/templates/ssr/app/app.ts.tpl +0 -97
  92. package/e2e/kitchen-sink/index.html +0 -12
  93. package/src/cli/create/templates/spa/app/app.ts.tpl +0 -93
  94. package/src/cli/create/templates/ssg/app/app.ts.tpl +0 -97
  95. package/src/cli/create/templates/ssr/app/app.ts.tpl +0 -97
@@ -0,0 +1,137 @@
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
+ resolveHtmlEntry,
21
+ generateDefaultHtml,
22
+ writeGeneratedDir,
23
+ } from '../../plugin/generated-dir.js'
24
+
25
+ const ROOT = '/project'
26
+ const mockConfig = {
27
+ root: ROOT,
28
+ srcDir: `${ROOT}/app`,
29
+ } as Parameters<typeof writeGeneratedDir>[0]
30
+
31
+ beforeEach(() => {
32
+ vi.mocked(existsSync).mockReturnValue(false)
33
+ vi.mocked(writeFileSync).mockClear()
34
+ vi.mocked(mkdirSync).mockClear()
35
+ vi.mocked(readFileSync).mockReturnValue('')
36
+ vi.mocked(appendFileSync).mockClear()
37
+ })
38
+
39
+ describe('GENERATED_DIR_NAME', () => {
40
+ it('is .cer', () => {
41
+ expect(GENERATED_DIR_NAME).toBe('.cer')
42
+ })
43
+ })
44
+
45
+ describe('getGeneratedDir', () => {
46
+ it('returns <root>/.cer', () => {
47
+ expect(getGeneratedDir(ROOT)).toBe(`${ROOT}/.cer`)
48
+ })
49
+ })
50
+
51
+ describe('resolveHtmlEntry', () => {
52
+ it('returns user index.html when it exists', () => {
53
+ vi.mocked(existsSync).mockReturnValue(true)
54
+ expect(resolveHtmlEntry(mockConfig)).toBe(`${ROOT}/index.html`)
55
+ })
56
+
57
+ it('returns .cer/index.html when user index.html is absent', () => {
58
+ vi.mocked(existsSync).mockReturnValue(false)
59
+ expect(resolveHtmlEntry(mockConfig)).toBe(`${ROOT}/.cer/index.html`)
60
+ })
61
+ })
62
+
63
+ describe('generateDefaultHtml', () => {
64
+ it('always references /.cer/app.ts', () => {
65
+ const html = generateDefaultHtml()
66
+ expect(html).toContain('/.cer/app.ts')
67
+ expect(html).not.toContain('/app/app.ts')
68
+ })
69
+
70
+ it('includes <cer-layout-view> mount point', () => {
71
+ const html = generateDefaultHtml()
72
+ expect(html).toContain('<cer-layout-view>')
73
+ })
74
+
75
+ it('is valid HTML with doctype', () => {
76
+ const html = generateDefaultHtml()
77
+ expect(html).toContain('<!DOCTYPE html>')
78
+ })
79
+ })
80
+
81
+ describe('writeGeneratedDir', () => {
82
+ it('creates the .cer directory when absent', () => {
83
+ // existsSync returns false for everything → dir is created
84
+ writeGeneratedDir(mockConfig)
85
+ expect(mkdirSync).toHaveBeenCalledWith(`${ROOT}/.cer`, { recursive: true })
86
+ })
87
+
88
+ it('does not re-create the directory when it already exists', () => {
89
+ // existsSync returns true → dir already present
90
+ vi.mocked(existsSync).mockReturnValue(true)
91
+ writeGeneratedDir(mockConfig)
92
+ expect(mkdirSync).not.toHaveBeenCalled()
93
+ })
94
+
95
+ it('always writes .cer/app.ts', () => {
96
+ writeGeneratedDir(mockConfig)
97
+ const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
98
+ expect(paths.some(p => p.endsWith('/.cer/app.ts'))).toBe(true)
99
+ })
100
+
101
+ it('always writes .cer/app.ts even when .cer/ dir already exists', () => {
102
+ vi.mocked(existsSync).mockReturnValue(true)
103
+ writeGeneratedDir(mockConfig)
104
+ const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
105
+ expect(paths.some(p => p.endsWith('/.cer/app.ts'))).toBe(true)
106
+ })
107
+
108
+ it('always writes .cer/index.html', () => {
109
+ writeGeneratedDir(mockConfig)
110
+ const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
111
+ expect(paths.some(p => p.endsWith('/.cer/index.html'))).toBe(true)
112
+ })
113
+
114
+ it('creates .gitignore when absent', () => {
115
+ // existsSync returns false → .cer/ dir created, app.ts written, .gitignore created
116
+ writeGeneratedDir(mockConfig)
117
+ const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
118
+ expect(paths.some(p => p.endsWith('/.gitignore'))).toBe(true)
119
+ })
120
+
121
+ it('appends .cer/ to existing .gitignore that does not contain it', () => {
122
+ // .gitignore exists (readFileSync returns '' — no .cer/ entry)
123
+ vi.mocked(existsSync).mockImplementation((p) => String(p).endsWith('.gitignore'))
124
+ vi.mocked(readFileSync).mockReturnValue('node_modules/\ndist/\n')
125
+ writeGeneratedDir(mockConfig)
126
+ expect(appendFileSync).toHaveBeenCalled()
127
+ const appendArg = vi.mocked(appendFileSync).mock.calls[0][1] as string
128
+ expect(appendArg).toContain('.cer/')
129
+ })
130
+
131
+ it('does not append to .gitignore when .cer/ is already present', () => {
132
+ vi.mocked(existsSync).mockImplementation((p) => String(p).endsWith('.gitignore'))
133
+ vi.mocked(readFileSync).mockReturnValue('node_modules/\n.cer/\ndist/\n')
134
+ writeGeneratedDir(mockConfig)
135
+ expect(appendFileSync).not.toHaveBeenCalled()
136
+ })
137
+ })
@@ -84,11 +84,6 @@ describe('resolveConfig', () => {
84
84
  expect(cfg.ssr.dsd).toBe(false)
85
85
  })
86
86
 
87
- it('defaults ssr.streaming to false', () => {
88
- const cfg = resolveConfig({}, ROOT)
89
- expect(cfg.ssr.streaming).toBe(false)
90
- })
91
-
92
87
  it('defaults ssg.routes to "auto"', () => {
93
88
  const cfg = resolveConfig({}, ROOT)
94
89
  expect(cfg.ssg.routes).toBe('auto')
@@ -12,7 +12,7 @@ describe('defineConfig', () => {
12
12
  })
13
13
 
14
14
  it('preserves nested ssr config', () => {
15
- const config = { ssr: { dsd: false, streaming: true } }
15
+ const config = { ssr: { dsd: false } }
16
16
  expect(defineConfig(config)).toEqual(config)
17
17
  })
18
18
 
@@ -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
- // Standard Vite build single-page app
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: resolve(root, 'dist'),
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
 
@@ -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, resolveConfig } from '../../plugin/index.js'
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
- const result = await build({
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?: Function
112
- default?: Function
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 Function | undefined) ??
156
- (route.handlers[method.toUpperCase()] as Function | undefined) ??
157
- (route.handlers['default'] as Function | undefined)
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 {
@@ -179,6 +179,10 @@ async function generateInlineTemplate(
179
179
  ): Promise<void> {
180
180
  await mkdir(join(targetDir, 'app/pages'), { recursive: true })
181
181
  await mkdir(join(targetDir, 'app/layouts'), { recursive: true })
182
+ await mkdir(join(targetDir, 'app/components'), { recursive: true })
183
+ await mkdir(join(targetDir, 'app/composables'), { recursive: true })
184
+ await mkdir(join(targetDir, 'app/plugins'), { recursive: true })
185
+ await mkdir(join(targetDir, 'app/middleware'), { recursive: true })
182
186
 
183
187
  // package.json
184
188
  await writeFile(
@@ -215,13 +219,6 @@ async function generateInlineTemplate(
215
219
  'utf-8',
216
220
  )
217
221
 
218
- // app/app.ts
219
- await writeFile(
220
- join(targetDir, 'app/app.ts'),
221
- `import '@jasonshimmy/custom-elements-runtime/css'\nimport 'virtual:cer-jit-css'\nimport 'virtual:cer-components'\nimport routes from 'virtual:cer-routes'\nimport layouts from 'virtual:cer-layouts'\nimport plugins from 'virtual:cer-plugins'\nimport { hasLoading, loadingTag } from 'virtual:cer-loading'\nimport { hasError, errorTag } from 'virtual:cer-error'\nimport { component, ref, provide, useOnConnected, useOnDisconnected, registerBuiltinComponents } from '@jasonshimmy/custom-elements-runtime'\nimport { initRouter } from '@jasonshimmy/custom-elements-runtime/router'\nimport { enableJITCSS } from '@jasonshimmy/custom-elements-runtime/jit-css'\n\nregisterBuiltinComponents()\nenableJITCSS()\n\nconst router = initRouter({ routes })\n\nconst isNavigating = ref(false)\nconst currentError = ref(null)\n;(globalThis as any).resetError = () => { currentError.value = null; router.replace(router.getCurrent().path) }\nconst _push = router.push.bind(router)\nconst _replace = router.replace.bind(router)\nrouter.push = async (path) => { isNavigating.value = true; currentError.value = null; try { await _push(path) } catch (err) { currentError.value = err instanceof Error ? err.message : String(err) } finally { isNavigating.value = false } }\nrouter.replace = async (path) => { isNavigating.value = true; currentError.value = null; try { await _replace(path) } catch (err) { currentError.value = err instanceof Error ? err.message : String(err) } finally { isNavigating.value = false } }\n\nconst _pluginProvides = new Map()\n;(globalThis as any).__cerPluginProvides = _pluginProvides\n\ncomponent('cer-layout-view', () => {\n for (const [key, value] of _pluginProvides) { provide(key, value) }\n const current = ref(router.getCurrent())\n let unsub: (() => void) | undefined\n useOnConnected(() => { unsub = router.subscribe((s: typeof current.value) => { current.value = s }) })\n useOnDisconnected(() => { unsub?.(); unsub = undefined })\n if (currentError.value !== null) {\n if (hasError && errorTag) return { tag: errorTag, props: { attrs: { error: String(currentError.value) } }, children: [] }\n return { tag: 'div', props: { attrs: { style: 'padding:2rem;font-family:monospace' } }, children: [String(currentError.value)] }\n }\n if (isNavigating.value && hasLoading && loadingTag) return { tag: loadingTag, props: {}, children: [] }\n const matched = router.matchRoute(current.value.path)\n const layoutName = (matched?.route as any)?.meta?.layout ?? 'default'\n const layoutTag = (layouts as Record<string, string>)[layoutName]\n const routerView = { tag: 'router-view', props: {}, children: [] }\n return layoutTag ? { tag: layoutTag, props: {}, children: [routerView] } : routerView\n})\n\nfor (const plugin of plugins ?? []) {\n if (plugin && typeof plugin.setup === 'function') {\n await plugin.setup({ router, provide: (key: string, value: unknown) => { _pluginProvides.set(key, value) }, config: {} })\n }\n}\n\nif (typeof window !== 'undefined') {\n const _initMatch = router.matchRoute(window.location.pathname)\n if (_initMatch?.route?.load) {\n try { await _initMatch.route.load() } catch { /* non-fatal */ }\n }\n}\n\nif (typeof window !== 'undefined') {\n await _replace(window.location.pathname + window.location.search + window.location.hash)\n delete (globalThis as any).__CER_DATA__\n}\n\nexport { router }\n`,
222
- 'utf-8',
223
- )
224
-
225
222
  // app/pages/index.ts
226
223
  await writeFile(
227
224
  join(targetDir, 'app/pages/index.ts'),
@@ -236,10 +233,17 @@ async function generateInlineTemplate(
236
233
  'utf-8',
237
234
  )
238
235
 
236
+ // .gitignore
237
+ await writeFile(
238
+ join(targetDir, '.gitignore'),
239
+ `# Dependencies\nnode_modules/\n\n# Build output\ndist/\n\n# CER App generated directory\n.cer/\n\n# Environment variables\n.env.local\n.env.*.local\n\n# Editor\n.vscode/\n.idea/\n*.suo\n*.sw?\n\n# OS\n.DS_Store\nThumbs.db\n\n# Logs\n*.log\n`,
240
+ 'utf-8',
241
+ )
242
+
239
243
  // index.html
240
244
  await writeFile(
241
245
  join(targetDir, 'index.html'),
242
- `<!DOCTYPE html>\n<html lang="en">\n <head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>${projectName}</title>\n </head>\n <body>\n <cer-layout-view></cer-layout-view>\n <script type="module" src="/app/app.ts"></script>\n </body>\n</html>\n`,
246
+ `<!DOCTYPE html>\n<html lang="en">\n <head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>${projectName}</title>\n </head>\n <body>\n <cer-layout-view></cer-layout-view>\n <script type="module" src="/.cer/app.ts"></script>\n </body>\n</html>\n`,
243
247
  'utf-8',
244
248
  )
245
249
  }
@@ -0,0 +1,25 @@
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build output
5
+ dist/
6
+
7
+ # CER App generated directory
8
+ .cer/
9
+
10
+ # Environment variables
11
+ .env.local
12
+ .env.*.local
13
+
14
+ # Editor
15
+ .vscode/
16
+ .idea/
17
+ *.suo
18
+ *.sw?
19
+
20
+ # OS
21
+ .DS_Store
22
+ Thumbs.db
23
+
24
+ # Logs
25
+ *.log
@@ -7,6 +7,6 @@
7
7
  </head>
8
8
  <body>
9
9
  <cer-layout-view></cer-layout-view>
10
- <script type="module" src="/app/app.ts"></script>
10
+ <script type="module" src="/.cer/app.ts"></script>
11
11
  </body>
12
12
  </html>
@@ -0,0 +1,25 @@
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build output
5
+ dist/
6
+
7
+ # CER App generated directory
8
+ .cer/
9
+
10
+ # Environment variables
11
+ .env.local
12
+ .env.*.local
13
+
14
+ # Editor
15
+ .vscode/
16
+ .idea/
17
+ *.suo
18
+ *.sw?
19
+
20
+ # OS
21
+ .DS_Store
22
+ Thumbs.db
23
+
24
+ # Logs
25
+ *.log
@@ -7,6 +7,6 @@
7
7
  </head>
8
8
  <body>
9
9
  <cer-layout-view></cer-layout-view>
10
- <script type="module" src="/app/app.ts"></script>
10
+ <script type="module" src="/.cer/app.ts"></script>
11
11
  </body>
12
12
  </html>
@@ -0,0 +1,25 @@
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build output
5
+ dist/
6
+
7
+ # CER App generated directory
8
+ .cer/
9
+
10
+ # Environment variables
11
+ .env.local
12
+ .env.*.local
13
+
14
+ # Editor
15
+ .vscode/
16
+ .idea/
17
+ *.suo
18
+ *.sw?
19
+
20
+ # OS
21
+ .DS_Store
22
+ Thumbs.db
23
+
24
+ # Logs
25
+ *.log
@@ -4,7 +4,6 @@ export default defineConfig({
4
4
  mode: 'ssr',
5
5
  ssr: {
6
6
  dsd: true,
7
- streaming: true,
8
7
  },
9
8
  autoImports: { components: true, composables: true, directives: true, runtime: true },
10
9
  })
@@ -7,6 +7,6 @@
7
7
  </head>
8
8
  <body>
9
9
  <cer-layout-view></cer-layout-view>
10
- <script type="module" src="/app/app.ts"></script>
10
+ <script type="module" src="/.cer/app.ts"></script>
11
11
  </body>
12
12
  </html>
@@ -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, build, type UserConfig } from 'vite'
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 Function)(mockReq, mockRes).catch(reject)
141
+ ;(handlerFn as (req: unknown, res: unknown) => Promise<void>)(mockReq, mockRes).catch(reject)
142
142
  })
143
143
  }
144
144
 
@@ -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 — preferred because Vite writes a
11
- * processed `index.html` to `dist/client/` with correct asset references,
12
- * which `renderPath` then uses as the shell template for SSG pages.
13
- * 2. `app/entry-client.ts` — fallback for projects that handle HTML injection
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'
@@ -15,7 +15,7 @@ export interface ResolvedCerConfig {
15
15
  serverApiDir: string
16
16
  serverMiddlewareDir: string
17
17
  port: number
18
- ssr: { dsd: boolean; streaming: boolean }
18
+ ssr: { dsd: boolean }
19
19
  ssg: { routes: 'auto' | string[]; concurrency: number; fallback: boolean }
20
20
  router: { base?: string; scrollToFragment?: boolean | object }
21
21
  jitCss: { content: string[]; extendedColors: boolean }
@@ -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 Function | undefined) ??
192
- (route.handlers[method.toUpperCase()] as Function | undefined) ??
193
- (route.handlers['default'] as Function | undefined)
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, existsSync } from 'node:fs'
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 `cer-tsconfig.json` to the project root containing path aliases for
8
- * the `~/` prefix. Users extend it from their `tsconfig.json`:
9
- * { "extends": "./cer-tsconfig.json" }
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 rel = './' + relative(root, srcDir).replace(/\\/g, '/')
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
- '~/*': [`${rel}/*`],
15
- '~/pages/*': [`${rel}/pages/*`],
16
- '~/layouts/*': [`${rel}/layouts/*`],
17
- '~/components/*': [`${rel}/components/*`],
18
- '~/composables/*': [`${rel}/composables/*`],
19
- '~/plugins/*': [`${rel}/plugins/*`],
20
- '~/middleware/*': [`${rel}/middleware/*`],
21
- '~/assets/*': [`${rel}/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
- const content = JSON.stringify({ compilerOptions: { paths } }, null, 2) + '\n'
24
- writeFileSync(join(root, 'cer-tsconfig.json'), content, 'utf-8')
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 both `cer-auto-imports.d.ts` and `cer-env.d.ts` to the project root.
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(root, 'cer-auto-imports.d.ts'), autoImportsContent, 'utf-8')
213
- writeFileSync(join(root, 'cer-env.d.ts'), envContent, 'utf-8')
236
+ writeFileSync(join(cerDir, 'auto-imports.d.ts'), autoImportsContent, 'utf-8')
237
+ writeFileSync(join(cerDir, 'env.d.ts'), envContent, 'utf-8')
214
238
  }