@jasonshimmy/vite-plugin-cer-app 0.2.0 → 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 (56) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +2 -0
  3. package/commits.txt +1 -1
  4. package/dist/cli/create/index.js +7 -3
  5. package/dist/cli/create/index.js.map +1 -1
  6. package/dist/cli/create/templates/spa/.gitignore.tpl +25 -0
  7. package/dist/cli/create/templates/spa/index.html.tpl +1 -1
  8. package/dist/cli/create/templates/ssg/.gitignore.tpl +25 -0
  9. package/dist/cli/create/templates/ssg/index.html.tpl +1 -1
  10. package/dist/cli/create/templates/ssr/.gitignore.tpl +25 -0
  11. package/dist/cli/create/templates/ssr/cer.config.ts.tpl +0 -1
  12. package/dist/cli/create/templates/ssr/index.html.tpl +1 -1
  13. package/dist/plugin/dev-server.d.ts +0 -1
  14. package/dist/plugin/dev-server.d.ts.map +1 -1
  15. package/dist/plugin/generated-dir.d.ts +5 -11
  16. package/dist/plugin/generated-dir.d.ts.map +1 -1
  17. package/dist/plugin/generated-dir.js +43 -31
  18. package/dist/plugin/generated-dir.js.map +1 -1
  19. package/dist/plugin/index.d.ts.map +1 -1
  20. package/dist/plugin/index.js +0 -1
  21. package/dist/plugin/index.js.map +1 -1
  22. package/dist/runtime/app-template.d.ts +5 -4
  23. package/dist/runtime/app-template.d.ts.map +1 -1
  24. package/dist/runtime/app-template.js +6 -5
  25. package/dist/runtime/app-template.js.map +1 -1
  26. package/dist/types/config.d.ts +0 -1
  27. package/dist/types/config.d.ts.map +1 -1
  28. package/dist/types/config.js.map +1 -1
  29. package/docs/cli.md +1 -1
  30. package/docs/configuration.md +2 -11
  31. package/docs/getting-started.md +2 -100
  32. package/docs/rendering-modes.md +2 -3
  33. package/package.json +1 -1
  34. package/src/__tests__/plugin/dev-server.test.ts +1 -1
  35. package/src/__tests__/plugin/generated-dir.test.ts +8 -39
  36. package/src/__tests__/plugin/resolve-config.test.ts +0 -5
  37. package/src/__tests__/types/config.test.ts +1 -1
  38. package/src/cli/create/index.ts +12 -8
  39. package/src/cli/create/templates/spa/.gitignore.tpl +25 -0
  40. package/src/cli/create/templates/spa/index.html.tpl +1 -1
  41. package/src/cli/create/templates/ssg/.gitignore.tpl +25 -0
  42. package/src/cli/create/templates/ssg/index.html.tpl +1 -1
  43. package/src/cli/create/templates/ssr/.gitignore.tpl +25 -0
  44. package/src/cli/create/templates/ssr/cer.config.ts.tpl +0 -1
  45. package/src/cli/create/templates/ssr/index.html.tpl +1 -1
  46. package/src/plugin/dev-server.ts +1 -1
  47. package/src/plugin/generated-dir.ts +44 -31
  48. package/src/plugin/index.ts +0 -1
  49. package/src/runtime/app-template.ts +6 -5
  50. package/src/types/config.ts +0 -1
  51. package/dist/cli/create/templates/spa/app/app.ts.tpl +0 -93
  52. package/dist/cli/create/templates/ssg/app/app.ts.tpl +0 -97
  53. package/dist/cli/create/templates/ssr/app/app.ts.tpl +0 -97
  54. package/src/cli/create/templates/spa/app/app.ts.tpl +0 -93
  55. package/src/cli/create/templates/ssg/app/app.ts.tpl +0 -97
  56. package/src/cli/create/templates/ssr/app/app.ts.tpl +0 -97
@@ -13,7 +13,6 @@ export default defineConfig({
13
13
 
14
14
  ssr: {
15
15
  dsd: true,
16
- streaming: false,
17
16
  },
18
17
 
19
18
  ssg: {
@@ -87,8 +86,7 @@ Controls SSR rendering behavior.
87
86
 
88
87
  ```ts
89
88
  ssr: {
90
- dsd: true, // Emit Declarative Shadow DOM
91
- streaming: false // Reserved for future use
89
+ dsd: true, // Emit Declarative Shadow DOM
92
90
  }
93
91
  ```
94
92
 
@@ -99,13 +97,6 @@ ssr: {
99
97
 
100
98
  When `true`, renders components with [Declarative Shadow DOM](https://developer.chrome.com/docs/css-ui/declarative-shadow-dom) markup. This eliminates Flash of Unstyled Content (FOUC) because styles are embedded directly in the HTML.
101
99
 
102
- ### `ssr.streaming`
103
-
104
- **Type:** `boolean`
105
- **Default:** `false`
106
-
107
- Reserved for future use. Currently, the SSR renderer always collects the full HTML string before sending the response.
108
-
109
100
  ---
110
101
 
111
102
  ## `ssg` options
@@ -247,7 +238,7 @@ export default defineConfig({
247
238
  plugins: [
248
239
  cerApp({
249
240
  mode: 'ssr',
250
- ssr: { dsd: true, streaming: true },
241
+ ssr: { dsd: true },
251
242
  }),
252
243
  ],
253
244
  })
@@ -128,110 +128,12 @@ component('layout-default', () => {
128
128
  </head>
129
129
  <body>
130
130
  <cer-layout-view></cer-layout-view>
131
- <script type="module" src="/app/app.ts"></script>
131
+ <script type="module" src="/.cer/app.ts"></script>
132
132
  </body>
133
133
  </html>
134
134
  ```
135
135
 
136
- ### 7. Create `app/app.ts` (auto-generated if absent)
137
-
138
- The framework generates this file when you scaffold a new project. It bootstraps the router, registers all auto-discovered components, runs plugins, and mounts the app:
139
-
140
- ```ts
141
- // app/app.ts
142
- import '@jasonshimmy/custom-elements-runtime/css'
143
- import 'virtual:cer-jit-css'
144
- import 'virtual:cer-components'
145
- import routes from 'virtual:cer-routes'
146
- import layouts from 'virtual:cer-layouts'
147
- import plugins from 'virtual:cer-plugins'
148
- import { hasLoading, loadingTag } from 'virtual:cer-loading'
149
- import { hasError, errorTag } from 'virtual:cer-error'
150
- import {
151
- component, ref, provide,
152
- useOnConnected, useOnDisconnected,
153
- registerBuiltinComponents,
154
- } from '@jasonshimmy/custom-elements-runtime'
155
- import { initRouter } from '@jasonshimmy/custom-elements-runtime/router'
156
- import { enableJITCSS } from '@jasonshimmy/custom-elements-runtime/jit-css'
157
-
158
- registerBuiltinComponents()
159
- enableJITCSS()
160
-
161
- const router = initRouter({ routes })
162
-
163
- const isNavigating = ref(false)
164
- const currentError = ref(null)
165
- ;(globalThis as any).resetError = () => {
166
- currentError.value = null
167
- router.replace(router.getCurrent().path)
168
- }
169
-
170
- const _push = router.push.bind(router)
171
- const _replace = router.replace.bind(router)
172
- router.push = async (path) => {
173
- isNavigating.value = true; currentError.value = null
174
- try { await _push(path) } catch (err) { currentError.value = err instanceof Error ? err.message : String(err) } finally { isNavigating.value = false }
175
- }
176
- router.replace = async (path) => {
177
- isNavigating.value = true; currentError.value = null
178
- try { await _replace(path) } catch (err) { currentError.value = err instanceof Error ? err.message : String(err) } finally { isNavigating.value = false }
179
- }
180
-
181
- // _pluginProvides is populated by plugin setup and forwarded into the component
182
- // context tree via provide() inside cer-layout-view so inject() works in all modes.
183
- // Also exposed on globalThis for the SSG timing edge case — see docs/plugins.md.
184
- const _pluginProvides = new Map<string, unknown>()
185
- ;(globalThis as any).__cerPluginProvides = _pluginProvides
186
-
187
- component('cer-layout-view', () => {
188
- for (const [key, value] of _pluginProvides) { provide(key, value) }
189
-
190
- const current = ref(router.getCurrent())
191
- let unsub: (() => void) | undefined
192
- useOnConnected(() => { unsub = router.subscribe((s) => { current.value = s }) })
193
- useOnDisconnected(() => { unsub?.(); unsub = undefined })
194
-
195
- if (currentError.value !== null) {
196
- if (hasError && errorTag) return { tag: errorTag, props: { attrs: { error: String(currentError.value) } }, children: [] }
197
- return { tag: 'div', props: { attrs: { style: 'padding:2rem;font-family:monospace' } }, children: String(currentError.value) }
198
- }
199
- if (isNavigating.value && hasLoading && loadingTag) return { tag: loadingTag, props: {}, children: [] }
200
-
201
- const matched = router.matchRoute(current.value.path)
202
- const layoutName = (matched?.route as any)?.meta?.layout ?? 'default'
203
- const layoutTag = (layouts as Record<string, string>)[layoutName]
204
- const routerView = { tag: 'router-view', props: {}, children: [] }
205
- return layoutTag ? { tag: layoutTag, props: {}, children: [routerView] } : routerView
206
- })
207
-
208
- // Plugins run AFTER cer-layout-view is defined so provide() calls from plugins
209
- // are forwarded into the component tree on the very first render.
210
- for (const plugin of plugins ?? []) {
211
- if (plugin && typeof plugin.setup === 'function') {
212
- await plugin.setup({ router, provide: (key, value) => { _pluginProvides.set(key, value) }, config: {} })
213
- }
214
- }
215
-
216
- // Pre-load the current page's route chunk AFTER plugins run.
217
- // This ensures cer-layout-view's first render (and its provide() calls) completes
218
- // before page component modules are imported and their renders are scheduled.
219
- if (typeof window !== 'undefined') {
220
- const _initMatch = router.matchRoute(window.location.pathname)
221
- if (_initMatch?.route?.load) {
222
- try { await _initMatch.route.load() } catch { /* non-fatal */ }
223
- }
224
- }
225
-
226
- if (typeof window !== 'undefined') {
227
- await _replace(window.location.pathname + window.location.search + window.location.hash)
228
- delete (globalThis as any).__CER_DATA__
229
- }
230
-
231
- export { router }
232
- ```
233
-
234
- > **Note:** Do not move the plugin loop before `component('cer-layout-view', …)`. The layout component must be defined first so that when plugins call `app.provide()`, the values are available to the component tree from the very first render. See [Plugins](plugins.md) for details.
136
+ > **Note:** The framework bootstrap lives in `.cer/app.ts` and is regenerated automatically on every dev server start and build. You never edit or own this file — updates to the plugin propagate to it immediately, just like Nuxt's `.nuxt/` directory.
235
137
 
236
138
  ---
237
139
 
@@ -93,8 +93,7 @@ export default handler
93
93
  export default defineConfig({
94
94
  mode: 'ssr',
95
95
  ssr: {
96
- dsd: true, // Declarative Shadow DOM (eliminates FOUC)
97
- streaming: false, // true = stream response; false = buffer full HTML
96
+ dsd: true, // Declarative Shadow DOM (eliminates FOUC)
98
97
  },
99
98
  })
100
99
  ```
@@ -242,7 +241,7 @@ Any CDN or static host. Upload the entire `dist/` directory (excluding `dist/ser
242
241
  | Dynamic routes | Yes | Yes | Requires `ssg.paths` |
243
242
  | API routes | Separate deploy | Same process | Separate deploy |
244
243
  | `useHead()` SSR injection | No | Yes | Yes |
245
- | Streaming | No | Optional | No |
244
+ | Streaming | No | No | No |
246
245
 
247
246
  ---
248
247
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jasonshimmy/vite-plugin-cer-app",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Nuxt-style meta-framework for @jasonshimmy/custom-elements-runtime",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -48,7 +48,7 @@ function makeConfig(overrides: Partial<ResolvedCerConfig> = {}): ResolvedCerConf
48
48
  serverApiDir: '/project/server/api',
49
49
  serverMiddlewareDir: '/project/server/middleware',
50
50
  port: 3000,
51
- ssr: { dsd: true, streaming: false },
51
+ ssr: { dsd: true },
52
52
  ssg: { routes: 'auto', concurrency: 2, fallback: false },
53
53
  router: {},
54
54
  jitCss: { content: [], extendedColors: false },
@@ -17,7 +17,6 @@ import { existsSync, writeFileSync, mkdirSync, readFileSync, appendFileSync } fr
17
17
  import {
18
18
  GENERATED_DIR_NAME,
19
19
  getGeneratedDir,
20
- resolveAppEntry,
21
20
  resolveHtmlEntry,
22
21
  generateDefaultHtml,
23
22
  writeGeneratedDir,
@@ -49,18 +48,6 @@ describe('getGeneratedDir', () => {
49
48
  })
50
49
  })
51
50
 
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
51
  describe('resolveHtmlEntry', () => {
65
52
  it('returns user index.html when it exists', () => {
66
53
  vi.mocked(existsSync).mockReturnValue(true)
@@ -74,26 +61,19 @@ describe('resolveHtmlEntry', () => {
74
61
  })
75
62
 
76
63
  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)
64
+ it('always references /.cer/app.ts', () => {
65
+ const html = generateDefaultHtml()
87
66
  expect(html).toContain('/.cer/app.ts')
67
+ expect(html).not.toContain('/app/app.ts')
88
68
  })
89
69
 
90
70
  it('includes <cer-layout-view> mount point', () => {
91
- const html = generateDefaultHtml(mockConfig)
71
+ const html = generateDefaultHtml()
92
72
  expect(html).toContain('<cer-layout-view>')
93
73
  })
94
74
 
95
75
  it('is valid HTML with doctype', () => {
96
- const html = generateDefaultHtml(mockConfig)
76
+ const html = generateDefaultHtml()
97
77
  expect(html).toContain('<!DOCTYPE html>')
98
78
  })
99
79
  })
@@ -112,28 +92,17 @@ describe('writeGeneratedDir', () => {
112
92
  expect(mkdirSync).not.toHaveBeenCalled()
113
93
  })
114
94
 
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
- })
95
+ it('always writes .cer/app.ts', () => {
126
96
  writeGeneratedDir(mockConfig)
127
97
  const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
128
98
  expect(paths.some(p => p.endsWith('/.cer/app.ts'))).toBe(true)
129
99
  })
130
100
 
131
- it('skips writing .cer/app.ts when app/app.ts exists', () => {
132
- // existsSync always returns true — dir exists, user entry exists
101
+ it('always writes .cer/app.ts even when .cer/ dir already exists', () => {
133
102
  vi.mocked(existsSync).mockReturnValue(true)
134
103
  writeGeneratedDir(mockConfig)
135
104
  const paths = vi.mocked(writeFileSync).mock.calls.map(([p]) => String(p))
136
- expect(paths.some(p => p.endsWith('/.cer/app.ts'))).toBe(false)
105
+ expect(paths.some(p => p.endsWith('/.cer/app.ts'))).toBe(true)
137
106
  })
138
107
 
139
108
  it('always writes .cer/index.html', () => {
@@ -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
 
@@ -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>
@@ -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 }
@@ -13,16 +13,6 @@ export function getGeneratedDir(root: string): string {
13
13
  return join(root, GENERATED_DIR_NAME)
14
14
  }
15
15
 
16
- /**
17
- * Returns the app entry file path to use for builds and the dev server.
18
- * Prefers the consumer's `app/app.ts` when it exists; falls back to `.cer/app.ts`.
19
- */
20
- export function resolveAppEntry(config: ResolvedCerConfig): string {
21
- const userEntry = join(config.srcDir, 'app.ts')
22
- if (existsSync(userEntry)) return userEntry
23
- return join(getGeneratedDir(config.root), 'app.ts')
24
- }
25
-
26
16
  /**
27
17
  * Returns the HTML entry path to use for builds.
28
18
  * Prefers the consumer's root-level `index.html` when it exists;
@@ -35,13 +25,10 @@ export function resolveHtmlEntry(config: ResolvedCerConfig): string {
35
25
  }
36
26
 
37
27
  /**
38
- * Generates the content for a default `index.html`.
39
- * The script src points to the consumer's `app/app.ts` if it exists,
40
- * otherwise to the generated `.cer/app.ts`.
28
+ * Generates the content for the default `.cer/index.html`.
29
+ * Always points to the generated `/.cer/app.ts` entry.
41
30
  */
42
- export function generateDefaultHtml(config: ResolvedCerConfig): string {
43
- const userEntry = join(config.srcDir, 'app.ts')
44
- const scriptSrc = existsSync(userEntry) ? '/app/app.ts' : '/.cer/app.ts'
31
+ export function generateDefaultHtml(): string {
45
32
  return `<!DOCTYPE html>
46
33
  <html lang="en">
47
34
  <head>
@@ -51,34 +38,61 @@ export function generateDefaultHtml(config: ResolvedCerConfig): string {
51
38
  </head>
52
39
  <body>
53
40
  <cer-layout-view></cer-layout-view>
54
- <script type="module" src="${scriptSrc}"></script>
41
+ <script type="module" src="/.cer/app.ts"></script>
55
42
  </body>
56
43
  </html>
57
44
  `
58
45
  }
59
46
 
47
+ const GITIGNORE_DEFAULTS = `# Dependencies
48
+ node_modules/
49
+
50
+ # Build output
51
+ dist/
52
+
53
+ # CER App generated directory
54
+ .cer/
55
+
56
+ # Environment variables
57
+ .env.local
58
+ .env.*.local
59
+
60
+ # Editor
61
+ .vscode/
62
+ .idea/
63
+ *.suo
64
+ *.sw?
65
+
66
+ # OS
67
+ .DS_Store
68
+ Thumbs.db
69
+
70
+ # Logs
71
+ *.log
72
+ `
73
+
60
74
  /**
61
- * Ensures `.cer/` is listed in the project's `.gitignore`.
62
- * Creates `.gitignore` if it does not exist.
75
+ * Ensures `.cer/`, `node_modules/`, `dist/`, and other common entries are
76
+ * listed in the project's `.gitignore`. Creates `.gitignore` if it does not exist.
63
77
  */
64
78
  function ensureGitignore(root: string): void {
65
79
  const gitignorePath = join(root, '.gitignore')
66
- const entry = `${GENERATED_DIR_NAME}/`
80
+ const cerEntry = `${GENERATED_DIR_NAME}/`
67
81
 
68
82
  if (existsSync(gitignorePath)) {
69
83
  const content = readFileSync(gitignorePath, 'utf-8')
70
- if (!content.includes(entry) && !content.includes(`${GENERATED_DIR_NAME}\n`)) {
71
- appendFileSync(gitignorePath, `\n# CER App generated directory\n${entry}\n`)
84
+ if (!content.includes(cerEntry) && !content.includes(`${GENERATED_DIR_NAME}\n`)) {
85
+ appendFileSync(gitignorePath, `\n# CER App generated directory\n${cerEntry}\n`)
72
86
  }
73
87
  } else {
74
- writeFileSync(gitignorePath, `# CER App generated directory\n${entry}\n`)
88
+ writeFileSync(gitignorePath, GITIGNORE_DEFAULTS)
75
89
  }
76
90
  }
77
91
 
78
92
  /**
79
93
  * Writes all generated files to `.cer/`:
80
- * - `.cer/app.ts` default entry (only when `app/app.ts` does not exist)
81
- * - `.cer/index.html` — default HTML shell
94
+ * - `.cer/app.ts` framework entry (always regenerated)
95
+ * - `.cer/index.html` — default HTML shell (always regenerated)
82
96
  * - `.cer/tsconfig.json` — written by dts-generator via writeTsconfigPaths
83
97
  *
84
98
  * Also ensures `.cer/` is listed in `.gitignore`.
@@ -89,14 +103,13 @@ export function writeGeneratedDir(config: ResolvedCerConfig): void {
89
103
  mkdirSync(dir, { recursive: true })
90
104
  }
91
105
 
92
- // Write default app.ts only when the consumer has not provided one.
93
- const userEntry = join(config.srcDir, 'app.ts')
94
- if (!existsSync(userEntry)) {
95
- writeFileSync(join(dir, 'app.ts'), APP_ENTRY_TEMPLATE, 'utf-8')
96
- }
106
+ // Always write the generated app.ts this is the framework entry point and
107
+ // is never user-owned. Regenerating it on every dev/build ensures consumers
108
+ // automatically get the latest bootstrap code on plugin update (Nuxt-style).
109
+ writeFileSync(join(dir, 'app.ts'), APP_ENTRY_TEMPLATE, 'utf-8')
97
110
 
98
111
  // Always write the default index.html so builds and the dev server can use it.
99
- writeFileSync(join(dir, 'index.html'), generateDefaultHtml(config), 'utf-8')
112
+ writeFileSync(join(dir, 'index.html'), generateDefaultHtml(), 'utf-8')
100
113
 
101
114
  ensureGitignore(config.root)
102
115
  }
@@ -62,7 +62,6 @@ export function resolveConfig(userConfig: CerAppConfig, root: string = process.c
62
62
  port: userConfig.port ?? 3000,
63
63
  ssr: {
64
64
  dsd: userConfig.ssr?.dsd ?? true,
65
- streaming: userConfig.ssr?.streaming ?? false,
66
65
  },
67
66
  ssg: {
68
67
  routes: userConfig.ssg?.routes ?? 'auto',
@@ -1,11 +1,12 @@
1
1
  /**
2
- * Template string for the default `.cer/app.ts` client entry point.
2
+ * Template string for `.cer/app.ts` — the framework client entry point.
3
3
  *
4
- * Written to `.cer/app.ts` when the consumer does not provide `app/app.ts`.
5
- * Consumers can override by creating their own `app/app.ts`.
4
+ * Always written to `.cer/app.ts` on every dev/build so consumers
5
+ * automatically receive the latest bootstrap code on plugin update.
6
+ * This file is gitignored and should never be edited directly.
6
7
  */
7
- export const APP_ENTRY_TEMPLATE = `// AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app
8
- // This is the default client entry point. Create app/app.ts to override it.
8
+ export const APP_ENTRY_TEMPLATE = `// AUTO-GENERATED by @jasonshimmy/vite-plugin-cer-app — do not edit.
9
+ // Regenerated automatically on every dev server start and build.
9
10
 
10
11
  import '@jasonshimmy/custom-elements-runtime/css'
11
12
  import 'virtual:cer-jit-css'
@@ -13,7 +13,6 @@ export interface JitCssConfig {
13
13
 
14
14
  export interface SsrConfig {
15
15
  dsd?: boolean
16
- streaming?: boolean
17
16
  }
18
17
 
19
18
  export interface AutoImportsConfig {