@useavalon/avalon 0.1.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 (159) hide show
  1. package/README.md +54 -0
  2. package/mod.ts +301 -0
  3. package/package.json +85 -0
  4. package/src/build/README.md +310 -0
  5. package/src/build/integration-bundler-plugin.ts +116 -0
  6. package/src/build/integration-config.ts +168 -0
  7. package/src/build/integration-detection-plugin.ts +117 -0
  8. package/src/build/integration-resolver-plugin.ts +90 -0
  9. package/src/build/island-manifest.ts +269 -0
  10. package/src/build/island-types-generator.ts +476 -0
  11. package/src/build/mdx-island-transform.ts +464 -0
  12. package/src/build/mdx-plugin.ts +98 -0
  13. package/src/build/page-island-transform.ts +598 -0
  14. package/src/build/prop-extractors/index.ts +21 -0
  15. package/src/build/prop-extractors/lit.ts +140 -0
  16. package/src/build/prop-extractors/qwik.ts +16 -0
  17. package/src/build/prop-extractors/solid.ts +125 -0
  18. package/src/build/prop-extractors/svelte.ts +194 -0
  19. package/src/build/prop-extractors/vue.ts +111 -0
  20. package/src/build/sidecar-file-manager.ts +104 -0
  21. package/src/build/sidecar-renderer.ts +30 -0
  22. package/src/client/adapters/index.ts +13 -0
  23. package/src/client/adapters/lit-adapter.ts +654 -0
  24. package/src/client/adapters/preact-adapter.ts +331 -0
  25. package/src/client/adapters/qwik-adapter.ts +345 -0
  26. package/src/client/adapters/react-adapter.ts +353 -0
  27. package/src/client/adapters/solid-adapter.ts +451 -0
  28. package/src/client/adapters/svelte-adapter.ts +524 -0
  29. package/src/client/adapters/vue-adapter.ts +467 -0
  30. package/src/client/components.ts +35 -0
  31. package/src/client/css-hmr-handler.ts +344 -0
  32. package/src/client/framework-adapter.ts +462 -0
  33. package/src/client/hmr-coordinator.ts +396 -0
  34. package/src/client/hmr-error-overlay.js +533 -0
  35. package/src/client/main.js +816 -0
  36. package/src/client/tests/css-hmr-handler.test.ts +360 -0
  37. package/src/client/tests/framework-adapter.test.ts +519 -0
  38. package/src/client/tests/hmr-coordinator.test.ts +176 -0
  39. package/src/client/tests/hydration-option-parsing.test.ts +107 -0
  40. package/src/client/tests/lit-adapter.test.ts +427 -0
  41. package/src/client/tests/preact-adapter.test.ts +353 -0
  42. package/src/client/tests/qwik-adapter.test.ts +343 -0
  43. package/src/client/tests/react-adapter.test.ts +317 -0
  44. package/src/client/tests/solid-adapter.test.ts +396 -0
  45. package/src/client/tests/svelte-adapter.test.ts +387 -0
  46. package/src/client/tests/vue-adapter.test.ts +407 -0
  47. package/src/client/types/framework-runtime.d.ts +68 -0
  48. package/src/client/types/vite-hmr.d.ts +46 -0
  49. package/src/client/types/vite-virtual-modules.d.ts +60 -0
  50. package/src/components/Image.tsx +123 -0
  51. package/src/components/IslandErrorBoundary.tsx +145 -0
  52. package/src/components/LayoutDataErrorBoundary.tsx +141 -0
  53. package/src/components/LayoutErrorBoundary.tsx +127 -0
  54. package/src/components/PersistentIsland.tsx +52 -0
  55. package/src/components/StreamingErrorBoundary.tsx +233 -0
  56. package/src/components/StreamingLayout.tsx +538 -0
  57. package/src/components/tests/component-analyzer.test.ts +96 -0
  58. package/src/components/tests/component-detection.test.ts +347 -0
  59. package/src/components/tests/persistent-islands.test.ts +398 -0
  60. package/src/core/components/component-analyzer.ts +192 -0
  61. package/src/core/components/component-detection.ts +508 -0
  62. package/src/core/components/enhanced-framework-detector.ts +500 -0
  63. package/src/core/components/framework-registry.ts +563 -0
  64. package/src/core/components/tests/enhanced-framework-detector.test.ts +577 -0
  65. package/src/core/components/tests/framework-registry.test.ts +465 -0
  66. package/src/core/content/mdx-processor.ts +46 -0
  67. package/src/core/integrations/README.md +282 -0
  68. package/src/core/integrations/index.ts +19 -0
  69. package/src/core/integrations/loader.ts +125 -0
  70. package/src/core/integrations/registry.ts +195 -0
  71. package/src/core/islands/island-persistence.ts +325 -0
  72. package/src/core/islands/island-state-serializer.ts +258 -0
  73. package/src/core/islands/persistent-island-context.tsx +80 -0
  74. package/src/core/islands/use-persistent-state.ts +68 -0
  75. package/src/core/layout/enhanced-layout-resolver.ts +322 -0
  76. package/src/core/layout/layout-cache-manager.ts +485 -0
  77. package/src/core/layout/layout-composer.ts +357 -0
  78. package/src/core/layout/layout-data-loader.ts +516 -0
  79. package/src/core/layout/layout-discovery.ts +243 -0
  80. package/src/core/layout/layout-matcher.ts +299 -0
  81. package/src/core/layout/layout-types.ts +110 -0
  82. package/src/core/layout/tests/enhanced-layout-resolver.test.ts +477 -0
  83. package/src/core/layout/tests/layout-cache-optimization.test.ts +149 -0
  84. package/src/core/layout/tests/layout-composer.test.ts +486 -0
  85. package/src/core/layout/tests/layout-data-loader.test.ts +443 -0
  86. package/src/core/layout/tests/layout-discovery.test.ts +253 -0
  87. package/src/core/layout/tests/layout-matcher.test.ts +480 -0
  88. package/src/core/modules/framework-module-resolver.ts +273 -0
  89. package/src/core/modules/tests/framework-module-resolver.test.ts +263 -0
  90. package/src/core/modules/tests/module-resolution-integration.test.ts +117 -0
  91. package/src/islands/component-analysis.ts +213 -0
  92. package/src/islands/css-utils.ts +565 -0
  93. package/src/islands/discovery/index.ts +80 -0
  94. package/src/islands/discovery/registry.ts +340 -0
  95. package/src/islands/discovery/resolver.ts +477 -0
  96. package/src/islands/discovery/scanner.ts +386 -0
  97. package/src/islands/discovery/tests/island-discovery.test.ts +881 -0
  98. package/src/islands/discovery/types.ts +117 -0
  99. package/src/islands/discovery/validator.ts +544 -0
  100. package/src/islands/discovery/watcher.ts +368 -0
  101. package/src/islands/framework-detection.ts +428 -0
  102. package/src/islands/integration-loader.ts +490 -0
  103. package/src/islands/island.tsx +565 -0
  104. package/src/islands/render-cache.ts +550 -0
  105. package/src/islands/types.ts +80 -0
  106. package/src/islands/universal-css-collector.ts +157 -0
  107. package/src/islands/universal-head-collector.ts +137 -0
  108. package/src/layout-system.d.ts +592 -0
  109. package/src/layout-system.ts +218 -0
  110. package/src/middleware/__tests__/discovery.test.ts +107 -0
  111. package/src/middleware/discovery.ts +268 -0
  112. package/src/middleware/executor.ts +315 -0
  113. package/src/middleware/index.ts +76 -0
  114. package/src/middleware/types.ts +99 -0
  115. package/src/nitro/build-config.ts +576 -0
  116. package/src/nitro/config.ts +483 -0
  117. package/src/nitro/error-handler.ts +636 -0
  118. package/src/nitro/index.ts +173 -0
  119. package/src/nitro/island-manifest.ts +584 -0
  120. package/src/nitro/middleware-adapter.ts +260 -0
  121. package/src/nitro/renderer.ts +1458 -0
  122. package/src/nitro/route-discovery.ts +439 -0
  123. package/src/nitro/types.ts +321 -0
  124. package/src/render/collect-css.ts +198 -0
  125. package/src/render/error-pages.ts +79 -0
  126. package/src/render/isolated-ssr-renderer.ts +654 -0
  127. package/src/render/ssr.ts +1030 -0
  128. package/src/schemas/api.ts +30 -0
  129. package/src/schemas/core.ts +64 -0
  130. package/src/schemas/index.ts +212 -0
  131. package/src/schemas/layout.ts +279 -0
  132. package/src/schemas/routing/index.ts +38 -0
  133. package/src/schemas/routing.ts +376 -0
  134. package/src/types/as-island.ts +20 -0
  135. package/src/types/image.d.ts +106 -0
  136. package/src/types/index.d.ts +22 -0
  137. package/src/types/island-jsx.d.ts +33 -0
  138. package/src/types/island-prop.d.ts +20 -0
  139. package/src/types/layout.ts +285 -0
  140. package/src/types/mdx.d.ts +6 -0
  141. package/src/types/routing.ts +555 -0
  142. package/src/types/tests/layout-types.test.ts +197 -0
  143. package/src/types/types.ts +5 -0
  144. package/src/types/urlpattern.d.ts +49 -0
  145. package/src/types/vite-env.d.ts +11 -0
  146. package/src/utils/dev-logger.ts +299 -0
  147. package/src/utils/fs.ts +151 -0
  148. package/src/vite-plugin/auto-discover.ts +551 -0
  149. package/src/vite-plugin/config.ts +266 -0
  150. package/src/vite-plugin/errors.ts +127 -0
  151. package/src/vite-plugin/image-optimization.ts +151 -0
  152. package/src/vite-plugin/integration-activator.ts +126 -0
  153. package/src/vite-plugin/island-sidecar-plugin.ts +176 -0
  154. package/src/vite-plugin/module-discovery.ts +189 -0
  155. package/src/vite-plugin/nitro-integration.ts +1334 -0
  156. package/src/vite-plugin/plugin.ts +329 -0
  157. package/src/vite-plugin/tests/image-optimization.test.ts +54 -0
  158. package/src/vite-plugin/types.ts +327 -0
  159. package/src/vite-plugin/validation.ts +228 -0
@@ -0,0 +1,398 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { IslandPersistence } from '../../core/islands/island-persistence.ts';
3
+ import { IslandStateSerializer } from '../../core/islands/island-state-serializer.ts';
4
+ import { createPersistentIslandContext } from '../../core/islands/persistent-island-context.tsx';
5
+ import type { IslandState } from '../../schemas/layout.ts';
6
+
7
+ // Mock Storage for testing
8
+ class MockStorage implements Storage {
9
+ private data: Map<string, string> = new Map();
10
+
11
+ get length(): number {
12
+ return this.data.size;
13
+ }
14
+
15
+ clear(): void {
16
+ this.data.clear();
17
+ }
18
+
19
+ getItem(key: string): string | null {
20
+ return this.data.get(key) || null;
21
+ }
22
+
23
+ key(index: number): string | null {
24
+ const keys = Array.from(this.data.keys());
25
+ return keys[index] || null;
26
+ }
27
+
28
+ removeItem(key: string): void {
29
+ this.data.delete(key);
30
+ }
31
+
32
+ setItem(key: string, value: string): void {
33
+ this.data.set(key, value);
34
+ }
35
+ }
36
+
37
+ // Mock global window and storage for testing
38
+ const mockStorage = new MockStorage();
39
+ (globalThis as any).window = {
40
+ sessionStorage: mockStorage,
41
+ localStorage: mockStorage,
42
+ };
43
+ (globalThis as any).sessionStorage = mockStorage;
44
+ (globalThis as any).localStorage = mockStorage;
45
+
46
+ describe('IslandPersistence - Basic Operations', () => {
47
+ beforeEach(() => { mockStorage.clear(); });
48
+
49
+ it('should save and load state', () => {
50
+ const persistence = new IslandPersistence();
51
+ const testState: IslandState = { count: 42, name: 'test' };
52
+
53
+ persistence.saveState('test-island', testState);
54
+ const loadedState = persistence.loadState('test-island');
55
+
56
+ expect(loadedState).toEqual(testState);
57
+ });
58
+
59
+ it('should return null for non-existent state', () => {
60
+ const persistence = new IslandPersistence();
61
+ const loadedState = persistence.loadState('non-existent');
62
+
63
+ expect(loadedState).toEqual(null);
64
+ });
65
+
66
+ it('should clear state', () => {
67
+ const persistence = new IslandPersistence();
68
+ const testState: IslandState = { count: 42 };
69
+
70
+ persistence.saveState('test-island', testState);
71
+ expect(persistence.hasState('test-island')).toEqual(true);
72
+
73
+ persistence.clearState('test-island');
74
+ expect(persistence.hasState('test-island')).toEqual(false);
75
+ expect(persistence.loadState('test-island')).toEqual(null);
76
+ });
77
+
78
+ it('should check if state exists', () => {
79
+ const persistence = new IslandPersistence();
80
+ const testState: IslandState = { count: 42 };
81
+
82
+ expect(persistence.hasState('test-island')).toEqual(false);
83
+
84
+ persistence.saveState('test-island', testState);
85
+ expect(persistence.hasState('test-island')).toEqual(true);
86
+
87
+ persistence.clearState('test-island');
88
+ });
89
+
90
+ it('should get stored IDs', () => {
91
+ const persistence = new IslandPersistence();
92
+
93
+ persistence.saveState('island-1', { count: 1 });
94
+ persistence.saveState('island-2', { count: 2 });
95
+
96
+ const storedIds = persistence.getStoredIds();
97
+ expect(storedIds.sort()).toEqual(['island-1', 'island-2']);
98
+ });
99
+
100
+ it('should clear all states', () => {
101
+ const persistence = new IslandPersistence();
102
+
103
+ persistence.saveState('island-1', { count: 1 });
104
+ persistence.saveState('island-2', { count: 2 });
105
+
106
+ expect(persistence.getStoredIds().length).toEqual(2);
107
+
108
+ persistence.clearAllStates();
109
+ expect(persistence.getStoredIds().length).toEqual(0);
110
+ });
111
+ });
112
+
113
+ describe('IslandPersistence - Configuration', () => {
114
+ beforeEach(() => { mockStorage.clear(); });
115
+
116
+ it('should use custom key prefix', () => {
117
+ const persistence = new IslandPersistence({ keyPrefix: 'custom-prefix' });
118
+ const testState: IslandState = { count: 42 };
119
+
120
+ persistence.saveState('test-island', testState);
121
+
122
+ const config = persistence.getConfig();
123
+ expect(config.keyPrefix).toEqual('custom-prefix');
124
+
125
+ const loadedState = persistence.loadState('test-island');
126
+ expect(loadedState).toEqual(testState);
127
+
128
+ persistence.clearState('test-island');
129
+ });
130
+
131
+ it('should provide storage stats', () => {
132
+ const persistence = new IslandPersistence();
133
+
134
+ persistence.saveState('island-1', { count: 1 });
135
+ persistence.saveState('island-2', { count: 2, name: 'test' });
136
+
137
+ const stats = persistence.getStorageStats();
138
+ expect(stats.islandKeys).toEqual(2);
139
+ expect(stats.estimatedSize > 0).toEqual(true);
140
+ });
141
+ });
142
+
143
+ describe('IslandStateSerializer - Basic Serialization', () => {
144
+ it('should serialize and deserialize basic types', () => {
145
+ const state: IslandState = {
146
+ string: 'hello',
147
+ number: 42,
148
+ boolean: true,
149
+ null: null,
150
+ array: [1, 2, 3],
151
+ object: { nested: 'value' },
152
+ };
153
+
154
+ const serialized = IslandStateSerializer.serialize(state);
155
+ const deserialized = IslandStateSerializer.deserialize(serialized);
156
+
157
+ expect(deserialized).toEqual(state);
158
+ });
159
+
160
+ it('should handle Date objects', () => {
161
+ const date = new Date('2023-01-01T00:00:00.000Z');
162
+ const state: IslandState = { timestamp: date };
163
+
164
+ const serialized = IslandStateSerializer.serialize(state);
165
+ const deserialized = IslandStateSerializer.deserialize(serialized);
166
+
167
+ expect(deserialized.timestamp instanceof Date).toEqual(true);
168
+ expect((deserialized.timestamp as Date).getTime()).toEqual(date.getTime());
169
+ });
170
+
171
+ it('should handle RegExp objects', () => {
172
+ const regex = /test/gi;
173
+ const state: IslandState = { pattern: regex };
174
+
175
+ const serialized = IslandStateSerializer.serialize(state);
176
+ const deserialized = IslandStateSerializer.deserialize(serialized);
177
+
178
+ expect(deserialized.pattern instanceof RegExp).toEqual(true);
179
+ expect((deserialized.pattern as RegExp).source).toEqual(regex.source);
180
+ expect((deserialized.pattern as RegExp).flags).toEqual(regex.flags);
181
+ });
182
+
183
+ it('should handle Map objects', () => {
184
+ const map = new Map([
185
+ ['key1', 'value1'],
186
+ ['key2', 'value2'],
187
+ ]);
188
+ const state: IslandState = { map };
189
+
190
+ const serialized = IslandStateSerializer.serialize(state);
191
+ const deserialized = IslandStateSerializer.deserialize(serialized);
192
+
193
+ expect(deserialized.map instanceof Map).toEqual(true);
194
+ expect((deserialized.map as Map<string, string>).get('key1')).toEqual('value1');
195
+ expect((deserialized.map as Map<string, string>).get('key2')).toEqual('value2');
196
+ });
197
+
198
+ it('should handle Set objects', () => {
199
+ const set = new Set(['value1', 'value2']);
200
+ const state: IslandState = { set };
201
+
202
+ const serialized = IslandStateSerializer.serialize(state);
203
+ const deserialized = IslandStateSerializer.deserialize(serialized);
204
+
205
+ expect(deserialized.set instanceof Set).toEqual(true);
206
+ expect((deserialized.set as Set<string>).has('value1')).toEqual(true);
207
+ expect((deserialized.set as Set<string>).has('value2')).toEqual(true);
208
+ });
209
+
210
+ it('should convert functions to null', () => {
211
+ const state: IslandState = {
212
+ func: () => 'test',
213
+ value: 42,
214
+ };
215
+
216
+ const serialized = IslandStateSerializer.serialize(state);
217
+ const deserialized = IslandStateSerializer.deserialize(serialized);
218
+
219
+ expect(deserialized.func).toEqual(null);
220
+ expect(deserialized.value).toEqual(42);
221
+ });
222
+
223
+ it('should convert undefined to null', () => {
224
+ const state: IslandState = {
225
+ undef: undefined,
226
+ value: 42,
227
+ };
228
+
229
+ const serialized = IslandStateSerializer.serialize(state);
230
+ const deserialized = IslandStateSerializer.deserialize(serialized);
231
+
232
+ expect(deserialized.undef).toEqual(null);
233
+ expect(deserialized.value).toEqual(42);
234
+ });
235
+ });
236
+
237
+ describe('IslandStateSerializer - Validation and Utilities', () => {
238
+ it('should validate serializable state', () => {
239
+ const validState: IslandState = { count: 42, name: 'test' };
240
+ const validation = IslandStateSerializer.validate(validState);
241
+
242
+ expect(validation.valid).toEqual(true);
243
+ expect(validation.errors.length).toEqual(0);
244
+ });
245
+
246
+ it('should calculate state size', () => {
247
+ const state: IslandState = { count: 42, name: 'test' };
248
+ const size = IslandStateSerializer.getSize(state);
249
+
250
+ expect(size.bytes > 0).toEqual(true);
251
+ expect(size.kilobytes > 0).toEqual(true);
252
+ expect(typeof size.readable).toEqual('string');
253
+ });
254
+
255
+ it('should clone state', () => {
256
+ const original: IslandState = {
257
+ count: 42,
258
+ nested: { value: 'test' },
259
+ date: new Date('2023-01-01'),
260
+ };
261
+
262
+ const cloned = IslandStateSerializer.clone(original);
263
+
264
+ expect(IslandStateSerializer.equals(original, cloned)).toEqual(true);
265
+ expect(original === cloned).toEqual(false);
266
+ expect(original.nested === cloned.nested).toEqual(false);
267
+ });
268
+
269
+ it('should compare states for equality', () => {
270
+ const state1: IslandState = { count: 42, name: 'test' };
271
+ const state2: IslandState = { count: 42, name: 'test' };
272
+ const state3: IslandState = { count: 43, name: 'test' };
273
+
274
+ expect(IslandStateSerializer.equals(state1, state2)).toEqual(true);
275
+ expect(IslandStateSerializer.equals(state1, state3)).toEqual(false);
276
+ });
277
+
278
+ it('should sanitize state', () => {
279
+ const state: IslandState = {
280
+ count: 42,
281
+ func: () => 'test',
282
+ undef: undefined,
283
+ date: new Date('2023-01-01'),
284
+ };
285
+
286
+ const sanitized = IslandStateSerializer.sanitize(state);
287
+
288
+ expect(sanitized.count).toEqual(42);
289
+ expect(sanitized.func).toEqual(null);
290
+ expect(sanitized.undef).toEqual(null);
291
+ expect(sanitized.date instanceof Date).toEqual(true);
292
+ });
293
+ });
294
+
295
+ describe('PersistentIslandContext - Context Creation', () => {
296
+ beforeEach(() => { mockStorage.clear(); });
297
+
298
+ it('should create context with save/load/clear functions', () => {
299
+ const context = createPersistentIslandContext('test-island');
300
+
301
+ expect(context.saveState).toBeDefined();
302
+ expect(context.loadState).toBeDefined();
303
+ expect(context.clearState).toBeDefined();
304
+
305
+ expect(typeof context.saveState).toEqual('function');
306
+ expect(typeof context.loadState).toEqual('function');
307
+ expect(typeof context.clearState).toEqual('function');
308
+ });
309
+
310
+ it('should save and load state through context', () => {
311
+ const persistence = new IslandPersistence();
312
+ const context = createPersistentIslandContext('test-island', persistence);
313
+ const testState: IslandState = { count: 42 };
314
+
315
+ context.saveState(testState);
316
+ const loadedState = context.loadState();
317
+
318
+ expect(loadedState).toEqual(testState);
319
+ });
320
+
321
+ it('should clear state through context', () => {
322
+ const persistence = new IslandPersistence();
323
+ const context = createPersistentIslandContext('test-island', persistence);
324
+ const testState: IslandState = { count: 42 };
325
+
326
+ context.saveState(testState);
327
+ expect(context.loadState()).toEqual(testState);
328
+
329
+ context.clearState();
330
+ expect(context.loadState()).toEqual(null);
331
+ });
332
+ });
333
+
334
+ describe('Integration - Complete Persistent Islands Flow', () => {
335
+ beforeEach(() => { mockStorage.clear(); });
336
+
337
+ it('should handle complete save/load/clear cycle', () => {
338
+ const persistence = new IslandPersistence();
339
+ const context = createPersistentIslandContext('integration-test', persistence);
340
+
341
+ const complexState: IslandState = {
342
+ counter: 42,
343
+ user: {
344
+ name: 'John Doe',
345
+ preferences: {
346
+ theme: 'dark',
347
+ notifications: true,
348
+ },
349
+ },
350
+ timestamps: [new Date('2023-01-01'), new Date('2023-01-02')],
351
+ patterns: [/test/gi, /another/i],
352
+ cache: new Map([
353
+ ['key1', { value: 'cached1', expires: new Date('2023-12-31') }],
354
+ ['key2', { value: 'cached2', expires: new Date('2023-12-31') }],
355
+ ]),
356
+ tags: new Set(['tag1', 'tag2', 'tag3']),
357
+ };
358
+
359
+ context.saveState(complexState);
360
+
361
+ expect(persistence.hasState('integration-test')).toEqual(true);
362
+
363
+ const loadedState = context.loadState();
364
+ expect(loadedState).toBeDefined();
365
+
366
+ expect(loadedState!.counter).toEqual(42);
367
+ expect(loadedState!.user.name).toEqual('John Doe');
368
+ expect(loadedState!.timestamps[0] instanceof Date).toEqual(true);
369
+ expect(loadedState!.patterns[0] instanceof RegExp).toEqual(true);
370
+ expect(loadedState!.cache instanceof Map).toEqual(true);
371
+ expect(loadedState!.tags instanceof Set).toEqual(true);
372
+
373
+ const loadedMap = loadedState!.cache as Map<string, any>;
374
+ expect(loadedMap.get('key1').value).toEqual('cached1');
375
+ expect(loadedMap.get('key1').expires instanceof Date).toEqual(true);
376
+
377
+ const loadedSet = loadedState!.tags as Set<string>;
378
+ expect(loadedSet.has('tag1')).toEqual(true);
379
+ expect(loadedSet.has('tag2')).toEqual(true);
380
+ expect(loadedSet.has('tag3')).toEqual(true);
381
+
382
+ context.clearState();
383
+ expect(context.loadState()).toEqual(null);
384
+ expect(persistence.hasState('integration-test')).toEqual(false);
385
+ });
386
+
387
+ it('should handle serialization errors gracefully', () => {
388
+ const persistence = new IslandPersistence();
389
+
390
+ const circularState: any = { count: 42 };
391
+ circularState.self = circularState;
392
+
393
+ persistence.saveState('circular-test', circularState);
394
+
395
+ const loadedState = persistence.loadState('circular-test');
396
+ expect(loadedState).toEqual(null);
397
+ });
398
+ });
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Component Analyzer - High-level interface for component detection system
3
+ *
4
+ * This module provides a convenient interface for analyzing components
5
+ * and making hydration decisions in the SSR system.
6
+ */
7
+
8
+ import {
9
+ analyzeComponent,
10
+ shouldHydrateComponent,
11
+ createComponentMetadata,
12
+ type ComponentAnalysis,
13
+ type DetectionResult,
14
+ type ComponentMetadata,
15
+ } from './component-detection.ts';
16
+
17
+ export interface AnalyzerOptions {
18
+ forceSSROnly?: boolean;
19
+ detectScripts?: boolean;
20
+ suppressWarnings?: boolean;
21
+ logDecisions?: boolean;
22
+ }
23
+
24
+ export interface AnalysisReport {
25
+ metadata: ComponentMetadata;
26
+ decision: DetectionResult;
27
+ analysis: ComponentAnalysis;
28
+ }
29
+
30
+ /**
31
+ * Analyzes a component file and returns comprehensive analysis report
32
+ */
33
+ export async function analyzeComponentFile(filePath: string, options: AnalyzerOptions = {}): Promise<AnalysisReport> {
34
+ try {
35
+ // Read component file
36
+ const { readFile } = await import('node:fs/promises');
37
+ const content = await readFile(filePath, 'utf-8');
38
+
39
+ // Perform analysis
40
+ const analysis = analyzeComponent(filePath, content);
41
+ const decision = shouldHydrateComponent(analysis, options);
42
+ const metadata = createComponentMetadata(filePath, content, analysis);
43
+
44
+ // Log decision if requested
45
+ if (options.logDecisions) {
46
+ logAnalysisDecision(filePath, analysis, decision);
47
+ }
48
+
49
+ return {
50
+ metadata,
51
+ decision,
52
+ analysis,
53
+ };
54
+ } catch (error) {
55
+ const errorMessage = error instanceof Error ? error.message : String(error);
56
+ throw new Error(`Failed to analyze component ${filePath}: ${errorMessage}`);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Analyzes component content directly (for in-memory components)
62
+ */
63
+ export function analyzeComponentContent(
64
+ filePath: string,
65
+ content: string,
66
+ options: AnalyzerOptions = {}
67
+ ): AnalysisReport {
68
+ const analysis = analyzeComponent(filePath, content);
69
+ const decision = shouldHydrateComponent(analysis, options);
70
+ const metadata = createComponentMetadata(filePath, content, analysis);
71
+
72
+ // Log decision if requested
73
+ if (options.logDecisions) {
74
+ logAnalysisDecision(filePath, analysis, decision);
75
+ }
76
+
77
+ return {
78
+ metadata,
79
+ decision,
80
+ analysis,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Batch analyze multiple components
86
+ */
87
+ export async function analyzeComponents(
88
+ filePaths: string[],
89
+ options: AnalyzerOptions = {}
90
+ ): Promise<Map<string, AnalysisReport>> {
91
+ const results = new Map<string, AnalysisReport>();
92
+
93
+ for (const filePath of filePaths) {
94
+ try {
95
+ const report = await analyzeComponentFile(filePath, options);
96
+ results.set(filePath, report);
97
+ } catch (error) {
98
+ const errorMessage = error instanceof Error ? error.message : String(error);
99
+ console.error(`Failed to analyze ${filePath}:`, errorMessage);
100
+ }
101
+ }
102
+
103
+ return results;
104
+ }
105
+
106
+ /**
107
+ * Quick check if a component should be hydrated
108
+ */
109
+ export async function shouldHydrate(filePath: string, options: AnalyzerOptions = {}): Promise<boolean> {
110
+ try {
111
+ const report = await analyzeComponentFile(filePath, options);
112
+ return report.decision.shouldHydrate;
113
+ } catch (error) {
114
+ const errorMessage = error instanceof Error ? error.message : String(error);
115
+ console.error(`Error checking hydration for ${filePath}:`, errorMessage);
116
+ // Default to hydration on error for safety
117
+ return true;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Get component framework type
123
+ */
124
+ export async function getComponentFramework(filePath: string): Promise<ComponentAnalysis['framework']> {
125
+ try {
126
+ const report = await analyzeComponentFile(filePath);
127
+ return report.analysis.framework;
128
+ } catch (error) {
129
+ const errorMessage = error instanceof Error ? error.message : String(error);
130
+ console.error(`Error detecting framework for ${filePath}:`, errorMessage);
131
+ return 'unknown';
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Logs analysis decision for debugging
137
+ */
138
+ function logAnalysisDecision(filePath: string, analysis: ComponentAnalysis, decision: DetectionResult): void {
139
+ const framework = analysis.framework.toUpperCase();
140
+ const strategy = decision.shouldHydrate ? 'HYDRATE' : 'SSR-ONLY';
141
+
142
+ console.log(`[Component Analysis] ${filePath}`);
143
+ console.log(` Framework: ${framework}`);
144
+ console.log(` Has Script: ${analysis.hasScript}`);
145
+ console.log(` Has Hydrate Function: ${analysis.hasHydrateFunction}`);
146
+ console.log(` Strategy: ${strategy}`);
147
+ console.log(` Reason: ${decision.reason}`);
148
+
149
+ if (decision.warnings && decision.warnings.length > 0) {
150
+ console.log(` Warnings:`);
151
+ decision.warnings.forEach(warning => console.log(` - ${warning}`));
152
+ }
153
+
154
+ console.log('');
155
+ }
156
+
157
+ /**
158
+ * Generate summary statistics for a batch of components
159
+ */
160
+ export function generateAnalysisSummary(reports: Map<string, AnalysisReport>): {
161
+ total: number;
162
+ byFramework: Record<string, number>;
163
+ byStrategy: Record<string, number>;
164
+ withWarnings: number;
165
+ } {
166
+ const summary = {
167
+ total: reports.size,
168
+ byFramework: {} as Record<string, number>,
169
+ byStrategy: {} as Record<string, number>,
170
+ withWarnings: 0,
171
+ };
172
+
173
+ for (const [, report] of reports) {
174
+ // Count by framework
175
+ const framework = report.analysis.framework;
176
+ summary.byFramework[framework] = (summary.byFramework[framework] || 0) + 1;
177
+
178
+ // Count by strategy
179
+ const strategy = report.decision.shouldHydrate ? 'hydrate' : 'ssr-only';
180
+ summary.byStrategy[strategy] = (summary.byStrategy[strategy] || 0) + 1;
181
+
182
+ // Count warnings
183
+ if (report.decision.warnings && report.decision.warnings.length > 0) {
184
+ summary.withWarnings++;
185
+ }
186
+ }
187
+
188
+ return summary;
189
+ }
190
+
191
+ // Export types for external use
192
+ export type { ComponentAnalysis, DetectionResult, ComponentMetadata } from './component-detection.ts';