@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 +8 -2
- package/dist/index.d.mts +7 -14
- package/dist/index.mjs +39 -50
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @void/edge
|
|
2
2
|
|
|
3
|
-
|
|
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`.
|
|
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).
|
|
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
|
-
*
|
|
85
|
-
* `
|
|
86
|
-
*
|
|
87
|
-
*
|
|
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
|
|
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.
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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).
|
|
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
|
-
*
|
|
112
|
-
* `
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
405
|
+
const metadata = env[config.versionMetadataBinding];
|
|
416
406
|
return Response.json({
|
|
417
407
|
ready: true,
|
|
418
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
29
|
+
"@void/deploy-core": "0.26.1"
|
|
24
30
|
},
|
|
25
31
|
"devDependencies": {
|
|
26
|
-
"@cloudflare/workers-types": "^
|
|
32
|
+
"@cloudflare/workers-types": "^5.20261009.1",
|
|
27
33
|
"typescript": "7.0.2",
|
|
28
|
-
"vite-plus": "1.
|
|
34
|
+
"vite-plus": "1.1.0"
|
|
29
35
|
},
|
|
30
36
|
"scripts": {
|
|
31
37
|
"build": "vp pack",
|