@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,192 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ export const memory: WebAssembly.Memory;
4
+ export const __wbg_assetmanifest_free: (a: number, b: number) => void;
5
+ export const __wbg_cssmanifest_free: (a: number, b: number) => void;
6
+ export const __wbg_fontmanifest_free: (a: number, b: number) => void;
7
+ export const __wbg_rendercontext_free: (a: number, b: number) => void;
8
+ export const assetmanifest_from_json: (
9
+ a: number,
10
+ b: number,
11
+ ) => [number, number, number];
12
+ export const assetmanifest_get_data_uri: (
13
+ a: number,
14
+ b: number,
15
+ c: number,
16
+ ) => [number, number];
17
+ export const assetmanifest_new: () => number;
18
+ export const cssmanifest_critical: (a: number) => [number, number];
19
+ export const cssmanifest_from_json: (
20
+ a: number,
21
+ b: number,
22
+ ) => [number, number, number];
23
+ export const cssmanifest_new: (a: number, b: number) => number;
24
+ export const extract_css_classes: (a: number, b: number) => [number, number];
25
+ export const fontmanifest_font_face_css: (a: number) => [number, number];
26
+ export const fontmanifest_from_json: (
27
+ a: number,
28
+ b: number,
29
+ ) => [number, number, number];
30
+ export const fontmanifest_new: () => number;
31
+ export const generate_css_for_classes: (
32
+ a: number,
33
+ b: number,
34
+ c: number,
35
+ d: number,
36
+ ) => [number, number];
37
+ export const generate_hydration_script: (
38
+ a: number,
39
+ b: number,
40
+ c: number,
41
+ d: number,
42
+ ) => [number, number];
43
+ export const render_page: (
44
+ a: number,
45
+ b: number,
46
+ c: number,
47
+ d: number,
48
+ e: number,
49
+ f: number,
50
+ g: number,
51
+ h: number,
52
+ i: number,
53
+ j: number,
54
+ k: number,
55
+ l: number,
56
+ ) => [number, number];
57
+ export const render_tree_to_html: (a: number, b: number) => [number, number];
58
+ export const rendercontext_get_collected_classes: (
59
+ a: number,
60
+ ) => [number, number];
61
+ export const rendercontext_get_interactive_nodes: (
62
+ a: number,
63
+ ) => [number, number];
64
+ export const rendercontext_new: (
65
+ a: number,
66
+ b: number,
67
+ c: number,
68
+ d: number,
69
+ e: number,
70
+ f: number,
71
+ ) => [number, number, number];
72
+ export const resolve_assets: (
73
+ a: number,
74
+ b: number,
75
+ c: number,
76
+ d: number,
77
+ ) => [number, number];
78
+ export const __wbg_componentregistry_free: (a: number, b: number) => void;
79
+ export const __wbg_treediff_free: (a: number, b: number) => void;
80
+ export const apply_patch: (
81
+ a: number,
82
+ b: number,
83
+ c: number,
84
+ d: number,
85
+ ) => [number, number];
86
+ export const componentregistry_has: (a: number, b: number, c: number) => number;
87
+ export const componentregistry_list: (a: number) => [number, number];
88
+ export const componentregistry_new: () => number;
89
+ export const componentregistry_register: (
90
+ a: number,
91
+ b: number,
92
+ c: number,
93
+ ) => void;
94
+ export const componentregistry_register_many: (
95
+ a: number,
96
+ b: number,
97
+ c: number,
98
+ ) => void;
99
+ export const diff_trees: (
100
+ a: number,
101
+ b: number,
102
+ c: number,
103
+ d: number,
104
+ ) => [number, number];
105
+ export const treediff_change_type: (a: number) => [number, number];
106
+ export const treediff_new: (
107
+ a: number,
108
+ b: number,
109
+ c: number,
110
+ d: number,
111
+ e: number,
112
+ f: number,
113
+ g: number,
114
+ h: number,
115
+ ) => number;
116
+ export const treediff_new_value: (a: number) => [number, number];
117
+ export const treediff_old_value: (a: number) => [number, number];
118
+ export const treediff_path: (a: number) => [number, number];
119
+ export const treediff_to_json: (a: number) => [number, number];
120
+ export const __wbg_routedefinition_free: (a: number, b: number) => void;
121
+ export const __wbg_routematch_free: (a: number, b: number) => void;
122
+ export const __wbg_serializedcomponent_free: (a: number, b: number) => void;
123
+ export const init: () => void;
124
+ export const routedefinition_component_id: (a: number) => [number, number];
125
+ export const routedefinition_is_aeon: (a: number) => number;
126
+ export const routedefinition_layout: (a: number) => [number, number];
127
+ export const routedefinition_new: (
128
+ a: number,
129
+ b: number,
130
+ c: number,
131
+ d: number,
132
+ e: number,
133
+ f: number,
134
+ g: number,
135
+ h: number,
136
+ i: number,
137
+ ) => number;
138
+ export const routedefinition_pattern: (a: number) => [number, number];
139
+ export const routedefinition_session_id: (a: number) => [number, number];
140
+ export const routematch_get_param: (
141
+ a: number,
142
+ b: number,
143
+ c: number,
144
+ ) => [number, number];
145
+ export const routematch_params_json: (a: number) => [number, number];
146
+ export const routematch_resolved_session_id: (a: number) => [number, number];
147
+ export const routematch_route: (a: number) => number;
148
+ export const serializedcomponent_add_component_child: (
149
+ a: number,
150
+ b: number,
151
+ ) => void;
152
+ export const serializedcomponent_add_text_child: (
153
+ a: number,
154
+ b: number,
155
+ c: number,
156
+ ) => void;
157
+ export const serializedcomponent_component_type: (
158
+ a: number,
159
+ ) => [number, number];
160
+ export const serializedcomponent_from_json: (
161
+ a: number,
162
+ b: number,
163
+ ) => [number, number, number];
164
+ export const serializedcomponent_new: (
165
+ a: number,
166
+ b: number,
167
+ c: number,
168
+ d: number,
169
+ ) => number;
170
+ export const serializedcomponent_props: (a: number) => [number, number];
171
+ export const serializedcomponent_to_json: (a: number) => [number, number];
172
+ export const __wbg_aeonrouter_free: (a: number, b: number) => void;
173
+ export const aeonrouter_add_route: (a: number, b: number) => void;
174
+ export const aeonrouter_get_routes_json: (a: number) => [number, number];
175
+ export const aeonrouter_has_route: (a: number, b: number, c: number) => number;
176
+ export const aeonrouter_match_route: (
177
+ a: number,
178
+ b: number,
179
+ c: number,
180
+ ) => number;
181
+ export const aeonrouter_new: () => number;
182
+ export const __wbindgen_free: (a: number, b: number, c: number) => void;
183
+ export const __wbindgen_malloc: (a: number, b: number) => number;
184
+ export const __wbindgen_realloc: (
185
+ a: number,
186
+ b: number,
187
+ c: number,
188
+ d: number,
189
+ ) => number;
190
+ export const __wbindgen_externrefs: WebAssembly.Table;
191
+ export const __externref_table_dealloc: (a: number) => void;
192
+ export const __wbindgen_start: () => void;
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "aeon-pages-runtime",
3
+ "type": "module",
4
+ "description": "WASM runtime for @affectively/aeon-pages - lightweight page routing and hydration",
5
+ "version": "0.1.0",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/affectively/aeon-pages"
10
+ },
11
+ "files": [
12
+ "aeon_pages_runtime_bg.wasm",
13
+ "aeon_pages_runtime.js",
14
+ "aeon_pages_runtime.d.ts"
15
+ ],
16
+ "main": "aeon_pages_runtime.js",
17
+ "types": "aeon_pages_runtime.d.ts",
18
+ "sideEffects": [
19
+ "./snippets/*"
20
+ ]
21
+ }
@@ -0,0 +1,352 @@
1
+ //! Component Hydration for Aeon Pages
2
+ //!
3
+ //! Handles serialization and deserialization of React component trees
4
+ //! for storage in Aeon sessions and rendering on client/server.
5
+
6
+ use wasm_bindgen::prelude::*;
7
+ use serde::{Deserialize, Serialize};
8
+ use std::collections::HashMap;
9
+
10
+ /// Component registry - maps component names to their render functions
11
+ #[wasm_bindgen]
12
+ pub struct ComponentRegistry {
13
+ /// Registered component names (actual render functions are in JS)
14
+ components: HashMap<String, bool>,
15
+ }
16
+
17
+ #[wasm_bindgen]
18
+ impl ComponentRegistry {
19
+ #[wasm_bindgen(constructor)]
20
+ pub fn new() -> Self {
21
+ Self {
22
+ components: HashMap::new(),
23
+ }
24
+ }
25
+
26
+ /// Register a component as available for rendering
27
+ pub fn register(&mut self, name: &str) {
28
+ self.components.insert(name.to_string(), true);
29
+ }
30
+
31
+ /// Check if a component is registered
32
+ pub fn has(&self, name: &str) -> bool {
33
+ self.components.contains_key(name)
34
+ }
35
+
36
+ /// Get all registered component names
37
+ pub fn list(&self) -> String {
38
+ let names: Vec<&String> = self.components.keys().collect();
39
+ serde_json::to_string(&names).unwrap_or_else(|_| "[]".to_string())
40
+ }
41
+
42
+ /// Register multiple components at once (from JSON array)
43
+ pub fn register_many(&mut self, names_json: &str) {
44
+ if let Ok(names) = serde_json::from_str::<Vec<String>>(names_json) {
45
+ for name in names {
46
+ self.register(&name);
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ impl Default for ComponentRegistry {
53
+ fn default() -> Self {
54
+ Self::new()
55
+ }
56
+ }
57
+
58
+ /// Diff result for component tree changes
59
+ #[wasm_bindgen]
60
+ #[derive(Clone, Debug, Serialize, Deserialize)]
61
+ pub struct TreeDiff {
62
+ /// Path to the changed node (e.g., "children.0.props.text")
63
+ path: String,
64
+ /// Type of change: "add", "remove", "update"
65
+ change_type: String,
66
+ /// Old value (JSON)
67
+ old_value: Option<String>,
68
+ /// New value (JSON)
69
+ new_value: Option<String>,
70
+ }
71
+
72
+ #[wasm_bindgen]
73
+ impl TreeDiff {
74
+ #[wasm_bindgen(constructor)]
75
+ pub fn new(
76
+ path: String,
77
+ change_type: String,
78
+ old_value: Option<String>,
79
+ new_value: Option<String>,
80
+ ) -> Self {
81
+ Self {
82
+ path,
83
+ change_type,
84
+ old_value,
85
+ new_value,
86
+ }
87
+ }
88
+
89
+ /// Get the path
90
+ #[wasm_bindgen(getter)]
91
+ pub fn path(&self) -> String {
92
+ self.path.clone()
93
+ }
94
+
95
+ /// Get the change type
96
+ #[wasm_bindgen(getter)]
97
+ pub fn change_type(&self) -> String {
98
+ self.change_type.clone()
99
+ }
100
+
101
+ /// Get the old value
102
+ #[wasm_bindgen(getter)]
103
+ pub fn old_value(&self) -> Option<String> {
104
+ self.old_value.clone()
105
+ }
106
+
107
+ /// Get the new value
108
+ #[wasm_bindgen(getter)]
109
+ pub fn new_value(&self) -> Option<String> {
110
+ self.new_value.clone()
111
+ }
112
+
113
+ pub fn to_json(&self) -> String {
114
+ serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string())
115
+ }
116
+ }
117
+
118
+ /// Compute diff between two component trees
119
+ #[wasm_bindgen]
120
+ pub fn diff_trees(old_json: &str, new_json: &str) -> String {
121
+ let diffs = compute_diff(old_json, new_json, "");
122
+ serde_json::to_string(&diffs).unwrap_or_else(|_| "[]".to_string())
123
+ }
124
+
125
+ /// Internal diff computation
126
+ fn compute_diff(old_json: &str, new_json: &str, path: &str) -> Vec<TreeDiff> {
127
+ let mut diffs = Vec::new();
128
+
129
+ let old: serde_json::Value = serde_json::from_str(old_json).unwrap_or(serde_json::Value::Null);
130
+ let new: serde_json::Value = serde_json::from_str(new_json).unwrap_or(serde_json::Value::Null);
131
+
132
+ diff_values(&old, &new, path, &mut diffs);
133
+ diffs
134
+ }
135
+
136
+ fn diff_values(old: &serde_json::Value, new: &serde_json::Value, path: &str, diffs: &mut Vec<TreeDiff>) {
137
+ use serde_json::Value;
138
+
139
+ match (old, new) {
140
+ (Value::Null, Value::Null) => {}
141
+ (Value::Null, _) => {
142
+ diffs.push(TreeDiff::new(
143
+ path.to_string(),
144
+ "add".to_string(),
145
+ None,
146
+ Some(new.to_string()),
147
+ ));
148
+ }
149
+ (_, Value::Null) => {
150
+ diffs.push(TreeDiff::new(
151
+ path.to_string(),
152
+ "remove".to_string(),
153
+ Some(old.to_string()),
154
+ None,
155
+ ));
156
+ }
157
+ (Value::Object(old_map), Value::Object(new_map)) => {
158
+ // Check for removed keys
159
+ for key in old_map.keys() {
160
+ if !new_map.contains_key(key) {
161
+ let child_path = if path.is_empty() {
162
+ key.clone()
163
+ } else {
164
+ format!("{}.{}", path, key)
165
+ };
166
+ diffs.push(TreeDiff::new(
167
+ child_path,
168
+ "remove".to_string(),
169
+ Some(old_map[key].to_string()),
170
+ None,
171
+ ));
172
+ }
173
+ }
174
+ // Check for added/changed keys
175
+ for (key, new_val) in new_map {
176
+ let child_path = if path.is_empty() {
177
+ key.clone()
178
+ } else {
179
+ format!("{}.{}", path, key)
180
+ };
181
+ if let Some(old_val) = old_map.get(key) {
182
+ diff_values(old_val, new_val, &child_path, diffs);
183
+ } else {
184
+ diffs.push(TreeDiff::new(
185
+ child_path,
186
+ "add".to_string(),
187
+ None,
188
+ Some(new_val.to_string()),
189
+ ));
190
+ }
191
+ }
192
+ }
193
+ (Value::Array(old_arr), Value::Array(new_arr)) => {
194
+ let max_len = old_arr.len().max(new_arr.len());
195
+ for i in 0..max_len {
196
+ let child_path = if path.is_empty() {
197
+ format!("{}", i)
198
+ } else {
199
+ format!("{}.{}", path, i)
200
+ };
201
+ let old_item = old_arr.get(i).unwrap_or(&Value::Null);
202
+ let new_item = new_arr.get(i).unwrap_or(&Value::Null);
203
+ diff_values(old_item, new_item, &child_path, diffs);
204
+ }
205
+ }
206
+ _ => {
207
+ if old != new {
208
+ diffs.push(TreeDiff::new(
209
+ path.to_string(),
210
+ "update".to_string(),
211
+ Some(old.to_string()),
212
+ Some(new.to_string()),
213
+ ));
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ /// Apply a patch to a component tree
220
+ #[wasm_bindgen]
221
+ pub fn apply_patch(tree_json: &str, patch_json: &str) -> String {
222
+ let mut tree: serde_json::Value = serde_json::from_str(tree_json)
223
+ .unwrap_or(serde_json::Value::Null);
224
+
225
+ let patches: Vec<TreeDiff> = serde_json::from_str(patch_json)
226
+ .unwrap_or_default();
227
+
228
+ for patch in patches {
229
+ apply_single_patch(&mut tree, &patch);
230
+ }
231
+
232
+ serde_json::to_string(&tree).unwrap_or_else(|_| "{}".to_string())
233
+ }
234
+
235
+ fn apply_single_patch(tree: &mut serde_json::Value, patch: &TreeDiff) {
236
+ let path_parts: Vec<&str> = patch.path.split('.').filter(|s| !s.is_empty()).collect();
237
+
238
+ if path_parts.is_empty() {
239
+ // Root-level change
240
+ match patch.change_type.as_str() {
241
+ "add" | "update" => {
242
+ if let Some(new_val) = &patch.new_value {
243
+ if let Ok(val) = serde_json::from_str(new_val) {
244
+ *tree = val;
245
+ }
246
+ }
247
+ }
248
+ "remove" => {
249
+ *tree = serde_json::Value::Null;
250
+ }
251
+ _ => {}
252
+ }
253
+ return;
254
+ }
255
+
256
+ // Navigate to parent
257
+ let mut current = tree;
258
+ for (i, part) in path_parts.iter().enumerate() {
259
+ if i == path_parts.len() - 1 {
260
+ // This is the target
261
+ match patch.change_type.as_str() {
262
+ "add" | "update" => {
263
+ if let Some(new_val) = &patch.new_value {
264
+ if let Ok(val) = serde_json::from_str(new_val) {
265
+ if let Ok(idx) = part.parse::<usize>() {
266
+ if let Some(arr) = current.as_array_mut() {
267
+ if idx < arr.len() {
268
+ arr[idx] = val;
269
+ } else {
270
+ arr.push(val);
271
+ }
272
+ }
273
+ } else if let Some(obj) = current.as_object_mut() {
274
+ obj.insert(part.to_string(), val);
275
+ }
276
+ }
277
+ }
278
+ }
279
+ "remove" => {
280
+ if let Ok(idx) = part.parse::<usize>() {
281
+ if let Some(arr) = current.as_array_mut() {
282
+ if idx < arr.len() {
283
+ arr.remove(idx);
284
+ }
285
+ }
286
+ } else if let Some(obj) = current.as_object_mut() {
287
+ obj.remove(*part);
288
+ }
289
+ }
290
+ _ => {}
291
+ }
292
+ } else {
293
+ // Navigate deeper
294
+ if let Ok(idx) = part.parse::<usize>() {
295
+ if let Some(arr) = current.as_array_mut() {
296
+ if idx < arr.len() {
297
+ current = &mut arr[idx];
298
+ } else {
299
+ return;
300
+ }
301
+ } else {
302
+ return;
303
+ }
304
+ } else if let Some(obj) = current.as_object_mut() {
305
+ if let Some(val) = obj.get_mut(*part) {
306
+ current = val;
307
+ } else {
308
+ return;
309
+ }
310
+ } else {
311
+ return;
312
+ }
313
+ }
314
+ }
315
+ }
316
+
317
+ #[cfg(test)]
318
+ mod tests {
319
+ use super::*;
320
+
321
+ #[test]
322
+ fn test_component_registry() {
323
+ let mut registry = ComponentRegistry::new();
324
+ registry.register("Button");
325
+ registry.register("Card");
326
+
327
+ assert!(registry.has("Button"));
328
+ assert!(registry.has("Card"));
329
+ assert!(!registry.has("Unknown"));
330
+ }
331
+
332
+ #[test]
333
+ fn test_diff_simple() {
334
+ let old = r#"{"text": "Hello"}"#;
335
+ let new = r#"{"text": "World"}"#;
336
+
337
+ let diffs = compute_diff(old, new, "");
338
+ assert_eq!(diffs.len(), 1);
339
+ assert_eq!(diffs[0].path, "text");
340
+ assert_eq!(diffs[0].change_type, "update");
341
+ }
342
+
343
+ #[test]
344
+ fn test_diff_nested() {
345
+ let old = r#"{"props": {"className": "old"}}"#;
346
+ let new = r#"{"props": {"className": "new"}}"#;
347
+
348
+ let diffs = compute_diff(old, new, "");
349
+ assert_eq!(diffs.len(), 1);
350
+ assert_eq!(diffs[0].path, "props.className");
351
+ }
352
+ }