@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,298 @@
|
|
|
1
|
+
//! Aeon Router - Fast path matching for Next.js-style routes
|
|
2
|
+
//!
|
|
3
|
+
//! Supports:
|
|
4
|
+
//! - Static routes: /about, /blog
|
|
5
|
+
//! - Dynamic segments: /blog/[slug]
|
|
6
|
+
//! - Catch-all segments: /api/[...path]
|
|
7
|
+
//! - Optional catch-all: /docs/[[...slug]]
|
|
8
|
+
//! - Route groups: (dashboard)/settings (ignored in URL)
|
|
9
|
+
|
|
10
|
+
use wasm_bindgen::prelude::*;
|
|
11
|
+
use std::collections::HashMap;
|
|
12
|
+
use crate::{RouteDefinition, RouteMatch};
|
|
13
|
+
|
|
14
|
+
/// Segment type for route pattern parsing
|
|
15
|
+
#[derive(Clone, Debug, PartialEq)]
|
|
16
|
+
enum Segment {
|
|
17
|
+
/// Static segment like "blog" or "about"
|
|
18
|
+
Static(String),
|
|
19
|
+
/// Dynamic segment like [slug]
|
|
20
|
+
Dynamic(String),
|
|
21
|
+
/// Catch-all segment like [...path]
|
|
22
|
+
CatchAll(String),
|
|
23
|
+
/// Optional catch-all like [[...slug]]
|
|
24
|
+
OptionalCatchAll(String),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Parsed route pattern
|
|
28
|
+
#[derive(Clone, Debug)]
|
|
29
|
+
struct ParsedRoute {
|
|
30
|
+
segments: Vec<Segment>,
|
|
31
|
+
definition: RouteDefinition,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// The Aeon Router - matches URLs to routes
|
|
35
|
+
#[wasm_bindgen]
|
|
36
|
+
pub struct AeonRouter {
|
|
37
|
+
routes: Vec<ParsedRoute>,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[wasm_bindgen]
|
|
41
|
+
impl AeonRouter {
|
|
42
|
+
#[wasm_bindgen(constructor)]
|
|
43
|
+
pub fn new() -> Self {
|
|
44
|
+
Self { routes: Vec::new() }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Add a route to the router
|
|
48
|
+
pub fn add_route(&mut self, definition: RouteDefinition) {
|
|
49
|
+
let segments = parse_pattern(&definition.pattern());
|
|
50
|
+
self.routes.push(ParsedRoute {
|
|
51
|
+
segments,
|
|
52
|
+
definition,
|
|
53
|
+
});
|
|
54
|
+
// Sort routes by specificity (static > dynamic > catch-all)
|
|
55
|
+
self.routes.sort_by(|a, b| {
|
|
56
|
+
route_specificity(&b.segments).cmp(&route_specificity(&a.segments))
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Match a URL path to a route
|
|
61
|
+
pub fn match_route(&self, path: &str) -> Option<RouteMatch> {
|
|
62
|
+
let path_segments: Vec<&str> = path
|
|
63
|
+
.trim_start_matches('/')
|
|
64
|
+
.trim_end_matches('/')
|
|
65
|
+
.split('/')
|
|
66
|
+
.filter(|s| !s.is_empty())
|
|
67
|
+
.collect();
|
|
68
|
+
|
|
69
|
+
for parsed in &self.routes {
|
|
70
|
+
if let Some(params) = match_segments(&parsed.segments, &path_segments) {
|
|
71
|
+
let resolved_session_id = resolve_session_id(
|
|
72
|
+
&parsed.definition.session_id(),
|
|
73
|
+
¶ms,
|
|
74
|
+
);
|
|
75
|
+
return Some(RouteMatch {
|
|
76
|
+
route: parsed.definition.clone(),
|
|
77
|
+
params,
|
|
78
|
+
resolved_session_id,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
None
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Get all registered routes (for debugging)
|
|
86
|
+
pub fn get_routes_json(&self) -> String {
|
|
87
|
+
let patterns: Vec<String> = self.routes
|
|
88
|
+
.iter()
|
|
89
|
+
.map(|r| r.definition.pattern())
|
|
90
|
+
.collect();
|
|
91
|
+
serde_json::to_string(&patterns).unwrap_or_else(|_| "[]".to_string())
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Check if a route exists for the given path
|
|
95
|
+
pub fn has_route(&self, path: &str) -> bool {
|
|
96
|
+
self.match_route(path).is_some()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
impl Default for AeonRouter {
|
|
101
|
+
fn default() -> Self {
|
|
102
|
+
Self::new()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Parse a route pattern into segments
|
|
107
|
+
fn parse_pattern(pattern: &str) -> Vec<Segment> {
|
|
108
|
+
pattern
|
|
109
|
+
.trim_start_matches('/')
|
|
110
|
+
.trim_end_matches('/')
|
|
111
|
+
.split('/')
|
|
112
|
+
.filter(|s| !s.is_empty())
|
|
113
|
+
.filter(|s| !is_route_group(s)) // Skip route groups like (dashboard)
|
|
114
|
+
.map(|s| {
|
|
115
|
+
if s.starts_with("[[...") && s.ends_with("]]") {
|
|
116
|
+
// Optional catch-all: [[...slug]]
|
|
117
|
+
let name = s[5..s.len() - 2].to_string();
|
|
118
|
+
Segment::OptionalCatchAll(name)
|
|
119
|
+
} else if s.starts_with("[...") && s.ends_with(']') {
|
|
120
|
+
// Catch-all: [...path]
|
|
121
|
+
let name = s[4..s.len() - 1].to_string();
|
|
122
|
+
Segment::CatchAll(name)
|
|
123
|
+
} else if s.starts_with('[') && s.ends_with(']') {
|
|
124
|
+
// Dynamic: [slug]
|
|
125
|
+
let name = s[1..s.len() - 1].to_string();
|
|
126
|
+
Segment::Dynamic(name)
|
|
127
|
+
} else {
|
|
128
|
+
// Static
|
|
129
|
+
Segment::Static(s.to_string())
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
.collect()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Check if a segment is a route group (parentheses)
|
|
136
|
+
fn is_route_group(segment: &str) -> bool {
|
|
137
|
+
segment.starts_with('(') && segment.ends_with(')')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Calculate route specificity for sorting (higher = more specific)
|
|
141
|
+
fn route_specificity(segments: &[Segment]) -> usize {
|
|
142
|
+
let mut score = 0;
|
|
143
|
+
for (i, segment) in segments.iter().enumerate() {
|
|
144
|
+
let position_weight = 1000 - i; // Earlier segments are more important
|
|
145
|
+
score += match segment {
|
|
146
|
+
Segment::Static(_) => position_weight * 10,
|
|
147
|
+
Segment::Dynamic(_) => position_weight * 5,
|
|
148
|
+
Segment::CatchAll(_) => 1,
|
|
149
|
+
Segment::OptionalCatchAll(_) => 0,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
score
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// Match path segments against route segments, returning extracted params
|
|
156
|
+
fn match_segments(
|
|
157
|
+
route_segments: &[Segment],
|
|
158
|
+
path_segments: &[&str],
|
|
159
|
+
) -> Option<HashMap<String, String>> {
|
|
160
|
+
let mut params = HashMap::new();
|
|
161
|
+
let mut path_idx = 0;
|
|
162
|
+
|
|
163
|
+
for (_route_idx, segment) in route_segments.iter().enumerate() {
|
|
164
|
+
match segment {
|
|
165
|
+
Segment::Static(expected) => {
|
|
166
|
+
if path_idx >= path_segments.len() {
|
|
167
|
+
return None;
|
|
168
|
+
}
|
|
169
|
+
if path_segments[path_idx] != expected {
|
|
170
|
+
return None;
|
|
171
|
+
}
|
|
172
|
+
path_idx += 1;
|
|
173
|
+
}
|
|
174
|
+
Segment::Dynamic(name) => {
|
|
175
|
+
if path_idx >= path_segments.len() {
|
|
176
|
+
return None;
|
|
177
|
+
}
|
|
178
|
+
params.insert(name.clone(), path_segments[path_idx].to_string());
|
|
179
|
+
path_idx += 1;
|
|
180
|
+
}
|
|
181
|
+
Segment::CatchAll(name) => {
|
|
182
|
+
if path_idx >= path_segments.len() {
|
|
183
|
+
return None; // Catch-all must match at least one segment
|
|
184
|
+
}
|
|
185
|
+
let remaining: Vec<&str> = path_segments[path_idx..].to_vec();
|
|
186
|
+
params.insert(name.clone(), remaining.join("/"));
|
|
187
|
+
path_idx = path_segments.len();
|
|
188
|
+
}
|
|
189
|
+
Segment::OptionalCatchAll(name) => {
|
|
190
|
+
// Optional catch-all can match zero or more segments
|
|
191
|
+
if path_idx < path_segments.len() {
|
|
192
|
+
let remaining: Vec<&str> = path_segments[path_idx..].to_vec();
|
|
193
|
+
params.insert(name.clone(), remaining.join("/"));
|
|
194
|
+
path_idx = path_segments.len();
|
|
195
|
+
}
|
|
196
|
+
// If no more segments, that's fine - it's optional
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// All path segments must be consumed (unless we had a catch-all)
|
|
202
|
+
if path_idx == path_segments.len() {
|
|
203
|
+
Some(params)
|
|
204
|
+
} else {
|
|
205
|
+
None
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Resolve session ID template with actual params
|
|
210
|
+
fn resolve_session_id(template: &str, params: &HashMap<String, String>) -> String {
|
|
211
|
+
let mut result = template.to_string();
|
|
212
|
+
for (key, value) in params {
|
|
213
|
+
result = result.replace(&format!("${}", key), value);
|
|
214
|
+
}
|
|
215
|
+
result
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#[cfg(test)]
|
|
219
|
+
mod tests {
|
|
220
|
+
use super::*;
|
|
221
|
+
|
|
222
|
+
#[test]
|
|
223
|
+
fn test_static_route() {
|
|
224
|
+
let mut router = AeonRouter::new();
|
|
225
|
+
router.add_route(RouteDefinition::new(
|
|
226
|
+
"/about".to_string(),
|
|
227
|
+
"about".to_string(),
|
|
228
|
+
"AboutPage".to_string(),
|
|
229
|
+
None,
|
|
230
|
+
false,
|
|
231
|
+
));
|
|
232
|
+
|
|
233
|
+
let result = router.match_route("/about");
|
|
234
|
+
assert!(result.is_some());
|
|
235
|
+
assert_eq!(result.unwrap().resolved_session_id(), "about");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#[test]
|
|
239
|
+
fn test_dynamic_route() {
|
|
240
|
+
let mut router = AeonRouter::new();
|
|
241
|
+
router.add_route(RouteDefinition::new(
|
|
242
|
+
"/blog/[slug]".to_string(),
|
|
243
|
+
"blog-$slug".to_string(),
|
|
244
|
+
"BlogPost".to_string(),
|
|
245
|
+
None,
|
|
246
|
+
true,
|
|
247
|
+
));
|
|
248
|
+
|
|
249
|
+
let result = router.match_route("/blog/hello-world");
|
|
250
|
+
assert!(result.is_some());
|
|
251
|
+
let m = result.unwrap();
|
|
252
|
+
assert_eq!(m.resolved_session_id(), "blog-hello-world");
|
|
253
|
+
assert_eq!(m.get_param("slug"), Some("hello-world".to_string()));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#[test]
|
|
257
|
+
fn test_catch_all_route() {
|
|
258
|
+
let mut router = AeonRouter::new();
|
|
259
|
+
router.add_route(RouteDefinition::new(
|
|
260
|
+
"/api/[...path]".to_string(),
|
|
261
|
+
"api-$path".to_string(),
|
|
262
|
+
"ApiHandler".to_string(),
|
|
263
|
+
None,
|
|
264
|
+
false,
|
|
265
|
+
));
|
|
266
|
+
|
|
267
|
+
let result = router.match_route("/api/users/123/posts");
|
|
268
|
+
assert!(result.is_some());
|
|
269
|
+
let m = result.unwrap();
|
|
270
|
+
assert_eq!(m.get_param("path"), Some("users/123/posts".to_string()));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#[test]
|
|
274
|
+
fn test_route_specificity() {
|
|
275
|
+
let mut router = AeonRouter::new();
|
|
276
|
+
|
|
277
|
+
// Add routes in random order
|
|
278
|
+
router.add_route(RouteDefinition::new(
|
|
279
|
+
"/blog/[slug]".to_string(),
|
|
280
|
+
"blog-$slug".to_string(),
|
|
281
|
+
"BlogPost".to_string(),
|
|
282
|
+
None,
|
|
283
|
+
true,
|
|
284
|
+
));
|
|
285
|
+
router.add_route(RouteDefinition::new(
|
|
286
|
+
"/blog/featured".to_string(),
|
|
287
|
+
"blog-featured".to_string(),
|
|
288
|
+
"FeaturedPost".to_string(),
|
|
289
|
+
None,
|
|
290
|
+
true,
|
|
291
|
+
));
|
|
292
|
+
|
|
293
|
+
// Static route should match before dynamic
|
|
294
|
+
let result = router.match_route("/blog/featured");
|
|
295
|
+
assert!(result.is_some());
|
|
296
|
+
assert_eq!(result.unwrap().resolved_session_id(), "blog-featured");
|
|
297
|
+
}
|
|
298
|
+
}
|