@xmachines/play-router 6.1.0 → 7.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/README.md +73 -16
- package/dist/base-route-map.js +240 -270
- package/dist/base-route-map.js.map +1 -1
- package/dist/build-tree.js +84 -96
- package/dist/build-tree.js.map +1 -1
- package/dist/create-route-map-from-tree.js +50 -47
- package/dist/create-route-map-from-tree.js.map +1 -1
- package/dist/errors.js +124 -133
- package/dist/errors.js.map +1 -1
- package/dist/find-route.js +88 -105
- package/dist/find-route.js.map +1 -1
- package/dist/index.js +10 -28
- package/dist/provider-lifecycle.js +109 -143
- package/dist/provider-lifecycle.js.map +1 -1
- package/dist/router-bridge-base.d.ts +4 -4
- package/dist/router-bridge-base.d.ts.map +1 -1
- package/dist/router-bridge-base.js +1026 -1601
- package/dist/router-bridge-base.js.map +1 -1
- package/dist/router-sync.js +107 -139
- package/dist/router-sync.js.map +1 -1
- package/dist/validate-routes.js +58 -66
- package/dist/validate-routes.js.map +1 -1
- package/dist/xstate/create-route-map.js +36 -35
- package/dist/xstate/create-route-map.js.map +1 -1
- package/dist/xstate/extract-routes.js +66 -83
- package/dist/xstate/extract-routes.js.map +1 -1
- package/dist/xstate/index.js +5 -33
- package/dist/xstate/machine-to-graph.js +98 -109
- package/dist/xstate/machine-to-graph.js.map +1 -1
- package/dist/xstate/query.js +190 -212
- package/dist/xstate/query.js.map +1 -1
- package/package.json +16 -16
- package/dist/index.js.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/xstate/index.js.map +0 -1
- package/dist/xstate/machine-types.js +0 -2
- package/dist/xstate/machine-types.js.map +0 -1
package/dist/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["import { PlayError } from \"@xmachines/play/errors\";\n\n// The errors of the URL language live in `@xmachines/play-url`, with the parser that\n// raises them. They are re-exported here, so that a consumer of this package sorts every\n// routing error from one import, exactly as before the split.\n//\n// The specifier is the `./errors` subpath, and not the root barrel. A consumer that\n// imports `@xmachines/play-router/errors` to name a fault in a `catch` then loads the\n// three classes alone: no pattern parser, no base path, and no `urlpattern-polyfill`. The\n// root barrel of this package reaches the rest of `@xmachines/play-url` for the consumer\n// that needs it.\nexport {\n\tInvalidRoutePatternError,\n\tInvalidBasePathError,\n\tMissingBasePathParamError,\n} from \"@xmachines/play-url/errors\";\n\n/**\n * The bridge throws this error when `RouterBridgeBase.syncActorFromRouter()`\n * cannot process a location change of the router and send the `play.route` event to\n * the actor.\n *\n * The `cause` property holds the original error.\n *\n * **Error code:** `PLAY_ROUTER_SYNC_FAILED`\n *\n * @example\n * ```typescript\n * import { RouterSyncError } from \"@xmachines/play-router/errors\";\n *\n * try {\n * bridge.connect();\n * } catch (err) {\n * if (err instanceof RouterSyncError) {\n * console.error(\"Router sync failed:\", err.message, err.cause);\n * }\n * }\n * ```\n */\nexport class RouterSyncError extends PlayError {\n\tconstructor(message: string, options?: ErrorOptions) {\n\t\tsuper(\"RouterBridgeBase\", \"PLAY_ROUTER_SYNC_FAILED\", message, options);\n\t\tthis.name = \"RouterSyncError\";\n\t}\n}\n\n/**\n * `RouterBridgeBase.connect()` throws this error when a second bridge connects to an\n * actor with an active bridge connection.\n *\n * One actor can hold one router bridge at a time. Two bridges of the same actor both\n * receive and process every navigation event of the router. They therefore send two\n * `play.route` events to the actor, and their `lastSyncedPath` values of the echo\n * suppression contradict each other.\n *\n * **The remedy:** call `bridge.disconnect()` on the first bridge before you connect\n * the second one. You can also permit one active `connectRouter()` call for each\n * actor at a time.\n *\n * **Error code:** `PLAY_ROUTER_DUPLICATE_BRIDGE`\n *\n * @example\n * ```typescript\n * import { DuplicateBridgeError } from \"@xmachines/play-router/errors\";\n *\n * try {\n * bridge.connect();\n * } catch (err) {\n * if (err instanceof DuplicateBridgeError) {\n * console.error(\"Actor already has an active router bridge — disconnect first.\");\n * }\n * }\n * ```\n */\nexport class DuplicateBridgeError extends PlayError {\n\tconstructor() {\n\t\tsuper(\n\t\t\t\"RouterBridgeBase\",\n\t\t\t\"PLAY_ROUTER_DUPLICATE_BRIDGE\",\n\t\t\t\"An actor can only be connected to one router bridge at a time. \" +\n\t\t\t\t\"Call disconnect() on the existing bridge before connecting a new one.\",\n\t\t);\n\t\tthis.name = \"DuplicateBridgeError\";\n\t}\n}\n\n/**\n * `validateRouteFormat()` throws this error when a state declares a `meta.route`\n * field with an empty string as its path.\n *\n * **Error code:** `PLAY_ROUTE_EMPTY_PATH`\n */\nexport class EmptyRoutePathError extends PlayError {\n\t/** The state ID with the empty route path. */\n\treadonly stateId: string;\n\n\tconstructor(stateId: string) {\n\t\tsuper(\n\t\t\t\"RouteMap\",\n\t\t\t\"PLAY_ROUTE_EMPTY_PATH\",\n\t\t\t`Empty route path in state \"${stateId}\": routes must have a non-empty path`,\n\t\t);\n\t\tthis.name = \"EmptyRoutePathError\";\n\t\tthis.stateId = stateId;\n\t}\n}\n\n/**\n * `validateStateExists()` throws this error when a route names a state ID that the\n * machine graph does not hold.\n *\n * **Error code:** `PLAY_ROUTE_INVALID_STATE_ID`\n */\nexport class InvalidStateIdError extends PlayError {\n\t/** The state ID of the reference. The graph does not hold it. */\n\treadonly stateId: string;\n\n\tconstructor(stateId: string) {\n\t\tsuper(\n\t\t\t\"RouteMap\",\n\t\t\t\"PLAY_ROUTE_INVALID_STATE_ID\",\n\t\t\t`Route references non-existent state ID: ${stateId}`,\n\t\t);\n\t\tthis.name = \"InvalidStateIdError\";\n\t\tthis.stateId = stateId;\n\t}\n}\n\n/**\n * `detectDuplicateRoutes()` throws this error when two states or more hold the same\n * URL path.\n *\n * **Error code:** `PLAY_ROUTE_DUPLICATE_PATH`\n */\nexport class DuplicateRoutePathError extends PlayError {\n\t/** The list of the path → state-ID mappings of the conflict, for a person to read. */\n\treadonly duplicates: string[];\n\n\tconstructor(duplicates: string[]) {\n\t\tsuper(\n\t\t\t\"RouteMap\",\n\t\t\t\"PLAY_ROUTE_DUPLICATE_PATH\",\n\t\t\t`Duplicate route paths detected:\\n${duplicates.join(\"\\n\")}\\n\\n` +\n\t\t\t\t`Each route path must map to exactly one state.\\n` +\n\t\t\t\t`Use different paths (e.g., /dashboard vs /) or conditional rendering within a single state.`,\n\t\t);\n\t\tthis.name = \"DuplicateRoutePathError\";\n\t\tthis.duplicates = duplicates;\n\t}\n}\n\n/**\n * `machineToGraph()` throws this error when an XState state node has a `.type` value\n * that the package does not know.\n *\n * **Error code:** `PLAY_ROUTE_UNKNOWN_STATE_TYPE`\n */\nexport class UnknownStateTypeError extends PlayError {\n\t/** The string of the state type that the package does not know. */\n\treadonly stateType: string;\n\t/** The ID of the state node with the unknown type. */\n\treadonly nodeId: string;\n\n\tconstructor(stateType: string, nodeId: string, validTypes: string[]) {\n\t\tsuper(\n\t\t\t\"machineToGraph\",\n\t\t\t\"PLAY_ROUTE_UNKNOWN_STATE_TYPE\",\n\t\t\t`Unknown XState state type \"${stateType}\" on node \"${nodeId}\". ` +\n\t\t\t\t`Expected one of: ${validTypes.join(\", \")}.`,\n\t\t);\n\t\tthis.name = \"UnknownStateTypeError\";\n\t\tthis.stateType = stateType;\n\t\tthis.nodeId = nodeId;\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,kBAAb,cAAqC,UAAU;CAC9C,YAAY,SAAiB,SAAwB;EACpD,MAAM,oBAAoB,2BAA2B,SAAS,OAAO;EACrE,KAAK,OAAO;CACb;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,IAAa,uBAAb,cAA0C,UAAU;CACnD,cAAc;EACb,MACC,oBACA,gCACA,sIAED;EACA,KAAK,OAAO;CACb;AACD;;;;;;;AAQA,IAAa,sBAAb,cAAyC,UAAU;;CAElD;CAEA,YAAY,SAAiB;EAC5B,MACC,YACA,yBACA,8BAA8B,QAAQ,qCACvC;EACA,KAAK,OAAO;EACZ,KAAK,UAAU;CAChB;AACD;;;;;;;AAQA,IAAa,sBAAb,cAAyC,UAAU;;CAElD;CAEA,YAAY,SAAiB;EAC5B,MACC,YACA,+BACA,2CAA2C,SAC5C;EACA,KAAK,OAAO;EACZ,KAAK,UAAU;CAChB;AACD;;;;;;;AAQA,IAAa,0BAAb,cAA6C,UAAU;;CAEtD;CAEA,YAAY,YAAsB;EACjC,MACC,YACA,6BACA,oCAAoC,WAAW,KAAK,IAAI,EAAE,gJAG3D;EACA,KAAK,OAAO;EACZ,KAAK,aAAa;CACnB;AACD;;;;;;;AAQA,IAAa,wBAAb,cAA2C,UAAU;;CAEpD;;CAEA;CAEA,YAAY,WAAmB,QAAgB,YAAsB;EACpE,MACC,kBACA,iCACA,8BAA8B,UAAU,aAAa,OAAO,sBACvC,WAAW,KAAK,IAAI,EAAE,EAC5C;EACA,KAAK,OAAO;EACZ,KAAK,YAAY;EACjB,KAAK,SAAS;CACf;AACD"}
|
package/dist/find-route.js
CHANGED
|
@@ -1,114 +1,97 @@
|
|
|
1
1
|
import { getCompiledPattern, parsePattern } from "@xmachines/play-url";
|
|
2
|
+
//#region packages/play-router/src/find-route.ts
|
|
2
3
|
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
4
|
+
* Tells you if a URL path matches a route pattern, with URLPattern.
|
|
5
|
+
*
|
|
6
|
+
* For a static pattern, which holds no param, the function makes a direct equality
|
|
7
|
+
* test, and it compiles nothing.
|
|
8
|
+
*
|
|
9
|
+
* The function compiles through `getCompiledPattern`, so a repeated lookup compiles
|
|
10
|
+
* the same pattern never again. That call also normalizes a param name with a hyphen
|
|
11
|
+
* (`:cat-id`) exactly like `RouteMap`: one match engine is below both.
|
|
12
|
+
*
|
|
13
|
+
* It throws the `InvalidRoutePatternError` of `getCompiledPattern` when URLPattern
|
|
14
|
+
* cannot compile the pattern.
|
|
15
|
+
*
|
|
16
|
+
* This function is private. The public API of the package does not export it.
|
|
17
|
+
*
|
|
18
|
+
* @param path - The URL path, for example '/settings' or '/settings/billing'
|
|
19
|
+
* @param pattern - The route pattern, for example '/settings/:section?'
|
|
20
|
+
* @param options - The caches to read. The default is the pair that
|
|
21
|
+
* `@xmachines/play-url` shares with every caller of the process.
|
|
22
|
+
* @returns True when the path matches the pattern
|
|
23
|
+
*/
|
|
23
24
|
function matchesPattern(path, pattern, options) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// pattern matches.
|
|
27
|
-
if (!parsePattern(pattern, options).parameterized)
|
|
28
|
-
return path === pattern;
|
|
29
|
-
// `getCompiledPattern` reports an invalid pattern itself, and with the REASON when it
|
|
30
|
-
// knows one — two params that land on one URLPattern group, for example. A wrap here
|
|
31
|
-
// replaced that message with the generic one and pushed the explanation down to
|
|
32
|
-
// `cause`, which is the same defect that `RouteMap` guards against in its constructor.
|
|
33
|
-
return getCompiledPattern(pattern, options).test({ pathname: path });
|
|
25
|
+
if (!parsePattern(pattern, options).parameterized) return path === pattern;
|
|
26
|
+
return getCompiledPattern(pattern, options).test({ pathname: path });
|
|
34
27
|
}
|
|
35
28
|
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
29
|
+
* Finds a route node by its state ID
|
|
30
|
+
*
|
|
31
|
+
* The function looks the route node up by the ID property of the state. It gives you
|
|
32
|
+
* the URL path of a state ID, for the update of the browser URL after a `play.route`
|
|
33
|
+
* transition.
|
|
34
|
+
*
|
|
35
|
+
* @param tree - The route tree, from extractMachineRoutes
|
|
36
|
+
* @param id - The state ID, for example 'dashboard' or 'settings.profile'
|
|
37
|
+
* @returns The route node, or undefined when the function finds none
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* const tree = extractMachineRoutes(machine);
|
|
42
|
+
* const node = findRouteById(tree, 'dashboard');
|
|
43
|
+
* if (node) {
|
|
44
|
+
* console.log(node.fullPath); // '/dashboard'
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
var findRouteById = (tree, id) => {
|
|
49
|
+
return tree.byStateId.get(id);
|
|
57
50
|
};
|
|
58
51
|
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// - a route with a parameter, for example '/settings/:section?'
|
|
101
|
-
// - more than one state at the same path, where a state with a route wins
|
|
102
|
-
for (const candidate of tree.byStateId.values()) {
|
|
103
|
-
if (!candidate.routable) {
|
|
104
|
-
continue; // Skip each node without a route
|
|
105
|
-
}
|
|
106
|
-
// Test the path against the pattern of this node
|
|
107
|
-
if (matchesPattern(path, candidate.fullPath, options)) {
|
|
108
|
-
return candidate;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
// 3. The fallback: return the exact match of a node without a route, when one is present
|
|
112
|
-
return node;
|
|
52
|
+
* Finds a route node by its URL path
|
|
53
|
+
*
|
|
54
|
+
* The function looks the route node up by the URL path. It gives you the state ID of
|
|
55
|
+
* a browser URL, for the `play.route` event of a navigation.
|
|
56
|
+
*
|
|
57
|
+
* When more than one state holds the same path, for example the root and a second
|
|
58
|
+
* state both at "/", the function prefers a node with a route, which means a node
|
|
59
|
+
* with a `meta.route` field, over a node without one.
|
|
60
|
+
*
|
|
61
|
+
* The function also matches a pattern of a dynamic route, for example
|
|
62
|
+
* '/settings/:section?'.
|
|
63
|
+
*
|
|
64
|
+
* @param tree - The route tree, from extractMachineRoutes
|
|
65
|
+
* @param path - The URL path, for example '/dashboard' or '/settings/profile'
|
|
66
|
+
* @param options - The caches that the pattern match reads. The default is the pair
|
|
67
|
+
* that `@xmachines/play-url` shares with every caller of the process. A server that
|
|
68
|
+
* serves many tenants passes a cache that the tenant owns, exactly as `RouteMap` and
|
|
69
|
+
* a bridge take one. A cache changes how often the work runs, and it changes no
|
|
70
|
+
* answer, so this argument is safe to leave out.
|
|
71
|
+
* @returns The route node, or undefined when the function finds none
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* const tree = extractMachineRoutes(machine);
|
|
76
|
+
* const node = findRouteByPath(tree, '/dashboard');
|
|
77
|
+
*
|
|
78
|
+
* // With a cache that one request owns and drops
|
|
79
|
+
* const node = findRouteByPath(tree, '/dashboard', {
|
|
80
|
+
* patternCache: createLruCache(),
|
|
81
|
+
* compiledCache: createLruCache(),
|
|
82
|
+
* });
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
var findRouteByPath = (tree, path, options) => {
|
|
86
|
+
const node = tree.byPath.get(path);
|
|
87
|
+
if (node?.routable) return node;
|
|
88
|
+
for (const candidate of tree.byStateId.values()) {
|
|
89
|
+
if (!candidate.routable) continue;
|
|
90
|
+
if (matchesPattern(path, candidate.fullPath, options)) return candidate;
|
|
91
|
+
}
|
|
92
|
+
return node;
|
|
113
93
|
};
|
|
94
|
+
//#endregion
|
|
95
|
+
export { findRouteById, findRouteByPath };
|
|
96
|
+
|
|
114
97
|
//# sourceMappingURL=find-route.js.map
|
package/dist/find-route.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"find-route.js","
|
|
1
|
+
{"version":3,"file":"find-route.js","names":[],"sources":["../src/find-route.ts"],"sourcesContent":["import { getCompiledPattern, parsePattern } from \"@xmachines/play-url\";\nimport type { CompileOptions } from \"@xmachines/play-url\";\nimport type { RouteTree, RouteNode } from \"./types.js\";\n\n/**\n * Tells you if a URL path matches a route pattern, with URLPattern.\n *\n * For a static pattern, which holds no param, the function makes a direct equality\n * test, and it compiles nothing.\n *\n * The function compiles through `getCompiledPattern`, so a repeated lookup compiles\n * the same pattern never again. That call also normalizes a param name with a hyphen\n * (`:cat-id`) exactly like `RouteMap`: one match engine is below both.\n *\n * It throws the `InvalidRoutePatternError` of `getCompiledPattern` when URLPattern\n * cannot compile the pattern.\n *\n * This function is private. The public API of the package does not export it.\n *\n * @param path - The URL path, for example '/settings' or '/settings/billing'\n * @param pattern - The route pattern, for example '/settings/:section?'\n * @param options - The caches to read. The default is the pair that\n * `@xmachines/play-url` shares with every caller of the process.\n * @returns True when the path matches the pattern\n */\nfunction matchesPattern(path: string, pattern: string, options?: CompileOptions): boolean {\n\t// The test reads the PARSE. `pattern.includes(\":\")` called `/docs/*` and `/a{/b}?/c`\n\t// static, and the equality test below then answered `false` for every path that the\n\t// pattern matches.\n\tif (!parsePattern(pattern, options).parameterized) return path === pattern;\n\t// `getCompiledPattern` reports an invalid pattern itself, and with the REASON when it\n\t// knows one — two params that land on one URLPattern group, for example. A wrap here\n\t// replaced that message with the generic one and pushed the explanation down to\n\t// `cause`, which is the same defect that `RouteMap` guards against in its constructor.\n\treturn getCompiledPattern(pattern, options).test({ pathname: path });\n}\n\n/**\n * Finds a route node by its state ID\n *\n * The function looks the route node up by the ID property of the state. It gives you\n * the URL path of a state ID, for the update of the browser URL after a `play.route`\n * transition.\n *\n * @param tree - The route tree, from extractMachineRoutes\n * @param id - The state ID, for example 'dashboard' or 'settings.profile'\n * @returns The route node, or undefined when the function finds none\n *\n * @example\n * ```typescript\n * const tree = extractMachineRoutes(machine);\n * const node = findRouteById(tree, 'dashboard');\n * if (node) {\n * console.log(node.fullPath); // '/dashboard'\n * }\n * ```\n */\nexport const findRouteById = (tree: RouteTree, id: string): RouteNode | undefined => {\n\treturn tree.byStateId.get(id);\n};\n\n/**\n * Finds a route node by its URL path\n *\n * The function looks the route node up by the URL path. It gives you the state ID of\n * a browser URL, for the `play.route` event of a navigation.\n *\n * When more than one state holds the same path, for example the root and a second\n * state both at \"/\", the function prefers a node with a route, which means a node\n * with a `meta.route` field, over a node without one.\n *\n * The function also matches a pattern of a dynamic route, for example\n * '/settings/:section?'.\n *\n * @param tree - The route tree, from extractMachineRoutes\n * @param path - The URL path, for example '/dashboard' or '/settings/profile'\n * @param options - The caches that the pattern match reads. The default is the pair\n * that `@xmachines/play-url` shares with every caller of the process. A server that\n * serves many tenants passes a cache that the tenant owns, exactly as `RouteMap` and\n * a bridge take one. A cache changes how often the work runs, and it changes no\n * answer, so this argument is safe to leave out.\n * @returns The route node, or undefined when the function finds none\n *\n * @example\n * ```typescript\n * const tree = extractMachineRoutes(machine);\n * const node = findRouteByPath(tree, '/dashboard');\n *\n * // With a cache that one request owns and drops\n * const node = findRouteByPath(tree, '/dashboard', {\n * patternCache: createLruCache(),\n * compiledCache: createLruCache(),\n * });\n * ```\n */\nexport const findRouteByPath = (\n\ttree: RouteTree,\n\tpath: string,\n\toptions?: CompileOptions,\n): RouteNode | undefined => {\n\t// 1. Try the exact match first. This is the fast path of a static route\n\tconst node = tree.byPath.get(path);\n\n\tif (node?.routable) {\n\t\treturn node;\n\t}\n\n\t// 2. Without an exact match, and for a node without a route, try the pattern match.\n\t// This handles three cases:\n\t// - a route with a parameter, for example '/settings/:section?'\n\t// - more than one state at the same path, where a state with a route wins\n\tfor (const candidate of tree.byStateId.values()) {\n\t\tif (!candidate.routable) {\n\t\t\tcontinue; // Skip each node without a route\n\t\t}\n\n\t\t// Test the path against the pattern of this node\n\t\tif (matchesPattern(path, candidate.fullPath, options)) {\n\t\t\treturn candidate;\n\t\t}\n\t}\n\n\t// 3. The fallback: return the exact match of a node without a route, when one is present\n\treturn node;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAS,eAAe,MAAc,SAAiB,SAAmC;CAIzF,IAAI,CAAC,aAAa,SAAS,OAAO,CAAC,CAAC,eAAe,OAAO,SAAS;CAKnE,OAAO,mBAAmB,SAAS,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,KAAK,CAAC;AACpE;;;;;;;;;;;;;;;;;;;;;AAsBA,IAAa,iBAAiB,MAAiB,OAAsC;CACpF,OAAO,KAAK,UAAU,IAAI,EAAE;AAC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,IAAa,mBACZ,MACA,MACA,YAC2B;CAE3B,MAAM,OAAO,KAAK,OAAO,IAAI,IAAI;CAEjC,IAAI,MAAM,UACT,OAAO;CAOR,KAAK,MAAM,aAAa,KAAK,UAAU,OAAO,GAAG;EAChD,IAAI,CAAC,UAAU,UACd;EAID,IAAI,eAAe,MAAM,UAAU,UAAU,OAAO,GACnD,OAAO;CAET;CAGA,OAAO;AACR"}
|
package/dist/index.js
CHANGED
|
@@ -1,28 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export { normalizeBasePath, resolveBasePath, stripBasePath, joinBasePath, NO_BASE_PATH, } from "@xmachines/play-url";
|
|
12
|
-
// The params of a framework router, reconciled with the pattern of the machine. A
|
|
13
|
-
// bridge whose framework parses the params itself calls this, and it keeps that parse.
|
|
14
|
-
export { cleanFrameworkParams, createLruCache, parsePattern, pickOwnParams, resolveFrameworkParams, sanitizePathname, splitLocation, } from "@xmachines/play-url";
|
|
15
|
-
// The framework-free half of a PlayRouterProvider: the props, the constructor shape,
|
|
16
|
-
// and the lifecycle of the bridge. A provider of a framework adds its own effects and
|
|
17
|
-
// nothing else. This entry stays framework-agnostic — nothing here imports one.
|
|
18
|
-
export { isMountableBridge, mountKey, createRouterConnection, openProviderBridge, repointProviderBridge, } from "./provider-lifecycle.js";
|
|
19
|
-
// The shared base class of the route map for both directions.
|
|
20
|
-
export { RouteMap } from "./base-route-map.js";
|
|
21
|
-
// The factories: each one creates a RouteMap from a machine or from a route tree.
|
|
22
|
-
export { createRouteMapFromTree } from "./create-route-map-from-tree.js";
|
|
23
|
-
// The helpers of a route lookup. matchesPattern stays private inside find-route.ts
|
|
24
|
-
export { findRouteById, findRouteByPath } from "./find-route.js";
|
|
25
|
-
// The tree builder and its options. `@xmachines/play-router/xstate` reads both: it walks
|
|
26
|
-
// a machine, and it hands the routes here to become a tree and then a route map.
|
|
27
|
-
export { buildRouteTree, ROOT_NODE_ID } from "./build-tree.js";
|
|
28
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
import { buildPlayRouteEvent, extractQuery, extractRouteParams } from "./router-sync.js";
|
|
2
|
+
import { RouterBridgeBase } from "./router-bridge-base.js";
|
|
3
|
+
import { detectDuplicateRoutes, validateRouteFormat, validateStateExists } from "./validate-routes.js";
|
|
4
|
+
import { createRouterConnection, isMountableBridge, mountKey, openProviderBridge, repointProviderBridge } from "./provider-lifecycle.js";
|
|
5
|
+
import { RouteMap } from "./base-route-map.js";
|
|
6
|
+
import { ROOT_NODE_ID, buildRouteTree } from "./build-tree.js";
|
|
7
|
+
import { createRouteMapFromTree } from "./create-route-map-from-tree.js";
|
|
8
|
+
import { findRouteById, findRouteByPath } from "./find-route.js";
|
|
9
|
+
import { NO_BASE_PATH, cleanFrameworkParams, createLruCache, joinBasePath, normalizeBasePath, parsePattern, pickOwnParams, resolveBasePath, resolveFrameworkParams, sanitizePathname, splitLocation, stripBasePath } from "@xmachines/play-url";
|
|
10
|
+
export { NO_BASE_PATH, ROOT_NODE_ID, RouteMap, RouterBridgeBase, buildPlayRouteEvent, buildRouteTree, cleanFrameworkParams, createLruCache, createRouteMapFromTree, createRouterConnection, detectDuplicateRoutes, extractQuery, extractRouteParams, findRouteById, findRouteByPath, isMountableBridge, joinBasePath, mountKey, normalizeBasePath, openProviderBridge, parsePattern, pickOwnParams, repointProviderBridge, resolveBasePath, resolveFrameworkParams, sanitizePathname, splitLocation, stripBasePath, validateRouteFormat, validateStateExists };
|
|
@@ -1,158 +1,124 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The framework-free half of a `PlayRouterProvider`.
|
|
3
|
-
*
|
|
4
|
-
* A provider of a framework is two things: the lifecycle of a bridge, and about fifteen
|
|
5
|
-
* lines that bind that lifecycle to the effects of the framework. The lifecycle carries
|
|
6
|
-
* every decision — when the bridge is built, in which order it connects and
|
|
7
|
-
* disconnects, when a mount moves rather than rebuilds, and how a bridge that never
|
|
8
|
-
* heard of `basePath` is detected — and none of those decisions is framework-specific.
|
|
9
|
-
* They therefore live here, in a module that imports no framework, and each adapter
|
|
10
|
-
* keeps only its own effects.
|
|
11
|
-
*
|
|
12
|
-
* Four adapters used to hold a copy of the whole provider: React Router beside TanStack
|
|
13
|
-
* React, Solid Router beside TanStack Solid. Neither member of a pair could import the
|
|
14
|
-
* other, because each declares its own router as a `peerDependency`. This module is the
|
|
15
|
-
* home they all already depend on, and it stays framework-agnostic, so a consumer of
|
|
16
|
-
* `@xmachines/play-router` gains no framework dependency of any kind.
|
|
17
|
-
*
|
|
18
|
-
* @see [Multi-router integration](../../docs/examples/multi-router-integration.md)
|
|
19
|
-
*/
|
|
20
1
|
import { normalizeBasePath } from "@xmachines/play-url";
|
|
2
|
+
//#region packages/play-router/src/provider-lifecycle.ts
|
|
3
|
+
/**
|
|
4
|
+
* The framework-free half of a `PlayRouterProvider`.
|
|
5
|
+
*
|
|
6
|
+
* A provider of a framework is two things: the lifecycle of a bridge, and about fifteen
|
|
7
|
+
* lines that bind that lifecycle to the effects of the framework. The lifecycle carries
|
|
8
|
+
* every decision — when the bridge is built, in which order it connects and
|
|
9
|
+
* disconnects, when a mount moves rather than rebuilds, and how a bridge that never
|
|
10
|
+
* heard of `basePath` is detected — and none of those decisions is framework-specific.
|
|
11
|
+
* They therefore live here, in a module that imports no framework, and each adapter
|
|
12
|
+
* keeps only its own effects.
|
|
13
|
+
*
|
|
14
|
+
* Four adapters used to hold a copy of the whole provider: React Router beside TanStack
|
|
15
|
+
* React, Solid Router beside TanStack Solid. Neither member of a pair could import the
|
|
16
|
+
* other, because each declares its own router as a `peerDependency`. This module is the
|
|
17
|
+
* home they all already depend on, and it stays framework-agnostic, so a consumer of
|
|
18
|
+
* `@xmachines/play-router` gains no framework dependency of any kind.
|
|
19
|
+
*
|
|
20
|
+
* @see [Multi-router integration](../../docs/examples/multi-router-integration.md)
|
|
21
|
+
*/
|
|
21
22
|
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// `undefined` as readily as `null`, and a strict test then read the property of it and
|
|
32
|
-
// threw a TypeError out of an effect.
|
|
33
|
-
return bridge != null && typeof bridge.setBasePath === "function";
|
|
23
|
+
* Tells you whether a bridge can move its mount.
|
|
24
|
+
*
|
|
25
|
+
* {@link PlayRouterBridgeConstructor} asks for a `RouterBridge`, the published
|
|
26
|
+
* contract, so a consumer bridge that never heard of `basePath` still compiles. Such a
|
|
27
|
+
* bridge ignores the prefix, exactly as it did before the option existed, so the probe
|
|
28
|
+
* runs at run time rather than in the type system.
|
|
29
|
+
*/
|
|
30
|
+
function isMountableBridge(bridge) {
|
|
31
|
+
return bridge != null && typeof bridge.setBasePath === "function";
|
|
34
32
|
}
|
|
35
33
|
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// `{ ...defaults, machineId }`, or one derived from a Map — gave a different key for
|
|
48
|
-
// the same mount, and every render then re-resolved the prefix.
|
|
49
|
-
//
|
|
50
|
-
// Each name, each value AND the prefix are escaped, because a raw join ALIASES. With
|
|
51
|
-
// "=" and "&" as literal separators, `{ a: "1&b=2" }` and `{ a: "1", b: "2" }` gave
|
|
52
|
-
// one key for two different mounts: the provider then saw no change, never
|
|
53
|
-
// re-pointed, and the bridge kept writing and stripping the old prefix — so every
|
|
54
|
-
// location read as foreign and the machine went silent.
|
|
55
|
-
// An absent value counts as an absent ENTRY, exactly as `resolveBasePath` reads it: a
|
|
56
|
-
// host that spreads an optional value — `{ ...defaults, region }` with no region —
|
|
57
|
-
// hands over `{ region: undefined }`, and `String(undefined)` put the text
|
|
58
|
-
// "undefined" in the key. That key differed from the key of the same mount without
|
|
59
|
-
// the entry, so every render re-pointed a bridge that had not moved.
|
|
60
|
-
const entries = Object.entries(basePathParams ?? {})
|
|
61
|
-
.filter(([, value]) => value !== undefined && value !== null)
|
|
62
|
-
.map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(String(value))}`)
|
|
63
|
-
.toSorted();
|
|
64
|
-
return `${encodeURIComponent(basePath ?? "")}|${entries.join("&")}`;
|
|
34
|
+
* A key that changes when the MOUNT changes, and never when its identity changes.
|
|
35
|
+
*
|
|
36
|
+
* The comparison is BY VALUE, because `basePathParams` is normally an inline object
|
|
37
|
+
* literal: its identity changes on every render while the prefix does not, and a
|
|
38
|
+
* provider that watched the identity would move the mount on each render.
|
|
39
|
+
*
|
|
40
|
+
* @returns A string to compare, or to give to the dependency list of an effect.
|
|
41
|
+
*/
|
|
42
|
+
function mountKey(basePath, basePathParams) {
|
|
43
|
+
const entries = Object.entries(basePathParams ?? {}).filter(([, value]) => value !== void 0 && value !== null).map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(String(value))}`).toSorted();
|
|
44
|
+
return `${encodeURIComponent(basePath ?? "")}|${entries.join("&")}`;
|
|
65
45
|
}
|
|
66
46
|
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// then claims the WHOLE router, and its correction of an unknown path drags the user
|
|
87
|
-
// off every page the host owns — which is the opposite of what a prefix was asked
|
|
88
|
-
// for. Say so, once, rather than let it fail silently.
|
|
89
|
-
// `normalizeBasePath` decides what "no prefix" means, and no list of literals here
|
|
90
|
-
// does: an absent value, `""`, `"/"`, and `"//"` all normalize to `""` — see
|
|
91
|
-
// `BasePathOptions.basePath`. A hand-written list warned that a prefix was ignored for
|
|
92
|
-
// a caller that asked for no prefix at all, and it had to be kept in step with that
|
|
93
|
-
// function by hand.
|
|
94
|
-
//
|
|
95
|
-
// The probe of the bridge runs FIRST, so the normalization reaches a mountable bridge
|
|
96
|
-
// never: the constructor of such a bridge has refused an invalid prefix already, and a
|
|
97
|
-
// second refusal here would replace its error with an identical one from another
|
|
98
|
-
// stack. For a bridge with no mount the normalization can still throw, and a prefix
|
|
99
|
-
// that it refuses is a prefix all the same — the warning is the answer, not the throw.
|
|
100
|
-
if (!isMountableBridge(bridge) && hasPrefix(options.basePath)) {
|
|
101
|
-
console.warn(`[@xmachines/play-router] The provider received basePath "${options.basePath}", and ${BridgeCtor.name || "the bridge"} takes no mount: it has no setBasePath(), so the prefix is ignored and the bridge claims the whole router. Extend RouterBridgeBase, or drop the basePath prop.`);
|
|
102
|
-
}
|
|
103
|
-
void bridge.connect();
|
|
104
|
-
return {
|
|
105
|
-
bridge,
|
|
106
|
-
close: () => {
|
|
107
|
-
void bridge.disconnect();
|
|
108
|
-
},
|
|
109
|
-
};
|
|
47
|
+
* Builds a bridge, connects it, and hands back the way to close it.
|
|
48
|
+
*
|
|
49
|
+
* The order is the contract: the constructor applies the mount, `connect()` runs the
|
|
50
|
+
* first synchronization against it, and `close()` disconnects exactly the bridge that
|
|
51
|
+
* this call opened. A provider that closed "the current bridge" instead would
|
|
52
|
+
* disconnect a later one when two mounts overlap.
|
|
53
|
+
*
|
|
54
|
+
* @returns The bridge, and a `close` that disconnects it one time.
|
|
55
|
+
*/
|
|
56
|
+
function openProviderBridge(BridgeCtor, { router, actor, routeMap, ...options }) {
|
|
57
|
+
const bridge = new BridgeCtor(router, actor, routeMap, options);
|
|
58
|
+
if (!isMountableBridge(bridge) && hasPrefix(options.basePath)) console.warn(`[@xmachines/play-router] The provider received basePath "${options.basePath}", and ${BridgeCtor.name || "the bridge"} takes no mount: it has no setBasePath(), so the prefix is ignored and the bridge claims the whole router. Extend RouterBridgeBase, or drop the basePath prop.`);
|
|
59
|
+
bridge.connect();
|
|
60
|
+
return {
|
|
61
|
+
bridge,
|
|
62
|
+
close: () => {
|
|
63
|
+
bridge.disconnect();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
110
66
|
}
|
|
111
67
|
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
68
|
+
* Tells you whether a `basePath` option asks for a prefix at all.
|
|
69
|
+
*
|
|
70
|
+
* An absent value, `""`, `"/"`, and `"//"` all mean "no prefix", and
|
|
71
|
+
* {@link normalizeBasePath} is the one place that says so. A prefix that the function
|
|
72
|
+
* REFUSES is still a prefix that the caller asked for, so the refusal counts as `true`
|
|
73
|
+
* here: the caller learns that its bridge ignores the option, which is the message that
|
|
74
|
+
* this test exists to raise.
|
|
75
|
+
*/
|
|
120
76
|
function hasPrefix(basePath) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
77
|
+
try {
|
|
78
|
+
return normalizeBasePath(basePath) !== "";
|
|
79
|
+
} catch {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
127
82
|
}
|
|
128
83
|
/**
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
bridge.setBasePath(basePath, basePathParams);
|
|
84
|
+
* Moves the mount of a live bridge, and rebuilds nothing.
|
|
85
|
+
*
|
|
86
|
+
* The call is safe directly after the open: it resolves to the prefix that the
|
|
87
|
+
* constructor applied already, and `setBasePath` then returns at once. A `null` bridge,
|
|
88
|
+
* and a bridge with no mount API, are both no-ops.
|
|
89
|
+
*/
|
|
90
|
+
function repointProviderBridge(bridge, basePath, basePathParams) {
|
|
91
|
+
if (isMountableBridge(bridge)) bridge.setBasePath(basePath, basePathParams);
|
|
138
92
|
}
|
|
139
93
|
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
94
|
+
* Wraps a live bridge in the {@link RouterConnection} that `connectRouter` returns.
|
|
95
|
+
*
|
|
96
|
+
* @param bridge - The bridge that `connectRouter` built and connected.
|
|
97
|
+
*/
|
|
98
|
+
function createRouterConnection(bridge) {
|
|
99
|
+
const disconnect = () => {
|
|
100
|
+
bridge.disconnect();
|
|
101
|
+
};
|
|
102
|
+
return Object.defineProperties(disconnect, {
|
|
103
|
+
disconnect: {
|
|
104
|
+
value: disconnect,
|
|
105
|
+
enumerable: true
|
|
106
|
+
},
|
|
107
|
+
basePath: {
|
|
108
|
+
get: () => bridge.basePath,
|
|
109
|
+
enumerable: true
|
|
110
|
+
},
|
|
111
|
+
basePathParams: {
|
|
112
|
+
get: () => bridge.basePathParams,
|
|
113
|
+
enumerable: true
|
|
114
|
+
},
|
|
115
|
+
setBasePath: {
|
|
116
|
+
value: (basePath, basePathParams) => bridge.setBasePath(basePath, basePathParams),
|
|
117
|
+
enumerable: true
|
|
118
|
+
}
|
|
119
|
+
});
|
|
157
120
|
}
|
|
121
|
+
//#endregion
|
|
122
|
+
export { createRouterConnection, isMountableBridge, mountKey, openProviderBridge, repointProviderBridge };
|
|
123
|
+
|
|
158
124
|
//# sourceMappingURL=provider-lifecycle.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider-lifecycle.js","sourceRoot":"","sources":["../src/provider-lifecycle.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"provider-lifecycle.js","names":[],"sources":["../src/provider-lifecycle.ts"],"sourcesContent":["/**\n * The framework-free half of a `PlayRouterProvider`.\n *\n * A provider of a framework is two things: the lifecycle of a bridge, and about fifteen\n * lines that bind that lifecycle to the effects of the framework. The lifecycle carries\n * every decision — when the bridge is built, in which order it connects and\n * disconnects, when a mount moves rather than rebuilds, and how a bridge that never\n * heard of `basePath` is detected — and none of those decisions is framework-specific.\n * They therefore live here, in a module that imports no framework, and each adapter\n * keeps only its own effects.\n *\n * Four adapters used to hold a copy of the whole provider: React Router beside TanStack\n * React, Solid Router beside TanStack Solid. Neither member of a pair could import the\n * other, because each declares its own router as a `peerDependency`. This module is the\n * home they all already depend on, and it stays framework-agnostic, so a consumer of\n * `@xmachines/play-router` gains no framework dependency of any kind.\n *\n * @see [Multi-router integration](../../docs/examples/multi-router-integration.md)\n */\nimport { normalizeBasePath, type BasePathOptions, type CompileOptions } from \"@xmachines/play-url\";\nimport type { RouterBridgeOptions } from \"./router-bridge-base.js\";\n\nimport type { MountableRouterBridge, RoutableActor, RouterBridge } from \"./types.js\";\nimport type { RouteMap } from \"./base-route-map.js\";\n\n/**\n * The props that every `PlayRouterProvider` of a framework shares.\n *\n * `TNode` is what the framework renders: `ReactNode` for React, `JSX.Element` for\n * Solid. It is the ONLY thing in these props that a framework decides, which is why\n * they live here and not in an adapter.\n *\n * An adapter re-exports a two-parameter alias of this type, with `TRouter` bound to\n * the type of its router instance and `TNode` bound to its own node type.\n */\nexport interface PlayRouterProviderBaseProps<\n\tTRouter,\n\tTActor extends RoutableActor,\n\tTNode,\n> extends CompileOptions {\n\t/**\n\t * The actor to keep in step with the router. It must be a stable reference: give\n\t * the same actor instance on every render. An actor in the JSX, or a new actor on\n\t * each render, makes the bridge disconnect and connect again each time.\n\t */\n\tactor: TActor;\n\t/** The router instance that the bridge keeps in step with the actor. It must be a stable reference. */\n\trouter: TRouter;\n\t/**\n\t * The route map of both directions, for the lookup between a state ID and a URL path.\n\t *\n\t * **It must be a stable reference.** The provider builds the bridge again on each\n\t * change of the identity of `routeMap`. A value in the JSX, for example\n\t * `createRouteMapFromTree(routeTree)`, makes a new object on every render, and the\n\t * bridge therefore connects again on every render. Hold the value with `useMemo`:\n\t *\n\t * ```tsx\n\t * const routeMap = useMemo(() => createRouteMapFromTree(routeTree), [routeTree]);\n\t * ```\n\t */\n\trouteMap: RouteMap;\n\t/**\n\t * Mounts the routes of the machine under a URL prefix that the host owns, so that\n\t * the routes of the host and the routes of the machine share one router.\n\t *\n\t * `actor`, `router`, and `routeMap` each need a stable reference. This prop needs\n\t * none, and it rebuilds the bridge never: a change moves the live bridge with\n\t * `setBasePath()`, so a host route that renders again with a new prefix keeps the\n\t * actor, the route map, and its cache.\n\t *\n\t * The prefix says WHERE an actor is mounted, and never WHICH actor is mounted. An\n\t * actor never changes identity, so a prefix segment that IDENTIFIES the actor gives\n\t * a different `actor` prop instead, and THAT rebuilds the bridge — which is correct,\n\t * because one actor takes one bridge.\n\t *\n\t * A location outside the prefix belongs completely to the host: the bridge sends no\n\t * event there, and it corrects no URL there.\n\t *\n\t * Every `:param` needs a value. An unresolved one throws a\n\t * `MissingBasePathParamError` out of the mount effect, because the bridge writes no\n\t * URL without it. Render this component only after the host holds the value, which\n\t * a route loader or a `useParams()` call does already.\n\t */\n\tbasePath?: string;\n\t/**\n\t * The values of the `:param` segments of\n\t * {@link PlayRouterProviderBaseProps.basePath}, for example `{ machineId }` for\n\t * `basePath=\"/:machineId/play\"`.\n\t *\n\t * An inline object literal is correct here: the provider compares this prop by\n\t * value, and a move to the same prefix does nothing.\n\t */\n\tbasePathParams?: Record<string, string | number>;\n\t/** The renderer callback receives the same concrete actor type as the prop. */\n\trenderer: (actor: TActor, router: TRouter) => TNode;\n}\n\n/**\n * The constructor shape that a bridge class must satisfy for a provider factory: `(router, actor, routeMap, options?) → RouterBridge`.\n *\n * `TActor` carries the actor type of the caller through to the constructor, so the spec\n * type of that actor reaches the bridge whole. A bridge that reads no spec binds nothing\n * and receives the default.\n *\n * The options are {@link RouterBridgeOptions}, because that is what\n * {@link openProviderBridge} hands to the constructor: the base path AND the caches of a\n * parse and a compilation. A `BasePathOptions` here named half of the value that travels,\n * so a bridge author read the type and learned of no cache. A bridge that declares the\n * narrower `BasePathOptions` still satisfies this type, and it reads the fields that it\n * knows.\n *\n * The return type is `RouterBridge`, the published contract of a bridge, so a\n * consumer bridge of its own keeps compiling. Every bridge on `RouterBridgeBase` also\n * satisfies `MountableRouterBridge`, and the provider finds that mount API at run\n * time: a bridge without it ignores `basePath`, exactly as it did before the option\n * existed.\n */\nexport type PlayRouterBridgeConstructor<\n\tTRouter,\n\tTActor extends RoutableActor = RoutableActor,\n> = new (\n\trouter: TRouter,\n\tactor: TActor,\n\trouteMap: RouteMap,\n\toptions?: RouterBridgeOptions,\n) => RouterBridge;\n\n/**\n * Tells you whether a bridge can move its mount.\n *\n * {@link PlayRouterBridgeConstructor} asks for a `RouterBridge`, the published\n * contract, so a consumer bridge that never heard of `basePath` still compiles. Such a\n * bridge ignores the prefix, exactly as it did before the option existed, so the probe\n * runs at run time rather than in the type system.\n */\nexport function isMountableBridge(\n\tbridge: RouterBridge | null | undefined,\n): bridge is MountableRouterBridge {\n\t// `!= null`, and not `!== null`: a binding that has not opened its bridge yet holds\n\t// `undefined` as readily as `null`, and a strict test then read the property of it and\n\t// threw a TypeError out of an effect.\n\treturn bridge != null && typeof (bridge as MountableRouterBridge).setBasePath === \"function\";\n}\n\n/**\n * A key that changes when the MOUNT changes, and never when its identity changes.\n *\n * The comparison is BY VALUE, because `basePathParams` is normally an inline object\n * literal: its identity changes on every render while the prefix does not, and a\n * provider that watched the identity would move the mount on each render.\n *\n * @returns A string to compare, or to give to the dependency list of an effect.\n */\nexport function mountKey(\n\tbasePath?: BasePathOptions[\"basePath\"],\n\tbasePathParams?: BasePathOptions[\"basePathParams\"],\n): string {\n\t// The entries are SORTED before they are joined, because `JSON.stringify` follows the\n\t// order of the properties: a params object that a host rebuilds in another order —\n\t// `{ ...defaults, machineId }`, or one derived from a Map — gave a different key for\n\t// the same mount, and every render then re-resolved the prefix.\n\t//\n\t// Each name, each value AND the prefix are escaped, because a raw join ALIASES. With\n\t// \"=\" and \"&\" as literal separators, `{ a: \"1&b=2\" }` and `{ a: \"1\", b: \"2\" }` gave\n\t// one key for two different mounts: the provider then saw no change, never\n\t// re-pointed, and the bridge kept writing and stripping the old prefix — so every\n\t// location read as foreign and the machine went silent.\n\t// An absent value counts as an absent ENTRY, exactly as `resolveBasePath` reads it: a\n\t// host that spreads an optional value — `{ ...defaults, region }` with no region —\n\t// hands over `{ region: undefined }`, and `String(undefined)` put the text\n\t// \"undefined\" in the key. That key differed from the key of the same mount without\n\t// the entry, so every render re-pointed a bridge that had not moved.\n\tconst entries = Object.entries(basePathParams ?? {})\n\t\t.filter(([, value]) => value !== undefined && value !== null)\n\t\t.map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(String(value))}`)\n\t\t.toSorted();\n\treturn `${encodeURIComponent(basePath ?? \"\")}|${entries.join(\"&\")}`;\n}\n\n/** What {@link openProviderBridge} needs to build and connect a bridge. */\nexport interface OpenProviderBridgeArgs<\n\tTRouter,\n\tTActor extends RoutableActor,\n> extends RouterBridgeOptions {\n\trouter: TRouter;\n\tactor: TActor;\n\trouteMap: RouteMap;\n}\n\n/**\n * Builds a bridge, connects it, and hands back the way to close it.\n *\n * The order is the contract: the constructor applies the mount, `connect()` runs the\n * first synchronization against it, and `close()` disconnects exactly the bridge that\n * this call opened. A provider that closed \"the current bridge\" instead would\n * disconnect a later one when two mounts overlap.\n *\n * @returns The bridge, and a `close` that disconnects it one time.\n */\nexport function openProviderBridge<TRouter, TActor extends RoutableActor>(\n\tBridgeCtor: PlayRouterBridgeConstructor<TRouter, TActor>,\n\t{ router, actor, routeMap, ...options }: OpenProviderBridgeArgs<TRouter, TActor>,\n): { bridge: RouterBridge; close: () => void } {\n\t// The REST travels whole, rather than two named fields. A bridge takes a base path and\n\t// the caches of a parse and a compilation, and a provider is the place a server builds\n\t// one for each request — the case the caches exist for. A destructure of the two\n\t// base-path fields dropped every cache in silence, so a provider of React, Solid, Vue\n\t// or TanStack isolated nothing. A rest also carries whatever a later release adds.\n\tconst bridge = new BridgeCtor(router, actor, routeMap, options);\n\n\t// A bridge on the three-argument signature drops the options object, and\n\t// `PlayRouterBridgeConstructor` accepts one: it asks for the published `RouterBridge`,\n\t// so a consumer bridge that never heard of `basePath` still compiles. Such a bridge\n\t// then claims the WHOLE router, and its correction of an unknown path drags the user\n\t// off every page the host owns — which is the opposite of what a prefix was asked\n\t// for. Say so, once, rather than let it fail silently.\n\t// `normalizeBasePath` decides what \"no prefix\" means, and no list of literals here\n\t// does: an absent value, `\"\"`, `\"/\"`, and `\"//\"` all normalize to `\"\"` — see\n\t// `BasePathOptions.basePath`. A hand-written list warned that a prefix was ignored for\n\t// a caller that asked for no prefix at all, and it had to be kept in step with that\n\t// function by hand.\n\t//\n\t// The probe of the bridge runs FIRST, so the normalization reaches a mountable bridge\n\t// never: the constructor of such a bridge has refused an invalid prefix already, and a\n\t// second refusal here would replace its error with an identical one from another\n\t// stack. For a bridge with no mount the normalization can still throw, and a prefix\n\t// that it refuses is a prefix all the same — the warning is the answer, not the throw.\n\tif (!isMountableBridge(bridge) && hasPrefix(options.basePath)) {\n\t\tconsole.warn(\n\t\t\t`[@xmachines/play-router] The provider received basePath \"${options.basePath}\", and ${\n\t\t\t\tBridgeCtor.name || \"the bridge\"\n\t\t\t} takes no mount: it has no setBasePath(), so the prefix is ignored and the bridge claims the whole router. Extend RouterBridgeBase, or drop the basePath prop.`,\n\t\t);\n\t}\n\n\tvoid bridge.connect();\n\treturn {\n\t\tbridge,\n\t\tclose: () => {\n\t\t\tvoid bridge.disconnect();\n\t\t},\n\t};\n}\n\n/**\n * Tells you whether a `basePath` option asks for a prefix at all.\n *\n * An absent value, `\"\"`, `\"/\"`, and `\"//\"` all mean \"no prefix\", and\n * {@link normalizeBasePath} is the one place that says so. A prefix that the function\n * REFUSES is still a prefix that the caller asked for, so the refusal counts as `true`\n * here: the caller learns that its bridge ignores the option, which is the message that\n * this test exists to raise.\n */\nfunction hasPrefix(basePath: BasePathOptions[\"basePath\"]): boolean {\n\ttry {\n\t\treturn normalizeBasePath(basePath) !== \"\";\n\t} catch {\n\t\treturn true;\n\t}\n}\n\n/**\n * Moves the mount of a live bridge, and rebuilds nothing.\n *\n * The call is safe directly after the open: it resolves to the prefix that the\n * constructor applied already, and `setBasePath` then returns at once. A `null` bridge,\n * and a bridge with no mount API, are both no-ops.\n */\nexport function repointProviderBridge(\n\tbridge: RouterBridge | null | undefined,\n\tbasePath?: BasePathOptions[\"basePath\"],\n\tbasePathParams?: BasePathOptions[\"basePathParams\"],\n): void {\n\tif (isMountableBridge(bridge)) bridge.setBasePath(basePath, basePathParams);\n}\n\n/**\n * What `connectRouter` of an adapter returns.\n *\n * The value is CALLABLE, so `const disconnect = connectRouter(…); disconnect();` keeps\n * working exactly as it did. It also carries the mount API, because a host that gives a\n * `basePath` needs the two things {@link MountableRouterBridge} promises: the resolved\n * mount to read back, and a way to move it. The bridge itself lives inside\n * `connectRouter`, so without this handle a host had to rebuild the actor and the bridge\n * to move a mount — which throws away the LRU cache of the route map, the very thing\n * `setBasePath` exists to keep.\n *\n * `basePath` and `basePathParams` are GETTERS on the live bridge, so they cannot go\n * stale.\n */\nexport interface RouterConnection {\n\t/** Stops the synchronization. The same call as {@link RouterConnection.disconnect}. */\n\t(): void;\n\t/**\n\t * Stops the synchronization. It is idempotent.\n\t *\n\t * A scope can own the connection: the connection is its own release, so\n\t * `asDisposable` of `@xmachines/core/utils` publishes it with no second argument.\n\t *\n\t * ```ts\n\t * {\n\t * using connection = asDisposable(connectRouter({ actor, router, routeMap }));\n\t * // disconnect() runs at the end of the scope, and after an exception too\n\t * }\n\t * ```\n\t */\n\tdisconnect(): void;\n\t/** The resolved prefix of the mount, or `\"\"` when the machine owns the whole router. */\n\treadonly basePath: string;\n\t/** The values of the `:param` segments of the mount. They travel in no event. */\n\treadonly basePathParams: Readonly<Record<string, string>>;\n\t/** Moves the mount of the live bridge, with no teardown. */\n\tsetBasePath(\n\t\tbasePath?: BasePathOptions[\"basePath\"],\n\t\tbasePathParams?: BasePathOptions[\"basePathParams\"],\n\t): void;\n}\n\n/**\n * Wraps a live bridge in the {@link RouterConnection} that `connectRouter` returns.\n *\n * @param bridge - The bridge that `connectRouter` built and connected.\n */\nexport function createRouterConnection(bridge: MountableRouterBridge): RouterConnection {\n\tconst disconnect = (): void => {\n\t\tbridge.disconnect();\n\t};\n\n\treturn Object.defineProperties(disconnect as RouterConnection, {\n\t\tdisconnect: { value: disconnect, enumerable: true },\n\t\tbasePath: { get: () => bridge.basePath, enumerable: true },\n\t\tbasePathParams: { get: () => bridge.basePathParams, enumerable: true },\n\t\tsetBasePath: {\n\t\t\tvalue: (\n\t\t\t\tbasePath?: BasePathOptions[\"basePath\"],\n\t\t\t\tbasePathParams?: BasePathOptions[\"basePathParams\"],\n\t\t\t) => bridge.setBasePath(basePath, basePathParams),\n\t\t\tenumerable: true,\n\t\t},\n\t});\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuIA,SAAgB,kBACf,QACkC;CAIlC,OAAO,UAAU,QAAQ,OAAQ,OAAiC,gBAAgB;AACnF;;;;;;;;;;AAWA,SAAgB,SACf,UACA,gBACS;CAgBT,MAAM,UAAU,OAAO,QAAQ,kBAAkB,CAAC,CAAC,CAAC,CAClD,QAAQ,GAAG,WAAW,UAAU,KAAA,KAAa,UAAU,IAAI,CAAC,CAC5D,KAAK,CAAC,MAAM,WAAW,GAAG,mBAAmB,IAAI,EAAE,GAAG,mBAAmB,OAAO,KAAK,CAAC,GAAG,CAAC,CAC1F,SAAS;CACX,OAAO,GAAG,mBAAmB,YAAY,EAAE,EAAE,GAAG,QAAQ,KAAK,GAAG;AACjE;;;;;;;;;;;AAsBA,SAAgB,mBACf,YACA,EAAE,QAAQ,OAAO,UAAU,GAAG,WACgB;CAM9C,MAAM,SAAS,IAAI,WAAW,QAAQ,OAAO,UAAU,OAAO;CAmB9D,IAAI,CAAC,kBAAkB,MAAM,KAAK,UAAU,QAAQ,QAAQ,GAC3D,QAAQ,KACP,4DAA4D,QAAQ,SAAS,SAC5E,WAAW,QAAQ,aACnB,+JACF;CAGD,OAAY,QAAQ;CACpB,OAAO;EACN;EACA,aAAa;GACZ,OAAY,WAAW;EACxB;CACD;AACD;;;;;;;;;;AAWA,SAAS,UAAU,UAAgD;CAClE,IAAI;EACH,OAAO,kBAAkB,QAAQ,MAAM;CACxC,QAAQ;EACP,OAAO;CACR;AACD;;;;;;;;AASA,SAAgB,sBACf,QACA,UACA,gBACO;CACP,IAAI,kBAAkB,MAAM,GAAG,OAAO,YAAY,UAAU,cAAc;AAC3E;;;;;;AAiDA,SAAgB,uBAAuB,QAAiD;CACvF,MAAM,mBAAyB;EAC9B,OAAO,WAAW;CACnB;CAEA,OAAO,OAAO,iBAAiB,YAAgC;EAC9D,YAAY;GAAE,OAAO;GAAY,YAAY;EAAK;EAClD,UAAU;GAAE,WAAW,OAAO;GAAU,YAAY;EAAK;EACzD,gBAAgB;GAAE,WAAW,OAAO;GAAgB,YAAY;EAAK;EACrE,aAAa;GACZ,QACC,UACA,mBACI,OAAO,YAAY,UAAU,cAAc;GAChD,YAAY;EACb;CACD,CAAC;AACF"}
|