@useavalon/avalon 0.1.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.
Files changed (159) hide show
  1. package/README.md +54 -0
  2. package/mod.ts +301 -0
  3. package/package.json +85 -0
  4. package/src/build/README.md +310 -0
  5. package/src/build/integration-bundler-plugin.ts +116 -0
  6. package/src/build/integration-config.ts +168 -0
  7. package/src/build/integration-detection-plugin.ts +117 -0
  8. package/src/build/integration-resolver-plugin.ts +90 -0
  9. package/src/build/island-manifest.ts +269 -0
  10. package/src/build/island-types-generator.ts +476 -0
  11. package/src/build/mdx-island-transform.ts +464 -0
  12. package/src/build/mdx-plugin.ts +98 -0
  13. package/src/build/page-island-transform.ts +598 -0
  14. package/src/build/prop-extractors/index.ts +21 -0
  15. package/src/build/prop-extractors/lit.ts +140 -0
  16. package/src/build/prop-extractors/qwik.ts +16 -0
  17. package/src/build/prop-extractors/solid.ts +125 -0
  18. package/src/build/prop-extractors/svelte.ts +194 -0
  19. package/src/build/prop-extractors/vue.ts +111 -0
  20. package/src/build/sidecar-file-manager.ts +104 -0
  21. package/src/build/sidecar-renderer.ts +30 -0
  22. package/src/client/adapters/index.ts +13 -0
  23. package/src/client/adapters/lit-adapter.ts +654 -0
  24. package/src/client/adapters/preact-adapter.ts +331 -0
  25. package/src/client/adapters/qwik-adapter.ts +345 -0
  26. package/src/client/adapters/react-adapter.ts +353 -0
  27. package/src/client/adapters/solid-adapter.ts +451 -0
  28. package/src/client/adapters/svelte-adapter.ts +524 -0
  29. package/src/client/adapters/vue-adapter.ts +467 -0
  30. package/src/client/components.ts +35 -0
  31. package/src/client/css-hmr-handler.ts +344 -0
  32. package/src/client/framework-adapter.ts +462 -0
  33. package/src/client/hmr-coordinator.ts +396 -0
  34. package/src/client/hmr-error-overlay.js +533 -0
  35. package/src/client/main.js +816 -0
  36. package/src/client/tests/css-hmr-handler.test.ts +360 -0
  37. package/src/client/tests/framework-adapter.test.ts +519 -0
  38. package/src/client/tests/hmr-coordinator.test.ts +176 -0
  39. package/src/client/tests/hydration-option-parsing.test.ts +107 -0
  40. package/src/client/tests/lit-adapter.test.ts +427 -0
  41. package/src/client/tests/preact-adapter.test.ts +353 -0
  42. package/src/client/tests/qwik-adapter.test.ts +343 -0
  43. package/src/client/tests/react-adapter.test.ts +317 -0
  44. package/src/client/tests/solid-adapter.test.ts +396 -0
  45. package/src/client/tests/svelte-adapter.test.ts +387 -0
  46. package/src/client/tests/vue-adapter.test.ts +407 -0
  47. package/src/client/types/framework-runtime.d.ts +68 -0
  48. package/src/client/types/vite-hmr.d.ts +46 -0
  49. package/src/client/types/vite-virtual-modules.d.ts +60 -0
  50. package/src/components/Image.tsx +123 -0
  51. package/src/components/IslandErrorBoundary.tsx +145 -0
  52. package/src/components/LayoutDataErrorBoundary.tsx +141 -0
  53. package/src/components/LayoutErrorBoundary.tsx +127 -0
  54. package/src/components/PersistentIsland.tsx +52 -0
  55. package/src/components/StreamingErrorBoundary.tsx +233 -0
  56. package/src/components/StreamingLayout.tsx +538 -0
  57. package/src/components/tests/component-analyzer.test.ts +96 -0
  58. package/src/components/tests/component-detection.test.ts +347 -0
  59. package/src/components/tests/persistent-islands.test.ts +398 -0
  60. package/src/core/components/component-analyzer.ts +192 -0
  61. package/src/core/components/component-detection.ts +508 -0
  62. package/src/core/components/enhanced-framework-detector.ts +500 -0
  63. package/src/core/components/framework-registry.ts +563 -0
  64. package/src/core/components/tests/enhanced-framework-detector.test.ts +577 -0
  65. package/src/core/components/tests/framework-registry.test.ts +465 -0
  66. package/src/core/content/mdx-processor.ts +46 -0
  67. package/src/core/integrations/README.md +282 -0
  68. package/src/core/integrations/index.ts +19 -0
  69. package/src/core/integrations/loader.ts +125 -0
  70. package/src/core/integrations/registry.ts +195 -0
  71. package/src/core/islands/island-persistence.ts +325 -0
  72. package/src/core/islands/island-state-serializer.ts +258 -0
  73. package/src/core/islands/persistent-island-context.tsx +80 -0
  74. package/src/core/islands/use-persistent-state.ts +68 -0
  75. package/src/core/layout/enhanced-layout-resolver.ts +322 -0
  76. package/src/core/layout/layout-cache-manager.ts +485 -0
  77. package/src/core/layout/layout-composer.ts +357 -0
  78. package/src/core/layout/layout-data-loader.ts +516 -0
  79. package/src/core/layout/layout-discovery.ts +243 -0
  80. package/src/core/layout/layout-matcher.ts +299 -0
  81. package/src/core/layout/layout-types.ts +110 -0
  82. package/src/core/layout/tests/enhanced-layout-resolver.test.ts +477 -0
  83. package/src/core/layout/tests/layout-cache-optimization.test.ts +149 -0
  84. package/src/core/layout/tests/layout-composer.test.ts +486 -0
  85. package/src/core/layout/tests/layout-data-loader.test.ts +443 -0
  86. package/src/core/layout/tests/layout-discovery.test.ts +253 -0
  87. package/src/core/layout/tests/layout-matcher.test.ts +480 -0
  88. package/src/core/modules/framework-module-resolver.ts +273 -0
  89. package/src/core/modules/tests/framework-module-resolver.test.ts +263 -0
  90. package/src/core/modules/tests/module-resolution-integration.test.ts +117 -0
  91. package/src/islands/component-analysis.ts +213 -0
  92. package/src/islands/css-utils.ts +565 -0
  93. package/src/islands/discovery/index.ts +80 -0
  94. package/src/islands/discovery/registry.ts +340 -0
  95. package/src/islands/discovery/resolver.ts +477 -0
  96. package/src/islands/discovery/scanner.ts +386 -0
  97. package/src/islands/discovery/tests/island-discovery.test.ts +881 -0
  98. package/src/islands/discovery/types.ts +117 -0
  99. package/src/islands/discovery/validator.ts +544 -0
  100. package/src/islands/discovery/watcher.ts +368 -0
  101. package/src/islands/framework-detection.ts +428 -0
  102. package/src/islands/integration-loader.ts +490 -0
  103. package/src/islands/island.tsx +565 -0
  104. package/src/islands/render-cache.ts +550 -0
  105. package/src/islands/types.ts +80 -0
  106. package/src/islands/universal-css-collector.ts +157 -0
  107. package/src/islands/universal-head-collector.ts +137 -0
  108. package/src/layout-system.d.ts +592 -0
  109. package/src/layout-system.ts +218 -0
  110. package/src/middleware/__tests__/discovery.test.ts +107 -0
  111. package/src/middleware/discovery.ts +268 -0
  112. package/src/middleware/executor.ts +315 -0
  113. package/src/middleware/index.ts +76 -0
  114. package/src/middleware/types.ts +99 -0
  115. package/src/nitro/build-config.ts +576 -0
  116. package/src/nitro/config.ts +483 -0
  117. package/src/nitro/error-handler.ts +636 -0
  118. package/src/nitro/index.ts +173 -0
  119. package/src/nitro/island-manifest.ts +584 -0
  120. package/src/nitro/middleware-adapter.ts +260 -0
  121. package/src/nitro/renderer.ts +1458 -0
  122. package/src/nitro/route-discovery.ts +439 -0
  123. package/src/nitro/types.ts +321 -0
  124. package/src/render/collect-css.ts +198 -0
  125. package/src/render/error-pages.ts +79 -0
  126. package/src/render/isolated-ssr-renderer.ts +654 -0
  127. package/src/render/ssr.ts +1030 -0
  128. package/src/schemas/api.ts +30 -0
  129. package/src/schemas/core.ts +64 -0
  130. package/src/schemas/index.ts +212 -0
  131. package/src/schemas/layout.ts +279 -0
  132. package/src/schemas/routing/index.ts +38 -0
  133. package/src/schemas/routing.ts +376 -0
  134. package/src/types/as-island.ts +20 -0
  135. package/src/types/image.d.ts +106 -0
  136. package/src/types/index.d.ts +22 -0
  137. package/src/types/island-jsx.d.ts +33 -0
  138. package/src/types/island-prop.d.ts +20 -0
  139. package/src/types/layout.ts +285 -0
  140. package/src/types/mdx.d.ts +6 -0
  141. package/src/types/routing.ts +555 -0
  142. package/src/types/tests/layout-types.test.ts +197 -0
  143. package/src/types/types.ts +5 -0
  144. package/src/types/urlpattern.d.ts +49 -0
  145. package/src/types/vite-env.d.ts +11 -0
  146. package/src/utils/dev-logger.ts +299 -0
  147. package/src/utils/fs.ts +151 -0
  148. package/src/vite-plugin/auto-discover.ts +551 -0
  149. package/src/vite-plugin/config.ts +266 -0
  150. package/src/vite-plugin/errors.ts +127 -0
  151. package/src/vite-plugin/image-optimization.ts +151 -0
  152. package/src/vite-plugin/integration-activator.ts +126 -0
  153. package/src/vite-plugin/island-sidecar-plugin.ts +176 -0
  154. package/src/vite-plugin/module-discovery.ts +189 -0
  155. package/src/vite-plugin/nitro-integration.ts +1334 -0
  156. package/src/vite-plugin/plugin.ts +329 -0
  157. package/src/vite-plugin/tests/image-optimization.test.ts +54 -0
  158. package/src/vite-plugin/types.ts +327 -0
  159. package/src/vite-plugin/validation.ts +228 -0
@@ -0,0 +1,584 @@
1
+ /**
2
+ * Island Manifest Generation Module for Nitro
3
+ *
4
+ * This module provides island manifest generation during production builds.
5
+ * The manifest contains information about all discovered islands, their
6
+ * compiled asset paths, and framework metadata for client-side hydration.
7
+ *
8
+ * ## Build Output
9
+ *
10
+ * During production build, this module generates:
11
+ * - `island-manifest.json` in the build output directory
12
+ * - Contains metadata for all islands (src, framework, css, preload)
13
+ * - Includes build timestamp and content hashes for cache busting
14
+ * - Generates preload hints for critical assets
15
+ *
16
+ * ## Nitro Integration
17
+ *
18
+ * The manifest is used by Nitro's renderer to:
19
+ * - Resolve compiled island paths for hydration scripts
20
+ * - Inject CSS assets for islands used on a page
21
+ * - Generate preload tags for critical resources
22
+ *
23
+ * ## Asset Metadata
24
+ *
25
+ * Each island entry includes:
26
+ * - `src`: Compiled JavaScript path (e.g., `/islands/Counter.abc123.js`)
27
+ * - `framework`: Detected framework (react, preact, vue, svelte, solid, lit)
28
+ * - `css`: Associated CSS files
29
+ * - `contentHash`: Hash for cache busting
30
+ * - `preloadDeps`: Dependencies to preload
31
+ *
32
+ * @module nitro/island-manifest
33
+ */
34
+
35
+ import { readFile } from "node:fs/promises";
36
+ import type { Plugin } from "vite";
37
+ import type { ResolvedAvalonConfig } from "../vite-plugin/types.ts";
38
+ import type { IslandManifest, IslandEntry } from "./types.ts";
39
+
40
+ /**
41
+ * Extended island entry with build-time metadata
42
+ */
43
+ export interface BuildIslandEntry extends IslandEntry {
44
+ /** Original source file path */
45
+ sourcePath: string;
46
+ /** Compiled JavaScript chunk name */
47
+ chunkName: string;
48
+ /** Content hash for cache busting */
49
+ contentHash: string;
50
+ /** Dependencies that need to be preloaded */
51
+ preloadDeps: string[];
52
+ /** Whether the island uses streaming */
53
+ usesStreaming: boolean;
54
+ }
55
+
56
+ /**
57
+ * Asset metadata for Nitro's asset manifest format
58
+ * This matches Nitro's expected asset metadata structure
59
+ */
60
+ export interface AssetMetadata {
61
+ /** MIME type of the asset */
62
+ type: string;
63
+ /** ETag for cache validation */
64
+ etag: string;
65
+ /** Last modification time (ISO string) */
66
+ mtime: string;
67
+ /** File size in bytes */
68
+ size: number;
69
+ }
70
+
71
+ /**
72
+ * Build-time island manifest with additional metadata
73
+ */
74
+ export interface BuildIslandManifest extends IslandManifest {
75
+ /** Build timestamp */
76
+ buildTime: number;
77
+ /** Build version/hash */
78
+ buildHash: string;
79
+ /** Avalon version */
80
+ avalonVersion: string;
81
+ /** All CSS assets to inject */
82
+ cssAssets: string[];
83
+ /** Preload hints for critical assets */
84
+ preloadHints: PreloadHint[];
85
+ /** Framework-specific bundles */
86
+ frameworkBundles: Record<string, string>;
87
+ /** Asset metadata for cache headers (Nitro format) */
88
+ assetMetadata?: Record<string, AssetMetadata>;
89
+ }
90
+
91
+ /**
92
+ * Preload hint for resource optimization
93
+ */
94
+ export interface PreloadHint {
95
+ /** Resource URL */
96
+ href: string;
97
+ /** Resource type (script, style, font, etc.) */
98
+ as: "script" | "style" | "font" | "image";
99
+ /** MIME type */
100
+ type?: string;
101
+ /** Cross-origin setting */
102
+ crossorigin?: "anonymous" | "use-credentials";
103
+ }
104
+
105
+ /**
106
+ * Options for island manifest generation
107
+ */
108
+ export interface IslandManifestOptions {
109
+ /** Output path for the manifest file */
110
+ outputPath?: string;
111
+ /** Include source maps in manifest */
112
+ includeSourceMaps?: boolean;
113
+ /** Generate preload hints */
114
+ generatePreloadHints?: boolean;
115
+ /** Verbose logging */
116
+ verbose?: boolean;
117
+ }
118
+
119
+ /**
120
+ * Default manifest generation options
121
+ */
122
+ export const DEFAULT_MANIFEST_OPTIONS: Required<IslandManifestOptions> = {
123
+ outputPath: "dist/island-manifest.json",
124
+ includeSourceMaps: false,
125
+ generatePreloadHints: true,
126
+ verbose: false,
127
+ };
128
+
129
+ /**
130
+ * Framework detection patterns for island files
131
+ */
132
+ const FRAMEWORK_PATTERNS: Record<string, RegExp[]> = {
133
+ react: [
134
+ /from\s+['"]react['"]/,
135
+ /from\s+['"]react-dom['"]/,
136
+ /@jsxImportSource\s+react/,
137
+ ],
138
+ preact: [
139
+ /from\s+['"]preact['"]/,
140
+ /from\s+['"]preact\/hooks['"]/,
141
+ /@jsxImportSource\s+preact/,
142
+ ],
143
+ vue: [
144
+ /from\s+['"]vue['"]/,
145
+ /\.vue$/,
146
+ ],
147
+ svelte: [
148
+ /from\s+['"]svelte['"]/,
149
+ /\.svelte$/,
150
+ ],
151
+ solid: [
152
+ /from\s+['"]solid-js['"]/,
153
+ /\.solid\.(tsx|jsx)$/,
154
+ ],
155
+ lit: [
156
+ /from\s+['"]lit['"]/,
157
+ /from\s+['"]@lit['"]/,
158
+ /\.lit\.(ts|js)$/,
159
+ ],
160
+ };
161
+
162
+ /**
163
+ * Detects the framework used by an island based on file content and extension
164
+ *
165
+ * @param filePath - Path to the island file
166
+ * @param content - File content
167
+ * @returns Detected framework name
168
+ */
169
+ export function detectIslandFramework(
170
+ filePath: string,
171
+ content: string
172
+ ): string {
173
+ // Check file extension first
174
+ if (filePath.endsWith(".vue")) return "vue";
175
+ if (filePath.endsWith(".svelte")) return "svelte";
176
+ if (filePath.includes(".solid.")) return "solid";
177
+ if (filePath.includes(".lit.")) return "lit";
178
+
179
+ // Check content patterns
180
+ for (const [framework, patterns] of Object.entries(FRAMEWORK_PATTERNS)) {
181
+ for (const pattern of patterns) {
182
+ if (pattern.test(content) || pattern.test(filePath)) {
183
+ return framework;
184
+ }
185
+ }
186
+ }
187
+
188
+ // Default to preact for JSX/TSX files
189
+ if (filePath.endsWith(".tsx") || filePath.endsWith(".jsx")) {
190
+ return "preact";
191
+ }
192
+
193
+ return "unknown";
194
+ }
195
+
196
+ /**
197
+ * Generates a content hash for cache busting
198
+ *
199
+ * @param content - Content to hash
200
+ * @returns Short hash string
201
+ */
202
+ export async function generateContentHash(content: string): Promise<string> {
203
+ const encoder = new TextEncoder();
204
+ const data = encoder.encode(content);
205
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
206
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
207
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
208
+ return hashHex.slice(0, 8);
209
+ }
210
+
211
+ /**
212
+ * Extracts dependencies from island file content
213
+ *
214
+ * @param content - File content
215
+ * @returns Array of dependency names
216
+ */
217
+ export function extractIslandDependencies(content: string): string[] {
218
+ const deps: string[] = [];
219
+ const importRegex = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
220
+ const dynamicImportRegex = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
221
+
222
+ let match;
223
+
224
+ // Static imports
225
+ while ((match = importRegex.exec(content)) !== null) {
226
+ const importPath = match[1];
227
+ if (!importPath.startsWith(".") && !importPath.startsWith("/")) {
228
+ deps.push(importPath);
229
+ }
230
+ }
231
+
232
+ // Dynamic imports
233
+ while ((match = dynamicImportRegex.exec(content)) !== null) {
234
+ const importPath = match[1];
235
+ if (!importPath.startsWith(".") && !importPath.startsWith("/")) {
236
+ deps.push(importPath);
237
+ }
238
+ }
239
+
240
+ return [...new Set(deps)];
241
+ }
242
+
243
+ /**
244
+ * Creates an island entry from discovered island information
245
+ *
246
+ * @param name - Island name
247
+ * @param sourcePath - Source file path
248
+ * @param content - File content
249
+ * @param compiledPath - Compiled asset path
250
+ * @returns Island entry
251
+ */
252
+ export async function createIslandEntry(
253
+ name: string,
254
+ sourcePath: string,
255
+ content: string,
256
+ compiledPath: string
257
+ ): Promise<BuildIslandEntry> {
258
+ const framework = detectIslandFramework(sourcePath, content);
259
+ const contentHash = await generateContentHash(content);
260
+ const deps = extractIslandDependencies(content);
261
+
262
+ return {
263
+ src: compiledPath,
264
+ framework,
265
+ css: [], // Will be populated during build
266
+ preload: deps.filter((d) => !d.includes("/")), // Only top-level packages
267
+ sourcePath,
268
+ chunkName: name,
269
+ contentHash,
270
+ preloadDeps: [],
271
+ usesStreaming: false,
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Generates preload hints for critical assets
277
+ *
278
+ * @param manifest - Island manifest
279
+ * @returns Array of preload hints
280
+ */
281
+ export function generatePreloadHints(
282
+ manifest: Partial<BuildIslandManifest>
283
+ ): PreloadHint[] {
284
+ const hints: PreloadHint[] = [];
285
+
286
+ // Add client entry script
287
+ if (manifest.clientEntry) {
288
+ hints.push({
289
+ href: manifest.clientEntry,
290
+ as: "script",
291
+ type: "text/javascript",
292
+ });
293
+ }
294
+
295
+ // Add CSS assets
296
+ for (const css of manifest.css ?? []) {
297
+ hints.push({
298
+ href: css,
299
+ as: "style",
300
+ type: "text/css",
301
+ });
302
+ }
303
+
304
+ // Add framework bundles
305
+ for (const bundle of Object.values(manifest.frameworkBundles ?? {})) {
306
+ hints.push({
307
+ href: bundle,
308
+ as: "script",
309
+ type: "text/javascript",
310
+ });
311
+ }
312
+
313
+ return hints;
314
+ }
315
+
316
+ /**
317
+ * Creates a Vite plugin for island manifest generation
318
+ *
319
+ * @param avalonConfig - Resolved Avalon configuration
320
+ * @param options - Manifest generation options
321
+ * @returns Vite plugin
322
+ */
323
+ export function createIslandManifestPlugin(
324
+ avalonConfig: ResolvedAvalonConfig,
325
+ options: IslandManifestOptions = {}
326
+ ): Plugin {
327
+ const opts = { ...DEFAULT_MANIFEST_OPTIONS, ...options };
328
+ const islands: Map<string, BuildIslandEntry> = new Map();
329
+ const cssAssets: Set<string> = new Set();
330
+ let clientEntry = "";
331
+
332
+ return {
333
+ name: "avalon:island-manifest",
334
+ enforce: "post",
335
+
336
+ // Track island chunks during build
337
+ generateBundle(_outputOptions, bundle) {
338
+ for (const [fileName, chunk] of Object.entries(bundle)) {
339
+ // Track CSS assets
340
+ if (fileName.endsWith(".css")) {
341
+ cssAssets.add(`/${fileName}`);
342
+ }
343
+
344
+ // Track client entry
345
+ if (
346
+ chunk.type === "chunk" &&
347
+ (chunk.name === "client" || chunk.name === "main")
348
+ ) {
349
+ clientEntry = `/${fileName}`;
350
+ }
351
+
352
+ // Track island chunks
353
+ if (
354
+ chunk.type === "chunk" &&
355
+ (chunk.facadeModuleId?.includes("/islands/") ||
356
+ chunk.name?.startsWith("islands/"))
357
+ ) {
358
+ const name = chunk.name?.replace("islands/", "") ?? fileName;
359
+ const existingEntry = islands.get(name);
360
+
361
+ if (existingEntry) {
362
+ // Update with compiled path
363
+ existingEntry.src = `/${fileName}`;
364
+ } else {
365
+ // Create new entry
366
+ islands.set(name, {
367
+ src: `/${fileName}`,
368
+ framework: detectFrameworkFromChunk(chunk),
369
+ css: [],
370
+ preload: [],
371
+ sourcePath: chunk.facadeModuleId ?? "",
372
+ chunkName: name,
373
+ contentHash: /\.([a-f0-9]+)\.js$/.exec(fileName)?.[1] ?? "",
374
+ preloadDeps: chunk.imports ?? [],
375
+ usesStreaming: false,
376
+ });
377
+ }
378
+ }
379
+ }
380
+ },
381
+
382
+ // Write manifest after build
383
+ async writeBundle(_options: unknown, bundle: Record<string, { type: string; source?: string | Uint8Array; code?: string }>) {
384
+ // Generate asset metadata for Nitro's format (type, etag, mtime, size)
385
+ const assetMetadata: Record<string, AssetMetadata> = {};
386
+ const buildTime = new Date().toISOString();
387
+
388
+ for (const [fileName, chunk] of Object.entries(bundle)) {
389
+ const entry = await buildAssetMetadataEntry(fileName, chunk, buildTime);
390
+ if (entry) assetMetadata[`/${fileName}`] = entry;
391
+ }
392
+
393
+ const manifest: BuildIslandManifest = {
394
+ islands: Object.fromEntries(islands),
395
+ clientEntry,
396
+ css: Array.from(cssAssets),
397
+ buildTime: Date.now(),
398
+ buildHash: await generateContentHash(JSON.stringify(Object.fromEntries(islands))),
399
+ avalonVersion: "1.0.0",
400
+ cssAssets: Array.from(cssAssets),
401
+ preloadHints: opts.generatePreloadHints
402
+ ? generatePreloadHints({
403
+ clientEntry,
404
+ css: Array.from(cssAssets),
405
+ islands: Object.fromEntries(islands),
406
+ })
407
+ : [],
408
+ frameworkBundles: {},
409
+ assetMetadata,
410
+ };
411
+
412
+ // Write manifest file
413
+ const manifestJson = JSON.stringify(manifest, null, 2);
414
+
415
+ if (opts.verbose) {
416
+ console.log("📋 Island manifest generated:");
417
+ console.log(` Islands: ${islands.size}`);
418
+ console.log(` CSS assets: ${cssAssets.size}`);
419
+ console.log(` Client entry: ${clientEntry}`);
420
+ console.log(` Asset metadata entries: ${Object.keys(assetMetadata).length}`);
421
+ }
422
+
423
+ // Emit the manifest as an asset
424
+ this.emitFile({
425
+ type: "asset",
426
+ fileName: "island-manifest.json",
427
+ source: manifestJson,
428
+ });
429
+ },
430
+ };
431
+ }
432
+
433
+ /**
434
+ * Generates asset metadata entry for a single bundle chunk
435
+ */
436
+ async function buildAssetMetadataEntry(
437
+ fileName: string,
438
+ chunk: { type: string; source?: string | Uint8Array; code?: string },
439
+ buildTime: string
440
+ ): Promise<AssetMetadata | null> {
441
+ let content: string | Uint8Array | undefined;
442
+ if (chunk.type === "asset" && chunk.source) {
443
+ content = chunk.source;
444
+ } else if (chunk.type === "chunk" && chunk.code) {
445
+ content = chunk.code;
446
+ }
447
+ if (!content) return null;
448
+
449
+ const size = typeof content === "string"
450
+ ? new TextEncoder().encode(content).length
451
+ : content.length;
452
+ const contentStr = typeof content === "string"
453
+ ? content
454
+ : new TextDecoder().decode(content);
455
+ const etag = await generateContentHash(contentStr);
456
+
457
+ const ext = fileName.substring(fileName.lastIndexOf("."));
458
+ const mimeTypes: Record<string, string> = {
459
+ ".js": "application/javascript",
460
+ ".mjs": "application/javascript",
461
+ ".css": "text/css",
462
+ ".json": "application/json",
463
+ ".html": "text/html",
464
+ ".map": "application/json",
465
+ };
466
+
467
+ return {
468
+ type: mimeTypes[ext] ?? "application/octet-stream",
469
+ etag: `"${etag}"`,
470
+ mtime: buildTime,
471
+ size,
472
+ };
473
+ }
474
+
475
+ /**
476
+ * @param chunk - Rollup output chunk
477
+ * @returns Detected framework name
478
+ */
479
+ function detectFrameworkFromChunk(chunk: {
480
+ facadeModuleId?: string | null;
481
+ code?: string;
482
+ }): string {
483
+ const filePath = chunk.facadeModuleId ?? "";
484
+ const content = chunk.code ?? "";
485
+
486
+ return detectIslandFramework(filePath, content);
487
+ }
488
+
489
+ /**
490
+ * Loads an existing island manifest from disk
491
+ *
492
+ * @param manifestPath - Path to the manifest file
493
+ * @returns Loaded manifest or null if not found
494
+ */
495
+ export async function loadIslandManifest(
496
+ manifestPath: string = "dist/island-manifest.json"
497
+ ): Promise<BuildIslandManifest | null> {
498
+ try {
499
+ const content = await readFile(manifestPath, "utf-8");
500
+ return JSON.parse(content) as BuildIslandManifest;
501
+ } catch {
502
+ return null;
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Gets the compiled asset path for an island
508
+ *
509
+ * @param islandName - Name of the island
510
+ * @param manifest - Island manifest
511
+ * @returns Compiled asset path or null if not found
512
+ */
513
+ export function getIslandAssetPath(
514
+ islandName: string,
515
+ manifest: BuildIslandManifest | null
516
+ ): string | null {
517
+ if (!manifest) return null;
518
+
519
+ const entry = manifest.islands[islandName];
520
+ return entry?.src ?? null;
521
+ }
522
+
523
+ /**
524
+ * Gets all CSS assets that should be injected for a page
525
+ *
526
+ * @param islandNames - Names of islands used on the page
527
+ * @param manifest - Island manifest
528
+ * @returns Array of CSS asset paths
529
+ */
530
+ export function getPageCssAssets(
531
+ islandNames: string[],
532
+ manifest: BuildIslandManifest | null
533
+ ): string[] {
534
+ if (!manifest) return [];
535
+
536
+ const cssAssets = new Set<string>();
537
+
538
+ // Add global CSS
539
+ for (const css of manifest.css) {
540
+ cssAssets.add(css);
541
+ }
542
+
543
+ // Add island-specific CSS
544
+ for (const name of islandNames) {
545
+ const entry = manifest.islands[name];
546
+ if (entry?.css) {
547
+ for (const css of entry.css) {
548
+ cssAssets.add(css);
549
+ }
550
+ }
551
+ }
552
+
553
+ return Array.from(cssAssets);
554
+ }
555
+
556
+ /**
557
+ * Generates HTML preload tags for critical assets
558
+ *
559
+ * @param manifest - Island manifest
560
+ * @returns HTML string with preload tags
561
+ */
562
+ export function generatePreloadTags(manifest: BuildIslandManifest): string {
563
+ const tags: string[] = [];
564
+
565
+ for (const hint of manifest.preloadHints) {
566
+ const attrs = [
567
+ `rel="preload"`,
568
+ `href="${hint.href}"`,
569
+ `as="${hint.as}"`,
570
+ ];
571
+
572
+ if (hint.type) {
573
+ attrs.push(`type="${hint.type}"`);
574
+ }
575
+
576
+ if (hint.crossorigin) {
577
+ attrs.push(`crossorigin="${hint.crossorigin}"`);
578
+ }
579
+
580
+ tags.push(`<link ${attrs.join(" ")}>`);
581
+ }
582
+
583
+ return tags.join("\n");
584
+ }