@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,1446 @@
1
+ # RFC-001: Zero-Dependency Page Rendering
2
+
3
+ **Status**: In Progress
4
+ **Author**: AFFECTIVELY Engineering
5
+ **Created**: 2026-02-06
6
+ **Target**: aeon-pages v2.0.0
7
+
8
+ ---
9
+
10
+ ## Summary
11
+
12
+ Implement a rendering pipeline that produces **completely self-contained HTML documents** requiring zero external requests. All CSS, images, fonts, and critical JS are inlined as data URIs or embedded directly. The WASM-based AST parser walks the DOM faster than browsers, computing styles and resolving assets at render time.
13
+
14
+ ## Motivation
15
+
16
+ ### Current Performance Profile
17
+
18
+ ```
19
+ Page Load Timeline (Current):
20
+ ├── HTML Document 50ms
21
+ ├── → /styles.css (105KB) 150ms [blocking]
22
+ ├── → /client.css (46KB) 100ms [blocking]
23
+ ├── → /fonts/inter.woff2 (50KB) 120ms
24
+ ├── → /images/logo.svg (5KB) 80ms
25
+ ├── → /client.js (200KB) 180ms
26
+ └── → First Paint ~500ms
27
+ → Interactive ~1200ms
28
+ ```
29
+
30
+ **Problems:**
31
+ 1. Multiple round-trips to origin/CDN
32
+ 2. CSS blocks first paint
33
+ 3. Font loading causes layout shift
34
+ 4. JS bundle too large for edge delivery
35
+ 5. Cache invalidation cascades
36
+
37
+ ### Target Performance Profile
38
+
39
+ ```
40
+ Page Load Timeline (Target):
41
+ ├── HTML Document (all-in-one) 80ms
42
+ └── → First Paint <100ms
43
+ → Interactive <200ms
44
+ ```
45
+
46
+ **Single request. Zero dependencies. Instant render.**
47
+
48
+ ---
49
+
50
+ ## Design
51
+
52
+ ### The WASM Render Pipeline
53
+
54
+ ```
55
+ ┌─────────────────────────────────────────────────────────────────────────────┐
56
+ │ BUILD TIME (Once per deploy) │
57
+ ├─────────────────────────────────────────────────────────────────────────────┤
58
+ │ │
59
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────────┐ │
60
+ │ │ Components │───▶│ AST Parser │───▶│ CSS Manifest │ │
61
+ │ │ (TSX files) │ │ (SWC) │ │ (class → rules mapping) │ │
62
+ │ └──────────────┘ └──────────────┘ └──────────────────────────────┘ │
63
+ │ │
64
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────────┐ │
65
+ │ │ Assets │───▶│ Optimizer │───▶│ Asset Manifest │ │
66
+ │ │ (SVG/PNG/...)│ │ (SVGO/Sharp) │ │ (path → data URI mapping) │ │
67
+ │ └──────────────┘ └──────────────┘ └──────────────────────────────┘ │
68
+ │ │
69
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────────┐ │
70
+ │ │ Fonts │───▶│ Subsetter │───▶│ Font Manifest │ │
71
+ │ │ (WOFF2) │ │ (fonttools) │ │ (family → base64 data URI) │ │
72
+ │ └──────────────┘ └──────────────┘ └──────────────────────────────┘ │
73
+ │ │
74
+ │ ▼ │
75
+ │ ┌────────────────────────┐ │
76
+ │ │ Unified Render Manifest │ ──▶ D1/KV │
77
+ │ └────────────────────────┘ │
78
+ │ │
79
+ └─────────────────────────────────────────────────────────────────────────────┘
80
+
81
+ ┌─────────────────────────────────────────────────────────────────────────────┐
82
+ │ RENDER TIME (Per request) │
83
+ ├─────────────────────────────────────────────────────────────────────────────┤
84
+ │ │
85
+ │ ┌──────────────┐ ┌──────────────────────────────────────────────────┐ │
86
+ │ │ Page Session │───▶│ WASM Renderer │ │
87
+ │ │ (from D1) │ │ │ │
88
+ │ └──────────────┘ │ 1. Walk component tree (parallel) │ │
89
+ │ │ 2. Collect CSS classes → lookup in manifest │ │
90
+ │ ┌──────────────┐ │ 3. Resolve asset refs → inline data URIs │ │
91
+ │ │ Render │───▶│ 4. Apply inline styles to nodes │ │
92
+ │ │ Manifest │ │ 5. Embed fonts as @font-face data URIs │ │
93
+ │ └──────────────┘ │ 6. Generate minimal hydration script │ │
94
+ │ │ │ │
95
+ │ └────────────────────┬─────────────────────────────┘ │
96
+ │ │ │
97
+ │ ▼ │
98
+ │ ┌────────────────────────────────────────────────┐ │
99
+ │ │ Self-Contained HTML Document │ │
100
+ │ │ │ │
101
+ │ │ <!DOCTYPE html> │ │
102
+ │ │ <html> │ │
103
+ │ │ <head> │ │
104
+ │ │ <style>/* All CSS inline */</style> │ │
105
+ │ │ <style>@font-face { src: data:... }</style> │ │
106
+ │ │ </head> │ │
107
+ │ │ <body> │ │
108
+ │ │ <img src="data:image/svg+xml;base64,..."> │ │
109
+ │ │ <script>/* Minimal hydration */</script> │ │
110
+ │ │ </body> │ │
111
+ │ │ </html> │ │
112
+ │ │ │ │
113
+ │ └────────────────────────────────────────────────┘ │
114
+ │ │ │
115
+ │ ┌──────────────────────┴──────────────────────┐ │
116
+ │ ▼ ▼ ▼ │
117
+ │ Edge Cache KV Cache D1 Cache │
118
+ │ (1-5ms) (5-10ms) (10-20ms) │
119
+ │ │
120
+ └─────────────────────────────────────────────────────────────────────────────┘
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Detailed Design
126
+
127
+ ### 1. WASM AST Renderer
128
+
129
+ The core of the system is a Rust/WASM module that walks the component tree and produces fully-resolved HTML.
130
+
131
+ ```rust
132
+ // packages/runtime-wasm/src/renderer.rs
133
+
134
+ use wasm_bindgen::prelude::*;
135
+ use std::collections::{HashMap, HashSet};
136
+
137
+ #[wasm_bindgen]
138
+ pub struct AeonRenderer {
139
+ css_manifest: CSSManifest,
140
+ asset_manifest: AssetManifest,
141
+ font_manifest: FontManifest,
142
+ }
143
+
144
+ #[wasm_bindgen]
145
+ impl AeonRenderer {
146
+ #[wasm_bindgen(constructor)]
147
+ pub fn new(
148
+ css_manifest: JsValue,
149
+ asset_manifest: JsValue,
150
+ font_manifest: JsValue,
151
+ ) -> AeonRenderer {
152
+ AeonRenderer {
153
+ css_manifest: serde_wasm_bindgen::from_value(css_manifest).unwrap(),
154
+ asset_manifest: serde_wasm_bindgen::from_value(asset_manifest).unwrap(),
155
+ font_manifest: serde_wasm_bindgen::from_value(font_manifest).unwrap(),
156
+ }
157
+ }
158
+
159
+ #[wasm_bindgen]
160
+ pub fn render(&self, component_tree: JsValue) -> JsValue {
161
+ let tree: ComponentNode = serde_wasm_bindgen::from_value(component_tree).unwrap();
162
+
163
+ let mut ctx = RenderContext::new();
164
+
165
+ // Walk tree, collecting all requirements
166
+ self.walk_tree(&tree, &mut ctx);
167
+
168
+ // Assemble final output
169
+ let output = RenderOutput {
170
+ html: ctx.html,
171
+ css: self.assemble_css(&ctx.classes),
172
+ fonts: self.assemble_fonts(&ctx.font_families),
173
+ critical_js: self.generate_hydration_script(&ctx.interactive_nodes),
174
+ };
175
+
176
+ serde_wasm_bindgen::to_value(&output).unwrap()
177
+ }
178
+
179
+ fn walk_tree(&self, node: &ComponentNode, ctx: &mut RenderContext) {
180
+ // Collect CSS classes
181
+ if let Some(class_name) = &node.class_name {
182
+ for class in class_name.split_whitespace() {
183
+ ctx.classes.insert(class.to_string());
184
+ }
185
+ }
186
+
187
+ // Resolve asset references
188
+ let resolved_props = self.resolve_assets(&node.props, ctx);
189
+
190
+ // Render this node
191
+ ctx.html.push_str(&self.render_node(node, &resolved_props));
192
+
193
+ // Recurse into children
194
+ for child in &node.children {
195
+ match child {
196
+ Child::Node(n) => self.walk_tree(n, ctx),
197
+ Child::Text(t) => ctx.html.push_str(&html_escape(t)),
198
+ }
199
+ }
200
+
201
+ // Close tag
202
+ if !is_void_element(&node.tag) {
203
+ ctx.html.push_str(&format!("</{}>", node.tag));
204
+ }
205
+ }
206
+
207
+ fn resolve_assets(&self, props: &Props, ctx: &mut RenderContext) -> Props {
208
+ let mut resolved = props.clone();
209
+
210
+ // Resolve src attributes (images)
211
+ if let Some(src) = resolved.get("src") {
212
+ if let Some(data_uri) = self.asset_manifest.get(src) {
213
+ resolved.insert("src".to_string(), data_uri.clone());
214
+ }
215
+ }
216
+
217
+ // Resolve href for stylesheets (shouldn't exist, but handle gracefully)
218
+ if let Some(href) = resolved.get("href") {
219
+ if href.ends_with(".css") {
220
+ // Remove external CSS reference
221
+ resolved.remove("href");
222
+ }
223
+ }
224
+
225
+ resolved
226
+ }
227
+
228
+ fn assemble_css(&self, classes: &HashSet<String>) -> String {
229
+ let mut css = String::new();
230
+ let mut seen = HashSet::new();
231
+
232
+ // Sort for deterministic output
233
+ let mut sorted_classes: Vec<_> = classes.iter().collect();
234
+ sorted_classes.sort();
235
+
236
+ for class in sorted_classes {
237
+ if let Some(rules) = self.css_manifest.get(class) {
238
+ for rule in rules {
239
+ if !seen.contains(rule) {
240
+ css.push_str(rule);
241
+ css.push('\n');
242
+ seen.insert(rule.clone());
243
+ }
244
+ }
245
+ }
246
+ }
247
+
248
+ css
249
+ }
250
+
251
+ fn assemble_fonts(&self, families: &HashSet<String>) -> String {
252
+ let mut css = String::new();
253
+
254
+ for family in families {
255
+ if let Some(font_data) = self.font_manifest.get(family) {
256
+ css.push_str(&format!(
257
+ r#"@font-face {{
258
+ font-family: '{}';
259
+ font-weight: {};
260
+ font-style: {};
261
+ font-display: swap;
262
+ src: url('data:font/woff2;base64,{}') format('woff2');
263
+ }}
264
+ "#,
265
+ font_data.family,
266
+ font_data.weight,
267
+ font_data.style,
268
+ font_data.base64
269
+ ));
270
+ }
271
+ }
272
+
273
+ css
274
+ }
275
+
276
+ fn generate_hydration_script(&self, nodes: &[InteractiveNode]) -> String {
277
+ if nodes.is_empty() {
278
+ return String::new();
279
+ }
280
+
281
+ // Minimal script that lazy-loads component JS only when needed
282
+ format!(
283
+ r#"<script type="module">
284
+ const h=async(e)=>{{const c=e.dataset.aeonComponent;const m=await import('/_aeon/c/'+c+'.js');m.hydrate(e)}};
285
+ document.querySelectorAll('[data-aeon-i]').forEach(e=>{{
286
+ if(e.dataset.aeonHydrate==='eager')h(e);
287
+ else if('IntersectionObserver' in window){{
288
+ new IntersectionObserver((es,o)=>{{es.forEach(en=>{{if(en.isIntersecting){{o.unobserve(en.target);h(en.target)}}}})}}).observe(e)
289
+ }}else h(e)
290
+ }});
291
+ </script>"#
292
+ )
293
+ }
294
+ }
295
+ ```
296
+
297
+ ### 2. CSS Manifest Generation (Build Time)
298
+
299
+ ```typescript
300
+ // packages/build/src/css-manifest.ts
301
+
302
+ interface CSSManifest {
303
+ version: string;
304
+ rules: Map<string, string[]>; // className → CSS rules
305
+ variants: Map<string, string>; // variant → media query
306
+ }
307
+
308
+ async function generateCSSManifest(config: AeonConfig): Promise<CSSManifest> {
309
+ // 1. Generate full Tailwind CSS
310
+ const fullCSS = await execTailwind(config.tailwindConfig);
311
+
312
+ // 2. Parse into AST
313
+ const ast = postcss.parse(fullCSS);
314
+
315
+ // 3. Build class → rules index
316
+ const rules = new Map<string, string[]>();
317
+
318
+ ast.walkRules((rule) => {
319
+ const classMatch = rule.selector.match(/^\.([a-zA-Z0-9_-]+)/);
320
+ if (!classMatch) return;
321
+
322
+ const className = classMatch[1];
323
+ const cssText = rule.toString();
324
+
325
+ const existing = rules.get(className) || [];
326
+ existing.push(cssText);
327
+ rules.set(className, existing);
328
+ });
329
+
330
+ // 4. Extract variant definitions
331
+ const variants = new Map<string, string>();
332
+ ast.walkAtRules('media', (atRule) => {
333
+ // Map responsive prefixes to media queries
334
+ if (atRule.params.includes('min-width: 640px')) {
335
+ variants.set('sm:', atRule.params);
336
+ }
337
+ // ... etc for md, lg, xl, 2xl
338
+ });
339
+
340
+ return { version: '1.0.0', rules, variants };
341
+ }
342
+ ```
343
+
344
+ ### 3. Asset Manifest Generation (Build Time)
345
+
346
+ ```typescript
347
+ // packages/build/src/asset-manifest.ts
348
+
349
+ interface AssetManifest {
350
+ version: string;
351
+ assets: Map<string, AssetEntry>;
352
+ }
353
+
354
+ interface AssetEntry {
355
+ originalPath: string;
356
+ dataUri: string;
357
+ size: number;
358
+ format: string;
359
+ }
360
+
361
+ async function generateAssetManifest(
362
+ assetsDir: string,
363
+ options: AssetOptions
364
+ ): Promise<AssetManifest> {
365
+ const assets = new Map<string, AssetEntry>();
366
+
367
+ // Scan all asset files
368
+ for await (const file of glob(`${assetsDir}/**/*.{svg,png,jpg,jpeg,gif,webp,ico}`)) {
369
+ const relativePath = path.relative(assetsDir, file);
370
+ const buffer = await Bun.file(file).arrayBuffer();
371
+
372
+ // Skip if too large
373
+ if (buffer.byteLength > options.maxInlineSize) {
374
+ console.warn(`Skipping ${relativePath}: too large (${buffer.byteLength} bytes)`);
375
+ continue;
376
+ }
377
+
378
+ let dataUri: string;
379
+
380
+ if (file.endsWith('.svg')) {
381
+ // SVG: Optimize with SVGO, then inline
382
+ const svgContent = await optimizeSVG(await Bun.file(file).text());
383
+ const base64 = Buffer.from(svgContent).toString('base64');
384
+ dataUri = `data:image/svg+xml;base64,${base64}`;
385
+ } else {
386
+ // Raster: Optionally convert to WebP, then base64
387
+ let finalBuffer = buffer;
388
+ let mimeType = getMimeType(file);
389
+
390
+ if (options.convertToWebP && !file.endsWith('.webp')) {
391
+ finalBuffer = await convertToWebP(buffer, options.webpQuality);
392
+ mimeType = 'image/webp';
393
+ }
394
+
395
+ const base64 = Buffer.from(finalBuffer).toString('base64');
396
+ dataUri = `data:${mimeType};base64,${base64}`;
397
+ }
398
+
399
+ assets.set(`/${relativePath}`, {
400
+ originalPath: file,
401
+ dataUri,
402
+ size: buffer.byteLength,
403
+ format: path.extname(file).slice(1),
404
+ });
405
+ }
406
+
407
+ return { version: '1.0.0', assets };
408
+ }
409
+
410
+ async function optimizeSVG(svgContent: string): Promise<string> {
411
+ const { optimize } = await import('svgo');
412
+
413
+ const result = optimize(svgContent, {
414
+ multipass: true,
415
+ plugins: [
416
+ 'preset-default',
417
+ 'removeDimensions',
418
+ {
419
+ name: 'removeAttrs',
420
+ params: { attrs: ['data-name', 'class'] },
421
+ },
422
+ ],
423
+ });
424
+
425
+ return result.data;
426
+ }
427
+ ```
428
+
429
+ ### 4. Font Manifest Generation (Build Time)
430
+
431
+ ```typescript
432
+ // packages/build/src/font-manifest.ts
433
+
434
+ interface FontManifest {
435
+ version: string;
436
+ fonts: Map<string, FontEntry>;
437
+ }
438
+
439
+ interface FontEntry {
440
+ family: string;
441
+ weight: number;
442
+ style: 'normal' | 'italic';
443
+ dataUri: string;
444
+ unicodeRange?: string;
445
+ }
446
+
447
+ async function generateFontManifest(
448
+ fontsDir: string,
449
+ options: FontOptions
450
+ ): Promise<FontManifest> {
451
+ const fonts = new Map<string, FontEntry>();
452
+
453
+ for await (const file of glob(`${fontsDir}/**/*.woff2`)) {
454
+ const buffer = await Bun.file(file).arrayBuffer();
455
+
456
+ // Optional: Subset font to reduce size
457
+ let finalBuffer = buffer;
458
+ if (options.subset) {
459
+ finalBuffer = await subsetFont(buffer, options.subset);
460
+ }
461
+
462
+ const base64 = Buffer.from(finalBuffer).toString('base64');
463
+ const { family, weight, style } = parseFileName(file);
464
+
465
+ fonts.set(`${family}-${weight}-${style}`, {
466
+ family,
467
+ weight,
468
+ style,
469
+ dataUri: `data:font/woff2;base64,${base64}`,
470
+ });
471
+ }
472
+
473
+ return { version: '1.0.0', fonts };
474
+ }
475
+
476
+ // Parse font file name like "Inter-Bold.woff2" → { family: "Inter", weight: 700, style: "normal" }
477
+ function parseFileName(file: string): { family: string; weight: number; style: 'normal' | 'italic' } {
478
+ const name = path.basename(file, '.woff2');
479
+ const parts = name.split('-');
480
+
481
+ const family = parts[0];
482
+ const weightMap: Record<string, number> = {
483
+ 'Thin': 100, 'ExtraLight': 200, 'Light': 300, 'Regular': 400,
484
+ 'Medium': 500, 'SemiBold': 600, 'Bold': 700, 'ExtraBold': 800, 'Black': 900,
485
+ };
486
+
487
+ let weight = 400;
488
+ let style: 'normal' | 'italic' = 'normal';
489
+
490
+ for (const part of parts.slice(1)) {
491
+ if (weightMap[part]) weight = weightMap[part];
492
+ if (part === 'Italic') style = 'italic';
493
+ }
494
+
495
+ return { family, weight, style };
496
+ }
497
+ ```
498
+
499
+ ### 5. Multi-Layer Cache Implementation
500
+
501
+ ```typescript
502
+ // packages/runtime/src/cache.ts
503
+
504
+ interface CacheConfig {
505
+ edge: { ttl: number };
506
+ kv: { ttl: number; namespace: string };
507
+ d1: { table: string };
508
+ }
509
+
510
+ class PageCache {
511
+ constructor(
512
+ private env: Env,
513
+ private config: CacheConfig
514
+ ) {}
515
+
516
+ async get(route: string): Promise<string | null> {
517
+ // Layer 1: Edge cache (handled by Cloudflare automatically via headers)
518
+
519
+ // Layer 2: KV cache
520
+ const kvKey = `page:${route}`;
521
+ const kvCached = await this.env.CACHE.get(kvKey);
522
+ if (kvCached) {
523
+ console.log(`[cache] KV hit: ${route}`);
524
+ return kvCached;
525
+ }
526
+
527
+ // Layer 3: D1 cache
528
+ const d1Result = await this.env.DB
529
+ .prepare(`SELECT html FROM ${this.config.d1.table} WHERE route = ?`)
530
+ .bind(route)
531
+ .first<{ html: string }>();
532
+
533
+ if (d1Result) {
534
+ console.log(`[cache] D1 hit: ${route}`);
535
+ // Promote to KV
536
+ await this.env.CACHE.put(kvKey, d1Result.html, {
537
+ expirationTtl: this.config.kv.ttl,
538
+ });
539
+ return d1Result.html;
540
+ }
541
+
542
+ console.log(`[cache] miss: ${route}`);
543
+ return null;
544
+ }
545
+
546
+ async set(route: string, html: string): Promise<void> {
547
+ const kvKey = `page:${route}`;
548
+
549
+ // Store in both KV and D1
550
+ await Promise.all([
551
+ this.env.CACHE.put(kvKey, html, {
552
+ expirationTtl: this.config.kv.ttl,
553
+ }),
554
+ this.env.DB
555
+ .prepare(`
556
+ INSERT OR REPLACE INTO ${this.config.d1.table} (route, html, rendered_at)
557
+ VALUES (?, ?, datetime('now'))
558
+ `)
559
+ .bind(route, html)
560
+ .run(),
561
+ ]);
562
+ }
563
+
564
+ async invalidate(routes: string[]): Promise<void> {
565
+ await Promise.all([
566
+ // Clear KV
567
+ ...routes.map(route => this.env.CACHE.delete(`page:${route}`)),
568
+
569
+ // Clear D1
570
+ this.env.DB
571
+ .prepare(`DELETE FROM ${this.config.d1.table} WHERE route IN (${routes.map(() => '?').join(',')})`)
572
+ .bind(...routes)
573
+ .run(),
574
+ ]);
575
+ }
576
+ }
577
+ ```
578
+
579
+ ### 6. Worker Entry Point
580
+
581
+ ```typescript
582
+ // packages/runtime/src/worker.ts
583
+
584
+ import { AeonRenderer } from './renderer.wasm';
585
+
586
+ export default {
587
+ async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
588
+ const url = new URL(request.url);
589
+ const route = url.pathname;
590
+
591
+ // Static assets bypass rendering
592
+ if (route.startsWith('/_aeon/')) {
593
+ return handleStaticAsset(route, env);
594
+ }
595
+
596
+ // Initialize cache
597
+ const cache = new PageCache(env, {
598
+ edge: { ttl: 3600 },
599
+ kv: { ttl: 3600, namespace: 'CACHE' },
600
+ d1: { table: 'rendered_pages' },
601
+ });
602
+
603
+ // Check cache first
604
+ const cached = await cache.get(route);
605
+ if (cached) {
606
+ return new Response(cached, {
607
+ headers: {
608
+ 'Content-Type': 'text/html; charset=utf-8',
609
+ 'Cache-Control': 'public, max-age=3600, s-maxage=86400',
610
+ 'X-Aeon-Cache': 'hit',
611
+ },
612
+ });
613
+ }
614
+
615
+ // Load manifests
616
+ const [cssManifest, assetManifest, fontManifest, pageSession] = await Promise.all([
617
+ loadManifest(env, 'css'),
618
+ loadManifest(env, 'assets'),
619
+ loadManifest(env, 'fonts'),
620
+ loadPageSession(env, route),
621
+ ]);
622
+
623
+ if (!pageSession) {
624
+ return new Response('Not Found', { status: 404 });
625
+ }
626
+
627
+ // Render with WASM
628
+ const renderer = new AeonRenderer(cssManifest, assetManifest, fontManifest);
629
+ const output = renderer.render(pageSession.tree);
630
+
631
+ // Assemble final HTML
632
+ const html = assembleHTML(output, pageSession, env);
633
+
634
+ // Cache for next time (non-blocking)
635
+ ctx.waitUntil(cache.set(route, html));
636
+
637
+ return new Response(html, {
638
+ headers: {
639
+ 'Content-Type': 'text/html; charset=utf-8',
640
+ 'Cache-Control': 'public, max-age=3600, s-maxage=86400',
641
+ 'X-Aeon-Cache': 'miss',
642
+ },
643
+ });
644
+ },
645
+ };
646
+
647
+ function assembleHTML(output: RenderOutput, session: PageSession, env: Env): string {
648
+ const title = session.data.title || 'AFFECTIVELY';
649
+ const description = session.data.description || '';
650
+
651
+ return `<!DOCTYPE html>
652
+ <html lang="en">
653
+ <head>
654
+ <meta charset="UTF-8">
655
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
656
+ <title>${escapeHtml(title)}</title>
657
+ ${description ? `<meta name="description" content="${escapeHtml(description)}">` : ''}
658
+ <style>${output.fonts}${output.css}</style>
659
+ </head>
660
+ <body>
661
+ <div id="root">${output.html}</div>
662
+ ${output.critical_js}
663
+ </body>
664
+ </html>`;
665
+ }
666
+ ```
667
+
668
+ ---
669
+
670
+ ## Performance Expectations
671
+
672
+ ### Render Time Breakdown (WASM)
673
+
674
+ | Operation | JavaScript | WASM | Speedup |
675
+ |-----------|------------|------|---------|
676
+ | Parse AST (1000 nodes) | 15ms | 2ms | 7.5x |
677
+ | Walk tree | 8ms | 0.5ms | 16x |
678
+ | CSS collection | 12ms | 1ms | 12x |
679
+ | Asset resolution | 20ms | 3ms | 6.7x |
680
+ | HTML generation | 10ms | 1ms | 10x |
681
+ | **Total** | **65ms** | **7.5ms** | **8.7x** |
682
+
683
+ ### Page Size Comparison
684
+
685
+ | Resource | Current | Target | Reduction |
686
+ |----------|---------|--------|-----------|
687
+ | HTML | 5KB | 15KB | -200% (larger, but includes everything) |
688
+ | CSS | 200KB (external) | 10KB (inline) | 95% |
689
+ | Fonts | 150KB (external) | 30KB (inline, subset) | 80% |
690
+ | Images | 100KB (external) | 50KB (inline, optimized) | 50% |
691
+ | JS | 200KB (external) | 5KB (critical only) | 97% |
692
+ | **Total Requests** | 15-30 | 1 | 97%+ |
693
+ | **Total Bytes** | ~655KB | ~110KB | 83% |
694
+
695
+ ### Time to First Paint
696
+
697
+ | Metric | Current | Target | Improvement |
698
+ |--------|---------|--------|-------------|
699
+ | TTFB | 100ms | 50ms | 50% |
700
+ | FCP | 500ms | 100ms | 80% |
701
+ | LCP | 1200ms | 200ms | 83% |
702
+ | TTI | 2000ms | 300ms | 85% |
703
+ | CLS | 0.05 | 0 | 100% |
704
+
705
+ ---
706
+
707
+ ## Migration Path
708
+
709
+ ### Phase 1: Build Manifests (Week 1-2)
710
+ - Implement CSS manifest generation
711
+ - Implement asset manifest generation
712
+ - Implement font manifest generation
713
+ - Store in D1 at deploy time
714
+
715
+ ### Phase 2: WASM Renderer (Week 3-4)
716
+ - Port AST walker to Rust
717
+ - Implement CSS collection
718
+ - Implement asset resolution
719
+ - Benchmark against JS renderer
720
+
721
+ ### Phase 3: Cache Layers (Week 5-6)
722
+ - Implement KV page cache
723
+ - Implement D1 page cache
724
+ - Add cache invalidation on deploy
725
+ - Add cache warming for popular routes
726
+
727
+ ### Phase 4: Progressive Rollout (Week 7-8)
728
+ - A/B test against current system
729
+ - Monitor performance metrics
730
+ - Fix edge cases
731
+ - Full rollout
732
+
733
+ ### Phase 5: Hydration Optimization (Week 9-10)
734
+ - Implement lazy hydration
735
+ - Reduce critical JS to minimum
736
+ - Add interaction observers
737
+ - Test interactive components
738
+
739
+ ---
740
+
741
+ ## Speculative Pre-Rendering
742
+
743
+ ### The Vision: Zero-Latency Navigation
744
+
745
+ Since Aeon already has a speculation engine predicting the next likely click, we can **pre-render entire pages in memory** before the user clicks. When they do click, it's just a DOM swap - no network request, no rendering delay.
746
+
747
+ ```
748
+ ┌─────────────────────────────────────────────────────────────────────────────┐
749
+ │ SPECULATIVE PRE-RENDERING │
750
+ ├─────────────────────────────────────────────────────────────────────────────┤
751
+ │ │
752
+ │ Current Page Loaded │
753
+ │ │ │
754
+ │ ▼ │
755
+ │ ┌─────────────────┐ │
756
+ │ │ Speculation │───▶ Predict next clicks: [/dashboard, /explore, /chat] │
757
+ │ │ Engine │ (based on: link proximity, user history, ML model) │
758
+ │ └─────────────────┘ │
759
+ │ │ │
760
+ │ ▼ │
761
+ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
762
+ │ │ Pre-Render │ │ Pre-Render │ │ Pre-Render │ │
763
+ │ │ /dashboard │ │ /explore │ │ /chat │ │
764
+ │ │ (full HTML) │ │ (full HTML) │ │ (full HTML) │ │
765
+ │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
766
+ │ │ │ │ │
767
+ │ └───────────────────────┴───────────────────────┘ │
768
+ │ │ │
769
+ │ ▼ │
770
+ │ ┌──────────────────────────────┐ │
771
+ │ │ In-Memory Page Cache │ │
772
+ │ │ │ │
773
+ │ │ Map<route, { │ │
774
+ │ │ html: string, │ │
775
+ │ │ prefetchedAt: Date, │ │
776
+ │ │ confidence: number, │ │
777
+ │ │ stale: boolean │ │
778
+ │ │ }> │ │
779
+ │ │ │ │
780
+ │ └──────────────────────────────┘ │
781
+ │ │ │
782
+ │ ▼ │
783
+ │ User Clicks Link ──────────▶ Instant DOM Swap (0ms network, 0ms render) │
784
+ │ │
785
+ └─────────────────────────────────────────────────────────────────────────────┘
786
+ ```
787
+
788
+ ### Implementation
789
+
790
+ ```typescript
791
+ // packages/runtime/src/speculation.ts
792
+
793
+ interface PreRenderedPage {
794
+ route: string;
795
+ html: string;
796
+ prefetchedAt: number;
797
+ confidence: number;
798
+ stale: boolean;
799
+ }
800
+
801
+ class SpeculativeRenderer {
802
+ private cache = new Map<string, PreRenderedPage>();
803
+ private renderer: AeonRenderer;
804
+ private observer: IntersectionObserver;
805
+
806
+ constructor(renderer: AeonRenderer) {
807
+ this.renderer = renderer;
808
+
809
+ // Watch for links entering viewport
810
+ this.observer = new IntersectionObserver(
811
+ (entries) => this.onLinksVisible(entries),
812
+ { rootMargin: '200px' } // Pre-render when link is 200px from viewport
813
+ );
814
+ }
815
+
816
+ init() {
817
+ // Observe all internal links
818
+ document.querySelectorAll('a[href^="/"]').forEach((link) => {
819
+ this.observer.observe(link);
820
+ });
821
+
822
+ // Also use speculation rules if browser supports them
823
+ this.injectSpeculationRules();
824
+ }
825
+
826
+ private async onLinksVisible(entries: IntersectionObserverEntry[]) {
827
+ for (const entry of entries) {
828
+ if (!entry.isIntersecting) continue;
829
+
830
+ const link = entry.target as HTMLAnchorElement;
831
+ const route = new URL(link.href).pathname;
832
+
833
+ // Skip if already cached
834
+ if (this.cache.has(route)) continue;
835
+
836
+ // Pre-render this page
837
+ await this.preRender(route);
838
+ }
839
+ }
840
+
841
+ private async preRender(route: string): Promise<void> {
842
+ console.log(`[speculation] Pre-rendering: ${route}`);
843
+
844
+ // Fetch page session from worker
845
+ const response = await fetch(`/_aeon/session${route}`);
846
+ if (!response.ok) return;
847
+
848
+ const session = await response.json();
849
+
850
+ // Render to HTML using WASM
851
+ const output = this.renderer.render(session.tree);
852
+ const html = this.assembleHTML(output, session);
853
+
854
+ // Store in memory
855
+ this.cache.set(route, {
856
+ route,
857
+ html,
858
+ prefetchedAt: Date.now(),
859
+ confidence: 0.8,
860
+ stale: false,
861
+ });
862
+
863
+ console.log(`[speculation] Cached: ${route} (${html.length} bytes)`);
864
+ }
865
+
866
+ private assembleHTML(output: RenderOutput, session: PageSession): string {
867
+ return `<!DOCTYPE html>
868
+ <html lang="en">
869
+ <head>
870
+ <meta charset="UTF-8">
871
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
872
+ <title>${session.data.title || 'AFFECTIVELY'}</title>
873
+ <style>${output.fonts}${output.css}</style>
874
+ </head>
875
+ <body>
876
+ <div id="root">${output.html}</div>
877
+ ${output.critical_js}
878
+ </body>
879
+ </html>`;
880
+ }
881
+
882
+ private injectSpeculationRules() {
883
+ // Use browser's native speculation rules API when available
884
+ const rules = {
885
+ prerender: [
886
+ {
887
+ source: 'document',
888
+ where: { href_matches: '/*' },
889
+ eagerness: 'moderate',
890
+ },
891
+ ],
892
+ };
893
+
894
+ const script = document.createElement('script');
895
+ script.type = 'speculationrules';
896
+ script.textContent = JSON.stringify(rules);
897
+ document.head.appendChild(script);
898
+ }
899
+
900
+ // Called when user clicks a link
901
+ async navigate(route: string): Promise<boolean> {
902
+ const cached = this.cache.get(route);
903
+
904
+ if (cached && !cached.stale) {
905
+ // Instant navigation - just swap the HTML
906
+ document.open();
907
+ document.write(cached.html);
908
+ document.close();
909
+
910
+ // Update URL
911
+ history.pushState({}, '', route);
912
+
913
+ // Re-initialize speculation for new page
914
+ this.init();
915
+
916
+ console.log(`[speculation] Instant nav to: ${route}`);
917
+ return true;
918
+ }
919
+
920
+ // Not cached - fall back to normal navigation
921
+ return false;
922
+ }
923
+
924
+ // Invalidate cached pages (called on data changes)
925
+ invalidate(routes?: string[]) {
926
+ if (routes) {
927
+ routes.forEach((route) => {
928
+ const cached = this.cache.get(route);
929
+ if (cached) cached.stale = true;
930
+ });
931
+ } else {
932
+ this.cache.forEach((page) => (page.stale = true));
933
+ }
934
+ }
935
+ }
936
+
937
+ // Integration with link clicks
938
+ document.addEventListener('click', async (e) => {
939
+ const link = (e.target as Element).closest('a[href^="/"]');
940
+ if (!link) return;
941
+
942
+ const route = new URL((link as HTMLAnchorElement).href).pathname;
943
+
944
+ // Try speculative navigation first
945
+ if (await speculativeRenderer.navigate(route)) {
946
+ e.preventDefault();
947
+ }
948
+ });
949
+ ```
950
+
951
+ ### On-Demand CSS (Tailwind v4 Style)
952
+
953
+ The key insight from Tailwind v4's Lightning CSS: generate CSS **only for the classes actually used** at render time.
954
+
955
+ ```typescript
956
+ // packages/runtime/src/on-demand-css.ts
957
+
958
+ class OnDemandCSS {
959
+ private ruleIndex: Map<string, string>; // className → CSS rule
960
+ private generated = new Set<string>(); // Already generated classes
961
+
962
+ constructor(cssManifest: CSSManifest) {
963
+ this.ruleIndex = cssManifest.rules;
964
+ }
965
+
966
+ // Generate CSS for a specific set of classes
967
+ generate(classes: Set<string>): string {
968
+ const newClasses = [...classes].filter((c) => !this.generated.has(c));
969
+
970
+ if (newClasses.length === 0) return '';
971
+
972
+ const css: string[] = [];
973
+
974
+ for (const className of newClasses) {
975
+ const rule = this.ruleIndex.get(className);
976
+ if (rule) {
977
+ css.push(rule);
978
+ this.generated.add(className);
979
+ }
980
+ }
981
+
982
+ return css.join('\n');
983
+ }
984
+
985
+ // Generate CSS for a component tree
986
+ generateForTree(tree: ComponentNode): string {
987
+ const classes = this.extractClasses(tree);
988
+ return this.generate(classes);
989
+ }
990
+
991
+ private extractClasses(node: ComponentNode, classes = new Set<string>()): Set<string> {
992
+ if (node.className) {
993
+ node.className.split(/\s+/).forEach((c) => c && classes.add(c));
994
+ }
995
+
996
+ for (const child of node.children || []) {
997
+ if (typeof child === 'object') {
998
+ this.extractClasses(child, classes);
999
+ }
1000
+ }
1001
+
1002
+ return classes;
1003
+ }
1004
+ }
1005
+ ```
1006
+
1007
+ ### The Complete Flow
1008
+
1009
+ ```
1010
+ User lands on /home
1011
+
1012
+
1013
+ ┌──────────────────┐
1014
+ │ 1. Render /home │──▶ On-demand CSS (only /home classes) + inline assets
1015
+ │ with WASM │
1016
+ └──────────────────┘
1017
+
1018
+
1019
+ ┌──────────────────┐
1020
+ │ 2. Speculation │──▶ Predict next: [/dashboard, /explore, /chat]
1021
+ │ Engine runs │
1022
+ └──────────────────┘
1023
+
1024
+
1025
+ ┌──────────────────┐
1026
+ │ 3. Pre-render │──▶ Full HTML for each, stored in memory
1027
+ │ predicted │ (each with their own on-demand CSS)
1028
+ │ pages │
1029
+ └──────────────────┘
1030
+
1031
+
1032
+ User clicks /dashboard
1033
+
1034
+
1035
+ ┌──────────────────┐
1036
+ │ 4. DOM swap │──▶ 0ms network, 0ms render
1037
+ │ from cache │ Instant page transition
1038
+ └──────────────────┘
1039
+
1040
+
1041
+ ┌──────────────────┐
1042
+ │ 5. Re-speculate │──▶ Predict next pages from /dashboard
1043
+ │ for new page │
1044
+ └──────────────────┘
1045
+ ```
1046
+
1047
+ This creates a **feel of a native app** - every navigation is instant because the page is already in memory, fully rendered with exactly the CSS it needs.
1048
+
1049
+ ---
1050
+
1051
+ ## Build-Time Pre-Rendering (Static Generation)
1052
+
1053
+ ### The Ultimate Optimization: Pre-Render Everything at Build
1054
+
1055
+ Why wait for runtime? We can **pre-render every single page during the build process** and seed them directly into D1/KV. First request for ANY route is a cache hit.
1056
+
1057
+ ```
1058
+ ┌─────────────────────────────────────────────────────────────────────────────┐
1059
+ │ BUILD TIME PRE-RENDERING │
1060
+ ├─────────────────────────────────────────────────────────────────────────────┤
1061
+ │ │
1062
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────────┐ │
1063
+ │ │ All Routes │───▶│ WASM │───▶│ Rendered HTML │ │
1064
+ │ │ from D1 │ │ Renderer │ │ (with inline CSS, assets) │ │
1065
+ │ └──────────────┘ └──────────────┘ └──────────────────────────────┘ │
1066
+ │ │ │
1067
+ │ ▼ │
1068
+ │ ┌───────────────────────────────────────────┐ │
1069
+ │ │ D1 Seed SQL │ │
1070
+ │ │ │ │
1071
+ │ │ INSERT INTO rendered_pages VALUES │ │
1072
+ │ │ ('/', '<html>...</html>'), │ │
1073
+ │ │ ('/about', '<html>...</html>'), │ │
1074
+ │ │ ('/dashboard', '<html>...</html>'), │ │
1075
+ │ │ ('/explore', '<html>...</html>'), │ │
1076
+ │ │ ... (every route pre-rendered) │ │
1077
+ │ │ │ │
1078
+ │ └───────────────────────────────────────────┘ │
1079
+ │ │ │
1080
+ │ ▼ │
1081
+ │ ┌───────────────────────────────────────────┐ │
1082
+ │ │ wrangler d1 execute --file │ │
1083
+ │ │ (seed all pages at deploy) │ │
1084
+ │ └───────────────────────────────────────────┘ │
1085
+ │ │
1086
+ └─────────────────────────────────────────────────────────────────────────────┘
1087
+ ```
1088
+
1089
+ ### Build Command
1090
+
1091
+ ```typescript
1092
+ // packages/cli/src/commands/build.ts
1093
+
1094
+ export async function build(options: BuildOptions): Promise<void> {
1095
+ // ... existing steps ...
1096
+
1097
+ // Step 9: Pre-render all pages
1098
+ console.log('9️⃣ Pre-rendering all pages...');
1099
+
1100
+ const renderer = new AeonRenderer(cssManifest, assetManifest, fontManifest);
1101
+ const pages = await getAllPageSessions(db);
1102
+ const rendered: Array<{ route: string; html: string }> = [];
1103
+
1104
+ for (const page of pages) {
1105
+ const output = renderer.render(page.tree);
1106
+ const html = assembleHTML(output, page);
1107
+
1108
+ rendered.push({
1109
+ route: page.route,
1110
+ html,
1111
+ });
1112
+
1113
+ console.log(` ✓ ${page.route} (${(html.length / 1024).toFixed(1)}KB)`);
1114
+ }
1115
+
1116
+ // Step 10: Generate seed SQL for pre-rendered pages
1117
+ console.log('🔟 Generating pre-render seed...');
1118
+
1119
+ const seedSQL = generatePreRenderSeed(rendered);
1120
+ await writeFile(
1121
+ join(outputDir, 'seed-prerender.sql'),
1122
+ seedSQL
1123
+ );
1124
+
1125
+ console.log(` ✓ seed-prerender.sql (${rendered.length} pages)`);
1126
+
1127
+ // Summary
1128
+ const totalSize = rendered.reduce((sum, p) => sum + p.html.length, 0);
1129
+ console.log(`\n📊 Pre-render summary:`);
1130
+ console.log(` Pages: ${rendered.length}`);
1131
+ console.log(` Total size: ${(totalSize / 1024 / 1024).toFixed(2)}MB`);
1132
+ console.log(` Avg per page: ${(totalSize / rendered.length / 1024).toFixed(1)}KB`);
1133
+ }
1134
+
1135
+ function generatePreRenderSeed(pages: Array<{ route: string; html: string }>): string {
1136
+ const lines: string[] = [
1137
+ '-- Pre-rendered pages',
1138
+ `-- Generated: ${new Date().toISOString()}`,
1139
+ `-- Total pages: ${pages.length}`,
1140
+ '',
1141
+ 'DELETE FROM rendered_pages;',
1142
+ '',
1143
+ ];
1144
+
1145
+ for (const page of pages) {
1146
+ // Escape single quotes in HTML
1147
+ const escapedHtml = page.html.replace(/'/g, "''");
1148
+ lines.push(
1149
+ `INSERT INTO rendered_pages (route, html, version, rendered_at) VALUES ('${page.route}', '${escapedHtml}', '${process.env.DEPLOY_VERSION || '1.0.0'}', datetime('now'));`
1150
+ );
1151
+ }
1152
+
1153
+ return lines.join('\n');
1154
+ }
1155
+ ```
1156
+
1157
+ ### Deploy Pipeline
1158
+
1159
+ ```yaml
1160
+ # .github/workflows/deploy.yml
1161
+
1162
+ name: Deploy
1163
+
1164
+ on:
1165
+ push:
1166
+ branches: [main]
1167
+
1168
+ jobs:
1169
+ build-and-deploy:
1170
+ runs-on: ubuntu-latest
1171
+ steps:
1172
+ - uses: actions/checkout@v4
1173
+
1174
+ - name: Setup Bun
1175
+ uses: oven-sh/setup-bun@v1
1176
+
1177
+ - name: Install dependencies
1178
+ run: bun install
1179
+
1180
+ - name: Build (includes pre-rendering)
1181
+ run: bun run build
1182
+ env:
1183
+ DEPLOY_VERSION: ${{ github.sha }}
1184
+
1185
+ - name: Deploy to Cloudflare
1186
+ run: |
1187
+ # 1. Deploy D1 migrations
1188
+ wrangler d1 execute aeon-flux --file=.aeon/migrations/0001_initial.sql
1189
+
1190
+ # 2. Seed manifests
1191
+ wrangler d1 execute aeon-flux --file=.aeon/seed-manifests.sql
1192
+
1193
+ # 3. Seed pre-rendered pages
1194
+ wrangler d1 execute aeon-flux --file=.aeon/seed-prerender.sql
1195
+
1196
+ # 4. Deploy worker
1197
+ wrangler deploy
1198
+ ```
1199
+
1200
+ ### Incremental Pre-Rendering
1201
+
1202
+ For large sites, we can do incremental pre-rendering:
1203
+
1204
+ ```typescript
1205
+ // packages/cli/src/commands/build.ts
1206
+
1207
+ async function incrementalPreRender(
1208
+ renderer: AeonRenderer,
1209
+ db: D1Database,
1210
+ changedRoutes: string[]
1211
+ ): Promise<void> {
1212
+ console.log(`Incremental pre-render: ${changedRoutes.length} pages`);
1213
+
1214
+ for (const route of changedRoutes) {
1215
+ const page = await getPageSession(db, route);
1216
+ if (!page) continue;
1217
+
1218
+ const output = renderer.render(page.tree);
1219
+ const html = assembleHTML(output, page);
1220
+
1221
+ // Update in D1
1222
+ await db
1223
+ .prepare(`
1224
+ INSERT OR REPLACE INTO rendered_pages (route, html, version, rendered_at)
1225
+ VALUES (?, ?, ?, datetime('now'))
1226
+ `)
1227
+ .bind(route, html, process.env.DEPLOY_VERSION)
1228
+ .run();
1229
+
1230
+ console.log(` ✓ ${route}`);
1231
+ }
1232
+ }
1233
+
1234
+ // Detect changed routes by comparing git diffs
1235
+ async function getChangedRoutes(): Promise<string[]> {
1236
+ const { stdout } = await exec('git diff --name-only HEAD~1');
1237
+ const changedFiles = stdout.split('\n');
1238
+
1239
+ // Map changed files to affected routes
1240
+ const routes = new Set<string>();
1241
+
1242
+ for (const file of changedFiles) {
1243
+ // Component change affects all routes using it
1244
+ if (file.includes('/components/')) {
1245
+ const componentName = path.basename(file, '.tsx');
1246
+ const affectedRoutes = await getRoutesUsingComponent(componentName);
1247
+ affectedRoutes.forEach((r) => routes.add(r));
1248
+ }
1249
+
1250
+ // Page change affects that route
1251
+ if (file.includes('/pages/') && file.endsWith('page.tsx')) {
1252
+ const route = filePathToRoute(file);
1253
+ routes.add(route);
1254
+ }
1255
+ }
1256
+
1257
+ return [...routes];
1258
+ }
1259
+ ```
1260
+
1261
+ ### The Complete Build Output
1262
+
1263
+ ```
1264
+ $ bun run build
1265
+
1266
+ 🔨 Building Aeon Flux for production...
1267
+
1268
+ 📁 Output: .aeon
1269
+
1270
+ 1️⃣ Parsing pages...
1271
+ Found 47 page(s)
1272
+
1273
+ 2️⃣ Generating route manifest...
1274
+ ✓ manifest.json
1275
+
1276
+ 3️⃣ Generating D1 migration...
1277
+ ✓ migrations/0001_initial.sql
1278
+
1279
+ 4️⃣ Generating D1 seed data...
1280
+ ✓ seed.sql
1281
+
1282
+ 5️⃣ Bundling WASM runtime...
1283
+ ✓ runtime.wasm (18KB)
1284
+
1285
+ 6️⃣ Generating CSS manifest...
1286
+ ✓ css-manifest.json (2,847 rules)
1287
+
1288
+ 7️⃣ Generating asset manifest...
1289
+ ✓ asset-manifest.json (156 assets, 892KB inline)
1290
+
1291
+ 8️⃣ Generating font manifest...
1292
+ ✓ font-manifest.json (4 fonts, 124KB inline)
1293
+
1294
+ 9️⃣ Pre-rendering all pages...
1295
+ ✓ / (23.4KB)
1296
+ ✓ /about (18.2KB)
1297
+ ✓ /dashboard (31.5KB)
1298
+ ✓ /explore (45.2KB)
1299
+ ✓ /chat (28.7KB)
1300
+ ... (42 more pages)
1301
+
1302
+ 🔟 Generating pre-render seed...
1303
+ ✓ seed-prerender.sql (47 pages)
1304
+
1305
+ 📊 Pre-render summary:
1306
+ Pages: 47
1307
+ Total size: 1.34MB
1308
+ Avg per page: 29.2KB
1309
+
1310
+ ✨ Build complete in 4.2s
1311
+
1312
+ Next steps:
1313
+ wrangler d1 execute aeon-flux --file=.aeon/migrations/0001_initial.sql
1314
+ wrangler d1 execute aeon-flux --file=.aeon/seed-prerender.sql
1315
+ wrangler deploy
1316
+ ```
1317
+
1318
+ ### Runtime Flow with Pre-Rendered Pages
1319
+
1320
+ ```
1321
+ Request: GET /dashboard
1322
+
1323
+
1324
+ ┌──────────────────┐
1325
+ │ Check D1 │──▶ SELECT html FROM rendered_pages WHERE route = '/dashboard'
1326
+ │ (pre-rendered) │
1327
+ └──────────────────┘
1328
+
1329
+ ▼ (cache hit - always, because build pre-rendered it)
1330
+
1331
+ ┌──────────────────┐
1332
+ │ Return HTML │──▶ <html>...entire page with inline CSS/assets...</html>
1333
+ │ (zero rendering) │
1334
+ └──────────────────┘
1335
+
1336
+
1337
+ Response: 200 OK (15ms total, including D1 query)
1338
+ ```
1339
+
1340
+ **Every single page is a cache hit from the moment the site deploys.** There's never a cold render path for any known route.
1341
+
1342
+ ---
1343
+
1344
+ ## Open Questions
1345
+
1346
+ 1. **Large Images**: What's the size threshold for inlining vs external? (Proposed: 10KB)
1347
+ 2. **Dynamic Content**: How do we handle user-specific content? (Proposed: separate data loading)
1348
+ 3. **Third-Party Scripts**: Analytics, chat widgets, etc.? (Proposed: load async post-paint)
1349
+ 4. **SEO**: Will inline SVGs affect image SEO? (Proposed: test with GSC)
1350
+ 5. **Memory**: What's the memory impact of large data URIs? (Proposed: benchmark)
1351
+
1352
+ ---
1353
+
1354
+ ## Appendix A: D1 Schema
1355
+
1356
+ ```sql
1357
+ -- Render manifests
1358
+ CREATE TABLE IF NOT EXISTS render_manifests (
1359
+ type TEXT PRIMARY KEY, -- 'css', 'assets', 'fonts'
1360
+ manifest TEXT NOT NULL,
1361
+ version TEXT NOT NULL,
1362
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
1363
+ );
1364
+
1365
+ -- Rendered pages cache
1366
+ CREATE TABLE IF NOT EXISTS rendered_pages (
1367
+ route TEXT PRIMARY KEY,
1368
+ html TEXT NOT NULL,
1369
+ version TEXT NOT NULL,
1370
+ rendered_at TEXT DEFAULT CURRENT_TIMESTAMP
1371
+ );
1372
+
1373
+ -- Indexes
1374
+ CREATE INDEX IF NOT EXISTS idx_rendered_pages_time ON rendered_pages(rendered_at);
1375
+ ```
1376
+
1377
+ ---
1378
+
1379
+ ## Appendix B: Benchmark Results
1380
+
1381
+ *To be filled in during implementation*
1382
+
1383
+ ---
1384
+
1385
+ ## References
1386
+
1387
+ - [Why Not document.write()?](https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#document.write())
1388
+ - [Critical Rendering Path](https://web.dev/critical-rendering-path/)
1389
+ - [WASM Performance](https://webassembly.org/docs/faq/)
1390
+ - [Data URIs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs)
1391
+ - [Font Loading Strategies](https://web.dev/font-best-practices/)
1392
+ - [Resource Hints](https://web.dev/preload-critical-assets/)
1393
+
1394
+ ---
1395
+
1396
+ ## Implementation Status
1397
+
1398
+ ### Phase 1: Build-Time Manifests ✅ COMPLETE
1399
+
1400
+ | Task | Status | Notes |
1401
+ |------|--------|-------|
1402
+ | CSS Manifest Generator | ✅ Complete | `packages/build/src/css-manifest.ts` - Generates CSS rules for Tailwind classes on-demand |
1403
+ | Asset Manifest Generator | ✅ Complete | `packages/build/src/asset-manifest.ts` - Inlines SVG/images as data URIs |
1404
+ | Font Manifest Generator | ✅ Complete | `packages/build/src/font-manifest.ts` - Embeds fonts as base64 data URIs |
1405
+ | Pre-render Module | ✅ Complete | `packages/build/src/prerender.ts` - Full page pre-rendering with inline CSS/assets/fonts |
1406
+ | Build Package Tests | ✅ Complete | 50 tests covering CSS generation, asset handling, pre-rendering |
1407
+ | CLI Integration | ✅ Complete | `packages/cli/src/commands/build.ts` - Integrated manifest building and pre-rendering |
1408
+ | D1 Manifest Storage | ✅ Complete | `generateManifestSeedSQL()` - Stores CSS/asset/font manifests in D1 |
1409
+
1410
+ ### Phase 2: WASM Renderer ✅ COMPLETE
1411
+
1412
+ | Task | Status | Notes |
1413
+ |------|--------|-------|
1414
+ | Port AST walker to Rust/WASM | ✅ Complete | `render.rs` - `extract_css_classes()`, `walk_tree_for_classes()` |
1415
+ | Implement CSS collection in WASM | ✅ Complete | `render.rs` - `generate_css_for_classes()` |
1416
+ | Implement asset resolution in WASM | ✅ Complete | `render.rs` - `resolve_assets()`, `resolve_assets_in_value()` |
1417
+ | WASM renderer tests | ✅ Complete | 4 tests for rendering, CSS extraction, HTML escaping |
1418
+
1419
+ ### Phase 3: Cache Layers ✅ COMPLETE
1420
+
1421
+ | Task | Status | Notes |
1422
+ |------|--------|-------|
1423
+ | KV page cache | ✅ Complete | `generateWorker()` - Layer 1 cache with `PAGES_CACHE` KV namespace |
1424
+ | D1 page cache | ✅ Complete | `getPreRenderedPage()` - Layer 2 cache from `rendered_pages` table |
1425
+ | Cache invalidation on deploy | ✅ Complete | `BUILD_VERSION` checking - stale cache auto-invalidated on new deploy |
1426
+ | Cache layer tests | ✅ Complete | 11 tests for multi-layer caching, version invalidation, cache headers |
1427
+
1428
+ ### Phase 4: Build-Time Pre-Rendering ✅ COMPLETE
1429
+
1430
+ | Task | Status | Notes |
1431
+ |------|--------|-------|
1432
+ | Pre-render all pages at build | ✅ Complete | `prerenderAllPages()` - All pages pre-rendered during build |
1433
+ | Integration tests | ✅ Complete | 32 tests covering full build pipeline including pre-rendering |
1434
+
1435
+ ### Phase 5: Speculative Pre-Rendering ⏳ IN PROGRESS
1436
+
1437
+ | Task | Status | Notes |
1438
+ |------|--------|-------|
1439
+ | Runtime speculative pre-rendering | ⏳ Pending | |
1440
+ | Lazy hydration | ✅ Complete | `generateHydrationScript()` - IntersectionObserver-based lazy hydration |
1441
+ | E2E tests | ⏳ Pending | |
1442
+
1443
+ ---
1444
+
1445
+ *Document Version: 1.0.1*
1446
+ *Last Updated: 2026-02-06*