@xmachines/play-pattern 5.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mikael Karon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # @xmachines/play-pattern
2
+
3
+ The route pattern language of the XMachines Play Architecture.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Version](https://img.shields.io/badge/version-5.0.0-blue)](https://www.npmjs.com/package/@xmachines/play-pattern)
6
+
7
+ One parse of the URLPattern pathname grammar, and the path that it builds back.
8
+
9
+ ## What this package is
10
+
11
+ `URLPattern` is the pattern language of XMachines. This package reads a route pattern ONE time and answers every question that a router asks about it, and it writes a path back from the same parse.
12
+
13
+ - **`parsePattern(pattern)`** — the parts, the params, which of them a path must fill, whether the pattern needs a match, the bucket key, the bare form, and the literal path.
14
+ - **`buildPath(pattern, params)`** — the path that the pattern describes, with each param filled. It reports a fault through its return value, and it throws never: `reason: "missing"` for a param that needs a value and has none, and `reason: "unresolvable"` for a param that carries a value no path can hold, such as a dot segment.
15
+ - **`getIndexKey(path)`** — the bucket key of a concrete path.
16
+ - **`isDotSegment(segment)`** — whether a URL resolves the segment away. `buildPath` reads it, and [`@xmachines/play-url`](../play-url/README.md) reads it for a base path, so ONE definition answers for both.
17
+
18
+ Both directions read the same parts, so a route that the match reads derives a URL that the match reads back.
19
+
20
+ ## What this package is NOT
21
+
22
+ It holds no `URLPattern` API. A parse is string work and it needs no constructor, so this package carries no polyfill. [`@xmachines/play-url`](../play-url/README.md) holds the API, the base path and the params of a framework router, and it reads this package for the language.
23
+
24
+ That line is why the package exists. [`@xmachines/play-xstate`](../play-xstate/README.md) BUILDS a URL and matches none, so it reads the grammar and installs no matching machinery for it.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pnpm add @xmachines/play-pattern
30
+ ```
31
+
32
+ Most applications install it through [`@xmachines/play-url`](../play-url/README.md), which re-exports the language whole. [`@xmachines/play-router`](../play-router/README.md) re-exports the part that a router needs: `parsePattern` and `createPatternCache`.
33
+
34
+ ## Usage
35
+
36
+ ```typescript
37
+ import { buildPath, parsePattern } from "@xmachines/play-pattern";
38
+
39
+ parsePattern("/settings/:section?").requiredNames; // []
40
+ parsePattern("/docs/:cat-id").names; // ["cat-id"]
41
+
42
+ buildPath("/profile/:userId", { userId: "alice" }); // { ok: true, path: "/profile/alice" }
43
+ buildPath("/settings/:section?", {}); // { ok: true, path: "/settings" }
44
+ buildPath("/profile/:userId", {}); // { ok: false, reason: "missing", missing: "userId" }
45
+ buildPath("/files/:name", { name: ".." }); // { ok: false, reason: "unresolvable", param: "name", value: ".." }
46
+ buildPath("/files/v:version", { version: ".." }); // { ok: true, path: "/files/v.." } — the value shares the segment
47
+ ```
48
+
49
+ ## The grammar
50
+
51
+ | form | meaning |
52
+ | ------------ | --------------------------------------------------------- |
53
+ | `/users` | a literal |
54
+ | `:name` | a named param, one path segment |
55
+ | `:name(\d+)` | a named param with a regular expression constraint |
56
+ | `(\d+)` | an anonymous param, numbered from `0` |
57
+ | `*` | an anonymous param that matches every remaining character |
58
+ | `{…}` | a group, which carries its own literals and params |
59
+ | `?` `+` `*` | the modifier of the part before it |
60
+ | `\:` | an escape, which makes the next character a literal |
61
+
62
+ A param name of this library holds a hyphen, which URLPattern does not: `/docs/:cat-id` is ONE param named `cat-id`, and `ParsedPattern.normalized` carries the form that URLPattern compiles. The [routing guide](../docs/guides/routing.md) states the divergence and which forms each direction reads.
63
+
64
+ ## License
65
+
66
+ MIT
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The cache contract of this package, and the default implementation of it.
3
+ *
4
+ * A parse is deterministic, so a cache changes no answer. It changes how often the parser
5
+ * runs, and it changes nothing else — which is what makes the contract below safe to
6
+ * adopt one caller at a time. A caller that passes no cache reads the shared default, and
7
+ * a caller that passes one reads that one. The two never disagree.
8
+ *
9
+ * ## Why a caller brings one
10
+ *
11
+ * The default cache is one object for the whole process, and a process serves more than
12
+ * one caller:
13
+ *
14
+ * - A server renders for many tenants. One route table for each tenant, and a shared
15
+ * cache that each tenant evicts for the others.
16
+ * - A test measures the parser and reaches a parse that an earlier test wrote.
17
+ * - A worker lives longer than a page, and the bound of a page is arbitrary for it.
18
+ * - A host owns an LRU already, and one eviction policy for every cache it holds.
19
+ *
20
+ * @see [Routing guide](../../docs/guides/routing.md)
21
+ */
22
+ import type { ParsedPattern } from "./pattern-grammar.js";
23
+ /**
24
+ * The options that every read of the pattern language takes.
25
+ *
26
+ * It is ONE field today, and it is an object rather than a positional argument so that a
27
+ * caller holds one object and hands it to every call — `parsePattern`, `buildPath`,
28
+ * `getCompiledPattern`, `findRouteByPath`, a route map and a bridge all take it, and
29
+ * `CompileOptions` and `RouteMapOptions` extend it with the caches of their own layer. A
30
+ * positional argument gave each layer a different position to remember.
31
+ */
32
+ export interface ParseOptions {
33
+ /**
34
+ * The cache of the PARSE of a pattern.
35
+ *
36
+ * The default is the cache that this package shares with every caller of the process.
37
+ */
38
+ readonly patternCache?: PatternCache<ParsedPattern> | undefined;
39
+ }
40
+ /**
41
+ * What this package needs from a cache: a read and a write, both by a string key.
42
+ *
43
+ * The contract is this small on purpose. A `Map` satisfies it, and so does an LRU of any
44
+ * library — a caller that wants a time to live or an eviction callback brings the module
45
+ * that has one, and this package needs to know nothing about it.
46
+ *
47
+ * A cache may forget an entry at any time, and it may forget none. Both are correct: the
48
+ * parser runs again for an entry that is absent.
49
+ */
50
+ export interface PatternCache<V> {
51
+ /** Reads one entry, or `undefined` when the cache holds none for the key. */
52
+ get(key: string): V | undefined;
53
+ /** Writes one entry. */
54
+ set(key: string, value: V): void;
55
+ }
56
+ /**
57
+ * Builds a cache that forgets its least recently used entries.
58
+ *
59
+ * ## The algorithm
60
+ *
61
+ * Two maps, and no list. `recent` takes every write. When a write fills `recent`, it
62
+ * becomes `old` and a new `recent` opens, which drops the previous `old` whole. A read of
63
+ * a key of `old` moves the entry into `recent`, and THAT is what makes the eviction an
64
+ * LRU one rather than a FIFO one: an entry that every lookup reads moves forward, and an
65
+ * entry that nothing reads falls out with the map that holds it.
66
+ *
67
+ * The cache therefore holds between `maxSize` and `2 * maxSize` entries, and it does no
68
+ * bookkeeping for a read that hits `recent`.
69
+ *
70
+ * @param maxSize - The number of entries that `recent` holds. It is `1` at the least, and
71
+ * the default is `500`. A value that is not a finite number reads as the default, so a
72
+ * cache with no eviction at all cannot happen by accident.
73
+ * @returns A cache. It is not shared: each call builds one.
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * const cache = createPatternCache<ParsedPattern>(50);
78
+ * parsePattern("/profile/:userId", { patternCache: cache });
79
+ * ```
80
+ */
81
+ export declare function createPatternCache<V>(maxSize?: number): PatternCache<V>;
82
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,YAAY,CAAC,aAAa,CAAC,GAAG,SAAS,CAAC;CAChE;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC9B,6EAA6E;IAC7E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;IAChC,wBAAwB;IACxB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,SAAM,GAAG,YAAY,CAAC,CAAC,CAAC,CAqCpE"}
package/dist/cache.js ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The cache contract of this package, and the default implementation of it.
3
+ *
4
+ * A parse is deterministic, so a cache changes no answer. It changes how often the parser
5
+ * runs, and it changes nothing else — which is what makes the contract below safe to
6
+ * adopt one caller at a time. A caller that passes no cache reads the shared default, and
7
+ * a caller that passes one reads that one. The two never disagree.
8
+ *
9
+ * ## Why a caller brings one
10
+ *
11
+ * The default cache is one object for the whole process, and a process serves more than
12
+ * one caller:
13
+ *
14
+ * - A server renders for many tenants. One route table for each tenant, and a shared
15
+ * cache that each tenant evicts for the others.
16
+ * - A test measures the parser and reaches a parse that an earlier test wrote.
17
+ * - A worker lives longer than a page, and the bound of a page is arbitrary for it.
18
+ * - A host owns an LRU already, and one eviction policy for every cache it holds.
19
+ *
20
+ * @see [Routing guide](../../docs/guides/routing.md)
21
+ */
22
+ /**
23
+ * Builds a cache that forgets its least recently used entries.
24
+ *
25
+ * ## The algorithm
26
+ *
27
+ * Two maps, and no list. `recent` takes every write. When a write fills `recent`, it
28
+ * becomes `old` and a new `recent` opens, which drops the previous `old` whole. A read of
29
+ * a key of `old` moves the entry into `recent`, and THAT is what makes the eviction an
30
+ * LRU one rather than a FIFO one: an entry that every lookup reads moves forward, and an
31
+ * entry that nothing reads falls out with the map that holds it.
32
+ *
33
+ * The cache therefore holds between `maxSize` and `2 * maxSize` entries, and it does no
34
+ * bookkeeping for a read that hits `recent`.
35
+ *
36
+ * @param maxSize - The number of entries that `recent` holds. It is `1` at the least, and
37
+ * the default is `500`. A value that is not a finite number reads as the default, so a
38
+ * cache with no eviction at all cannot happen by accident.
39
+ * @returns A cache. It is not shared: each call builds one.
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const cache = createPatternCache<ParsedPattern>(50);
44
+ * parsePattern("/profile/:userId", { patternCache: cache });
45
+ * ```
46
+ */
47
+ export function createPatternCache(maxSize = 500) {
48
+ // `Math.max(1, NaN)` answers `NaN`, and `recent.size > NaN` is false for ever, so a
49
+ // non-finite argument turned the bound off and the cache grew for the life of the
50
+ // process. A caller reads the size from a configuration value often enough — an absent
51
+ // environment variable gives `Number(undefined)` — so the guard reads the number
52
+ // itself, and it falls back to the default that the parameter names.
53
+ const bound = Number.isFinite(maxSize) ? Math.max(1, Math.floor(maxSize)) : 500;
54
+ let recent = new Map();
55
+ let old = new Map();
56
+ const write = (key, value) => {
57
+ recent.set(key, value);
58
+ if (recent.size > bound) {
59
+ old = recent;
60
+ recent = new Map();
61
+ }
62
+ };
63
+ return {
64
+ get(key) {
65
+ const hit = recent.get(key);
66
+ // A `has` costs a second lookup, and a stored `undefined` is the only value that
67
+ // this tells apart. No caller of this package stores one: a parse is an object,
68
+ // and a path match stores `null` for "no match".
69
+ if (hit !== undefined)
70
+ return hit;
71
+ const stale = old.get(key);
72
+ if (stale === undefined)
73
+ return undefined;
74
+ // The entry survives, so the read promotes it. Without the promotion the eviction
75
+ // drops an entry that every lookup reads.
76
+ old.delete(key);
77
+ write(key, stale);
78
+ return stale;
79
+ },
80
+ set: write,
81
+ };
82
+ }
83
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAuCH;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,kBAAkB,CAAI,OAAO,GAAG,GAAG;IAClD,oFAAoF;IACpF,kFAAkF;IAClF,uFAAuF;IACvF,iFAAiF;IACjF,qEAAqE;IACrE,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAChF,IAAI,MAAM,GAAG,IAAI,GAAG,EAAa,CAAC;IAClC,IAAI,GAAG,GAAG,IAAI,GAAG,EAAa,CAAC;IAE/B,MAAM,KAAK,GAAG,CAAC,GAAW,EAAE,KAAQ,EAAQ,EAAE;QAC7C,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACvB,IAAI,MAAM,CAAC,IAAI,GAAG,KAAK,EAAE,CAAC;YACzB,GAAG,GAAG,MAAM,CAAC;YACb,MAAM,GAAG,IAAI,GAAG,EAAE,CAAC;QACpB,CAAC;IACF,CAAC,CAAC;IAEF,OAAO;QACN,GAAG,CAAC,GAAW;YACd,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC5B,iFAAiF;YACjF,gFAAgF;YAChF,iDAAiD;YACjD,IAAI,GAAG,KAAK,SAAS;gBAAE,OAAO,GAAG,CAAC;YAElC,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,SAAS,CAAC;YAE1C,kFAAkF;YAClF,0CAA0C;YAC1C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAChB,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YAClB,OAAO,KAAK,CAAC;QACd,CAAC;QACD,GAAG,EAAE,KAAK;KACV,CAAC;AACH,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The route pattern language of XMachines.
3
+ *
4
+ * `URLPattern` is that language, and this package reads it ONE time for each pattern.
5
+ * `parsePattern` answers every question that a router asks about a route — which params
6
+ * the pattern declares, which of them a path must fill, whether the pattern needs a match
7
+ * at all, which bucket it belongs to, and which path it matches when every optional part
8
+ * is absent. `buildPath` walks the SAME parts back into a path, so a route that the match
9
+ * reads derives a URL that the match reads back.
10
+ *
11
+ * ## What this package does NOT hold
12
+ *
13
+ * The URLPattern API. A parse is string work, and it needs no constructor: this package
14
+ * therefore carries no polyfill. `@xmachines/play-url` holds the API, the base path and
15
+ * the params of a framework router, and it reads this package for the language.
16
+ *
17
+ * That line is the reason the package exists. `@xmachines/play-xstate` BUILDS a URL and
18
+ * it matches none, so it reads the grammar and it installs no matching machinery for it.
19
+ *
20
+ * @packageDocumentation
21
+ * @see [Routing guide](../../docs/guides/routing.md)
22
+ */
23
+ export { buildPath, getIndexKey, isDotSegment, parsePattern } from "./pattern-grammar.js";
24
+ export { createPatternCache } from "./cache.js";
25
+ export type { ParseOptions, PatternCache } from "./cache.js";
26
+ export type { GroupPart, LiteralPart, ParamPart, ParsedPattern, PathBuildResult, PathParams, PatternModifier, PatternParam, PatternPart, } from "./pattern-grammar.js";
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC7D,YAAY,EACX,SAAS,EACT,WAAW,EACX,SAAS,EACT,aAAa,EACb,eAAe,EACf,UAAU,EACV,eAAe,EACf,YAAY,EACZ,WAAW,GACX,MAAM,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The route pattern language of XMachines.
3
+ *
4
+ * `URLPattern` is that language, and this package reads it ONE time for each pattern.
5
+ * `parsePattern` answers every question that a router asks about a route — which params
6
+ * the pattern declares, which of them a path must fill, whether the pattern needs a match
7
+ * at all, which bucket it belongs to, and which path it matches when every optional part
8
+ * is absent. `buildPath` walks the SAME parts back into a path, so a route that the match
9
+ * reads derives a URL that the match reads back.
10
+ *
11
+ * ## What this package does NOT hold
12
+ *
13
+ * The URLPattern API. A parse is string work, and it needs no constructor: this package
14
+ * therefore carries no polyfill. `@xmachines/play-url` holds the API, the base path and
15
+ * the params of a framework router, and it reads this package for the language.
16
+ *
17
+ * That line is the reason the package exists. `@xmachines/play-xstate` BUILDS a URL and
18
+ * it matches none, so it reads the grammar and it installs no matching machinery for it.
19
+ *
20
+ * @packageDocumentation
21
+ * @see [Routing guide](../../docs/guides/routing.md)
22
+ */
23
+ export { buildPath, getIndexKey, isDotSegment, parsePattern } from "./pattern-grammar.js";
24
+ export { createPatternCache } from "./cache.js";
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,295 @@
1
+ /**
2
+ * The pattern language of this library, read and written in ONE place.
3
+ *
4
+ * `parsePattern` reads a pattern into parts, and `buildPath` walks those same parts back
5
+ * into a path. Both directions therefore hold one grammar, and neither can drift from the
6
+ * other. `index.ts` exports both.
7
+ *
8
+ * `URLPattern` is the pattern language of XMachines, and this module is the only reader
9
+ * of it. Every question that the router asks about a route pattern comes from one parse:
10
+ * which params the pattern declares, which of them a path must fill, whether the pattern
11
+ * needs a match at all, which bucket it belongs to, and which path the pattern matches
12
+ * when every optional part is absent.
13
+ *
14
+ * Four regular expressions asked those questions before, each over a different subset of
15
+ * the grammar, and they disagreed. `getRequiredPatternParamNames` read a bare `?` alone,
16
+ * so it called `:path*` and `{/:id}?` required. `isParameterizedPattern` looked for a
17
+ * `:` or a `*`, so it called `/a{/b}?/c` a static path and sent it to the exact-match
18
+ * map, where it matched nothing. One parse answers all of them, and the answers cannot
19
+ * move apart.
20
+ *
21
+ * ## The grammar
22
+ *
23
+ * The parser reads the pathname grammar of URLPattern:
24
+ *
25
+ * | form | meaning |
26
+ * | --- | --- |
27
+ * | `/users` | a literal |
28
+ * | `:name` | a named param, one path segment |
29
+ * | `:name(\d+)` | a named param with a regular expression constraint |
30
+ * | `(\d+)` | an anonymous param, numbered from `0` |
31
+ * | `*` | an anonymous param that matches every remaining character |
32
+ * | `{…}` | a group, which carries its own literals and params |
33
+ * | `?` `+` `*` | the modifier of the part before it: optional, one or more, zero or more |
34
+ * | `\:` | an escape, which makes the next character a literal |
35
+ *
36
+ * ## The prefix rule
37
+ *
38
+ * A `/` directly before a param belongs TO that param, and a `/` directly before a group
39
+ * does not. `/settings/:section?` therefore matches `/settings`, and `/a/{b}?/c` matches
40
+ * `/a//c` and not `/a/c`. The rule decides every bare form below, and each case of it is
41
+ * measured against the native API and against the polyfill in
42
+ * `test/pattern-grammar.test.ts`.
43
+ *
44
+ * ## The one divergence from URLPattern
45
+ *
46
+ * A param name of URLPattern is a JavaScript identifier, and it holds no hyphen:
47
+ * `/docs/:cat-id` is the param `cat` and the literal `-id` for the standard. This library
48
+ * reads `cat-id` as ONE name and compiles it as `:cat_id`, because a route of a machine
49
+ * names a param the way the application does. The guide states the divergence, and
50
+ * {@link ParsedPattern.normalized} carries the form that URLPattern compiles.
51
+ */
52
+ import type { ParseOptions } from "./cache.js";
53
+ /** The modifier of a part. `""` means that the part carries none. */
54
+ export type PatternModifier = "" | "?" | "+" | "*";
55
+ /** A run of literal characters, with every escape already resolved. */
56
+ export interface LiteralPart {
57
+ readonly kind: "literal";
58
+ /** The characters that a path must carry, for example `/settings`. */
59
+ readonly text: string;
60
+ }
61
+ /**
62
+ * A param: `:name`, `(\d+)`, or `*`.
63
+ *
64
+ * An anonymous param carries no `name`. URLPattern numbers such a group from `0`, and
65
+ * this library reports it to no machine — see {@link ParsedPattern.params}.
66
+ */
67
+ export interface ParamPart {
68
+ readonly kind: "param";
69
+ /** The declared name, with each hyphen kept, or `null` for an anonymous param. */
70
+ readonly name: string | null;
71
+ /**
72
+ * The name that URLPattern compiles, with each hyphen rewritten to an underscore, or
73
+ * `null` for an anonymous param.
74
+ *
75
+ * The rewrite happens ONE time, here, and {@link ParsedPattern.normalized} carries the
76
+ * same characters. A caller that needs the group name of a param reads this field, and
77
+ * it rewrites nothing of its own.
78
+ */
79
+ readonly groupName: string | null;
80
+ /** The source of the constraint of `:id(\d+)`, or `null` when the param declares none. */
81
+ readonly regexp: string | null;
82
+ /** The `/` that the param absorbed from the literal before it, or `""`. */
83
+ readonly prefix: string;
84
+ readonly modifier: PatternModifier;
85
+ }
86
+ /** A `{…}` group, which carries its own parts and its own modifier. */
87
+ export interface GroupPart {
88
+ readonly kind: "group";
89
+ readonly parts: readonly PatternPart[];
90
+ readonly modifier: PatternModifier;
91
+ }
92
+ export type PatternPart = LiteralPart | ParamPart | GroupPart;
93
+ /** One named param of a pattern, with the question that every caller asks about it. */
94
+ export interface PatternParam {
95
+ /** The name as the pattern declares it, for example `cat-id`. */
96
+ readonly name: string;
97
+ /** The name that URLPattern compiles, for example `cat_id`. */
98
+ readonly groupName: string;
99
+ /**
100
+ * True when a matching path can carry no value for this param.
101
+ *
102
+ * The modifier `?` and the modifier `*` make a param optional, and so does EVERY
103
+ * group above it that carries one of the two. The modifier `+` does not: it repeats a
104
+ * param that one value must still fill.
105
+ */
106
+ readonly optional: boolean;
107
+ }
108
+ /** Everything that this library reads from one route pattern. */
109
+ export interface ParsedPattern {
110
+ /** The pattern as the caller wrote it. */
111
+ readonly source: string;
112
+ /** The parts, in the order that the pattern declares them. */
113
+ readonly parts: readonly PatternPart[];
114
+ /** The NAMED params, in the order that the pattern declares them. */
115
+ readonly params: readonly PatternParam[];
116
+ /**
117
+ * The name of every param of {@link params}, in the same order.
118
+ *
119
+ * `resolveFrameworkParams` runs on EVERY navigation and it needs this list each time.
120
+ * The parse holds it, so a navigation allocates none. A caller that hands the array to
121
+ * somebody else copies it: the cache holds this one.
122
+ */
123
+ readonly names: readonly string[];
124
+ /**
125
+ * The name of every param that a path must fill, in the order of {@link params}.
126
+ *
127
+ * The same rule as {@link names}: the parse holds the array, and a navigation reads it.
128
+ */
129
+ readonly requiredNames: readonly string[];
130
+ /**
131
+ * True when the pattern needs a URLPattern match, and false when it is a literal path.
132
+ *
133
+ * A malformed pattern reports `true`, so that it reaches the URLPattern constructor
134
+ * and fails there with the error of the API. A parser that called it static would
135
+ * send it to the exact-match map instead, where it matches nothing and reports no
136
+ * fault.
137
+ */
138
+ readonly parameterized: boolean;
139
+ /**
140
+ * The path that the pattern matches when every optional part is absent, or `null`
141
+ * when the pattern holds a part that a path must fill.
142
+ *
143
+ * `/settings/:section?` gives `/settings`, `/a/{b}?/c` gives `/a//c`, and
144
+ * `/profile/:userId` gives `null`.
145
+ */
146
+ readonly bareForm: string | null;
147
+ /**
148
+ * The ONE path that the pattern matches, with each escape resolved, or `null` when the
149
+ * pattern describes a SET of paths.
150
+ *
151
+ * `/users` gives `/users`, `/tags/c\+\+` gives `/tags/c++`, `/x{y}z` gives `/xyz`, and
152
+ * `/profile/:userId` gives `null`.
153
+ *
154
+ * A bridge writes a real browser URL from this field. The SOURCE of the pattern is no
155
+ * substitute for it: `/tags/c\+\+` carries two backslashes that the address bar must
156
+ * not hold, and a location that carries them matches the route never.
157
+ */
158
+ readonly literalPath: string | null;
159
+ /**
160
+ * The key of the bucket index: the first path segment, or `"*"` when a param can
161
+ * stand in the first segment.
162
+ */
163
+ readonly indexKey: string;
164
+ /**
165
+ * The pattern in the form that URLPattern compiles: each param name that holds a
166
+ * hyphen carries an underscore instead.
167
+ */
168
+ readonly normalized: string;
169
+ /**
170
+ * The map from each rewritten group name back to the name that the pattern declares,
171
+ * for example `cat_id` to `cat-id`.
172
+ *
173
+ * The map holds an entry only for a name that the rewrite changed. It is empty for
174
+ * most patterns, and a caller that reads a group name falls back to the name itself.
175
+ */
176
+ readonly declaredNames: ReadonlyMap<string, string>;
177
+ }
178
+ /**
179
+ * Computes the key of the bucket index of a concrete PATH.
180
+ *
181
+ * The key is the first path segment, for example `"settings"` of `"/settings/billing"`.
182
+ * The root path `"/"` gives the key `"/"`, and a path whose first segment is EMPTY —
183
+ * `"//docs"` — gives the key `""`. The two are different keys, so a pattern that matches
184
+ * both asks {@link ParsedPattern.indexKey} for the wildcard key instead.
185
+ *
186
+ * {@link ParsedPattern.indexKey} computes the key of a PATTERN, and it calls this
187
+ * function for every pattern whose first segment is a literal. One function therefore
188
+ * answers both sides, and a pattern in the wrong bucket cannot happen.
189
+ *
190
+ * @param path - The string of a URL path.
191
+ */
192
+ export declare function getIndexKey(path: string): string;
193
+ /**
194
+ * Reads a route pattern, one time for each distinct pattern.
195
+ *
196
+ * The parser reports a fault never. A pattern that it cannot read reports
197
+ * `parameterized: true` and `bareForm: null`, which sends it to the URLPattern
198
+ * constructor and keeps every derived answer on the safe side.
199
+ *
200
+ * @param pattern - The route pattern, for example `/profile/:userId`.
201
+ * @returns The parse. The caller treats it as read-only: the cache holds this object.
202
+ */
203
+ export declare function parsePattern(pattern: string, options?: ParseOptions): ParsedPattern;
204
+ /** The answer of {@link buildPath}. */
205
+ export type PathBuildResult =
206
+ /** The pattern built a path, and every part that needs a value has one. */
207
+ {
208
+ readonly ok: true;
209
+ readonly path: string;
210
+ }
211
+ /**
212
+ * A part needs a value that the caller did not give.
213
+ *
214
+ * `missing` names the param as the PATTERN declares it, with each hyphen kept, so a
215
+ * caller reports the name that its author wrote.
216
+ */
217
+ | {
218
+ readonly ok: false;
219
+ readonly reason: "missing";
220
+ readonly missing: string;
221
+ }
222
+ /**
223
+ * A param carries a value that no path can hold, so the pattern matches the result
224
+ * back never. `param` names the param, and `value` holds what the caller gave.
225
+ *
226
+ * A dot segment is the one such value today: see {@link isDotSegment}.
227
+ */
228
+ | {
229
+ readonly ok: false;
230
+ readonly reason: "unresolvable";
231
+ readonly param: string;
232
+ readonly value: string;
233
+ };
234
+ /**
235
+ * The values that a caller gives, keyed by the declared name of the param.
236
+ *
237
+ * The value is `unknown`, because a caller reads it from the context of an actor and that
238
+ * context carries whatever the machine put there. {@link buildPath} calls `String` on a
239
+ * value that is neither `undefined` nor `null`.
240
+ */
241
+ export type PathParams = Readonly<Record<string, unknown>>;
242
+ /**
243
+ * Builds the path that a pattern describes, with each param filled from `params`.
244
+ *
245
+ * This is the OUTBOUND half of the language, and it reads the same parse as the match.
246
+ * The walk drops an optional part that has no value TOGETHER with the `/` that the part
247
+ * absorbed, which is the prefix rule of the module comment: `/settings/:section?` builds
248
+ * `/settings`, and not `/settings/`.
249
+ *
250
+ * The function reports a fault through its return value, and it throws never. The
251
+ * `reason` field names the fault: `missing` for a param that needs a value and has none,
252
+ * and `unresolvable` for a param that carries a value no path can hold. A caller raises
253
+ * the error of its own layer — `@xmachines/play-xstate` raises `MissingRouteParamError`
254
+ * for the first one and `InvalidRouteParamError` for the second.
255
+ *
256
+ * @param pattern - The route pattern, for example `/profile/:userId`.
257
+ * @param params - The value of each param, keyed by the name that the pattern declares.
258
+ * @returns The path, or the reason that stopped the walk with the param that carries it.
259
+ *
260
+ * @example
261
+ * ```typescript
262
+ * buildPath("/profile/:userId", { userId: "alice" }); // { ok: true, path: "/profile/alice" }
263
+ * buildPath("/settings/:section?", {}); // { ok: true, path: "/settings" }
264
+ * buildPath("/tags/c\\+\\+", {}); // { ok: true, path: "/tags/c++" }
265
+ * buildPath("/profile/:userId", {}); // { ok: false, reason: "missing", missing: "userId" }
266
+ * buildPath("/files/:name", { name: ".." }); // { ok: false, reason: "unresolvable", … }
267
+ * ```
268
+ */
269
+ export declare function buildPath(pattern: string, params?: PathParams, options?: ParseOptions): PathBuildResult;
270
+ /**
271
+ * Answers whether one segment resolves away in a URL, rather than naming itself.
272
+ *
273
+ * A URL parser reads the dot segments BEFORE it decodes the percent escapes, and it counts
274
+ * `%2e` as the dot. The WHATWG URL standard names six forms, and a browser resolves every
275
+ * one of them out of a location: `.` and `%2e` for the single dot, and `..`, `.%2e`, `%2e.`
276
+ * and `%2e%2e` for the double dot. Each percent test is case-insensitive.
277
+ *
278
+ * NO encoding of `.` or `..` therefore survives. A param that carries one describes a path
279
+ * that its own pattern matches back never, so {@link buildPath} refuses it, and
280
+ * `normalizeBasePath` of `@xmachines/play-url` refuses the same value in a base path.
281
+ *
282
+ * Give the segment in the form that the URL carries, and encode nothing here. The two
283
+ * callers reach that form by two roads, and both are correct. {@link buildPath} encodes a
284
+ * param value first, so a value that HOLDS a percent sign arrives with that sign escaped:
285
+ * the string `"%2e"` encodes to `"%252e"`, which names itself and stays. `normalizeBasePath`
286
+ * of `@xmachines/play-url` reads a path that an author wrote, which is encoded already, so
287
+ * it passes the segment RAW and `"%2e"` there is the dot. A caller that encodes an
288
+ * already-encoded segment a second time hides every `%2e` from this test.
289
+ *
290
+ * @param segment - ONE path segment, with no separator around it, in the form that the URL
291
+ * carries it.
292
+ * @returns `true` when a URL resolves the segment away.
293
+ */
294
+ export declare function isDotSegment(segment: string): boolean;
295
+ //# sourceMappingURL=pattern-grammar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pattern-grammar.d.ts","sourceRoot":"","sources":["../src/pattern-grammar.ts"],"names":[],"mappings":"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"}