@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.
Files changed (38) hide show
  1. package/README.md +73 -16
  2. package/dist/base-route-map.js +240 -270
  3. package/dist/base-route-map.js.map +1 -1
  4. package/dist/build-tree.js +84 -96
  5. package/dist/build-tree.js.map +1 -1
  6. package/dist/create-route-map-from-tree.js +50 -47
  7. package/dist/create-route-map-from-tree.js.map +1 -1
  8. package/dist/errors.js +124 -133
  9. package/dist/errors.js.map +1 -1
  10. package/dist/find-route.js +88 -105
  11. package/dist/find-route.js.map +1 -1
  12. package/dist/index.js +10 -28
  13. package/dist/provider-lifecycle.js +109 -143
  14. package/dist/provider-lifecycle.js.map +1 -1
  15. package/dist/router-bridge-base.d.ts +4 -4
  16. package/dist/router-bridge-base.d.ts.map +1 -1
  17. package/dist/router-bridge-base.js +1026 -1601
  18. package/dist/router-bridge-base.js.map +1 -1
  19. package/dist/router-sync.js +107 -139
  20. package/dist/router-sync.js.map +1 -1
  21. package/dist/validate-routes.js +58 -66
  22. package/dist/validate-routes.js.map +1 -1
  23. package/dist/xstate/create-route-map.js +36 -35
  24. package/dist/xstate/create-route-map.js.map +1 -1
  25. package/dist/xstate/extract-routes.js +66 -83
  26. package/dist/xstate/extract-routes.js.map +1 -1
  27. package/dist/xstate/index.js +5 -33
  28. package/dist/xstate/machine-to-graph.js +98 -109
  29. package/dist/xstate/machine-to-graph.js.map +1 -1
  30. package/dist/xstate/query.js +190 -212
  31. package/dist/xstate/query.js.map +1 -1
  32. package/package.json +16 -16
  33. package/dist/index.js.map +0 -1
  34. package/dist/types.js +0 -2
  35. package/dist/types.js.map +0 -1
  36. package/dist/xstate/index.js.map +0 -1
  37. package/dist/xstate/machine-types.js +0 -2
  38. package/dist/xstate/machine-types.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"router-sync.js","sourceRoot":"","sources":["../src/router-sync.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"router-sync.js","names":[],"sources":["../src/router-sync.ts"],"sourcesContent":["import {\n\tgetCompiledPattern,\n\tparsePattern,\n\tsanitizePathname,\n\ttype CompileOptions,\n\ttype URLPatternLike,\n} from \"@xmachines/play-url\";\nimport { createNullRecord } from \"@xmachines/core/utils\";\nimport type { PlayRouteEvent } from \"./types.js\";\n\nexport interface RouteMatch {\n\tto: string | null | undefined;\n\tparams?: Record<string, string>;\n}\n\nexport interface BuildPlayRouteEventOptions {\n\tpathname: string;\n\tsearch?: string | undefined;\n\tmatch: (sanitizedPathname: string) => RouteMatch;\n}\n\nexport interface RouterStateIdMap {\n\tgetStateIdByPath(path: string): string | null | undefined;\n\tgetPathByStateId(id: string): string | null | undefined;\n}\n\n/**\n * Matches a clean pathname against a route map, and reads each param of the match.\n *\n * The low-level sync path and the framework sync path share this function.\n * Therefore the semantics of the match in the route map stay identical.\n */\nexport function matchRouteMap(\n\tpathname: string,\n\trouteMap: RouterStateIdMap,\n\textractParams: (pathname: string, stateId: string) => Record<string, string>,\n): RouteMatch {\n\tconst stateId = routeMap.getStateIdByPath(pathname);\n\tif (!stateId) {\n\t\treturn { to: null, params: {} };\n\t}\n\n\treturn {\n\t\tto: stateId,\n\t\tparams: extractParams(pathname, stateId),\n\t};\n}\n\n/**\n * Reads the named path parameters of a URL, with the URLPattern API.\n *\n * The function takes the string of the pattern directly. Use it when you know the\n * pattern already. For a read through the lookup of a state ID, see\n * `RouterBridgeBase.extractParams`.\n *\n * The object of the return holds no undefined value. Such a value comes from an\n * optional segment without a match, for example `/settings` against\n * `/settings/:section?`.\n *\n * The function normalizes the pattern with the rules of the match in `RouteMap`: a\n * name with a hyphen, such as `:cat-id`, becomes `:cat_id` for URLPattern. It then\n * maps each group name back to the original param name. Therefore `/docs/123`\n * against `/docs/:cat-id` gives `{ \"cat-id\": \"123\" }`.\n *\n * The function decodes each percent sequence of a value (`john%20doe` → `john doe`),\n * as a framework router does, for example vue-router. It keeps a malformed sequence\n * raw.\n *\n * @param pathname - The concrete URL pathname, for example `/profile/alice`\n * @param pattern - The template of the URL pattern, for example `/profile/:username`\n * @returns The record of the parameter values of the read, or `{}` for a static pattern.\n * @throws {InvalidRoutePatternError} When URLPattern cannot compile the pattern.\n *\n * @example\n * ```typescript\n * import { extractRouteParams } from \"@xmachines/play-router\";\n *\n * extractRouteParams(\"/profile/alice\", \"/profile/:username\")\n * // → { username: \"alice\" }\n *\n * extractRouteParams(\"/settings\", \"/settings/:section?\")\n * // → {} — the optional param is absent, and the object holds it not\n * ```\n */\nexport function extractRouteParams(\n\tpathname: string,\n\tpattern: string,\n\toptions?: CompileOptions,\n): Record<string, string> {\n\t// ONE parse answers both questions of this function: does the pattern need a match,\n\t// and which name does each URLPattern group carry.\n\tconst parsed = parsePattern(pattern, options);\n\tif (!parsed.parameterized) return {};\n\t// Compile the pattern with the SAME normalization as the match in RouteMap.\n\t// Without it, URLPattern reads \":cat-id\" as the group \":cat\" and the literal \"-id\",\n\t// and the read of the params then contradicts the map that matched the path.\n\tconst urlPattern: URLPatternLike = getCompiledPattern(pattern, options);\n\tconst match = urlPattern.exec({ pathname });\n\tif (!match?.pathname.groups) return {};\n\t// Map each normalized group name (\":cat_id\") back to its original param name (\"cat-id\").\n\tconst nameMap = parsed.declaredNames;\n\t// A null prototype for the accumulator, exactly as `cleanFrameworkParams` uses: a plain\n\t// `{}` turns `params[\"__proto__\"] = value` into a write of the PROTOTYPE setter, which\n\t// drops the value in silence. A machine that declares `/:__proto__` then received `{}`\n\t// from this extraction while the parse of Vue Router or SolidJS Router carried the\n\t// value, so the two sources of the same params disagreed.\n\tconst params: Record<string, string> = createNullRecord<string>();\n\tfor (const [key, value] of Object.entries(match.pathname.groups)) {\n\t\tif (value !== undefined) {\n\t\t\tparams[nameMap.get(key) ?? key] = decodeParamValue(value); // nosemgrep: gitlab.eslint.detect-object-injection\n\t\t}\n\t}\n\t// Spread at the boundary: `event.params` reaches a machine of the user, and a plain\n\t// object is what it received before. A spread creates OWN properties, so a param named\n\t// \"__proto__\" stays a key while the value keeps a prototype.\n\treturn { ...params };\n}\n\n/**\n * Decodes each percent sequence of a URLPattern group value. It keeps the raw value\n * when the sequence is malformed, because `decodeURIComponent` then throws a\n * `URIError`.\n */\nfunction decodeParamValue(value: string): string {\n\ttry {\n\t\treturn decodeURIComponent(value);\n\t} catch {\n\t\treturn value;\n\t}\n}\n\n/**\n * Parses a URL search string into the plain object that a `play.route` event\n * needs.\n */\nexport function extractQuery(search: string): Record<string, string> {\n\ttry {\n\t\t// A null prototype for the accumulator, and a spread at the boundary: the same two\n\t\t// steps as `extractRouteParams` above, and for the same reason. A plain `{}` turns\n\t\t// `params[\"__proto__\"] = value` into a write of the PROTOTYPE setter, which drops\n\t\t// the value in silence — so a location of `?__proto__=nav` reached the machine with\n\t\t// an EMPTY query, while the same name in the path reached it as a param. The two\n\t\t// halves of one `play.route` event answered one name differently.\n\t\tconst params: Record<string, string> = createNullRecord<string>();\n\t\tnew URLSearchParams(search).forEach((value, key) => {\n\t\t\tparams[key] = value; // nosemgrep: gitlab.eslint.detect-object-injection\n\t\t});\n\t\treturn { ...params };\n\t} catch {\n\t\treturn {};\n\t}\n}\n\n/**\n * Builds a normalized `play.route` event from the raw input of a router or of a\n * browser.\n *\n * `connectRouter()` of `@xmachines/play-dom-router` and `RouterBridgeBase` both use\n * this helper. Therefore a low-level adapter and a framework adapter clean each\n * pathname, match each route, and read each query in the same way.\n */\nexport function buildPlayRouteEvent(\n\toptions: BuildPlayRouteEventOptions,\n): { pathname: string; event: PlayRouteEvent } | null {\n\tconst pathname = sanitizePathname(options.pathname);\n\tif (pathname === null) return null;\n\n\tconst match = options.match(pathname);\n\tif (!match.to) return null;\n\n\t// The one place of the canonical form of the emitted event: a `play.route` event\n\t// always carries a target with a '#' prefix. A route map can register a bare\n\t// stateId, and it can return one, because RouteMap keeps the registered form.\n\t// Therefore this code formats the target of the event, and no other code does\n\t// it.\n\tconst to = match.to.startsWith(\"#\") ? match.to : `#${match.to}`;\n\treturn {\n\t\tpathname,\n\t\tevent: {\n\t\t\ttype: \"play.route\",\n\t\t\tto,\n\t\t\tparams: match.params ?? {},\n\t\t\tquery: extractQuery(options.search ?? \"\"),\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;;;;AAgCA,SAAgB,cACf,UACA,UACA,eACa;CACb,MAAM,UAAU,SAAS,iBAAiB,QAAQ;CAClD,IAAI,CAAC,SACJ,OAAO;EAAE,IAAI;EAAM,QAAQ,CAAC;CAAE;CAG/B,OAAO;EACN,IAAI;EACJ,QAAQ,cAAc,UAAU,OAAO;CACxC;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,mBACf,UACA,SACA,SACyB;CAGzB,MAAM,SAAS,aAAa,SAAS,OAAO;CAC5C,IAAI,CAAC,OAAO,eAAe,OAAO,CAAC;CAKnC,MAAM,QAD6B,mBAAmB,SAAS,OACjD,CAAA,CAAW,KAAK,EAAE,SAAS,CAAC;CAC1C,IAAI,CAAC,OAAO,SAAS,QAAQ,OAAO,CAAC;CAErC,MAAM,UAAU,OAAO;CAMvB,MAAM,SAAiC,iBAAyB;CAChE,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,SAAS,MAAM,GAC9D,IAAI,UAAU,KAAA,GACb,OAAO,QAAQ,IAAI,GAAG,KAAK,OAAO,iBAAiB,KAAK;CAM1D,OAAO,EAAE,GAAG,OAAO;AACpB;;;;;;AAOA,SAAS,iBAAiB,OAAuB;CAChD,IAAI;EACH,OAAO,mBAAmB,KAAK;CAChC,QAAQ;EACP,OAAO;CACR;AACD;;;;;AAMA,SAAgB,aAAa,QAAwC;CACpE,IAAI;EAOH,MAAM,SAAiC,iBAAyB;EAChE,IAAI,gBAAgB,MAAM,CAAC,CAAC,SAAS,OAAO,QAAQ;GACnD,OAAO,OAAO;EACf,CAAC;EACD,OAAO,EAAE,GAAG,OAAO;CACpB,QAAQ;EACP,OAAO,CAAC;CACT;AACD;;;;;;;;;AAUA,SAAgB,oBACf,SACqD;CACrD,MAAM,WAAW,iBAAiB,QAAQ,QAAQ;CAClD,IAAI,aAAa,MAAM,OAAO;CAE9B,MAAM,QAAQ,QAAQ,MAAM,QAAQ;CACpC,IAAI,CAAC,MAAM,IAAI,OAAO;CAQtB,OAAO;EACN;EACA,OAAO;GACN,MAAM;GACN,IALS,MAAM,GAAG,WAAW,GAAG,IAAI,MAAM,KAAK,IAAI,MAAM;GAMzD,QAAQ,MAAM,UAAU,CAAC;GACzB,OAAO,aAAa,QAAQ,UAAU,EAAE;EACzC;CACD;AACD"}
@@ -1,73 +1,65 @@
1
- import { EmptyRoutePathError, InvalidStateIdError, DuplicateRoutePathError } from "./errors.js";
1
+ import { DuplicateRoutePathError, EmptyRoutePathError, InvalidStateIdError } from "./errors.js";
2
+ //#region packages/play-router/src/validate-routes.ts
2
3
  /**
3
- * Checks the format of a route path
4
- *
5
- * The function requires a string that is not empty. It accepts an absolute path
6
- * ("/foo") and also a relative path ("foo" or "child/nested"). A relative route
7
- * receives the path prefix of its parent state during the construction of the tree.
8
- *
9
- * @param routePath - The route path to check: absolute or relative
10
- * @param stateId - The identifier of the state, for the error message
11
- * @throws {EmptyRoutePathError} When the route path is empty
12
- */
13
- export const validateRouteFormat = (routePath, stateId) => {
14
- if (!routePath) {
15
- throw new EmptyRoutePathError(stateId);
16
- }
4
+ * Checks the format of a route path
5
+ *
6
+ * The function requires a string that is not empty. It accepts an absolute path
7
+ * ("/foo") and also a relative path ("foo" or "child/nested"). A relative route
8
+ * receives the path prefix of its parent state during the construction of the tree.
9
+ *
10
+ * @param routePath - The route path to check: absolute or relative
11
+ * @param stateId - The identifier of the state, for the error message
12
+ * @throws {EmptyRoutePathError} When the route path is empty
13
+ */
14
+ var validateRouteFormat = (routePath, stateId) => {
15
+ if (!routePath) throw new EmptyRoutePathError(stateId);
17
16
  };
18
17
  /**
19
- * Checks that the set of the state IDs holds a state
20
- *
21
- * The function requires each state ID of a reference in the machine graph. This
22
- * check at the build time therefore stops a broken route reference.
23
- *
24
- * @param stateId - The identifier of the state to check
25
- * @param stateIds - The set of every known state ID of the machine graph
26
- * @throws {InvalidStateIdError} When the set does not hold the state ID
27
- */
28
- export const validateStateExists = (stateId, stateIds) => {
29
- if (!stateIds.has(stateId)) {
30
- throw new InvalidStateIdError(stateId);
31
- }
18
+ * Checks that the set of the state IDs holds a state
19
+ *
20
+ * The function requires each state ID of a reference in the machine graph. This
21
+ * check at the build time therefore stops a broken route reference.
22
+ *
23
+ * @param stateId - The identifier of the state to check
24
+ * @param stateIds - The set of every known state ID of the machine graph
25
+ * @throws {InvalidStateIdError} When the set does not hold the state ID
26
+ */
27
+ var validateStateExists = (stateId, stateIds) => {
28
+ if (!stateIds.has(stateId)) throw new InvalidStateIdError(stateId);
32
29
  };
33
30
  /**
34
- * Finds each duplicate route path
35
- *
36
- * The function THROWS AN ERROR when more than one state holds the same RESOLVED
37
- * complete path.
38
- *
39
- * The detection works on the resolved complete paths, after `buildRouteTree` put the
40
- * path of each parent before its relative routes. It does not work on the raw route
41
- * strings: two relative `"settings"` routes below two different parents resolve to
42
- * two different complete paths, and they are valid; a relative route and an absolute
43
- * route that resolve to the same URL are a real collision, and the function refuses
44
- * them. `buildRouteTree` calls this function when the complete paths are known.
45
- *
46
- * The reason: a routing on the URL requires one state for each URL. Two states at
47
- * the same path make the browser navigation ambiguous, for the BACK button and for a
48
- * direct URL. Use a different path for each state, or use one state with a
49
- * conditional render.
50
- *
51
- * @param routes - The array of the route entries with their resolved complete paths, for example the `RouteNode` objects
52
- * @throws {DuplicateRoutePathError} When the function finds two resolved route paths that are equal
53
- */
54
- export const detectDuplicateRoutes = (routes) => {
55
- const routeMap = new Map();
56
- // Build the map from each resolved complete path to its state IDs
57
- for (const route of routes) {
58
- const stateIds = routeMap.get(route.fullPath) || [];
59
- stateIds.push(route.stateId);
60
- routeMap.set(route.fullPath, stateIds);
61
- }
62
- // Find each duplicate, and throw an error
63
- const duplicates = [];
64
- for (const [routePath, stateIds] of routeMap.entries()) {
65
- if (stateIds.length > 1) {
66
- duplicates.push(` ${routePath}: [${stateIds.join(", ")}]`);
67
- }
68
- }
69
- if (duplicates.length > 0) {
70
- throw new DuplicateRoutePathError(duplicates);
71
- }
31
+ * Finds each duplicate route path
32
+ *
33
+ * The function THROWS AN ERROR when more than one state holds the same RESOLVED
34
+ * complete path.
35
+ *
36
+ * The detection works on the resolved complete paths, after `buildRouteTree` put the
37
+ * path of each parent before its relative routes. It does not work on the raw route
38
+ * strings: two relative `"settings"` routes below two different parents resolve to
39
+ * two different complete paths, and they are valid; a relative route and an absolute
40
+ * route that resolve to the same URL are a real collision, and the function refuses
41
+ * them. `buildRouteTree` calls this function when the complete paths are known.
42
+ *
43
+ * The reason: a routing on the URL requires one state for each URL. Two states at
44
+ * the same path make the browser navigation ambiguous, for the BACK button and for a
45
+ * direct URL. Use a different path for each state, or use one state with a
46
+ * conditional render.
47
+ *
48
+ * @param routes - The array of the route entries with their resolved complete paths, for example the `RouteNode` objects
49
+ * @throws {DuplicateRoutePathError} When the function finds two resolved route paths that are equal
50
+ */
51
+ var detectDuplicateRoutes = (routes) => {
52
+ const routeMap = /* @__PURE__ */ new Map();
53
+ for (const route of routes) {
54
+ const stateIds = routeMap.get(route.fullPath) || [];
55
+ stateIds.push(route.stateId);
56
+ routeMap.set(route.fullPath, stateIds);
57
+ }
58
+ const duplicates = [];
59
+ for (const [routePath, stateIds] of routeMap.entries()) if (stateIds.length > 1) duplicates.push(` ${routePath}: [${stateIds.join(", ")}]`);
60
+ if (duplicates.length > 0) throw new DuplicateRoutePathError(duplicates);
72
61
  };
62
+ //#endregion
63
+ export { detectDuplicateRoutes, validateRouteFormat, validateStateExists };
64
+
73
65
  //# sourceMappingURL=validate-routes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate-routes.js","sourceRoot":"","sources":["../src/validate-routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAehG;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,SAAiB,EAAE,OAAe,EAAQ,EAAE;IAC/E,IAAI,CAAC,SAAS,EAAE,CAAC;QAChB,MAAM,IAAI,mBAAmB,CAAC,OAAO,CAAC,CAAC;IACxC,CAAC;AACF,CAAC,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,OAAe,EAAE,QAAqB,EAAQ,EAAE;IACnF,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,mBAAmB,CAAC,OAAO,CAAC,CAAC;IACxC,CAAC;AACF,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,MAA2B,EAAQ,EAAE;IAC1E,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAoB,CAAC;IAE7C,kEAAkE;IAClE,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC5B,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACpD,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC7B,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACxC,CAAC;IAED,0CAA0C;IAC1C,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,KAAK,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,IAAI,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;QACxD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzB,UAAU,CAAC,IAAI,CAAC,KAAK,SAAS,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7D,CAAC;IACF,CAAC;IAED,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,uBAAuB,CAAC,UAAU,CAAC,CAAC;IAC/C,CAAC;AACF,CAAC,CAAC"}
1
+ {"version":3,"file":"validate-routes.js","names":[],"sources":["../src/validate-routes.ts"],"sourcesContent":["import { EmptyRoutePathError, InvalidStateIdError, DuplicateRoutePathError } from \"./errors.js\";\n\n/**\n * A route entry with its RESOLVED complete path, which is the path after the tree\n * construction put the path of the parent before each relative route.\n *\n * `RouteNode` satisfies this shape structurally.\n */\nexport interface ResolvedRoutePath {\n\t/** The identifier of the state that owns the route, for example `\"app.dashboard.settings\"`. */\n\tstateId: string;\n\t/** The resolved absolute route path, for example `\"/dashboard/settings\"`. */\n\tfullPath: string;\n}\n\n/**\n * Checks the format of a route path\n *\n * The function requires a string that is not empty. It accepts an absolute path\n * (\"/foo\") and also a relative path (\"foo\" or \"child/nested\"). A relative route\n * receives the path prefix of its parent state during the construction of the tree.\n *\n * @param routePath - The route path to check: absolute or relative\n * @param stateId - The identifier of the state, for the error message\n * @throws {EmptyRoutePathError} When the route path is empty\n */\nexport const validateRouteFormat = (routePath: string, stateId: string): void => {\n\tif (!routePath) {\n\t\tthrow new EmptyRoutePathError(stateId);\n\t}\n};\n\n/**\n * Checks that the set of the state IDs holds a state\n *\n * The function requires each state ID of a reference in the machine graph. This\n * check at the build time therefore stops a broken route reference.\n *\n * @param stateId - The identifier of the state to check\n * @param stateIds - The set of every known state ID of the machine graph\n * @throws {InvalidStateIdError} When the set does not hold the state ID\n */\nexport const validateStateExists = (stateId: string, stateIds: Set<string>): void => {\n\tif (!stateIds.has(stateId)) {\n\t\tthrow new InvalidStateIdError(stateId);\n\t}\n};\n\n/**\n * Finds each duplicate route path\n *\n * The function THROWS AN ERROR when more than one state holds the same RESOLVED\n * complete path.\n *\n * The detection works on the resolved complete paths, after `buildRouteTree` put the\n * path of each parent before its relative routes. It does not work on the raw route\n * strings: two relative `\"settings\"` routes below two different parents resolve to\n * two different complete paths, and they are valid; a relative route and an absolute\n * route that resolve to the same URL are a real collision, and the function refuses\n * them. `buildRouteTree` calls this function when the complete paths are known.\n *\n * The reason: a routing on the URL requires one state for each URL. Two states at\n * the same path make the browser navigation ambiguous, for the BACK button and for a\n * direct URL. Use a different path for each state, or use one state with a\n * conditional render.\n *\n * @param routes - The array of the route entries with their resolved complete paths, for example the `RouteNode` objects\n * @throws {DuplicateRoutePathError} When the function finds two resolved route paths that are equal\n */\nexport const detectDuplicateRoutes = (routes: ResolvedRoutePath[]): void => {\n\tconst routeMap = new Map<string, string[]>();\n\n\t// Build the map from each resolved complete path to its state IDs\n\tfor (const route of routes) {\n\t\tconst stateIds = routeMap.get(route.fullPath) || [];\n\t\tstateIds.push(route.stateId);\n\t\trouteMap.set(route.fullPath, stateIds);\n\t}\n\n\t// Find each duplicate, and throw an error\n\tconst duplicates: string[] = [];\n\tfor (const [routePath, stateIds] of routeMap.entries()) {\n\t\tif (stateIds.length > 1) {\n\t\t\tduplicates.push(` ${routePath}: [${stateIds.join(\", \")}]`);\n\t\t}\n\t}\n\n\tif (duplicates.length > 0) {\n\t\tthrow new DuplicateRoutePathError(duplicates);\n\t}\n};\n"],"mappings":";;;;;;;;;;;;;AA0BA,IAAa,uBAAuB,WAAmB,YAA0B;CAChF,IAAI,CAAC,WACJ,MAAM,IAAI,oBAAoB,OAAO;AAEvC;;;;;;;;;;;AAYA,IAAa,uBAAuB,SAAiB,aAAgC;CACpF,IAAI,CAAC,SAAS,IAAI,OAAO,GACxB,MAAM,IAAI,oBAAoB,OAAO;AAEvC;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAa,yBAAyB,WAAsC;CAC3E,MAAM,2BAAW,IAAI,IAAsB;CAG3C,KAAK,MAAM,SAAS,QAAQ;EAC3B,MAAM,WAAW,SAAS,IAAI,MAAM,QAAQ,KAAK,CAAC;EAClD,SAAS,KAAK,MAAM,OAAO;EAC3B,SAAS,IAAI,MAAM,UAAU,QAAQ;CACtC;CAGA,MAAM,aAAuB,CAAC;CAC9B,KAAK,MAAM,CAAC,WAAW,aAAa,SAAS,QAAQ,GACpD,IAAI,SAAS,SAAS,GACrB,WAAW,KAAK,KAAK,UAAU,KAAK,SAAS,KAAK,IAAI,EAAE,EAAE;CAI5D,IAAI,WAAW,SAAS,GACvB,MAAM,IAAI,wBAAwB,UAAU;AAE9C"}
@@ -1,40 +1,41 @@
1
+ import { RouteMap } from "../base-route-map.js";
1
2
  import { extractMachineRoutes } from "./extract-routes.js";
2
3
  import { getRoutableRoutes } from "./query.js";
3
- import { RouteMap } from "../base-route-map.js";
4
+ //#region packages/play-router/src/xstate/create-route-map.ts
4
5
  /**
5
- * Creates a `RouteMap` from an XState state machine.
6
- *
7
- * The function reads every state with a route, which means each state with a
8
- * `meta.route` field. It keeps the routable states alone, and it drops the synthetic
9
- * root of the tree. `createRouteMapFromTree` of the main entry point drops the same
10
- * node, and the two functions therefore answer the same map for the same machine. It then builds the lookup structure between a path and a
11
- * stateId, for both directions. A subclass of `RouterBridgeBase` uses the map: it
12
- * converts each change of the browser URL into a `play.route` actor event, and each
13
- * actor route into a URL.
14
- *
15
- * @param machine - The XState v5 state machine, with a `meta.route` annotation on each state with a route.
16
- * @param options - The optional configuration. Give `{ pathCache }` to replace the
17
- * default LRU cache of the parameterized path lookups with one that you own.
18
- * @returns A `RouteMap` for each adapter on `RouterBridgeBase`.
19
- *
20
- * @example
21
- * ```typescript
22
- * import { createRouteMap } from '@xmachines/play-router/xstate';
23
- * import { connectRouter } from '@xmachines/play-dom-router';
24
- *
25
- * const routeMap = createRouteMap(myMachine);
26
- * const disconnect = connectRouter({ actor, router, routeMap });
27
- * ```
28
- */
29
- export function createRouteMap(machine, options) {
30
- // The options reach the EXTRACTION too. `extractMachineRoutes` parses the route of
31
- // every node, so a caller that passes a cache for the isolation of a tenant would
32
- // otherwise write each of those patterns into the shared default cache.
33
- const routeTree = extractMachineRoutes(machine, options);
34
- const routes = getRoutableRoutes(routeTree);
35
- return new RouteMap(routes.map((node) => ({
36
- stateId: node.stateId,
37
- path: node.fullPath,
38
- })), options);
6
+ * Creates a `RouteMap` from an XState state machine.
7
+ *
8
+ * The function reads every state with a route, which means each state with a
9
+ * `meta.route` field. It keeps the routable states alone, and it drops the synthetic
10
+ * root of the tree. `createRouteMapFromTree` of the main entry point drops the same
11
+ * node, and the two functions therefore answer the same map for the same machine. It then builds the lookup structure between a path and a
12
+ * stateId, for both directions. A subclass of `RouterBridgeBase` uses the map: it
13
+ * converts each change of the browser URL into a `play.route` actor event, and each
14
+ * actor route into a URL.
15
+ *
16
+ * @param machine - The XState v5 state machine, with a `meta.route` annotation on each state with a route.
17
+ * @param options - The optional configuration. Give `{ pathCache }` to replace the
18
+ * default LRU cache of the parameterized path lookups with one that you own.
19
+ * @returns A `RouteMap` for each adapter on `RouterBridgeBase`.
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * import { createRouteMap } from '@xmachines/play-router/xstate';
24
+ * import { connectRouter } from '@xmachines/play-dom-router';
25
+ *
26
+ * const routeMap = createRouteMap(myMachine);
27
+ * const disconnect = connectRouter({ actor, router, routeMap });
28
+ * ```
29
+ */
30
+ function createRouteMap(machine, options) {
31
+ const routeTree = extractMachineRoutes(machine, options);
32
+ const routes = getRoutableRoutes(routeTree);
33
+ return new RouteMap(routes.map((node) => ({
34
+ stateId: node.stateId,
35
+ path: node.fullPath
36
+ })), options);
39
37
  }
38
+ //#endregion
39
+ export { createRouteMap };
40
+
40
41
  //# sourceMappingURL=create-route-map.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-route-map.js","sourceRoot":"","sources":["../../src/xstate/create-route-map.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGhD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,cAAc,CAAC,OAAwB,EAAE,OAAyB;IACjF,mFAAmF;IACnF,kFAAkF;IAClF,wEAAwE;IACxE,MAAM,SAAS,GAAG,oBAAoB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACzD,MAAM,MAAM,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC5C,OAAO,IAAI,QAAQ,CAClB,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACrB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,IAAI,EAAE,IAAI,CAAC,QAAQ;KACnB,CAAC,CAAC,EACH,OAAO,CACP,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"create-route-map.js","names":[],"sources":["../../src/xstate/create-route-map.ts"],"sourcesContent":["import type { AnyStateMachine } from \"xstate\";\nimport { extractMachineRoutes } from \"./extract-routes.js\";\nimport { getRoutableRoutes } from \"./query.js\";\nimport { RouteMap } from \"../base-route-map.js\";\nimport type { RouteMapOptions } from \"../types.js\";\n\n/**\n * Creates a `RouteMap` from an XState state machine.\n *\n * The function reads every state with a route, which means each state with a\n * `meta.route` field. It keeps the routable states alone, and it drops the synthetic\n * root of the tree. `createRouteMapFromTree` of the main entry point drops the same\n * node, and the two functions therefore answer the same map for the same machine. It then builds the lookup structure between a path and a\n * stateId, for both directions. A subclass of `RouterBridgeBase` uses the map: it\n * converts each change of the browser URL into a `play.route` actor event, and each\n * actor route into a URL.\n *\n * @param machine - The XState v5 state machine, with a `meta.route` annotation on each state with a route.\n * @param options - The optional configuration. Give `{ pathCache }` to replace the\n * default LRU cache of the parameterized path lookups with one that you own.\n * @returns A `RouteMap` for each adapter on `RouterBridgeBase`.\n *\n * @example\n * ```typescript\n * import { createRouteMap } from '@xmachines/play-router/xstate';\n * import { connectRouter } from '@xmachines/play-dom-router';\n *\n * const routeMap = createRouteMap(myMachine);\n * const disconnect = connectRouter({ actor, router, routeMap });\n * ```\n */\nexport function createRouteMap(machine: AnyStateMachine, options?: RouteMapOptions): RouteMap {\n\t// The options reach the EXTRACTION too. `extractMachineRoutes` parses the route of\n\t// every node, so a caller that passes a cache for the isolation of a tenant would\n\t// otherwise write each of those patterns into the shared default cache.\n\tconst routeTree = extractMachineRoutes(machine, options);\n\tconst routes = getRoutableRoutes(routeTree);\n\treturn new RouteMap(\n\t\troutes.map((node) => ({\n\t\t\tstateId: node.stateId,\n\t\t\tpath: node.fullPath,\n\t\t})),\n\t\toptions,\n\t);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,eAAe,SAA0B,SAAqC;CAI7F,MAAM,YAAY,qBAAqB,SAAS,OAAO;CACvD,MAAM,SAAS,kBAAkB,SAAS;CAC1C,OAAO,IAAI,SACV,OAAO,KAAK,UAAU;EACrB,SAAS,KAAK;EACd,MAAM,KAAK;CACZ,EAAE,GACF,OACD;AACD"}
@@ -1,92 +1,75 @@
1
- import { parsePattern } from "@xmachines/play-url";
2
- import { machineToGraph } from "./machine-to-graph.js";
3
1
  import { validateRouteFormat, validateStateExists } from "../validate-routes.js";
4
2
  import { buildRouteTree } from "../build-tree.js";
3
+ import { machineToGraph } from "./machine-to-graph.js";
4
+ import { parsePattern } from "@xmachines/play-url";
5
+ //#region packages/play-router/src/xstate/extract-routes.ts
5
6
  /**
6
- * Builds the statePath of a graph node, and it walks the chain of the parentId
7
- * values.
8
- *
9
- * The function makes an array of the stateId segments, from the root to this node,
10
- * and the array holds no root. This is the format of the parent lookup in
11
- * buildRouteTree.
12
- *
13
- * @param graphNode - The node of the path
14
- * @param graphNodeMap - The map from a node id to a GraphNode, for a parent lookup in O(1)
15
- * @returns The array of the stateId segments, for example ["dashboard", "overview"]
16
- */
17
- const buildStatePath = (graphNode, graphNodeMap) => {
18
- const path = [];
19
- let current = graphNode;
20
- while (current && current.parentId !== undefined && current.parentId !== null) {
21
- path.unshift(current.data.stateId);
22
- current = graphNodeMap.get(current.parentId);
23
- }
24
- return path;
7
+ * Builds the statePath of a graph node, and it walks the chain of the parentId
8
+ * values.
9
+ *
10
+ * The function makes an array of the stateId segments, from the root to this node,
11
+ * and the array holds no root. This is the format of the parent lookup in
12
+ * buildRouteTree.
13
+ *
14
+ * @param graphNode - The node of the path
15
+ * @param graphNodeMap - The map from a node id to a GraphNode, for a parent lookup in O(1)
16
+ * @returns The array of the stateId segments, for example ["dashboard", "overview"]
17
+ */
18
+ var buildStatePath = (graphNode, graphNodeMap) => {
19
+ const path = [];
20
+ let current = graphNode;
21
+ while (current && current.parentId !== void 0 && current.parentId !== null) {
22
+ path.unshift(current.data.stateId);
23
+ current = graphNodeMap.get(current.parentId);
24
+ }
25
+ return path;
25
26
  };
26
27
  /**
27
- * Reads the route information of the MachineNodeData object of a graph node.
28
- * The function returns null when the node has no route.
29
- */
30
- const extractRouteFromNode = (graphNode, stateIds, graphNodeMap, options) => {
31
- const { stateId, route, meta } = graphNode.data;
32
- // The node has no route
33
- if (!route)
34
- return null;
35
- // Check the format. The path must start with /
36
- validateRouteFormat(route, stateId);
37
- // Check that the graph holds the state. This test is redundant, and it guards against a corruption
38
- validateStateExists(stateId, stateIds);
39
- // Build the statePath from the chain of the parentId values. This works for an explicit ID and also for a generated one
40
- const statePath = buildStatePath(graphNode, graphNodeMap);
41
- // The parse answers this, and not a search for a ":". `route.includes(":")` was the
42
- // last expression that read the grammar on its own: it reported no pattern for
43
- // `/docs/*`, for `/books{/:id}?` and for an escaped literal, so a consumer that reads
44
- // `node.pattern` learned that a parameterized route is a literal path.
45
- const pattern = parsePattern(route, options).parameterized ? route : undefined;
46
- // Build the original shape of the metadata again, from the data of the graph
47
- const metadata = meta && typeof meta === "object" && "route" in meta
48
- ? meta["route"]
49
- : route;
50
- const result = {
51
- stateId,
52
- statePath,
53
- routePath: route,
54
- isAbsolute: route.startsWith("/"),
55
- routable: true,
56
- metadata,
57
- };
58
- if (pattern)
59
- result.pattern = pattern;
60
- return result;
28
+ * Reads the route information of the MachineNodeData object of a graph node.
29
+ * The function returns null when the node has no route.
30
+ */
31
+ var extractRouteFromNode = (graphNode, stateIds, graphNodeMap, options) => {
32
+ const { stateId, route, meta } = graphNode.data;
33
+ if (!route) return null;
34
+ validateRouteFormat(route, stateId);
35
+ validateStateExists(stateId, stateIds);
36
+ const statePath = buildStatePath(graphNode, graphNodeMap);
37
+ const pattern = parsePattern(route, options).parameterized ? route : void 0;
38
+ const metadata = meta && typeof meta === "object" && "route" in meta ? meta["route"] : route;
39
+ const result = {
40
+ stateId,
41
+ statePath,
42
+ routePath: route,
43
+ isAbsolute: route.startsWith("/"),
44
+ routable: true,
45
+ metadata
46
+ };
47
+ if (pattern) result.pattern = pattern;
48
+ return result;
61
49
  };
62
50
  /**
63
- * Reads the complete route tree from the graph of a state machine
64
- *
65
- * The function converts the XState machine into a Graph of @statelyai/graph, with
66
- * machineToGraph(). It then walks the graph nodes: it reads the route metadata of
67
- * each node, it checks each route reference, and it builds a hierarchical RouteTree.
68
- * The RouteTree of the return value also holds the graph, for a later query that
69
- * needs the transitions.
70
- *
71
- * @param machine - The XState v5 state machine
72
- * @returns The route tree, with its root, its byStateId map, its byPath map, and its graph
73
- */
74
- export const extractMachineRoutes = (machine, options) => {
75
- // 1. Convert the machine into a graph
76
- const graph = machineToGraph(machine);
77
- // 2. Build the set of the state IDs, for the check, and the node map, for a parent lookup in O(1)
78
- const stateIds = new Set(graph.nodes.map((n) => n.data.stateId));
79
- const graphNodeMap = new Map(graph.nodes.map((n) => [n.id, n]));
80
- // 3. Read the routes of the graph nodes
81
- const routes = graph.nodes
82
- .map((node) => extractRouteFromNode(node, stateIds, graphNodeMap, options))
83
- .filter((r) => r !== null);
84
- // 4. Build the hierarchical tree, and attach the graph.
85
- // buildRouteTree finds each duplicate route, AFTER it resolved each relative route
86
- // to a complete path. The raw route strings are not comparable, because two
87
- // relative "settings" routes below two different parents are two different
88
- // routes.
89
- const tree = buildRouteTree(routes);
90
- return { ...tree, graph };
51
+ * Reads the complete route tree from the graph of a state machine
52
+ *
53
+ * The function converts the XState machine into a Graph of @statelyai/graph, with
54
+ * machineToGraph(). It then walks the graph nodes: it reads the route metadata of
55
+ * each node, it checks each route reference, and it builds a hierarchical RouteTree.
56
+ * The RouteTree of the return value also holds the graph, for a later query that
57
+ * needs the transitions.
58
+ *
59
+ * @param machine - The XState v5 state machine
60
+ * @returns The route tree, with its root, its byStateId map, its byPath map, and its graph
61
+ */
62
+ var extractMachineRoutes = (machine, options) => {
63
+ const graph = machineToGraph(machine);
64
+ const stateIds = new Set(graph.nodes.map((n) => n.data.stateId));
65
+ const graphNodeMap = new Map(graph.nodes.map((n) => [n.id, n]));
66
+ const routes = graph.nodes.map((node) => extractRouteFromNode(node, stateIds, graphNodeMap, options)).filter((r) => r !== null);
67
+ return {
68
+ ...buildRouteTree(routes),
69
+ graph
70
+ };
91
71
  };
72
+ //#endregion
73
+ export { extractMachineRoutes };
74
+
92
75
  //# sourceMappingURL=extract-routes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"extract-routes.js","sourceRoot":"","sources":["../../src/xstate/extract-routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"extract-routes.js","names":[],"sources":["../../src/xstate/extract-routes.ts"],"sourcesContent":["import { parsePattern } from \"@xmachines/play-url\";\nimport type { ParseOptions } from \"@xmachines/play-url\";\nimport type { AnyStateMachine } from \"xstate\";\nimport type { GraphNode } from \"@statelyai/graph\";\nimport type { RouteTree, RouteInfo, RouteMetadata } from \"../types.js\";\nimport type { MachineNodeData } from \"./machine-types.js\";\nimport { machineToGraph, type MachineGraph } from \"./machine-to-graph.js\";\nimport { validateRouteFormat, validateStateExists } from \"../validate-routes.js\";\nimport { buildRouteTree } from \"../build-tree.js\";\n\n/**\n * Builds the statePath of a graph node, and it walks the chain of the parentId\n * values.\n *\n * The function makes an array of the stateId segments, from the root to this node,\n * and the array holds no root. This is the format of the parent lookup in\n * buildRouteTree.\n *\n * @param graphNode - The node of the path\n * @param graphNodeMap - The map from a node id to a GraphNode, for a parent lookup in O(1)\n * @returns The array of the stateId segments, for example [\"dashboard\", \"overview\"]\n */\nconst buildStatePath = (\n\tgraphNode: GraphNode<MachineNodeData>,\n\tgraphNodeMap: Map<string, GraphNode<MachineNodeData>>,\n): string[] => {\n\tconst path: string[] = [];\n\tlet current: GraphNode<MachineNodeData> | undefined = graphNode;\n\n\twhile (current && current.parentId !== undefined && current.parentId !== null) {\n\t\tpath.unshift(current.data.stateId);\n\t\tcurrent = graphNodeMap.get(current.parentId);\n\t}\n\n\treturn path;\n};\n\n/**\n * Reads the route information of the MachineNodeData object of a graph node.\n * The function returns null when the node has no route.\n */\nconst extractRouteFromNode = (\n\tgraphNode: GraphNode<MachineNodeData>,\n\tstateIds: Set<string>,\n\tgraphNodeMap: Map<string, GraphNode<MachineNodeData>>,\n\toptions?: ParseOptions,\n): RouteInfo | null => {\n\tconst { stateId, route, meta } = graphNode.data;\n\n\t// The node has no route\n\tif (!route) return null;\n\n\t// Check the format. The path must start with /\n\tvalidateRouteFormat(route, stateId);\n\n\t// Check that the graph holds the state. This test is redundant, and it guards against a corruption\n\tvalidateStateExists(stateId, stateIds);\n\n\t// Build the statePath from the chain of the parentId values. This works for an explicit ID and also for a generated one\n\tconst statePath = buildStatePath(graphNode, graphNodeMap);\n\n\t// The parse answers this, and not a search for a \":\". `route.includes(\":\")` was the\n\t// last expression that read the grammar on its own: it reported no pattern for\n\t// `/docs/*`, for `/books{/:id}?` and for an escaped literal, so a consumer that reads\n\t// `node.pattern` learned that a parameterized route is a literal path.\n\tconst pattern = parsePattern(route, options).parameterized ? route : undefined;\n\n\t// Build the original shape of the metadata again, from the data of the graph\n\tconst metadata: RouteMetadata =\n\t\tmeta && typeof meta === \"object\" && \"route\" in meta\n\t\t\t? (meta[\"route\"] as RouteMetadata)\n\t\t\t: route;\n\n\tconst result: RouteInfo = {\n\t\tstateId,\n\t\tstatePath,\n\t\troutePath: route,\n\t\tisAbsolute: route.startsWith(\"/\"),\n\t\troutable: true,\n\t\tmetadata,\n\t};\n\n\tif (pattern) result.pattern = pattern;\n\n\treturn result;\n};\n\n/**\n * Reads the complete route tree from the graph of a state machine\n *\n * The function converts the XState machine into a Graph of @statelyai/graph, with\n * machineToGraph(). It then walks the graph nodes: it reads the route metadata of\n * each node, it checks each route reference, and it builds a hierarchical RouteTree.\n * The RouteTree of the return value also holds the graph, for a later query that\n * needs the transitions.\n *\n * @param machine - The XState v5 state machine\n * @returns The route tree, with its root, its byStateId map, its byPath map, and its graph\n */\nexport const extractMachineRoutes = (\n\tmachine: AnyStateMachine,\n\toptions?: ParseOptions,\n): RouteTree<MachineGraph> => {\n\t// 1. Convert the machine into a graph\n\tconst graph: MachineGraph = machineToGraph(machine);\n\n\t// 2. Build the set of the state IDs, for the check, and the node map, for a parent lookup in O(1)\n\tconst stateIds = new Set(graph.nodes.map((n) => n.data.stateId));\n\tconst graphNodeMap = new Map(graph.nodes.map((n) => [n.id, n]));\n\n\t// 3. Read the routes of the graph nodes\n\tconst routes = graph.nodes\n\t\t.map((node) => extractRouteFromNode(node, stateIds, graphNodeMap, options))\n\t\t.filter((r): r is RouteInfo => r !== null);\n\n\t// 4. Build the hierarchical tree, and attach the graph.\n\t// buildRouteTree finds each duplicate route, AFTER it resolved each relative route\n\t// to a complete path. The raw route strings are not comparable, because two\n\t// relative \"settings\" routes below two different parents are two different\n\t// routes.\n\tconst tree = buildRouteTree(routes);\n\treturn { ...tree, graph };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAsBA,IAAM,kBACL,WACA,iBACc;CACd,MAAM,OAAiB,CAAC;CACxB,IAAI,UAAkD;CAEtD,OAAO,WAAW,QAAQ,aAAa,KAAA,KAAa,QAAQ,aAAa,MAAM;EAC9E,KAAK,QAAQ,QAAQ,KAAK,OAAO;EACjC,UAAU,aAAa,IAAI,QAAQ,QAAQ;CAC5C;CAEA,OAAO;AACR;;;;;AAMA,IAAM,wBACL,WACA,UACA,cACA,YACsB;CACtB,MAAM,EAAE,SAAS,OAAO,SAAS,UAAU;CAG3C,IAAI,CAAC,OAAO,OAAO;CAGnB,oBAAoB,OAAO,OAAO;CAGlC,oBAAoB,SAAS,QAAQ;CAGrC,MAAM,YAAY,eAAe,WAAW,YAAY;CAMxD,MAAM,UAAU,aAAa,OAAO,OAAO,CAAC,CAAC,gBAAgB,QAAQ,KAAA;CAGrE,MAAM,WACL,QAAQ,OAAO,SAAS,YAAY,WAAW,OAC3C,KAAK,WACN;CAEJ,MAAM,SAAoB;EACzB;EACA;EACA,WAAW;EACX,YAAY,MAAM,WAAW,GAAG;EAChC,UAAU;EACV;CACD;CAEA,IAAI,SAAS,OAAO,UAAU;CAE9B,OAAO;AACR;;;;;;;;;;;;;AAcA,IAAa,wBACZ,SACA,YAC6B;CAE7B,MAAM,QAAsB,eAAe,OAAO;CAGlD,MAAM,WAAW,IAAI,IAAI,MAAM,MAAM,KAAK,MAAM,EAAE,KAAK,OAAO,CAAC;CAC/D,MAAM,eAAe,IAAI,IAAI,MAAM,MAAM,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;CAG9D,MAAM,SAAS,MAAM,MACnB,KAAK,SAAS,qBAAqB,MAAM,UAAU,cAAc,OAAO,CAAC,CAAC,CAC1E,QAAQ,MAAsB,MAAM,IAAI;CAQ1C,OAAO;EAAE,GADI,eAAe,MAChB;EAAM;CAAM;AACzB"}
@@ -1,33 +1,5 @@
1
- /**
2
- * `@xmachines/play-router/xstate` - the routes of an XState machine
3
- *
4
- * A route of XMachines is a fact about a STATE. This entry point reads that fact out of an
5
- * XState v5 machine: it walks the machine, collects each `meta.route`, builds the route
6
- * tree, and answers the questions that the tree can be asked — which routes are reachable,
7
- * which are navigable from a state, and which path maps to which state id.
8
- *
9
- * The MAIN entry point holds what happens AFTERWARDS: the route map, the match of a
10
- * location, the bridge, and the provider lifecycle. It names no machine, and it reads this
11
- * module never. `xstate` is therefore an OPTIONAL peer of this package, and a bundler drops
12
- * `@statelyai/graph` for a consumer that imports this entry point nowhere.
13
- *
14
- * This is the same shape as `@xmachines/play-xstate/routing` and
15
- * `@xmachines/play-xstate/view`: an optional capability behind its own entry point, with
16
- * its own dependency.
17
- *
18
- * @example
19
- * ```typescript
20
- * import { createRouteMap } from "@xmachines/play-router/xstate";
21
- * import { connectRouter } from "@xmachines/play-dom-router";
22
- *
23
- * const routeMap = createRouteMap(myMachine);
24
- * ```
25
- *
26
- * @packageDocumentation
27
- * @see [Routing guide](../../../docs/guides/routing.md)
28
- */
29
- export { extractMachineRoutes } from "./extract-routes.js";
30
- export { createRouteMap } from "./create-route-map.js";
31
- export { machineToGraph } from "./machine-to-graph.js";
32
- export { getNavigableRoutes, getRoutableRoutes, routeExists, getTransitionReachableRoutes, isRouteReachable, getRouteMappings, } from "./query.js";
33
- //# sourceMappingURL=index.js.map
1
+ import { machineToGraph } from "./machine-to-graph.js";
2
+ import { extractMachineRoutes } from "./extract-routes.js";
3
+ import { getNavigableRoutes, getRoutableRoutes, getRouteMappings, getTransitionReachableRoutes, isRouteReachable, routeExists } from "./query.js";
4
+ import { createRouteMap } from "./create-route-map.js";
5
+ export { createRouteMap, extractMachineRoutes, getNavigableRoutes, getRoutableRoutes, getRouteMappings, getTransitionReachableRoutes, isRouteReachable, machineToGraph, routeExists };