@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.
- package/CHANGELOG.md +112 -0
- package/README.md +625 -0
- package/examples/basic/aeon.config.ts +39 -0
- package/examples/basic/components/Cursor.tsx +86 -0
- package/examples/basic/components/OfflineIndicator.tsx +103 -0
- package/examples/basic/components/PresenceBar.tsx +77 -0
- package/examples/basic/package.json +20 -0
- package/examples/basic/pages/index.tsx +80 -0
- package/package.json +101 -0
- package/packages/analytics/README.md +309 -0
- package/packages/analytics/build.ts +35 -0
- package/packages/analytics/package.json +50 -0
- package/packages/analytics/src/click-tracker.ts +368 -0
- package/packages/analytics/src/context-bridge.ts +319 -0
- package/packages/analytics/src/data-layer.ts +302 -0
- package/packages/analytics/src/gtm-loader.ts +239 -0
- package/packages/analytics/src/index.ts +230 -0
- package/packages/analytics/src/merkle-tree.ts +489 -0
- package/packages/analytics/src/provider.tsx +300 -0
- package/packages/analytics/src/types.ts +320 -0
- package/packages/analytics/src/use-analytics.ts +296 -0
- package/packages/analytics/tsconfig.json +19 -0
- package/packages/benchmarks/src/benchmark.test.ts +691 -0
- package/packages/cli/dist/index.js +61899 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/commands/build.test.ts +682 -0
- package/packages/cli/src/commands/build.ts +890 -0
- package/packages/cli/src/commands/dev.ts +473 -0
- package/packages/cli/src/commands/init.ts +409 -0
- package/packages/cli/src/commands/start.ts +297 -0
- package/packages/cli/src/index.ts +105 -0
- package/packages/directives/src/use-aeon.ts +272 -0
- package/packages/mcp-server/package.json +51 -0
- package/packages/mcp-server/src/index.ts +178 -0
- package/packages/mcp-server/src/resources.ts +346 -0
- package/packages/mcp-server/src/tools/index.ts +36 -0
- package/packages/mcp-server/src/tools/navigation.ts +545 -0
- package/packages/mcp-server/tsconfig.json +21 -0
- package/packages/react/package.json +40 -0
- package/packages/react/src/Link.tsx +388 -0
- package/packages/react/src/components/InstallPrompt.tsx +286 -0
- package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
- package/packages/react/src/components/PushNotifications.tsx +453 -0
- package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
- package/packages/react/src/hooks/useConflicts.ts +277 -0
- package/packages/react/src/hooks/useNetworkState.ts +209 -0
- package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
- package/packages/react/src/hooks/useServiceWorker.ts +278 -0
- package/packages/react/src/hooks.ts +195 -0
- package/packages/react/src/index.ts +151 -0
- package/packages/react/src/provider.tsx +467 -0
- package/packages/react/tsconfig.json +19 -0
- package/packages/runtime/README.md +399 -0
- package/packages/runtime/build.ts +48 -0
- package/packages/runtime/package.json +71 -0
- package/packages/runtime/schema.sql +40 -0
- package/packages/runtime/src/api-routes.ts +465 -0
- package/packages/runtime/src/benchmark.ts +171 -0
- package/packages/runtime/src/cache.ts +479 -0
- package/packages/runtime/src/durable-object.ts +1341 -0
- package/packages/runtime/src/index.ts +360 -0
- package/packages/runtime/src/navigation.test.ts +421 -0
- package/packages/runtime/src/navigation.ts +422 -0
- package/packages/runtime/src/nextjs-adapter.ts +272 -0
- package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
- package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
- package/packages/runtime/src/offline/encryption.test.ts +412 -0
- package/packages/runtime/src/offline/encryption.ts +397 -0
- package/packages/runtime/src/offline/types.ts +465 -0
- package/packages/runtime/src/predictor.ts +371 -0
- package/packages/runtime/src/registry.ts +351 -0
- package/packages/runtime/src/router/context-extractor.ts +661 -0
- package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
- package/packages/runtime/src/router/esi-control.ts +541 -0
- package/packages/runtime/src/router/esi-cyrano.ts +779 -0
- package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
- package/packages/runtime/src/router/esi-react.tsx +1065 -0
- package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
- package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
- package/packages/runtime/src/router/esi-translate.ts +503 -0
- package/packages/runtime/src/router/esi.ts +666 -0
- package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
- package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
- package/packages/runtime/src/router/index.ts +298 -0
- package/packages/runtime/src/router/merkle-capability.ts +473 -0
- package/packages/runtime/src/router/speculation.ts +451 -0
- package/packages/runtime/src/router/types.ts +630 -0
- package/packages/runtime/src/router.test.ts +470 -0
- package/packages/runtime/src/router.ts +302 -0
- package/packages/runtime/src/server.ts +481 -0
- package/packages/runtime/src/service-worker-push.ts +319 -0
- package/packages/runtime/src/service-worker.ts +553 -0
- package/packages/runtime/src/skeleton-hydrate.ts +237 -0
- package/packages/runtime/src/speculation.test.ts +389 -0
- package/packages/runtime/src/speculation.ts +486 -0
- package/packages/runtime/src/storage.test.ts +1297 -0
- package/packages/runtime/src/storage.ts +1048 -0
- package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
- package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
- package/packages/runtime/src/sync/coordinator.test.ts +608 -0
- package/packages/runtime/src/sync/coordinator.ts +596 -0
- package/packages/runtime/src/tree-compiler.ts +295 -0
- package/packages/runtime/src/types.ts +728 -0
- package/packages/runtime/src/worker.ts +327 -0
- package/packages/runtime/tsconfig.json +20 -0
- package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
- package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
- package/packages/runtime/wasm/package.json +21 -0
- package/packages/runtime/wrangler.toml +41 -0
- package/packages/runtime-wasm/Cargo.lock +436 -0
- package/packages/runtime-wasm/Cargo.toml +29 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
- package/packages/runtime-wasm/pkg/package.json +21 -0
- package/packages/runtime-wasm/src/hydrate.rs +352 -0
- package/packages/runtime-wasm/src/lib.rs +191 -0
- package/packages/runtime-wasm/src/render.rs +629 -0
- package/packages/runtime-wasm/src/router.rs +298 -0
- package/packages/runtime-wasm/src/skeleton.rs +430 -0
- 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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
|
+
}
|