vinext 0.2.0 → 1.0.0-beta.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.
Files changed (168) hide show
  1. package/README.md +50 -29
  2. package/dist/build/client-build-config.d.ts +13 -92
  3. package/dist/build/client-build-config.js +17 -95
  4. package/dist/build/css-url-assets.d.ts +3 -1
  5. package/dist/build/css-url-assets.js +18 -1
  6. package/dist/build/inject-pregenerated-paths.d.ts +3 -0
  7. package/dist/build/inject-pregenerated-paths.js +4 -1
  8. package/dist/build/prerender-paths.d.ts +23 -0
  9. package/dist/build/prerender-paths.js +296 -0
  10. package/dist/build/prerender-server-entry.d.ts +1 -0
  11. package/dist/build/prerender-server-entry.js +49 -0
  12. package/dist/build/prerender-server-pool.d.ts +44 -0
  13. package/dist/build/prerender-server-pool.js +194 -0
  14. package/dist/build/prerender.d.ts +12 -1
  15. package/dist/build/prerender.js +87 -23
  16. package/dist/build/run-prerender.js +2 -1
  17. package/dist/cache/cache-adapters-virtual.d.ts +7 -1
  18. package/dist/cache/cache-adapters-virtual.js +26 -2
  19. package/dist/check.d.ts +4 -6
  20. package/dist/check.js +13 -9
  21. package/dist/cli.js +34 -54
  22. package/dist/client/vinext-next-data.d.ts +4 -1
  23. package/dist/config/config-matchers.js +33 -11
  24. package/dist/config/next-config.d.ts +36 -6
  25. package/dist/config/next-config.js +17 -4
  26. package/dist/config/prerender.d.ts +11 -1
  27. package/dist/config/prerender.js +19 -1
  28. package/dist/config/tsconfig-paths.js +5 -1
  29. package/dist/entries/app-browser-entry.js +5 -1
  30. package/dist/entries/app-rsc-entry.d.ts +3 -2
  31. package/dist/entries/app-rsc-entry.js +4 -0
  32. package/dist/entries/pages-client-entry.js +14 -3
  33. package/dist/entries/pages-server-entry.js +18 -4
  34. package/dist/image/image-adapters-virtual.js +1 -0
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.js +549 -152
  37. package/dist/init-cloudflare.js +12 -3
  38. package/dist/init-platform.d.ts +10 -1
  39. package/dist/init-platform.js +78 -12
  40. package/dist/init.d.ts +11 -3
  41. package/dist/init.js +129 -31
  42. package/dist/plugins/fonts.js +1 -1
  43. package/dist/plugins/ignore-dynamic-requests.js +1 -1
  44. package/dist/plugins/og-assets.js +2 -1
  45. package/dist/plugins/optimize-imports.js +1 -1
  46. package/dist/plugins/rsc-reference-validation-normalizer.d.ts +12 -0
  47. package/dist/plugins/rsc-reference-validation-normalizer.js +48 -0
  48. package/dist/plugins/sass.d.ts +17 -1
  49. package/dist/plugins/sass.js +74 -1
  50. package/dist/plugins/styled-jsx.d.ts +16 -0
  51. package/dist/plugins/styled-jsx.js +149 -0
  52. package/dist/routing/app-route-graph.js +22 -16
  53. package/dist/routing/file-matcher.d.ts +8 -1
  54. package/dist/routing/file-matcher.js +15 -3
  55. package/dist/server/app-browser-entry.js +44 -14
  56. package/dist/server/app-browser-navigation-controller.js +5 -2
  57. package/dist/server/app-browser-server-action-client.js +5 -3
  58. package/dist/server/app-browser-state.d.ts +1 -0
  59. package/dist/server/app-browser-state.js +1 -1
  60. package/dist/server/app-layout-param-observation.d.ts +1 -1
  61. package/dist/server/app-page-boundary.js +2 -1
  62. package/dist/server/app-page-cache-finalizer.d.ts +1 -0
  63. package/dist/server/app-page-cache-finalizer.js +3 -3
  64. package/dist/server/app-page-cache-render.d.ts +1 -1
  65. package/dist/server/app-page-cache.js +23 -6
  66. package/dist/server/app-page-dispatch.d.ts +2 -1
  67. package/dist/server/app-page-dispatch.js +27 -10
  68. package/dist/server/app-page-element-builder.js +1 -0
  69. package/dist/server/app-page-execution.js +2 -1
  70. package/dist/server/app-page-render-identity.d.ts +1 -0
  71. package/dist/server/app-page-render-identity.js +2 -1
  72. package/dist/server/app-page-render.d.ts +3 -1
  73. package/dist/server/app-page-render.js +67 -16
  74. package/dist/server/app-page-response.d.ts +7 -0
  75. package/dist/server/app-page-response.js +16 -4
  76. package/dist/server/app-page-route-wiring.js +3 -3
  77. package/dist/server/app-page-stream.d.ts +2 -1
  78. package/dist/server/app-page-stream.js +1 -1
  79. package/dist/server/app-route-handler-execution.d.ts +1 -1
  80. package/dist/server/app-route-tree-prefetch.d.ts +43 -0
  81. package/dist/server/app-route-tree-prefetch.js +187 -0
  82. package/dist/server/app-router-entry.js +1 -1
  83. package/dist/server/app-rsc-cache-busting.d.ts +2 -1
  84. package/dist/server/app-rsc-cache-busting.js +9 -5
  85. package/dist/server/app-rsc-handler.d.ts +6 -0
  86. package/dist/server/app-rsc-handler.js +88 -10
  87. package/dist/server/app-rsc-render-mode.d.ts +3 -5
  88. package/dist/server/app-rsc-render-mode.js +5 -12
  89. package/dist/server/app-rsc-request-normalization.d.ts +3 -4
  90. package/dist/server/app-rsc-request-normalization.js +4 -5
  91. package/dist/server/app-server-action-execution.js +6 -5
  92. package/dist/server/app-ssr-entry.js +1 -1
  93. package/dist/server/cache-control.d.ts +3 -1
  94. package/dist/server/cache-control.js +13 -1
  95. package/dist/server/dev-module-runner.js +1 -1
  96. package/dist/server/dev-origin-check.d.ts +2 -2
  97. package/dist/server/dev-origin-check.js +2 -2
  98. package/dist/server/dev-server.d.ts +11 -1
  99. package/dist/server/dev-server.js +82 -24
  100. package/dist/server/headers.d.ts +6 -2
  101. package/dist/server/headers.js +11 -5
  102. package/dist/server/image-optimization.d.ts +12 -1
  103. package/dist/server/image-optimization.js +13 -1
  104. package/dist/server/isr-cache.d.ts +13 -4
  105. package/dist/server/isr-cache.js +8 -4
  106. package/dist/server/pages-data-route.d.ts +4 -2
  107. package/dist/server/pages-data-route.js +18 -4
  108. package/dist/server/pages-dev-module-url.d.ts +2 -1
  109. package/dist/server/pages-dev-module-url.js +6 -3
  110. package/dist/server/pages-node-compat.d.ts +12 -1
  111. package/dist/server/pages-node-compat.js +50 -1
  112. package/dist/server/pages-page-data.d.ts +9 -0
  113. package/dist/server/pages-page-data.js +18 -10
  114. package/dist/server/pages-page-handler.js +19 -7
  115. package/dist/server/pages-page-response.d.ts +1 -0
  116. package/dist/server/pages-page-response.js +5 -4
  117. package/dist/server/pages-request-pipeline.d.ts +8 -6
  118. package/dist/server/pages-request-pipeline.js +31 -7
  119. package/dist/server/pages-router-entry.js +1 -1
  120. package/dist/server/prerender-manifest.d.ts +15 -1
  121. package/dist/server/prerender-manifest.js +29 -1
  122. package/dist/server/prod-server.d.ts +10 -7
  123. package/dist/server/prod-server.js +43 -28
  124. package/dist/server/request-pipeline.js +1 -1
  125. package/dist/server/static-file-cache.js +1 -1
  126. package/dist/shims/cache-handler.js +8 -1
  127. package/dist/shims/cache.d.ts +1 -1
  128. package/dist/shims/cache.js +3 -0
  129. package/dist/shims/cdn-cache.d.ts +2 -7
  130. package/dist/shims/cdn-cache.js +2 -14
  131. package/dist/shims/fetch-cache.d.ts +3 -1
  132. package/dist/shims/fetch-cache.js +77 -52
  133. package/dist/shims/internal/app-prefetch-fetch-queue.d.ts +10 -0
  134. package/dist/shims/internal/app-prefetch-fetch-queue.js +61 -0
  135. package/dist/shims/internal/hybrid-client-route-owner.d.ts +2 -1
  136. package/dist/shims/internal/hybrid-client-route-owner.js +34 -1
  137. package/dist/shims/internal/pages-data-fetch-dedup.d.ts +2 -1
  138. package/dist/shims/internal/pages-data-fetch-dedup.js +13 -2
  139. package/dist/shims/internal/pages-data-target.d.ts +8 -3
  140. package/dist/shims/internal/pages-data-target.js +9 -4
  141. package/dist/shims/link.d.ts +1 -1
  142. package/dist/shims/link.js +179 -42
  143. package/dist/shims/navigation.d.ts +15 -3
  144. package/dist/shims/navigation.js +202 -39
  145. package/dist/shims/request-context.js +18 -0
  146. package/dist/shims/request-state-types.d.ts +2 -2
  147. package/dist/shims/router.js +31 -15
  148. package/dist/shims/unified-request-context.d.ts +1 -1
  149. package/dist/shims/unified-request-context.js +1 -0
  150. package/dist/typegen.js +1 -1
  151. package/dist/utils/middleware-request-headers.js +1 -1
  152. package/dist/utils/project.d.ts +1 -1
  153. package/dist/utils/protocol-headers.d.ts +7 -1
  154. package/dist/utils/protocol-headers.js +7 -1
  155. package/dist/utils/vite-version.d.ts +4 -12
  156. package/dist/utils/vite-version.js +39 -21
  157. package/package.json +31 -13
  158. package/dist/cloudflare/index.d.ts +0 -3
  159. package/dist/cloudflare/index.js +0 -3
  160. package/dist/packages/cloudflare/src/cache/cdn-adapter.runtime.js +0 -102
  161. package/dist/packages/cloudflare/src/cache/kv-data-adapter.runtime.d.ts +0 -126
  162. package/dist/packages/cloudflare/src/cache/kv-data-adapter.runtime.js +0 -435
  163. package/dist/packages/cloudflare/src/deploy-config.js +0 -150
  164. package/dist/packages/cloudflare/src/deploy-help.js +0 -55
  165. package/dist/packages/cloudflare/src/deploy.js +0 -276
  166. package/dist/packages/cloudflare/src/tpr.d.ts +0 -45
  167. package/dist/packages/cloudflare/src/tpr.js +0 -561
  168. package/dist/packages/cloudflare/src/utils/cache-control-metadata.js +0 -20
@@ -1,435 +0,0 @@
1
- import { getRequestExecutionContext } from "../../../../shims/request-context.js";
2
- import { isUnknownRecord, readCacheControlNumberField } from "../utils/cache-control-metadata.js";
3
- import { Buffer } from "node:buffer";
4
- //#region ../cloudflare/src/cache/kv-data-adapter.runtime.ts
5
- /**
6
- * Cloudflare KV data cache — implementation + runtime factory.
7
- *
8
- * `KVCacheHandler` provides persistent ISR caching on Cloudflare Workers using
9
- * KV as the storage backend (time-based expiry / stale-while-revalidate and
10
- * tag-based invalidation). The default export is the adapter factory the
11
- * generated `virtual:vinext-cache-adapters` registration imports: it reads the
12
- * KV namespace from `env[binding]` at request time and constructs the handler.
13
- *
14
- * Configure it from vite.config via the {@link kvDataAdapter} builder in
15
- * `./kv-data-adapter.ts` (which `require.resolve`s this file). The legacy
16
- * imperative path (constructing `KVCacheHandler` and calling
17
- * `setDataCacheHandler` from a worker entry) is deprecated — prefer the
18
- * `cache.data` plugin option.
19
- *
20
- * Wrangler config (wrangler.jsonc):
21
- *
22
- * { "kv_namespaces": [{ "binding": "VINEXT_KV_CACHE", "id": "<your-kv-namespace-id>" }] }
23
- */
24
- /** Key prefix for tag invalidation timestamps. */
25
- const TAG_PREFIX = "__tag:";
26
- /** Key prefix for cache entries. */
27
- const ENTRY_PREFIX = "cache:";
28
- /** Prefix used by revalidatePath for path-based tags. */
29
- const PATH_TAG_PREFIX = "_N_T_";
30
- /** Max tag length to prevent KV key abuse. */
31
- const MAX_TAG_LENGTH = 256;
32
- /** Matches a valid base64 string (standard alphabet with optional padding). */
33
- const BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
34
- /**
35
- * Validate a cache tag. Returns null if invalid.
36
- * Note: `:` is rejected because TAG_PREFIX and ENTRY_PREFIX use `:` as a
37
- * separator — allowing `:` in user tags could cause ambiguous key lookups.
38
- */
39
- function validateTag(tag) {
40
- if (typeof tag !== "string" || tag.length === 0 || tag.length > MAX_TAG_LENGTH) return null;
41
- if (/[\x00-\x1f\\:]/.test(tag)) return null;
42
- return tag;
43
- }
44
- function readStringArrayField(ctx, field) {
45
- const value = ctx?.[field];
46
- if (!Array.isArray(value)) return [];
47
- return value.filter((item) => typeof item === "string");
48
- }
49
- function validUniqueTags(tags) {
50
- const result = [];
51
- const seen = /* @__PURE__ */ new Set();
52
- for (const tag of tags) {
53
- const validTag = validateTag(tag);
54
- if (!validTag || seen.has(validTag)) continue;
55
- seen.add(validTag);
56
- result.push(validTag);
57
- }
58
- return result;
59
- }
60
- /**
61
- * Segment-aware path prefix check. Returns true if `path` is equal to
62
- * `prefix` or is a child route (next char after prefix is `/`).
63
- * Prevents `/dashboard` from matching `/dashboard-admin`.
64
- */
65
- function isPathChildOf(path, prefix) {
66
- if (prefix === "/") return path.startsWith("/");
67
- if (path === prefix) return true;
68
- return path.startsWith(prefix + "/");
69
- }
70
- /**
71
- * Cloudflare KV data cache handler.
72
- *
73
- * @deprecated Consumers should not instantiate or register this handler by
74
- * hand. Configure it declaratively via the `cache.data` option on the
75
- * `vinext()` plugin, using the {@link kvDataAdapter} builder:
76
- *
77
- * ```ts
78
- * import { vinext } from "vinext";
79
- * import { kvDataAdapter } from "@vinext/cloudflare/cache/kv-data-adapter";
80
- *
81
- * export default defineConfig({
82
- * plugins: [vinext({ cache: { data: kvDataAdapter({ binding: "VINEXT_KV_CACHE" }) } })],
83
- * });
84
- * ```
85
- *
86
- * `kvDataAdapter()` is safe to call from `vite.config.ts` — it never touches
87
- * the Workers runtime — and the plugin defers instantiation of this handler to
88
- * the first request. This class stays exported for the runtime factory and for
89
- * backwards compatibility, but is not the recommended consumer API.
90
- */
91
- var KVCacheHandler = class {
92
- kv;
93
- prefix;
94
- ctx;
95
- ttlSeconds;
96
- /** Local in-memory cache for tag invalidation timestamps. Avoids redundant KV reads. */
97
- _tagCache = /* @__PURE__ */ new Map();
98
- /** TTL (ms) for local tag cache entries. After this, re-fetch from KV. */
99
- _tagCacheTtl;
100
- constructor(kvNamespace, options) {
101
- this.kv = kvNamespace;
102
- this.prefix = options?.appPrefix ? `${options.appPrefix}:` : "";
103
- this.ctx = options?.ctx;
104
- this.ttlSeconds = options?.ttlSeconds ?? 720 * 3600;
105
- this._tagCacheTtl = options?.tagCacheTtlMs ?? 5e3;
106
- }
107
- async get(key, _ctx) {
108
- const kvKey = this.prefix + ENTRY_PREFIX + key;
109
- const raw = await this.kv.get(kvKey);
110
- if (!raw) return null;
111
- let parsed;
112
- try {
113
- parsed = JSON.parse(raw);
114
- } catch {
115
- this._deleteInBackground(kvKey);
116
- return null;
117
- }
118
- const entry = validateCacheEntry(parsed);
119
- if (!entry) {
120
- console.error("[vinext] Invalid cache entry shape for key:", key);
121
- this._deleteInBackground(kvKey);
122
- return null;
123
- }
124
- let restoredValue = null;
125
- if (entry.value) {
126
- restoredValue = restoreArrayBuffers(entry.value);
127
- if (!restoredValue) {
128
- this._deleteInBackground(kvKey);
129
- return null;
130
- }
131
- }
132
- if (await this._hasRevalidatedTag(validUniqueTags(entry.tags), entry.lastModified)) {
133
- this._deleteInBackground(kvKey);
134
- return null;
135
- }
136
- const softTags = validUniqueTags(readStringArrayField(_ctx, "softTags"));
137
- if (await this._hasRevalidatedTag(softTags, entry.lastModified)) return null;
138
- if (entry.expireAt !== void 0 && entry.expireAt !== null && Date.now() > entry.expireAt) {
139
- this._deleteInBackground(kvKey);
140
- return null;
141
- }
142
- if (entry.revalidateAt !== null && Date.now() > entry.revalidateAt) return {
143
- lastModified: entry.lastModified,
144
- value: restoredValue,
145
- cacheState: "stale",
146
- cacheControl: entry.cacheControl
147
- };
148
- return {
149
- lastModified: entry.lastModified,
150
- value: restoredValue,
151
- cacheControl: entry.cacheControl
152
- };
153
- }
154
- /**
155
- * Check tag invalidation markers for stored tags or read-time soft tags.
156
- * Uses a local in-memory cache to avoid redundant KV reads for recently-seen tags.
157
- */
158
- async _hasRevalidatedTag(tags, lastModified) {
159
- if (tags.length === 0) return false;
160
- const now = Date.now();
161
- const uncachedTags = [];
162
- for (const tag of tags) {
163
- const cached = this._tagCache.get(tag);
164
- if (cached && now - cached.fetchedAt < this._tagCacheTtl) {
165
- if (Number.isNaN(cached.timestamp) || cached.timestamp >= lastModified) return true;
166
- } else {
167
- if (cached) this._tagCache.delete(tag);
168
- uncachedTags.push(tag);
169
- }
170
- }
171
- if (uncachedTags.length > 0) {
172
- const tagResults = await Promise.all(uncachedTags.map((tag) => this.kv.get(this.prefix + TAG_PREFIX + tag)));
173
- for (let i = 0; i < uncachedTags.length; i++) {
174
- const tagTime = tagResults[i];
175
- const tagTimestamp = tagTime ? Number(tagTime) : 0;
176
- this._tagCache.set(uncachedTags[i], {
177
- timestamp: tagTimestamp,
178
- fetchedAt: now
179
- });
180
- }
181
- for (const tag of uncachedTags) {
182
- const cached = this._tagCache.get(tag);
183
- if (!cached || cached.timestamp === 0) continue;
184
- if (Number.isNaN(cached.timestamp) || cached.timestamp >= lastModified) return true;
185
- }
186
- }
187
- return false;
188
- }
189
- set(key, data, ctx) {
190
- const tagSet = /* @__PURE__ */ new Set();
191
- if (data && "tags" in data && Array.isArray(data.tags)) for (const t of data.tags) {
192
- const validated = validateTag(t);
193
- if (validated) tagSet.add(validated);
194
- }
195
- if (ctx && "tags" in ctx && Array.isArray(ctx.tags)) for (const t of ctx.tags) {
196
- const validated = validateTag(t);
197
- if (validated) tagSet.add(validated);
198
- }
199
- const tags = [...tagSet];
200
- let effectiveRevalidate;
201
- let effectiveExpire;
202
- effectiveRevalidate = readCacheControlNumberField(ctx, "revalidate");
203
- effectiveExpire = readCacheControlNumberField(ctx, "expire");
204
- if (data && "revalidate" in data && typeof data.revalidate === "number") effectiveRevalidate = data.revalidate;
205
- if (effectiveRevalidate === 0) return Promise.resolve();
206
- const now = Date.now();
207
- const revalidateAt = typeof effectiveRevalidate === "number" && effectiveRevalidate > 0 ? now + effectiveRevalidate * 1e3 : null;
208
- const expireAt = typeof effectiveExpire === "number" && effectiveExpire > 0 ? now + effectiveExpire * 1e3 : null;
209
- const cacheControl = typeof effectiveRevalidate === "number" ? effectiveExpire === void 0 ? { revalidate: effectiveRevalidate } : {
210
- revalidate: effectiveRevalidate,
211
- expire: effectiveExpire
212
- } : void 0;
213
- const entry = {
214
- value: data ? serializeForJSON(data) : null,
215
- tags,
216
- lastModified: now,
217
- revalidateAt,
218
- expireAt,
219
- cacheControl
220
- };
221
- const expirationTtl = revalidateAt !== null ? this.ttlSeconds : void 0;
222
- const metadata = JSON.stringify({ tags }).length <= 1024 ? { tags } : void 0;
223
- return this._put(this.prefix + ENTRY_PREFIX + key, JSON.stringify(entry), {
224
- expirationTtl,
225
- metadata
226
- });
227
- }
228
- async revalidateTag(tags, _durations) {
229
- const tagList = Array.isArray(tags) ? tags : [tags];
230
- const now = Date.now();
231
- const validTags = tagList.filter((t) => validateTag(t) !== null);
232
- await Promise.all(validTags.map((tag) => this.kv.put(this.prefix + TAG_PREFIX + tag, String(now), { expirationTtl: 720 * 3600 })));
233
- for (const tag of validTags) this._tagCache.set(tag, {
234
- timestamp: now,
235
- fetchedAt: now
236
- });
237
- }
238
- /**
239
- * Invalidate all cache entries whose path tags fall under `pathPrefix`.
240
- *
241
- * Uses KV list metadata to discover tags without fetching entry values —
242
- * entries written by `set()` store their tags in KV metadata, so
243
- * `kv.list()` returns them inline with each key. This makes prefix
244
- * invalidation O(list_pages) instead of O(entries × get).
245
- *
246
- * Entries written before metadata was added (no metadata.tags) are
247
- * gracefully skipped — they'll be picked up on next `set()` which
248
- * writes metadata.
249
- *
250
- * When present, this method fully replaces the `revalidateTag` call
251
- * path in `revalidatePath()` — implementors own all path-based tag
252
- * handling.
253
- */
254
- async revalidateByPathPrefix(pathPrefix) {
255
- const tagsToInvalidate = /* @__PURE__ */ new Set();
256
- let cursor;
257
- const listPrefix = this.prefix + ENTRY_PREFIX;
258
- do {
259
- const page = await this.kv.list({
260
- prefix: listPrefix,
261
- cursor
262
- });
263
- for (const key of page.keys) {
264
- const tags = key.metadata?.tags;
265
- if (!Array.isArray(tags)) continue;
266
- for (const tag of tags) {
267
- if (typeof tag !== "string") continue;
268
- const rawPath = tag.startsWith(PATH_TAG_PREFIX) ? tag.slice(5) : tag;
269
- if (rawPath.startsWith("/") && isPathChildOf(rawPath, pathPrefix)) tagsToInvalidate.add(tag);
270
- }
271
- }
272
- cursor = page.list_complete ? void 0 : page.cursor;
273
- } while (cursor);
274
- if (tagsToInvalidate.size > 0) await this.revalidateTag([...tagsToInvalidate]);
275
- }
276
- /**
277
- * Clear the in-memory tag cache for this KVCacheHandler instance.
278
- *
279
- * Note: KVCacheHandler instances are typically reused across multiple
280
- * requests in a Cloudflare Worker. The `_tagCache` is intentionally
281
- * cross-request — it reduces redundant KV reads for recently-seen tags
282
- * across all requests hitting the same isolate, bounded by `tagCacheTtlMs`
283
- * (default 5s). vinext does NOT call this method per request.
284
- *
285
- * This is an opt-in escape hatch for callers that need stricter isolation
286
- * (e.g., tests, or environments with custom lifecycle management).
287
- * Callers that require per-request isolation should either construct a
288
- * fresh KVCacheHandler per request or invoke this method explicitly.
289
- */
290
- resetRequestCache() {
291
- this._tagCache.clear();
292
- }
293
- /**
294
- * Fire a KV delete in the background.
295
- * Prefers the per-request ExecutionContext from ALS (set by
296
- * runWithExecutionContext in the worker entry) so that background KV
297
- * operations are registered with the correct request's waitUntil().
298
- * Falls back to the constructor-provided ctx for callers that set it
299
- * explicitly, and to fire-and-forget when neither is available (Node.js dev).
300
- */
301
- _deleteInBackground(kvKey) {
302
- const promise = this.kv.delete(kvKey);
303
- const ctx = getRequestExecutionContext() ?? this.ctx;
304
- if (ctx) ctx.waitUntil(promise);
305
- }
306
- /**
307
- * Execute a KV put and return the promise so callers can await completion.
308
- * Also registers with ctx.waitUntil() so the Workers runtime keeps the
309
- * isolate alive even if the caller does not await the returned promise.
310
- */
311
- _put(kvKey, value, options) {
312
- const promise = this.kv.put(kvKey, value, options);
313
- const ctx = getRequestExecutionContext() ?? this.ctx;
314
- if (ctx) ctx.waitUntil(promise);
315
- return promise;
316
- }
317
- };
318
- const VALID_KINDS = new Set([
319
- "FETCH",
320
- "APP_PAGE",
321
- "PAGES",
322
- "APP_ROUTE",
323
- "REDIRECT",
324
- "IMAGE"
325
- ]);
326
- /**
327
- * Validate that a parsed JSON value has the expected KVCacheEntry shape.
328
- * Returns the validated entry or null if the shape is invalid.
329
- */
330
- function validateCacheEntry(raw) {
331
- if (!raw || typeof raw !== "object") return null;
332
- const obj = raw;
333
- if (typeof obj.lastModified !== "number") return null;
334
- if (!Array.isArray(obj.tags)) return null;
335
- if (obj.revalidateAt !== null && typeof obj.revalidateAt !== "number") return null;
336
- if (obj.expireAt !== void 0 && obj.expireAt !== null && typeof obj.expireAt !== "number") return null;
337
- if (obj.cacheControl !== void 0) {
338
- if (!isUnknownRecord(obj.cacheControl)) return null;
339
- if (typeof obj.cacheControl.revalidate !== "number") return null;
340
- if (obj.cacheControl.expire !== void 0 && typeof obj.cacheControl.expire !== "number") return null;
341
- }
342
- if (obj.value !== null) {
343
- if (!obj.value || typeof obj.value !== "object") return null;
344
- const value = obj.value;
345
- if (typeof value.kind !== "string" || !VALID_KINDS.has(value.kind)) return null;
346
- }
347
- return raw;
348
- }
349
- /**
350
- * Deep-clone a cache value, converting ArrayBuffer fields to base64 strings
351
- * so the entire structure can be JSON.stringify'd for KV storage.
352
- */
353
- function serializeForJSON(value) {
354
- if (value.kind === "APP_PAGE") return {
355
- ...value,
356
- rscData: value.rscData ? arrayBufferToBase64(value.rscData) : void 0
357
- };
358
- if (value.kind === "APP_ROUTE") return {
359
- ...value,
360
- body: arrayBufferToBase64(value.body)
361
- };
362
- if (value.kind === "IMAGE") return {
363
- ...value,
364
- buffer: arrayBufferToBase64(value.buffer)
365
- };
366
- return value;
367
- }
368
- /**
369
- * Restore base64 strings back to ArrayBuffers after JSON.parse.
370
- * Returns the restored `IncrementalCacheValue`, or `null` if any base64
371
- * decode fails (corrupted entry).
372
- */
373
- function restoreArrayBuffers(value) {
374
- if (value.kind === "APP_PAGE") {
375
- if (typeof value.rscData === "string") {
376
- const decoded = safeBase64ToArrayBuffer(value.rscData);
377
- if (!decoded) return null;
378
- return {
379
- ...value,
380
- rscData: decoded
381
- };
382
- }
383
- return value;
384
- }
385
- if (value.kind === "APP_ROUTE") {
386
- if (typeof value.body === "string") {
387
- const decoded = safeBase64ToArrayBuffer(value.body);
388
- if (!decoded) return null;
389
- return {
390
- ...value,
391
- body: decoded
392
- };
393
- }
394
- return value;
395
- }
396
- if (value.kind === "IMAGE") {
397
- if (typeof value.buffer === "string") {
398
- const decoded = safeBase64ToArrayBuffer(value.buffer);
399
- if (!decoded) return null;
400
- return {
401
- ...value,
402
- buffer: decoded
403
- };
404
- }
405
- return value;
406
- }
407
- return value;
408
- }
409
- function arrayBufferToBase64(buffer) {
410
- return Buffer.from(buffer).toString("base64");
411
- }
412
- /**
413
- * Decode a base64 string to an ArrayBuffer.
414
- * Validates the input against the base64 alphabet before decoding,
415
- * since Buffer.from(str, "base64") silently ignores invalid characters.
416
- */
417
- function base64ToArrayBuffer(base64) {
418
- if (!BASE64_RE.test(base64) || base64.length % 4 !== 0) throw new Error("Invalid base64 string");
419
- const buf = Buffer.from(base64, "base64");
420
- return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
421
- }
422
- /**
423
- * Safely decode base64 to ArrayBuffer. Returns null on invalid input
424
- * instead of throwing.
425
- */
426
- function safeBase64ToArrayBuffer(base64) {
427
- try {
428
- return base64ToArrayBuffer(base64);
429
- } catch {
430
- console.error("[vinext] Invalid base64 in cache entry");
431
- return null;
432
- }
433
- }
434
- //#endregion
435
- export { ENTRY_PREFIX, KVCacheHandler };
@@ -1,150 +0,0 @@
1
- import { findViteConfigPath } from "../../../utils/project.js";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- //#region ../cloudflare/src/deploy-config.ts
5
- function escapeRegExp(value) {
6
- return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
7
- }
8
- /**
9
- * Check whether an existing vite.config file already imports and uses the
10
- * Cloudflare Vite plugin. This is a heuristic text scan — it doesn't execute
11
- * the config — so it may produce false negatives for unusual configurations.
12
- *
13
- * Returns true when the config both imports the package and invokes
14
- * `cloudflare(...)` in the plugin list.
15
- */
16
- function viteConfigHasCloudflarePlugin(root) {
17
- const configPath = findViteConfigPath(root);
18
- if (!configPath) return false;
19
- try {
20
- const content = fs.readFileSync(configPath, "utf-8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
21
- const requireBinding = content.match(/(?:const|let|var)\s*{([^}]*)}\s*=\s*require\s*\(\s*["']@cloudflare\/vite-plugin["']\s*\)/)?.[1];
22
- if (requireBinding) {
23
- const requireMatch = requireBinding.match(/(?:^|,)\s*cloudflare\s*(?::\s*([A-Za-z_$][\w$]*))?\s*(?:,|$)/);
24
- if (requireMatch) {
25
- const binding = requireMatch[1] ?? "cloudflare";
26
- return new RegExp(`\\b${escapeRegExp(binding)}\\s*\\(`).test(content);
27
- }
28
- }
29
- const namedImports = content.match(/import\s*{([^}]*)}\s*from\s*["']@cloudflare\/vite-plugin["']/)?.[1];
30
- if (!namedImports) return false;
31
- const importMatch = namedImports.match(/(?:^|,)\s*cloudflare\s*(?:as\s+([A-Za-z_$][\w$]*))?\s*(?:,|$)/);
32
- if (!importMatch) return false;
33
- const binding = importMatch[1] ?? "cloudflare";
34
- return new RegExp(`\\b${escapeRegExp(binding)}\\s*\\(`).test(content);
35
- } catch {
36
- return false;
37
- }
38
- }
39
- /**
40
- * Extract the object-literal text of the `cache:` key (the `{ ... }` passed as
41
- * `vinext({ cache })`) from a Vite config source, via brace matching. Returns
42
- * null if there is no `cache:` object literal.
43
- */
44
- function extractCacheBlock(content) {
45
- const m = /\bcache\s*:\s*\{/.exec(content);
46
- if (!m) return null;
47
- const open = m.index + m[0].length - 1;
48
- let depth = 0;
49
- for (let i = open; i < content.length; i++) {
50
- const ch = content[i];
51
- if (ch === "{") depth++;
52
- else if (ch === "}" && --depth === 0) return content.slice(open, i + 1);
53
- }
54
- return null;
55
- }
56
- /**
57
- * Whether a `cdn` / `data` field inside the cache object is assigned a real
58
- * value (not absent, `undefined`, or `null`). Reads the value up to the next
59
- * comma / closing brace / newline, which is enough to tell an assignment like
60
- * `data: kvDataAdapter()` from `data: undefined`.
61
- */
62
- function cacheFieldAssigned(cacheBlock, field) {
63
- const m = new RegExp(`\\b${field}\\s*:\\s*([^,}\\n]+)`).exec(cacheBlock);
64
- if (!m) return false;
65
- const value = m[1].trim();
66
- return value.length > 0 && value !== "undefined" && value !== "null";
67
- }
68
- /**
69
- * Detect whether the Vite config assigns a CDN or data cache adapter — i.e. the
70
- * `cdn` or `data` field of the `vinext({ cache })` option is given a value.
71
- * This is a source-level check on those exact object fields, not a fuzzy scan
72
- * for adapter names. Mirrors {@link viteConfigHasCloudflarePlugin}'s leniency:
73
- * an unreadable or absent config is treated as configured so a deploy is never
74
- * blocked on a false negative.
75
- */
76
- function viteConfigHasCacheAdapter(root) {
77
- const configPath = findViteConfigPath(root);
78
- if (!configPath) return true;
79
- let content;
80
- try {
81
- content = fs.readFileSync(configPath, "utf-8");
82
- } catch {
83
- return true;
84
- }
85
- const block = extractCacheBlock(content);
86
- if (!block) return false;
87
- return cacheFieldAssigned(block, "cdn") || cacheFieldAssigned(block, "data");
88
- }
89
- function viteConfigHasImageAdapter(root) {
90
- const configPath = findViteConfigPath(root);
91
- if (!configPath) return true;
92
- try {
93
- const content = fs.readFileSync(configPath, "utf-8");
94
- const match = /\bimages\s*:\s*\{/.exec(content);
95
- if (!match) return false;
96
- const open = match.index + match[0].length - 1;
97
- let depth = 0;
98
- for (let index = open; index < content.length; index++) if (content[index] === "{") depth++;
99
- else if (content[index] === "}" && --depth === 0) {
100
- const optimizer = /\boptimizer\s*:\s*([^,}\n]+)/.exec(content.slice(open, index + 1))?.[1];
101
- return Boolean(optimizer && !/^(?:undefined|null)$/.test(optimizer.trim()));
102
- }
103
- return false;
104
- } catch {
105
- return true;
106
- }
107
- }
108
- /**
109
- * Detect whether an existing user-authored Worker entry wires up a cache
110
- * backend imperatively via one of the `setCacheHandler` / `setDataCacheHandler`
111
- * / `setCdnCacheAdapter` setters. These setters are deprecated in favour of the
112
- * declarative `vinext({ cache })` option, but older apps that scaffolded a KV
113
- * cache handler into their Worker entry must keep working — so a deploy should
114
- * not be blocked when the Worker entry already configures a backend.
115
- *
116
- * This is a heuristic text scan (it doesn't execute the entry), mirroring
117
- * {@link viteConfigHasCacheAdapter}'s leniency: an unreadable Worker entry is
118
- * treated as configured so a deploy is never blocked on a false negative. A
119
- * missing Worker entry returns false (nothing to inspect — defer to other
120
- * checks).
121
- */
122
- function workerEntryHasCacheHandler(root) {
123
- const candidates = [path.join(root, "worker", "index.ts"), path.join(root, "worker", "index.js")];
124
- for (const candidate of candidates) {
125
- if (!fs.existsSync(candidate)) continue;
126
- let content;
127
- try {
128
- content = fs.readFileSync(candidate, "utf-8");
129
- } catch {
130
- return true;
131
- }
132
- return /\b(?:setCacheHandler|setDataCacheHandler|setCdnCacheAdapter)\s*\(/.test(content);
133
- }
134
- return false;
135
- }
136
- /**
137
- * Build the error thrown when an ISR/cached app is deployed without a cache
138
- * adapter configured in the Vite config. Production deployments need a
139
- * persistent cache backend; vinext no longer scaffolds one into the Worker
140
- * entry, so it must be declared via `vinext({ cache })`.
141
- */
142
- function formatMissingCacheAdapterError(options) {
143
- const configRef = options.configFile ? options.configFile : "your Vite config";
144
- 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 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`;
145
- }
146
- function formatImageOptimizationHint() {
147
- 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.";
148
- }
149
- //#endregion
150
- export { formatImageOptimizationHint, formatMissingCacheAdapterError, viteConfigHasCacheAdapter, viteConfigHasCloudflarePlugin, viteConfigHasImageAdapter, workerEntryHasCacheHandler };
@@ -1,55 +0,0 @@
1
- //#region ../cloudflare/src/deploy-help.ts
2
- function formatDeployHelp() {
3
- return `
4
- vinext-cloudflare deploy - Deploy to Cloudflare Workers
5
-
6
- Usage: vinext-cloudflare deploy [options]
7
-
8
- One-command deployment to Cloudflare Workers. Automatically:
9
- - Detects App Router or Pages Router
10
- - Validates setup from vinext init --platform=cloudflare
11
- - Builds the project with Vite
12
- - Deploys via wrangler
13
-
14
- Options:
15
- --preview Deploy to preview environment (same as --env preview)
16
- --env <name> Deploy using wrangler env.<name>
17
- --name <name> Custom Worker name (default: from package.json)
18
- --skip-build Skip the build step (use existing dist/)
19
- --dry-run Validate setup without building or deploying
20
- --prerender-all Pre-render discovered routes after building (future
21
- releases will auto-populate the remote cache)
22
- --prerender-concurrency <count>
23
- Maximum number of routes to pre-render in parallel
24
- -h, --help Show this help
25
-
26
- Experimental:
27
- --experimental-tpr Enable Traffic-aware Pre-Rendering
28
- --tpr-coverage <pct> Traffic coverage target, 0-100 (default: 90)
29
- --tpr-limit <count> Hard cap on pages to pre-render (default: 1000)
30
- --tpr-window <hours> Analytics lookback window in hours (default: 24)
31
-
32
- TPR (Traffic-aware Pre-Rendering) uses Cloudflare zone analytics to determine
33
- which pages get the most traffic and pre-renders them into KV cache during
34
- deploy. This feature is experimental and must be explicitly enabled. Requires
35
- a custom domain (zone analytics are unavailable on *.workers.dev) and the
36
- CLOUDFLARE_API_TOKEN environment variable with Zone.Analytics read permission.
37
-
38
- Examples:
39
- npx @vinext/cloudflare deploy Build and deploy to production
40
- vpx @vinext/cloudflare deploy Build and deploy with Vite+
41
- vp exec vinext-cloudflare deploy Run the locally installed Vite+ bin
42
- vinext-cloudflare deploy --preview Deploy to a preview URL
43
- vinext-cloudflare deploy --env staging Deploy using wrangler env.staging
44
- vinext-cloudflare deploy --dry-run Validate setup without building or deploying
45
- vinext-cloudflare deploy --name my-app Deploy with a custom Worker name
46
- vinext-cloudflare deploy --experimental-tpr Enable TPR during deploy
47
- vinext-cloudflare deploy --experimental-tpr --tpr-coverage 95 Cover 95% of traffic
48
- vinext-cloudflare deploy --experimental-tpr --tpr-limit 500 Cap at 500 pages
49
- `;
50
- }
51
- function printDeployHelp() {
52
- console.log(formatDeployHelp());
53
- }
54
- //#endregion
55
- export { printDeployHelp };