@useavalon/core 0.1.1 → 0.1.3
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/base-integration.ts +111 -111
- package/package.json +14 -5
- package/types.ts +156 -156
- package/utils.ts +234 -234
- package/vitest.config.ts +0 -13
package/base-integration.ts
CHANGED
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Abstract base class for framework integrations.
|
|
3
|
-
* Provides common functionality and enforces the Integration interface.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
Integration,
|
|
8
|
-
IntegrationConfig,
|
|
9
|
-
RenderParams,
|
|
10
|
-
RenderResult,
|
|
11
|
-
} from "./types.ts";
|
|
12
|
-
import type { Plugin } from "vite";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Abstract base class that integrations can extend to inherit common functionality
|
|
16
|
-
*/
|
|
17
|
-
export abstract class BaseIntegration implements Integration {
|
|
18
|
-
abstract name: string;
|
|
19
|
-
abstract version: string;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Render a component to HTML (must be implemented by subclass)
|
|
23
|
-
*/
|
|
24
|
-
abstract render(params: RenderParams): Promise<RenderResult>;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Get hydration script (must be implemented by subclass)
|
|
28
|
-
*/
|
|
29
|
-
abstract getHydrationScript(): string;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Get integration configuration (must be implemented by subclass)
|
|
33
|
-
*/
|
|
34
|
-
abstract config(): IntegrationConfig;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Optional Vite plugin configuration
|
|
38
|
-
* Subclasses can override this to provide build-time processing
|
|
39
|
-
*/
|
|
40
|
-
vitePlugin?(): Promise<Plugin | Plugin[]> {
|
|
41
|
-
return Promise.resolve(undefined as unknown as Plugin);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Validate render parameters
|
|
46
|
-
* @param params - Parameters to validate
|
|
47
|
-
* @throws Error if parameters are invalid
|
|
48
|
-
*/
|
|
49
|
-
protected validateRenderParams(params: RenderParams): void {
|
|
50
|
-
if (!params.src) {
|
|
51
|
-
throw new Error(
|
|
52
|
-
`[${this.name}] Missing required parameter: src`,
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (typeof params.props !== "object" || params.props === null) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
`[${this.name}] Invalid props: must be an object`,
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Create a render error with integration context
|
|
65
|
-
* @param message - Error message
|
|
66
|
-
* @param cause - Original error
|
|
67
|
-
* @returns Error with context
|
|
68
|
-
*/
|
|
69
|
-
protected createRenderError(message: string, cause?: unknown): Error {
|
|
70
|
-
const error = new Error(
|
|
71
|
-
`[${this.name}] ${message}`,
|
|
72
|
-
cause ? { cause } : undefined,
|
|
73
|
-
);
|
|
74
|
-
return error;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Log a warning message with integration context
|
|
79
|
-
* @param message - Warning message
|
|
80
|
-
*/
|
|
81
|
-
protected warn(message: string): void {
|
|
82
|
-
console.warn(`[${this.name}] ${message}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Log an error message with integration context
|
|
87
|
-
* @param message - Error message
|
|
88
|
-
* @param error - Optional error object
|
|
89
|
-
*/
|
|
90
|
-
protected error(message: string, error?: unknown): void {
|
|
91
|
-
console.error(`[${this.name}] ${message}`, error || "");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Check if running in development mode
|
|
96
|
-
* @param params - Render parameters
|
|
97
|
-
* @returns True if in development mode
|
|
98
|
-
*/
|
|
99
|
-
protected isDevelopment(params: RenderParams): boolean {
|
|
100
|
-
return params.isDev ?? process.env.NODE_ENV !== "production";
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Get the Vite dev server if available
|
|
105
|
-
* @param params - Render parameters
|
|
106
|
-
* @returns Vite dev server or undefined
|
|
107
|
-
*/
|
|
108
|
-
protected getViteServer(params: RenderParams): unknown {
|
|
109
|
-
return params.viteServer ?? (globalThis as Record<string, unknown>).__viteDevServer;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Abstract base class for framework integrations.
|
|
3
|
+
* Provides common functionality and enforces the Integration interface.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
Integration,
|
|
8
|
+
IntegrationConfig,
|
|
9
|
+
RenderParams,
|
|
10
|
+
RenderResult,
|
|
11
|
+
} from "./types.ts";
|
|
12
|
+
import type { Plugin } from "vite";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Abstract base class that integrations can extend to inherit common functionality
|
|
16
|
+
*/
|
|
17
|
+
export abstract class BaseIntegration implements Integration {
|
|
18
|
+
abstract name: string;
|
|
19
|
+
abstract version: string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Render a component to HTML (must be implemented by subclass)
|
|
23
|
+
*/
|
|
24
|
+
abstract render(params: RenderParams): Promise<RenderResult>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get hydration script (must be implemented by subclass)
|
|
28
|
+
*/
|
|
29
|
+
abstract getHydrationScript(): string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get integration configuration (must be implemented by subclass)
|
|
33
|
+
*/
|
|
34
|
+
abstract config(): IntegrationConfig;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Optional Vite plugin configuration
|
|
38
|
+
* Subclasses can override this to provide build-time processing
|
|
39
|
+
*/
|
|
40
|
+
vitePlugin?(): Promise<Plugin | Plugin[]> {
|
|
41
|
+
return Promise.resolve(undefined as unknown as Plugin);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validate render parameters
|
|
46
|
+
* @param params - Parameters to validate
|
|
47
|
+
* @throws Error if parameters are invalid
|
|
48
|
+
*/
|
|
49
|
+
protected validateRenderParams(params: RenderParams): void {
|
|
50
|
+
if (!params.src) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`[${this.name}] Missing required parameter: src`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof params.props !== "object" || params.props === null) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`[${this.name}] Invalid props: must be an object`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create a render error with integration context
|
|
65
|
+
* @param message - Error message
|
|
66
|
+
* @param cause - Original error
|
|
67
|
+
* @returns Error with context
|
|
68
|
+
*/
|
|
69
|
+
protected createRenderError(message: string, cause?: unknown): Error {
|
|
70
|
+
const error = new Error(
|
|
71
|
+
`[${this.name}] ${message}`,
|
|
72
|
+
cause ? { cause } : undefined,
|
|
73
|
+
);
|
|
74
|
+
return error;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Log a warning message with integration context
|
|
79
|
+
* @param message - Warning message
|
|
80
|
+
*/
|
|
81
|
+
protected warn(message: string): void {
|
|
82
|
+
console.warn(`[${this.name}] ${message}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Log an error message with integration context
|
|
87
|
+
* @param message - Error message
|
|
88
|
+
* @param error - Optional error object
|
|
89
|
+
*/
|
|
90
|
+
protected error(message: string, error?: unknown): void {
|
|
91
|
+
console.error(`[${this.name}] ${message}`, error || "");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if running in development mode
|
|
96
|
+
* @param params - Render parameters
|
|
97
|
+
* @returns True if in development mode
|
|
98
|
+
*/
|
|
99
|
+
protected isDevelopment(params: RenderParams): boolean {
|
|
100
|
+
return params.isDev ?? process.env.NODE_ENV !== "production";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get the Vite dev server if available
|
|
105
|
+
* @param params - Render parameters
|
|
106
|
+
* @returns Vite dev server or undefined
|
|
107
|
+
*/
|
|
108
|
+
protected getViteServer(params: RenderParams): unknown {
|
|
109
|
+
return params.viteServer ?? (globalThis as Record<string, unknown>).__viteDevServer;
|
|
110
|
+
}
|
|
111
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@useavalon/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Core types and utilities for Avalon framework integrations",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,15 +10,24 @@
|
|
|
10
10
|
"directory": "packages/integrations/core"
|
|
11
11
|
},
|
|
12
12
|
"homepage": "https://useavalon.dev",
|
|
13
|
-
"keywords": [
|
|
13
|
+
"keywords": [
|
|
14
|
+
"avalon",
|
|
15
|
+
"islands",
|
|
16
|
+
"framework",
|
|
17
|
+
"core"
|
|
18
|
+
],
|
|
14
19
|
"exports": {
|
|
15
20
|
".": "./types.ts",
|
|
16
21
|
"./types": "./types.ts",
|
|
17
22
|
"./utils": "./utils.ts",
|
|
18
23
|
"./base": "./base-integration.ts"
|
|
19
24
|
},
|
|
20
|
-
"files": [
|
|
21
|
-
|
|
22
|
-
"
|
|
25
|
+
"files": [
|
|
26
|
+
"*.ts",
|
|
27
|
+
"!vitest.config.ts",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"vite": "^8.0.0"
|
|
23
32
|
}
|
|
24
33
|
}
|
package/types.ts
CHANGED
|
@@ -1,156 +1,156 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core types for the Avalon framework integration system.
|
|
3
|
-
* These types define the contract that all framework integrations must implement.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Plugin, ViteDevServer } from 'vite';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Hydration condition types that determine when an island should become interactive
|
|
10
|
-
*/
|
|
11
|
-
export type HydrationCondition =
|
|
12
|
-
| 'on:client' // Hydrate immediately on page load
|
|
13
|
-
| 'on:visible' // Hydrate when island enters viewport
|
|
14
|
-
| 'on:interaction' // Hydrate on first user interaction
|
|
15
|
-
| 'on:idle' // Hydrate when browser is idle
|
|
16
|
-
| `media:${string}`; // Hydrate when media query matches
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Parameters passed to the integration's render function
|
|
20
|
-
*/
|
|
21
|
-
export interface RenderParams {
|
|
22
|
-
/** The component to render (may be null if integration loads it) */
|
|
23
|
-
component: unknown;
|
|
24
|
-
/** Props to pass to the component */
|
|
25
|
-
props: Record<string, unknown>;
|
|
26
|
-
/** Source path to the component file */
|
|
27
|
-
src: string;
|
|
28
|
-
/** Hydration condition for the island */
|
|
29
|
-
condition?: HydrationCondition;
|
|
30
|
-
/** If true, component will not hydrate on client */
|
|
31
|
-
ssrOnly?: boolean;
|
|
32
|
-
/** Vite dev server instance (available in development) */
|
|
33
|
-
viteServer?: ViteDevServer;
|
|
34
|
-
/** Whether running in development mode */
|
|
35
|
-
isDev?: boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Result returned from the integration's render function
|
|
40
|
-
*/
|
|
41
|
-
export interface RenderResult {
|
|
42
|
-
/** Rendered HTML string */
|
|
43
|
-
html: string;
|
|
44
|
-
/** CSS to inject (for frameworks with scoped styles) */
|
|
45
|
-
css?: string;
|
|
46
|
-
/** Additional head content (scripts, meta tags, etc.) */
|
|
47
|
-
head?: string;
|
|
48
|
-
/** Scope ID for CSS scoping (e.g., Vue scoped styles) */
|
|
49
|
-
scopeId?: string;
|
|
50
|
-
/** Data to serialize for client-side hydration */
|
|
51
|
-
hydrationData?: Record<string, unknown>;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Configuration for a framework integration
|
|
56
|
-
*/
|
|
57
|
-
export interface IntegrationConfig {
|
|
58
|
-
/** Unique name of the integration (e.g., "preact", "vue") */
|
|
59
|
-
name: string;
|
|
60
|
-
/** File extensions this integration handles */
|
|
61
|
-
fileExtensions: string[];
|
|
62
|
-
/** JSX import sources for this framework */
|
|
63
|
-
jsxImportSources?: string[];
|
|
64
|
-
/** Patterns to detect if a file uses this framework */
|
|
65
|
-
detectionPatterns: {
|
|
66
|
-
/** Regex patterns to match import statements */
|
|
67
|
-
imports: RegExp[];
|
|
68
|
-
/** Regex patterns to match code content */
|
|
69
|
-
content: RegExp[];
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Main integration interface that all framework integrations must implement
|
|
75
|
-
*/
|
|
76
|
-
export interface Integration {
|
|
77
|
-
/** Unique name of the integration */
|
|
78
|
-
name: string;
|
|
79
|
-
/** Version of the integration package */
|
|
80
|
-
version: string;
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Render a component to HTML on the server
|
|
84
|
-
* @param params - Rendering parameters
|
|
85
|
-
* @returns Promise resolving to render result
|
|
86
|
-
*/
|
|
87
|
-
render(params: RenderParams): Promise<RenderResult>;
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Get the hydration script for client-side initialization
|
|
91
|
-
* @returns JavaScript code as a string
|
|
92
|
-
*/
|
|
93
|
-
getHydrationScript(): string;
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Get the integration configuration
|
|
97
|
-
* @returns Integration configuration object
|
|
98
|
-
*/
|
|
99
|
-
config(): IntegrationConfig;
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Optional: Provide Vite plugins required for this framework's build-time processing.
|
|
103
|
-
*
|
|
104
|
-
* Each integration encapsulates its own Vite plugin configuration, eliminating
|
|
105
|
-
* the need for manual framework plugin setup in the user's vite.config.ts.
|
|
106
|
-
*
|
|
107
|
-
* **Plugin Ordering Requirements:**
|
|
108
|
-
* - Lit integration plugins MUST be placed first (DOM shim requirement)
|
|
109
|
-
* - MDX plugins should come after Lit but before other framework plugins
|
|
110
|
-
* - Framework-specific plugins (React, Vue, Svelte, etc.) come last
|
|
111
|
-
*
|
|
112
|
-
* The Avalon plugin handles ordering automatically when collecting plugins
|
|
113
|
-
* from all activated integrations.
|
|
114
|
-
*
|
|
115
|
-
* @returns Promise resolving to a single Vite plugin or array of plugins
|
|
116
|
-
*
|
|
117
|
-
* @example
|
|
118
|
-
* ```typescript
|
|
119
|
-
* async vitePlugin(): Promise<Plugin | Plugin[]> {
|
|
120
|
-
* const { default: vue } = await import('@vitejs/plugin-vue');
|
|
121
|
-
* return vue({
|
|
122
|
-
* template: {
|
|
123
|
-
* compilerOptions: {
|
|
124
|
-
* isCustomElement: tag => tag === 'avalon-island',
|
|
125
|
-
* },
|
|
126
|
-
* },
|
|
127
|
-
* });
|
|
128
|
-
* }
|
|
129
|
-
* ```
|
|
130
|
-
*/
|
|
131
|
-
vitePlugin?(): Promise<Plugin | Plugin[]>;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Context available during component loading
|
|
136
|
-
*/
|
|
137
|
-
export interface LoadContext {
|
|
138
|
-
/** Whether running in development mode */
|
|
139
|
-
isDev: boolean;
|
|
140
|
-
/** Vite dev server instance (available in development) */
|
|
141
|
-
viteServer?: ViteDevServer;
|
|
142
|
-
/** Path to build output directory (production) */
|
|
143
|
-
buildOutput?: string;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Options for component loading
|
|
148
|
-
*/
|
|
149
|
-
export interface ComponentLoadOptions {
|
|
150
|
-
/** Source path to the component */
|
|
151
|
-
src: string;
|
|
152
|
-
/** Load context */
|
|
153
|
-
context: LoadContext;
|
|
154
|
-
/** Whether to load for SSR or client */
|
|
155
|
-
target?: 'ssr' | 'client';
|
|
156
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Core types for the Avalon framework integration system.
|
|
3
|
+
* These types define the contract that all framework integrations must implement.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Plugin, ViteDevServer } from 'vite';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hydration condition types that determine when an island should become interactive
|
|
10
|
+
*/
|
|
11
|
+
export type HydrationCondition =
|
|
12
|
+
| 'on:client' // Hydrate immediately on page load
|
|
13
|
+
| 'on:visible' // Hydrate when island enters viewport
|
|
14
|
+
| 'on:interaction' // Hydrate on first user interaction
|
|
15
|
+
| 'on:idle' // Hydrate when browser is idle
|
|
16
|
+
| `media:${string}`; // Hydrate when media query matches
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parameters passed to the integration's render function
|
|
20
|
+
*/
|
|
21
|
+
export interface RenderParams {
|
|
22
|
+
/** The component to render (may be null if integration loads it) */
|
|
23
|
+
component: unknown;
|
|
24
|
+
/** Props to pass to the component */
|
|
25
|
+
props: Record<string, unknown>;
|
|
26
|
+
/** Source path to the component file */
|
|
27
|
+
src: string;
|
|
28
|
+
/** Hydration condition for the island */
|
|
29
|
+
condition?: HydrationCondition;
|
|
30
|
+
/** If true, component will not hydrate on client */
|
|
31
|
+
ssrOnly?: boolean;
|
|
32
|
+
/** Vite dev server instance (available in development) */
|
|
33
|
+
viteServer?: ViteDevServer;
|
|
34
|
+
/** Whether running in development mode */
|
|
35
|
+
isDev?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Result returned from the integration's render function
|
|
40
|
+
*/
|
|
41
|
+
export interface RenderResult {
|
|
42
|
+
/** Rendered HTML string */
|
|
43
|
+
html: string;
|
|
44
|
+
/** CSS to inject (for frameworks with scoped styles) */
|
|
45
|
+
css?: string;
|
|
46
|
+
/** Additional head content (scripts, meta tags, etc.) */
|
|
47
|
+
head?: string;
|
|
48
|
+
/** Scope ID for CSS scoping (e.g., Vue scoped styles) */
|
|
49
|
+
scopeId?: string;
|
|
50
|
+
/** Data to serialize for client-side hydration */
|
|
51
|
+
hydrationData?: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Configuration for a framework integration
|
|
56
|
+
*/
|
|
57
|
+
export interface IntegrationConfig {
|
|
58
|
+
/** Unique name of the integration (e.g., "preact", "vue") */
|
|
59
|
+
name: string;
|
|
60
|
+
/** File extensions this integration handles */
|
|
61
|
+
fileExtensions: string[];
|
|
62
|
+
/** JSX import sources for this framework */
|
|
63
|
+
jsxImportSources?: string[];
|
|
64
|
+
/** Patterns to detect if a file uses this framework */
|
|
65
|
+
detectionPatterns: {
|
|
66
|
+
/** Regex patterns to match import statements */
|
|
67
|
+
imports: RegExp[];
|
|
68
|
+
/** Regex patterns to match code content */
|
|
69
|
+
content: RegExp[];
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Main integration interface that all framework integrations must implement
|
|
75
|
+
*/
|
|
76
|
+
export interface Integration {
|
|
77
|
+
/** Unique name of the integration */
|
|
78
|
+
name: string;
|
|
79
|
+
/** Version of the integration package */
|
|
80
|
+
version: string;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Render a component to HTML on the server
|
|
84
|
+
* @param params - Rendering parameters
|
|
85
|
+
* @returns Promise resolving to render result
|
|
86
|
+
*/
|
|
87
|
+
render(params: RenderParams): Promise<RenderResult>;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get the hydration script for client-side initialization
|
|
91
|
+
* @returns JavaScript code as a string
|
|
92
|
+
*/
|
|
93
|
+
getHydrationScript(): string;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get the integration configuration
|
|
97
|
+
* @returns Integration configuration object
|
|
98
|
+
*/
|
|
99
|
+
config(): IntegrationConfig;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Optional: Provide Vite plugins required for this framework's build-time processing.
|
|
103
|
+
*
|
|
104
|
+
* Each integration encapsulates its own Vite plugin configuration, eliminating
|
|
105
|
+
* the need for manual framework plugin setup in the user's vite.config.ts.
|
|
106
|
+
*
|
|
107
|
+
* **Plugin Ordering Requirements:**
|
|
108
|
+
* - Lit integration plugins MUST be placed first (DOM shim requirement)
|
|
109
|
+
* - MDX plugins should come after Lit but before other framework plugins
|
|
110
|
+
* - Framework-specific plugins (React, Vue, Svelte, etc.) come last
|
|
111
|
+
*
|
|
112
|
+
* The Avalon plugin handles ordering automatically when collecting plugins
|
|
113
|
+
* from all activated integrations.
|
|
114
|
+
*
|
|
115
|
+
* @returns Promise resolving to a single Vite plugin or array of plugins
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* async vitePlugin(): Promise<Plugin | Plugin[]> {
|
|
120
|
+
* const { default: vue } = await import('@vitejs/plugin-vue');
|
|
121
|
+
* return vue({
|
|
122
|
+
* template: {
|
|
123
|
+
* compilerOptions: {
|
|
124
|
+
* isCustomElement: tag => tag === 'avalon-island',
|
|
125
|
+
* },
|
|
126
|
+
* },
|
|
127
|
+
* });
|
|
128
|
+
* }
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
vitePlugin?(): Promise<Plugin | Plugin[]>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Context available during component loading
|
|
136
|
+
*/
|
|
137
|
+
export interface LoadContext {
|
|
138
|
+
/** Whether running in development mode */
|
|
139
|
+
isDev: boolean;
|
|
140
|
+
/** Vite dev server instance (available in development) */
|
|
141
|
+
viteServer?: ViteDevServer;
|
|
142
|
+
/** Path to build output directory (production) */
|
|
143
|
+
buildOutput?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Options for component loading
|
|
148
|
+
*/
|
|
149
|
+
export interface ComponentLoadOptions {
|
|
150
|
+
/** Source path to the component */
|
|
151
|
+
src: string;
|
|
152
|
+
/** Load context */
|
|
153
|
+
context: LoadContext;
|
|
154
|
+
/** Whether to load for SSR or client */
|
|
155
|
+
target?: 'ssr' | 'client';
|
|
156
|
+
}
|
package/utils.ts
CHANGED
|
@@ -1,234 +1,234 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Common utilities for framework integrations.
|
|
3
|
-
* Provides shared functionality for component loading, path resolution, and more.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ComponentLoadOptions, LoadContext } from './types.ts';
|
|
7
|
-
import type { ViteDevServer } from 'vite';
|
|
8
|
-
import { join, resolve } from 'node:path';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Load a component module in development or production
|
|
12
|
-
* @param options - Component load options
|
|
13
|
-
* @returns The loaded component module
|
|
14
|
-
*/
|
|
15
|
-
export async function loadComponent(options: ComponentLoadOptions) {
|
|
16
|
-
const { src, context, target = 'ssr' } = options;
|
|
17
|
-
|
|
18
|
-
if (context.isDev && context.viteServer) {
|
|
19
|
-
return await loadComponentDev(src, context.viteServer);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return await loadComponentProd(src, context.buildOutput, target);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Load a component in development mode using Vite's SSR loader
|
|
27
|
-
* @param src - Source path to component
|
|
28
|
-
* @param viteServer - Vite dev server instance
|
|
29
|
-
* @returns The loaded component module
|
|
30
|
-
*/
|
|
31
|
-
async function loadComponentDev(src: string, viteServer: ViteDevServer) {
|
|
32
|
-
try {
|
|
33
|
-
const module = await viteServer.ssrLoadModule(src);
|
|
34
|
-
return module.default || module;
|
|
35
|
-
} catch (error) {
|
|
36
|
-
throw new Error(`Failed to load component in development: ${src}`, { cause: error });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Load a component in production mode from build output
|
|
42
|
-
* @param src - Source path to component
|
|
43
|
-
* @param buildOutput - Path to build output directory
|
|
44
|
-
* @param target - Whether loading for SSR or client
|
|
45
|
-
* @returns The loaded component module
|
|
46
|
-
*/
|
|
47
|
-
async function loadComponentProd(src: string, buildOutput: string | undefined, target: 'ssr' | 'client') {
|
|
48
|
-
const outputPath = resolveProductionPath(src, buildOutput, target);
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
const module = await import(/* @vite-ignore */ toImportSpecifier(outputPath));
|
|
52
|
-
return module.default || module;
|
|
53
|
-
} catch (error) {
|
|
54
|
-
throw new Error(`Failed to load component in production: ${outputPath}`, { cause: error });
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Resolve the production build path for a component
|
|
60
|
-
* @param src - Source path to component
|
|
61
|
-
* @param buildOutput - Path to build output directory
|
|
62
|
-
* @param target - Whether loading for SSR or client
|
|
63
|
-
* @returns Resolved path to built component
|
|
64
|
-
*/
|
|
65
|
-
export function resolveProductionPath(src: string, buildOutput: string | undefined, target: 'ssr' | 'client') {
|
|
66
|
-
const base = buildOutput || 'dist';
|
|
67
|
-
const targetDir = target === 'ssr' ? 'ssr' : 'client';
|
|
68
|
-
|
|
69
|
-
// Convert source path to output path
|
|
70
|
-
// e.g., /islands/Counter.tsx -> /dist/ssr/islands/Counter.js
|
|
71
|
-
const outputPath = src
|
|
72
|
-
.replace(/^\//, '') // Remove leading slash
|
|
73
|
-
.replace(/\.(tsx|jsx|ts|js|vue|svelte)$/, '.js'); // Change extension to .js
|
|
74
|
-
|
|
75
|
-
return join(base, targetDir, outputPath);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Normalize a component path to be absolute
|
|
80
|
-
* @param src - Source path (may be relative or absolute)
|
|
81
|
-
* @param baseDir - Base directory to resolve relative paths from
|
|
82
|
-
* @returns Absolute path
|
|
83
|
-
*/
|
|
84
|
-
export function normalizePath(src: string, baseDir?: string) {
|
|
85
|
-
if (src.startsWith('/') || src.startsWith('file://')) {
|
|
86
|
-
return src;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (baseDir) {
|
|
90
|
-
return resolve(baseDir, src);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return resolve(src);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Extract the file extension from a path
|
|
98
|
-
* @param path - File path
|
|
99
|
-
* @returns File extension (including the dot)
|
|
100
|
-
*/
|
|
101
|
-
export function getExtension(path: string) {
|
|
102
|
-
const match = /\.[^.]+$/.exec(path);
|
|
103
|
-
return match ? match[0] : '';
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Check if a path matches any of the given extensions
|
|
108
|
-
* @param path - File path to check
|
|
109
|
-
* @param extensions - Array of extensions (e.g., [".tsx", ".jsx"])
|
|
110
|
-
* @returns True if path matches any extension
|
|
111
|
-
*/
|
|
112
|
-
export function hasExtension(path: string, extensions: string[]) {
|
|
113
|
-
const ext = getExtension(path);
|
|
114
|
-
return extensions.includes(ext);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Generate a unique scope ID for a component (useful for CSS scoping)
|
|
119
|
-
* @param src - Source path to component
|
|
120
|
-
* @returns Unique scope identifier
|
|
121
|
-
*/
|
|
122
|
-
export function generateScopeId(src: string) {
|
|
123
|
-
// Create a simple hash from the path
|
|
124
|
-
const hash = src
|
|
125
|
-
.replaceAll(/[^a-zA-Z0-9]/g, '')
|
|
126
|
-
.toLowerCase()
|
|
127
|
-
.slice(-8);
|
|
128
|
-
|
|
129
|
-
return `data-v-${hash}`;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Serialize props for client-side hydration
|
|
134
|
-
* @param props - Props object to serialize
|
|
135
|
-
* @returns JSON string safe for HTML attributes
|
|
136
|
-
*/
|
|
137
|
-
export function serializeProps(props: Record<string, unknown>) {
|
|
138
|
-
try {
|
|
139
|
-
return JSON.stringify(props)
|
|
140
|
-
.replaceAll('<', String.raw`\u003c`)
|
|
141
|
-
.replaceAll('>', String.raw`\u003e`)
|
|
142
|
-
.replaceAll('&', String.raw`\u0026`)
|
|
143
|
-
.replaceAll("'", String.raw`\u0027`)
|
|
144
|
-
.replaceAll('"', String.raw`\u0022`);
|
|
145
|
-
} catch (error) {
|
|
146
|
-
console.error('Failed to serialize props:', error);
|
|
147
|
-
return '{}';
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Deserialize props from a JSON string
|
|
153
|
-
* @param propsString - JSON string to deserialize
|
|
154
|
-
* @returns Deserialized props object
|
|
155
|
-
*/
|
|
156
|
-
export function deserializeProps(propsString: string) {
|
|
157
|
-
try {
|
|
158
|
-
return JSON.parse(propsString);
|
|
159
|
-
} catch (error) {
|
|
160
|
-
console.error('Failed to deserialize props:', error);
|
|
161
|
-
return {};
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Create a load context from environment and parameters
|
|
167
|
-
* @param viteServer - Optional Vite dev server
|
|
168
|
-
* @param buildOutput - Optional build output directory
|
|
169
|
-
* @returns Load context object
|
|
170
|
-
*/
|
|
171
|
-
export function createLoadContext(viteServer?: ViteDevServer, buildOutput?: string) {
|
|
172
|
-
const isDev = process.env.NODE_ENV !== 'production';
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
isDev,
|
|
176
|
-
viteServer: isDev ? viteServer : undefined,
|
|
177
|
-
buildOutput: isDev ? undefined : buildOutput,
|
|
178
|
-
} satisfies LoadContext;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Escape HTML special characters in a string
|
|
183
|
-
* @param str - String to escape
|
|
184
|
-
* @returns Escaped string
|
|
185
|
-
*/
|
|
186
|
-
export function escapeHtml(str: string) {
|
|
187
|
-
return str
|
|
188
|
-
.replaceAll('&', '&')
|
|
189
|
-
.replaceAll('<', '<')
|
|
190
|
-
.replaceAll('>', '>')
|
|
191
|
-
.replaceAll('"', '"')
|
|
192
|
-
.replaceAll("'", ''');
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Check if a module has a default export
|
|
197
|
-
* @param module - Module to check
|
|
198
|
-
* @returns True if module has a default export
|
|
199
|
-
*/
|
|
200
|
-
export function hasDefaultExport(module: unknown) {
|
|
201
|
-
return module !== null && typeof module === 'object' && 'default' in module;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Get the component from a module (handles default and named exports)
|
|
206
|
-
* @param module - Module to extract component from
|
|
207
|
-
* @returns The component
|
|
208
|
-
*/
|
|
209
|
-
export function getComponentFromModule(module: Record<string, unknown>) {
|
|
210
|
-
if (hasDefaultExport(module)) {
|
|
211
|
-
return (module as Record<string, unknown>).default;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// If no default export, return the module itself
|
|
215
|
-
// (some frameworks export the component directly)
|
|
216
|
-
return module;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Converts an absolute file path to a valid ESM import specifier.
|
|
221
|
-
* Windows absolute paths (C:\...) are converted to file:// URLs.
|
|
222
|
-
* On Unix, the path is returned as-is (no-op).
|
|
223
|
-
*
|
|
224
|
-
* This ensures dynamic import() calls work cross-platform.
|
|
225
|
-
*
|
|
226
|
-
* @param filePath - Absolute file path to convert
|
|
227
|
-
* @returns A valid ESM import specifier
|
|
228
|
-
*/
|
|
229
|
-
export function toImportSpecifier(filePath: string): string {
|
|
230
|
-
if (/^[A-Za-z]:[\\/]/.test(filePath)) {
|
|
231
|
-
return `file:///${filePath.replaceAll('\\', '/')}`;
|
|
232
|
-
}
|
|
233
|
-
return filePath;
|
|
234
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Common utilities for framework integrations.
|
|
3
|
+
* Provides shared functionality for component loading, path resolution, and more.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ComponentLoadOptions, LoadContext } from './types.ts';
|
|
7
|
+
import type { ViteDevServer } from 'vite';
|
|
8
|
+
import { join, resolve } from 'node:path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Load a component module in development or production
|
|
12
|
+
* @param options - Component load options
|
|
13
|
+
* @returns The loaded component module
|
|
14
|
+
*/
|
|
15
|
+
export async function loadComponent(options: ComponentLoadOptions) {
|
|
16
|
+
const { src, context, target = 'ssr' } = options;
|
|
17
|
+
|
|
18
|
+
if (context.isDev && context.viteServer) {
|
|
19
|
+
return await loadComponentDev(src, context.viteServer);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return await loadComponentProd(src, context.buildOutput, target);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Load a component in development mode using Vite's SSR loader
|
|
27
|
+
* @param src - Source path to component
|
|
28
|
+
* @param viteServer - Vite dev server instance
|
|
29
|
+
* @returns The loaded component module
|
|
30
|
+
*/
|
|
31
|
+
async function loadComponentDev(src: string, viteServer: ViteDevServer) {
|
|
32
|
+
try {
|
|
33
|
+
const module = await viteServer.ssrLoadModule(src);
|
|
34
|
+
return module.default || module;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new Error(`Failed to load component in development: ${src}`, { cause: error });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Load a component in production mode from build output
|
|
42
|
+
* @param src - Source path to component
|
|
43
|
+
* @param buildOutput - Path to build output directory
|
|
44
|
+
* @param target - Whether loading for SSR or client
|
|
45
|
+
* @returns The loaded component module
|
|
46
|
+
*/
|
|
47
|
+
async function loadComponentProd(src: string, buildOutput: string | undefined, target: 'ssr' | 'client') {
|
|
48
|
+
const outputPath = resolveProductionPath(src, buildOutput, target);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const module = await import(/* @vite-ignore */ toImportSpecifier(outputPath));
|
|
52
|
+
return module.default || module;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new Error(`Failed to load component in production: ${outputPath}`, { cause: error });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the production build path for a component
|
|
60
|
+
* @param src - Source path to component
|
|
61
|
+
* @param buildOutput - Path to build output directory
|
|
62
|
+
* @param target - Whether loading for SSR or client
|
|
63
|
+
* @returns Resolved path to built component
|
|
64
|
+
*/
|
|
65
|
+
export function resolveProductionPath(src: string, buildOutput: string | undefined, target: 'ssr' | 'client') {
|
|
66
|
+
const base = buildOutput || 'dist';
|
|
67
|
+
const targetDir = target === 'ssr' ? 'ssr' : 'client';
|
|
68
|
+
|
|
69
|
+
// Convert source path to output path
|
|
70
|
+
// e.g., /islands/Counter.tsx -> /dist/ssr/islands/Counter.js
|
|
71
|
+
const outputPath = src
|
|
72
|
+
.replace(/^\//, '') // Remove leading slash
|
|
73
|
+
.replace(/\.(tsx|jsx|ts|js|vue|svelte)$/, '.js'); // Change extension to .js
|
|
74
|
+
|
|
75
|
+
return join(base, targetDir, outputPath);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Normalize a component path to be absolute
|
|
80
|
+
* @param src - Source path (may be relative or absolute)
|
|
81
|
+
* @param baseDir - Base directory to resolve relative paths from
|
|
82
|
+
* @returns Absolute path
|
|
83
|
+
*/
|
|
84
|
+
export function normalizePath(src: string, baseDir?: string) {
|
|
85
|
+
if (src.startsWith('/') || src.startsWith('file://')) {
|
|
86
|
+
return src;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (baseDir) {
|
|
90
|
+
return resolve(baseDir, src);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return resolve(src);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Extract the file extension from a path
|
|
98
|
+
* @param path - File path
|
|
99
|
+
* @returns File extension (including the dot)
|
|
100
|
+
*/
|
|
101
|
+
export function getExtension(path: string) {
|
|
102
|
+
const match = /\.[^.]+$/.exec(path);
|
|
103
|
+
return match ? match[0] : '';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if a path matches any of the given extensions
|
|
108
|
+
* @param path - File path to check
|
|
109
|
+
* @param extensions - Array of extensions (e.g., [".tsx", ".jsx"])
|
|
110
|
+
* @returns True if path matches any extension
|
|
111
|
+
*/
|
|
112
|
+
export function hasExtension(path: string, extensions: string[]) {
|
|
113
|
+
const ext = getExtension(path);
|
|
114
|
+
return extensions.includes(ext);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate a unique scope ID for a component (useful for CSS scoping)
|
|
119
|
+
* @param src - Source path to component
|
|
120
|
+
* @returns Unique scope identifier
|
|
121
|
+
*/
|
|
122
|
+
export function generateScopeId(src: string) {
|
|
123
|
+
// Create a simple hash from the path
|
|
124
|
+
const hash = src
|
|
125
|
+
.replaceAll(/[^a-zA-Z0-9]/g, '')
|
|
126
|
+
.toLowerCase()
|
|
127
|
+
.slice(-8);
|
|
128
|
+
|
|
129
|
+
return `data-v-${hash}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Serialize props for client-side hydration
|
|
134
|
+
* @param props - Props object to serialize
|
|
135
|
+
* @returns JSON string safe for HTML attributes
|
|
136
|
+
*/
|
|
137
|
+
export function serializeProps(props: Record<string, unknown>) {
|
|
138
|
+
try {
|
|
139
|
+
return JSON.stringify(props)
|
|
140
|
+
.replaceAll('<', String.raw`\u003c`)
|
|
141
|
+
.replaceAll('>', String.raw`\u003e`)
|
|
142
|
+
.replaceAll('&', String.raw`\u0026`)
|
|
143
|
+
.replaceAll("'", String.raw`\u0027`)
|
|
144
|
+
.replaceAll('"', String.raw`\u0022`);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error('Failed to serialize props:', error);
|
|
147
|
+
return '{}';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Deserialize props from a JSON string
|
|
153
|
+
* @param propsString - JSON string to deserialize
|
|
154
|
+
* @returns Deserialized props object
|
|
155
|
+
*/
|
|
156
|
+
export function deserializeProps(propsString: string) {
|
|
157
|
+
try {
|
|
158
|
+
return JSON.parse(propsString);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('Failed to deserialize props:', error);
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create a load context from environment and parameters
|
|
167
|
+
* @param viteServer - Optional Vite dev server
|
|
168
|
+
* @param buildOutput - Optional build output directory
|
|
169
|
+
* @returns Load context object
|
|
170
|
+
*/
|
|
171
|
+
export function createLoadContext(viteServer?: ViteDevServer, buildOutput?: string) {
|
|
172
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
isDev,
|
|
176
|
+
viteServer: isDev ? viteServer : undefined,
|
|
177
|
+
buildOutput: isDev ? undefined : buildOutput,
|
|
178
|
+
} satisfies LoadContext;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Escape HTML special characters in a string
|
|
183
|
+
* @param str - String to escape
|
|
184
|
+
* @returns Escaped string
|
|
185
|
+
*/
|
|
186
|
+
export function escapeHtml(str: string) {
|
|
187
|
+
return str
|
|
188
|
+
.replaceAll('&', '&')
|
|
189
|
+
.replaceAll('<', '<')
|
|
190
|
+
.replaceAll('>', '>')
|
|
191
|
+
.replaceAll('"', '"')
|
|
192
|
+
.replaceAll("'", ''');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Check if a module has a default export
|
|
197
|
+
* @param module - Module to check
|
|
198
|
+
* @returns True if module has a default export
|
|
199
|
+
*/
|
|
200
|
+
export function hasDefaultExport(module: unknown) {
|
|
201
|
+
return module !== null && typeof module === 'object' && 'default' in module;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get the component from a module (handles default and named exports)
|
|
206
|
+
* @param module - Module to extract component from
|
|
207
|
+
* @returns The component
|
|
208
|
+
*/
|
|
209
|
+
export function getComponentFromModule(module: Record<string, unknown>) {
|
|
210
|
+
if (hasDefaultExport(module)) {
|
|
211
|
+
return (module as Record<string, unknown>).default;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// If no default export, return the module itself
|
|
215
|
+
// (some frameworks export the component directly)
|
|
216
|
+
return module;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Converts an absolute file path to a valid ESM import specifier.
|
|
221
|
+
* Windows absolute paths (C:\...) are converted to file:// URLs.
|
|
222
|
+
* On Unix, the path is returned as-is (no-op).
|
|
223
|
+
*
|
|
224
|
+
* This ensures dynamic import() calls work cross-platform.
|
|
225
|
+
*
|
|
226
|
+
* @param filePath - Absolute file path to convert
|
|
227
|
+
* @returns A valid ESM import specifier
|
|
228
|
+
*/
|
|
229
|
+
export function toImportSpecifier(filePath: string): string {
|
|
230
|
+
if (/^[A-Za-z]:[\\/]/.test(filePath)) {
|
|
231
|
+
return `file:///${filePath.replaceAll('\\', '/')}`;
|
|
232
|
+
}
|
|
233
|
+
return filePath;
|
|
234
|
+
}
|
package/vitest.config.ts
DELETED