@forgebook/sveltekit 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +152 -2
- package/dist/src/app/env.d.ts +8 -0
- package/dist/src/app/env.js +15 -0
- package/dist/src/app/forms.d.ts +68 -0
- package/dist/src/app/forms.js +125 -0
- package/dist/src/app/manifest.d.ts +21 -0
- package/dist/src/app/manifest.js +17 -0
- package/dist/src/app/navigation.d.ts +97 -0
- package/dist/src/app/navigation.js +147 -0
- package/dist/src/app/paths.d.ts +32 -0
- package/dist/src/app/paths.js +56 -0
- package/dist/src/app/state.d.ts +27 -0
- package/dist/src/app/state.js +60 -0
- package/dist/src/app/stores.d.ts +31 -0
- package/dist/src/app/stores.js +36 -0
- package/dist/src/appSettings.d.ts +44 -0
- package/dist/src/appSettings.js +55 -0
- package/dist/src/kitParameters.d.ts +272 -0
- package/dist/src/kitParameters.js +291 -0
- package/dist/src/kitRenderer.d.ts +3 -0
- package/dist/src/kitRenderer.js +276 -0
- package/dist/src/kitStore.d.ts +90 -0
- package/dist/src/kitStore.js +301 -0
- package/dist/src/loadFetch.d.ts +33 -0
- package/dist/src/loadFetch.js +171 -0
- package/dist/src/mountFrame.d.ts +14 -0
- package/dist/src/mountFrame.js +15 -0
- package/dist/src/routeIds.d.ts +44 -0
- package/dist/src/routeIds.js +150 -0
- package/dist/src/routeOrder.d.ts +18 -0
- package/dist/src/routeOrder.js +187 -0
- package/dist/src/stories.d.ts +48 -0
- package/dist/src/stories.js +11 -0
- package/dist/src/storyCode.d.ts +3 -0
- package/dist/src/storyCode.js +9 -0
- package/dist/src/storyPage.d.ts +35 -0
- package/dist/src/storyPage.js +19 -0
- package/dist/src/vite/envModules.d.ts +67 -0
- package/dist/src/vite/envModules.js +181 -0
- package/dist/src/vite/extractDocs.d.ts +14 -0
- package/dist/src/vite/extractDocs.js +20 -0
- package/dist/src/vite/kitPlugin.d.ts +28 -0
- package/dist/src/vite/kitPlugin.js +241 -0
- package/dist/src/vite/kitProject.d.ts +27 -0
- package/dist/src/vite/kitProject.js +176 -0
- package/dist/src/vite/projectFiles.d.ts +14 -0
- package/dist/src/vite/projectFiles.js +70 -0
- package/dist/src/vite/sveltekit.d.ts +26 -0
- package/dist/src/vite/sveltekit.js +39 -0
- package/package.json +120 -3
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `fetch` a story's `load` gets: in a page, the frame's own, which the catalogue's mocks answer; on a server, the
|
|
3
|
+
* story's mock handlers run in place of the network for an address on the story's page's origin.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* SvelteKit's `fetch` on a server reads a path from the page's address, so a server render (the generated server
|
|
7
|
+
* tests) would ask `http://localhost` for it: whatever answers on that machine, or nothing, would decide what the page
|
|
8
|
+
* draws. A server render never reaches the network for the page's own origin: the story's handlers
|
|
9
|
+
* (`parameters.msw.handlers`, the ones the frame's worker serves) answer it through MSW's `getResponse`, a plain call
|
|
10
|
+
* with no interception, so concurrent renders never see each other's handlers. A request no handler answers, or one
|
|
11
|
+
* a handler sends on to the network (`passthrough()`), fails the render with what to give instead. An address on
|
|
12
|
+
* another origin is fetched as it is. A handler's `HttpResponse.error()` rejects the fetch and its redirect is followed,
|
|
13
|
+
* as in the frame.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
/** A load's `fetch`. */
|
|
18
|
+
export type LoadFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
19
|
+
/**
|
|
20
|
+
* The fetch a load gets.
|
|
21
|
+
*
|
|
22
|
+
* @param pageUrl - the address of the page being loaded, which a path is read from.
|
|
23
|
+
* @param handlers - the story's mock handlers.
|
|
24
|
+
* @param onUnanswered - told of a request on the page's origin that no handler answered on a server, so the render
|
|
25
|
+
* fails even when the load catches the error and draws something else.
|
|
26
|
+
*/
|
|
27
|
+
export declare const loadFetch: (pageUrl: URL, handlers: readonly unknown[], onUnanswered: (error: Error) => void) => LoadFetch;
|
|
28
|
+
/**
|
|
29
|
+
* The handlers a story's parameters declare, flat: a list, or a record of lists as MSW's Storybook addon writes them.
|
|
30
|
+
*
|
|
31
|
+
* @param parameters - the story's parameters.
|
|
32
|
+
*/
|
|
33
|
+
export declare const mockHandlersOf: (parameters: Readonly<Record<string, unknown>>) => readonly unknown[];
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `fetch` a story's `load` gets: in a page, the frame's own, which the catalogue's mocks answer; on a server, the
|
|
3
|
+
* story's mock handlers run in place of the network for an address on the story's page's origin.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* SvelteKit's `fetch` on a server reads a path from the page's address, so a server render (the generated server
|
|
7
|
+
* tests) would ask `http://localhost` for it: whatever answers on that machine, or nothing, would decide what the page
|
|
8
|
+
* draws. A server render never reaches the network for the page's own origin: the story's handlers
|
|
9
|
+
* (`parameters.msw.handlers`, the ones the frame's worker serves) answer it through MSW's `getResponse`, a plain call
|
|
10
|
+
* with no interception, so concurrent renders never see each other's handlers. A request no handler answers, or one
|
|
11
|
+
* a handler sends on to the network (`passthrough()`), fails the render with what to give instead. An address on
|
|
12
|
+
* another origin is fetched as it is. A handler's `HttpResponse.error()` rejects the fetch and its redirect is followed,
|
|
13
|
+
* as in the frame.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
18
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
19
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
20
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return path;
|
|
24
|
+
};
|
|
25
|
+
/** The module MSW's request handling is imported from, by the project that installs it. */
|
|
26
|
+
const MSW_MODULE = "msw";
|
|
27
|
+
/**
|
|
28
|
+
* The fetch a load gets.
|
|
29
|
+
*
|
|
30
|
+
* @param pageUrl - the address of the page being loaded, which a path is read from.
|
|
31
|
+
* @param handlers - the story's mock handlers.
|
|
32
|
+
* @param onUnanswered - told of a request on the page's origin that no handler answered on a server, so the render
|
|
33
|
+
* fails even when the load catches the error and draws something else.
|
|
34
|
+
*/
|
|
35
|
+
export const loadFetch = (pageUrl, handlers, onUnanswered) => {
|
|
36
|
+
// in a page, a path is read from the frame's address, where the catalogue's mocks answer it
|
|
37
|
+
if (typeof location !== "undefined")
|
|
38
|
+
return (input, init) => globalThis.fetch(input, init);
|
|
39
|
+
restoreHandlers(handlers);
|
|
40
|
+
const send = async (request, redirects) => {
|
|
41
|
+
if (new URL(request.url).origin !== pageUrl.origin)
|
|
42
|
+
return globalThis.fetch(request);
|
|
43
|
+
// kept for a redirect that sends the request again, body included, after a handler read it
|
|
44
|
+
const kept = request.clone();
|
|
45
|
+
const answer = await mockAnswer(handlers, request, pageUrl.origin);
|
|
46
|
+
if (answer === undefined) {
|
|
47
|
+
const error = unansweredError(request);
|
|
48
|
+
onUnanswered(error);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
// MSW's mocked network error: `fetch` rejects, as it does in the frame
|
|
52
|
+
if (answer.type === "error")
|
|
53
|
+
throw new TypeError("Failed to fetch", { cause: answer });
|
|
54
|
+
const next = redirectOf(answer, kept);
|
|
55
|
+
if (next === undefined)
|
|
56
|
+
return answer;
|
|
57
|
+
if (redirects >= MOST_REDIRECTS)
|
|
58
|
+
throw new TypeError(`Too many redirects, the last to ${next.url}`);
|
|
59
|
+
return send(next, redirects + 1);
|
|
60
|
+
};
|
|
61
|
+
return (input, init) => send(new Request(typeof input === "string" ? new URL(input, pageUrl) : input, init), 0);
|
|
62
|
+
};
|
|
63
|
+
/** How many redirects a fetch follows, as browsers and Node's `fetch` do. */
|
|
64
|
+
const MOST_REDIRECTS = 20;
|
|
65
|
+
/**
|
|
66
|
+
* The request a mocked redirect leads to, as `fetch` follows it: the same method and body for a 307 or a 308, a GET
|
|
67
|
+
* for a 303 and for a POST's 301 or 302; none when the answer is no redirect or the request does not follow one.
|
|
68
|
+
*
|
|
69
|
+
* @param answer - a handler's answer.
|
|
70
|
+
* @param request - the request it answered, its body unread.
|
|
71
|
+
* @throws a `TypeError` for a redirect a request with `redirect: "error"` gets, as `fetch` does.
|
|
72
|
+
*/
|
|
73
|
+
const redirectOf = (answer, request) => {
|
|
74
|
+
const location = answer.headers.get("location");
|
|
75
|
+
if (![301, 302, 303, 307, 308].includes(answer.status) || location === null)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (request.redirect === "manual")
|
|
78
|
+
return undefined;
|
|
79
|
+
if (request.redirect === "error")
|
|
80
|
+
throw new TypeError(`Redirected to ${location}, which the request refuses`);
|
|
81
|
+
const target = new URL(location, request.url);
|
|
82
|
+
const isGet = answer.status === 303 || (request.method === "POST" && answer.status !== 307 && answer.status !== 308);
|
|
83
|
+
if (!isGet)
|
|
84
|
+
return new Request(target, request);
|
|
85
|
+
return new Request(target, {
|
|
86
|
+
method: request.method === "HEAD" ? "HEAD" : "GET",
|
|
87
|
+
headers: request.headers
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* The handlers a story's parameters declare, flat: a list, or a record of lists as MSW's Storybook addon writes them.
|
|
92
|
+
*
|
|
93
|
+
* @param parameters - the story's parameters.
|
|
94
|
+
*/
|
|
95
|
+
export const mockHandlersOf = (parameters) => {
|
|
96
|
+
const msw = parameters["msw"];
|
|
97
|
+
const handlers = isRecord(msw) ? msw["handlers"] : undefined;
|
|
98
|
+
if (Array.isArray(handlers))
|
|
99
|
+
return handlers;
|
|
100
|
+
return isRecord(handlers) ? Object.values(handlers).flat() : [];
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Makes a story's `{ once: true }` handlers answer again, as MSW's `restoreHandlers` does: each page load on a server is
|
|
104
|
+
* a page of its own, as each frame is, while the handler objects outlive it in the test process.
|
|
105
|
+
*
|
|
106
|
+
* @param handlers - the story's handlers.
|
|
107
|
+
*/
|
|
108
|
+
const restoreHandlers = (handlers) => {
|
|
109
|
+
for (const handler of handlers)
|
|
110
|
+
if (isRecord(handler) && typeof handler["isUsed"] === "boolean")
|
|
111
|
+
Reflect.set(handler, "isUsed", false);
|
|
112
|
+
};
|
|
113
|
+
/** Whether a value is an object with named fields. */
|
|
114
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
115
|
+
/**
|
|
116
|
+
* What the story's handlers answer: none when they do not, or when the one that does sends the request on to the
|
|
117
|
+
* network, which a server render never reaches.
|
|
118
|
+
*
|
|
119
|
+
* @param handlers - the story's handlers.
|
|
120
|
+
* @param request - the request.
|
|
121
|
+
* @param origin - the page's origin, which a handler's path (`/api/users`) is read from, as the frame reads it.
|
|
122
|
+
*/
|
|
123
|
+
const mockAnswer = async (handlers, request, origin) => {
|
|
124
|
+
if (handlers.length === 0)
|
|
125
|
+
return undefined;
|
|
126
|
+
const { getResponse } = await mswCore();
|
|
127
|
+
const answer = await getResponse([...handlers], request, { baseUrl: origin });
|
|
128
|
+
return answer === undefined || isPassthrough(answer) ? undefined : answer;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Whether an answer is MSW's `passthrough()`: MSW marks it with this status and header, which its own
|
|
132
|
+
* `isPassthroughResponse` reads (exported from MSW 3 only).
|
|
133
|
+
*
|
|
134
|
+
* @param answer - a handler's answer.
|
|
135
|
+
*/
|
|
136
|
+
const isPassthrough = (answer) => answer.status === 302 && answer.headers.get("x-msw-intention") === "passthrough";
|
|
137
|
+
/**
|
|
138
|
+
* Whether a module is MSW's, as much as a server render uses.
|
|
139
|
+
*
|
|
140
|
+
* @param module - the module, as `import()` gave it.
|
|
141
|
+
*/
|
|
142
|
+
const isMswCore = (module) => typeof module === "object" &&
|
|
143
|
+
module !== null &&
|
|
144
|
+
"getResponse" in module &&
|
|
145
|
+
typeof module.getResponse === "function";
|
|
146
|
+
/**
|
|
147
|
+
* Loads MSW, which only a story with handlers needs on a server.
|
|
148
|
+
*
|
|
149
|
+
* @throws when MSW is not installed, or is older than its `getResponse`.
|
|
150
|
+
*/
|
|
151
|
+
const mswCore = async () => {
|
|
152
|
+
let module;
|
|
153
|
+
try {
|
|
154
|
+
// named by a variable, so a frame's build never resolves MSW: the frame fetches through the page
|
|
155
|
+
module = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ MSW_MODULE));
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
throw new Error("Answering a load's fetch with the story's mocks on a server needs MSW: npm install --save-dev msw", { cause: error });
|
|
159
|
+
}
|
|
160
|
+
if (!isMswCore(module))
|
|
161
|
+
throw new Error("The installed MSW has no getResponse: MSW 2.11 or later is needed");
|
|
162
|
+
return module;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* The error of a request on the page's origin that no handler answered: what was asked, and what to give.
|
|
166
|
+
*
|
|
167
|
+
* @param request - the request.
|
|
168
|
+
*/
|
|
169
|
+
const unansweredError = (request) => new Error(`A load fetched ${request.method} ${request.url} on a server, and no mock of the story answers it: a server render ` +
|
|
170
|
+
"never reaches the network. Answer it with a handler in parameters.msw.handlers, or give the page's data " +
|
|
171
|
+
"with parameters.sveltekit.serverData or parameters.sveltekit.page.data.");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frame page of a SvelteKit catalogue: Forgebook's frame, drawing its stories with Svelte and each story's
|
|
3
|
+
* SvelteKit page. Called by the page the plugin generates from the catalogue's configuration.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { type FrameConfig } from "forgebook/frame";
|
|
8
|
+
/**
|
|
9
|
+
* Shows the story the address names in the given element, drawn with Svelte for a SvelteKit project.
|
|
10
|
+
*
|
|
11
|
+
* @param container - the element the story renders into.
|
|
12
|
+
* @param config - the sources and the languages, as the shell's configuration gives them.
|
|
13
|
+
*/
|
|
14
|
+
export declare const mountFrame: (container: HTMLElement, config: Omit<FrameConfig, "renderer">) => Promise<void>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frame page of a SvelteKit catalogue: Forgebook's frame, drawing its stories with Svelte and each story's
|
|
3
|
+
* SvelteKit page. Called by the page the plugin generates from the catalogue's configuration.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { mountFrame as mountForgebookFrame } from "forgebook/frame";
|
|
8
|
+
import { kitRenderer } from "./kitRenderer.js";
|
|
9
|
+
/**
|
|
10
|
+
* Shows the story the address names in the given element, drawn with Svelte for a SvelteKit project.
|
|
11
|
+
*
|
|
12
|
+
* @param container - the element the story renders into.
|
|
13
|
+
* @param config - the sources and the languages, as the shell's configuration gives them.
|
|
14
|
+
*/
|
|
15
|
+
export const mountFrame = (container, config) => mountForgebookFrame(container, { ...config, renderer: kitRenderer });
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvelteKit's route ids, read as SvelteKit reads them: which route a path matches and with which params, and the path a
|
|
3
|
+
* route id gives with its params filled in.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Pure: ids and paths in, matches out, so it is tested without a page. The ids are the project's own, listed from its
|
|
7
|
+
* `src/routes` by the Vite plugin, so a story's links and `goto` calls land on the route SvelteKit would choose:
|
|
8
|
+
* `/blog/[slug]` for `/blog/hello`, with `{ slug: "hello" }`. A param's matcher (`[id=integer]`) is not run, since the
|
|
9
|
+
* catalogue does not load the project's matchers: any value matches it.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
/** A route's params, by name: SvelteKit gives each as one text, a rest param's slashes included. */
|
|
14
|
+
export type RouteParams = Readonly<Record<string, string>>;
|
|
15
|
+
/** A route a path matched: its id and its params. */
|
|
16
|
+
export type RouteMatch = {
|
|
17
|
+
/** The route's id (`/blog/[slug]`). */
|
|
18
|
+
readonly id: string;
|
|
19
|
+
/** The params read from the path. */
|
|
20
|
+
readonly params: RouteParams;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The route a path matches among a project's routes, and its params.
|
|
24
|
+
*
|
|
25
|
+
* @param routes - the project's route ids.
|
|
26
|
+
* @param pathname - the path, without the base path (`/blog/hello`).
|
|
27
|
+
* @returns the match, or none when no route matches.
|
|
28
|
+
*/
|
|
29
|
+
export declare const matchRoute: (routes: readonly string[], pathname: string) => RouteMatch | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* The path a route id gives with its params filled in, as `resolve` from `$app/paths` writes it: the layout groups
|
|
32
|
+
* left out, an optional or rest param with no value dropped with its slash.
|
|
33
|
+
*
|
|
34
|
+
* @param id - the route's id (`/blog/[slug]`).
|
|
35
|
+
* @param params - the params, by name.
|
|
36
|
+
* @returns the path (`/blog/hello`).
|
|
37
|
+
*/
|
|
38
|
+
export declare const filledRoute: (id: string, params: RouteParams) => string;
|
|
39
|
+
/**
|
|
40
|
+
* Whether a text is a route id rather than a path: it names a param or a layout group.
|
|
41
|
+
*
|
|
42
|
+
* @param text - the text.
|
|
43
|
+
*/
|
|
44
|
+
export declare const isRouteId: (text: string) => boolean;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvelteKit's route ids, read as SvelteKit reads them: which route a path matches and with which params, and the path a
|
|
3
|
+
* route id gives with its params filled in.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Pure: ids and paths in, matches out, so it is tested without a page. The ids are the project's own, listed from its
|
|
7
|
+
* `src/routes` by the Vite plugin, so a story's links and `goto` calls land on the route SvelteKit would choose:
|
|
8
|
+
* `/blog/[slug]` for `/blog/hello`, with `{ slug: "hello" }`. A param's matcher (`[id=integer]`) is not run, since the
|
|
9
|
+
* catalogue does not load the project's matchers: any value matches it.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import { byRouteOrder } from "./routeOrder.js";
|
|
14
|
+
/** A param in a route id: `[name]`, `[[name]]`, `[...name]`, each with an optional `=matcher`. */
|
|
15
|
+
const PARAM = /\[(\[)?(\.\.\.)?([A-Za-z_$][\w$]*)(?:=[\w$]+)?\]?\]/g;
|
|
16
|
+
/** A layout group, `(app)`: a folder that is not a segment of the path. */
|
|
17
|
+
const GROUP = /^\(.+\)$/;
|
|
18
|
+
/**
|
|
19
|
+
* The segments of a route id that are in its paths: its layout groups left out.
|
|
20
|
+
*
|
|
21
|
+
* @param id - the route's id (`/(app)/blog/[slug]`).
|
|
22
|
+
*/
|
|
23
|
+
const pathSegments = (id) => id.split("/").filter((segment) => segment !== "" && !GROUP.test(segment));
|
|
24
|
+
/**
|
|
25
|
+
* A segment's parts: its texts and its params.
|
|
26
|
+
*
|
|
27
|
+
* @param segment - a segment of a route id (`[a]-[b]`).
|
|
28
|
+
*/
|
|
29
|
+
const segmentParts = (segment) => {
|
|
30
|
+
const parts = [];
|
|
31
|
+
let index = 0;
|
|
32
|
+
for (const match of segment.matchAll(PARAM)) {
|
|
33
|
+
if (match.index > index)
|
|
34
|
+
parts.push({ kind: "text", text: segment.slice(index, match.index) });
|
|
35
|
+
parts.push({
|
|
36
|
+
kind: "param",
|
|
37
|
+
name: match[3] ?? "",
|
|
38
|
+
isRest: match[2] !== undefined,
|
|
39
|
+
isOptional: match[1] !== undefined
|
|
40
|
+
});
|
|
41
|
+
index = match.index + match[0].length;
|
|
42
|
+
}
|
|
43
|
+
if (index < segment.length)
|
|
44
|
+
parts.push({ kind: "text", text: segment.slice(index) });
|
|
45
|
+
return parts;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* A text written so a regular expression reads it as itself.
|
|
49
|
+
*
|
|
50
|
+
* @param text - the text.
|
|
51
|
+
*/
|
|
52
|
+
const escaped = (text) => text.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
53
|
+
/**
|
|
54
|
+
* The regular expression one segment of a route id matches, its slash included.
|
|
55
|
+
*
|
|
56
|
+
* @param parts - the segment's parts.
|
|
57
|
+
* @param names - where its params' names are added, in order.
|
|
58
|
+
*/
|
|
59
|
+
const segmentSource = (parts, names) => {
|
|
60
|
+
const only = parts.length === 1 ? parts[0] : undefined;
|
|
61
|
+
// a segment that is a rest or optional param alone may be absent from the path, its slash with it
|
|
62
|
+
if (only?.kind === "param" && (only.isRest || only.isOptional)) {
|
|
63
|
+
names.push(only.name);
|
|
64
|
+
return only.isRest ? "(?:/(.*))?" : "(?:/([^/]+))?";
|
|
65
|
+
}
|
|
66
|
+
const written = parts.map((part) => {
|
|
67
|
+
if (part.kind === "text")
|
|
68
|
+
return escaped(part.text);
|
|
69
|
+
names.push(part.name);
|
|
70
|
+
return part.isRest ? "(.*?)" : "([^/]+?)";
|
|
71
|
+
});
|
|
72
|
+
return `/${written.join("")}`;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The regular expression a route id matches paths with, and its params' names in order.
|
|
76
|
+
*
|
|
77
|
+
* @param id - the route's id.
|
|
78
|
+
*/
|
|
79
|
+
const routePattern = (id) => {
|
|
80
|
+
const names = [];
|
|
81
|
+
const source = pathSegments(id)
|
|
82
|
+
.map((segment) => segmentSource(segmentParts(segment), names))
|
|
83
|
+
.join("");
|
|
84
|
+
return { pattern: new RegExp(`^${source}/?$`), names };
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* A path's parts decoded, as SvelteKit decodes a param: a malformed escape left as it is.
|
|
88
|
+
*
|
|
89
|
+
* @param value - the text.
|
|
90
|
+
*/
|
|
91
|
+
const decoded = (value) => {
|
|
92
|
+
try {
|
|
93
|
+
return decodeURIComponent(value);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* The route a path matches among a project's routes, and its params.
|
|
101
|
+
*
|
|
102
|
+
* @param routes - the project's route ids.
|
|
103
|
+
* @param pathname - the path, without the base path (`/blog/hello`).
|
|
104
|
+
* @returns the match, or none when no route matches.
|
|
105
|
+
*/
|
|
106
|
+
export const matchRoute = (routes, pathname) => {
|
|
107
|
+
for (const id of [...routes].sort(byRouteOrder)) {
|
|
108
|
+
const { pattern, names } = routePattern(id);
|
|
109
|
+
const found = pattern.exec(pathname);
|
|
110
|
+
if (found === null)
|
|
111
|
+
continue;
|
|
112
|
+
const params = {};
|
|
113
|
+
names.forEach((name, index) => {
|
|
114
|
+
params[name] = decoded(found[index + 1] ?? "");
|
|
115
|
+
});
|
|
116
|
+
return { id, params };
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* The path a route id gives with its params filled in, as `resolve` from `$app/paths` writes it: the layout groups
|
|
122
|
+
* left out, an optional or rest param with no value dropped with its slash.
|
|
123
|
+
*
|
|
124
|
+
* @param id - the route's id (`/blog/[slug]`).
|
|
125
|
+
* @param params - the params, by name.
|
|
126
|
+
* @returns the path (`/blog/hello`).
|
|
127
|
+
*/
|
|
128
|
+
export const filledRoute = (id, params) => {
|
|
129
|
+
const segments = pathSegments(id).flatMap((segment) => {
|
|
130
|
+
const parts = segmentParts(segment);
|
|
131
|
+
const text = parts
|
|
132
|
+
.map((part) => {
|
|
133
|
+
if (part.kind === "text")
|
|
134
|
+
return part.text;
|
|
135
|
+
// inserted as given, as SvelteKit's `resolve` does: the address the page builds from it encodes what it must
|
|
136
|
+
return params[part.name] ?? "";
|
|
137
|
+
})
|
|
138
|
+
.join("");
|
|
139
|
+
return text === "" ? [] : [text];
|
|
140
|
+
});
|
|
141
|
+
return `/${segments.join("/")}`;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Whether a text is a route id rather than a path: it names a param or a layout group.
|
|
145
|
+
*
|
|
146
|
+
* @param text - the text.
|
|
147
|
+
*/
|
|
148
|
+
export const isRouteId = (text) => text
|
|
149
|
+
.split("/")
|
|
150
|
+
.some((segment) => GROUP.test(segment) || segmentParts(segment).some((part) => part.kind === "param"));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The order SvelteKit tries its routes in, so a path two routes match lands on the one SvelteKit would choose.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* SvelteKit's own `sort_routes` (MIT), written again in this package's TypeScript: the rules are SvelteKit's to the
|
|
6
|
+
* letter, since a route chosen otherwise gives a component other params than the app would. A text segment outranks a
|
|
7
|
+
* param, a param with a matcher one without, a required param an optional one, `[...rest]/x` a plain param; a shallower
|
|
8
|
+
* path a deeper one; an optional param that is not last is left out of the comparison.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Orders two routes, the one SvelteKit tries first first.
|
|
14
|
+
*
|
|
15
|
+
* @param first - a route id.
|
|
16
|
+
* @param second - another.
|
|
17
|
+
*/
|
|
18
|
+
export declare const byRouteOrder: (first: string, second: string) => number;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The order SvelteKit tries its routes in, so a path two routes match lands on the one SvelteKit would choose.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* SvelteKit's own `sort_routes` (MIT), written again in this package's TypeScript: the rules are SvelteKit's to the
|
|
6
|
+
* letter, since a route chosen otherwise gives a component other params than the app would. A text segment outranks a
|
|
7
|
+
* param, a param with a matcher one without, a required param an optional one, `[...rest]/x` a plain param; a shallower
|
|
8
|
+
* path a deeper one; an optional param that is not last is left out of the comparison.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/** The part a segment past a route's end compares as. */
|
|
13
|
+
const EMPTY = { type: "static", content: "", isMatched: false };
|
|
14
|
+
/** A param's type, by the character after its `[`: `[[optional]]`, `[...rest]`, any other `[required]`. */
|
|
15
|
+
const PARAM_TYPES = { "[": "optional", ".": "rest" };
|
|
16
|
+
/** A layout group, `(app)`: a folder that is not a segment of the path. */
|
|
17
|
+
const GROUP = /^\(.+\)$/;
|
|
18
|
+
/**
|
|
19
|
+
* A segment's parts: a text first (empty when the segment opens with a param), then params and texts in turn.
|
|
20
|
+
*
|
|
21
|
+
* @param segment - the segment.
|
|
22
|
+
*/
|
|
23
|
+
const partsOf = (segment) => {
|
|
24
|
+
const parts = [];
|
|
25
|
+
let index = 0;
|
|
26
|
+
while (index <= segment.length) {
|
|
27
|
+
const start = segment.indexOf("[", index);
|
|
28
|
+
if (start === -1) {
|
|
29
|
+
parts.push({ type: "static", content: segment.slice(index), isMatched: false });
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
parts.push({ type: "static", content: segment.slice(index, start), isMatched: false });
|
|
33
|
+
const type = PARAM_TYPES[segment[start + 1] ?? ""] ?? "required";
|
|
34
|
+
const delimiter = type === "optional" ? "]]" : "]";
|
|
35
|
+
const end = segment.indexOf(delimiter, start);
|
|
36
|
+
if (end === -1)
|
|
37
|
+
return [{ type: "static", content: segment, isMatched: false }];
|
|
38
|
+
index = end + delimiter.length;
|
|
39
|
+
const content = segment.slice(start, index);
|
|
40
|
+
parts.push({ type, content, isMatched: content.includes("=") });
|
|
41
|
+
}
|
|
42
|
+
return parts;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* A route id's segments as the sort compares them: an optional param that is not at the end left out, groups too.
|
|
46
|
+
*
|
|
47
|
+
* @param id - the route id.
|
|
48
|
+
*/
|
|
49
|
+
const sortedSegments = (id) => id
|
|
50
|
+
.replace(/\[\[[^\]]+\]\](?!(?:\/\([^/]+\))*$)/g, "")
|
|
51
|
+
.slice(1)
|
|
52
|
+
.split("/")
|
|
53
|
+
.filter((segment) => segment !== "" && !GROUP.test(segment))
|
|
54
|
+
.map(partsOf);
|
|
55
|
+
/**
|
|
56
|
+
* Orders two texts as SvelteKit does: by their characters, except that `foobar` outranks `foo`.
|
|
57
|
+
*
|
|
58
|
+
* @param first - a text.
|
|
59
|
+
* @param second - another.
|
|
60
|
+
*/
|
|
61
|
+
const byText = (first, second) => {
|
|
62
|
+
for (let index = 0;; index += 1) {
|
|
63
|
+
const a = first[index];
|
|
64
|
+
const b = second[index];
|
|
65
|
+
if (a === b) {
|
|
66
|
+
if (a === undefined)
|
|
67
|
+
return 0;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (a === undefined)
|
|
71
|
+
return 1;
|
|
72
|
+
if (b === undefined)
|
|
73
|
+
return -1;
|
|
74
|
+
return a < b ? -1 : 1;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Orders two params at the same place of two routes, or none when they rank the same.
|
|
79
|
+
*
|
|
80
|
+
* @param a - the first route's param.
|
|
81
|
+
* @param b - the second's.
|
|
82
|
+
* @param nextA - the text after the first's, in its segment or the next.
|
|
83
|
+
* @param nextB - the text after the second's.
|
|
84
|
+
*/
|
|
85
|
+
const byParam = (a, b, nextA, nextB) => {
|
|
86
|
+
if (a.type === "rest" || b.type === "rest")
|
|
87
|
+
return byRest(a, b, nextA, nextB);
|
|
88
|
+
if (a.isMatched !== b.isMatched)
|
|
89
|
+
return a.isMatched ? -1 : 1;
|
|
90
|
+
if (a.type !== b.type) {
|
|
91
|
+
if (a.type === "required")
|
|
92
|
+
return -1;
|
|
93
|
+
if (b.type === "required")
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Orders two params at the same place when one is a rest param: `[...rest]/x` outranks a plain param, which outranks
|
|
100
|
+
* a bare `[...rest]`.
|
|
101
|
+
*
|
|
102
|
+
* @param a - the first route's param.
|
|
103
|
+
* @param b - the second's.
|
|
104
|
+
* @param nextA - the text after the first's.
|
|
105
|
+
* @param nextB - the text after the second's.
|
|
106
|
+
*/
|
|
107
|
+
const byRest = (a, b, nextA, nextB) => {
|
|
108
|
+
if (a.type === "rest" && b.type === "rest") {
|
|
109
|
+
if (nextA !== "" && nextB !== "")
|
|
110
|
+
return undefined;
|
|
111
|
+
if (nextA !== "")
|
|
112
|
+
return -1;
|
|
113
|
+
return nextB !== "" ? 1 : undefined;
|
|
114
|
+
}
|
|
115
|
+
if (a.type === "rest")
|
|
116
|
+
return nextA !== "" && nextB === "" ? -1 : 1;
|
|
117
|
+
return nextB !== "" && nextA === "" ? 1 : -1;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* The text that follows a part: the next part of its segment, or the first of the next segment.
|
|
121
|
+
*
|
|
122
|
+
* @param segments - the route's segments.
|
|
123
|
+
* @param segment - the part's segment.
|
|
124
|
+
* @param part - the part.
|
|
125
|
+
*/
|
|
126
|
+
const nextText = (segments, segment, part) => segments[segment]?.[part + 1]?.content || segments[segment + 1]?.[0]?.content || "";
|
|
127
|
+
/**
|
|
128
|
+
* Orders two segments at the same place of two routes, or none when they rank the same.
|
|
129
|
+
*
|
|
130
|
+
* @param segmentsA - the first route's segments.
|
|
131
|
+
* @param segmentsB - the second's.
|
|
132
|
+
* @param index - the segments' place.
|
|
133
|
+
*/
|
|
134
|
+
const bySegment = (segmentsA, segmentsB, index) => {
|
|
135
|
+
const segmentA = segmentsA[index] ?? [EMPTY];
|
|
136
|
+
const segmentB = segmentsB[index] ?? [EMPTY];
|
|
137
|
+
for (let part = 0; part < Math.max(segmentA.length, segmentB.length); part += 1) {
|
|
138
|
+
const order = byPart(segmentA[part], segmentB[part], part % 2 === 1, () => [
|
|
139
|
+
nextText(segmentsA, index, part),
|
|
140
|
+
nextText(segmentsB, index, part)
|
|
141
|
+
]);
|
|
142
|
+
if (order !== undefined)
|
|
143
|
+
return order;
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Orders two parts at the same place of two segments, or none when they rank the same.
|
|
149
|
+
*
|
|
150
|
+
* @param a - the first segment's part, or none past its end.
|
|
151
|
+
* @param b - the second's.
|
|
152
|
+
* @param isParam - whether the place holds params: a segment starts with a text, maybe empty, then alternates.
|
|
153
|
+
* @param nextTexts - the texts after each param.
|
|
154
|
+
*/
|
|
155
|
+
const byPart = (a, b, isParam, nextTexts) => {
|
|
156
|
+
if (isParam) {
|
|
157
|
+
if (a === undefined)
|
|
158
|
+
return -1;
|
|
159
|
+
if (b === undefined)
|
|
160
|
+
return 1;
|
|
161
|
+
const [nextA, nextB] = nextTexts();
|
|
162
|
+
return byParam(a, b, nextA, nextB);
|
|
163
|
+
}
|
|
164
|
+
if (a?.content === b?.content)
|
|
165
|
+
return undefined;
|
|
166
|
+
if (a === undefined || a === EMPTY)
|
|
167
|
+
return -1;
|
|
168
|
+
if (b === undefined || b === EMPTY)
|
|
169
|
+
return 1;
|
|
170
|
+
return byText(a.content, b.content);
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Orders two routes, the one SvelteKit tries first first.
|
|
174
|
+
*
|
|
175
|
+
* @param first - a route id.
|
|
176
|
+
* @param second - another.
|
|
177
|
+
*/
|
|
178
|
+
export const byRouteOrder = (first, second) => {
|
|
179
|
+
const segmentsA = sortedSegments(first);
|
|
180
|
+
const segmentsB = sortedSegments(second);
|
|
181
|
+
for (let index = 0; index < Math.max(segmentsA.length, segmentsB.length); index += 1) {
|
|
182
|
+
const order = bySegment(segmentsA, segmentsB, index);
|
|
183
|
+
if (order !== undefined)
|
|
184
|
+
return order;
|
|
185
|
+
}
|
|
186
|
+
return first < second ? 1 : -1;
|
|
187
|
+
};
|