@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,890 @@
1
+ /**
2
+ * aeon build - Build for production
3
+ *
4
+ * The key insight: pages come from D1, not files!
5
+ *
6
+ * Build process:
7
+ * 1. Parse JSX/TSX pages into AST
8
+ * 2. Serialize component trees to Aeon session format
9
+ * 3. Build CSS manifest (on-demand, tree-shaken)
10
+ * 4. Build asset manifest (inline SVG, images as data URIs)
11
+ * 5. Build font manifest (embedded fonts with @font-face)
12
+ * 6. Pre-render all pages with inline CSS, assets, fonts
13
+ * 7. Seed D1 database with routes, sessions, and pre-rendered HTML
14
+ * 8. Bundle WASM runtime (~20KB)
15
+ * 9. Generate Cloudflare Worker entry point
16
+ *
17
+ * Output: Everything needed for `wrangler deploy`
18
+ */
19
+
20
+ import {
21
+ readFile,
22
+ readdir,
23
+ writeFile,
24
+ mkdir,
25
+ copyFile,
26
+ stat,
27
+ } from 'fs/promises';
28
+ import { join, resolve, relative } from 'path';
29
+ // Import from build package (relative path for workspace development)
30
+ import {
31
+ // CSS Manifest
32
+ buildCSSManifest,
33
+ extractClassesFromTree,
34
+ generateCSSForClasses,
35
+ generateCriticalCSS,
36
+ type CSSManifest,
37
+ // Asset Manifest
38
+ buildAssetManifest,
39
+ resolveAssetsInTree,
40
+ type AssetManifest,
41
+ // Font Manifest
42
+ buildFontManifest,
43
+ getFontFaceCSS,
44
+ type FontManifest,
45
+ // Pre-render
46
+ prerenderPage,
47
+ prerenderAllPages,
48
+ generatePreRenderSeedSQL,
49
+ generatePreRenderMigrationSQL,
50
+ generateManifestSeedSQL,
51
+ type PreRenderOptions,
52
+ // Types
53
+ type PageSession,
54
+ type PreRenderedPage,
55
+ } from '../../../build/src/index';
56
+
57
+ interface BuildOptions {
58
+ config?: string;
59
+ }
60
+
61
+ interface AeonConfig {
62
+ pagesDir: string;
63
+ componentsDir?: string;
64
+ assetsDir?: string;
65
+ fontsDir?: string;
66
+ runtime: 'bun' | 'cloudflare';
67
+ output?: { dir?: string };
68
+ aeon?: {
69
+ sync?: { mode: string };
70
+ };
71
+ prerender?: {
72
+ enabled?: boolean;
73
+ hydration?: boolean;
74
+ fontFamilies?: string[];
75
+ };
76
+ env?: Record<string, string>;
77
+ }
78
+
79
+ interface ParsedPage {
80
+ route: string;
81
+ filePath: string;
82
+ isAeon: boolean;
83
+ componentTree: SerializedComponent;
84
+ layout?: string;
85
+ }
86
+
87
+ interface SerializedComponent {
88
+ type: string;
89
+ props?: Record<string, unknown>;
90
+ children?: (SerializedComponent | string)[];
91
+ }
92
+
93
+ export async function build(options: BuildOptions): Promise<void> {
94
+ const cwd = process.cwd();
95
+ const configPath = options.config || 'aeon.config.ts';
96
+ const startTime = Date.now();
97
+
98
+ console.log('\n🔨 Building Aeon Flux for production...\n');
99
+
100
+ // Load config
101
+ const config = await loadConfig(resolve(cwd, configPath));
102
+ const outputDir = resolve(cwd, config.output?.dir || '.aeon');
103
+ const pagesDir = resolve(cwd, config.pagesDir || './pages');
104
+ const assetsDir = config.assetsDir
105
+ ? resolve(cwd, config.assetsDir)
106
+ : resolve(cwd, './public');
107
+ const fontsDir = config.fontsDir
108
+ ? resolve(cwd, config.fontsDir)
109
+ : resolve(cwd, './fonts');
110
+ const prerenderEnabled = config.prerender?.enabled !== false;
111
+ const version = `${Date.now().toString(36)}`;
112
+
113
+ // Create output directories
114
+ await mkdir(join(outputDir, 'dist'), { recursive: true });
115
+ await mkdir(join(outputDir, 'migrations'), { recursive: true });
116
+ await mkdir(join(outputDir, 'manifests'), { recursive: true });
117
+
118
+ console.log('📁 Output: ' + relative(cwd, outputDir));
119
+ console.log('');
120
+
121
+ // Step 1: Scan and parse pages
122
+ console.log('1️⃣ Parsing pages...');
123
+ const pages = await parsePages(pagesDir);
124
+ console.log(` Found ${pages.length} page(s)`);
125
+
126
+ // Step 2: Build manifests (CSS, assets, fonts)
127
+ console.log('2️⃣ Building manifests...');
128
+
129
+ // Build CSS manifest
130
+ const allClasses = new Set<string>();
131
+ for (const page of pages) {
132
+ const classes = extractClassesFromTree(page.componentTree);
133
+ classes.forEach((c) => allClasses.add(c));
134
+ }
135
+ const cssManifest = buildCSSManifest(allClasses);
136
+ await writeFile(
137
+ join(outputDir, 'manifests', 'css.json'),
138
+ JSON.stringify(cssManifest, null, 2),
139
+ );
140
+ console.log(` ✓ CSS manifest (${allClasses.size} classes)`);
141
+
142
+ // Build asset manifest
143
+ let assetManifest: AssetManifest;
144
+ try {
145
+ assetManifest = await buildAssetManifest(assetsDir);
146
+ await writeFile(
147
+ join(outputDir, 'manifests', 'assets.json'),
148
+ JSON.stringify(assetManifest, null, 2),
149
+ );
150
+ console.log(
151
+ ` ✓ Asset manifest (${assetManifest.totalCount} assets, ${(assetManifest.totalSize / 1024).toFixed(1)}KB)`,
152
+ );
153
+ } catch {
154
+ assetManifest = {
155
+ version: '1.0.0',
156
+ generatedAt: new Date().toISOString(),
157
+ assets: {},
158
+ totalSize: 0,
159
+ totalCount: 0,
160
+ };
161
+ console.log(' ⚠️ No assets directory found, skipping');
162
+ }
163
+
164
+ // Build font manifest
165
+ let fontManifest: FontManifest;
166
+ try {
167
+ fontManifest = await buildFontManifest(fontsDir);
168
+ await writeFile(
169
+ join(outputDir, 'manifests', 'fonts.json'),
170
+ JSON.stringify(fontManifest, null, 2),
171
+ );
172
+ console.log(
173
+ ` ✓ Font manifest (${fontManifest.totalCount} fonts, ${(fontManifest.totalSize / 1024).toFixed(1)}KB)`,
174
+ );
175
+ } catch {
176
+ fontManifest = {
177
+ version: '1.0.0',
178
+ generatedAt: new Date().toISOString(),
179
+ fonts: {},
180
+ fontFaceCSS: '',
181
+ totalSize: 0,
182
+ totalCount: 0,
183
+ };
184
+ console.log(' ⚠️ No fonts directory found, skipping');
185
+ }
186
+
187
+ // Step 3: Generate route manifest
188
+ console.log('3️⃣ Generating route manifest...');
189
+ const manifest = generateManifest(pages);
190
+ await writeFile(
191
+ join(outputDir, 'manifest.json'),
192
+ JSON.stringify(manifest, null, 2),
193
+ );
194
+ console.log(' ✓ manifest.json');
195
+
196
+ // Step 4: Generate D1 migration
197
+ console.log('4️⃣ Generating D1 migration...');
198
+ const migration = generateMigration(pages);
199
+ const prerenderMigration = generatePreRenderMigrationSQL();
200
+ await writeFile(
201
+ join(outputDir, 'migrations', '0001_initial.sql'),
202
+ migration + '\n' + prerenderMigration,
203
+ );
204
+ console.log(' ✓ migrations/0001_initial.sql');
205
+
206
+ // Step 5: Pre-render pages (if enabled)
207
+ let preRenderedPages: PreRenderedPage[] = [];
208
+ if (prerenderEnabled && pages.length > 0) {
209
+ console.log('5️⃣ Pre-rendering pages...');
210
+ const sessions: PageSession[] = pages.map((page) => ({
211
+ route: page.route,
212
+ tree: page.componentTree,
213
+ data: {
214
+ title: extractTitle(page.componentTree) || 'Aeon Flux',
215
+ description: '',
216
+ },
217
+ schema: { version: '1.0.0' },
218
+ }));
219
+
220
+ const prerenderOptions: PreRenderOptions = {
221
+ cssManifest,
222
+ assetManifest,
223
+ fontManifest,
224
+ fontFamilies: config.prerender?.fontFamilies,
225
+ addHydrationScript: config.prerender?.hydration !== false,
226
+ env: config.env,
227
+ };
228
+
229
+ const result = await prerenderAllPages(sessions, prerenderOptions);
230
+ preRenderedPages = result.pages;
231
+
232
+ // Write pre-render seed SQL
233
+ const prerenderSeed = generatePreRenderSeedSQL(preRenderedPages, version);
234
+ await writeFile(join(outputDir, 'prerender-seed.sql'), prerenderSeed);
235
+ console.log(
236
+ ` ✓ prerender-seed.sql (${preRenderedPages.length} pages, ${(result.totalSize / 1024).toFixed(1)}KB total)`,
237
+ );
238
+ } else {
239
+ console.log('5️⃣ Pre-rendering skipped (disabled or no pages)');
240
+ }
241
+
242
+ // Step 6: Store manifests for D1 (for runtime re-rendering)
243
+ console.log('6️⃣ Generating manifest seed SQL...');
244
+ const manifestSeed = generateManifestSeedSQL(
245
+ cssManifest,
246
+ assetManifest,
247
+ fontManifest,
248
+ version,
249
+ );
250
+ await writeFile(join(outputDir, 'manifest-seed.sql'), manifestSeed);
251
+ console.log(' ✓ manifest-seed.sql (CSS, assets, fonts)');
252
+
253
+ // Step 7: Generate seed data
254
+ console.log('7️⃣ Generating D1 seed data...');
255
+ const seedData = generateSeedData(pages);
256
+ await writeFile(join(outputDir, 'seed.sql'), seedData);
257
+ console.log(' ✓ seed.sql');
258
+
259
+ // Step 8: Copy WASM runtime
260
+ console.log('8️⃣ Bundling WASM runtime...');
261
+ try {
262
+ // Try to find the WASM package
263
+ const wasmPkgPath = resolve(
264
+ cwd,
265
+ 'node_modules/@affectively/aeon-pages-runtime-wasm',
266
+ );
267
+ await copyFile(
268
+ join(wasmPkgPath, 'aeon_pages_runtime_bg.wasm'),
269
+ join(outputDir, 'dist', 'runtime.wasm'),
270
+ );
271
+ await copyFile(
272
+ join(wasmPkgPath, 'aeon_pages_runtime.js'),
273
+ join(outputDir, 'dist', 'runtime.js'),
274
+ );
275
+ console.log(' ✓ runtime.wasm (~20KB)');
276
+ } catch {
277
+ console.log(' ⚠️ WASM runtime not found, skipping');
278
+ }
279
+
280
+ // Step 9: Generate Cloudflare Worker
281
+ console.log('9️⃣ Generating Cloudflare Worker...');
282
+ const workerCode = generateWorker(pages, prerenderEnabled);
283
+ await writeFile(join(outputDir, 'dist', 'worker.js'), workerCode);
284
+ console.log(' ✓ worker.js');
285
+
286
+ // Step 10: Generate wrangler.toml
287
+ console.log('🔟 Generating wrangler config...');
288
+ const wranglerConfig = generateWranglerConfig();
289
+ await writeFile(join(outputDir, 'wrangler.toml'), wranglerConfig);
290
+ console.log(' ✓ wrangler.toml');
291
+
292
+ const elapsed = Date.now() - startTime;
293
+ console.log(`\n✨ Build complete in ${elapsed}ms\n`);
294
+
295
+ if (prerenderEnabled && preRenderedPages.length > 0) {
296
+ console.log('📊 Pre-render stats:');
297
+ console.log(` Pages: ${preRenderedPages.length}`);
298
+ console.log(
299
+ ` Total size: ${(preRenderedPages.reduce((a, p) => a + p.size, 0) / 1024).toFixed(1)}KB`,
300
+ );
301
+ console.log(
302
+ ` Avg size: ${(preRenderedPages.reduce((a, p) => a + p.size, 0) / preRenderedPages.length / 1024).toFixed(1)}KB per page`,
303
+ );
304
+ console.log('');
305
+ }
306
+
307
+ console.log('Next steps:');
308
+ console.log(' 1. Create D1 database:');
309
+ console.log(' wrangler d1 create aeon-flux');
310
+ console.log('');
311
+ console.log(' 2. Create KV namespace (edge cache):');
312
+ console.log(' wrangler kv:namespace create PAGES_CACHE');
313
+ console.log('');
314
+ console.log(' 3. Run migration:');
315
+ console.log(
316
+ ` wrangler d1 execute aeon-flux --file=${relative(cwd, join(outputDir, 'migrations/0001_initial.sql'))}`,
317
+ );
318
+ console.log('');
319
+ console.log(' 4. Seed data:');
320
+ console.log(
321
+ ` wrangler d1 execute aeon-flux --file=${relative(cwd, join(outputDir, 'seed.sql'))}`,
322
+ );
323
+ console.log(
324
+ ` wrangler d1 execute aeon-flux --file=${relative(cwd, join(outputDir, 'manifest-seed.sql'))}`,
325
+ );
326
+ if (prerenderEnabled && preRenderedPages.length > 0) {
327
+ console.log(
328
+ ` wrangler d1 execute aeon-flux --file=${relative(cwd, join(outputDir, 'prerender-seed.sql'))}`,
329
+ );
330
+ }
331
+ console.log('');
332
+ console.log(' 5. Update wrangler.toml with your IDs:');
333
+ console.log(' - database_id: from step 1 output');
334
+ console.log(' - kv namespace id: from step 2 output');
335
+ console.log('');
336
+ console.log(' 6. Deploy:');
337
+ console.log(` cd ${relative(cwd, outputDir)} && wrangler deploy`);
338
+ console.log('');
339
+ }
340
+
341
+ // Extract title from component tree
342
+ function extractTitle(tree: SerializedComponent): string | undefined {
343
+ if (tree.type === 'title' && tree.children?.[0]) {
344
+ return String(tree.children[0]);
345
+ }
346
+ if (tree.type === 'h1' && tree.children?.[0]) {
347
+ return String(tree.children[0]);
348
+ }
349
+ if (tree.children) {
350
+ for (const child of tree.children) {
351
+ if (typeof child !== 'string') {
352
+ const title = extractTitle(child);
353
+ if (title) return title;
354
+ }
355
+ }
356
+ }
357
+ return undefined;
358
+ }
359
+
360
+ async function loadConfig(configPath: string): Promise<AeonConfig> {
361
+ try {
362
+ const module = await import(configPath);
363
+ return module.default || module;
364
+ } catch {
365
+ return {
366
+ pagesDir: './pages',
367
+ runtime: 'cloudflare',
368
+ };
369
+ }
370
+ }
371
+
372
+ async function parsePages(pagesDir: string): Promise<ParsedPage[]> {
373
+ const pages: ParsedPage[] = [];
374
+
375
+ async function scan(dir: string, routePath: string): Promise<void> {
376
+ let entries;
377
+ try {
378
+ entries = await readdir(dir, { withFileTypes: true });
379
+ } catch {
380
+ return;
381
+ }
382
+
383
+ for (const entry of entries) {
384
+ const fullPath = join(dir, entry.name);
385
+
386
+ if (entry.isDirectory()) {
387
+ let segment = entry.name;
388
+ if (segment.startsWith('[') && segment.endsWith(']')) {
389
+ if (segment.startsWith('[...')) {
390
+ segment = '[...' + segment.slice(4, -1) + ']';
391
+ } else if (segment.startsWith('[[...')) {
392
+ segment = '[[...' + segment.slice(5, -2) + ']]';
393
+ }
394
+ }
395
+ await scan(fullPath, `${routePath}/${segment}`);
396
+ } else if (
397
+ entry.isFile() &&
398
+ (entry.name === 'page.tsx' ||
399
+ entry.name === 'page.ts' ||
400
+ entry.name === 'page.jsx')
401
+ ) {
402
+ const content = await readFile(fullPath, 'utf-8');
403
+ const isAeon =
404
+ content.includes("'use aeon'") || content.includes('"use aeon"');
405
+
406
+ // Parse JSX to component tree (simplified - real impl would use proper parser)
407
+ const componentTree = parseJSXToTree(content);
408
+
409
+ // Check for layout
410
+ const layoutPath = join(dir, 'layout.tsx');
411
+ let layout: string | undefined;
412
+ try {
413
+ await stat(layoutPath);
414
+ layout = routePath || '/';
415
+ } catch {
416
+ // No layout
417
+ }
418
+
419
+ pages.push({
420
+ route: routePath || '/',
421
+ filePath: fullPath,
422
+ isAeon,
423
+ componentTree,
424
+ layout,
425
+ });
426
+ }
427
+ }
428
+ }
429
+
430
+ await scan(pagesDir, '');
431
+ return pages;
432
+ }
433
+
434
+ function parseJSXToTree(content: string): SerializedComponent {
435
+ // Simplified JSX parser - extracts component structure
436
+ // Real implementation would use @babel/parser or swc
437
+
438
+ // Find the return statement in the component
439
+ const returnMatch = content.match(/return\s*\(\s*([\s\S]*?)\s*\);?\s*\}/);
440
+
441
+ if (!returnMatch) {
442
+ return { type: 'div', children: ['Page content'] };
443
+ }
444
+
445
+ const jsx = returnMatch[1];
446
+
447
+ // Very basic extraction - just get the root element type
448
+ const rootMatch = jsx.match(/<(\w+)/);
449
+ const rootType = rootMatch?.[1] || 'div';
450
+
451
+ // Extract text content (simplified)
452
+ const textContent = jsx
453
+ .replace(/<[^>]+>/g, '')
454
+ .replace(/\{[^}]+\}/g, '')
455
+ .trim()
456
+ .slice(0, 100);
457
+
458
+ return {
459
+ type: rootType,
460
+ props: { className: 'aeon-page' },
461
+ children: textContent ? [textContent] : ['Page content'],
462
+ };
463
+ }
464
+
465
+ interface RouteManifest {
466
+ version: string;
467
+ routes: Array<{
468
+ pattern: string;
469
+ sessionId: string;
470
+ componentId: string;
471
+ isAeon: boolean;
472
+ layout?: string;
473
+ }>;
474
+ }
475
+
476
+ function generateManifest(pages: ParsedPage[]): RouteManifest {
477
+ return {
478
+ version: '1.0.0',
479
+ routes: pages.map((page) => ({
480
+ pattern: page.route,
481
+ sessionId: routeToSessionId(page.route),
482
+ componentId: routeToSessionId(page.route),
483
+ isAeon: page.isAeon,
484
+ layout: page.layout,
485
+ })),
486
+ };
487
+ }
488
+
489
+ function generateMigration(_pages: ParsedPage[]): string {
490
+ return `-- Aeon Flux D1 Migration
491
+ -- Generated: ${new Date().toISOString()}
492
+
493
+ -- Routes table
494
+ CREATE TABLE IF NOT EXISTS routes (
495
+ path TEXT PRIMARY KEY,
496
+ pattern TEXT NOT NULL,
497
+ session_id TEXT NOT NULL,
498
+ component_id TEXT NOT NULL,
499
+ layout TEXT,
500
+ is_aeon INTEGER DEFAULT 1,
501
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
502
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
503
+ );
504
+
505
+ -- Sessions table (page content)
506
+ CREATE TABLE IF NOT EXISTS sessions (
507
+ session_id TEXT PRIMARY KEY,
508
+ route TEXT NOT NULL,
509
+ tree TEXT NOT NULL,
510
+ data TEXT DEFAULT '{}',
511
+ schema_version TEXT DEFAULT '1.0.0',
512
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
513
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
514
+ );
515
+
516
+ -- Presence tracking
517
+ CREATE TABLE IF NOT EXISTS presence (
518
+ session_id TEXT,
519
+ user_id TEXT,
520
+ role TEXT DEFAULT 'user',
521
+ cursor_x INTEGER,
522
+ cursor_y INTEGER,
523
+ editing TEXT,
524
+ status TEXT DEFAULT 'online',
525
+ last_activity TEXT DEFAULT CURRENT_TIMESTAMP,
526
+ PRIMARY KEY (session_id, user_id)
527
+ );
528
+
529
+ -- Indexes
530
+ CREATE INDEX IF NOT EXISTS idx_routes_pattern ON routes(pattern);
531
+ CREATE INDEX IF NOT EXISTS idx_sessions_route ON sessions(route);
532
+ CREATE INDEX IF NOT EXISTS idx_presence_session ON presence(session_id);
533
+ `;
534
+ }
535
+
536
+ function generateSeedData(pages: ParsedPage[]): string {
537
+ const lines: string[] = [
538
+ '-- Aeon Flux Seed Data',
539
+ `-- Generated: ${new Date().toISOString()}`,
540
+ '',
541
+ '-- Routes',
542
+ ];
543
+
544
+ for (const page of pages) {
545
+ const sessionId = routeToSessionId(page.route);
546
+ const escapedPattern = page.route.replace(/'/g, "''");
547
+ const escapedLayout = page.layout
548
+ ? `'${page.layout.replace(/'/g, "''")}'`
549
+ : 'NULL';
550
+
551
+ lines.push(
552
+ `INSERT OR REPLACE INTO routes (path, pattern, session_id, component_id, layout, is_aeon) VALUES ('${escapedPattern}', '${escapedPattern}', '${sessionId}', '${sessionId}', ${escapedLayout}, ${page.isAeon ? 1 : 0});`,
553
+ );
554
+ }
555
+
556
+ lines.push('');
557
+ lines.push('-- Sessions');
558
+
559
+ for (const page of pages) {
560
+ const sessionId = routeToSessionId(page.route);
561
+ const escapedRoute = page.route.replace(/'/g, "''");
562
+ const tree = JSON.stringify(page.componentTree).replace(/'/g, "''");
563
+
564
+ lines.push(
565
+ `INSERT OR REPLACE INTO sessions (session_id, route, tree, data, schema_version) VALUES ('${sessionId}', '${escapedRoute}', '${tree}', '{}', '1.0.0');`,
566
+ );
567
+ }
568
+
569
+ return lines.join('\n');
570
+ }
571
+
572
+ function generateWorker(
573
+ pages: ParsedPage[],
574
+ prerenderEnabled: boolean = true,
575
+ ): string {
576
+ const routes = pages.map((p) => ({
577
+ pattern: p.route,
578
+ sessionId: routeToSessionId(p.route),
579
+ isAeon: p.isAeon,
580
+ }));
581
+
582
+ return `/**
583
+ * Aeon Flux Cloudflare Worker
584
+ * Generated: ${new Date().toISOString()}
585
+ *
586
+ * Zero-dependency rendering with multi-layer caching:
587
+ * 1. First, try KV cache (edge, ~1ms)
588
+ * 2. Then, try D1 pre-rendered pages (~5ms)
589
+ * 3. Fallback to session-based rendering (~50ms)
590
+ *
591
+ * Pre-rendered pages include:
592
+ * - Inline CSS (tree-shaken, only what's needed)
593
+ * - Inline assets (SVG, images as data URIs)
594
+ * - Inline fonts (@font-face with data URIs)
595
+ * - Minimal hydration script for interactivity
596
+ */
597
+
598
+ // Route manifest (baked in at build time)
599
+ const ROUTES = ${JSON.stringify(routes, null, 2)};
600
+ const PRERENDER_ENABLED = ${prerenderEnabled};
601
+ const CACHE_TTL = 3600; // 1 hour in KV
602
+ const BUILD_VERSION = '${Date.now().toString(36)}'; // For cache invalidation
603
+
604
+ export default {
605
+ async fetch(request, env, ctx) {
606
+ const url = new URL(request.url);
607
+
608
+ // WebSocket upgrade for real-time collaboration
609
+ if (request.headers.get('Upgrade') === 'websocket') {
610
+ return handleWebSocket(request, env, url);
611
+ }
612
+
613
+ // Match route
614
+ const match = matchRoute(url.pathname, ROUTES);
615
+ if (!match) {
616
+ return new Response('Not Found', { status: 404 });
617
+ }
618
+
619
+ const cacheKey = \`page:\${match.pattern}\`;
620
+
621
+ // Layer 1: Try KV cache first (edge, fastest ~1ms)
622
+ // Only use cache if version matches current build (auto-invalidation on deploy)
623
+ if (env.PAGES_CACHE) {
624
+ const cached = await getFromKV(env.PAGES_CACHE, cacheKey);
625
+ if (cached && cached.version === BUILD_VERSION) {
626
+ return new Response(cached.html, {
627
+ headers: {
628
+ 'Content-Type': 'text/html; charset=utf-8',
629
+ 'X-Aeon-Cache': 'HIT-KV',
630
+ 'X-Aeon-Version': cached.version,
631
+ 'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
632
+ },
633
+ });
634
+ }
635
+ // Stale cache from old version - will be replaced with fresh content
636
+ }
637
+
638
+ // Layer 2: Try D1 pre-rendered page (~5ms)
639
+ if (PRERENDER_ENABLED) {
640
+ const preRendered = await getPreRenderedPage(env.DB, match.pattern);
641
+ if (preRendered) {
642
+ // Cache in KV for next request
643
+ if (env.PAGES_CACHE) {
644
+ ctx.waitUntil(
645
+ env.PAGES_CACHE.put(cacheKey, JSON.stringify(preRendered), {
646
+ expirationTtl: CACHE_TTL
647
+ })
648
+ );
649
+ }
650
+ return new Response(preRendered.html, {
651
+ headers: {
652
+ 'Content-Type': 'text/html; charset=utf-8',
653
+ 'X-Aeon-Cache': 'HIT-D1',
654
+ 'X-Aeon-Version': preRendered.version,
655
+ 'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
656
+ },
657
+ });
658
+ }
659
+ }
660
+
661
+ // Layer 3: Fallback to session-based rendering (~50ms)
662
+ const session = await getSession(env.DB, match.sessionId);
663
+ if (!session) {
664
+ return new Response('Session not found', { status: 404 });
665
+ }
666
+
667
+ // Render page
668
+ const html = renderPage(session, match);
669
+
670
+ // Cache the rendered page in KV for next request
671
+ if (env.PAGES_CACHE) {
672
+ const cacheData = { html, version: BUILD_VERSION };
673
+ ctx.waitUntil(
674
+ env.PAGES_CACHE.put(cacheKey, JSON.stringify(cacheData), {
675
+ expirationTtl: CACHE_TTL
676
+ })
677
+ );
678
+ }
679
+
680
+ return new Response(html, {
681
+ headers: {
682
+ 'Content-Type': 'text/html; charset=utf-8',
683
+ 'X-Aeon-Cache': 'MISS',
684
+ },
685
+ });
686
+ },
687
+ };
688
+
689
+ // Get page from KV cache
690
+ async function getFromKV(kv, key) {
691
+ try {
692
+ const value = await kv.get(key);
693
+ if (value) {
694
+ return JSON.parse(value);
695
+ }
696
+ } catch {
697
+ // Cache miss or parse error
698
+ }
699
+ return null;
700
+ }
701
+
702
+ function matchRoute(path, routes) {
703
+ const pathParts = path.split('/').filter(Boolean);
704
+
705
+ for (const route of routes) {
706
+ const params = matchPattern(pathParts, route.pattern);
707
+ if (params !== null) {
708
+ return { ...route, params };
709
+ }
710
+ }
711
+ return null;
712
+ }
713
+
714
+ function matchPattern(pathParts, pattern) {
715
+ const patternParts = pattern.split('/').filter(Boolean);
716
+ const params = {};
717
+
718
+ let pi = 0, pati = 0;
719
+ while (pi < pathParts.length && pati < patternParts.length) {
720
+ const pathPart = pathParts[pi];
721
+ const patternPart = patternParts[pati];
722
+
723
+ if (patternPart.startsWith('[[...') && patternPart.endsWith(']]')) {
724
+ // Optional catch-all
725
+ const name = patternPart.slice(5, -2);
726
+ params[name] = pathParts.slice(pi).join('/');
727
+ return params;
728
+ } else if (patternPart.startsWith('[...') && patternPart.endsWith(']')) {
729
+ // Catch-all
730
+ const name = patternPart.slice(4, -1);
731
+ params[name] = pathParts.slice(pi).join('/');
732
+ return params;
733
+ } else if (patternPart.startsWith('[') && patternPart.endsWith(']')) {
734
+ // Dynamic
735
+ const name = patternPart.slice(1, -1);
736
+ params[name] = pathPart;
737
+ pi++;
738
+ pati++;
739
+ } else if (pathPart === patternPart) {
740
+ pi++;
741
+ pati++;
742
+ } else {
743
+ return null;
744
+ }
745
+ }
746
+
747
+ if (pati < patternParts.length) {
748
+ const last = patternParts[pati];
749
+ if (last.startsWith('[[...') && last.endsWith(']]')) {
750
+ params[last.slice(5, -2)] = '';
751
+ return params;
752
+ }
753
+ }
754
+
755
+ return pi === pathParts.length && pati === patternParts.length ? params : null;
756
+ }
757
+
758
+ // Get pre-rendered page from D1 (zero-rendering path)
759
+ async function getPreRenderedPage(db, route) {
760
+ try {
761
+ const result = await db
762
+ .prepare('SELECT html, version FROM rendered_pages WHERE route = ?')
763
+ .bind(route)
764
+ .first();
765
+
766
+ return result ? { html: result.html, version: result.version } : null;
767
+ } catch {
768
+ return null;
769
+ }
770
+ }
771
+
772
+ async function getSession(db, sessionId) {
773
+ const result = await db
774
+ .prepare('SELECT * FROM sessions WHERE session_id = ?')
775
+ .bind(sessionId)
776
+ .first();
777
+
778
+ if (!result) return null;
779
+
780
+ return {
781
+ route: result.route,
782
+ tree: JSON.parse(result.tree),
783
+ data: JSON.parse(result.data || '{}'),
784
+ schema: { version: result.schema_version },
785
+ };
786
+ }
787
+
788
+ function renderPage(session, match) {
789
+ const tree = session.tree;
790
+ const html = renderTree(tree);
791
+
792
+ return \`<!DOCTYPE html>
793
+ <html lang="en">
794
+ <head>
795
+ <meta charset="UTF-8">
796
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
797
+ <title>Aeon Flux</title>
798
+ <link href="https://cdn.jsdelivr.net/npm/tailwindcss@3/dist/tailwind.min.css" rel="stylesheet">
799
+ \${match.isAeon ? '<script src="/__aeon_flux.js" defer></script>' : ''}
800
+ </head>
801
+ <body>
802
+ <div id="root">\${html}</div>
803
+ \${match.isAeon ? \`
804
+ <script>
805
+ window.__AEON_FLUX__ = {
806
+ route: "\${match.pattern}",
807
+ sessionId: "\${match.sessionId}",
808
+ params: \${JSON.stringify(match.params)},
809
+ };
810
+ </script>
811
+ \` : ''}
812
+ </body>
813
+ </html>\`;
814
+ }
815
+
816
+ function renderTree(node) {
817
+ if (typeof node === 'string') return escapeHtml(node);
818
+ if (!node) return '';
819
+
820
+ const { type, props = {}, children = [] } = node;
821
+ const attrs = Object.entries(props)
822
+ .map(([k, v]) => {
823
+ if (k === 'className') return \`class="\${v}"\`;
824
+ if (typeof v === 'boolean') return v ? k : '';
825
+ return \`\${k}="\${escapeHtml(String(v))}"\`;
826
+ })
827
+ .filter(Boolean)
828
+ .join(' ');
829
+
830
+ const childHtml = children.map(renderTree).join('');
831
+ return \`<\${type}\${attrs ? ' ' + attrs : ''}>\${childHtml}</\${type}>\`;
832
+ }
833
+
834
+ function escapeHtml(s) {
835
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
836
+ }
837
+
838
+ async function handleWebSocket(request, env, url) {
839
+ const sessionId = url.searchParams.get('session') || 'index';
840
+
841
+ // Get Durable Object for this session
842
+ const id = env.AEON_SESSIONS.idFromName(sessionId);
843
+ const stub = env.AEON_SESSIONS.get(id);
844
+
845
+ return stub.fetch(request);
846
+ }
847
+
848
+ // Export Durable Object classes for wrangler
849
+ export { AeonPageSession } from './durable-object.js';
850
+ `;
851
+ }
852
+
853
+ function generateWranglerConfig(): string {
854
+ return `# Aeon Flux Cloudflare Worker Configuration
855
+ # Generated: ${new Date().toISOString()}
856
+
857
+ name = "aeon-flux"
858
+ main = "dist/worker.js"
859
+ compatibility_date = "2024-01-01"
860
+
861
+ # D1 Database - pages come from here!
862
+ [[d1_databases]]
863
+ binding = "DB"
864
+ database_name = "aeon-flux"
865
+ database_id = "YOUR_DATABASE_ID" # Replace after \`wrangler d1 create\`
866
+
867
+ # KV Namespace - edge page cache (~1ms)
868
+ [[kv_namespaces]]
869
+ binding = "PAGES_CACHE"
870
+ id = "YOUR_KV_ID" # Replace after \`wrangler kv:namespace create PAGES_CACHE\`
871
+
872
+ # Durable Objects - real-time collaboration
873
+ [durable_objects]
874
+ bindings = [
875
+ { name = "AEON_SESSIONS", class_name = "AeonPageSession" }
876
+ ]
877
+
878
+ [[migrations]]
879
+ tag = "v1"
880
+ new_classes = ["AeonPageSession"]
881
+
882
+ # Optional: Assets
883
+ # [site]
884
+ # bucket = "./public"
885
+ `;
886
+ }
887
+
888
+ function routeToSessionId(route: string): string {
889
+ return route.replace(/^\/|\/$/g, '').replace(/\//g, '-') || 'index';
890
+ }