@void/edge 0.25.0 → 0.26.1

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/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @void/edge
2
2
 
3
- Shared routing and static asset helpers for Void's direct Cloudflare deployments and platform dispatch runtime.
3
+ Routing and static asset helpers shared by [Void](https://void.cloud)'s direct Cloudflare deployments and platform runtime.
4
4
 
5
- Configure redirects, rewrites, headers, and asset behavior through `void.config.ts`. See [Static Assets](https://void.cloud/guide/edge/static-assets) and the [configuration reference](https://void.cloud/reference/config#routing).
5
+ Configure redirects, rewrites, headers, and asset behavior through `void.config.ts`.
6
+
7
+ ## Documentation
8
+
9
+ See [Static Assets](https://void.cloud/guide/edge/static-assets) and the [configuration reference](https://void.cloud/reference/config#routing).
10
+
11
+ Read the [Void documentation](https://void.cloud) online or use the Markdown guides and references in `node_modules/void/docs/`, included with the `void` package.
package/dist/index.d.mts CHANGED
@@ -71,8 +71,7 @@ export declare function compilePattern(source: string): CompiledPattern;
71
71
  */
72
72
  export declare function matchPatternCaptures(pattern: string, pathname: string): Record<string, string> | null;
73
73
  /**
74
- * Match a pattern against a pathname (boolean form). Header-rule callers only
75
- * need a yes/no — they don't care about captures. Malformed patterns yield
74
+ * Match a pattern against a pathname (boolean form). Malformed patterns yield
76
75
  * `false` rather than throwing (see `matchPatternCaptures`).
77
76
  */
78
77
  export declare function matchPattern(pattern: string, pathname: string): boolean;
@@ -81,14 +80,10 @@ export declare function matchPattern(pattern: string, pathname: string): boolean
81
80
  * captures. Only `*` is special (matches any characters including `/`); every
82
81
  * other character (including `:`) is treated as a literal.
83
82
  *
84
- * This preserves the pre-`feat/rewrites` semantics of header rules,
85
- * `routing.revalidate` keys, and `routing.revalidateQueryAllowlist` keys —
86
- * those callers never had `:name` capture syntax and existing tenant configs
87
- * may include literal `:` (e.g. `/api/v1:health`, `:batchGet`-style URLs).
88
- *
89
- * The capture-aware `matchPattern` above continues to be used by routing
90
- * rules (`redirects`/`rewrites`/`fallbacks`) where `:name` capture syntax was
91
- * intentionally introduced (see design-doc 0064).
83
+ * Used for header rules and `routing.revalidateQueryAllowlist` keys, which
84
+ * never had `:name` capture syntax; existing tenant configs may include literal
85
+ * `:` (e.g. `/api/v1:health`, `:batchGet`-style URLs). Routing rules
86
+ * (`redirects`/`rewrites`/`fallbacks`) use the capture-aware `compilePattern`.
92
87
  */
93
88
  export declare function matchLiteralPattern(pattern: string, pathname: string): boolean;
94
89
  /**
@@ -111,8 +106,7 @@ export declare function matchLiteralPattern(pattern: string, pathname: string):
111
106
  * from page routes on the deploy write side (`[slug]` → `/blog/:slug`,
112
107
  * `[...path]` → `/docs/:path{.+}`), so the `:name` here is a real route param
113
108
  * and MUST bind the same way the framework's own client route matcher
114
- * (`patternToRegex` in `pages/manifest.ts`) binds it — matching the write-side
115
- * matcher in `packages/api/src/routes/deploy.ts`.
109
+ * (`patternToRegex` in `pages/manifest.ts`) binds it.
116
110
  *
117
111
  * It is intentionally NOT used for user-authored header rules
118
112
  * (`applyHeaderRules`) or `revalidateQueryAllowlist` keys
@@ -230,8 +224,7 @@ type ResolvedAsset = Readonly<{
230
224
  *
231
225
  * Known limitation: a file whose name contains a literal `%` (e.g. `logo%20.svg`,
232
226
  * requested as `logo%2520.svg`) is not resolved — the path encoder leaves `%`
233
- * as-is, so the round-trip can't reproduce the double-escaped request. Such
234
- * names 404'd before this change too, so this is not a regression.
227
+ * as-is, so the round-trip can't reproduce the double-escaped request.
235
228
  */
236
229
  export declare function safeDecodePathname(pathname: string): string;
237
230
  /**
package/dist/index.mjs CHANGED
@@ -15,13 +15,26 @@ const MAX_CACHE = 256;
15
15
  const compileCache = /* @__PURE__ */ new Map();
16
16
  const REGEX_SPECIAL = /[.+?^${}()|[\]\\]/;
17
17
  const PARAM_CHAR = /[A-Za-z0-9_]/;
18
- function compilePattern(source) {
19
- const cached = compileCache.get(source);
20
- if (cached) {
21
- compileCache.delete(source);
22
- compileCache.set(source, cached);
23
- return cached;
18
+ /** LRU read: re-inserting a hit moves it to the most-recently-used end of Map order. */
19
+ function lruGet(cache, key) {
20
+ const value = cache.get(key);
21
+ if (value !== void 0) {
22
+ cache.delete(key);
23
+ cache.set(key, value);
24
+ }
25
+ return value;
26
+ }
27
+ /** LRU write: evict the least-recently-used entry once the cache is full. */
28
+ function lruSet(cache, key, value) {
29
+ if (cache.size >= MAX_CACHE) {
30
+ const first = cache.keys().next();
31
+ if (!first.done) cache.delete(first.value);
24
32
  }
33
+ cache.set(key, value);
34
+ }
35
+ function compilePattern(source) {
36
+ const cached = lruGet(compileCache, source);
37
+ if (cached) return cached;
25
38
  const paramNames = [];
26
39
  let legacySplatCaptured = false;
27
40
  let pattern = "";
@@ -37,12 +50,11 @@ function compilePattern(source) {
37
50
  index++;
38
51
  continue;
39
52
  }
53
+ paramNames.push(name);
40
54
  if (source[nameEnd] === "*") {
41
- paramNames.push(name);
42
55
  pattern += `(?<${name}>.*)`;
43
56
  index = nameEnd + 1;
44
57
  } else {
45
- paramNames.push(name);
46
58
  pattern += `(?<${name}>[^/]+)`;
47
59
  index = nameEnd;
48
60
  }
@@ -64,11 +76,7 @@ function compilePattern(source) {
64
76
  regex: new RegExp(`^${pattern}$`),
65
77
  paramNames
66
78
  };
67
- if (compileCache.size >= MAX_CACHE) {
68
- const first = compileCache.keys().next();
69
- if (!first.done) compileCache.delete(first.value);
70
- }
71
- compileCache.set(source, compiled);
79
+ lruSet(compileCache, source, compiled);
72
80
  return compiled;
73
81
  }
74
82
  /**
@@ -91,8 +99,7 @@ function matchPatternCaptures(pattern, pathname) {
91
99
  return match.groups ?? {};
92
100
  }
93
101
  /**
94
- * Match a pattern against a pathname (boolean form). Header-rule callers only
95
- * need a yes/no — they don't care about captures. Malformed patterns yield
102
+ * Match a pattern against a pathname (boolean form). Malformed patterns yield
96
103
  * `false` rather than throwing (see `matchPatternCaptures`).
97
104
  */
98
105
  function matchPattern(pattern, pathname) {
@@ -108,32 +115,21 @@ const literalRegexCache = /* @__PURE__ */ new Map();
108
115
  * captures. Only `*` is special (matches any characters including `/`); every
109
116
  * other character (including `:`) is treated as a literal.
110
117
  *
111
- * This preserves the pre-`feat/rewrites` semantics of header rules,
112
- * `routing.revalidate` keys, and `routing.revalidateQueryAllowlist` keys —
113
- * those callers never had `:name` capture syntax and existing tenant configs
114
- * may include literal `:` (e.g. `/api/v1:health`, `:batchGet`-style URLs).
115
- *
116
- * The capture-aware `matchPattern` above continues to be used by routing
117
- * rules (`redirects`/`rewrites`/`fallbacks`) where `:name` capture syntax was
118
- * intentionally introduced (see design-doc 0064).
118
+ * Used for header rules and `routing.revalidateQueryAllowlist` keys, which
119
+ * never had `:name` capture syntax; existing tenant configs may include literal
120
+ * `:` (e.g. `/api/v1:health`, `:batchGet`-style URLs). Routing rules
121
+ * (`redirects`/`rewrites`/`fallbacks`) use the capture-aware `compilePattern`.
119
122
  */
120
123
  function matchLiteralPattern(pattern, pathname) {
121
- let regex = literalRegexCache.get(pattern);
122
- if (regex) {
123
- literalRegexCache.delete(pattern);
124
- literalRegexCache.set(pattern, regex);
125
- } else {
124
+ let regex = lruGet(literalRegexCache, pattern);
125
+ if (!regex) {
126
126
  try {
127
127
  const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
128
128
  regex = new RegExp(`^${escaped}$`);
129
129
  } catch {
130
130
  return false;
131
131
  }
132
- if (literalRegexCache.size >= MAX_CACHE) {
133
- const first = literalRegexCache.keys().next();
134
- if (!first.done) literalRegexCache.delete(first.value);
135
- }
136
- literalRegexCache.set(pattern, regex);
132
+ lruSet(literalRegexCache, pattern, regex);
137
133
  }
138
134
  try {
139
135
  return regex.test(pathname);
@@ -162,8 +158,7 @@ const segmentRegexCache = /* @__PURE__ */ new Map();
162
158
  * from page routes on the deploy write side (`[slug]` → `/blog/:slug`,
163
159
  * `[...path]` → `/docs/:path{.+}`), so the `:name` here is a real route param
164
160
  * and MUST bind the same way the framework's own client route matcher
165
- * (`patternToRegex` in `pages/manifest.ts`) binds it — matching the write-side
166
- * matcher in `packages/api/src/routes/deploy.ts`.
161
+ * (`patternToRegex` in `pages/manifest.ts`) binds it.
167
162
  *
168
163
  * It is intentionally NOT used for user-authored header rules
169
164
  * (`applyHeaderRules`) or `revalidateQueryAllowlist` keys
@@ -176,22 +171,15 @@ const segmentRegexCache = /* @__PURE__ */ new Map();
176
171
  * not be shared or they would cross-contaminate.
177
172
  */
178
173
  function matchSegmentPattern(pattern, pathname) {
179
- let regex = segmentRegexCache.get(pattern);
180
- if (regex) {
181
- segmentRegexCache.delete(pattern);
182
- segmentRegexCache.set(pattern, regex);
183
- } else {
174
+ let regex = lruGet(segmentRegexCache, pattern);
175
+ if (!regex) {
184
176
  try {
185
177
  const escaped = pattern.replace(/(^|\/):[^/{}]+\{\.\+\}(?=\/|$)/g, "$1").replace(/(^|\/):[^/]+(?=\/|$)/g, "$1").replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*").replaceAll("", "[^/]+").replaceAll("", ".+");
186
178
  regex = new RegExp(`^${escaped}$`);
187
179
  } catch {
188
180
  return false;
189
181
  }
190
- if (segmentRegexCache.size >= MAX_CACHE) {
191
- const first = segmentRegexCache.keys().next();
192
- if (!first.done) segmentRegexCache.delete(first.value);
193
- }
194
- segmentRegexCache.set(pattern, regex);
182
+ lruSet(segmentRegexCache, pattern, regex);
195
183
  }
196
184
  try {
197
185
  return regex.test(pathname);
@@ -205,9 +193,11 @@ function matchSegmentPattern(pattern, pathname) {
205
193
  * because `compilePattern` captures anonymous `*` as `splat`.
206
194
  */
207
195
  function substituteParams(destination, params) {
208
- return destination.replace(/:([A-Za-z0-9_]+)\*?/g, (whole, name) => {
196
+ const result = destination.replace(/:([A-Za-z0-9_]+)\*?/g, (whole, name) => {
209
197
  return params[name] ?? whole;
210
198
  });
199
+ if (/^[/\\]{2}/.test(result) && !/^[/\\]{2}/.test(destination)) return "/" + result.replace(/^[/\\]+/, "");
200
+ return result;
211
201
  }
212
202
  //#endregion
213
203
  //#region src/headers.ts
@@ -412,10 +402,10 @@ function createStaticEdgeHandler(config) {
412
402
  const originalUrl = new URL(request.url);
413
403
  if (request.method === "GET" && originalUrl.pathname === "/__void/ready") {
414
404
  if (!config.versionMetadataBinding) return Response.json({ ready: true });
415
- const metadata = config.versionMetadataBinding ? env[config.versionMetadataBinding] : void 0;
405
+ const metadata = env[config.versionMetadataBinding];
416
406
  return Response.json({
417
407
  ready: true,
418
- ...config.versionMetadataBinding ? { versionId: metadata?.id } : {}
408
+ versionId: metadata?.id
419
409
  }, { headers: { "cache-control": "no-store" } });
420
410
  }
421
411
  let effectiveUrl = new URL(originalUrl);
@@ -481,8 +471,7 @@ function rewriteRequest(request, url, originalUrl) {
481
471
  *
482
472
  * Known limitation: a file whose name contains a literal `%` (e.g. `logo%20.svg`,
483
473
  * requested as `logo%2520.svg`) is not resolved — the path encoder leaves `%`
484
- * as-is, so the round-trip can't reproduce the double-escaped request. Such
485
- * names 404'd before this change too, so this is not a regression.
474
+ * as-is, so the round-trip can't reproduce the double-escaped request.
486
475
  */
487
476
  function safeDecodePathname(pathname) {
488
477
  let decoded;
package/package.json CHANGED
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "name": "@void/edge",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
4
+ "description": "Routing, headers, and static asset helpers for Void runtimes.",
5
+ "homepage": "https://void.cloud",
6
+ "bugs": {
7
+ "url": "https://github.com/voidzero-dev/void/issues"
8
+ },
9
+ "license": "MIT",
4
10
  "repository": {
5
11
  "type": "git",
6
12
  "url": "git+https://github.com/voidzero-dev/void.git",
@@ -20,12 +26,12 @@
20
26
  "access": "public"
21
27
  },
22
28
  "dependencies": {
23
- "@void/deploy-core": "0.25.0"
29
+ "@void/deploy-core": "0.26.1"
24
30
  },
25
31
  "devDependencies": {
26
- "@cloudflare/workers-types": "^4.20260702.1",
32
+ "@cloudflare/workers-types": "^5.20261009.1",
27
33
  "typescript": "7.0.2",
28
- "vite-plus": "1.0.0"
34
+ "vite-plus": "1.1.0"
29
35
  },
30
36
  "scripts": {
31
37
  "build": "vp pack",