@buenojs/bueno 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Island Architecture Implementation
|
|
3
|
+
*
|
|
4
|
+
* Provides partial hydration capabilities:
|
|
5
|
+
* - Mark components as interactive islands
|
|
6
|
+
* - Framework-agnostic island definitions
|
|
7
|
+
* - Lazy/eager/visible hydration strategies
|
|
8
|
+
* - State serialization for islands
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createLogger, type Logger } from "../logger/index.js";
|
|
12
|
+
import type {
|
|
13
|
+
IslandConfig,
|
|
14
|
+
IslandDefinition,
|
|
15
|
+
IslandHydrationStrategy,
|
|
16
|
+
IslandRegistry,
|
|
17
|
+
IslandRenderResult,
|
|
18
|
+
IslandState,
|
|
19
|
+
IslandHydrationScript,
|
|
20
|
+
FrontendFramework,
|
|
21
|
+
SSRElement,
|
|
22
|
+
} from "./types.js";
|
|
23
|
+
|
|
24
|
+
// ============= Constants =============
|
|
25
|
+
|
|
26
|
+
const ISLAND_MARKER = "data-island";
|
|
27
|
+
const ISLAND_ID = "data-island-id";
|
|
28
|
+
const ISLAND_COMPONENT = "data-island-component";
|
|
29
|
+
const ISLAND_PROPS = "data-island-props";
|
|
30
|
+
const ISLAND_STRATEGY = "data-island-strategy";
|
|
31
|
+
|
|
32
|
+
// ============= Island Manager Class =============
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Island Manager handles partial hydration of components
|
|
36
|
+
*
|
|
37
|
+
* Features:
|
|
38
|
+
* - Register interactive components as islands
|
|
39
|
+
* - Multiple hydration strategies (lazy, eager, visible, idle)
|
|
40
|
+
* - Framework-agnostic island definitions
|
|
41
|
+
* - State serialization for client hydration
|
|
42
|
+
*/
|
|
43
|
+
export class IslandManager {
|
|
44
|
+
private registry: IslandRegistry = new Map();
|
|
45
|
+
private logger: Logger;
|
|
46
|
+
private islandCounter = 0;
|
|
47
|
+
private framework: FrontendFramework;
|
|
48
|
+
|
|
49
|
+
constructor(framework: FrontendFramework) {
|
|
50
|
+
this.framework = framework;
|
|
51
|
+
this.logger = createLogger({
|
|
52
|
+
level: "debug",
|
|
53
|
+
pretty: true,
|
|
54
|
+
context: { component: "IslandManager" },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Register an island component
|
|
60
|
+
*/
|
|
61
|
+
register(definition: IslandDefinition): string {
|
|
62
|
+
const id = definition.id || `island-${++this.islandCounter}`;
|
|
63
|
+
|
|
64
|
+
this.registry.set(id, {
|
|
65
|
+
...definition,
|
|
66
|
+
id,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
this.logger.debug(`Registered island: ${id} (${definition.component})`);
|
|
70
|
+
return id;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Register multiple islands
|
|
75
|
+
*/
|
|
76
|
+
registerAll(definitions: IslandDefinition[]): string[] {
|
|
77
|
+
return definitions.map(def => this.register(def));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Unregister an island
|
|
82
|
+
*/
|
|
83
|
+
unregister(id: string): boolean {
|
|
84
|
+
return this.registry.delete(id);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get island by ID
|
|
89
|
+
*/
|
|
90
|
+
getIsland(id: string): IslandDefinition | undefined {
|
|
91
|
+
return this.registry.get(id);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get all registered islands
|
|
96
|
+
*/
|
|
97
|
+
getAllIslands(): IslandDefinition[] {
|
|
98
|
+
return Array.from(this.registry.values());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Render an island to HTML
|
|
103
|
+
*/
|
|
104
|
+
renderIsland(
|
|
105
|
+
componentName: string,
|
|
106
|
+
props: Record<string, unknown> = {},
|
|
107
|
+
options: Partial<IslandConfig> = {}
|
|
108
|
+
): IslandRenderResult {
|
|
109
|
+
const id = options.id || `island-${++this.islandCounter}`;
|
|
110
|
+
const strategy = options.strategy || "lazy";
|
|
111
|
+
|
|
112
|
+
// Find the island definition
|
|
113
|
+
const definition = this.findIslandByComponent(componentName);
|
|
114
|
+
|
|
115
|
+
if (!definition) {
|
|
116
|
+
this.logger.warn(`Island not found: ${componentName}`);
|
|
117
|
+
return {
|
|
118
|
+
html: `<!-- Island not found: ${componentName} -->`,
|
|
119
|
+
id,
|
|
120
|
+
component: componentName,
|
|
121
|
+
hydrated: false,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Generate the island HTML wrapper
|
|
126
|
+
const propsJson = JSON.stringify(props);
|
|
127
|
+
const escapedProps = this.escapeHtml(propsJson);
|
|
128
|
+
|
|
129
|
+
const wrapperAttrs = {
|
|
130
|
+
[ISLAND_MARKER]: "true",
|
|
131
|
+
[ISLAND_ID]: id,
|
|
132
|
+
[ISLAND_COMPONENT]: componentName,
|
|
133
|
+
[ISLAND_PROPS]: escapedProps,
|
|
134
|
+
[ISLAND_STRATEGY]: strategy,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const attrString = Object.entries(wrapperAttrs)
|
|
138
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
139
|
+
.join(" ");
|
|
140
|
+
|
|
141
|
+
// Generate placeholder or SSR content
|
|
142
|
+
const placeholder = options.placeholder || this.generatePlaceholder(componentName, props);
|
|
143
|
+
|
|
144
|
+
const html = `<div ${attrString}>${placeholder}</div>`;
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
html,
|
|
148
|
+
id,
|
|
149
|
+
component: componentName,
|
|
150
|
+
hydrated: false,
|
|
151
|
+
props,
|
|
152
|
+
strategy,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Render island with SSR content
|
|
158
|
+
*/
|
|
159
|
+
renderIslandSSR(
|
|
160
|
+
componentName: string,
|
|
161
|
+
ssrContent: string,
|
|
162
|
+
props: Record<string, unknown> = {},
|
|
163
|
+
options: Partial<IslandConfig> = {}
|
|
164
|
+
): IslandRenderResult {
|
|
165
|
+
const id = options.id || `island-${++this.islandCounter}`;
|
|
166
|
+
const strategy = options.strategy || "lazy";
|
|
167
|
+
|
|
168
|
+
const propsJson = JSON.stringify(props);
|
|
169
|
+
const escapedProps = this.escapeHtml(propsJson);
|
|
170
|
+
|
|
171
|
+
const wrapperAttrs = {
|
|
172
|
+
[ISLAND_MARKER]: "true",
|
|
173
|
+
[ISLAND_ID]: id,
|
|
174
|
+
[ISLAND_COMPONENT]: componentName,
|
|
175
|
+
[ISLAND_PROPS]: escapedProps,
|
|
176
|
+
[ISLAND_STRATEGY]: strategy,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const attrString = Object.entries(wrapperAttrs)
|
|
180
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
181
|
+
.join(" ");
|
|
182
|
+
|
|
183
|
+
const html = `<div ${attrString}>${ssrContent}</div>`;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
html,
|
|
187
|
+
id,
|
|
188
|
+
component: componentName,
|
|
189
|
+
hydrated: false,
|
|
190
|
+
props,
|
|
191
|
+
strategy,
|
|
192
|
+
ssrContent,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Find island by component name
|
|
198
|
+
*/
|
|
199
|
+
private findIslandByComponent(componentName: string): IslandDefinition | undefined {
|
|
200
|
+
for (const island of this.registry.values()) {
|
|
201
|
+
if (island.component === componentName) {
|
|
202
|
+
return island;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Generate placeholder content
|
|
210
|
+
*/
|
|
211
|
+
private generatePlaceholder(
|
|
212
|
+
componentName: string,
|
|
213
|
+
props: Record<string, unknown>
|
|
214
|
+
): string {
|
|
215
|
+
// Generate a simple placeholder based on component type
|
|
216
|
+
if (props.children && typeof props.children === "string") {
|
|
217
|
+
return props.children;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Return empty placeholder - will be filled on hydration
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Escape HTML in JSON string
|
|
226
|
+
*/
|
|
227
|
+
private escapeHtml(str: string): string {
|
|
228
|
+
return str
|
|
229
|
+
.replace(/&/g, "&")
|
|
230
|
+
.replace(/</g, "<")
|
|
231
|
+
.replace(/>/g, ">")
|
|
232
|
+
.replace(/"/g, "\"")
|
|
233
|
+
.replace(/'/g, "'");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get hydration script for all islands
|
|
238
|
+
*/
|
|
239
|
+
getHydrationScript(): string {
|
|
240
|
+
const islands = this.getAllIslands();
|
|
241
|
+
const framework = this.framework;
|
|
242
|
+
|
|
243
|
+
const islandData = islands.map(island => ({
|
|
244
|
+
id: island.id,
|
|
245
|
+
component: island.component,
|
|
246
|
+
entry: island.entry,
|
|
247
|
+
strategy: island.strategy,
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
return `
|
|
251
|
+
(function() {
|
|
252
|
+
const islands = ${JSON.stringify(islandData)};
|
|
253
|
+
const framework = "${framework}";
|
|
254
|
+
|
|
255
|
+
// Island hydration manager
|
|
256
|
+
window.__ISLANDS__ = {
|
|
257
|
+
pending: new Map(),
|
|
258
|
+
hydrated: new Set(),
|
|
259
|
+
registry: new Map(islands.map(i => [i.id, i])),
|
|
260
|
+
|
|
261
|
+
// Hydrate a single island
|
|
262
|
+
async hydrate(islandId) {
|
|
263
|
+
if (this.hydrated.has(islandId)) return;
|
|
264
|
+
|
|
265
|
+
const island = this.registry.get(islandId);
|
|
266
|
+
if (!island) return;
|
|
267
|
+
|
|
268
|
+
const element = document.querySelector('[data-island-id="' + islandId + '"]');
|
|
269
|
+
if (!element) return;
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const props = JSON.parse(element.getAttribute('data-island-props') || '{}');
|
|
273
|
+
const module = await import(island.entry);
|
|
274
|
+
|
|
275
|
+
if (framework === 'react') {
|
|
276
|
+
const { hydrate } = await import('react-dom/client');
|
|
277
|
+
hydrate(module.default(element, props), element);
|
|
278
|
+
} else if (framework === 'vue') {
|
|
279
|
+
const { createApp } = await import('vue');
|
|
280
|
+
createApp(module.default, props).mount(element, true);
|
|
281
|
+
} else if (framework === 'svelte') {
|
|
282
|
+
module.mount(element, { props, hydrate: true });
|
|
283
|
+
} else if (framework === 'solid') {
|
|
284
|
+
const { hydrate } = await import('solid-js/web');
|
|
285
|
+
hydrate(() => module.default(props), element);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.hydrated.add(islandId);
|
|
289
|
+
console.log('[Islands] Hydrated:', islandId);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error('[Islands] Hydration failed:', islandId, error);
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
// Hydrate all islands with eager strategy
|
|
296
|
+
async hydrateEager() {
|
|
297
|
+
for (const [id, island] of this.registry) {
|
|
298
|
+
if (island.strategy === 'eager') {
|
|
299
|
+
await this.hydrate(id);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
// Hydrate islands when visible
|
|
305
|
+
hydrateVisible() {
|
|
306
|
+
const observer = new IntersectionObserver((entries) => {
|
|
307
|
+
for (const entry of entries) {
|
|
308
|
+
if (entry.isIntersecting) {
|
|
309
|
+
const id = entry.target.getAttribute('data-island-id');
|
|
310
|
+
if (id) this.hydrate(id);
|
|
311
|
+
observer.unobserve(entry.target);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}, { rootMargin: '50px' });
|
|
315
|
+
|
|
316
|
+
for (const [id, island] of this.registry) {
|
|
317
|
+
if (island.strategy === 'visible') {
|
|
318
|
+
const element = document.querySelector('[data-island-id="' + id + '"]');
|
|
319
|
+
if (element) observer.observe(element);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
// Hydrate islands when idle
|
|
325
|
+
hydrateIdle() {
|
|
326
|
+
for (const [id, island] of this.registry) {
|
|
327
|
+
if (island.strategy === 'idle') {
|
|
328
|
+
if ('requestIdleCallback' in window) {
|
|
329
|
+
requestIdleCallback(() => this.hydrate(id));
|
|
330
|
+
} else {
|
|
331
|
+
setTimeout(() => this.hydrate(id), 1);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
// Hydrate lazy islands on interaction
|
|
338
|
+
hydrateLazy() {
|
|
339
|
+
for (const [id, island] of this.registry) {
|
|
340
|
+
if (island.strategy === 'lazy') {
|
|
341
|
+
const element = document.querySelector('[data-island-id="' + id + '"]');
|
|
342
|
+
if (element) {
|
|
343
|
+
const events = ['mouseenter', 'focus', 'touchstart', 'click'];
|
|
344
|
+
const handler = () => {
|
|
345
|
+
this.hydrate(id);
|
|
346
|
+
events.forEach(e => element.removeEventListener(e, handler));
|
|
347
|
+
};
|
|
348
|
+
events.forEach(e => element.addEventListener(e, handler, { once: true }));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
// Initialize all islands
|
|
355
|
+
init() {
|
|
356
|
+
this.hydrateEager();
|
|
357
|
+
this.hydrateVisible();
|
|
358
|
+
this.hydrateIdle();
|
|
359
|
+
this.hydrateLazy();
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// Initialize on DOM ready
|
|
364
|
+
if (document.readyState === 'loading') {
|
|
365
|
+
document.addEventListener('DOMContentLoaded', () => window.__ISLANDS__.init());
|
|
366
|
+
} else {
|
|
367
|
+
window.__ISLANDS__.init();
|
|
368
|
+
}
|
|
369
|
+
})();
|
|
370
|
+
`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Get client island script (minimal)
|
|
375
|
+
*/
|
|
376
|
+
getClientScript(): string {
|
|
377
|
+
return `
|
|
378
|
+
import { createIslandHydrator } from 'bueno/frontend/islands-client';
|
|
379
|
+
|
|
380
|
+
const hydrator = createIslandHydrator('${this.framework}');
|
|
381
|
+
hydrator.init();
|
|
382
|
+
`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Get island count
|
|
387
|
+
*/
|
|
388
|
+
getIslandCount(): number {
|
|
389
|
+
return this.registry.size;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Clear all islands
|
|
394
|
+
*/
|
|
395
|
+
clear(): void {
|
|
396
|
+
this.registry.clear();
|
|
397
|
+
this.islandCounter = 0;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Get framework
|
|
402
|
+
*/
|
|
403
|
+
getFramework(): FrontendFramework {
|
|
404
|
+
return this.framework;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Set framework
|
|
409
|
+
*/
|
|
410
|
+
setFramework(framework: FrontendFramework): void {
|
|
411
|
+
this.framework = framework;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ============= Factory Function =============
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Create an island manager
|
|
419
|
+
*/
|
|
420
|
+
export function createIslandManager(framework: FrontendFramework): IslandManager {
|
|
421
|
+
return new IslandManager(framework);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ============= Utility Functions =============
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Create an island definition
|
|
428
|
+
*/
|
|
429
|
+
export function defineIsland(
|
|
430
|
+
component: string,
|
|
431
|
+
entry: string,
|
|
432
|
+
options: Partial<IslandDefinition> = {}
|
|
433
|
+
): IslandDefinition {
|
|
434
|
+
return {
|
|
435
|
+
id: options.id || `island-${component}`,
|
|
436
|
+
component,
|
|
437
|
+
entry,
|
|
438
|
+
strategy: options.strategy || "lazy",
|
|
439
|
+
...options,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Element-like interface for DOM elements (for type checking without DOM lib)
|
|
445
|
+
*/
|
|
446
|
+
interface ElementLike {
|
|
447
|
+
hasAttribute(name: string): boolean;
|
|
448
|
+
getAttribute(name: string): string | null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Check if an element is an island
|
|
453
|
+
*/
|
|
454
|
+
export function isIslandElement(element: ElementLike): boolean {
|
|
455
|
+
return element.hasAttribute(ISLAND_MARKER);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Get island data from element
|
|
460
|
+
*/
|
|
461
|
+
export function getIslandData(element: ElementLike): IslandState | null {
|
|
462
|
+
if (!isIslandElement(element)) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
id: element.getAttribute(ISLAND_ID) || "",
|
|
468
|
+
component: element.getAttribute(ISLAND_COMPONENT) || "",
|
|
469
|
+
props: JSON.parse(element.getAttribute(ISLAND_PROPS) || "{}"),
|
|
470
|
+
strategy: (element.getAttribute(ISLAND_STRATEGY) as IslandHydrationStrategy) || "lazy",
|
|
471
|
+
hydrated: false,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Generate island wrapper attributes
|
|
477
|
+
*/
|
|
478
|
+
export function getIslandAttributes(
|
|
479
|
+
id: string,
|
|
480
|
+
component: string,
|
|
481
|
+
props: Record<string, unknown>,
|
|
482
|
+
strategy: IslandHydrationStrategy = "lazy"
|
|
483
|
+
): Record<string, string> {
|
|
484
|
+
return {
|
|
485
|
+
[ISLAND_MARKER]: "true",
|
|
486
|
+
[ISLAND_ID]: id,
|
|
487
|
+
[ISLAND_COMPONENT]: component,
|
|
488
|
+
[ISLAND_PROPS]: JSON.stringify(props),
|
|
489
|
+
[ISLAND_STRATEGY]: strategy,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Create island SSR element
|
|
495
|
+
*/
|
|
496
|
+
export function createIslandElement(
|
|
497
|
+
id: string,
|
|
498
|
+
component: string,
|
|
499
|
+
props: Record<string, unknown>,
|
|
500
|
+
strategy: IslandHydrationStrategy = "lazy",
|
|
501
|
+
children?: SSRElement[]
|
|
502
|
+
): SSRElement {
|
|
503
|
+
return {
|
|
504
|
+
tag: "div",
|
|
505
|
+
attrs: getIslandAttributes(id, component, props, strategy),
|
|
506
|
+
children,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Parse islands from HTML string
|
|
512
|
+
*/
|
|
513
|
+
export function parseIslandsFromHTML(html: string): IslandState[] {
|
|
514
|
+
const islands: IslandState[] = [];
|
|
515
|
+
const regex = /data-island-id="([^"]+)"[^>]*data-island-component="([^"]+)"[^>]*data-island-props="([^"]+)"[^>]*data-island-strategy="([^"]+)"/g;
|
|
516
|
+
|
|
517
|
+
let match;
|
|
518
|
+
while ((match = regex.exec(html)) !== null) {
|
|
519
|
+
islands.push({
|
|
520
|
+
id: match[1],
|
|
521
|
+
component: match[2],
|
|
522
|
+
props: JSON.parse(match[3].replace(/"/g, '"').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')),
|
|
523
|
+
strategy: match[4] as IslandHydrationStrategy,
|
|
524
|
+
hydrated: false,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return islands;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Get hydration priority
|
|
533
|
+
*/
|
|
534
|
+
export function getHydrationPriority(strategy: IslandHydrationStrategy): number {
|
|
535
|
+
const priorities: Record<IslandHydrationStrategy, number> = {
|
|
536
|
+
eager: 1,
|
|
537
|
+
visible: 2,
|
|
538
|
+
idle: 3,
|
|
539
|
+
lazy: 4,
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
return priorities[strategy] || 4;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Sort islands by hydration priority
|
|
547
|
+
*/
|
|
548
|
+
export function sortIslandsByPriority(islands: IslandDefinition[]): IslandDefinition[] {
|
|
549
|
+
return [...islands].sort(
|
|
550
|
+
(a, b) => getHydrationPriority(a.strategy) - getHydrationPriority(b.strategy)
|
|
551
|
+
);
|
|
552
|
+
}
|