@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,285 @@
1
+ /**
2
+ * Aeon Router - TypeScript wrapper for WASM router
3
+ *
4
+ * Handles route matching with Next.js-style patterns:
5
+ * - Static: /about
6
+ * - Dynamic: /blog/[slug]
7
+ * - Catch-all: /api/[...path]
8
+ * - Optional catch-all: /docs/[[...slug]]
9
+ */
10
+
11
+ import type { RouteDefinition, RouteMatch } from './types';
12
+ import { readdir, stat } from 'fs/promises';
13
+ import { join, relative } from 'path';
14
+
15
+ interface RouterOptions {
16
+ routesDir: string;
17
+ componentsDir?: string;
18
+ }
19
+
20
+ interface ParsedRoute {
21
+ pattern: string;
22
+ segments: Segment[];
23
+ definition: RouteDefinition;
24
+ }
25
+
26
+ type Segment =
27
+ | { type: 'static'; value: string }
28
+ | { type: 'dynamic'; name: string }
29
+ | { type: 'catchAll'; name: string }
30
+ | { type: 'optionalCatchAll'; name: string };
31
+
32
+ /**
33
+ * Aeon Router - file-based routing with dynamic segment support
34
+ */
35
+ export class AeonRouter {
36
+ private routes: ParsedRoute[] = [];
37
+ private routesDir: string;
38
+ private componentsDir?: string;
39
+
40
+ constructor(options: RouterOptions) {
41
+ this.routesDir = options.routesDir;
42
+ this.componentsDir = options.componentsDir;
43
+ }
44
+
45
+ /**
46
+ * Scan the routes directory and build the route table
47
+ */
48
+ async scan(): Promise<void> {
49
+ this.routes = [];
50
+ await this.scanDirectory(this.routesDir, '');
51
+ this.sortRoutes();
52
+ }
53
+
54
+ /**
55
+ * Reload routes (for hot reload)
56
+ */
57
+ async reload(): Promise<void> {
58
+ await this.scan();
59
+ }
60
+
61
+ /**
62
+ * Match a URL path to a route
63
+ */
64
+ match(path: string): RouteMatch | null {
65
+ const pathSegments = path
66
+ .replace(/^\/|\/$/g, '')
67
+ .split('/')
68
+ .filter(Boolean);
69
+
70
+ for (const parsed of this.routes) {
71
+ const params = this.matchSegments(parsed.segments, pathSegments);
72
+ if (params !== null) {
73
+ const sessionId = this.resolveSessionId(parsed.definition.sessionId, params);
74
+ return {
75
+ route: parsed.definition,
76
+ params,
77
+ sessionId,
78
+ componentId: parsed.definition.componentId,
79
+ isAeon: parsed.definition.isAeon,
80
+ };
81
+ }
82
+ }
83
+
84
+ return null;
85
+ }
86
+
87
+ /**
88
+ * Check if a route exists
89
+ */
90
+ hasRoute(path: string): boolean {
91
+ return this.match(path) !== null;
92
+ }
93
+
94
+ /**
95
+ * Get all registered routes
96
+ */
97
+ getRoutes(): RouteDefinition[] {
98
+ return this.routes.map((r) => r.definition);
99
+ }
100
+
101
+ /**
102
+ * Add a route manually (for dynamic creation)
103
+ */
104
+ addRoute(definition: RouteDefinition): void {
105
+ const segments = this.parsePattern(definition.pattern);
106
+ this.routes.push({ pattern: definition.pattern, segments, definition });
107
+ this.sortRoutes();
108
+ }
109
+
110
+ // Private methods
111
+
112
+ private async scanDirectory(dir: string, prefix: string): Promise<void> {
113
+ try {
114
+ const entries = await readdir(dir, { withFileTypes: true });
115
+
116
+ for (const entry of entries) {
117
+ const fullPath = join(dir, entry.name);
118
+
119
+ if (entry.isDirectory()) {
120
+ // Skip route groups in URL but process their contents
121
+ const isRouteGroup = entry.name.startsWith('(') && entry.name.endsWith(')');
122
+ const newPrefix = isRouteGroup ? prefix : `${prefix}/${entry.name}`;
123
+ await this.scanDirectory(fullPath, newPrefix);
124
+ } else if (entry.name === 'page.tsx' || entry.name === 'page.ts') {
125
+ // Found a page file
126
+ const route = await this.createRouteFromFile(fullPath, prefix);
127
+ if (route) {
128
+ this.routes.push(route);
129
+ }
130
+ }
131
+ }
132
+ } catch (error) {
133
+ console.error(`[aeon] Error scanning directory ${dir}:`, error);
134
+ }
135
+ }
136
+
137
+ private async createRouteFromFile(filePath: string, prefix: string): Promise<ParsedRoute | null> {
138
+ try {
139
+ // Read file to check for 'use aeon' directive
140
+ const file = Bun.file(filePath);
141
+ const content = await file.text();
142
+ const isAeon = content.includes("'use aeon'") || content.includes('"use aeon"');
143
+
144
+ // Convert file path to route pattern
145
+ const pattern = prefix || '/';
146
+ const segments = this.parsePattern(pattern);
147
+
148
+ // Generate session ID template
149
+ const sessionId = this.generateSessionId(pattern);
150
+
151
+ // Component ID from file path
152
+ const componentId = relative(this.routesDir, filePath)
153
+ .replace(/\.(tsx?|jsx?)$/, '')
154
+ .replace(/\//g, '-')
155
+ .replace(/page$/, '')
156
+ .replace(/-$/, '') || 'index';
157
+
158
+ const definition: RouteDefinition = {
159
+ pattern,
160
+ sessionId,
161
+ componentId,
162
+ isAeon,
163
+ };
164
+
165
+ return { pattern, segments, definition };
166
+ } catch (error) {
167
+ console.error(`[aeon] Error reading file ${filePath}:`, error);
168
+ return null;
169
+ }
170
+ }
171
+
172
+ private parsePattern(pattern: string): Segment[] {
173
+ return pattern
174
+ .replace(/^\/|\/$/g, '')
175
+ .split('/')
176
+ .filter(Boolean)
177
+ .filter((s) => !(s.startsWith('(') && s.endsWith(')'))) // Skip route groups
178
+ .map((s): Segment => {
179
+ if (s.startsWith('[[...') && s.endsWith(']]')) {
180
+ return { type: 'optionalCatchAll', name: s.slice(5, -2) };
181
+ }
182
+ if (s.startsWith('[...') && s.endsWith(']')) {
183
+ return { type: 'catchAll', name: s.slice(4, -1) };
184
+ }
185
+ if (s.startsWith('[') && s.endsWith(']')) {
186
+ return { type: 'dynamic', name: s.slice(1, -1) };
187
+ }
188
+ return { type: 'static', value: s };
189
+ });
190
+ }
191
+
192
+ private matchSegments(
193
+ routeSegments: Segment[],
194
+ pathSegments: string[]
195
+ ): Record<string, string> | null {
196
+ const params: Record<string, string> = {};
197
+ let pathIdx = 0;
198
+
199
+ for (const segment of routeSegments) {
200
+ switch (segment.type) {
201
+ case 'static':
202
+ if (pathIdx >= pathSegments.length || pathSegments[pathIdx] !== segment.value) {
203
+ return null;
204
+ }
205
+ pathIdx++;
206
+ break;
207
+
208
+ case 'dynamic':
209
+ if (pathIdx >= pathSegments.length) {
210
+ return null;
211
+ }
212
+ params[segment.name] = pathSegments[pathIdx];
213
+ pathIdx++;
214
+ break;
215
+
216
+ case 'catchAll':
217
+ if (pathIdx >= pathSegments.length) {
218
+ return null; // Must match at least one segment
219
+ }
220
+ params[segment.name] = pathSegments.slice(pathIdx).join('/');
221
+ pathIdx = pathSegments.length;
222
+ break;
223
+
224
+ case 'optionalCatchAll':
225
+ if (pathIdx < pathSegments.length) {
226
+ params[segment.name] = pathSegments.slice(pathIdx).join('/');
227
+ pathIdx = pathSegments.length;
228
+ }
229
+ break;
230
+ }
231
+ }
232
+
233
+ // All path segments must be consumed
234
+ return pathIdx === pathSegments.length ? params : null;
235
+ }
236
+
237
+ private generateSessionId(pattern: string): string {
238
+ return pattern
239
+ .replace(/^\/|\/$/g, '')
240
+ .replace(/\[\.\.\.(\w+)\]/g, '$$$1')
241
+ .replace(/\[\[\.\.\.(\w+)\]\]/g, '$$$1')
242
+ .replace(/\[(\w+)\]/g, '$$$1')
243
+ .replace(/\//g, '-') || 'index';
244
+ }
245
+
246
+ private resolveSessionId(template: string, params: Record<string, string>): string {
247
+ let result = template;
248
+ for (const [key, value] of Object.entries(params)) {
249
+ result = result.replace(`$${key}`, value);
250
+ }
251
+ return result;
252
+ }
253
+
254
+ private sortRoutes(): void {
255
+ // Sort by specificity: static > dynamic > catch-all
256
+ this.routes.sort((a, b) => {
257
+ const scoreA = this.routeSpecificity(a.segments);
258
+ const scoreB = this.routeSpecificity(b.segments);
259
+ return scoreB - scoreA;
260
+ });
261
+ }
262
+
263
+ private routeSpecificity(segments: Segment[]): number {
264
+ let score = 0;
265
+ for (let i = 0; i < segments.length; i++) {
266
+ const positionWeight = 1000 - i;
267
+ const segment = segments[i];
268
+ switch (segment.type) {
269
+ case 'static':
270
+ score += positionWeight * 10;
271
+ break;
272
+ case 'dynamic':
273
+ score += positionWeight * 5;
274
+ break;
275
+ case 'catchAll':
276
+ score += 1;
277
+ break;
278
+ case 'optionalCatchAll':
279
+ score += 0;
280
+ break;
281
+ }
282
+ }
283
+ return score;
284
+ }
285
+ }