@shohojdhara/atomix 0.4.8 → 0.4.9
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/atomix.config.ts +58 -1
- package/dist/atomix.css +148 -120
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +1 -1
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.d.ts +33 -0
- package/dist/charts.js +1227 -122
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +33 -10
- package/dist/core.js +1052 -41
- package/dist/core.js.map +1 -1
- package/dist/forms.d.ts +33 -0
- package/dist/forms.js +2086 -1035
- package/dist/forms.js.map +1 -1
- package/dist/heavy.d.ts +42 -1
- package/dist/heavy.js +1620 -600
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +441 -270
- package/dist/index.esm.js +1900 -638
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1935 -670
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/package.json +6 -3
- package/scripts/atomix-cli.js +148 -4
- package/scripts/cli/__tests__/basic.test.js +3 -2
- package/scripts/cli/__tests__/clean.test.js +278 -0
- package/scripts/cli/__tests__/component-validator.test.js +433 -0
- package/scripts/cli/__tests__/generator.test.js +613 -0
- package/scripts/cli/__tests__/glass-motion.test.js +256 -0
- package/scripts/cli/__tests__/integration.test.js +719 -108
- package/scripts/cli/__tests__/migrate.test.js +74 -0
- package/scripts/cli/__tests__/security.test.js +206 -0
- package/scripts/cli/__tests__/test-setup.js +3 -1
- package/scripts/cli/__tests__/theme-bridge.test.js +507 -0
- package/scripts/cli/__tests__/token-provider.test.js +361 -0
- package/scripts/cli/__tests__/utils.test.js +5 -5
- package/scripts/cli/commands/benchmark.js +105 -0
- package/scripts/cli/commands/build-theme.js +4 -1
- package/scripts/cli/commands/clean.js +109 -0
- package/scripts/cli/commands/doctor.js +88 -0
- package/scripts/cli/commands/generate.js +135 -14
- package/scripts/cli/commands/init.js +45 -18
- package/scripts/cli/commands/migrate.js +106 -0
- package/scripts/cli/commands/sync-tokens.js +206 -0
- package/scripts/cli/commands/theme-bridge.js +248 -0
- package/scripts/cli/commands/tokens.js +157 -0
- package/scripts/cli/commands/validate.js +194 -0
- package/scripts/cli/internal/ai-engine.js +156 -0
- package/scripts/cli/internal/component-validator.js +443 -0
- package/scripts/cli/internal/config-loader.js +162 -0
- package/scripts/cli/internal/filesystem.js +102 -2
- package/scripts/cli/internal/generator.js +359 -39
- package/scripts/cli/internal/glass-generator.js +398 -0
- package/scripts/cli/internal/hook-generator.js +369 -0
- package/scripts/cli/internal/hooks.js +61 -0
- package/scripts/cli/internal/itcss-generator.js +565 -0
- package/scripts/cli/internal/motion-generator.js +679 -0
- package/scripts/cli/internal/template-engine.js +301 -0
- package/scripts/cli/internal/theme-bridge.js +664 -0
- package/scripts/cli/internal/tokens/engine.js +122 -0
- package/scripts/cli/internal/tokens/provider.js +34 -0
- package/scripts/cli/internal/tokens/providers/figma.js +50 -0
- package/scripts/cli/internal/tokens/providers/style-dictionary.js +48 -0
- package/scripts/cli/internal/tokens/providers/w3c.js +48 -0
- package/scripts/cli/internal/tokens/token-provider.js +443 -0
- package/scripts/cli/internal/tokens/token-validator.js +513 -0
- package/scripts/cli/internal/validator.js +276 -0
- package/scripts/cli/internal/wizard.js +60 -6
- package/scripts/cli/mappings.js +23 -0
- package/scripts/cli/migration-tools.js +164 -94
- package/scripts/cli/plugins/style-dictionary.js +46 -0
- package/scripts/cli/templates/README.md +525 -95
- package/scripts/cli/templates/common-templates.js +40 -14
- package/scripts/cli/templates/components/react-component.ts +282 -0
- package/scripts/cli/templates/config/project-config.ts +112 -0
- package/scripts/cli/templates/hooks/use-component.ts +477 -0
- package/scripts/cli/templates/index.js +19 -4
- package/scripts/cli/templates/index.ts +171 -0
- package/scripts/cli/templates/next-templates.js +72 -0
- package/scripts/cli/templates/react-templates.js +70 -126
- package/scripts/cli/templates/scss-templates.js +35 -35
- package/scripts/cli/templates/stories/storybook-story.ts +241 -0
- package/scripts/cli/templates/styles/scss-component.ts +255 -0
- package/scripts/cli/templates/tests/vitest-test.ts +229 -0
- package/scripts/cli/templates/token-templates.js +337 -1
- package/scripts/cli/templates/tokens/token-generators.ts +1088 -0
- package/scripts/cli/templates/types/component-types.ts +145 -0
- package/scripts/cli/templates/utils/testing-utils.ts +144 -0
- package/scripts/cli/templates/vanilla-templates.js +39 -0
- package/scripts/cli/token-manager.js +8 -2
- package/scripts/cli/utils/cache-manager.js +240 -0
- package/scripts/cli/utils/detector.js +46 -0
- package/scripts/cli/utils/diagnostics.js +289 -0
- package/scripts/cli/utils/error.js +45 -3
- package/scripts/cli/utils/helpers.js +24 -0
- package/scripts/cli/utils/logger.js +1 -1
- package/scripts/cli/utils/security.js +302 -0
- package/scripts/cli/utils/telemetry.js +115 -0
- package/scripts/cli/utils/validation.js +4 -38
- package/scripts/cli/utils.js +46 -0
- package/src/components/Accordion/Accordion.stories.tsx +0 -18
- package/src/components/Accordion/Accordion.test.tsx +0 -17
- package/src/components/Accordion/Accordion.tsx +0 -4
- package/src/components/AtomixGlass/AtomixGlass.tsx +102 -2
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +125 -12
- package/src/components/AtomixGlass/PerformanceDashboard.tsx +219 -0
- package/src/components/AtomixGlass/README.md +25 -10
- package/src/components/AtomixGlass/animation-system.ts +578 -0
- package/src/components/AtomixGlass/shader-utils.ts +4 -1
- package/src/components/AtomixGlass/stories/Overview.stories.tsx +157 -6
- package/src/components/AtomixGlass/stories/Phase1-Animation.stories.tsx +653 -0
- package/src/components/AtomixGlass/stories/Phase1-Test.stories.tsx +95 -0
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +51 -51
- package/src/components/AtomixGlass/stories/shared-components.tsx +6 -0
- package/src/components/Avatar/Avatar.tsx +1 -1
- package/src/components/Button/Button.stories.disabled-link.tsx +10 -0
- package/src/components/Button/Button.stories.tsx +10 -0
- package/src/components/Button/Button.test.tsx +16 -11
- package/src/components/Button/Button.tsx +4 -4
- package/src/components/Card/Card.tsx +1 -1
- package/src/components/Dropdown/Dropdown.tsx +12 -12
- package/src/components/Form/Select.tsx +62 -3
- package/src/components/Modal/Modal.tsx +14 -3
- package/src/components/Navigation/Navbar/Navbar.tsx +44 -0
- package/src/components/Slider/Slider.stories.tsx +3 -3
- package/src/components/Slider/Slider.tsx +38 -0
- package/src/components/Steps/Steps.tsx +3 -3
- package/src/components/Tabs/Tabs.tsx +77 -8
- package/src/components/Testimonial/Testimonial.tsx +1 -1
- package/src/components/TypedButton/TypedButton.stories.tsx +59 -0
- package/src/components/TypedButton/TypedButton.tsx +39 -0
- package/src/components/TypedButton/index.ts +2 -0
- package/src/components/VideoPlayer/VideoPlayer.tsx +11 -4
- package/src/lib/composables/index.ts +4 -7
- package/src/lib/composables/types.ts +45 -0
- package/src/lib/composables/useAccordion.ts +0 -7
- package/src/lib/composables/useAtomixGlass.ts +144 -5
- package/src/lib/composables/useChartExport.ts +3 -13
- package/src/lib/composables/useDropdown.ts +66 -0
- package/src/lib/composables/useFocusTrap.ts +80 -0
- package/src/lib/composables/usePerformanceMonitor.ts +448 -0
- package/src/lib/composables/useResponsiveGlass.presets.ts +192 -0
- package/src/lib/composables/useResponsiveGlass.ts +441 -0
- package/src/lib/composables/useTooltip.ts +16 -0
- package/src/lib/composables/useTypedButton.ts +66 -0
- package/src/lib/config/index.ts +62 -5
- package/src/lib/constants/components.ts +55 -0
- package/src/lib/theme/devtools/__tests__/useHistory.test.tsx +150 -0
- package/src/lib/theme/tokens/centralized-tokens.ts +120 -0
- package/src/lib/theme/utils/__tests__/domUtils.test.ts +101 -0
- package/src/lib/types/components.ts +37 -11
- package/src/lib/types/glass.ts +35 -0
- package/src/lib/types/index.ts +1 -0
- package/src/lib/utils/displacement-generator.ts +1 -1
- package/src/styles/01-settings/_settings.testtypecheck.scss +53 -0
- package/src/styles/01-settings/_settings.typedbutton.scss +53 -0
- package/src/styles/06-components/_components.testbutton.scss +212 -0
- package/src/styles/06-components/_components.testtypecheck.scss +212 -0
- package/src/styles/06-components/_components.typedbutton.scss +212 -0
- package/src/styles/99-utilities/_index.scss +1 -0
- package/src/styles/99-utilities/_utilities.text.scss +1 -1
- package/src/styles/99-utilities/_utilities.touch-target.scss +36 -0
- package/src/styles/06-components/old.chart.styles.scss +0 -2788
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Structure Validator
|
|
3
|
+
* Enforces Atomix design system architecture patterns
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFile } from 'fs/promises';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { logger } from '../utils/logger.js';
|
|
10
|
+
import { AtomixCLIError } from '../utils/error.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Component validation rule severity levels
|
|
14
|
+
*/
|
|
15
|
+
export const COMPONENT_SEVERITY = {
|
|
16
|
+
ERROR: 'error',
|
|
17
|
+
WARNING: 'warning',
|
|
18
|
+
INFO: 'info'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Required component structure rules
|
|
23
|
+
*/
|
|
24
|
+
export const COMPONENT_RULES = {
|
|
25
|
+
/**
|
|
26
|
+
* forwardRef usage - All components must use forwardRef
|
|
27
|
+
*/
|
|
28
|
+
FORWARD_REF_REQUIRED: {
|
|
29
|
+
name: 'forward-ref-required',
|
|
30
|
+
description: 'Components must use forwardRef for accessibility and ref forwarding',
|
|
31
|
+
severity: COMPONENT_SEVERITY.ERROR,
|
|
32
|
+
validate: (content) => {
|
|
33
|
+
const issues = [];
|
|
34
|
+
|
|
35
|
+
if (!content.includes('forwardRef')) {
|
|
36
|
+
issues.push({
|
|
37
|
+
rule: 'forward-ref-required',
|
|
38
|
+
message: 'Component missing forwardRef wrapper',
|
|
39
|
+
suggestion: 'Wrap component with forwardRef<HTMLDivElement, ComponentProps>',
|
|
40
|
+
severity: COMPONENT_SEVERITY.ERROR
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return issues;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* displayName assignment - Required for debugging
|
|
50
|
+
*/
|
|
51
|
+
DISPLAY_NAME_REQUIRED: {
|
|
52
|
+
name: 'display-name-required',
|
|
53
|
+
description: 'Components must have displayName property',
|
|
54
|
+
severity: COMPONENT_SEVERITY.ERROR,
|
|
55
|
+
validate: (content, componentName) => {
|
|
56
|
+
const issues = [];
|
|
57
|
+
const displayNamePattern = new RegExp(`${componentName}\\.displayName\\s*=\\s*['"]${componentName}['"]`);
|
|
58
|
+
|
|
59
|
+
if (!displayNamePattern.test(content)) {
|
|
60
|
+
issues.push({
|
|
61
|
+
rule: 'display-name-required',
|
|
62
|
+
message: `Missing or incorrect displayName assignment`,
|
|
63
|
+
suggestion: `Add: ${componentName}.displayName = '${componentName}';`,
|
|
64
|
+
severity: COMPONENT_SEVERITY.ERROR
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return issues;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* JSDoc documentation - Required for all components
|
|
74
|
+
*/
|
|
75
|
+
JSDOC_REQUIRED: {
|
|
76
|
+
name: 'jsdoc-required',
|
|
77
|
+
description: 'Components must have JSDoc documentation',
|
|
78
|
+
severity: COMPONENT_SEVERITY.WARNING,
|
|
79
|
+
validate: (content) => {
|
|
80
|
+
const issues = [];
|
|
81
|
+
|
|
82
|
+
// Check for JSDoc comment block
|
|
83
|
+
if (!/\/\*\*[\s\S]*?\*\//.test(content)) {
|
|
84
|
+
issues.push({
|
|
85
|
+
rule: 'jsdoc-required',
|
|
86
|
+
message: 'Component missing JSDoc documentation',
|
|
87
|
+
suggestion: 'Add JSDoc comment with @param and @returns tags',
|
|
88
|
+
severity: COMPONENT_SEVERITY.WARNING
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return issues;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* TypeScript Props interface/type - Type safety required
|
|
98
|
+
*/
|
|
99
|
+
TYPESCRIPT_TYPES_REQUIRED: {
|
|
100
|
+
name: 'typescript-types-required',
|
|
101
|
+
description: 'Components must define Props interface or type',
|
|
102
|
+
severity: COMPONENT_SEVERITY.ERROR,
|
|
103
|
+
validate: (content, componentName) => {
|
|
104
|
+
const issues = [];
|
|
105
|
+
const propsPattern = new RegExp(`(interface|type)\\s+${componentName}Props`);
|
|
106
|
+
|
|
107
|
+
// Check if Props type is defined locally OR imported
|
|
108
|
+
const hasLocalDef = propsPattern.test(content);
|
|
109
|
+
const hasImport = new RegExp(`import.*{.*${componentName}Props.*}`).test(content);
|
|
110
|
+
|
|
111
|
+
if (!hasLocalDef && !hasImport) {
|
|
112
|
+
issues.push({
|
|
113
|
+
rule: 'typescript-types-required',
|
|
114
|
+
message: `Missing ${componentName}Props type/interface definition or import`,
|
|
115
|
+
suggestion: `Define interface ${componentName}Props { ... } or import it`,
|
|
116
|
+
severity: COMPONENT_SEVERITY.ERROR
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return issues;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Accessibility attributes - ARIA support required
|
|
126
|
+
*/
|
|
127
|
+
ACCESSIBILITY_ATTRIBUTES: {
|
|
128
|
+
name: 'accessibility-attributes',
|
|
129
|
+
description: 'Components should include accessibility attributes',
|
|
130
|
+
severity: COMPONENT_SEVERITY.WARNING,
|
|
131
|
+
validate: (content) => {
|
|
132
|
+
const issues = [];
|
|
133
|
+
|
|
134
|
+
// Check for aria-* attributes
|
|
135
|
+
const hasAriaAttributes = /aria-[a-z]+/.test(content);
|
|
136
|
+
|
|
137
|
+
// Check for role attribute
|
|
138
|
+
const hasRole = /role=["'][^"']+["']/.test(content);
|
|
139
|
+
|
|
140
|
+
// Check for tabIndex
|
|
141
|
+
const hasTabIndex = /tabIndex/.test(content);
|
|
142
|
+
|
|
143
|
+
if (!hasAriaAttributes && !hasRole && !hasTabIndex) {
|
|
144
|
+
issues.push({
|
|
145
|
+
rule: 'accessibility-attributes',
|
|
146
|
+
message: 'Component missing accessibility attributes',
|
|
147
|
+
suggestion: 'Add aria-label, aria-describedby, role, or tabIndex as appropriate',
|
|
148
|
+
severity: COMPONENT_SEVERITY.WARNING
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return issues;
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* No hardcoded colors - Design token enforcement
|
|
158
|
+
*/
|
|
159
|
+
NO_HARDCODED_COLORS: {
|
|
160
|
+
name: 'no-hardcoded-colors',
|
|
161
|
+
description: 'Components must use design tokens instead of hardcoded colors',
|
|
162
|
+
severity: COMPONENT_SEVERITY.ERROR,
|
|
163
|
+
validate: (content) => {
|
|
164
|
+
const issues = [];
|
|
165
|
+
|
|
166
|
+
// Check for hex colors
|
|
167
|
+
const hexColorRegex = /#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})\b/g;
|
|
168
|
+
const hexMatches = content.match(hexColorRegex) || [];
|
|
169
|
+
|
|
170
|
+
// Check for RGB/RGBA colors
|
|
171
|
+
const rgbRegex = /rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(,\s*[\d.]+\s*)?\)/gi;
|
|
172
|
+
const rgbMatches = content.match(rgbRegex) || [];
|
|
173
|
+
|
|
174
|
+
// Check for HSL/HSLA colors
|
|
175
|
+
const hslRegex = /hsla?\(\s*\d+\s*,\s*[\d.]+%?\s*,\s*[\d.]+%?\s*(,\s*[\d.]+\s*)?\)/gi;
|
|
176
|
+
const hslMatches = content.match(hslRegex) || [];
|
|
177
|
+
|
|
178
|
+
const allMatches = [...hexMatches, ...rgbMatches, ...hslMatches];
|
|
179
|
+
|
|
180
|
+
if (allMatches.length > 0) {
|
|
181
|
+
issues.push({
|
|
182
|
+
rule: 'no-hardcoded-colors',
|
|
183
|
+
message: `Found ${allMatches.length} hardcoded color(s): ${allMatches.slice(0, 3).join(', ')}${allMatches.length > 3 ? '...' : ''}`,
|
|
184
|
+
suggestion: 'Replace with design tokens (e.g., var(--color-primary) or theme.colors.primary)',
|
|
185
|
+
severity: COMPONENT_SEVERITY.ERROR,
|
|
186
|
+
matches: allMatches
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return issues;
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Memo usage - Performance optimization
|
|
196
|
+
*/
|
|
197
|
+
MEMO_USAGE: {
|
|
198
|
+
name: 'memo-usage',
|
|
199
|
+
description: 'Components should use React.memo for performance',
|
|
200
|
+
severity: COMPONENT_SEVERITY.INFO,
|
|
201
|
+
validate: (content) => {
|
|
202
|
+
const issues = [];
|
|
203
|
+
|
|
204
|
+
if (!content.includes('memo') && !content.includes('React.memo')) {
|
|
205
|
+
issues.push({
|
|
206
|
+
rule: 'memo-usage',
|
|
207
|
+
message: 'Component not wrapped in React.memo',
|
|
208
|
+
suggestion: 'Consider using React.memo() for performance optimization',
|
|
209
|
+
severity: COMPONENT_SEVERITY.INFO
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return issues;
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Composable hook pattern - Atomix architecture
|
|
219
|
+
*/
|
|
220
|
+
COMPOSABLE_HOOK_PATTERN: {
|
|
221
|
+
name: 'composable-hook-pattern',
|
|
222
|
+
description: 'Components should use composable hook pattern',
|
|
223
|
+
severity: COMPONENT_SEVERITY.INFO,
|
|
224
|
+
validate: (content, componentName) => {
|
|
225
|
+
const issues = [];
|
|
226
|
+
|
|
227
|
+
const hookPattern = new RegExp(`use${componentName}`);
|
|
228
|
+
|
|
229
|
+
if (!hookPattern.test(content)) {
|
|
230
|
+
issues.push({
|
|
231
|
+
rule: 'composable-hook-pattern',
|
|
232
|
+
message: `Component not using composable hook (use${componentName})`,
|
|
233
|
+
suggestion: `Create and use lib/composables/use${componentName} hook`,
|
|
234
|
+
severity: COMPONENT_SEVERITY.INFO
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return issues;
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Theme naming utility - Consistent class names
|
|
244
|
+
*/
|
|
245
|
+
THEME_NAMING_USAGE: {
|
|
246
|
+
name: 'theme-naming-usage',
|
|
247
|
+
description: 'Components should use ThemeNaming utility for class names',
|
|
248
|
+
severity: COMPONENT_SEVERITY.INFO,
|
|
249
|
+
validate: (content) => {
|
|
250
|
+
const issues = [];
|
|
251
|
+
|
|
252
|
+
// Check for ThemeNaming import or usage
|
|
253
|
+
const hasThemeNaming = /ThemeNaming\./.test(content) || /themeNaming\./.test(content);
|
|
254
|
+
|
|
255
|
+
// Check for variantClass, sizeClass, stateClass patterns
|
|
256
|
+
const hasVariantPattern = /(variant|size|state)Class/.test(content);
|
|
257
|
+
|
|
258
|
+
if (!hasThemeNaming && !hasVariantPattern) {
|
|
259
|
+
issues.push({
|
|
260
|
+
rule: 'theme-naming-usage',
|
|
261
|
+
message: 'Component not using ThemeNaming utility for variants/sizes/states',
|
|
262
|
+
suggestion: 'Use ThemeNaming.variantClass(), sizeClass(), stateClass() for consistency',
|
|
263
|
+
severity: COMPONENT_SEVERITY.INFO
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return issues;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* ComponentValidator class for enforcing design system architecture
|
|
274
|
+
*/
|
|
275
|
+
export class ComponentValidator {
|
|
276
|
+
constructor(options = {}) {
|
|
277
|
+
this.rules = new Map();
|
|
278
|
+
this.enabledRules = options.enabledRules || Object.keys(COMPONENT_RULES);
|
|
279
|
+
|
|
280
|
+
// Register built-in rules
|
|
281
|
+
for (const [key, rule] of Object.entries(COMPONENT_RULES)) {
|
|
282
|
+
this.registerRule(key, rule);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Register a custom validation rule
|
|
288
|
+
* @param {string} name - Rule name
|
|
289
|
+
* @param {Object} rule - Rule definition
|
|
290
|
+
*/
|
|
291
|
+
registerRule(name, rule) {
|
|
292
|
+
if (!rule.name || !rule.validate || !rule.severity) {
|
|
293
|
+
throw new Error(`Invalid rule "${name}": must have name, validate, and severity`);
|
|
294
|
+
}
|
|
295
|
+
this.rules.set(name, rule);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Enable or disable a rule
|
|
300
|
+
* @param {string} ruleName - Rule to toggle
|
|
301
|
+
* @param {boolean} enabled - Whether to enable
|
|
302
|
+
*/
|
|
303
|
+
toggleRule(ruleName, enabled) {
|
|
304
|
+
if (enabled) {
|
|
305
|
+
if (!this.enabledRules.includes(ruleName)) {
|
|
306
|
+
this.enabledRules.push(ruleName);
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
this.enabledRules = this.enabledRules.filter(r => r !== ruleName);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Validate component code against all enabled rules
|
|
315
|
+
* @param {string} content - Component source code
|
|
316
|
+
* @param {string} componentName - Component name (PascalCase)
|
|
317
|
+
* @returns {Object} Validation results
|
|
318
|
+
*/
|
|
319
|
+
validate(content, componentName) {
|
|
320
|
+
const results = {
|
|
321
|
+
valid: true,
|
|
322
|
+
issues: [],
|
|
323
|
+
summary: {
|
|
324
|
+
errors: 0,
|
|
325
|
+
warnings: 0,
|
|
326
|
+
info: 0
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
for (const ruleName of this.enabledRules) {
|
|
331
|
+
const rule = this.rules.get(ruleName);
|
|
332
|
+
|
|
333
|
+
if (!rule) {
|
|
334
|
+
logger.debug(`Rule "${ruleName}" not found, skipping`);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
const issues = rule.validate(content, componentName);
|
|
340
|
+
|
|
341
|
+
if (issues.length > 0) {
|
|
342
|
+
results.valid = false;
|
|
343
|
+
results.issues.push(...issues);
|
|
344
|
+
|
|
345
|
+
// Update summary
|
|
346
|
+
for (const issue of issues) {
|
|
347
|
+
if (issue.severity === COMPONENT_SEVERITY.ERROR) {
|
|
348
|
+
results.summary.errors++;
|
|
349
|
+
} else if (issue.severity === COMPONENT_SEVERITY.WARNING) {
|
|
350
|
+
results.summary.warnings++;
|
|
351
|
+
} else {
|
|
352
|
+
results.summary.info++;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} catch (error) {
|
|
357
|
+
logger.warn(`Rule "${ruleName}" failed: ${error.message}`);
|
|
358
|
+
results.issues.push({
|
|
359
|
+
rule: ruleName,
|
|
360
|
+
message: `Validation rule error: ${error.message}`,
|
|
361
|
+
severity: COMPONENT_SEVERITY.WARNING
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return results;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Validate component from file path
|
|
371
|
+
* @param {string} filePath - Path to component file
|
|
372
|
+
* @param {string} componentName - Component name
|
|
373
|
+
* @returns {Promise<Object>} Validation results
|
|
374
|
+
*/
|
|
375
|
+
async validateFile(filePath, componentName) {
|
|
376
|
+
if (!existsSync(filePath)) {
|
|
377
|
+
throw new AtomixCLIError(
|
|
378
|
+
`Component file not found: ${filePath}`,
|
|
379
|
+
'FILE_NOT_FOUND',
|
|
380
|
+
['Verify the component file exists']
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const content = await readFile(filePath, 'utf8');
|
|
385
|
+
return this.validate(content, componentName);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Get detailed report of validation results
|
|
390
|
+
* @param {Object} results - Validation results
|
|
391
|
+
* @param {string} componentName - Component name
|
|
392
|
+
* @returns {string} Formatted report
|
|
393
|
+
*/
|
|
394
|
+
getReport(results, componentName) {
|
|
395
|
+
const lines = [
|
|
396
|
+
`\n🔍 Component Validation Report: ${componentName}`,
|
|
397
|
+
'='.repeat(60),
|
|
398
|
+
`Status: ${results.valid ? '✅ PASSED' : '❌ FAILED'}`,
|
|
399
|
+
'',
|
|
400
|
+
'Summary:',
|
|
401
|
+
` Errors: ${results.summary.errors}`,
|
|
402
|
+
` Warnings: ${results.summary.warnings}`,
|
|
403
|
+
` Info: ${results.summary.info}`,
|
|
404
|
+
''
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
if (results.issues.length > 0) {
|
|
408
|
+
lines.push('Issues:');
|
|
409
|
+
|
|
410
|
+
// Sort by severity (errors first)
|
|
411
|
+
const sortedIssues = [...results.issues].sort((a, b) => {
|
|
412
|
+
const severityOrder = {
|
|
413
|
+
[COMPONENT_SEVERITY.ERROR]: 0,
|
|
414
|
+
[COMPONENT_SEVERITY.WARNING]: 1,
|
|
415
|
+
[COMPONENT_SEVERITY.INFO]: 2
|
|
416
|
+
};
|
|
417
|
+
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
for (const issue of sortedIssues) {
|
|
421
|
+
const icon = issue.severity === COMPONENT_SEVERITY.ERROR ? '❌' :
|
|
422
|
+
issue.severity === COMPONENT_SEVERITY.WARNING ? '⚠️' : 'ℹ️';
|
|
423
|
+
|
|
424
|
+
lines.push(` ${icon} [${issue.rule}] ${issue.message}`);
|
|
425
|
+
|
|
426
|
+
if (issue.suggestion) {
|
|
427
|
+
lines.push(` 💡 ${issue.suggestion}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
lines.push('='.repeat(60));
|
|
433
|
+
|
|
434
|
+
return lines.join('\n');
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Create a singleton validator instance
|
|
440
|
+
*/
|
|
441
|
+
export const componentValidator = new ComponentValidator();
|
|
442
|
+
|
|
443
|
+
export default componentValidator;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomix CLI Configuration Loader
|
|
3
|
+
* Supports loading atomix.config.ts and atomix.config.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { pathToFileURL } from 'url';
|
|
9
|
+
import { logger } from '../utils/logger.js';
|
|
10
|
+
import { hookManager } from './hooks.js';
|
|
11
|
+
|
|
12
|
+
export class ConfigLoader {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.config = null;
|
|
15
|
+
this.configPath = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Loads the configuration from the project root
|
|
20
|
+
* @param {string} projectRoot - Root directory of the project
|
|
21
|
+
* @returns {Promise<Object>} - Loaded configuration
|
|
22
|
+
*/
|
|
23
|
+
async load(projectRoot = process.cwd()) {
|
|
24
|
+
if (this.config) return this.config;
|
|
25
|
+
|
|
26
|
+
const configFiles = ['atomix.config.ts', 'atomix.config.js'];
|
|
27
|
+
let foundFile = null;
|
|
28
|
+
|
|
29
|
+
for (const file of configFiles) {
|
|
30
|
+
const fullPath = join(projectRoot, file);
|
|
31
|
+
if (existsSync(fullPath)) {
|
|
32
|
+
foundFile = fullPath;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!foundFile) {
|
|
38
|
+
logger.debug('No configuration file found. Using defaults.');
|
|
39
|
+
this.config = { prefix: 'atomix' };
|
|
40
|
+
return this.config;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.configPath = foundFile;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// If it's a TypeScript file, we need to register ts-node
|
|
47
|
+
if (foundFile.endsWith('.ts')) {
|
|
48
|
+
// Dynamic import to avoid issues in pure JS environments
|
|
49
|
+
const { register } = await import('ts-node');
|
|
50
|
+
register({
|
|
51
|
+
transpileOnly: true,
|
|
52
|
+
esm: true,
|
|
53
|
+
compilerOptions: {
|
|
54
|
+
module: 'ESNext',
|
|
55
|
+
target: 'ESNext'
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Use dynamic import for ESM compatibility
|
|
61
|
+
const configModule = await import(pathToFileURL(foundFile).href);
|
|
62
|
+
this.config = configModule.default || configModule;
|
|
63
|
+
|
|
64
|
+
logger.debug(`Loaded configuration from ${foundFile}`);
|
|
65
|
+
|
|
66
|
+
// Initialize plugins if present
|
|
67
|
+
if (this.config.plugins) {
|
|
68
|
+
await this._initializePlugins();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return this.config;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
logger.error(`Failed to load configuration from ${foundFile}: ${error.message}`);
|
|
74
|
+
if (process.env.ATOMIX_DEBUG) {
|
|
75
|
+
console.error(error);
|
|
76
|
+
}
|
|
77
|
+
this.config = { prefix: 'atomix' };
|
|
78
|
+
return this.config;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Initializes plugins from the configuration
|
|
84
|
+
* @private
|
|
85
|
+
*/
|
|
86
|
+
async _initializePlugins() {
|
|
87
|
+
for (const pluginEntry of this.config.plugins) {
|
|
88
|
+
let pluginName = '';
|
|
89
|
+
let pluginOptions = {};
|
|
90
|
+
|
|
91
|
+
if (typeof pluginEntry === 'string') {
|
|
92
|
+
pluginName = pluginEntry;
|
|
93
|
+
} else {
|
|
94
|
+
pluginName = pluginEntry.name;
|
|
95
|
+
pluginOptions = pluginEntry.options || {};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
let pluginModule;
|
|
100
|
+
|
|
101
|
+
// Check if it's a local plugin (starts with ./ or ../)
|
|
102
|
+
if (pluginName.startsWith('.')) {
|
|
103
|
+
const pluginPath = join(process.cwd(), pluginName);
|
|
104
|
+
pluginModule = await import(pathToFileURL(pluginPath).href);
|
|
105
|
+
} else {
|
|
106
|
+
// Assume it's an npm package
|
|
107
|
+
pluginModule = await import(pluginName);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const plugin = pluginModule.default || pluginModule;
|
|
111
|
+
|
|
112
|
+
if (typeof plugin === 'function') {
|
|
113
|
+
// Initialize plugin with API and options
|
|
114
|
+
await plugin(this._getPluginAPI(), pluginOptions);
|
|
115
|
+
logger.debug(`Initialized plugin: ${pluginName}`);
|
|
116
|
+
} else if (plugin && typeof plugin.init === 'function') {
|
|
117
|
+
await plugin.init(this._getPluginAPI(), pluginOptions);
|
|
118
|
+
logger.debug(`Initialized plugin: ${pluginName}`);
|
|
119
|
+
} else {
|
|
120
|
+
logger.warn(`Plugin ${pluginName} does not export a valid initializer.`);
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
logger.error(`Failed to load plugin ${pluginName}: ${error.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Returns the stable API provided to plugins
|
|
130
|
+
* @private
|
|
131
|
+
*/
|
|
132
|
+
_getPluginAPI() {
|
|
133
|
+
return {
|
|
134
|
+
logger,
|
|
135
|
+
config: this.config,
|
|
136
|
+
hooks: {
|
|
137
|
+
register: (hook, cb) => hookManager.register(hook, cb)
|
|
138
|
+
},
|
|
139
|
+
// Plugins might need file system access
|
|
140
|
+
fs: {
|
|
141
|
+
// We'll expose a subset of filesystem utilities later if needed
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Returns the currently loaded configuration
|
|
148
|
+
*/
|
|
149
|
+
getConfig() {
|
|
150
|
+
return this.config || { prefix: 'atomix' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Returns a specific configuration value
|
|
155
|
+
*/
|
|
156
|
+
get(key) {
|
|
157
|
+
if (!this.config) return null;
|
|
158
|
+
return this.config[key];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const configLoader = new ConfigLoader();
|