@jasonshimmy/vite-plugin-cer-app 0.11.0 → 0.12.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.
- package/CHANGELOG.md +4 -0
- package/IMPLEMENTATION_PLAN.md +4 -0
- package/commits.txt +1 -1
- package/dist/cli/adapters/cloudflare.js +3 -3
- package/dist/cli/adapters/netlify.js +3 -3
- package/dist/cli/adapters/vercel.js +3 -3
- package/docs/rendering-modes.md +15 -5
- package/package.json +1 -1
- package/src/__tests__/cli/adapters/cloudflare.test.ts +7 -0
- package/src/__tests__/cli/adapters/netlify.test.ts +7 -0
- package/src/__tests__/cli/adapters/vercel.test.ts +8 -1
- package/src/cli/adapters/cloudflare.ts +3 -3
- package/src/cli/adapters/netlify.ts +3 -3
- package/src/cli/adapters/vercel.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
|
+
## [v0.12.0] - 2026-03-22
|
|
5
|
+
|
|
6
|
+
- feat: implement ISR support with isrHandler for SSR fallback in Cloudflare, Netlify, and Vercel adapters (ddcc4d4)
|
|
7
|
+
|
|
4
8
|
## [v0.11.0] - 2026-03-22
|
|
5
9
|
|
|
6
10
|
- feat: add Cloudflare Pages adapter for SSR and SSG support (2734c26)
|
package/IMPLEMENTATION_PLAN.md
CHANGED
|
@@ -287,6 +287,10 @@ Requires a `revalidate` option per route (via `meta.ssg.revalidate: 60`).
|
|
|
287
287
|
|
|
288
288
|
**Files:**
|
|
289
289
|
- `src/cli/commands/preview.ts` — ISR cache layer
|
|
290
|
+
- `src/runtime/isr-handler.ts` — `createIsrHandler` factory (shared by all platforms)
|
|
291
|
+
- `src/cli/adapters/vercel.ts` — uses `isrHandler` for SSR fallback
|
|
292
|
+
- `src/cli/adapters/netlify.ts` — uses `isrHandler` for SSR fallback
|
|
293
|
+
- `src/cli/adapters/cloudflare.ts` — uses `isrHandler` for SSR fallback
|
|
290
294
|
- `src/plugin/virtual/routes.ts` — include `meta.ssg.revalidate` in route
|
|
291
295
|
- `src/types/page.ts` — add `revalidate` to `PageSsgConfig`
|
|
292
296
|
|
package/commits.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
- feat:
|
|
1
|
+
- feat: implement ISR support with isrHandler for SSR fallback in Cloudflare, Netlify, and Vercel adapters (ddcc4d4)
|
|
@@ -51,7 +51,7 @@ import { Readable } from 'node:stream'
|
|
|
51
51
|
// Must be set before the dynamic import below resolves.
|
|
52
52
|
globalThis.__CER_CLIENT_TEMPLATE__ = \`${escaped}\`
|
|
53
53
|
|
|
54
|
-
const { handler, apiRoutes, runServerMiddleware, runWithRequestContext } = await import('./server/server.js')
|
|
54
|
+
const { handler, isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } = await import('./server/server.js')
|
|
55
55
|
|
|
56
56
|
function matchApiPattern(pattern, urlPath) {
|
|
57
57
|
const pp = pattern.split('/')
|
|
@@ -172,8 +172,8 @@ async function handleRequest(webReq) {
|
|
|
172
172
|
return new Response('Not Found', { status: 404 })
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
// All other requests: SSR.
|
|
176
|
-
|
|
175
|
+
// All other requests: SSR (with ISR for routes that declare meta.ssg.revalidate).
|
|
176
|
+
isrHandler(nodeReq, res).catch(() => {
|
|
177
177
|
if (!res.writableEnded) {
|
|
178
178
|
res.statusCode = 500
|
|
179
179
|
res.end('Internal Server Error')
|
|
@@ -38,7 +38,7 @@ import { existsSync, mkdirSync, writeFileSync, copyFileSync, cpSync, readdirSync
|
|
|
38
38
|
const NETLIFY_SSR_BRIDGE = `\
|
|
39
39
|
// Auto-generated by @jasonshimmy/vite-plugin-cer-app — do not edit.
|
|
40
40
|
import { Readable } from 'node:stream'
|
|
41
|
-
import { handler, apiRoutes, runServerMiddleware, runWithRequestContext } from '../../dist/server/server.js'
|
|
41
|
+
import { handler, isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } from '../../dist/server/server.js'
|
|
42
42
|
|
|
43
43
|
function matchApiPattern(pattern, urlPath) {
|
|
44
44
|
const pp = pattern.split('/')
|
|
@@ -161,8 +161,8 @@ export default async (webReq) => {
|
|
|
161
161
|
return new Response('Not Found', { status: 404 })
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
// All other requests: SSR.
|
|
165
|
-
|
|
164
|
+
// All other requests: SSR (with ISR for routes that declare meta.ssg.revalidate).
|
|
165
|
+
isrHandler(nodeReq, res).catch(() => {
|
|
166
166
|
if (!res.writableEnded) {
|
|
167
167
|
res.statusCode = 500
|
|
168
168
|
res.end('Internal Server Error')
|
|
@@ -30,7 +30,7 @@ import { existsSync, mkdirSync, writeFileSync, copyFileSync, cpSync, readdirSync
|
|
|
30
30
|
*/
|
|
31
31
|
const VERCEL_LAUNCHER = `\
|
|
32
32
|
// Auto-generated by @jasonshimmy/vite-plugin-cer-app — do not edit.
|
|
33
|
-
import { handler, apiRoutes, runServerMiddleware, runWithRequestContext } from './server/server.js'
|
|
33
|
+
import { handler, isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } from './server/server.js'
|
|
34
34
|
|
|
35
35
|
function matchApiPattern(pattern, urlPath) {
|
|
36
36
|
const pp = pattern.split('/')
|
|
@@ -120,8 +120,8 @@ export default async function cerAppHandler(req, res) {
|
|
|
120
120
|
return
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
// All other requests: SSR.
|
|
124
|
-
await
|
|
123
|
+
// All other requests: SSR (with ISR for routes that declare meta.ssg.revalidate).
|
|
124
|
+
await isrHandler(req, res)
|
|
125
125
|
}
|
|
126
126
|
`;
|
|
127
127
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
package/docs/rendering-modes.md
CHANGED
|
@@ -227,7 +227,7 @@ Any CDN or static host. Upload the entire `dist/` directory (excluding `dist/ser
|
|
|
227
227
|
|
|
228
228
|
## ISR — Incremental Static Regeneration
|
|
229
229
|
|
|
230
|
-
ISR is a per-route cache layer
|
|
230
|
+
ISR is a per-route cache layer built into the SSR server bundle. Pages with `meta.ssg.revalidate` set are rendered once, cached in memory, and re-rendered in the background when the TTL expires (stale-while-revalidate). It works identically in the preview server, on Vercel, on Netlify, and on Cloudflare Pages — no extra configuration required.
|
|
231
231
|
|
|
232
232
|
### How it works
|
|
233
233
|
|
|
@@ -266,9 +266,9 @@ export const meta = {
|
|
|
266
266
|
|
|
267
267
|
ISR caches by **path only** — query strings are stripped from the cache key. Requests to `/blog/post?preview=true` and `/blog/post` share the same cache entry. Use `render: 'server'` (no `revalidate`) for routes where query parameters affect the rendered output.
|
|
268
268
|
|
|
269
|
-
### Decision order
|
|
269
|
+
### Decision order (preview server and hosting adapters)
|
|
270
270
|
|
|
271
|
-
The built-in preview server
|
|
271
|
+
The built-in preview server — and the generated entry points for Vercel, Netlify, and Cloudflare — resolve each request using this precedence:
|
|
272
272
|
|
|
273
273
|
1. Static asset (`dist/client/**.*`) — served directly
|
|
274
274
|
2. `render: 'spa'` — returns `dist/client/index.html` (SPA shell)
|
|
@@ -293,9 +293,19 @@ ISR is controlled solely by `meta.ssg.revalidate`. The `meta.render` override is
|
|
|
293
293
|
|
|
294
294
|
### Availability
|
|
295
295
|
|
|
296
|
-
ISR is active
|
|
296
|
+
ISR is active everywhere the `isrHandler` from the server bundle is used:
|
|
297
297
|
|
|
298
|
-
|
|
298
|
+
| Environment | ISR supported |
|
|
299
|
+
|---|---|
|
|
300
|
+
| `cer-app preview` (built-in preview server) | ✅ |
|
|
301
|
+
| Vercel (via `cer-app adapt vercel`) | ✅ |
|
|
302
|
+
| Netlify (via `cer-app adapt netlify`) | ✅ |
|
|
303
|
+
| Cloudflare Pages (via `cer-app adapt cloudflare`) | ✅ |
|
|
304
|
+
| Custom Express / Node.js server | ✅ (use `isrHandler` directly) |
|
|
305
|
+
|
|
306
|
+
The in-memory ISR cache is per-process. On platforms that spin up multiple instances (Vercel, Netlify), each instance maintains its own cache — this is consistent with how Next.js and other frameworks handle ISR at the edge.
|
|
307
|
+
|
|
308
|
+
**Custom Node.js server (Express):**
|
|
299
309
|
```ts
|
|
300
310
|
import express from 'express'
|
|
301
311
|
import sirv from 'sirv'
|
package/package.json
CHANGED
|
@@ -76,6 +76,13 @@ describe('runCloudflareAdapter — SSR mode', () => {
|
|
|
76
76
|
expect(worker).toContain('Readable')
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
+
it('worker imports isrHandler and uses it for SSR fallback (enables ISR stale-while-revalidate)', async () => {
|
|
80
|
+
await runCloudflareAdapter(root)
|
|
81
|
+
const worker = readText(root, 'dist/_worker.js')
|
|
82
|
+
expect(worker).toContain('isrHandler')
|
|
83
|
+
expect(worker).toContain('isrHandler(nodeReq, res)')
|
|
84
|
+
})
|
|
85
|
+
|
|
79
86
|
it('worker handles /api/* routing via matchApiPattern', async () => {
|
|
80
87
|
await runCloudflareAdapter(root)
|
|
81
88
|
const worker = readText(root, 'dist/_worker.js')
|
|
@@ -51,6 +51,13 @@ describe('runNetlifyAdapter — SSR mode', () => {
|
|
|
51
51
|
expect(bridge).toContain("from '../../dist/server/server.js'")
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
+
it('bridge imports isrHandler and uses it for SSR fallback (enables ISR stale-while-revalidate)', async () => {
|
|
55
|
+
await runNetlifyAdapter(root)
|
|
56
|
+
const bridge = readText(root, 'netlify/functions/ssr.mjs')
|
|
57
|
+
expect(bridge).toContain('isrHandler')
|
|
58
|
+
expect(bridge).toContain('isrHandler(nodeReq, res)')
|
|
59
|
+
})
|
|
60
|
+
|
|
54
61
|
it('bridge exports a default async function', async () => {
|
|
55
62
|
await runNetlifyAdapter(root)
|
|
56
63
|
const bridge = readText(root, 'netlify/functions/ssr.mjs')
|
|
@@ -96,10 +96,17 @@ describe('runVercelAdapter — SSR mode', () => {
|
|
|
96
96
|
await runVercelAdapter(root)
|
|
97
97
|
expect(existsSync(join(root, '.vercel/output/functions/index.func/index.js'))).toBe(true)
|
|
98
98
|
const launcher = readText(root, '.vercel/output/functions/index.func/index.js')
|
|
99
|
-
expect(launcher).toContain("import { handler, apiRoutes, runServerMiddleware, runWithRequestContext } from './server/server.js'")
|
|
99
|
+
expect(launcher).toContain("import { handler, isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } from './server/server.js'")
|
|
100
100
|
expect(launcher).toContain('export default async function cerAppHandler')
|
|
101
101
|
})
|
|
102
102
|
|
|
103
|
+
it('launcher uses isrHandler for SSR fallback (enables ISR stale-while-revalidate)', async () => {
|
|
104
|
+
await runVercelAdapter(root)
|
|
105
|
+
const launcher = readText(root, '.vercel/output/functions/index.func/index.js')
|
|
106
|
+
expect(launcher).toContain('isrHandler')
|
|
107
|
+
expect(launcher).toContain('await isrHandler(req, res)')
|
|
108
|
+
})
|
|
109
|
+
|
|
103
110
|
it('launcher routes /api/* requests to apiRoutes', async () => {
|
|
104
111
|
await runVercelAdapter(root)
|
|
105
112
|
const launcher = readText(root, '.vercel/output/functions/index.func/index.js')
|
|
@@ -64,7 +64,7 @@ import { Readable } from 'node:stream'
|
|
|
64
64
|
// Must be set before the dynamic import below resolves.
|
|
65
65
|
globalThis.__CER_CLIENT_TEMPLATE__ = \`${escaped}\`
|
|
66
66
|
|
|
67
|
-
const { handler, apiRoutes, runServerMiddleware, runWithRequestContext } = await import('./server/server.js')
|
|
67
|
+
const { handler, isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } = await import('./server/server.js')
|
|
68
68
|
|
|
69
69
|
function matchApiPattern(pattern, urlPath) {
|
|
70
70
|
const pp = pattern.split('/')
|
|
@@ -185,8 +185,8 @@ async function handleRequest(webReq) {
|
|
|
185
185
|
return new Response('Not Found', { status: 404 })
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
// All other requests: SSR.
|
|
189
|
-
|
|
188
|
+
// All other requests: SSR (with ISR for routes that declare meta.ssg.revalidate).
|
|
189
|
+
isrHandler(nodeReq, res).catch(() => {
|
|
190
190
|
if (!res.writableEnded) {
|
|
191
191
|
res.statusCode = 500
|
|
192
192
|
res.end('Internal Server Error')
|
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
const NETLIFY_SSR_BRIDGE = `\
|
|
51
51
|
// Auto-generated by @jasonshimmy/vite-plugin-cer-app — do not edit.
|
|
52
52
|
import { Readable } from 'node:stream'
|
|
53
|
-
import { handler, apiRoutes, runServerMiddleware, runWithRequestContext } from '../../dist/server/server.js'
|
|
53
|
+
import { handler, isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } from '../../dist/server/server.js'
|
|
54
54
|
|
|
55
55
|
function matchApiPattern(pattern, urlPath) {
|
|
56
56
|
const pp = pattern.split('/')
|
|
@@ -173,8 +173,8 @@ export default async (webReq) => {
|
|
|
173
173
|
return new Response('Not Found', { status: 404 })
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
// All other requests: SSR.
|
|
177
|
-
|
|
176
|
+
// All other requests: SSR (with ISR for routes that declare meta.ssg.revalidate).
|
|
177
|
+
isrHandler(nodeReq, res).catch(() => {
|
|
178
178
|
if (!res.writableEnded) {
|
|
179
179
|
res.statusCode = 500
|
|
180
180
|
res.end('Internal Server Error')
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
*/
|
|
43
43
|
const VERCEL_LAUNCHER = `\
|
|
44
44
|
// Auto-generated by @jasonshimmy/vite-plugin-cer-app — do not edit.
|
|
45
|
-
import { handler, apiRoutes, runServerMiddleware, runWithRequestContext } from './server/server.js'
|
|
45
|
+
import { handler, isrHandler, apiRoutes, runServerMiddleware, runWithRequestContext } from './server/server.js'
|
|
46
46
|
|
|
47
47
|
function matchApiPattern(pattern, urlPath) {
|
|
48
48
|
const pp = pattern.split('/')
|
|
@@ -132,8 +132,8 @@ export default async function cerAppHandler(req, res) {
|
|
|
132
132
|
return
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
// All other requests: SSR.
|
|
136
|
-
await
|
|
135
|
+
// All other requests: SSR (with ISR for routes that declare meta.ssg.revalidate).
|
|
136
|
+
await isrHandler(req, res)
|
|
137
137
|
}
|
|
138
138
|
`
|
|
139
139
|
|