@xmachines/play-router 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/.oxfmtrc.json +3 -0
  2. package/.oxlintrc.json +3 -0
  3. package/README.md +436 -0
  4. package/coverage/base.css +224 -0
  5. package/coverage/block-navigation.js +87 -0
  6. package/coverage/build-tree.ts.html +316 -0
  7. package/coverage/connect-router.ts.html +505 -0
  8. package/coverage/coverage-summary.json +15 -0
  9. package/coverage/crawl-machine.ts.html +385 -0
  10. package/coverage/create-browser-history.ts.html +556 -0
  11. package/coverage/create-route-map.ts.html +400 -0
  12. package/coverage/create-router.ts.html +328 -0
  13. package/coverage/extract-route.ts.html +322 -0
  14. package/coverage/extract-routes.ts.html +286 -0
  15. package/coverage/favicon.png +0 -0
  16. package/coverage/index.html +296 -0
  17. package/coverage/index.ts.html +610 -0
  18. package/coverage/prettify.css +1 -0
  19. package/coverage/prettify.js +2 -0
  20. package/coverage/query.ts.html +307 -0
  21. package/coverage/router-bridge-base.ts.html +919 -0
  22. package/coverage/sort-arrow-sprite.png +0 -0
  23. package/coverage/sorter.js +210 -0
  24. package/coverage/types.ts.html +787 -0
  25. package/coverage/validate-routes.ts.html +319 -0
  26. package/dist/build-tree.d.ts +13 -0
  27. package/dist/build-tree.d.ts.map +1 -0
  28. package/dist/build-tree.js +67 -0
  29. package/dist/build-tree.js.map +1 -0
  30. package/dist/connect-router.d.ts +56 -0
  31. package/dist/connect-router.d.ts.map +1 -0
  32. package/dist/connect-router.js +119 -0
  33. package/dist/connect-router.js.map +1 -0
  34. package/dist/crawl-machine.d.ts +74 -0
  35. package/dist/crawl-machine.d.ts.map +1 -0
  36. package/dist/crawl-machine.js +95 -0
  37. package/dist/crawl-machine.js.map +1 -0
  38. package/dist/create-browser-history.d.ts +68 -0
  39. package/dist/create-browser-history.d.ts.map +1 -0
  40. package/dist/create-browser-history.js +94 -0
  41. package/dist/create-browser-history.js.map +1 -0
  42. package/dist/create-route-map.d.ts +46 -0
  43. package/dist/create-route-map.d.ts.map +1 -0
  44. package/dist/create-route-map.js +73 -0
  45. package/dist/create-route-map.js.map +1 -0
  46. package/dist/create-router.d.ts +73 -0
  47. package/dist/create-router.d.ts.map +1 -0
  48. package/dist/create-router.js +63 -0
  49. package/dist/create-router.js.map +1 -0
  50. package/dist/extract-route.d.ts +25 -0
  51. package/dist/extract-route.d.ts.map +1 -0
  52. package/dist/extract-route.js +63 -0
  53. package/dist/extract-route.js.map +1 -0
  54. package/dist/extract-routes.d.ts +41 -0
  55. package/dist/extract-routes.d.ts.map +1 -0
  56. package/dist/extract-routes.js +61 -0
  57. package/dist/extract-routes.js.map +1 -0
  58. package/dist/index.d.ts +56 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +141 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/query.d.ts +52 -0
  63. package/dist/query.d.ts.map +1 -0
  64. package/dist/query.js +69 -0
  65. package/dist/query.js.map +1 -0
  66. package/dist/router-bridge-base.d.ts +150 -0
  67. package/dist/router-bridge-base.d.ts.map +1 -0
  68. package/dist/router-bridge-base.js +240 -0
  69. package/dist/router-bridge-base.js.map +1 -0
  70. package/dist/types.d.ts +228 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +2 -0
  73. package/dist/types.js.map +1 -0
  74. package/dist/validate-routes.d.ts +39 -0
  75. package/dist/validate-routes.d.ts.map +1 -0
  76. package/dist/validate-routes.js +65 -0
  77. package/dist/validate-routes.js.map +1 -0
  78. package/examples/demo/README.md +127 -0
  79. package/examples/demo/index.html +41 -0
  80. package/examples/demo/package.json +27 -0
  81. package/examples/demo/src/main.ts +28 -0
  82. package/examples/demo/src/router.ts +37 -0
  83. package/examples/demo/src/shell.ts +316 -0
  84. package/examples/demo/test/browser/auth-flow.browser.test.ts +60 -0
  85. package/examples/demo/test/browser/startup.browser.test.ts +37 -0
  86. package/examples/demo/test/library-pattern.test.ts +51 -0
  87. package/examples/demo/tsconfig.json +17 -0
  88. package/examples/demo/vite.config.ts +7 -0
  89. package/examples/demo/vitest.browser.config.ts +20 -0
  90. package/examples/demo/vitest.config.ts +9 -0
  91. package/examples/shared/dist/auth-machine.d.ts +20 -0
  92. package/examples/shared/dist/auth-machine.d.ts.map +1 -0
  93. package/examples/shared/dist/auth-machine.js +212 -0
  94. package/examples/shared/dist/auth-machine.js.map +1 -0
  95. package/examples/shared/dist/catalog.d.ts +85 -0
  96. package/examples/shared/dist/catalog.d.ts.map +1 -0
  97. package/examples/shared/dist/catalog.js +86 -0
  98. package/examples/shared/dist/catalog.js.map +1 -0
  99. package/examples/shared/dist/index.d.ts +4 -0
  100. package/examples/shared/dist/index.d.ts.map +1 -0
  101. package/examples/shared/dist/index.js +3 -0
  102. package/examples/shared/dist/index.js.map +1 -0
  103. package/examples/shared/package.json +37 -0
  104. package/examples/shared/src/auth-machine.ts +234 -0
  105. package/examples/shared/src/catalog.ts +95 -0
  106. package/examples/shared/src/index.css +3 -0
  107. package/examples/shared/src/index.ts +3 -0
  108. package/examples/shared/src/styles/layout.css +413 -0
  109. package/examples/shared/src/styles/reset.css +42 -0
  110. package/examples/shared/src/styles/tokens.css +183 -0
  111. package/examples/shared/tsconfig.json +14 -0
  112. package/examples/shared/tsconfig.tsbuildinfo +1 -0
  113. package/package.json +44 -0
  114. package/src/build-tree.ts +77 -0
  115. package/src/connect-router.ts +142 -0
  116. package/src/crawl-machine.ts +100 -0
  117. package/src/create-browser-history.ts +157 -0
  118. package/src/create-route-map.ts +105 -0
  119. package/src/create-router.ts +87 -0
  120. package/src/extract-route.ts +79 -0
  121. package/src/extract-routes.ts +67 -0
  122. package/src/index.ts +175 -0
  123. package/src/query.ts +74 -0
  124. package/src/router-bridge-base.ts +279 -0
  125. package/src/types.ts +234 -0
  126. package/src/validate-routes.ts +76 -0
  127. package/test/connect-route-map.test.ts +320 -0
  128. package/test/crawl-extract.test.js +473 -0
  129. package/test/create-browser-history.test.ts +123 -0
  130. package/test/create-router.test.ts +23 -0
  131. package/test/extract-routes.test.ts +80 -0
  132. package/test/find-route-by-path-patterns.test.ts +69 -0
  133. package/test/integration.test.js +438 -0
  134. package/test/query.test.ts +56 -0
  135. package/test/router-bridge-base-edge.test.ts +165 -0
  136. package/test/router-bridge-base.test.ts +119 -0
  137. package/test/tree-query.test.js +692 -0
  138. package/test/validation.test.js +158 -0
  139. package/tsconfig.json +14 -0
  140. package/tsconfig.tsbuildinfo +1 -0
  141. package/vitest.config.ts +35 -0
@@ -0,0 +1,320 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { Signal } from "@xmachines/play-signals";
3
+ import { createRouteMap } from "../src/create-route-map.js";
4
+ import { connectRouter } from "../src/connect-router.js";
5
+
6
+ function createNode(id: string, fullPath: string, pattern?: string): any {
7
+ return {
8
+ id,
9
+ path: fullPath,
10
+ fullPath,
11
+ pattern,
12
+ stateId: id,
13
+ routable: true,
14
+ children: [] as any[],
15
+ parent: null as any,
16
+ metadata: {},
17
+ };
18
+ }
19
+
20
+ function createRouteTree(...nodes: Array<ReturnType<typeof createNode>>) {
21
+ const root = createNode("root", "/");
22
+ root.children = nodes;
23
+ for (const node of nodes) {
24
+ node.parent = root;
25
+ }
26
+
27
+ const byPath = new Map<string, (typeof nodes)[number]>();
28
+ const byStateId = new Map<string, (typeof nodes)[number]>();
29
+ for (const node of nodes) {
30
+ byPath.set(node.fullPath, node);
31
+ byStateId.set(node.id, node);
32
+ }
33
+
34
+ return {
35
+ root,
36
+ byPath,
37
+ byStateId,
38
+ };
39
+ }
40
+
41
+ describe("createRouteMap", () => {
42
+ test("resolves exact matches without params", () => {
43
+ const tree = createRouteTree(createNode("dashboard", "/dashboard"));
44
+ const routeMap = createRouteMap(tree as any);
45
+
46
+ expect(routeMap.resolve("/dashboard")).toEqual({
47
+ to: "#dashboard",
48
+ params: {},
49
+ });
50
+ });
51
+
52
+ test("resolves URLPattern routes with params", () => {
53
+ const tree = createRouteTree(createNode("docs", "/docs/*", "/docs/*"));
54
+ const routeMap = createRouteMap(tree as any);
55
+
56
+ const resolved = routeMap.resolve("/docs/guides/install");
57
+ expect(resolved.to).toBe("#docs");
58
+ expect(Object.keys(resolved.params).length).toBeGreaterThan(0);
59
+ });
60
+
61
+ test("ignores invalid patterns and warns once", () => {
62
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
63
+ const tree = createRouteTree(createNode("broken", "/broken", "("));
64
+
65
+ const routeMap = createRouteMap(tree as any);
66
+ expect(routeMap.resolve("/broken")).toEqual({ to: "#broken", params: {} });
67
+ expect(warnSpy).toHaveBeenCalled();
68
+
69
+ warnSpy.mockRestore();
70
+ });
71
+ });
72
+
73
+ describe("connectRouter", () => {
74
+ test("sends initial play.route when URL and actor route differ", () => {
75
+ const send = vi.fn();
76
+ const actor = {
77
+ send,
78
+ currentRoute: new Signal.State("/home"),
79
+ } as any;
80
+
81
+ let listener: ((location: { pathname: string }) => void) | undefined;
82
+ const unsubscribe = vi.fn();
83
+ const history = {
84
+ location: { pathname: "/dashboard", search: "", hash: "", state: null },
85
+ subscribe(fn: (location: { pathname: string }) => void) {
86
+ listener = fn;
87
+ return unsubscribe;
88
+ },
89
+ push: vi.fn(),
90
+ replace: vi.fn(),
91
+ } as any;
92
+
93
+ const disconnect = connectRouter({
94
+ actor,
95
+ router: { history } as any,
96
+ routeMap: {
97
+ resolve(pathname: string) {
98
+ return pathname === "/dashboard"
99
+ ? { to: "#dashboard", params: {} }
100
+ : { to: null, params: {} };
101
+ },
102
+ },
103
+ });
104
+
105
+ expect(listener).toBeTypeOf("function");
106
+ expect(send).toHaveBeenCalledWith({
107
+ type: "play.route",
108
+ to: "#dashboard",
109
+ params: {},
110
+ });
111
+
112
+ disconnect();
113
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
114
+ });
115
+
116
+ test("replaces history when actor redirects during history navigation", () => {
117
+ const actor = {
118
+ send: vi.fn(),
119
+ currentRoute: new Signal.State("/login"),
120
+ } as any;
121
+
122
+ let listener: ((location: { pathname: string }) => void) | undefined;
123
+ const history = {
124
+ location: { pathname: "/login", search: "", hash: "", state: null },
125
+ subscribe(fn: (location: { pathname: string }) => void) {
126
+ listener = fn;
127
+ return () => {};
128
+ },
129
+ push: vi.fn(),
130
+ replace: vi.fn(),
131
+ } as any;
132
+
133
+ const disconnect = connectRouter({
134
+ actor,
135
+ router: { history } as any,
136
+ routeMap: {
137
+ resolve(pathname: string) {
138
+ return pathname === "/login"
139
+ ? { to: "#login", params: {} }
140
+ : { to: null, params: {} };
141
+ },
142
+ },
143
+ });
144
+
145
+ actor.currentRoute.set("/");
146
+ listener?.({ pathname: "/login" });
147
+
148
+ expect(history.replace).toHaveBeenCalledWith("/");
149
+ disconnect();
150
+ });
151
+
152
+ test("does not replace history when actor route equals incoming location", () => {
153
+ const actor = {
154
+ send: vi.fn(),
155
+ currentRoute: new Signal.State("/login"),
156
+ } as any;
157
+
158
+ let listener: ((location: { pathname: string }) => void) | undefined;
159
+ const history = {
160
+ location: { pathname: "/home", search: "", hash: "", state: null },
161
+ subscribe(fn: (location: { pathname: string }) => void) {
162
+ listener = fn;
163
+ return () => {};
164
+ },
165
+ push: vi.fn(),
166
+ replace: vi.fn(),
167
+ } as any;
168
+
169
+ const disconnect = connectRouter({
170
+ actor,
171
+ router: { history } as any,
172
+ routeMap: {
173
+ resolve(pathname: string) {
174
+ return pathname === "/login"
175
+ ? { to: "#login", params: {} }
176
+ : { to: null, params: {} };
177
+ },
178
+ },
179
+ });
180
+
181
+ listener?.({ pathname: "/login" });
182
+
183
+ expect(history.replace).not.toHaveBeenCalled();
184
+ disconnect();
185
+ });
186
+
187
+ test("ignores unknown history path and does not send play.route", () => {
188
+ const actor = {
189
+ send: vi.fn(),
190
+ currentRoute: new Signal.State("/home"),
191
+ } as any;
192
+
193
+ let listener: ((location: { pathname: string }) => void) | undefined;
194
+ const history = {
195
+ location: { pathname: "/home", search: "", hash: "", state: null },
196
+ subscribe(fn: (location: { pathname: string }) => void) {
197
+ listener = fn;
198
+ return () => {};
199
+ },
200
+ push: vi.fn(),
201
+ replace: vi.fn(),
202
+ } as any;
203
+
204
+ const disconnect = connectRouter({
205
+ actor,
206
+ router: { history } as any,
207
+ routeMap: {
208
+ resolve() {
209
+ return { to: null, params: {} };
210
+ },
211
+ },
212
+ });
213
+
214
+ actor.send.mockClear();
215
+ listener?.({ pathname: "/unknown" });
216
+
217
+ expect(actor.send).not.toHaveBeenCalled();
218
+ disconnect();
219
+ });
220
+
221
+ test("skips history subscriber when history change was triggered by actor sync", async () => {
222
+ const actor = {
223
+ send: vi.fn(),
224
+ currentRoute: new Signal.State("/home"),
225
+ } as any;
226
+
227
+ let listener: ((location: { pathname: string }) => void) | undefined;
228
+ const history = {
229
+ location: { pathname: "/home", search: "", hash: "", state: null },
230
+ subscribe(fn: (location: { pathname: string }) => void) {
231
+ listener = fn;
232
+ return () => {};
233
+ },
234
+ push(path: string) {
235
+ listener?.({ pathname: path });
236
+ },
237
+ replace: vi.fn(),
238
+ } as any;
239
+
240
+ const disconnect = connectRouter({
241
+ actor,
242
+ router: { history } as any,
243
+ routeMap: {
244
+ resolve(pathname: string) {
245
+ return pathname === "/dashboard"
246
+ ? { to: "#dashboard", params: {} }
247
+ : { to: null, params: {} };
248
+ },
249
+ },
250
+ });
251
+
252
+ actor.send.mockClear();
253
+ actor.currentRoute.set("/dashboard");
254
+ await new Promise<void>((resolve) => queueMicrotask(resolve));
255
+
256
+ expect(actor.send).not.toHaveBeenCalled();
257
+ disconnect();
258
+ });
259
+
260
+ test("does not push history when actor route is unchanged", async () => {
261
+ const actor = {
262
+ send: vi.fn(),
263
+ currentRoute: new Signal.State("/home"),
264
+ } as any;
265
+
266
+ const history = {
267
+ location: { pathname: "/home", search: "", hash: "", state: null },
268
+ subscribe() {
269
+ return () => {};
270
+ },
271
+ push: vi.fn(),
272
+ replace: vi.fn(),
273
+ } as any;
274
+
275
+ const disconnect = connectRouter({
276
+ actor,
277
+ router: { history } as any,
278
+ routeMap: {
279
+ resolve() {
280
+ return { to: null, params: {} };
281
+ },
282
+ },
283
+ });
284
+
285
+ actor.currentRoute.set("/home");
286
+ await new Promise<void>((resolve) => queueMicrotask(resolve));
287
+
288
+ expect(history.push).not.toHaveBeenCalled();
289
+ disconnect();
290
+ });
291
+
292
+ test("skips initial route send when URL differs but route map has no match", () => {
293
+ const actor = {
294
+ send: vi.fn(),
295
+ currentRoute: new Signal.State("/home"),
296
+ } as any;
297
+
298
+ const history = {
299
+ location: { pathname: "/unknown", search: "", hash: "", state: null },
300
+ subscribe() {
301
+ return () => {};
302
+ },
303
+ push: vi.fn(),
304
+ replace: vi.fn(),
305
+ } as any;
306
+
307
+ const disconnect = connectRouter({
308
+ actor,
309
+ router: { history } as any,
310
+ routeMap: {
311
+ resolve() {
312
+ return { to: null, params: {} };
313
+ },
314
+ },
315
+ });
316
+
317
+ expect(actor.send).not.toHaveBeenCalled();
318
+ disconnect();
319
+ });
320
+ });