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