@affectively/aeon-pages 1.3.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 (124) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +625 -0
  3. package/examples/basic/aeon.config.ts +39 -0
  4. package/examples/basic/components/Cursor.tsx +86 -0
  5. package/examples/basic/components/OfflineIndicator.tsx +103 -0
  6. package/examples/basic/components/PresenceBar.tsx +77 -0
  7. package/examples/basic/package.json +20 -0
  8. package/examples/basic/pages/index.tsx +80 -0
  9. package/package.json +101 -0
  10. package/packages/analytics/README.md +309 -0
  11. package/packages/analytics/build.ts +35 -0
  12. package/packages/analytics/package.json +50 -0
  13. package/packages/analytics/src/click-tracker.ts +368 -0
  14. package/packages/analytics/src/context-bridge.ts +319 -0
  15. package/packages/analytics/src/data-layer.ts +302 -0
  16. package/packages/analytics/src/gtm-loader.ts +239 -0
  17. package/packages/analytics/src/index.ts +230 -0
  18. package/packages/analytics/src/merkle-tree.ts +489 -0
  19. package/packages/analytics/src/provider.tsx +300 -0
  20. package/packages/analytics/src/types.ts +320 -0
  21. package/packages/analytics/src/use-analytics.ts +296 -0
  22. package/packages/analytics/tsconfig.json +19 -0
  23. package/packages/benchmarks/src/benchmark.test.ts +691 -0
  24. package/packages/cli/dist/index.js +61899 -0
  25. package/packages/cli/package.json +43 -0
  26. package/packages/cli/src/commands/build.test.ts +682 -0
  27. package/packages/cli/src/commands/build.ts +890 -0
  28. package/packages/cli/src/commands/dev.ts +473 -0
  29. package/packages/cli/src/commands/init.ts +409 -0
  30. package/packages/cli/src/commands/start.ts +297 -0
  31. package/packages/cli/src/index.ts +105 -0
  32. package/packages/directives/src/use-aeon.ts +272 -0
  33. package/packages/mcp-server/package.json +51 -0
  34. package/packages/mcp-server/src/index.ts +178 -0
  35. package/packages/mcp-server/src/resources.ts +346 -0
  36. package/packages/mcp-server/src/tools/index.ts +36 -0
  37. package/packages/mcp-server/src/tools/navigation.ts +545 -0
  38. package/packages/mcp-server/tsconfig.json +21 -0
  39. package/packages/react/package.json +40 -0
  40. package/packages/react/src/Link.tsx +388 -0
  41. package/packages/react/src/components/InstallPrompt.tsx +286 -0
  42. package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
  43. package/packages/react/src/components/PushNotifications.tsx +453 -0
  44. package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
  45. package/packages/react/src/hooks/useConflicts.ts +277 -0
  46. package/packages/react/src/hooks/useNetworkState.ts +209 -0
  47. package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
  48. package/packages/react/src/hooks/useServiceWorker.ts +278 -0
  49. package/packages/react/src/hooks.ts +195 -0
  50. package/packages/react/src/index.ts +151 -0
  51. package/packages/react/src/provider.tsx +467 -0
  52. package/packages/react/tsconfig.json +19 -0
  53. package/packages/runtime/README.md +399 -0
  54. package/packages/runtime/build.ts +48 -0
  55. package/packages/runtime/package.json +71 -0
  56. package/packages/runtime/schema.sql +40 -0
  57. package/packages/runtime/src/api-routes.ts +465 -0
  58. package/packages/runtime/src/benchmark.ts +171 -0
  59. package/packages/runtime/src/cache.ts +479 -0
  60. package/packages/runtime/src/durable-object.ts +1341 -0
  61. package/packages/runtime/src/index.ts +360 -0
  62. package/packages/runtime/src/navigation.test.ts +421 -0
  63. package/packages/runtime/src/navigation.ts +422 -0
  64. package/packages/runtime/src/nextjs-adapter.ts +272 -0
  65. package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
  66. package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
  67. package/packages/runtime/src/offline/encryption.test.ts +412 -0
  68. package/packages/runtime/src/offline/encryption.ts +397 -0
  69. package/packages/runtime/src/offline/types.ts +465 -0
  70. package/packages/runtime/src/predictor.ts +371 -0
  71. package/packages/runtime/src/registry.ts +351 -0
  72. package/packages/runtime/src/router/context-extractor.ts +661 -0
  73. package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
  74. package/packages/runtime/src/router/esi-control.ts +541 -0
  75. package/packages/runtime/src/router/esi-cyrano.ts +779 -0
  76. package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
  77. package/packages/runtime/src/router/esi-react.tsx +1065 -0
  78. package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
  79. package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
  80. package/packages/runtime/src/router/esi-translate.ts +503 -0
  81. package/packages/runtime/src/router/esi.ts +666 -0
  82. package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
  83. package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
  84. package/packages/runtime/src/router/index.ts +298 -0
  85. package/packages/runtime/src/router/merkle-capability.ts +473 -0
  86. package/packages/runtime/src/router/speculation.ts +451 -0
  87. package/packages/runtime/src/router/types.ts +630 -0
  88. package/packages/runtime/src/router.test.ts +470 -0
  89. package/packages/runtime/src/router.ts +302 -0
  90. package/packages/runtime/src/server.ts +481 -0
  91. package/packages/runtime/src/service-worker-push.ts +319 -0
  92. package/packages/runtime/src/service-worker.ts +553 -0
  93. package/packages/runtime/src/skeleton-hydrate.ts +237 -0
  94. package/packages/runtime/src/speculation.test.ts +389 -0
  95. package/packages/runtime/src/speculation.ts +486 -0
  96. package/packages/runtime/src/storage.test.ts +1297 -0
  97. package/packages/runtime/src/storage.ts +1048 -0
  98. package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
  99. package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
  100. package/packages/runtime/src/sync/coordinator.test.ts +608 -0
  101. package/packages/runtime/src/sync/coordinator.ts +596 -0
  102. package/packages/runtime/src/tree-compiler.ts +295 -0
  103. package/packages/runtime/src/types.ts +728 -0
  104. package/packages/runtime/src/worker.ts +327 -0
  105. package/packages/runtime/tsconfig.json +20 -0
  106. package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
  107. package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
  108. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
  109. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
  110. package/packages/runtime/wasm/package.json +21 -0
  111. package/packages/runtime/wrangler.toml +41 -0
  112. package/packages/runtime-wasm/Cargo.lock +436 -0
  113. package/packages/runtime-wasm/Cargo.toml +29 -0
  114. package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
  115. package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
  116. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
  117. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
  118. package/packages/runtime-wasm/pkg/package.json +21 -0
  119. package/packages/runtime-wasm/src/hydrate.rs +352 -0
  120. package/packages/runtime-wasm/src/lib.rs +191 -0
  121. package/packages/runtime-wasm/src/render.rs +629 -0
  122. package/packages/runtime-wasm/src/router.rs +298 -0
  123. package/packages/runtime-wasm/src/skeleton.rs +430 -0
  124. package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
@@ -0,0 +1,470 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
2
+ import { AeonRouter } from './router';
3
+ import type { RouteDefinition } from './types';
4
+ import { mkdir, writeFile, rm } from 'fs/promises';
5
+ import { join } from 'path';
6
+
7
+ describe('AeonRouter', () => {
8
+ let router: AeonRouter;
9
+
10
+ beforeEach(() => {
11
+ router = new AeonRouter({ routesDir: './pages' });
12
+ });
13
+
14
+ describe('addRoute and match', () => {
15
+ test('matches static route', () => {
16
+ router.addRoute({
17
+ pattern: '/about',
18
+ sessionId: 'about',
19
+ componentId: 'about',
20
+ isAeon: true,
21
+ });
22
+
23
+ const match = router.match('/about');
24
+
25
+ expect(match).not.toBeNull();
26
+ expect(match!.route.pattern).toBe('/about');
27
+ expect(match!.params).toEqual({});
28
+ expect(match!.sessionId).toBe('about');
29
+ });
30
+
31
+ test('matches root route', () => {
32
+ router.addRoute({
33
+ pattern: '/',
34
+ sessionId: 'index',
35
+ componentId: 'index',
36
+ isAeon: true,
37
+ });
38
+
39
+ const match = router.match('/');
40
+
41
+ expect(match).not.toBeNull();
42
+ expect(match!.route.pattern).toBe('/');
43
+ });
44
+
45
+ test('matches dynamic route [slug]', () => {
46
+ router.addRoute({
47
+ pattern: '/blog/[slug]',
48
+ sessionId: 'blog-$slug',
49
+ componentId: 'blog-slug',
50
+ isAeon: true,
51
+ });
52
+
53
+ const match = router.match('/blog/my-first-post');
54
+
55
+ expect(match).not.toBeNull();
56
+ expect(match!.params).toEqual({ slug: 'my-first-post' });
57
+ expect(match!.sessionId).toBe('blog-my-first-post');
58
+ });
59
+
60
+ test('matches multiple dynamic segments', () => {
61
+ router.addRoute({
62
+ pattern: '/users/[userId]/posts/[postId]',
63
+ sessionId: 'users-$userId-posts-$postId',
64
+ componentId: 'user-post',
65
+ isAeon: true,
66
+ });
67
+
68
+ const match = router.match('/users/123/posts/456');
69
+
70
+ expect(match).not.toBeNull();
71
+ expect(match!.params).toEqual({ userId: '123', postId: '456' });
72
+ expect(match!.sessionId).toBe('users-123-posts-456');
73
+ });
74
+
75
+ test('matches catch-all route [...path]', () => {
76
+ router.addRoute({
77
+ pattern: '/api/[...path]',
78
+ sessionId: 'api-$path',
79
+ componentId: 'api-catchall',
80
+ isAeon: false,
81
+ });
82
+
83
+ const match = router.match('/api/users/123/posts');
84
+
85
+ expect(match).not.toBeNull();
86
+ expect(match!.params).toEqual({ path: 'users/123/posts' });
87
+ });
88
+
89
+ test('catch-all requires at least one segment', () => {
90
+ router.addRoute({
91
+ pattern: '/api/[...path]',
92
+ sessionId: 'api-$path',
93
+ componentId: 'api-catchall',
94
+ isAeon: false,
95
+ });
96
+
97
+ const match = router.match('/api');
98
+
99
+ expect(match).toBeNull();
100
+ });
101
+
102
+ test('matches optional catch-all [[...slug]]', () => {
103
+ router.addRoute({
104
+ pattern: '/docs/[[...slug]]',
105
+ sessionId: 'docs-$slug',
106
+ componentId: 'docs',
107
+ isAeon: true,
108
+ });
109
+
110
+ // With segments
111
+ const matchWithSlug = router.match('/docs/getting-started/installation');
112
+ expect(matchWithSlug).not.toBeNull();
113
+ expect(matchWithSlug!.params).toEqual({
114
+ slug: 'getting-started/installation',
115
+ });
116
+
117
+ // Without segments (optional)
118
+ const matchWithoutSlug = router.match('/docs');
119
+ expect(matchWithoutSlug).not.toBeNull();
120
+ expect(matchWithoutSlug!.params).toEqual({});
121
+ });
122
+
123
+ test('returns null for non-matching route', () => {
124
+ router.addRoute({
125
+ pattern: '/about',
126
+ sessionId: 'about',
127
+ componentId: 'about',
128
+ isAeon: true,
129
+ });
130
+
131
+ const match = router.match('/contact');
132
+
133
+ expect(match).toBeNull();
134
+ });
135
+
136
+ test('static route takes precedence over dynamic', () => {
137
+ router.addRoute({
138
+ pattern: '/blog/[slug]',
139
+ sessionId: 'blog-$slug',
140
+ componentId: 'blog-slug',
141
+ isAeon: true,
142
+ });
143
+ router.addRoute({
144
+ pattern: '/blog/featured',
145
+ sessionId: 'blog-featured',
146
+ componentId: 'blog-featured',
147
+ isAeon: true,
148
+ });
149
+
150
+ const match = router.match('/blog/featured');
151
+
152
+ expect(match).not.toBeNull();
153
+ expect(match!.route.pattern).toBe('/blog/featured');
154
+ expect(match!.params).toEqual({});
155
+ });
156
+
157
+ test('dynamic route takes precedence over catch-all', () => {
158
+ router.addRoute({
159
+ pattern: '/api/[...path]',
160
+ sessionId: 'api-$path',
161
+ componentId: 'api-catchall',
162
+ isAeon: false,
163
+ });
164
+ router.addRoute({
165
+ pattern: '/api/[version]',
166
+ sessionId: 'api-$version',
167
+ componentId: 'api-version',
168
+ isAeon: false,
169
+ });
170
+
171
+ const match = router.match('/api/v1');
172
+
173
+ expect(match).not.toBeNull();
174
+ expect(match!.route.pattern).toBe('/api/[version]');
175
+ expect(match!.params).toEqual({ version: 'v1' });
176
+ });
177
+ });
178
+
179
+ describe('hasRoute', () => {
180
+ test('returns true for existing route', () => {
181
+ router.addRoute({
182
+ pattern: '/about',
183
+ sessionId: 'about',
184
+ componentId: 'about',
185
+ isAeon: true,
186
+ });
187
+
188
+ expect(router.hasRoute('/about')).toBe(true);
189
+ });
190
+
191
+ test('returns false for non-existing route', () => {
192
+ expect(router.hasRoute('/nonexistent')).toBe(false);
193
+ });
194
+ });
195
+
196
+ describe('getRoutes', () => {
197
+ test('returns all registered routes', () => {
198
+ router.addRoute({
199
+ pattern: '/',
200
+ sessionId: 'index',
201
+ componentId: 'index',
202
+ isAeon: true,
203
+ });
204
+ router.addRoute({
205
+ pattern: '/about',
206
+ sessionId: 'about',
207
+ componentId: 'about',
208
+ isAeon: true,
209
+ });
210
+ router.addRoute({
211
+ pattern: '/blog/[slug]',
212
+ sessionId: 'blog-$slug',
213
+ componentId: 'blog-slug',
214
+ isAeon: true,
215
+ });
216
+
217
+ const routes = router.getRoutes();
218
+
219
+ expect(routes).toHaveLength(3);
220
+ expect(routes.map((r) => r.pattern)).toContain('/');
221
+ expect(routes.map((r) => r.pattern)).toContain('/about');
222
+ expect(routes.map((r) => r.pattern)).toContain('/blog/[slug]');
223
+ });
224
+ });
225
+
226
+ describe('route groups', () => {
227
+ test('skips route groups in pattern', () => {
228
+ // Route groups like (marketing) should not appear in URL
229
+ router.addRoute({
230
+ pattern: '/(marketing)/about',
231
+ sessionId: 'about',
232
+ componentId: 'about',
233
+ isAeon: true,
234
+ });
235
+
236
+ // Should match /about, not /(marketing)/about
237
+ const match = router.match('/about');
238
+ expect(match).not.toBeNull();
239
+ });
240
+ });
241
+
242
+ describe('edge cases', () => {
243
+ test('handles trailing slashes', () => {
244
+ router.addRoute({
245
+ pattern: '/about',
246
+ sessionId: 'about',
247
+ componentId: 'about',
248
+ isAeon: true,
249
+ });
250
+
251
+ expect(router.match('/about/')).not.toBeNull();
252
+ expect(router.match('/about')).not.toBeNull();
253
+ });
254
+
255
+ test('handles leading slashes', () => {
256
+ router.addRoute({
257
+ pattern: '/about',
258
+ sessionId: 'about',
259
+ componentId: 'about',
260
+ isAeon: true,
261
+ });
262
+
263
+ expect(router.match('about')).not.toBeNull();
264
+ expect(router.match('/about')).not.toBeNull();
265
+ });
266
+
267
+ test('handles empty path as root', () => {
268
+ router.addRoute({
269
+ pattern: '/',
270
+ sessionId: 'index',
271
+ componentId: 'index',
272
+ isAeon: true,
273
+ });
274
+
275
+ expect(router.match('')).not.toBeNull();
276
+ expect(router.match('/')).not.toBeNull();
277
+ });
278
+
279
+ test('does not match partial segments', () => {
280
+ router.addRoute({
281
+ pattern: '/blog',
282
+ sessionId: 'blog',
283
+ componentId: 'blog',
284
+ isAeon: true,
285
+ });
286
+
287
+ expect(router.match('/blogger')).toBeNull();
288
+ expect(router.match('/blog-posts')).toBeNull();
289
+ });
290
+
291
+ test('handles deeply nested routes', () => {
292
+ router.addRoute({
293
+ pattern: '/a/b/c/d/e/f',
294
+ sessionId: 'deep',
295
+ componentId: 'deep',
296
+ isAeon: true,
297
+ });
298
+
299
+ const match = router.match('/a/b/c/d/e/f');
300
+ expect(match).not.toBeNull();
301
+ expect(match!.route.pattern).toBe('/a/b/c/d/e/f');
302
+ });
303
+
304
+ test('handles special characters in dynamic segments', () => {
305
+ router.addRoute({
306
+ pattern: '/posts/[slug]',
307
+ sessionId: 'posts-$slug',
308
+ componentId: 'posts-slug',
309
+ isAeon: true,
310
+ });
311
+
312
+ const match = router.match('/posts/hello-world-2024');
313
+ expect(match).not.toBeNull();
314
+ expect(match!.params.slug).toBe('hello-world-2024');
315
+ });
316
+ });
317
+
318
+ describe('session ID resolution', () => {
319
+ test('resolves session ID with single param', () => {
320
+ router.addRoute({
321
+ pattern: '/blog/[slug]',
322
+ sessionId: 'blog-$slug',
323
+ componentId: 'blog-slug',
324
+ isAeon: true,
325
+ });
326
+
327
+ const match = router.match('/blog/my-post');
328
+ expect(match!.sessionId).toBe('blog-my-post');
329
+ });
330
+
331
+ test('resolves session ID with multiple params', () => {
332
+ router.addRoute({
333
+ pattern: '/[lang]/blog/[slug]',
334
+ sessionId: '$lang-blog-$slug',
335
+ componentId: 'lang-blog-slug',
336
+ isAeon: true,
337
+ });
338
+
339
+ const match = router.match('/en/blog/hello-world');
340
+ expect(match!.sessionId).toBe('en-blog-hello-world');
341
+ });
342
+
343
+ test('resolves session ID with catch-all', () => {
344
+ router.addRoute({
345
+ pattern: '/docs/[...path]',
346
+ sessionId: 'docs-$path',
347
+ componentId: 'docs-path',
348
+ isAeon: true,
349
+ });
350
+
351
+ const match = router.match('/docs/api/reference/auth');
352
+ expect(match!.sessionId).toBe('docs-api/reference/auth');
353
+ });
354
+ });
355
+ });
356
+
357
+ describe('AeonRouter file scanning', () => {
358
+ const testPagesDir = '.aeon/test-pages';
359
+ let router: AeonRouter;
360
+
361
+ beforeEach(async () => {
362
+ // Create test pages directory structure
363
+ await mkdir(join(testPagesDir, 'blog', '[slug]'), { recursive: true });
364
+ await mkdir(join(testPagesDir, 'about'), { recursive: true });
365
+ await mkdir(join(testPagesDir, '(marketing)', 'landing'), {
366
+ recursive: true,
367
+ });
368
+
369
+ // Create page files
370
+ await writeFile(
371
+ join(testPagesDir, 'page.tsx'),
372
+ `'use aeon';\nexport default function Home() { return <div>Home</div>; }`,
373
+ );
374
+ await writeFile(
375
+ join(testPagesDir, 'about', 'page.tsx'),
376
+ `export default function About() { return <div>About</div>; }`,
377
+ );
378
+ await writeFile(
379
+ join(testPagesDir, 'blog', '[slug]', 'page.tsx'),
380
+ `'use aeon';\nexport default function BlogPost() { return <div>Blog</div>; }`,
381
+ );
382
+ await writeFile(
383
+ join(testPagesDir, '(marketing)', 'landing', 'page.tsx'),
384
+ `"use aeon";\nexport default function Landing() { return <div>Landing</div>; }`,
385
+ );
386
+
387
+ router = new AeonRouter({ routesDir: testPagesDir });
388
+ });
389
+
390
+ afterEach(async () => {
391
+ await rm(testPagesDir, { recursive: true, force: true });
392
+ });
393
+
394
+ test('scans directory and finds pages', async () => {
395
+ await router.scan();
396
+ const routes = router.getRoutes();
397
+
398
+ expect(routes.length).toBeGreaterThanOrEqual(3);
399
+ });
400
+
401
+ test('detects use aeon directive with single quotes', async () => {
402
+ await router.scan();
403
+
404
+ const homeMatch = router.match('/');
405
+ expect(homeMatch).not.toBeNull();
406
+ expect(homeMatch!.isAeon).toBe(true);
407
+ });
408
+
409
+ test('detects use aeon directive with double quotes', async () => {
410
+ await router.scan();
411
+
412
+ // Landing page uses double quotes for 'use aeon'
413
+ const landingMatch = router.match('/landing');
414
+ expect(landingMatch).not.toBeNull();
415
+ expect(landingMatch!.isAeon).toBe(true);
416
+ });
417
+
418
+ test('detects non-aeon pages', async () => {
419
+ await router.scan();
420
+
421
+ const aboutMatch = router.match('/about');
422
+ expect(aboutMatch).not.toBeNull();
423
+ expect(aboutMatch!.isAeon).toBe(false);
424
+ });
425
+
426
+ test('handles dynamic segments from directory names', async () => {
427
+ await router.scan();
428
+
429
+ const blogMatch = router.match('/blog/my-post');
430
+ expect(blogMatch).not.toBeNull();
431
+ expect(blogMatch!.params.slug).toBe('my-post');
432
+ });
433
+
434
+ test('skips route groups in URL pattern', async () => {
435
+ await router.scan();
436
+
437
+ // (marketing) should not appear in URL
438
+ const landingMatch = router.match('/landing');
439
+ expect(landingMatch).not.toBeNull();
440
+
441
+ // Should NOT match with route group in URL
442
+ const withGroup = router.match('/(marketing)/landing');
443
+ expect(withGroup).toBeNull();
444
+ });
445
+
446
+ test('reload rescans directory', async () => {
447
+ await router.scan();
448
+ const initialRoutes = router.getRoutes().length;
449
+
450
+ // Add a new page
451
+ await mkdir(join(testPagesDir, 'contact'), { recursive: true });
452
+ await writeFile(
453
+ join(testPagesDir, 'contact', 'page.tsx'),
454
+ `export default function Contact() { return <div>Contact</div>; }`,
455
+ );
456
+
457
+ await router.reload();
458
+ const newRoutes = router.getRoutes().length;
459
+
460
+ expect(newRoutes).toBeGreaterThan(initialRoutes);
461
+ });
462
+
463
+ test('handles non-existent directory gracefully', async () => {
464
+ const badRouter = new AeonRouter({ routesDir: './nonexistent-dir' });
465
+ await badRouter.scan();
466
+
467
+ const routes = badRouter.getRoutes();
468
+ expect(routes).toEqual([]);
469
+ });
470
+ });