@vinext/cloudflare 1.0.0-beta.1 → 1.0.0-beta.3

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.
@@ -1,4 +1,5 @@
1
1
  import { KvDataAdapterOptions } from "./kv-data-adapter.js";
2
+ import { ENTRY_PREFIX } from "./kv-key.js";
2
3
  import { ExecutionContextLike } from "vinext/shims/request-context";
3
4
  import { CacheHandler, CacheHandlerValue, IncrementalCacheValue } from "vinext/shims/cache";
4
5
 
@@ -28,8 +29,6 @@ type KVNamespace = {
28
29
  cursor?: string;
29
30
  }>;
30
31
  };
31
- /** Key prefix for cache entries. */
32
- declare const ENTRY_PREFIX = "cache:";
33
32
  /**
34
33
  * Cloudflare KV data cache handler.
35
34
  *
@@ -53,7 +52,7 @@ declare const ENTRY_PREFIX = "cache:";
53
52
  */
54
53
  declare class KVCacheHandler implements CacheHandler {
55
54
  private kv;
56
- private prefix;
55
+ private keySpace;
57
56
  private ctx;
58
57
  private ttlSeconds;
59
58
  /** Local in-memory cache for tag invalidation timestamps. Avoids redundant KV reads. */
@@ -66,6 +65,8 @@ declare class KVCacheHandler implements CacheHandler {
66
65
  ttlSeconds?: number; /** TTL in milliseconds for the local tag cache. Defaults to 5000ms. */
67
66
  tagCacheTtlMs?: number;
68
67
  });
68
+ private _entryKey;
69
+ private _tagKey;
69
70
  get(key: string, _ctx?: Record<string, unknown>): Promise<CacheHandlerValue | null>;
70
71
  /**
71
72
  * Check tag invalidation markers for stored tags or read-time soft tags.
@@ -1,6 +1,7 @@
1
+ import { ENTRY_PREFIX, createKvKeySpace } from "./kv-key.js";
1
2
  import { isUnknownRecord, readCacheControlNumberField } from "../utils/cache-control-metadata.js";
2
- import { Buffer } from "node:buffer";
3
3
  import { getRequestExecutionContext } from "vinext/shims/request-context";
4
+ import { Buffer } from "node:buffer";
4
5
  //#region src/cache/kv-data-adapter.runtime.ts
5
6
  /**
6
7
  * Cloudflare KV data cache — implementation + runtime factory.
@@ -23,10 +24,6 @@ import { getRequestExecutionContext } from "vinext/shims/request-context";
23
24
  */
24
25
  /** Default KV namespace binding name read from the Worker `env`. */
25
26
  const DEFAULT_BINDING = "VINEXT_KV_CACHE";
26
- /** Key prefix for tag invalidation timestamps. */
27
- const TAG_PREFIX = "__tag:";
28
- /** Key prefix for cache entries. */
29
- const ENTRY_PREFIX = "cache:";
30
27
  /** Prefix used by revalidatePath for path-based tags. */
31
28
  const PATH_TAG_PREFIX = "_N_T_";
32
29
  /** Max tag length to prevent KV key abuse. */
@@ -96,7 +93,7 @@ function isPathChildOf(path, prefix) {
96
93
  */
97
94
  var KVCacheHandler = class {
98
95
  kv;
99
- prefix;
96
+ keySpace;
100
97
  ctx;
101
98
  ttlSeconds;
102
99
  /** Local in-memory cache for tag invalidation timestamps. Avoids redundant KV reads. */
@@ -105,13 +102,19 @@ var KVCacheHandler = class {
105
102
  _tagCacheTtl;
106
103
  constructor(kvNamespace, options) {
107
104
  this.kv = kvNamespace;
108
- this.prefix = options?.appPrefix ? `${options.appPrefix}:` : "";
105
+ this.keySpace = createKvKeySpace(options?.appPrefix);
109
106
  this.ctx = options?.ctx;
110
107
  this.ttlSeconds = options?.ttlSeconds ?? 720 * 3600;
111
108
  this._tagCacheTtl = options?.tagCacheTtlMs ?? 5e3;
112
109
  }
110
+ _entryKey(key) {
111
+ return this.keySpace.entryKey(key);
112
+ }
113
+ _tagKey(tag) {
114
+ return this.keySpace.tagKey(tag);
115
+ }
113
116
  async get(key, _ctx) {
114
- const kvKey = this.prefix + ENTRY_PREFIX + key;
117
+ const kvKey = this._entryKey(key);
115
118
  const raw = await this.kv.get(kvKey);
116
119
  if (!raw) return null;
117
120
  let parsed;
@@ -178,7 +181,7 @@ var KVCacheHandler = class {
178
181
  }
179
182
  }
180
183
  if (uncachedTags.length > 0) {
181
- const tagResults = await Promise.all(uncachedTags.map((tag) => this.kv.get(this.prefix + TAG_PREFIX + tag)));
184
+ const tagResults = await Promise.all(uncachedTags.map((tag) => this.kv.get(this._tagKey(tag))));
182
185
  for (let i = 0; i < uncachedTags.length; i++) {
183
186
  const tagTime = tagResults[i];
184
187
  const tagTimestamp = tagTime ? Number(tagTime) : 0;
@@ -229,7 +232,7 @@ var KVCacheHandler = class {
229
232
  };
230
233
  const expirationTtl = revalidateAt !== null ? this.ttlSeconds : void 0;
231
234
  const metadata = JSON.stringify({ tags }).length <= 1024 ? { tags } : void 0;
232
- return this._put(this.prefix + ENTRY_PREFIX + key, JSON.stringify(entry), {
235
+ return this._put(this._entryKey(key), JSON.stringify(entry), {
233
236
  expirationTtl,
234
237
  metadata
235
238
  });
@@ -238,7 +241,7 @@ var KVCacheHandler = class {
238
241
  const tagList = Array.isArray(tags) ? tags : [tags];
239
242
  const now = Date.now();
240
243
  const validTags = tagList.filter((t) => validateTag(t) !== null);
241
- await Promise.all(validTags.map((tag) => this.kv.put(this.prefix + TAG_PREFIX + tag, String(now), { expirationTtl: 720 * 3600 })));
244
+ await Promise.all(validTags.map((tag) => this.kv.put(this._tagKey(tag), String(now), { expirationTtl: 720 * 3600 })));
242
245
  for (const tag of validTags) this._tagCache.set(tag, {
243
246
  timestamp: now,
244
247
  fetchedAt: now
@@ -263,7 +266,7 @@ var KVCacheHandler = class {
263
266
  async revalidateByPathPrefix(pathPrefix) {
264
267
  const tagsToInvalidate = /* @__PURE__ */ new Set();
265
268
  let cursor;
266
- const listPrefix = this.prefix + ENTRY_PREFIX;
269
+ const listPrefix = this.keySpace.entryPrefix;
267
270
  do {
268
271
  const page = await this.kv.list({
269
272
  prefix: listPrefix,
@@ -0,0 +1,15 @@
1
+ //#region src/cache/kv-key.d.ts
2
+ /** Key prefix for cache entries. */
3
+ declare const ENTRY_PREFIX = "cache:";
4
+ type KvKeySpace = {
5
+ /** Prefix shared by every cache entry, including entries with hashed logical keys. */entryPrefix: string;
6
+ entryKey(logicalKey: string): string;
7
+ tagKey(tag: string): string;
8
+ };
9
+ /**
10
+ * Create the deterministic key namespace shared by runtime cache operations
11
+ * and deploy-time prerender population.
12
+ */
13
+ declare function createKvKeySpace(appPrefix: string | undefined): KvKeySpace;
14
+ //#endregion
15
+ export { ENTRY_PREFIX, KvKeySpace, createKvKeySpace };
@@ -0,0 +1,43 @@
1
+ import { fnv1a64 } from "vinext/internal/utils/hash";
2
+ //#region src/cache/kv-key.ts
3
+ /** Key prefix for tag invalidation timestamps. */
4
+ const TAG_PREFIX = "__tag:";
5
+ /** Key prefix for cache entries. */
6
+ const ENTRY_PREFIX = "cache:";
7
+ /** Cloudflare KV's maximum UTF-8 encoded key length. */
8
+ const KV_KEY_MAX_BYTES = 512;
9
+ /** Marker for logical keys that must be hashed to fit in KV. */
10
+ const HASHED_KEY_PREFIX = "__hash:";
11
+ const KV_KEY_ENCODER = new TextEncoder();
12
+ function kvKeyByteLength(key) {
13
+ return KV_KEY_ENCODER.encode(key).length;
14
+ }
15
+ /**
16
+ * Keep short app prefixes readable, but bound the namespace portion so a
17
+ * hashed entry or tag key is always able to fit within Cloudflare KV's limit.
18
+ */
19
+ function normalizeAppPrefix(appPrefix) {
20
+ if (!appPrefix) return "";
21
+ const prefix = `${appPrefix}:`;
22
+ if (kvKeyByteLength(`${prefix}cache:${HASHED_KEY_PREFIX}${fnv1a64("")}`) <= KV_KEY_MAX_BYTES) return prefix;
23
+ return `__app:${fnv1a64(appPrefix)}:`;
24
+ }
25
+ function buildStorageKey(prefix, categoryPrefix, logicalKey) {
26
+ const key = `${prefix}${categoryPrefix}${logicalKey}`;
27
+ if (kvKeyByteLength(key) <= KV_KEY_MAX_BYTES) return key;
28
+ return `${prefix}${categoryPrefix}${HASHED_KEY_PREFIX}${fnv1a64(logicalKey)}`;
29
+ }
30
+ /**
31
+ * Create the deterministic key namespace shared by runtime cache operations
32
+ * and deploy-time prerender population.
33
+ */
34
+ function createKvKeySpace(appPrefix) {
35
+ const prefix = normalizeAppPrefix(appPrefix);
36
+ return {
37
+ entryPrefix: `${prefix}${ENTRY_PREFIX}`,
38
+ entryKey: (logicalKey) => buildStorageKey(prefix, ENTRY_PREFIX, logicalKey),
39
+ tagKey: (tag) => buildStorageKey(prefix, TAG_PREFIX, tag)
40
+ };
41
+ }
42
+ //#endregion
43
+ export { ENTRY_PREFIX, createKvKeySpace };
package/dist/deploy.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { readPrerenderWarmPaths, warmCdnCache } from "./cdn-warm.js";
2
2
  import { parseWranglerConfig, runTPR } from "./tpr.js";
3
3
  import { formatImageOptimizationHint, formatMissingCacheAdapterError, resolveKvDataAdapterConfig, viteConfigHasCacheAdapter, viteConfigHasCloudflarePlugin, viteConfigHasImageAdapter, workerEntryHasCacheHandler } from "./deploy-config.js";
4
- import { parseWorkersDevUrl } from "./workers-dev-url.js";
5
4
  import { runWranglerDeploymentStatus, runWranglerTriggersDeploy, runWranglerVersionDeploy, runWranglerVersionUpload } from "./version-deploy.js";
5
+ import { parseWorkerDeploymentUrl } from "./worker-deployment-url.js";
6
6
  import { buildPrerenderKVPairs } from "./prerender-kv-populate.js";
7
7
  import { createRequire } from "node:module";
8
8
  import path from "node:path";
@@ -344,7 +344,7 @@ async function runWranglerDeploy(root, options, execute = spawn) {
344
344
  reject(/* @__PURE__ */ new Error(`Wrangler deploy failed with ${exitReason}.`));
345
345
  });
346
346
  });
347
- return parseWorkersDevUrl(output) ?? "(URL not detected in wrangler output)";
347
+ return parseWorkerDeploymentUrl(output) ?? "(URL not detected in wrangler output)";
348
348
  }
349
349
  async function deployWithCdnWarmup(root, paths, options) {
350
350
  const upload = runWranglerVersionUpload(root, options);
@@ -1,4 +1,4 @@
1
- import { ENTRY_PREFIX } from "./cache/kv-data-adapter.runtime.js";
1
+ import { createKvKeySpace } from "./cache/kv-key.js";
2
2
  import path from "node:path";
3
3
  import fs from "node:fs";
4
4
  import { getRenderedAppRoutes, readPrerenderManifest } from "vinext/internal/server/prerender-manifest";
@@ -28,9 +28,6 @@ function formatUnknownError(error) {
28
28
  if (error instanceof Error && error.message) return error.message;
29
29
  return String(error);
30
30
  }
31
- function buildKVKey(appPrefix, cacheKey) {
32
- return `${appPrefix ? `${appPrefix}:` : ""}${ENTRY_PREFIX}${cacheKey}`;
33
- }
34
31
  function buildCacheEntry(value, tags, now, revalidateSeconds, expireSeconds) {
35
32
  const cacheControl = revalidateSeconds === void 0 ? void 0 : expireSeconds === void 0 ? { revalidate: revalidateSeconds } : {
36
33
  revalidate: revalidateSeconds,
@@ -64,6 +61,7 @@ function buildPrerenderKVPairs(serverDir, options) {
64
61
  const now = options?.now ?? Date.now();
65
62
  const ttlSeconds = options?.ttlSeconds ?? DEFAULT_KV_TTL_SECONDS;
66
63
  const trailingSlash = manifest.trailingSlash ?? false;
64
+ const keySpace = createKvKeySpace(options?.appPrefix);
67
65
  let routeCount = 0;
68
66
  for (const route of getRenderedAppRoutes(manifest.routes)) {
69
67
  const artifactPathname = route.path ?? route.route;
@@ -87,7 +85,7 @@ function buildPrerenderKVPairs(serverDir, options) {
87
85
  const htmlKey = appIsrCacheKey(cachePathname, "html", manifest.buildId);
88
86
  const rscKey = appIsrCacheKey(cachePathname, "rsc", manifest.buildId);
89
87
  pairs.push({
90
- key: buildKVKey(options?.appPrefix, htmlKey),
88
+ key: keySpace.entryKey(htmlKey),
91
89
  value: buildCacheEntry({
92
90
  kind: "APP_PAGE",
93
91
  html: fs.readFileSync(htmlPath, "utf-8"),
@@ -99,7 +97,7 @@ function buildPrerenderKVPairs(serverDir, options) {
99
97
  if (fs.existsSync(rscPath)) {
100
98
  const rscData = fs.readFileSync(rscPath).toString("base64");
101
99
  pairs.push({
102
- key: buildKVKey(options?.appPrefix, rscKey),
100
+ key: keySpace.entryKey(rscKey),
103
101
  value: buildCacheEntry({
104
102
  kind: "APP_PAGE",
105
103
  html: "",
package/dist/tpr.d.ts CHANGED
@@ -71,7 +71,7 @@ declare function resolveVinextProdServerPath(root: string): string;
71
71
  * Build KV bulk API pairs from pre-rendered entries.
72
72
  *
73
73
  * Key format matches the runtime KVCacheHandler exactly:
74
- * ENTRY_PREFIX + isrCacheKey("app", pathname, buildId) + ":html"
74
+ * createKvKeySpace().entryKey(isrCacheKey("app", pathname, buildId) + ":html")
75
75
  * → "cache:app:<buildId>:<pathname>:html"
76
76
  */
77
77
  declare function buildTprKVPairs(entries: Map<string, PrerenderResult>, buildId: string | undefined, defaultRevalidateSeconds: number): Array<{
package/dist/tpr.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ENTRY_PREFIX } from "./cache/kv-data-adapter.runtime.js";
1
+ import { createKvKeySpace } from "./cache/kv-key.js";
2
2
  import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
4
  import fs from "node:fs";
@@ -578,12 +578,13 @@ const MAX_KV_TTL_SECONDS = 720 * 3600;
578
578
  * Build KV bulk API pairs from pre-rendered entries.
579
579
  *
580
580
  * Key format matches the runtime KVCacheHandler exactly:
581
- * ENTRY_PREFIX + isrCacheKey("app", pathname, buildId) + ":html"
581
+ * createKvKeySpace().entryKey(isrCacheKey("app", pathname, buildId) + ":html")
582
582
  * → "cache:app:<buildId>:<pathname>:html"
583
583
  */
584
584
  function buildTprKVPairs(entries, buildId, defaultRevalidateSeconds) {
585
585
  const now = Date.now();
586
586
  const pairs = [];
587
+ const keySpace = createKvKeySpace(void 0);
587
588
  for (const [routePath, result] of entries) {
588
589
  if (!isTprCacheable(result.headers)) continue;
589
590
  const revalidateSeconds = readTprRevalidateHeader(result.headers) ?? defaultRevalidateSeconds;
@@ -602,7 +603,7 @@ function buildTprKVPairs(entries, buildId, defaultRevalidateSeconds) {
602
603
  lastModified: now,
603
604
  revalidateAt
604
605
  };
605
- const cacheKey = ENTRY_PREFIX + isrCacheKey("app", routePath, buildId) + ":html";
606
+ const cacheKey = keySpace.entryKey(isrCacheKey("app", routePath, buildId) + ":html");
606
607
  pairs.push({
607
608
  key: cacheKey,
608
609
  value: JSON.stringify(entry),
@@ -0,0 +1,4 @@
1
+ //#region src/worker-deployment-url.d.ts
2
+ declare function parseWorkerDeploymentUrl(output: string): string | null;
3
+ //#endregion
4
+ export { parseWorkerDeploymentUrl };
@@ -0,0 +1,23 @@
1
+ import { parseWorkersDevUrl } from "./workers-dev-url.js";
2
+ //#region src/worker-deployment-url.ts
3
+ function parseWorkerDeploymentUrl(output) {
4
+ return parseWorkersDevUrl(output) ?? parseCustomDomainUrl(output);
5
+ }
6
+ function parseCustomDomainUrl(output) {
7
+ for (const line of output.split(/\r?\n/)) {
8
+ const match = /^\s*(\S+)\s+\(custom domain(?: - zone (?:id|name): [^)]+)?\)(?: \[([^\]\r\n]+)\])?\s*$/.exec(line);
9
+ const hostname = match?.[1];
10
+ if (!hostname) continue;
11
+ if (match[2]?.split(",").some((flag) => flag.trim() === "disabled")) continue;
12
+ try {
13
+ const url = new URL(`https://${hostname}`);
14
+ if (url.username !== "" || url.password !== "" || url.port !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "") continue;
15
+ return url.origin;
16
+ } catch {
17
+ continue;
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+ //#endregion
23
+ export { parseWorkerDeploymentUrl };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vinext/cloudflare",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.3",
4
4
  "description": "Cloudflare adapters for vinext (KV data cache and Images-backed image optimization).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -54,10 +54,10 @@
54
54
  "@types/node": "^25.9.2",
55
55
  "vite": "npm:@voidzero-dev/vite-plus-core@0.2.2",
56
56
  "vite-plus": "0.2.2",
57
- "vinext": "1.0.0-beta.1"
57
+ "vinext": "1.0.0-beta.3"
58
58
  },
59
59
  "peerDependencies": {
60
- "vinext": ">=0.0.0"
60
+ "vinext": "^1.0.0-beta.3"
61
61
  },
62
62
  "engines": {
63
63
  "node": ">=22"