shelving 1.192.1 → 1.193.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/api/endpoint/util.js +4 -17
- package/api/provider/APIProvider.d.ts +2 -2
- package/api/provider/ClientAPIProvider.d.ts +2 -2
- package/api/provider/ClientAPIProvider.js +1 -1
- package/api/provider/DebugAPIProvider.js +5 -5
- package/api/provider/ThroughAPIProvider.d.ts +2 -2
- package/package.json +1 -1
- package/store/PathStore.d.ts +5 -5
- package/store/PathStore.js +4 -4
- package/util/path.d.ts +43 -14
- package/util/path.js +66 -24
- package/util/url.d.ts +43 -18
- package/util/url.js +75 -40
package/api/endpoint/util.js
CHANGED
|
@@ -3,18 +3,15 @@ import { ValueError } from "../../error/ValueError.js";
|
|
|
3
3
|
import { requireDictionary } from "../../util/dictionary.js";
|
|
4
4
|
import { getResponse, isRequestMethod, parseRequestBody } from "../../util/http.js";
|
|
5
5
|
import { isPlainObject } from "../../util/object.js";
|
|
6
|
-
import {
|
|
6
|
+
import { matchURLPrefix } from "../../util/url.js";
|
|
7
7
|
export function handleEndpoints(base, handlers, request, context, caller = handleEndpoints) {
|
|
8
8
|
const { url, method } = request;
|
|
9
9
|
if (!isRequestMethod(method))
|
|
10
10
|
throw new MethodNotAllowedError("Unsupported request method", { received: method, caller });
|
|
11
|
-
const {
|
|
12
|
-
const
|
|
13
|
-
if (baseOrigin !== requestOrigin)
|
|
14
|
-
throw new NotFoundError("No matching base origin", { expected: baseOrigin, received: requestOrigin, caller });
|
|
15
|
-
const targetPath = _stripPathPrefix(requestPath, basePath);
|
|
11
|
+
const { pathname: requestPath, searchParams } = new URL(url, base);
|
|
12
|
+
const targetPath = matchURLPrefix(url, base);
|
|
16
13
|
if (!targetPath)
|
|
17
|
-
throw new NotFoundError("No matching base path", { received: requestPath,
|
|
14
|
+
throw new NotFoundError("No matching base path", { received: requestPath, caller });
|
|
18
15
|
for (const handler of handlers) {
|
|
19
16
|
const pathParams = handler.endpoint.match(method, targetPath, caller);
|
|
20
17
|
if (!pathParams)
|
|
@@ -45,13 +42,3 @@ params, request, context, caller) {
|
|
|
45
42
|
throw thrown;
|
|
46
43
|
}
|
|
47
44
|
}
|
|
48
|
-
/** Strip a prefix like `/a/b` from a path like `/a/b/c/d` to produce a remainder path like `/c/d`. */
|
|
49
|
-
function _stripPathPrefix(path, prefix) {
|
|
50
|
-
prefix = prefix === "/" ? "/" : prefix.replace(/\/$/, "");
|
|
51
|
-
if (prefix === "/")
|
|
52
|
-
return path;
|
|
53
|
-
if (path === prefix)
|
|
54
|
-
return "/";
|
|
55
|
-
if (path.startsWith(`${prefix}/`))
|
|
56
|
-
return path.slice(prefix.length);
|
|
57
|
-
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { AnyCaller } from "../../util/function.js";
|
|
2
2
|
import type { RequestOptions } from "../../util/http.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { BaseURL, URL } from "../../util/url.js";
|
|
4
4
|
import type { Endpoint } from "../endpoint/Endpoint.js";
|
|
5
5
|
/** Provider for API endpoints rooted at a common base URL. */
|
|
6
6
|
export declare abstract class APIProvider<P = unknown, R = unknown> {
|
|
7
7
|
/** The base URL for this API. */
|
|
8
|
-
abstract readonly url:
|
|
8
|
+
abstract readonly url: BaseURL;
|
|
9
9
|
/**
|
|
10
10
|
* Render the full final URL for an API request to a given endpoint with a given payload.
|
|
11
11
|
* - Includes `?query` params if this is a `HEAD` or `GET` request.
|
|
@@ -2,7 +2,7 @@ import type { AnyCaller } from "../../util/function.js";
|
|
|
2
2
|
import { type RequestBodyMethod, type RequestHeadMethod, type RequestOptions } from "../../util/http.js";
|
|
3
3
|
import type { Nullish } from "../../util/null.js";
|
|
4
4
|
import { type PossibleURIParams } from "../../util/uri.js";
|
|
5
|
-
import { type
|
|
5
|
+
import { type BaseURL, type PossibleURL, type URL } from "../../util/url.js";
|
|
6
6
|
import type { Endpoint } from "../endpoint/Endpoint.js";
|
|
7
7
|
import { APIProvider } from "./APIProvider.js";
|
|
8
8
|
/** Options for a `ClientAPIProvider`. */
|
|
@@ -27,7 +27,7 @@ export interface ClientAPIProviderOptions {
|
|
|
27
27
|
*/
|
|
28
28
|
export declare class ClientAPIProvider<P = unknown, R = unknown> extends APIProvider<P, R> {
|
|
29
29
|
/** The common base URL for all rendered endpoint requests. */
|
|
30
|
-
readonly url:
|
|
30
|
+
readonly url: BaseURL;
|
|
31
31
|
/** Default options used for HTTP requests created with `this.getRequest()` and `this.fetch()` */
|
|
32
32
|
readonly options: RequestOptions;
|
|
33
33
|
/** Timeout in milliseconds, or `undefined` for no timeout. */
|
|
@@ -23,7 +23,7 @@ export class ClientAPIProvider extends APIProvider {
|
|
|
23
23
|
timeout;
|
|
24
24
|
constructor({ url, options = {}, timeout }) {
|
|
25
25
|
super();
|
|
26
|
-
this.url = requireBaseURL(url,
|
|
26
|
+
this.url = requireBaseURL(url, ClientAPIProvider);
|
|
27
27
|
this.options = options;
|
|
28
28
|
this.timeout = timeout;
|
|
29
29
|
}
|
|
@@ -9,29 +9,29 @@ export class DebugAPIProvider extends ThroughAPIProvider {
|
|
|
9
9
|
request = this.getRequest(endpoint, payload, options, caller);
|
|
10
10
|
}
|
|
11
11
|
catch (reason) {
|
|
12
|
-
console.error("✘ FETCH", this.url, endpoint.toString(), payload, reason);
|
|
12
|
+
console.error("✘ FETCH", this.url.toString(), endpoint.toString(), payload, reason);
|
|
13
13
|
throw reason;
|
|
14
14
|
}
|
|
15
15
|
const debuggedRequest = await debugFullRequest(request);
|
|
16
|
-
console.debug("… FETCH", this.url, endpoint.toString(), payload, debuggedRequest);
|
|
16
|
+
console.debug("… FETCH", this.url.toString(), endpoint.toString(), payload, debuggedRequest);
|
|
17
17
|
// Fetch the response and debug if it throws.
|
|
18
18
|
let response;
|
|
19
19
|
try {
|
|
20
20
|
response = await this.fetch(request);
|
|
21
21
|
}
|
|
22
22
|
catch (reason) {
|
|
23
|
-
console.error("✘ FETCH", this.url, endpoint.toString(), payload, debuggedRequest, reason);
|
|
23
|
+
console.error("✘ FETCH", this.url.toString(), endpoint.toString(), payload, debuggedRequest, reason);
|
|
24
24
|
throw reason;
|
|
25
25
|
}
|
|
26
26
|
const debuggedResponse = await debugFullResponse(response);
|
|
27
27
|
// Convert the result or any parsing error.
|
|
28
28
|
try {
|
|
29
29
|
const result = await this.parseResponse(endpoint, response, caller);
|
|
30
|
-
console.debug("✔ FETCH", this.url, endpoint.toString(), payload, debuggedRequest, debuggedResponse, result);
|
|
30
|
+
console.debug("✔ FETCH", this.url.toString(), endpoint.toString(), payload, debuggedRequest, debuggedResponse, result);
|
|
31
31
|
return result;
|
|
32
32
|
}
|
|
33
33
|
catch (reason) {
|
|
34
|
-
console.error("✘ FETCH", this.url, endpoint.toString(), payload, debuggedRequest, debuggedResponse, reason);
|
|
34
|
+
console.error("✘ FETCH", this.url.toString(), endpoint.toString(), payload, debuggedRequest, debuggedResponse, reason);
|
|
35
35
|
throw reason;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AnyCaller } from "../../util/function.js";
|
|
2
2
|
import type { RequestOptions } from "../../util/http.js";
|
|
3
3
|
import type { Sourceable } from "../../util/source.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { BaseURL, URL } from "../../util/url.js";
|
|
5
5
|
import type { Endpoint } from "../endpoint/Endpoint.js";
|
|
6
6
|
import { APIProvider } from "./APIProvider.js";
|
|
7
7
|
/**
|
|
@@ -9,7 +9,7 @@ import { APIProvider } from "./APIProvider.js";
|
|
|
9
9
|
* - Extend this when you want to intercept only selected API operations, such as injecting auth headers or logging.
|
|
10
10
|
*/
|
|
11
11
|
export declare class ThroughAPIProvider<P, R> extends APIProvider<P, R> implements Sourceable<APIProvider<P, R>> {
|
|
12
|
-
get url():
|
|
12
|
+
get url(): BaseURL;
|
|
13
13
|
readonly source: APIProvider<P, R>;
|
|
14
14
|
constructor(source: APIProvider<P, R>);
|
|
15
15
|
renderURL<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, caller?: AnyCaller): URL;
|
package/package.json
CHANGED
package/store/PathStore.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AnyCaller } from "../util/function.js";
|
|
2
|
-
import type { AbsolutePath,
|
|
2
|
+
import type { AbsolutePath, RelativePath } from "../util/path.js";
|
|
3
3
|
import { BusyStore } from "./BusyStore.js";
|
|
4
4
|
/**
|
|
5
5
|
* Store an absolute path, e.g. `/a/b/c`
|
|
@@ -7,16 +7,16 @@ import { BusyStore } from "./BusyStore.js";
|
|
|
7
7
|
* @param path: The initial value for the store.
|
|
8
8
|
* @param base: The base path to resolve relative paths against.
|
|
9
9
|
*/
|
|
10
|
-
export declare class PathStore extends BusyStore<AbsolutePath,
|
|
10
|
+
export declare class PathStore extends BusyStore<AbsolutePath, AbsolutePath | RelativePath> {
|
|
11
11
|
readonly base: AbsolutePath;
|
|
12
|
-
constructor(path?:
|
|
13
|
-
protected _convert(possible:
|
|
12
|
+
constructor(path?: AbsolutePath | RelativePath, base?: AbsolutePath);
|
|
13
|
+
protected _convert(possible: AbsolutePath | RelativePath, caller: AnyCaller): AbsolutePath;
|
|
14
14
|
protected _equal(a: AbsolutePath, b: AbsolutePath): boolean;
|
|
15
15
|
/** Based on the current store path, is a path active? */
|
|
16
16
|
isActive(path: AbsolutePath): boolean;
|
|
17
17
|
/** Based on the current store path, is a path proud (i.e. a child of the current store path)? */
|
|
18
18
|
isProud(path: AbsolutePath): boolean;
|
|
19
19
|
/** Get an absolute path from a path relative to the current store path. */
|
|
20
|
-
getPath(path:
|
|
20
|
+
getPath(path: AbsolutePath | RelativePath): AbsolutePath;
|
|
21
21
|
toString(): string;
|
|
22
22
|
}
|
package/store/PathStore.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isPathActive, isPathProud,
|
|
1
|
+
import { isPathActive, isPathProud, requireAbsolutePath } from "../util/path.js";
|
|
2
2
|
import { BusyStore } from "./BusyStore.js";
|
|
3
3
|
/**
|
|
4
4
|
* Store an absolute path, e.g. `/a/b/c`
|
|
@@ -10,12 +10,12 @@ export class PathStore extends BusyStore {
|
|
|
10
10
|
base;
|
|
11
11
|
// Override to set default path to `.` and base to `/`
|
|
12
12
|
constructor(path = ".", base = "/") {
|
|
13
|
-
super(
|
|
13
|
+
super(requireAbsolutePath(path, base, PathStore));
|
|
14
14
|
this.base = base;
|
|
15
15
|
}
|
|
16
16
|
// Override to convert a possible path to an absolute path (relative to `this.base`).
|
|
17
17
|
_convert(possible, caller) {
|
|
18
|
-
return
|
|
18
|
+
return requireAbsolutePath(possible, this.base, caller);
|
|
19
19
|
}
|
|
20
20
|
// Override for fast equality.
|
|
21
21
|
_equal(a, b) {
|
|
@@ -31,7 +31,7 @@ export class PathStore extends BusyStore {
|
|
|
31
31
|
}
|
|
32
32
|
/** Get an absolute path from a path relative to the current store path. */
|
|
33
33
|
getPath(path) {
|
|
34
|
-
return
|
|
34
|
+
return requireAbsolutePath(path, this.value);
|
|
35
35
|
}
|
|
36
36
|
toString() {
|
|
37
37
|
return this.value;
|
package/util/path.d.ts
CHANGED
|
@@ -1,37 +1,66 @@
|
|
|
1
|
+
import type { ImmutableArray } from "./array.js";
|
|
1
2
|
import type { AnyCaller } from "./function.js";
|
|
2
3
|
import type { Nullish } from "./null.js";
|
|
3
4
|
/** Absolute path string starts with `/` slash. */
|
|
4
5
|
export type AbsolutePath = `/` | `/${string}`;
|
|
5
|
-
/**
|
|
6
|
-
export type
|
|
7
|
-
/**
|
|
8
|
-
export type
|
|
6
|
+
/** Base path string starts with `/` slash and ends with `/` */
|
|
7
|
+
export type BasePath = `/` | `/${string}`;
|
|
8
|
+
/** Relative path string is `.` dot, or starts with `./` dot slash. */
|
|
9
|
+
export type RelativePath = `.` | `./` | `./${string}`;
|
|
10
|
+
/** Simple path string like `a/b/c` */
|
|
11
|
+
export type SimplePath = string;
|
|
9
12
|
/** Things that can be converted to a path. */
|
|
10
|
-
export type PossiblePath = string
|
|
11
|
-
/**
|
|
12
|
-
export
|
|
13
|
+
export type PossiblePath = string;
|
|
14
|
+
/** List of non-empty path segments. */
|
|
15
|
+
export type PathSegments = ImmutableArray<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Is a string a valid path segment, e.g. `abc` or `myFile.pdf` or `.myFile`
|
|
18
|
+
* - Disallows `""` empty string.
|
|
19
|
+
* - Disallows runs of `.` as the only contents (e.g. `.`, `..` or `...`).
|
|
20
|
+
* - Disallows characters outside `A-Za-z0-9_.-`
|
|
21
|
+
*/
|
|
22
|
+
export declare function isPathSegment(segment: string): boolean;
|
|
23
|
+
/** Get a simple path segment after filtering out invalid characters. */
|
|
24
|
+
export declare function getPathSegment(input: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Is a string a simple path, e.g. `a/b/c`
|
|
27
|
+
* - Separated by `/` slashes.
|
|
28
|
+
* - No leading/trailing slashes.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isPath(path: string): boolean;
|
|
13
31
|
/** Is a string path an absolute path? */
|
|
14
|
-
export declare function
|
|
32
|
+
export declare function isAbsolutePath(path: string): path is AbsolutePath;
|
|
33
|
+
/** Is a string path an relative path? */
|
|
34
|
+
export declare function isRelativePath(path: string): path is RelativePath;
|
|
35
|
+
/** Split a path into simple path segments, e.g. `a/b/c` -> ["a", "b", "c"] */
|
|
36
|
+
export declare function splitPath(path: string): PathSegments;
|
|
37
|
+
/** Join path segments into an absolute path, e.g. ["a", "b", "c"] -> `/a/b/c` */
|
|
38
|
+
export declare function joinPath(...segments: PathSegments): AbsolutePath;
|
|
15
39
|
/**
|
|
16
40
|
* Resolve a relative or absolute path and return the absolute path, or `undefined` if not a valid path.
|
|
17
|
-
* - Uses `new URL` to do path processing, so URL strings can also be resolved.
|
|
18
41
|
* - Returned paths are cleaned with `cleanPath()` so runs of slashes and trailing slashes are removed.
|
|
19
42
|
*
|
|
20
|
-
* @param
|
|
43
|
+
* @param path Absolute path e.g. `/a/b/c`, relative path e.g. `./a` or `b` or `../c`, URL string e.g. `http://shax.com/a/b/c`, or `URL` instance.
|
|
21
44
|
* @param base Absolute path used for resolving relative paths in `possible`
|
|
22
|
-
* @return Absolute path with a leading trailing slash, e.g. `/a/c/b`
|
|
45
|
+
* @return Absolute path with a leading slash but no trailing slash, e.g. `/a/c/b`
|
|
23
46
|
*/
|
|
24
|
-
export declare function
|
|
47
|
+
export declare function getAbsolutePath(path: Nullish<PossiblePath>, base?: AbsolutePath): AbsolutePath | undefined;
|
|
25
48
|
/**
|
|
26
49
|
* Resolve a relative or absolute path and return the path, or throw `RequiredError` if not a valid path.
|
|
27
50
|
* - Internally uses `new URL` to do path processing but shouldn't ever reveal that fact.
|
|
28
51
|
* - Returned paths are cleaned with `cleanPath()` so runs of slashes and trailing slashes are removed.
|
|
29
52
|
*
|
|
30
|
-
* @param
|
|
53
|
+
* @param path Absolute path e.g. `/a/b/c`, relative path e.g. `./a` or `b` or `../c`, URL string e.g. `http://shax.com/a/b/c`, or `URL` instance.
|
|
31
54
|
* @param base Absolute path used for resolving relative paths in `possible`
|
|
32
55
|
* @return Absolute path with a leading trailing slash, e.g. `/a/c/b`
|
|
33
56
|
*/
|
|
34
|
-
export declare function
|
|
57
|
+
export declare function requireAbsolutePath(path: AbsolutePath | RelativePath, base?: AbsolutePath, caller?: AnyCaller): AbsolutePath;
|
|
58
|
+
/**
|
|
59
|
+
* Match and strip a base path prefix from a path using segment-aware pathname rules.
|
|
60
|
+
* - Both inputs must be absolute paths that begin with `/`.
|
|
61
|
+
* - Returns `/` when the paths are an exact match.
|
|
62
|
+
*/
|
|
63
|
+
export declare function matchPathPrefix(target: AbsolutePath | RelativePath, base: AbsolutePath, caller?: AnyCaller): AbsolutePath | undefined;
|
|
35
64
|
/** Is a target path active? */
|
|
36
65
|
export declare function isPathActive(target: AbsolutePath, current: AbsolutePath): boolean;
|
|
37
66
|
/** Is a target path proud (i.e. is the current path, or is a child of the current path)? */
|
package/util/path.js
CHANGED
|
@@ -1,49 +1,91 @@
|
|
|
1
1
|
import { RequiredError } from "../error/RequiredError.js";
|
|
2
|
-
|
|
2
|
+
const SPLIT_SEGMENTS = /[\\/]+/g;
|
|
3
|
+
const ALL_DOTS = /^\.+$/;
|
|
4
|
+
const UNSAFE_CHARS = /[^A-Za-z0-9_.-]+/g;
|
|
5
|
+
/**
|
|
6
|
+
* Is a string a valid path segment, e.g. `abc` or `myFile.pdf` or `.myFile`
|
|
7
|
+
* - Disallows `""` empty string.
|
|
8
|
+
* - Disallows runs of `.` as the only contents (e.g. `.`, `..` or `...`).
|
|
9
|
+
* - Disallows characters outside `A-Za-z0-9_.-`
|
|
10
|
+
*/
|
|
11
|
+
export function isPathSegment(segment) {
|
|
12
|
+
return segment.length > 0 && !ALL_DOTS.test(segment) && !UNSAFE_CHARS.test(segment);
|
|
13
|
+
}
|
|
14
|
+
/** Get a simple path segment after filtering out invalid characters. */
|
|
15
|
+
export function getPathSegment(input) {
|
|
16
|
+
const output = input.replace(UNSAFE_CHARS, "");
|
|
17
|
+
return ALL_DOTS.test(output) ? "" : output;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Is a string a simple path, e.g. `a/b/c`
|
|
21
|
+
* - Separated by `/` slashes.
|
|
22
|
+
* - No leading/trailing slashes.
|
|
23
|
+
*/
|
|
24
|
+
export function isPath(path) {
|
|
25
|
+
return !path.length || path.split("/").every(isPathSegment);
|
|
26
|
+
}
|
|
3
27
|
/** Is a string path an absolute path? */
|
|
4
28
|
export function isAbsolutePath(path) {
|
|
5
|
-
return path.startsWith("/");
|
|
29
|
+
return path.startsWith("/") && isPath(path.slice(1));
|
|
6
30
|
}
|
|
7
|
-
/** Is a string path an
|
|
31
|
+
/** Is a string path an relative path? */
|
|
8
32
|
export function isRelativePath(path) {
|
|
9
|
-
return path.startsWith("./")
|
|
33
|
+
return path === "." || (path.startsWith("./") && isPath(path.slice(2)));
|
|
10
34
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
35
|
+
/** Split a path into simple path segments, e.g. `a/b/c` -> ["a", "b", "c"] */
|
|
36
|
+
export function splitPath(path) {
|
|
37
|
+
return path?.split(SPLIT_SEGMENTS).map(getPathSegment).filter(Boolean) ?? [];
|
|
38
|
+
}
|
|
39
|
+
/** Join path segments into an absolute path, e.g. ["a", "b", "c"] -> `/a/b/c` */
|
|
40
|
+
export function joinPath(...segments) {
|
|
41
|
+
return `/${segments.join("/")}`;
|
|
15
42
|
}
|
|
16
43
|
/**
|
|
17
44
|
* Resolve a relative or absolute path and return the absolute path, or `undefined` if not a valid path.
|
|
18
|
-
* - Uses `new URL` to do path processing, so URL strings can also be resolved.
|
|
19
45
|
* - Returned paths are cleaned with `cleanPath()` so runs of slashes and trailing slashes are removed.
|
|
20
46
|
*
|
|
21
|
-
* @param
|
|
47
|
+
* @param path Absolute path e.g. `/a/b/c`, relative path e.g. `./a` or `b` or `../c`, URL string e.g. `http://shax.com/a/b/c`, or `URL` instance.
|
|
22
48
|
* @param base Absolute path used for resolving relative paths in `possible`
|
|
23
|
-
* @return Absolute path with a leading trailing slash, e.g. `/a/c/b`
|
|
49
|
+
* @return Absolute path with a leading slash but no trailing slash, e.g. `/a/c/b`
|
|
24
50
|
*/
|
|
25
|
-
export function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
51
|
+
export function getAbsolutePath(path, base = "/") {
|
|
52
|
+
if (!path)
|
|
53
|
+
return;
|
|
54
|
+
if (path === "/")
|
|
55
|
+
return "/"; // Hot passthrough.
|
|
56
|
+
if (path.startsWith("/"))
|
|
57
|
+
return joinPath(...splitPath(path));
|
|
58
|
+
return joinPath(...splitPath(base), ...splitPath(path));
|
|
32
59
|
}
|
|
33
60
|
/**
|
|
34
61
|
* Resolve a relative or absolute path and return the path, or throw `RequiredError` if not a valid path.
|
|
35
62
|
* - Internally uses `new URL` to do path processing but shouldn't ever reveal that fact.
|
|
36
63
|
* - Returned paths are cleaned with `cleanPath()` so runs of slashes and trailing slashes are removed.
|
|
37
64
|
*
|
|
38
|
-
* @param
|
|
65
|
+
* @param path Absolute path e.g. `/a/b/c`, relative path e.g. `./a` or `b` or `../c`, URL string e.g. `http://shax.com/a/b/c`, or `URL` instance.
|
|
39
66
|
* @param base Absolute path used for resolving relative paths in `possible`
|
|
40
67
|
* @return Absolute path with a leading trailing slash, e.g. `/a/c/b`
|
|
41
68
|
*/
|
|
42
|
-
export function
|
|
43
|
-
const
|
|
44
|
-
if (!
|
|
45
|
-
throw new RequiredError("Invalid path", { received:
|
|
46
|
-
return
|
|
69
|
+
export function requireAbsolutePath(path, base, caller = requireAbsolutePath) {
|
|
70
|
+
const output = getAbsolutePath(path, base);
|
|
71
|
+
if (!output)
|
|
72
|
+
throw new RequiredError("Invalid path", { received: path, caller });
|
|
73
|
+
return output;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Match and strip a base path prefix from a path using segment-aware pathname rules.
|
|
77
|
+
* - Both inputs must be absolute paths that begin with `/`.
|
|
78
|
+
* - Returns `/` when the paths are an exact match.
|
|
79
|
+
*/
|
|
80
|
+
export function matchPathPrefix(target, base, caller = matchPathPrefix) {
|
|
81
|
+
const basePath = requireAbsolutePath(base, undefined, caller);
|
|
82
|
+
const targetPath = requireAbsolutePath(target, basePath, caller);
|
|
83
|
+
if (basePath === "/")
|
|
84
|
+
return targetPath;
|
|
85
|
+
if (targetPath === basePath)
|
|
86
|
+
return "/";
|
|
87
|
+
if (targetPath.startsWith(`${basePath}/`))
|
|
88
|
+
return targetPath.slice(basePath.length);
|
|
47
89
|
}
|
|
48
90
|
/** Is a target path active? */
|
|
49
91
|
export function isPathActive(target, current) {
|
package/util/url.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { AnyCaller } from "./function.js";
|
|
2
|
-
import {
|
|
3
|
-
import type { AbsolutePath
|
|
2
|
+
import type { Nullish } from "./null.js";
|
|
3
|
+
import type { AbsolutePath } from "./path.js";
|
|
4
4
|
import type { URI } from "./uri.js";
|
|
5
|
+
type BuiltinURL = globalThis.URL;
|
|
6
|
+
declare const BuiltinURL: {
|
|
7
|
+
new (url: string | globalThis.URL, base?: string | globalThis.URL): globalThis.URL;
|
|
8
|
+
prototype: globalThis.URL;
|
|
9
|
+
canParse(url: string | globalThis.URL, base?: string | globalThis.URL): boolean;
|
|
10
|
+
createObjectURL(obj: Blob | MediaSource): string;
|
|
11
|
+
parse(url: string | globalThis.URL, base?: string | globalThis.URL): globalThis.URL | null;
|
|
12
|
+
revokeObjectURL(url: string): void;
|
|
13
|
+
};
|
|
5
14
|
/**
|
|
6
15
|
* A URL string has a protocol and a `//`.
|
|
7
16
|
* - The `//` at the start of a URL indicates that it has a hierarchical path component, so this makes it a URL.
|
|
@@ -25,9 +34,9 @@ export type URLString = `${string}://${string}`;
|
|
|
25
34
|
* - You can tell the difference because a URL will have a non-empty `host` property, whereas URIs will never have a `host` (it will be `""` empty string).
|
|
26
35
|
*/
|
|
27
36
|
export interface URL extends URI {
|
|
28
|
-
href: URLString;
|
|
29
|
-
origin: URLString;
|
|
30
|
-
pathname:
|
|
37
|
+
readonly href: URLString;
|
|
38
|
+
readonly origin: URLString;
|
|
39
|
+
readonly pathname: `/` | `/${string}`;
|
|
31
40
|
}
|
|
32
41
|
/**
|
|
33
42
|
* Construct a correctly-typed `URL` object.
|
|
@@ -38,11 +47,11 @@ export interface URL extends URI {
|
|
|
38
47
|
*/
|
|
39
48
|
export interface URLConstructor {
|
|
40
49
|
new (input: URLString | URL, base?: URLString | URL): URL;
|
|
41
|
-
new (input: URLString | URL |
|
|
50
|
+
new (input: URLString | URL | string, base: URLString | URL): URL;
|
|
42
51
|
}
|
|
43
52
|
export declare const URL: URLConstructor;
|
|
44
53
|
/** Values that can be converted to a URL instance. */
|
|
45
|
-
export type PossibleURL = string |
|
|
54
|
+
export type PossibleURL = string | BuiltinURL;
|
|
46
55
|
/**
|
|
47
56
|
* Is an unknown value a URL object?
|
|
48
57
|
* - Must be a `URL` instance and its origin must start with `scheme://`
|
|
@@ -50,18 +59,34 @@ export type PossibleURL = string | globalThis.URL;
|
|
|
50
59
|
export declare function isURL(value: unknown): value is URL;
|
|
51
60
|
/** Assert that an unknown value is a URL object. */
|
|
52
61
|
export declare function assertURL(value: unknown, caller?: AnyCaller): asserts value is URL;
|
|
53
|
-
/** Convert a possible URL to a URL, or return `undefined` if conversion fails. */
|
|
54
|
-
export declare function getURL(possible: Nullish<PossibleURL>, base?: PossibleURL | undefined): URL | undefined;
|
|
55
|
-
/** Convert a possible URL to a URL, or throw `RequiredError` if conversion fails. */
|
|
56
|
-
export declare function requireURL(possible: PossibleURL, base?: PossibleURL | undefined, caller?: AnyCaller): URL;
|
|
57
62
|
/**
|
|
58
|
-
* Convert a possible URL to a
|
|
59
|
-
* -
|
|
60
|
-
* -
|
|
63
|
+
* Convert a possible URL to a URL, or return `undefined` if conversion fails.
|
|
64
|
+
* - Slightly subverts the builtin relative URL parsing by appending a '/' trailing slash to `base`
|
|
65
|
+
* - This means if the current URL has a path segment like `/a/b/c` then a relative path like `d/e/f` will be parsed relative to `c` (default behaviour is to strip segments without a trailing slash, which is usually unexpected).
|
|
61
66
|
*/
|
|
62
|
-
export declare function
|
|
67
|
+
export declare function getURL(possible: Nullish<PossibleURL>, base?: PossibleURL): URL | undefined;
|
|
68
|
+
/** Convert a possible URL to a URL, or throw `RequiredError` if conversion fails. */
|
|
69
|
+
export declare function requireURL(possible: PossibleURL, base?: PossibleURL, caller?: AnyCaller): URL;
|
|
63
70
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
71
|
+
* Resolve and match a target URL/path against a base URL and return the remaining path.
|
|
72
|
+
*
|
|
73
|
+
* - Need to be valid _URLs_ not just _URIs_, i.e. needs to have `protocol://` at the start.
|
|
74
|
+
* - Origins need to match, i.e. `http://localhost` !== `http://localhost:4020`
|
|
75
|
+
* - Relative targets are resolved against the normalized base URL.
|
|
76
|
+
*
|
|
77
|
+
* @param target URL to match against `base` — if this is a relative path it will be resolved against `base`
|
|
78
|
+
*
|
|
79
|
+
* @returns Absolute path starting with `/`, or `undefined` for origin mismatches or non-matching paths.
|
|
66
80
|
*/
|
|
67
|
-
export declare function
|
|
81
|
+
export declare function matchURLPrefix(target: PossibleURL, base: PossibleURL, caller?: AnyCaller): AbsolutePath | undefined;
|
|
82
|
+
/** BaseURL is a URL with a guaranteed trailing slash on pathname. */
|
|
83
|
+
export interface BaseURL extends URL {
|
|
84
|
+
readonly pathname: `/` | `/${string}/`;
|
|
85
|
+
}
|
|
86
|
+
/** Is an unknown value a valid Base URL. */
|
|
87
|
+
export declare function isBaseURL(value: PossibleURL): value is BaseURL;
|
|
88
|
+
/** Get a Base URL. */
|
|
89
|
+
export declare function getBaseURL(input: Nullish<PossibleURL>): BaseURL | undefined;
|
|
90
|
+
/** Require a Base URL. */
|
|
91
|
+
export declare function requireBaseURL(value: PossibleURL, caller: AnyCaller): BaseURL;
|
|
92
|
+
export {};
|
package/util/url.js
CHANGED
|
@@ -1,41 +1,46 @@
|
|
|
1
1
|
import { RequiredError } from "../error/RequiredError.js";
|
|
2
|
-
|
|
3
|
-
export const URL =
|
|
2
|
+
const BuiltinURL = globalThis.URL;
|
|
3
|
+
export const URL = BuiltinURL;
|
|
4
4
|
/**
|
|
5
5
|
* Is an unknown value a URL object?
|
|
6
6
|
* - Must be a `URL` instance and its origin must start with `scheme://`
|
|
7
7
|
*/
|
|
8
8
|
export function isURL(value) {
|
|
9
|
-
return value instanceof
|
|
9
|
+
return value instanceof BuiltinURL && _isURL(value);
|
|
10
10
|
}
|
|
11
|
-
function
|
|
12
|
-
return
|
|
11
|
+
function _isURL(uri) {
|
|
12
|
+
return uri.href.startsWith(`${uri.protocol}//`);
|
|
13
13
|
}
|
|
14
14
|
/** Assert that an unknown value is a URL object. */
|
|
15
15
|
export function assertURL(value, caller = assertURL) {
|
|
16
16
|
if (!isURL(value))
|
|
17
17
|
throw new RequiredError("Invalid URL", { received: value, caller });
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Convert a possible URL to a URL, or return `undefined` if conversion fails.
|
|
21
|
+
* - Slightly subverts the builtin relative URL parsing by appending a '/' trailing slash to `base`
|
|
22
|
+
* - This means if the current URL has a path segment like `/a/b/c` then a relative path like `d/e/f` will be parsed relative to `c` (default behaviour is to strip segments without a trailing slash, which is usually unexpected).
|
|
23
|
+
*/
|
|
24
|
+
export function getURL(possible, base) {
|
|
25
|
+
if (!possible)
|
|
26
|
+
return;
|
|
27
|
+
const uri = _getBuiltinURL(possible, base);
|
|
28
|
+
if (uri && _isURL(uri))
|
|
29
|
+
return uri;
|
|
30
|
+
}
|
|
31
|
+
function _getBuiltinURL(possible, base) {
|
|
32
|
+
if (possible instanceof BuiltinURL)
|
|
33
|
+
return possible;
|
|
34
|
+
try {
|
|
35
|
+
// We need a base URL to potentially parse this URL against.
|
|
36
|
+
// Use the document base (if set) as the default URL.
|
|
37
|
+
const baseURL = getBaseURL(base ?? (typeof document === "object" ? document.baseURI : undefined));
|
|
38
|
+
return new BuiltinURL(possible, baseURL);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
//
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
|
-
const _BASE = typeof document === "object" ? document.baseURI : undefined;
|
|
39
44
|
/** Convert a possible URL to a URL, or throw `RequiredError` if conversion fails. */
|
|
40
45
|
export function requireURL(possible, base, caller = requireURL) {
|
|
41
46
|
const url = getURL(possible, base);
|
|
@@ -43,24 +48,54 @@ export function requireURL(possible, base, caller = requireURL) {
|
|
|
43
48
|
return url;
|
|
44
49
|
}
|
|
45
50
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* -
|
|
51
|
+
* Resolve and match a target URL/path against a base URL and return the remaining path.
|
|
52
|
+
*
|
|
53
|
+
* - Need to be valid _URLs_ not just _URIs_, i.e. needs to have `protocol://` at the start.
|
|
54
|
+
* - Origins need to match, i.e. `http://localhost` !== `http://localhost:4020`
|
|
55
|
+
* - Relative targets are resolved against the normalized base URL.
|
|
56
|
+
*
|
|
57
|
+
* @param target URL to match against `base` — if this is a relative path it will be resolved against `base`
|
|
58
|
+
*
|
|
59
|
+
* @returns Absolute path starting with `/`, or `undefined` for origin mismatches or non-matching paths.
|
|
49
60
|
*/
|
|
50
|
-
export function
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
export function matchURLPrefix(target, base, caller = matchURLPrefix) {
|
|
62
|
+
const baseURL = requireBaseURL(base, caller);
|
|
63
|
+
const targetURL = requireURL(target, baseURL, caller);
|
|
64
|
+
if (targetURL.origin !== baseURL.origin)
|
|
65
|
+
return;
|
|
66
|
+
const basePath = baseURL.pathname;
|
|
67
|
+
const targetPath = targetURL.pathname;
|
|
68
|
+
if (basePath === "/")
|
|
69
|
+
return targetPath;
|
|
70
|
+
if (targetPath === basePath.slice(0, -1))
|
|
71
|
+
return "/"; // e.g. `/abc` and `/abc/`
|
|
72
|
+
if (targetPath.startsWith(basePath))
|
|
73
|
+
return targetPath.slice(basePath.length - 1);
|
|
54
74
|
}
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
/** Is an unknown value a valid Base URL. */
|
|
76
|
+
export function isBaseURL(value) {
|
|
77
|
+
return isURL(value) && _isBaseURL(value);
|
|
57
78
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
*/
|
|
62
|
-
export function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
79
|
+
function _isBaseURL(uri) {
|
|
80
|
+
return uri.pathname.endsWith("/");
|
|
81
|
+
}
|
|
82
|
+
/** Get a Base URL. */
|
|
83
|
+
export function getBaseURL(input) {
|
|
84
|
+
if (!input)
|
|
85
|
+
return;
|
|
86
|
+
const uri = _getBuiltinURL(input, undefined);
|
|
87
|
+
if (!uri || !_isURL(uri))
|
|
88
|
+
return;
|
|
89
|
+
if (_isBaseURL(uri))
|
|
90
|
+
return uri;
|
|
91
|
+
const base = typeof input === "string" ? uri : new BuiltinURL(uri);
|
|
92
|
+
base.pathname = `${uri.pathname}/`; // Add a trailing slash.
|
|
93
|
+
return base;
|
|
94
|
+
}
|
|
95
|
+
/** Require a Base URL. */
|
|
96
|
+
export function requireBaseURL(value, caller) {
|
|
97
|
+
const url = getBaseURL(value);
|
|
98
|
+
if (!url)
|
|
99
|
+
throw new RequiredError("Invalid base URL", { received: value, caller });
|
|
100
|
+
return url;
|
|
66
101
|
}
|