@useavalon/avalon 0.1.11 → 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 (141) hide show
  1. package/README.md +54 -54
  2. package/mod.ts +302 -302
  3. package/package.json +49 -26
  4. package/src/build/integration-bundler-plugin.ts +116 -116
  5. package/src/build/integration-config.ts +168 -168
  6. package/src/build/integration-detection-plugin.ts +117 -117
  7. package/src/build/integration-resolver-plugin.ts +90 -90
  8. package/src/build/island-manifest.ts +269 -269
  9. package/src/build/island-types-generator.ts +476 -476
  10. package/src/build/mdx-island-transform.ts +464 -464
  11. package/src/build/mdx-plugin.ts +98 -98
  12. package/src/build/page-island-transform.ts +598 -598
  13. package/src/build/prop-extractors/index.ts +21 -21
  14. package/src/build/prop-extractors/lit.ts +140 -140
  15. package/src/build/prop-extractors/qwik.ts +16 -16
  16. package/src/build/prop-extractors/solid.ts +125 -125
  17. package/src/build/prop-extractors/svelte.ts +194 -194
  18. package/src/build/prop-extractors/vue.ts +111 -111
  19. package/src/build/sidecar-file-manager.ts +104 -104
  20. package/src/build/sidecar-renderer.ts +30 -30
  21. package/src/client/adapters/index.ts +21 -13
  22. package/src/client/components.ts +35 -35
  23. package/src/client/css-hmr-handler.ts +344 -344
  24. package/src/client/framework-adapter.ts +462 -462
  25. package/src/client/hmr-coordinator.ts +396 -396
  26. package/src/client/hmr-error-overlay.js +533 -533
  27. package/src/client/main.js +824 -816
  28. package/src/client/types/framework-runtime.d.ts +68 -68
  29. package/src/client/types/vite-hmr.d.ts +46 -46
  30. package/src/client/types/vite-virtual-modules.d.ts +70 -60
  31. package/src/components/Image.tsx +123 -123
  32. package/src/components/IslandErrorBoundary.tsx +145 -145
  33. package/src/components/LayoutDataErrorBoundary.tsx +141 -141
  34. package/src/components/LayoutErrorBoundary.tsx +127 -127
  35. package/src/components/PersistentIsland.tsx +52 -52
  36. package/src/components/StreamingErrorBoundary.tsx +233 -233
  37. package/src/components/StreamingLayout.tsx +538 -538
  38. package/src/core/components/component-analyzer.ts +192 -192
  39. package/src/core/components/component-detection.ts +508 -508
  40. package/src/core/components/enhanced-framework-detector.ts +500 -500
  41. package/src/core/components/framework-registry.ts +563 -563
  42. package/src/core/content/mdx-processor.ts +46 -46
  43. package/src/core/integrations/index.ts +19 -19
  44. package/src/core/integrations/loader.ts +125 -125
  45. package/src/core/integrations/registry.ts +175 -175
  46. package/src/core/islands/island-persistence.ts +325 -325
  47. package/src/core/islands/island-state-serializer.ts +258 -258
  48. package/src/core/islands/persistent-island-context.tsx +80 -80
  49. package/src/core/islands/use-persistent-state.ts +68 -68
  50. package/src/core/layout/enhanced-layout-resolver.ts +322 -322
  51. package/src/core/layout/layout-cache-manager.ts +485 -485
  52. package/src/core/layout/layout-composer.ts +357 -357
  53. package/src/core/layout/layout-data-loader.ts +516 -516
  54. package/src/core/layout/layout-discovery.ts +243 -243
  55. package/src/core/layout/layout-matcher.ts +299 -299
  56. package/src/core/layout/layout-types.ts +110 -110
  57. package/src/core/modules/framework-module-resolver.ts +273 -273
  58. package/src/islands/component-analysis.ts +213 -213
  59. package/src/islands/css-utils.ts +565 -565
  60. package/src/islands/discovery/index.ts +80 -80
  61. package/src/islands/discovery/registry.ts +340 -340
  62. package/src/islands/discovery/resolver.ts +477 -477
  63. package/src/islands/discovery/scanner.ts +386 -386
  64. package/src/islands/discovery/types.ts +117 -117
  65. package/src/islands/discovery/validator.ts +544 -544
  66. package/src/islands/discovery/watcher.ts +368 -368
  67. package/src/islands/framework-detection.ts +428 -428
  68. package/src/islands/integration-loader.ts +490 -490
  69. package/src/islands/island.tsx +565 -565
  70. package/src/islands/render-cache.ts +550 -550
  71. package/src/islands/types.ts +80 -80
  72. package/src/islands/universal-css-collector.ts +157 -157
  73. package/src/islands/universal-head-collector.ts +137 -137
  74. package/src/layout-system.d.ts +592 -592
  75. package/src/layout-system.ts +218 -218
  76. package/src/middleware/discovery.ts +268 -268
  77. package/src/middleware/executor.ts +315 -315
  78. package/src/middleware/index.ts +76 -76
  79. package/src/middleware/types.ts +99 -99
  80. package/src/nitro/build-config.ts +575 -575
  81. package/src/nitro/config.ts +483 -483
  82. package/src/nitro/error-handler.ts +636 -636
  83. package/src/nitro/index.ts +173 -173
  84. package/src/nitro/island-manifest.ts +584 -584
  85. package/src/nitro/middleware-adapter.ts +260 -260
  86. package/src/nitro/renderer.ts +1471 -1471
  87. package/src/nitro/route-discovery.ts +439 -439
  88. package/src/nitro/types.ts +321 -321
  89. package/src/render/collect-css.ts +198 -198
  90. package/src/render/error-pages.ts +79 -79
  91. package/src/render/isolated-ssr-renderer.ts +654 -654
  92. package/src/render/ssr.ts +1030 -1030
  93. package/src/schemas/api.ts +30 -30
  94. package/src/schemas/core.ts +64 -64
  95. package/src/schemas/index.ts +212 -212
  96. package/src/schemas/layout.ts +279 -279
  97. package/src/schemas/routing/index.ts +38 -38
  98. package/src/schemas/routing.ts +376 -376
  99. package/src/types/as-island.ts +20 -20
  100. package/src/types/image.d.ts +106 -106
  101. package/src/types/index.d.ts +22 -22
  102. package/src/types/island-jsx.d.ts +33 -33
  103. package/src/types/island-prop.d.ts +20 -20
  104. package/src/types/layout.ts +285 -285
  105. package/src/types/mdx.d.ts +6 -6
  106. package/src/types/routing.ts +555 -555
  107. package/src/types/types.ts +5 -5
  108. package/src/types/urlpattern.d.ts +49 -49
  109. package/src/types/vite-env.d.ts +11 -11
  110. package/src/utils/dev-logger.ts +299 -299
  111. package/src/utils/fs.ts +151 -151
  112. package/src/vite-plugin/auto-discover.ts +551 -551
  113. package/src/vite-plugin/config.ts +266 -266
  114. package/src/vite-plugin/errors.ts +127 -127
  115. package/src/vite-plugin/image-optimization.ts +156 -156
  116. package/src/vite-plugin/integration-activator.ts +126 -126
  117. package/src/vite-plugin/island-sidecar-plugin.ts +176 -176
  118. package/src/vite-plugin/module-discovery.ts +189 -189
  119. package/src/vite-plugin/nitro-integration.ts +1354 -1354
  120. package/src/vite-plugin/plugin.ts +403 -409
  121. package/src/vite-plugin/types.ts +327 -327
  122. package/src/vite-plugin/validation.ts +228 -228
  123. package/src/client/adapters/index.js +0 -12
  124. package/src/client/adapters/lit-adapter.js +0 -467
  125. package/src/client/adapters/lit-adapter.ts +0 -654
  126. package/src/client/adapters/preact-adapter.js +0 -223
  127. package/src/client/adapters/preact-adapter.ts +0 -331
  128. package/src/client/adapters/qwik-adapter.js +0 -259
  129. package/src/client/adapters/qwik-adapter.ts +0 -345
  130. package/src/client/adapters/react-adapter.js +0 -220
  131. package/src/client/adapters/react-adapter.ts +0 -353
  132. package/src/client/adapters/solid-adapter.js +0 -295
  133. package/src/client/adapters/solid-adapter.ts +0 -451
  134. package/src/client/adapters/svelte-adapter.js +0 -368
  135. package/src/client/adapters/svelte-adapter.ts +0 -524
  136. package/src/client/adapters/vue-adapter.js +0 -278
  137. package/src/client/adapters/vue-adapter.ts +0 -467
  138. package/src/client/components.js +0 -23
  139. package/src/client/css-hmr-handler.js +0 -263
  140. package/src/client/framework-adapter.js +0 -283
  141. package/src/client/hmr-coordinator.js +0 -274
@@ -1,490 +1,490 @@
1
- import { registry } from "../core/integrations/registry.ts";
2
- import type { Integration } from "@useavalon/core";
3
- import { devWarn } from "../utils/dev-logger.ts";
4
-
5
- /**
6
- * Cache for loaded integrations to avoid repeated lookups
7
- */
8
- const frameworkCache = new Map<string, Integration>();
9
-
10
- // Pattern to match nested island paths like /modules/*/islands/ or /src/*/islands/
11
- const NESTED_ISLANDS_PATTERN = /\/(?:src\/)?(?:modules\/)?([^/]+\/)*islands\//;
12
-
13
- /**
14
- * Load an integration by framework name
15
- * Uses cache to avoid repeated dynamic imports
16
- *
17
- * This function supports on-demand loading: if an integration hasn't been
18
- * preloaded, it will be loaded and cached on first use. This enables
19
- * lazy loading at server startup while ensuring fast subsequent renders.
20
- */
21
- export async function loadIntegration(framework: string) {
22
- // Check local cache first (fastest path)
23
- if (frameworkCache.has(framework)) {
24
- return frameworkCache.get(framework)!;
25
- }
26
-
27
- // Check if already loaded in registry (e.g., by native preloader)
28
- if (registry.has(framework)) {
29
- const integration = registry.get(framework)!;
30
- frameworkCache.set(framework, integration);
31
- return integration;
32
- }
33
-
34
- // On-demand loading: load the integration now and cache it
35
- try {
36
- const integration = await registry.load(framework);
37
- frameworkCache.set(framework, integration);
38
- return integration;
39
- } catch (error) {
40
- throw new Error(
41
- `Integration '${framework}' could not be loaded. Make sure @useavalon/${framework} is installed.`,
42
- { cause: error }
43
- );
44
- }
45
- }
46
-
47
- /**
48
- * Detect framework from file path and load the appropriate integration
49
- */
50
- export async function detectAndLoadIntegration(src: string) {
51
- const framework = detectFrameworkFromPath(src);
52
- return await loadIntegration(framework);
53
- }
54
-
55
- /**
56
- * Detect framework from file path based on extension and naming conventions.
57
- *
58
- * Updated to support nested island paths like:
59
- * - /src/islands/Counter.tsx
60
- * - /src/modules/auth/islands/Counter.tsx
61
- * - /modules/dashboard/islands/Chart.vue
62
- *
63
- * @param src - The source path to detect framework from
64
- * @returns The detected framework name
65
- */
66
- export function detectFrameworkFromPath(src: string) {
67
- // Normalize path separators
68
- const normalizedSrc = src.replaceAll('\\', "/");
69
-
70
- // Vue files (.vue)
71
- if (normalizedSrc.endsWith(".vue")) {
72
- return "vue";
73
- }
74
-
75
- // Svelte files (.svelte)
76
- if (normalizedSrc.endsWith(".svelte")) {
77
- return "svelte";
78
- }
79
-
80
- // Solid files (convention: .solid.tsx or .solid.jsx)
81
- if (normalizedSrc.includes(".solid.")) {
82
- return "solid";
83
- }
84
-
85
- // Qwik files (convention: .qwik.tsx or .qwik.jsx)
86
- if (normalizedSrc.includes(".qwik.")) {
87
- return "qwik";
88
- }
89
-
90
- // React files (convention: .react.tsx or .react.jsx)
91
- if (normalizedSrc.includes(".react.")) {
92
- return "react";
93
- }
94
-
95
- // Lit files (convention: .lit.ts or .lit.js, or files starting with "Lit")
96
- if (normalizedSrc.includes(".lit.")) {
97
- return "lit";
98
- }
99
-
100
- // Lit files by naming convention (LitComponent.ts)
101
- const fileName = normalizedSrc.split("/").pop() || "";
102
- if (fileName.startsWith("Lit") && (normalizedSrc.endsWith(".ts") || normalizedSrc.endsWith(".js"))) {
103
- return "lit";
104
- }
105
-
106
- // Check if path is in any islands directory (including nested)
107
- // Plain .ts/.js files in islands are likely Lit components (Lit doesn't use JSX)
108
- if (isInIslandsDirectory(normalizedSrc) && (normalizedSrc.endsWith(".ts") || normalizedSrc.endsWith(".js"))) {
109
- return "lit";
110
- }
111
-
112
- // Default to Preact for .tsx and .jsx files
113
- if (normalizedSrc.endsWith(".tsx") || normalizedSrc.endsWith(".jsx")) {
114
- return "preact";
115
- }
116
-
117
- // Fallback to Preact
118
- return "preact";
119
- }
120
-
121
- /**
122
- * Check if a path is within any islands directory (including nested).
123
- *
124
- * Matches patterns like:
125
- * - /islands/
126
- * - /src/islands/
127
- * - /src/modules/auth/islands/
128
- * - /modules/dashboard/islands/
129
- * - /src/features/user/islands/
130
- *
131
- * @param path - The path to check
132
- * @returns True if the path is in an islands directory
133
- */
134
- export function isInIslandsDirectory(path: string): boolean {
135
- const normalized = path.replaceAll('\\', "/");
136
-
137
- // Check for /islands/ anywhere in the path
138
- return normalized.includes("/islands/");
139
- }
140
-
141
- /**
142
- * Check if a path is a nested island path (not in default /src/islands/).
143
- *
144
- * @param path - The path to check
145
- * @returns True if the path is a nested island path
146
- */
147
- export function isNestedIslandPath(path: string): boolean {
148
- const normalized = path.replaceAll('\\', "/");
149
-
150
- // Check if it contains /islands/ but not at the root level
151
- if (!normalized.includes("/islands/")) {
152
- return false;
153
- }
154
-
155
- // Default path patterns
156
- const defaultPatterns = [
157
- /^\/islands\//,
158
- /^\/src\/islands\//,
159
- /^src\/islands\//,
160
- /^islands\//,
161
- ];
162
-
163
- for (const pattern of defaultPatterns) {
164
- if (pattern.test(normalized)) {
165
- return false;
166
- }
167
- }
168
-
169
- // If it contains /islands/ but doesn't match default patterns, it's nested
170
- return true;
171
- }
172
-
173
- /**
174
- * Extract the namespace from a nested island path.
175
- *
176
- * Examples:
177
- * - /src/modules/auth/islands/Counter.tsx -> "modules/auth"
178
- * - /src/features/user/islands/Profile.tsx -> "features/user"
179
- * - /src/islands/Button.tsx -> ""
180
- *
181
- * @param path - The path to extract namespace from
182
- * @returns The namespace or empty string for default islands
183
- */
184
- export function extractNamespaceFromPath(path: string): string {
185
- const normalized = path.replaceAll('\\', "/");
186
-
187
- // Match patterns like /src/modules/auth/islands/ or /modules/auth/islands/
188
- const match = new RegExp(/(?:\/src)?\/(.+?)\/islands\//).exec(normalized);
189
- if (match) {
190
- return match[1];
191
- }
192
-
193
- return "";
194
- }
195
-
196
- /**
197
- * Detect framework from file content by analyzing imports and patterns.
198
- *
199
- * Updated to support nested island paths.
200
- *
201
- * @param src - The source path
202
- * @param content - The file content to analyze
203
- * @returns The detected framework name
204
- */
205
- export function detectFrameworkFromContent(
206
- src: string,
207
- content: string
208
- ) {
209
- // First try path-based detection
210
- const pathFramework = detectFrameworkFromPath(src);
211
-
212
- // If we have a definitive answer from path (not default), use it
213
- if (pathFramework === "vue" || pathFramework === "svelte" || pathFramework === "react" || pathFramework === "lit") {
214
- return pathFramework;
215
- }
216
-
217
- // For .tsx/.jsx files, analyze content to distinguish between frameworks
218
-
219
- // Check for React imports (must check before Preact since they share hooks)
220
- if (
221
- content.includes("from 'react'") ||
222
- content.includes('from "react"') ||
223
- content.includes("from 'react-dom'") ||
224
- content.includes('from "react-dom"') ||
225
- content.includes('"use client"') ||
226
- content.includes("'use client'") ||
227
- content.includes('"use server"') ||
228
- content.includes("'use server'")
229
- ) {
230
- return "react";
231
- }
232
-
233
- // Check for Lit imports
234
- if (
235
- content.includes("from 'lit'") ||
236
- content.includes('from "lit"') ||
237
- content.includes("@lit-labs/ssr") ||
238
- content.includes("LitElement") ||
239
- content.includes("@customElement")
240
- ) {
241
- return "lit";
242
- }
243
-
244
- // Check for Solid imports
245
- if (
246
- content.includes("solid-js") ||
247
- content.includes("from 'solid-js'") ||
248
- content.includes('from "solid-js"')
249
- ) {
250
- return "solid";
251
- }
252
-
253
- // Check for Preact imports
254
- if (
255
- content.includes("from 'preact'") ||
256
- content.includes('from "preact"') ||
257
- content.includes("preact/hooks")
258
- ) {
259
- return "preact";
260
- }
261
-
262
- // Check for Lit-specific patterns
263
- if (
264
- content.includes("extends LitElement") ||
265
- content.includes("@property") ||
266
- content.includes("@state") ||
267
- content.includes("html`") ||
268
- content.includes("css`")
269
- ) {
270
- return "lit";
271
- }
272
-
273
- // Check for Solid-specific patterns
274
- if (
275
- content.includes("createSignal") ||
276
- content.includes("createEffect") ||
277
- content.includes("createMemo")
278
- ) {
279
- return "solid";
280
- }
281
-
282
- // Check for React/Preact-specific patterns (hooks)
283
- // Note: React and Preact share the same hooks API, so we default to Preact
284
- // unless React imports are explicitly detected above
285
- if (
286
- content.includes("useState") ||
287
- content.includes("useEffect") ||
288
- content.includes("useRef")
289
- ) {
290
- return "preact";
291
- }
292
-
293
- // Default to path-based detection
294
- return pathFramework;
295
- }
296
-
297
- /**
298
- * Get integration for a specific framework, with error handling
299
- */
300
- export async function getIntegration(framework: string) {
301
- try {
302
- return await loadIntegration(framework);
303
- } catch (error) {
304
- console.error(`Failed to load integration for ${framework}:`, error);
305
- return null;
306
- }
307
- }
308
-
309
- /**
310
- * Check if an integration is available for a framework
311
- */
312
- export async function hasIntegration(framework: string) {
313
- try {
314
- await loadIntegration(framework);
315
- return true;
316
- } catch {
317
- return false;
318
- }
319
- }
320
-
321
- /**
322
- * Get all loaded integrations from cache
323
- */
324
- export function getLoadedIntegrations() {
325
- return Array.from(frameworkCache.values());
326
- }
327
-
328
- /**
329
- * Get all loaded framework names from cache
330
- */
331
- export function getLoadedFrameworks() {
332
- return Array.from(frameworkCache.keys());
333
- }
334
-
335
- /**
336
- * Clear the integration cache
337
- * Useful for testing or hot module replacement
338
- */
339
- export function clearIntegrationCache() {
340
- frameworkCache.clear();
341
- }
342
-
343
- /**
344
- * Check if an integration is loaded in cache
345
- */
346
- export function isIntegrationLoaded(framework: string) {
347
- return frameworkCache.has(framework);
348
- }
349
-
350
- /**
351
- * Default frameworks to preload at server startup
352
- * These are the most commonly used frameworks in island architecture
353
- */
354
- export const DEFAULT_PRELOAD_FRAMEWORKS = ['preact', 'react', 'vue', 'svelte', 'solid', 'lit'] as const;
355
-
356
- /**
357
- * Options for preloading integrations
358
- */
359
- export interface PreloadIntegrationsOptions {
360
- /**
361
- * When true, only preload integrations that are actually used on the page.
362
- * This is determined by analyzing page components for framework usage.
363
- * When false (default), preload all specified frameworks.
364
- */
365
- lazy?: boolean;
366
-
367
- /**
368
- * Array of framework names to preload.
369
- * Defaults to DEFAULT_PRELOAD_FRAMEWORKS.
370
- */
371
- frameworks?: readonly string[];
372
-
373
- /**
374
- * Array of detected frameworks from page analysis.
375
- * Only used when lazy=true to filter which frameworks to preload.
376
- */
377
- detectedFrameworks?: string[];
378
- }
379
-
380
- /**
381
- * Preload integrations for multiple frameworks
382
- * Useful for warming up the cache during build or startup
383
- *
384
- * Uses Promise.allSettled to load all integrations concurrently,
385
- * ensuring that one failed integration doesn't block others.
386
- *
387
- * @param options - Preload options
388
- * @returns Promise that resolves when all preloading attempts complete
389
- */
390
- export async function preloadIntegrations(
391
- options?: PreloadIntegrationsOptions
392
- ): Promise<void> {
393
- let frameworks: readonly string[];
394
- let lazy = false;
395
- let detectedFrameworks: string[] | undefined;
396
-
397
- if (options) {
398
- frameworks = options.frameworks ?? DEFAULT_PRELOAD_FRAMEWORKS;
399
- lazy = options.lazy ?? false;
400
- detectedFrameworks = options.detectedFrameworks;
401
- } else {
402
- frameworks = DEFAULT_PRELOAD_FRAMEWORKS;
403
- }
404
-
405
- // When lazy mode is enabled, only preload detected frameworks
406
- if (lazy && detectedFrameworks && detectedFrameworks.length > 0) {
407
- // Filter to only frameworks that are both in the default list and detected
408
- const frameworksToLoad = frameworks.filter(fw =>
409
- detectedFrameworks.includes(fw)
410
- );
411
-
412
- if (frameworksToLoad.length === 0) {
413
- return;
414
- }
415
-
416
- frameworks = frameworksToLoad;
417
- } else if (lazy && (!detectedFrameworks || detectedFrameworks.length === 0)) {
418
- // Lazy mode but no detected frameworks - skip preloading entirely
419
- return;
420
- }
421
-
422
- const results = await Promise.allSettled(
423
- frameworks.map(framework => loadIntegration(framework))
424
- );
425
-
426
- // Track success/failure counts for logging
427
- let successCount = 0;
428
- let failureCount = 0;
429
-
430
- // Log any failures (dev mode only)
431
- results.forEach((result, index) => {
432
- if (result.status === "rejected") {
433
- failureCount++;
434
- devWarn(
435
- `⚠️ Failed to preload integration '${frameworks[index]}':`,
436
- result.reason
437
- );
438
- } else {
439
- successCount++;
440
- }
441
- });
442
- }
443
-
444
- /**
445
- * Detect frameworks used in a page by analyzing component imports.
446
- * This is used for lazy integration loading to only preload what's needed.
447
- *
448
- * @param pageContent - The content of the page file to analyze
449
- * @returns Array of detected framework names
450
- */
451
- export function detectFrameworksFromPageContent(pageContent: string): string[] {
452
- const detectedFrameworks: Set<string> = new Set();
453
-
454
- // Check for island imports and their framework hints
455
- // Look for patterns like: <Island src="/src/islands/Counter.tsx" framework="preact" />
456
- const frameworkPropMatches = pageContent.matchAll(/framework\s*=\s*["'](\w+)["']/g);
457
- for (const match of frameworkPropMatches) {
458
- detectedFrameworks.add(match[1]);
459
- }
460
-
461
- // Check for island source paths to detect framework from file extension
462
- const srcMatches = pageContent.matchAll(/src\s*=\s*["']([^"']+)["']/g);
463
- for (const match of srcMatches) {
464
- const src = match[1];
465
- const framework = detectFrameworkFromPath(src);
466
- detectedFrameworks.add(framework);
467
- }
468
-
469
- // Check for direct framework imports
470
- if (pageContent.includes("from 'react'") || pageContent.includes('from "react"')) {
471
- detectedFrameworks.add('react');
472
- }
473
- if (pageContent.includes("from 'preact'") || pageContent.includes('from "preact"')) {
474
- detectedFrameworks.add('preact');
475
- }
476
- if (pageContent.includes("from 'vue'") || pageContent.includes('from "vue"')) {
477
- detectedFrameworks.add('vue');
478
- }
479
- if (pageContent.includes("from 'svelte'") || pageContent.includes('from "svelte"')) {
480
- detectedFrameworks.add('svelte');
481
- }
482
- if (pageContent.includes("from 'solid-js'") || pageContent.includes('from "solid-js"')) {
483
- detectedFrameworks.add('solid');
484
- }
485
- if (pageContent.includes("from 'lit'") || pageContent.includes('from "lit"')) {
486
- detectedFrameworks.add('lit');
487
- }
488
-
489
- return Array.from(detectedFrameworks);
490
- }
1
+ import { registry } from "../core/integrations/registry.ts";
2
+ import type { Integration } from "@useavalon/core";
3
+ import { devWarn } from "../utils/dev-logger.ts";
4
+
5
+ /**
6
+ * Cache for loaded integrations to avoid repeated lookups
7
+ */
8
+ const frameworkCache = new Map<string, Integration>();
9
+
10
+ // Pattern to match nested island paths like /modules/*/islands/ or /src/*/islands/
11
+ const NESTED_ISLANDS_PATTERN = /\/(?:src\/)?(?:modules\/)?([^/]+\/)*islands\//;
12
+
13
+ /**
14
+ * Load an integration by framework name
15
+ * Uses cache to avoid repeated dynamic imports
16
+ *
17
+ * This function supports on-demand loading: if an integration hasn't been
18
+ * preloaded, it will be loaded and cached on first use. This enables
19
+ * lazy loading at server startup while ensuring fast subsequent renders.
20
+ */
21
+ export async function loadIntegration(framework: string) {
22
+ // Check local cache first (fastest path)
23
+ if (frameworkCache.has(framework)) {
24
+ return frameworkCache.get(framework)!;
25
+ }
26
+
27
+ // Check if already loaded in registry (e.g., by native preloader)
28
+ if (registry.has(framework)) {
29
+ const integration = registry.get(framework)!;
30
+ frameworkCache.set(framework, integration);
31
+ return integration;
32
+ }
33
+
34
+ // On-demand loading: load the integration now and cache it
35
+ try {
36
+ const integration = await registry.load(framework);
37
+ frameworkCache.set(framework, integration);
38
+ return integration;
39
+ } catch (error) {
40
+ throw new Error(
41
+ `Integration '${framework}' could not be loaded. Make sure @useavalon/${framework} is installed.`,
42
+ { cause: error }
43
+ );
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Detect framework from file path and load the appropriate integration
49
+ */
50
+ export async function detectAndLoadIntegration(src: string) {
51
+ const framework = detectFrameworkFromPath(src);
52
+ return await loadIntegration(framework);
53
+ }
54
+
55
+ /**
56
+ * Detect framework from file path based on extension and naming conventions.
57
+ *
58
+ * Updated to support nested island paths like:
59
+ * - /src/islands/Counter.tsx
60
+ * - /src/modules/auth/islands/Counter.tsx
61
+ * - /modules/dashboard/islands/Chart.vue
62
+ *
63
+ * @param src - The source path to detect framework from
64
+ * @returns The detected framework name
65
+ */
66
+ export function detectFrameworkFromPath(src: string) {
67
+ // Normalize path separators
68
+ const normalizedSrc = src.replaceAll('\\', "/");
69
+
70
+ // Vue files (.vue)
71
+ if (normalizedSrc.endsWith(".vue")) {
72
+ return "vue";
73
+ }
74
+
75
+ // Svelte files (.svelte)
76
+ if (normalizedSrc.endsWith(".svelte")) {
77
+ return "svelte";
78
+ }
79
+
80
+ // Solid files (convention: .solid.tsx or .solid.jsx)
81
+ if (normalizedSrc.includes(".solid.")) {
82
+ return "solid";
83
+ }
84
+
85
+ // Qwik files (convention: .qwik.tsx or .qwik.jsx)
86
+ if (normalizedSrc.includes(".qwik.")) {
87
+ return "qwik";
88
+ }
89
+
90
+ // React files (convention: .react.tsx or .react.jsx)
91
+ if (normalizedSrc.includes(".react.")) {
92
+ return "react";
93
+ }
94
+
95
+ // Lit files (convention: .lit.ts or .lit.js, or files starting with "Lit")
96
+ if (normalizedSrc.includes(".lit.")) {
97
+ return "lit";
98
+ }
99
+
100
+ // Lit files by naming convention (LitComponent.ts)
101
+ const fileName = normalizedSrc.split("/").pop() || "";
102
+ if (fileName.startsWith("Lit") && (normalizedSrc.endsWith(".ts") || normalizedSrc.endsWith(".js"))) {
103
+ return "lit";
104
+ }
105
+
106
+ // Check if path is in any islands directory (including nested)
107
+ // Plain .ts/.js files in islands are likely Lit components (Lit doesn't use JSX)
108
+ if (isInIslandsDirectory(normalizedSrc) && (normalizedSrc.endsWith(".ts") || normalizedSrc.endsWith(".js"))) {
109
+ return "lit";
110
+ }
111
+
112
+ // Default to Preact for .tsx and .jsx files
113
+ if (normalizedSrc.endsWith(".tsx") || normalizedSrc.endsWith(".jsx")) {
114
+ return "preact";
115
+ }
116
+
117
+ // Fallback to Preact
118
+ return "preact";
119
+ }
120
+
121
+ /**
122
+ * Check if a path is within any islands directory (including nested).
123
+ *
124
+ * Matches patterns like:
125
+ * - /islands/
126
+ * - /src/islands/
127
+ * - /src/modules/auth/islands/
128
+ * - /modules/dashboard/islands/
129
+ * - /src/features/user/islands/
130
+ *
131
+ * @param path - The path to check
132
+ * @returns True if the path is in an islands directory
133
+ */
134
+ export function isInIslandsDirectory(path: string): boolean {
135
+ const normalized = path.replaceAll('\\', "/");
136
+
137
+ // Check for /islands/ anywhere in the path
138
+ return normalized.includes("/islands/");
139
+ }
140
+
141
+ /**
142
+ * Check if a path is a nested island path (not in default /src/islands/).
143
+ *
144
+ * @param path - The path to check
145
+ * @returns True if the path is a nested island path
146
+ */
147
+ export function isNestedIslandPath(path: string): boolean {
148
+ const normalized = path.replaceAll('\\', "/");
149
+
150
+ // Check if it contains /islands/ but not at the root level
151
+ if (!normalized.includes("/islands/")) {
152
+ return false;
153
+ }
154
+
155
+ // Default path patterns
156
+ const defaultPatterns = [
157
+ /^\/islands\//,
158
+ /^\/src\/islands\//,
159
+ /^src\/islands\//,
160
+ /^islands\//,
161
+ ];
162
+
163
+ for (const pattern of defaultPatterns) {
164
+ if (pattern.test(normalized)) {
165
+ return false;
166
+ }
167
+ }
168
+
169
+ // If it contains /islands/ but doesn't match default patterns, it's nested
170
+ return true;
171
+ }
172
+
173
+ /**
174
+ * Extract the namespace from a nested island path.
175
+ *
176
+ * Examples:
177
+ * - /src/modules/auth/islands/Counter.tsx -> "modules/auth"
178
+ * - /src/features/user/islands/Profile.tsx -> "features/user"
179
+ * - /src/islands/Button.tsx -> ""
180
+ *
181
+ * @param path - The path to extract namespace from
182
+ * @returns The namespace or empty string for default islands
183
+ */
184
+ export function extractNamespaceFromPath(path: string): string {
185
+ const normalized = path.replaceAll('\\', "/");
186
+
187
+ // Match patterns like /src/modules/auth/islands/ or /modules/auth/islands/
188
+ const match = new RegExp(/(?:\/src)?\/(.+?)\/islands\//).exec(normalized);
189
+ if (match) {
190
+ return match[1];
191
+ }
192
+
193
+ return "";
194
+ }
195
+
196
+ /**
197
+ * Detect framework from file content by analyzing imports and patterns.
198
+ *
199
+ * Updated to support nested island paths.
200
+ *
201
+ * @param src - The source path
202
+ * @param content - The file content to analyze
203
+ * @returns The detected framework name
204
+ */
205
+ export function detectFrameworkFromContent(
206
+ src: string,
207
+ content: string
208
+ ) {
209
+ // First try path-based detection
210
+ const pathFramework = detectFrameworkFromPath(src);
211
+
212
+ // If we have a definitive answer from path (not default), use it
213
+ if (pathFramework === "vue" || pathFramework === "svelte" || pathFramework === "react" || pathFramework === "lit") {
214
+ return pathFramework;
215
+ }
216
+
217
+ // For .tsx/.jsx files, analyze content to distinguish between frameworks
218
+
219
+ // Check for React imports (must check before Preact since they share hooks)
220
+ if (
221
+ content.includes("from 'react'") ||
222
+ content.includes('from "react"') ||
223
+ content.includes("from 'react-dom'") ||
224
+ content.includes('from "react-dom"') ||
225
+ content.includes('"use client"') ||
226
+ content.includes("'use client'") ||
227
+ content.includes('"use server"') ||
228
+ content.includes("'use server'")
229
+ ) {
230
+ return "react";
231
+ }
232
+
233
+ // Check for Lit imports
234
+ if (
235
+ content.includes("from 'lit'") ||
236
+ content.includes('from "lit"') ||
237
+ content.includes("@lit-labs/ssr") ||
238
+ content.includes("LitElement") ||
239
+ content.includes("@customElement")
240
+ ) {
241
+ return "lit";
242
+ }
243
+
244
+ // Check for Solid imports
245
+ if (
246
+ content.includes("solid-js") ||
247
+ content.includes("from 'solid-js'") ||
248
+ content.includes('from "solid-js"')
249
+ ) {
250
+ return "solid";
251
+ }
252
+
253
+ // Check for Preact imports
254
+ if (
255
+ content.includes("from 'preact'") ||
256
+ content.includes('from "preact"') ||
257
+ content.includes("preact/hooks")
258
+ ) {
259
+ return "preact";
260
+ }
261
+
262
+ // Check for Lit-specific patterns
263
+ if (
264
+ content.includes("extends LitElement") ||
265
+ content.includes("@property") ||
266
+ content.includes("@state") ||
267
+ content.includes("html`") ||
268
+ content.includes("css`")
269
+ ) {
270
+ return "lit";
271
+ }
272
+
273
+ // Check for Solid-specific patterns
274
+ if (
275
+ content.includes("createSignal") ||
276
+ content.includes("createEffect") ||
277
+ content.includes("createMemo")
278
+ ) {
279
+ return "solid";
280
+ }
281
+
282
+ // Check for React/Preact-specific patterns (hooks)
283
+ // Note: React and Preact share the same hooks API, so we default to Preact
284
+ // unless React imports are explicitly detected above
285
+ if (
286
+ content.includes("useState") ||
287
+ content.includes("useEffect") ||
288
+ content.includes("useRef")
289
+ ) {
290
+ return "preact";
291
+ }
292
+
293
+ // Default to path-based detection
294
+ return pathFramework;
295
+ }
296
+
297
+ /**
298
+ * Get integration for a specific framework, with error handling
299
+ */
300
+ export async function getIntegration(framework: string) {
301
+ try {
302
+ return await loadIntegration(framework);
303
+ } catch (error) {
304
+ console.error(`Failed to load integration for ${framework}:`, error);
305
+ return null;
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Check if an integration is available for a framework
311
+ */
312
+ export async function hasIntegration(framework: string) {
313
+ try {
314
+ await loadIntegration(framework);
315
+ return true;
316
+ } catch {
317
+ return false;
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Get all loaded integrations from cache
323
+ */
324
+ export function getLoadedIntegrations() {
325
+ return Array.from(frameworkCache.values());
326
+ }
327
+
328
+ /**
329
+ * Get all loaded framework names from cache
330
+ */
331
+ export function getLoadedFrameworks() {
332
+ return Array.from(frameworkCache.keys());
333
+ }
334
+
335
+ /**
336
+ * Clear the integration cache
337
+ * Useful for testing or hot module replacement
338
+ */
339
+ export function clearIntegrationCache() {
340
+ frameworkCache.clear();
341
+ }
342
+
343
+ /**
344
+ * Check if an integration is loaded in cache
345
+ */
346
+ export function isIntegrationLoaded(framework: string) {
347
+ return frameworkCache.has(framework);
348
+ }
349
+
350
+ /**
351
+ * Default frameworks to preload at server startup
352
+ * These are the most commonly used frameworks in island architecture
353
+ */
354
+ export const DEFAULT_PRELOAD_FRAMEWORKS = ['preact', 'react', 'vue', 'svelte', 'solid', 'lit'] as const;
355
+
356
+ /**
357
+ * Options for preloading integrations
358
+ */
359
+ export interface PreloadIntegrationsOptions {
360
+ /**
361
+ * When true, only preload integrations that are actually used on the page.
362
+ * This is determined by analyzing page components for framework usage.
363
+ * When false (default), preload all specified frameworks.
364
+ */
365
+ lazy?: boolean;
366
+
367
+ /**
368
+ * Array of framework names to preload.
369
+ * Defaults to DEFAULT_PRELOAD_FRAMEWORKS.
370
+ */
371
+ frameworks?: readonly string[];
372
+
373
+ /**
374
+ * Array of detected frameworks from page analysis.
375
+ * Only used when lazy=true to filter which frameworks to preload.
376
+ */
377
+ detectedFrameworks?: string[];
378
+ }
379
+
380
+ /**
381
+ * Preload integrations for multiple frameworks
382
+ * Useful for warming up the cache during build or startup
383
+ *
384
+ * Uses Promise.allSettled to load all integrations concurrently,
385
+ * ensuring that one failed integration doesn't block others.
386
+ *
387
+ * @param options - Preload options
388
+ * @returns Promise that resolves when all preloading attempts complete
389
+ */
390
+ export async function preloadIntegrations(
391
+ options?: PreloadIntegrationsOptions
392
+ ): Promise<void> {
393
+ let frameworks: readonly string[];
394
+ let lazy = false;
395
+ let detectedFrameworks: string[] | undefined;
396
+
397
+ if (options) {
398
+ frameworks = options.frameworks ?? DEFAULT_PRELOAD_FRAMEWORKS;
399
+ lazy = options.lazy ?? false;
400
+ detectedFrameworks = options.detectedFrameworks;
401
+ } else {
402
+ frameworks = DEFAULT_PRELOAD_FRAMEWORKS;
403
+ }
404
+
405
+ // When lazy mode is enabled, only preload detected frameworks
406
+ if (lazy && detectedFrameworks && detectedFrameworks.length > 0) {
407
+ // Filter to only frameworks that are both in the default list and detected
408
+ const frameworksToLoad = frameworks.filter(fw =>
409
+ detectedFrameworks.includes(fw)
410
+ );
411
+
412
+ if (frameworksToLoad.length === 0) {
413
+ return;
414
+ }
415
+
416
+ frameworks = frameworksToLoad;
417
+ } else if (lazy && (!detectedFrameworks || detectedFrameworks.length === 0)) {
418
+ // Lazy mode but no detected frameworks - skip preloading entirely
419
+ return;
420
+ }
421
+
422
+ const results = await Promise.allSettled(
423
+ frameworks.map(framework => loadIntegration(framework))
424
+ );
425
+
426
+ // Track success/failure counts for logging
427
+ let successCount = 0;
428
+ let failureCount = 0;
429
+
430
+ // Log any failures (dev mode only)
431
+ results.forEach((result, index) => {
432
+ if (result.status === "rejected") {
433
+ failureCount++;
434
+ devWarn(
435
+ `⚠️ Failed to preload integration '${frameworks[index]}':`,
436
+ result.reason
437
+ );
438
+ } else {
439
+ successCount++;
440
+ }
441
+ });
442
+ }
443
+
444
+ /**
445
+ * Detect frameworks used in a page by analyzing component imports.
446
+ * This is used for lazy integration loading to only preload what's needed.
447
+ *
448
+ * @param pageContent - The content of the page file to analyze
449
+ * @returns Array of detected framework names
450
+ */
451
+ export function detectFrameworksFromPageContent(pageContent: string): string[] {
452
+ const detectedFrameworks: Set<string> = new Set();
453
+
454
+ // Check for island imports and their framework hints
455
+ // Look for patterns like: <Island src="/src/islands/Counter.tsx" framework="preact" />
456
+ const frameworkPropMatches = pageContent.matchAll(/framework\s*=\s*["'](\w+)["']/g);
457
+ for (const match of frameworkPropMatches) {
458
+ detectedFrameworks.add(match[1]);
459
+ }
460
+
461
+ // Check for island source paths to detect framework from file extension
462
+ const srcMatches = pageContent.matchAll(/src\s*=\s*["']([^"']+)["']/g);
463
+ for (const match of srcMatches) {
464
+ const src = match[1];
465
+ const framework = detectFrameworkFromPath(src);
466
+ detectedFrameworks.add(framework);
467
+ }
468
+
469
+ // Check for direct framework imports
470
+ if (pageContent.includes("from 'react'") || pageContent.includes('from "react"')) {
471
+ detectedFrameworks.add('react');
472
+ }
473
+ if (pageContent.includes("from 'preact'") || pageContent.includes('from "preact"')) {
474
+ detectedFrameworks.add('preact');
475
+ }
476
+ if (pageContent.includes("from 'vue'") || pageContent.includes('from "vue"')) {
477
+ detectedFrameworks.add('vue');
478
+ }
479
+ if (pageContent.includes("from 'svelte'") || pageContent.includes('from "svelte"')) {
480
+ detectedFrameworks.add('svelte');
481
+ }
482
+ if (pageContent.includes("from 'solid-js'") || pageContent.includes('from "solid-js"')) {
483
+ detectedFrameworks.add('solid');
484
+ }
485
+ if (pageContent.includes("from 'lit'") || pageContent.includes('from "lit"')) {
486
+ detectedFrameworks.add('lit');
487
+ }
488
+
489
+ return Array.from(detectedFrameworks);
490
+ }