@useavalon/avalon 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/dist/mod.js +1 -0
  2. package/dist/src/build/integration-bundler-plugin.js +1 -0
  3. package/dist/src/build/integration-config.js +1 -0
  4. package/dist/src/build/integration-detection-plugin.js +1 -0
  5. package/dist/src/build/integration-resolver-plugin.js +1 -0
  6. package/dist/src/build/island-manifest.js +1 -0
  7. package/dist/src/build/island-types-generator.js +5 -0
  8. package/dist/src/build/mdx-island-transform.js +2 -0
  9. package/dist/src/build/mdx-plugin.js +1 -0
  10. package/dist/src/build/page-island-transform.js +3 -0
  11. package/dist/src/build/prop-extractors/index.js +1 -0
  12. package/dist/src/build/prop-extractors/lit.js +1 -0
  13. package/dist/src/build/prop-extractors/qwik.js +1 -0
  14. package/dist/src/build/prop-extractors/solid.js +1 -0
  15. package/dist/src/build/prop-extractors/svelte.js +1 -0
  16. package/dist/src/build/prop-extractors/vue.js +1 -0
  17. package/dist/src/build/sidecar-file-manager.js +1 -0
  18. package/dist/src/build/sidecar-renderer.js +6 -0
  19. package/dist/src/client/adapters/index.js +1 -0
  20. package/dist/src/client/components.js +1 -0
  21. package/dist/src/client/css-hmr-handler.js +1 -0
  22. package/dist/src/client/framework-adapter.js +13 -0
  23. package/dist/src/client/hmr-coordinator.js +1 -0
  24. package/dist/src/client/hmr-error-overlay.js +214 -0
  25. package/dist/src/client/main.js +39 -0
  26. package/dist/src/components/Image.js +1 -0
  27. package/dist/src/components/IslandErrorBoundary.js +1 -0
  28. package/dist/src/components/LayoutDataErrorBoundary.js +1 -0
  29. package/dist/src/components/LayoutErrorBoundary.js +1 -0
  30. package/dist/src/components/PersistentIsland.js +1 -0
  31. package/dist/src/components/StreamingErrorBoundary.js +1 -0
  32. package/dist/src/components/StreamingLayout.js +29 -0
  33. package/dist/src/core/components/component-analyzer.js +1 -0
  34. package/dist/src/core/components/component-detection.js +5 -0
  35. package/dist/src/core/components/enhanced-framework-detector.js +1 -0
  36. package/dist/src/core/components/framework-registry.js +1 -0
  37. package/dist/src/core/content/mdx-processor.js +1 -0
  38. package/dist/src/core/integrations/index.js +1 -0
  39. package/dist/src/core/integrations/loader.js +1 -0
  40. package/dist/src/core/integrations/registry.js +1 -0
  41. package/dist/src/core/islands/island-persistence.js +1 -0
  42. package/dist/src/core/islands/island-state-serializer.js +1 -0
  43. package/dist/src/core/islands/persistent-island-context.js +1 -0
  44. package/dist/src/core/islands/use-persistent-state.js +1 -0
  45. package/dist/src/core/layout/enhanced-layout-resolver.js +1 -0
  46. package/dist/src/core/layout/layout-cache-manager.js +1 -0
  47. package/dist/src/core/layout/layout-composer.js +1 -0
  48. package/dist/src/core/layout/layout-data-loader.js +1 -0
  49. package/dist/src/core/layout/layout-discovery.js +1 -0
  50. package/dist/src/core/layout/layout-matcher.js +1 -0
  51. package/dist/src/core/layout/layout-types.js +1 -0
  52. package/dist/src/core/modules/framework-module-resolver.js +1 -0
  53. package/dist/src/islands/component-analysis.js +1 -0
  54. package/dist/src/islands/css-utils.js +17 -0
  55. package/dist/src/islands/discovery/index.js +1 -0
  56. package/dist/src/islands/discovery/registry.js +1 -0
  57. package/dist/src/islands/discovery/resolver.js +2 -0
  58. package/dist/src/islands/discovery/scanner.js +1 -0
  59. package/dist/src/islands/discovery/types.js +1 -0
  60. package/dist/src/islands/discovery/validator.js +18 -0
  61. package/dist/src/islands/discovery/watcher.js +1 -0
  62. package/dist/src/islands/framework-detection.js +1 -0
  63. package/dist/src/islands/integration-loader.js +1 -0
  64. package/dist/src/islands/island.js +1 -0
  65. package/dist/src/islands/render-cache.js +1 -0
  66. package/dist/src/islands/types.js +1 -0
  67. package/dist/src/islands/universal-css-collector.js +5 -0
  68. package/dist/src/islands/universal-head-collector.js +2 -0
  69. package/dist/src/layout-system.js +1 -0
  70. package/dist/src/middleware/discovery.js +1 -0
  71. package/dist/src/middleware/executor.js +1 -0
  72. package/dist/src/middleware/index.js +1 -0
  73. package/dist/src/middleware/types.js +1 -0
  74. package/dist/src/nitro/build-config.js +1 -0
  75. package/dist/src/nitro/config.js +1 -0
  76. package/dist/src/nitro/error-handler.js +198 -0
  77. package/dist/src/nitro/index.js +1 -0
  78. package/dist/src/nitro/island-manifest.js +2 -0
  79. package/dist/src/nitro/middleware-adapter.js +1 -0
  80. package/dist/src/nitro/renderer.js +183 -0
  81. package/dist/src/nitro/route-discovery.js +1 -0
  82. package/dist/src/nitro/types.js +1 -0
  83. package/dist/src/render/collect-css.js +3 -0
  84. package/dist/src/render/error-pages.js +48 -0
  85. package/dist/src/render/isolated-ssr-renderer.js +1 -0
  86. package/dist/src/render/ssr.js +90 -0
  87. package/dist/src/schemas/api.js +1 -0
  88. package/dist/src/schemas/core.js +1 -0
  89. package/dist/src/schemas/index.js +1 -0
  90. package/dist/src/schemas/layout.js +1 -0
  91. package/dist/src/schemas/routing/index.js +1 -0
  92. package/dist/src/schemas/routing.js +1 -0
  93. package/dist/src/types/as-island.js +1 -0
  94. package/dist/src/types/layout.js +1 -0
  95. package/dist/src/types/routing.js +1 -0
  96. package/dist/src/types/types.js +1 -0
  97. package/dist/src/utils/dev-logger.js +12 -0
  98. package/dist/src/utils/fs.js +1 -0
  99. package/dist/src/vite-plugin/auto-discover.js +1 -0
  100. package/dist/src/vite-plugin/config.js +1 -0
  101. package/dist/src/vite-plugin/errors.js +1 -0
  102. package/dist/src/vite-plugin/image-optimization.js +45 -0
  103. package/dist/src/vite-plugin/integration-activator.js +1 -0
  104. package/dist/src/vite-plugin/island-sidecar-plugin.js +1 -0
  105. package/dist/src/vite-plugin/module-discovery.js +1 -0
  106. package/dist/src/vite-plugin/nitro-integration.js +42 -0
  107. package/dist/src/vite-plugin/plugin.js +1 -0
  108. package/dist/src/vite-plugin/types.js +1 -0
  109. package/dist/src/vite-plugin/validation.js +2 -0
  110. package/package.json +14 -20
  111. package/mod.ts +0 -302
  112. package/src/build/integration-bundler-plugin.ts +0 -116
  113. package/src/build/integration-config.ts +0 -168
  114. package/src/build/integration-detection-plugin.ts +0 -117
  115. package/src/build/integration-resolver-plugin.ts +0 -90
  116. package/src/build/island-manifest.ts +0 -269
  117. package/src/build/island-types-generator.ts +0 -476
  118. package/src/build/mdx-island-transform.ts +0 -464
  119. package/src/build/mdx-plugin.ts +0 -98
  120. package/src/build/page-island-transform.ts +0 -598
  121. package/src/build/prop-extractors/index.ts +0 -21
  122. package/src/build/prop-extractors/lit.ts +0 -140
  123. package/src/build/prop-extractors/qwik.ts +0 -16
  124. package/src/build/prop-extractors/solid.ts +0 -125
  125. package/src/build/prop-extractors/svelte.ts +0 -194
  126. package/src/build/prop-extractors/vue.ts +0 -111
  127. package/src/build/sidecar-file-manager.ts +0 -104
  128. package/src/build/sidecar-renderer.ts +0 -30
  129. package/src/client/adapters/index.ts +0 -21
  130. package/src/client/components.ts +0 -35
  131. package/src/client/css-hmr-handler.ts +0 -344
  132. package/src/client/framework-adapter.ts +0 -462
  133. package/src/client/hmr-coordinator.ts +0 -396
  134. package/src/client/hmr-error-overlay.js +0 -533
  135. package/src/client/main.js +0 -824
  136. package/src/components/Image.tsx +0 -123
  137. package/src/components/IslandErrorBoundary.tsx +0 -145
  138. package/src/components/LayoutDataErrorBoundary.tsx +0 -141
  139. package/src/components/LayoutErrorBoundary.tsx +0 -127
  140. package/src/components/PersistentIsland.tsx +0 -52
  141. package/src/components/StreamingErrorBoundary.tsx +0 -233
  142. package/src/components/StreamingLayout.tsx +0 -538
  143. package/src/core/components/component-analyzer.ts +0 -192
  144. package/src/core/components/component-detection.ts +0 -508
  145. package/src/core/components/enhanced-framework-detector.ts +0 -500
  146. package/src/core/components/framework-registry.ts +0 -563
  147. package/src/core/content/mdx-processor.ts +0 -46
  148. package/src/core/integrations/index.ts +0 -19
  149. package/src/core/integrations/loader.ts +0 -125
  150. package/src/core/integrations/registry.ts +0 -175
  151. package/src/core/islands/island-persistence.ts +0 -325
  152. package/src/core/islands/island-state-serializer.ts +0 -258
  153. package/src/core/islands/persistent-island-context.tsx +0 -80
  154. package/src/core/islands/use-persistent-state.ts +0 -68
  155. package/src/core/layout/enhanced-layout-resolver.ts +0 -322
  156. package/src/core/layout/layout-cache-manager.ts +0 -485
  157. package/src/core/layout/layout-composer.ts +0 -357
  158. package/src/core/layout/layout-data-loader.ts +0 -516
  159. package/src/core/layout/layout-discovery.ts +0 -243
  160. package/src/core/layout/layout-matcher.ts +0 -299
  161. package/src/core/layout/layout-types.ts +0 -110
  162. package/src/core/modules/framework-module-resolver.ts +0 -273
  163. package/src/islands/component-analysis.ts +0 -213
  164. package/src/islands/css-utils.ts +0 -565
  165. package/src/islands/discovery/index.ts +0 -80
  166. package/src/islands/discovery/registry.ts +0 -340
  167. package/src/islands/discovery/resolver.ts +0 -477
  168. package/src/islands/discovery/scanner.ts +0 -386
  169. package/src/islands/discovery/types.ts +0 -117
  170. package/src/islands/discovery/validator.ts +0 -544
  171. package/src/islands/discovery/watcher.ts +0 -368
  172. package/src/islands/framework-detection.ts +0 -428
  173. package/src/islands/integration-loader.ts +0 -490
  174. package/src/islands/island.tsx +0 -565
  175. package/src/islands/render-cache.ts +0 -550
  176. package/src/islands/types.ts +0 -80
  177. package/src/islands/universal-css-collector.ts +0 -157
  178. package/src/islands/universal-head-collector.ts +0 -137
  179. package/src/layout-system.ts +0 -218
  180. package/src/middleware/discovery.ts +0 -268
  181. package/src/middleware/executor.ts +0 -315
  182. package/src/middleware/index.ts +0 -76
  183. package/src/middleware/types.ts +0 -99
  184. package/src/nitro/build-config.ts +0 -576
  185. package/src/nitro/config.ts +0 -483
  186. package/src/nitro/error-handler.ts +0 -636
  187. package/src/nitro/index.ts +0 -173
  188. package/src/nitro/island-manifest.ts +0 -584
  189. package/src/nitro/middleware-adapter.ts +0 -260
  190. package/src/nitro/renderer.ts +0 -1471
  191. package/src/nitro/route-discovery.ts +0 -439
  192. package/src/nitro/types.ts +0 -321
  193. package/src/render/collect-css.ts +0 -198
  194. package/src/render/error-pages.ts +0 -79
  195. package/src/render/isolated-ssr-renderer.ts +0 -654
  196. package/src/render/ssr.ts +0 -1030
  197. package/src/schemas/api.ts +0 -30
  198. package/src/schemas/core.ts +0 -64
  199. package/src/schemas/index.ts +0 -212
  200. package/src/schemas/layout.ts +0 -279
  201. package/src/schemas/routing/index.ts +0 -38
  202. package/src/schemas/routing.ts +0 -376
  203. package/src/types/as-island.ts +0 -20
  204. package/src/types/layout.ts +0 -285
  205. package/src/types/routing.ts +0 -555
  206. package/src/types/types.ts +0 -5
  207. package/src/utils/dev-logger.ts +0 -299
  208. package/src/utils/fs.ts +0 -151
  209. package/src/vite-plugin/auto-discover.ts +0 -551
  210. package/src/vite-plugin/config.ts +0 -266
  211. package/src/vite-plugin/errors.ts +0 -127
  212. package/src/vite-plugin/image-optimization.ts +0 -156
  213. package/src/vite-plugin/integration-activator.ts +0 -126
  214. package/src/vite-plugin/island-sidecar-plugin.ts +0 -176
  215. package/src/vite-plugin/module-discovery.ts +0 -189
  216. package/src/vite-plugin/nitro-integration.ts +0 -1354
  217. package/src/vite-plugin/plugin.ts +0 -403
  218. package/src/vite-plugin/types.ts +0 -327
  219. package/src/vite-plugin/validation.ts +0 -228
  220. /package/{src → dist/src}/client/types/framework-runtime.d.ts +0 -0
  221. /package/{src → dist/src}/client/types/vite-hmr.d.ts +0 -0
  222. /package/{src → dist/src}/client/types/vite-virtual-modules.d.ts +0 -0
  223. /package/{src → dist/src}/layout-system.d.ts +0 -0
  224. /package/{src → dist/src}/types/image.d.ts +0 -0
  225. /package/{src → dist/src}/types/index.d.ts +0 -0
  226. /package/{src → dist/src}/types/island-jsx.d.ts +0 -0
  227. /package/{src → dist/src}/types/island-prop.d.ts +0 -0
  228. /package/{src → dist/src}/types/mdx.d.ts +0 -0
  229. /package/{src → dist/src}/types/urlpattern.d.ts +0 -0
  230. /package/{src → dist/src}/types/vite-env.d.ts +0 -0
@@ -1,544 +0,0 @@
1
- /**
2
- * Island Validator
3
- *
4
- * Validates island components and directory structure.
5
- * Provides validation for exports, naming conventions, and circular dependencies.
6
- */
7
-
8
- import { resolve, relative, basename, extname } from "node:path";
9
- import { readFile, stat as fsStat, readdir } from "node:fs/promises";
10
- import type {
11
- IslandDirectory,
12
- DiscoveredIsland,
13
- } from "./types.ts";
14
- import { isSupportedIslandExtension } from "./types.ts";
15
-
16
- /**
17
- * Result of validating an island or directory
18
- */
19
- export interface ValidationResult {
20
- /** Whether validation passed (no errors) */
21
- valid: boolean;
22
- /** Validation errors (failures) */
23
- errors: ValidationError[];
24
- /** Validation warnings (non-fatal issues) */
25
- warnings: ValidationWarning[];
26
- }
27
-
28
- /**
29
- * A validation error (causes validation to fail)
30
- */
31
- export interface ValidationError {
32
- /** Type of error */
33
- type: "invalid-export" | "circular-dependency" | "naming-convention";
34
- /** Human-readable error message */
35
- message: string;
36
- /** Absolute file path where error occurred */
37
- filePath: string;
38
- /** Line number (1-indexed) if applicable */
39
- line?: number;
40
- /** Column number (1-indexed) if applicable */
41
- column?: number;
42
- /** Suggested fix for the error */
43
- suggestion?: string;
44
- }
45
-
46
- /**
47
- * A validation warning (non-fatal issue)
48
- */
49
- export interface ValidationWarning {
50
- /** Type of warning */
51
- type: "empty-directory" | "naming-collision" | "deprecated-pattern";
52
- /** Human-readable warning message */
53
- message: string;
54
- /** File path if applicable */
55
- filePath?: string;
56
- /** Suggested fix for the warning */
57
- suggestion?: string;
58
- }
59
-
60
- /**
61
- * Represents a circular dependency chain
62
- */
63
- export interface CircularDependency {
64
- /** The cycle as an array of file paths */
65
- cycle: string[];
66
- /** Human-readable description of the cycle */
67
- description: string;
68
- }
69
-
70
- const NAMING_PATTERNS = {
71
- pascalCase: /^[A-Z][a-zA-Z0-9]*$/,
72
- validFileName: /^[a-zA-Z][a-zA-Z0-9._-]*$/,
73
- frameworkSuffixes: [".solid", ".react", ".lit", ".preact"],
74
- };
75
-
76
- const DOCS_URL = "https://avalon.dev/docs/islands";
77
-
78
-
79
- /**
80
- * Island Validator class
81
- */
82
- export class IslandValidator {
83
- private _projectRoot: string;
84
-
85
- constructor(projectRoot: string) {
86
- this._projectRoot = projectRoot;
87
- }
88
-
89
- get projectRoot(): string {
90
- return this._projectRoot;
91
- }
92
-
93
- private extractComponentName(filePath: string): string {
94
- const fileName = basename(filePath);
95
- for (const suffix of NAMING_PATTERNS.frameworkSuffixes) {
96
- if (fileName.includes(suffix)) {
97
- const idx = fileName.indexOf(suffix);
98
- return fileName.slice(0, idx);
99
- }
100
- }
101
- const ext = extname(fileName);
102
- return fileName.slice(0, -ext.length);
103
- }
104
-
105
- private toPascalCase(str: string): string {
106
- return str
107
- .split(/[-_\s]+/)
108
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
109
- .join("");
110
- }
111
-
112
- private hasValidJsExport(content: string): boolean {
113
- const hasDefaultExport =
114
- /export\s+default\s+/.test(content) ||
115
- /export\s*\{\s*[^}]*\s+as\s+default\s*[,}]/.test(content);
116
- const hasNamedComponentExport =
117
- /export\s+(function|class|const)\s+[A-Z]/.test(content);
118
- const hasLitElement =
119
- /@customElement\s*\(/.test(content) ||
120
- /customElements\.define\s*\(/.test(content);
121
- return hasDefaultExport || hasNamedComponentExport || hasLitElement;
122
- }
123
-
124
- private isValidVueComponent(content: string): boolean {
125
- return /<template[\s>]/.test(content) || /<script[\s>]/.test(content);
126
- }
127
-
128
- private isValidSvelteComponent(content: string): boolean {
129
- return content.trim().length > 0;
130
- }
131
-
132
- private createExportError(filePath: string, type: "js" | "vue" | "svelte"): ValidationError {
133
- const componentName = this.extractComponentName(filePath);
134
- let suggestion: string;
135
- switch (type) {
136
- case "vue":
137
- suggestion = "Add a <template> or <script> section to your Vue component";
138
- break;
139
- case "svelte":
140
- suggestion = "Add component markup to your Svelte file";
141
- break;
142
- default:
143
- suggestion = `Add a default export: export default function ${componentName}() { return <div>...</div>; }`;
144
- }
145
- return {
146
- type: "invalid-export",
147
- message: `Island component "${componentName}" does not export a valid component`,
148
- filePath,
149
- line: 1,
150
- column: 1,
151
- suggestion: `${suggestion}\n\nSee: ${DOCS_URL}#component-exports`,
152
- };
153
- }
154
-
155
- private async validateExports(filePath: string): Promise<ValidationResult> {
156
- const errors: ValidationError[] = [];
157
- const warnings: ValidationWarning[] = [];
158
- try {
159
- const content = await readFile(filePath, 'utf-8');
160
- const ext = extname(filePath).toLowerCase();
161
- if (ext === ".vue") {
162
- if (!this.isValidVueComponent(content)) {
163
- errors.push(this.createExportError(filePath, "vue"));
164
- }
165
- } else if (ext === ".svelte") {
166
- if (!this.isValidSvelteComponent(content)) {
167
- errors.push(this.createExportError(filePath, "svelte"));
168
- }
169
- } else {
170
- if (!this.hasValidJsExport(content)) {
171
- errors.push(this.createExportError(filePath, "js"));
172
- }
173
- }
174
- } catch {
175
- errors.push({
176
- type: "invalid-export",
177
- message: `Cannot read file: ${filePath}`,
178
- filePath,
179
- suggestion: "Check file permissions and encoding",
180
- });
181
- }
182
- return { valid: errors.length === 0, errors, warnings };
183
- }
184
-
185
- private extractJsImports(content: string): string[] {
186
- const imports: string[] = [];
187
- const es6ImportRegex = /import\s+(?:[\w\s{},*]+\s+from\s+)?['"]([^'"]+)['"]/g;
188
- let match;
189
- while ((match = es6ImportRegex.exec(content)) !== null) {
190
- imports.push(match[1]);
191
- }
192
- const dynamicImportRegex = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
193
- while ((match = dynamicImportRegex.exec(content)) !== null) {
194
- imports.push(match[1]);
195
- }
196
- const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
197
- while ((match = requireRegex.exec(content)) !== null) {
198
- imports.push(match[1]);
199
- }
200
- return imports;
201
- }
202
-
203
- private extractImports(content: string, filePath: string): string[] {
204
- const imports: string[] = [];
205
- const ext = extname(filePath).toLowerCase();
206
- if (ext === ".vue" || ext === ".svelte") {
207
- const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/gi);
208
- if (scriptMatch) {
209
- for (const script of scriptMatch) {
210
- imports.push(...this.extractJsImports(script));
211
- }
212
- }
213
- } else {
214
- imports.push(...this.extractJsImports(content));
215
- }
216
- return imports;
217
- }
218
-
219
- private resolveImportToIsland(
220
- importPath: string,
221
- fromIsland: DiscoveredIsland,
222
- islandByName: Map<string, DiscoveredIsland>,
223
- islandPaths: Set<string>
224
- ): DiscoveredIsland | null {
225
- if (!importPath.startsWith(".") && !importPath.startsWith("/")) {
226
- return null;
227
- }
228
- if (importPath.startsWith(".")) {
229
- const fromDir = resolve(fromIsland.filePath, "..");
230
- const resolvedPath = resolve(fromDir, importPath);
231
- if (!extname(resolvedPath)) {
232
- const extensions = [".tsx", ".ts", ".jsx", ".js", ".vue", ".svelte"];
233
- for (const ext of extensions) {
234
- if (islandPaths.has(resolvedPath + ext)) {
235
- for (const island of islandByName.values()) {
236
- if (island.filePath === resolvedPath + ext) {
237
- return island;
238
- }
239
- }
240
- }
241
- }
242
- }
243
- if (islandPaths.has(resolvedPath)) {
244
- for (const island of islandByName.values()) {
245
- if (island.filePath === resolvedPath) {
246
- return island;
247
- }
248
- }
249
- }
250
- }
251
- const componentName = basename(importPath).replace(/\.[^.]+$/, "");
252
- return islandByName.get(componentName) || null;
253
- }
254
-
255
- private async buildImportGraph(islands: DiscoveredIsland[]): Promise<Map<string, string[]>> {
256
- const graph = new Map<string, string[]>();
257
- const islandPaths = new Set(islands.map(i => i.filePath));
258
- const islandByName = new Map<string, DiscoveredIsland>();
259
- for (const island of islands) {
260
- islandByName.set(island.name, island);
261
- const relPath = island.relativePath.replace(/\.[^.]+$/, "");
262
- islandByName.set(relPath, island);
263
- }
264
- for (const island of islands) {
265
- const imports: string[] = [];
266
- try {
267
- const content = await readFile(island.filePath, 'utf-8');
268
- const importedPaths = this.extractImports(content, island.filePath);
269
- for (const importPath of importedPaths) {
270
- const resolvedIsland = this.resolveImportToIsland(importPath, island, islandByName, islandPaths);
271
- if (resolvedIsland) {
272
- imports.push(resolvedIsland.filePath);
273
- }
274
- }
275
- } catch {
276
- // Skip files that can't be read
277
- }
278
- graph.set(island.filePath, imports);
279
- }
280
- return graph;
281
- }
282
-
283
- private cycleExists(cycles: string[][], newCycle: string[]): boolean {
284
- const newSet = new Set(newCycle);
285
- for (const existing of cycles) {
286
- if (existing.length !== newCycle.length - 1) continue;
287
- const existingSet = new Set(existing);
288
- let allMatch = true;
289
- for (const node of newSet) {
290
- if (!existingSet.has(node)) {
291
- allMatch = false;
292
- break;
293
- }
294
- }
295
- if (allMatch) return true;
296
- }
297
- return false;
298
- }
299
-
300
- private findCycles(graph: Map<string, string[]>): string[][] {
301
- const cycles: string[][] = [];
302
- const visited = new Set<string>();
303
- const recursionStack = new Set<string>();
304
- const path: string[] = [];
305
- const dfs = (node: string): void => {
306
- visited.add(node);
307
- recursionStack.add(node);
308
- path.push(node);
309
- const neighbors = graph.get(node) || [];
310
- for (const neighbor of neighbors) {
311
- if (!visited.has(neighbor)) {
312
- dfs(neighbor);
313
- } else if (recursionStack.has(neighbor)) {
314
- const cycleStart = path.indexOf(neighbor);
315
- if (cycleStart !== -1) {
316
- const cycle = [...path.slice(cycleStart), neighbor];
317
- if (!this.cycleExists(cycles, cycle)) {
318
- cycles.push(cycle);
319
- }
320
- }
321
- }
322
- }
323
- path.pop();
324
- recursionStack.delete(node);
325
- };
326
- for (const node of graph.keys()) {
327
- if (!visited.has(node)) {
328
- dfs(node);
329
- }
330
- }
331
- return cycles;
332
- }
333
-
334
- private formatCycleDescription(cycle: string[]): string {
335
- const relativePaths = cycle.map(p => relative(this._projectRoot, p));
336
- return `Circular dependency detected:\n ${relativePaths.join("\n → ")}`;
337
- }
338
-
339
-
340
- async validateComponent(filePath: string): Promise<ValidationResult> {
341
- const errors: ValidationError[] = [];
342
- const warnings: ValidationWarning[] = [];
343
- try {
344
- const statResult = await fsStat(filePath);
345
- if (!statResult.isFile()) {
346
- errors.push({ type: "invalid-export", message: `Path is not a file: ${filePath}`, filePath });
347
- return { valid: false, errors, warnings };
348
- }
349
- } catch {
350
- errors.push({ type: "invalid-export", message: `File not found: ${filePath}`, filePath });
351
- return { valid: false, errors, warnings };
352
- }
353
- const ext = extname(filePath);
354
- if (!isSupportedIslandExtension(ext)) {
355
- errors.push({
356
- type: "invalid-export",
357
- message: `Unsupported file extension: ${ext}`,
358
- filePath,
359
- suggestion: `Use one of: .tsx, .ts, .jsx, .js, .vue, .svelte`,
360
- });
361
- return { valid: false, errors, warnings };
362
- }
363
- const namingResult = this.validateNamingConvention(this.extractComponentName(filePath), filePath);
364
- errors.push(...namingResult.errors);
365
- warnings.push(...namingResult.warnings);
366
- const exportResult = await this.validateExports(filePath);
367
- errors.push(...exportResult.errors);
368
- warnings.push(...exportResult.warnings);
369
- return { valid: errors.length === 0, errors, warnings };
370
- }
371
-
372
- async validateDirectory(directory: IslandDirectory): Promise<ValidationResult> {
373
- const errors: ValidationError[] = [];
374
- const warnings: ValidationWarning[] = [];
375
- let hasIslands = false;
376
- try {
377
- const entries = await readdir(directory.path, { withFileTypes: true });
378
- for (const entry of entries) {
379
- if (!entry.isFile()) continue;
380
- const ext = extname(entry.name);
381
- if (!isSupportedIslandExtension(ext)) continue;
382
- hasIslands = true;
383
- const filePath = resolve(directory.path, entry.name);
384
- const result = await this.validateComponent(filePath);
385
- errors.push(...result.errors);
386
- warnings.push(...result.warnings);
387
- }
388
- } catch {
389
- errors.push({
390
- type: "invalid-export",
391
- message: `Cannot read directory: ${directory.path}`,
392
- filePath: directory.path,
393
- suggestion: "Check directory permissions",
394
- });
395
- return { valid: false, errors, warnings };
396
- }
397
- if (!hasIslands) {
398
- warnings.push({
399
- type: "empty-directory",
400
- message: `Islands directory is empty: ${directory.relativePath}`,
401
- filePath: directory.path,
402
- suggestion: "Add island components or remove the empty directory",
403
- });
404
- }
405
- return { valid: errors.length === 0, errors, warnings };
406
- }
407
-
408
- validateNamingConvention(name: string, filePath: string): ValidationResult {
409
- const errors: ValidationError[] = [];
410
- const warnings: ValidationWarning[] = [];
411
- if (!NAMING_PATTERNS.pascalCase.test(name)) {
412
- if (name.length === 0) {
413
- errors.push({
414
- type: "naming-convention",
415
- message: `Invalid component name: empty name`,
416
- filePath,
417
- suggestion: `Use PascalCase naming (e.g., "Counter", "UserProfile")`,
418
- });
419
- } else if (/^[a-z]/.test(name)) {
420
- warnings.push({
421
- type: "deprecated-pattern",
422
- message: `Component name "${name}" should use PascalCase`,
423
- filePath,
424
- suggestion: `Rename to "${this.toPascalCase(name)}"`,
425
- });
426
- } else if (/[^a-zA-Z0-9]/.test(name)) {
427
- warnings.push({
428
- type: "deprecated-pattern",
429
- message: `Component name "${name}" contains special characters`,
430
- filePath,
431
- suggestion: `Use only letters and numbers in component names`,
432
- });
433
- }
434
- }
435
- return { valid: errors.length === 0, errors, warnings };
436
- }
437
-
438
- async detectCircularDependencies(islands: DiscoveredIsland[]): Promise<CircularDependency[]> {
439
- const graph = await this.buildImportGraph(islands);
440
- const cycles = this.findCycles(graph);
441
- return cycles.map((cycle: string[]) => ({
442
- cycle,
443
- description: this.formatCycleDescription(cycle),
444
- }));
445
- }
446
- }
447
-
448
-
449
- export function formatValidationError(error: ValidationError, projectRoot: string): string {
450
- const relativePath = relative(projectRoot, error.filePath);
451
- const location = error.line
452
- ? `${relativePath}:${error.line}${error.column ? `:${error.column}` : ""}`
453
- : relativePath;
454
- let output = `Error: ${error.message}\n\n`;
455
- output += ` File: ${location}\n`;
456
- if (error.suggestion) {
457
- output += `\n ${error.suggestion}\n`;
458
- }
459
- return output;
460
- }
461
-
462
- export function formatValidationWarning(warning: ValidationWarning, projectRoot: string): string {
463
- let output = `Warning: ${warning.message}\n`;
464
- if (warning.filePath) {
465
- const relativePath = relative(projectRoot, warning.filePath);
466
- output += ` File: ${relativePath}\n`;
467
- }
468
- if (warning.suggestion) {
469
- output += ` Suggestion: ${warning.suggestion}\n`;
470
- }
471
- return output;
472
- }
473
-
474
- export function formatCircularDependency(circular: CircularDependency, projectRoot: string): string {
475
- const relativePaths = circular.cycle.map(p => relative(projectRoot, p));
476
- let output = `Error: Circular dependency detected\n\n`;
477
- output += ` Dependency chain:\n`;
478
- for (let i = 0; i < relativePaths.length; i++) {
479
- const isLast = i === relativePaths.length - 1;
480
- const prefix = isLast ? " └─" : " ├─";
481
- output += `${prefix} ${relativePaths[i]}\n`;
482
- if (!isLast) {
483
- output += ` │ ↓\n`;
484
- }
485
- }
486
- output += `\n Suggestion: Break the cycle by:\n`;
487
- output += ` - Moving shared code to a separate module\n`;
488
- output += ` - Using dynamic imports for one of the dependencies\n`;
489
- output += ` - Restructuring the component hierarchy\n`;
490
- output += `\n See: ${DOCS_URL}#circular-dependencies\n`;
491
- return output;
492
- }
493
-
494
- export function formatValidationResult(result: ValidationResult, projectRoot: string): string {
495
- const parts: string[] = [];
496
- if (result.errors.length > 0) {
497
- parts.push(`Found ${result.errors.length} error(s):\n`);
498
- for (const error of result.errors) {
499
- parts.push(formatValidationError(error, projectRoot));
500
- }
501
- }
502
- if (result.warnings.length > 0) {
503
- if (parts.length > 0) parts.push("\n");
504
- parts.push(`Found ${result.warnings.length} warning(s):\n`);
505
- for (const warning of result.warnings) {
506
- parts.push(formatValidationWarning(warning, projectRoot));
507
- }
508
- }
509
- if (result.valid && result.warnings.length === 0) {
510
- parts.push("✓ Validation passed\n");
511
- } else if (result.valid) {
512
- parts.push("\n✓ Validation passed with warnings\n");
513
- } else {
514
- parts.push("\n✗ Validation failed\n");
515
- }
516
- return parts.join("\n");
517
- }
518
-
519
- export function createIslandValidator(projectRoot: string): IslandValidator {
520
- return new IslandValidator(projectRoot);
521
- }
522
-
523
- export async function validateAllIslands(
524
- islands: DiscoveredIsland[],
525
- projectRoot: string
526
- ): Promise<ValidationResult> {
527
- const validator = createIslandValidator(projectRoot);
528
- const errors: ValidationError[] = [];
529
- const warnings: ValidationWarning[] = [];
530
- for (const island of islands) {
531
- const result = await validator.validateComponent(island.filePath);
532
- errors.push(...result.errors);
533
- warnings.push(...result.warnings);
534
- }
535
- const circularDeps = await validator.detectCircularDependencies(islands);
536
- for (const circular of circularDeps) {
537
- errors.push({
538
- type: "circular-dependency",
539
- message: circular.description,
540
- filePath: circular.cycle[0],
541
- });
542
- }
543
- return { valid: errors.length === 0, errors, warnings };
544
- }