@vinext/cloudflare 1.2.0 → 1.3.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/dist/cache/app-page-parts.d.ts +13 -0
- package/dist/cache/app-page-parts.js +18 -0
- package/dist/cache/cdn-adapter-config.d.ts +2 -1
- package/dist/cache/cdn-adapter.runtime.d.ts +1 -0
- package/dist/cache/cdn-adapter.runtime.js +1 -0
- package/dist/cache/cdn-adapter.worker.js +222 -34
- package/dist/cache/config.d.ts +2 -1
- package/dist/cache/kv-data-adapter.runtime.d.ts +7 -3
- package/dist/cache/kv-data-adapter.runtime.js +62 -15
- package/dist/cache/response-store-adapter.worker.d.ts +2 -1
- package/dist/cache/response-store-adapter.worker.js +163 -53
- package/dist/cache/response-store-cdn.runtime.js +4 -1
- package/dist/cache/response-store-data.runtime.d.ts +12 -0
- package/dist/cache/response-store-data.runtime.js +51 -12
- package/dist/cache/static-assets-adapter.runtime.d.ts +2 -1
- package/dist/cache/workers-cache-invalidation.d.ts +2 -1
- package/dist/cdn-warm.d.ts +2 -1
- package/dist/cli.js +2 -0
- package/dist/deploy-config.d.ts +4 -4
- package/dist/deploy-config.js +10 -7
- package/dist/deploy-help.js +5 -3
- package/dist/deploy.d.ts +5 -1
- package/dist/deploy.js +24 -10
- package/dist/previews-deploy.d.ts +1 -0
- package/dist/previews-deploy.js +4 -2
- package/dist/utils/cache-control-metadata.d.ts +5 -0
- package/dist/utils/cache-control-metadata.js +9 -1
- package/dist/version-headers.js +1 -2
- package/dist/wrangler-config.d.ts +2 -1
- package/package.json +7 -8
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { hasDocumentPart, isCanonicalAppPage } from "./app-page-parts.js";
|
|
1
2
|
import { SHARED_RESPONSE_STAGE_HEADER, finalizeGatewayResponse } from "./browser-cache-policy.js";
|
|
2
3
|
import { captureResponseStoreDataRegeneration, runWithResponseStoreInvocation, setResponseStore } from "./response-store-data.runtime.js";
|
|
3
|
-
import { VINEXT_PARAMS_HEADER, VINEXT_RENDERED_PATH_AND_SEARCH_HEADER, VINEXT_SPECIAL_ERROR_STATUS_HEADER } from "vinext/internal/server/headers";
|
|
4
|
-
import { VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_VARY_HEADER as VINEXT_RSC_VARY_HEADER$1, applyRscCompatibilityIdHeader, applyRscDeploymentIdHeader, createCanonicalRscRequestHeaders, createCanonicalRscRequestUrl } from "vinext/internal/server/app-rsc-cache-busting";
|
|
4
|
+
import { NEXTJS_DEPLOYMENT_ID_HEADER, VINEXT_PARAMS_HEADER, VINEXT_RENDERED_PATH_AND_SEARCH_HEADER, VINEXT_RSC_COMPLETION_METADATA_HEADER, VINEXT_SPECIAL_ERROR_STATUS_HEADER, VINEXT_STALE_TIME_PENDING_HEADER } from "vinext/internal/server/headers";
|
|
5
|
+
import { VINEXT_RSC_BUILD_ID_HEADER, VINEXT_RSC_COMPATIBILITY_ID_HEADER, VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_VARY_HEADER as VINEXT_RSC_VARY_HEADER$1, applyRscCompatibilityIdHeader, applyRscDeploymentIdHeader, createCanonicalRscRequestHeaders, createCanonicalRscRequestUrl, stripRscCacheBustingSearchParam } from "vinext/internal/server/app-rsc-cache-busting";
|
|
5
6
|
import { hasSharedCacheCredentials, isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
|
|
6
7
|
import { loadVinextRequestStage } from "vinext/server/request-stage";
|
|
7
8
|
import { loadVinextResponseStage } from "vinext/server/response-stage";
|
|
@@ -9,6 +10,8 @@ import { traceCachedResponseStart } from "vinext/internal/server/response-start-
|
|
|
9
10
|
import { isPageNotModified } from "vinext/internal/server/http-conditional";
|
|
10
11
|
//#region src/cache/response-store-adapter.worker.ts
|
|
11
12
|
const ROUTE_REVALIDATOR_ID = "vinext:response";
|
|
13
|
+
/** Leads the revalidator args of a page part whose render stores both parts. */
|
|
14
|
+
const PAGE_PARTS_REVALIDATOR_ARG = "vinext:page-parts";
|
|
12
15
|
const RESPONSE_STORE_KEY_PARAM = "__workers_response_store";
|
|
13
16
|
const AGE_BASIS_HEADER = "X-Workers-Response-Store-Age-Basis";
|
|
14
17
|
const WARMUP_USER_AGENT = "vinext-cloudflare-cdn-warm";
|
|
@@ -58,6 +61,15 @@ function prepareInvocation(request, props) {
|
|
|
58
61
|
function serializeInvocation(request, props) {
|
|
59
62
|
return JSON.stringify(prepareInvocation(request, props));
|
|
60
63
|
}
|
|
64
|
+
function serializeDispatchInvocation(invocation) {
|
|
65
|
+
return JSON.stringify({
|
|
66
|
+
...invocation,
|
|
67
|
+
request: {
|
|
68
|
+
...invocation.request,
|
|
69
|
+
headers: [...invocation.request.headers, ["host", new URL(invocation.request.url).host]]
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
61
73
|
function parseInvocation(value) {
|
|
62
74
|
if (typeof value !== "string") throw new TypeError("Invalid vinext response-store invocation");
|
|
63
75
|
const invocation = JSON.parse(value);
|
|
@@ -95,16 +107,29 @@ function createVinextResponseStoreOptions(configuration) {
|
|
|
95
107
|
async regenerate(input, { env, ctx }) {
|
|
96
108
|
if (input.id === ROUTE_REVALIDATOR_ID) {
|
|
97
109
|
const invocation = parseInvocation(input.args.at(-1));
|
|
98
|
-
const
|
|
110
|
+
const request = restoreRequest(invocation);
|
|
111
|
+
const otherPart = input.args.length > 1 && input.args[0] === PAGE_PARTS_REVALIDATOR_ARG ? otherPagePart(request, invocation.props) : void 0;
|
|
112
|
+
const capture = otherPagePartCapture(otherPart);
|
|
113
|
+
const response = await invokeResponseStage(request, invocation.props, env, ctx, {
|
|
99
114
|
cache: "shared",
|
|
100
115
|
regeneratesStoredEntry: true,
|
|
101
116
|
renderWholeDocument: true
|
|
102
|
-
});
|
|
117
|
+
}, capture);
|
|
103
118
|
if (!isCacheable(response)) {
|
|
104
119
|
await response.body?.cancel().catch(() => {});
|
|
120
|
+
capture.rscData?.catch(() => {});
|
|
105
121
|
throw new Error("Vinext response-stage regeneration was not cacheable");
|
|
106
122
|
}
|
|
107
|
-
|
|
123
|
+
const stored = withoutRequestScopedHeaders(response, invocation.props);
|
|
124
|
+
try {
|
|
125
|
+
await putOtherPagePart(otherPart, capture, stored);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(JSON.stringify({
|
|
128
|
+
message: "Vinext response-store regeneration failed to store the page's other part",
|
|
129
|
+
error: error instanceof Error ? error.message : String(error)
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
return stored;
|
|
108
133
|
}
|
|
109
134
|
if (input.id === "vinext:cache-function" && typeof input.args[0] === "string" && typeof input.args[1] === "string") {
|
|
110
135
|
const invocation = JSON.parse(input.args[1]);
|
|
@@ -137,7 +162,8 @@ async function cacheRequest(invocation) {
|
|
|
137
162
|
url.searchParams.set(RESPONSE_STORE_KEY_PARAM, `v1.${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`);
|
|
138
163
|
return new Request(url, {
|
|
139
164
|
headers: CACHE_REQUEST_VARY_HEADERS,
|
|
140
|
-
method: "GET"
|
|
165
|
+
method: "GET",
|
|
166
|
+
redirect: "manual"
|
|
141
167
|
});
|
|
142
168
|
}
|
|
143
169
|
/**
|
|
@@ -157,6 +183,116 @@ function withoutRequestScopedHeaders(response, responseStageProps) {
|
|
|
157
183
|
statusText: response.statusText
|
|
158
184
|
});
|
|
159
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* The revalidator of a page part. One whose render stores the page's other
|
|
188
|
+
* part says so, as a public config policy's full-URL entry stores its own.
|
|
189
|
+
*/
|
|
190
|
+
function routeRevalidator(serializedInvocation, storesOtherPart) {
|
|
191
|
+
return {
|
|
192
|
+
id: ROUTE_REVALIDATOR_ID,
|
|
193
|
+
args: storesOtherPart ? [PAGE_PARTS_REVALIDATOR_ARG, serializedInvocation] : [serializedInvocation]
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** The invocation an RSC navigation to an HTML invocation's page stores under. */
|
|
197
|
+
function rscPartInvocation(htmlRequest, htmlProps) {
|
|
198
|
+
return prepareInvocation(new Request(new URL(createCanonicalRscRequestUrl(htmlRequest.url), htmlRequest.url), { headers: createCanonicalRscRequestHeaders() }), {
|
|
199
|
+
...htmlProps,
|
|
200
|
+
isRscRequest: true,
|
|
201
|
+
renderMode: "navigation"
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
/** The invocation an HTML request for an RSC invocation's page stores under. */
|
|
205
|
+
function documentPartInvocation(rscRequest, rscProps) {
|
|
206
|
+
const url = new URL(rscRequest.url);
|
|
207
|
+
stripRscCacheBustingSearchParam(url);
|
|
208
|
+
return prepareInvocation(new Request(url), {
|
|
209
|
+
...rscProps,
|
|
210
|
+
isRscRequest: false
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The page's RSC payload an HTML render produced, with its document's headers
|
|
215
|
+
* and status.
|
|
216
|
+
*/
|
|
217
|
+
function rscPartResponse(htmlResponse, rscData) {
|
|
218
|
+
const headers = new Headers(htmlResponse.headers);
|
|
219
|
+
headers.delete("Content-Length");
|
|
220
|
+
headers.delete("Link");
|
|
221
|
+
headers.delete("X-Vinext-Response-Store-Replayable");
|
|
222
|
+
headers.delete(VINEXT_PARAMS_HEADER);
|
|
223
|
+
headers.delete(VINEXT_RENDERED_PATH_AND_SEARCH_HEADER);
|
|
224
|
+
headers.set("Content-Type", VINEXT_RSC_CONTENT_TYPE);
|
|
225
|
+
headers.set("Vary", VINEXT_RSC_VARY_HEADER$1);
|
|
226
|
+
applyRscCompatibilityIdHeader(headers);
|
|
227
|
+
applyRscDeploymentIdHeader(headers);
|
|
228
|
+
if (isStoredErrorStatus(htmlResponse.status)) headers.set(VINEXT_SPECIAL_ERROR_STATUS_HEADER, String(htmlResponse.status));
|
|
229
|
+
return new Response(rscData, {
|
|
230
|
+
headers,
|
|
231
|
+
status: htmlResponse.status >= 300 && htmlResponse.status < 400 ? 200 : htmlResponse.status
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The page's document an RSC render produced, with the headers an HTML request
|
|
236
|
+
* for the page stores: the RSC response's, without the payload's own.
|
|
237
|
+
*/
|
|
238
|
+
function documentPartResponse(rscResponse, document) {
|
|
239
|
+
const headers = new Headers(rscResponse.headers);
|
|
240
|
+
for (const name of [
|
|
241
|
+
"Content-Length",
|
|
242
|
+
NEXTJS_DEPLOYMENT_ID_HEADER,
|
|
243
|
+
VINEXT_RSC_BUILD_ID_HEADER,
|
|
244
|
+
VINEXT_RSC_COMPATIBILITY_ID_HEADER,
|
|
245
|
+
VINEXT_RSC_COMPLETION_METADATA_HEADER,
|
|
246
|
+
VINEXT_STALE_TIME_PENDING_HEADER
|
|
247
|
+
]) headers.delete(name);
|
|
248
|
+
headers.set("Content-Type", "text/html; charset=utf-8");
|
|
249
|
+
if (document.linkHeader) headers.append("Link", document.linkHeader);
|
|
250
|
+
return new Response(document.html, {
|
|
251
|
+
headers,
|
|
252
|
+
status: rscResponse.status
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The other part of the page a canonical App page render stores. Next.js
|
|
257
|
+
* keeps a page's document and RSC payload in one entry, so a render that
|
|
258
|
+
* stores one part also stores the other, under the key a request for that
|
|
259
|
+
* part reads. A variant has no other part.
|
|
260
|
+
*/
|
|
261
|
+
function otherPagePart(identityRequest, identityProps) {
|
|
262
|
+
if (isCanonicalAppPage(identityProps, false)) return {
|
|
263
|
+
invocation: rscPartInvocation(identityRequest, identityProps),
|
|
264
|
+
kind: "rsc"
|
|
265
|
+
};
|
|
266
|
+
if (isCanonicalAppPage(identityProps, true) && hasDocumentPart(identityRequest.url)) return {
|
|
267
|
+
invocation: documentPartInvocation(identityRequest, identityProps),
|
|
268
|
+
kind: "document"
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
/** What a render captures for the other part of its page. */
|
|
272
|
+
function otherPagePartCapture(part) {
|
|
273
|
+
if (part?.kind === "rsc") return { captureRscData: true };
|
|
274
|
+
if (part?.kind === "document") return { captureDocument: true };
|
|
275
|
+
return {};
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Store the other part of the page from the render that stores this one.
|
|
279
|
+
* Admission settles the document before it completes the response. A miss
|
|
280
|
+
* stores it first, so a failed write leaves the render's own part missing too
|
|
281
|
+
* and the next miss retries both. A regeneration's other part stays stale
|
|
282
|
+
* when it fails, so the next read of that part regenerates it. Concurrent
|
|
283
|
+
* misses coalesce each key on its own, so their parts can come from different
|
|
284
|
+
* renders of the page.
|
|
285
|
+
*/
|
|
286
|
+
async function putOtherPagePart(part, capture, stored) {
|
|
287
|
+
let response;
|
|
288
|
+
if (part?.kind === "rsc" && capture.rscData) response = rscPartResponse(stored, await capture.rscData);
|
|
289
|
+
else if (part?.kind === "document" && capture.document) response = documentPartResponse(stored, capture.document);
|
|
290
|
+
else return;
|
|
291
|
+
if (!(await responseStore.put(await cacheRequest(part.invocation), response, {
|
|
292
|
+
coalesce: true,
|
|
293
|
+
revalidator: routeRevalidator(JSON.stringify(part.invocation), true)
|
|
294
|
+
})).backingStoreUpdated) throw new Error(`Vinext response-store did not publish the App page ${part.kind} part`);
|
|
295
|
+
}
|
|
160
296
|
function isStoredErrorStatus(status) {
|
|
161
297
|
return status === 401 || status === 403 || status === 404;
|
|
162
298
|
}
|
|
@@ -275,17 +411,9 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
275
411
|
const identityRequest = options.cacheIdentity?.request ?? stageRequest;
|
|
276
412
|
const identityProps = options.cacheIdentity?.props ?? props;
|
|
277
413
|
const isWarmup = request.headers.get("user-agent") === WARMUP_USER_AGENT;
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
...identityProps,
|
|
281
|
-
isRscRequest: true,
|
|
282
|
-
renderMode: "navigation"
|
|
283
|
-
},
|
|
284
|
-
request: new Request(new URL(createCanonicalRscRequestUrl(identityRequest.url), identityRequest.url), { headers: createCanonicalRscRequestHeaders() })
|
|
285
|
-
} : void 0;
|
|
414
|
+
const otherPart = isWarmup || options.cacheIdentity?.queryFree ? otherPagePart(identityRequest, identityProps) : void 0;
|
|
415
|
+
const rscKey = isWarmup && otherPart?.kind === "rsc" ? await cacheRequest(otherPart.invocation) : void 0;
|
|
286
416
|
const invocation = prepareInvocation(identityRequest, identityProps);
|
|
287
|
-
const rscInvocation = rscSeed ? prepareInvocation(rscSeed.request, rscSeed.props) : void 0;
|
|
288
|
-
const rscKey = rscInvocation ? await cacheRequest(rscInvocation) : void 0;
|
|
289
417
|
const key = await cacheRequest(invocation);
|
|
290
418
|
const stored = await readStoredResponse(key);
|
|
291
419
|
if (stored) {
|
|
@@ -297,8 +425,12 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
297
425
|
}
|
|
298
426
|
stored.body?.cancel().catch(() => {});
|
|
299
427
|
}
|
|
300
|
-
const capture =
|
|
428
|
+
const capture = {
|
|
429
|
+
...isWarmup ? {} : { streamResponse: true },
|
|
430
|
+
...otherPagePartCapture(otherPart)
|
|
431
|
+
};
|
|
301
432
|
const serializedInvocation = JSON.stringify(invocation);
|
|
433
|
+
const revalidatorInvocation = !options.cacheIdentity ? serializeDispatchInvocation(invocation) : options.cacheIdentity.regenerateFromDispatch ? serializeDispatchInvocation(prepareInvocation(stageRequest, props)) : serializedInvocation;
|
|
302
434
|
const rendered = await invokeResponseStage(stageRequest, props, env, ctx, isWarmup ? {
|
|
303
435
|
cache: "shared",
|
|
304
436
|
renderWholeDocument: true
|
|
@@ -311,14 +443,19 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
311
443
|
ctx.waitUntil(capture.admittedResponse.then(async (admitted) => {
|
|
312
444
|
if (!isCacheable(admitted)) {
|
|
313
445
|
await admitted.body?.cancel().catch(() => {});
|
|
446
|
+
capture.rscData?.catch(() => {});
|
|
314
447
|
return;
|
|
315
448
|
}
|
|
316
|
-
|
|
449
|
+
const stored = withoutRequestScopedHeaders(admitted, props);
|
|
450
|
+
try {
|
|
451
|
+
await putOtherPagePart(otherPart, capture, stored);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
await stored.body?.cancel().catch(() => {});
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
await responseStore.put(key, stored, {
|
|
317
457
|
coalesce: true,
|
|
318
|
-
revalidator:
|
|
319
|
-
id: ROUTE_REVALIDATOR_ID,
|
|
320
|
-
args: [serializedInvocation]
|
|
321
|
-
}
|
|
458
|
+
revalidator: routeRevalidator(revalidatorInvocation, otherPart !== void 0)
|
|
322
459
|
});
|
|
323
460
|
}).catch((error) => {
|
|
324
461
|
console.error(JSON.stringify({
|
|
@@ -335,22 +472,20 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
335
472
|
}), "MISS", props) : publicResponse(notModified, "MISS", props, true);
|
|
336
473
|
}
|
|
337
474
|
if (!isCacheable(rendered)) {
|
|
338
|
-
capture
|
|
475
|
+
capture.rscData?.catch(() => {});
|
|
339
476
|
return publicResponse(foreground(rendered), "BYPASS", props);
|
|
340
477
|
}
|
|
341
|
-
if (
|
|
478
|
+
if (rscKey && !capture.rscData) {
|
|
342
479
|
await rendered.body?.cancel();
|
|
343
480
|
throw new Error("Vinext response-store warmup did not capture the App page RSC payload");
|
|
344
481
|
}
|
|
345
482
|
const [foregroundBody, cacheBody] = rendered.body ? rendered.body.tee() : [null, null];
|
|
346
483
|
const cacheResponse = withoutRequestScopedHeaders(new Response(cacheBody, rendered), props);
|
|
347
484
|
try {
|
|
485
|
+
await putOtherPagePart(otherPart, capture, cacheResponse);
|
|
348
486
|
await responseStore.put(key, cacheResponse, {
|
|
349
487
|
coalesce: true,
|
|
350
|
-
revalidator:
|
|
351
|
-
id: ROUTE_REVALIDATOR_ID,
|
|
352
|
-
args: [serializedInvocation]
|
|
353
|
-
}
|
|
488
|
+
revalidator: routeRevalidator(revalidatorInvocation, otherPart !== void 0)
|
|
354
489
|
});
|
|
355
490
|
} catch (error) {
|
|
356
491
|
if (isWarmup) throw error;
|
|
@@ -360,31 +495,6 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
360
495
|
error: error instanceof Error ? error.message : String(error)
|
|
361
496
|
}));
|
|
362
497
|
}
|
|
363
|
-
if (rscSeed && rscInvocation && rscKey && capture?.rscData) {
|
|
364
|
-
const rscData = await capture.rscData;
|
|
365
|
-
const rscHeaders = new Headers(rendered.headers);
|
|
366
|
-
rscHeaders.delete("Content-Length");
|
|
367
|
-
rscHeaders.delete("Link");
|
|
368
|
-
rscHeaders.delete("X-Vinext-Response-Store-Replayable");
|
|
369
|
-
rscHeaders.delete(VINEXT_PARAMS_HEADER);
|
|
370
|
-
rscHeaders.delete(VINEXT_RENDERED_PATH_AND_SEARCH_HEADER);
|
|
371
|
-
rscHeaders.set("Content-Type", VINEXT_RSC_CONTENT_TYPE);
|
|
372
|
-
rscHeaders.set("Vary", VINEXT_RSC_VARY_HEADER$1);
|
|
373
|
-
applyRscCompatibilityIdHeader(rscHeaders);
|
|
374
|
-
applyRscDeploymentIdHeader(rscHeaders);
|
|
375
|
-
if (isStoredErrorStatus(rendered.status)) rscHeaders.set(VINEXT_SPECIAL_ERROR_STATUS_HEADER, String(rendered.status));
|
|
376
|
-
const serializedRscInvocation = JSON.stringify(rscInvocation);
|
|
377
|
-
await responseStore.put(rscKey, new Response(rscData, {
|
|
378
|
-
headers: rscHeaders,
|
|
379
|
-
status: rendered.status >= 300 && rendered.status < 400 ? 200 : rendered.status
|
|
380
|
-
}), {
|
|
381
|
-
coalesce: true,
|
|
382
|
-
revalidator: {
|
|
383
|
-
id: ROUTE_REVALIDATOR_ID,
|
|
384
|
-
args: [serializedRscInvocation]
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
498
|
return publicResponse(foreground(new Response(foregroundBody, rendered)), "MISS", props);
|
|
389
499
|
};
|
|
390
500
|
const { handleRequestStage } = await loadVinextRequestStage();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import createCloudflareCdnCacheAdapter from "./cdn-adapter.runtime.js";
|
|
2
|
-
import { captureResponseStoreRscData, deferResponseStoreAdmission, deferResponseStoreNotModified } from "./response-store-data.runtime.js";
|
|
2
|
+
import { captureResponseStoreAppPageDocument, captureResponseStoreRscData, deferResponseStoreAdmission, deferResponseStoreNotModified } from "./response-store-data.runtime.js";
|
|
3
3
|
//#region src/cache/response-store-cdn.runtime.ts
|
|
4
4
|
/** Response Store owns page serving and SWR; vinext only emits admitted response policy. */
|
|
5
5
|
var ResponseStoreCdnCacheAdapter = class {
|
|
@@ -33,6 +33,9 @@ var ResponseStoreCdnCacheAdapter = class {
|
|
|
33
33
|
captureAppPageRscData(rscData) {
|
|
34
34
|
captureResponseStoreRscData(rscData);
|
|
35
35
|
}
|
|
36
|
+
captureAppPageDocument() {
|
|
37
|
+
return captureResponseStoreAppPageDocument();
|
|
38
|
+
}
|
|
36
39
|
deferNotModifiedResponse() {
|
|
37
40
|
return deferResponseStoreNotModified();
|
|
38
41
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { CdnAppPageDocument } from "vinext/shims/cdn-cache";
|
|
1
2
|
import { WorkersResponseStore } from "@cloudflare/workers-response-store";
|
|
2
3
|
import { CacheHandler, CacheHandlerValue, IncrementalCacheValue } from "vinext/shims/cache";
|
|
3
4
|
//#region src/cache/response-store-data.runtime.d.ts
|
|
4
5
|
export type ResponseStoreInvocationCapture = {
|
|
5
6
|
admittedResponse?: Promise<Response>;
|
|
7
|
+
/** Store the page's document beside an App page RSC response. */
|
|
8
|
+
captureDocument?: boolean;
|
|
6
9
|
captureRscData?: boolean;
|
|
10
|
+
/** The page's document, set before admission completes the response. */
|
|
11
|
+
document?: CdnAppPageDocument;
|
|
7
12
|
/** The render matched the request's If-None-Match; the adapter sends the 304. */
|
|
8
13
|
notModified?: boolean;
|
|
9
14
|
rscData?: Promise<ArrayBuffer>;
|
|
@@ -17,6 +22,11 @@ export declare function setResponseStore(store: WorkersResponseStore): void;
|
|
|
17
22
|
export declare function runWithResponseStoreInvocation<T>(serialized: string, replayable: boolean, callback: () => T, capture?: ResponseStoreInvocationCapture): T;
|
|
18
23
|
/** Retain the RSC side stream only when the outer response-store invocation requested it. */
|
|
19
24
|
export declare function captureResponseStoreRscData(rscData: Promise<ArrayBuffer>): void;
|
|
25
|
+
/**
|
|
26
|
+
* Retain the page's document only when the outer response-store invocation
|
|
27
|
+
* requested it. Admission settles it before it completes the response.
|
|
28
|
+
*/
|
|
29
|
+
export declare function captureResponseStoreAppPageDocument(): ((document: CdnAppPageDocument | null) => void) | undefined;
|
|
20
30
|
/** A shared invocation stores the page, so its adapter sends the page's 304. */
|
|
21
31
|
export declare function deferResponseStoreNotModified(): boolean;
|
|
22
32
|
/** Stream the foreground body while retaining an independent admission branch. */
|
|
@@ -26,6 +36,8 @@ export declare function captureResponseStoreDataRegeneration(key: string, callba
|
|
|
26
36
|
export declare class WorkersResponseStoreCacheHandler implements CacheHandler {
|
|
27
37
|
private readonly store;
|
|
28
38
|
private readonly tagExpirations;
|
|
39
|
+
/** The revision behind each entry returned stale by the Store, for the write refreshing it. */
|
|
40
|
+
private readonly staleRevisions;
|
|
29
41
|
constructor(store?: WorkersResponseStore);
|
|
30
42
|
get(key: string, context?: Record<string, unknown>): Promise<CacheHandlerValue | null>;
|
|
31
43
|
set(key: string, value: IncrementalCacheValue | null, context?: Record<string, unknown>): Promise<void>;
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { isStaleTagInvalidation } from "./workers-cache-invalidation.js";
|
|
2
2
|
import { encodeCloudflareCacheTag } from "./cdn-adapter.runtime.js";
|
|
3
|
+
import { readCacheTimestamp } from "../utils/cache-control-metadata.js";
|
|
3
4
|
import { Buffer } from "node:buffer";
|
|
4
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
6
|
import { cacheForRequest } from "vinext/cache";
|
|
6
7
|
//#region src/cache/response-store-data.runtime.ts
|
|
7
8
|
const ARRAY_BUFFER_MARKER = "$vinextArrayBuffer";
|
|
8
9
|
const CACHE_MAX_AGE_SECONDS = 31536e4;
|
|
9
|
-
const CACHE_MAX_AGE = `public, max-age=${CACHE_MAX_AGE_SECONDS}`;
|
|
10
10
|
const DATA_ENTRY_PATH = "__vinext_data";
|
|
11
11
|
const REPLAYABLE_HEADER = "X-Vinext-Response-Store-Replayable";
|
|
12
|
+
/** The Store's internal header for when a response's generation started. */
|
|
13
|
+
const GENERATED_AT_HEADER = "X-Workers-Response-Store-Generated-At";
|
|
12
14
|
const MAX_CACHE_TAG_HEADER_BYTES = 16384;
|
|
15
|
+
/** How long past its revalidate a current Store's hit can still be marked fresh: write latency. */
|
|
16
|
+
const STALE_FRESH_HIT_GRACE_MS = 5e3;
|
|
13
17
|
const DATA_REVALIDATOR_ID = "vinext:data";
|
|
14
18
|
const CACHE_FUNCTION_REVALIDATOR_ID = "vinext:cache-function";
|
|
15
19
|
const invocationStorage = new AsyncLocalStorage();
|
|
@@ -33,6 +37,17 @@ function captureResponseStoreRscData(rscData) {
|
|
|
33
37
|
if (capture?.captureRscData) capture.rscData = rscData;
|
|
34
38
|
else rscData.catch(() => {});
|
|
35
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Retain the page's document only when the outer response-store invocation
|
|
42
|
+
* requested it. Admission settles it before it completes the response.
|
|
43
|
+
*/
|
|
44
|
+
function captureResponseStoreAppPageDocument() {
|
|
45
|
+
const capture = invocationStorage.getStore()?.capture;
|
|
46
|
+
if (!capture?.captureDocument) return void 0;
|
|
47
|
+
return (document) => {
|
|
48
|
+
capture.document = document ?? void 0;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
36
51
|
/** A shared invocation stores the page, so its adapter sends the page's 304. */
|
|
37
52
|
function deferResponseStoreNotModified() {
|
|
38
53
|
const capture = invocationStorage.getStore()?.capture;
|
|
@@ -194,13 +209,15 @@ function deserialize(value) {
|
|
|
194
209
|
return entry;
|
|
195
210
|
}
|
|
196
211
|
function cachePolicy(revalidate, expire) {
|
|
197
|
-
if (revalidate === false || revalidate === void 0) return `public, max-age=${CACHE_MAX_AGE_SECONDS}`;
|
|
198
|
-
const
|
|
199
|
-
return `public, max-age=${Math.max(0,
|
|
212
|
+
if (revalidate === false || revalidate === void 0) return `public, max-age=${expire === void 0 ? CACHE_MAX_AGE_SECONDS : Math.max(1, Math.ceil(expire))}`;
|
|
213
|
+
const maxAge = revalidate > 0 ? Math.max(1, Math.floor(revalidate)) : 0;
|
|
214
|
+
return `public, max-age=${maxAge}, stale-while-revalidate=${expire === void 0 ? CACHE_MAX_AGE_SECONDS : Math.max(0, Math.ceil(expire) - maxAge)}`;
|
|
200
215
|
}
|
|
201
216
|
var WorkersResponseStoreCacheHandler = class {
|
|
202
217
|
store;
|
|
203
218
|
tagExpirations = cacheForRequest(() => /* @__PURE__ */ new Map());
|
|
219
|
+
/** The revision behind each entry returned stale by the Store, for the write refreshing it. */
|
|
220
|
+
staleRevisions = /* @__PURE__ */ new WeakMap();
|
|
204
221
|
constructor(store = responseStore) {
|
|
205
222
|
this.store = store;
|
|
206
223
|
if (!store) throw new Error("[vinext] The Workers Response Store adapter must run through its generated Cloudflare worker entry.");
|
|
@@ -237,25 +254,37 @@ var WorkersResponseStoreCacheHandler = class {
|
|
|
237
254
|
await this.store.purge({ pathPrefixes: [new URL(request.url).pathname] });
|
|
238
255
|
return null;
|
|
239
256
|
}
|
|
257
|
+
let tagExpiration;
|
|
240
258
|
if (softTags.length) {
|
|
241
259
|
const expiration = eagerExpiration ? await eagerExpiration : { value: await getTagExpiration() };
|
|
242
260
|
if ("error" in expiration) throw expiration.error;
|
|
243
261
|
if (expiration.value >= entry.lastModified) return null;
|
|
262
|
+
tagExpiration = {
|
|
263
|
+
tags: softTags,
|
|
264
|
+
expiration: expiration.value
|
|
265
|
+
};
|
|
244
266
|
}
|
|
245
267
|
const age = Date.now() - entry.lastModified;
|
|
246
268
|
const requestedRevalidate = readCacheControlField(context, "revalidate");
|
|
269
|
+
const storedRevalidate = entry.cacheControl?.revalidate;
|
|
247
270
|
const requestedStale = typeof requestedRevalidate === "number" && requestedRevalidate > 0 && age > requestedRevalidate * 1e3;
|
|
248
271
|
let cacheState;
|
|
249
272
|
if (response.headers.get(REPLAYABLE_HEADER) === "1") {
|
|
250
273
|
if (requestedStale && storeStatus !== "BLOB-STALE") cacheState = "stale";
|
|
251
274
|
} else if (typeof entry.cacheControl?.expire === "number" && age > entry.cacheControl.expire * 1e3) cacheState = "expired";
|
|
252
|
-
else if (
|
|
253
|
-
|
|
275
|
+
else if (storeStatus === "BLOB-STALE" || requestedStale && typeof requestedRevalidate === "number" && (typeof storedRevalidate !== "number" || requestedRevalidate < storedRevalidate) || typeof storedRevalidate === "number" && storedRevalidate > 0 && age > storedRevalidate * 1e3 + STALE_FRESH_HIT_GRACE_MS) cacheState = "stale";
|
|
276
|
+
const value = {
|
|
254
277
|
lastModified: entry.lastModified,
|
|
255
278
|
value: entry.value,
|
|
256
279
|
...cacheState ? { cacheState } : {},
|
|
257
280
|
...entry.cacheControl ? { cacheControl: entry.cacheControl } : {}
|
|
258
281
|
};
|
|
282
|
+
const revision = Number(response.headers.get("X-Workers-Response-Store-Revision"));
|
|
283
|
+
if (cacheState && storeStatus === "BLOB-STALE" && Number.isSafeInteger(revision) && revision > 0) this.staleRevisions.set(value, {
|
|
284
|
+
revision,
|
|
285
|
+
...tagExpiration ? { tagExpiration } : {}
|
|
286
|
+
});
|
|
287
|
+
return value;
|
|
259
288
|
} catch (error) {
|
|
260
289
|
console.error(JSON.stringify({
|
|
261
290
|
message: "Vinext response-store data lookup failed; treating as a cache miss",
|
|
@@ -274,9 +303,11 @@ var WorkersResponseStoreCacheHandler = class {
|
|
|
274
303
|
let revalidate = readCacheControlField(context, "revalidate");
|
|
275
304
|
if (value && "revalidate" in value) revalidate = value.revalidate;
|
|
276
305
|
if (revalidate === 0) return;
|
|
306
|
+
if (typeof revalidate === "number" && !Number.isFinite(revalidate)) revalidate = false;
|
|
277
307
|
const rawExpire = readCacheControlField(context, "expire");
|
|
278
|
-
const expire = typeof rawExpire === "number" ? rawExpire : void 0;
|
|
279
|
-
const
|
|
308
|
+
const expire = typeof rawExpire === "number" && Number.isFinite(rawExpire) ? rawExpire : void 0;
|
|
309
|
+
const rawStale = readCacheControlField(context, "stale");
|
|
310
|
+
const stale = typeof rawStale === "number" && Number.isFinite(rawStale) ? rawStale : void 0;
|
|
280
311
|
const cacheControl = typeof revalidate === "number" || revalidate === false ? {
|
|
281
312
|
revalidate,
|
|
282
313
|
...typeof expire === "number" ? { expire } : {},
|
|
@@ -298,12 +329,14 @@ var WorkersResponseStoreCacheHandler = class {
|
|
|
298
329
|
id: DATA_REVALIDATOR_ID,
|
|
299
330
|
args: [key, invocation.serialized]
|
|
300
331
|
} : void 0;
|
|
332
|
+
const lastModified = readCacheTimestamp(context, Date.now());
|
|
301
333
|
const response = new Response(serialize({
|
|
302
334
|
...cacheControl ? { cacheControl } : {},
|
|
303
|
-
lastModified
|
|
335
|
+
lastModified,
|
|
304
336
|
value
|
|
305
337
|
}), { headers: {
|
|
306
|
-
|
|
338
|
+
...lastModified === context?.timestamp ? { [GENERATED_AT_HEADER]: String(lastModified) } : {},
|
|
339
|
+
"Cache-Control": cachePolicy(revalidate, expire),
|
|
307
340
|
...tagHeader ? { "Cache-Tag": tagHeader } : {},
|
|
308
341
|
"Content-Type": "application/json",
|
|
309
342
|
...revalidator ? { [REPLAYABLE_HEADER]: "1" } : {}
|
|
@@ -314,11 +347,17 @@ var WorkersResponseStoreCacheHandler = class {
|
|
|
314
347
|
return;
|
|
315
348
|
}
|
|
316
349
|
if (regeneration && !revalidator) return;
|
|
350
|
+
const staleEntry = context?.staleEntry;
|
|
351
|
+
const replaces = staleEntry && typeof staleEntry === "object" ? this.staleRevisions.get(staleEntry) : void 0;
|
|
317
352
|
await this.store.put(await cacheRequest(key), response, {
|
|
318
353
|
...revalidator ? { revalidator } : {},
|
|
319
354
|
...revalidator?.id !== "vinext:cache-function" ? { expiryBehavior: "miss" } : {},
|
|
320
355
|
coalesce: true,
|
|
321
|
-
purgeExisting: true
|
|
356
|
+
purgeExisting: true,
|
|
357
|
+
...replaces ? {
|
|
358
|
+
replacesRevision: replaces.revision,
|
|
359
|
+
...replaces.tagExpiration ? { replacesTagExpiration: replaces.tagExpiration } : {}
|
|
360
|
+
} : {}
|
|
322
361
|
});
|
|
323
362
|
}
|
|
324
363
|
async revalidateTag(tags, durations) {
|
|
@@ -335,4 +374,4 @@ function createResponseStoreDataCacheAdapter() {
|
|
|
335
374
|
return new WorkersResponseStoreCacheHandler();
|
|
336
375
|
}
|
|
337
376
|
//#endregion
|
|
338
|
-
export { CACHE_FUNCTION_REVALIDATOR_ID, DATA_REVALIDATOR_ID, WorkersResponseStoreCacheHandler, captureResponseStoreDataRegeneration, captureResponseStoreRscData, createResponseStoreDataCacheAdapter as default, deferResponseStoreAdmission, deferResponseStoreNotModified, runWithResponseStoreInvocation, setResponseStore };
|
|
377
|
+
export { CACHE_FUNCTION_REVALIDATOR_ID, DATA_REVALIDATOR_ID, WorkersResponseStoreCacheHandler, captureResponseStoreAppPageDocument, captureResponseStoreDataRegeneration, captureResponseStoreRscData, createResponseStoreDataCacheAdapter as default, deferResponseStoreAdmission, deferResponseStoreNotModified, runWithResponseStoreInvocation, setResponseStore };
|
|
@@ -27,4 +27,5 @@ export declare function hasCacheMethod<Name extends CacheMethodName>(value: unkn
|
|
|
27
27
|
* `undefined` when the cache supports neither.
|
|
28
28
|
*/
|
|
29
29
|
export declare function invalidateOrPurge(cache: unknown, options: CacheInvalidationOptions): unknown;
|
|
30
|
-
//#endregion
|
|
30
|
+
//#endregion
|
|
31
|
+
export {};
|
package/dist/cdn-warm.d.ts
CHANGED
|
@@ -130,4 +130,5 @@ export declare function waitForCdnWarmTargetReadiness(options: Pick<CdnWarmOptio
|
|
|
130
130
|
}): Promise<CdnWarmReadinessResult>;
|
|
131
131
|
export declare function warmCdnCache(options: CdnWarmOptions): Promise<CdnWarmResult>;
|
|
132
132
|
export declare function warmCdnCacheFromPrerender(options: PrerenderCdnWarmOptions): Promise<CdnWarmResult>;
|
|
133
|
-
//#endregion
|
|
133
|
+
//#endregion
|
|
134
|
+
export {};
|
package/dist/cli.js
CHANGED
|
@@ -45,6 +45,7 @@ async function deployCommand() {
|
|
|
45
45
|
preview: parsed.preview,
|
|
46
46
|
env: parsed.env,
|
|
47
47
|
config: parsed.config,
|
|
48
|
+
viteConfig: parsed.viteConfig,
|
|
48
49
|
skipBuild: parsed.skipBuild,
|
|
49
50
|
dryRun: parsed.dryRun,
|
|
50
51
|
verbose: parsed.verbose,
|
|
@@ -92,6 +93,7 @@ async function previewsCommand() {
|
|
|
92
93
|
root: process.cwd(),
|
|
93
94
|
env: options.mode,
|
|
94
95
|
skipBuild: options.skipBuild,
|
|
96
|
+
viteConfig: options.viteConfig,
|
|
95
97
|
target: {
|
|
96
98
|
type: "preview",
|
|
97
99
|
name: options.name
|
package/dist/deploy-config.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { VinextCacheConfig } from "vinext/internal/config/prerender";
|
|
|
8
8
|
* Returns true when the config both imports the package and invokes
|
|
9
9
|
* `cloudflare(...)` in the plugin list.
|
|
10
10
|
*/
|
|
11
|
-
export declare function viteConfigHasCloudflarePlugin(root: string): boolean;
|
|
11
|
+
export declare function viteConfigHasCloudflarePlugin(root: string, configFile?: string): boolean;
|
|
12
12
|
/**
|
|
13
13
|
* Detect whether the Vite config assigns a CDN or data cache adapter — i.e. the
|
|
14
14
|
* `cdn` or `data` field of the `vinext({ cache })` option is given a value.
|
|
@@ -17,12 +17,12 @@ export declare function viteConfigHasCloudflarePlugin(root: string): boolean;
|
|
|
17
17
|
* an unreadable or absent config is treated as configured so a deploy is never
|
|
18
18
|
* blocked on a false negative.
|
|
19
19
|
*/
|
|
20
|
-
export declare function viteConfigHasCacheAdapter(root: string): boolean;
|
|
20
|
+
export declare function viteConfigHasCacheAdapter(root: string, configFile?: string): boolean;
|
|
21
21
|
export type ResolvedCdnAdapterConfig = {
|
|
22
22
|
versionMetadataBinding: string;
|
|
23
23
|
};
|
|
24
24
|
export declare function resolveCdnAdapterConfig(cache: VinextCacheConfig | null | undefined): ResolvedCdnAdapterConfig | null;
|
|
25
|
-
export declare function viteConfigHasImageAdapter(root: string): boolean;
|
|
25
|
+
export declare function viteConfigHasImageAdapter(root: string, configFile?: string): boolean;
|
|
26
26
|
/**
|
|
27
27
|
* Detect whether an existing user-authored Worker entry wires up a cache
|
|
28
28
|
* backend imperatively via one of the `setCacheHandler` / `setDataCacheHandler`
|
|
@@ -48,5 +48,5 @@ export declare function formatMissingCacheAdapterError(options: {
|
|
|
48
48
|
configFile?: string;
|
|
49
49
|
typedConfig?: boolean;
|
|
50
50
|
}): string;
|
|
51
|
-
export declare function formatImageOptimizationHint(typedConfig?: boolean): string;
|
|
51
|
+
export declare function formatImageOptimizationHint(typedConfig?: boolean, configFile?: string): string;
|
|
52
52
|
//#endregion
|
package/dist/deploy-config.js
CHANGED
|
@@ -15,8 +15,8 @@ function escapeRegExp(value) {
|
|
|
15
15
|
* Returns true when the config both imports the package and invokes
|
|
16
16
|
* `cloudflare(...)` in the plugin list.
|
|
17
17
|
*/
|
|
18
|
-
function viteConfigHasCloudflarePlugin(root) {
|
|
19
|
-
const configPath = findViteConfigPath(root);
|
|
18
|
+
function viteConfigHasCloudflarePlugin(root, configFile) {
|
|
19
|
+
const configPath = configFile ?? findViteConfigPath(root);
|
|
20
20
|
if (!configPath) return false;
|
|
21
21
|
try {
|
|
22
22
|
const content = fs.readFileSync(configPath, "utf-8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
@@ -75,8 +75,8 @@ function cacheFieldAssigned(cacheBlock, field) {
|
|
|
75
75
|
* an unreadable or absent config is treated as configured so a deploy is never
|
|
76
76
|
* blocked on a false negative.
|
|
77
77
|
*/
|
|
78
|
-
function viteConfigHasCacheAdapter(root) {
|
|
79
|
-
const configPath = findViteConfigPath(root);
|
|
78
|
+
function viteConfigHasCacheAdapter(root, configFile) {
|
|
79
|
+
const configPath = configFile ?? findViteConfigPath(root);
|
|
80
80
|
if (!configPath) return true;
|
|
81
81
|
let content;
|
|
82
82
|
try {
|
|
@@ -104,8 +104,8 @@ function resolveCdnAdapterConfig(cache) {
|
|
|
104
104
|
const options = cdn.options;
|
|
105
105
|
return { versionMetadataBinding: typeof options?.versionMetadataBinding === "string" && options.versionMetadataBinding.length > 0 ? options.versionMetadataBinding : DEFAULT_CDN_VERSION_METADATA_BINDING };
|
|
106
106
|
}
|
|
107
|
-
function viteConfigHasImageAdapter(root) {
|
|
108
|
-
const configPath = findViteConfigPath(root);
|
|
107
|
+
function viteConfigHasImageAdapter(root, configFile) {
|
|
108
|
+
const configPath = configFile ?? findViteConfigPath(root);
|
|
109
109
|
if (!configPath) return true;
|
|
110
110
|
try {
|
|
111
111
|
const content = fs.readFileSync(configPath, "utf-8");
|
|
@@ -161,7 +161,10 @@ function formatMissingCacheAdapterError(options) {
|
|
|
161
161
|
const configRef = options.configFile ? options.configFile : "your Vite config";
|
|
162
162
|
return `[vinext] This app uses ISR / caching but no cache adapter is configured in ${configRef}.\n\n Production deployments need a persistent cache backend. Declare one on the\n vinext() plugin in ${configRef}:\n\n import { kvDataAdapter } from "@vinext/cloudflare/cache/kv-data-adapter";\n\n export default defineConfig({\n plugins: [\n vinext({\n cache: {\n data: kvDataAdapter(), // KV-backed data cache (binding: VINEXT_KV_CACHE)\n },\n }),\n cloudflare(),\n ],\n });\n\n` + (options.typedConfig ? ` Configure the VINEXT_KV_CACHE namespace binding in cloudflare.config.ts.` : " The VINEXT_KV_CACHE namespace binding is added to wrangler.jsonc for you.\n Create the namespace with:\n\n npx wrangler kv namespace create VINEXT_KV_CACHE");
|
|
163
163
|
}
|
|
164
|
-
function formatImageOptimizationHint(typedConfig = false) {
|
|
164
|
+
function formatImageOptimizationHint(typedConfig = false, configFile) {
|
|
165
|
+
if (configFile) return ` [vinext] next/image is served unoptimized. To enable edge image
|
|
166
|
+
optimization via Cloudflare Images, configure imagesOptimizer() in
|
|
167
|
+
${configFile} and the IMAGES binding in ${typedConfig ? "cloudflare.config.ts" : "your Wrangler config"}.`;
|
|
165
168
|
if (typedConfig) return " [vinext] next/image is served unoptimized. To enable edge image\n optimization via Cloudflare Images, configure imagesOptimizer() in your\n Vite config and the IMAGES binding in cloudflare.config.ts.";
|
|
166
169
|
return " [vinext] next/image is served unoptimized. To enable edge image\n optimization via Cloudflare Images, run:\n\n vinext init --platform=cloudflare --image-optimization=cloudflare-images\n\n This adds the imagesOptimizer() option to your Vite config and the matching\n IMAGES binding to your Wrangler config without replacing existing settings.";
|
|
167
170
|
}
|