@depup/h3 2.0.1-depup.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/LICENSE +21 -0
- package/README.md +25 -0
- package/bin/h3.mjs +36 -0
- package/changes.json +5 -0
- package/dist/THIRD-PARTY-LICENSES.md +70 -0
- package/dist/_entries/bun.d.mts +6 -0
- package/dist/_entries/bun.mjs +16 -0
- package/dist/_entries/cloudflare.d.mts +6 -0
- package/dist/_entries/cloudflare.mjs +16 -0
- package/dist/_entries/deno.d.mts +6 -0
- package/dist/_entries/deno.mjs +16 -0
- package/dist/_entries/generic.d.mts +6 -0
- package/dist/_entries/generic.mjs +16 -0
- package/dist/_entries/node.d.mts +10 -0
- package/dist/_entries/node.mjs +19 -0
- package/dist/_entries/service-worker.d.mts +6 -0
- package/dist/_entries/service-worker.mjs +16 -0
- package/dist/_utils.mjs +240 -0
- package/dist/cache.mjs +599 -0
- package/dist/cache2.mjs +50 -0
- package/dist/cors.mjs +292 -0
- package/dist/docs/0.guide/0.index/index.md +117 -0
- package/dist/docs/0.guide/1.basics/0.lifecycle.md +68 -0
- package/dist/docs/0.guide/1.basics/1.routing.md +167 -0
- package/dist/docs/0.guide/1.basics/2.middleware.md +97 -0
- package/dist/docs/0.guide/1.basics/3.handler.md +165 -0
- package/dist/docs/0.guide/1.basics/4.response.md +171 -0
- package/dist/docs/0.guide/1.basics/5.error.md +117 -0
- package/dist/docs/0.guide/1.basics/6.nested-apps.md +57 -0
- package/dist/docs/0.guide/2.rules.md +698 -0
- package/dist/docs/0.guide/3.api/0.h3.md +144 -0
- package/dist/docs/0.guide/3.api/1.h3event.md +160 -0
- package/dist/docs/0.guide/4.advanced/0.plugins.md +50 -0
- package/dist/docs/0.guide/4.advanced/1.websocket.md +176 -0
- package/dist/docs/0.guide/4.advanced/2.nightly.md +13 -0
- package/dist/docs/1.utils/0.index/index.md +46 -0
- package/dist/docs/1.utils/1.request.md +447 -0
- package/dist/docs/1.utils/2.response.md +172 -0
- package/dist/docs/1.utils/3.cookie.md +33 -0
- package/dist/docs/1.utils/4.security.md +175 -0
- package/dist/docs/1.utils/5.proxy.md +57 -0
- package/dist/docs/1.utils/6.mcp.md +75 -0
- package/dist/docs/1.utils/7.more.md +117 -0
- package/dist/docs/1.utils/8.community.md +48 -0
- package/dist/docs/2.examples/0.index/index.md +17 -0
- package/dist/docs/2.examples/1.handle-cookie.md +67 -0
- package/dist/docs/2.examples/2.handle-query.md +76 -0
- package/dist/docs/2.examples/3.handle-session.md +210 -0
- package/dist/docs/2.examples/4.serve-static-assets.md +66 -0
- package/dist/docs/2.examples/5.stream-response.md +76 -0
- package/dist/docs/2.examples/6.validate-data.md +193 -0
- package/dist/docs/3.migration/0.index/index.md +204 -0
- package/dist/docs/README.md +37 -0
- package/dist/h3.d.mts +1669 -0
- package/dist/h3.mjs +1809 -0
- package/dist/index.d.mts +1634 -0
- package/dist/match.d.mts +123 -0
- package/dist/middleware.mjs +123 -0
- package/dist/normalize.mjs +645 -0
- package/dist/path.mjs +42 -0
- package/dist/proxy.mjs +254 -0
- package/dist/response.mjs +465 -0
- package/dist/rules/cache.d.mts +29 -0
- package/dist/rules/cache.mjs +163 -0
- package/dist/rules/compiler.d.mts +94 -0
- package/dist/rules/compiler.mjs +173 -0
- package/dist/rules/index.d.mts +77 -0
- package/dist/rules/index.mjs +34 -0
- package/dist/rules/proxy.d.mts +3 -0
- package/dist/rules/proxy.mjs +14 -0
- package/dist/tracing.d.mts +33 -0
- package/dist/tracing.mjs +89 -0
- package/package.json +148 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import { getURLPathname, isNonCanonicalPathname, normalizeRoute, withLeadingSlash } from "./response.mjs";
|
|
2
|
+
import { handleCors, redirect as redirect$1 } from "./cors.mjs";
|
|
3
|
+
import { canonicalPath, decodedPath, encodedReading, isHostPositionSplat, isPathInServedScope, mergedCanonicalPath, needsCanonicalPasses, prepareRuleTarget } from "./_utils.mjs";
|
|
4
|
+
import { addRoute, compareRoutes, createRouter, findAllRoutes } from "rou3";
|
|
5
|
+
const HTTP_METHODS = /* @__PURE__ */ new Set([
|
|
6
|
+
"GET",
|
|
7
|
+
"HEAD",
|
|
8
|
+
"POST",
|
|
9
|
+
"PUT",
|
|
10
|
+
"PATCH",
|
|
11
|
+
"DELETE",
|
|
12
|
+
"OPTIONS",
|
|
13
|
+
"CONNECT",
|
|
14
|
+
"TRACE",
|
|
15
|
+
"QUERY"
|
|
16
|
+
]);
|
|
17
|
+
const METHOD_KEY_RE = /^([A-Za-z]+)\s+(\/.*)$/;
|
|
18
|
+
function parseRouteKey(key) {
|
|
19
|
+
const match = METHOD_KEY_RE.exec(key);
|
|
20
|
+
if (match) {
|
|
21
|
+
const method = match[1].toUpperCase();
|
|
22
|
+
if (HTTP_METHODS.has(method)) return {
|
|
23
|
+
method,
|
|
24
|
+
path: withLeadingSlash(match[2])
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
method: "",
|
|
29
|
+
path: withLeadingSlash(key)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function unknownMethodPrefix(key) {
|
|
33
|
+
const match = METHOD_KEY_RE.exec(key);
|
|
34
|
+
return match && !HTTP_METHODS.has(match[1].toUpperCase()) ? match[1] : void 0;
|
|
35
|
+
}
|
|
36
|
+
function formatRouteKey(method, path) {
|
|
37
|
+
return method ? `${method} ${path}` : path;
|
|
38
|
+
}
|
|
39
|
+
function decodeRoutePattern(path) {
|
|
40
|
+
for (let prev = ""; prev !== path;) {
|
|
41
|
+
prev = path;
|
|
42
|
+
path = normalizeRoute(path);
|
|
43
|
+
}
|
|
44
|
+
return path;
|
|
45
|
+
}
|
|
46
|
+
function recordResets(resets, resetEntries, routeRules, canOverride) {
|
|
47
|
+
for (const entry of resetEntries) {
|
|
48
|
+
const rule = routeRules[entry.name];
|
|
49
|
+
if (!rule || !canOverride?.(entry.route, rule.route)) addReset(resets, entry.name, entry.route);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function addReset(resets, name, route) {
|
|
53
|
+
const routes = resets.get(name);
|
|
54
|
+
if (!routes) resets.set(name, [route]);
|
|
55
|
+
else if (!routes.includes(route)) routes.push(route);
|
|
56
|
+
}
|
|
57
|
+
function canReinstate(resetRoutes, route, canOverride) {
|
|
58
|
+
return !resetRoutes || canOverride !== void 0 && resetRoutes.every((resetRoute) => canOverride(resetRoute, route));
|
|
59
|
+
}
|
|
60
|
+
function mergeMatchedRouteRules(rawLayers, altLayers, canOverride) {
|
|
61
|
+
if (!altLayers) return resolveLayers(rawLayers);
|
|
62
|
+
const resets = /* @__PURE__ */ new Map();
|
|
63
|
+
const routeRules = resolveLayers(rawLayers, resets, canOverride);
|
|
64
|
+
const resolved = altLayers.map((layers) => layers?.length ? resolveLayers(layers, resets, canOverride) : void 0);
|
|
65
|
+
for (const rules of resolved) if (rules) unionRules(routeRules, rules, resets, canOverride);
|
|
66
|
+
return routeRules;
|
|
67
|
+
}
|
|
68
|
+
function unionRules(routeRules, resolved, resets, canOverride) {
|
|
69
|
+
for (const [name, rule] of Object.entries(resolved)) {
|
|
70
|
+
const current = routeRules[name];
|
|
71
|
+
if (current) {
|
|
72
|
+
if (canOverride && !canOverride(current.route, rule.route)) continue;
|
|
73
|
+
} else if (!rule.handler?.restricting && !canReinstate(resets.get(name), rule.route, canOverride)) continue;
|
|
74
|
+
mergeRouteRule(routeRules, name, rule, rule.params);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function resolveLayers(layers, resets, canOverride) {
|
|
78
|
+
const firstData = layers?.[0]?.data;
|
|
79
|
+
if (firstData && !Array.isArray(firstData)) return resolvePreMergedLayers(layers, resets);
|
|
80
|
+
const routeRules = emptyRouteRules();
|
|
81
|
+
let resetEntries;
|
|
82
|
+
for (const layer of orderedLayers(layers)) for (const entry of layer.data) {
|
|
83
|
+
if (resets && entry.options === false) (resetEntries ||= []).push(entry);
|
|
84
|
+
mergeRouteRule(routeRules, entry.name, entry, layer.params);
|
|
85
|
+
}
|
|
86
|
+
if (resetEntries) recordResets(resets, resetEntries, routeRules, canOverride);
|
|
87
|
+
return routeRules;
|
|
88
|
+
}
|
|
89
|
+
function isMergeableObject(value) {
|
|
90
|
+
return value !== null && typeof value === "object";
|
|
91
|
+
}
|
|
92
|
+
function emptyRouteRules() {
|
|
93
|
+
return Object.create(null);
|
|
94
|
+
}
|
|
95
|
+
function mergeRuleOptions(current, incoming) {
|
|
96
|
+
return isMergeableObject(current) && isMergeableObject(incoming) ? {
|
|
97
|
+
...current,
|
|
98
|
+
...incoming
|
|
99
|
+
} : incoming;
|
|
100
|
+
}
|
|
101
|
+
function orderedLayers(layers) {
|
|
102
|
+
if (!layers || layers.length < 2) return layers || [];
|
|
103
|
+
let ordered = layers;
|
|
104
|
+
for (let i = 1; i < ordered.length; i++) {
|
|
105
|
+
const layer = ordered[i];
|
|
106
|
+
const rank = layerRank(layer);
|
|
107
|
+
let j = i - 1;
|
|
108
|
+
while (j >= 0 && layerRank(ordered[j]) > rank) {
|
|
109
|
+
if (ordered === layers) ordered = [...layers];
|
|
110
|
+
ordered[j + 1] = ordered[j];
|
|
111
|
+
j--;
|
|
112
|
+
}
|
|
113
|
+
if (j + 1 !== i) ordered[j + 1] = layer;
|
|
114
|
+
}
|
|
115
|
+
return ordered;
|
|
116
|
+
}
|
|
117
|
+
function layerRank(layer) {
|
|
118
|
+
return layer.data[0]?.rank ?? 0;
|
|
119
|
+
}
|
|
120
|
+
function resolvePreMergedLayers(rawLayers, resets) {
|
|
121
|
+
const layers = rawLayers.length < 2 ? rawLayers : [...rawLayers].sort((a, b) => a.data.rank - b.data.rank);
|
|
122
|
+
const routeRules = emptyRouteRules();
|
|
123
|
+
const winning = layers[layers.length - 1].data;
|
|
124
|
+
if (resets && winning.resets) for (const name of winning.resets) addReset(resets, name, winning.route);
|
|
125
|
+
for (const entry of winning.rules) {
|
|
126
|
+
const paramRoutes = entry.paramRoutes;
|
|
127
|
+
let params;
|
|
128
|
+
for (const layer of layers) {
|
|
129
|
+
const layerParams = layer.params;
|
|
130
|
+
if (!layerParams) continue;
|
|
131
|
+
const layerRoute = layer.data.route;
|
|
132
|
+
if (paramRoutes ? paramRoutes.includes(layerRoute) : layerRoute === entry.route) params = params ? {
|
|
133
|
+
...params,
|
|
134
|
+
...layerParams
|
|
135
|
+
} : layerParams;
|
|
136
|
+
}
|
|
137
|
+
routeRules[entry.name] = {
|
|
138
|
+
route: entry.route,
|
|
139
|
+
options: entry.options,
|
|
140
|
+
handler: entry.handler,
|
|
141
|
+
params
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return routeRules;
|
|
145
|
+
}
|
|
146
|
+
function mergeRouteRule(routeRules, ruleName, rule, params) {
|
|
147
|
+
const name = ruleName;
|
|
148
|
+
const currentRule = routeRules[name];
|
|
149
|
+
if (currentRule) {
|
|
150
|
+
if (rule.options === false) {
|
|
151
|
+
delete routeRules[name];
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
currentRule.options = mergeRuleOptions(currentRule.options, rule.options);
|
|
155
|
+
currentRule.route = rule.route;
|
|
156
|
+
if (currentRule.params || params) currentRule.params = {
|
|
157
|
+
...currentRule.params,
|
|
158
|
+
...params
|
|
159
|
+
};
|
|
160
|
+
} else if (rule.options !== false) routeRules[name] = {
|
|
161
|
+
route: rule.route,
|
|
162
|
+
options: rule.options,
|
|
163
|
+
handler: rule.handler,
|
|
164
|
+
params
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function routeContainmentRanks(paths) {
|
|
168
|
+
const ranks = new Map(paths.map((path) => [path, 0]));
|
|
169
|
+
for (let i = 0; i < paths.length; i++) for (let j = i + 1; j < paths.length; j++) {
|
|
170
|
+
const [a, b] = [paths[i], paths[j]];
|
|
171
|
+
let rel;
|
|
172
|
+
try {
|
|
173
|
+
rel = compareRoutes(a, b);
|
|
174
|
+
} catch {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (rel === "superset") ranks.set(b, ranks.get(b) + 1);
|
|
178
|
+
else if (rel === "subset") ranks.set(a, ranks.get(a) + 1);
|
|
179
|
+
}
|
|
180
|
+
return ranks;
|
|
181
|
+
}
|
|
182
|
+
function preMergeRuleLayers(byPath) {
|
|
183
|
+
const paths = [...byPath.keys()];
|
|
184
|
+
const subsumers = new Map(paths.map((path) => [path, []]));
|
|
185
|
+
for (let i = 0; i < paths.length; i++) for (let j = i + 1; j < paths.length; j++) {
|
|
186
|
+
const [a, b] = [paths[i], paths[j]];
|
|
187
|
+
switch (compareRoutes(a, b)) {
|
|
188
|
+
case "disjoint": break;
|
|
189
|
+
case "equal": throw new Error(`[h3] rules: preMerge: \`${a}\` and \`${b}\` match the same paths — merge them into one rule`);
|
|
190
|
+
case "superset":
|
|
191
|
+
subsumers.get(b).push(a);
|
|
192
|
+
break;
|
|
193
|
+
case "subset":
|
|
194
|
+
subsumers.get(a).push(b);
|
|
195
|
+
break;
|
|
196
|
+
case "partial": throw new Error(`[h3] rules: preMerge: \`${a}\` and \`${b}\` partially overlap — the most specific match is ambiguous. Split the overlap into explicit rules or disable preMerge.`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const methodsUsed = /* @__PURE__ */ new Set();
|
|
200
|
+
for (const methods of byPath.values()) for (const method of methods.keys()) if (method) methodsUsed.add(method);
|
|
201
|
+
const result = /* @__PURE__ */ new Map();
|
|
202
|
+
for (const path of paths) {
|
|
203
|
+
const chainSubsumers = subsumers.get(path);
|
|
204
|
+
const chain = [...chainSubsumers].sort((a, b) => subsumers.get(a).length - subsumers.get(b).length).concat(path);
|
|
205
|
+
const registrations = /* @__PURE__ */ new Map();
|
|
206
|
+
for (const method of ["", ...methodsUsed]) {
|
|
207
|
+
if (method && !chain.some((route) => byPath.get(route).has(method))) continue;
|
|
208
|
+
const merged = /* @__PURE__ */ new Map();
|
|
209
|
+
const resets = /* @__PURE__ */ new Set();
|
|
210
|
+
for (const route of chain) {
|
|
211
|
+
const methods = byPath.get(route);
|
|
212
|
+
const agnostic = methods.get("") || [];
|
|
213
|
+
const scoped = method && methods.get(method) || [];
|
|
214
|
+
for (const entry of [...agnostic, ...scoped]) mergeChainRule(merged, entry, resets);
|
|
215
|
+
}
|
|
216
|
+
registrations.set(method, {
|
|
217
|
+
route: path,
|
|
218
|
+
rank: chainSubsumers.length,
|
|
219
|
+
rules: [...merged.values()].map(({ paramRoutes, ...rule }) => paramRoutes.length === 1 && paramRoutes[0] === rule.route ? rule : {
|
|
220
|
+
...rule,
|
|
221
|
+
paramRoutes
|
|
222
|
+
}),
|
|
223
|
+
...resets.size > 0 && { resets: [...resets].filter((name) => !merged.has(name)) }
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
result.set(path, registrations);
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
function mergeChainRule(merged, entry, resets) {
|
|
231
|
+
if (entry.options === false) resets.add(entry.name);
|
|
232
|
+
const current = merged.get(entry.name);
|
|
233
|
+
if (current) {
|
|
234
|
+
if (entry.options === false) {
|
|
235
|
+
merged.delete(entry.name);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
current.options = mergeRuleOptions(current.options, entry.options);
|
|
239
|
+
current.route = entry.route;
|
|
240
|
+
if (!current.paramRoutes.includes(entry.route)) current.paramRoutes.push(entry.route);
|
|
241
|
+
} else if (entry.options !== false) merged.set(entry.name, {
|
|
242
|
+
...entry,
|
|
243
|
+
paramRoutes: [entry.route]
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
let warnedCredentialsWildcard = false;
|
|
247
|
+
function safeCorsOptions(options) {
|
|
248
|
+
const { origin, credentials } = options;
|
|
249
|
+
if (credentials === true && (!origin || origin === "*")) {
|
|
250
|
+
if (!warnedCredentialsWildcard) {
|
|
251
|
+
warnedCredentialsWildcard = true;
|
|
252
|
+
console.warn("[h3] rules: `cors` rule resolved to `credentials: true` with a wildcard origin after merge — dropping `credentials` (an `Access-Control-Allow-Origin: *` + credentials response is rejected by browsers). Set an explicit `origin` allowlist on the more specific rule.");
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
...options,
|
|
256
|
+
credentials: false
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return options;
|
|
260
|
+
}
|
|
261
|
+
const cors = {
|
|
262
|
+
order: -3,
|
|
263
|
+
handler: (m) => function corsRouteRule(event, next) {
|
|
264
|
+
const preflight = handleCors(event, safeCorsOptions(m.options || {}));
|
|
265
|
+
return preflight === false ? next() : preflight;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
const headers = {
|
|
269
|
+
order: -1,
|
|
270
|
+
handler: (m) => {
|
|
271
|
+
const entries = Object.entries(m.options || {});
|
|
272
|
+
return async function headersRouteRule(event, next) {
|
|
273
|
+
try {
|
|
274
|
+
return await next();
|
|
275
|
+
} finally {
|
|
276
|
+
for (const [key, value] of entries) {
|
|
277
|
+
event.res.headers.set(key, value);
|
|
278
|
+
event.res.errHeaders.set(key, value);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
function prepareAtTargetCheck(options) {
|
|
285
|
+
const target = options?.to;
|
|
286
|
+
if (!target?.startsWith("/") || target[1] === "/" || target[1] === "\\") return;
|
|
287
|
+
const isAtPath = prepareTargetPathCheck(getURLPathname(target), options.base, target.endsWith("/**"));
|
|
288
|
+
return isAtPath && ((event) => isAtPath(event.url.pathname));
|
|
289
|
+
}
|
|
290
|
+
function prepareTargetPathCheck(path, base, appendsTail) {
|
|
291
|
+
if (path.endsWith("/**") && (appendsTail || base !== void 0)) {
|
|
292
|
+
const prefix = servedPath(path.slice(0, -3));
|
|
293
|
+
if (!prefix) return;
|
|
294
|
+
return (pathname) => (pathname === prefix || pathname.startsWith(prefix + "/")) && isPathInServedScope(pathname, prefix);
|
|
295
|
+
}
|
|
296
|
+
const first = base === void 0 ? -1 : path.indexOf("**");
|
|
297
|
+
if (first === -1) {
|
|
298
|
+
const exact = servedPath(path);
|
|
299
|
+
return (pathname) => pathname === exact;
|
|
300
|
+
}
|
|
301
|
+
const rawPrefix = path.slice(0, first);
|
|
302
|
+
if (!rawPrefix.endsWith("/")) return;
|
|
303
|
+
const prefix = servedPath(rawPrefix);
|
|
304
|
+
const suffix = servedPath("/" + path.slice(path.lastIndexOf("**") + 2)).slice(1);
|
|
305
|
+
if (prefix === "/" && !suffix) return;
|
|
306
|
+
const scope = prefix.slice(0, -1);
|
|
307
|
+
const minLength = prefix.length + suffix.length;
|
|
308
|
+
return (pathname) => pathname.length >= minLength && pathname.startsWith(prefix) && pathname.endsWith(suffix) && isPathInServedScope(pathname, scope);
|
|
309
|
+
}
|
|
310
|
+
function servedPath(path) {
|
|
311
|
+
return path && new URL("http://h" + path).pathname;
|
|
312
|
+
}
|
|
313
|
+
const redirect = {
|
|
314
|
+
order: 1,
|
|
315
|
+
handler: (m) => {
|
|
316
|
+
const options = m.options;
|
|
317
|
+
const resolveTarget = prepareRuleTarget(options);
|
|
318
|
+
if (!resolveTarget) return function redirectRouteRule() {};
|
|
319
|
+
const isAtTarget = prepareAtTargetCheck(options);
|
|
320
|
+
return function redirectRouteRule(event) {
|
|
321
|
+
const location = resolveTarget(event);
|
|
322
|
+
if (isAtTarget?.(event)) return;
|
|
323
|
+
return redirect$1(location, options?.status);
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
const ruleHandlers = {
|
|
328
|
+
headers,
|
|
329
|
+
redirect,
|
|
330
|
+
cors
|
|
331
|
+
};
|
|
332
|
+
function createRulesRouter(rules, handlers, baseURL, preMerge) {
|
|
333
|
+
let base = baseURL || "";
|
|
334
|
+
if (base.endsWith("/")) base = base.slice(0, -1);
|
|
335
|
+
const registrations = [];
|
|
336
|
+
for (const [key, rule] of Object.entries(rules)) {
|
|
337
|
+
const { method, path } = parseRouteKey(key);
|
|
338
|
+
const entries = [];
|
|
339
|
+
for (const [name, options] of Object.entries(rule)) {
|
|
340
|
+
if (options === void 0) continue;
|
|
341
|
+
entries.push({
|
|
342
|
+
name,
|
|
343
|
+
route: path,
|
|
344
|
+
options: base ? withScopeBase(name, options, base) : options,
|
|
345
|
+
handler: Object.hasOwn(handlers, name) ? handlers[name] : void 0
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
registrations.push([
|
|
349
|
+
method,
|
|
350
|
+
registeredPattern(base + path),
|
|
351
|
+
entries
|
|
352
|
+
]);
|
|
353
|
+
}
|
|
354
|
+
const baseSpelling = registrations[0]?.[1].slice(0, base.length) ?? base;
|
|
355
|
+
const prefix = registrations.every(([, full]) => full.startsWith(baseSpelling)) ? baseSpelling : "";
|
|
356
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
357
|
+
for (const [method, full, entries] of registrations) {
|
|
358
|
+
const pattern = full.slice(prefix.length);
|
|
359
|
+
for (const entry of entries) entry.route = full.slice(base.length);
|
|
360
|
+
let methods = byPath.get(pattern);
|
|
361
|
+
if (!methods) byPath.set(pattern, methods = /* @__PURE__ */ new Map());
|
|
362
|
+
methods.set(method, [...methods.get(method) || [], ...entries]);
|
|
363
|
+
}
|
|
364
|
+
for (const methods of byPath.values()) {
|
|
365
|
+
const get = methods.get("GET");
|
|
366
|
+
if (get) methods.set("HEAD", [...get, ...methods.get("HEAD") || []]);
|
|
367
|
+
}
|
|
368
|
+
const router = createRouter();
|
|
369
|
+
if (preMerge) {
|
|
370
|
+
for (const [pattern, methods] of preMergeRuleLayers(byPath)) for (const [method, data] of methods) {
|
|
371
|
+
data.route = (prefix + pattern).slice(base.length);
|
|
372
|
+
addRuleRoute(router, method, prefix + pattern, data);
|
|
373
|
+
}
|
|
374
|
+
return router;
|
|
375
|
+
}
|
|
376
|
+
for (const [path, rank] of routeContainmentRanks([...byPath.keys()])) {
|
|
377
|
+
if (rank === 0) continue;
|
|
378
|
+
for (const entries of byPath.get(path).values()) for (const entry of entries) entry.rank = rank;
|
|
379
|
+
}
|
|
380
|
+
for (const [pattern, methods] of byPath) for (const [method, entries] of methods) addRuleRoute(router, method, prefix + pattern, entries);
|
|
381
|
+
return router;
|
|
382
|
+
}
|
|
383
|
+
function createRouteRulesMatcher(rules, opts) {
|
|
384
|
+
const handlers = {
|
|
385
|
+
...ruleHandlers,
|
|
386
|
+
...opts?.handlers
|
|
387
|
+
};
|
|
388
|
+
requireOptInHandler(rules, handlers, "cache", "cache`/`swr", "Install `ocache` and pass `handlers: { cache }` from \"h3/rules/cache\", provide your own via `createCacheRuleHandler`, or pass `handlers: { cache: undefined }` to keep the rule data-only.");
|
|
389
|
+
requireOptInHandler(rules, handlers, "proxy", "proxy", "Pass `handlers: { proxy }` from \"h3/rules/proxy\", or `handlers: { proxy: undefined }` to keep the rule data-only.");
|
|
390
|
+
const router = createRulesRouter(rules, handlers, opts?.baseURL, opts?.preMerge);
|
|
391
|
+
const findRouteRules = (method, pathname) => findAllRoutes(router, method, pathname);
|
|
392
|
+
return createMatcherFromFind(findRouteRules, canOverrideRoute);
|
|
393
|
+
}
|
|
394
|
+
const canOverrideRoute = (currentRoute, incomingRoute) => {
|
|
395
|
+
if (currentRoute === incomingRoute) return true;
|
|
396
|
+
const rel = compareRoutes(currentRoute, incomingRoute);
|
|
397
|
+
return rel === "superset" || rel === "equal";
|
|
398
|
+
};
|
|
399
|
+
const WHOLE_PARAM_SEGMENT_RE = /^:[A-Za-z_]\w*[?+*]?$/;
|
|
400
|
+
const CONCRETE_SEGMENT_RE = /^[^:*(){}\\]+$/;
|
|
401
|
+
const DOT_SEGMENT_RE = /(?:^|\/)(?:\.|%2e){1,2}(?:\/|$)/i;
|
|
402
|
+
const GROUP_RE = /[{}]/;
|
|
403
|
+
const canOverrideRouteShape = (currentRoute, incomingRoute) => {
|
|
404
|
+
if (DOT_SEGMENT_RE.test(currentRoute) || DOT_SEGMENT_RE.test(incomingRoute)) return false;
|
|
405
|
+
if (currentRoute === incomingRoute) return true;
|
|
406
|
+
if (GROUP_RE.test(currentRoute) || GROUP_RE.test(incomingRoute)) return false;
|
|
407
|
+
const current = currentRoute.split("/");
|
|
408
|
+
const incoming = incomingRoute.split("/");
|
|
409
|
+
for (let i = 0; i < current.length; i++) {
|
|
410
|
+
const cur = current[i];
|
|
411
|
+
if (cur === "**") return i === current.length - 1 && incoming.length > i && !current.slice(1, i).includes("");
|
|
412
|
+
const inc = incoming[i];
|
|
413
|
+
if (inc === void 0) return false;
|
|
414
|
+
if (cur === inc) continue;
|
|
415
|
+
if ((cur === "*" || WHOLE_PARAM_SEGMENT_RE.test(cur)) && CONCRETE_SEGMENT_RE.test(inc)) continue;
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
return current.length === incoming.length;
|
|
419
|
+
};
|
|
420
|
+
function createMatcherFromFind(findRouteRules, canOverride = canOverrideRouteShape) {
|
|
421
|
+
return (method, pathname) => {
|
|
422
|
+
const rawLayers = findRouteRules(method, pathname);
|
|
423
|
+
let altLayers;
|
|
424
|
+
let hasAltMatch = false;
|
|
425
|
+
const readings = alternateReadings(pathname);
|
|
426
|
+
if (readings) {
|
|
427
|
+
altLayers = [];
|
|
428
|
+
for (const reading of readings) {
|
|
429
|
+
const layers = findRouteRules(method, reading);
|
|
430
|
+
if (layers?.length) hasAltMatch = true;
|
|
431
|
+
altLayers.push(layers);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (!rawLayers?.length && !hasAltMatch) return {
|
|
435
|
+
routeRules: {},
|
|
436
|
+
matchedRules: {},
|
|
437
|
+
routeRuleMiddleware: []
|
|
438
|
+
};
|
|
439
|
+
const matchedRules = mergeMatchedRouteRules(rawLayers, altLayers, canOverride);
|
|
440
|
+
return {
|
|
441
|
+
routeRules: toRouteRules(matchedRules),
|
|
442
|
+
matchedRules,
|
|
443
|
+
routeRuleMiddleware: buildRouteRuleMiddleware(matchedRules)
|
|
444
|
+
};
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function toRouteRules(matchedRules) {
|
|
448
|
+
const routeRules = Object.create(null);
|
|
449
|
+
for (const name in matchedRules) routeRules[name] = matchedRules[name].options;
|
|
450
|
+
return routeRules;
|
|
451
|
+
}
|
|
452
|
+
function buildRouteRuleMiddleware(matchedRules) {
|
|
453
|
+
const routeRuleMiddleware = [];
|
|
454
|
+
const rules = Object.entries(matchedRules);
|
|
455
|
+
if (rules.length > 1) rules.sort(compareRuleOrder);
|
|
456
|
+
for (const [, rule] of rules) {
|
|
457
|
+
if (!rule.handler) continue;
|
|
458
|
+
routeRuleMiddleware.push(rule.handler.handler(rule));
|
|
459
|
+
}
|
|
460
|
+
return routeRuleMiddleware;
|
|
461
|
+
}
|
|
462
|
+
function memoizeRouteRulesMatcher(matcher, opts) {
|
|
463
|
+
const max = opts?.max ?? 1024;
|
|
464
|
+
if (max <= 0) return matcher;
|
|
465
|
+
const memo = /* @__PURE__ */ new Map();
|
|
466
|
+
let hand;
|
|
467
|
+
const evict = () => {
|
|
468
|
+
for (;;) {
|
|
469
|
+
let next = hand?.next();
|
|
470
|
+
if (!next || next.done) {
|
|
471
|
+
hand = memo.values();
|
|
472
|
+
next = hand.next();
|
|
473
|
+
if (next.done) return;
|
|
474
|
+
}
|
|
475
|
+
const entry = next.value;
|
|
476
|
+
if (entry.visited) entry.visited = false;
|
|
477
|
+
else {
|
|
478
|
+
memo.delete(entry.key);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
return (method, pathname) => {
|
|
484
|
+
const key = method + " " + pathname;
|
|
485
|
+
const entry = memo.get(key);
|
|
486
|
+
if (entry) {
|
|
487
|
+
entry.visited = true;
|
|
488
|
+
return entry.result;
|
|
489
|
+
}
|
|
490
|
+
const result = matcher(method, pathname);
|
|
491
|
+
if (memo.size >= max) evict();
|
|
492
|
+
memo.set(key, {
|
|
493
|
+
key,
|
|
494
|
+
result,
|
|
495
|
+
visited: false
|
|
496
|
+
});
|
|
497
|
+
return result;
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function alternateReadings(pathname) {
|
|
501
|
+
const recase = pathname.includes("%") && !isNonCanonicalPathname(pathname);
|
|
502
|
+
const upper = recase ? recaseEscapes(pathname, true) : pathname;
|
|
503
|
+
const lower = recase ? recaseEscapes(pathname, false) : pathname;
|
|
504
|
+
const decoded = decodedPath(pathname);
|
|
505
|
+
const encoded = encodedReading(decoded);
|
|
506
|
+
const lowerEncoded = recaseEscapes(encoded, false);
|
|
507
|
+
if (upper === pathname && lower === pathname && decoded === pathname && encoded === pathname && lowerEncoded === pathname && !needsCanonicalPasses(pathname)) return;
|
|
508
|
+
const readings = [];
|
|
509
|
+
pushReading(readings, pathname, upper);
|
|
510
|
+
pushReading(readings, pathname, lower);
|
|
511
|
+
const spellings = [pathname];
|
|
512
|
+
for (const spelling of [
|
|
513
|
+
encoded,
|
|
514
|
+
lowerEncoded,
|
|
515
|
+
decoded
|
|
516
|
+
]) if (!spellings.includes(spelling)) spellings.push(spelling);
|
|
517
|
+
for (const spelling of spellings) {
|
|
518
|
+
if (!needsCanonicalPasses(spelling)) {
|
|
519
|
+
pushReading(readings, pathname, spelling);
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const canonical = canonicalPath(spelling);
|
|
523
|
+
pushReading(readings, pathname, canonical);
|
|
524
|
+
const merged = mergedCanonicalPath(spelling, canonical);
|
|
525
|
+
if (merged !== void 0) pushReading(readings, pathname, merged);
|
|
526
|
+
}
|
|
527
|
+
return readings.length > 0 ? readings : void 0;
|
|
528
|
+
}
|
|
529
|
+
const ESCAPE_RE_G = /%[\da-f]{2}/gi;
|
|
530
|
+
const ROU3_ENCODED_RE = /[\0- "#<>^`\x7F-\uFFFF]|\\[{}?]/;
|
|
531
|
+
const LOWER_HEX_ESCAPE_RE = /%(?:[a-f][\da-fA-F]|[\dA-F][a-f])/;
|
|
532
|
+
const UPPER_HEX_ESCAPE_RE = /%(?:[A-F][\da-fA-F]|[\da-f][A-F])/;
|
|
533
|
+
function registeredPattern(pattern) {
|
|
534
|
+
return LOWER_HEX_ESCAPE_RE.test(pattern) && (UPPER_HEX_ESCAPE_RE.test(pattern) || ROU3_ENCODED_RE.test(pattern)) && !hasConstraint(pattern) ? recaseEscapes(pattern, true) : pattern;
|
|
535
|
+
}
|
|
536
|
+
function hasConstraint(pattern) {
|
|
537
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
538
|
+
const c = pattern.charCodeAt(i);
|
|
539
|
+
if (c === 92) i++;
|
|
540
|
+
else if (c === 40) return true;
|
|
541
|
+
}
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
function recaseEscapes(path, upper) {
|
|
545
|
+
return path.includes("%") ? path.replace(ESCAPE_RE_G, (escape) => upper ? escape.toUpperCase() : escape.toLowerCase()) : path;
|
|
546
|
+
}
|
|
547
|
+
function pushReading(readings, pathname, reading) {
|
|
548
|
+
if (reading !== pathname && !readings.includes(reading)) readings.push(reading);
|
|
549
|
+
}
|
|
550
|
+
function requireOptInHandler(rules, handlers, name, label, hint) {
|
|
551
|
+
if (name in handlers) return;
|
|
552
|
+
for (const key in rules) if (rules[key][name]) throw new Error(`[h3] rules: rules use \`${label}\` (\`${key}\`) but no \`${name}\` handler is registered. ${hint}`);
|
|
553
|
+
}
|
|
554
|
+
const compareRuleOrder = (a, b) => orderWeight(a[1].handler) - orderWeight(b[1].handler) || (a[0] < b[0] ? -1 : 1);
|
|
555
|
+
function orderWeight(handler) {
|
|
556
|
+
return handler?.order ?? 0;
|
|
557
|
+
}
|
|
558
|
+
function withScopeBase(name, options, baseURL) {
|
|
559
|
+
if ((name === "redirect" || name === "proxy") && options !== null && typeof options === "object" && typeof options.base === "string") return {
|
|
560
|
+
...options,
|
|
561
|
+
base: baseURL + options.base
|
|
562
|
+
};
|
|
563
|
+
return options;
|
|
564
|
+
}
|
|
565
|
+
function addRuleRoute(router, method, path, data) {
|
|
566
|
+
try {
|
|
567
|
+
addRoute(router, method, path, data);
|
|
568
|
+
} catch (error) {
|
|
569
|
+
throw new Error(`[h3] rules: invalid route pattern \`${path}\`: ${error.message}`, { cause: error });
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function normalizeRouteRules(config) {
|
|
573
|
+
const normalizedRules = {};
|
|
574
|
+
for (const key in config) {
|
|
575
|
+
const routeConfig = config[key];
|
|
576
|
+
const unknownMethod = unknownMethodPrefix(key);
|
|
577
|
+
if (unknownMethod !== void 0) throw new Error(`[h3] rules: \`${key}\` looks method-scoped but \`${unknownMethod}\` is not a recognized HTTP method — as a literal path this rule can never match. Use one of ${[...HTTP_METHODS].join(", ")}, remove the prefix for an all-methods rule, or add a leading \`/\` for a literal path`);
|
|
578
|
+
const { method, path: rawPath } = parseRouteKey(key);
|
|
579
|
+
const path = decodeRoutePattern(rawPath);
|
|
580
|
+
const canonicalKey = formatRouteKey(method, path);
|
|
581
|
+
validateBuiltinRules(routeConfig, canonicalKey);
|
|
582
|
+
const { redirect, proxy, cors, swr, cache, ...rest } = routeConfig;
|
|
583
|
+
const routeRules = rest;
|
|
584
|
+
if (redirect) {
|
|
585
|
+
const redirectOptions = {
|
|
586
|
+
to: "/",
|
|
587
|
+
status: 307,
|
|
588
|
+
...typeof redirect === "string" ? { to: redirect } : redirect
|
|
589
|
+
};
|
|
590
|
+
if (path.endsWith("/**")) redirectOptions.base = path.slice(0, -3);
|
|
591
|
+
assertNoHostSplat(redirectOptions.to, "redirect", canonicalKey);
|
|
592
|
+
routeRules.redirect = redirectOptions;
|
|
593
|
+
}
|
|
594
|
+
if (proxy) {
|
|
595
|
+
const proxyOptions = typeof proxy === "string" ? { to: proxy } : { ...proxy };
|
|
596
|
+
if (path.endsWith("/**")) proxyOptions.base = path.slice(0, -3);
|
|
597
|
+
assertNoHostSplat(proxyOptions.to, "proxy", canonicalKey);
|
|
598
|
+
routeRules.proxy = proxyOptions;
|
|
599
|
+
}
|
|
600
|
+
if (cors !== void 0 && cors !== false) {
|
|
601
|
+
const corsOptions = cors === true ? {} : { ...cors };
|
|
602
|
+
if (corsOptions.credentials === true && (!corsOptions.origin || corsOptions.origin === "*" || Array.isArray(corsOptions.origin) && corsOptions.origin.includes("*"))) throw new Error(`[h3] rules: \`cors\` rule for \`${canonicalKey}\` sets \`credentials: true\` with a wildcard origin — \`Access-Control-Allow-Origin: *\` is invalid for credentialed requests; set an explicit \`origin\` allowlist (or validation function)`);
|
|
603
|
+
routeRules.cors = corsOptions;
|
|
604
|
+
}
|
|
605
|
+
if (swr !== void 0 && swr !== false) {
|
|
606
|
+
const cacheOptions = { ...cache || void 0 };
|
|
607
|
+
cacheOptions.swr = true;
|
|
608
|
+
if (typeof swr === "number") cacheOptions.maxAge = swr;
|
|
609
|
+
routeRules.cache = cacheOptions;
|
|
610
|
+
} else if (swr === false && cache === void 0) routeRules.cache = false;
|
|
611
|
+
else if (cache !== void 0 && cache !== false) routeRules.cache = cache;
|
|
612
|
+
if (cache === false) routeRules.cache = false;
|
|
613
|
+
if (redirect === false) routeRules.redirect = false;
|
|
614
|
+
if (proxy === false) routeRules.proxy = false;
|
|
615
|
+
if (cors === false) routeRules.cors = false;
|
|
616
|
+
for (const name in routeRules) {
|
|
617
|
+
if (name === "__proto__" || name === "constructor" || name === "prototype") throw new Error(`[h3] rules: \`${name}\` is a reserved name and cannot be used as a rule for \`${canonicalKey}\``);
|
|
618
|
+
if (Array.isArray(routeRules[name])) throw new Error(`[h3] rules: \`${name}\` rule for \`${canonicalKey}\` is an array — rule options cannot be top-level arrays (ambiguous merge semantics); wrap it in an object`);
|
|
619
|
+
}
|
|
620
|
+
const existing = normalizedRules[canonicalKey];
|
|
621
|
+
if (existing) for (const [name, options] of Object.entries(routeRules)) existing[name] = mergeRuleOptions(existing[name], options);
|
|
622
|
+
else normalizedRules[canonicalKey] = routeRules;
|
|
623
|
+
}
|
|
624
|
+
return normalizedRules;
|
|
625
|
+
}
|
|
626
|
+
function assertNoHostSplat(to, name, canonicalKey) {
|
|
627
|
+
if (isHostPositionSplat(to)) throw new Error(`[h3] rules: \`${name}\` rule for \`${canonicalKey}\` has \`**\` in the target host (\`${to}\`) — \`**\` interpolates the matched path tail and must come after the target's host, in its path, query, or fragment`);
|
|
628
|
+
}
|
|
629
|
+
const BUILTIN_RULE_NAMES = [
|
|
630
|
+
"cache",
|
|
631
|
+
"headers",
|
|
632
|
+
"redirect",
|
|
633
|
+
"proxy",
|
|
634
|
+
"cors",
|
|
635
|
+
"swr"
|
|
636
|
+
];
|
|
637
|
+
function validateBuiltinRules(routeConfig, canonicalKey) {
|
|
638
|
+
for (const name of BUILTIN_RULE_NAMES) {
|
|
639
|
+
const value = routeConfig[name];
|
|
640
|
+
if (value || value === void 0 || value === false) continue;
|
|
641
|
+
if (name === "swr" && value === 0) continue;
|
|
642
|
+
throw new Error(`[h3] rules: \`${name}\` rule for \`${canonicalKey}\` is \`${String(value)}\` — use \`false\` to disable a rule inherited from a less-specific pattern, or provide options`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
export { HTTP_METHODS, buildRouteRuleMiddleware, cors, createMatcherFromFind, createRouteRulesMatcher, createRulesRouter, headers, memoizeRouteRulesMatcher, mergeMatchedRouteRules, normalizeRouteRules, parseRouteKey, redirect, ruleHandlers };
|
package/dist/path.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { normalizeRoute as normalizeRoute$1 } from "./response.mjs";
|
|
2
|
+
const DOT_SEGMENT_SRC = String.raw`(?:^|/)(?:\.|%(?:25)*2e){1,2}(?:/|$)`;
|
|
3
|
+
const ENCODED_SEP_SRC = String.raw`%(?:25)*(?:2f|5c)`;
|
|
4
|
+
const ENCODED_SEP_RE_G = /* @__PURE__ */ new RegExp(ENCODED_SEP_SRC, "gi");
|
|
5
|
+
const TRIGGER_RES = /* @__PURE__ */ (() => {
|
|
6
|
+
const base = String.raw`\\|` + DOT_SEGMENT_SRC;
|
|
7
|
+
return [
|
|
8
|
+
new RegExp(base, "i"),
|
|
9
|
+
new RegExp(`${base}|${ENCODED_SEP_SRC}`, "i"),
|
|
10
|
+
new RegExp(`${base}|//`, "i"),
|
|
11
|
+
new RegExp(`${base}|${ENCODED_SEP_SRC}|//`, "i")
|
|
12
|
+
];
|
|
13
|
+
})();
|
|
14
|
+
const ENCODED_DOT_RE_G = /%(?:25)*2e/gi;
|
|
15
|
+
function resolveDotSegments(path, opts) {
|
|
16
|
+
if (path[0] !== "/" || path[1] === "/" || path[1] === "\\") path = "/" + path.replace(/^[/\\]+/, "");
|
|
17
|
+
if (isCanonicalPath(path, opts)) return path;
|
|
18
|
+
const decodeSlashes = opts?.decodeSlashes;
|
|
19
|
+
const mergeSlashes = opts?.mergeSlashes;
|
|
20
|
+
let normalized = path.includes("\\") ? path.replaceAll("\\", "/") : path;
|
|
21
|
+
if (decodeSlashes) normalized = normalized.replace(ENCODED_SEP_RE_G, "/");
|
|
22
|
+
const segments = normalized.split("/");
|
|
23
|
+
const lastIndex = segments.length - 1;
|
|
24
|
+
const resolved = [];
|
|
25
|
+
for (let i = 0; i <= lastIndex; i++) {
|
|
26
|
+
const segment = segments[i];
|
|
27
|
+
const normalizedSegment = segment.includes("%") ? segment.replace(ENCODED_DOT_RE_G, ".") : segment;
|
|
28
|
+
const isDotSegment = normalizedSegment === "." || normalizedSegment === "..";
|
|
29
|
+
if (normalizedSegment === "..") {
|
|
30
|
+
if (resolved.length > 1) resolved.pop();
|
|
31
|
+
} else if (mergeSlashes && normalizedSegment === "" && i > 0 && i < lastIndex) {} else if (!isDotSegment) resolved.push(segment);
|
|
32
|
+
if (isDotSegment && i === lastIndex) resolved.push("");
|
|
33
|
+
}
|
|
34
|
+
return (resolved.join("/") || "/").replace(/^\/+/, "/");
|
|
35
|
+
}
|
|
36
|
+
function isCanonicalPath(path, opts) {
|
|
37
|
+
return path[0] === "/" && path[1] !== "/" && path[1] !== "\\" && !TRIGGER_RES[(opts?.decodeSlashes ? 1 : 0) | (opts?.mergeSlashes ? 2 : 0)].test(path);
|
|
38
|
+
}
|
|
39
|
+
function normalizeRoute(route) {
|
|
40
|
+
return normalizeRoute$1(route);
|
|
41
|
+
}
|
|
42
|
+
export { isCanonicalPath, normalizeRoute, resolveDotSegments };
|