@useavalon/avalon 0.1.12 → 0.1.13

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 (230) hide show
  1. package/mod.ts +302 -0
  2. package/package.json +9 -17
  3. package/src/build/integration-bundler-plugin.ts +116 -0
  4. package/src/build/integration-config.ts +168 -0
  5. package/src/build/integration-detection-plugin.ts +117 -0
  6. package/src/build/integration-resolver-plugin.ts +90 -0
  7. package/src/build/island-manifest.ts +269 -0
  8. package/src/build/island-types-generator.ts +476 -0
  9. package/src/build/mdx-island-transform.ts +464 -0
  10. package/src/build/mdx-plugin.ts +98 -0
  11. package/src/build/page-island-transform.ts +598 -0
  12. package/src/build/prop-extractors/index.ts +21 -0
  13. package/src/build/prop-extractors/lit.ts +140 -0
  14. package/src/build/prop-extractors/qwik.ts +16 -0
  15. package/src/build/prop-extractors/solid.ts +125 -0
  16. package/src/build/prop-extractors/svelte.ts +194 -0
  17. package/src/build/prop-extractors/vue.ts +111 -0
  18. package/src/build/sidecar-file-manager.ts +104 -0
  19. package/src/build/sidecar-renderer.ts +30 -0
  20. package/src/client/adapters/index.ts +21 -0
  21. package/src/client/components.ts +35 -0
  22. package/src/client/css-hmr-handler.ts +344 -0
  23. package/src/client/framework-adapter.ts +462 -0
  24. package/src/client/hmr-coordinator.ts +396 -0
  25. package/src/client/hmr-error-overlay.js +533 -0
  26. package/src/client/main.js +824 -0
  27. package/src/components/Image.tsx +123 -0
  28. package/src/components/IslandErrorBoundary.tsx +145 -0
  29. package/src/components/LayoutDataErrorBoundary.tsx +141 -0
  30. package/src/components/LayoutErrorBoundary.tsx +127 -0
  31. package/src/components/PersistentIsland.tsx +52 -0
  32. package/src/components/StreamingErrorBoundary.tsx +233 -0
  33. package/src/components/StreamingLayout.tsx +538 -0
  34. package/src/core/components/component-analyzer.ts +192 -0
  35. package/src/core/components/component-detection.ts +508 -0
  36. package/src/core/components/enhanced-framework-detector.ts +500 -0
  37. package/src/core/components/framework-registry.ts +563 -0
  38. package/src/core/content/mdx-processor.ts +46 -0
  39. package/src/core/integrations/index.ts +19 -0
  40. package/src/core/integrations/loader.ts +125 -0
  41. package/src/core/integrations/registry.ts +175 -0
  42. package/src/core/islands/island-persistence.ts +325 -0
  43. package/src/core/islands/island-state-serializer.ts +258 -0
  44. package/src/core/islands/persistent-island-context.tsx +80 -0
  45. package/src/core/islands/use-persistent-state.ts +68 -0
  46. package/src/core/layout/enhanced-layout-resolver.ts +322 -0
  47. package/src/core/layout/layout-cache-manager.ts +485 -0
  48. package/src/core/layout/layout-composer.ts +357 -0
  49. package/src/core/layout/layout-data-loader.ts +516 -0
  50. package/src/core/layout/layout-discovery.ts +243 -0
  51. package/src/core/layout/layout-matcher.ts +299 -0
  52. package/src/core/layout/layout-types.ts +110 -0
  53. package/src/core/modules/framework-module-resolver.ts +273 -0
  54. package/src/islands/component-analysis.ts +213 -0
  55. package/src/islands/css-utils.ts +565 -0
  56. package/src/islands/discovery/index.ts +80 -0
  57. package/src/islands/discovery/registry.ts +340 -0
  58. package/src/islands/discovery/resolver.ts +477 -0
  59. package/src/islands/discovery/scanner.ts +386 -0
  60. package/src/islands/discovery/types.ts +117 -0
  61. package/src/islands/discovery/validator.ts +544 -0
  62. package/src/islands/discovery/watcher.ts +368 -0
  63. package/src/islands/framework-detection.ts +428 -0
  64. package/src/islands/integration-loader.ts +490 -0
  65. package/src/islands/island.tsx +565 -0
  66. package/src/islands/render-cache.ts +550 -0
  67. package/src/islands/types.ts +80 -0
  68. package/src/islands/universal-css-collector.ts +157 -0
  69. package/src/islands/universal-head-collector.ts +137 -0
  70. package/src/layout-system.ts +218 -0
  71. package/src/middleware/discovery.ts +268 -0
  72. package/src/middleware/executor.ts +315 -0
  73. package/src/middleware/index.ts +76 -0
  74. package/src/middleware/types.ts +99 -0
  75. package/src/nitro/build-config.ts +576 -0
  76. package/src/nitro/config.ts +483 -0
  77. package/src/nitro/error-handler.ts +636 -0
  78. package/src/nitro/index.ts +173 -0
  79. package/src/nitro/island-manifest.ts +584 -0
  80. package/src/nitro/middleware-adapter.ts +260 -0
  81. package/src/nitro/renderer.ts +1471 -0
  82. package/src/nitro/route-discovery.ts +439 -0
  83. package/src/nitro/types.ts +321 -0
  84. package/src/render/collect-css.ts +198 -0
  85. package/src/render/error-pages.ts +79 -0
  86. package/src/render/isolated-ssr-renderer.ts +654 -0
  87. package/src/render/ssr.ts +1030 -0
  88. package/src/schemas/api.ts +30 -0
  89. package/src/schemas/core.ts +64 -0
  90. package/src/schemas/index.ts +212 -0
  91. package/src/schemas/layout.ts +279 -0
  92. package/src/schemas/routing/index.ts +38 -0
  93. package/src/schemas/routing.ts +376 -0
  94. package/src/types/as-island.ts +20 -0
  95. package/src/types/layout.ts +285 -0
  96. package/src/types/routing.ts +555 -0
  97. package/src/types/types.ts +5 -0
  98. package/src/utils/dev-logger.ts +299 -0
  99. package/src/utils/fs.ts +151 -0
  100. package/src/vite-plugin/auto-discover.ts +551 -0
  101. package/src/vite-plugin/config.ts +266 -0
  102. package/src/vite-plugin/errors.ts +127 -0
  103. package/src/vite-plugin/image-optimization.ts +156 -0
  104. package/src/vite-plugin/integration-activator.ts +126 -0
  105. package/src/vite-plugin/island-sidecar-plugin.ts +176 -0
  106. package/src/vite-plugin/module-discovery.ts +189 -0
  107. package/src/vite-plugin/nitro-integration.ts +1354 -0
  108. package/src/vite-plugin/plugin.ts +403 -0
  109. package/src/vite-plugin/types.ts +327 -0
  110. package/src/vite-plugin/validation.ts +228 -0
  111. package/dist/mod.js +0 -1
  112. package/dist/src/build/integration-bundler-plugin.js +0 -1
  113. package/dist/src/build/integration-config.js +0 -1
  114. package/dist/src/build/integration-detection-plugin.js +0 -1
  115. package/dist/src/build/integration-resolver-plugin.js +0 -1
  116. package/dist/src/build/island-manifest.js +0 -1
  117. package/dist/src/build/island-types-generator.js +0 -5
  118. package/dist/src/build/mdx-island-transform.js +0 -2
  119. package/dist/src/build/mdx-plugin.js +0 -1
  120. package/dist/src/build/page-island-transform.js +0 -3
  121. package/dist/src/build/prop-extractors/index.js +0 -1
  122. package/dist/src/build/prop-extractors/lit.js +0 -1
  123. package/dist/src/build/prop-extractors/qwik.js +0 -1
  124. package/dist/src/build/prop-extractors/solid.js +0 -1
  125. package/dist/src/build/prop-extractors/svelte.js +0 -1
  126. package/dist/src/build/prop-extractors/vue.js +0 -1
  127. package/dist/src/build/sidecar-file-manager.js +0 -1
  128. package/dist/src/build/sidecar-renderer.js +0 -6
  129. package/dist/src/client/adapters/index.js +0 -1
  130. package/dist/src/client/components.js +0 -1
  131. package/dist/src/client/css-hmr-handler.js +0 -1
  132. package/dist/src/client/framework-adapter.js +0 -13
  133. package/dist/src/client/hmr-coordinator.js +0 -1
  134. package/dist/src/client/hmr-error-overlay.js +0 -214
  135. package/dist/src/client/main.js +0 -39
  136. package/dist/src/components/Image.js +0 -1
  137. package/dist/src/components/IslandErrorBoundary.js +0 -1
  138. package/dist/src/components/LayoutDataErrorBoundary.js +0 -1
  139. package/dist/src/components/LayoutErrorBoundary.js +0 -1
  140. package/dist/src/components/PersistentIsland.js +0 -1
  141. package/dist/src/components/StreamingErrorBoundary.js +0 -1
  142. package/dist/src/components/StreamingLayout.js +0 -29
  143. package/dist/src/core/components/component-analyzer.js +0 -1
  144. package/dist/src/core/components/component-detection.js +0 -5
  145. package/dist/src/core/components/enhanced-framework-detector.js +0 -1
  146. package/dist/src/core/components/framework-registry.js +0 -1
  147. package/dist/src/core/content/mdx-processor.js +0 -1
  148. package/dist/src/core/integrations/index.js +0 -1
  149. package/dist/src/core/integrations/loader.js +0 -1
  150. package/dist/src/core/integrations/registry.js +0 -1
  151. package/dist/src/core/islands/island-persistence.js +0 -1
  152. package/dist/src/core/islands/island-state-serializer.js +0 -1
  153. package/dist/src/core/islands/persistent-island-context.js +0 -1
  154. package/dist/src/core/islands/use-persistent-state.js +0 -1
  155. package/dist/src/core/layout/enhanced-layout-resolver.js +0 -1
  156. package/dist/src/core/layout/layout-cache-manager.js +0 -1
  157. package/dist/src/core/layout/layout-composer.js +0 -1
  158. package/dist/src/core/layout/layout-data-loader.js +0 -1
  159. package/dist/src/core/layout/layout-discovery.js +0 -1
  160. package/dist/src/core/layout/layout-matcher.js +0 -1
  161. package/dist/src/core/layout/layout-types.js +0 -1
  162. package/dist/src/core/modules/framework-module-resolver.js +0 -1
  163. package/dist/src/islands/component-analysis.js +0 -1
  164. package/dist/src/islands/css-utils.js +0 -17
  165. package/dist/src/islands/discovery/index.js +0 -1
  166. package/dist/src/islands/discovery/registry.js +0 -1
  167. package/dist/src/islands/discovery/resolver.js +0 -2
  168. package/dist/src/islands/discovery/scanner.js +0 -1
  169. package/dist/src/islands/discovery/types.js +0 -1
  170. package/dist/src/islands/discovery/validator.js +0 -18
  171. package/dist/src/islands/discovery/watcher.js +0 -1
  172. package/dist/src/islands/framework-detection.js +0 -1
  173. package/dist/src/islands/integration-loader.js +0 -1
  174. package/dist/src/islands/island.js +0 -1
  175. package/dist/src/islands/render-cache.js +0 -1
  176. package/dist/src/islands/types.js +0 -1
  177. package/dist/src/islands/universal-css-collector.js +0 -5
  178. package/dist/src/islands/universal-head-collector.js +0 -2
  179. package/dist/src/layout-system.js +0 -1
  180. package/dist/src/middleware/discovery.js +0 -1
  181. package/dist/src/middleware/executor.js +0 -1
  182. package/dist/src/middleware/index.js +0 -1
  183. package/dist/src/middleware/types.js +0 -1
  184. package/dist/src/nitro/build-config.js +0 -1
  185. package/dist/src/nitro/config.js +0 -1
  186. package/dist/src/nitro/error-handler.js +0 -198
  187. package/dist/src/nitro/index.js +0 -1
  188. package/dist/src/nitro/island-manifest.js +0 -2
  189. package/dist/src/nitro/middleware-adapter.js +0 -1
  190. package/dist/src/nitro/renderer.js +0 -183
  191. package/dist/src/nitro/route-discovery.js +0 -1
  192. package/dist/src/nitro/types.js +0 -1
  193. package/dist/src/render/collect-css.js +0 -3
  194. package/dist/src/render/error-pages.js +0 -48
  195. package/dist/src/render/isolated-ssr-renderer.js +0 -1
  196. package/dist/src/render/ssr.js +0 -90
  197. package/dist/src/schemas/api.js +0 -1
  198. package/dist/src/schemas/core.js +0 -1
  199. package/dist/src/schemas/index.js +0 -1
  200. package/dist/src/schemas/layout.js +0 -1
  201. package/dist/src/schemas/routing/index.js +0 -1
  202. package/dist/src/schemas/routing.js +0 -1
  203. package/dist/src/types/as-island.js +0 -1
  204. package/dist/src/types/layout.js +0 -1
  205. package/dist/src/types/routing.js +0 -1
  206. package/dist/src/types/types.js +0 -1
  207. package/dist/src/utils/dev-logger.js +0 -12
  208. package/dist/src/utils/fs.js +0 -1
  209. package/dist/src/vite-plugin/auto-discover.js +0 -1
  210. package/dist/src/vite-plugin/config.js +0 -1
  211. package/dist/src/vite-plugin/errors.js +0 -1
  212. package/dist/src/vite-plugin/image-optimization.js +0 -45
  213. package/dist/src/vite-plugin/integration-activator.js +0 -1
  214. package/dist/src/vite-plugin/island-sidecar-plugin.js +0 -1
  215. package/dist/src/vite-plugin/module-discovery.js +0 -1
  216. package/dist/src/vite-plugin/nitro-integration.js +0 -42
  217. package/dist/src/vite-plugin/plugin.js +0 -1
  218. package/dist/src/vite-plugin/types.js +0 -1
  219. package/dist/src/vite-plugin/validation.js +0 -2
  220. /package/{dist/src → src}/client/types/framework-runtime.d.ts +0 -0
  221. /package/{dist/src → src}/client/types/vite-hmr.d.ts +0 -0
  222. /package/{dist/src → src}/client/types/vite-virtual-modules.d.ts +0 -0
  223. /package/{dist/src → src}/layout-system.d.ts +0 -0
  224. /package/{dist/src → src}/types/image.d.ts +0 -0
  225. /package/{dist/src → src}/types/index.d.ts +0 -0
  226. /package/{dist/src → src}/types/island-jsx.d.ts +0 -0
  227. /package/{dist/src → src}/types/island-prop.d.ts +0 -0
  228. /package/{dist/src → src}/types/mdx.d.ts +0 -0
  229. /package/{dist/src → src}/types/urlpattern.d.ts +0 -0
  230. /package/{dist/src → src}/types/vite-env.d.ts +0 -0
@@ -0,0 +1,125 @@
1
+ import { registry } from "./registry.ts";
2
+ import type { Integration } from "@useavalon/core";
3
+
4
+ /**
5
+ * Cache for loaded integrations to avoid repeated dynamic imports
6
+ */
7
+ const integrationCache = new Map<string, Integration>();
8
+
9
+ /**
10
+ * Load an integration by name, using cache if available
11
+ */
12
+ export async function loadIntegration(framework: string): Promise<Integration> {
13
+ // Check cache first
14
+ if (integrationCache.has(framework)) {
15
+ return integrationCache.get(framework)!;
16
+ }
17
+
18
+ // Load from registry (which handles dynamic imports)
19
+ const integration = await registry.load(framework);
20
+
21
+ // Cache the loaded integration
22
+ integrationCache.set(framework, integration);
23
+
24
+ return integration;
25
+ }
26
+
27
+ /**
28
+ * Detect framework from file path and load the appropriate integration
29
+ */
30
+ export async function detectAndLoadIntegration(src: string): Promise<Integration> {
31
+ const framework = detectFrameworkFromPath(src);
32
+ return await loadIntegration(framework);
33
+ }
34
+
35
+ /**
36
+ * Detect framework from file path based on extension and naming conventions
37
+ */
38
+ export function detectFrameworkFromPath(src: string): string {
39
+ // Vue files
40
+ if (src.endsWith(".vue")) {
41
+ return "vue";
42
+ }
43
+
44
+ // Svelte files
45
+ if (src.endsWith(".svelte")) {
46
+ return "svelte";
47
+ }
48
+
49
+ // Solid files (convention: .solid.tsx or .solid.jsx)
50
+ if (src.includes(".solid.")) {
51
+ return "solid";
52
+ }
53
+
54
+ // Qwik files (convention: .qwik.tsx or .qwik.jsx)
55
+ if (src.includes(".qwik.")) {
56
+ return "qwik";
57
+ }
58
+
59
+ // Default to Preact for .tsx and .jsx files
60
+ return "preact";
61
+ }
62
+
63
+ /**
64
+ * Detect framework from file content by analyzing imports and patterns
65
+ */
66
+ export function detectFrameworkFromContent(
67
+ src: string,
68
+ content?: string
69
+ ): string {
70
+ // First try path-based detection
71
+ const pathFramework = detectFrameworkFromPath(src);
72
+
73
+ // If we have a definitive answer from path, use it
74
+ if (pathFramework !== "preact" || !content) {
75
+ return pathFramework;
76
+ }
77
+
78
+ // For .tsx/.jsx files, analyze content to distinguish between Preact and Solid
79
+ if (content.includes("solid-js")) {
80
+ return "solid";
81
+ }
82
+
83
+ if (content.includes("@builder.io/qwik")) {
84
+ return "qwik";
85
+ }
86
+
87
+ if (content.includes("preact")) {
88
+ return "preact";
89
+ }
90
+
91
+ // Default to Preact
92
+ return "preact";
93
+ }
94
+
95
+ /**
96
+ * Preload integrations for the given frameworks
97
+ * Useful for warming up the cache during build or startup
98
+ */
99
+ export async function preloadIntegrations(frameworks: string[]): Promise<void> {
100
+ await Promise.all(
101
+ frameworks.map(framework => loadIntegration(framework))
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Get all currently loaded integrations
107
+ */
108
+ export function getLoadedIntegrations(): Integration[] {
109
+ return Array.from(integrationCache.values());
110
+ }
111
+
112
+ /**
113
+ * Clear the integration cache
114
+ * Useful for testing or hot module replacement
115
+ */
116
+ export function clearIntegrationCache(): void {
117
+ integrationCache.clear();
118
+ }
119
+
120
+ /**
121
+ * Check if an integration is loaded in cache
122
+ */
123
+ export function isIntegrationLoaded(framework: string): boolean {
124
+ return integrationCache.has(framework);
125
+ }
@@ -0,0 +1,175 @@
1
+ import type { Integration } from "@useavalon/core";
2
+ import { dirname, join } from "node:path";
3
+ import { statSync } from "node:fs";
4
+
5
+ /**
6
+ * Find the root of the Avalon monorepo by looking for packages/integrations
7
+ */
8
+ function findMonorepoRoot(): string {
9
+ let currentDir = process.cwd();
10
+
11
+ // Walk up the directory tree looking for packages/integrations
12
+ for (let i = 0; i < 10; i++) {
13
+ try {
14
+ const integrationsPath = join(currentDir, "packages", "integrations");
15
+ const stat = statSync(integrationsPath);
16
+ if (stat.isDirectory()) {
17
+ return currentDir;
18
+ }
19
+ } catch {
20
+ // Directory doesn't exist, try parent
21
+ }
22
+
23
+ const parent = dirname(currentDir);
24
+ if (parent === currentDir) {
25
+ // Reached root, stop
26
+ break;
27
+ }
28
+ currentDir = parent;
29
+ }
30
+
31
+ // Fallback to cwd
32
+ return process.cwd();
33
+ }
34
+
35
+ /**
36
+ * IntegrationRegistry manages loaded framework integrations.
37
+ * It provides registration, retrieval, and dynamic loading of integrations.
38
+ */
39
+ export class IntegrationRegistry {
40
+ private readonly integrations = new Map<string, Integration>();
41
+ private readonly loadingPromises = new Map<string, Promise<Integration>>();
42
+
43
+ /**
44
+ * Register an integration instance
45
+ */
46
+ register(integration: Integration): void {
47
+ if (!integration.name) {
48
+ throw new Error("Integration must have a name");
49
+ }
50
+ this.integrations.set(integration.name, integration);
51
+ }
52
+
53
+ /**
54
+ * Get a registered integration by name
55
+ */
56
+ get(name: string): Integration | undefined {
57
+ return this.integrations.get(name);
58
+ }
59
+
60
+ /**
61
+ * Check if an integration is registered
62
+ */
63
+ has(name: string): boolean {
64
+ return this.integrations.has(name);
65
+ }
66
+
67
+ /**
68
+ * Dynamically load an integration by name
69
+ * Returns cached integration if already loaded
70
+ */
71
+ async load(name: string): Promise<Integration> {
72
+ // Check if already loaded
73
+ const existing = this.integrations.get(name);
74
+ if (existing) {
75
+ return existing;
76
+ }
77
+
78
+ // Check if currently loading (prevent duplicate loads)
79
+ const loadingPromise = this.loadingPromises.get(name);
80
+ if (loadingPromise) {
81
+ return loadingPromise;
82
+ }
83
+
84
+ // Start loading
85
+ const promise = this.loadIntegration(name);
86
+ this.loadingPromises.set(name, promise);
87
+
88
+ try {
89
+ const integration = await promise;
90
+ this.register(integration);
91
+ return integration;
92
+ } finally {
93
+ this.loadingPromises.delete(name);
94
+ }
95
+ }
96
+
97
+ private async loadIntegration(name: string): Promise<Integration> {
98
+ const integrationKey = `${name}Integration`;
99
+
100
+ // 1. Try loading from the installed npm package first (@useavalon/<name>)
101
+ try {
102
+ const packageName = `@useavalon/${name}`;
103
+ const module = await import(/* @vite-ignore */ packageName);
104
+ const integration = module[integrationKey] || module.default;
105
+ if (integration) return integration as Integration;
106
+ } catch {
107
+ // Package not installed or import failed — try monorepo path
108
+ }
109
+
110
+ // 2. Monorepo fallback: resolve via packages/integrations/<name>/mod.ts
111
+ try {
112
+ const monorepoRoot = findMonorepoRoot();
113
+ const integrationPath = join(monorepoRoot, "packages", "integrations", name, "mod.ts");
114
+ const fileUrl = `file://${integrationPath}`;
115
+ const module = await import(/* @vite-ignore */ fileUrl);
116
+ const integration = module[integrationKey] || module.default;
117
+ if (integration) return integration as Integration;
118
+ } catch {
119
+ // Monorepo path also failed
120
+ }
121
+
122
+ throw new Error(
123
+ `Failed to load integration for framework '${name}'. ` +
124
+ `Make sure @useavalon/${name} is installed.\n` +
125
+ `Install it with: bun add @useavalon/${name}`,
126
+ );
127
+ }
128
+
129
+ /**
130
+ * Get all registered integrations
131
+ */
132
+ getAll(): Integration[] {
133
+ return Array.from(this.integrations.values());
134
+ }
135
+
136
+ /**
137
+ * Get all registered integration names
138
+ */
139
+ getAllNames(): string[] {
140
+ return Array.from(this.integrations.keys());
141
+ }
142
+
143
+ /**
144
+ * Unregister an integration
145
+ */
146
+ unregister(name: string): boolean {
147
+ return this.integrations.delete(name);
148
+ }
149
+
150
+ /**
151
+ * Clear all registered integrations
152
+ */
153
+ clear(): void {
154
+ this.integrations.clear();
155
+ this.loadingPromises.clear();
156
+ }
157
+
158
+ /**
159
+ * Get the count of registered integrations
160
+ */
161
+ get size(): number {
162
+ return this.integrations.size;
163
+ }
164
+ }
165
+
166
+ // Global singleton registry instance
167
+ // Use globalThis to ensure the registry is shared across all module contexts
168
+ // This is important because Vite's ssrLoadModule creates new module contexts
169
+ declare global {
170
+ var __avalonIntegrationRegistry: IntegrationRegistry | undefined;
171
+ }
172
+
173
+ globalThis.__avalonIntegrationRegistry ??= new IntegrationRegistry();
174
+
175
+ export const registry = globalThis.__avalonIntegrationRegistry;
@@ -0,0 +1,325 @@
1
+ import type { IslandState } from '../../schemas/layout.ts';
2
+ import type { IIslandPersistence } from '../../types/layout.ts';
3
+ import { IslandStateSerializer } from './island-state-serializer.ts';
4
+
5
+ /**
6
+ * IslandPersistence class for state management across navigation
7
+ *
8
+ * Handles saving, loading, and clearing island state using browser storage
9
+ * with support for both sessionStorage and localStorage persistence strategies.
10
+ */
11
+ export class IslandPersistence implements IIslandPersistence {
12
+ private storageType: 'session' | 'local';
13
+ private keyPrefix: string;
14
+ private storage: Storage | null = null;
15
+
16
+ constructor(
17
+ options: {
18
+ storageType?: 'session' | 'local';
19
+ keyPrefix?: string;
20
+ } = {}
21
+ ) {
22
+ this.storageType = options.storageType || 'session';
23
+ this.keyPrefix = options.keyPrefix || 'island-state';
24
+
25
+ // Initialize storage if available (browser environment)
26
+ if (typeof window !== 'undefined') {
27
+ this.storage = this.storageType === 'session' ? sessionStorage : localStorage;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Save island state to browser storage
33
+ */
34
+ saveState(id: string, state: IslandState): void {
35
+ if (!this.storage) {
36
+ console.warn('Island persistence: Storage not available (server-side or unsupported browser)');
37
+ return;
38
+ }
39
+
40
+ try {
41
+ const key = this.getStorageKey(id);
42
+
43
+ // Use the IslandStateSerializer for proper serialization
44
+ const serializedState = IslandStateSerializer.serialize({
45
+ state,
46
+ timestamp: Date.now(),
47
+ version: '1.0',
48
+ });
49
+
50
+ this.storage.setItem(key, serializedState);
51
+ console.log(`Island state saved for ${id}`);
52
+ } catch (error) {
53
+ console.error(`Failed to save island state for ${id}:`, error);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Load island state from browser storage
59
+ */
60
+ loadState(id: string): IslandState | null {
61
+ if (!this.storage) {
62
+ console.warn('Island persistence: Storage not available (server-side or unsupported browser)');
63
+ return null;
64
+ }
65
+
66
+ try {
67
+ const key = this.getStorageKey(id);
68
+ const serializedState = this.storage.getItem(key);
69
+
70
+ if (!serializedState) {
71
+ return null;
72
+ }
73
+
74
+ // Use the IslandStateSerializer for proper deserialization
75
+ const parsed = IslandStateSerializer.deserialize(serializedState);
76
+
77
+ // Validate the stored data structure
78
+ if (!parsed.state || !parsed.timestamp || !parsed.version) {
79
+ console.warn(`Invalid island state format for ${id}, clearing...`);
80
+ this.clearState(id);
81
+ return null;
82
+ }
83
+
84
+ console.log(`Island state loaded for ${id}`);
85
+ return parsed.state as Record<string, unknown>;
86
+ } catch (error) {
87
+ console.error(`Failed to load island state for ${id}:`, error);
88
+ // Clear corrupted state
89
+ this.clearState(id);
90
+ return null;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Clear island state from browser storage
96
+ */
97
+ clearState(id: string): void {
98
+ if (!this.storage) {
99
+ return;
100
+ }
101
+
102
+ try {
103
+ const key = this.getStorageKey(id);
104
+ this.storage.removeItem(key);
105
+ console.log(`Island state cleared for ${id}`);
106
+ } catch (error) {
107
+ console.error(`Failed to clear island state for ${id}:`, error);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Check if state exists for an island
113
+ */
114
+ hasState(id: string): boolean {
115
+ if (!this.storage) {
116
+ return false;
117
+ }
118
+
119
+ try {
120
+ const key = this.getStorageKey(id);
121
+ return this.storage.getItem(key) !== null;
122
+ } catch (error) {
123
+ console.error(`Failed to check island state for ${id}:`, error);
124
+ return false;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Get all stored island IDs
130
+ */
131
+ getStoredIds(): string[] {
132
+ if (!this.storage) {
133
+ return [];
134
+ }
135
+
136
+ try {
137
+ const ids: string[] = [];
138
+ const prefixLength = this.keyPrefix.length + 1; // +1 for the separator
139
+
140
+ for (let i = 0; i < this.storage.length; i++) {
141
+ const key = this.storage.key(i);
142
+ if (key && key.startsWith(this.keyPrefix + ':')) {
143
+ ids.push(key.substring(prefixLength));
144
+ }
145
+ }
146
+
147
+ return ids;
148
+ } catch (error) {
149
+ console.error('Failed to get stored island IDs:', error);
150
+ return [];
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Clear all stored states
156
+ */
157
+ clearAllStates(): void {
158
+ if (!this.storage) {
159
+ return;
160
+ }
161
+
162
+ try {
163
+ const keysToRemove: string[] = [];
164
+
165
+ for (let i = 0; i < this.storage.length; i++) {
166
+ const key = this.storage.key(i);
167
+ if (key && key.startsWith(this.keyPrefix + ':')) {
168
+ keysToRemove.push(key);
169
+ }
170
+ }
171
+
172
+ keysToRemove.forEach(key => this.storage!.removeItem(key));
173
+ console.log(`Cleared ${keysToRemove.length} island states`);
174
+ } catch (error) {
175
+ console.error('Failed to clear all island states:', error);
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Get the storage key for an island ID
181
+ */
182
+ private getStorageKey(id: string): string {
183
+ return `${this.keyPrefix}:${id}`;
184
+ }
185
+
186
+ /**
187
+ * Get current storage configuration
188
+ */
189
+ getConfig(): { storageType: string; keyPrefix: string; available: boolean } {
190
+ return {
191
+ storageType: this.storageType,
192
+ keyPrefix: this.keyPrefix,
193
+ available: this.storage !== null,
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Get storage usage statistics
199
+ */
200
+ getStorageStats(): { totalKeys: number; islandKeys: number; estimatedSize: number } {
201
+ if (!this.storage) {
202
+ return { totalKeys: 0, islandKeys: 0, estimatedSize: 0 };
203
+ }
204
+
205
+ try {
206
+ let islandKeys = 0;
207
+ let estimatedSize = 0;
208
+
209
+ for (let i = 0; i < this.storage.length; i++) {
210
+ const key = this.storage.key(i);
211
+ if (key && key.startsWith(this.keyPrefix + ':')) {
212
+ islandKeys++;
213
+ const value = this.storage.getItem(key);
214
+ if (value) {
215
+ estimatedSize += key.length + value.length;
216
+ }
217
+ }
218
+ }
219
+
220
+ return {
221
+ totalKeys: this.storage.length,
222
+ islandKeys,
223
+ estimatedSize,
224
+ };
225
+ } catch (error) {
226
+ console.error('Failed to get storage stats:', error);
227
+ return { totalKeys: 0, islandKeys: 0, estimatedSize: 0 };
228
+ }
229
+ }
230
+
231
+ /**
232
+ * JSON.stringify replacer function to handle special types
233
+ */
234
+ private replacer(key: string, value: unknown): unknown {
235
+ // Handle Date objects
236
+ if (value instanceof Date) {
237
+ return {
238
+ __type: 'Date',
239
+ __value: value.toISOString(),
240
+ };
241
+ }
242
+
243
+ // Handle RegExp objects
244
+ if (value instanceof RegExp) {
245
+ return {
246
+ __type: 'RegExp',
247
+ __value: {
248
+ source: value.source,
249
+ flags: value.flags,
250
+ },
251
+ };
252
+ }
253
+
254
+ // Handle Map objects
255
+ if (value instanceof Map) {
256
+ return {
257
+ __type: 'Map',
258
+ __value: Array.from(value.entries()),
259
+ };
260
+ }
261
+
262
+ // Handle Set objects
263
+ if (value instanceof Set) {
264
+ return {
265
+ __type: 'Set',
266
+ __value: Array.from(value.values()),
267
+ };
268
+ }
269
+
270
+ // Handle functions (convert to null - functions can't be serialized)
271
+ if (typeof value === 'function') {
272
+ console.warn(`Function found in island state at key "${key}", converting to null`);
273
+ return null;
274
+ }
275
+
276
+ // Handle undefined (convert to null)
277
+ if (value === undefined) {
278
+ return null;
279
+ }
280
+
281
+ return value;
282
+ }
283
+
284
+ /**
285
+ * JSON.parse reviver function to restore special types
286
+ */
287
+ private reviver(key: string, value: unknown): unknown {
288
+ // Check if this is a special type object
289
+ if (value && typeof value === 'object') {
290
+ const obj = value as Record<string, unknown>;
291
+ if (obj.__type && obj.__value !== undefined) {
292
+ switch (obj.__type) {
293
+ case 'Date':
294
+ return new Date(obj.__value as string);
295
+
296
+ case 'RegExp': {
297
+ const rv = obj.__value as { source: string; flags: string };
298
+ return new RegExp(rv.source, rv.flags);
299
+ }
300
+
301
+ case 'Map':
302
+ return new Map(obj.__value as Iterable<[unknown, unknown]>);
303
+
304
+ case 'Set':
305
+ return new Set(obj.__value as Iterable<unknown>);
306
+
307
+ default:
308
+ console.warn(`Unknown special type "${obj.__type}" in serialized state`);
309
+ return obj.__value;
310
+ }
311
+ }
312
+ }
313
+
314
+ return value;
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Default island persistence instance
320
+ * Uses sessionStorage by default for better privacy and performance
321
+ */
322
+ export const defaultIslandPersistence = new IslandPersistence({
323
+ storageType: 'session',
324
+ keyPrefix: 'island-state',
325
+ });