@jasonshimmy/vite-plugin-cer-app 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/IMPLEMENTATION_PLAN.md +1 -1
  3. package/README.md +7 -7
  4. package/VITE_PLUGIN_FRAMEWORK_PLAN.md +12 -12
  5. package/commits.txt +1 -3
  6. package/dist/cli/commands/generate.d.ts.map +1 -1
  7. package/dist/cli/commands/generate.js +2 -0
  8. package/dist/cli/commands/generate.js.map +1 -1
  9. package/dist/cli/commands/preview.d.ts.map +1 -1
  10. package/dist/cli/commands/preview.js +21 -2
  11. package/dist/cli/commands/preview.js.map +1 -1
  12. package/dist/cli/create/index.js +2 -2
  13. package/dist/cli/create/index.js.map +1 -1
  14. package/dist/cli/index.js +1 -1
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/plugin/build-ssg.d.ts.map +1 -1
  17. package/dist/plugin/build-ssg.js +10 -21
  18. package/dist/plugin/build-ssg.js.map +1 -1
  19. package/dist/plugin/build-ssr.d.ts.map +1 -1
  20. package/dist/plugin/build-ssr.js +151 -28
  21. package/dist/plugin/build-ssr.js.map +1 -1
  22. package/dist/plugin/dts-generator.js +4 -4
  23. package/dist/plugin/dts-generator.js.map +1 -1
  24. package/dist/plugin/index.js +2 -2
  25. package/dist/plugin/index.js.map +1 -1
  26. package/dist/plugin/transforms/auto-import.js +3 -3
  27. package/dist/plugin/transforms/auto-import.js.map +1 -1
  28. package/dist/plugin/virtual/components.js +3 -3
  29. package/dist/plugin/virtual/components.js.map +1 -1
  30. package/dist/plugin/virtual/composables.js +3 -3
  31. package/dist/plugin/virtual/composables.js.map +1 -1
  32. package/dist/plugin/virtual/error.js +2 -2
  33. package/dist/plugin/virtual/error.js.map +1 -1
  34. package/dist/plugin/virtual/layouts.js +3 -3
  35. package/dist/plugin/virtual/layouts.js.map +1 -1
  36. package/dist/plugin/virtual/loading.js +2 -2
  37. package/dist/plugin/virtual/loading.js.map +1 -1
  38. package/dist/plugin/virtual/middleware.js +3 -3
  39. package/dist/plugin/virtual/middleware.js.map +1 -1
  40. package/dist/plugin/virtual/plugins.js +3 -3
  41. package/dist/plugin/virtual/plugins.js.map +1 -1
  42. package/dist/plugin/virtual/routes.d.ts.map +1 -1
  43. package/dist/plugin/virtual/routes.js +14 -4
  44. package/dist/plugin/virtual/routes.js.map +1 -1
  45. package/dist/plugin/virtual/server-api.js +3 -3
  46. package/dist/plugin/virtual/server-api.js.map +1 -1
  47. package/dist/plugin/virtual/server-middleware.js +3 -3
  48. package/dist/plugin/virtual/server-middleware.js.map +1 -1
  49. package/dist/runtime/app-template.d.ts +1 -1
  50. package/dist/runtime/app-template.d.ts.map +1 -1
  51. package/dist/runtime/app-template.js +6 -0
  52. package/dist/runtime/app-template.js.map +1 -1
  53. package/dist/runtime/composables/use-page-data.d.ts +15 -6
  54. package/dist/runtime/composables/use-page-data.d.ts.map +1 -1
  55. package/dist/runtime/composables/use-page-data.js +30 -9
  56. package/dist/runtime/composables/use-page-data.js.map +1 -1
  57. package/dist/runtime/entry-server-template.d.ts +1 -1
  58. package/dist/runtime/entry-server-template.d.ts.map +1 -1
  59. package/dist/runtime/entry-server-template.js +138 -17
  60. package/dist/runtime/entry-server-template.js.map +1 -1
  61. package/docs/cli.md +2 -2
  62. package/docs/configuration.md +4 -4
  63. package/docs/data-loading.md +8 -7
  64. package/docs/getting-started.md +5 -5
  65. package/docs/head-management.md +3 -3
  66. package/docs/middleware.md +2 -2
  67. package/docs/plugins.md +1 -1
  68. package/docs/rendering-modes.md +1 -1
  69. package/docs/routing.md +1 -1
  70. package/docs/server-api.md +10 -1
  71. package/docs/testing.md +4 -4
  72. package/package.json +1 -1
  73. package/src/__tests__/index.test.ts +21 -0
  74. package/src/__tests__/plugin/build-ssg.test.ts +265 -0
  75. package/src/__tests__/plugin/build-ssr.test.ts +180 -0
  76. package/src/__tests__/plugin/cer-app-plugin.test.ts +409 -0
  77. package/src/__tests__/plugin/dts-generator.test.ts +246 -0
  78. package/src/__tests__/plugin/resolve-config.test.ts +158 -0
  79. package/src/__tests__/plugin/transforms/auto-import.test.ts +1 -1
  80. package/src/__tests__/plugin/virtual/components.test.ts +1 -1
  81. package/src/__tests__/plugin/virtual/composables.test.ts +1 -1
  82. package/src/__tests__/plugin/virtual/error.test.ts +71 -0
  83. package/src/__tests__/plugin/virtual/layouts.test.ts +1 -1
  84. package/src/__tests__/plugin/virtual/loading.test.ts +72 -0
  85. package/src/__tests__/plugin/virtual/middleware.test.ts +1 -1
  86. package/src/__tests__/plugin/virtual/plugins.test.ts +1 -1
  87. package/src/__tests__/plugin/virtual/routes.test.ts +1 -1
  88. package/src/__tests__/plugin/virtual/server-api.test.ts +1 -1
  89. package/src/__tests__/plugin/virtual/server-middleware.test.ts +102 -0
  90. package/src/__tests__/runtime/use-page-data.test.ts +81 -5
  91. package/src/__tests__/types/config.test.ts +23 -0
  92. package/src/cli/commands/generate.ts +2 -0
  93. package/src/cli/commands/preview.ts +21 -2
  94. package/src/cli/create/index.ts +2 -2
  95. package/src/cli/create/templates/spa/cer.config.ts.tpl +1 -1
  96. package/src/cli/create/templates/spa/package.json.tpl +1 -1
  97. package/src/cli/create/templates/ssg/cer.config.ts.tpl +1 -1
  98. package/src/cli/create/templates/ssg/package.json.tpl +1 -1
  99. package/src/cli/create/templates/ssr/cer.config.ts.tpl +1 -1
  100. package/src/cli/create/templates/ssr/package.json.tpl +1 -1
  101. package/src/cli/index.ts +1 -1
  102. package/src/plugin/build-ssg.ts +9 -22
  103. package/src/plugin/build-ssr.ts +150 -28
  104. package/src/plugin/dts-generator.ts +4 -4
  105. package/src/plugin/index.ts +2 -2
  106. package/src/plugin/transforms/auto-import.ts +3 -3
  107. package/src/plugin/virtual/components.ts +3 -3
  108. package/src/plugin/virtual/composables.ts +3 -3
  109. package/src/plugin/virtual/error.ts +2 -2
  110. package/src/plugin/virtual/layouts.ts +3 -3
  111. package/src/plugin/virtual/loading.ts +2 -2
  112. package/src/plugin/virtual/middleware.ts +3 -3
  113. package/src/plugin/virtual/plugins.ts +3 -3
  114. package/src/plugin/virtual/routes.ts +15 -4
  115. package/src/plugin/virtual/server-api.ts +3 -3
  116. package/src/plugin/virtual/server-middleware.ts +3 -3
  117. package/src/runtime/app-template.ts +6 -0
  118. package/src/runtime/composables/use-page-data.ts +31 -9
  119. package/src/runtime/entry-server-template.ts +138 -17
  120. package/tsconfig.build.json +1 -1
  121. package/dist/__tests__/plugin/path-utils.test.d.ts +0 -2
  122. package/dist/__tests__/plugin/path-utils.test.d.ts.map +0 -1
  123. package/dist/__tests__/plugin/path-utils.test.js +0 -305
  124. package/dist/__tests__/plugin/path-utils.test.js.map +0 -1
  125. package/dist/__tests__/plugin/scanner.test.d.ts +0 -2
  126. package/dist/__tests__/plugin/scanner.test.d.ts.map +0 -1
  127. package/dist/__tests__/plugin/scanner.test.js +0 -143
  128. package/dist/__tests__/plugin/scanner.test.js.map +0 -1
  129. package/dist/__tests__/plugin/transforms/auto-import.test.d.ts +0 -2
  130. package/dist/__tests__/plugin/transforms/auto-import.test.d.ts.map +0 -1
  131. package/dist/__tests__/plugin/transforms/auto-import.test.js +0 -151
  132. package/dist/__tests__/plugin/transforms/auto-import.test.js.map +0 -1
  133. package/dist/__tests__/plugin/transforms/head-inject.test.d.ts +0 -2
  134. package/dist/__tests__/plugin/transforms/head-inject.test.d.ts.map +0 -1
  135. package/dist/__tests__/plugin/transforms/head-inject.test.js +0 -151
  136. package/dist/__tests__/plugin/transforms/head-inject.test.js.map +0 -1
  137. package/dist/__tests__/plugin/virtual/components.test.d.ts +0 -2
  138. package/dist/__tests__/plugin/virtual/components.test.d.ts.map +0 -1
  139. package/dist/__tests__/plugin/virtual/components.test.js +0 -47
  140. package/dist/__tests__/plugin/virtual/components.test.js.map +0 -1
  141. package/dist/__tests__/plugin/virtual/composables.test.d.ts +0 -2
  142. package/dist/__tests__/plugin/virtual/composables.test.d.ts.map +0 -1
  143. package/dist/__tests__/plugin/virtual/composables.test.js +0 -48
  144. package/dist/__tests__/plugin/virtual/composables.test.js.map +0 -1
  145. package/dist/__tests__/plugin/virtual/layouts.test.d.ts +0 -2
  146. package/dist/__tests__/plugin/virtual/layouts.test.d.ts.map +0 -1
  147. package/dist/__tests__/plugin/virtual/layouts.test.js +0 -59
  148. package/dist/__tests__/plugin/virtual/layouts.test.js.map +0 -1
  149. package/dist/__tests__/plugin/virtual/middleware.test.d.ts +0 -2
  150. package/dist/__tests__/plugin/virtual/middleware.test.d.ts.map +0 -1
  151. package/dist/__tests__/plugin/virtual/middleware.test.js +0 -58
  152. package/dist/__tests__/plugin/virtual/middleware.test.js.map +0 -1
  153. package/dist/__tests__/plugin/virtual/plugins.test.d.ts +0 -2
  154. package/dist/__tests__/plugin/virtual/plugins.test.d.ts.map +0 -1
  155. package/dist/__tests__/plugin/virtual/plugins.test.js +0 -73
  156. package/dist/__tests__/plugin/virtual/plugins.test.js.map +0 -1
  157. package/dist/__tests__/plugin/virtual/routes.test.d.ts +0 -2
  158. package/dist/__tests__/plugin/virtual/routes.test.d.ts.map +0 -1
  159. package/dist/__tests__/plugin/virtual/routes.test.js +0 -167
  160. package/dist/__tests__/plugin/virtual/routes.test.js.map +0 -1
  161. package/dist/__tests__/plugin/virtual/server-api.test.d.ts +0 -2
  162. package/dist/__tests__/plugin/virtual/server-api.test.d.ts.map +0 -1
  163. package/dist/__tests__/plugin/virtual/server-api.test.js +0 -72
  164. package/dist/__tests__/plugin/virtual/server-api.test.js.map +0 -1
  165. package/dist/__tests__/runtime/use-head.test.d.ts +0 -2
  166. package/dist/__tests__/runtime/use-head.test.d.ts.map +0 -1
  167. package/dist/__tests__/runtime/use-head.test.js +0 -202
  168. package/dist/__tests__/runtime/use-head.test.js.map +0 -1
  169. package/dist/__tests__/runtime/use-page-data.test.d.ts +0 -2
  170. package/dist/__tests__/runtime/use-page-data.test.d.ts.map +0 -1
  171. package/dist/__tests__/runtime/use-page-data.test.js +0 -41
  172. package/dist/__tests__/runtime/use-page-data.test.js.map +0 -1
@@ -8,7 +8,7 @@ Each page can export a `loader` function that runs on the server before the page
8
8
 
9
9
  ```ts
10
10
  // app/pages/blog/[slug].ts
11
- import type { PageLoader } from 'vite-plugin-cer-app/types'
11
+ import type { PageLoader } from '@jasonshimmy/vite-plugin-cer-app/types'
12
12
 
13
13
  component('page-blog-slug', () => {
14
14
  const props = useProps({ slug: '', title: '', body: '' })
@@ -54,20 +54,21 @@ interface PageLoaderContext<P extends Record<string, string>> {
54
54
  1. A request arrives for `/blog/hello-world`
55
55
  2. The router matches `app/pages/blog/[slug].ts`
56
56
  3. The server calls `loader({ params: { slug: 'hello-world' }, query, req })`
57
- 4. The returned data is serialized as `window.__CER_DATA__` in a `<script>` tag in the HTML:
57
+ 4. The returned data is serialized as `window.__CER_DATA__` in a `<script>` tag in the HTML `<head>`:
58
58
  ```html
59
59
  <script>window.__CER_DATA__ = {"title":"Hello World","body":"..."}</script>
60
60
  ```
61
- 5. `renderToString` or `renderToStream` renders `<page-blog-slug>` with the data as props
62
- 6. The full HTML is sent to the browser
61
+ 5. The server renders `<page-blog-slug>` directly into the layout using Declarative Shadow DOM
62
+ 6. The full HTML (pre-rendered content + client scripts) is sent to the browser
63
63
 
64
64
  ---
65
65
 
66
66
  ## Client hydration flow
67
67
 
68
- 1. Browser receives the full HTML (no layout flash because of Declarative Shadow DOM)
69
- 2. The runtime reads `window.__CER_DATA__` and passes the values as component props
70
- 3. Components attach event listeners to the pre-rendered DOM no re-fetch required
68
+ 1. Browser receives the full HTML content is immediately visible via Declarative Shadow DOM before any JS runs
69
+ 2. Client JS boots; `usePageData()` reads `window.__CER_DATA__` and returns the hydrated values
70
+ 3. The value is cleared after the first read so subsequent client-side navigations trigger a fresh fetch
71
+ 4. Components that received SSR data skip their `useOnConnected` fetch — no duplicate request
71
72
 
72
73
  ---
73
74
 
@@ -49,7 +49,7 @@ npx create-cer-app my-app --mode ssr
49
49
  ### 1. Install
50
50
 
51
51
  ```sh
52
- npm install -D vite-plugin-cer-app
52
+ npm install -D @jasonshimmy/vite-plugin-cer-app
53
53
  npm install @jasonshimmy/custom-elements-runtime
54
54
  ```
55
55
 
@@ -58,7 +58,7 @@ npm install @jasonshimmy/custom-elements-runtime
58
58
  ```ts
59
59
  // vite.config.ts
60
60
  import { defineConfig } from 'vite'
61
- import { cerApp } from 'vite-plugin-cer-app'
61
+ import { cerApp } from '@jasonshimmy/vite-plugin-cer-app'
62
62
 
63
63
  export default defineConfig({
64
64
  plugins: [cerApp()],
@@ -69,7 +69,7 @@ Or use a separate `cer.config.ts` (picked up automatically by the CLI, and passe
69
69
 
70
70
  ```ts
71
71
  // cer.config.ts
72
- import { defineConfig } from 'vite-plugin-cer-app'
72
+ import { defineConfig } from '@jasonshimmy/vite-plugin-cer-app'
73
73
 
74
74
  export default defineConfig({
75
75
  mode: 'spa',
@@ -79,7 +79,7 @@ export default defineConfig({
79
79
  ```ts
80
80
  // vite.config.ts (when using cer.config.ts)
81
81
  import { defineConfig } from 'vite'
82
- import { cerApp } from 'vite-plugin-cer-app'
82
+ import { cerApp } from '@jasonshimmy/vite-plugin-cer-app'
83
83
  import cerConfig from './cer.config.ts'
84
84
 
85
85
  export default defineConfig({
@@ -127,7 +127,7 @@ component('layout-default', () => {
127
127
  <title>My App</title>
128
128
  </head>
129
129
  <body>
130
- <router-view></router-view>
130
+ <cer-layout-view></cer-layout-view>
131
131
  <script type="module" src="/app/app.ts"></script>
132
132
  </body>
133
133
  </html>
@@ -10,7 +10,7 @@ The `useHead()` composable manages `<title>`, `<meta>`, `<link>`, `<script>`, an
10
10
  ## Import
11
11
 
12
12
  ```ts
13
- import { useHead } from 'vite-plugin-cer-app/composables'
13
+ import { useHead } from '@jasonshimmy/vite-plugin-cer-app/composables'
14
14
  ```
15
15
 
16
16
  ---
@@ -175,10 +175,10 @@ Style tags are not deduplicated.
175
175
 
176
176
  During SSR rendering, `useHead()` calls are collected via a request-scoped array. After rendering is complete, the collected tags are serialized and injected before `</head>` in the HTML shell.
177
177
 
178
- You can use the underlying primitives directly from `vite-plugin-cer-app/composables` if you need manual control:
178
+ You can use the underlying primitives directly from `@jasonshimmy/vite-plugin-cer-app/composables` if you need manual control:
179
179
 
180
180
  ```ts
181
- import { beginHeadCollection, endHeadCollection, serializeHeadTags } from 'vite-plugin-cer-app/composables'
181
+ import { beginHeadCollection, endHeadCollection, serializeHeadTags } from '@jasonshimmy/vite-plugin-cer-app/composables'
182
182
 
183
183
  // Before rendering:
184
184
  beginHeadCollection()
@@ -15,7 +15,7 @@ Create a file in `app/middleware/`. It runs before every route navigation:
15
15
 
16
16
  ```ts
17
17
  // app/middleware/auth.ts
18
- import type { RouteMiddleware } from 'vite-plugin-cer-app/types'
18
+ import type { RouteMiddleware } from '@jasonshimmy/vite-plugin-cer-app/types'
19
19
 
20
20
  const auth: RouteMiddleware = async (to, from, next) => {
21
21
  const session = await getSession()
@@ -89,7 +89,7 @@ Server middleware runs on the HTTP level — before API routes and before SSR re
89
89
 
90
90
  ```ts
91
91
  // server/middleware/cors.ts
92
- import type { ServerMiddleware } from 'vite-plugin-cer-app/types'
92
+ import type { ServerMiddleware } from '@jasonshimmy/vite-plugin-cer-app/types'
93
93
 
94
94
  const cors: ServerMiddleware = (req, res, next) => {
95
95
  res.setHeader('Access-Control-Allow-Origin', '*')
package/docs/plugins.md CHANGED
@@ -8,7 +8,7 @@ Plugins are loaded once before the app renders. They receive an `app` context wi
8
8
 
9
9
  ```ts
10
10
  // app/plugins/01.store.ts
11
- import type { AppPlugin } from 'vite-plugin-cer-app/types'
11
+ import type { AppPlugin } from '@jasonshimmy/vite-plugin-cer-app/types'
12
12
  import { createStore } from '@jasonshimmy/custom-elements-runtime/store'
13
13
 
14
14
  export default {
@@ -22,7 +22,7 @@ The simplest mode. Vite builds a standard client-only bundle. No server required
22
22
 
23
23
  ### How it works
24
24
 
25
- - `index.html` is the entry point with `<router-view>` as the root element
25
+ - `index.html` is the entry point with `<cer-layout-view>` as the app mount element
26
26
  - All routing is client-side; the server returns the same `index.html` for every URL
27
27
  - `virtual:cer-routes` injects all routes into the client-side router
28
28
 
package/docs/routing.md CHANGED
@@ -94,7 +94,7 @@ Export a `meta` object from any page file to customize behavior:
94
94
 
95
95
  ```ts
96
96
  // app/pages/blog/[slug].ts
97
- import type { PageMeta } from 'vite-plugin-cer-app/types'
97
+ import type { PageMeta } from '@jasonshimmy/vite-plugin-cer-app/types'
98
98
 
99
99
  component('page-blog-slug', () => { /* ... */ })
100
100
 
@@ -10,7 +10,7 @@ Export one function per HTTP method. Method names must be uppercase:
10
10
 
11
11
  ```ts
12
12
  // server/api/users/index.ts → GET /api/users, POST /api/users
13
- import type { ApiHandler } from 'vite-plugin-cer-app/types'
13
+ import type { ApiHandler } from '@jasonshimmy/vite-plugin-cer-app/types'
14
14
 
15
15
  export const GET: ApiHandler = async (req, res) => {
16
16
  const users = await db.user.findAll()
@@ -160,6 +160,15 @@ export default async function handler(req, res) {
160
160
  | **SPA production** | Not included — deploy API routes separately or use a proxy |
161
161
  | **SSG production** | Optionally called at build time for data; otherwise deployed separately |
162
162
 
163
+ ### SPA mode — by design
164
+
165
+ In SPA mode (`mode: 'spa'`) the build output is a pure client bundle with no server component. API routes defined in `server/api/` are **only active during development** (Vite dev server middleware). At runtime the SPA has no server to serve them from.
166
+
167
+ Options for SPA + API:
168
+ - **Separate API server** — deploy a Node.js/Express server alongside the SPA that mounts the same `server/api/` handlers.
169
+ - **Reverse proxy** — proxy `/api/*` requests from your CDN or web server to a backend service.
170
+ - **Switch to SSR mode** — `mode: 'ssr'` gives you a full Node.js server that serves both the SSR pages and the API routes from a single process.
171
+
163
172
  ---
164
173
 
165
174
  ## Virtual module
package/docs/testing.md CHANGED
@@ -9,7 +9,7 @@ This guide walks through how to manually test every major feature of the framewo
9
9
  Build the plugin first:
10
10
 
11
11
  ```sh
12
- cd /path/to/vite-plugin-cer-app
12
+ cd /path/to/@jasonshimmy/vite-plugin-cer-app
13
13
  npm install
14
14
  npm run build
15
15
  ```
@@ -31,7 +31,7 @@ node dist/cli/create/index.js my-ssg --mode ssg
31
31
  In each directory, install dependencies and link the plugin:
32
32
 
33
33
  ```sh
34
- cd my-spa && npm install && npm link /path/to/vite-plugin-cer-app
34
+ cd my-spa && npm install && npm link /path/to/@jasonshimmy/vite-plugin-cer-app
35
35
  ```
36
36
 
37
37
  ### Start the dev server
@@ -325,7 +325,7 @@ EOF
325
325
 
326
326
  ```ts
327
327
  // app/pages/about.ts
328
- import { useHead } from 'vite-plugin-cer-app/composables'
328
+ import { useHead } from '@jasonshimmy/vite-plugin-cer-app/composables'
329
329
 
330
330
  component('page-about', () => {
331
331
  useHead({
@@ -431,7 +431,7 @@ cat dist/items/1/index.html # should contain "Item 1"
431
431
  The framework ships with 211 unit and integration tests:
432
432
 
433
433
  ```sh
434
- cd /path/to/vite-plugin-cer-app
434
+ cd /path/to/@jasonshimmy/vite-plugin-cer-app
435
435
  npm test
436
436
  ```
437
437
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jasonshimmy/vite-plugin-cer-app",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Nuxt-style meta-framework for @jasonshimmy/custom-elements-runtime",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -0,0 +1,21 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ // Import from the main package entry to trigger coverage of src/index.ts
4
+ // re-exports. These are all re-exports of other modules so we just verify
5
+ // the named exports resolve without errors.
6
+ import { defineConfig, cerApp } from '../index.js'
7
+
8
+ describe('package main entry', () => {
9
+ it('exports defineConfig', () => {
10
+ expect(typeof defineConfig).toBe('function')
11
+ })
12
+
13
+ it('defineConfig is a pass-through', () => {
14
+ const cfg = { mode: 'spa' as const }
15
+ expect(defineConfig(cfg)).toBe(cfg)
16
+ })
17
+
18
+ it('exports cerApp', () => {
19
+ expect(typeof cerApp).toBe('function')
20
+ })
21
+ })
@@ -0,0 +1,265 @@
1
+ import { vi, describe, it, expect, beforeEach } from 'vitest'
2
+
3
+ vi.mock('node:fs', () => ({ existsSync: vi.fn().mockReturnValue(false) }))
4
+ vi.mock('node:fs/promises', () => ({
5
+ writeFile: vi.fn().mockResolvedValue(undefined),
6
+ mkdir: vi.fn().mockResolvedValue(undefined),
7
+ }))
8
+ vi.mock('fast-glob', () => ({ default: vi.fn().mockResolvedValue([]) }))
9
+ vi.mock('vite', () => ({
10
+ build: vi.fn().mockResolvedValue(undefined),
11
+ createServer: vi.fn(),
12
+ }))
13
+ vi.mock('../../plugin/build-ssr.js', () => ({ buildSSR: vi.fn().mockResolvedValue(undefined) }))
14
+ vi.mock('../../plugin/path-utils.js', () => ({ buildRouteEntry: vi.fn() }))
15
+
16
+ import { existsSync } from 'node:fs'
17
+ import { writeFile, mkdir } from 'node:fs/promises'
18
+ import fg from 'fast-glob'
19
+ import { createServer } from 'vite'
20
+ import { buildSSR } from '../../plugin/build-ssr.js'
21
+ import { buildRouteEntry } from '../../plugin/path-utils.js'
22
+ import { buildSSG } from '../../plugin/build-ssg.js'
23
+ import type { ResolvedCerConfig } from '../../plugin/dev-server.js'
24
+
25
+ function makeConfig(overrides: Partial<ResolvedCerConfig> = {}): ResolvedCerConfig {
26
+ return {
27
+ root: '/project',
28
+ srcDir: '/project/app',
29
+ pagesDir: '/project/app/pages',
30
+ mode: 'ssg',
31
+ ssr: { dsd: true },
32
+ ssg: { concurrency: 4 },
33
+ ...overrides,
34
+ } as unknown as ResolvedCerConfig
35
+ }
36
+
37
+ beforeEach(() => {
38
+ vi.mocked(buildSSR).mockClear()
39
+ vi.mocked(writeFile).mockClear()
40
+ vi.mocked(mkdir).mockClear()
41
+ vi.mocked(fg).mockClear()
42
+ vi.mocked(existsSync).mockReturnValue(false)
43
+ vi.mocked(fg).mockResolvedValue([])
44
+ vi.mocked(buildRouteEntry).mockReset()
45
+ })
46
+
47
+ describe('buildSSG — buildSSR delegation', () => {
48
+ it('calls buildSSR as step 1', async () => {
49
+ await buildSSG(makeConfig())
50
+ expect(buildSSR).toHaveBeenCalledTimes(1)
51
+ })
52
+
53
+ it('passes the config to buildSSR', async () => {
54
+ const config = makeConfig()
55
+ await buildSSG(config)
56
+ expect(vi.mocked(buildSSR).mock.calls[0][0]).toBe(config)
57
+ })
58
+ })
59
+
60
+ describe('buildSSG — ssg-manifest.json', () => {
61
+ it('writes ssg-manifest.json to the dist directory', async () => {
62
+ await buildSSG(makeConfig())
63
+ const manifestCall = vi.mocked(writeFile).mock.calls.find(([path]) =>
64
+ String(path).includes('ssg-manifest.json'),
65
+ )
66
+ expect(manifestCall).toBeDefined()
67
+ expect(String(manifestCall![0])).toContain('/project/dist/ssg-manifest.json')
68
+ })
69
+
70
+ it('manifest JSON contains generatedAt field', async () => {
71
+ await buildSSG(makeConfig())
72
+ const manifestCall = vi.mocked(writeFile).mock.calls.find(([p]) =>
73
+ String(p).includes('ssg-manifest.json'),
74
+ )
75
+ const manifest = JSON.parse(String(manifestCall![1]))
76
+ expect(manifest).toHaveProperty('generatedAt')
77
+ expect(typeof manifest.generatedAt).toBe('string')
78
+ })
79
+
80
+ it('manifest JSON contains paths array', async () => {
81
+ await buildSSG(makeConfig())
82
+ const manifestCall = vi.mocked(writeFile).mock.calls.find(([p]) =>
83
+ String(p).includes('ssg-manifest.json'),
84
+ )
85
+ const manifest = JSON.parse(String(manifestCall![1]))
86
+ expect(manifest).toHaveProperty('paths')
87
+ expect(Array.isArray(manifest.paths)).toBe(true)
88
+ })
89
+
90
+ it('manifest JSON contains errors array', async () => {
91
+ await buildSSG(makeConfig())
92
+ const manifestCall = vi.mocked(writeFile).mock.calls.find(([p]) =>
93
+ String(p).includes('ssg-manifest.json'),
94
+ )
95
+ const manifest = JSON.parse(String(manifestCall![1]))
96
+ expect(manifest).toHaveProperty('errors')
97
+ expect(Array.isArray(manifest.errors)).toBe(true)
98
+ })
99
+
100
+ it('records render errors in manifest (missing server bundle)', async () => {
101
+ const config = makeConfig({ ssg: { routes: ['/about'], concurrency: 1 } } as Partial<ResolvedCerConfig>)
102
+ await buildSSG(config)
103
+ const manifestCall = vi.mocked(writeFile).mock.calls.find(([p]) =>
104
+ String(p).includes('ssg-manifest.json'),
105
+ )
106
+ const manifest = JSON.parse(String(manifestCall![1]))
107
+ // paths + errors together must cover every route we attempted to render
108
+ expect(manifest.paths.length + manifest.errors.length).toBe(1)
109
+ })
110
+
111
+ it('error entries have path and error fields', async () => {
112
+ const config = makeConfig({ ssg: { routes: ['/fail'], concurrency: 1 } } as Partial<ResolvedCerConfig>)
113
+ await buildSSG(config)
114
+ const manifestCall = vi.mocked(writeFile).mock.calls.find(([p]) =>
115
+ String(p).includes('ssg-manifest.json'),
116
+ )
117
+ const manifest = JSON.parse(String(manifestCall![1]))
118
+ if (manifest.errors.length > 0) {
119
+ expect(manifest.errors[0]).toHaveProperty('path')
120
+ expect(manifest.errors[0]).toHaveProperty('error')
121
+ }
122
+ })
123
+ })
124
+
125
+ describe('buildSSG — path collection', () => {
126
+ it('uses ssg.routes when explicitly provided (skips auto-discovery)', async () => {
127
+ const config = makeConfig({
128
+ ssg: { routes: ['/a', '/b'], concurrency: 1 },
129
+ } as Partial<ResolvedCerConfig>)
130
+ await buildSSG(config)
131
+ // fast-glob should NOT have been called — routes are explicit
132
+ expect(fg).not.toHaveBeenCalled()
133
+ })
134
+
135
+ it('calls fg when pagesDir exists and no explicit routes', async () => {
136
+ vi.mocked(existsSync).mockReturnValue(true)
137
+ await buildSSG(makeConfig())
138
+ expect(fg).toHaveBeenCalledTimes(1)
139
+ })
140
+
141
+ it('skips Vite dev server when all discovered pages are static', async () => {
142
+ vi.mocked(existsSync).mockReturnValue(true)
143
+ vi.mocked(fg).mockResolvedValue([
144
+ '/project/app/pages/index.ts',
145
+ '/project/app/pages/about.ts',
146
+ ])
147
+ vi.mocked(buildRouteEntry)
148
+ .mockReturnValueOnce({ routePath: '/', isDynamic: false, isCatchAll: false } as ReturnType<typeof buildRouteEntry>)
149
+ .mockReturnValueOnce({ routePath: '/about', isDynamic: false, isCatchAll: false } as ReturnType<typeof buildRouteEntry>)
150
+
151
+ await buildSSG(makeConfig())
152
+
153
+ expect(createServer).not.toHaveBeenCalled()
154
+ })
155
+
156
+ it('spawns Vite dev server for dynamic pages', async () => {
157
+ vi.mocked(existsSync).mockReturnValue(true)
158
+ vi.mocked(fg).mockResolvedValue(['/project/app/pages/[slug].ts'])
159
+ vi.mocked(buildRouteEntry).mockReturnValueOnce({
160
+ routePath: '/:slug',
161
+ isDynamic: true,
162
+ isCatchAll: false,
163
+ } as ReturnType<typeof buildRouteEntry>)
164
+
165
+ const closeFn = vi.fn().mockResolvedValue(undefined)
166
+ vi.mocked(createServer).mockResolvedValue({
167
+ ssrLoadModule: vi.fn().mockResolvedValue({}),
168
+ close: closeFn,
169
+ } as unknown as Awaited<ReturnType<typeof createServer>>)
170
+
171
+ await buildSSG(makeConfig())
172
+
173
+ expect(createServer).toHaveBeenCalledTimes(1)
174
+ expect(closeFn).toHaveBeenCalledTimes(1)
175
+ })
176
+
177
+ it('closes Vite dev server even when ssrLoadModule throws', async () => {
178
+ vi.mocked(existsSync).mockReturnValue(true)
179
+ vi.mocked(fg).mockResolvedValue(['/project/app/pages/[slug].ts'])
180
+ vi.mocked(buildRouteEntry).mockReturnValueOnce({
181
+ routePath: '/:slug',
182
+ isDynamic: true,
183
+ isCatchAll: false,
184
+ } as ReturnType<typeof buildRouteEntry>)
185
+
186
+ const closeFn = vi.fn().mockResolvedValue(undefined)
187
+ vi.mocked(createServer).mockResolvedValue({
188
+ ssrLoadModule: vi.fn().mockRejectedValue(new Error('load failed')),
189
+ close: closeFn,
190
+ } as unknown as Awaited<ReturnType<typeof createServer>>)
191
+
192
+ await buildSSG(makeConfig())
193
+
194
+ expect(closeFn).toHaveBeenCalledTimes(1)
195
+ })
196
+
197
+ it('expands dynamic ssg.paths into concrete URL paths', async () => {
198
+ vi.mocked(existsSync).mockReturnValue(true)
199
+ vi.mocked(fg).mockResolvedValue(['/project/app/pages/[id].ts'])
200
+ vi.mocked(buildRouteEntry).mockReturnValueOnce({
201
+ routePath: '/:id',
202
+ isDynamic: true,
203
+ isCatchAll: false,
204
+ } as ReturnType<typeof buildRouteEntry>)
205
+
206
+ const ssgPathsFn = vi.fn().mockResolvedValue([
207
+ { params: { id: '1' } },
208
+ { params: { id: '2' } },
209
+ ])
210
+ const closeFn = vi.fn().mockResolvedValue(undefined)
211
+ vi.mocked(createServer).mockResolvedValue({
212
+ ssrLoadModule: vi.fn().mockResolvedValue({ meta: { ssg: { paths: ssgPathsFn } } }),
213
+ close: closeFn,
214
+ } as unknown as Awaited<ReturnType<typeof createServer>>)
215
+
216
+ const config = makeConfig({ ssg: { concurrency: 1 } } as Partial<ResolvedCerConfig>)
217
+ await buildSSG(config)
218
+
219
+ // The manifest should attempt to render '/', '/1', '/2' (3 paths total)
220
+ const manifestCall = vi.mocked(writeFile).mock.calls.find(([p]) =>
221
+ String(p).includes('ssg-manifest.json'),
222
+ )
223
+ const manifest = JSON.parse(String(manifestCall![1]))
224
+ expect(manifest.paths.length + manifest.errors.length).toBe(3)
225
+ })
226
+
227
+ it('skips catch-all pages when auto-discovering paths', async () => {
228
+ vi.mocked(existsSync).mockReturnValue(true)
229
+ vi.mocked(fg).mockResolvedValue(['/project/app/pages/[...all].ts'])
230
+ vi.mocked(buildRouteEntry).mockReturnValueOnce({
231
+ routePath: '/:all*',
232
+ isDynamic: true,
233
+ isCatchAll: true,
234
+ } as ReturnType<typeof buildRouteEntry>)
235
+
236
+ await buildSSG(makeConfig())
237
+
238
+ // Only '/' (always added) should be attempted
239
+ const manifestCall = vi.mocked(writeFile).mock.calls.find(([p]) =>
240
+ String(p).includes('ssg-manifest.json'),
241
+ )
242
+ const manifest = JSON.parse(String(manifestCall![1]))
243
+ expect(manifest.paths.length + manifest.errors.length).toBe(1)
244
+ })
245
+
246
+ it('deduplicates collected paths', async () => {
247
+ vi.mocked(existsSync).mockReturnValue(true)
248
+ vi.mocked(fg).mockResolvedValue([
249
+ '/project/app/pages/index.ts',
250
+ '/project/app/pages/home.ts',
251
+ ])
252
+ // Both resolve to '/' — should deduplicate to a single path
253
+ vi.mocked(buildRouteEntry)
254
+ .mockReturnValueOnce({ routePath: '/', isDynamic: false, isCatchAll: false } as ReturnType<typeof buildRouteEntry>)
255
+ .mockReturnValueOnce({ routePath: '/', isDynamic: false, isCatchAll: false } as ReturnType<typeof buildRouteEntry>)
256
+
257
+ await buildSSG(makeConfig({ ssg: { concurrency: 1 } } as Partial<ResolvedCerConfig>))
258
+
259
+ const manifestCall = vi.mocked(writeFile).mock.calls.find(([p]) =>
260
+ String(p).includes('ssg-manifest.json'),
261
+ )
262
+ const manifest = JSON.parse(String(manifestCall![1]))
263
+ expect(manifest.paths.length + manifest.errors.length).toBe(1)
264
+ })
265
+ })