@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
package/src/islands/island.tsx
CHANGED
|
@@ -1,565 +1,565 @@
|
|
|
1
|
-
import type { JSX } from 'preact';
|
|
2
|
-
import { h } from 'preact';
|
|
3
|
-
import type { ViteDevServer } from 'vite';
|
|
4
|
-
import type { AnalyzerOptions } from '../core/components/component-analyzer.ts';
|
|
5
|
-
import type { Framework } from './types.ts';
|
|
6
|
-
import { detectFramework } from './framework-detection.ts';
|
|
7
|
-
import { analyzeComponentFile, renderComponentSSROnly } from './component-analysis.ts';
|
|
8
|
-
import { loadIntegration, detectFrameworkFromPath } from './integration-loader.ts';
|
|
9
|
-
import { addUniversalCSS } from './universal-css-collector.ts';
|
|
10
|
-
import { addUniversalHead } from './universal-head-collector.ts';
|
|
11
|
-
import { getIslandBundlePath } from '../build/island-manifest.ts';
|
|
12
|
-
import type { Integration } from '../../../integrations/core/types.ts';
|
|
13
|
-
import { isDev, devLog, devWarn, devError, logRenderTiming } from '../utils/dev-logger.ts';
|
|
14
|
-
|
|
15
|
-
// Enhanced global CSS collector for SSR with scoping support
|
|
16
|
-
declare global {
|
|
17
|
-
var __viteDevServer: ViteDevServer | undefined;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Supported hydration conditions for island components */
|
|
21
|
-
export type HydrationCondition = 'on:visible' | 'on:interaction' | 'on:idle' | 'on:client' | `media:${string}`;
|
|
22
|
-
|
|
23
|
-
/** Supported framework identifiers (without "unknown") */
|
|
24
|
-
export type FrameworkId = Exclude<Framework, 'unknown'>;
|
|
25
|
-
|
|
26
|
-
export interface IslandProps {
|
|
27
|
-
/** Path to the island component (e.g., "/islands/Counter.tsx") */
|
|
28
|
-
src: string;
|
|
29
|
-
/** Hydration condition */
|
|
30
|
-
condition?: HydrationCondition;
|
|
31
|
-
/** Props to pass to the island component */
|
|
32
|
-
props?: Record<string, unknown>;
|
|
33
|
-
/** Children to render inside the island (for SSR) */
|
|
34
|
-
children?: import('preact').ComponentChildren;
|
|
35
|
-
/** Whether to render server-side (default: true unless condition is 'on:client') */
|
|
36
|
-
ssr?: boolean;
|
|
37
|
-
/** Framework hint for client hydration */
|
|
38
|
-
framework?: FrameworkId;
|
|
39
|
-
/** Force SSR-only rendering without hydration */
|
|
40
|
-
ssrOnly?: boolean;
|
|
41
|
-
/** Component render options for intelligent detection */
|
|
42
|
-
renderOptions?: AnalyzerOptions;
|
|
43
|
-
/** Hydration data from integration renderer */
|
|
44
|
-
hydrationData?: Record<string, unknown>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
// Shared helpers (extracted to reduce cognitive complexity of Island/renderIsland)
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
/** Generate a deterministic island element ID from the source path */
|
|
52
|
-
function toIslandId(src: string): string {
|
|
53
|
-
return `island-${src.replaceAll(/[^a-zA-Z0-9]/g, '-')}`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Build the extra hydration data-attributes from integration render output */
|
|
57
|
-
function buildHydrationDataAttrs(hydrationData: Record<string, unknown>): Record<string, string> {
|
|
58
|
-
const attrs: Record<string, string> = {};
|
|
59
|
-
if (hydrationData.renderId) {
|
|
60
|
-
attrs['data-solid-render-id'] = hydrationData.renderId as string;
|
|
61
|
-
}
|
|
62
|
-
const metadata = hydrationData.metadata as Record<string, unknown> | undefined;
|
|
63
|
-
if (metadata?.tagName) {
|
|
64
|
-
attrs['data-tag-name'] = metadata.tagName as string;
|
|
65
|
-
}
|
|
66
|
-
return attrs;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Build the full set of attributes for an `<avalon-island>` element that will be hydrated */
|
|
70
|
-
function buildHydrateAttributes(
|
|
71
|
-
src: string,
|
|
72
|
-
condition: HydrationCondition,
|
|
73
|
-
props: Record<string, unknown>,
|
|
74
|
-
hydrationData: Record<string, unknown>,
|
|
75
|
-
): Record<string, string> {
|
|
76
|
-
return {
|
|
77
|
-
'data-condition': condition,
|
|
78
|
-
'data-src': getIslandBundlePath(src),
|
|
79
|
-
'data-props': JSON.stringify(props),
|
|
80
|
-
'data-render-strategy': 'hydrate',
|
|
81
|
-
...buildHydrationDataAttrs(hydrationData),
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Detect the head-content type from an HTML string returned by an integration */
|
|
86
|
-
function classifyHeadContent(headContent: string): 'script' | 'meta' | 'link' | 'style' | 'other' {
|
|
87
|
-
if (headContent.startsWith('<script')) return 'script';
|
|
88
|
-
if (headContent.startsWith('<style')) return 'style';
|
|
89
|
-
if (headContent.startsWith('<meta')) return 'meta';
|
|
90
|
-
if (headContent.startsWith('<link')) return 'link';
|
|
91
|
-
if (headContent.includes('window._$HY') || headContent.includes('_$HY=')) return 'script';
|
|
92
|
-
return 'other';
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Extract CSS content from a <style> tag */
|
|
96
|
-
function extractCSSFromStyleTag(styleTag: string): string | null {
|
|
97
|
-
const match = styleTag.match(/<style[^>]*>([\s\S]*?)<\/style>/i);
|
|
98
|
-
return match ? match[1].trim() : null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Collect CSS and head content produced by an integration render */
|
|
102
|
-
function collectRenderAssets(
|
|
103
|
-
renderResult: { css?: string; head?: string; scopeId?: string },
|
|
104
|
-
src: string,
|
|
105
|
-
framework: string,
|
|
106
|
-
logPrefix: string,
|
|
107
|
-
): void {
|
|
108
|
-
if (renderResult.css) {
|
|
109
|
-
addUniversalCSS(renderResult.css, src, framework, (renderResult as { scopeId?: string }).scopeId);
|
|
110
|
-
}
|
|
111
|
-
if (renderResult.head) {
|
|
112
|
-
const headContent = renderResult.head.trim();
|
|
113
|
-
const contentType = classifyHeadContent(headContent);
|
|
114
|
-
if (contentType === 'style') {
|
|
115
|
-
// Extract CSS from <style> tag and add to universal CSS collector
|
|
116
|
-
const cssContent = extractCSSFromStyleTag(headContent);
|
|
117
|
-
if (cssContent) {
|
|
118
|
-
devLog(`${logPrefix} Extracting CSS from head <style> tag`);
|
|
119
|
-
addUniversalCSS(cssContent, src, framework, (renderResult as { scopeId?: string }).scopeId);
|
|
120
|
-
}
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
addUniversalHead(renderResult.head, src, framework, contentType);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ---------------------------------------------------------------------------
|
|
128
|
-
// Island – the synchronous component that emits <avalon-island> custom elements
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
|
|
131
|
-
/** Render the SSR path: we already have rendered children to embed */
|
|
132
|
-
function renderIslandSSR(opts: {
|
|
133
|
-
islandId: string;
|
|
134
|
-
detectedFramework: string;
|
|
135
|
-
shouldSkipHydration: boolean;
|
|
136
|
-
src: string;
|
|
137
|
-
condition: HydrationCondition;
|
|
138
|
-
props: Record<string, unknown>;
|
|
139
|
-
hydrationData: Record<string, unknown>;
|
|
140
|
-
children: import('preact').ComponentChildren;
|
|
141
|
-
}): JSX.Element {
|
|
142
|
-
const { islandId, detectedFramework, shouldSkipHydration, src, condition, props, hydrationData, children } = opts;
|
|
143
|
-
const baseAttributes: Record<string, string> = {
|
|
144
|
-
id: islandId,
|
|
145
|
-
'data-framework': detectedFramework,
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const hydrationAttributes = shouldSkipHydration
|
|
149
|
-
? { 'data-render-strategy': 'ssr-only' }
|
|
150
|
-
: buildHydrateAttributes(src, condition, props, hydrationData);
|
|
151
|
-
|
|
152
|
-
if (detectedFramework === 'lit') {
|
|
153
|
-
devLog(`🔍 [Island Component] ${src} - Lit hydration data:`, {
|
|
154
|
-
hydrationDataKeys: Object.keys(hydrationData),
|
|
155
|
-
metadata: hydrationData.metadata,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const allAttributes = { ...baseAttributes, ...hydrationAttributes };
|
|
160
|
-
|
|
161
|
-
if (typeof children === 'string') {
|
|
162
|
-
return h('avalon-island', { ...allAttributes, dangerouslySetInnerHTML: { __html: children } });
|
|
163
|
-
}
|
|
164
|
-
return h('avalon-island', allAttributes, children);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Render the client-only path: empty shell that will be hydrated on the client */
|
|
168
|
-
function renderIslandClientOnly(
|
|
169
|
-
islandId: string,
|
|
170
|
-
detectedFramework: string,
|
|
171
|
-
shouldSkipHydration: boolean,
|
|
172
|
-
src: string,
|
|
173
|
-
condition: HydrationCondition,
|
|
174
|
-
props: Record<string, unknown>,
|
|
175
|
-
hydrationData: Record<string, unknown>,
|
|
176
|
-
): JSX.Element {
|
|
177
|
-
if (shouldSkipHydration) {
|
|
178
|
-
return h('avalon-island', {
|
|
179
|
-
id: islandId,
|
|
180
|
-
'data-render-strategy': 'ssr-only',
|
|
181
|
-
'data-framework': detectedFramework,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return h('avalon-island', {
|
|
186
|
-
id: islandId,
|
|
187
|
-
'data-condition': condition,
|
|
188
|
-
'data-src': getIslandBundlePath(src),
|
|
189
|
-
'data-props': JSON.stringify(props),
|
|
190
|
-
'data-render-strategy': 'hydrate',
|
|
191
|
-
'data-framework': detectedFramework,
|
|
192
|
-
...buildHydrationDataAttrs(hydrationData),
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Universal Island component – renders `<avalon-island>` custom elements for better DOM structure.
|
|
198
|
-
*
|
|
199
|
-
* Uses custom elements instead of div wrappers for cleaner, more semantic markup.
|
|
200
|
-
* Supports intelligent rendering strategy detection to skip hydration for SSR-only components.
|
|
201
|
-
*/
|
|
202
|
-
export default function Island({
|
|
203
|
-
src,
|
|
204
|
-
condition = 'on:client',
|
|
205
|
-
props = {},
|
|
206
|
-
children,
|
|
207
|
-
ssr = condition !== 'on:client',
|
|
208
|
-
framework,
|
|
209
|
-
ssrOnly = false,
|
|
210
|
-
renderOptions = {},
|
|
211
|
-
hydrationData = {},
|
|
212
|
-
}: IslandProps): JSX.Element {
|
|
213
|
-
const islandId = toIslandId(src);
|
|
214
|
-
const shouldSkipHydration = ssrOnly || !!renderOptions.forceSSROnly;
|
|
215
|
-
const detectedFramework = framework || detectFrameworkFromPath(src);
|
|
216
|
-
const hasValidChildren = children !== undefined && children !== null && children !== '';
|
|
217
|
-
|
|
218
|
-
devLog(`🔍 [Island Component] ${src}`, { ssr, ssrOnly, hasChildren: hasValidChildren, framework, condition });
|
|
219
|
-
|
|
220
|
-
if (ssr && hasValidChildren) {
|
|
221
|
-
return renderIslandSSR({
|
|
222
|
-
islandId,
|
|
223
|
-
detectedFramework,
|
|
224
|
-
shouldSkipHydration,
|
|
225
|
-
src,
|
|
226
|
-
condition,
|
|
227
|
-
props,
|
|
228
|
-
hydrationData,
|
|
229
|
-
children,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (ssr && !hasValidChildren && shouldSkipHydration) {
|
|
234
|
-
devWarn(`${src}: SSR-only component has no rendered content. This may indicate a rendering error.`);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return renderIslandClientOnly(islandId, detectedFramework, shouldSkipHydration, src, condition, props, hydrationData);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// ---------------------------------------------------------------------------
|
|
241
|
-
// renderErrorPlaceholder
|
|
242
|
-
// ---------------------------------------------------------------------------
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Render an error placeholder when island SSR fails.
|
|
246
|
-
* @internal
|
|
247
|
-
*/
|
|
248
|
-
function renderErrorPlaceholder(src: string, error: unknown): JSX.Element {
|
|
249
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
250
|
-
devError(`🚨 Island SSR failed for ${src}:`, error);
|
|
251
|
-
if (error instanceof Error && error.stack) {
|
|
252
|
-
devError(`Stack trace:`, error.stack);
|
|
253
|
-
}
|
|
254
|
-
return h('avalon-island', {
|
|
255
|
-
id: toIslandId(src),
|
|
256
|
-
'data-src': getIslandBundlePath(src),
|
|
257
|
-
'data-ssr-error': errorMessage,
|
|
258
|
-
'data-render-strategy': 'client-only',
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// ---------------------------------------------------------------------------
|
|
263
|
-
// renderWithExplicitFramework (fast path)
|
|
264
|
-
// ---------------------------------------------------------------------------
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Render an island using the fast path when framework is explicitly provided.
|
|
268
|
-
* @internal
|
|
269
|
-
*/
|
|
270
|
-
async function renderWithExplicitFramework({
|
|
271
|
-
src,
|
|
272
|
-
condition,
|
|
273
|
-
props,
|
|
274
|
-
children,
|
|
275
|
-
ssr,
|
|
276
|
-
framework,
|
|
277
|
-
ssrOnly,
|
|
278
|
-
renderOptions,
|
|
279
|
-
}: {
|
|
280
|
-
src: string;
|
|
281
|
-
condition: IslandProps['condition'];
|
|
282
|
-
props: Record<string, unknown>;
|
|
283
|
-
children?: import('preact').ComponentChildren;
|
|
284
|
-
ssr: boolean;
|
|
285
|
-
framework: NonNullable<IslandProps['framework']>;
|
|
286
|
-
ssrOnly: boolean;
|
|
287
|
-
renderOptions: AnalyzerOptions;
|
|
288
|
-
}): Promise<JSX.Element> {
|
|
289
|
-
const logPrefix = `🏝️ [${src}]`;
|
|
290
|
-
|
|
291
|
-
if (!ssr || children) {
|
|
292
|
-
return Island({ src, condition, props, children, ssr, framework, ssrOnly, renderOptions });
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
let integration: Integration;
|
|
296
|
-
try {
|
|
297
|
-
integration = await loadIntegration(framework);
|
|
298
|
-
} catch (error) {
|
|
299
|
-
devError(`${logPrefix} Failed to load ${framework} integration:`, error);
|
|
300
|
-
return Island({ src, condition, props, ssr: false, framework, ssrOnly, renderOptions });
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
try {
|
|
304
|
-
const renderResult = await integration.render({
|
|
305
|
-
component: null,
|
|
306
|
-
props,
|
|
307
|
-
src,
|
|
308
|
-
condition,
|
|
309
|
-
ssrOnly,
|
|
310
|
-
viteServer: globalThis.__viteDevServer,
|
|
311
|
-
isDev: isDev(),
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
collectRenderAssets(renderResult, src, framework, logPrefix);
|
|
315
|
-
|
|
316
|
-
return Island({
|
|
317
|
-
src,
|
|
318
|
-
condition,
|
|
319
|
-
props,
|
|
320
|
-
children: renderResult.html,
|
|
321
|
-
ssr: true,
|
|
322
|
-
framework,
|
|
323
|
-
ssrOnly,
|
|
324
|
-
renderOptions,
|
|
325
|
-
hydrationData: ssrOnly ? undefined : renderResult.hydrationData,
|
|
326
|
-
});
|
|
327
|
-
} catch (error) {
|
|
328
|
-
devError(`${logPrefix} Fast path SSR failed:`, error);
|
|
329
|
-
return Island({ src, condition, props, ssr: false, framework, ssrOnly, renderOptions });
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// ---------------------------------------------------------------------------
|
|
334
|
-
// renderIsland slow-path helpers
|
|
335
|
-
// ---------------------------------------------------------------------------
|
|
336
|
-
|
|
337
|
-
/** Determine whether the component should skip hydration via analysis */
|
|
338
|
-
async function analyzeHydrationStrategy(
|
|
339
|
-
src: string,
|
|
340
|
-
ssrOnly: boolean,
|
|
341
|
-
renderOptions: AnalyzerOptions,
|
|
342
|
-
logPrefix: string,
|
|
343
|
-
): Promise<boolean> {
|
|
344
|
-
if (ssrOnly || renderOptions.detectScripts === false) return ssrOnly;
|
|
345
|
-
|
|
346
|
-
try {
|
|
347
|
-
const analysisResult = await analyzeComponentFile(src, renderOptions);
|
|
348
|
-
if (analysisResult.decision.warnings?.length) {
|
|
349
|
-
for (const warning of analysisResult.decision.warnings) {
|
|
350
|
-
devWarn(`${logPrefix} Analysis warning: ${warning}`);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
return !analysisResult.decision.shouldHydrate;
|
|
354
|
-
} catch (error) {
|
|
355
|
-
devWarn(`${logPrefix} Component analysis failed:`, error);
|
|
356
|
-
return ssrOnly;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/** Auto-detect framework from file extension / content */
|
|
361
|
-
async function detectFrameworkForSrc(src: string): Promise<string> {
|
|
362
|
-
if (src.endsWith('.vue')) return 'vue';
|
|
363
|
-
if (src.endsWith('.svelte')) return 'svelte';
|
|
364
|
-
if (src.endsWith('.tsx') || src.endsWith('.jsx') || src.endsWith('.ts') || src.endsWith('.js')) {
|
|
365
|
-
return detectFramework(src);
|
|
366
|
-
}
|
|
367
|
-
return 'unknown';
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/** Load an integration and render the component, returning the Island element */
|
|
371
|
-
async function renderSlowPathSSR(
|
|
372
|
-
src: string,
|
|
373
|
-
condition: HydrationCondition,
|
|
374
|
-
props: Record<string, unknown>,
|
|
375
|
-
ssrOnly: boolean,
|
|
376
|
-
renderOptions: AnalyzerOptions,
|
|
377
|
-
logPrefix: string,
|
|
378
|
-
): Promise<JSX.Element> {
|
|
379
|
-
const detectedFramework = await detectFrameworkForSrc(src);
|
|
380
|
-
const frameworkId = detectedFramework as FrameworkId;
|
|
381
|
-
|
|
382
|
-
const integration = await loadIntegrationOrThrow(detectedFramework, logPrefix);
|
|
383
|
-
const renderResult = await integration.render({
|
|
384
|
-
component: null,
|
|
385
|
-
props,
|
|
386
|
-
src,
|
|
387
|
-
condition,
|
|
388
|
-
ssrOnly,
|
|
389
|
-
viteServer: globalThis.__viteDevServer,
|
|
390
|
-
isDev: isDev(),
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
collectRenderAssets(renderResult, src, detectedFramework, logPrefix);
|
|
394
|
-
|
|
395
|
-
const result = Island({
|
|
396
|
-
src,
|
|
397
|
-
condition,
|
|
398
|
-
props,
|
|
399
|
-
children: renderResult.html,
|
|
400
|
-
ssr: true,
|
|
401
|
-
framework: frameworkId,
|
|
402
|
-
ssrOnly,
|
|
403
|
-
renderOptions,
|
|
404
|
-
hydrationData: ssrOnly ? undefined : renderResult.hydrationData,
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
return result;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/** Load an integration, throwing a descriptive error on failure */
|
|
411
|
-
async function loadIntegrationOrThrow(framework: string, logPrefix: string): Promise<Integration> {
|
|
412
|
-
try {
|
|
413
|
-
devLog(`${logPrefix} Loading integration for framework: ${framework}`);
|
|
414
|
-
const integration = await loadIntegration(framework);
|
|
415
|
-
devLog(`${logPrefix} ✅ Integration loaded successfully`);
|
|
416
|
-
return integration;
|
|
417
|
-
} catch (error) {
|
|
418
|
-
devError(`${logPrefix} Failed to load ${framework} integration:`, error);
|
|
419
|
-
throw new Error(
|
|
420
|
-
`Failed to load integration for framework '${framework}'. ` +
|
|
421
|
-
`Make sure @useavalon/${framework} is installed.\n` +
|
|
422
|
-
`Install it with: deno add @useavalon/${framework}`,
|
|
423
|
-
{ cause: error },
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// ---------------------------------------------------------------------------
|
|
429
|
-
// renderIsland – the main async entry point
|
|
430
|
-
// ---------------------------------------------------------------------------
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* Universal renderIsland function – auto-detects framework and handles SSR + hydration.
|
|
434
|
-
*
|
|
435
|
-
* This is the main function you should use – it automatically:
|
|
436
|
-
* - Detects the component framework (Vue, Solid.js, Preact/React)
|
|
437
|
-
* - Analyzes component for intelligent rendering strategy detection
|
|
438
|
-
* - Handles server-side rendering when possible
|
|
439
|
-
* - Falls back to client-only rendering when needed
|
|
440
|
-
* - Returns the appropriate Island component
|
|
441
|
-
*
|
|
442
|
-
* Performance tip: Providing an explicit `framework` prop skips component analysis
|
|
443
|
-
* and framework detection, significantly improving render performance.
|
|
444
|
-
*
|
|
445
|
-
* Error isolation: If SSR fails, returns an error placeholder instead of throwing,
|
|
446
|
-
* allowing the page to continue rendering other islands.
|
|
447
|
-
*/
|
|
448
|
-
export async function renderIsland({
|
|
449
|
-
src,
|
|
450
|
-
condition = 'on:client',
|
|
451
|
-
props = {},
|
|
452
|
-
children,
|
|
453
|
-
ssr = condition !== 'on:client',
|
|
454
|
-
framework,
|
|
455
|
-
ssrOnly = false,
|
|
456
|
-
renderOptions = {},
|
|
457
|
-
}: IslandProps): Promise<JSX.Element> {
|
|
458
|
-
const startTime = isDev() ? performance.now() : 0;
|
|
459
|
-
const logPrefix = `🏝️ [${src}]`;
|
|
460
|
-
|
|
461
|
-
try {
|
|
462
|
-
// If ssrOnly is true we MUST enable SSR to render the component
|
|
463
|
-
if (ssrOnly && !ssr) {
|
|
464
|
-
ssr = true;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Fast path: explicit framework skips all analysis/detection
|
|
468
|
-
if (framework) {
|
|
469
|
-
return await renderWithExplicitFramework({
|
|
470
|
-
src,
|
|
471
|
-
condition,
|
|
472
|
-
props,
|
|
473
|
-
children,
|
|
474
|
-
ssr,
|
|
475
|
-
framework,
|
|
476
|
-
ssrOnly,
|
|
477
|
-
renderOptions,
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Slow path: detect framework and analyze component
|
|
482
|
-
return await renderIslandSlowPath({
|
|
483
|
-
src,
|
|
484
|
-
condition,
|
|
485
|
-
props,
|
|
486
|
-
children,
|
|
487
|
-
ssr,
|
|
488
|
-
ssrOnly,
|
|
489
|
-
renderOptions,
|
|
490
|
-
logPrefix,
|
|
491
|
-
});
|
|
492
|
-
} catch (error) {
|
|
493
|
-
return renderErrorPlaceholder(src, error);
|
|
494
|
-
} finally {
|
|
495
|
-
if (isDev()) {
|
|
496
|
-
logRenderTiming(src, performance.now() - startTime);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/** Slow path for renderIsland – framework detection + component analysis */
|
|
502
|
-
async function renderIslandSlowPath(opts: {
|
|
503
|
-
src: string;
|
|
504
|
-
condition: HydrationCondition;
|
|
505
|
-
props: Record<string, unknown>;
|
|
506
|
-
children: import('preact').ComponentChildren | undefined;
|
|
507
|
-
ssr: boolean;
|
|
508
|
-
ssrOnly: boolean;
|
|
509
|
-
renderOptions: AnalyzerOptions;
|
|
510
|
-
logPrefix: string;
|
|
511
|
-
}): Promise<JSX.Element> {
|
|
512
|
-
const { src, condition, props, children, ssr, ssrOnly, renderOptions, logPrefix } = opts;
|
|
513
|
-
devLog(`🔍 [renderIsland] ${src} - Starting render (slow path)`, {
|
|
514
|
-
ssr,
|
|
515
|
-
ssrOnly,
|
|
516
|
-
hasChildren: !!children,
|
|
517
|
-
condition,
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
const shouldSkipHydration = await analyzeHydrationStrategy(src, ssrOnly, renderOptions, logPrefix);
|
|
521
|
-
|
|
522
|
-
if (shouldSkipHydration) {
|
|
523
|
-
return renderSSROnlyPath(src, condition, props, children, ssr, renderOptions, logPrefix);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// If SSR is disabled or we already have children, use basic Island
|
|
527
|
-
if (!ssr || children) {
|
|
528
|
-
return Island({ src, condition, props, children, ssr, renderOptions });
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// Full SSR rendering with auto-detected framework
|
|
532
|
-
try {
|
|
533
|
-
return await renderSlowPathSSR(src, condition, props, ssrOnly, renderOptions, logPrefix);
|
|
534
|
-
} catch (error) {
|
|
535
|
-
const detectedFramework = await detectFrameworkForSrc(src);
|
|
536
|
-
devError(`${logPrefix} Framework rendering failed:`, error);
|
|
537
|
-
return Island({
|
|
538
|
-
src,
|
|
539
|
-
condition,
|
|
540
|
-
props,
|
|
541
|
-
ssr: false,
|
|
542
|
-
framework: detectedFramework as FrameworkId,
|
|
543
|
-
renderOptions,
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
/** Handle the SSR-only path when hydration should be skipped */
|
|
549
|
-
function renderSSROnlyPath(
|
|
550
|
-
src: string,
|
|
551
|
-
condition: HydrationCondition,
|
|
552
|
-
props: Record<string, unknown>,
|
|
553
|
-
children: import('preact').ComponentChildren | undefined,
|
|
554
|
-
ssr: boolean,
|
|
555
|
-
renderOptions: AnalyzerOptions,
|
|
556
|
-
logPrefix: string,
|
|
557
|
-
): Promise<JSX.Element> | JSX.Element {
|
|
558
|
-
if (ssr && !children) {
|
|
559
|
-
return renderComponentSSROnly({ src, condition, props, renderOptions }).catch(error => {
|
|
560
|
-
devError(`${logPrefix} SSR failed for SSR-only component:`, error);
|
|
561
|
-
return Island({ src, condition, props, ssr: false, ssrOnly: true, renderOptions });
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
return Island({ src, condition, props, children, ssr, ssrOnly: true, renderOptions });
|
|
565
|
-
}
|
|
1
|
+
import type { JSX } from 'preact';
|
|
2
|
+
import { h } from 'preact';
|
|
3
|
+
import type { ViteDevServer } from 'vite';
|
|
4
|
+
import type { AnalyzerOptions } from '../core/components/component-analyzer.ts';
|
|
5
|
+
import type { Framework } from './types.ts';
|
|
6
|
+
import { detectFramework } from './framework-detection.ts';
|
|
7
|
+
import { analyzeComponentFile, renderComponentSSROnly } from './component-analysis.ts';
|
|
8
|
+
import { loadIntegration, detectFrameworkFromPath } from './integration-loader.ts';
|
|
9
|
+
import { addUniversalCSS } from './universal-css-collector.ts';
|
|
10
|
+
import { addUniversalHead } from './universal-head-collector.ts';
|
|
11
|
+
import { getIslandBundlePath } from '../build/island-manifest.ts';
|
|
12
|
+
import type { Integration } from '../../../integrations/core/types.ts';
|
|
13
|
+
import { isDev, devLog, devWarn, devError, logRenderTiming } from '../utils/dev-logger.ts';
|
|
14
|
+
|
|
15
|
+
// Enhanced global CSS collector for SSR with scoping support
|
|
16
|
+
declare global {
|
|
17
|
+
var __viteDevServer: ViteDevServer | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Supported hydration conditions for island components */
|
|
21
|
+
export type HydrationCondition = 'on:visible' | 'on:interaction' | 'on:idle' | 'on:client' | `media:${string}`;
|
|
22
|
+
|
|
23
|
+
/** Supported framework identifiers (without "unknown") */
|
|
24
|
+
export type FrameworkId = Exclude<Framework, 'unknown'>;
|
|
25
|
+
|
|
26
|
+
export interface IslandProps {
|
|
27
|
+
/** Path to the island component (e.g., "/islands/Counter.tsx") */
|
|
28
|
+
src: string;
|
|
29
|
+
/** Hydration condition */
|
|
30
|
+
condition?: HydrationCondition;
|
|
31
|
+
/** Props to pass to the island component */
|
|
32
|
+
props?: Record<string, unknown>;
|
|
33
|
+
/** Children to render inside the island (for SSR) */
|
|
34
|
+
children?: import('preact').ComponentChildren;
|
|
35
|
+
/** Whether to render server-side (default: true unless condition is 'on:client') */
|
|
36
|
+
ssr?: boolean;
|
|
37
|
+
/** Framework hint for client hydration */
|
|
38
|
+
framework?: FrameworkId;
|
|
39
|
+
/** Force SSR-only rendering without hydration */
|
|
40
|
+
ssrOnly?: boolean;
|
|
41
|
+
/** Component render options for intelligent detection */
|
|
42
|
+
renderOptions?: AnalyzerOptions;
|
|
43
|
+
/** Hydration data from integration renderer */
|
|
44
|
+
hydrationData?: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Shared helpers (extracted to reduce cognitive complexity of Island/renderIsland)
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/** Generate a deterministic island element ID from the source path */
|
|
52
|
+
function toIslandId(src: string): string {
|
|
53
|
+
return `island-${src.replaceAll(/[^a-zA-Z0-9]/g, '-')}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Build the extra hydration data-attributes from integration render output */
|
|
57
|
+
function buildHydrationDataAttrs(hydrationData: Record<string, unknown>): Record<string, string> {
|
|
58
|
+
const attrs: Record<string, string> = {};
|
|
59
|
+
if (hydrationData.renderId) {
|
|
60
|
+
attrs['data-solid-render-id'] = hydrationData.renderId as string;
|
|
61
|
+
}
|
|
62
|
+
const metadata = hydrationData.metadata as Record<string, unknown> | undefined;
|
|
63
|
+
if (metadata?.tagName) {
|
|
64
|
+
attrs['data-tag-name'] = metadata.tagName as string;
|
|
65
|
+
}
|
|
66
|
+
return attrs;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Build the full set of attributes for an `<avalon-island>` element that will be hydrated */
|
|
70
|
+
function buildHydrateAttributes(
|
|
71
|
+
src: string,
|
|
72
|
+
condition: HydrationCondition,
|
|
73
|
+
props: Record<string, unknown>,
|
|
74
|
+
hydrationData: Record<string, unknown>,
|
|
75
|
+
): Record<string, string> {
|
|
76
|
+
return {
|
|
77
|
+
'data-condition': condition,
|
|
78
|
+
'data-src': getIslandBundlePath(src),
|
|
79
|
+
'data-props': JSON.stringify(props),
|
|
80
|
+
'data-render-strategy': 'hydrate',
|
|
81
|
+
...buildHydrationDataAttrs(hydrationData),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Detect the head-content type from an HTML string returned by an integration */
|
|
86
|
+
function classifyHeadContent(headContent: string): 'script' | 'meta' | 'link' | 'style' | 'other' {
|
|
87
|
+
if (headContent.startsWith('<script')) return 'script';
|
|
88
|
+
if (headContent.startsWith('<style')) return 'style';
|
|
89
|
+
if (headContent.startsWith('<meta')) return 'meta';
|
|
90
|
+
if (headContent.startsWith('<link')) return 'link';
|
|
91
|
+
if (headContent.includes('window._$HY') || headContent.includes('_$HY=')) return 'script';
|
|
92
|
+
return 'other';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Extract CSS content from a <style> tag */
|
|
96
|
+
function extractCSSFromStyleTag(styleTag: string): string | null {
|
|
97
|
+
const match = styleTag.match(/<style[^>]*>([\s\S]*?)<\/style>/i);
|
|
98
|
+
return match ? match[1].trim() : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Collect CSS and head content produced by an integration render */
|
|
102
|
+
function collectRenderAssets(
|
|
103
|
+
renderResult: { css?: string; head?: string; scopeId?: string },
|
|
104
|
+
src: string,
|
|
105
|
+
framework: string,
|
|
106
|
+
logPrefix: string,
|
|
107
|
+
): void {
|
|
108
|
+
if (renderResult.css) {
|
|
109
|
+
addUniversalCSS(renderResult.css, src, framework, (renderResult as { scopeId?: string }).scopeId);
|
|
110
|
+
}
|
|
111
|
+
if (renderResult.head) {
|
|
112
|
+
const headContent = renderResult.head.trim();
|
|
113
|
+
const contentType = classifyHeadContent(headContent);
|
|
114
|
+
if (contentType === 'style') {
|
|
115
|
+
// Extract CSS from <style> tag and add to universal CSS collector
|
|
116
|
+
const cssContent = extractCSSFromStyleTag(headContent);
|
|
117
|
+
if (cssContent) {
|
|
118
|
+
devLog(`${logPrefix} Extracting CSS from head <style> tag`);
|
|
119
|
+
addUniversalCSS(cssContent, src, framework, (renderResult as { scopeId?: string }).scopeId);
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
addUniversalHead(renderResult.head, src, framework, contentType);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Island – the synchronous component that emits <avalon-island> custom elements
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
/** Render the SSR path: we already have rendered children to embed */
|
|
132
|
+
function renderIslandSSR(opts: {
|
|
133
|
+
islandId: string;
|
|
134
|
+
detectedFramework: string;
|
|
135
|
+
shouldSkipHydration: boolean;
|
|
136
|
+
src: string;
|
|
137
|
+
condition: HydrationCondition;
|
|
138
|
+
props: Record<string, unknown>;
|
|
139
|
+
hydrationData: Record<string, unknown>;
|
|
140
|
+
children: import('preact').ComponentChildren;
|
|
141
|
+
}): JSX.Element {
|
|
142
|
+
const { islandId, detectedFramework, shouldSkipHydration, src, condition, props, hydrationData, children } = opts;
|
|
143
|
+
const baseAttributes: Record<string, string> = {
|
|
144
|
+
id: islandId,
|
|
145
|
+
'data-framework': detectedFramework,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const hydrationAttributes = shouldSkipHydration
|
|
149
|
+
? { 'data-render-strategy': 'ssr-only' }
|
|
150
|
+
: buildHydrateAttributes(src, condition, props, hydrationData);
|
|
151
|
+
|
|
152
|
+
if (detectedFramework === 'lit') {
|
|
153
|
+
devLog(`🔍 [Island Component] ${src} - Lit hydration data:`, {
|
|
154
|
+
hydrationDataKeys: Object.keys(hydrationData),
|
|
155
|
+
metadata: hydrationData.metadata,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const allAttributes = { ...baseAttributes, ...hydrationAttributes };
|
|
160
|
+
|
|
161
|
+
if (typeof children === 'string') {
|
|
162
|
+
return h('avalon-island', { ...allAttributes, dangerouslySetInnerHTML: { __html: children } });
|
|
163
|
+
}
|
|
164
|
+
return h('avalon-island', allAttributes, children);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Render the client-only path: empty shell that will be hydrated on the client */
|
|
168
|
+
function renderIslandClientOnly(
|
|
169
|
+
islandId: string,
|
|
170
|
+
detectedFramework: string,
|
|
171
|
+
shouldSkipHydration: boolean,
|
|
172
|
+
src: string,
|
|
173
|
+
condition: HydrationCondition,
|
|
174
|
+
props: Record<string, unknown>,
|
|
175
|
+
hydrationData: Record<string, unknown>,
|
|
176
|
+
): JSX.Element {
|
|
177
|
+
if (shouldSkipHydration) {
|
|
178
|
+
return h('avalon-island', {
|
|
179
|
+
id: islandId,
|
|
180
|
+
'data-render-strategy': 'ssr-only',
|
|
181
|
+
'data-framework': detectedFramework,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return h('avalon-island', {
|
|
186
|
+
id: islandId,
|
|
187
|
+
'data-condition': condition,
|
|
188
|
+
'data-src': getIslandBundlePath(src),
|
|
189
|
+
'data-props': JSON.stringify(props),
|
|
190
|
+
'data-render-strategy': 'hydrate',
|
|
191
|
+
'data-framework': detectedFramework,
|
|
192
|
+
...buildHydrationDataAttrs(hydrationData),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Universal Island component – renders `<avalon-island>` custom elements for better DOM structure.
|
|
198
|
+
*
|
|
199
|
+
* Uses custom elements instead of div wrappers for cleaner, more semantic markup.
|
|
200
|
+
* Supports intelligent rendering strategy detection to skip hydration for SSR-only components.
|
|
201
|
+
*/
|
|
202
|
+
export default function Island({
|
|
203
|
+
src,
|
|
204
|
+
condition = 'on:client',
|
|
205
|
+
props = {},
|
|
206
|
+
children,
|
|
207
|
+
ssr = condition !== 'on:client',
|
|
208
|
+
framework,
|
|
209
|
+
ssrOnly = false,
|
|
210
|
+
renderOptions = {},
|
|
211
|
+
hydrationData = {},
|
|
212
|
+
}: IslandProps): JSX.Element {
|
|
213
|
+
const islandId = toIslandId(src);
|
|
214
|
+
const shouldSkipHydration = ssrOnly || !!renderOptions.forceSSROnly;
|
|
215
|
+
const detectedFramework = framework || detectFrameworkFromPath(src);
|
|
216
|
+
const hasValidChildren = children !== undefined && children !== null && children !== '';
|
|
217
|
+
|
|
218
|
+
devLog(`🔍 [Island Component] ${src}`, { ssr, ssrOnly, hasChildren: hasValidChildren, framework, condition });
|
|
219
|
+
|
|
220
|
+
if (ssr && hasValidChildren) {
|
|
221
|
+
return renderIslandSSR({
|
|
222
|
+
islandId,
|
|
223
|
+
detectedFramework,
|
|
224
|
+
shouldSkipHydration,
|
|
225
|
+
src,
|
|
226
|
+
condition,
|
|
227
|
+
props,
|
|
228
|
+
hydrationData,
|
|
229
|
+
children,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (ssr && !hasValidChildren && shouldSkipHydration) {
|
|
234
|
+
devWarn(`${src}: SSR-only component has no rendered content. This may indicate a rendering error.`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return renderIslandClientOnly(islandId, detectedFramework, shouldSkipHydration, src, condition, props, hydrationData);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// renderErrorPlaceholder
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Render an error placeholder when island SSR fails.
|
|
246
|
+
* @internal
|
|
247
|
+
*/
|
|
248
|
+
function renderErrorPlaceholder(src: string, error: unknown): JSX.Element {
|
|
249
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
250
|
+
devError(`🚨 Island SSR failed for ${src}:`, error);
|
|
251
|
+
if (error instanceof Error && error.stack) {
|
|
252
|
+
devError(`Stack trace:`, error.stack);
|
|
253
|
+
}
|
|
254
|
+
return h('avalon-island', {
|
|
255
|
+
id: toIslandId(src),
|
|
256
|
+
'data-src': getIslandBundlePath(src),
|
|
257
|
+
'data-ssr-error': errorMessage,
|
|
258
|
+
'data-render-strategy': 'client-only',
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// renderWithExplicitFramework (fast path)
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Render an island using the fast path when framework is explicitly provided.
|
|
268
|
+
* @internal
|
|
269
|
+
*/
|
|
270
|
+
async function renderWithExplicitFramework({
|
|
271
|
+
src,
|
|
272
|
+
condition,
|
|
273
|
+
props,
|
|
274
|
+
children,
|
|
275
|
+
ssr,
|
|
276
|
+
framework,
|
|
277
|
+
ssrOnly,
|
|
278
|
+
renderOptions,
|
|
279
|
+
}: {
|
|
280
|
+
src: string;
|
|
281
|
+
condition: IslandProps['condition'];
|
|
282
|
+
props: Record<string, unknown>;
|
|
283
|
+
children?: import('preact').ComponentChildren;
|
|
284
|
+
ssr: boolean;
|
|
285
|
+
framework: NonNullable<IslandProps['framework']>;
|
|
286
|
+
ssrOnly: boolean;
|
|
287
|
+
renderOptions: AnalyzerOptions;
|
|
288
|
+
}): Promise<JSX.Element> {
|
|
289
|
+
const logPrefix = `🏝️ [${src}]`;
|
|
290
|
+
|
|
291
|
+
if (!ssr || children) {
|
|
292
|
+
return Island({ src, condition, props, children, ssr, framework, ssrOnly, renderOptions });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let integration: Integration;
|
|
296
|
+
try {
|
|
297
|
+
integration = await loadIntegration(framework);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
devError(`${logPrefix} Failed to load ${framework} integration:`, error);
|
|
300
|
+
return Island({ src, condition, props, ssr: false, framework, ssrOnly, renderOptions });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const renderResult = await integration.render({
|
|
305
|
+
component: null,
|
|
306
|
+
props,
|
|
307
|
+
src,
|
|
308
|
+
condition,
|
|
309
|
+
ssrOnly,
|
|
310
|
+
viteServer: globalThis.__viteDevServer,
|
|
311
|
+
isDev: isDev(),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
collectRenderAssets(renderResult, src, framework, logPrefix);
|
|
315
|
+
|
|
316
|
+
return Island({
|
|
317
|
+
src,
|
|
318
|
+
condition,
|
|
319
|
+
props,
|
|
320
|
+
children: renderResult.html,
|
|
321
|
+
ssr: true,
|
|
322
|
+
framework,
|
|
323
|
+
ssrOnly,
|
|
324
|
+
renderOptions,
|
|
325
|
+
hydrationData: ssrOnly ? undefined : renderResult.hydrationData,
|
|
326
|
+
});
|
|
327
|
+
} catch (error) {
|
|
328
|
+
devError(`${logPrefix} Fast path SSR failed:`, error);
|
|
329
|
+
return Island({ src, condition, props, ssr: false, framework, ssrOnly, renderOptions });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// renderIsland slow-path helpers
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
/** Determine whether the component should skip hydration via analysis */
|
|
338
|
+
async function analyzeHydrationStrategy(
|
|
339
|
+
src: string,
|
|
340
|
+
ssrOnly: boolean,
|
|
341
|
+
renderOptions: AnalyzerOptions,
|
|
342
|
+
logPrefix: string,
|
|
343
|
+
): Promise<boolean> {
|
|
344
|
+
if (ssrOnly || renderOptions.detectScripts === false) return ssrOnly;
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const analysisResult = await analyzeComponentFile(src, renderOptions);
|
|
348
|
+
if (analysisResult.decision.warnings?.length) {
|
|
349
|
+
for (const warning of analysisResult.decision.warnings) {
|
|
350
|
+
devWarn(`${logPrefix} Analysis warning: ${warning}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return !analysisResult.decision.shouldHydrate;
|
|
354
|
+
} catch (error) {
|
|
355
|
+
devWarn(`${logPrefix} Component analysis failed:`, error);
|
|
356
|
+
return ssrOnly;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Auto-detect framework from file extension / content */
|
|
361
|
+
async function detectFrameworkForSrc(src: string): Promise<string> {
|
|
362
|
+
if (src.endsWith('.vue')) return 'vue';
|
|
363
|
+
if (src.endsWith('.svelte')) return 'svelte';
|
|
364
|
+
if (src.endsWith('.tsx') || src.endsWith('.jsx') || src.endsWith('.ts') || src.endsWith('.js')) {
|
|
365
|
+
return detectFramework(src);
|
|
366
|
+
}
|
|
367
|
+
return 'unknown';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Load an integration and render the component, returning the Island element */
|
|
371
|
+
async function renderSlowPathSSR(
|
|
372
|
+
src: string,
|
|
373
|
+
condition: HydrationCondition,
|
|
374
|
+
props: Record<string, unknown>,
|
|
375
|
+
ssrOnly: boolean,
|
|
376
|
+
renderOptions: AnalyzerOptions,
|
|
377
|
+
logPrefix: string,
|
|
378
|
+
): Promise<JSX.Element> {
|
|
379
|
+
const detectedFramework = await detectFrameworkForSrc(src);
|
|
380
|
+
const frameworkId = detectedFramework as FrameworkId;
|
|
381
|
+
|
|
382
|
+
const integration = await loadIntegrationOrThrow(detectedFramework, logPrefix);
|
|
383
|
+
const renderResult = await integration.render({
|
|
384
|
+
component: null,
|
|
385
|
+
props,
|
|
386
|
+
src,
|
|
387
|
+
condition,
|
|
388
|
+
ssrOnly,
|
|
389
|
+
viteServer: globalThis.__viteDevServer,
|
|
390
|
+
isDev: isDev(),
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
collectRenderAssets(renderResult, src, detectedFramework, logPrefix);
|
|
394
|
+
|
|
395
|
+
const result = Island({
|
|
396
|
+
src,
|
|
397
|
+
condition,
|
|
398
|
+
props,
|
|
399
|
+
children: renderResult.html,
|
|
400
|
+
ssr: true,
|
|
401
|
+
framework: frameworkId,
|
|
402
|
+
ssrOnly,
|
|
403
|
+
renderOptions,
|
|
404
|
+
hydrationData: ssrOnly ? undefined : renderResult.hydrationData,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Load an integration, throwing a descriptive error on failure */
|
|
411
|
+
async function loadIntegrationOrThrow(framework: string, logPrefix: string): Promise<Integration> {
|
|
412
|
+
try {
|
|
413
|
+
devLog(`${logPrefix} Loading integration for framework: ${framework}`);
|
|
414
|
+
const integration = await loadIntegration(framework);
|
|
415
|
+
devLog(`${logPrefix} ✅ Integration loaded successfully`);
|
|
416
|
+
return integration;
|
|
417
|
+
} catch (error) {
|
|
418
|
+
devError(`${logPrefix} Failed to load ${framework} integration:`, error);
|
|
419
|
+
throw new Error(
|
|
420
|
+
`Failed to load integration for framework '${framework}'. ` +
|
|
421
|
+
`Make sure @useavalon/${framework} is installed.\n` +
|
|
422
|
+
`Install it with: deno add @useavalon/${framework}`,
|
|
423
|
+
{ cause: error },
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ---------------------------------------------------------------------------
|
|
429
|
+
// renderIsland – the main async entry point
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Universal renderIsland function – auto-detects framework and handles SSR + hydration.
|
|
434
|
+
*
|
|
435
|
+
* This is the main function you should use – it automatically:
|
|
436
|
+
* - Detects the component framework (Vue, Solid.js, Preact/React)
|
|
437
|
+
* - Analyzes component for intelligent rendering strategy detection
|
|
438
|
+
* - Handles server-side rendering when possible
|
|
439
|
+
* - Falls back to client-only rendering when needed
|
|
440
|
+
* - Returns the appropriate Island component
|
|
441
|
+
*
|
|
442
|
+
* Performance tip: Providing an explicit `framework` prop skips component analysis
|
|
443
|
+
* and framework detection, significantly improving render performance.
|
|
444
|
+
*
|
|
445
|
+
* Error isolation: If SSR fails, returns an error placeholder instead of throwing,
|
|
446
|
+
* allowing the page to continue rendering other islands.
|
|
447
|
+
*/
|
|
448
|
+
export async function renderIsland({
|
|
449
|
+
src,
|
|
450
|
+
condition = 'on:client',
|
|
451
|
+
props = {},
|
|
452
|
+
children,
|
|
453
|
+
ssr = condition !== 'on:client',
|
|
454
|
+
framework,
|
|
455
|
+
ssrOnly = false,
|
|
456
|
+
renderOptions = {},
|
|
457
|
+
}: IslandProps): Promise<JSX.Element> {
|
|
458
|
+
const startTime = isDev() ? performance.now() : 0;
|
|
459
|
+
const logPrefix = `🏝️ [${src}]`;
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
// If ssrOnly is true we MUST enable SSR to render the component
|
|
463
|
+
if (ssrOnly && !ssr) {
|
|
464
|
+
ssr = true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Fast path: explicit framework skips all analysis/detection
|
|
468
|
+
if (framework) {
|
|
469
|
+
return await renderWithExplicitFramework({
|
|
470
|
+
src,
|
|
471
|
+
condition,
|
|
472
|
+
props,
|
|
473
|
+
children,
|
|
474
|
+
ssr,
|
|
475
|
+
framework,
|
|
476
|
+
ssrOnly,
|
|
477
|
+
renderOptions,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Slow path: detect framework and analyze component
|
|
482
|
+
return await renderIslandSlowPath({
|
|
483
|
+
src,
|
|
484
|
+
condition,
|
|
485
|
+
props,
|
|
486
|
+
children,
|
|
487
|
+
ssr,
|
|
488
|
+
ssrOnly,
|
|
489
|
+
renderOptions,
|
|
490
|
+
logPrefix,
|
|
491
|
+
});
|
|
492
|
+
} catch (error) {
|
|
493
|
+
return renderErrorPlaceholder(src, error);
|
|
494
|
+
} finally {
|
|
495
|
+
if (isDev()) {
|
|
496
|
+
logRenderTiming(src, performance.now() - startTime);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** Slow path for renderIsland – framework detection + component analysis */
|
|
502
|
+
async function renderIslandSlowPath(opts: {
|
|
503
|
+
src: string;
|
|
504
|
+
condition: HydrationCondition;
|
|
505
|
+
props: Record<string, unknown>;
|
|
506
|
+
children: import('preact').ComponentChildren | undefined;
|
|
507
|
+
ssr: boolean;
|
|
508
|
+
ssrOnly: boolean;
|
|
509
|
+
renderOptions: AnalyzerOptions;
|
|
510
|
+
logPrefix: string;
|
|
511
|
+
}): Promise<JSX.Element> {
|
|
512
|
+
const { src, condition, props, children, ssr, ssrOnly, renderOptions, logPrefix } = opts;
|
|
513
|
+
devLog(`🔍 [renderIsland] ${src} - Starting render (slow path)`, {
|
|
514
|
+
ssr,
|
|
515
|
+
ssrOnly,
|
|
516
|
+
hasChildren: !!children,
|
|
517
|
+
condition,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const shouldSkipHydration = await analyzeHydrationStrategy(src, ssrOnly, renderOptions, logPrefix);
|
|
521
|
+
|
|
522
|
+
if (shouldSkipHydration) {
|
|
523
|
+
return renderSSROnlyPath(src, condition, props, children, ssr, renderOptions, logPrefix);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// If SSR is disabled or we already have children, use basic Island
|
|
527
|
+
if (!ssr || children) {
|
|
528
|
+
return Island({ src, condition, props, children, ssr, renderOptions });
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Full SSR rendering with auto-detected framework
|
|
532
|
+
try {
|
|
533
|
+
return await renderSlowPathSSR(src, condition, props, ssrOnly, renderOptions, logPrefix);
|
|
534
|
+
} catch (error) {
|
|
535
|
+
const detectedFramework = await detectFrameworkForSrc(src);
|
|
536
|
+
devError(`${logPrefix} Framework rendering failed:`, error);
|
|
537
|
+
return Island({
|
|
538
|
+
src,
|
|
539
|
+
condition,
|
|
540
|
+
props,
|
|
541
|
+
ssr: false,
|
|
542
|
+
framework: detectedFramework as FrameworkId,
|
|
543
|
+
renderOptions,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** Handle the SSR-only path when hydration should be skipped */
|
|
549
|
+
function renderSSROnlyPath(
|
|
550
|
+
src: string,
|
|
551
|
+
condition: HydrationCondition,
|
|
552
|
+
props: Record<string, unknown>,
|
|
553
|
+
children: import('preact').ComponentChildren | undefined,
|
|
554
|
+
ssr: boolean,
|
|
555
|
+
renderOptions: AnalyzerOptions,
|
|
556
|
+
logPrefix: string,
|
|
557
|
+
): Promise<JSX.Element> | JSX.Element {
|
|
558
|
+
if (ssr && !children) {
|
|
559
|
+
return renderComponentSSROnly({ src, condition, props, renderOptions }).catch(error => {
|
|
560
|
+
devError(`${logPrefix} SSR failed for SSR-only component:`, error);
|
|
561
|
+
return Island({ src, condition, props, ssr: false, ssrOnly: true, renderOptions });
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return Island({ src, condition, props, children, ssr, ssrOnly: true, renderOptions });
|
|
565
|
+
}
|