@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.
@@ -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.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": ["avalon", "islands", "framework", "core"],
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": ["*.ts", "README.md"],
21
- "dependencies": {
22
- "vite": "8.0.0"
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('&', '&amp;')
189
- .replaceAll('<', '&lt;')
190
- .replaceAll('>', '&gt;')
191
- .replaceAll('"', '&quot;')
192
- .replaceAll("'", '&#39;');
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('&', '&amp;')
189
+ .replaceAll('<', '&lt;')
190
+ .replaceAll('>', '&gt;')
191
+ .replaceAll('"', '&quot;')
192
+ .replaceAll("'", '&#39;');
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
@@ -1,13 +0,0 @@
1
- import { defineConfig } from 'vitest/config';
2
-
3
- export default defineConfig({
4
- test: {
5
- root: import.meta.dirname,
6
- include: ['__tests__/**/*.test.ts'],
7
- server: {
8
- deps: {
9
- inline: ['zod'],
10
- },
11
- },
12
- },
13
- });