@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,159 @@
1
+ /**
2
+ * Template string for `.cer/app.ts` — the framework client entry point.
3
+ *
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.
7
+ */
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.
10
+
11
+ import '@jasonshimmy/custom-elements-runtime/css'
12
+ import 'virtual:cer-jit-css'
13
+ import 'virtual:cer-components'
14
+ import routes from 'virtual:cer-routes'
15
+ import layouts from 'virtual:cer-layouts'
16
+ import plugins from 'virtual:cer-plugins'
17
+ import { hasLoading, loadingTag } from 'virtual:cer-loading'
18
+ import { hasError, errorTag } from 'virtual:cer-error'
19
+ import {
20
+ component,
21
+ ref,
22
+ provide,
23
+ useOnConnected,
24
+ useOnDisconnected,
25
+ registerBuiltinComponents,
26
+ } from '@jasonshimmy/custom-elements-runtime'
27
+ import { initRouter } from '@jasonshimmy/custom-elements-runtime/router'
28
+ import { enableJITCSS } from '@jasonshimmy/custom-elements-runtime/jit-css'
29
+
30
+ registerBuiltinComponents()
31
+ enableJITCSS()
32
+
33
+ const router = initRouter({ routes })
34
+
35
+ // ─── Navigation state ────────────────────────────────────────────────────────
36
+
37
+ const isNavigating = ref(false)
38
+ const currentError = ref(null)
39
+
40
+ const resetError = (): void => {
41
+ currentError.value = null
42
+ void router.replace(router.getCurrent().path)
43
+ }
44
+ ;(globalThis as Record<string, unknown>).resetError = resetError
45
+
46
+ const _push = router.push.bind(router)
47
+ const _replace = router.replace.bind(router)
48
+
49
+ router.push = async (path) => {
50
+ isNavigating.value = true
51
+ currentError.value = null
52
+ try {
53
+ await _push(path)
54
+ } catch (err) {
55
+ currentError.value = err instanceof Error ? err.message : String(err)
56
+ } finally {
57
+ isNavigating.value = false
58
+ }
59
+ }
60
+
61
+ router.replace = async (path) => {
62
+ isNavigating.value = true
63
+ currentError.value = null
64
+ try {
65
+ await _replace(path)
66
+ } catch (err) {
67
+ currentError.value = err instanceof Error ? err.message : String(err)
68
+ } finally {
69
+ isNavigating.value = false
70
+ }
71
+ }
72
+
73
+ // ─── Plugins ─────────────────────────────────────────────────────────────────
74
+
75
+ // Collect plugin-provided values so cer-layout-view can forward them into
76
+ // the component context tree via the real provide() hook (which inject() walks).
77
+ // Declared BEFORE component('cer-layout-view') to avoid a temporal dead zone
78
+ // ReferenceError: customElements.define() upgrades existing DOM elements
79
+ // synchronously, calling the render function immediately.
80
+ const _pluginProvides = new Map<string, unknown>()
81
+ // Expose plugin provides globally so page components can read them synchronously
82
+ // regardless of render order.
83
+ ;(globalThis as Record<string, unknown>).__cerPluginProvides = _pluginProvides
84
+
85
+ // ─── <cer-layout-view> ───────────────────────────────────────────────────────
86
+
87
+ component('cer-layout-view', () => {
88
+ // Forward plugin-provided values into the component context so inject() in
89
+ // any descendant component can resolve them by walking up the DOM tree.
90
+ for (const [key, value] of _pluginProvides) {
91
+ provide(key, value)
92
+ }
93
+
94
+ const current = ref(router.getCurrent())
95
+ let unsub: (() => void) | undefined
96
+
97
+ useOnConnected(() => {
98
+ unsub = router.subscribe((s: typeof current.value) => { current.value = s })
99
+ })
100
+ useOnDisconnected(() => { unsub?.(); unsub = undefined })
101
+
102
+ if (currentError.value !== null) {
103
+ if (hasError && errorTag) {
104
+ return { tag: errorTag, props: { attrs: { error: String(currentError.value) } }, children: [] }
105
+ }
106
+ return { tag: 'div', props: { attrs: { style: 'padding:2rem;font-family:monospace' } }, children: String(currentError.value) }
107
+ }
108
+
109
+ if (isNavigating.value && hasLoading && loadingTag) {
110
+ return { tag: loadingTag, props: {}, children: [] }
111
+ }
112
+
113
+ const matched = router.matchRoute(current.value.path)
114
+ const routeMeta = matched?.route?.meta as { layout?: string } | undefined
115
+ const layoutName = routeMeta?.layout ?? 'default'
116
+ const layoutTag = (layouts as Record<string, string>)[layoutName]
117
+ const routerView = { tag: 'router-view', props: {}, children: [] }
118
+
119
+ if (layoutTag) return { tag: layoutTag, props: {}, children: [routerView] }
120
+ return routerView
121
+ })
122
+
123
+ for (const plugin of plugins) {
124
+ if (plugin && typeof plugin.setup === 'function') {
125
+ await plugin.setup({
126
+ router,
127
+ provide: (key: string, value: unknown) => { _pluginProvides.set(key, value) },
128
+ config: {},
129
+ })
130
+ }
131
+ }
132
+
133
+ // ─── Pre-load initial route ───────────────────────────────────────────────────
134
+ // Download the current page's route chunk AFTER plugins run so that
135
+ // cer-layout-view's first render (which calls provide()) completes before
136
+ // page components are defined and their renders are scheduled. This ensures
137
+ // inject() in child components can find values stored by provide().
138
+
139
+ if (typeof window !== 'undefined') {
140
+ const _initMatch = router.matchRoute(window.location.pathname)
141
+ if (_initMatch?.route?.load) {
142
+ try { await _initMatch.route.load() } catch { /* non-fatal */ }
143
+ }
144
+ }
145
+
146
+ // ─── Initial navigation ──────────────────────────────────────────────────────
147
+
148
+ if (typeof window !== 'undefined') {
149
+ // Use the original (unwrapped) replace so isNavigating stays false during
150
+ // the initial paint — the loading component must not flash over pre-rendered content.
151
+ await _replace(window.location.pathname + window.location.search + window.location.hash)
152
+ // Clear SSR loader data after initial navigation so subsequent client-side
153
+ // navigations don't accidentally reuse stale server data.
154
+ delete (globalThis as Record<string, unknown>).__CER_DATA__
155
+ }
156
+
157
+ export { router }
158
+ `;
159
+ //# sourceMappingURL=app-template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-template.js","sourceRoot":"","sources":["../../src/runtime/app-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsJjC,CAAA"}
@@ -10,7 +10,6 @@ export interface JitCssConfig {
10
10
  }
11
11
  export interface SsrConfig {
12
12
  dsd?: boolean;
13
- streaming?: boolean;
14
13
  }
15
14
  export interface AutoImportsConfig {
16
15
  components?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6CAA6C,CAAA;AAE/E,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAA;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,GAAG,CAAC,EAAE,SAAS,CAAA;IACf,MAAM,CAAC,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,kBAAkB,CAAC,CAAA;IACxD,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,GAAG,CAAC,EAAE,SAAS,CAAA;IACf,WAAW,CAAC,EAAE,iBAAiB,CAAA;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAE/D"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6CAA6C,CAAA;AAE/E,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,EAAE,OAAO,CAAA;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAA;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,GAAG,CAAC,EAAE,SAAS,CAAA;IACf,MAAM,CAAC,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,kBAAkB,CAAC,CAAA;IACxD,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,GAAG,CAAC,EAAE,SAAS,CAAA;IACf,WAAW,CAAC,EAAE,iBAAiB,CAAA;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAE/D"}
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAoCA,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO,MAAM,CAAA;AACf,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAmCA,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO,MAAM,CAAA;AACf,CAAC"}
package/docs/cli.md CHANGED
@@ -187,7 +187,7 @@ my-app/
187
187
  | Mode | `cer.config.ts` | `package.json` scripts |
188
188
  |---|---|---|
189
189
  | SPA | `mode: 'spa'` | `dev`, `build`, `preview` |
190
- | SSR | `mode: 'ssr'`, `ssr.streaming: true` | `dev`, `build`, `preview --ssr` |
190
+ | SSR | `mode: 'ssr'`, `ssr.dsd: true` | `dev`, `build`, `preview --ssr` |
191
191
  | SSG | `mode: 'ssg'`, `ssg.routes: 'auto'` | `dev`, `build`, `preview`, `generate` |
192
192
 
193
193
  ---
@@ -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 // Use streaming renderer (createStreamingSSRHandler)
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
- When `true`, uses the streaming SSR renderer (`createStreamingSSRHandler`) to progressively flush HTML to the client. When `false`, collects the full HTML string before sending.
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
 
@@ -63,9 +63,9 @@ The server renders HTML for each request. Uses Declarative Shadow DOM (DSD) to e
63
63
  3. API route handlers run if the URL matches `/api/`
64
64
  4. For HTML requests, the router matches the URL to a page
65
65
  5. The page's `loader` is called (if present)
66
- 6. The component tree is rendered to HTML via `createStreamingSSRHandler`
66
+ 6. The component tree is rendered to HTML with Declarative Shadow DOM via `renderToStringWithJITCSSDSD`
67
67
  7. `useHead()` calls are collected and injected before `</head>`
68
- 8. The response is streamed or sent in full
68
+ 8. The rendered HTML is merged with the Vite client bundle shell and sent as a full response
69
69
 
70
70
  ### Build output
71
71
 
@@ -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/docs/routing.md CHANGED
@@ -10,7 +10,7 @@ Routes are automatically derived from files in the `app/pages/` directory. No ma
10
10
  |---|---|---|
11
11
  | `app/pages/index.ts` | `/` | `page-index` |
12
12
  | `app/pages/about.ts` | `/about` | `page-about` |
13
- | `app/pages/blog/index.ts` | `/blog` | `page-blog-index` |
13
+ | `app/pages/blog/index.ts` | `/blog` | `page-blog` |
14
14
  | `app/pages/blog/[slug].ts` | `/blog/:slug` | `page-blog-slug` |
15
15
  | `app/pages/users/[id]/edit.ts` | `/users/:id/edit` | `page-users-id-edit` |
16
16
  | `app/pages/404.ts` | `/:all*` (catch-all) | `page-404` |
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "./.cer/tsconfig.json"
3
+ }
@@ -0,0 +1,22 @@
1
+ import tseslint from 'typescript-eslint'
2
+
3
+ export default tseslint.config(
4
+ {
5
+ ignores: [
6
+ 'dist/**',
7
+ 'node_modules/**',
8
+ 'coverage/**',
9
+ 'e2e/**',
10
+ 'src/__tests__/**',
11
+ 'src/cli/create/templates/**',
12
+ ],
13
+ },
14
+ {
15
+ files: ['src/**/*.ts'],
16
+ extends: [...tseslint.configs.recommended],
17
+ rules: {
18
+ '@typescript-eslint/no-explicit-any': 'error',
19
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
20
+ },
21
+ },
22
+ )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jasonshimmy/vite-plugin-cer-app",
3
- "version": "0.1.6",
3
+ "version": "0.3.0",
4
4
  "description": "Nuxt-style meta-framework for @jasonshimmy/custom-elements-runtime",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -44,8 +44,10 @@
44
44
  "create-cer-app": "./dist/cli/create/index.js"
45
45
  },
46
46
  "scripts": {
47
+ "validate": "npm run lint && npm test && npm run build && npm run e2e",
47
48
  "build": "tsc -p tsconfig.build.json && cp -r src/cli/create/templates dist/cli/create/templates",
48
49
  "dev": "tsc -p tsconfig.build.json --watch",
50
+ "lint": "eslint",
49
51
  "test": "vitest run",
50
52
  "test:watch": "vitest",
51
53
  "test:coverage": "vitest run --coverage",
@@ -80,9 +82,12 @@
80
82
  "@types/node": "^25.5.0",
81
83
  "@vitest/coverage-v8": "^4.1.0",
82
84
  "cypress": "^15.12.0",
85
+ "eslint": "^10.0.3",
83
86
  "happy-dom": "^20.8.4",
87
+ "jiti": "^2.6.1",
84
88
  "start-server-and-test": "^2.1.5",
85
89
  "typescript": "^5.9.3",
90
+ "typescript-eslint": "^8.57.1",
86
91
  "vite": "^8.0.1",
87
92
  "vitest": "^4.1.0"
88
93
  },
@@ -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.mockReturnValue(true) // index.html exists
210
+ existsSyncMock.mockImplementation((p: unknown) => String(p).endsWith('index.html'))
206
211
  await buildSSR(makeConfig())
207
- const clientInput = (buildMock.mock.calls[0][0] as any).build.rollupOptions.input
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 entry-client.ts when index.html is absent', async () => {
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 any).build.rollupOptions.input
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 neither index.html nor entry-client.ts exist', async () => {
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 any).build.rollupOptions.input
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()
@@ -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 },
@@ -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 cer-tsconfig.json to the root directory', () => {
37
+ it('writes tsconfig.json to the .cer directory', () => {
36
38
  writeTsconfigPaths(ROOT, `${ROOT}/app`)
37
39
  expect(writeFileSync).toHaveBeenCalledWith(
38
- `${ROOT}/cer-tsconfig.json`,
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 cer-auto-imports.d.ts to root', async () => {
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-auto-imports.d.ts'))).toBe(true)
242
+ expect(paths.some(p => p.includes('.cer/auto-imports.d.ts'))).toBe(true)
234
243
  })
235
244
 
236
- it('writes cer-env.d.ts to root', async () => {
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-env.d.ts'))).toBe(true)
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 () => {