@affectively/aeon-pages-runtime 0.5.1 → 0.5.2
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/dist/chunk-ean7k8vr.js +667 -0
- package/dist/chunk-ef6e0ra8.js +4322 -0
- package/dist/index.js +2 -2
- package/dist/router/context-extractor.d.ts +33 -0
- package/dist/router/esi-translate.d.ts +100 -0
- package/dist/router/index.js +1 -1
- package/dist/router/types.d.ts +41 -1
- package/dist/server.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HeuristicAdapter,
|
|
3
|
+
addSpeculationHeaders,
|
|
4
|
+
extractUserContext,
|
|
5
|
+
setContextCookies
|
|
6
|
+
} from "./chunk-ef6e0ra8.js";
|
|
7
|
+
import {
|
|
8
|
+
join,
|
|
9
|
+
relative
|
|
10
|
+
} from "./chunk-e71hvfe9.js";
|
|
11
|
+
import {
|
|
12
|
+
__require
|
|
13
|
+
} from "./chunk-tgx0r0vn.js";
|
|
14
|
+
|
|
15
|
+
// src/router.ts
|
|
16
|
+
var {readdir} = (() => ({}));
|
|
17
|
+
class AeonRouter {
|
|
18
|
+
routes = [];
|
|
19
|
+
routesDir;
|
|
20
|
+
componentsDir;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.routesDir = options.routesDir;
|
|
23
|
+
this.componentsDir = options.componentsDir;
|
|
24
|
+
}
|
|
25
|
+
async scan() {
|
|
26
|
+
this.routes = [];
|
|
27
|
+
await this.scanDirectory(this.routesDir, "");
|
|
28
|
+
this.sortRoutes();
|
|
29
|
+
}
|
|
30
|
+
async reload() {
|
|
31
|
+
await this.scan();
|
|
32
|
+
}
|
|
33
|
+
match(path) {
|
|
34
|
+
const pathSegments = path.replace(/^\/|\/$/g, "").split("/").filter(Boolean);
|
|
35
|
+
for (const parsed of this.routes) {
|
|
36
|
+
const params = this.matchSegments(parsed.segments, pathSegments);
|
|
37
|
+
if (params !== null) {
|
|
38
|
+
const sessionId = this.resolveSessionId(parsed.definition.sessionId, params);
|
|
39
|
+
return {
|
|
40
|
+
route: parsed.definition,
|
|
41
|
+
params,
|
|
42
|
+
sessionId,
|
|
43
|
+
componentId: parsed.definition.componentId,
|
|
44
|
+
isAeon: parsed.definition.isAeon
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
hasRoute(path) {
|
|
51
|
+
return this.match(path) !== null;
|
|
52
|
+
}
|
|
53
|
+
getRoutes() {
|
|
54
|
+
return this.routes.map((r) => r.definition);
|
|
55
|
+
}
|
|
56
|
+
addRoute(definition) {
|
|
57
|
+
const segments = this.parsePattern(definition.pattern);
|
|
58
|
+
this.routes.push({ pattern: definition.pattern, segments, definition });
|
|
59
|
+
this.sortRoutes();
|
|
60
|
+
}
|
|
61
|
+
async scanDirectory(dir, prefix) {
|
|
62
|
+
try {
|
|
63
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
const fullPath = join(dir, entry.name);
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
const isRouteGroup = entry.name.startsWith("(") && entry.name.endsWith(")");
|
|
68
|
+
const newPrefix = isRouteGroup ? prefix : `${prefix}/${entry.name}`;
|
|
69
|
+
await this.scanDirectory(fullPath, newPrefix);
|
|
70
|
+
} else if (entry.name === "page.tsx" || entry.name === "page.ts") {
|
|
71
|
+
const route = await this.createRouteFromFile(fullPath, prefix);
|
|
72
|
+
if (route) {
|
|
73
|
+
this.routes.push(route);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(`[aeon] Error scanning directory ${dir}:`, error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async createRouteFromFile(filePath, prefix) {
|
|
82
|
+
try {
|
|
83
|
+
const file = Bun.file(filePath);
|
|
84
|
+
const content = await file.text();
|
|
85
|
+
const isAeon = content.includes("'use aeon'") || content.includes('"use aeon"');
|
|
86
|
+
const pattern = prefix || "/";
|
|
87
|
+
const segments = this.parsePattern(pattern);
|
|
88
|
+
const sessionId = this.generateSessionId(pattern);
|
|
89
|
+
const componentId = relative(this.routesDir, filePath).replace(/\.(tsx?|jsx?)$/, "").replace(/\//g, "-").replace(/page$/, "").replace(/-$/, "") || "index";
|
|
90
|
+
const definition = {
|
|
91
|
+
pattern,
|
|
92
|
+
sessionId,
|
|
93
|
+
componentId,
|
|
94
|
+
isAeon
|
|
95
|
+
};
|
|
96
|
+
return { pattern, segments, definition };
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(`[aeon] Error reading file ${filePath}:`, error);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
parsePattern(pattern) {
|
|
103
|
+
return pattern.replace(/^\/|\/$/g, "").split("/").filter(Boolean).filter((s) => !(s.startsWith("(") && s.endsWith(")"))).map((s) => {
|
|
104
|
+
if (s.startsWith("[[...") && s.endsWith("]]")) {
|
|
105
|
+
return { type: "optionalCatchAll", name: s.slice(5, -2) };
|
|
106
|
+
}
|
|
107
|
+
if (s.startsWith("[...") && s.endsWith("]")) {
|
|
108
|
+
return { type: "catchAll", name: s.slice(4, -1) };
|
|
109
|
+
}
|
|
110
|
+
if (s.startsWith("[") && s.endsWith("]")) {
|
|
111
|
+
return { type: "dynamic", name: s.slice(1, -1) };
|
|
112
|
+
}
|
|
113
|
+
return { type: "static", value: s };
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
matchSegments(routeSegments, pathSegments) {
|
|
117
|
+
const params = {};
|
|
118
|
+
let pathIdx = 0;
|
|
119
|
+
for (const segment of routeSegments) {
|
|
120
|
+
switch (segment.type) {
|
|
121
|
+
case "static":
|
|
122
|
+
if (pathIdx >= pathSegments.length || pathSegments[pathIdx] !== segment.value) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
pathIdx++;
|
|
126
|
+
break;
|
|
127
|
+
case "dynamic":
|
|
128
|
+
if (pathIdx >= pathSegments.length) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
params[segment.name] = pathSegments[pathIdx];
|
|
132
|
+
pathIdx++;
|
|
133
|
+
break;
|
|
134
|
+
case "catchAll":
|
|
135
|
+
if (pathIdx >= pathSegments.length) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
params[segment.name] = pathSegments.slice(pathIdx).join("/");
|
|
139
|
+
pathIdx = pathSegments.length;
|
|
140
|
+
break;
|
|
141
|
+
case "optionalCatchAll":
|
|
142
|
+
if (pathIdx < pathSegments.length) {
|
|
143
|
+
params[segment.name] = pathSegments.slice(pathIdx).join("/");
|
|
144
|
+
pathIdx = pathSegments.length;
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return pathIdx === pathSegments.length ? params : null;
|
|
150
|
+
}
|
|
151
|
+
generateSessionId(pattern) {
|
|
152
|
+
return pattern.replace(/^\/|\/$/g, "").replace(/\[\.\.\.(\w+)\]/g, "$$$1").replace(/\[\[\.\.\.(\w+)\]\]/g, "$$$1").replace(/\[(\w+)\]/g, "$$$1").replace(/\//g, "-") || "index";
|
|
153
|
+
}
|
|
154
|
+
resolveSessionId(template, params) {
|
|
155
|
+
let result = template;
|
|
156
|
+
for (const [key, value] of Object.entries(params)) {
|
|
157
|
+
result = result.replace(`$${key}`, value);
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
sortRoutes() {
|
|
162
|
+
this.routes.sort((a, b) => {
|
|
163
|
+
const scoreA = this.routeSpecificity(a.segments);
|
|
164
|
+
const scoreB = this.routeSpecificity(b.segments);
|
|
165
|
+
return scoreB - scoreA;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
routeSpecificity(segments) {
|
|
169
|
+
let score = 0;
|
|
170
|
+
for (let i = 0;i < segments.length; i++) {
|
|
171
|
+
const positionWeight = 1000 - i;
|
|
172
|
+
const segment = segments[i];
|
|
173
|
+
switch (segment.type) {
|
|
174
|
+
case "static":
|
|
175
|
+
score += positionWeight * 10;
|
|
176
|
+
break;
|
|
177
|
+
case "dynamic":
|
|
178
|
+
score += positionWeight * 5;
|
|
179
|
+
break;
|
|
180
|
+
case "catchAll":
|
|
181
|
+
score += 1;
|
|
182
|
+
break;
|
|
183
|
+
case "optionalCatchAll":
|
|
184
|
+
score += 0;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return score;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/registry.ts
|
|
193
|
+
class AeonRouteRegistry {
|
|
194
|
+
routes = new Map;
|
|
195
|
+
coordinator = null;
|
|
196
|
+
reconciler = null;
|
|
197
|
+
versions = null;
|
|
198
|
+
syncMode;
|
|
199
|
+
versioningEnabled;
|
|
200
|
+
mutationCallbacks = [];
|
|
201
|
+
connectedSockets = new Set;
|
|
202
|
+
constructor(options) {
|
|
203
|
+
this.syncMode = options.syncMode;
|
|
204
|
+
this.versioningEnabled = options.versioningEnabled;
|
|
205
|
+
this.initializeAeonModules();
|
|
206
|
+
}
|
|
207
|
+
async initializeAeonModules() {
|
|
208
|
+
try {
|
|
209
|
+
const aeon = await import("@affectively/aeon");
|
|
210
|
+
if (this.syncMode === "distributed") {
|
|
211
|
+
this.coordinator = new aeon.SyncCoordinator;
|
|
212
|
+
this.reconciler = new aeon.StateReconciler;
|
|
213
|
+
this.coordinator.on("sync-completed", (session) => {
|
|
214
|
+
this.handleSyncCompleted(session);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (this.versioningEnabled) {
|
|
218
|
+
this.versions = new aeon.SchemaVersionManager;
|
|
219
|
+
this.versions.registerVersion("1.0.0", {
|
|
220
|
+
description: "Initial route schema"
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.warn("[aeon-registry] Aeon modules not available, running in standalone mode");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async addRoute(path, component, metadata) {
|
|
228
|
+
const operation = {
|
|
229
|
+
type: "route-add",
|
|
230
|
+
path,
|
|
231
|
+
component,
|
|
232
|
+
metadata,
|
|
233
|
+
timestamp: new Date().toISOString(),
|
|
234
|
+
nodeId: this.coordinator?.getLocalNodeId() ?? "local"
|
|
235
|
+
};
|
|
236
|
+
if (this.syncMode === "distributed" && this.coordinator) {
|
|
237
|
+
const participants = this.coordinator.getOnlineNodes().map((n) => n.id);
|
|
238
|
+
if (participants.length > 0) {
|
|
239
|
+
await this.coordinator.createSyncSession(this.coordinator.getLocalNodeId(), participants);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const version = this.versioningEnabled && this.versions ? await this.versions.getCurrentVersion() : "1.0.0";
|
|
243
|
+
this.routes.set(path, {
|
|
244
|
+
path,
|
|
245
|
+
component,
|
|
246
|
+
metadata,
|
|
247
|
+
version
|
|
248
|
+
});
|
|
249
|
+
this.notifyMutation(operation);
|
|
250
|
+
await this.persistRoute(path, component);
|
|
251
|
+
}
|
|
252
|
+
async updateRoute(path, updates) {
|
|
253
|
+
const existing = this.routes.get(path);
|
|
254
|
+
if (!existing) {
|
|
255
|
+
throw new Error(`Route not found: ${path}`);
|
|
256
|
+
}
|
|
257
|
+
const operation = {
|
|
258
|
+
type: "route-update",
|
|
259
|
+
path,
|
|
260
|
+
component: updates.component,
|
|
261
|
+
metadata: {
|
|
262
|
+
...existing.metadata,
|
|
263
|
+
updatedAt: new Date().toISOString(),
|
|
264
|
+
updatedBy: this.coordinator?.getLocalNodeId() ?? "local"
|
|
265
|
+
},
|
|
266
|
+
timestamp: new Date().toISOString(),
|
|
267
|
+
nodeId: this.coordinator?.getLocalNodeId() ?? "local"
|
|
268
|
+
};
|
|
269
|
+
this.routes.set(path, {
|
|
270
|
+
...existing,
|
|
271
|
+
...updates,
|
|
272
|
+
metadata: operation.metadata
|
|
273
|
+
});
|
|
274
|
+
this.notifyMutation(operation);
|
|
275
|
+
}
|
|
276
|
+
async removeRoute(path) {
|
|
277
|
+
const operation = {
|
|
278
|
+
type: "route-remove",
|
|
279
|
+
path,
|
|
280
|
+
timestamp: new Date().toISOString(),
|
|
281
|
+
nodeId: this.coordinator?.getLocalNodeId() ?? "local"
|
|
282
|
+
};
|
|
283
|
+
this.routes.delete(path);
|
|
284
|
+
this.notifyMutation(operation);
|
|
285
|
+
}
|
|
286
|
+
getRoute(path) {
|
|
287
|
+
return this.routes.get(path);
|
|
288
|
+
}
|
|
289
|
+
getSessionId(path) {
|
|
290
|
+
return path.replace(/^\/|\/$/g, "").replace(/\//g, "-") || "index";
|
|
291
|
+
}
|
|
292
|
+
getAllRoutes() {
|
|
293
|
+
return Array.from(this.routes.values());
|
|
294
|
+
}
|
|
295
|
+
subscribeToMutations(callback) {
|
|
296
|
+
this.mutationCallbacks.push(callback);
|
|
297
|
+
return () => {
|
|
298
|
+
const idx = this.mutationCallbacks.indexOf(callback);
|
|
299
|
+
if (idx >= 0) {
|
|
300
|
+
this.mutationCallbacks.splice(idx, 1);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
handleConnect(ws) {
|
|
305
|
+
this.connectedSockets.add(ws);
|
|
306
|
+
}
|
|
307
|
+
handleDisconnect(ws) {
|
|
308
|
+
this.connectedSockets.delete(ws);
|
|
309
|
+
}
|
|
310
|
+
handleSyncMessage(ws, message) {
|
|
311
|
+
try {
|
|
312
|
+
const data = typeof message === "string" ? JSON.parse(message) : message;
|
|
313
|
+
if (data.type === "route-operation") {
|
|
314
|
+
this.applyRemoteOperation(data.operation);
|
|
315
|
+
}
|
|
316
|
+
} catch (error) {
|
|
317
|
+
console.error("[aeon-registry] Error handling sync message:", error);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
notifyMutation(operation) {
|
|
321
|
+
for (const callback of this.mutationCallbacks) {
|
|
322
|
+
try {
|
|
323
|
+
callback(operation);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.error("[aeon-registry] Error in mutation callback:", error);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const message = JSON.stringify({ type: "route-operation", operation });
|
|
329
|
+
for (const ws of this.connectedSockets) {
|
|
330
|
+
try {
|
|
331
|
+
ws.send?.(message);
|
|
332
|
+
} catch {}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
handleSyncCompleted(session) {
|
|
336
|
+
if (this.reconciler) {
|
|
337
|
+
const result = this.reconciler.reconcile();
|
|
338
|
+
if (result?.state) {
|
|
339
|
+
const routes = result.state;
|
|
340
|
+
for (const [path, route] of routes) {
|
|
341
|
+
this.routes.set(path, route);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
applyRemoteOperation(operation) {
|
|
347
|
+
switch (operation.type) {
|
|
348
|
+
case "route-add":
|
|
349
|
+
if (operation.component && operation.metadata) {
|
|
350
|
+
this.routes.set(operation.path, {
|
|
351
|
+
path: operation.path,
|
|
352
|
+
component: operation.component,
|
|
353
|
+
metadata: operation.metadata,
|
|
354
|
+
version: "1.0.0"
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
break;
|
|
358
|
+
case "route-update":
|
|
359
|
+
const existing = this.routes.get(operation.path);
|
|
360
|
+
if (existing && operation.component) {
|
|
361
|
+
this.routes.set(operation.path, {
|
|
362
|
+
...existing,
|
|
363
|
+
component: operation.component,
|
|
364
|
+
metadata: operation.metadata ?? existing.metadata
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
case "route-remove":
|
|
369
|
+
this.routes.delete(operation.path);
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
for (const callback of this.mutationCallbacks) {
|
|
373
|
+
try {
|
|
374
|
+
callback(operation);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
console.error("[aeon-registry] Error in mutation callback:", error);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async persistRoute(path, component) {
|
|
381
|
+
const filePath = path === "/" ? "page.tsx" : `${path.slice(1)}/page.tsx`;
|
|
382
|
+
const content = `'use aeon';
|
|
383
|
+
|
|
384
|
+
export default function Page() {
|
|
385
|
+
return <${component} />;
|
|
386
|
+
}
|
|
387
|
+
`;
|
|
388
|
+
try {
|
|
389
|
+
console.log(`[aeon-registry] Would persist route to: ${filePath}`);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
console.error(`[aeon-registry] Error persisting route:`, error);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/server.ts
|
|
397
|
+
function createMinimalTree(match) {
|
|
398
|
+
const nodes = new Map;
|
|
399
|
+
const rootId = match?.componentId || "root";
|
|
400
|
+
nodes.set(rootId, {
|
|
401
|
+
id: rootId,
|
|
402
|
+
type: "page",
|
|
403
|
+
props: {},
|
|
404
|
+
children: []
|
|
405
|
+
});
|
|
406
|
+
return {
|
|
407
|
+
rootId,
|
|
408
|
+
nodes,
|
|
409
|
+
getNode: (id) => nodes.get(id),
|
|
410
|
+
getChildren: () => [],
|
|
411
|
+
getSchema: () => ({
|
|
412
|
+
rootId,
|
|
413
|
+
nodeCount: nodes.size,
|
|
414
|
+
nodeTypes: ["page"],
|
|
415
|
+
depth: 1
|
|
416
|
+
}),
|
|
417
|
+
clone: () => createMinimalTree(match)
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function createRouterAdapter(routerConfig) {
|
|
421
|
+
if (!routerConfig) {
|
|
422
|
+
return new HeuristicAdapter;
|
|
423
|
+
}
|
|
424
|
+
if (typeof routerConfig.adapter === "object") {
|
|
425
|
+
return routerConfig.adapter;
|
|
426
|
+
}
|
|
427
|
+
switch (routerConfig.adapter) {
|
|
428
|
+
case "heuristic":
|
|
429
|
+
default:
|
|
430
|
+
return new HeuristicAdapter;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async function createAeonServer(options) {
|
|
434
|
+
const { config, router: routerConfig, onRouteChange, onRouteDecision } = options;
|
|
435
|
+
const router = new AeonRouter({
|
|
436
|
+
routesDir: config.pagesDir,
|
|
437
|
+
componentsDir: config.componentsDir
|
|
438
|
+
});
|
|
439
|
+
const registry = new AeonRouteRegistry({
|
|
440
|
+
syncMode: config.aeon?.sync?.mode ?? "distributed",
|
|
441
|
+
versioningEnabled: config.aeon?.versioning?.enabled ?? true
|
|
442
|
+
});
|
|
443
|
+
const personalizedRouter = createRouterAdapter(routerConfig);
|
|
444
|
+
if (config.runtime === "bun" && true) {
|
|
445
|
+
await watchFiles(config.pagesDir, async (path, type) => {
|
|
446
|
+
console.log(`[aeon] File ${type}: ${path}`);
|
|
447
|
+
await router.reload();
|
|
448
|
+
onRouteChange?.(path, type === "create" ? "add" : type === "delete" ? "remove" : "update");
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
registry.subscribeToMutations((operation) => {
|
|
452
|
+
console.log(`[aeon] Collaborative route mutation:`, operation);
|
|
453
|
+
router.reload();
|
|
454
|
+
onRouteChange?.(operation.path, operation.type);
|
|
455
|
+
});
|
|
456
|
+
await router.scan();
|
|
457
|
+
return Bun.serve({
|
|
458
|
+
port: config.port ?? 3000,
|
|
459
|
+
async fetch(req) {
|
|
460
|
+
const url = new URL(req.url);
|
|
461
|
+
const path = url.pathname;
|
|
462
|
+
if (path.startsWith("/_aeon/")) {
|
|
463
|
+
return handleStaticAsset(path, config);
|
|
464
|
+
}
|
|
465
|
+
if (path === "/_aeon/ws" && req.headers.get("upgrade") === "websocket") {
|
|
466
|
+
return handleWebSocketUpgrade(req, registry);
|
|
467
|
+
}
|
|
468
|
+
const match = router.match(path);
|
|
469
|
+
if (!match) {
|
|
470
|
+
if (config.aeon?.dynamicRoutes !== false) {
|
|
471
|
+
return handleDynamicCreation(path, req, registry);
|
|
472
|
+
}
|
|
473
|
+
return new Response("Not Found", { status: 404 });
|
|
474
|
+
}
|
|
475
|
+
const userContext = await extractUserContext(req);
|
|
476
|
+
const tree = createMinimalTree(match);
|
|
477
|
+
const decision = await personalizedRouter.route(path, userContext, tree);
|
|
478
|
+
onRouteDecision?.(decision, userContext);
|
|
479
|
+
let response = await renderRoute(match, req, config, decision);
|
|
480
|
+
response = setContextCookies(response, userContext, path);
|
|
481
|
+
response = addSpeculationHeaders(response, decision.prefetch || [], decision.prerender || []);
|
|
482
|
+
return response;
|
|
483
|
+
},
|
|
484
|
+
websocket: {
|
|
485
|
+
message(ws, message) {
|
|
486
|
+
registry.handleSyncMessage(ws, message);
|
|
487
|
+
},
|
|
488
|
+
open(ws) {
|
|
489
|
+
registry.handleConnect(ws);
|
|
490
|
+
},
|
|
491
|
+
close(ws) {
|
|
492
|
+
registry.handleDisconnect(ws);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
async function watchFiles(dir, callback) {
|
|
498
|
+
const { watch } = await import("fs");
|
|
499
|
+
const { join: join2 } = await import("./chunk-nj84xhja.js");
|
|
500
|
+
watch(dir, { recursive: true }, (eventType, filename) => {
|
|
501
|
+
if (!filename)
|
|
502
|
+
return;
|
|
503
|
+
if (!filename.endsWith(".tsx") && !filename.endsWith(".ts"))
|
|
504
|
+
return;
|
|
505
|
+
const fullPath = join2(dir, filename);
|
|
506
|
+
const type = eventType === "rename" ? "create" : "update";
|
|
507
|
+
callback(fullPath, type);
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
function handleStaticAsset(path, config) {
|
|
511
|
+
const assetPath = path.replace("/_aeon/", "");
|
|
512
|
+
const fullPath = `${config.output?.dir ?? ".aeon"}/${assetPath}`;
|
|
513
|
+
try {
|
|
514
|
+
const file = Bun.file(fullPath);
|
|
515
|
+
return new Response(file);
|
|
516
|
+
} catch {
|
|
517
|
+
return new Response("Not Found", { status: 404 });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function handleWebSocketUpgrade(req, _registry) {
|
|
521
|
+
const server = Bun.serve.prototype;
|
|
522
|
+
if ("upgrade" in server) {
|
|
523
|
+
const success = server.upgrade(req);
|
|
524
|
+
if (success) {
|
|
525
|
+
return new Response(null, { status: 101 });
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return new Response("WebSocket upgrade failed", { status: 500 });
|
|
529
|
+
}
|
|
530
|
+
async function handleDynamicCreation(path, req, registry) {
|
|
531
|
+
const authHeader = req.headers.get("Authorization");
|
|
532
|
+
if (!authHeader) {
|
|
533
|
+
return new Response("Not Found", { status: 404 });
|
|
534
|
+
}
|
|
535
|
+
await registry.addRoute(path, "DynamicPage", {
|
|
536
|
+
createdAt: new Date().toISOString(),
|
|
537
|
+
createdBy: "dynamic"
|
|
538
|
+
});
|
|
539
|
+
return new Response(JSON.stringify({
|
|
540
|
+
message: "Route created",
|
|
541
|
+
path,
|
|
542
|
+
session: registry.getSessionId(path)
|
|
543
|
+
}), {
|
|
544
|
+
status: 201,
|
|
545
|
+
headers: { "Content-Type": "application/json" }
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
async function renderRoute(match, _req, config, decision) {
|
|
549
|
+
if (!match) {
|
|
550
|
+
return new Response("Not Found", { status: 404 });
|
|
551
|
+
}
|
|
552
|
+
if (match.isAeon) {
|
|
553
|
+
const html2 = generateAeonPageHtml(match, config, decision);
|
|
554
|
+
return new Response(html2, {
|
|
555
|
+
headers: { "Content-Type": "text/html" }
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
const html = generateStaticPageHtml(match, config, decision);
|
|
559
|
+
return new Response(html, {
|
|
560
|
+
headers: { "Content-Type": "text/html" }
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
function generateSpeculationScript(decision) {
|
|
564
|
+
if (!decision?.prefetch?.length && !decision?.prerender?.length) {
|
|
565
|
+
return "";
|
|
566
|
+
}
|
|
567
|
+
const rules = {};
|
|
568
|
+
if (decision.prerender?.length) {
|
|
569
|
+
rules.prerender = [{ urls: decision.prerender }];
|
|
570
|
+
}
|
|
571
|
+
if (decision.prefetch?.length) {
|
|
572
|
+
rules.prefetch = [{ urls: decision.prefetch }];
|
|
573
|
+
}
|
|
574
|
+
return `<script type="speculationrules">${JSON.stringify(rules)}</script>`;
|
|
575
|
+
}
|
|
576
|
+
function generatePersonalizationStyles(decision) {
|
|
577
|
+
if (!decision)
|
|
578
|
+
return "";
|
|
579
|
+
const vars = [];
|
|
580
|
+
if (decision.accent) {
|
|
581
|
+
vars.push(`--aeon-accent: ${decision.accent}`);
|
|
582
|
+
}
|
|
583
|
+
if (decision.theme) {
|
|
584
|
+
vars.push(`--aeon-theme: ${decision.theme}`);
|
|
585
|
+
}
|
|
586
|
+
if (decision.density) {
|
|
587
|
+
const spacingMap = { compact: "0.5rem", normal: "1rem", comfortable: "1.5rem" };
|
|
588
|
+
vars.push(`--aeon-spacing: ${spacingMap[decision.density]}`);
|
|
589
|
+
}
|
|
590
|
+
if (vars.length === 0)
|
|
591
|
+
return "";
|
|
592
|
+
return `<style>:root { ${vars.join("; ")} }</style>`;
|
|
593
|
+
}
|
|
594
|
+
function generateAeonPageHtml(match, config, decision) {
|
|
595
|
+
const { sessionId, params, componentId } = match;
|
|
596
|
+
const colorScheme = decision?.theme === "dark" ? "dark" : decision?.theme === "light" ? "light" : "";
|
|
597
|
+
const colorSchemeAttr = colorScheme ? ` data-theme="${colorScheme}"` : "";
|
|
598
|
+
return `<!DOCTYPE html>
|
|
599
|
+
<html lang="en"${colorSchemeAttr}>
|
|
600
|
+
<head>
|
|
601
|
+
<meta charset="UTF-8">
|
|
602
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
603
|
+
<title>Aeon Page</title>
|
|
604
|
+
${generatePersonalizationStyles(decision)}
|
|
605
|
+
${generateSpeculationScript(decision)}
|
|
606
|
+
<script type="module">
|
|
607
|
+
// Aeon hydration script
|
|
608
|
+
import { hydrate, initAeonSync } from '/_aeon/runtime.js';
|
|
609
|
+
|
|
610
|
+
const sessionId = '${sessionId}';
|
|
611
|
+
const params = ${JSON.stringify(params)};
|
|
612
|
+
const componentId = '${componentId}';
|
|
613
|
+
const routeDecision = ${JSON.stringify(decision || {})};
|
|
614
|
+
|
|
615
|
+
// Initialize Aeon sync
|
|
616
|
+
const sync = await initAeonSync({
|
|
617
|
+
sessionId,
|
|
618
|
+
wsUrl: 'ws://' + window.location.host + '/_aeon/ws',
|
|
619
|
+
presence: ${config.aeon?.presence?.enabled ?? true},
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// Hydrate the page from session state
|
|
623
|
+
const session = await sync.getSession(sessionId);
|
|
624
|
+
hydrate(session.tree, document.getElementById('root'), {
|
|
625
|
+
componentOrder: routeDecision.componentOrder,
|
|
626
|
+
hiddenComponents: routeDecision.hiddenComponents,
|
|
627
|
+
featureFlags: routeDecision.featureFlags,
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Subscribe to real-time updates
|
|
631
|
+
sync.subscribe((update) => {
|
|
632
|
+
hydrate(update.tree, document.getElementById('root'), {
|
|
633
|
+
componentOrder: routeDecision.componentOrder,
|
|
634
|
+
hiddenComponents: routeDecision.hiddenComponents,
|
|
635
|
+
featureFlags: routeDecision.featureFlags,
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
</script>
|
|
639
|
+
</head>
|
|
640
|
+
<body>
|
|
641
|
+
<div id="root">
|
|
642
|
+
<!-- Server-rendered content would go here -->
|
|
643
|
+
<noscript>This page requires JavaScript for collaborative features.</noscript>
|
|
644
|
+
</div>
|
|
645
|
+
</body>
|
|
646
|
+
</html>`;
|
|
647
|
+
}
|
|
648
|
+
function generateStaticPageHtml(match, _config, decision) {
|
|
649
|
+
const colorScheme = decision?.theme === "dark" ? "dark" : decision?.theme === "light" ? "light" : "";
|
|
650
|
+
const colorSchemeAttr = colorScheme ? ` data-theme="${colorScheme}"` : "";
|
|
651
|
+
return `<!DOCTYPE html>
|
|
652
|
+
<html lang="en"${colorSchemeAttr}>
|
|
653
|
+
<head>
|
|
654
|
+
<meta charset="UTF-8">
|
|
655
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
656
|
+
<title>Static Page</title>
|
|
657
|
+
${generatePersonalizationStyles(decision)}
|
|
658
|
+
${generateSpeculationScript(decision)}
|
|
659
|
+
</head>
|
|
660
|
+
<body>
|
|
661
|
+
<div id="root">
|
|
662
|
+
<!-- Render ${match.componentId} here -->
|
|
663
|
+
</div>
|
|
664
|
+
</body>
|
|
665
|
+
</html>`;
|
|
666
|
+
}
|
|
667
|
+
export { AeonRouter, AeonRouteRegistry, createAeonServer };
|