@affectively/aeon-flux 0.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 (72) hide show
  1. package/README.md +438 -0
  2. package/examples/basic/aeon.config.ts +39 -0
  3. package/examples/basic/components/Cursor.tsx +88 -0
  4. package/examples/basic/components/OfflineIndicator.tsx +93 -0
  5. package/examples/basic/components/PresenceBar.tsx +68 -0
  6. package/examples/basic/package.json +20 -0
  7. package/examples/basic/pages/index.tsx +73 -0
  8. package/package.json +90 -0
  9. package/packages/benchmarks/src/benchmark.test.ts +644 -0
  10. package/packages/cli/package.json +43 -0
  11. package/packages/cli/src/commands/build.test.ts +649 -0
  12. package/packages/cli/src/commands/build.ts +853 -0
  13. package/packages/cli/src/commands/dev.ts +463 -0
  14. package/packages/cli/src/commands/init.ts +395 -0
  15. package/packages/cli/src/commands/start.ts +289 -0
  16. package/packages/cli/src/index.ts +102 -0
  17. package/packages/directives/src/use-aeon.ts +266 -0
  18. package/packages/react/package.json +34 -0
  19. package/packages/react/src/Link.tsx +355 -0
  20. package/packages/react/src/hooks/useAeonNavigation.ts +204 -0
  21. package/packages/react/src/hooks/usePilotNavigation.ts +253 -0
  22. package/packages/react/src/hooks/useServiceWorker.ts +276 -0
  23. package/packages/react/src/hooks.ts +192 -0
  24. package/packages/react/src/index.ts +89 -0
  25. package/packages/react/src/provider.tsx +428 -0
  26. package/packages/runtime/package.json +70 -0
  27. package/packages/runtime/schema.sql +40 -0
  28. package/packages/runtime/src/api-routes.ts +453 -0
  29. package/packages/runtime/src/benchmark.ts +145 -0
  30. package/packages/runtime/src/cache.ts +287 -0
  31. package/packages/runtime/src/durable-object.ts +847 -0
  32. package/packages/runtime/src/index.ts +235 -0
  33. package/packages/runtime/src/navigation.test.ts +432 -0
  34. package/packages/runtime/src/navigation.ts +412 -0
  35. package/packages/runtime/src/nextjs-adapter.ts +254 -0
  36. package/packages/runtime/src/predictor.ts +368 -0
  37. package/packages/runtime/src/registry.ts +339 -0
  38. package/packages/runtime/src/router/context-extractor.ts +394 -0
  39. package/packages/runtime/src/router/esi-control-react.tsx +1172 -0
  40. package/packages/runtime/src/router/esi-control.ts +488 -0
  41. package/packages/runtime/src/router/esi-react.tsx +600 -0
  42. package/packages/runtime/src/router/esi.ts +595 -0
  43. package/packages/runtime/src/router/heuristic-adapter.test.ts +272 -0
  44. package/packages/runtime/src/router/heuristic-adapter.ts +544 -0
  45. package/packages/runtime/src/router/index.ts +158 -0
  46. package/packages/runtime/src/router/speculation.ts +442 -0
  47. package/packages/runtime/src/router/types.ts +514 -0
  48. package/packages/runtime/src/router.test.ts +466 -0
  49. package/packages/runtime/src/router.ts +285 -0
  50. package/packages/runtime/src/server.ts +446 -0
  51. package/packages/runtime/src/service-worker.ts +418 -0
  52. package/packages/runtime/src/speculation.test.ts +360 -0
  53. package/packages/runtime/src/speculation.ts +456 -0
  54. package/packages/runtime/src/storage.test.ts +1201 -0
  55. package/packages/runtime/src/storage.ts +1031 -0
  56. package/packages/runtime/src/tree-compiler.ts +252 -0
  57. package/packages/runtime/src/types.ts +444 -0
  58. package/packages/runtime/src/worker.ts +300 -0
  59. package/packages/runtime/tsconfig.json +19 -0
  60. package/packages/runtime/wrangler.toml +41 -0
  61. package/packages/runtime-wasm/Cargo.lock +436 -0
  62. package/packages/runtime-wasm/Cargo.toml +29 -0
  63. package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +328 -0
  64. package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1267 -0
  65. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
  66. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +73 -0
  67. package/packages/runtime-wasm/pkg/package.json +21 -0
  68. package/packages/runtime-wasm/src/hydrate.rs +352 -0
  69. package/packages/runtime-wasm/src/lib.rs +189 -0
  70. package/packages/runtime-wasm/src/render.rs +629 -0
  71. package/packages/runtime-wasm/src/router.rs +298 -0
  72. package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
@@ -0,0 +1,463 @@
1
+ /**
2
+ * aeon dev - Start development server with hot reload
3
+ *
4
+ * Features:
5
+ * - Hot module reload
6
+ * - 'use aeon' directive processing
7
+ * - Real-time sync preview
8
+ * - AST ↔ Source bidirectional editing
9
+ * - Error overlay
10
+ */
11
+
12
+ import { watch } from 'fs';
13
+ import { readFile, stat } from 'fs/promises';
14
+ import { resolve, join, relative } from 'path';
15
+
16
+ interface DevOptions {
17
+ port: number;
18
+ config?: string;
19
+ }
20
+
21
+ interface AeonConfig {
22
+ pagesDir: string;
23
+ componentsDir?: string;
24
+ runtime: 'bun' | 'cloudflare';
25
+ port?: number;
26
+ aeon?: {
27
+ sync?: { mode: string };
28
+ presence?: { enabled: boolean };
29
+ versioning?: { enabled: boolean };
30
+ offline?: { enabled: boolean };
31
+ };
32
+ }
33
+
34
+ export async function dev(options: DevOptions): Promise<void> {
35
+ const cwd = process.cwd();
36
+ const configPath = options.config || 'aeon.config.ts';
37
+
38
+ console.log('\n🚀 Starting Aeon Flux development server...\n');
39
+
40
+ // Load config
41
+ const config = await loadConfig(resolve(cwd, configPath));
42
+ const port = options.port || config.port || 3000;
43
+
44
+ // Resolve directories
45
+ const pagesDir = resolve(cwd, config.pagesDir || './pages');
46
+ const componentsDir = resolve(cwd, config.componentsDir || './components');
47
+
48
+ console.log(`📁 Pages: ${relative(cwd, pagesDir)}`);
49
+ console.log(`📁 Components: ${relative(cwd, componentsDir)}`);
50
+ console.log(`🌐 Port: ${port}`);
51
+ console.log('');
52
+
53
+ // Scan for routes
54
+ const routes = await scanRoutes(pagesDir);
55
+ console.log(`📄 Found ${routes.length} route(s):`);
56
+ for (const route of routes) {
57
+ const aeonBadge = route.isAeon ? ' [aeon]' : '';
58
+ console.log(` ${route.pattern}${aeonBadge}`);
59
+ }
60
+ console.log('');
61
+
62
+ // Start file watcher
63
+ const watcher = startWatcher(pagesDir, componentsDir, async (event, filename) => {
64
+ console.log(`\n♻️ ${event}: ${filename}`);
65
+ // Trigger HMR
66
+ broadcastHMR(filename);
67
+ });
68
+
69
+ // Start server
70
+ const server = Bun.serve({
71
+ port,
72
+ async fetch(req) {
73
+ const url = new URL(req.url);
74
+
75
+ // HMR WebSocket
76
+ if (url.pathname === '/__aeon_hmr') {
77
+ const upgraded = server.upgrade(req);
78
+ if (!upgraded) {
79
+ return new Response('WebSocket upgrade failed', { status: 400 });
80
+ }
81
+ return undefined;
82
+ }
83
+
84
+ // Dev overlay script
85
+ if (url.pathname === '/__aeon_dev.js') {
86
+ return new Response(DEV_OVERLAY_SCRIPT, {
87
+ headers: { 'Content-Type': 'application/javascript' },
88
+ });
89
+ }
90
+
91
+ // Match route
92
+ const match = matchRoute(url.pathname, routes);
93
+ if (!match) {
94
+ return new Response('Not found', { status: 404 });
95
+ }
96
+
97
+ // Read and process page
98
+ try {
99
+ const content = await readFile(match.filePath, 'utf-8');
100
+ const html = await renderPage(content, match, config);
101
+
102
+ // Inject dev overlay
103
+ const devHtml = injectDevOverlay(html, port);
104
+
105
+ return new Response(devHtml, {
106
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
107
+ });
108
+ } catch (err) {
109
+ console.error('Render error:', err);
110
+ return new Response(renderErrorPage(err as Error), {
111
+ status: 500,
112
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
113
+ });
114
+ }
115
+ },
116
+ websocket: {
117
+ open(ws) {
118
+ hmrClients.add(ws);
119
+ console.log(' HMR client connected');
120
+ },
121
+ close(ws) {
122
+ hmrClients.delete(ws);
123
+ },
124
+ message(_ws, _message) {
125
+ // Handle client messages if needed
126
+ },
127
+ },
128
+ });
129
+
130
+ console.log(`\n✨ Ready at http://localhost:${port}\n`);
131
+ console.log(' Press Ctrl+C to stop\n');
132
+
133
+ // Handle graceful shutdown
134
+ process.on('SIGINT', () => {
135
+ console.log('\n\n👋 Shutting down...\n');
136
+ watcher.close();
137
+ server.stop();
138
+ process.exit(0);
139
+ });
140
+ }
141
+
142
+ // HMR clients
143
+ const hmrClients = new Set<unknown>();
144
+
145
+ function broadcastHMR(filename: string): void {
146
+ const message = JSON.stringify({ type: 'reload', filename });
147
+ for (const client of hmrClients) {
148
+ try {
149
+ (client as { send: (msg: string) => void }).send(message);
150
+ } catch {
151
+ hmrClients.delete(client);
152
+ }
153
+ }
154
+ }
155
+
156
+ async function loadConfig(configPath: string): Promise<AeonConfig> {
157
+ try {
158
+ const module = await import(configPath);
159
+ return module.default || module;
160
+ } catch {
161
+ console.log('⚠️ No config found, using defaults');
162
+ return {
163
+ pagesDir: './pages',
164
+ runtime: 'bun',
165
+ };
166
+ }
167
+ }
168
+
169
+ interface RouteInfo {
170
+ pattern: string;
171
+ filePath: string;
172
+ isAeon: boolean;
173
+ layout?: string;
174
+ }
175
+
176
+ async function scanRoutes(pagesDir: string): Promise<RouteInfo[]> {
177
+ const routes: RouteInfo[] = [];
178
+
179
+ async function scan(dir: string, routePath: string): Promise<void> {
180
+ const { readdir } = await import('fs/promises');
181
+ const entries = await readdir(dir, { withFileTypes: true });
182
+
183
+ for (const entry of entries) {
184
+ const fullPath = join(dir, entry.name);
185
+
186
+ if (entry.isDirectory()) {
187
+ let segment = entry.name;
188
+ // Convert Next.js dynamic routes to Aeon format
189
+ if (segment.startsWith('[') && segment.endsWith(']')) {
190
+ if (segment.startsWith('[...')) {
191
+ // Catch-all: [...slug] -> *slug
192
+ segment = '*' + segment.slice(4, -1);
193
+ } else if (segment.startsWith('[[...')) {
194
+ // Optional catch-all: [[...slug]] -> **slug
195
+ segment = '**' + segment.slice(5, -2);
196
+ } else {
197
+ // Dynamic: [id] -> :id
198
+ segment = ':' + segment.slice(1, -1);
199
+ }
200
+ }
201
+ await scan(fullPath, `${routePath}/${segment}`);
202
+ } else if (entry.isFile()) {
203
+ const isPage =
204
+ entry.name === 'page.tsx' ||
205
+ entry.name === 'page.ts' ||
206
+ entry.name === 'page.jsx' ||
207
+ entry.name === 'page.js' ||
208
+ (entry.name.endsWith('.tsx') && !entry.name.startsWith('_'));
209
+
210
+ if (isPage) {
211
+ const content = await readFile(fullPath, 'utf-8');
212
+ const isAeon = content.includes("'use aeon'") || content.includes('"use aeon"');
213
+
214
+ // Check for layout
215
+ const layoutPath = join(dir, 'layout.tsx');
216
+ let layout: string | undefined;
217
+ try {
218
+ await stat(layoutPath);
219
+ layout = layoutPath;
220
+ } catch {
221
+ // No layout
222
+ }
223
+
224
+ routes.push({
225
+ pattern: routePath || '/',
226
+ filePath: fullPath,
227
+ isAeon,
228
+ layout,
229
+ });
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ await scan(pagesDir, '');
236
+ return routes.sort((a, b) => {
237
+ // Static routes first
238
+ const aStatic = !a.pattern.includes(':') && !a.pattern.includes('*');
239
+ const bStatic = !b.pattern.includes(':') && !b.pattern.includes('*');
240
+ if (aStatic && !bStatic) return -1;
241
+ if (!aStatic && bStatic) return 1;
242
+ return a.pattern.localeCompare(b.pattern);
243
+ });
244
+ }
245
+
246
+ function matchRoute(
247
+ path: string,
248
+ routes: RouteInfo[]
249
+ ): (RouteInfo & { params: Record<string, string> }) | null {
250
+ for (const route of routes) {
251
+ const params = matchPattern(path, route.pattern);
252
+ if (params !== null) {
253
+ return { ...route, params };
254
+ }
255
+ }
256
+ return null;
257
+ }
258
+
259
+ function matchPattern(path: string, pattern: string): Record<string, string> | null {
260
+ const pathParts = path.split('/').filter(Boolean);
261
+ const patternParts = pattern.split('/').filter(Boolean);
262
+ const params: Record<string, string> = {};
263
+
264
+ let pi = 0;
265
+ let pati = 0;
266
+
267
+ while (pi < pathParts.length && pati < patternParts.length) {
268
+ const pathPart = pathParts[pi];
269
+ const patternPart = patternParts[pati];
270
+
271
+ if (patternPart.startsWith('**')) {
272
+ // Optional catch-all
273
+ const paramName = patternPart.slice(2);
274
+ params[paramName] = pathParts.slice(pi).join('/');
275
+ return params;
276
+ } else if (patternPart.startsWith('*')) {
277
+ // Catch-all
278
+ const paramName = patternPart.slice(1);
279
+ params[paramName] = pathParts.slice(pi).join('/');
280
+ return params;
281
+ } else if (patternPart.startsWith(':')) {
282
+ // Dynamic segment
283
+ const paramName = patternPart.slice(1);
284
+ params[paramName] = pathPart;
285
+ pi++;
286
+ pati++;
287
+ } else if (pathPart === patternPart) {
288
+ pi++;
289
+ pati++;
290
+ } else {
291
+ return null;
292
+ }
293
+ }
294
+
295
+ // Handle optional catch-all at end
296
+ if (pati < patternParts.length && patternParts[pati].startsWith('**')) {
297
+ const paramName = patternParts[pati].slice(2);
298
+ params[paramName] = '';
299
+ return params;
300
+ }
301
+
302
+ if (pi === pathParts.length && pati === patternParts.length) {
303
+ return params;
304
+ }
305
+
306
+ return null;
307
+ }
308
+
309
+ async function renderPage(
310
+ content: string,
311
+ match: RouteInfo & { params: Record<string, string> },
312
+ _config: AeonConfig
313
+ ): Promise<string> {
314
+ // For dev mode, we do a simple transform
315
+ // In production, this would use the WASM runtime
316
+
317
+ // Check for 'use aeon' directive
318
+ const isAeon = match.isAeon;
319
+
320
+ // Extract the default export component
321
+ const componentMatch = content.match(
322
+ /export\s+default\s+function\s+(\w+)/
323
+ );
324
+ const componentName = componentMatch?.[1] || 'Page';
325
+
326
+ // Simple dev rendering - in production this uses React SSR
327
+ const html = `<!DOCTYPE html>
328
+ <html lang="en">
329
+ <head>
330
+ <meta charset="UTF-8">
331
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
332
+ <title>${componentName} - Aeon Flux</title>
333
+ <link href="https://cdn.jsdelivr.net/npm/tailwindcss@3/dist/tailwind.min.css" rel="stylesheet">
334
+ <script type="importmap">
335
+ {
336
+ "imports": {
337
+ "react": "https://esm.sh/react@18",
338
+ "react-dom": "https://esm.sh/react-dom@18",
339
+ "react-dom/client": "https://esm.sh/react-dom@18/client",
340
+ "@affectively/aeon-pages/react": "/__aeon_runtime.js"
341
+ }
342
+ }
343
+ </script>
344
+ </head>
345
+ <body>
346
+ <div id="root"></div>
347
+ ${isAeon ? `
348
+ <script type="module">
349
+ // Aeon Flux runtime
350
+ window.__AEON_PAGE__ = {
351
+ route: "${match.pattern}",
352
+ params: ${JSON.stringify(match.params)},
353
+ isAeon: true,
354
+ };
355
+ </script>
356
+ ` : ''}
357
+ <script type="module">
358
+ // Dev mode hydration
359
+ import React from 'react';
360
+ import { createRoot } from 'react-dom/client';
361
+
362
+ // Simple placeholder for dev mode
363
+ const App = () => React.createElement('div', {
364
+ style: { padding: '2rem', fontFamily: 'system-ui' }
365
+ }, [
366
+ React.createElement('h1', { key: 'h1' }, '${componentName}'),
367
+ React.createElement('p', { key: 'p' }, 'Route: ${match.pattern}'),
368
+ ${isAeon ? `React.createElement('p', { key: 'aeon', style: { color: 'green' } }, '✓ Aeon Flux enabled'),` : ''}
369
+ React.createElement('p', { key: 'note', style: { color: '#666', marginTop: '1rem' } },
370
+ 'This is dev mode. Full rendering coming soon.')
371
+ ]);
372
+
373
+ const root = createRoot(document.getElementById('root'));
374
+ root.render(React.createElement(App));
375
+ </script>
376
+ </body>
377
+ </html>`;
378
+
379
+ return html;
380
+ }
381
+
382
+ function injectDevOverlay(html: string, port: number): string {
383
+ const script = `<script>
384
+ // Aeon Flux HMR
385
+ const ws = new WebSocket('ws://localhost:${port}/__aeon_hmr');
386
+ ws.onmessage = (e) => {
387
+ const msg = JSON.parse(e.data);
388
+ if (msg.type === 'reload') {
389
+ console.log('[Aeon Flux] Reloading:', msg.filename);
390
+ location.reload();
391
+ }
392
+ };
393
+ ws.onclose = () => {
394
+ console.log('[Aeon Flux] Server disconnected, reconnecting...');
395
+ setTimeout(() => location.reload(), 1000);
396
+ };
397
+ </script>`;
398
+
399
+ return html.replace('</body>', `${script}</body>`);
400
+ }
401
+
402
+ function renderErrorPage(error: Error): string {
403
+ return `<!DOCTYPE html>
404
+ <html>
405
+ <head>
406
+ <title>Error - Aeon Flux</title>
407
+ <style>
408
+ body { font-family: system-ui; padding: 2rem; background: #1a1a1a; color: #fff; }
409
+ pre { background: #2d2d2d; padding: 1rem; border-radius: 8px; overflow: auto; }
410
+ h1 { color: #ff6b6b; }
411
+ </style>
412
+ </head>
413
+ <body>
414
+ <h1>⚠️ Error</h1>
415
+ <pre>${escapeHtml(error.message)}\n\n${escapeHtml(error.stack || '')}</pre>
416
+ </body>
417
+ </html>`;
418
+ }
419
+
420
+ function escapeHtml(str: string): string {
421
+ return str
422
+ .replace(/&/g, '&amp;')
423
+ .replace(/</g, '&lt;')
424
+ .replace(/>/g, '&gt;')
425
+ .replace(/"/g, '&quot;');
426
+ }
427
+
428
+ function startWatcher(
429
+ pagesDir: string,
430
+ componentsDir: string,
431
+ callback: (event: string, filename: string) => void
432
+ ): { close: () => void } {
433
+ const watchers: ReturnType<typeof watch>[] = [];
434
+
435
+ try {
436
+ watchers.push(
437
+ watch(pagesDir, { recursive: true }, (event, filename) => {
438
+ if (filename) callback(event, `pages/${filename}`);
439
+ })
440
+ );
441
+ } catch {
442
+ console.log('⚠️ Could not watch pages directory');
443
+ }
444
+
445
+ try {
446
+ watchers.push(
447
+ watch(componentsDir, { recursive: true }, (event, filename) => {
448
+ if (filename) callback(event, `components/${filename}`);
449
+ })
450
+ );
451
+ } catch {
452
+ // Components dir may not exist
453
+ }
454
+
455
+ return {
456
+ close: () => watchers.forEach((w) => w.close()),
457
+ };
458
+ }
459
+
460
+ const DEV_OVERLAY_SCRIPT = `
461
+ // Aeon Flux Dev Overlay
462
+ console.log('[Aeon Flux] Dev mode active');
463
+ `;