@useavalon/avalon 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +54 -54
  2. package/mod.ts +302 -302
  3. package/package.json +49 -26
  4. package/src/build/integration-bundler-plugin.ts +116 -116
  5. package/src/build/integration-config.ts +168 -168
  6. package/src/build/integration-detection-plugin.ts +117 -117
  7. package/src/build/integration-resolver-plugin.ts +90 -90
  8. package/src/build/island-manifest.ts +269 -269
  9. package/src/build/island-types-generator.ts +476 -476
  10. package/src/build/mdx-island-transform.ts +464 -464
  11. package/src/build/mdx-plugin.ts +98 -98
  12. package/src/build/page-island-transform.ts +598 -598
  13. package/src/build/prop-extractors/index.ts +21 -21
  14. package/src/build/prop-extractors/lit.ts +140 -140
  15. package/src/build/prop-extractors/qwik.ts +16 -16
  16. package/src/build/prop-extractors/solid.ts +125 -125
  17. package/src/build/prop-extractors/svelte.ts +194 -194
  18. package/src/build/prop-extractors/vue.ts +111 -111
  19. package/src/build/sidecar-file-manager.ts +104 -104
  20. package/src/build/sidecar-renderer.ts +30 -30
  21. package/src/client/adapters/index.ts +21 -13
  22. package/src/client/components.ts +35 -35
  23. package/src/client/css-hmr-handler.ts +344 -344
  24. package/src/client/framework-adapter.ts +462 -462
  25. package/src/client/hmr-coordinator.ts +396 -396
  26. package/src/client/hmr-error-overlay.js +533 -533
  27. package/src/client/main.js +824 -816
  28. package/src/client/types/framework-runtime.d.ts +68 -68
  29. package/src/client/types/vite-hmr.d.ts +46 -46
  30. package/src/client/types/vite-virtual-modules.d.ts +70 -60
  31. package/src/components/Image.tsx +123 -123
  32. package/src/components/IslandErrorBoundary.tsx +145 -145
  33. package/src/components/LayoutDataErrorBoundary.tsx +141 -141
  34. package/src/components/LayoutErrorBoundary.tsx +127 -127
  35. package/src/components/PersistentIsland.tsx +52 -52
  36. package/src/components/StreamingErrorBoundary.tsx +233 -233
  37. package/src/components/StreamingLayout.tsx +538 -538
  38. package/src/core/components/component-analyzer.ts +192 -192
  39. package/src/core/components/component-detection.ts +508 -508
  40. package/src/core/components/enhanced-framework-detector.ts +500 -500
  41. package/src/core/components/framework-registry.ts +563 -563
  42. package/src/core/content/mdx-processor.ts +46 -46
  43. package/src/core/integrations/index.ts +19 -19
  44. package/src/core/integrations/loader.ts +125 -125
  45. package/src/core/integrations/registry.ts +175 -175
  46. package/src/core/islands/island-persistence.ts +325 -325
  47. package/src/core/islands/island-state-serializer.ts +258 -258
  48. package/src/core/islands/persistent-island-context.tsx +80 -80
  49. package/src/core/islands/use-persistent-state.ts +68 -68
  50. package/src/core/layout/enhanced-layout-resolver.ts +322 -322
  51. package/src/core/layout/layout-cache-manager.ts +485 -485
  52. package/src/core/layout/layout-composer.ts +357 -357
  53. package/src/core/layout/layout-data-loader.ts +516 -516
  54. package/src/core/layout/layout-discovery.ts +243 -243
  55. package/src/core/layout/layout-matcher.ts +299 -299
  56. package/src/core/layout/layout-types.ts +110 -110
  57. package/src/core/modules/framework-module-resolver.ts +273 -273
  58. package/src/islands/component-analysis.ts +213 -213
  59. package/src/islands/css-utils.ts +565 -565
  60. package/src/islands/discovery/index.ts +80 -80
  61. package/src/islands/discovery/registry.ts +340 -340
  62. package/src/islands/discovery/resolver.ts +477 -477
  63. package/src/islands/discovery/scanner.ts +386 -386
  64. package/src/islands/discovery/types.ts +117 -117
  65. package/src/islands/discovery/validator.ts +544 -544
  66. package/src/islands/discovery/watcher.ts +368 -368
  67. package/src/islands/framework-detection.ts +428 -428
  68. package/src/islands/integration-loader.ts +490 -490
  69. package/src/islands/island.tsx +565 -565
  70. package/src/islands/render-cache.ts +550 -550
  71. package/src/islands/types.ts +80 -80
  72. package/src/islands/universal-css-collector.ts +157 -157
  73. package/src/islands/universal-head-collector.ts +137 -137
  74. package/src/layout-system.d.ts +592 -592
  75. package/src/layout-system.ts +218 -218
  76. package/src/middleware/discovery.ts +268 -268
  77. package/src/middleware/executor.ts +315 -315
  78. package/src/middleware/index.ts +76 -76
  79. package/src/middleware/types.ts +99 -99
  80. package/src/nitro/build-config.ts +575 -575
  81. package/src/nitro/config.ts +483 -483
  82. package/src/nitro/error-handler.ts +636 -636
  83. package/src/nitro/index.ts +173 -173
  84. package/src/nitro/island-manifest.ts +584 -584
  85. package/src/nitro/middleware-adapter.ts +260 -260
  86. package/src/nitro/renderer.ts +1471 -1471
  87. package/src/nitro/route-discovery.ts +439 -439
  88. package/src/nitro/types.ts +321 -321
  89. package/src/render/collect-css.ts +198 -198
  90. package/src/render/error-pages.ts +79 -79
  91. package/src/render/isolated-ssr-renderer.ts +654 -654
  92. package/src/render/ssr.ts +1030 -1030
  93. package/src/schemas/api.ts +30 -30
  94. package/src/schemas/core.ts +64 -64
  95. package/src/schemas/index.ts +212 -212
  96. package/src/schemas/layout.ts +279 -279
  97. package/src/schemas/routing/index.ts +38 -38
  98. package/src/schemas/routing.ts +376 -376
  99. package/src/types/as-island.ts +20 -20
  100. package/src/types/image.d.ts +106 -106
  101. package/src/types/index.d.ts +22 -22
  102. package/src/types/island-jsx.d.ts +33 -33
  103. package/src/types/island-prop.d.ts +20 -20
  104. package/src/types/layout.ts +285 -285
  105. package/src/types/mdx.d.ts +6 -6
  106. package/src/types/routing.ts +555 -555
  107. package/src/types/types.ts +5 -5
  108. package/src/types/urlpattern.d.ts +49 -49
  109. package/src/types/vite-env.d.ts +11 -11
  110. package/src/utils/dev-logger.ts +299 -299
  111. package/src/utils/fs.ts +151 -151
  112. package/src/vite-plugin/auto-discover.ts +551 -551
  113. package/src/vite-plugin/config.ts +266 -266
  114. package/src/vite-plugin/errors.ts +127 -127
  115. package/src/vite-plugin/image-optimization.ts +156 -156
  116. package/src/vite-plugin/integration-activator.ts +126 -126
  117. package/src/vite-plugin/island-sidecar-plugin.ts +176 -176
  118. package/src/vite-plugin/module-discovery.ts +189 -189
  119. package/src/vite-plugin/nitro-integration.ts +1354 -1354
  120. package/src/vite-plugin/plugin.ts +403 -409
  121. package/src/vite-plugin/types.ts +327 -327
  122. package/src/vite-plugin/validation.ts +228 -228
  123. package/src/client/adapters/index.js +0 -12
  124. package/src/client/adapters/lit-adapter.js +0 -467
  125. package/src/client/adapters/lit-adapter.ts +0 -654
  126. package/src/client/adapters/preact-adapter.js +0 -223
  127. package/src/client/adapters/preact-adapter.ts +0 -331
  128. package/src/client/adapters/qwik-adapter.js +0 -259
  129. package/src/client/adapters/qwik-adapter.ts +0 -345
  130. package/src/client/adapters/react-adapter.js +0 -220
  131. package/src/client/adapters/react-adapter.ts +0 -353
  132. package/src/client/adapters/solid-adapter.js +0 -295
  133. package/src/client/adapters/solid-adapter.ts +0 -451
  134. package/src/client/adapters/svelte-adapter.js +0 -368
  135. package/src/client/adapters/svelte-adapter.ts +0 -524
  136. package/src/client/adapters/vue-adapter.js +0 -278
  137. package/src/client/adapters/vue-adapter.ts +0 -467
  138. package/src/client/components.js +0 -23
  139. package/src/client/css-hmr-handler.js +0 -263
  140. package/src/client/framework-adapter.js +0 -283
  141. package/src/client/hmr-coordinator.js +0 -274
@@ -1,409 +1,403 @@
1
- /**
2
- * Avalon Vite Plugin
3
- *
4
- * This module provides the main `avalon()` function that creates a unified Vite plugin
5
- * for the Avalon framework. It handles configuration resolution, integration activation,
6
- * Nitro server integration, and wires up all the necessary Vite hooks.
7
- *
8
- * ISLAND DETECTION:
9
- * Islands are detected by usage - any component used with an `island` prop in pages
10
- * or layouts is automatically treated as an island. No fixed islands directory required.
11
- */
12
-
13
- import type { Plugin, PluginOption, ResolvedConfig, ViteDevServer } from "vite";
14
- import type {
15
- AvalonPluginConfig,
16
- IntegrationName,
17
- ResolvedAvalonConfig,
18
- } from "./types.ts";
19
- import { createRequire } from "node:module";
20
- import { dirname, join } from "node:path";
21
- import { resolveConfig, checkDirectoriesExist } from "./config.ts";
22
- import { activateIntegrations, activateSingleIntegration } from "./integration-activator.ts";
23
- import { discoverIntegrationsFromIslandUsage } from "./auto-discover.ts";
24
- import { validateActiveIntegrations, formatValidationResults } from "./validation.ts";
25
- import { createMDXPlugin } from "../build/mdx-plugin.ts";
26
- import { mdxIslandTransform } from "../build/mdx-island-transform.ts";
27
- import { pageIslandTransform } from "../build/page-island-transform.ts";
28
- import { registry } from "../core/integrations/registry.ts";
29
- import { createNitroIntegration } from "./nitro-integration.ts";
30
- import { islandSidecarPlugin } from "./island-sidecar-plugin.ts";
31
- import { createImagePlugin } from "./image-optimization.ts";
32
- import type { NitroConfigOutput } from "../nitro/config.ts";
33
- declare global {
34
- var __avalonConfig: ResolvedAvalonConfig | undefined;
35
- var __viteDevServer: ViteDevServer | undefined;
36
- var __nitroConfig: NitroConfigOutput | undefined;
37
- }
38
-
39
- /**
40
- * Collects Vite plugins from all activated integrations.
41
- *
42
- * This function iterates through the activated integrations and calls their
43
- * vitePlugin() method if implemented. The returned plugins are collected and
44
- * flattened into a single array.
45
- *
46
- * Plugin ordering is handled to ensure correct application:
47
- * - Lit plugins come first (DOM shim requirement)
48
- * - Other framework plugins follow
49
- *
50
- * @param activeIntegrations - Set of activated integration names
51
- * @param verbose - Whether to log detailed information
52
- * @returns Promise resolving to an array of Vite plugins from integrations
53
- */
54
- export async function collectIntegrationPlugins(
55
- activeIntegrations: Set<IntegrationName>,
56
- verbose: boolean = false
57
- ): Promise<Plugin[]> {
58
- const plugins: Plugin[] = [];
59
- const litPlugins: Plugin[] = [];
60
-
61
- for (const name of activeIntegrations) {
62
- const validPlugins = await loadPluginsForIntegration(name, verbose);
63
- if (name === "lit") {
64
- litPlugins.push(...validPlugins);
65
- } else {
66
- plugins.push(...validPlugins);
67
- }
68
- }
69
-
70
- return [...litPlugins, ...plugins];
71
- }
72
-
73
- async function loadPluginsForIntegration(name: IntegrationName, _verbose: boolean): Promise<Plugin[]> {
74
- const integration = registry.get(name);
75
- if (!integration) return [];
76
- if (typeof integration.vitePlugin !== "function") return [];
77
-
78
- try {
79
- const result = await integration.vitePlugin();
80
- const pluginArray = Array.isArray(result) ? result : [result];
81
- return pluginArray.filter((p): p is Plugin => p != null);
82
- } catch (error) {
83
- console.warn(`[avalon] Failed to load vite plugin for ${name}:`, error instanceof Error ? error.message : error);
84
- return [];
85
- }
86
- }
87
-
88
- /**
89
- * Discovers which integrations are actually needed by scanning pages/layouts for island prop usage.
90
- * This enables lazy loading - only load Vite plugins for frameworks that are actually used.
91
- *
92
- * @param config - The resolved Avalon configuration
93
- * @param projectRoot - The project root directory (defaults to cwd)
94
- * @returns Set of integration names that are actually needed
95
- */
96
- async function discoverNeededIntegrations(
97
- config: ResolvedAvalonConfig,
98
- projectRoot?: string
99
- ): Promise<Set<IntegrationName>> {
100
- const needed = new Set<IntegrationName>();
101
-
102
- try {
103
- // Scan pages, layouts, and modules for components used with island prop
104
- const discovered = await discoverIntegrationsFromIslandUsage(
105
- config.pagesDir,
106
- config.layoutsDir,
107
- projectRoot,
108
- config.modules?.dir
109
- );
110
-
111
- // Only include integrations that are both discovered AND configured
112
- for (const integration of discovered) {
113
- if (config.integrations.includes(integration)) {
114
- needed.add(integration);
115
- }
116
- }
117
- } catch {
118
- // If discovery fails, fall back to all configured integrations
119
- for (const integration of config.integrations) {
120
- needed.add(integration);
121
- }
122
- }
123
-
124
- return needed;
125
- }
126
-
127
- async function resolveIntegrationsToLoad(
128
- preResolvedConfig: ResolvedAvalonConfig
129
- ): Promise<IntegrationName[]> {
130
- if (!preResolvedConfig.lazyIntegrations || preResolvedConfig.integrations.length === 0) {
131
- return [...preResolvedConfig.integrations];
132
- }
133
-
134
- const needed = await discoverNeededIntegrations(preResolvedConfig);
135
- if (needed.size === 0) {
136
- return [...preResolvedConfig.integrations];
137
- }
138
-
139
- return Array.from(needed);
140
- }
141
-
142
- async function setupMDXPlugins(preResolvedConfig: ResolvedAvalonConfig): Promise<Plugin[]> {
143
- try {
144
- const mdxPlugins = await createMDXPlugin({
145
- jsxImportSource: preResolvedConfig.mdx.jsxImportSource,
146
- syntaxHighlighting: preResolvedConfig.mdx.syntaxHighlighting,
147
- remarkPlugins: preResolvedConfig.mdx.remarkPlugins as import("unified").Pluggable[],
148
- rehypePlugins: preResolvedConfig.mdx.rehypePlugins as import("unified").Pluggable[],
149
- development: true,
150
- });
151
- mdxPlugins.push(mdxIslandTransform({ verbose: preResolvedConfig.verbose }));
152
- return mdxPlugins;
153
- } catch (error) {
154
- if (preResolvedConfig.showWarnings) {
155
- console.warn("⚠️ Could not configure MDX plugin:", error);
156
- }
157
- return [];
158
- }
159
- }
160
-
161
- function setupNitroPlugins(
162
- preResolvedConfig: ResolvedAvalonConfig,
163
- nitroConfig: NonNullable<AvalonPluginConfig["nitro"]>,
164
- _verbose?: boolean
165
- ): { plugins: Plugin[]; options: NitroConfigOutput } {
166
- const { plugins, nitroOptions } = createNitroIntegration(preResolvedConfig, nitroConfig);
167
- globalThis.__nitroConfig = nitroOptions;
168
- return { plugins, options: nitroOptions };
169
- }
170
-
171
- async function runAutoDiscovery(
172
- resolvedConfig: ResolvedAvalonConfig,
173
- viteRoot: string,
174
- activeIntegrations: Set<IntegrationName>
175
- ): Promise<void> {
176
- if (!resolvedConfig.autoDiscoverIntegrations) return;
177
-
178
- try {
179
- const discovered = await discoverIntegrationsFromIslandUsage(
180
- resolvedConfig.pagesDir,
181
- resolvedConfig.layoutsDir,
182
- viteRoot,
183
- resolvedConfig.modules?.dir
184
- );
185
- for (const name of discovered) {
186
- if (activeIntegrations.has(name)) continue;
187
- try {
188
- await activateSingleIntegration(name, activeIntegrations, resolvedConfig.verbose);
189
- } catch (error) {
190
- if (resolvedConfig.showWarnings) console.warn(` ⚠️ Could not auto-load integration: ${name}`, error);
191
- }
192
- }
193
- } catch (error) {
194
- if (resolvedConfig.showWarnings) console.warn(" ⚠️ Auto-discovery failed:", error);
195
- }
196
- }
197
-
198
- function runValidation(
199
- resolvedConfig: ResolvedAvalonConfig,
200
- activeIntegrations: Set<IntegrationName>
201
- ): void {
202
- if (!resolvedConfig.validateIntegrations || activeIntegrations.size === 0) return;
203
-
204
- const validationSummary = validateActiveIntegrations(activeIntegrations, resolvedConfig.showWarnings);
205
- if (!validationSummary.allValid) {
206
- console.error(formatValidationResults(validationSummary));
207
- if (resolvedConfig.showWarnings) console.warn(" ⚠️ Some integrations have validation issues.");
208
- }
209
- }
210
-
211
- /**
212
- * Creates the Avalon Vite plugin array
213
- *
214
- * @param config - Avalon configuration options
215
- * @returns A promise that resolves to an array of Vite plugins that handle all Avalon functionality.
216
- * Returns PluginOption[] to avoid TypeScript's excessive stack depth issues
217
- * when comparing Plugin<any> arrays in Vite 8's complex type system.
218
- */
219
- export async function avalon(config?: AvalonPluginConfig): Promise<PluginOption[]> {
220
- // Resolved configuration with defaults applied
221
- let resolvedConfig: ResolvedAvalonConfig;
222
-
223
- // Reference to Vite's resolved config
224
- let viteConfig: ResolvedConfig;
225
-
226
- // Track which integrations are activated
227
- const activeIntegrations = new Set<IntegrationName>();
228
-
229
- // Pre-resolve config to get MDX settings and integration list
230
- // We use isDev=true as a default; the actual value will be set in configResolved
231
- const preResolvedConfig = resolveConfig(config, true);
232
-
233
- const integrationsToLoad = await resolveIntegrationsToLoad(preResolvedConfig);
234
-
235
- if (integrationsToLoad.length > 0) {
236
- await activateIntegrations({ ...preResolvedConfig, integrations: integrationsToLoad }, activeIntegrations);
237
- }
238
- const mdxPlugins = await setupMDXPlugins(preResolvedConfig);
239
-
240
- // Image optimization plugins (vite-imagetools wrapper)
241
- const imagePlugins = await createImagePlugin(preResolvedConfig.image, preResolvedConfig.verbose);
242
-
243
- let integrationPlugins: Plugin[] = [];
244
- if (activeIntegrations.size > 0) {
245
- integrationPlugins = await collectIntegrationPlugins(activeIntegrations, preResolvedConfig.verbose);
246
- }
247
-
248
- let nitroPlugins: Plugin[] = [];
249
- if (config?.nitro) {
250
- const { plugins } = setupNitroPlugins(preResolvedConfig, config.nitro, preResolvedConfig.verbose);
251
- nitroPlugins = plugins;
252
- }
253
-
254
- // Sidecar plugin for Vue/Svelte/Solid type declarations
255
- const sidecarPlugin = islandSidecarPlugin({
256
- verbose: preResolvedConfig.verbose,
257
- });
258
-
259
- // Pre-resolve paths for standalone projects.
260
- // In the monorepo www/ project these are handled by manual resolve.alias.
261
- const require = createRequire(import.meta.url);
262
-
263
- let clientMainResolved: string | null = null;
264
- try {
265
- const clientEntry = require.resolve("@useavalon/avalon/client");
266
- clientMainResolved = join(dirname(clientEntry), "main.js");
267
- } catch {
268
- // Monorepo — www/ sets its own alias
269
- }
270
-
271
- // Resolve /@useavalon/*/client virtual imports used by main.js
272
- // These are resolved dynamically in the resolveId hook using Vite's resolver
273
- const integrationClientIds = new Set(
274
- ['preact', 'react', 'vue', 'svelte', 'solid', 'lit', 'qwik'].map(
275
- name => `/@useavalon/${name}/client`
276
- )
277
- );
278
-
279
- // The main Avalon plugin
280
- const avalonPlugin: Plugin = {
281
- name: "avalon",
282
- enforce: "pre",
283
-
284
- config() {
285
- // @useavalon packages ship raw .ts source for SSR and pre-compiled
286
- // .js for client-side code.
287
- //
288
- // oxc.exclude: Prevents Vite's built-in OXC from processing @useavalon
289
- // .ts files. Without this, OXC applies integration plugins' global
290
- // jsx: 'automatic' config to plain .ts files, causing errors.
291
- // Our transform hook below handles TS stripping for SSR instead.
292
- // Client-side loads .js files which OXC skips by default (/\.js$/).
293
- //
294
- // ssr.noExternal: Ensures Vite processes @useavalon packages through
295
- // the SSR transform pipeline instead of treating them as external CJS.
296
- return {
297
- oxc: {
298
- exclude: [/node_modules\/@useavalon\/.*\.tsx?$/],
299
- },
300
- ssr: {
301
- noExternal: [/^@useavalon\//],
302
- },
303
- };
304
- },
305
-
306
- configResolved(resolvedViteConfig: ResolvedConfig) {
307
- viteConfig = resolvedViteConfig;
308
- const isDev = resolvedViteConfig.command === "serve";
309
- resolvedConfig = resolveConfig(config, isDev);
310
-
311
- globalThis.__avalonConfig = resolvedConfig;
312
-
313
- checkDirectoriesExist(resolvedConfig, resolvedViteConfig.root);
314
- },
315
-
316
- async resolveId(id: string) {
317
- if (id === "/src/client/main.js" && clientMainResolved) {
318
- return clientMainResolved;
319
- }
320
- // /@useavalon/*/client resolve through Vite's pipeline so it finds
321
- // workspace-linked or npm-installed integration packages from the
322
- // consuming project's node_modules, not from avalon's own context.
323
- if (integrationClientIds.has(id)) {
324
- const packageId = id.slice(1); // strip leading /
325
- const resolved = await this.resolve(packageId);
326
- return resolved?.id ?? null;
327
- }
328
- return null;
329
- },
330
-
331
- async transform(code: string, id: string) {
332
- // For SSR: strip TypeScript from @useavalon packages ourselves.
333
- // Integration plugins (react, preact) set jsx: 'automatic' which Vite's
334
- // OXC applies to all files — causing "Invalid jsx option" errors on
335
- // plain .ts files during SSR. We intercept and strip TS without JSX config.
336
- // For client-side: main.js imports pre-compiled .js files that OXC
337
- // skips entirely (default exclude: /\.js$/), avoiding the jsx conflict.
338
- if (
339
- this.environment?.config?.consumer === 'server' &&
340
- id.includes('@useavalon/') &&
341
- /\.tsx?$/.test(id)
342
- ) {
343
- const { transform: oxcTransform } = await import('oxc-transform');
344
- const result = await oxcTransform(id, code, {
345
- sourcemap: true,
346
- typescript: { onlyRemoveTypeImports: false },
347
- });
348
- return { code: result.code, map: result.map, moduleType: 'js' };
349
- }
350
- },
351
-
352
- async buildStart() {
353
- await runAutoDiscovery(resolvedConfig, viteConfig?.root, activeIntegrations);
354
- runValidation(resolvedConfig, activeIntegrations);
355
- },
356
-
357
- configureServer(server: ViteDevServer) {
358
-
359
- (globalThis as any).__viteDevServer = server;
360
- },
361
- };
362
-
363
- // Extract Lit plugins for proper ordering
364
- const litPlugins = integrationPlugins.filter(p => p.name?.includes("lit"));
365
- const otherIntegrationPlugins = integrationPlugins.filter(p => !p.name?.includes("lit"));
366
-
367
- // Page island transform: auto-wraps components with `island` prop
368
- const pageTransformPlugin = pageIslandTransform({
369
- pagesDir: preResolvedConfig.pagesDir,
370
- layoutsDir: preResolvedConfig.layoutsDir,
371
- modules: preResolvedConfig.modules,
372
- verbose: preResolvedConfig.verbose,
373
- });
374
-
375
- return [
376
- pageTransformPlugin,
377
- ...imagePlugins,
378
- ...litPlugins,
379
- ...mdxPlugins,
380
- avalonPlugin,
381
- sidecarPlugin,
382
- ...nitroPlugins,
383
- ...otherIntegrationPlugins,
384
- ] as PluginOption[];
385
- }
386
-
387
- export function getResolvedConfig(): ResolvedAvalonConfig | undefined {
388
- return globalThis.__avalonConfig;
389
- }
390
-
391
- export function getPagesDir(): string {
392
- return globalThis.__avalonConfig?.pagesDir ?? "src/pages";
393
- }
394
-
395
- export function getLayoutsDir(): string {
396
- return globalThis.__avalonConfig?.layoutsDir ?? "src/layouts";
397
- }
398
-
399
-
400
- export function getNitroConfig(): NitroConfigOutput | undefined {
401
- return globalThis.__nitroConfig;
402
- }
403
-
404
- export function isNitroEnabled(): boolean {
405
- return globalThis.__nitroConfig !== undefined;
406
- }
407
-
408
- export type { AvalonPluginConfig, IntegrationName, ResolvedAvalonConfig, ImageConfig, ResolvedImageConfig } from "./types.ts";
409
- export type { AvalonNitroConfig, NitroConfigOutput } from "../nitro/config.ts";
1
+ /**
2
+ * Avalon Vite Plugin
3
+ *
4
+ * This module provides the main `avalon()` function that creates a unified Vite plugin
5
+ * for the Avalon framework. It handles configuration resolution, integration activation,
6
+ * Nitro server integration, and wires up all the necessary Vite hooks.
7
+ *
8
+ * ISLAND DETECTION:
9
+ * Islands are detected by usage - any component used with an `island` prop in pages
10
+ * or layouts is automatically treated as an island. No fixed islands directory required.
11
+ */
12
+
13
+ import type { Plugin, PluginOption, ResolvedConfig, ViteDevServer } from 'vite';
14
+ import type { AvalonPluginConfig, IntegrationName, ResolvedAvalonConfig } from './types.ts';
15
+ import { createRequire } from 'node:module';
16
+ import { dirname, join } from 'node:path';
17
+ import { resolveConfig, checkDirectoriesExist } from './config.ts';
18
+ import { activateIntegrations, activateSingleIntegration } from './integration-activator.ts';
19
+ import { discoverIntegrationsFromIslandUsage } from './auto-discover.ts';
20
+ import { validateActiveIntegrations, formatValidationResults } from './validation.ts';
21
+ import { createMDXPlugin } from '../build/mdx-plugin.ts';
22
+ import { mdxIslandTransform } from '../build/mdx-island-transform.ts';
23
+ import { pageIslandTransform } from '../build/page-island-transform.ts';
24
+ import { registry } from '../core/integrations/registry.ts';
25
+ import { createNitroIntegration } from './nitro-integration.ts';
26
+ import { islandSidecarPlugin } from './island-sidecar-plugin.ts';
27
+ import { createImagePlugin } from './image-optimization.ts';
28
+ import type { NitroConfigOutput } from '../nitro/config.ts';
29
+ declare global {
30
+ var __avalonConfig: ResolvedAvalonConfig | undefined;
31
+ var __viteDevServer: ViteDevServer | undefined;
32
+ var __nitroConfig: NitroConfigOutput | undefined;
33
+ }
34
+
35
+ /**
36
+ * Collects Vite plugins from all activated integrations.
37
+ *
38
+ * This function iterates through the activated integrations and calls their
39
+ * vitePlugin() method if implemented. The returned plugins are collected and
40
+ * flattened into a single array.
41
+ *
42
+ * Plugin ordering is handled to ensure correct application:
43
+ * - Lit plugins come first (DOM shim requirement)
44
+ * - Other framework plugins follow
45
+ *
46
+ * @param activeIntegrations - Set of activated integration names
47
+ * @param verbose - Whether to log detailed information
48
+ * @returns Promise resolving to an array of Vite plugins from integrations
49
+ */
50
+ export async function collectIntegrationPlugins(
51
+ activeIntegrations: Set<IntegrationName>,
52
+ verbose: boolean = false,
53
+ ): Promise<Plugin[]> {
54
+ const plugins: Plugin[] = [];
55
+ const litPlugins: Plugin[] = [];
56
+
57
+ for (const name of activeIntegrations) {
58
+ const validPlugins = await loadPluginsForIntegration(name, verbose);
59
+ if (name === 'lit') {
60
+ litPlugins.push(...validPlugins);
61
+ } else {
62
+ plugins.push(...validPlugins);
63
+ }
64
+ }
65
+
66
+ return [...litPlugins, ...plugins];
67
+ }
68
+
69
+ async function loadPluginsForIntegration(name: IntegrationName, _verbose: boolean): Promise<Plugin[]> {
70
+ const integration = registry.get(name);
71
+ if (!integration) return [];
72
+ if (typeof integration.vitePlugin !== 'function') return [];
73
+
74
+ try {
75
+ const result = await integration.vitePlugin();
76
+ const pluginArray = Array.isArray(result) ? result : [result];
77
+ return pluginArray.filter((p): p is Plugin => p != null);
78
+ } catch (error) {
79
+ console.warn(`[avalon] Failed to load vite plugin for ${name}:`, error instanceof Error ? error.message : error);
80
+ return [];
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Discovers which integrations are actually needed by scanning pages/layouts for island prop usage.
86
+ * This enables lazy loading - only load Vite plugins for frameworks that are actually used.
87
+ *
88
+ * @param config - The resolved Avalon configuration
89
+ * @param projectRoot - The project root directory (defaults to cwd)
90
+ * @returns Set of integration names that are actually needed
91
+ */
92
+ async function discoverNeededIntegrations(
93
+ config: ResolvedAvalonConfig,
94
+ projectRoot?: string,
95
+ ): Promise<Set<IntegrationName>> {
96
+ const needed = new Set<IntegrationName>();
97
+
98
+ try {
99
+ // Scan pages, layouts, and modules for components used with island prop
100
+ const discovered = await discoverIntegrationsFromIslandUsage(
101
+ config.pagesDir,
102
+ config.layoutsDir,
103
+ projectRoot,
104
+ config.modules?.dir,
105
+ );
106
+
107
+ // Only include integrations that are both discovered AND configured
108
+ for (const integration of discovered) {
109
+ if (config.integrations.includes(integration)) {
110
+ needed.add(integration);
111
+ }
112
+ }
113
+ } catch {
114
+ // If discovery fails, fall back to all configured integrations
115
+ for (const integration of config.integrations) {
116
+ needed.add(integration);
117
+ }
118
+ }
119
+
120
+ return needed;
121
+ }
122
+
123
+ async function resolveIntegrationsToLoad(preResolvedConfig: ResolvedAvalonConfig): Promise<IntegrationName[]> {
124
+ if (!preResolvedConfig.lazyIntegrations || preResolvedConfig.integrations.length === 0) {
125
+ return [...preResolvedConfig.integrations];
126
+ }
127
+
128
+ const needed = await discoverNeededIntegrations(preResolvedConfig);
129
+ if (needed.size === 0) {
130
+ return [...preResolvedConfig.integrations];
131
+ }
132
+
133
+ return Array.from(needed);
134
+ }
135
+
136
+ async function setupMDXPlugins(preResolvedConfig: ResolvedAvalonConfig): Promise<Plugin[]> {
137
+ try {
138
+ const mdxPlugins = await createMDXPlugin({
139
+ jsxImportSource: preResolvedConfig.mdx.jsxImportSource,
140
+ syntaxHighlighting: preResolvedConfig.mdx.syntaxHighlighting,
141
+ remarkPlugins: preResolvedConfig.mdx.remarkPlugins as import('unified').Pluggable[],
142
+ rehypePlugins: preResolvedConfig.mdx.rehypePlugins as import('unified').Pluggable[],
143
+ development: true,
144
+ });
145
+ mdxPlugins.push(mdxIslandTransform({ verbose: preResolvedConfig.verbose }));
146
+ return mdxPlugins;
147
+ } catch (error) {
148
+ if (preResolvedConfig.showWarnings) {
149
+ console.warn('⚠️ Could not configure MDX plugin:', error);
150
+ }
151
+ return [];
152
+ }
153
+ }
154
+
155
+ function setupNitroPlugins(
156
+ preResolvedConfig: ResolvedAvalonConfig,
157
+ nitroConfig: NonNullable<AvalonPluginConfig['nitro']>,
158
+ _verbose?: boolean,
159
+ ): { plugins: Plugin[]; options: NitroConfigOutput } {
160
+ const { plugins, nitroOptions } = createNitroIntegration(preResolvedConfig, nitroConfig);
161
+ globalThis.__nitroConfig = nitroOptions;
162
+ return { plugins, options: nitroOptions };
163
+ }
164
+
165
+ async function runAutoDiscovery(
166
+ resolvedConfig: ResolvedAvalonConfig,
167
+ viteRoot: string,
168
+ activeIntegrations: Set<IntegrationName>,
169
+ ): Promise<void> {
170
+ if (!resolvedConfig.autoDiscoverIntegrations) return;
171
+
172
+ try {
173
+ const discovered = await discoverIntegrationsFromIslandUsage(
174
+ resolvedConfig.pagesDir,
175
+ resolvedConfig.layoutsDir,
176
+ viteRoot,
177
+ resolvedConfig.modules?.dir,
178
+ );
179
+ for (const name of discovered) {
180
+ if (activeIntegrations.has(name)) continue;
181
+ try {
182
+ await activateSingleIntegration(name, activeIntegrations, resolvedConfig.verbose);
183
+ } catch (error) {
184
+ if (resolvedConfig.showWarnings) console.warn(` ⚠️ Could not auto-load integration: ${name}`, error);
185
+ }
186
+ }
187
+ } catch (error) {
188
+ if (resolvedConfig.showWarnings) console.warn(' ⚠️ Auto-discovery failed:', error);
189
+ }
190
+ }
191
+
192
+ function runValidation(resolvedConfig: ResolvedAvalonConfig, activeIntegrations: Set<IntegrationName>): void {
193
+ if (!resolvedConfig.validateIntegrations || activeIntegrations.size === 0) return;
194
+
195
+ const validationSummary = validateActiveIntegrations(activeIntegrations, resolvedConfig.showWarnings);
196
+ if (!validationSummary.allValid) {
197
+ console.error(formatValidationResults(validationSummary));
198
+ if (resolvedConfig.showWarnings) console.warn(' ⚠️ Some integrations have validation issues.');
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Creates the Avalon Vite plugin array
204
+ *
205
+ * @param config - Avalon configuration options
206
+ * @returns A promise that resolves to an array of Vite plugins that handle all Avalon functionality.
207
+ * Returns PluginOption[] to avoid TypeScript's excessive stack depth issues
208
+ * when comparing Plugin<any> arrays in Vite 8's complex type system.
209
+ */
210
+ export async function avalon(config?: AvalonPluginConfig): Promise<PluginOption[]> {
211
+ // Resolved configuration with defaults applied
212
+ let resolvedConfig: ResolvedAvalonConfig;
213
+
214
+ // Reference to Vite's resolved config
215
+ let viteConfig: ResolvedConfig;
216
+
217
+ // Track which integrations are activated
218
+ const activeIntegrations = new Set<IntegrationName>();
219
+
220
+ // Pre-resolve config to get MDX settings and integration list
221
+ // We use isDev=true as a default; the actual value will be set in configResolved
222
+ const preResolvedConfig = resolveConfig(config, true);
223
+
224
+ const integrationsToLoad = await resolveIntegrationsToLoad(preResolvedConfig);
225
+
226
+ if (integrationsToLoad.length > 0) {
227
+ await activateIntegrations({ ...preResolvedConfig, integrations: integrationsToLoad }, activeIntegrations);
228
+ }
229
+ const mdxPlugins = await setupMDXPlugins(preResolvedConfig);
230
+
231
+ // Image optimization plugins (vite-imagetools wrapper)
232
+ const imagePlugins = await createImagePlugin(preResolvedConfig.image, preResolvedConfig.verbose);
233
+
234
+ let integrationPlugins: Plugin[] = [];
235
+ if (activeIntegrations.size > 0) {
236
+ integrationPlugins = await collectIntegrationPlugins(activeIntegrations, preResolvedConfig.verbose);
237
+ }
238
+
239
+ let nitroPlugins: Plugin[] = [];
240
+ if (config?.nitro) {
241
+ const { plugins } = setupNitroPlugins(preResolvedConfig, config.nitro, preResolvedConfig.verbose);
242
+ nitroPlugins = plugins;
243
+ }
244
+
245
+ // Sidecar plugin for Vue/Svelte/Solid type declarations
246
+ const sidecarPlugin = islandSidecarPlugin({
247
+ verbose: preResolvedConfig.verbose,
248
+ });
249
+
250
+ // Pre-resolve paths for standalone projects.
251
+ // In the monorepo www/ project these are handled by manual resolve.alias.
252
+ const require = createRequire(import.meta.url);
253
+
254
+ let clientMainResolved: string | null = null;
255
+ try {
256
+ const clientEntry = require.resolve('@useavalon/avalon/client');
257
+ clientMainResolved = join(dirname(clientEntry), 'main.js');
258
+ } catch {
259
+ // Monorepo www/ sets its own alias
260
+ }
261
+
262
+ // Resolve /@useavalon/*/client and /@useavalon/*/client/hmr virtual imports
263
+ // used by main.js. These are resolved dynamically in the resolveId hook
264
+ // using Vite's resolver.
265
+ const integrationVirtualIds = new Set(
266
+ ['preact', 'react', 'vue', 'svelte', 'solid', 'lit', 'qwik'].flatMap(name => [
267
+ `/@useavalon/${name}/client`,
268
+ `/@useavalon/${name}/client/hmr`,
269
+ ]),
270
+ );
271
+
272
+ // The main Avalon plugin
273
+ const avalonPlugin: Plugin = {
274
+ name: 'avalon',
275
+ enforce: 'pre',
276
+
277
+ config() {
278
+ // @useavalon packages ship raw .ts source for SSR and pre-compiled
279
+ // .js for client-side code.
280
+ //
281
+ // oxc.exclude: Prevents Vite's built-in OXC from processing @useavalon
282
+ // .ts files. Without this, OXC applies integration plugins' global
283
+ // jsx: 'automatic' config to plain .ts files, causing errors.
284
+ // Our transform hook below handles TS stripping for SSR instead.
285
+ // Client-side loads .js files which OXC skips by default (/\.js$/).
286
+ //
287
+ // ssr.noExternal: Ensures Vite processes @useavalon packages through
288
+ // the SSR transform pipeline instead of treating them as external CJS.
289
+ return {
290
+ oxc: {
291
+ exclude: [/node_modules\/@useavalon\/.*\.tsx?$/],
292
+ },
293
+ ssr: {
294
+ noExternal: [/^@useavalon\//],
295
+ },
296
+ };
297
+ },
298
+
299
+ configResolved(resolvedViteConfig: ResolvedConfig) {
300
+ viteConfig = resolvedViteConfig;
301
+ const isDev = resolvedViteConfig.command === 'serve';
302
+ resolvedConfig = resolveConfig(config, isDev);
303
+
304
+ globalThis.__avalonConfig = resolvedConfig;
305
+
306
+ checkDirectoriesExist(resolvedConfig, resolvedViteConfig.root);
307
+ },
308
+
309
+ async resolveId(id: string) {
310
+ if (id === '/src/client/main.js' && clientMainResolved) {
311
+ return clientMainResolved;
312
+ }
313
+ // /@useavalon/*/client and /@useavalon/*/client/hmr — resolve through
314
+ // Vite's pipeline so it finds workspace-linked or npm-installed
315
+ // integration packages from the consuming project's node_modules,
316
+ // not from avalon's own context.
317
+ if (integrationVirtualIds.has(id)) {
318
+ const packageId = id.slice(1); // strip leading /
319
+ const resolved = await this.resolve(packageId);
320
+ return resolved?.id ?? null;
321
+ }
322
+ return null;
323
+ },
324
+
325
+ async transform(code: string, id: string) {
326
+ // For SSR: strip TypeScript from @useavalon packages ourselves.
327
+ // Integration plugins (react, preact) set jsx: 'automatic' which Vite's
328
+ // OXC applies to all files — causing "Invalid jsx option" errors on
329
+ // plain .ts files during SSR. We intercept and strip TS without JSX config.
330
+ // For client-side: main.js imports pre-compiled .js files that OXC
331
+ // skips entirely (default exclude: /\.js$/), avoiding the jsx conflict.
332
+ if (this.environment?.config?.consumer === 'server' && id.includes('@useavalon/') && /\.tsx?$/.test(id)) {
333
+ const { transform: oxcTransform } = await import('oxc-transform');
334
+ const result = await oxcTransform(id, code, {
335
+ sourcemap: true,
336
+ typescript: { onlyRemoveTypeImports: false },
337
+ });
338
+ return { code: result.code, map: result.map, moduleType: 'js' };
339
+ }
340
+ },
341
+
342
+ async buildStart() {
343
+ await runAutoDiscovery(resolvedConfig, viteConfig?.root, activeIntegrations);
344
+ runValidation(resolvedConfig, activeIntegrations);
345
+ },
346
+
347
+ configureServer(server: ViteDevServer) {
348
+ (globalThis as any).__viteDevServer = server;
349
+ },
350
+ };
351
+
352
+ // Extract Lit plugins for proper ordering
353
+ const litPlugins = integrationPlugins.filter(p => p.name?.includes('lit'));
354
+ const otherIntegrationPlugins = integrationPlugins.filter(p => !p.name?.includes('lit'));
355
+
356
+ // Page island transform: auto-wraps components with `island` prop
357
+ const pageTransformPlugin = pageIslandTransform({
358
+ pagesDir: preResolvedConfig.pagesDir,
359
+ layoutsDir: preResolvedConfig.layoutsDir,
360
+ modules: preResolvedConfig.modules,
361
+ verbose: preResolvedConfig.verbose,
362
+ });
363
+
364
+ return [
365
+ pageTransformPlugin,
366
+ ...imagePlugins,
367
+ ...litPlugins,
368
+ ...mdxPlugins,
369
+ avalonPlugin,
370
+ sidecarPlugin,
371
+ ...nitroPlugins,
372
+ ...otherIntegrationPlugins,
373
+ ] as PluginOption[];
374
+ }
375
+
376
+ export function getResolvedConfig(): ResolvedAvalonConfig | undefined {
377
+ return globalThis.__avalonConfig;
378
+ }
379
+
380
+ export function getPagesDir(): string {
381
+ return globalThis.__avalonConfig?.pagesDir ?? 'src/pages';
382
+ }
383
+
384
+ export function getLayoutsDir(): string {
385
+ return globalThis.__avalonConfig?.layoutsDir ?? 'src/layouts';
386
+ }
387
+
388
+ export function getNitroConfig(): NitroConfigOutput | undefined {
389
+ return globalThis.__nitroConfig;
390
+ }
391
+
392
+ export function isNitroEnabled(): boolean {
393
+ return globalThis.__nitroConfig !== undefined;
394
+ }
395
+
396
+ export type {
397
+ AvalonPluginConfig,
398
+ IntegrationName,
399
+ ResolvedAvalonConfig,
400
+ ImageConfig,
401
+ ResolvedImageConfig,
402
+ } from './types.ts';
403
+ export type { AvalonNitroConfig, NitroConfigOutput } from '../nitro/config.ts';