nitro 3.0.260415-beta → 3.0.260429-beta
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/dist/THIRD-PARTY-LICENSES.md +32 -2
- package/dist/_build/common.mjs +43 -29
- package/dist/_build/rolldown.mjs +1 -1
- package/dist/_build/rollup.mjs +2 -2
- package/dist/_build/vite.build.mjs +1 -1
- package/dist/_chunks/nitro.mjs +5 -5
- package/dist/_chunks/nitro2.mjs +1 -1
- package/dist/_chunks/nitro3.mjs +1 -1
- package/dist/_chunks/nitro4.mjs +2 -2
- package/dist/_chunks/utils.mjs +2 -2
- package/dist/_common.mjs +1 -1
- package/dist/_dev.mjs +3 -2
- package/dist/_libs/c12+rc9.mjs +21 -9
- package/dist/_libs/hasown+resolve+deepmerge.mjs +47 -34
- package/dist/_libs/httpxy.d.mts +1 -3
- package/dist/_libs/httpxy.mjs +5 -5
- package/dist/_libs/nypm+tinyexec.mjs +129 -98
- package/dist/_libs/pkg-types.d.mts +7 -4
- package/dist/_libs/plugin-inject.mjs +3 -1
- package/dist/_libs/pluginutils.mjs +4 -2
- package/dist/_libs/readdirp+chokidar.mjs +2 -1
- package/dist/_libs/{unimport+unplugin.mjs → unimport+unplugin+oxc-parser.mjs} +49 -37
- package/dist/_libs/unplugin+unimport.d.mts +1 -1
- package/dist/_presets.mjs +2 -1
- package/dist/cli/_chunks/build.mjs +1 -1
- package/dist/cli/_chunks/deploy.mjs +1 -1
- package/dist/cli/_chunks/dev.mjs +1 -1
- package/dist/cli/_chunks/list.mjs +1 -1
- package/dist/cli/_chunks/prepare.mjs +1 -1
- package/dist/cli/_chunks/preview.mjs +1 -1
- package/dist/cli/_chunks/run.mjs +1 -1
- package/dist/docs/0.docs/5.cache.md +53 -9
- package/dist/node_modules/ufo/dist/index.mjs +2 -2
- package/dist/node_modules/ufo/package.json +9 -9
- package/dist/presets/azure/runtime/azure-swa.mjs +2 -0
- package/dist/runtime/internal/route-rules.d.mts +1 -0
- package/dist/runtime/internal/route-rules.mjs +29 -1
- package/dist/runtime/internal/routes/scalar.mjs +1 -0
- package/dist/types/index.d.mts +922 -64
- package/dist/vite.d.mts +1 -1
- package/dist/vite.mjs +11 -5
- package/package.json +28 -27
- package/dist/_libs/_.mjs +0 -2
- package/dist/_libs/_2.mjs +0 -2
- package/dist/_libs/_3.mjs +0 -2
- package/dist/_libs/_4.mjs +0 -2
- package/dist/_libs/_5.mjs +0 -2
- package/dist/_libs/_6.mjs +0 -2
- package/dist/_libs/_7.mjs +0 -2
- package/dist/_libs/escape-string-regexp.mjs +0 -5
|
@@ -1,47 +1,46 @@
|
|
|
1
|
-
import { i as __toESM } from "../_common.mjs";
|
|
2
|
-
import { N as require_picomatch, S as MagicString, T as parse$
|
|
1
|
+
import { i as __toESM, n as __exportAll } from "../_common.mjs";
|
|
2
|
+
import { N as require_picomatch, S as MagicString, T as parse$2, b as getMagicString, ct as isAbsolute$2, ft as resolve$1, lt as join$1, y as createUnimport } from "../_build/common.mjs";
|
|
3
3
|
import { t as walk } from "./estree-walker.mjs";
|
|
4
4
|
import { t as remapping } from "./remapping.mjs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import fs, { promises } from "node:fs";
|
|
7
7
|
import path, { basename, dirname, extname, isAbsolute, normalize, resolve } from "node:path";
|
|
8
8
|
import process$1 from "node:process";
|
|
9
|
+
import { parseSync } from "rolldown/utils";
|
|
9
10
|
import { Buffer } from "node:buffer";
|
|
10
11
|
import * as querystring from "node:querystring";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
12
|
+
function createEstreeDetector(parse) {
|
|
13
|
+
return async (code, ctx, options) => {
|
|
14
|
+
const s = getMagicString(code);
|
|
15
|
+
const map = await ctx.getImportMap();
|
|
16
|
+
let matchedImports = [];
|
|
17
|
+
const enableAutoImport = options?.autoImport !== false;
|
|
18
|
+
const enableTransformVirtualImports = options?.transformVirtualImports !== false && ctx.options.virtualImports?.length;
|
|
19
|
+
if (enableAutoImport || enableTransformVirtualImports) {
|
|
20
|
+
const ast = parse(s.original);
|
|
21
|
+
const virtualImports = createVirtualImports(map, ctx.options.virtualImports);
|
|
22
|
+
const scopes = traveseScopes(ast, enableTransformVirtualImports ? virtualImports.walk : {});
|
|
23
|
+
if (enableAutoImport) {
|
|
24
|
+
const identifiers = scopes.unmatched;
|
|
25
|
+
matchedImports.push(...Array.from(identifiers).map((name) => {
|
|
26
|
+
const item = map.get(name);
|
|
27
|
+
if (item && !item.disabled) return item;
|
|
28
|
+
return null;
|
|
29
|
+
}).filter(Boolean));
|
|
30
|
+
for (const addon of ctx.addons) matchedImports = await addon.matchImports?.call(ctx, identifiers, matchedImports) || matchedImports;
|
|
31
|
+
}
|
|
32
|
+
virtualImports.ranges.forEach(([start, end]) => {
|
|
33
|
+
s.remove(start, end);
|
|
34
|
+
});
|
|
35
|
+
matchedImports.push(...virtualImports.imports);
|
|
33
36
|
}
|
|
34
|
-
|
|
35
|
-
s
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
strippedCode: code.toString(),
|
|
42
|
-
matchedImports,
|
|
43
|
-
isCJSContext: false,
|
|
44
|
-
firstOccurrence: 0
|
|
37
|
+
return {
|
|
38
|
+
s,
|
|
39
|
+
strippedCode: code.toString(),
|
|
40
|
+
matchedImports,
|
|
41
|
+
isCJSContext: false,
|
|
42
|
+
firstOccurrence: 0
|
|
43
|
+
};
|
|
45
44
|
};
|
|
46
45
|
}
|
|
47
46
|
function traveseScopes(ast, additionalWalk) {
|
|
@@ -164,7 +163,7 @@ function traveseScopes(ast, additionalWalk) {
|
|
|
164
163
|
scopes
|
|
165
164
|
};
|
|
166
165
|
}
|
|
167
|
-
function
|
|
166
|
+
function createVirtualImports(importMap, virtualImports = []) {
|
|
168
167
|
const imports = [];
|
|
169
168
|
const ranges = [];
|
|
170
169
|
return {
|
|
@@ -190,6 +189,14 @@ function createVirtualImportsAcronWalker(importMap, virtualImports = []) {
|
|
|
190
189
|
} }
|
|
191
190
|
};
|
|
192
191
|
}
|
|
192
|
+
var detect_acorn_exports = /* @__PURE__ */ __exportAll({ detectImportsAcorn: () => detectImportsAcorn });
|
|
193
|
+
const detectImportsAcorn = createEstreeDetector((code) => parse$2(code, {
|
|
194
|
+
sourceType: "module",
|
|
195
|
+
ecmaVersion: "latest",
|
|
196
|
+
locations: true
|
|
197
|
+
}));
|
|
198
|
+
var detect_oxc_exports = /* @__PURE__ */ __exportAll({ detectImportsOxc: () => detectImportsOxc });
|
|
199
|
+
const detectImportsOxc = createEstreeDetector((code) => parseSync("", code, { sourceType: "module" }).program);
|
|
193
200
|
var import_picomatch = /* @__PURE__ */ __toESM(require_picomatch(), 1);
|
|
194
201
|
function toArray$2(array) {
|
|
195
202
|
array = array || [];
|
|
@@ -1602,6 +1609,11 @@ function createFilter(include, exclude, options) {
|
|
|
1602
1609
|
return !includeMatchers.length;
|
|
1603
1610
|
};
|
|
1604
1611
|
}
|
|
1612
|
+
var unplugin_exports = /* @__PURE__ */ __exportAll({
|
|
1613
|
+
default: () => unplugin,
|
|
1614
|
+
defaultExcludes: () => defaultExcludes,
|
|
1615
|
+
defaultIncludes: () => defaultIncludes
|
|
1616
|
+
});
|
|
1605
1617
|
const defaultIncludes = [
|
|
1606
1618
|
/\.[jt]sx?$/,
|
|
1607
1619
|
/\.vue$/,
|
|
@@ -1638,4 +1650,4 @@ const unplugin = createUnplugin((options = {}) => {
|
|
|
1638
1650
|
}
|
|
1639
1651
|
};
|
|
1640
1652
|
});
|
|
1641
|
-
export {
|
|
1653
|
+
export { detect_oxc_exports as n, detect_acorn_exports as r, unplugin_exports as t };
|
package/dist/_presets.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { F as prettyPath$1, G as findFile, J as readGitConfig, K as findNearestFile, L as writeFile$2, M as glob, Q as a, U as v, V as f, X as i, Y as readPackageJSON, dt as relative$1, ft as resolve$1, lt as join$1, ot as dirname$1, rt as resolveModulePath, tt as r, z as R } from "./_build/common.mjs";
|
|
2
2
|
import { r as resolveCompatibilityDatesFromEnv, t as formatCompatibilityDate } from "./_libs/compatx.mjs";
|
|
3
3
|
import { t as Router } from "./_chunks/nitro3.mjs";
|
|
4
4
|
import { t as importDep } from "./_chunks/utils.mjs";
|
|
@@ -1365,6 +1365,7 @@ async function generateFunctionFiles(nitro) {
|
|
|
1365
1365
|
launcherType: "Nodejs",
|
|
1366
1366
|
shouldAddHelpers: false,
|
|
1367
1367
|
supportsResponseStreaming: true,
|
|
1368
|
+
...nitro.options.sourcemap ? { shouldAddSourcemapSupport: true } : {},
|
|
1368
1369
|
...nitro.options.vercel?.functions
|
|
1369
1370
|
};
|
|
1370
1371
|
if (Array.isArray(baseFunctionConfig.experimentalTriggers) && baseFunctionConfig.experimentalTriggers.length > 0) nitro.logger.warn("`experimentalTriggers` on the base `vercel.functions` config applies to the catch-all function and is likely not what you want. Routes with queue triggers are not accessible on the web. Use `vercel.functionRules` to attach triggers to specific routes instead.");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ft as resolve } from "../../_build/common.mjs";
|
|
2
2
|
import { t as defineCommand } from "../../_libs/citty.mjs";
|
|
3
3
|
import { t as commonArgs } from "./common.mjs";
|
|
4
4
|
import { build, copyPublicAssets, createNitro, prepare, prerender } from "nitro/builder";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { dt as
|
|
1
|
+
import { dt as relative, ft as resolve, m as getBuildInfo } from "../../_build/common.mjs";
|
|
2
2
|
import { t as defineCommand } from "../../_libs/citty.mjs";
|
|
3
3
|
import build_default, { buildArgs } from "./build.mjs";
|
|
4
4
|
import consola from "consola";
|
package/dist/cli/_chunks/dev.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ft as resolve } from "../../_build/common.mjs";
|
|
2
2
|
import { t as NitroDevServer } from "../../_dev.mjs";
|
|
3
3
|
import { t as defineCommand } from "../../_libs/citty.mjs";
|
|
4
4
|
import { t as commonArgs } from "./common.mjs";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ft as resolve } from "../../_build/common.mjs";
|
|
2
2
|
import { t as defineCommand } from "../../_libs/citty.mjs";
|
|
3
3
|
import { consola as consola$1 } from "consola";
|
|
4
4
|
import { listTasks, loadOptions } from "nitro/builder";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ft as resolve } from "../../_build/common.mjs";
|
|
2
2
|
import { t as defineCommand } from "../../_libs/citty.mjs";
|
|
3
3
|
import { t as commonArgs } from "./common.mjs";
|
|
4
4
|
import { createNitro, writeTypes } from "nitro/builder";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ft as resolve } from "../../_build/common.mjs";
|
|
2
2
|
import { t as defineCommand } from "../../_libs/citty.mjs";
|
|
3
3
|
import { t as startPreview } from "../../_chunks/nitro4.mjs";
|
|
4
4
|
import { t as commonArgs } from "./common.mjs";
|
package/dist/cli/_chunks/run.mjs
CHANGED
|
@@ -357,7 +357,59 @@ When `swr` is disabled and a cached value has expired:
|
|
|
357
357
|
1. The stale entry is cleared.
|
|
358
358
|
2. The client waits for the function/handler to resolve with a fresh value.
|
|
359
359
|
|
|
360
|
-
## Cache
|
|
360
|
+
## Cache invalidation
|
|
361
|
+
Cached entries can be invalidated programmatically at runtime (for example from a webhook when the underlying data changes) without waiting for `maxAge` to expire.
|
|
362
|
+
|
|
363
|
+
### `.invalidate()` method
|
|
364
|
+
|
|
365
|
+
Every function created with `defineCachedFunction` exposes an `.invalidate()` method. Arguments are passed through `getKey` to generate the cache key.
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
import { defineCachedFunction } from "nitro/cache";
|
|
369
|
+
|
|
370
|
+
const cachedGHStars = defineCachedFunction(async (repo: string) => {
|
|
371
|
+
const data = await fetch(`https://api.github.com/repos/${repo}`).then(res => res.json());
|
|
372
|
+
return data.stargazers_count;
|
|
373
|
+
}, {
|
|
374
|
+
maxAge: 60 * 60,
|
|
375
|
+
name: "ghStars",
|
|
376
|
+
getKey: (repo: string) => repo,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
await cachedGHStars("unjs/nitro"); // populates the cache
|
|
380
|
+
await cachedGHStars.invalidate("unjs/nitro"); // removes the entry
|
|
381
|
+
await cachedGHStars("unjs/nitro"); // re-invokes the function
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
If no cached entry matches the given arguments, `.invalidate()` resolves without error and leaves storage unchanged.
|
|
385
|
+
|
|
386
|
+
### `invalidateCache()` helper
|
|
387
|
+
|
|
388
|
+
`invalidateCache` is a helper from `ocache`. It invalidates a cached entry from the cache options used to define it, so invalidation can live anywhere in your app, independent of where the cached function is defined.
|
|
389
|
+
|
|
390
|
+
Pass the same `name`, `group`, `base`, and `getKey` used when defining the function, along with the `args` identifying the entry to remove:
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
import { invalidateCache } from "ocache";
|
|
394
|
+
|
|
395
|
+
await invalidateCache({
|
|
396
|
+
options: {
|
|
397
|
+
name: "ghStars",
|
|
398
|
+
group: "nitro/functions",
|
|
399
|
+
getKey: (repo: string) => repo,
|
|
400
|
+
},
|
|
401
|
+
args: ["unjs/nitro"],
|
|
402
|
+
});
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
<important>
|
|
406
|
+
|
|
407
|
+
The `name`, `group`, `base`, and `getKey` passed to `invalidateCache` must match the ones used when the cached function was defined. Mismatched options resolve to a different storage key and will not invalidate the intended entry. :br :br
|
|
408
|
+
Nitro defaults to `group: 'nitro/functions'` for cached functions and `group: 'nitro/handlers'` for cached handlers (or `'nitro/route-rules'` when using [route rules](#using-route-rules)).
|
|
409
|
+
</important>
|
|
410
|
+
|
|
411
|
+
## Cache keys
|
|
412
|
+
|
|
361
413
|
When using the `defineCachedFunction` or `defineCachedHandler` functions, the cache key is generated using the following pattern:
|
|
362
414
|
|
|
363
415
|
```ts
|
|
@@ -384,14 +436,6 @@ Will generate the following cache key:
|
|
|
384
436
|
cache:nitro/functions:getAccessToken:default.json
|
|
385
437
|
```
|
|
386
438
|
|
|
387
|
-
You can invalidate the cached function entry with:
|
|
388
|
-
|
|
389
|
-
```ts
|
|
390
|
-
import { useStorage } from "nitro/storage";
|
|
391
|
-
|
|
392
|
-
await useStorage('cache').removeItem('nitro/functions:getAccessToken:default.json')
|
|
393
|
-
```
|
|
394
|
-
|
|
395
439
|
<note>
|
|
396
440
|
|
|
397
441
|
For cached handlers, the cache key includes a hash of the URL path and, when using the [`varies`](#handler-only-options) option, hashes of the specified header values appended to the key.
|
|
@@ -286,8 +286,8 @@ function withoutBase(input, base) {
|
|
|
286
286
|
if (nextChar && nextChar !== "/" && nextChar !== "?") {
|
|
287
287
|
return input;
|
|
288
288
|
}
|
|
289
|
-
const trimmed = input.slice(_base.length);
|
|
290
|
-
return
|
|
289
|
+
const trimmed = input.slice(_base.length).replace(/^\/+/, "");
|
|
290
|
+
return "/" + trimmed;
|
|
291
291
|
}
|
|
292
292
|
function withQuery(input, query) {
|
|
293
293
|
const parsed = parseURL(input);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ufo",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.4",
|
|
4
4
|
"description": "URL utils for humans",
|
|
5
5
|
"repository": "unjs/ufo",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,18 +31,18 @@
|
|
|
31
31
|
"test": "pnpm lint && vitest run --typecheck"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@types/node": "^25.0
|
|
35
|
-
"@vitest/coverage-v8": "^4.
|
|
36
|
-
"automd": "^0.4.
|
|
34
|
+
"@types/node": "^25.6.0",
|
|
35
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
36
|
+
"automd": "^0.4.3",
|
|
37
37
|
"changelogen": "^0.6.2",
|
|
38
|
-
"eslint": "^
|
|
38
|
+
"eslint": "^10.2.1",
|
|
39
39
|
"eslint-config-unjs": "^0.6.2",
|
|
40
40
|
"jiti": "^2.6.1",
|
|
41
|
-
"prettier": "^3.
|
|
42
|
-
"typescript": "^
|
|
41
|
+
"prettier": "^3.8.3",
|
|
42
|
+
"typescript": "^6.0.3",
|
|
43
43
|
"unbuild": "^3.6.1",
|
|
44
44
|
"untyped": "^2.0.0",
|
|
45
|
-
"vitest": "^4.
|
|
45
|
+
"vitest": "^4.1.5"
|
|
46
46
|
},
|
|
47
|
-
"packageManager": "pnpm@10.
|
|
47
|
+
"packageManager": "pnpm@10.33.2"
|
|
48
48
|
}
|
|
@@ -8,4 +8,5 @@ export declare const redirect: RouteRuleCtor<"redirect">;
|
|
|
8
8
|
export declare const proxy: RouteRuleCtor<"proxy">;
|
|
9
9
|
export declare const cache: RouteRuleCtor<"cache">;
|
|
10
10
|
export declare const basicAuth: RouteRuleCtor<"auth">;
|
|
11
|
+
export declare function isPathInScope(pathname: string, base: string): boolean;
|
|
11
12
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { proxyRequest, redirect as sendRedirect, requireBasicAuth } from "h3";
|
|
1
|
+
import { HTTPError, proxyRequest, redirect as sendRedirect, requireBasicAuth } from "h3";
|
|
2
2
|
import { joinURL, withQuery, withoutBase } from "ufo";
|
|
3
3
|
import { defineCachedHandler } from "./cache.mjs";
|
|
4
4
|
|
|
@@ -17,7 +17,12 @@ export const redirect = ((m) => function redirectRouteRule(event) {
|
|
|
17
17
|
let targetPath = event.url.pathname + event.url.search;
|
|
18
18
|
const strpBase = m.options._redirectStripBase;
|
|
19
19
|
if (strpBase) {
|
|
20
|
+
if (!isPathInScope(event.url.pathname, strpBase)) {
|
|
21
|
+
throw new HTTPError({ status: 400 });
|
|
22
|
+
}
|
|
20
23
|
targetPath = withoutBase(targetPath, strpBase);
|
|
24
|
+
} else if (targetPath.startsWith("//")) {
|
|
25
|
+
targetPath = targetPath.replace(/^\/+/, "/");
|
|
21
26
|
}
|
|
22
27
|
target = joinURL(target.slice(0, -3), targetPath);
|
|
23
28
|
} else if (event.url.search) {
|
|
@@ -35,7 +40,12 @@ export const proxy = ((m) => function proxyRouteRule(event) {
|
|
|
35
40
|
let targetPath = event.url.pathname + event.url.search;
|
|
36
41
|
const strpBase = m.options._proxyStripBase;
|
|
37
42
|
if (strpBase) {
|
|
43
|
+
if (!isPathInScope(event.url.pathname, strpBase)) {
|
|
44
|
+
throw new HTTPError({ status: 400 });
|
|
45
|
+
}
|
|
38
46
|
targetPath = withoutBase(targetPath, strpBase);
|
|
47
|
+
} else if (targetPath.startsWith("//")) {
|
|
48
|
+
targetPath = targetPath.replace(/^\/+/, "/");
|
|
39
49
|
}
|
|
40
50
|
target = joinURL(target.slice(0, -3), targetPath);
|
|
41
51
|
} else if (event.url.search) {
|
|
@@ -72,3 +82,21 @@ export const basicAuth = /* @__PURE__ */ Object.assign(((m) => async function au
|
|
|
72
82
|
await requireBasicAuth(event, m.options);
|
|
73
83
|
return next();
|
|
74
84
|
}), { order: -1 });
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
export function isPathInScope(pathname, base) {
|
|
94
|
+
let canonical;
|
|
95
|
+
try {
|
|
96
|
+
const pre = pathname.replace(/%2f/gi, "/").replace(/%5c/gi, "\\");
|
|
97
|
+
canonical = new URL(pre, "http://_").pathname;
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return !base || canonical === base || canonical.startsWith(base + "/");
|
|
102
|
+
}
|