@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.
- package/CHANGELOG.md +8 -0
- package/IMPLEMENTATION_PLAN.md +1 -1
- package/README.md +7 -7
- package/VITE_PLUGIN_FRAMEWORK_PLAN.md +12 -12
- package/commits.txt +1 -3
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +2 -0
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/preview.d.ts.map +1 -1
- package/dist/cli/commands/preview.js +21 -2
- package/dist/cli/commands/preview.js.map +1 -1
- package/dist/cli/create/index.js +2 -2
- package/dist/cli/create/index.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/plugin/build-ssg.d.ts.map +1 -1
- package/dist/plugin/build-ssg.js +10 -21
- package/dist/plugin/build-ssg.js.map +1 -1
- package/dist/plugin/build-ssr.d.ts.map +1 -1
- package/dist/plugin/build-ssr.js +151 -28
- package/dist/plugin/build-ssr.js.map +1 -1
- package/dist/plugin/dts-generator.js +4 -4
- package/dist/plugin/dts-generator.js.map +1 -1
- package/dist/plugin/index.js +2 -2
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/transforms/auto-import.js +3 -3
- package/dist/plugin/transforms/auto-import.js.map +1 -1
- package/dist/plugin/virtual/components.js +3 -3
- package/dist/plugin/virtual/components.js.map +1 -1
- package/dist/plugin/virtual/composables.js +3 -3
- package/dist/plugin/virtual/composables.js.map +1 -1
- package/dist/plugin/virtual/error.js +2 -2
- package/dist/plugin/virtual/error.js.map +1 -1
- package/dist/plugin/virtual/layouts.js +3 -3
- package/dist/plugin/virtual/layouts.js.map +1 -1
- package/dist/plugin/virtual/loading.js +2 -2
- package/dist/plugin/virtual/loading.js.map +1 -1
- package/dist/plugin/virtual/middleware.js +3 -3
- package/dist/plugin/virtual/middleware.js.map +1 -1
- package/dist/plugin/virtual/plugins.js +3 -3
- package/dist/plugin/virtual/plugins.js.map +1 -1
- package/dist/plugin/virtual/routes.d.ts.map +1 -1
- package/dist/plugin/virtual/routes.js +14 -4
- package/dist/plugin/virtual/routes.js.map +1 -1
- package/dist/plugin/virtual/server-api.js +3 -3
- package/dist/plugin/virtual/server-api.js.map +1 -1
- package/dist/plugin/virtual/server-middleware.js +3 -3
- package/dist/plugin/virtual/server-middleware.js.map +1 -1
- package/dist/runtime/app-template.d.ts +1 -1
- package/dist/runtime/app-template.d.ts.map +1 -1
- package/dist/runtime/app-template.js +6 -0
- package/dist/runtime/app-template.js.map +1 -1
- package/dist/runtime/composables/use-page-data.d.ts +15 -6
- package/dist/runtime/composables/use-page-data.d.ts.map +1 -1
- package/dist/runtime/composables/use-page-data.js +30 -9
- package/dist/runtime/composables/use-page-data.js.map +1 -1
- package/dist/runtime/entry-server-template.d.ts +1 -1
- package/dist/runtime/entry-server-template.d.ts.map +1 -1
- package/dist/runtime/entry-server-template.js +138 -17
- package/dist/runtime/entry-server-template.js.map +1 -1
- package/docs/cli.md +2 -2
- package/docs/configuration.md +4 -4
- package/docs/data-loading.md +8 -7
- package/docs/getting-started.md +5 -5
- package/docs/head-management.md +3 -3
- package/docs/middleware.md +2 -2
- package/docs/plugins.md +1 -1
- package/docs/rendering-modes.md +1 -1
- package/docs/routing.md +1 -1
- package/docs/server-api.md +10 -1
- package/docs/testing.md +4 -4
- package/package.json +1 -1
- package/src/__tests__/index.test.ts +21 -0
- package/src/__tests__/plugin/build-ssg.test.ts +265 -0
- package/src/__tests__/plugin/build-ssr.test.ts +180 -0
- package/src/__tests__/plugin/cer-app-plugin.test.ts +409 -0
- package/src/__tests__/plugin/dts-generator.test.ts +246 -0
- package/src/__tests__/plugin/resolve-config.test.ts +158 -0
- package/src/__tests__/plugin/transforms/auto-import.test.ts +1 -1
- package/src/__tests__/plugin/virtual/components.test.ts +1 -1
- package/src/__tests__/plugin/virtual/composables.test.ts +1 -1
- package/src/__tests__/plugin/virtual/error.test.ts +71 -0
- package/src/__tests__/plugin/virtual/layouts.test.ts +1 -1
- package/src/__tests__/plugin/virtual/loading.test.ts +72 -0
- package/src/__tests__/plugin/virtual/middleware.test.ts +1 -1
- package/src/__tests__/plugin/virtual/plugins.test.ts +1 -1
- package/src/__tests__/plugin/virtual/routes.test.ts +1 -1
- package/src/__tests__/plugin/virtual/server-api.test.ts +1 -1
- package/src/__tests__/plugin/virtual/server-middleware.test.ts +102 -0
- package/src/__tests__/runtime/use-page-data.test.ts +81 -5
- package/src/__tests__/types/config.test.ts +23 -0
- package/src/cli/commands/generate.ts +2 -0
- package/src/cli/commands/preview.ts +21 -2
- package/src/cli/create/index.ts +2 -2
- package/src/cli/create/templates/spa/cer.config.ts.tpl +1 -1
- package/src/cli/create/templates/spa/package.json.tpl +1 -1
- package/src/cli/create/templates/ssg/cer.config.ts.tpl +1 -1
- package/src/cli/create/templates/ssg/package.json.tpl +1 -1
- package/src/cli/create/templates/ssr/cer.config.ts.tpl +1 -1
- package/src/cli/create/templates/ssr/package.json.tpl +1 -1
- package/src/cli/index.ts +1 -1
- package/src/plugin/build-ssg.ts +9 -22
- package/src/plugin/build-ssr.ts +150 -28
- package/src/plugin/dts-generator.ts +4 -4
- package/src/plugin/index.ts +2 -2
- package/src/plugin/transforms/auto-import.ts +3 -3
- package/src/plugin/virtual/components.ts +3 -3
- package/src/plugin/virtual/composables.ts +3 -3
- package/src/plugin/virtual/error.ts +2 -2
- package/src/plugin/virtual/layouts.ts +3 -3
- package/src/plugin/virtual/loading.ts +2 -2
- package/src/plugin/virtual/middleware.ts +3 -3
- package/src/plugin/virtual/plugins.ts +3 -3
- package/src/plugin/virtual/routes.ts +15 -4
- package/src/plugin/virtual/server-api.ts +3 -3
- package/src/plugin/virtual/server-middleware.ts +3 -3
- package/src/runtime/app-template.ts +6 -0
- package/src/runtime/composables/use-page-data.ts +31 -9
- package/src/runtime/entry-server-template.ts +138 -17
- package/tsconfig.build.json +1 -1
- package/dist/__tests__/plugin/path-utils.test.d.ts +0 -2
- package/dist/__tests__/plugin/path-utils.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/path-utils.test.js +0 -305
- package/dist/__tests__/plugin/path-utils.test.js.map +0 -1
- package/dist/__tests__/plugin/scanner.test.d.ts +0 -2
- package/dist/__tests__/plugin/scanner.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/scanner.test.js +0 -143
- package/dist/__tests__/plugin/scanner.test.js.map +0 -1
- package/dist/__tests__/plugin/transforms/auto-import.test.d.ts +0 -2
- package/dist/__tests__/plugin/transforms/auto-import.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/transforms/auto-import.test.js +0 -151
- package/dist/__tests__/plugin/transforms/auto-import.test.js.map +0 -1
- package/dist/__tests__/plugin/transforms/head-inject.test.d.ts +0 -2
- package/dist/__tests__/plugin/transforms/head-inject.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/transforms/head-inject.test.js +0 -151
- package/dist/__tests__/plugin/transforms/head-inject.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/components.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/components.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/components.test.js +0 -47
- package/dist/__tests__/plugin/virtual/components.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/composables.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/composables.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/composables.test.js +0 -48
- package/dist/__tests__/plugin/virtual/composables.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/layouts.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/layouts.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/layouts.test.js +0 -59
- package/dist/__tests__/plugin/virtual/layouts.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/middleware.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/middleware.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/middleware.test.js +0 -58
- package/dist/__tests__/plugin/virtual/middleware.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/plugins.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/plugins.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/plugins.test.js +0 -73
- package/dist/__tests__/plugin/virtual/plugins.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/routes.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/routes.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/routes.test.js +0 -167
- package/dist/__tests__/plugin/virtual/routes.test.js.map +0 -1
- package/dist/__tests__/plugin/virtual/server-api.test.d.ts +0 -2
- package/dist/__tests__/plugin/virtual/server-api.test.d.ts.map +0 -1
- package/dist/__tests__/plugin/virtual/server-api.test.js +0 -72
- package/dist/__tests__/plugin/virtual/server-api.test.js.map +0 -1
- package/dist/__tests__/runtime/use-head.test.d.ts +0 -2
- package/dist/__tests__/runtime/use-head.test.d.ts.map +0 -1
- package/dist/__tests__/runtime/use-head.test.js +0 -202
- package/dist/__tests__/runtime/use-head.test.js.map +0 -1
- package/dist/__tests__/runtime/use-page-data.test.d.ts +0 -2
- package/dist/__tests__/runtime/use-page-data.test.d.ts.map +0 -1
- package/dist/__tests__/runtime/use-page-data.test.js +0 -41
- package/dist/__tests__/runtime/use-page-data.test.js.map +0 -1
package/docs/data-loading.md
CHANGED
|
@@ -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.
|
|
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
|
|
69
|
-
2.
|
|
70
|
-
3.
|
|
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
|
|
package/docs/getting-started.md
CHANGED
|
@@ -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
|
-
<
|
|
130
|
+
<cer-layout-view></cer-layout-view>
|
|
131
131
|
<script type="module" src="/app/app.ts"></script>
|
|
132
132
|
</body>
|
|
133
133
|
</html>
|
package/docs/head-management.md
CHANGED
|
@@ -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
|
|
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()
|
package/docs/middleware.md
CHANGED
|
@@ -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 {
|
package/docs/rendering-modes.md
CHANGED
|
@@ -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 `<
|
|
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
|
|
package/docs/server-api.md
CHANGED
|
@@ -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
|
@@ -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
|
+
})
|