@useavalon/avalon 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -54
- package/mod.ts +302 -302
- package/package.json +49 -26
- package/src/build/integration-bundler-plugin.ts +116 -116
- package/src/build/integration-config.ts +168 -168
- package/src/build/integration-detection-plugin.ts +117 -117
- package/src/build/integration-resolver-plugin.ts +90 -90
- package/src/build/island-manifest.ts +269 -269
- package/src/build/island-types-generator.ts +476 -476
- package/src/build/mdx-island-transform.ts +464 -464
- package/src/build/mdx-plugin.ts +98 -98
- package/src/build/page-island-transform.ts +598 -598
- package/src/build/prop-extractors/index.ts +21 -21
- package/src/build/prop-extractors/lit.ts +140 -140
- package/src/build/prop-extractors/qwik.ts +16 -16
- package/src/build/prop-extractors/solid.ts +125 -125
- package/src/build/prop-extractors/svelte.ts +194 -194
- package/src/build/prop-extractors/vue.ts +111 -111
- package/src/build/sidecar-file-manager.ts +104 -104
- package/src/build/sidecar-renderer.ts +30 -30
- package/src/client/adapters/index.ts +21 -13
- package/src/client/components.ts +35 -35
- package/src/client/css-hmr-handler.ts +344 -344
- package/src/client/framework-adapter.ts +462 -462
- package/src/client/hmr-coordinator.ts +396 -396
- package/src/client/hmr-error-overlay.js +533 -533
- package/src/client/main.js +824 -816
- package/src/client/types/framework-runtime.d.ts +68 -68
- package/src/client/types/vite-hmr.d.ts +46 -46
- package/src/client/types/vite-virtual-modules.d.ts +70 -60
- package/src/components/Image.tsx +123 -123
- package/src/components/IslandErrorBoundary.tsx +145 -145
- package/src/components/LayoutDataErrorBoundary.tsx +141 -141
- package/src/components/LayoutErrorBoundary.tsx +127 -127
- package/src/components/PersistentIsland.tsx +52 -52
- package/src/components/StreamingErrorBoundary.tsx +233 -233
- package/src/components/StreamingLayout.tsx +538 -538
- package/src/core/components/component-analyzer.ts +192 -192
- package/src/core/components/component-detection.ts +508 -508
- package/src/core/components/enhanced-framework-detector.ts +500 -500
- package/src/core/components/framework-registry.ts +563 -563
- package/src/core/content/mdx-processor.ts +46 -46
- package/src/core/integrations/index.ts +19 -19
- package/src/core/integrations/loader.ts +125 -125
- package/src/core/integrations/registry.ts +175 -175
- package/src/core/islands/island-persistence.ts +325 -325
- package/src/core/islands/island-state-serializer.ts +258 -258
- package/src/core/islands/persistent-island-context.tsx +80 -80
- package/src/core/islands/use-persistent-state.ts +68 -68
- package/src/core/layout/enhanced-layout-resolver.ts +322 -322
- package/src/core/layout/layout-cache-manager.ts +485 -485
- package/src/core/layout/layout-composer.ts +357 -357
- package/src/core/layout/layout-data-loader.ts +516 -516
- package/src/core/layout/layout-discovery.ts +243 -243
- package/src/core/layout/layout-matcher.ts +299 -299
- package/src/core/layout/layout-types.ts +110 -110
- package/src/core/modules/framework-module-resolver.ts +273 -273
- package/src/islands/component-analysis.ts +213 -213
- package/src/islands/css-utils.ts +565 -565
- package/src/islands/discovery/index.ts +80 -80
- package/src/islands/discovery/registry.ts +340 -340
- package/src/islands/discovery/resolver.ts +477 -477
- package/src/islands/discovery/scanner.ts +386 -386
- package/src/islands/discovery/types.ts +117 -117
- package/src/islands/discovery/validator.ts +544 -544
- package/src/islands/discovery/watcher.ts +368 -368
- package/src/islands/framework-detection.ts +428 -428
- package/src/islands/integration-loader.ts +490 -490
- package/src/islands/island.tsx +565 -565
- package/src/islands/render-cache.ts +550 -550
- package/src/islands/types.ts +80 -80
- package/src/islands/universal-css-collector.ts +157 -157
- package/src/islands/universal-head-collector.ts +137 -137
- package/src/layout-system.d.ts +592 -592
- package/src/layout-system.ts +218 -218
- package/src/middleware/discovery.ts +268 -268
- package/src/middleware/executor.ts +315 -315
- package/src/middleware/index.ts +76 -76
- package/src/middleware/types.ts +99 -99
- package/src/nitro/build-config.ts +575 -575
- package/src/nitro/config.ts +483 -483
- package/src/nitro/error-handler.ts +636 -636
- package/src/nitro/index.ts +173 -173
- package/src/nitro/island-manifest.ts +584 -584
- package/src/nitro/middleware-adapter.ts +260 -260
- package/src/nitro/renderer.ts +1471 -1471
- package/src/nitro/route-discovery.ts +439 -439
- package/src/nitro/types.ts +321 -321
- package/src/render/collect-css.ts +198 -198
- package/src/render/error-pages.ts +79 -79
- package/src/render/isolated-ssr-renderer.ts +654 -654
- package/src/render/ssr.ts +1030 -1030
- package/src/schemas/api.ts +30 -30
- package/src/schemas/core.ts +64 -64
- package/src/schemas/index.ts +212 -212
- package/src/schemas/layout.ts +279 -279
- package/src/schemas/routing/index.ts +38 -38
- package/src/schemas/routing.ts +376 -376
- package/src/types/as-island.ts +20 -20
- package/src/types/image.d.ts +106 -106
- package/src/types/index.d.ts +22 -22
- package/src/types/island-jsx.d.ts +33 -33
- package/src/types/island-prop.d.ts +20 -20
- package/src/types/layout.ts +285 -285
- package/src/types/mdx.d.ts +6 -6
- package/src/types/routing.ts +555 -555
- package/src/types/types.ts +5 -5
- package/src/types/urlpattern.d.ts +49 -49
- package/src/types/vite-env.d.ts +11 -11
- package/src/utils/dev-logger.ts +299 -299
- package/src/utils/fs.ts +151 -151
- package/src/vite-plugin/auto-discover.ts +551 -551
- package/src/vite-plugin/config.ts +266 -266
- package/src/vite-plugin/errors.ts +127 -127
- package/src/vite-plugin/image-optimization.ts +156 -156
- package/src/vite-plugin/integration-activator.ts +126 -126
- package/src/vite-plugin/island-sidecar-plugin.ts +176 -176
- package/src/vite-plugin/module-discovery.ts +189 -189
- package/src/vite-plugin/nitro-integration.ts +1354 -1354
- package/src/vite-plugin/plugin.ts +403 -409
- package/src/vite-plugin/types.ts +327 -327
- package/src/vite-plugin/validation.ts +228 -228
- package/src/client/adapters/index.js +0 -12
- package/src/client/adapters/lit-adapter.js +0 -467
- package/src/client/adapters/lit-adapter.ts +0 -654
- package/src/client/adapters/preact-adapter.js +0 -223
- package/src/client/adapters/preact-adapter.ts +0 -331
- package/src/client/adapters/qwik-adapter.js +0 -259
- package/src/client/adapters/qwik-adapter.ts +0 -345
- package/src/client/adapters/react-adapter.js +0 -220
- package/src/client/adapters/react-adapter.ts +0 -353
- package/src/client/adapters/solid-adapter.js +0 -295
- package/src/client/adapters/solid-adapter.ts +0 -451
- package/src/client/adapters/svelte-adapter.js +0 -368
- package/src/client/adapters/svelte-adapter.ts +0 -524
- package/src/client/adapters/vue-adapter.js +0 -278
- package/src/client/adapters/vue-adapter.ts +0 -467
- package/src/client/components.js +0 -23
- package/src/client/css-hmr-handler.js +0 -263
- package/src/client/framework-adapter.js +0 -283
- package/src/client/hmr-coordinator.js +0 -274
|
@@ -1,126 +1,126 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration Activator for Avalon Vite Plugin
|
|
3
|
-
*
|
|
4
|
-
* This module handles the activation of framework integrations based on
|
|
5
|
-
* the plugin configuration. It validates integration names and loads
|
|
6
|
-
* the appropriate integration packages.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { IntegrationName, ResolvedAvalonConfig } from "./types.ts";
|
|
10
|
-
import { loadIntegration } from "../islands/integration-loader.ts";
|
|
11
|
-
import { IntegrationError } from "./errors.ts";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Valid integration names that can be activated
|
|
15
|
-
* This array is used for validation and error messages
|
|
16
|
-
*/
|
|
17
|
-
export const VALID_INTEGRATION_NAMES: readonly IntegrationName[] = [
|
|
18
|
-
"react",
|
|
19
|
-
"preact",
|
|
20
|
-
"vue",
|
|
21
|
-
"svelte",
|
|
22
|
-
"solid",
|
|
23
|
-
"lit",
|
|
24
|
-
"qwik",
|
|
25
|
-
] as const;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Check if a string is a valid integration name
|
|
29
|
-
*
|
|
30
|
-
* @param name - The name to validate
|
|
31
|
-
* @returns True if the name is a valid IntegrationName
|
|
32
|
-
*/
|
|
33
|
-
export function isValidIntegrationName(name: string): name is IntegrationName {
|
|
34
|
-
return VALID_INTEGRATION_NAMES.includes(name as IntegrationName);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Activates the specified integrations
|
|
39
|
-
*
|
|
40
|
-
* Uses the existing integration loader and registry to load and activate
|
|
41
|
-
* framework integrations based on the configuration.
|
|
42
|
-
*
|
|
43
|
-
* @param config - The resolved Avalon configuration
|
|
44
|
-
* @param activeIntegrations - Set to track which integrations have been activated
|
|
45
|
-
* @throws IntegrationError if an invalid integration name is provided or loading fails
|
|
46
|
-
*
|
|
47
|
-
* @example
|
|
48
|
-
* ```ts
|
|
49
|
-
* const activeIntegrations = new Set<IntegrationName>();
|
|
50
|
-
* await activateIntegrations(resolvedConfig, activeIntegrations);
|
|
51
|
-
* // activeIntegrations now contains all successfully loaded integrations
|
|
52
|
-
* ```
|
|
53
|
-
*/
|
|
54
|
-
export async function activateIntegrations(
|
|
55
|
-
config: ResolvedAvalonConfig,
|
|
56
|
-
activeIntegrations: Set<IntegrationName>
|
|
57
|
-
): Promise<void> {
|
|
58
|
-
const { integrations } = config;
|
|
59
|
-
|
|
60
|
-
// Load explicitly specified integrations
|
|
61
|
-
for (const name of integrations) {
|
|
62
|
-
// Validate integration name
|
|
63
|
-
if (!isValidIntegrationName(name)) {
|
|
64
|
-
throw new IntegrationError(
|
|
65
|
-
`Invalid integration name '${name}'. Valid integration names are: ${VALID_INTEGRATION_NAMES.join(", ")}`,
|
|
66
|
-
name
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Skip if already activated
|
|
71
|
-
if (activeIntegrations.has(name)) {
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
await loadIntegration(name);
|
|
77
|
-
activeIntegrations.add(name);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
throw new IntegrationError(
|
|
80
|
-
`Failed to activate integration. Is @useavalon/${name} installed?`,
|
|
81
|
-
name,
|
|
82
|
-
error as Error
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Activate a single integration by name
|
|
90
|
-
*
|
|
91
|
-
* @param name - The integration name to activate
|
|
92
|
-
* @param activeIntegrations - Set to track which integrations have been activated
|
|
93
|
-
* @param verbose - Whether to log activation messages
|
|
94
|
-
* @returns True if the integration was activated, false if already active
|
|
95
|
-
* @throws IntegrationError if the name is invalid or loading fails
|
|
96
|
-
*/
|
|
97
|
-
export async function activateSingleIntegration(
|
|
98
|
-
name: string,
|
|
99
|
-
activeIntegrations: Set<IntegrationName>,
|
|
100
|
-
_verbose: boolean = false
|
|
101
|
-
): Promise<boolean> {
|
|
102
|
-
// Validate integration name
|
|
103
|
-
if (!isValidIntegrationName(name)) {
|
|
104
|
-
throw new IntegrationError(
|
|
105
|
-
`Invalid integration name '${name}'. Valid integration names are: ${VALID_INTEGRATION_NAMES.join(", ")}`,
|
|
106
|
-
name
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Skip if already activated
|
|
111
|
-
if (activeIntegrations.has(name)) {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
await loadIntegration(name);
|
|
117
|
-
activeIntegrations.add(name);
|
|
118
|
-
return true;
|
|
119
|
-
} catch (error) {
|
|
120
|
-
throw new IntegrationError(
|
|
121
|
-
`Failed to activate integration. Is @useavalon/${name} installed?`,
|
|
122
|
-
name,
|
|
123
|
-
error as Error
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Integration Activator for Avalon Vite Plugin
|
|
3
|
+
*
|
|
4
|
+
* This module handles the activation of framework integrations based on
|
|
5
|
+
* the plugin configuration. It validates integration names and loads
|
|
6
|
+
* the appropriate integration packages.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { IntegrationName, ResolvedAvalonConfig } from "./types.ts";
|
|
10
|
+
import { loadIntegration } from "../islands/integration-loader.ts";
|
|
11
|
+
import { IntegrationError } from "./errors.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Valid integration names that can be activated
|
|
15
|
+
* This array is used for validation and error messages
|
|
16
|
+
*/
|
|
17
|
+
export const VALID_INTEGRATION_NAMES: readonly IntegrationName[] = [
|
|
18
|
+
"react",
|
|
19
|
+
"preact",
|
|
20
|
+
"vue",
|
|
21
|
+
"svelte",
|
|
22
|
+
"solid",
|
|
23
|
+
"lit",
|
|
24
|
+
"qwik",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if a string is a valid integration name
|
|
29
|
+
*
|
|
30
|
+
* @param name - The name to validate
|
|
31
|
+
* @returns True if the name is a valid IntegrationName
|
|
32
|
+
*/
|
|
33
|
+
export function isValidIntegrationName(name: string): name is IntegrationName {
|
|
34
|
+
return VALID_INTEGRATION_NAMES.includes(name as IntegrationName);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Activates the specified integrations
|
|
39
|
+
*
|
|
40
|
+
* Uses the existing integration loader and registry to load and activate
|
|
41
|
+
* framework integrations based on the configuration.
|
|
42
|
+
*
|
|
43
|
+
* @param config - The resolved Avalon configuration
|
|
44
|
+
* @param activeIntegrations - Set to track which integrations have been activated
|
|
45
|
+
* @throws IntegrationError if an invalid integration name is provided or loading fails
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* const activeIntegrations = new Set<IntegrationName>();
|
|
50
|
+
* await activateIntegrations(resolvedConfig, activeIntegrations);
|
|
51
|
+
* // activeIntegrations now contains all successfully loaded integrations
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export async function activateIntegrations(
|
|
55
|
+
config: ResolvedAvalonConfig,
|
|
56
|
+
activeIntegrations: Set<IntegrationName>
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
const { integrations } = config;
|
|
59
|
+
|
|
60
|
+
// Load explicitly specified integrations
|
|
61
|
+
for (const name of integrations) {
|
|
62
|
+
// Validate integration name
|
|
63
|
+
if (!isValidIntegrationName(name)) {
|
|
64
|
+
throw new IntegrationError(
|
|
65
|
+
`Invalid integration name '${name}'. Valid integration names are: ${VALID_INTEGRATION_NAMES.join(", ")}`,
|
|
66
|
+
name
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Skip if already activated
|
|
71
|
+
if (activeIntegrations.has(name)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await loadIntegration(name);
|
|
77
|
+
activeIntegrations.add(name);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new IntegrationError(
|
|
80
|
+
`Failed to activate integration. Is @useavalon/${name} installed?`,
|
|
81
|
+
name,
|
|
82
|
+
error as Error
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Activate a single integration by name
|
|
90
|
+
*
|
|
91
|
+
* @param name - The integration name to activate
|
|
92
|
+
* @param activeIntegrations - Set to track which integrations have been activated
|
|
93
|
+
* @param verbose - Whether to log activation messages
|
|
94
|
+
* @returns True if the integration was activated, false if already active
|
|
95
|
+
* @throws IntegrationError if the name is invalid or loading fails
|
|
96
|
+
*/
|
|
97
|
+
export async function activateSingleIntegration(
|
|
98
|
+
name: string,
|
|
99
|
+
activeIntegrations: Set<IntegrationName>,
|
|
100
|
+
_verbose: boolean = false
|
|
101
|
+
): Promise<boolean> {
|
|
102
|
+
// Validate integration name
|
|
103
|
+
if (!isValidIntegrationName(name)) {
|
|
104
|
+
throw new IntegrationError(
|
|
105
|
+
`Invalid integration name '${name}'. Valid integration names are: ${VALID_INTEGRATION_NAMES.join(", ")}`,
|
|
106
|
+
name
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Skip if already activated
|
|
111
|
+
if (activeIntegrations.has(name)) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await loadIntegration(name);
|
|
117
|
+
activeIntegrations.add(name);
|
|
118
|
+
return true;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw new IntegrationError(
|
|
121
|
+
`Failed to activate integration. Is @useavalon/${name} installed?`,
|
|
122
|
+
name,
|
|
123
|
+
error as Error
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -1,176 +1,176 @@
|
|
|
1
|
-
import type { Plugin } from "vite";
|
|
2
|
-
import { readFile, access } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { detectFrameworkFromPath } from "../islands/integration-loader.ts";
|
|
5
|
-
import { EXTRACTOR_MAP } from "../build/prop-extractors/index.ts";
|
|
6
|
-
import { renderSidecarContent } from "../build/sidecar-renderer.ts";
|
|
7
|
-
import {
|
|
8
|
-
getSidecarPath,
|
|
9
|
-
writeSidecarIfChanged,
|
|
10
|
-
deleteSidecar,
|
|
11
|
-
isSidecarFresh,
|
|
12
|
-
} from "../build/sidecar-file-manager.ts";
|
|
13
|
-
|
|
14
|
-
export interface SidecarPluginOptions {
|
|
15
|
-
/** Whether to log verbose output */
|
|
16
|
-
verbose?: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Frameworks that should be skipped — they already work natively with Preact/React JSX */
|
|
20
|
-
const SKIP_FRAMEWORKS = new Set(["react", "preact"]);
|
|
21
|
-
|
|
22
|
-
/** File extensions that qualify as island source files for HMR */
|
|
23
|
-
const ISLAND_EXTENSIONS = [".vue", ".svelte", ".lit.ts", ".solid.tsx", ".qwik.tsx"];
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Check tsconfig.json for `allowArbitraryExtensions` and warn if missing.
|
|
27
|
-
*/
|
|
28
|
-
export async function checkTsConfigForArbitraryExtensions(
|
|
29
|
-
projectRoot: string,
|
|
30
|
-
): Promise<void> {
|
|
31
|
-
const tsconfigPath = path.join(projectRoot, "tsconfig.json");
|
|
32
|
-
try {
|
|
33
|
-
const raw = await readFile(tsconfigPath, "utf-8");
|
|
34
|
-
const tsconfig = JSON.parse(raw);
|
|
35
|
-
if (tsconfig?.compilerOptions?.allowArbitraryExtensions !== true) {
|
|
36
|
-
console.warn(
|
|
37
|
-
'[avalon] tsconfig.json is missing "allowArbitraryExtensions: true" — sidecar .d.[ext].ts files require this setting',
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
} catch {
|
|
41
|
-
// tsconfig doesn't exist or can't be parsed - skip warning
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Check if a file path is already a sidecar declaration file.
|
|
47
|
-
* Sidecar files contain `.d.` before the framework extension.
|
|
48
|
-
*/
|
|
49
|
-
function isSidecarFile(filePath: string): boolean {
|
|
50
|
-
const basename = path.basename(filePath);
|
|
51
|
-
return /\.d\.(vue|svelte|lit|solid\.tsx)/.test(basename);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Check if a file path looks like a supported component file that needs a sidecar.
|
|
56
|
-
* Excludes files that are already sidecar declaration files.
|
|
57
|
-
*/
|
|
58
|
-
function needsSidecar(filePath: string): boolean {
|
|
59
|
-
if (isSidecarFile(filePath)) {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
return ISLAND_EXTENSIONS.some((ext) => filePath.endsWith(ext));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Generate a sidecar for a single component file.
|
|
67
|
-
* Returns true if a sidecar was written/updated, false otherwise.
|
|
68
|
-
*/
|
|
69
|
-
async function generateSidecarForFile(filePath: string, verbose?: boolean): Promise<boolean> {
|
|
70
|
-
try {
|
|
71
|
-
const framework = detectFrameworkFromPath(filePath);
|
|
72
|
-
if (SKIP_FRAMEWORKS.has(framework)) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const extractor = EXTRACTOR_MAP[framework];
|
|
77
|
-
if (!extractor) {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const source = await readFile(filePath, "utf-8");
|
|
82
|
-
const result = extractor(source);
|
|
83
|
-
const content = renderSidecarContent(result.propsType);
|
|
84
|
-
const sidecarPath = getSidecarPath(filePath);
|
|
85
|
-
return await writeSidecarIfChanged(sidecarPath, content);
|
|
86
|
-
} catch (err) {
|
|
87
|
-
if (verbose) {
|
|
88
|
-
console.warn(
|
|
89
|
-
`[avalon] Failed to generate sidecar for ${filePath}:`,
|
|
90
|
-
err instanceof Error ? err.message : err,
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Vite plugin that auto-generates `.d.[ext].ts` sidecar declaration files
|
|
99
|
-
* for non-React/Preact components when they are used as islands.
|
|
100
|
-
*
|
|
101
|
-
* Sidecars are generated on-demand when component files are loaded,
|
|
102
|
-
* rather than scanning a fixed directory at startup.
|
|
103
|
-
*/
|
|
104
|
-
export function islandSidecarPlugin(options: SidecarPluginOptions = {}): Plugin {
|
|
105
|
-
let projectRoot: string;
|
|
106
|
-
const processedFiles = new Set<string>();
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
name: "avalon:island-sidecar",
|
|
110
|
-
|
|
111
|
-
configResolved(config) {
|
|
112
|
-
projectRoot = config.root;
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
async buildStart() {
|
|
116
|
-
await checkTsConfigForArbitraryExtensions(projectRoot);
|
|
117
|
-
processedFiles.clear();
|
|
118
|
-
},
|
|
119
|
-
|
|
120
|
-
// Generate sidecar when a component file is loaded
|
|
121
|
-
async load(id) {
|
|
122
|
-
if (!needsSidecar(id) || processedFiles.has(id)) {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
processedFiles.add(id);
|
|
127
|
-
|
|
128
|
-
// Check if sidecar needs regeneration
|
|
129
|
-
const sidecarPath = getSidecarPath(id);
|
|
130
|
-
if (await isSidecarFresh(id, sidecarPath)) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const wrote = await generateSidecarForFile(id, options.verbose);
|
|
135
|
-
if (wrote && options.verbose) {
|
|
136
|
-
console.log(`[avalon] Generated sidecar for: ${id}`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return null; // Let Vite handle the actual file loading
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
async handleHotUpdate(ctx) {
|
|
143
|
-
const filePath = ctx.file;
|
|
144
|
-
|
|
145
|
-
if (!needsSidecar(filePath)) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Check if the file was deleted
|
|
150
|
-
let fileExists = true;
|
|
151
|
-
try {
|
|
152
|
-
await access(filePath);
|
|
153
|
-
} catch {
|
|
154
|
-
fileExists = false;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (!fileExists) {
|
|
158
|
-
// File was deleted — remove the sidecar
|
|
159
|
-
const sidecarPath = getSidecarPath(filePath);
|
|
160
|
-
const deleted = await deleteSidecar(sidecarPath);
|
|
161
|
-
if (deleted && options.verbose) {
|
|
162
|
-
console.log(`[avalon] Deleted sidecar for removed file: ${filePath}`);
|
|
163
|
-
}
|
|
164
|
-
processedFiles.delete(filePath);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// File was changed — regenerate sidecar
|
|
169
|
-
processedFiles.delete(filePath); // Allow re-processing
|
|
170
|
-
const wrote = await generateSidecarForFile(filePath, options.verbose);
|
|
171
|
-
if (wrote && options.verbose) {
|
|
172
|
-
console.log(`[avalon] Updated sidecar for: ${filePath}`);
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
}
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import { readFile, access } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { detectFrameworkFromPath } from "../islands/integration-loader.ts";
|
|
5
|
+
import { EXTRACTOR_MAP } from "../build/prop-extractors/index.ts";
|
|
6
|
+
import { renderSidecarContent } from "../build/sidecar-renderer.ts";
|
|
7
|
+
import {
|
|
8
|
+
getSidecarPath,
|
|
9
|
+
writeSidecarIfChanged,
|
|
10
|
+
deleteSidecar,
|
|
11
|
+
isSidecarFresh,
|
|
12
|
+
} from "../build/sidecar-file-manager.ts";
|
|
13
|
+
|
|
14
|
+
export interface SidecarPluginOptions {
|
|
15
|
+
/** Whether to log verbose output */
|
|
16
|
+
verbose?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Frameworks that should be skipped — they already work natively with Preact/React JSX */
|
|
20
|
+
const SKIP_FRAMEWORKS = new Set(["react", "preact"]);
|
|
21
|
+
|
|
22
|
+
/** File extensions that qualify as island source files for HMR */
|
|
23
|
+
const ISLAND_EXTENSIONS = [".vue", ".svelte", ".lit.ts", ".solid.tsx", ".qwik.tsx"];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check tsconfig.json for `allowArbitraryExtensions` and warn if missing.
|
|
27
|
+
*/
|
|
28
|
+
export async function checkTsConfigForArbitraryExtensions(
|
|
29
|
+
projectRoot: string,
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
const tsconfigPath = path.join(projectRoot, "tsconfig.json");
|
|
32
|
+
try {
|
|
33
|
+
const raw = await readFile(tsconfigPath, "utf-8");
|
|
34
|
+
const tsconfig = JSON.parse(raw);
|
|
35
|
+
if (tsconfig?.compilerOptions?.allowArbitraryExtensions !== true) {
|
|
36
|
+
console.warn(
|
|
37
|
+
'[avalon] tsconfig.json is missing "allowArbitraryExtensions: true" — sidecar .d.[ext].ts files require this setting',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// tsconfig doesn't exist or can't be parsed - skip warning
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if a file path is already a sidecar declaration file.
|
|
47
|
+
* Sidecar files contain `.d.` before the framework extension.
|
|
48
|
+
*/
|
|
49
|
+
function isSidecarFile(filePath: string): boolean {
|
|
50
|
+
const basename = path.basename(filePath);
|
|
51
|
+
return /\.d\.(vue|svelte|lit|solid\.tsx)/.test(basename);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if a file path looks like a supported component file that needs a sidecar.
|
|
56
|
+
* Excludes files that are already sidecar declaration files.
|
|
57
|
+
*/
|
|
58
|
+
function needsSidecar(filePath: string): boolean {
|
|
59
|
+
if (isSidecarFile(filePath)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return ISLAND_EXTENSIONS.some((ext) => filePath.endsWith(ext));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate a sidecar for a single component file.
|
|
67
|
+
* Returns true if a sidecar was written/updated, false otherwise.
|
|
68
|
+
*/
|
|
69
|
+
async function generateSidecarForFile(filePath: string, verbose?: boolean): Promise<boolean> {
|
|
70
|
+
try {
|
|
71
|
+
const framework = detectFrameworkFromPath(filePath);
|
|
72
|
+
if (SKIP_FRAMEWORKS.has(framework)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const extractor = EXTRACTOR_MAP[framework];
|
|
77
|
+
if (!extractor) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const source = await readFile(filePath, "utf-8");
|
|
82
|
+
const result = extractor(source);
|
|
83
|
+
const content = renderSidecarContent(result.propsType);
|
|
84
|
+
const sidecarPath = getSidecarPath(filePath);
|
|
85
|
+
return await writeSidecarIfChanged(sidecarPath, content);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (verbose) {
|
|
88
|
+
console.warn(
|
|
89
|
+
`[avalon] Failed to generate sidecar for ${filePath}:`,
|
|
90
|
+
err instanceof Error ? err.message : err,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Vite plugin that auto-generates `.d.[ext].ts` sidecar declaration files
|
|
99
|
+
* for non-React/Preact components when they are used as islands.
|
|
100
|
+
*
|
|
101
|
+
* Sidecars are generated on-demand when component files are loaded,
|
|
102
|
+
* rather than scanning a fixed directory at startup.
|
|
103
|
+
*/
|
|
104
|
+
export function islandSidecarPlugin(options: SidecarPluginOptions = {}): Plugin {
|
|
105
|
+
let projectRoot: string;
|
|
106
|
+
const processedFiles = new Set<string>();
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
name: "avalon:island-sidecar",
|
|
110
|
+
|
|
111
|
+
configResolved(config) {
|
|
112
|
+
projectRoot = config.root;
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async buildStart() {
|
|
116
|
+
await checkTsConfigForArbitraryExtensions(projectRoot);
|
|
117
|
+
processedFiles.clear();
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// Generate sidecar when a component file is loaded
|
|
121
|
+
async load(id) {
|
|
122
|
+
if (!needsSidecar(id) || processedFiles.has(id)) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
processedFiles.add(id);
|
|
127
|
+
|
|
128
|
+
// Check if sidecar needs regeneration
|
|
129
|
+
const sidecarPath = getSidecarPath(id);
|
|
130
|
+
if (await isSidecarFresh(id, sidecarPath)) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const wrote = await generateSidecarForFile(id, options.verbose);
|
|
135
|
+
if (wrote && options.verbose) {
|
|
136
|
+
console.log(`[avalon] Generated sidecar for: ${id}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return null; // Let Vite handle the actual file loading
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
async handleHotUpdate(ctx) {
|
|
143
|
+
const filePath = ctx.file;
|
|
144
|
+
|
|
145
|
+
if (!needsSidecar(filePath)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check if the file was deleted
|
|
150
|
+
let fileExists = true;
|
|
151
|
+
try {
|
|
152
|
+
await access(filePath);
|
|
153
|
+
} catch {
|
|
154
|
+
fileExists = false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!fileExists) {
|
|
158
|
+
// File was deleted — remove the sidecar
|
|
159
|
+
const sidecarPath = getSidecarPath(filePath);
|
|
160
|
+
const deleted = await deleteSidecar(sidecarPath);
|
|
161
|
+
if (deleted && options.verbose) {
|
|
162
|
+
console.log(`[avalon] Deleted sidecar for removed file: ${filePath}`);
|
|
163
|
+
}
|
|
164
|
+
processedFiles.delete(filePath);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// File was changed — regenerate sidecar
|
|
169
|
+
processedFiles.delete(filePath); // Allow re-processing
|
|
170
|
+
const wrote = await generateSidecarForFile(filePath, options.verbose);
|
|
171
|
+
if (wrote && options.verbose) {
|
|
172
|
+
console.log(`[avalon] Updated sidecar for: ${filePath}`);
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|