@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 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)
@@ -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: add Cloudflare Pages adapter for SSR and SSG support (2734c26)
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
- handler(nodeReq, res).catch(() => {
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
- handler(nodeReq, res).catch(() => {
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 handler(req, res)
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 ───────────────────────────────────────────────────────────────
@@ -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 in the SSR preview server. Pages with `meta.ssg.revalidate` set are rendered once, cached, and re-rendered in the background when the TTL expires (stale-while-revalidate).
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 in the preview server
269
+ ### Decision order (preview server and hosting adapters)
270
270
 
271
- The built-in preview server resolves each request using this precedence:
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 in the built-in **preview server** (`cer-app preview`) and in production via the `isrHandler` export from the server bundle.
296
+ ISR is active everywhere the `isrHandler` from the server bundle is used:
297
297
 
298
- **Production (Express):**
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jasonshimmy/vite-plugin-cer-app",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Nuxt-style meta-framework for @jasonshimmy/custom-elements-runtime",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -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
- handler(nodeReq, res).catch(() => {
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
- handler(nodeReq, res).catch(() => {
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 handler(req, res)
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