@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.
- package/README.md +54 -54
- package/mod.ts +302 -302
- package/package.json +49 -26
- package/src/build/integration-bundler-plugin.ts +116 -116
- package/src/build/integration-config.ts +168 -168
- package/src/build/integration-detection-plugin.ts +117 -117
- package/src/build/integration-resolver-plugin.ts +90 -90
- package/src/build/island-manifest.ts +269 -269
- package/src/build/island-types-generator.ts +476 -476
- package/src/build/mdx-island-transform.ts +464 -464
- package/src/build/mdx-plugin.ts +98 -98
- package/src/build/page-island-transform.ts +598 -598
- package/src/build/prop-extractors/index.ts +21 -21
- package/src/build/prop-extractors/lit.ts +140 -140
- package/src/build/prop-extractors/qwik.ts +16 -16
- package/src/build/prop-extractors/solid.ts +125 -125
- package/src/build/prop-extractors/svelte.ts +194 -194
- package/src/build/prop-extractors/vue.ts +111 -111
- package/src/build/sidecar-file-manager.ts +104 -104
- package/src/build/sidecar-renderer.ts +30 -30
- package/src/client/adapters/index.ts +21 -13
- package/src/client/components.ts +35 -35
- package/src/client/css-hmr-handler.ts +344 -344
- package/src/client/framework-adapter.ts +462 -462
- package/src/client/hmr-coordinator.ts +396 -396
- package/src/client/hmr-error-overlay.js +533 -533
- package/src/client/main.js +824 -816
- package/src/client/types/framework-runtime.d.ts +68 -68
- package/src/client/types/vite-hmr.d.ts +46 -46
- package/src/client/types/vite-virtual-modules.d.ts +70 -60
- package/src/components/Image.tsx +123 -123
- package/src/components/IslandErrorBoundary.tsx +145 -145
- package/src/components/LayoutDataErrorBoundary.tsx +141 -141
- package/src/components/LayoutErrorBoundary.tsx +127 -127
- package/src/components/PersistentIsland.tsx +52 -52
- package/src/components/StreamingErrorBoundary.tsx +233 -233
- package/src/components/StreamingLayout.tsx +538 -538
- package/src/core/components/component-analyzer.ts +192 -192
- package/src/core/components/component-detection.ts +508 -508
- package/src/core/components/enhanced-framework-detector.ts +500 -500
- package/src/core/components/framework-registry.ts +563 -563
- package/src/core/content/mdx-processor.ts +46 -46
- package/src/core/integrations/index.ts +19 -19
- package/src/core/integrations/loader.ts +125 -125
- package/src/core/integrations/registry.ts +175 -175
- package/src/core/islands/island-persistence.ts +325 -325
- package/src/core/islands/island-state-serializer.ts +258 -258
- package/src/core/islands/persistent-island-context.tsx +80 -80
- package/src/core/islands/use-persistent-state.ts +68 -68
- package/src/core/layout/enhanced-layout-resolver.ts +322 -322
- package/src/core/layout/layout-cache-manager.ts +485 -485
- package/src/core/layout/layout-composer.ts +357 -357
- package/src/core/layout/layout-data-loader.ts +516 -516
- package/src/core/layout/layout-discovery.ts +243 -243
- package/src/core/layout/layout-matcher.ts +299 -299
- package/src/core/layout/layout-types.ts +110 -110
- package/src/core/modules/framework-module-resolver.ts +273 -273
- package/src/islands/component-analysis.ts +213 -213
- package/src/islands/css-utils.ts +565 -565
- package/src/islands/discovery/index.ts +80 -80
- package/src/islands/discovery/registry.ts +340 -340
- package/src/islands/discovery/resolver.ts +477 -477
- package/src/islands/discovery/scanner.ts +386 -386
- package/src/islands/discovery/types.ts +117 -117
- package/src/islands/discovery/validator.ts +544 -544
- package/src/islands/discovery/watcher.ts +368 -368
- package/src/islands/framework-detection.ts +428 -428
- package/src/islands/integration-loader.ts +490 -490
- package/src/islands/island.tsx +565 -565
- package/src/islands/render-cache.ts +550 -550
- package/src/islands/types.ts +80 -80
- package/src/islands/universal-css-collector.ts +157 -157
- package/src/islands/universal-head-collector.ts +137 -137
- package/src/layout-system.d.ts +592 -592
- package/src/layout-system.ts +218 -218
- package/src/middleware/discovery.ts +268 -268
- package/src/middleware/executor.ts +315 -315
- package/src/middleware/index.ts +76 -76
- package/src/middleware/types.ts +99 -99
- package/src/nitro/build-config.ts +575 -575
- package/src/nitro/config.ts +483 -483
- package/src/nitro/error-handler.ts +636 -636
- package/src/nitro/index.ts +173 -173
- package/src/nitro/island-manifest.ts +584 -584
- package/src/nitro/middleware-adapter.ts +260 -260
- package/src/nitro/renderer.ts +1471 -1471
- package/src/nitro/route-discovery.ts +439 -439
- package/src/nitro/types.ts +321 -321
- package/src/render/collect-css.ts +198 -198
- package/src/render/error-pages.ts +79 -79
- package/src/render/isolated-ssr-renderer.ts +654 -654
- package/src/render/ssr.ts +1030 -1030
- package/src/schemas/api.ts +30 -30
- package/src/schemas/core.ts +64 -64
- package/src/schemas/index.ts +212 -212
- package/src/schemas/layout.ts +279 -279
- package/src/schemas/routing/index.ts +38 -38
- package/src/schemas/routing.ts +376 -376
- package/src/types/as-island.ts +20 -20
- package/src/types/image.d.ts +106 -106
- package/src/types/index.d.ts +22 -22
- package/src/types/island-jsx.d.ts +33 -33
- package/src/types/island-prop.d.ts +20 -20
- package/src/types/layout.ts +285 -285
- package/src/types/mdx.d.ts +6 -6
- package/src/types/routing.ts +555 -555
- package/src/types/types.ts +5 -5
- package/src/types/urlpattern.d.ts +49 -49
- package/src/types/vite-env.d.ts +11 -11
- package/src/utils/dev-logger.ts +299 -299
- package/src/utils/fs.ts +151 -151
- package/src/vite-plugin/auto-discover.ts +551 -551
- package/src/vite-plugin/config.ts +266 -266
- package/src/vite-plugin/errors.ts +127 -127
- package/src/vite-plugin/image-optimization.ts +156 -156
- package/src/vite-plugin/integration-activator.ts +126 -126
- package/src/vite-plugin/island-sidecar-plugin.ts +176 -176
- package/src/vite-plugin/module-discovery.ts +189 -189
- package/src/vite-plugin/nitro-integration.ts +1354 -1354
- package/src/vite-plugin/plugin.ts +403 -409
- package/src/vite-plugin/types.ts +327 -327
- package/src/vite-plugin/validation.ts +228 -228
- package/src/client/adapters/index.js +0 -12
- package/src/client/adapters/lit-adapter.js +0 -467
- package/src/client/adapters/lit-adapter.ts +0 -654
- package/src/client/adapters/preact-adapter.js +0 -223
- package/src/client/adapters/preact-adapter.ts +0 -331
- package/src/client/adapters/qwik-adapter.js +0 -259
- package/src/client/adapters/qwik-adapter.ts +0 -345
- package/src/client/adapters/react-adapter.js +0 -220
- package/src/client/adapters/react-adapter.ts +0 -353
- package/src/client/adapters/solid-adapter.js +0 -295
- package/src/client/adapters/solid-adapter.ts +0 -451
- package/src/client/adapters/svelte-adapter.js +0 -368
- package/src/client/adapters/svelte-adapter.ts +0 -524
- package/src/client/adapters/vue-adapter.js +0 -278
- package/src/client/adapters/vue-adapter.ts +0 -467
- package/src/client/components.js +0 -23
- package/src/client/css-hmr-handler.js +0 -263
- package/src/client/framework-adapter.js +0 -283
- 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
|
+
}
|