@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,422 @@
1
+ /**
2
+ * Aeon Navigation Engine
3
+ *
4
+ * Cutting-edge navigation with:
5
+ * - Speculative prefetching
6
+ * - Total preload capability
7
+ * - View transitions
8
+ * - Presence awareness
9
+ * - Predictive navigation
10
+ */
11
+
12
+ import { AeonRouter } from './router.js';
13
+ import type { RouteMatch } from './types';
14
+ import {
15
+ NavigationCache,
16
+ type CachedSession,
17
+ getNavigationCache,
18
+ } from './cache';
19
+
20
+ export interface NavigationOptions {
21
+ transition?: 'slide' | 'fade' | 'morph' | 'none';
22
+ replace?: boolean;
23
+ }
24
+
25
+ export interface PrefetchOptions {
26
+ data?: boolean;
27
+ presence?: boolean;
28
+ priority?: 'high' | 'normal' | 'low';
29
+ }
30
+
31
+ export interface NavigationState {
32
+ current: string;
33
+ previous: string | null;
34
+ history: string[];
35
+ isNavigating: boolean;
36
+ }
37
+
38
+ export interface PresenceInfo {
39
+ route: string;
40
+ count: number;
41
+ editing: number;
42
+ hot: boolean;
43
+ users?: { userId: string; name?: string }[];
44
+ }
45
+
46
+ export interface PredictedRoute {
47
+ route: string;
48
+ probability: number;
49
+ reason: 'history' | 'hover' | 'visibility' | 'community';
50
+ }
51
+
52
+ type NavigationListener = (state: NavigationState) => void;
53
+ type PresenceListener = (route: string, presence: PresenceInfo) => void;
54
+
55
+ export class AeonNavigationEngine {
56
+ private router: AeonRouter;
57
+ private cache: NavigationCache;
58
+ private state: NavigationState;
59
+ private navigationListeners: Set<NavigationListener> = new Set();
60
+ private presenceListeners: Set<PresenceListener> = new Set();
61
+ private presenceCache: Map<string, PresenceInfo> = new Map();
62
+ private navigationHistory: Map<string, Map<string, number>> = new Map();
63
+ private pendingPrefetches: Map<string, Promise<CachedSession>> = new Map();
64
+ private observer: IntersectionObserver | null = null;
65
+ private sessionFetcher?: (sessionId: string) => Promise<CachedSession>;
66
+ private presenceFetcher?: (route: string) => Promise<PresenceInfo>;
67
+
68
+ constructor(
69
+ options: {
70
+ router?: AeonRouter;
71
+ cache?: NavigationCache;
72
+ initialRoute?: string;
73
+ sessionFetcher?: (sessionId: string) => Promise<CachedSession>;
74
+ presenceFetcher?: (route: string) => Promise<PresenceInfo>;
75
+ } = {},
76
+ ) {
77
+ this.router = options.router ?? new AeonRouter({ routesDir: './pages' });
78
+ this.cache = options.cache ?? getNavigationCache();
79
+ this.sessionFetcher = options.sessionFetcher;
80
+ this.presenceFetcher = options.presenceFetcher;
81
+
82
+ this.state = {
83
+ current: options.initialRoute ?? '/',
84
+ previous: null,
85
+ history: [options.initialRoute ?? '/'],
86
+ isNavigating: false,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Navigate to a route with optional view transition
92
+ */
93
+ async navigate(href: string, options: NavigationOptions = {}): Promise<void> {
94
+ const { transition = 'fade', replace = false } = options;
95
+
96
+ // Match route
97
+ const match = this.router.match(href);
98
+ if (!match) {
99
+ throw new Error(`Route not found: ${href}`);
100
+ }
101
+
102
+ // Update state
103
+ const previousRoute = this.state.current;
104
+ this.state.isNavigating = true;
105
+ this.notifyListeners();
106
+
107
+ try {
108
+ // Get session (from cache or fetch)
109
+ const session = await this.getSession(match.sessionId);
110
+
111
+ // Perform navigation with view transition
112
+ if (
113
+ transition !== 'none' &&
114
+ typeof document !== 'undefined' &&
115
+ 'startViewTransition' in document
116
+ ) {
117
+ await (document as any).startViewTransition(() => {
118
+ this.updateDOM(session, match);
119
+ }).finished;
120
+ } else {
121
+ this.updateDOM(session, match);
122
+ }
123
+
124
+ // Update state
125
+ this.state.previous = previousRoute;
126
+ this.state.current = href;
127
+ if (!replace) {
128
+ this.state.history.push(href);
129
+ } else {
130
+ this.state.history[this.state.history.length - 1] = href;
131
+ }
132
+
133
+ // Update browser history
134
+ if (typeof window !== 'undefined') {
135
+ if (replace) {
136
+ window.history.replaceState({ route: href }, '', href);
137
+ } else {
138
+ window.history.pushState({ route: href }, '', href);
139
+ }
140
+ }
141
+
142
+ // Record for prediction
143
+ this.recordNavigation(previousRoute, href);
144
+
145
+ // Prefetch predicted next routes
146
+ const predictions = this.predict(href);
147
+ for (const prediction of predictions.slice(0, 3)) {
148
+ if (prediction.probability > 0.3) {
149
+ this.prefetch(prediction.route);
150
+ }
151
+ }
152
+ } finally {
153
+ this.state.isNavigating = false;
154
+ this.notifyListeners();
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Prefetch a route
160
+ */
161
+ async prefetch(href: string, options: PrefetchOptions = {}): Promise<void> {
162
+ const { data = true, presence = false, priority = 'normal' } = options;
163
+
164
+ const match = this.router.match(href);
165
+ if (!match) return;
166
+
167
+ // Avoid duplicate prefetches
168
+ const cacheKey = `${match.sessionId}:${data}:${presence}`;
169
+ if (this.pendingPrefetches.has(cacheKey)) {
170
+ return;
171
+ }
172
+
173
+ const prefetchPromise = (async () => {
174
+ const promises: Promise<unknown>[] = [];
175
+
176
+ // Prefetch session data
177
+ if (data && this.sessionFetcher) {
178
+ promises.push(
179
+ this.cache.prefetch(match.sessionId, () =>
180
+ this.sessionFetcher!(match.sessionId),
181
+ ),
182
+ );
183
+ }
184
+
185
+ // Prefetch presence
186
+ if (presence && this.presenceFetcher) {
187
+ promises.push(this.prefetchPresence(href));
188
+ }
189
+
190
+ await Promise.all(promises);
191
+ return this.cache.get(match.sessionId)!;
192
+ })();
193
+
194
+ this.pendingPrefetches.set(cacheKey, prefetchPromise);
195
+
196
+ try {
197
+ await prefetchPromise;
198
+ } finally {
199
+ this.pendingPrefetches.delete(cacheKey);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Prefetch presence for a route
205
+ */
206
+ async prefetchPresence(route: string): Promise<PresenceInfo | null> {
207
+ if (!this.presenceFetcher) return null;
208
+
209
+ try {
210
+ const presence = await this.presenceFetcher(route);
211
+ this.presenceCache.set(route, presence);
212
+ this.notifyPresenceListeners(route, presence);
213
+ return presence;
214
+ } catch {
215
+ return null;
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Check if a route is preloaded
221
+ */
222
+ isPreloaded(href: string): boolean {
223
+ const match = this.router.match(href);
224
+ if (!match) return false;
225
+ return this.cache.has(match.sessionId);
226
+ }
227
+
228
+ /**
229
+ * Get cached presence for a route
230
+ */
231
+ getPresence(route: string): PresenceInfo | null {
232
+ return this.presenceCache.get(route) ?? null;
233
+ }
234
+
235
+ /**
236
+ * Observe links for visibility-based prefetch
237
+ */
238
+ observeLinks(container: Element): () => void {
239
+ if (typeof IntersectionObserver === 'undefined') {
240
+ return () => {};
241
+ }
242
+
243
+ this.observer = new IntersectionObserver(
244
+ (entries) => {
245
+ for (const entry of entries) {
246
+ if (entry.isIntersecting) {
247
+ const link = entry.target as HTMLAnchorElement;
248
+ const href = link.getAttribute('href');
249
+ if (href && href.startsWith('/')) {
250
+ this.prefetch(href);
251
+ }
252
+ }
253
+ }
254
+ },
255
+ { rootMargin: '100px' },
256
+ );
257
+
258
+ const links = container.querySelectorAll('a[href^="/"]');
259
+ links.forEach((link) => this.observer!.observe(link));
260
+
261
+ return () => {
262
+ this.observer?.disconnect();
263
+ this.observer = null;
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Predict next navigation destinations
269
+ */
270
+ predict(currentRoute: string): PredictedRoute[] {
271
+ const predictions: PredictedRoute[] = [];
272
+
273
+ // From navigation history
274
+ const fromHistory = this.navigationHistory.get(currentRoute);
275
+ if (fromHistory) {
276
+ const total = Array.from(fromHistory.values()).reduce((a, b) => a + b, 0);
277
+ for (const [route, count] of fromHistory) {
278
+ predictions.push({
279
+ route,
280
+ probability: count / total,
281
+ reason: 'history',
282
+ });
283
+ }
284
+ }
285
+
286
+ // Sort by probability
287
+ predictions.sort((a, b) => b.probability - a.probability);
288
+
289
+ return predictions;
290
+ }
291
+
292
+ /**
293
+ * Go back in navigation history
294
+ */
295
+ async back(): Promise<void> {
296
+ if (this.state.history.length <= 1) return;
297
+
298
+ this.state.history.pop();
299
+ const previousRoute = this.state.history[this.state.history.length - 1];
300
+
301
+ await this.navigate(previousRoute, { replace: true });
302
+ }
303
+
304
+ /**
305
+ * Get current navigation state
306
+ */
307
+ getState(): NavigationState {
308
+ return { ...this.state };
309
+ }
310
+
311
+ /**
312
+ * Subscribe to navigation changes
313
+ */
314
+ subscribe(listener: NavigationListener): () => void {
315
+ this.navigationListeners.add(listener);
316
+ return () => this.navigationListeners.delete(listener);
317
+ }
318
+
319
+ /**
320
+ * Subscribe to presence changes for a route
321
+ */
322
+ subscribePresence(listener: PresenceListener): () => void {
323
+ this.presenceListeners.add(listener);
324
+ return () => this.presenceListeners.delete(listener);
325
+ }
326
+
327
+ /**
328
+ * Preload all routes (total preload strategy)
329
+ */
330
+ async preloadAll(
331
+ onProgress?: (loaded: number, total: number) => void,
332
+ ): Promise<void> {
333
+ if (!this.sessionFetcher) {
334
+ throw new Error('sessionFetcher required for preloadAll');
335
+ }
336
+
337
+ const routes = this.router.getRoutes();
338
+ const manifest = routes.map((r) => ({
339
+ sessionId: this.router.match(r.pattern)?.sessionId ?? r.pattern,
340
+ route: r.pattern,
341
+ }));
342
+
343
+ await this.cache.preloadAll(manifest, this.sessionFetcher, { onProgress });
344
+ }
345
+
346
+ /**
347
+ * Get cache statistics
348
+ */
349
+ getCacheStats() {
350
+ return this.cache.getStats();
351
+ }
352
+
353
+ // Private methods
354
+
355
+ private async getSession(sessionId: string): Promise<CachedSession> {
356
+ const cached = this.cache.get(sessionId);
357
+ if (cached) return cached;
358
+
359
+ if (!this.sessionFetcher) {
360
+ throw new Error('Session not cached and no fetcher provided');
361
+ }
362
+
363
+ const session = await this.sessionFetcher(sessionId);
364
+ this.cache.set(session);
365
+ return session;
366
+ }
367
+
368
+ private updateDOM(session: CachedSession, match: RouteMatch): void {
369
+ // This is a placeholder - actual implementation would render the page
370
+ // In practice, this integrates with React/the rendering layer
371
+ if (typeof document !== 'undefined') {
372
+ // Dispatch custom event for React integration
373
+ const event = new CustomEvent('aeon:navigate', {
374
+ detail: { session, match },
375
+ });
376
+ document.dispatchEvent(event);
377
+ }
378
+ }
379
+
380
+ private recordNavigation(from: string, to: string): void {
381
+ if (!this.navigationHistory.has(from)) {
382
+ this.navigationHistory.set(from, new Map());
383
+ }
384
+ const fromMap = this.navigationHistory.get(from)!;
385
+ fromMap.set(to, (fromMap.get(to) ?? 0) + 1);
386
+ }
387
+
388
+ private notifyListeners(): void {
389
+ for (const listener of this.navigationListeners) {
390
+ listener(this.getState());
391
+ }
392
+ }
393
+
394
+ private notifyPresenceListeners(route: string, presence: PresenceInfo): void {
395
+ for (const listener of this.presenceListeners) {
396
+ listener(route, presence);
397
+ }
398
+ }
399
+ }
400
+
401
+ // Singleton instance
402
+ let globalNavigator: AeonNavigationEngine | null = null;
403
+
404
+ export function getNavigator(): AeonNavigationEngine {
405
+ if (!globalNavigator) {
406
+ globalNavigator = new AeonNavigationEngine();
407
+ }
408
+ return globalNavigator;
409
+ }
410
+
411
+ export function setNavigator(navigator: AeonNavigationEngine): void {
412
+ globalNavigator = navigator;
413
+ }
414
+
415
+ // Browser history integration
416
+ if (typeof window !== 'undefined') {
417
+ window.addEventListener('popstate', (event) => {
418
+ const navigator = getNavigator();
419
+ const route = event.state?.route ?? window.location.pathname;
420
+ navigator.navigate(route, { replace: true });
421
+ });
422
+ }
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Next.js Route Adapter
3
+ *
4
+ * Adapts Next.js App Router API routes to run on Cloudflare Workers.
5
+ * This enables "one is all" - Aeon serves both pages AND API routes.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { createAeonApp } from '@affectively/aeon-pages-runtime';
10
+ *
11
+ * // Automatically load all routes from Next.js app/api directory
12
+ * export default createAeonApp({
13
+ * // Pages from Aeon
14
+ * pagesDir: './pages',
15
+ *
16
+ * // API routes from Next.js (or local)
17
+ * apiRoutes: await loadNextjsRoutes('../web-app/src/app/api'),
18
+ * });
19
+ * ```
20
+ */
21
+
22
+ import type { AeonEnv, AeonContext, ExecutionContext } from './types';
23
+
24
+ // =============================================================================
25
+ // TYPES
26
+ // =============================================================================
27
+
28
+ /** Next.js style request with nextUrl */
29
+ export interface NextRequest extends Request {
30
+ nextUrl: URL;
31
+ cookies: {
32
+ get(name: string): { value: string } | undefined;
33
+ getAll(): Array<{ name: string; value: string }>;
34
+ };
35
+ headers: Headers;
36
+ geo?: { city?: string; country?: string; region?: string };
37
+ ip?: string;
38
+ }
39
+
40
+ /** Next.js style response */
41
+ export interface NextResponse extends Response {
42
+ cookies: {
43
+ set(
44
+ name: string,
45
+ value: string,
46
+ options?: { path?: string; maxAge?: number; httpOnly?: boolean },
47
+ ): void;
48
+ delete(name: string): void;
49
+ };
50
+ }
51
+
52
+ /** Next.js route handler signature */
53
+ export type NextRouteHandler = (
54
+ request: NextRequest,
55
+ context?: { params: Record<string, string | string[]> },
56
+ ) => Response | Promise<Response>;
57
+
58
+ /** Next.js route module */
59
+ export interface NextRouteModule {
60
+ GET?: NextRouteHandler;
61
+ POST?: NextRouteHandler;
62
+ PUT?: NextRouteHandler;
63
+ PATCH?: NextRouteHandler;
64
+ DELETE?: NextRouteHandler;
65
+ HEAD?: NextRouteHandler;
66
+ OPTIONS?: NextRouteHandler;
67
+ }
68
+
69
+ // =============================================================================
70
+ // REQUEST ADAPTER
71
+ // =============================================================================
72
+
73
+ /**
74
+ * Adapt a standard Request to Next.js style NextRequest
75
+ */
76
+ export function adaptRequest(
77
+ request: Request,
78
+ params: Record<string, string>,
79
+ ): NextRequest {
80
+ const url = new URL(request.url);
81
+
82
+ // Create cookies accessor
83
+ const cookieHeader = request.headers.get('Cookie') || '';
84
+ const cookies = parseCookies(cookieHeader);
85
+
86
+ const nextRequest = request as NextRequest;
87
+
88
+ // Add nextUrl
89
+ Object.defineProperty(nextRequest, 'nextUrl', {
90
+ value: url,
91
+ writable: false,
92
+ });
93
+
94
+ // Add cookies accessor
95
+ Object.defineProperty(nextRequest, 'cookies', {
96
+ value: {
97
+ get(name: string) {
98
+ const value = cookies[name];
99
+ return value ? { value } : undefined;
100
+ },
101
+ getAll() {
102
+ return Object.entries(cookies).map(([name, value]) => ({
103
+ name,
104
+ value,
105
+ }));
106
+ },
107
+ },
108
+ writable: false,
109
+ });
110
+
111
+ // Add Cloudflare-specific properties
112
+ const cfProps = (request as unknown as { cf?: Record<string, unknown> }).cf;
113
+ if (cfProps) {
114
+ Object.defineProperty(nextRequest, 'geo', {
115
+ value: {
116
+ city: cfProps.city as string,
117
+ country: cfProps.country as string,
118
+ region: cfProps.region as string,
119
+ },
120
+ writable: false,
121
+ });
122
+ Object.defineProperty(nextRequest, 'ip', {
123
+ value: request.headers.get('CF-Connecting-IP') || undefined,
124
+ writable: false,
125
+ });
126
+ }
127
+
128
+ return nextRequest;
129
+ }
130
+
131
+ function parseCookies(cookieHeader: string): Record<string, string> {
132
+ const cookies: Record<string, string> = {};
133
+ if (!cookieHeader) return cookies;
134
+
135
+ cookieHeader.split(';').forEach((cookie) => {
136
+ const [name, ...valueParts] = cookie.trim().split('=');
137
+ if (name) {
138
+ cookies[name] = valueParts.join('=');
139
+ }
140
+ });
141
+
142
+ return cookies;
143
+ }
144
+
145
+ // =============================================================================
146
+ // ROUTE HANDLER ADAPTER
147
+ // =============================================================================
148
+
149
+ /**
150
+ * Wrap a Next.js route handler to work with Aeon context
151
+ */
152
+ export function adaptHandler<E extends AeonEnv = AeonEnv>(
153
+ handler: NextRouteHandler,
154
+ ): (ctx: AeonContext<E>) => Promise<Response> {
155
+ return async (ctx: AeonContext<E>): Promise<Response> => {
156
+ const nextRequest = adaptRequest(ctx.request, ctx.params);
157
+
158
+ // Convert params to Next.js format (string | string[])
159
+ const nextParams: Record<string, string | string[]> = {};
160
+ for (const [key, value] of Object.entries(ctx.params)) {
161
+ // Handle catch-all routes
162
+ if (value.includes('/')) {
163
+ nextParams[key] = value.split('/');
164
+ } else {
165
+ nextParams[key] = value;
166
+ }
167
+ }
168
+
169
+ try {
170
+ const response = await handler(nextRequest, { params: nextParams });
171
+ return response;
172
+ } catch (error) {
173
+ console.error('Next.js route handler error:', error);
174
+ return new Response(
175
+ JSON.stringify({
176
+ error: 'Internal server error',
177
+ message: error instanceof Error ? error.message : 'Unknown error',
178
+ }),
179
+ { status: 500, headers: { 'Content-Type': 'application/json' } },
180
+ );
181
+ }
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Adapt an entire Next.js route module
187
+ */
188
+ export function adaptRouteModule<E extends AeonEnv = AeonEnv>(
189
+ module: NextRouteModule,
190
+ ): Record<string, (ctx: AeonContext<E>) => Promise<Response>> {
191
+ const adapted: Record<string, (ctx: AeonContext<E>) => Promise<Response>> =
192
+ {};
193
+
194
+ const methods: (keyof NextRouteModule)[] = [
195
+ 'GET',
196
+ 'POST',
197
+ 'PUT',
198
+ 'PATCH',
199
+ 'DELETE',
200
+ 'HEAD',
201
+ 'OPTIONS',
202
+ ];
203
+
204
+ for (const method of methods) {
205
+ const handler = module[method];
206
+ if (handler) {
207
+ adapted[method] = adaptHandler(handler);
208
+ }
209
+ }
210
+
211
+ return adapted;
212
+ }
213
+
214
+ // =============================================================================
215
+ // NEXT.JS RESPONSE HELPERS
216
+ // =============================================================================
217
+
218
+ /**
219
+ * Create a NextResponse.json() compatible response
220
+ */
221
+ export function json<T>(data: T, init?: ResponseInit): Response {
222
+ return new Response(JSON.stringify(data), {
223
+ ...init,
224
+ headers: {
225
+ 'Content-Type': 'application/json',
226
+ ...init?.headers,
227
+ },
228
+ });
229
+ }
230
+
231
+ /**
232
+ * Create a NextResponse.redirect() compatible response
233
+ */
234
+ export function redirect(
235
+ url: string | URL,
236
+ status: 301 | 302 | 303 | 307 | 308 = 307,
237
+ ): Response {
238
+ return new Response(null, {
239
+ status,
240
+ headers: { Location: url.toString() },
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Create a NextResponse.rewrite() compatible response
246
+ * Note: In Workers, this is just a redirect
247
+ */
248
+ export function rewrite(url: string | URL): Response {
249
+ return new Response(null, {
250
+ status: 307,
251
+ headers: { Location: url.toString() },
252
+ });
253
+ }
254
+
255
+ /**
256
+ * Create a NextResponse.next() compatible response
257
+ * Used in middleware to continue to the next handler
258
+ */
259
+ export function next(): Response {
260
+ return new Response(null, {
261
+ status: 200,
262
+ headers: { 'x-middleware-next': '1' },
263
+ });
264
+ }
265
+
266
+ // Export as NextResponse-like object
267
+ export const NextResponse = {
268
+ json,
269
+ redirect,
270
+ rewrite,
271
+ next,
272
+ };