@useavalon/avalon 0.1.13 → 0.1.15
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/dist/mod.js +1 -0
- package/dist/src/build/integration-bundler-plugin.js +1 -0
- package/dist/src/build/integration-config.js +1 -0
- package/dist/src/build/integration-detection-plugin.js +1 -0
- package/dist/src/build/integration-resolver-plugin.js +1 -0
- package/dist/src/build/island-manifest.js +1 -0
- package/dist/src/build/island-types-generator.js +5 -0
- package/dist/src/build/mdx-island-transform.js +2 -0
- package/dist/src/build/mdx-plugin.js +1 -0
- package/dist/src/build/page-island-transform.js +3 -0
- package/dist/src/build/prop-extractors/index.js +1 -0
- package/dist/src/build/prop-extractors/lit.js +1 -0
- package/dist/src/build/prop-extractors/qwik.js +1 -0
- package/dist/src/build/prop-extractors/solid.js +1 -0
- package/dist/src/build/prop-extractors/svelte.js +1 -0
- package/dist/src/build/prop-extractors/vue.js +1 -0
- package/dist/src/build/sidecar-file-manager.js +1 -0
- package/dist/src/build/sidecar-renderer.js +6 -0
- package/dist/src/client/adapters/index.js +1 -0
- package/dist/src/client/components.js +1 -0
- package/dist/src/client/css-hmr-handler.js +1 -0
- package/dist/src/client/framework-adapter.js +13 -0
- package/dist/src/client/hmr-coordinator.js +1 -0
- package/dist/src/client/hmr-error-overlay.js +214 -0
- package/dist/src/client/main.js +39 -0
- package/dist/src/components/Image.js +1 -0
- package/dist/src/components/IslandErrorBoundary.js +1 -0
- package/dist/src/components/LayoutDataErrorBoundary.js +1 -0
- package/dist/src/components/LayoutErrorBoundary.js +1 -0
- package/dist/src/components/PersistentIsland.js +1 -0
- package/dist/src/components/StreamingErrorBoundary.js +1 -0
- package/dist/src/components/StreamingLayout.js +29 -0
- package/dist/src/core/components/component-analyzer.js +1 -0
- package/dist/src/core/components/component-detection.js +5 -0
- package/dist/src/core/components/enhanced-framework-detector.js +1 -0
- package/dist/src/core/components/framework-registry.js +1 -0
- package/dist/src/core/content/mdx-processor.js +1 -0
- package/dist/src/core/integrations/index.js +1 -0
- package/dist/src/core/integrations/loader.js +1 -0
- package/dist/src/core/integrations/registry.js +1 -0
- package/dist/src/core/islands/island-persistence.js +1 -0
- package/dist/src/core/islands/island-state-serializer.js +1 -0
- package/dist/src/core/islands/persistent-island-context.js +1 -0
- package/dist/src/core/islands/use-persistent-state.js +1 -0
- package/dist/src/core/layout/enhanced-layout-resolver.js +1 -0
- package/dist/src/core/layout/layout-cache-manager.js +1 -0
- package/dist/src/core/layout/layout-composer.js +1 -0
- package/dist/src/core/layout/layout-data-loader.js +1 -0
- package/dist/src/core/layout/layout-discovery.js +1 -0
- package/dist/src/core/layout/layout-matcher.js +1 -0
- package/dist/src/core/layout/layout-types.js +1 -0
- package/dist/src/core/modules/framework-module-resolver.js +1 -0
- package/dist/src/islands/component-analysis.js +1 -0
- package/dist/src/islands/css-utils.js +17 -0
- package/dist/src/islands/discovery/index.js +1 -0
- package/dist/src/islands/discovery/registry.js +1 -0
- package/dist/src/islands/discovery/resolver.js +2 -0
- package/dist/src/islands/discovery/scanner.js +1 -0
- package/dist/src/islands/discovery/types.js +1 -0
- package/dist/src/islands/discovery/validator.js +18 -0
- package/dist/src/islands/discovery/watcher.js +1 -0
- package/dist/src/islands/framework-detection.js +1 -0
- package/dist/src/islands/integration-loader.js +1 -0
- package/dist/src/islands/island.js +1 -0
- package/dist/src/islands/render-cache.js +1 -0
- package/dist/src/islands/types.js +1 -0
- package/dist/src/islands/universal-css-collector.js +5 -0
- package/dist/src/islands/universal-head-collector.js +2 -0
- package/dist/src/layout-system.js +1 -0
- package/dist/src/middleware/discovery.js +1 -0
- package/dist/src/middleware/executor.js +1 -0
- package/dist/src/middleware/index.js +1 -0
- package/dist/src/middleware/types.js +1 -0
- package/dist/src/nitro/build-config.js +1 -0
- package/dist/src/nitro/config.js +1 -0
- package/dist/src/nitro/error-handler.js +198 -0
- package/dist/src/nitro/index.js +1 -0
- package/dist/src/nitro/island-manifest.js +2 -0
- package/dist/src/nitro/middleware-adapter.js +1 -0
- package/dist/src/nitro/renderer.js +183 -0
- package/dist/src/nitro/route-discovery.js +1 -0
- package/dist/src/nitro/types.js +1 -0
- package/dist/src/render/collect-css.js +3 -0
- package/dist/src/render/error-pages.js +48 -0
- package/dist/src/render/isolated-ssr-renderer.js +1 -0
- package/dist/src/render/ssr.js +90 -0
- package/dist/src/schemas/api.js +1 -0
- package/dist/src/schemas/core.js +1 -0
- package/dist/src/schemas/index.js +1 -0
- package/dist/src/schemas/layout.js +1 -0
- package/dist/src/schemas/routing/index.js +1 -0
- package/dist/src/schemas/routing.js +1 -0
- package/dist/src/types/as-island.js +1 -0
- package/dist/src/types/layout.js +1 -0
- package/dist/src/types/routing.js +1 -0
- package/dist/src/types/types.js +1 -0
- package/dist/src/utils/dev-logger.js +12 -0
- package/dist/src/utils/fs.js +1 -0
- package/dist/src/vite-plugin/auto-discover.js +1 -0
- package/dist/src/vite-plugin/config.js +1 -0
- package/dist/src/vite-plugin/errors.js +1 -0
- package/dist/src/vite-plugin/image-optimization.js +45 -0
- package/dist/src/vite-plugin/integration-activator.js +1 -0
- package/dist/src/vite-plugin/island-sidecar-plugin.js +1 -0
- package/dist/src/vite-plugin/module-discovery.js +1 -0
- package/dist/src/vite-plugin/nitro-integration.js +42 -0
- package/dist/src/vite-plugin/plugin.js +1 -0
- package/dist/src/vite-plugin/types.js +1 -0
- package/dist/src/vite-plugin/validation.js +2 -0
- package/package.json +14 -20
- package/mod.ts +0 -302
- package/src/build/integration-bundler-plugin.ts +0 -116
- package/src/build/integration-config.ts +0 -168
- package/src/build/integration-detection-plugin.ts +0 -117
- package/src/build/integration-resolver-plugin.ts +0 -90
- package/src/build/island-manifest.ts +0 -269
- package/src/build/island-types-generator.ts +0 -476
- package/src/build/mdx-island-transform.ts +0 -464
- package/src/build/mdx-plugin.ts +0 -98
- package/src/build/page-island-transform.ts +0 -598
- package/src/build/prop-extractors/index.ts +0 -21
- package/src/build/prop-extractors/lit.ts +0 -140
- package/src/build/prop-extractors/qwik.ts +0 -16
- package/src/build/prop-extractors/solid.ts +0 -125
- package/src/build/prop-extractors/svelte.ts +0 -194
- package/src/build/prop-extractors/vue.ts +0 -111
- package/src/build/sidecar-file-manager.ts +0 -104
- package/src/build/sidecar-renderer.ts +0 -30
- package/src/client/adapters/index.ts +0 -21
- package/src/client/components.ts +0 -35
- package/src/client/css-hmr-handler.ts +0 -344
- package/src/client/framework-adapter.ts +0 -462
- package/src/client/hmr-coordinator.ts +0 -396
- package/src/client/hmr-error-overlay.js +0 -533
- package/src/client/main.js +0 -824
- package/src/components/Image.tsx +0 -123
- package/src/components/IslandErrorBoundary.tsx +0 -145
- package/src/components/LayoutDataErrorBoundary.tsx +0 -141
- package/src/components/LayoutErrorBoundary.tsx +0 -127
- package/src/components/PersistentIsland.tsx +0 -52
- package/src/components/StreamingErrorBoundary.tsx +0 -233
- package/src/components/StreamingLayout.tsx +0 -538
- package/src/core/components/component-analyzer.ts +0 -192
- package/src/core/components/component-detection.ts +0 -508
- package/src/core/components/enhanced-framework-detector.ts +0 -500
- package/src/core/components/framework-registry.ts +0 -563
- package/src/core/content/mdx-processor.ts +0 -46
- package/src/core/integrations/index.ts +0 -19
- package/src/core/integrations/loader.ts +0 -125
- package/src/core/integrations/registry.ts +0 -175
- package/src/core/islands/island-persistence.ts +0 -325
- package/src/core/islands/island-state-serializer.ts +0 -258
- package/src/core/islands/persistent-island-context.tsx +0 -80
- package/src/core/islands/use-persistent-state.ts +0 -68
- package/src/core/layout/enhanced-layout-resolver.ts +0 -322
- package/src/core/layout/layout-cache-manager.ts +0 -485
- package/src/core/layout/layout-composer.ts +0 -357
- package/src/core/layout/layout-data-loader.ts +0 -516
- package/src/core/layout/layout-discovery.ts +0 -243
- package/src/core/layout/layout-matcher.ts +0 -299
- package/src/core/layout/layout-types.ts +0 -110
- package/src/core/modules/framework-module-resolver.ts +0 -273
- package/src/islands/component-analysis.ts +0 -213
- package/src/islands/css-utils.ts +0 -565
- package/src/islands/discovery/index.ts +0 -80
- package/src/islands/discovery/registry.ts +0 -340
- package/src/islands/discovery/resolver.ts +0 -477
- package/src/islands/discovery/scanner.ts +0 -386
- package/src/islands/discovery/types.ts +0 -117
- package/src/islands/discovery/validator.ts +0 -544
- package/src/islands/discovery/watcher.ts +0 -368
- package/src/islands/framework-detection.ts +0 -428
- package/src/islands/integration-loader.ts +0 -490
- package/src/islands/island.tsx +0 -565
- package/src/islands/render-cache.ts +0 -550
- package/src/islands/types.ts +0 -80
- package/src/islands/universal-css-collector.ts +0 -157
- package/src/islands/universal-head-collector.ts +0 -137
- package/src/layout-system.ts +0 -218
- package/src/middleware/discovery.ts +0 -268
- package/src/middleware/executor.ts +0 -315
- package/src/middleware/index.ts +0 -76
- package/src/middleware/types.ts +0 -99
- package/src/nitro/build-config.ts +0 -576
- package/src/nitro/config.ts +0 -483
- package/src/nitro/error-handler.ts +0 -636
- package/src/nitro/index.ts +0 -173
- package/src/nitro/island-manifest.ts +0 -584
- package/src/nitro/middleware-adapter.ts +0 -260
- package/src/nitro/renderer.ts +0 -1471
- package/src/nitro/route-discovery.ts +0 -439
- package/src/nitro/types.ts +0 -321
- package/src/render/collect-css.ts +0 -198
- package/src/render/error-pages.ts +0 -79
- package/src/render/isolated-ssr-renderer.ts +0 -654
- package/src/render/ssr.ts +0 -1030
- package/src/schemas/api.ts +0 -30
- package/src/schemas/core.ts +0 -64
- package/src/schemas/index.ts +0 -212
- package/src/schemas/layout.ts +0 -279
- package/src/schemas/routing/index.ts +0 -38
- package/src/schemas/routing.ts +0 -376
- package/src/types/as-island.ts +0 -20
- package/src/types/layout.ts +0 -285
- package/src/types/routing.ts +0 -555
- package/src/types/types.ts +0 -5
- package/src/utils/dev-logger.ts +0 -299
- package/src/utils/fs.ts +0 -151
- package/src/vite-plugin/auto-discover.ts +0 -551
- package/src/vite-plugin/config.ts +0 -266
- package/src/vite-plugin/errors.ts +0 -127
- package/src/vite-plugin/image-optimization.ts +0 -156
- package/src/vite-plugin/integration-activator.ts +0 -126
- package/src/vite-plugin/island-sidecar-plugin.ts +0 -176
- package/src/vite-plugin/module-discovery.ts +0 -189
- package/src/vite-plugin/nitro-integration.ts +0 -1354
- package/src/vite-plugin/plugin.ts +0 -403
- package/src/vite-plugin/types.ts +0 -327
- package/src/vite-plugin/validation.ts +0 -228
- /package/{src → dist/src}/client/types/framework-runtime.d.ts +0 -0
- /package/{src → dist/src}/client/types/vite-hmr.d.ts +0 -0
- /package/{src → dist/src}/client/types/vite-virtual-modules.d.ts +0 -0
- /package/{src → dist/src}/layout-system.d.ts +0 -0
- /package/{src → dist/src}/types/image.d.ts +0 -0
- /package/{src → dist/src}/types/index.d.ts +0 -0
- /package/{src → dist/src}/types/island-jsx.d.ts +0 -0
- /package/{src → dist/src}/types/island-prop.d.ts +0 -0
- /package/{src → dist/src}/types/mdx.d.ts +0 -0
- /package/{src → dist/src}/types/urlpattern.d.ts +0 -0
- /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
|
-
}
|