@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,481 @@
1
+ /**
2
+ * Aeon Pages Bun Server
3
+ *
4
+ * Lightweight server for serving Aeon pages with:
5
+ * - Hot reload in development
6
+ * - Collaborative route mutations via Aeon sync
7
+ * - File system persistence
8
+ * - Personalized routing with speculation
9
+ */
10
+
11
+ import { AeonRouter } from './router';
12
+ import { AeonRouteRegistry } from './registry';
13
+ import {
14
+ HeuristicAdapter,
15
+ extractUserContext,
16
+ setContextCookies,
17
+ addSpeculationHeaders,
18
+ } from './router/index';
19
+ import type { AeonConfig } from './types';
20
+ import type {
21
+ RouterAdapter,
22
+ RouterConfig,
23
+ RouteDecision,
24
+ ComponentTree,
25
+ ComponentNode,
26
+ UserContext,
27
+ } from './router/types';
28
+
29
+ export interface ServerOptions {
30
+ config: AeonConfig;
31
+ /** Personalized router configuration */
32
+ router?: RouterConfig;
33
+ onRouteChange?: (route: string, type: 'add' | 'update' | 'remove') => void;
34
+ onRouteDecision?: (decision: RouteDecision, context: UserContext) => void;
35
+ }
36
+
37
+ /**
38
+ * Create a minimal component tree for routing decisions
39
+ */
40
+ function createMinimalTree(
41
+ match: ReturnType<AeonRouter['match']>,
42
+ ): ComponentTree {
43
+ const nodes = new Map<string, ComponentNode>();
44
+ const rootId = match?.componentId || 'root';
45
+
46
+ nodes.set(rootId, {
47
+ id: rootId,
48
+ type: 'page',
49
+ props: {},
50
+ children: [],
51
+ });
52
+
53
+ return {
54
+ rootId,
55
+ nodes,
56
+ getNode: (id) => nodes.get(id),
57
+ getChildren: () => [],
58
+ getSchema: () => ({
59
+ rootId,
60
+ nodeCount: nodes.size,
61
+ nodeTypes: ['page'],
62
+ depth: 1,
63
+ }),
64
+ clone: () => createMinimalTree(match),
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Create the personalized router adapter
70
+ */
71
+ function createRouterAdapter(routerConfig?: RouterConfig): RouterAdapter {
72
+ if (!routerConfig) {
73
+ return new HeuristicAdapter();
74
+ }
75
+
76
+ if (typeof routerConfig.adapter === 'object') {
77
+ return routerConfig.adapter;
78
+ }
79
+
80
+ switch (routerConfig.adapter) {
81
+ case 'heuristic':
82
+ default:
83
+ return new HeuristicAdapter();
84
+ // AI and hybrid adapters can be added here in the future
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Create an Aeon Pages server using Bun's native server
90
+ */
91
+ export async function createAeonServer(options: ServerOptions) {
92
+ const {
93
+ config,
94
+ router: routerConfig,
95
+ onRouteChange,
96
+ onRouteDecision,
97
+ } = options;
98
+
99
+ const router = new AeonRouter({
100
+ routesDir: config.pagesDir,
101
+ componentsDir: config.componentsDir,
102
+ });
103
+
104
+ const registry = new AeonRouteRegistry({
105
+ syncMode: config.aeon?.sync?.mode ?? 'distributed',
106
+ versioningEnabled: config.aeon?.versioning?.enabled ?? true,
107
+ });
108
+
109
+ // Create personalized router adapter
110
+ const personalizedRouter = createRouterAdapter(routerConfig);
111
+
112
+ // Watch for file changes in development
113
+ if (config.runtime === 'bun' && process.env.NODE_ENV !== 'production') {
114
+ await watchFiles(config.pagesDir, async (path, type) => {
115
+ console.log(`[aeon] File ${type}: ${path}`);
116
+ await router.reload();
117
+ onRouteChange?.(
118
+ path,
119
+ type === 'create' ? 'add' : type === 'delete' ? 'remove' : 'update',
120
+ );
121
+ });
122
+ }
123
+
124
+ // Subscribe to collaborative route mutations
125
+ registry.subscribeToMutations((operation) => {
126
+ console.log(`[aeon] Collaborative route mutation:`, operation);
127
+ router.reload();
128
+ onRouteChange?.(
129
+ operation.path,
130
+ operation.type as 'add' | 'update' | 'remove',
131
+ );
132
+ });
133
+
134
+ // Initialize routes from file system
135
+ await router.scan();
136
+
137
+ return Bun.serve({
138
+ port: config.port ?? 3000,
139
+
140
+ async fetch(req: Request): Promise<Response> {
141
+ const url = new URL(req.url);
142
+ const path = url.pathname;
143
+
144
+ // Static assets
145
+ if (path.startsWith('/_aeon/')) {
146
+ return handleStaticAsset(path, config);
147
+ }
148
+
149
+ // WebSocket upgrade for Aeon sync
150
+ if (path === '/_aeon/ws' && req.headers.get('upgrade') === 'websocket') {
151
+ return handleWebSocketUpgrade(req, registry);
152
+ }
153
+
154
+ // Try to match a route
155
+ const match = router.match(path);
156
+
157
+ if (!match) {
158
+ // Dynamic route creation for unclaimed paths
159
+ if (config.aeon?.dynamicRoutes !== false) {
160
+ return handleDynamicCreation(path, req, registry);
161
+ }
162
+ return new Response('Not Found', { status: 404 });
163
+ }
164
+
165
+ // Extract user context for personalized routing
166
+ const userContext = await extractUserContext(req);
167
+
168
+ // Create component tree for routing decision
169
+ const tree = createMinimalTree(match);
170
+
171
+ // Get personalized route decision
172
+ const decision = await personalizedRouter.route(path, userContext, tree);
173
+
174
+ // Notify callback if provided
175
+ onRouteDecision?.(decision, userContext);
176
+
177
+ // Render the matched route with personalization
178
+ let response = await renderRoute(match, req, config, decision);
179
+
180
+ // Add context tracking cookies
181
+ response = setContextCookies(response, userContext, path);
182
+
183
+ // Add speculation headers for prefetching
184
+ response = addSpeculationHeaders(
185
+ response,
186
+ decision.prefetch || [],
187
+ decision.prerender || [],
188
+ );
189
+
190
+ return response;
191
+ },
192
+
193
+ // WebSocket handling for Aeon sync
194
+ websocket: {
195
+ message(ws, message) {
196
+ registry.handleSyncMessage(ws, message);
197
+ },
198
+ open(ws) {
199
+ registry.handleConnect(ws);
200
+ },
201
+ close(ws) {
202
+ registry.handleDisconnect(ws);
203
+ },
204
+ },
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Watch files for changes (hot reload)
210
+ */
211
+ async function watchFiles(
212
+ dir: string,
213
+ callback: (path: string, type: 'create' | 'update' | 'delete') => void,
214
+ ) {
215
+ const { watch } = await import('fs');
216
+ const { join } = await import('path');
217
+
218
+ watch(dir, { recursive: true }, (eventType, filename) => {
219
+ if (!filename) return;
220
+ if (!filename.endsWith('.tsx') && !filename.endsWith('.ts')) return;
221
+
222
+ const fullPath = join(dir, filename);
223
+ const type = eventType === 'rename' ? 'create' : 'update';
224
+ callback(fullPath, type);
225
+ });
226
+ }
227
+
228
+ /**
229
+ * Handle static assets from .aeon build directory
230
+ */
231
+ function handleStaticAsset(path: string, config: AeonConfig): Response {
232
+ const assetPath = path.replace('/_aeon/', '');
233
+ const fullPath = `${config.output?.dir ?? '.aeon'}/${assetPath}`;
234
+
235
+ try {
236
+ const file = Bun.file(fullPath);
237
+ return new Response(file);
238
+ } catch {
239
+ return new Response('Not Found', { status: 404 });
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Handle WebSocket upgrade for Aeon sync
245
+ */
246
+ function handleWebSocketUpgrade(
247
+ req: Request,
248
+ _registry: AeonRouteRegistry,
249
+ ): Response {
250
+ const server = Bun.serve.prototype; // This is a placeholder - actual upgrade happens in Bun
251
+ if ('upgrade' in server) {
252
+ const success = (server as { upgrade: (req: Request) => boolean }).upgrade(
253
+ req,
254
+ );
255
+ if (success) {
256
+ return new Response(null, { status: 101 });
257
+ }
258
+ }
259
+ return new Response('WebSocket upgrade failed', { status: 500 });
260
+ }
261
+
262
+ /**
263
+ * Handle dynamic route creation for unclaimed paths
264
+ */
265
+ async function handleDynamicCreation(
266
+ path: string,
267
+ req: Request,
268
+ registry: AeonRouteRegistry,
269
+ ): Promise<Response> {
270
+ // Check if user has permission to create routes
271
+ const authHeader = req.headers.get('Authorization');
272
+ if (!authHeader) {
273
+ return new Response('Not Found', { status: 404 });
274
+ }
275
+
276
+ // Create a new route via the registry
277
+ await registry.addRoute(path, 'DynamicPage', {
278
+ createdAt: new Date().toISOString(),
279
+ createdBy: 'dynamic',
280
+ });
281
+
282
+ // Return a placeholder response
283
+ return new Response(
284
+ JSON.stringify({
285
+ message: 'Route created',
286
+ path,
287
+ session: registry.getSessionId(path),
288
+ }),
289
+ {
290
+ status: 201,
291
+ headers: { 'Content-Type': 'application/json' },
292
+ },
293
+ );
294
+ }
295
+
296
+ /**
297
+ * Render a matched route with personalization
298
+ */
299
+ async function renderRoute(
300
+ match: ReturnType<AeonRouter['match']>,
301
+ _req: Request,
302
+ config: AeonConfig,
303
+ decision?: RouteDecision,
304
+ ): Promise<Response> {
305
+ if (!match) {
306
+ return new Response('Not Found', { status: 404 });
307
+ }
308
+
309
+ // For Aeon pages, we return the session data + hydration script
310
+ if (match.isAeon) {
311
+ const html = generateAeonPageHtml(match, config, decision);
312
+ return new Response(html, {
313
+ headers: { 'Content-Type': 'text/html' },
314
+ });
315
+ }
316
+
317
+ // For non-Aeon pages, do standard SSR
318
+ const html = generateStaticPageHtml(match, config, decision);
319
+ return new Response(html, {
320
+ headers: { 'Content-Type': 'text/html' },
321
+ });
322
+ }
323
+
324
+ /**
325
+ * Generate speculation rules script for prefetching
326
+ */
327
+ function generateSpeculationScript(decision?: RouteDecision): string {
328
+ if (!decision?.prefetch?.length && !decision?.prerender?.length) {
329
+ return '';
330
+ }
331
+
332
+ const rules: {
333
+ prerender?: Array<{ urls: string[] }>;
334
+ prefetch?: Array<{ urls: string[] }>;
335
+ } = {};
336
+
337
+ if (decision.prerender?.length) {
338
+ rules.prerender = [{ urls: decision.prerender }];
339
+ }
340
+
341
+ if (decision.prefetch?.length) {
342
+ rules.prefetch = [{ urls: decision.prefetch }];
343
+ }
344
+
345
+ return `<script type="speculationrules">${JSON.stringify(rules)}</script>`;
346
+ }
347
+
348
+ /**
349
+ * Generate personalization CSS variables
350
+ */
351
+ function generatePersonalizationStyles(decision?: RouteDecision): string {
352
+ if (!decision) return '';
353
+
354
+ const vars: string[] = [];
355
+
356
+ if (decision.accent) {
357
+ vars.push(`--aeon-accent: ${decision.accent}`);
358
+ }
359
+
360
+ if (decision.theme) {
361
+ vars.push(`--aeon-theme: ${decision.theme}`);
362
+ }
363
+
364
+ if (decision.density) {
365
+ const spacingMap = {
366
+ compact: '0.5rem',
367
+ normal: '1rem',
368
+ comfortable: '1.5rem',
369
+ };
370
+ vars.push(`--aeon-spacing: ${spacingMap[decision.density]}`);
371
+ }
372
+
373
+ if (vars.length === 0) return '';
374
+
375
+ return `<style>:root { ${vars.join('; ')} }</style>`;
376
+ }
377
+
378
+ /**
379
+ * Generate HTML for an Aeon-enabled page
380
+ */
381
+ function generateAeonPageHtml(
382
+ match: NonNullable<ReturnType<AeonRouter['match']>>,
383
+ config: AeonConfig,
384
+ decision?: RouteDecision,
385
+ ): string {
386
+ const { sessionId, params, componentId } = match;
387
+
388
+ // Determine color scheme from decision
389
+ const colorScheme =
390
+ decision?.theme === 'dark'
391
+ ? 'dark'
392
+ : decision?.theme === 'light'
393
+ ? 'light'
394
+ : '';
395
+ const colorSchemeAttr = colorScheme ? ` data-theme="${colorScheme}"` : '';
396
+
397
+ return `<!DOCTYPE html>
398
+ <html lang="en"${colorSchemeAttr}>
399
+ <head>
400
+ <meta charset="UTF-8">
401
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
402
+ <title>Aeon Page</title>
403
+ ${generatePersonalizationStyles(decision)}
404
+ ${generateSpeculationScript(decision)}
405
+ <script type="module">
406
+ // Aeon hydration script
407
+ import { hydrate, initAeonSync } from '/_aeon/runtime.js';
408
+
409
+ const sessionId = '${sessionId}';
410
+ const params = ${JSON.stringify(params)};
411
+ const componentId = '${componentId}';
412
+ const routeDecision = ${JSON.stringify(decision || {})};
413
+
414
+ // Initialize Aeon sync
415
+ const sync = await initAeonSync({
416
+ sessionId,
417
+ wsUrl: 'ws://' + window.location.host + '/_aeon/ws',
418
+ presence: ${config.aeon?.presence?.enabled ?? true},
419
+ });
420
+
421
+ // Hydrate the page from session state
422
+ const session = await sync.getSession(sessionId);
423
+ hydrate(session.tree, document.getElementById('root'), {
424
+ componentOrder: routeDecision.componentOrder,
425
+ hiddenComponents: routeDecision.hiddenComponents,
426
+ featureFlags: routeDecision.featureFlags,
427
+ });
428
+
429
+ // Subscribe to real-time updates
430
+ sync.subscribe((update) => {
431
+ hydrate(update.tree, document.getElementById('root'), {
432
+ componentOrder: routeDecision.componentOrder,
433
+ hiddenComponents: routeDecision.hiddenComponents,
434
+ featureFlags: routeDecision.featureFlags,
435
+ });
436
+ });
437
+ </script>
438
+ </head>
439
+ <body>
440
+ <div id="root">
441
+ <!-- Server-rendered content would go here -->
442
+ <noscript>This page requires JavaScript for collaborative features.</noscript>
443
+ </div>
444
+ </body>
445
+ </html>`;
446
+ }
447
+
448
+ /**
449
+ * Generate HTML for a static (non-Aeon) page
450
+ */
451
+ function generateStaticPageHtml(
452
+ match: NonNullable<ReturnType<AeonRouter['match']>>,
453
+ _config: AeonConfig,
454
+ decision?: RouteDecision,
455
+ ): string {
456
+ const colorScheme =
457
+ decision?.theme === 'dark'
458
+ ? 'dark'
459
+ : decision?.theme === 'light'
460
+ ? 'light'
461
+ : '';
462
+ const colorSchemeAttr = colorScheme ? ` data-theme="${colorScheme}"` : '';
463
+
464
+ return `<!DOCTYPE html>
465
+ <html lang="en"${colorSchemeAttr}>
466
+ <head>
467
+ <meta charset="UTF-8">
468
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
469
+ <title>Static Page</title>
470
+ ${generatePersonalizationStyles(decision)}
471
+ ${generateSpeculationScript(decision)}
472
+ </head>
473
+ <body>
474
+ <div id="root">
475
+ <!-- Render ${match.componentId} here -->
476
+ </div>
477
+ </body>
478
+ </html>`;
479
+ }
480
+
481
+ export { AeonRouter, AeonRouteRegistry };