@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,302 @@
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(
74
+ parsed.definition.sessionId,
75
+ params,
76
+ );
77
+ return {
78
+ route: parsed.definition,
79
+ params,
80
+ sessionId,
81
+ componentId: parsed.definition.componentId,
82
+ isAeon: parsed.definition.isAeon,
83
+ };
84
+ }
85
+ }
86
+
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * Check if a route exists
92
+ */
93
+ hasRoute(path: string): boolean {
94
+ return this.match(path) !== null;
95
+ }
96
+
97
+ /**
98
+ * Get all registered routes
99
+ */
100
+ getRoutes(): RouteDefinition[] {
101
+ return this.routes.map((r) => r.definition);
102
+ }
103
+
104
+ /**
105
+ * Add a route manually (for dynamic creation)
106
+ */
107
+ addRoute(definition: RouteDefinition): void {
108
+ const segments = this.parsePattern(definition.pattern);
109
+ this.routes.push({ pattern: definition.pattern, segments, definition });
110
+ this.sortRoutes();
111
+ }
112
+
113
+ // Private methods
114
+
115
+ private async scanDirectory(dir: string, prefix: string): Promise<void> {
116
+ try {
117
+ const entries = await readdir(dir, { withFileTypes: true });
118
+
119
+ for (const entry of entries) {
120
+ const fullPath = join(dir, entry.name);
121
+
122
+ if (entry.isDirectory()) {
123
+ // Skip route groups in URL but process their contents
124
+ const isRouteGroup =
125
+ entry.name.startsWith('(') && entry.name.endsWith(')');
126
+ const newPrefix = isRouteGroup ? prefix : `${prefix}/${entry.name}`;
127
+ await this.scanDirectory(fullPath, newPrefix);
128
+ } else if (entry.name === 'page.tsx' || entry.name === 'page.ts') {
129
+ // Found a page file
130
+ const route = await this.createRouteFromFile(fullPath, prefix);
131
+ if (route) {
132
+ this.routes.push(route);
133
+ }
134
+ }
135
+ }
136
+ } catch (error) {
137
+ console.error(`[aeon] Error scanning directory ${dir}:`, error);
138
+ }
139
+ }
140
+
141
+ private async createRouteFromFile(
142
+ filePath: string,
143
+ prefix: string,
144
+ ): Promise<ParsedRoute | null> {
145
+ try {
146
+ // Read file to check for 'use aeon' directive
147
+ const file = Bun.file(filePath);
148
+ const content = await file.text();
149
+ const isAeon =
150
+ content.includes("'use aeon'") || content.includes('"use aeon"');
151
+
152
+ // Convert file path to route pattern
153
+ const pattern = prefix || '/';
154
+ const segments = this.parsePattern(pattern);
155
+
156
+ // Generate session ID template
157
+ const sessionId = this.generateSessionId(pattern);
158
+
159
+ // Component ID from file path
160
+ const componentId =
161
+ relative(this.routesDir, filePath)
162
+ .replace(/\.(tsx?|jsx?)$/, '')
163
+ .replace(/\//g, '-')
164
+ .replace(/page$/, '')
165
+ .replace(/-$/, '') || 'index';
166
+
167
+ const definition: RouteDefinition = {
168
+ pattern,
169
+ sessionId,
170
+ componentId,
171
+ isAeon,
172
+ };
173
+
174
+ return { pattern, segments, definition };
175
+ } catch (error) {
176
+ console.error(`[aeon] Error reading file ${filePath}:`, error);
177
+ return null;
178
+ }
179
+ }
180
+
181
+ private parsePattern(pattern: string): Segment[] {
182
+ return pattern
183
+ .replace(/^\/|\/$/g, '')
184
+ .split('/')
185
+ .filter(Boolean)
186
+ .filter((s) => !(s.startsWith('(') && s.endsWith(')'))) // Skip route groups
187
+ .map((s): Segment => {
188
+ if (s.startsWith('[[...') && s.endsWith(']]')) {
189
+ return { type: 'optionalCatchAll', name: s.slice(5, -2) };
190
+ }
191
+ if (s.startsWith('[...') && s.endsWith(']')) {
192
+ return { type: 'catchAll', name: s.slice(4, -1) };
193
+ }
194
+ if (s.startsWith('[') && s.endsWith(']')) {
195
+ return { type: 'dynamic', name: s.slice(1, -1) };
196
+ }
197
+ return { type: 'static', value: s };
198
+ });
199
+ }
200
+
201
+ private matchSegments(
202
+ routeSegments: Segment[],
203
+ pathSegments: string[],
204
+ ): Record<string, string> | null {
205
+ const params: Record<string, string> = {};
206
+ let pathIdx = 0;
207
+
208
+ for (const segment of routeSegments) {
209
+ switch (segment.type) {
210
+ case 'static':
211
+ if (
212
+ pathIdx >= pathSegments.length ||
213
+ pathSegments[pathIdx] !== segment.value
214
+ ) {
215
+ return null;
216
+ }
217
+ pathIdx++;
218
+ break;
219
+
220
+ case 'dynamic':
221
+ if (pathIdx >= pathSegments.length) {
222
+ return null;
223
+ }
224
+ params[segment.name] = pathSegments[pathIdx];
225
+ pathIdx++;
226
+ break;
227
+
228
+ case 'catchAll':
229
+ if (pathIdx >= pathSegments.length) {
230
+ return null; // Must match at least one segment
231
+ }
232
+ params[segment.name] = pathSegments.slice(pathIdx).join('/');
233
+ pathIdx = pathSegments.length;
234
+ break;
235
+
236
+ case 'optionalCatchAll':
237
+ if (pathIdx < pathSegments.length) {
238
+ params[segment.name] = pathSegments.slice(pathIdx).join('/');
239
+ pathIdx = pathSegments.length;
240
+ }
241
+ break;
242
+ }
243
+ }
244
+
245
+ // All path segments must be consumed
246
+ return pathIdx === pathSegments.length ? params : null;
247
+ }
248
+
249
+ private generateSessionId(pattern: string): string {
250
+ return (
251
+ pattern
252
+ .replace(/^\/|\/$/g, '')
253
+ .replace(/\[\.\.\.(\w+)\]/g, '$$$1')
254
+ .replace(/\[\[\.\.\.(\w+)\]\]/g, '$$$1')
255
+ .replace(/\[(\w+)\]/g, '$$$1')
256
+ .replace(/\//g, '-') || 'index'
257
+ );
258
+ }
259
+
260
+ private resolveSessionId(
261
+ template: string,
262
+ params: Record<string, string>,
263
+ ): string {
264
+ let result = template;
265
+ for (const [key, value] of Object.entries(params)) {
266
+ result = result.replace(`$${key}`, value);
267
+ }
268
+ return result;
269
+ }
270
+
271
+ private sortRoutes(): void {
272
+ // Sort by specificity: static > dynamic > catch-all
273
+ this.routes.sort((a, b) => {
274
+ const scoreA = this.routeSpecificity(a.segments);
275
+ const scoreB = this.routeSpecificity(b.segments);
276
+ return scoreB - scoreA;
277
+ });
278
+ }
279
+
280
+ private routeSpecificity(segments: Segment[]): number {
281
+ let score = 0;
282
+ for (let i = 0; i < segments.length; i++) {
283
+ const positionWeight = 1000 - i;
284
+ const segment = segments[i];
285
+ switch (segment.type) {
286
+ case 'static':
287
+ score += positionWeight * 10;
288
+ break;
289
+ case 'dynamic':
290
+ score += positionWeight * 5;
291
+ break;
292
+ case 'catchAll':
293
+ score += 1;
294
+ break;
295
+ case 'optionalCatchAll':
296
+ score += 0;
297
+ break;
298
+ }
299
+ }
300
+ return score;
301
+ }
302
+ }