@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.
- package/CHANGELOG.md +112 -0
- package/README.md +625 -0
- package/examples/basic/aeon.config.ts +39 -0
- package/examples/basic/components/Cursor.tsx +86 -0
- package/examples/basic/components/OfflineIndicator.tsx +103 -0
- package/examples/basic/components/PresenceBar.tsx +77 -0
- package/examples/basic/package.json +20 -0
- package/examples/basic/pages/index.tsx +80 -0
- package/package.json +101 -0
- package/packages/analytics/README.md +309 -0
- package/packages/analytics/build.ts +35 -0
- package/packages/analytics/package.json +50 -0
- package/packages/analytics/src/click-tracker.ts +368 -0
- package/packages/analytics/src/context-bridge.ts +319 -0
- package/packages/analytics/src/data-layer.ts +302 -0
- package/packages/analytics/src/gtm-loader.ts +239 -0
- package/packages/analytics/src/index.ts +230 -0
- package/packages/analytics/src/merkle-tree.ts +489 -0
- package/packages/analytics/src/provider.tsx +300 -0
- package/packages/analytics/src/types.ts +320 -0
- package/packages/analytics/src/use-analytics.ts +296 -0
- package/packages/analytics/tsconfig.json +19 -0
- package/packages/benchmarks/src/benchmark.test.ts +691 -0
- package/packages/cli/dist/index.js +61899 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/commands/build.test.ts +682 -0
- package/packages/cli/src/commands/build.ts +890 -0
- package/packages/cli/src/commands/dev.ts +473 -0
- package/packages/cli/src/commands/init.ts +409 -0
- package/packages/cli/src/commands/start.ts +297 -0
- package/packages/cli/src/index.ts +105 -0
- package/packages/directives/src/use-aeon.ts +272 -0
- package/packages/mcp-server/package.json +51 -0
- package/packages/mcp-server/src/index.ts +178 -0
- package/packages/mcp-server/src/resources.ts +346 -0
- package/packages/mcp-server/src/tools/index.ts +36 -0
- package/packages/mcp-server/src/tools/navigation.ts +545 -0
- package/packages/mcp-server/tsconfig.json +21 -0
- package/packages/react/package.json +40 -0
- package/packages/react/src/Link.tsx +388 -0
- package/packages/react/src/components/InstallPrompt.tsx +286 -0
- package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
- package/packages/react/src/components/PushNotifications.tsx +453 -0
- package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
- package/packages/react/src/hooks/useConflicts.ts +277 -0
- package/packages/react/src/hooks/useNetworkState.ts +209 -0
- package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
- package/packages/react/src/hooks/useServiceWorker.ts +278 -0
- package/packages/react/src/hooks.ts +195 -0
- package/packages/react/src/index.ts +151 -0
- package/packages/react/src/provider.tsx +467 -0
- package/packages/react/tsconfig.json +19 -0
- package/packages/runtime/README.md +399 -0
- package/packages/runtime/build.ts +48 -0
- package/packages/runtime/package.json +71 -0
- package/packages/runtime/schema.sql +40 -0
- package/packages/runtime/src/api-routes.ts +465 -0
- package/packages/runtime/src/benchmark.ts +171 -0
- package/packages/runtime/src/cache.ts +479 -0
- package/packages/runtime/src/durable-object.ts +1341 -0
- package/packages/runtime/src/index.ts +360 -0
- package/packages/runtime/src/navigation.test.ts +421 -0
- package/packages/runtime/src/navigation.ts +422 -0
- package/packages/runtime/src/nextjs-adapter.ts +272 -0
- package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
- package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
- package/packages/runtime/src/offline/encryption.test.ts +412 -0
- package/packages/runtime/src/offline/encryption.ts +397 -0
- package/packages/runtime/src/offline/types.ts +465 -0
- package/packages/runtime/src/predictor.ts +371 -0
- package/packages/runtime/src/registry.ts +351 -0
- package/packages/runtime/src/router/context-extractor.ts +661 -0
- package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
- package/packages/runtime/src/router/esi-control.ts +541 -0
- package/packages/runtime/src/router/esi-cyrano.ts +779 -0
- package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
- package/packages/runtime/src/router/esi-react.tsx +1065 -0
- package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
- package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
- package/packages/runtime/src/router/esi-translate.ts +503 -0
- package/packages/runtime/src/router/esi.ts +666 -0
- package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
- package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
- package/packages/runtime/src/router/index.ts +298 -0
- package/packages/runtime/src/router/merkle-capability.ts +473 -0
- package/packages/runtime/src/router/speculation.ts +451 -0
- package/packages/runtime/src/router/types.ts +630 -0
- package/packages/runtime/src/router.test.ts +470 -0
- package/packages/runtime/src/router.ts +302 -0
- package/packages/runtime/src/server.ts +481 -0
- package/packages/runtime/src/service-worker-push.ts +319 -0
- package/packages/runtime/src/service-worker.ts +553 -0
- package/packages/runtime/src/skeleton-hydrate.ts +237 -0
- package/packages/runtime/src/speculation.test.ts +389 -0
- package/packages/runtime/src/speculation.ts +486 -0
- package/packages/runtime/src/storage.test.ts +1297 -0
- package/packages/runtime/src/storage.ts +1048 -0
- package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
- package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
- package/packages/runtime/src/sync/coordinator.test.ts +608 -0
- package/packages/runtime/src/sync/coordinator.ts +596 -0
- package/packages/runtime/src/tree-compiler.ts +295 -0
- package/packages/runtime/src/types.ts +728 -0
- package/packages/runtime/src/worker.ts +327 -0
- package/packages/runtime/tsconfig.json +20 -0
- package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
- package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
- package/packages/runtime/wasm/package.json +21 -0
- package/packages/runtime/wrangler.toml +41 -0
- package/packages/runtime-wasm/Cargo.lock +436 -0
- package/packages/runtime-wasm/Cargo.toml +29 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
- package/packages/runtime-wasm/pkg/package.json +21 -0
- package/packages/runtime-wasm/src/hydrate.rs +352 -0
- package/packages/runtime-wasm/src/lib.rs +191 -0
- package/packages/runtime-wasm/src/render.rs +629 -0
- package/packages/runtime-wasm/src/router.rs +298 -0
- package/packages/runtime-wasm/src/skeleton.rs +430 -0
- 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 };
|