@pyreon/zero 0.15.0 → 0.18.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/lib/{api-routes-DANluJic.js → api-routes-Ci0kVmM4.js} +2 -2
- package/lib/client.js +4 -1
- package/lib/env.js +6 -6
- package/lib/font.js +3 -3
- package/lib/{fs-router-ZebyutPa.js → fs-router-MewHc5SB.js} +25 -30
- package/lib/i18n-routing.js +112 -1
- package/lib/image.js +140 -58
- package/lib/index.js +252 -82
- package/lib/og-image.js +5 -5
- package/lib/rolldown-runtime-CjeV3_4I.js +18 -0
- package/lib/script.js +114 -25
- package/lib/seo.js +186 -15
- package/lib/server.js +274 -564
- package/lib/types/config.d.ts +307 -3
- package/lib/types/env.d.ts +2 -2
- package/lib/types/i18n-routing.d.ts +193 -2
- package/lib/types/image.d.ts +105 -5
- package/lib/types/index.d.ts +666 -182
- package/lib/types/script.d.ts +78 -6
- package/lib/types/seo.d.ts +128 -4
- package/lib/types/server.d.ts +607 -72
- package/lib/vite-plugin-y0NmCLJA.js +2476 -0
- package/package.json +11 -10
- package/src/adapters/bun.ts +20 -1
- package/src/adapters/cloudflare.ts +78 -1
- package/src/adapters/index.ts +25 -3
- package/src/adapters/netlify.ts +63 -1
- package/src/adapters/node.ts +25 -1
- package/src/adapters/static.ts +26 -1
- package/src/adapters/validate.ts +8 -1
- package/src/adapters/vercel.ts +76 -1
- package/src/adapters/warn-missing-env.ts +49 -0
- package/src/app.ts +14 -0
- package/src/client.ts +18 -0
- package/src/entry-server.ts +55 -5
- package/src/env.ts +7 -7
- package/src/font.ts +3 -3
- package/src/fs-router.ts +72 -3
- package/src/i18n-routing.ts +246 -12
- package/src/image.tsx +242 -91
- package/src/index.ts +4 -4
- package/src/isr.ts +24 -6
- package/src/manifest.ts +675 -0
- package/src/og-image.ts +5 -5
- package/src/script.tsx +159 -36
- package/src/seo.ts +346 -15
- package/src/server.ts +10 -2
- package/src/ssg-plugin.ts +1211 -54
- package/src/types.ts +333 -10
- package/src/vercel-revalidate-handler.ts +204 -0
- package/src/vite-plugin.ts +171 -41
- package/lib/vite-plugin-E4BHYvYW.js +0 -855
package/src/image.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { VNodeChild } from '@pyreon/core'
|
|
1
|
+
import type { Ref, VNodeChild } from '@pyreon/core'
|
|
2
2
|
import { createRef } from '@pyreon/core'
|
|
3
3
|
import { signal } from '@pyreon/reactivity'
|
|
4
4
|
import type { FormatSource } from './image-plugin'
|
|
@@ -13,6 +13,12 @@ import { useIntersectionObserver } from './utils/use-intersection-observer'
|
|
|
13
13
|
// - Multi-format support via <picture> (WebP/AVIF with fallback)
|
|
14
14
|
// - Blur-up placeholder while loading
|
|
15
15
|
// - Priority loading for above-the-fold images
|
|
16
|
+
//
|
|
17
|
+
// Three levels of API (mirrors @pyreon/zero/link):
|
|
18
|
+
//
|
|
19
|
+
// 1. useImage(props) — composable returning resolved attributes + signals
|
|
20
|
+
// 2. createImage(Comp) — HOC wrapping any component with image optimization
|
|
21
|
+
// 3. Image — default <div><img/></div> wrapper (built on createImage)
|
|
16
22
|
|
|
17
23
|
export interface ImageProps {
|
|
18
24
|
/** Image source URL. */
|
|
@@ -47,6 +53,9 @@ export interface ImageProps {
|
|
|
47
53
|
* Raw mode — renders a plain `<img>` without the container div,
|
|
48
54
|
* aspect-ratio, max-width, or lazy loading wrapper.
|
|
49
55
|
* Use when the Image is inside a custom layout (absolute positioning, etc.).
|
|
56
|
+
*
|
|
57
|
+
* Note: `raw` skips the three-layer API entirely. `useImage` / `createImage`
|
|
58
|
+
* do not apply when `raw: true` — the component returns a bare `<img>`.
|
|
50
59
|
*/
|
|
51
60
|
raw?: boolean
|
|
52
61
|
}
|
|
@@ -56,37 +65,83 @@ export interface ImageSource {
|
|
|
56
65
|
width: number
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
/** Return type of {@link useImage}. */
|
|
69
|
+
export interface UseImageReturn {
|
|
70
|
+
/** Ref — attach to the container element for IntersectionObserver. */
|
|
71
|
+
containerRef: Ref<HTMLElement>
|
|
72
|
+
/** Whether the image has entered the viewport (and started loading). */
|
|
73
|
+
inView: () => boolean
|
|
74
|
+
/** Whether the `<img>` onLoad has fired. */
|
|
75
|
+
loaded: () => boolean
|
|
76
|
+
/** Resolved `src` accessor — empty string until inView, then `props.src`. */
|
|
77
|
+
src: () => string
|
|
78
|
+
/** Resolved srcSet accessor — empty until inView; empty when `formats` is set (srcset moves to `<source>` elements). */
|
|
79
|
+
srcSet: () => string
|
|
80
|
+
/** `sizes` attribute or undefined when no srcset. */
|
|
81
|
+
sizes: string | undefined
|
|
82
|
+
/** `aspect-ratio` CSS value (`"${width} / ${height}"`). */
|
|
83
|
+
aspectRatio: string
|
|
84
|
+
/** Resolved CSS for the container — position + overflow + aspect-ratio + max-width + caller's `style`. */
|
|
85
|
+
containerStyle: string
|
|
86
|
+
/** Resolved CSS accessor for the `<img>` — fit + transition + opacity (placeholder fade). */
|
|
87
|
+
imageStyle: () => string
|
|
88
|
+
/** Resolved CSS accessor for the placeholder `<img>` (only meaningful when `placeholder` is set). */
|
|
89
|
+
placeholderStyle: () => string
|
|
90
|
+
/** `loading` attribute — eager when priority/eager, else lazy. */
|
|
91
|
+
loading: 'lazy' | 'eager'
|
|
92
|
+
/** `fetchPriority` — 'high' when priority, else undefined. */
|
|
93
|
+
fetchPriority: 'high' | undefined
|
|
94
|
+
/** onLoad handler — sets the loaded signal. Wire into the rendered `<img>`. */
|
|
95
|
+
handleLoad: () => void
|
|
96
|
+
/** Resolved per-format <source> descriptors (or undefined when no formats). */
|
|
97
|
+
formats: FormatSource[] | undefined
|
|
98
|
+
/** Whether `formats` is non-empty (i.e. consumer should render a `<picture>` wrapper). */
|
|
99
|
+
hasFormats: boolean
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Props passed to a custom component via {@link createImage}. */
|
|
103
|
+
export interface ImageRenderProps {
|
|
104
|
+
/** Container ref. */
|
|
105
|
+
containerRef: Ref<HTMLElement>
|
|
106
|
+
/** CSS class for the container. */
|
|
107
|
+
class: string | undefined
|
|
108
|
+
/** Resolved container `style` string. */
|
|
109
|
+
containerStyle: string
|
|
110
|
+
/** Pre-rendered placeholder `<img>` (or `null` when `placeholder` is unset). */
|
|
111
|
+
placeholder: VNodeChild
|
|
112
|
+
/** Pre-rendered image — either a bare `<img>` or a `<picture>` tree when `formats` is set. */
|
|
113
|
+
image: VNodeChild
|
|
114
|
+
}
|
|
115
|
+
|
|
59
116
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
117
|
+
* Composable that provides all image optimization behavior — lazy loading,
|
|
118
|
+
* srcset/sizes resolution, format selection, blur-placeholder state,
|
|
119
|
+
* load tracking.
|
|
62
120
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* <Image {...hero} alt="Hero" priority />
|
|
121
|
+
* Use this for full control when `createImage` is too opinionated about
|
|
122
|
+
* the surrounding markup (e.g. custom container layouts, non-`<div>`
|
|
123
|
+
* wrappers, additional overlay elements).
|
|
67
124
|
*
|
|
68
125
|
* @example
|
|
69
|
-
*
|
|
70
|
-
*
|
|
126
|
+
* function MyImage(props: ImageProps) {
|
|
127
|
+
* const img = useImage(props)
|
|
128
|
+
* return (
|
|
129
|
+
* <figure ref={img.containerRef} style={img.containerStyle}>
|
|
130
|
+
* <img
|
|
131
|
+
* src={img.src}
|
|
132
|
+
* srcSet={img.srcSet}
|
|
133
|
+
* sizes={img.sizes}
|
|
134
|
+
* alt={props.alt}
|
|
135
|
+
* loading={img.loading}
|
|
136
|
+
* onLoad={img.handleLoad}
|
|
137
|
+
* style={img.imageStyle}
|
|
138
|
+
* />
|
|
139
|
+
* <figcaption>{props.alt}</figcaption>
|
|
140
|
+
* </figure>
|
|
141
|
+
* )
|
|
142
|
+
* }
|
|
71
143
|
*/
|
|
72
|
-
export function
|
|
73
|
-
// Raw mode: plain <img> without container, lazy loading, or layout constraints
|
|
74
|
-
if (props.raw) {
|
|
75
|
-
return (
|
|
76
|
-
<img
|
|
77
|
-
src={props.src}
|
|
78
|
-
alt={props.alt}
|
|
79
|
-
width={props.width}
|
|
80
|
-
height={props.height}
|
|
81
|
-
class={props.class}
|
|
82
|
-
style={props.style}
|
|
83
|
-
decoding={props.decoding ?? 'async'}
|
|
84
|
-
loading={props.loading ?? 'lazy'}
|
|
85
|
-
fetchPriority={props.priority ? 'high' : undefined}
|
|
86
|
-
/>
|
|
87
|
-
) as any
|
|
88
|
-
}
|
|
89
|
-
|
|
144
|
+
export function useImage(props: ImageProps): UseImageReturn {
|
|
90
145
|
const isEager = props.priority || props.loading === 'eager'
|
|
91
146
|
const loaded = signal(isEager)
|
|
92
147
|
const inView = signal(isEager)
|
|
@@ -100,7 +155,7 @@ export function Image(props: ImageProps): VNodeChild {
|
|
|
100
155
|
|
|
101
156
|
const sizes = props.sizes ?? '100vw'
|
|
102
157
|
const fit = props.fit ?? 'cover'
|
|
103
|
-
const hasFormats = props.formats && props.formats.length > 0
|
|
158
|
+
const hasFormats = !!(props.formats && props.formats.length > 0)
|
|
104
159
|
const aspectRatio = `${props.width} / ${props.height}`
|
|
105
160
|
|
|
106
161
|
if (!isEager) {
|
|
@@ -110,7 +165,6 @@ export function Image(props: ImageProps): VNodeChild {
|
|
|
110
165
|
)
|
|
111
166
|
}
|
|
112
167
|
|
|
113
|
-
// Static styles (don't depend on signals)
|
|
114
168
|
const containerStyle = [
|
|
115
169
|
'position: relative',
|
|
116
170
|
'overflow: hidden',
|
|
@@ -122,68 +176,165 @@ export function Image(props: ImageProps): VNodeChild {
|
|
|
122
176
|
.filter(Boolean)
|
|
123
177
|
.join('; ')
|
|
124
178
|
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
179
|
+
const imageStyle = () =>
|
|
180
|
+
[
|
|
181
|
+
'display: block',
|
|
182
|
+
'width: 100%',
|
|
183
|
+
'height: 100%',
|
|
184
|
+
`object-fit: ${fit}`,
|
|
185
|
+
'transition: opacity 0.3s ease',
|
|
186
|
+
props.placeholder && !loaded() ? 'opacity: 0' : 'opacity: 1',
|
|
187
|
+
].join('; ')
|
|
188
|
+
|
|
189
|
+
const placeholderStyle = () =>
|
|
190
|
+
[
|
|
191
|
+
'position: absolute',
|
|
192
|
+
'inset: 0',
|
|
193
|
+
'width: 100%',
|
|
194
|
+
'height: 100%',
|
|
195
|
+
'object-fit: cover',
|
|
196
|
+
'filter: blur(20px)',
|
|
197
|
+
'transform: scale(1.1)',
|
|
198
|
+
'transition: opacity 0.4s ease',
|
|
199
|
+
loaded() ? 'opacity: 0; pointer-events: none' : 'opacity: 1',
|
|
200
|
+
].join('; ')
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
containerRef,
|
|
204
|
+
inView,
|
|
205
|
+
loaded,
|
|
206
|
+
src: () => (inView() ? props.src : ''),
|
|
207
|
+
srcSet: () => (!hasFormats && inView() && resolvedSrcset ? resolvedSrcset : ''),
|
|
208
|
+
sizes: resolvedSrcset ? sizes : undefined,
|
|
209
|
+
aspectRatio,
|
|
210
|
+
containerStyle,
|
|
211
|
+
imageStyle,
|
|
212
|
+
placeholderStyle,
|
|
213
|
+
loading: isEager ? 'eager' : 'lazy',
|
|
214
|
+
fetchPriority: props.priority ? 'high' : undefined,
|
|
215
|
+
handleLoad: () => loaded.set(true),
|
|
216
|
+
formats: props.formats,
|
|
217
|
+
hasFormats,
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Higher-order component that wraps any component with image optimization.
|
|
223
|
+
*
|
|
224
|
+
* The wrapped component receives {@link ImageRenderProps} with the pre-rendered
|
|
225
|
+
* `image` JSX (bare `<img>` OR `<picture>` tree depending on formats), the
|
|
226
|
+
* pre-rendered `placeholder` JSX, and the container ref + styles. Consumers
|
|
227
|
+
* compose those pieces with whatever wrapper element / layout they want.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* // Custom figure-based image with caption
|
|
231
|
+
* const FigureImage = createImage((props) => (
|
|
232
|
+
* <figure ref={props.containerRef} class={props.class} style={props.containerStyle}>
|
|
233
|
+
* {props.placeholder}
|
|
234
|
+
* {props.image}
|
|
235
|
+
* <figcaption>Caption goes here</figcaption>
|
|
236
|
+
* </figure>
|
|
237
|
+
* ))
|
|
238
|
+
*
|
|
239
|
+
* // Usage — identical to default <Image>
|
|
240
|
+
* <FigureImage src="/hero.jpg" alt="Hero" width={1200} height={630} />
|
|
241
|
+
*/
|
|
242
|
+
export function createImage(
|
|
243
|
+
Component: (p: ImageRenderProps) => any,
|
|
244
|
+
): (props: ImageProps) => any {
|
|
245
|
+
return function WrappedImage(props: ImageProps) {
|
|
246
|
+
// `raw` mode short-circuits — returns a bare <img> with no optimization
|
|
247
|
+
// wrapper, no container, no createImage composition. Documented as the
|
|
248
|
+
// no-optimization escape hatch.
|
|
249
|
+
if (props.raw) {
|
|
250
|
+
return (
|
|
153
251
|
<img
|
|
154
|
-
src={props.
|
|
155
|
-
alt=
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
'height: 100%',
|
|
164
|
-
'object-fit: cover',
|
|
165
|
-
'filter: blur(20px)',
|
|
166
|
-
'transform: scale(1.1)',
|
|
167
|
-
'transition: opacity 0.4s ease',
|
|
168
|
-
loaded() ? 'opacity: 0; pointer-events: none' : 'opacity: 1',
|
|
169
|
-
].join('; ')
|
|
170
|
-
}
|
|
252
|
+
src={props.src}
|
|
253
|
+
alt={props.alt}
|
|
254
|
+
width={props.width}
|
|
255
|
+
height={props.height}
|
|
256
|
+
class={props.class}
|
|
257
|
+
style={props.style}
|
|
258
|
+
decoding={props.decoding ?? 'async'}
|
|
259
|
+
loading={props.loading ?? 'lazy'}
|
|
260
|
+
fetchPriority={props.priority ? 'high' : undefined}
|
|
171
261
|
/>
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const img = useImage(props)
|
|
266
|
+
|
|
267
|
+
const imgEl = (
|
|
268
|
+
<img
|
|
269
|
+
src={img.src}
|
|
270
|
+
srcSet={img.srcSet}
|
|
271
|
+
sizes={img.sizes}
|
|
272
|
+
alt={props.alt}
|
|
273
|
+
width={props.width}
|
|
274
|
+
height={props.height}
|
|
275
|
+
loading={img.loading}
|
|
276
|
+
decoding={props.decoding ?? 'async'}
|
|
277
|
+
fetchPriority={img.fetchPriority}
|
|
278
|
+
onLoad={img.handleLoad}
|
|
279
|
+
style={img.imageStyle}
|
|
280
|
+
/>
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
const placeholderEl = props.placeholder
|
|
284
|
+
? (
|
|
285
|
+
<img
|
|
286
|
+
src={props.placeholder}
|
|
287
|
+
alt=""
|
|
288
|
+
aria-hidden="true"
|
|
289
|
+
loading="eager"
|
|
290
|
+
style={img.placeholderStyle}
|
|
291
|
+
/>
|
|
292
|
+
)
|
|
293
|
+
: null
|
|
294
|
+
|
|
295
|
+
const imageEl = img.hasFormats
|
|
296
|
+
? (
|
|
297
|
+
<picture>
|
|
298
|
+
{img.formats?.map((fmt) => (
|
|
299
|
+
<source
|
|
300
|
+
type={fmt.type}
|
|
301
|
+
srcSet={() => (img.inView() ? (fmt.srcset ?? '') : '')}
|
|
302
|
+
sizes={img.sizes}
|
|
303
|
+
/>
|
|
304
|
+
))}
|
|
305
|
+
{imgEl}
|
|
306
|
+
</picture>
|
|
307
|
+
)
|
|
308
|
+
: imgEl
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<Component
|
|
312
|
+
containerRef={img.containerRef}
|
|
313
|
+
class={props.class}
|
|
314
|
+
containerStyle={img.containerStyle}
|
|
315
|
+
placeholder={placeholderEl}
|
|
316
|
+
image={imageEl}
|
|
317
|
+
/>
|
|
318
|
+
)
|
|
319
|
+
}
|
|
189
320
|
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Default optimized image component with lazy loading, responsive srcset,
|
|
324
|
+
* `<picture>` multi-format support, and blur-up placeholders.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* // With imagePlugin — spread the import directly
|
|
328
|
+
* import hero from "./hero.jpg?optimize"
|
|
329
|
+
* <Image {...hero} alt="Hero" priority />
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* // Manual usage
|
|
333
|
+
* <Image src="/hero.jpg" alt="Hero" width={1200} height={630} />
|
|
334
|
+
*/
|
|
335
|
+
export const Image: (props: ImageProps) => any = createImage((props) => (
|
|
336
|
+
<div ref={props.containerRef} class={props.class} style={props.containerStyle}>
|
|
337
|
+
{props.placeholder}
|
|
338
|
+
{props.image}
|
|
339
|
+
</div>
|
|
340
|
+
))
|
package/src/index.ts
CHANGED
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
|
|
14
14
|
// ─── Components (browser-safe) ──────────────────────────────────────────────
|
|
15
15
|
|
|
16
|
-
export type { ImageProps, ImageSource } from "./image";
|
|
17
|
-
export { Image } from "./image";
|
|
16
|
+
export type { ImageProps, ImageRenderProps, ImageSource, UseImageReturn } from "./image";
|
|
17
|
+
export { createImage, Image, useImage } from "./image";
|
|
18
18
|
export type { LinkProps, LinkRenderProps, UseLinkReturn } from "./link";
|
|
19
19
|
export { createLink, Link, prefetchRoute, useLink } from "./link";
|
|
20
|
-
export type { ScriptProps, ScriptStrategy } from "./script";
|
|
21
|
-
export { Script } from "./script";
|
|
20
|
+
export type { ScriptProps, ScriptRenderProps, ScriptStrategy, UseScriptReturn } from "./script";
|
|
21
|
+
export { createScript, Script, useScript } from "./script";
|
|
22
22
|
export type { MetaProps } from "./meta";
|
|
23
23
|
export { buildMetaTags, Meta } from "./meta";
|
|
24
24
|
|
package/src/isr.ts
CHANGED
|
@@ -29,6 +29,14 @@ export function createISRHandler(
|
|
|
29
29
|
const revalidating = new Set<string>()
|
|
30
30
|
const revalidateMs = config.revalidate * 1000
|
|
31
31
|
const maxEntries = Math.max(1, config.maxEntries ?? 1000)
|
|
32
|
+
// M1.1 — cache-key derivation. Default keys by pathname only (the
|
|
33
|
+
// pre-M1 behaviour). User-supplied `cacheKey` opts in to varying
|
|
34
|
+
// by cookies / query / headers — required for auth-gated pages.
|
|
35
|
+
// See `ISRConfig.cacheKey` JSDoc for the auth-incompatibility caveat.
|
|
36
|
+
const deriveKey: (req: Request, url: URL) => string
|
|
37
|
+
= typeof config.cacheKey === 'function'
|
|
38
|
+
? (req, _url) => (config.cacheKey as (r: Request) => string)(req)
|
|
39
|
+
: (_req, url) => url.pathname
|
|
32
40
|
|
|
33
41
|
function set(key: string, entry: CacheEntry): void {
|
|
34
42
|
// LRU: re-inserting moves the key to the newest position. Then if we're
|
|
@@ -51,13 +59,23 @@ export function createISRHandler(
|
|
|
51
59
|
return entry
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
async function revalidate(url: URL) {
|
|
55
|
-
|
|
62
|
+
async function revalidate(url: URL, originalReq: Request) {
|
|
63
|
+
// Re-derive key from the ORIGINAL request so cookies / headers /
|
|
64
|
+
// query that varied the cache entry are preserved across revalidation.
|
|
65
|
+
// Without this, a user-supplied `cacheKey` that reads cookies would
|
|
66
|
+
// re-render against a no-cookie request and stomp the cached entry
|
|
67
|
+
// with the wrong-user content.
|
|
68
|
+
const key = deriveKey(originalReq, url)
|
|
56
69
|
if (revalidating.has(key)) return
|
|
57
70
|
revalidating.add(key)
|
|
58
71
|
|
|
59
72
|
try {
|
|
60
|
-
|
|
73
|
+
// Forward the original request shape (headers + method) so the
|
|
74
|
+
// re-render sees the same auth context as the user's read.
|
|
75
|
+
const req = new Request(url.href, {
|
|
76
|
+
method: 'GET',
|
|
77
|
+
headers: originalReq.headers,
|
|
78
|
+
})
|
|
61
79
|
const res = await handler(req)
|
|
62
80
|
const html = await res.text()
|
|
63
81
|
const headers: Record<string, string> = {}
|
|
@@ -80,7 +98,7 @@ export function createISRHandler(
|
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
const url = new URL(req.url)
|
|
83
|
-
const key = url
|
|
101
|
+
const key = deriveKey(req, url)
|
|
84
102
|
// `touch` moves the entry to the newest LRU position on read so
|
|
85
103
|
// hot paths survive eviction even when the cap is small. `get`
|
|
86
104
|
// wouldn't update ordering.
|
|
@@ -91,7 +109,7 @@ export function createISRHandler(
|
|
|
91
109
|
|
|
92
110
|
if (age > revalidateMs) {
|
|
93
111
|
// Stale — serve cached but revalidate in background
|
|
94
|
-
revalidate(url)
|
|
112
|
+
revalidate(url, req)
|
|
95
113
|
}
|
|
96
114
|
|
|
97
115
|
return new Response(entry.html, {
|
|
@@ -113,7 +131,7 @@ export function createISRHandler(
|
|
|
113
131
|
headers[k] = v
|
|
114
132
|
})
|
|
115
133
|
|
|
116
|
-
|
|
134
|
+
set(key, { html, headers, timestamp: Date.now() })
|
|
117
135
|
|
|
118
136
|
return new Response(html, {
|
|
119
137
|
status: 200,
|