@mindees/router 0.1.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 +31 -0
- package/README.md +196 -0
- package/dist/components.d.ts +50 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +94 -0
- package/dist/components.js.map +1 -0
- package/dist/data.d.ts +38 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +151 -0
- package/dist/data.js.map +1 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -0
- package/dist/history.d.ts +75 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +125 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/pattern.d.ts +81 -0
- package/dist/pattern.d.ts.map +1 -0
- package/dist/pattern.js +156 -0
- package/dist/pattern.js.map +1 -0
- package/dist/router.d.ts +217 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +263 -0
- package/dist/router.js.map +1 -0
- package/dist/search.d.ts +50 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +112 -0
- package/dist/search.js.map +1 -0
- package/dist/standard-schema.d.ts +90 -0
- package/dist/standard-schema.d.ts.map +1 -0
- package/package.json +50 -0
package/dist/errors.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/** An error thrown by the Quantum router. */
|
|
3
|
+
var RouterError = class extends Error {
|
|
4
|
+
/** Machine-readable error code. */
|
|
5
|
+
code;
|
|
6
|
+
/** Standard Schema issues, present only for `VALIDATE_SEARCH`. */
|
|
7
|
+
issues;
|
|
8
|
+
constructor(code, message, issues) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "RouterError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
if (issues !== void 0) this.issues = issues;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { RouterError };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Router error types. A single {@link RouterError} carries a machine-readable\n * `code` (so callers can branch without string-matching messages) and, for\n * search-validation failures, the Standard Schema issues that caused it.\n *\n * @module\n */\n\nimport type { StandardSchemaV1 } from './standard-schema'\n\n/** Machine-readable router error codes. */\nexport type RouterErrorCode =\n /** A path pattern was malformed (e.g. a catch-all that is not the last segment). */\n | 'INVALID_PATTERN'\n /** {@link buildPath} was called without a value for a required param. */\n | 'MISSING_PARAM'\n /** Search-param validation failed against the route's schema. */\n | 'VALIDATE_SEARCH'\n /**\n * A Standard Schema returned a `Promise`. Navigation-time parsing must be\n * synchronous, so async schemas are rejected.\n */\n | 'ASYNC_SCHEMA'\n\n/** An error thrown by the Quantum router. */\nexport class RouterError extends Error {\n /** Machine-readable error code. */\n readonly code: RouterErrorCode\n /** Standard Schema issues, present only for `VALIDATE_SEARCH`. */\n readonly issues?: ReadonlyArray<StandardSchemaV1.Issue>\n\n constructor(\n code: RouterErrorCode,\n message: string,\n issues?: ReadonlyArray<StandardSchemaV1.Issue>,\n ) {\n super(message)\n this.name = 'RouterError'\n this.code = code\n if (issues !== undefined) this.issues = issues\n }\n}\n"],"mappings":";;AAyBA,IAAa,cAAb,cAAiC,MAAM;;CAErC;;CAEA;CAEA,YACE,MACA,SACA,QACA;EACA,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,IAAI,WAAW,KAAA,GAAW,KAAK,SAAS;CAC1C;AACF"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//#region src/history.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* History — the navigation capability the router is built on.
|
|
4
|
+
*
|
|
5
|
+
* {@link RouterHistory} is a tiny observable over a location. Two adapters ship:
|
|
6
|
+
* {@link createMemoryHistory} (in-memory, the primary tested path — no DOM, so
|
|
7
|
+
* the whole router is deterministically testable headless) and
|
|
8
|
+
* {@link createBrowserHistory} (binds `window.history` + `popstate`).
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/** A parsed location: pathname plus the raw search and hash strings. */
|
|
13
|
+
interface RouterLocation {
|
|
14
|
+
/** Path portion, always starting with `/` (e.g. `/posts/42`). */
|
|
15
|
+
readonly pathname: string;
|
|
16
|
+
/** Raw search string including the leading `?` (e.g. `?page=2`), or `''`. */
|
|
17
|
+
readonly search: string;
|
|
18
|
+
/** Raw hash including the leading `#`, or `''`. */
|
|
19
|
+
readonly hash: string;
|
|
20
|
+
}
|
|
21
|
+
/** A listener notified on every location change. */
|
|
22
|
+
type HistoryListener = (location: RouterLocation) => void;
|
|
23
|
+
/** The navigation capability: read the location, navigate, and subscribe. */
|
|
24
|
+
interface RouterHistory {
|
|
25
|
+
/** The current location. */
|
|
26
|
+
location(): RouterLocation;
|
|
27
|
+
/** Push a new entry (forward history is discarded). Notifies synchronously. */
|
|
28
|
+
push(to: string): void;
|
|
29
|
+
/** Replace the current entry in place. Notifies synchronously. */
|
|
30
|
+
replace(to: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* Move within the stack by a relative delta.
|
|
33
|
+
*
|
|
34
|
+
* Timing differs by adapter: {@link createMemoryHistory} updates and notifies
|
|
35
|
+
* **synchronously**, whereas {@link createBrowserHistory} delegates to
|
|
36
|
+
* `window.history` and the location change is observed **asynchronously** via
|
|
37
|
+
* the `popstate` event — so reading `location()` immediately after `go()` may
|
|
38
|
+
* still return the previous location in a browser.
|
|
39
|
+
*/
|
|
40
|
+
go(delta: number): void;
|
|
41
|
+
/** Shorthand for `go(-1)`. See {@link RouterHistory.go} for timing caveats. */
|
|
42
|
+
back(): void;
|
|
43
|
+
/** Shorthand for `go(1)`. See {@link RouterHistory.go} for timing caveats. */
|
|
44
|
+
forward(): void;
|
|
45
|
+
/** Subscribe to location changes; returns an unsubscribe function. */
|
|
46
|
+
subscribe(listener: HistoryListener): () => void;
|
|
47
|
+
}
|
|
48
|
+
/** Parse an href string into a {@link RouterLocation} (no base required). */
|
|
49
|
+
declare function parseHref(href: string): RouterLocation;
|
|
50
|
+
/** Serialize a {@link RouterLocation} back into an href string. */
|
|
51
|
+
declare function createHref(location: RouterLocation): string;
|
|
52
|
+
/** Options for {@link createMemoryHistory}. */
|
|
53
|
+
interface MemoryHistoryOptions {
|
|
54
|
+
/** Initial entries (hrefs). Defaults to `['/']`. */
|
|
55
|
+
initialEntries?: readonly string[];
|
|
56
|
+
/** Initial index into `initialEntries`. Defaults to the last entry. */
|
|
57
|
+
initialIndex?: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create an in-memory history. Deterministic and DOM-free — the primary tested
|
|
61
|
+
* path and the right adapter for SSR, tests, and non-browser hosts.
|
|
62
|
+
*/
|
|
63
|
+
declare function createMemoryHistory(options?: MemoryHistoryOptions): RouterHistory;
|
|
64
|
+
/**
|
|
65
|
+
* Create a history bound to the browser's `window.history`. `push`/`replace` use
|
|
66
|
+
* the History API; `popstate` (back/forward) is observed and forwarded to
|
|
67
|
+
* subscribers. The `popstate` listener is attached lazily while there is at
|
|
68
|
+
* least one subscriber and removed when the last one unsubscribes.
|
|
69
|
+
*
|
|
70
|
+
* Requires a DOM (`window`). Use {@link createMemoryHistory} elsewhere.
|
|
71
|
+
*/
|
|
72
|
+
declare function createBrowserHistory(): RouterHistory;
|
|
73
|
+
//#endregion
|
|
74
|
+
export { HistoryListener, MemoryHistoryOptions, RouterHistory, RouterLocation, createBrowserHistory, createHref, createMemoryHistory, parseHref };
|
|
75
|
+
//# sourceMappingURL=history.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.d.ts","names":[],"sources":["../src/history.ts"],"mappings":";;AAYA;;;;;;;;AAMe;AAIf;AAAA,UAViB,cAAA;;WAEN,QAAA;EAQ4C;EAAA,SAN5C,MAAA;EASmB;EAAA,SAPnB,IAAA;AAAA;;KAIC,eAAA,IAAmB,QAAwB,EAAd,cAAc;;UAGtC,aAAA;EAMf;EAJA,QAAA,IAAY,cAAA;EAcZ;EAZA,IAAA,CAAK,EAAA;EAcL;EAZA,OAAA,CAAQ,EAAA;EAgBR;;;;AAAmC;AAMrC;;;;EAZE,EAAA,CAAG,KAAA;EAgCW;EA9Bd,IAAA;;EAEA,OAAA;EA4BiD;EA1BjD,SAAA,CAAU,QAAA,EAAU,eAAe;AAAA;;iBAMrB,SAAA,CAAU,IAAA,WAAe,cAAc;AA6BzC;AAAA,iBATE,UAAA,CAAW,QAAwB,EAAd,cAAc;;UAKlC,oBAAA;EAgBqE;EAdpF,cAAA;EAckC;EAZlC,YAAY;AAAA;AAYwE;AAoDtF;;;AApDsF,iBAAtE,mBAAA,CAAoB,OAAA,GAAS,oBAAA,GAA4B,aAAa;AAoDjC;;;;;;;;AAAA,iBAArC,oBAAA,IAAwB,aAAa"}
|
package/dist/history.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
//#region src/history.ts
|
|
2
|
+
const ROOT = {
|
|
3
|
+
pathname: "/",
|
|
4
|
+
search: "",
|
|
5
|
+
hash: ""
|
|
6
|
+
};
|
|
7
|
+
/** Parse an href string into a {@link RouterLocation} (no base required). */
|
|
8
|
+
function parseHref(href) {
|
|
9
|
+
let rest = href;
|
|
10
|
+
let hash = "";
|
|
11
|
+
const hashIndex = rest.indexOf("#");
|
|
12
|
+
if (hashIndex !== -1) {
|
|
13
|
+
hash = rest.slice(hashIndex);
|
|
14
|
+
rest = rest.slice(0, hashIndex);
|
|
15
|
+
}
|
|
16
|
+
let search = "";
|
|
17
|
+
const queryIndex = rest.indexOf("?");
|
|
18
|
+
if (queryIndex !== -1) {
|
|
19
|
+
search = rest.slice(queryIndex);
|
|
20
|
+
rest = rest.slice(0, queryIndex);
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
pathname: rest.length === 0 ? "/" : rest.startsWith("/") ? rest : `/${rest}`,
|
|
24
|
+
search,
|
|
25
|
+
hash
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Serialize a {@link RouterLocation} back into an href string. */
|
|
29
|
+
function createHref(location) {
|
|
30
|
+
return `${location.pathname}${location.search}${location.hash}`;
|
|
31
|
+
}
|
|
32
|
+
/** Clamp `n` to the inclusive range `[min, max]`. */
|
|
33
|
+
function clamp(n, min, max) {
|
|
34
|
+
return Math.min(max, Math.max(min, n));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create an in-memory history. Deterministic and DOM-free — the primary tested
|
|
38
|
+
* path and the right adapter for SSR, tests, and non-browser hosts.
|
|
39
|
+
*/
|
|
40
|
+
function createMemoryHistory(options = {}) {
|
|
41
|
+
const entries = (options.initialEntries && options.initialEntries.length > 0 ? options.initialEntries : ["/"]).map(parseHref);
|
|
42
|
+
let index = clamp(options.initialIndex ?? entries.length - 1, 0, entries.length - 1);
|
|
43
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
44
|
+
const current = () => entries[index] ?? ROOT;
|
|
45
|
+
const notify = () => {
|
|
46
|
+
const location = current();
|
|
47
|
+
for (const listener of listeners) listener(location);
|
|
48
|
+
};
|
|
49
|
+
const go = (delta) => {
|
|
50
|
+
const next = clamp(index + delta, 0, entries.length - 1);
|
|
51
|
+
if (next !== index) {
|
|
52
|
+
index = next;
|
|
53
|
+
notify();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
location: current,
|
|
58
|
+
push(to) {
|
|
59
|
+
entries.splice(index + 1);
|
|
60
|
+
entries.push(parseHref(to));
|
|
61
|
+
index = entries.length - 1;
|
|
62
|
+
notify();
|
|
63
|
+
},
|
|
64
|
+
replace(to) {
|
|
65
|
+
entries[index] = parseHref(to);
|
|
66
|
+
notify();
|
|
67
|
+
},
|
|
68
|
+
go,
|
|
69
|
+
back: () => go(-1),
|
|
70
|
+
forward: () => go(1),
|
|
71
|
+
subscribe(listener) {
|
|
72
|
+
listeners.add(listener);
|
|
73
|
+
return () => {
|
|
74
|
+
listeners.delete(listener);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create a history bound to the browser's `window.history`. `push`/`replace` use
|
|
81
|
+
* the History API; `popstate` (back/forward) is observed and forwarded to
|
|
82
|
+
* subscribers. The `popstate` listener is attached lazily while there is at
|
|
83
|
+
* least one subscriber and removed when the last one unsubscribes.
|
|
84
|
+
*
|
|
85
|
+
* Requires a DOM (`window`). Use {@link createMemoryHistory} elsewhere.
|
|
86
|
+
*/
|
|
87
|
+
function createBrowserHistory() {
|
|
88
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
89
|
+
const current = () => ({
|
|
90
|
+
pathname: window.location.pathname,
|
|
91
|
+
search: window.location.search,
|
|
92
|
+
hash: window.location.hash
|
|
93
|
+
});
|
|
94
|
+
const notify = () => {
|
|
95
|
+
const location = current();
|
|
96
|
+
for (const listener of listeners) listener(location);
|
|
97
|
+
};
|
|
98
|
+
const onPopState = () => notify();
|
|
99
|
+
return {
|
|
100
|
+
location: current,
|
|
101
|
+
push(to) {
|
|
102
|
+
window.history.pushState(null, "", createHref(parseHref(to)));
|
|
103
|
+
notify();
|
|
104
|
+
},
|
|
105
|
+
replace(to) {
|
|
106
|
+
window.history.replaceState(null, "", createHref(parseHref(to)));
|
|
107
|
+
notify();
|
|
108
|
+
},
|
|
109
|
+
go: (delta) => window.history.go(delta),
|
|
110
|
+
back: () => window.history.back(),
|
|
111
|
+
forward: () => window.history.forward(),
|
|
112
|
+
subscribe(listener) {
|
|
113
|
+
if (listeners.size === 0) window.addEventListener("popstate", onPopState);
|
|
114
|
+
listeners.add(listener);
|
|
115
|
+
return () => {
|
|
116
|
+
listeners.delete(listener);
|
|
117
|
+
if (listeners.size === 0) window.removeEventListener("popstate", onPopState);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
export { createBrowserHistory, createHref, createMemoryHistory, parseHref };
|
|
124
|
+
|
|
125
|
+
//# sourceMappingURL=history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.js","names":[],"sources":["../src/history.ts"],"sourcesContent":["/**\n * History — the navigation capability the router is built on.\n *\n * {@link RouterHistory} is a tiny observable over a location. Two adapters ship:\n * {@link createMemoryHistory} (in-memory, the primary tested path — no DOM, so\n * the whole router is deterministically testable headless) and\n * {@link createBrowserHistory} (binds `window.history` + `popstate`).\n *\n * @module\n */\n\n/** A parsed location: pathname plus the raw search and hash strings. */\nexport interface RouterLocation {\n /** Path portion, always starting with `/` (e.g. `/posts/42`). */\n readonly pathname: string\n /** Raw search string including the leading `?` (e.g. `?page=2`), or `''`. */\n readonly search: string\n /** Raw hash including the leading `#`, or `''`. */\n readonly hash: string\n}\n\n/** A listener notified on every location change. */\nexport type HistoryListener = (location: RouterLocation) => void\n\n/** The navigation capability: read the location, navigate, and subscribe. */\nexport interface RouterHistory {\n /** The current location. */\n location(): RouterLocation\n /** Push a new entry (forward history is discarded). Notifies synchronously. */\n push(to: string): void\n /** Replace the current entry in place. Notifies synchronously. */\n replace(to: string): void\n /**\n * Move within the stack by a relative delta.\n *\n * Timing differs by adapter: {@link createMemoryHistory} updates and notifies\n * **synchronously**, whereas {@link createBrowserHistory} delegates to\n * `window.history` and the location change is observed **asynchronously** via\n * the `popstate` event — so reading `location()` immediately after `go()` may\n * still return the previous location in a browser.\n */\n go(delta: number): void\n /** Shorthand for `go(-1)`. See {@link RouterHistory.go} for timing caveats. */\n back(): void\n /** Shorthand for `go(1)`. See {@link RouterHistory.go} for timing caveats. */\n forward(): void\n /** Subscribe to location changes; returns an unsubscribe function. */\n subscribe(listener: HistoryListener): () => void\n}\n\nconst ROOT: RouterLocation = { pathname: '/', search: '', hash: '' }\n\n/** Parse an href string into a {@link RouterLocation} (no base required). */\nexport function parseHref(href: string): RouterLocation {\n let rest = href\n let hash = ''\n const hashIndex = rest.indexOf('#')\n if (hashIndex !== -1) {\n hash = rest.slice(hashIndex)\n rest = rest.slice(0, hashIndex)\n }\n let search = ''\n const queryIndex = rest.indexOf('?')\n if (queryIndex !== -1) {\n search = rest.slice(queryIndex)\n rest = rest.slice(0, queryIndex)\n }\n // Guarantee the documented invariant: pathname always starts with '/'.\n const pathname = rest.length === 0 ? '/' : rest.startsWith('/') ? rest : `/${rest}`\n return { pathname, search, hash }\n}\n\n/** Serialize a {@link RouterLocation} back into an href string. */\nexport function createHref(location: RouterLocation): string {\n return `${location.pathname}${location.search}${location.hash}`\n}\n\n/** Options for {@link createMemoryHistory}. */\nexport interface MemoryHistoryOptions {\n /** Initial entries (hrefs). Defaults to `['/']`. */\n initialEntries?: readonly string[]\n /** Initial index into `initialEntries`. Defaults to the last entry. */\n initialIndex?: number\n}\n\n/** Clamp `n` to the inclusive range `[min, max]`. */\nfunction clamp(n: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, n))\n}\n\n/**\n * Create an in-memory history. Deterministic and DOM-free — the primary tested\n * path and the right adapter for SSR, tests, and non-browser hosts.\n */\nexport function createMemoryHistory(options: MemoryHistoryOptions = {}): RouterHistory {\n const initial =\n options.initialEntries && options.initialEntries.length > 0 ? options.initialEntries : ['/']\n const entries: RouterLocation[] = initial.map(parseHref)\n let index = clamp(options.initialIndex ?? entries.length - 1, 0, entries.length - 1)\n const listeners = new Set<HistoryListener>()\n\n const current = (): RouterLocation => entries[index] ?? ROOT\n const notify = (): void => {\n const location = current()\n for (const listener of listeners) listener(location)\n }\n const go = (delta: number): void => {\n const next = clamp(index + delta, 0, entries.length - 1)\n if (next !== index) {\n index = next\n notify()\n }\n }\n\n return {\n location: current,\n push(to) {\n entries.splice(index + 1)\n entries.push(parseHref(to))\n index = entries.length - 1\n notify()\n },\n replace(to) {\n entries[index] = parseHref(to)\n notify()\n },\n go,\n back: () => go(-1),\n forward: () => go(1),\n subscribe(listener) {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n }\n}\n\n/**\n * Create a history bound to the browser's `window.history`. `push`/`replace` use\n * the History API; `popstate` (back/forward) is observed and forwarded to\n * subscribers. The `popstate` listener is attached lazily while there is at\n * least one subscriber and removed when the last one unsubscribes.\n *\n * Requires a DOM (`window`). Use {@link createMemoryHistory} elsewhere.\n */\nexport function createBrowserHistory(): RouterHistory {\n const listeners = new Set<HistoryListener>()\n const current = (): RouterLocation => ({\n pathname: window.location.pathname,\n search: window.location.search,\n hash: window.location.hash,\n })\n const notify = (): void => {\n const location = current()\n for (const listener of listeners) listener(location)\n }\n const onPopState = (): void => notify()\n\n return {\n location: current,\n push(to) {\n // Normalize via parseHref so the browser adapter treats `to` as an href\n // (not browser-relative), matching createMemoryHistory exactly. Router\n // navigation already passes absolute hrefs; this only affects direct calls.\n window.history.pushState(null, '', createHref(parseHref(to)))\n notify()\n },\n replace(to) {\n window.history.replaceState(null, '', createHref(parseHref(to)))\n notify()\n },\n go: (delta) => window.history.go(delta),\n back: () => window.history.back(),\n forward: () => window.history.forward(),\n subscribe(listener) {\n if (listeners.size === 0) window.addEventListener('popstate', onPopState)\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n if (listeners.size === 0) window.removeEventListener('popstate', onPopState)\n }\n },\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { HistoryListener, MemoryHistoryOptions, RouterHistory, RouterLocation, createBrowserHistory, createHref, createMemoryHistory, parseHref } from "./history.js";
|
|
2
|
+
import { LoaderContext, LoaderData, LoaderDepsFn, LoaderFn, LoaderStatus } from "./data.js";
|
|
3
|
+
import { HasPathParams, PathParams, buildPath, compareSpecificity, matchPattern, parsePattern } from "./pattern.js";
|
|
4
|
+
import { StandardSchemaV1 } from "./standard-schema.js";
|
|
5
|
+
import { QueryValue, ValidationResult, parseQuery, safeValidateSearch, stringifyQuery, validateSearch } from "./search.js";
|
|
6
|
+
import { BeforeNavigate, CreateRouterOptions, NavTarget, NavigateOptions, RouteComponentProps, RouteMatch, RouteRecord, Router, RouterState, createRouter, resolvePath } from "./router.js";
|
|
7
|
+
import { LinkComponent, LinkOptions, LinkProps, RouterViewOptions, createLink, createRouterView } from "./components.js";
|
|
8
|
+
import { RouterError, RouterErrorCode } from "./errors.js";
|
|
9
|
+
import { Maturity, NotImplementedError, PackageInfo, notImplemented } from "@mindees/core";
|
|
10
|
+
|
|
11
|
+
//#region src/index.d.ts
|
|
12
|
+
/** The npm package name. */
|
|
13
|
+
declare const name = "@mindees/router";
|
|
14
|
+
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
15
|
+
declare const VERSION = "0.1.0";
|
|
16
|
+
/**
|
|
17
|
+
* Current maturity. Router I (typed params, Standard-Schema search, history, the
|
|
18
|
+
* signals-native router, selector-isolated state, typed + relative navigation)
|
|
19
|
+
* and Router II (nested rendering, typed links, SWR loaders, navigation guards,
|
|
20
|
+
* view transitions) are implemented and tested. The global typed route registry
|
|
21
|
+
* and file-based route scanning are a later phase — see `STATUS.md`.
|
|
22
|
+
*/
|
|
23
|
+
declare const maturity: Maturity;
|
|
24
|
+
/** Static identity + maturity metadata for this package. */
|
|
25
|
+
declare const info: PackageInfo;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { type BeforeNavigate, type CreateRouterOptions, type HasPathParams, type HistoryListener, type LinkComponent, type LinkOptions, type LinkProps, type LoaderContext, type LoaderData, type LoaderDepsFn, type LoaderFn, type LoaderStatus, type Maturity, type MemoryHistoryOptions, type NavTarget, type NavigateOptions, NotImplementedError, type PackageInfo, type PathParams, type QueryValue, type RouteComponentProps, type RouteMatch, type RouteRecord, type Router, RouterError, type RouterErrorCode, type RouterHistory, type RouterLocation, type RouterState, type RouterViewOptions, type StandardSchemaV1, VERSION, type ValidationResult, buildPath, compareSpecificity, createBrowserHistory, createHref, createLink, createMemoryHistory, createRouter, createRouterView, info, matchPattern, maturity, name, notImplemented, parseHref, parsePattern, parseQuery, resolvePath, safeValidateSearch, stringifyQuery, validateSearch };
|
|
28
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;;;;;cAyFa,IAAA;;cAGA,OAAA;;;;;;;;cASA,QAAA,EAAU,QAAyB;;cAGnC,IAAA,EAAM,WAAkD"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { RouterError } from "./errors.js";
|
|
2
|
+
import { buildPath, compareSpecificity, matchPattern, parsePattern } from "./pattern.js";
|
|
3
|
+
import { parseQuery, safeValidateSearch, stringifyQuery, validateSearch } from "./search.js";
|
|
4
|
+
import { createLink, createRouterView } from "./components.js";
|
|
5
|
+
import { createBrowserHistory, createHref, createMemoryHistory, parseHref } from "./history.js";
|
|
6
|
+
import { createRouter, resolvePath } from "./router.js";
|
|
7
|
+
import { NotImplementedError, notImplemented } from "@mindees/core";
|
|
8
|
+
//#region src/index.ts
|
|
9
|
+
/** The npm package name. */
|
|
10
|
+
const name = "@mindees/router";
|
|
11
|
+
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
12
|
+
const VERSION = "0.1.0";
|
|
13
|
+
/**
|
|
14
|
+
* Current maturity. Router I (typed params, Standard-Schema search, history, the
|
|
15
|
+
* signals-native router, selector-isolated state, typed + relative navigation)
|
|
16
|
+
* and Router II (nested rendering, typed links, SWR loaders, navigation guards,
|
|
17
|
+
* view transitions) are implemented and tested. The global typed route registry
|
|
18
|
+
* and file-based route scanning are a later phase — see `STATUS.md`.
|
|
19
|
+
*/
|
|
20
|
+
const maturity = "experimental";
|
|
21
|
+
/** Static identity + maturity metadata for this package. */
|
|
22
|
+
const info = {
|
|
23
|
+
name,
|
|
24
|
+
version: VERSION,
|
|
25
|
+
maturity
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
28
|
+
export { NotImplementedError, RouterError, VERSION, buildPath, compareSpecificity, createBrowserHistory, createHref, createLink, createMemoryHistory, createRouter, createRouterView, info, matchPattern, maturity, name, notImplemented, parseHref, parsePattern, parseQuery, resolvePath, safeValidateSearch, stringifyQuery, validateSearch };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@mindees/router` — **Quantum**, the typed router for MindeesNative.\n *\n * Router I (Phase 6): codegen-free typed path params ({@link PathParams}),\n * Standard-Schema validated search params, a signals-native router with typed +\n * relative navigation and selector-isolated state, and an injectable history\n * (memory + browser). See ADR-0003.\n *\n * Router II (Phase 7): render integration — {@link createRouterView} (nested,\n * fine-grained, layout-preserving) and typed {@link createLink} — plus SWR data\n * loaders (with `AbortSignal`, {@link Router.invalidate}/{@link Router.preload}),\n * navigation guards ({@link BeforeNavigate} cancel/redirect + idempotent\n * navigation), and web view transitions. See ADR-0004 and ADR-0005.\n *\n * Still a later phase (not exported): the global typed route registry and\n * file-based route scanning + bundler plugin. See `STATUS.md`.\n *\n * @module\n */\n\nimport type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** Render integration: nested view + typed links (Router II). */\nexport {\n createLink,\n createRouterView,\n type LinkComponent,\n type LinkOptions,\n type LinkProps,\n type RouterViewOptions,\n} from './components'\n/** Loaders + data (SWR). */\nexport type {\n LoaderContext,\n LoaderData,\n LoaderDepsFn,\n LoaderFn,\n LoaderStatus,\n} from './data'\n/** Errors. */\nexport { RouterError, type RouterErrorCode } from './errors'\n/** History capability. */\nexport {\n createBrowserHistory,\n createHref,\n createMemoryHistory,\n type HistoryListener,\n type MemoryHistoryOptions,\n parseHref,\n type RouterHistory,\n type RouterLocation,\n} from './history'\n/** Route patterns + codegen-free typed params. */\nexport {\n buildPath,\n compareSpecificity,\n type HasPathParams,\n matchPattern,\n type PathParams,\n parsePattern,\n} from './pattern'\n/** Router. */\nexport {\n type BeforeNavigate,\n type CreateRouterOptions,\n createRouter,\n type NavigateOptions,\n type NavTarget,\n type RouteComponentProps,\n type RouteMatch,\n type RouteRecord,\n type Router,\n type RouterState,\n resolvePath,\n} from './router'\n/** Search (query) params. */\nexport {\n parseQuery,\n type QueryValue,\n safeValidateSearch,\n stringifyQuery,\n type ValidationResult,\n validateSearch,\n} from './search'\n/** Standard Schema — the validator-agnostic interface (vendored, types only). */\nexport type { StandardSchemaV1 } from './standard-schema'\n\n/** The npm package name. */\nexport const name = '@mindees/router'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.1.0'\n\n/**\n * Current maturity. Router I (typed params, Standard-Schema search, history, the\n * signals-native router, selector-isolated state, typed + relative navigation)\n * and Router II (nested rendering, typed links, SWR loaders, navigation guards,\n * view transitions) are implemented and tested. The global typed route registry\n * and file-based route scanning are a later phase — see `STATUS.md`.\n */\nexport const maturity: Maturity = 'experimental'\n\n/** Static identity + maturity metadata for this package. */\nexport const info: PackageInfo = { name, version: VERSION, maturity }\n\nexport type { Maturity, PackageInfo }\nexport { NotImplementedError, notImplemented }\n"],"mappings":";;;;;;;;;AAyFA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;;;;;;;AASvB,MAAa,WAAqB;;AAGlC,MAAa,OAAoB;CAAE;CAAM,SAAS;CAAS;AAAS"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//#region src/pattern.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Route patterns — matching, building, and **codegen-free** typed params.
|
|
4
|
+
*
|
|
5
|
+
* A pattern is a `/`-separated path where each segment is one of:
|
|
6
|
+
* - a **static** segment (`posts`) — matches itself literally;
|
|
7
|
+
* - a **dynamic** segment (`:postId`) — matches exactly one non-empty segment;
|
|
8
|
+
* - a **catch-all** segment (`:rest*`) — must be last; matches the remaining
|
|
9
|
+
* segments (zero or more), joined with `/`.
|
|
10
|
+
*
|
|
11
|
+
* This mirrors the manifest paths emitted by `@mindees/compiler`
|
|
12
|
+
* (`buildRouteManifest`: `[param]` → `:param`, `[...rest]` → `:rest*`).
|
|
13
|
+
*
|
|
14
|
+
* The headline win over Expo Router / React Router: params are typed by parsing
|
|
15
|
+
* the pattern string with **template-literal types** ({@link PathParams}) — no
|
|
16
|
+
* generated `.d.ts`, no dev server, and required params are typed as *required*
|
|
17
|
+
* (not optional). See ADR-0003.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
/** Flatten an intersection of object types into a single readable object type. */
|
|
22
|
+
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
23
|
+
/** The param contributed by a single pattern segment. */
|
|
24
|
+
type SegmentParam<Seg extends string> = Seg extends `:${infer Name}*` ? { [K in Name]: string } : Seg extends `:${infer Name}` ? { [K in Name]: string } : {};
|
|
25
|
+
/** Split a pattern on `/` into a tuple of segments. */
|
|
26
|
+
type SplitSegments<P extends string> = P extends `${infer Head}/${infer Tail}` ? [Head, ...SplitSegments<Tail>] : [P];
|
|
27
|
+
/** Intersect the param contributions of every segment. */
|
|
28
|
+
type MergeSegments<Segs extends readonly string[]> = Segs extends [infer Head extends string, ...infer Tail extends string[]] ? SegmentParam<Head> & MergeSegments<Tail> : {};
|
|
29
|
+
/**
|
|
30
|
+
* The params object for a pattern, inferred at the type level.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* type A = PathParams<'/posts/:postId'> // { postId: string }
|
|
34
|
+
* type B = PathParams<'/files/:rest*'> // { rest: string }
|
|
35
|
+
* type C = PathParams<'/about'> // {}
|
|
36
|
+
* type D = PathParams<'/u/:userId/p/:postId'> // { userId: string; postId: string }
|
|
37
|
+
*/
|
|
38
|
+
type PathParams<P extends string> = Prettify<MergeSegments<SplitSegments<P>>>;
|
|
39
|
+
/** Whether a pattern has any dynamic params (used to make params required vs optional). */
|
|
40
|
+
type HasPathParams<P extends string> = keyof PathParams<P> extends never ? false : true;
|
|
41
|
+
interface Segment {
|
|
42
|
+
readonly kind: 'static' | 'param' | 'catchAll';
|
|
43
|
+
/** Static text, or the param name. */
|
|
44
|
+
readonly value: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Parse a pattern into segments, validating it. Throws {@link RouterError}
|
|
48
|
+
* (`INVALID_PATTERN`) if a catch-all is not the final segment, or a param name
|
|
49
|
+
* is empty.
|
|
50
|
+
*/
|
|
51
|
+
declare function parsePattern(pattern: string): Segment[];
|
|
52
|
+
/**
|
|
53
|
+
* Match a `pathname` against a `pattern`. Returns the extracted params, or
|
|
54
|
+
* `null` if it does not match. Param values are URI-decoded.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* matchPattern('/posts/:id', '/posts/42') // { id: '42' }
|
|
58
|
+
* matchPattern('/files/:rest*', '/files/a/b') // { rest: 'a/b' }
|
|
59
|
+
* matchPattern('/about', '/contact') // null
|
|
60
|
+
*/
|
|
61
|
+
declare function matchPattern(pattern: string, pathname: string): Record<string, string> | null;
|
|
62
|
+
/**
|
|
63
|
+
* Build a pathname from a `pattern` and `params`. Param values are URI-encoded.
|
|
64
|
+
* Throws {@link RouterError} (`MISSING_PARAM`) if a required dynamic param is
|
|
65
|
+
* absent. A missing/empty catch-all simply contributes nothing.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* buildPath('/posts/:id', { id: '42' }) // '/posts/42'
|
|
69
|
+
* buildPath('/files/:rest*', { rest: 'a/b' }) // '/files/a/b'
|
|
70
|
+
* buildPath('/about', {}) // '/about'
|
|
71
|
+
*/
|
|
72
|
+
declare function buildPath(pattern: string, params?: Record<string, string | number>): string;
|
|
73
|
+
/**
|
|
74
|
+
* Compare two patterns by specificity. Returns a negative number if `a` is more
|
|
75
|
+
* specific than `b` (so `routes.sort(compareSpecificity)` puts the most specific
|
|
76
|
+
* first), positive if less specific, 0 if equal.
|
|
77
|
+
*/
|
|
78
|
+
declare function compareSpecificity(a: string, b: string): number;
|
|
79
|
+
//#endregion
|
|
80
|
+
export { HasPathParams, PathParams, buildPath, compareSpecificity, matchPattern, parsePattern };
|
|
81
|
+
//# sourceMappingURL=pattern.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pattern.d.ts","names":[],"sources":["../src/pattern.ts"],"mappings":";;;;;;;;;;;;;;;;AA2BwC;AAAA;;;;KAAnC,QAAA,oBAA4B,CAAA,GAAI,CAAA,CAAE,CAAA;;KAGlC,YAAA,uBAAmC,GAAA,qCAC5B,IAAA,cACR,GAAA,oCACU,IAAA;;KAKT,aAAA,qBAAkC,CAAA,0CAClC,IAAA,KAAS,aAAA,CAAc,IAAA,MACvB,CAAA;;KAGA,aAAA,mCAAgD,IAAA,uEAIjD,YAAA,CAAa,IAAA,IAAQ,aAAA,CAAc,IAAA;AAdrB;;;;;;;;;AAAA,KA2BN,UAAA,qBAA+B,QAAA,CAAS,aAAA,CAAc,aAAA,CAAc,CAAA;;KAGpE,aAAA,2BAAwC,UAAU,CAAC,CAAA;AAAA,UAMrD,OAAA;EAAA,SACC,IAAA;EA/BN;EAAA,SAiCM,KAAK;AAAA;;;AAhCV;AAAA;;iBAqDU,YAAA,CAAa,OAAA,WAAkB,OAAO;;;;;;;;;;iBA8BtC,YAAA,CAAa,OAAA,UAAiB,QAAA,WAAmB,MAAM;;;;;;;AA5E5B;AAa3C;;;iBA+GgB,SAAA,CAAU,OAAA,UAAiB,MAAA,GAAQ,MAAM;;;;;;iBA0DzC,kBAAA,CAAmB,CAAA,UAAW,CAAS"}
|
package/dist/pattern.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { RouterError } from "./errors.js";
|
|
2
|
+
//#region src/pattern.ts
|
|
3
|
+
/**
|
|
4
|
+
* Route patterns — matching, building, and **codegen-free** typed params.
|
|
5
|
+
*
|
|
6
|
+
* A pattern is a `/`-separated path where each segment is one of:
|
|
7
|
+
* - a **static** segment (`posts`) — matches itself literally;
|
|
8
|
+
* - a **dynamic** segment (`:postId`) — matches exactly one non-empty segment;
|
|
9
|
+
* - a **catch-all** segment (`:rest*`) — must be last; matches the remaining
|
|
10
|
+
* segments (zero or more), joined with `/`.
|
|
11
|
+
*
|
|
12
|
+
* This mirrors the manifest paths emitted by `@mindees/compiler`
|
|
13
|
+
* (`buildRouteManifest`: `[param]` → `:param`, `[...rest]` → `:rest*`).
|
|
14
|
+
*
|
|
15
|
+
* The headline win over Expo Router / React Router: params are typed by parsing
|
|
16
|
+
* the pattern string with **template-literal types** ({@link PathParams}) — no
|
|
17
|
+
* generated `.d.ts`, no dev server, and required params are typed as *required*
|
|
18
|
+
* (not optional). See ADR-0003.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
/** Split a pathname into non-empty segments (tolerates leading/trailing slashes). */
|
|
23
|
+
function splitPath(path) {
|
|
24
|
+
return path.split("/").filter((s) => s.length > 0);
|
|
25
|
+
}
|
|
26
|
+
/** Reject an empty param name (`/:` or `/:*`) so downstream never sees `params['']`. */
|
|
27
|
+
function requireName(name, pattern) {
|
|
28
|
+
if (name.length === 0) throw new RouterError("INVALID_PATTERN", `Param name cannot be empty in pattern "${pattern}".`);
|
|
29
|
+
return name;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parse a pattern into segments, validating it. Throws {@link RouterError}
|
|
33
|
+
* (`INVALID_PATTERN`) if a catch-all is not the final segment, or a param name
|
|
34
|
+
* is empty.
|
|
35
|
+
*/
|
|
36
|
+
function parsePattern(pattern) {
|
|
37
|
+
const segments = splitPath(pattern).map((s) => {
|
|
38
|
+
if (s.startsWith(":") && s.endsWith("*")) return {
|
|
39
|
+
kind: "catchAll",
|
|
40
|
+
value: requireName(s.slice(1, -1), pattern)
|
|
41
|
+
};
|
|
42
|
+
if (s.startsWith(":")) return {
|
|
43
|
+
kind: "param",
|
|
44
|
+
value: requireName(s.slice(1), pattern)
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
kind: "static",
|
|
48
|
+
value: s
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
const catchAllIndex = segments.findIndex((s) => s.kind === "catchAll");
|
|
52
|
+
if (catchAllIndex !== -1 && catchAllIndex !== segments.length - 1) throw new RouterError("INVALID_PATTERN", `Catch-all segment must be last in pattern "${pattern}".`);
|
|
53
|
+
return segments;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Match a `pathname` against a `pattern`. Returns the extracted params, or
|
|
57
|
+
* `null` if it does not match. Param values are URI-decoded.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* matchPattern('/posts/:id', '/posts/42') // { id: '42' }
|
|
61
|
+
* matchPattern('/files/:rest*', '/files/a/b') // { rest: 'a/b' }
|
|
62
|
+
* matchPattern('/about', '/contact') // null
|
|
63
|
+
*/
|
|
64
|
+
function matchPattern(pattern, pathname) {
|
|
65
|
+
const segments = parsePattern(pattern);
|
|
66
|
+
const parts = splitPath(pathname);
|
|
67
|
+
const params = {};
|
|
68
|
+
for (let i = 0; i < segments.length; i++) {
|
|
69
|
+
const seg = segments[i];
|
|
70
|
+
if (seg === void 0) return null;
|
|
71
|
+
if (seg.kind === "catchAll") {
|
|
72
|
+
params[seg.value] = parts.slice(i).map((p) => safeDecode(p)).join("/");
|
|
73
|
+
return params;
|
|
74
|
+
}
|
|
75
|
+
const part = parts[i];
|
|
76
|
+
if (part === void 0) return null;
|
|
77
|
+
if (seg.kind === "static") {
|
|
78
|
+
if (part !== seg.value) return null;
|
|
79
|
+
} else params[seg.value] = safeDecode(part);
|
|
80
|
+
}
|
|
81
|
+
return parts.length === segments.length ? params : null;
|
|
82
|
+
}
|
|
83
|
+
/** Decode a URI segment, falling back to the raw value on malformed input. */
|
|
84
|
+
function safeDecode(value) {
|
|
85
|
+
try {
|
|
86
|
+
return decodeURIComponent(value);
|
|
87
|
+
} catch {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Build a pathname from a `pattern` and `params`. Param values are URI-encoded.
|
|
93
|
+
* Throws {@link RouterError} (`MISSING_PARAM`) if a required dynamic param is
|
|
94
|
+
* absent. A missing/empty catch-all simply contributes nothing.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* buildPath('/posts/:id', { id: '42' }) // '/posts/42'
|
|
98
|
+
* buildPath('/files/:rest*', { rest: 'a/b' }) // '/files/a/b'
|
|
99
|
+
* buildPath('/about', {}) // '/about'
|
|
100
|
+
*/
|
|
101
|
+
function buildPath(pattern, params = {}) {
|
|
102
|
+
const segments = parsePattern(pattern);
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const seg of segments) {
|
|
105
|
+
if (seg.kind === "static") {
|
|
106
|
+
out.push(seg.value);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const value = params[seg.value];
|
|
110
|
+
if (seg.kind === "param") {
|
|
111
|
+
if (value === void 0 || value === "") throw new RouterError("MISSING_PARAM", `Missing value for required param ":${seg.value}" in pattern "${pattern}".`);
|
|
112
|
+
out.push(encodeURIComponent(String(value)));
|
|
113
|
+
} else if (value !== void 0 && value !== "") out.push(String(value).split("/").filter((s) => s.length > 0).map((s) => encodeURIComponent(s)).join("/"));
|
|
114
|
+
}
|
|
115
|
+
return `/${out.join("/")}`;
|
|
116
|
+
}
|
|
117
|
+
/** Per-segment specificity weights: static > param > (end of pattern) > catch-all. */
|
|
118
|
+
const SEGMENT_WEIGHT = {
|
|
119
|
+
static: 4,
|
|
120
|
+
param: 3,
|
|
121
|
+
catchAll: 1
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Weight for a "missing" segment slot (the pattern ended). It outranks a
|
|
125
|
+
* catch-all (so the root `/` beats a bare `/:rest*`) but loses to a static or
|
|
126
|
+
* dynamic segment (so a longer, more specific pattern still wins).
|
|
127
|
+
*/
|
|
128
|
+
const END_WEIGHT = 2;
|
|
129
|
+
/**
|
|
130
|
+
* Specificity score for a pattern: a per-segment weight tuple. Static segments
|
|
131
|
+
* outrank dynamic, which outrank a pattern's end, which outranks a catch-all.
|
|
132
|
+
* Used to sort routes so the most specific match wins.
|
|
133
|
+
*/
|
|
134
|
+
function score(pattern) {
|
|
135
|
+
return parsePattern(pattern).map((s) => SEGMENT_WEIGHT[s.kind]);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Compare two patterns by specificity. Returns a negative number if `a` is more
|
|
139
|
+
* specific than `b` (so `routes.sort(compareSpecificity)` puts the most specific
|
|
140
|
+
* first), positive if less specific, 0 if equal.
|
|
141
|
+
*/
|
|
142
|
+
function compareSpecificity(a, b) {
|
|
143
|
+
const sa = score(a);
|
|
144
|
+
const sb = score(b);
|
|
145
|
+
const len = Math.max(sa.length, sb.length);
|
|
146
|
+
for (let i = 0; i < len; i++) {
|
|
147
|
+
const wa = sa[i] ?? END_WEIGHT;
|
|
148
|
+
const wb = sb[i] ?? END_WEIGHT;
|
|
149
|
+
if (wa !== wb) return wb - wa;
|
|
150
|
+
}
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
export { buildPath, compareSpecificity, matchPattern, parsePattern };
|
|
155
|
+
|
|
156
|
+
//# sourceMappingURL=pattern.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pattern.js","names":[],"sources":["../src/pattern.ts"],"sourcesContent":["/**\n * Route patterns — matching, building, and **codegen-free** typed params.\n *\n * A pattern is a `/`-separated path where each segment is one of:\n * - a **static** segment (`posts`) — matches itself literally;\n * - a **dynamic** segment (`:postId`) — matches exactly one non-empty segment;\n * - a **catch-all** segment (`:rest*`) — must be last; matches the remaining\n * segments (zero or more), joined with `/`.\n *\n * This mirrors the manifest paths emitted by `@mindees/compiler`\n * (`buildRouteManifest`: `[param]` → `:param`, `[...rest]` → `:rest*`).\n *\n * The headline win over Expo Router / React Router: params are typed by parsing\n * the pattern string with **template-literal types** ({@link PathParams}) — no\n * generated `.d.ts`, no dev server, and required params are typed as *required*\n * (not optional). See ADR-0003.\n *\n * @module\n */\n\nimport { RouterError } from './errors'\n\n// ---------------------------------------------------------------------------\n// Type-level: infer params from a pattern string\n// ---------------------------------------------------------------------------\n\n/** Flatten an intersection of object types into a single readable object type. */\ntype Prettify<T> = { [K in keyof T]: T[K] } & {}\n\n/** The param contributed by a single pattern segment. */\ntype SegmentParam<Seg extends string> = Seg extends `:${infer Name}*`\n ? { [K in Name]: string }\n : Seg extends `:${infer Name}`\n ? { [K in Name]: string }\n : // biome-ignore lint/complexity/noBannedTypes: empty object = \"this segment adds no params\"\n {}\n\n/** Split a pattern on `/` into a tuple of segments. */\ntype SplitSegments<P extends string> = P extends `${infer Head}/${infer Tail}`\n ? [Head, ...SplitSegments<Tail>]\n : [P]\n\n/** Intersect the param contributions of every segment. */\ntype MergeSegments<Segs extends readonly string[]> = Segs extends [\n infer Head extends string,\n ...infer Tail extends string[],\n]\n ? SegmentParam<Head> & MergeSegments<Tail>\n : // biome-ignore lint/complexity/noBannedTypes: base case = no params\n {}\n\n/**\n * The params object for a pattern, inferred at the type level.\n *\n * @example\n * type A = PathParams<'/posts/:postId'> // { postId: string }\n * type B = PathParams<'/files/:rest*'> // { rest: string }\n * type C = PathParams<'/about'> // {}\n * type D = PathParams<'/u/:userId/p/:postId'> // { userId: string; postId: string }\n */\nexport type PathParams<P extends string> = Prettify<MergeSegments<SplitSegments<P>>>\n\n/** Whether a pattern has any dynamic params (used to make params required vs optional). */\nexport type HasPathParams<P extends string> = keyof PathParams<P> extends never ? false : true\n\n// ---------------------------------------------------------------------------\n// Runtime: parse, match, build\n// ---------------------------------------------------------------------------\n\ninterface Segment {\n readonly kind: 'static' | 'param' | 'catchAll'\n /** Static text, or the param name. */\n readonly value: string\n}\n\n/** Split a pathname into non-empty segments (tolerates leading/trailing slashes). */\nfunction splitPath(path: string): string[] {\n return path.split('/').filter((s) => s.length > 0)\n}\n\n/** Reject an empty param name (`/:` or `/:*`) so downstream never sees `params['']`. */\nfunction requireName(name: string, pattern: string): string {\n if (name.length === 0) {\n throw new RouterError('INVALID_PATTERN', `Param name cannot be empty in pattern \"${pattern}\".`)\n }\n return name\n}\n\n/**\n * Parse a pattern into segments, validating it. Throws {@link RouterError}\n * (`INVALID_PATTERN`) if a catch-all is not the final segment, or a param name\n * is empty.\n */\nexport function parsePattern(pattern: string): Segment[] {\n const raw = splitPath(pattern)\n const segments: Segment[] = raw.map((s) => {\n if (s.startsWith(':') && s.endsWith('*')) {\n return { kind: 'catchAll', value: requireName(s.slice(1, -1), pattern) }\n }\n if (s.startsWith(':')) {\n return { kind: 'param', value: requireName(s.slice(1), pattern) }\n }\n return { kind: 'static', value: s }\n })\n const catchAllIndex = segments.findIndex((s) => s.kind === 'catchAll')\n if (catchAllIndex !== -1 && catchAllIndex !== segments.length - 1) {\n throw new RouterError(\n 'INVALID_PATTERN',\n `Catch-all segment must be last in pattern \"${pattern}\".`,\n )\n }\n return segments\n}\n\n/**\n * Match a `pathname` against a `pattern`. Returns the extracted params, or\n * `null` if it does not match. Param values are URI-decoded.\n *\n * @example\n * matchPattern('/posts/:id', '/posts/42') // { id: '42' }\n * matchPattern('/files/:rest*', '/files/a/b') // { rest: 'a/b' }\n * matchPattern('/about', '/contact') // null\n */\nexport function matchPattern(pattern: string, pathname: string): Record<string, string> | null {\n const segments = parsePattern(pattern)\n const parts = splitPath(pathname)\n const params: Record<string, string> = {}\n\n for (let i = 0; i < segments.length; i++) {\n const seg = segments[i]\n // `seg` is always defined for i < length; the cast satisfies noUncheckedIndexedAccess.\n if (seg === undefined) return null\n if (seg.kind === 'catchAll') {\n params[seg.value] = parts\n .slice(i)\n .map((p) => safeDecode(p))\n .join('/')\n return params\n }\n const part = parts[i]\n if (part === undefined) return null\n if (seg.kind === 'static') {\n if (part !== seg.value) return null\n } else {\n params[seg.value] = safeDecode(part)\n }\n }\n\n // No catch-all consumed the tail: lengths must match exactly.\n return parts.length === segments.length ? params : null\n}\n\n/** Decode a URI segment, falling back to the raw value on malformed input. */\nfunction safeDecode(value: string): string {\n try {\n return decodeURIComponent(value)\n } catch {\n return value\n }\n}\n\n/**\n * Build a pathname from a `pattern` and `params`. Param values are URI-encoded.\n * Throws {@link RouterError} (`MISSING_PARAM`) if a required dynamic param is\n * absent. A missing/empty catch-all simply contributes nothing.\n *\n * @example\n * buildPath('/posts/:id', { id: '42' }) // '/posts/42'\n * buildPath('/files/:rest*', { rest: 'a/b' }) // '/files/a/b'\n * buildPath('/about', {}) // '/about'\n */\nexport function buildPath(pattern: string, params: Record<string, string | number> = {}): string {\n const segments = parsePattern(pattern)\n const out: string[] = []\n\n for (const seg of segments) {\n if (seg.kind === 'static') {\n out.push(seg.value)\n continue\n }\n const value = params[seg.value]\n if (seg.kind === 'param') {\n if (value === undefined || value === '') {\n throw new RouterError(\n 'MISSING_PARAM',\n `Missing value for required param \":${seg.value}\" in pattern \"${pattern}\".`,\n )\n }\n out.push(encodeURIComponent(String(value)))\n } else {\n // catch-all: optional; encode each sub-segment so '/' stays a separator.\n if (value !== undefined && value !== '') {\n out.push(\n String(value)\n .split('/')\n .filter((s) => s.length > 0)\n .map((s) => encodeURIComponent(s))\n .join('/'),\n )\n }\n }\n }\n\n return `/${out.join('/')}`\n}\n\n/** Per-segment specificity weights: static > param > (end of pattern) > catch-all. */\nconst SEGMENT_WEIGHT = { static: 4, param: 3, catchAll: 1 } as const\n/**\n * Weight for a \"missing\" segment slot (the pattern ended). It outranks a\n * catch-all (so the root `/` beats a bare `/:rest*`) but loses to a static or\n * dynamic segment (so a longer, more specific pattern still wins).\n */\nconst END_WEIGHT = 2\n\n/**\n * Specificity score for a pattern: a per-segment weight tuple. Static segments\n * outrank dynamic, which outrank a pattern's end, which outranks a catch-all.\n * Used to sort routes so the most specific match wins.\n */\nfunction score(pattern: string): number[] {\n return parsePattern(pattern).map((s) => SEGMENT_WEIGHT[s.kind])\n}\n\n/**\n * Compare two patterns by specificity. Returns a negative number if `a` is more\n * specific than `b` (so `routes.sort(compareSpecificity)` puts the most specific\n * first), positive if less specific, 0 if equal.\n */\nexport function compareSpecificity(a: string, b: string): number {\n const sa = score(a)\n const sb = score(b)\n const len = Math.max(sa.length, sb.length)\n for (let i = 0; i < len; i++) {\n const wa = sa[i] ?? END_WEIGHT\n const wb = sb[i] ?? END_WEIGHT\n if (wa !== wb) return wb - wa\n }\n return 0\n}\n"],"mappings":"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"}
|