@vinext/cloudflare 1.1.0 → 1.2.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/cdn-adapter.runtime.js +10 -4
- package/dist/cache/cdn-adapter.worker.js +61 -8
- package/dist/cache/config.d.ts +3 -3
- package/dist/cache/config.js +1 -1
- package/dist/cache/kv-key.js +21 -2
- package/dist/cache/response-store-adapter.worker.js +70 -9
- package/dist/cache/response-store-cdn.runtime.js +4 -1
- package/dist/cache/response-store-data.runtime.d.ts +4 -0
- package/dist/cache/response-store-data.runtime.js +8 -1
- package/dist/cache/static-assets-adapter.build.js +5 -4
- package/dist/cache/static-assets-adapter.runtime.d.ts +3 -1
- package/dist/cache/static-assets-adapter.runtime.js +23 -2
- package/dist/cdn-warm.d.ts +0 -3
- package/dist/cdn-warm.js +3 -16
- package/dist/cli.js +38 -3
- package/dist/deploy-help.d.ts +2 -0
- package/dist/deploy-help.js +32 -4
- package/dist/deploy.d.ts +9 -5
- package/dist/deploy.js +80 -15
- package/dist/previews-deploy.d.ts +9 -0
- package/dist/previews-deploy.js +43 -0
- package/dist/worker-deployment-url.d.ts +3 -1
- package/dist/worker-deployment-url.js +4 -2
- package/package.json +4 -4
|
@@ -132,6 +132,7 @@ function toEdgeCacheControl(cacheControl) {
|
|
|
132
132
|
*/
|
|
133
133
|
const MAX_CACHE_TAG_BYTES = 16384;
|
|
134
134
|
const CACHE_TAG_PREFIX = "vinext-";
|
|
135
|
+
const MAX_TAGS_PER_PURGE = 100;
|
|
135
136
|
/** Encode a case-sensitive Next.js tag into a fixed Workers Cache tag. */
|
|
136
137
|
function encodeCloudflareCacheTag(tag) {
|
|
137
138
|
return `${CACHE_TAG_PREFIX}${fnv1a64(`0:${tag}`)}${fnv1a64(`1:${tag}`)}`;
|
|
@@ -222,11 +223,16 @@ var CloudflareCdnCacheAdapter = class {
|
|
|
222
223
|
if (!cache) return;
|
|
223
224
|
const tagList = (Array.isArray(tags) ? tags : [tags]).filter((t) => typeof t === "string");
|
|
224
225
|
if (tagList.length === 0) return;
|
|
225
|
-
const
|
|
226
|
+
const encodedTags = [...new Set(tagList.map(encodeCloudflareCacheTag))];
|
|
226
227
|
const operation = isStaleTagInvalidation(durations) ? "invalidate" : "purge";
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
const chunks = [];
|
|
229
|
+
for (let i = 0; i < encodedTags.length; i += MAX_TAGS_PER_PURGE) chunks.push(encodedTags.slice(i, i + MAX_TAGS_PER_PURGE));
|
|
230
|
+
const settled = await Promise.allSettled(chunks.map(async (chunk) => await (operation === "invalidate" ? invalidateOrPurge(cache, { tags: chunk }) : cache.purge({ tags: chunk }))));
|
|
231
|
+
const rejection = settled.find((result) => result.status === "rejected");
|
|
232
|
+
if (rejection) throw rejection.reason;
|
|
233
|
+
const failures = settled.flatMap((result) => result.status === "fulfilled" && result.value?.success === false ? [result.value] : []);
|
|
234
|
+
if (failures.length > 0) {
|
|
235
|
+
const errors = failures.flatMap((result) => result.errors).map(({ code, message }) => `${code}: ${message}`).join(", ");
|
|
230
236
|
throw new Error(`[vinext] Workers Cache ${operation} failed${errors ? `: ${errors}` : ""}`);
|
|
231
237
|
}
|
|
232
238
|
}
|
|
@@ -2,7 +2,7 @@ import { VINEXT_CDN_BUILD_ID_HEADER, getVinextCdnBuildIdentity } from "./cdn-bui
|
|
|
2
2
|
import { SHARED_RESPONSE_STAGE_HEADER, finalizeGatewayResponse } from "./browser-cache-policy.js";
|
|
3
3
|
import { hasCacheMethod, invalidateOrPurge } from "./workers-cache-invalidation.js";
|
|
4
4
|
import { VINEXT_PRERENDER_READINESS_PATH, VINEXT_RSC_VARY_HEADER } from "vinext/internal/server/headers";
|
|
5
|
-
import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
|
|
5
|
+
import { isNonCacheableCacheControl, splitCacheControlDirectives } from "vinext/shims/cdn-cache";
|
|
6
6
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
7
7
|
import "vinext/internal/server/cloudflare-workers-tracing";
|
|
8
8
|
import { loadVinextRequestStage } from "vinext/server/request-stage";
|
|
@@ -132,7 +132,8 @@ async function createCacheFacingRequest(request, serializedInvocation) {
|
|
|
132
132
|
return new Request(url, {
|
|
133
133
|
cf: { vary: { default: { action: "passthrough" } } },
|
|
134
134
|
headers,
|
|
135
|
-
method: request.method
|
|
135
|
+
method: request.method,
|
|
136
|
+
redirect: "manual"
|
|
136
137
|
});
|
|
137
138
|
}
|
|
138
139
|
function restoreResponseStageRequest(request, requestUrl, requestMethod) {
|
|
@@ -183,18 +184,63 @@ function isAppPageDocumentInvocation(invocation) {
|
|
|
183
184
|
const props = invocation.props;
|
|
184
185
|
return props !== null && typeof props === "object" && Reflect.get(props, "kind") === "app-page" && Reflect.get(props, "isRscRequest") === false;
|
|
185
186
|
}
|
|
187
|
+
/** A Pages page render, as an HTML document or a `_next/data` payload. */
|
|
188
|
+
function isPagesPageInvocation(invocation) {
|
|
189
|
+
const props = invocation.props;
|
|
190
|
+
if (props === null || typeof props !== "object") return false;
|
|
191
|
+
const kind = Reflect.get(props, "kind");
|
|
192
|
+
return kind === "pages-page" || kind === "hybrid-pages" && Reflect.get(props, "resourceKind") === "page";
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Workers Cache revalidates an entry that has a validator with a conditional
|
|
196
|
+
* request, and serves the stale entry meanwhile or fills an expired one.
|
|
197
|
+
*/
|
|
198
|
+
function revalidatesStoredEntry(request) {
|
|
199
|
+
return request.headers.has("If-None-Match") || request.headers.has("If-Modified-Since");
|
|
200
|
+
}
|
|
186
201
|
/**
|
|
187
|
-
* No client waits on a deployment warm-up
|
|
188
|
-
*
|
|
189
|
-
* Store's, and serves the stale entry meanwhile or fills an expired one.
|
|
202
|
+
* No client waits on a deployment warm-up or on a revalidation, as with the
|
|
203
|
+
* Response Store's.
|
|
190
204
|
*/
|
|
191
205
|
function rendersWholeDocument(request) {
|
|
192
|
-
return request.headers.get("user-agent") === WARMUP_USER_AGENT ||
|
|
206
|
+
return request.headers.get("user-agent") === WARMUP_USER_AGENT || revalidatesStoredEntry(request);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* A conditional revalidation of a Pages page skips getStaticPaths(), so only a
|
|
210
|
+
* successful render may be stored with a validator. Bot renders carry an ETag
|
|
211
|
+
* whatever their status. A 304 answers a revalidation of a stored 200, and a
|
|
212
|
+
* response a shared cache may not store keeps its validators.
|
|
213
|
+
*/
|
|
214
|
+
function withPagesRevalidationValidator(response) {
|
|
215
|
+
if (response.status === 200) return withRevalidationValidator(response);
|
|
216
|
+
const policy = selectedCachePolicy(response);
|
|
217
|
+
if (response.status === 304 || policy !== null && forbidsSharedStorage(policy) || !response.headers.has("ETag") && !response.headers.has("Last-Modified")) return response;
|
|
218
|
+
const headers = new Headers(response.headers);
|
|
219
|
+
headers.delete("ETag");
|
|
220
|
+
headers.delete("Last-Modified");
|
|
221
|
+
return new Response(response.body, {
|
|
222
|
+
headers,
|
|
223
|
+
status: response.status,
|
|
224
|
+
statusText: response.statusText
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* `no-cache` and a field-qualified `private` still let a shared cache store the
|
|
229
|
+
* response (RFC 9111 sections 5.2.2.4 and 5.2.2.7).
|
|
230
|
+
*/
|
|
231
|
+
function forbidsSharedStorage(policy) {
|
|
232
|
+
return splitCacheControlDirectives(policy).some((directive) => {
|
|
233
|
+
const normalized = directive.toLowerCase();
|
|
234
|
+
return normalized === "private" || normalized.split("=", 1)[0].trim() === "no-store";
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function selectedCachePolicy(response) {
|
|
238
|
+
return response.headers.get("Cloudflare-CDN-Cache-Control") ?? response.headers.get("CDN-Cache-Control") ?? response.headers.get("Cache-Control");
|
|
193
239
|
}
|
|
194
240
|
/** Give a cacheable document a validator, so Workers Cache revalidates it conditionally. */
|
|
195
241
|
function withRevalidationValidator(response) {
|
|
196
242
|
if (response.headers.has("ETag") || response.headers.has("Last-Modified")) return response;
|
|
197
|
-
const policy =
|
|
243
|
+
const policy = selectedCachePolicy(response);
|
|
198
244
|
if (policy === null || isNonCacheableCacheControl(policy)) return response;
|
|
199
245
|
const headers = new Headers(response.headers);
|
|
200
246
|
headers.set("Last-Modified", (/* @__PURE__ */ new Date()).toUTCString());
|
|
@@ -319,14 +365,21 @@ var VinextCachedResponse = class extends WorkerEntrypoint {
|
|
|
319
365
|
if (invocation.expectedResponseStageBuildIdentity !== void 0 && invocation.expectedResponseStageBuildIdentity !== getVinextCdnBuildIdentity()) return stampResponseStageBuildIdentity(responseStageUnavailable());
|
|
320
366
|
const restored = restoreResponseStageRequest(request, invocation.requestUrl, invocation.requestMethod);
|
|
321
367
|
const isAppPageDocument = isAppPageDocumentInvocation(invocation);
|
|
368
|
+
const isPagesPage = isPagesPageInvocation(invocation);
|
|
322
369
|
const response = await invokeResponseStage(restored.request, this.env, context, isAppPageDocument && rendersWholeDocument(request) ? {
|
|
323
370
|
...invocation,
|
|
324
371
|
options: {
|
|
325
372
|
...invocation.options,
|
|
326
373
|
renderWholeDocument: true
|
|
327
374
|
}
|
|
375
|
+
} : isPagesPage && revalidatesStoredEntry(request) ? {
|
|
376
|
+
...invocation,
|
|
377
|
+
options: {
|
|
378
|
+
...invocation.options,
|
|
379
|
+
regeneratesStoredEntry: true
|
|
380
|
+
}
|
|
328
381
|
} : invocation);
|
|
329
|
-
return stampResponseStageBuildIdentity(restored.didAccessRequestCf() || hasTaggedCustomVary(response) ? preventResponseCaching(response) : isAppPageDocument ? withRevalidationValidator(response) : response);
|
|
382
|
+
return stampResponseStageBuildIdentity(restored.didAccessRequestCf() || hasTaggedCustomVary(response) ? preventResponseCaching(response) : isAppPageDocument ? withRevalidationValidator(response) : isPagesPage ? withPagesRevalidationValidator(response) : response);
|
|
330
383
|
}
|
|
331
384
|
async purge(options) {
|
|
332
385
|
const cache = Reflect.get(this.ctx, "cache");
|
package/dist/cache/config.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ type ResponseStoreStorageOptions = {
|
|
|
6
6
|
};
|
|
7
7
|
export declare function createWorkersResponseStoreSelfContainedConfig(options: ResponseStoreStorageOptions): Promise<{
|
|
8
8
|
cache: {
|
|
9
|
-
enabled:
|
|
9
|
+
enabled: false;
|
|
10
10
|
};
|
|
11
11
|
exports: {
|
|
12
12
|
CacheMetadata: {
|
|
@@ -45,7 +45,7 @@ export declare function createWorkersResponseStoreServiceBindingConfig(options:
|
|
|
45
45
|
accountId: string | undefined;
|
|
46
46
|
serviceBindingWorker: {
|
|
47
47
|
cache: {
|
|
48
|
-
enabled:
|
|
48
|
+
enabled: false;
|
|
49
49
|
};
|
|
50
50
|
exports: {
|
|
51
51
|
CacheMetadata: {
|
|
@@ -84,7 +84,7 @@ export declare function createWorkersResponseStoreServiceBindingConfig(options:
|
|
|
84
84
|
env: {
|
|
85
85
|
RESPONSE_STORE: import("cf/config").WorkerBinding<{
|
|
86
86
|
cache: {
|
|
87
|
-
enabled:
|
|
87
|
+
enabled: false;
|
|
88
88
|
};
|
|
89
89
|
exports: {
|
|
90
90
|
CacheMetadata: {
|
package/dist/cache/config.js
CHANGED
|
@@ -10,7 +10,7 @@ const RESPONSE_STORE_SERVICE_ENTRYPOINT = "@cloudflare/workers-response-store/se
|
|
|
10
10
|
async function createResponseStoreWorkerConfig(options) {
|
|
11
11
|
const { bindings, exports } = await import("cf/config");
|
|
12
12
|
return {
|
|
13
|
-
cache: { enabled:
|
|
13
|
+
cache: { enabled: false },
|
|
14
14
|
exports: {
|
|
15
15
|
[CACHE_METADATA_CLASS]: {
|
|
16
16
|
...exports.durableObject({ storage: "sqlite" }),
|
package/dist/cache/kv-key.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { fnv1a64 } from "vinext/internal/utils/hash";
|
|
2
3
|
//#region src/cache/kv-key.ts
|
|
3
4
|
/** Key prefix for tag invalidation timestamps. */
|
|
@@ -27,19 +28,37 @@ function kvKeyByteLength(key) {
|
|
|
27
28
|
return KV_KEY_ENCODER.encode(key).length;
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
31
|
+
* Digest an entry key that is too long to store verbatim. Logical keys can
|
|
32
|
+
* carry request input, so the digest must be collision resistant: two keys
|
|
33
|
+
* sharing a digest would share an entry.
|
|
34
|
+
*/
|
|
35
|
+
function digestEntryKey(key) {
|
|
36
|
+
return createHash("sha256").update(key).digest("hex");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Digest a tag that is too long or unsafe to store verbatim. Tags sharing a
|
|
40
|
+
* digest share one invalidation marker, which can only invalidate extra
|
|
41
|
+
* entries. Markers are permanent, so a new digest would drop the
|
|
42
|
+
* invalidations already recorded for entries still in KV.
|
|
43
|
+
*/
|
|
44
|
+
function digestTag(tag) {
|
|
45
|
+
return fnv1a64(tag);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
30
48
|
* Keep short app prefixes readable, but bound the namespace portion so a
|
|
31
49
|
* hashed entry or tag key is always able to fit within Cloudflare KV's limit.
|
|
32
50
|
*/
|
|
33
51
|
function normalizeAppPrefix(appPrefix) {
|
|
34
52
|
if (!appPrefix) return "";
|
|
35
53
|
const prefix = `${appPrefix}:`;
|
|
36
|
-
if (kvKeyByteLength(`${prefix}cache:${HASHED_KEY_PREFIX}${
|
|
54
|
+
if (kvKeyByteLength(`${prefix}cache:${HASHED_KEY_PREFIX}${digestEntryKey("")}`) <= KV_KEY_MAX_BYTES) return prefix;
|
|
37
55
|
return `__app:${fnv1a64(appPrefix)}:`;
|
|
38
56
|
}
|
|
39
57
|
function buildStorageKey(prefix, categoryPrefix, logicalKey) {
|
|
40
58
|
const key = `${prefix}${categoryPrefix}${logicalKey}`;
|
|
41
59
|
if (!(categoryPrefix === TAG_PREFIX && logicalKey.includes(":")) && kvKeyByteLength(key) <= KV_KEY_MAX_BYTES) return key;
|
|
42
|
-
|
|
60
|
+
const digest = categoryPrefix === TAG_PREFIX ? digestTag(logicalKey) : digestEntryKey(logicalKey);
|
|
61
|
+
return `${prefix}${categoryPrefix}${HASHED_KEY_PREFIX}${digest}`;
|
|
43
62
|
}
|
|
44
63
|
/** Create the deterministic key namespace for runtime cache operations. */
|
|
45
64
|
function createKvKeySpace(appPrefix) {
|
|
@@ -2,10 +2,11 @@ import { SHARED_RESPONSE_STAGE_HEADER, finalizeGatewayResponse } from "./browser
|
|
|
2
2
|
import { captureResponseStoreDataRegeneration, runWithResponseStoreInvocation, setResponseStore } from "./response-store-data.runtime.js";
|
|
3
3
|
import { VINEXT_PARAMS_HEADER, VINEXT_RENDERED_PATH_AND_SEARCH_HEADER, VINEXT_SPECIAL_ERROR_STATUS_HEADER } from "vinext/internal/server/headers";
|
|
4
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";
|
|
5
|
-
import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
|
|
5
|
+
import { hasSharedCacheCredentials, isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
|
|
6
6
|
import { loadVinextRequestStage } from "vinext/server/request-stage";
|
|
7
7
|
import { loadVinextResponseStage } from "vinext/server/response-stage";
|
|
8
8
|
import { traceCachedResponseStart } from "vinext/internal/server/response-start-tracing";
|
|
9
|
+
import { isPageNotModified } from "vinext/internal/server/http-conditional";
|
|
9
10
|
//#region src/cache/response-store-adapter.worker.ts
|
|
10
11
|
const ROUTE_REVALIDATOR_ID = "vinext:response";
|
|
11
12
|
const RESPONSE_STORE_KEY_PARAM = "__workers_response_store";
|
|
@@ -41,7 +42,7 @@ function replayHeaders(request) {
|
|
|
41
42
|
function isReplayableInvocation(request, props) {
|
|
42
43
|
const method = request.method.toUpperCase();
|
|
43
44
|
if (method !== "GET" && method !== "HEAD") return false;
|
|
44
|
-
if (
|
|
45
|
+
if (hasSharedCacheCredentials(request.headers)) return false;
|
|
45
46
|
return !(props && typeof props === "object" && ["draftModeCookie", "middlewareCookieOverlay"].some((name) => name in props && Reflect.get(props, name) !== null));
|
|
46
47
|
}
|
|
47
48
|
function prepareInvocation(request, props) {
|
|
@@ -96,6 +97,7 @@ function createVinextResponseStoreOptions(configuration) {
|
|
|
96
97
|
const invocation = parseInvocation(input.args.at(-1));
|
|
97
98
|
const response = await invokeResponseStage(restoreRequest(invocation), invocation.props, env, ctx, {
|
|
98
99
|
cache: "shared",
|
|
100
|
+
regeneratesStoredEntry: true,
|
|
99
101
|
renderWholeDocument: true
|
|
100
102
|
});
|
|
101
103
|
if (!isCacheable(response)) {
|
|
@@ -160,7 +162,59 @@ function isStoredErrorStatus(status) {
|
|
|
160
162
|
}
|
|
161
163
|
function isCacheable(response) {
|
|
162
164
|
const policy = response.headers.get("Cloudflare-CDN-Cache-Control") ?? response.headers.get("CDN-Cache-Control") ?? response.headers.get("Cache-Control");
|
|
163
|
-
return response.status >= 200 && (response.status < 400 || isStoredErrorStatus(response.status)) && policy !== null && !isNonCacheableCacheControl(policy);
|
|
165
|
+
return response.status >= 200 && response.status !== 304 && (response.status < 400 || isStoredErrorStatus(response.status)) && policy !== null && !isNonCacheableCacheControl(policy);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* A page's 304 headers. Next.js sends the 304 before it sets the payload's
|
|
169
|
+
* headers, so it carries no representation or body framing headers.
|
|
170
|
+
*/
|
|
171
|
+
function notModifiedHeaders(source) {
|
|
172
|
+
const headers = new Headers(source);
|
|
173
|
+
for (const name of [
|
|
174
|
+
"Content-Encoding",
|
|
175
|
+
"Content-Length",
|
|
176
|
+
"Content-Type",
|
|
177
|
+
"Transfer-Encoding"
|
|
178
|
+
]) headers.delete(name);
|
|
179
|
+
return headers;
|
|
180
|
+
}
|
|
181
|
+
/** Send the 304 for a page whose ETag matched the request's If-None-Match. */
|
|
182
|
+
function notModifiedResponse(response) {
|
|
183
|
+
response.body?.cancel().catch(() => {});
|
|
184
|
+
return new Response(null, {
|
|
185
|
+
headers: notModifiedHeaders(response.headers),
|
|
186
|
+
status: 304
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Whether Next.js sends this stored response through `sendRenderResult`, which
|
|
191
|
+
* answers a conditional request by the page's ETag. It sends every App page
|
|
192
|
+
* that way. A Pages redirect, and a Pages data request's notFound, are sent
|
|
193
|
+
* without an ETag, as is a route handler's response. A Pages API response is
|
|
194
|
+
* never stored.
|
|
195
|
+
*/
|
|
196
|
+
function answersConditionalRequest(responseStageProps, status) {
|
|
197
|
+
if (responseStageProps === null || typeof responseStageProps !== "object") return false;
|
|
198
|
+
const kind = Reflect.get(responseStageProps, "kind");
|
|
199
|
+
if (kind === "app-page" || kind === "app-not-found") return true;
|
|
200
|
+
let isDataRequest;
|
|
201
|
+
if (kind === "hybrid-pages") {
|
|
202
|
+
if (Reflect.get(responseStageProps, "resourceKind") !== "page") return false;
|
|
203
|
+
isDataRequest = Reflect.get(responseStageProps, "isDataRequest") === true;
|
|
204
|
+
} else if (kind === "pages-page") {
|
|
205
|
+
const renderOptions = Reflect.get(responseStageProps, "renderOptions");
|
|
206
|
+
isDataRequest = renderOptions !== null && typeof renderOptions === "object" && Reflect.get(renderOptions, "isDataReq") === true;
|
|
207
|
+
} else return false;
|
|
208
|
+
return status >= 200 && status < 300 || status === 404 && !isDataRequest;
|
|
209
|
+
}
|
|
210
|
+
/** Answer a stored page's conditional request as Next.js does for a cached page. */
|
|
211
|
+
function storedForRequest(stored, request, responseStageProps) {
|
|
212
|
+
const etag = stored.headers.get("ETag");
|
|
213
|
+
return etag && answersConditionalRequest(responseStageProps, stored.status) && isPageNotModified({
|
|
214
|
+
cacheControl: request.headers.get("Cache-Control") ?? void 0,
|
|
215
|
+
ifModifiedSince: request.headers.get("If-Modified-Since") ?? void 0,
|
|
216
|
+
ifNoneMatch: request.headers.get("If-None-Match") ?? void 0
|
|
217
|
+
}, etag) ? notModifiedResponse(stored) : stored;
|
|
164
218
|
}
|
|
165
219
|
async function readStoredResponse(key) {
|
|
166
220
|
try {
|
|
@@ -235,11 +289,11 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
235
289
|
const key = await cacheRequest(invocation);
|
|
236
290
|
const stored = await readStoredResponse(key);
|
|
237
291
|
if (stored) {
|
|
238
|
-
if (!rscKey) return publicResponse(stored, "HIT", props);
|
|
292
|
+
if (!rscKey) return publicResponse(storedForRequest(stored, stageRequest, props), "HIT", props);
|
|
239
293
|
const storedRsc = await readStoredResponse(rscKey);
|
|
240
294
|
if (storedRsc) {
|
|
241
295
|
storedRsc.body?.cancel().catch(() => {});
|
|
242
|
-
return publicResponse(stored, "HIT", props);
|
|
296
|
+
return publicResponse(storedForRequest(stored, stageRequest, props), "HIT", props);
|
|
243
297
|
}
|
|
244
298
|
stored.body?.cancel().catch(() => {});
|
|
245
299
|
}
|
|
@@ -252,6 +306,7 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
252
306
|
replayable: isReplayableInvocation(stageRequest, props),
|
|
253
307
|
serialized: options.cacheIdentity ? serializeInvocation(stageRequest, props) : serializedInvocation
|
|
254
308
|
});
|
|
309
|
+
const foreground = (response) => capture.notModified ? notModifiedResponse(response) : response;
|
|
255
310
|
if (capture.admittedResponse) {
|
|
256
311
|
ctx.waitUntil(capture.admittedResponse.then(async (admitted) => {
|
|
257
312
|
if (!isCacheable(admitted)) {
|
|
@@ -271,17 +326,23 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
271
326
|
error: error instanceof Error ? error.message : String(error)
|
|
272
327
|
}));
|
|
273
328
|
}));
|
|
274
|
-
return publicResponse(rendered, "MISS", props, true);
|
|
329
|
+
if (!capture.notModified) return publicResponse(rendered, "MISS", props, true);
|
|
330
|
+
const notModified = notModifiedResponse(rendered);
|
|
331
|
+
const admitted = await capture.admittedResponse.catch(() => null);
|
|
332
|
+
return admitted ? publicResponse(new Response(null, {
|
|
333
|
+
headers: notModifiedHeaders(admitted.headers),
|
|
334
|
+
status: 304
|
|
335
|
+
}), "MISS", props) : publicResponse(notModified, "MISS", props, true);
|
|
275
336
|
}
|
|
276
337
|
if (!isCacheable(rendered)) {
|
|
277
338
|
capture?.rscData?.catch(() => {});
|
|
278
|
-
return publicResponse(rendered, "BYPASS", props);
|
|
339
|
+
return publicResponse(foreground(rendered), "BYPASS", props);
|
|
279
340
|
}
|
|
280
341
|
if (rscSeed && !capture?.rscData) {
|
|
281
342
|
await rendered.body?.cancel();
|
|
282
343
|
throw new Error("Vinext response-store warmup did not capture the App page RSC payload");
|
|
283
344
|
}
|
|
284
|
-
const [
|
|
345
|
+
const [foregroundBody, cacheBody] = rendered.body ? rendered.body.tee() : [null, null];
|
|
285
346
|
const cacheResponse = withoutRequestScopedHeaders(new Response(cacheBody, rendered), props);
|
|
286
347
|
try {
|
|
287
348
|
await responseStore.put(key, cacheResponse, {
|
|
@@ -324,7 +385,7 @@ const handler = { async fetch(request, env, ctx) {
|
|
|
324
385
|
}
|
|
325
386
|
});
|
|
326
387
|
}
|
|
327
|
-
return publicResponse(new Response(
|
|
388
|
+
return publicResponse(foreground(new Response(foregroundBody, rendered)), "MISS", props);
|
|
328
389
|
};
|
|
329
390
|
const { handleRequestStage } = await loadVinextRequestStage();
|
|
330
391
|
const sharedResponses = /* @__PURE__ */ new Map();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import createCloudflareCdnCacheAdapter from "./cdn-adapter.runtime.js";
|
|
2
|
-
import { captureResponseStoreRscData, deferResponseStoreAdmission } from "./response-store-data.runtime.js";
|
|
2
|
+
import { 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
|
+
deferNotModifiedResponse() {
|
|
37
|
+
return deferResponseStoreNotModified();
|
|
38
|
+
}
|
|
36
39
|
async revalidateTag() {}
|
|
37
40
|
};
|
|
38
41
|
function createResponseStoreCdnCacheAdapter(context) {
|
|
@@ -4,6 +4,8 @@ import { CacheHandler, CacheHandlerValue, IncrementalCacheValue } from "vinext/s
|
|
|
4
4
|
export type ResponseStoreInvocationCapture = {
|
|
5
5
|
admittedResponse?: Promise<Response>;
|
|
6
6
|
captureRscData?: boolean;
|
|
7
|
+
/** The render matched the request's If-None-Match; the adapter sends the 304. */
|
|
8
|
+
notModified?: boolean;
|
|
7
9
|
rscData?: Promise<ArrayBuffer>;
|
|
8
10
|
streamResponse?: boolean;
|
|
9
11
|
};
|
|
@@ -15,6 +17,8 @@ export declare function setResponseStore(store: WorkersResponseStore): void;
|
|
|
15
17
|
export declare function runWithResponseStoreInvocation<T>(serialized: string, replayable: boolean, callback: () => T, capture?: ResponseStoreInvocationCapture): T;
|
|
16
18
|
/** Retain the RSC side stream only when the outer response-store invocation requested it. */
|
|
17
19
|
export declare function captureResponseStoreRscData(rscData: Promise<ArrayBuffer>): void;
|
|
20
|
+
/** A shared invocation stores the page, so its adapter sends the page's 304. */
|
|
21
|
+
export declare function deferResponseStoreNotModified(): boolean;
|
|
18
22
|
/** Stream the foreground body while retaining an independent admission branch. */
|
|
19
23
|
export declare function deferResponseStoreAdmission(response: Response, complete: (response: Response) => Promise<Response>): Response | null;
|
|
20
24
|
/** Re-render an invocation and return the exact rewritten data-cache entry. */
|
|
@@ -33,6 +33,13 @@ function captureResponseStoreRscData(rscData) {
|
|
|
33
33
|
if (capture?.captureRscData) capture.rscData = rscData;
|
|
34
34
|
else rscData.catch(() => {});
|
|
35
35
|
}
|
|
36
|
+
/** A shared invocation stores the page, so its adapter sends the page's 304. */
|
|
37
|
+
function deferResponseStoreNotModified() {
|
|
38
|
+
const capture = invocationStorage.getStore()?.capture;
|
|
39
|
+
if (!capture) return false;
|
|
40
|
+
capture.notModified = true;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
36
43
|
/** Stream the foreground body while retaining an independent admission branch. */
|
|
37
44
|
function deferResponseStoreAdmission(response, complete) {
|
|
38
45
|
const capture = invocationStorage.getStore()?.capture;
|
|
@@ -328,4 +335,4 @@ function createResponseStoreDataCacheAdapter() {
|
|
|
328
335
|
return new WorkersResponseStoreCacheHandler();
|
|
329
336
|
}
|
|
330
337
|
//#endregion
|
|
331
|
-
export { CACHE_FUNCTION_REVALIDATOR_ID, DATA_REVALIDATOR_ID, WorkersResponseStoreCacheHandler, captureResponseStoreDataRegeneration, captureResponseStoreRscData, createResponseStoreDataCacheAdapter as default, deferResponseStoreAdmission, runWithResponseStoreInvocation, setResponseStore };
|
|
338
|
+
export { CACHE_FUNCTION_REVALIDATOR_ID, DATA_REVALIDATOR_ID, WorkersResponseStoreCacheHandler, captureResponseStoreDataRegeneration, captureResponseStoreRscData, createResponseStoreDataCacheAdapter as default, deferResponseStoreAdmission, deferResponseStoreNotModified, runWithResponseStoreInvocation, setResponseStore };
|
|
@@ -2,8 +2,8 @@ import { STATIC_ASSET_CACHE_PATH } from "./static-assets-adapter.shared.js";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
-
import { readPrerenderManifest } from "vinext/internal/server/prerender-manifest";
|
|
6
|
-
import { appIsrCacheKey, pagesIsrCacheKey } from "vinext/internal/server/isr-cache";
|
|
5
|
+
import { isFallbackShellArtifactPath, readPrerenderManifest } from "vinext/internal/server/prerender-manifest";
|
|
6
|
+
import { appIsrCacheKey, appIsrFallbackShellKey, pagesIsrCacheKey } from "vinext/internal/server/isr-cache";
|
|
7
7
|
import { extractVinextNextDataJson } from "vinext/internal/client/vinext-next-data";
|
|
8
8
|
import { normalizePregeneratedPathname } from "vinext/internal/server/pregenerated-concrete-paths";
|
|
9
9
|
import { getAppRouteOutputPath, getOutputPath, getRscOutputPath } from "vinext/internal/utils/prerender-output-paths";
|
|
@@ -67,8 +67,9 @@ function finalizeStaticAssetsPrerenderOutput(root, options = {}) {
|
|
|
67
67
|
const pathname = route.path ?? route.route;
|
|
68
68
|
const cachePathname = normalizePregeneratedPathname(pathname);
|
|
69
69
|
if (route.router === "app") {
|
|
70
|
-
|
|
71
|
-
count += Number(writeCacheAsset(outputDir, index,
|
|
70
|
+
const appKey = isFallbackShellArtifactPath(pathname, route) ? appIsrFallbackShellKey : appIsrCacheKey;
|
|
71
|
+
count += Number(writeCacheAsset(outputDir, index, appKey(cachePathname, "html", manifest.buildId), "html", path.join(prerenderDir, getOutputPath(pathname, manifest.trailingSlash ?? false)), route));
|
|
72
|
+
count += Number(writeCacheAsset(outputDir, index, appKey(cachePathname, "rsc", manifest.buildId), "rsc", path.join(prerenderDir, getRscOutputPath(pathname)), route));
|
|
72
73
|
} else if (route.router === "pages") {
|
|
73
74
|
const sourcePath = path.join(prerenderDir, getOutputPath(pathname, manifest.trailingSlash ?? false));
|
|
74
75
|
if (!fs.existsSync(sourcePath)) continue;
|
|
@@ -9,9 +9,11 @@ export declare class StaticAssetsCacheAdapter implements CdnCacheAdapter {
|
|
|
9
9
|
readonly assets: AssetFetcher;
|
|
10
10
|
readonly hasPrerenderedPages = true;
|
|
11
11
|
readonly ownsBackgroundRevalidation = false;
|
|
12
|
-
private
|
|
12
|
+
private index;
|
|
13
|
+
private pendingIndex;
|
|
13
14
|
constructor(assets: AssetFetcher);
|
|
14
15
|
private loadIndex;
|
|
16
|
+
private fetchIndex;
|
|
15
17
|
get(key: string): Promise<CacheHandlerValue | null>;
|
|
16
18
|
set(_key: string, _data: IncrementalCacheValue | null, _ctx?: Record<string, unknown>): Promise<void>;
|
|
17
19
|
buildResponseHeaders(input: CdnCacheableHeaderInput): CdnResponseHeaders;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { STATIC_ASSET_CACHE_PATH } from "./static-assets-adapter.shared.js";
|
|
2
2
|
import { DefaultCdnCacheAdapter } from "vinext/shims/cdn-cache";
|
|
3
|
+
import { getRequestExecutionContext } from "vinext/shims/request-context";
|
|
3
4
|
//#region src/cache/static-assets-adapter.runtime.ts
|
|
4
5
|
async function cacheAssetId(key) {
|
|
5
6
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(key));
|
|
@@ -12,12 +13,32 @@ var StaticAssetsCacheAdapter = class {
|
|
|
12
13
|
assets;
|
|
13
14
|
hasPrerenderedPages = true;
|
|
14
15
|
ownsBackgroundRevalidation = false;
|
|
15
|
-
|
|
16
|
+
index;
|
|
17
|
+
pendingIndex;
|
|
16
18
|
constructor(assets) {
|
|
17
19
|
this.assets = assets;
|
|
18
20
|
}
|
|
19
21
|
loadIndex() {
|
|
20
|
-
|
|
22
|
+
if (this.index !== void 0) return Promise.resolve(this.index);
|
|
23
|
+
if (this.pendingIndex) return this.pendingIndex;
|
|
24
|
+
const ctx = getRequestExecutionContext();
|
|
25
|
+
if (!ctx) return this.fetchIndex();
|
|
26
|
+
const pending = this.fetchIndex().finally(() => {
|
|
27
|
+
if (this.pendingIndex === pending) this.pendingIndex = void 0;
|
|
28
|
+
});
|
|
29
|
+
this.pendingIndex = pending;
|
|
30
|
+
ctx.waitUntil(pending.catch(() => {}));
|
|
31
|
+
return pending;
|
|
32
|
+
}
|
|
33
|
+
async fetchIndex() {
|
|
34
|
+
const response = await this.assets.fetch(`https://vinext.invalid${STATIC_ASSET_CACHE_PATH}/index.json`);
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
response.body?.cancel().catch(() => {});
|
|
37
|
+
if (response.status === 404) this.index = null;
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const value = await response.json();
|
|
41
|
+
return this.index = value !== null && typeof value === "object" ? value : null;
|
|
21
42
|
}
|
|
22
43
|
async get(key) {
|
|
23
44
|
const id = await cacheAssetId(key);
|
package/dist/cdn-warm.d.ts
CHANGED
|
@@ -43,7 +43,6 @@ export declare const DEFAULT_STAGED_READINESS_INTERVAL_MS = 1000;
|
|
|
43
43
|
export declare const DEFAULT_STAGED_READINESS_PHASE_TIMEOUT_MS = 120000;
|
|
44
44
|
export type PrerenderCdnWarmOptions = Omit<CdnWarmOptions, "paths"> & {
|
|
45
45
|
root: string;
|
|
46
|
-
includeFallbackShells?: boolean;
|
|
47
46
|
/** Use the manifest build identity unless the configured adapter cannot expose it. */
|
|
48
47
|
validateBuildIdentity?: boolean;
|
|
49
48
|
};
|
|
@@ -92,13 +91,11 @@ export type PrerenderWarmPlan = {
|
|
|
92
91
|
};
|
|
93
92
|
type PrerenderWarmPlanOptions = {
|
|
94
93
|
includeCanonicalRsc?: boolean;
|
|
95
|
-
includeFallbackShells?: boolean;
|
|
96
94
|
strict?: boolean;
|
|
97
95
|
};
|
|
98
96
|
export declare function createPrerenderWarmPlan(root: string, manifest: PrerenderPathManifest, options?: PrerenderWarmPlanOptions): PrerenderWarmPlan;
|
|
99
97
|
export declare function readPrerenderWarmPlan(root: string, options?: PrerenderWarmPlanOptions): PrerenderWarmPlan;
|
|
100
98
|
export declare function readPrerenderWarmPaths(root: string, options?: {
|
|
101
|
-
includeFallbackShells?: boolean;
|
|
102
99
|
strict?: boolean;
|
|
103
100
|
}): string[];
|
|
104
101
|
export declare function getWarmPathsFromPrerenderManifest(manifest: PrerenderManifest, options?: PrerenderedPathSelectionOptions): string[];
|
package/dist/cdn-warm.js
CHANGED
|
@@ -6,7 +6,7 @@ import { randomUUID } from "node:crypto";
|
|
|
6
6
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
7
7
|
import { VINEXT_CACHE_HEADER, VINEXT_PRERENDER_READINESS_HEADER, VINEXT_PRERENDER_READINESS_PATH, VINEXT_PRERENDER_SECRET_HEADER } from "vinext/internal/server/headers";
|
|
8
8
|
import { PRERENDER_PATHS_MANIFEST } from "vinext/internal/build/prerender-paths";
|
|
9
|
-
import { getPrerenderedConcretePaths
|
|
9
|
+
import { getPrerenderedConcretePaths } from "vinext/internal/server/prerender-manifest";
|
|
10
10
|
import { VINEXT_RSC_BUILD_ID_HEADER, VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_VARY_HEADER as VINEXT_RSC_VARY_HEADER$1, createCanonicalLoadingShellRscRequestHeaders, createCanonicalRscRequestHeaders, createCanonicalRscRequestUrl, createRscRequestUrl } from "vinext/internal/server/app-rsc-cache-busting";
|
|
11
11
|
import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
|
|
12
12
|
import { normalizePathTrailingSlash } from "vinext/shims/url-utils";
|
|
@@ -73,17 +73,7 @@ function createPrerenderWarmPlan(root, manifest, options) {
|
|
|
73
73
|
const supportsCanonicalRsc = (manifest.responseVary === "verbatim" || options?.includeCanonicalRsc === true) && manifest.rscPaths !== void 0 && manifest.rscBuildId !== void 0;
|
|
74
74
|
const applyConfig = (pathname) => applyWarmPathConfig(pathname, manifest);
|
|
75
75
|
const routePatterns = manifest.routePatterns ? Object.fromEntries(Object.entries(manifest.routePatterns).map(([pathname, route]) => [pathname.includes("/_next/data/") ? pathname : applyConfig(pathname), route])) : void 0;
|
|
76
|
-
|
|
77
|
-
if (options?.includeFallbackShells === true) {
|
|
78
|
-
const prerenderManifest = readPrerenderManifest(path.join(root, "dist", "server", "vinext-prerender.json"));
|
|
79
|
-
if (!prerenderManifest) console.warn("[vinext] CDN warmup fallback shells requested, but prerender manifest not found; warming build-discovered paths only.");
|
|
80
|
-
else if (!prerenderManifest.buildId || prerenderManifest.buildId !== manifest.buildId) {
|
|
81
|
-
const message = "[vinext] CDN warmup skipped: prerender manifest buildId does not match dist/server/BUILD_ID.";
|
|
82
|
-
if (options.strict) throw new Error(message);
|
|
83
|
-
console.warn(message);
|
|
84
|
-
htmlPaths = [];
|
|
85
|
-
} else htmlPaths = getPrerenderedConcretePaths(prerenderManifest, { includeFallbackShells: true }).filter((pathname) => pathname.startsWith("/")).filter((pathname) => !manifest.excludedWarmPaths?.includes(pathname)).map(applyConfig);
|
|
86
|
-
}
|
|
76
|
+
const htmlPaths = pathPlan.paths;
|
|
87
77
|
return {
|
|
88
78
|
...manifest.appPaths ? { appPaths: manifest.appPaths.map(applyConfig) } : {},
|
|
89
79
|
buildId: manifest.buildId,
|
|
@@ -959,10 +949,7 @@ async function warmCdnCache(options) {
|
|
|
959
949
|
return result;
|
|
960
950
|
}
|
|
961
951
|
async function warmCdnCacheFromPrerender(options) {
|
|
962
|
-
const plan = readPrerenderWarmPlan(options.root, {
|
|
963
|
-
includeFallbackShells: options.includeFallbackShells,
|
|
964
|
-
strict: options.strict
|
|
965
|
-
});
|
|
952
|
+
const plan = readPrerenderWarmPlan(options.root, { strict: options.strict });
|
|
966
953
|
const warmPlan = {
|
|
967
954
|
deploymentId: plan.deploymentId,
|
|
968
955
|
loadingShellPaths: plan.loadingShellPaths,
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { deploy, parseDeployArgs } from "./deploy.js";
|
|
3
|
-
import { printDeployHelp } from "./deploy-help.js";
|
|
3
|
+
import { printDeployHelp, printPreviewsDeployHelp } from "./deploy-help.js";
|
|
4
|
+
import { parsePreviewsDeployArgs } from "./previews-deploy.js";
|
|
4
5
|
import fs from "node:fs";
|
|
5
6
|
//#region src/cli.ts
|
|
6
7
|
const VERSION = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version;
|
|
@@ -11,6 +12,10 @@ function printHelp(commandName) {
|
|
|
11
12
|
printDeployHelp();
|
|
12
13
|
return;
|
|
13
14
|
}
|
|
15
|
+
if (commandName === "previews") {
|
|
16
|
+
printPreviewsDeployHelp();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
14
19
|
console.log(`
|
|
15
20
|
vinext-cloudflare v${VERSION}
|
|
16
21
|
|
|
@@ -21,7 +26,8 @@ function printHelp(commandName) {
|
|
|
21
26
|
vp exec vinext-cloudflare <command> Run the locally installed bin
|
|
22
27
|
|
|
23
28
|
Commands:
|
|
24
|
-
deploy
|
|
29
|
+
deploy Deploy to Cloudflare Workers
|
|
30
|
+
previews deploy [preview-name] Deploy a Cloudflare Worker Preview
|
|
25
31
|
|
|
26
32
|
Options:
|
|
27
33
|
-h, --help Show this help
|
|
@@ -62,13 +68,36 @@ async function deployCommand() {
|
|
|
62
68
|
dangerouslyPromoteOnCdnWarmError: parsed.dangerouslyPromoteOnCdnWarmError,
|
|
63
69
|
warmCdnPromote: parsed.warmCdnPromote,
|
|
64
70
|
warmCdnPromotionDelay: parsed.warmCdnPromotionDelay,
|
|
65
|
-
warmCdnIncludeFallbacks: parsed.warmCdnIncludeFallbacks,
|
|
66
71
|
experimentalTPR: parsed.experimentalTPR,
|
|
67
72
|
tprCoverage: parsed.tprCoverage,
|
|
68
73
|
tprLimit: parsed.tprLimit,
|
|
69
74
|
tprWindow: parsed.tprWindow
|
|
70
75
|
});
|
|
71
76
|
}
|
|
77
|
+
async function previewsCommand() {
|
|
78
|
+
const subcommand = rawArgs[0];
|
|
79
|
+
if (subcommand !== "deploy") {
|
|
80
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
81
|
+
printHelp("previews");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`Unknown previews command: ${subcommand}`);
|
|
85
|
+
}
|
|
86
|
+
const options = parsePreviewsDeployArgs(rawArgs.slice(1));
|
|
87
|
+
if (options.help) {
|
|
88
|
+
printHelp("previews");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
await deploy({
|
|
92
|
+
root: process.cwd(),
|
|
93
|
+
env: options.mode,
|
|
94
|
+
skipBuild: options.skipBuild,
|
|
95
|
+
target: {
|
|
96
|
+
type: "preview",
|
|
97
|
+
name: options.name
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
72
101
|
if (command === "--version" || command === "-v") {
|
|
73
102
|
console.log(`vinext-cloudflare v${VERSION}`);
|
|
74
103
|
process.exit(0);
|
|
@@ -84,6 +113,12 @@ switch (command) {
|
|
|
84
113
|
process.exit(1);
|
|
85
114
|
});
|
|
86
115
|
break;
|
|
116
|
+
case "previews":
|
|
117
|
+
previewsCommand().catch((error) => {
|
|
118
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
});
|
|
121
|
+
break;
|
|
87
122
|
default:
|
|
88
123
|
console.error(`Unknown command: ${command}`);
|
|
89
124
|
printHelp();
|
package/dist/deploy-help.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
//#region src/deploy-help.d.ts
|
|
2
2
|
export declare function formatDeployHelp(): string;
|
|
3
3
|
export declare function printDeployHelp(): void;
|
|
4
|
+
export declare function formatPreviewsDeployHelp(): string;
|
|
5
|
+
export declare function printPreviewsDeployHelp(): void;
|
|
4
6
|
//#endregion
|
package/dist/deploy-help.js
CHANGED
|
@@ -67,8 +67,6 @@ function formatDeployHelp() {
|
|
|
67
67
|
verified (never bypasses --warm-cache-certify)
|
|
68
68
|
--warm-cache-promotion-delay <ms>
|
|
69
69
|
Delay before promotion after warmup (default: 15000)
|
|
70
|
-
--warm-cache-include-fallbacks
|
|
71
|
-
Also warm PPR fallback-shell placeholder paths
|
|
72
70
|
-h, --help Show this help
|
|
73
71
|
|
|
74
72
|
Traffic-aware warming:
|
|
@@ -89,11 +87,14 @@ function formatDeployHelp() {
|
|
|
89
87
|
be reused outside the data center reached by the warmup request after cache
|
|
90
88
|
propagation, but the deploy does not wait for every edge location to fill.
|
|
91
89
|
|
|
90
|
+
--preview is an alias for --env preview. It selects a mode/environment,
|
|
91
|
+
not a Preview deployment. Use previews deploy to deploy a Worker Preview.
|
|
92
|
+
|
|
92
93
|
Examples:
|
|
93
94
|
npx @vinext/cloudflare deploy Build and deploy to production
|
|
94
95
|
vpx @vinext/cloudflare deploy Build and deploy with Vite+
|
|
95
96
|
vp exec vinext-cloudflare deploy Run the locally installed Vite+ bin
|
|
96
|
-
vinext-cloudflare deploy --preview Deploy
|
|
97
|
+
vinext-cloudflare deploy --preview Deploy using mode/environment preview
|
|
97
98
|
vinext-cloudflare deploy --env staging Deploy using Cloudflare mode/environment staging
|
|
98
99
|
vinext-cloudflare deploy --config dist/server/wrangler.json Deploy using a generated Wrangler config
|
|
99
100
|
vinext-cloudflare deploy --dry-run Validate setup without building or deploying
|
|
@@ -115,5 +116,32 @@ function formatDeployHelp() {
|
|
|
115
116
|
function printDeployHelp() {
|
|
116
117
|
console.log(formatDeployHelp());
|
|
117
118
|
}
|
|
119
|
+
function formatPreviewsDeployHelp() {
|
|
120
|
+
return `
|
|
121
|
+
vinext-cloudflare previews deploy - Deploy a Cloudflare Worker Preview
|
|
122
|
+
|
|
123
|
+
Usage: vinext-cloudflare previews deploy [preview-name] [options]
|
|
124
|
+
|
|
125
|
+
Builds the app with isPreview=true, then deploys its Preview Build Output
|
|
126
|
+
using cf previews deploy. Requires cloudflare.config.ts and the cf CLI.
|
|
127
|
+
|
|
128
|
+
Arguments:
|
|
129
|
+
preview-name Optional Preview name (cf uses the CI or Git branch if omitted)
|
|
130
|
+
|
|
131
|
+
Options:
|
|
132
|
+
--mode <mode> Vite mode to build and deploy in (default: production)
|
|
133
|
+
--skip-build Deploy existing Preview Build Output without rebuilding
|
|
134
|
+
-h, --help Show this help
|
|
135
|
+
|
|
136
|
+
Examples:
|
|
137
|
+
vinext-cloudflare previews deploy
|
|
138
|
+
vinext-cloudflare previews deploy pr-123
|
|
139
|
+
vinext-cloudflare previews deploy pr-123 --mode staging
|
|
140
|
+
vinext-cloudflare previews deploy pr-123 --skip-build
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
function printPreviewsDeployHelp() {
|
|
144
|
+
console.log(formatPreviewsDeployHelp());
|
|
145
|
+
}
|
|
118
146
|
//#endregion
|
|
119
|
-
export { formatDeployHelp, printDeployHelp };
|
|
147
|
+
export { formatDeployHelp, formatPreviewsDeployHelp, printDeployHelp, printPreviewsDeployHelp };
|
package/dist/deploy.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export type DeployOptions = {
|
|
|
12
12
|
root: string;
|
|
13
13
|
/** Deploy to preview environment (default: production) */
|
|
14
14
|
preview?: boolean;
|
|
15
|
+
/** Deploy to Preview deployment, supported only by cf. */
|
|
16
|
+
target?: {
|
|
17
|
+
type: "preview";
|
|
18
|
+
name?: string;
|
|
19
|
+
};
|
|
15
20
|
/** Wrangler environment name from wrangler.jsonc env.<name> */
|
|
16
21
|
env?: string;
|
|
17
22
|
/** Custom project name for the Worker */
|
|
@@ -62,8 +67,6 @@ export type DeployOptions = {
|
|
|
62
67
|
warmCdnPromote?: boolean;
|
|
63
68
|
/** Delay between successful warmup and promotion in milliseconds */
|
|
64
69
|
warmCdnPromotionDelay?: number;
|
|
65
|
-
/** Include PPR fallback-shell placeholder paths during CDN warmup */
|
|
66
|
-
warmCdnIncludeFallbacks?: boolean;
|
|
67
70
|
/** Select CDN pre-warm routes using traffic analytics */
|
|
68
71
|
experimentalTPR?: boolean;
|
|
69
72
|
/** TPR: traffic coverage percentage target (0–100, default: 90) */
|
|
@@ -104,7 +107,6 @@ export declare function parseDeployArgs(args: string[]): {
|
|
|
104
107
|
dangerouslyPromoteOnCdnWarmError: boolean;
|
|
105
108
|
warmCdnPromote: boolean;
|
|
106
109
|
warmCdnPromotionDelay: number | undefined;
|
|
107
|
-
warmCdnIncludeFallbacks: boolean;
|
|
108
110
|
experimentalTPR: boolean;
|
|
109
111
|
tprCoverage: number | undefined;
|
|
110
112
|
tprLimit: number | undefined;
|
|
@@ -123,6 +125,8 @@ export declare function parseDeployArgs(args: string[]): {
|
|
|
123
125
|
* Passing `undefined` is a no-op; the callback runs with `process.env` untouched.
|
|
124
126
|
*/
|
|
125
127
|
export declare function withCloudflareEnv<T>(env: string | undefined, fn: () => Promise<T>): Promise<T>;
|
|
128
|
+
/** Cloudflare Vite Plugin reads this flag when evaluating the typed config and emitting Build Output. */
|
|
129
|
+
export declare function withCloudflarePreviewBuild<T>(fn: () => Promise<T>): Promise<T>;
|
|
126
130
|
export declare function resolveViteBuildMode(deploymentTool: DeploymentTool, env: string | undefined): string;
|
|
127
131
|
type WranglerDeployArgs = {
|
|
128
132
|
args: string[];
|
|
@@ -136,7 +140,7 @@ export declare function validateWranglerEnvName(env: string): string;
|
|
|
136
140
|
export declare function buildWranglerDeployArgs(options: Pick<DeployOptions, "preview" | "env" | "name" | "config">): WranglerDeployArgs;
|
|
137
141
|
export declare function resolveDeploymentTool(root: string): DeploymentTool;
|
|
138
142
|
export declare function isCfCliInstalled(root: string, resolvePackageJson?: (root: string) => string | null): boolean;
|
|
139
|
-
export declare function buildCfDeployArgs(options: Pick<DeployOptions, "preview" | "env">): CfDeployArgs;
|
|
143
|
+
export declare function buildCfDeployArgs(options: Pick<DeployOptions, "preview" | "env" | "target">): CfDeployArgs;
|
|
140
144
|
/**
|
|
141
145
|
* Resolve Wrangler's JavaScript CLI entrypoint in node_modules.
|
|
142
146
|
*
|
|
@@ -157,7 +161,7 @@ export declare function buildWranglerInvocation(root: string, options: Pick<Depl
|
|
|
157
161
|
export declare function runWranglerDeploy(root: string, options: Pick<DeployOptions, "preview" | "env" | "name" | "config" | "verbose"> & {
|
|
158
162
|
promote?: boolean;
|
|
159
163
|
}, execute?: typeof spawn): Promise<string>;
|
|
160
|
-
export declare function runCfDeploy(root: string, options: Pick<DeployOptions, "preview" | "env">, execute?: typeof spawn): Promise<string>;
|
|
164
|
+
export declare function runCfDeploy(root: string, options: Pick<DeployOptions, "preview" | "env" | "target">, execute?: typeof spawn): Promise<string>;
|
|
161
165
|
export declare function hasCdnWarmRequests(plan: Omit<CdnWarmRequestPlan, "pagesDataPaths"> & {
|
|
162
166
|
pagesDataPaths?: readonly string[];
|
|
163
167
|
}): boolean;
|
package/dist/deploy.js
CHANGED
|
@@ -16,7 +16,7 @@ import { setTimeout } from "node:timers/promises";
|
|
|
16
16
|
import { DEFAULT_REMOTE_PATH_DISCOVERY_PHASE_TIMEOUT_MS, DEFAULT_REMOTE_PATH_DISCOVERY_RETRY_DELAY_MS, discoverPrerenderPathManifest, emitPrerenderPathManifest } from "vinext/internal/build/prerender-paths";
|
|
17
17
|
import { normalizePathTrailingSlash } from "vinext/shims/url-utils";
|
|
18
18
|
import { spawn } from "node:child_process";
|
|
19
|
-
import { parseArgs } from "node:util";
|
|
19
|
+
import { parseArgs, stripVTControlCharacters } from "node:util";
|
|
20
20
|
import { pathToFileURL } from "node:url";
|
|
21
21
|
import { VINEXT_BUILD_LIFECYCLE_CONFIG } from "vinext/internal/build/lifecycle";
|
|
22
22
|
import { runPrerender } from "vinext/internal/build/run-prerender";
|
|
@@ -228,7 +228,7 @@ function parseDeployArgs(args) {
|
|
|
228
228
|
values["warm-cache-readiness-probe-delay"] ??= values["warm-cdn-readiness-probe-delay"];
|
|
229
229
|
values["warm-cache-no-promote"] ||= values["warm-cdn-no-promote"];
|
|
230
230
|
values["warm-cache-promotion-delay"] ??= values["warm-cdn-promotion-delay"];
|
|
231
|
-
values["warm-cache-include-fallbacks"]
|
|
231
|
+
if (values["warm-cache-include-fallbacks"] || values["warm-cdn-include-fallbacks"]) console.warn("[vinext] --warm-cache-include-fallbacks is deprecated and has no effect.");
|
|
232
232
|
values["traffic-aware-warm-cache"] ||= values["experimental-traffic-aware-warm-cache"];
|
|
233
233
|
const trafficAwareWarmCache = values["traffic-aware-warm-cache"] || values["experimental-tpr"];
|
|
234
234
|
const warming = values["warm-cache"] || trafficAwareWarmCache;
|
|
@@ -271,7 +271,6 @@ function parseDeployArgs(args) {
|
|
|
271
271
|
dangerouslyPromoteOnCdnWarmError: values["dangerously-promote-on-warm-cache-error"],
|
|
272
272
|
warmCdnPromote: !values["no-promote"] && !values["warm-cache-no-promote"],
|
|
273
273
|
warmCdnPromotionDelay: values["warm-cache-promotion-delay"] === void 0 ? void 0 : validatePromotionDelay(parseNonNegativeIntegerArg(values["warm-cache-promotion-delay"], "--warm-cache-promotion-delay"), values["warm-cache-promotion-delay"]),
|
|
274
|
-
warmCdnIncludeFallbacks: values["warm-cache-include-fallbacks"],
|
|
275
274
|
experimentalTPR: trafficAwareWarmCache,
|
|
276
275
|
tprCoverage: parseIntArg("traffic-aware-coverage", values["traffic-aware-coverage"] ?? values["tpr-coverage"]),
|
|
277
276
|
tprLimit: parseIntArg("traffic-aware-limit", values["traffic-aware-limit"] ?? values["tpr-limit"]),
|
|
@@ -302,6 +301,18 @@ async function withCloudflareEnv(env, fn) {
|
|
|
302
301
|
else delete process.env.CLOUDFLARE_ENV;
|
|
303
302
|
}
|
|
304
303
|
}
|
|
304
|
+
/** Cloudflare Vite Plugin reads this flag when evaluating the typed config and emitting Build Output. */
|
|
305
|
+
async function withCloudflarePreviewBuild(fn) {
|
|
306
|
+
const hadPrevious = "CLOUDFLARE_PREVIEW_BUILD" in process.env;
|
|
307
|
+
const previous = process.env.CLOUDFLARE_PREVIEW_BUILD;
|
|
308
|
+
process.env.CLOUDFLARE_PREVIEW_BUILD = "true";
|
|
309
|
+
try {
|
|
310
|
+
return await fn();
|
|
311
|
+
} finally {
|
|
312
|
+
if (hadPrevious) process.env.CLOUDFLARE_PREVIEW_BUILD = previous;
|
|
313
|
+
else delete process.env.CLOUDFLARE_PREVIEW_BUILD;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
305
316
|
async function loadProjectViteApi(root) {
|
|
306
317
|
let vitePath;
|
|
307
318
|
try {
|
|
@@ -385,7 +396,10 @@ function isCfCliInstalled(root, resolvePackageJson = (projectRoot) => {
|
|
|
385
396
|
}
|
|
386
397
|
function buildCfDeployArgs(options) {
|
|
387
398
|
const mode = options.env || (options.preview ? "preview" : void 0);
|
|
388
|
-
const
|
|
399
|
+
const isPreviewDeploy = options.target?.type === "preview";
|
|
400
|
+
const args = isPreviewDeploy ? ["previews", "deploy"] : ["deploy"];
|
|
401
|
+
if (isPreviewDeploy && options.target?.name) args.push(options.target.name);
|
|
402
|
+
args.push("--prebuilt");
|
|
389
403
|
if (mode) args.push("--mode", validateWranglerEnvName(mode));
|
|
390
404
|
return {
|
|
391
405
|
args,
|
|
@@ -483,7 +497,8 @@ async function runWranglerDeploy(root, options, execute = spawn) {
|
|
|
483
497
|
}
|
|
484
498
|
async function runCfDeploy(root, options, execute = spawn) {
|
|
485
499
|
const { args, mode } = buildCfDeployArgs(options);
|
|
486
|
-
|
|
500
|
+
const isPreview = options.target?.type === "preview";
|
|
501
|
+
console.log(mode ? `\n Deploying ${isPreview ? "Preview " : ""}Build Output in mode: ${mode}...` : "\n Deploying Build Output...");
|
|
487
502
|
const child = execute(process.execPath, [resolveCfBin(root), ...args], {
|
|
488
503
|
cwd: root,
|
|
489
504
|
stdio: [
|
|
@@ -494,9 +509,11 @@ async function runCfDeploy(root, options, execute = spawn) {
|
|
|
494
509
|
shell: false
|
|
495
510
|
});
|
|
496
511
|
let output = "";
|
|
512
|
+
let stdout = "";
|
|
497
513
|
child.stdout?.on("data", (chunk) => {
|
|
498
514
|
const text = chunk.toString();
|
|
499
515
|
output += text;
|
|
516
|
+
stdout += text;
|
|
500
517
|
process.stdout.write(text);
|
|
501
518
|
});
|
|
502
519
|
child.stderr?.on("data", (chunk) => {
|
|
@@ -508,11 +525,27 @@ async function runCfDeploy(root, options, execute = spawn) {
|
|
|
508
525
|
child.once("error", reject);
|
|
509
526
|
child.once("close", (code, signal) => {
|
|
510
527
|
if (code === 0) resolve();
|
|
511
|
-
else reject(/* @__PURE__ */ new Error(`cf deploy failed with ${signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`}.`));
|
|
528
|
+
else reject(/* @__PURE__ */ new Error(`cf ${isPreview ? "previews deploy" : "deploy"} failed with ${signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`}.`));
|
|
512
529
|
});
|
|
513
530
|
});
|
|
531
|
+
if (isPreview) return parseCfPreviewUrl(stdout) ?? parseWorkerDeploymentUrl(output) ?? "(Preview URL not detected in cf output)";
|
|
514
532
|
return parseWorkerDeploymentUrl(output) ?? "(URL not detected in cf output)";
|
|
515
533
|
}
|
|
534
|
+
/** `cf previews deploy` prints a JSON result; prefer the named Preview URL over the deployment URL. */
|
|
535
|
+
function parseCfPreviewUrl(stdout) {
|
|
536
|
+
const text = stripVTControlCharacters(stdout);
|
|
537
|
+
const start = text.indexOf("{");
|
|
538
|
+
const end = text.lastIndexOf("}");
|
|
539
|
+
if (start === -1 || end < start) return null;
|
|
540
|
+
let result;
|
|
541
|
+
try {
|
|
542
|
+
result = JSON.parse(text.slice(start, end + 1));
|
|
543
|
+
} catch {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
for (const urls of [result.preview_urls, result.deployment_urls]) if (Array.isArray(urls) && typeof urls[0] === "string") return urls[0];
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
516
549
|
function hasCdnWarmRequests(plan) {
|
|
517
550
|
return plan.paths.length + (plan.pagesDataPaths?.length ?? 0) + (plan.routeHandlerPaths?.length ?? 0) + plan.rscPaths.length + plan.loadingShellPaths.length > 0;
|
|
518
551
|
}
|
|
@@ -1131,13 +1164,30 @@ function withPromotedVersionWarmupNote(error) {
|
|
|
1131
1164
|
const message = error instanceof Error ? error.message : String(error);
|
|
1132
1165
|
return new Error(`${message} The uploaded version is already promoted to 100% and its Worker triggers/routes were updated; rerun deploy to retry cache warming or create a deployment for the previous version.`, { cause: error });
|
|
1133
1166
|
}
|
|
1167
|
+
/** Worker Previews skip production-only steps, so reject options that would otherwise be ignored. */
|
|
1168
|
+
function assertPreviewDeployOptions(options) {
|
|
1169
|
+
const unsupported = Object.entries({
|
|
1170
|
+
preview: options.preview,
|
|
1171
|
+
name: options.name !== void 0,
|
|
1172
|
+
config: options.config !== void 0,
|
|
1173
|
+
warmCdnCache: options.warmCdnCache,
|
|
1174
|
+
warmCdnTarget: options.warmCdnTarget !== void 0,
|
|
1175
|
+
warmCdnCertify: options.warmCdnCertify,
|
|
1176
|
+
warmCdnPromote: options.warmCdnPromote === false,
|
|
1177
|
+
experimentalTPR: options.experimentalTPR
|
|
1178
|
+
}).filter(([, isSet]) => isSet).map(([option]) => option);
|
|
1179
|
+
if (unsupported.length > 0) throw new Error(`Worker Preview deployments do not support: ${unsupported.join(", ")}.`);
|
|
1180
|
+
}
|
|
1134
1181
|
async function deploy(options) {
|
|
1182
|
+
const isPreview = options.target?.type === "preview";
|
|
1183
|
+
if (isPreview) assertPreviewDeployOptions(options);
|
|
1135
1184
|
if (options.warmCdnTarget !== void 0 && !options.warmCdnCache && !options.experimentalTPR) throw new Error("--warm-cache-target requires --warm-cache or --traffic-aware-warm-cache.");
|
|
1136
1185
|
const warmCdnTarget = options.warmCdnTarget === void 0 ? void 0 : validateCdnWarmTarget(options.warmCdnTarget);
|
|
1137
1186
|
const deployEnv = options.env || (options.preview ? "preview" : void 0);
|
|
1138
1187
|
if (deployEnv) validateWranglerEnvName(deployEnv);
|
|
1139
1188
|
const root = path.resolve(options.root);
|
|
1140
1189
|
const deploymentTool = resolveDeploymentTool(root);
|
|
1190
|
+
if (isPreview && deploymentTool !== "cf") throw new Error("Worker Previews require a cloudflare.config.ts project and the cf CLI.");
|
|
1141
1191
|
if (deploymentTool === "cf" && options.name) throw new Error("--name is not supported for cloudflare.config.ts projects. Set `name` in cloudflare.config.ts instead.");
|
|
1142
1192
|
const viteMode = resolveViteBuildMode(deploymentTool, deployEnv);
|
|
1143
1193
|
const wranglerFallbackEnv = deploymentTool === "cf" ? void 0 : deployEnv;
|
|
@@ -1145,7 +1195,9 @@ async function deploy(options) {
|
|
|
1145
1195
|
root,
|
|
1146
1196
|
mode: viteMode
|
|
1147
1197
|
});
|
|
1148
|
-
|
|
1198
|
+
const cloudflareEnv = isPreview ? void 0 : deployEnv;
|
|
1199
|
+
const withBuildContext = (fn) => withCloudflareEnv(cloudflareEnv, () => isPreview ? withCloudflarePreviewBuild(fn) : fn());
|
|
1200
|
+
console.log(isPreview ? "\n vinext-cloudflare previews deploy\n" : "\n vinext-cloudflare deploy\n");
|
|
1149
1201
|
const info = detectProject(root);
|
|
1150
1202
|
if (!info.isAppRouter && !info.isPagesRouter) {
|
|
1151
1203
|
console.error(" Error: No app/ or pages/ directory found.");
|
|
@@ -1172,9 +1224,17 @@ async function deploy(options) {
|
|
|
1172
1224
|
console.log("\n Dry run complete. No build or deploy performed.\n");
|
|
1173
1225
|
return;
|
|
1174
1226
|
}
|
|
1175
|
-
|
|
1227
|
+
if (isPreview && options.skipBuild) {
|
|
1228
|
+
console.log("\n Skipping build (--skip-build)");
|
|
1229
|
+
printDeployUrl("Preview URL", await runCfDeploy(root, {
|
|
1230
|
+
env: viteMode,
|
|
1231
|
+
target: options.target
|
|
1232
|
+
}));
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
const viteConfigMetadata = await withBuildContext(() => loadDeployViteConfigMetadata(info.root, viteMode));
|
|
1176
1236
|
const cdnAdapterConfig = resolveCdnAdapterConfig(viteConfigMetadata.cacheConfig);
|
|
1177
|
-
const nextConfig = await
|
|
1237
|
+
const nextConfig = await withBuildContext(async () => {
|
|
1178
1238
|
const inlineNextConfig = viteConfigMetadata.nextConfig;
|
|
1179
1239
|
const rawNextConfig = inlineNextConfig ? await resolveNextConfigInput(inlineNextConfig, PHASE_PRODUCTION_BUILD) : await loadNextConfig(info.root, PHASE_PRODUCTION_BUILD);
|
|
1180
1240
|
return resolveNextConfig(rawNextConfig, info.root);
|
|
@@ -1195,10 +1255,10 @@ async function deploy(options) {
|
|
|
1195
1255
|
const shouldEmitPrerenderPathManifest = !options.skipBuild && shouldPrerenderLocally;
|
|
1196
1256
|
if (prerenderDecision && !shouldPrerenderLocally) {
|
|
1197
1257
|
const trigger = prerenderDecision.reason === "flag" ? "--prerender-all" : "vinext prerender config";
|
|
1198
|
-
const replacement = options.warmCdnCache ? "Routes will be rendered and warmed through the staged Worker instead." : "Use --warm-cache to render and warm routes through the deployed Worker instead.";
|
|
1258
|
+
const replacement = isPreview ? "Preview deployments render these routes on request." : options.warmCdnCache ? "Routes will be rendered and warmed through the staged Worker instead." : "Use --warm-cache to render and warm routes through the deployed Worker instead.";
|
|
1199
1259
|
console.warn(`\n Warning: ${trigger} is ignored by Cloudflare deploy. ${replacement}`);
|
|
1200
1260
|
}
|
|
1201
|
-
if (!options.skipBuild) await runBuild(info,
|
|
1261
|
+
if (!options.skipBuild) await withBuildContext(() => runBuild(info, cloudflareEnv, viteMode));
|
|
1202
1262
|
else console.log("\n Skipping build (--skip-build)");
|
|
1203
1263
|
const prerenderOutputDirs = resolvePrerenderOutputDirs(info.root, deploymentTool, viteConfigMetadata.routeRootConfig);
|
|
1204
1264
|
const canWarmTpr = options.experimentalTPR && !shouldPrerenderLocally && hasBuildIdentityHeader;
|
|
@@ -1328,7 +1388,6 @@ async function deploy(options) {
|
|
|
1328
1388
|
};
|
|
1329
1389
|
return createPrerenderWarmPlan(root, discovery, {
|
|
1330
1390
|
includeCanonicalRsc: hasCanonicalRscWarmup,
|
|
1331
|
-
includeFallbackShells: options.warmCdnIncludeFallbacks,
|
|
1332
1391
|
strict: options.warmCdnCertify === true || !options.dangerouslyPromoteOnCdnWarmError
|
|
1333
1392
|
});
|
|
1334
1393
|
},
|
|
@@ -1357,15 +1416,21 @@ async function deploy(options) {
|
|
|
1357
1416
|
}
|
|
1358
1417
|
if (url === void 0) {
|
|
1359
1418
|
if (deploymentTool === "cf" && options.warmCdnPromote === false) url = runCfVersionUpload(root, wranglerOptions).previewUrl ?? "(Preview URL not detected in cf output)";
|
|
1360
|
-
else if (deploymentTool === "cf") url = await runCfDeploy(root,
|
|
1419
|
+
else if (deploymentTool === "cf") url = await runCfDeploy(root, {
|
|
1420
|
+
...wranglerOptions,
|
|
1421
|
+
target: options.target
|
|
1422
|
+
});
|
|
1361
1423
|
else url = await runWranglerDeploy(root, {
|
|
1362
1424
|
...wranglerOptions,
|
|
1363
1425
|
promote: options.warmCdnPromote
|
|
1364
1426
|
});
|
|
1365
1427
|
}
|
|
1428
|
+
printDeployUrl(isPreview ? "Preview URL" : options.warmCdnPromote === false ? "Version URL" : "Deployed to", url);
|
|
1429
|
+
}
|
|
1430
|
+
function printDeployUrl(label, url) {
|
|
1366
1431
|
console.log("\n ─────────────────────────────────────────");
|
|
1367
|
-
console.log(` ${
|
|
1432
|
+
console.log(` ${label}: ${url}`);
|
|
1368
1433
|
console.log(" ─────────────────────────────────────────\n");
|
|
1369
1434
|
}
|
|
1370
1435
|
//#endregion
|
|
1371
|
-
export { DEFAULT_CDN_WARM_PROMOTION_DELAY_MS, buildCfDeployArgs, buildNodeCliInvocation, buildVersionOverrideHeaders, buildWranglerDeployArgs, buildWranglerInvocation, deploy, deployWithCdnWarmup, getZeroPercentStagingTraffic, hasCdnWarmRequests, isCfCliInstalled, parseDeployArgs, projectRequiresRouteCacheabilityProbeManifest, resolveCdnWarmupTargetUrl, resolveCfBin, resolveDeploymentControlPlaneOptions, resolveDeploymentTool, resolvePrerenderOutputDirs, resolveViteBuildMode, resolveWorkerNameForVersionOverride, resolveWranglerBin, runCfDeploy, runWranglerDeploy, selectTPRWarmPlan, validateWranglerEnvName, withCloudflareEnv };
|
|
1436
|
+
export { DEFAULT_CDN_WARM_PROMOTION_DELAY_MS, buildCfDeployArgs, buildNodeCliInvocation, buildVersionOverrideHeaders, buildWranglerDeployArgs, buildWranglerInvocation, deploy, deployWithCdnWarmup, getZeroPercentStagingTraffic, hasCdnWarmRequests, isCfCliInstalled, parseDeployArgs, projectRequiresRouteCacheabilityProbeManifest, resolveCdnWarmupTargetUrl, resolveCfBin, resolveDeploymentControlPlaneOptions, resolveDeploymentTool, resolvePrerenderOutputDirs, resolveViteBuildMode, resolveWorkerNameForVersionOverride, resolveWranglerBin, runCfDeploy, runWranglerDeploy, selectTPRWarmPlan, validateWranglerEnvName, withCloudflareEnv, withCloudflarePreviewBuild };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/previews-deploy.d.ts
|
|
2
|
+
/** CLI options for `vinext-cloudflare previews deploy`. */
|
|
3
|
+
export declare function parsePreviewsDeployArgs(args: string[]): {
|
|
4
|
+
help: boolean;
|
|
5
|
+
name: string | undefined;
|
|
6
|
+
mode: string | undefined;
|
|
7
|
+
skipBuild: boolean;
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
//#region src/previews-deploy.ts
|
|
3
|
+
function validatePreviewName(name) {
|
|
4
|
+
const trimmed = name.trim();
|
|
5
|
+
if (!trimmed) throw new Error("Preview name must not be empty.");
|
|
6
|
+
if (trimmed.includes("\0")) throw new Error("Preview names cannot contain null bytes.");
|
|
7
|
+
if (trimmed.startsWith("-")) throw new Error("Preview name must not start with '-'.");
|
|
8
|
+
return trimmed;
|
|
9
|
+
}
|
|
10
|
+
function validateMode(mode) {
|
|
11
|
+
const trimmed = mode.trim();
|
|
12
|
+
if (!trimmed) throw new Error("--mode requires a non-empty value.");
|
|
13
|
+
return trimmed;
|
|
14
|
+
}
|
|
15
|
+
/** CLI options for `vinext-cloudflare previews deploy`. */
|
|
16
|
+
function parsePreviewsDeployArgs(args) {
|
|
17
|
+
const { values, positionals } = parseArgs({
|
|
18
|
+
args,
|
|
19
|
+
allowPositionals: true,
|
|
20
|
+
strict: true,
|
|
21
|
+
options: {
|
|
22
|
+
help: {
|
|
23
|
+
type: "boolean",
|
|
24
|
+
short: "h",
|
|
25
|
+
default: false
|
|
26
|
+
},
|
|
27
|
+
mode: { type: "string" },
|
|
28
|
+
"skip-build": {
|
|
29
|
+
type: "boolean",
|
|
30
|
+
default: false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
if (positionals.length > 1) throw new Error("Expected at most one Preview name: previews deploy [preview-name].");
|
|
35
|
+
return {
|
|
36
|
+
help: values.help,
|
|
37
|
+
name: positionals[0] === void 0 ? void 0 : validatePreviewName(positionals[0]),
|
|
38
|
+
mode: values.mode === void 0 ? void 0 : validateMode(values.mode),
|
|
39
|
+
skipBuild: values["skip-build"]
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { parsePreviewsDeployArgs };
|
|
@@ -3,7 +3,9 @@ export declare function parseWorkerDeploymentUrl(output: string): string | null;
|
|
|
3
3
|
/**
|
|
4
4
|
* Parse the concrete hostname that Wrangler reports after applying Worker
|
|
5
5
|
* triggers. Catch-all routes are valid warmup targets even though they are not
|
|
6
|
-
* canonical deployment URLs for general CLI reporting.
|
|
6
|
+
* canonical deployment URLs for general CLI reporting. Warmup requests carry the
|
|
7
|
+
* prerender secret, so an `http://`-only route (which Cloudflare does not match
|
|
8
|
+
* for HTTPS requests) is never used as a target.
|
|
7
9
|
*/
|
|
8
10
|
export declare function parseCdnWarmupDeploymentUrl(output: string): string | null;
|
|
9
11
|
//#endregion
|
|
@@ -6,7 +6,9 @@ function parseWorkerDeploymentUrl(output) {
|
|
|
6
6
|
/**
|
|
7
7
|
* Parse the concrete hostname that Wrangler reports after applying Worker
|
|
8
8
|
* triggers. Catch-all routes are valid warmup targets even though they are not
|
|
9
|
-
* canonical deployment URLs for general CLI reporting.
|
|
9
|
+
* canonical deployment URLs for general CLI reporting. Warmup requests carry the
|
|
10
|
+
* prerender secret, so an `http://`-only route (which Cloudflare does not match
|
|
11
|
+
* for HTTPS requests) is never used as a target.
|
|
10
12
|
*/
|
|
11
13
|
function parseCdnWarmupDeploymentUrl(output) {
|
|
12
14
|
return parseCustomDomainUrl(output) ?? parseCatchAllWorkerRouteOrigin(output) ?? parseWorkersDevUrl(output);
|
|
@@ -17,7 +19,7 @@ function parseCatchAllWorkerRouteOrigin(output) {
|
|
|
17
19
|
if (!route || !route.endsWith("/*")) continue;
|
|
18
20
|
try {
|
|
19
21
|
const url = new URL(route.includes("://") ? route : `https://${route}`);
|
|
20
|
-
if (url.protocol !== "https:"
|
|
22
|
+
if (url.protocol !== "https:" || url.hostname.includes("*") || url.username !== "" || url.password !== "" || url.port !== "" || url.pathname !== "/*" || url.search !== "" || url.hash !== "") continue;
|
|
21
23
|
return url.origin;
|
|
22
24
|
} catch {
|
|
23
25
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vinext/cloudflare",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Cloudflare cache and image adapters for vinext.",
|
|
5
5
|
"homepage": "https://vinext.dev/",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@cloudflare/workers-response-store": "1.1.
|
|
51
|
+
"@cloudflare/workers-response-store": "1.1.2"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "^25.9.2",
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
"vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
|
|
58
58
|
"vite-plus": "1.1.0",
|
|
59
59
|
"vitest": "5.0.3",
|
|
60
|
-
"vinext": "1.
|
|
60
|
+
"vinext": "1.3.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"@cloudflare/vite-plugin": "*",
|
|
64
64
|
"cf": "*",
|
|
65
|
-
"vinext": "^1.
|
|
65
|
+
"vinext": "^1.3.0"
|
|
66
66
|
},
|
|
67
67
|
"peerDependenciesMeta": {
|
|
68
68
|
"@cloudflare/vite-plugin": {
|