@shohojdhara/atomix 0.4.7 → 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 +172 -157
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +4 -4
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.d.ts +33 -0
- package/dist/charts.js +1274 -164
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +33 -10
- package/dist/core.js +1099 -83
- package/dist/core.js.map +1 -1
- package/dist/forms.d.ts +33 -0
- package/dist/forms.js +2106 -1050
- package/dist/forms.js.map +1 -1
- package/dist/heavy.d.ts +42 -1
- package/dist/heavy.js +1663 -638
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +442 -270
- package/dist/index.esm.js +1947 -680
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1982 -712
- 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 +136 -1827
- 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 +115 -0
- package/scripts/cli/commands/clean.js +109 -0
- package/scripts/cli/commands/doctor.js +88 -0
- package/scripts/cli/commands/generate.js +218 -0
- package/scripts/cli/commands/init.js +73 -0
- 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/compiler.js +114 -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 +158 -0
- package/scripts/cli/internal/generator.js +430 -0
- 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 +115 -0
- 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 +89 -0
- package/scripts/cli/utils/helpers.js +67 -0
- package/scripts/cli/utils/logger.js +75 -0
- package/scripts/cli/utils/security.js +302 -0
- package/scripts/cli/utils/telemetry.js +115 -0
- package/scripts/cli/utils/validation.js +37 -0
- package/scripts/cli/utils.js +28 -341
- 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.test.tsx +37 -3
- package/src/components/AtomixGlass/AtomixGlass.tsx +143 -31
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +129 -31
- package/src/components/AtomixGlass/PerformanceDashboard.tsx +219 -0
- package/src/components/AtomixGlass/README.md +25 -10
- package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +216 -0
- 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 +148 -6
- package/src/lib/composables/useAtomixGlassStyles.ts +9 -7
- 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 +62 -7
- 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.atomix-glass.scss +17 -21
- package/src/styles/06-components/_components.edge-panel.scss +1 -5
- package/src/styles/06-components/_components.modal.scss +1 -4
- package/src/styles/06-components/_components.navbar.scss +1 -1
- 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.tooltip.scss +9 -5
- 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/scripts/cli/component-generator.js +0 -564
- package/scripts/cli/interactive-init.js +0 -357
- package/src/styles/06-components/old.chart.styles.scss +0 -2788
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Provider & Validator Tests
|
|
3
|
+
* Tests for Phase 1: Enhanced Design Token Integration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
TokenProvider,
|
|
9
|
+
TOKEN_FORMATS,
|
|
10
|
+
TOKEN_CATEGORIES,
|
|
11
|
+
tokenProvider
|
|
12
|
+
} from '../internal/tokens/token-provider.js';
|
|
13
|
+
import {
|
|
14
|
+
TokenValidator,
|
|
15
|
+
VALIDATION_RULES,
|
|
16
|
+
SEVERITY,
|
|
17
|
+
tokenValidator
|
|
18
|
+
} from '../internal/tokens/token-validator.js';
|
|
19
|
+
|
|
20
|
+
describe('TokenProvider', () => {
|
|
21
|
+
let provider;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
provider = new TokenProvider();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('Initialization', () => {
|
|
28
|
+
it('should create a new TokenProvider instance', () => {
|
|
29
|
+
expect(provider).toBeInstanceOf(TokenProvider);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should have default options', () => {
|
|
33
|
+
expect(provider.tokenPath).toBe('./design-tokens');
|
|
34
|
+
expect(provider.format).toBe(TOKEN_FORMATS.JSON);
|
|
35
|
+
expect(provider.tokens).toEqual({});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should accept custom options', () => {
|
|
39
|
+
const customProvider = new TokenProvider({
|
|
40
|
+
tokenPath: './custom-tokens',
|
|
41
|
+
format: TOKEN_FORMATS.CSS
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(customProvider.tokenPath).toBe('./custom-tokens');
|
|
45
|
+
expect(customProvider.format).toBe(TOKEN_FORMATS.CSS);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('Token Loading', () => {
|
|
50
|
+
it('should throw error when file not found', async () => {
|
|
51
|
+
await expect(provider.loadTokens('./non-existent.json'))
|
|
52
|
+
.rejects
|
|
53
|
+
.toThrow('Token file not found');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should categorize tokens correctly', () => {
|
|
57
|
+
const categorizeToken = provider.categorizeToken.bind(provider);
|
|
58
|
+
|
|
59
|
+
expect(categorizeToken('color-primary')).toBe(TOKEN_CATEGORIES.COLOR);
|
|
60
|
+
expect(categorizeToken('space-sm')).toBe(TOKEN_CATEGORIES.SPACING);
|
|
61
|
+
expect(categorizeToken('font-size-lg')).toBe(TOKEN_CATEGORIES.TYPOGRAPHY);
|
|
62
|
+
expect(categorizeToken('shadow-md')).toBe(TOKEN_CATEGORIES.SHADOW);
|
|
63
|
+
expect(categorizeToken('radius-lg')).toBe(TOKEN_CATEGORIES.RADIUS);
|
|
64
|
+
expect(categorizeToken('duration-fast')).toBe(TOKEN_CATEGORIES.ANIMATION);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should infer token types from values', () => {
|
|
68
|
+
const inferTokenType = provider.inferTokenType.bind(provider);
|
|
69
|
+
|
|
70
|
+
expect(inferTokenType('#ff0000')).toBe('color');
|
|
71
|
+
expect(inferTokenType('rgb(255, 0, 0)')).toBe('color');
|
|
72
|
+
expect(inferTokenType('hsl(0, 100%, 50%)')).toBe('color');
|
|
73
|
+
expect(inferTokenType('16px')).toBe('dimension');
|
|
74
|
+
expect(inferTokenType('1rem')).toBe('dimension');
|
|
75
|
+
expect(inferTokenType('300ms')).toBe('duration');
|
|
76
|
+
expect(inferTokenType('0.5s')).toBe('duration');
|
|
77
|
+
expect(inferTokenType(42)).toBe('number');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('Token Merging', () => {
|
|
82
|
+
it('should merge multiple token sets', () => {
|
|
83
|
+
const existing = {
|
|
84
|
+
color: { primary: { value: '#000' } },
|
|
85
|
+
spacing: { sm: { value: '8px' } }
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const newTokens = {
|
|
89
|
+
color: { secondary: { value: '#fff' } },
|
|
90
|
+
typography: { base: { value: '16px' } }
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const merged = provider.mergeTokens(existing, newTokens);
|
|
94
|
+
|
|
95
|
+
expect(merged.color.primary).toBeDefined();
|
|
96
|
+
expect(merged.color.secondary).toBeDefined();
|
|
97
|
+
expect(merged.spacing.sm).toBeDefined();
|
|
98
|
+
expect(merged.typography.base).toBeDefined();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('Token Export', () => {
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
provider.tokens = {
|
|
105
|
+
color: {
|
|
106
|
+
primary: { value: '#007bff', type: 'color' },
|
|
107
|
+
secondary: { value: '#6c757d', type: 'color' }
|
|
108
|
+
},
|
|
109
|
+
spacing: {
|
|
110
|
+
sm: { value: '8px', type: 'dimension' },
|
|
111
|
+
lg: { value: '16px', type: 'dimension' }
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should export to JSON format', () => {
|
|
117
|
+
const json = provider.exportTokens(TOKEN_FORMATS.JSON, { pretty: false });
|
|
118
|
+
const parsed = JSON.parse(json);
|
|
119
|
+
|
|
120
|
+
expect(parsed.color.primary.value).toBe('#007bff');
|
|
121
|
+
expect(parsed.spacing.lg.value).toBe('16px');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should export to CSS custom properties', () => {
|
|
125
|
+
const css = provider.exportTokens(TOKEN_FORMATS.CSS, {
|
|
126
|
+
selector: ':root',
|
|
127
|
+
prefix: 'atomix'
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(css).toContain(':root {');
|
|
131
|
+
expect(css).toContain('--atomix-color-primary: #007bff;');
|
|
132
|
+
expect(css).toContain('--atomix-spacing-lg: 16px;');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should export to W3C DTCG format', () => {
|
|
136
|
+
const dtcg = provider.exportTokens(TOKEN_FORMATS.W3C_DTCG);
|
|
137
|
+
|
|
138
|
+
expect(dtcg.$schema).toBe('https://design-tokens.org/schema.json');
|
|
139
|
+
expect(dtcg.tokens.color.primary.value).toBe('#007bff');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('TokenValidator', () => {
|
|
145
|
+
let validator;
|
|
146
|
+
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
validator = new TokenValidator();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('Initialization', () => {
|
|
152
|
+
it('should create a new TokenValidator instance', () => {
|
|
153
|
+
expect(validator).toBeInstanceOf(TokenValidator);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should register all built-in rules', () => {
|
|
157
|
+
expect(validator.rules.size).toBeGreaterThan(0);
|
|
158
|
+
expect(validator.rules.has('COLOR_CONTRAST')).toBe(true);
|
|
159
|
+
expect(validator.rules.has('SEMANTIC_NAMING')).toBe(true);
|
|
160
|
+
expect(validator.rules.has('NO_HARDCODED_COLORS')).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should have all rules enabled by default', () => {
|
|
164
|
+
expect(validator.enabledRules.length).toBeGreaterThan(0);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('Validation Rules', () => {
|
|
169
|
+
it('should validate color contrast', () => {
|
|
170
|
+
const tokens = {
|
|
171
|
+
color: {
|
|
172
|
+
text: { value: '#999999' },
|
|
173
|
+
background: { value: '#ffffff' }
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const result = validator.validate(tokens);
|
|
178
|
+
|
|
179
|
+
// Low contrast should be flagged
|
|
180
|
+
const contrastIssue = result.issues.find(i => i.rule === 'color-contrast');
|
|
181
|
+
expect(contrastIssue).toBeDefined();
|
|
182
|
+
expect(contrastIssue.severity).toBe(SEVERITY.ERROR);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should detect semantic naming issues', () => {
|
|
186
|
+
const tokens = {
|
|
187
|
+
color: {
|
|
188
|
+
blue: { value: '#0000ff' },
|
|
189
|
+
red: { value: '#ff0000' }
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const result = validator.validate(tokens);
|
|
194
|
+
|
|
195
|
+
const namingIssue = result.issues.find(i => i.rule === 'semantic-naming');
|
|
196
|
+
expect(namingIssue).toBeDefined();
|
|
197
|
+
expect(namingIssue.severity).toBe(SEVERITY.WARNING);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should pass semantic naming for brand colors', () => {
|
|
201
|
+
const tokens = {
|
|
202
|
+
color: {
|
|
203
|
+
brandBlue: { value: '#0000ff' },
|
|
204
|
+
primaryRed: { value: '#ff0000' }
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const result = validator.validate(tokens);
|
|
209
|
+
|
|
210
|
+
// Filter out token-completeness warnings which are expected
|
|
211
|
+
const namingIssue = result.issues.find(i => i.rule === 'semantic-naming');
|
|
212
|
+
// Should not flag brand colors or primary/secondary prefixed colors
|
|
213
|
+
expect(namingIssue).toBeUndefined();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should detect hardcoded colors in code', () => {
|
|
217
|
+
const codeContent = `
|
|
218
|
+
const styles = {
|
|
219
|
+
color: '#ff0000',
|
|
220
|
+
backgroundColor: 'rgb(0, 123, 255)',
|
|
221
|
+
borderColor: 'hsl(120, 100%, 50%)'
|
|
222
|
+
};
|
|
223
|
+
`;
|
|
224
|
+
|
|
225
|
+
const result = validator.validate({}, { codeContent });
|
|
226
|
+
|
|
227
|
+
const hardcodedIssue = result.issues.find(i => i.rule === 'no-hardcoded-colors');
|
|
228
|
+
expect(hardcodedIssue).toBeDefined();
|
|
229
|
+
expect(hardcodedIssue.matches).toHaveLength(3);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should validate token completeness', () => {
|
|
233
|
+
const tokens = {
|
|
234
|
+
color: { primary: { value: '#000' } }
|
|
235
|
+
// Missing spacing and typography
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const result = validator.validate(tokens);
|
|
239
|
+
|
|
240
|
+
const completenessIssues = result.issues.filter(i => i.rule === 'token-completeness');
|
|
241
|
+
expect(completenessIssues.length).toBeGreaterThan(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should detect duplicate tokens', () => {
|
|
245
|
+
const tokens = {
|
|
246
|
+
color: {
|
|
247
|
+
primary: { value: '#007bff' },
|
|
248
|
+
main: { value: '#007bff' }
|
|
249
|
+
},
|
|
250
|
+
spacing: {
|
|
251
|
+
sm: { value: '8px' },
|
|
252
|
+
small: { value: '8px' }
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const result = validator.validate(tokens);
|
|
257
|
+
|
|
258
|
+
const duplicateIssues = result.issues.filter(i => i.rule === 'duplicate-detection');
|
|
259
|
+
expect(duplicateIssues.length).toBeGreaterThan(0);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('Rule Management', () => {
|
|
264
|
+
it('should toggle rules on/off', () => {
|
|
265
|
+
const initialCount = validator.enabledRules.length;
|
|
266
|
+
|
|
267
|
+
validator.toggleRule('COLOR_CONTRAST', false);
|
|
268
|
+
expect(validator.enabledRules).not.toContain('COLOR_CONTRAST');
|
|
269
|
+
|
|
270
|
+
validator.toggleRule('COLOR_CONTRAST', true);
|
|
271
|
+
expect(validator.enabledRules).toContain('COLOR_CONTRAST');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should allow registering custom rules', () => {
|
|
275
|
+
const customRule = {
|
|
276
|
+
name: 'custom-rule',
|
|
277
|
+
description: 'Custom validation rule',
|
|
278
|
+
severity: SEVERITY.INFO,
|
|
279
|
+
validate: (tokens) => []
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
validator.registerRule('CUSTOM_RULE', customRule);
|
|
283
|
+
|
|
284
|
+
expect(validator.rules.has('CUSTOM_RULE')).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should reject invalid rules', () => {
|
|
288
|
+
expect(() => {
|
|
289
|
+
validator.registerRule('INVALID', { name: 'test' });
|
|
290
|
+
}).toThrow('must have name, validate, and severity');
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe('Validation Report', () => {
|
|
295
|
+
it('should generate formatted report', () => {
|
|
296
|
+
const tokens = {
|
|
297
|
+
color: {
|
|
298
|
+
lowContrast: { value: '#ccc' },
|
|
299
|
+
background: { value: '#fff' }
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const result = validator.validate(tokens);
|
|
304
|
+
const report = validator.getReport(result);
|
|
305
|
+
|
|
306
|
+
expect(report).toContain('Token Validation Report');
|
|
307
|
+
expect(report).toContain('Status:');
|
|
308
|
+
expect(report).toContain('Summary:');
|
|
309
|
+
expect(report).toContain('Errors:');
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('Integration Tests', () => {
|
|
315
|
+
it('should load and validate tokens together', async () => {
|
|
316
|
+
const provider = new TokenProvider();
|
|
317
|
+
const validator = new TokenValidator();
|
|
318
|
+
|
|
319
|
+
// Simulate loading tokens
|
|
320
|
+
provider.tokens = {
|
|
321
|
+
color: {
|
|
322
|
+
primary: { value: '#007bff', type: 'color' },
|
|
323
|
+
success: { value: '#22c55e', type: 'color' }
|
|
324
|
+
},
|
|
325
|
+
spacing: {
|
|
326
|
+
4: { value: '1rem', type: 'dimension' },
|
|
327
|
+
8: { value: '2rem', type: 'dimension' }
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const tokens = provider.getAllTokens();
|
|
332
|
+
const result = validator.validate(tokens);
|
|
333
|
+
|
|
334
|
+
expect(result).toBeDefined();
|
|
335
|
+
expect(typeof result.valid).toBe('boolean');
|
|
336
|
+
expect(result.summary).toBeDefined();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should validate component with loaded tokens', () => {
|
|
340
|
+
const provider = new TokenProvider();
|
|
341
|
+
const validator = new TokenValidator();
|
|
342
|
+
|
|
343
|
+
provider.tokens = {
|
|
344
|
+
color: {
|
|
345
|
+
primary: { value: '#007bff' }
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const cleanCode = `
|
|
350
|
+
const Component = () => {
|
|
351
|
+
return <div style={{ color: 'var(--color-primary)' }} />;
|
|
352
|
+
};
|
|
353
|
+
`;
|
|
354
|
+
|
|
355
|
+
const result = validator.validateComponent(cleanCode, provider.getAllTokens());
|
|
356
|
+
|
|
357
|
+
// Should not have hardcoded color errors
|
|
358
|
+
const hardcodedIssue = result.issues.find(i => i.rule === 'no-hardcoded-colors');
|
|
359
|
+
expect(hardcodedIssue).toBeUndefined();
|
|
360
|
+
});
|
|
361
|
+
});
|
|
@@ -30,13 +30,13 @@ describe('CLI Utils', () => {
|
|
|
30
30
|
it('should accept valid relative paths', () => {
|
|
31
31
|
const result = validatePath('./src/components', tempDir);
|
|
32
32
|
expect(result.isValid).toBe(true);
|
|
33
|
-
expect(result.safePath).
|
|
33
|
+
expect(result.safePath != null ? result.safePath : '').toMatch(/src\/components/);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
it('should reject paths outside project directory', () => {
|
|
37
37
|
const result = validatePath('../../etc/passwd', tempDir);
|
|
38
38
|
expect(result.isValid).toBe(false);
|
|
39
|
-
expect(result.error).
|
|
39
|
+
expect(result.error).toMatch(/outside|traversal/);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
it('should reject sensitive files', () => {
|
|
@@ -54,7 +54,7 @@ describe('CLI Utils', () => {
|
|
|
54
54
|
it('should normalize paths correctly', () => {
|
|
55
55
|
const result = validatePath('./src/../src/components', tempDir);
|
|
56
56
|
expect(result.isValid).toBe(true);
|
|
57
|
-
expect(result.safePath).
|
|
57
|
+
expect(result.safePath != null ? result.safePath : '').toMatch(/src\/components/);
|
|
58
58
|
});
|
|
59
59
|
});
|
|
60
60
|
|
|
@@ -69,8 +69,8 @@ describe('CLI Utils', () => {
|
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
it('should reject invalid names', () => {
|
|
72
|
-
const invalidNames = ['
|
|
73
|
-
|
|
72
|
+
const invalidNames = ['123Button', '', '1', 'Component', 'React'];
|
|
73
|
+
|
|
74
74
|
invalidNames.forEach(name => {
|
|
75
75
|
const result = validateComponentName(name);
|
|
76
76
|
expect(result.isValid).toBe(false);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomix CLI - Benchmark Command
|
|
3
|
+
* Profiles CLI speed and provides performance metrics
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
import { telemetry } from '../utils/telemetry.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Benchmark action handler
|
|
12
|
+
*/
|
|
13
|
+
export async function benchmarkAction(options) {
|
|
14
|
+
logger.info(chalk.blue('📊 CLI Performance Benchmark...'));
|
|
15
|
+
|
|
16
|
+
const logs = await telemetry.getLogs();
|
|
17
|
+
|
|
18
|
+
if (logs.length === 0) {
|
|
19
|
+
logger.info('Benchmark collects metrics from previous CLI runs.');
|
|
20
|
+
logger.warn('No performance data found. Run some commands first!');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Calculate stats
|
|
25
|
+
const stats = calculateStats(logs);
|
|
26
|
+
|
|
27
|
+
// Display results
|
|
28
|
+
displayBenchmarkResults(stats);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Calculate performance statistics from logs
|
|
33
|
+
*/
|
|
34
|
+
function calculateStats(logs) {
|
|
35
|
+
const byCommand = {};
|
|
36
|
+
|
|
37
|
+
logs.forEach(log => {
|
|
38
|
+
if (!byCommand[log.command]) {
|
|
39
|
+
byCommand[log.command] = {
|
|
40
|
+
name: log.command,
|
|
41
|
+
runs: 0,
|
|
42
|
+
successes: 0,
|
|
43
|
+
totalDuration: 0,
|
|
44
|
+
min: Infinity,
|
|
45
|
+
max: -Infinity,
|
|
46
|
+
durations: []
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const cmd = byCommand[log.command];
|
|
51
|
+
cmd.runs++;
|
|
52
|
+
if (log.success) cmd.successes++;
|
|
53
|
+
cmd.totalDuration += log.duration;
|
|
54
|
+
cmd.min = Math.min(cmd.min, log.duration);
|
|
55
|
+
cmd.max = Math.max(cmd.max, log.duration);
|
|
56
|
+
cmd.durations.push(log.duration);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Calculate averages and percentiles
|
|
60
|
+
Object.values(byCommand).forEach(cmd => {
|
|
61
|
+
cmd.avg = parseFloat((cmd.totalDuration / cmd.runs).toFixed(2));
|
|
62
|
+
|
|
63
|
+
// Simple 95th percentile
|
|
64
|
+
cmd.durations.sort((a, b) => a - b);
|
|
65
|
+
const p95Index = Math.floor(cmd.durations.length * 0.95);
|
|
66
|
+
cmd.p95 = cmd.durations[p95Index];
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return Object.values(byCommand);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Display benchmark results in a clean table
|
|
74
|
+
*/
|
|
75
|
+
function displayBenchmarkResults(stats) {
|
|
76
|
+
console.log('\n' + chalk.bold.underline('CLI Performance Metrics (Local)'));
|
|
77
|
+
console.log(chalk.gray('Last 100 commands recorded\n'));
|
|
78
|
+
|
|
79
|
+
// Header
|
|
80
|
+
console.log(
|
|
81
|
+
chalk.bold(
|
|
82
|
+
'Command'.padEnd(15) +
|
|
83
|
+
'Runs'.padEnd(8) +
|
|
84
|
+
'Avg (ms)'.padEnd(12) +
|
|
85
|
+
'P95 (ms)'.padEnd(12) +
|
|
86
|
+
'Success Rate'
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
console.log(chalk.gray('-'.repeat(60)));
|
|
90
|
+
|
|
91
|
+
stats.forEach(s => {
|
|
92
|
+
const successRate = ((s.successes / s.runs) * 100).toFixed(0) + '%';
|
|
93
|
+
const color = s.avg < 1000 ? chalk.green : s.avg < 2000 ? chalk.yellow : chalk.red;
|
|
94
|
+
|
|
95
|
+
console.log(
|
|
96
|
+
s.name.padEnd(15) +
|
|
97
|
+
s.runs.toString().padEnd(8) +
|
|
98
|
+
color(s.avg.toString().padEnd(12)) +
|
|
99
|
+
color(s.p95.toString().padEnd(12)) +
|
|
100
|
+
(s.successes === s.runs ? chalk.green(successRate) : chalk.yellow(successRate))
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
console.log('\n' + chalk.blue('Performance Budget: ') + chalk.bold('< 2000ms per command'));
|
|
105
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomix CLI Build Theme Command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve, basename, dirname } from 'path';
|
|
6
|
+
import chokidar from 'chokidar';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
import { AtomixCLIError } from '../utils/error.js';
|
|
9
|
+
import { filesystem } from '../internal/filesystem.js';
|
|
10
|
+
import { themeCompiler } from '../internal/compiler.js';
|
|
11
|
+
import { sanitizeInput } from '../utils/helpers.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Action logic for building a theme
|
|
15
|
+
* @param {string} themePath - Input path to theme SCSS
|
|
16
|
+
* @param {object} options - Command options
|
|
17
|
+
*/
|
|
18
|
+
export async function buildThemeAction(themePath, options) {
|
|
19
|
+
const spinner = logger.spinner('Initializing theme build...').start();
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const sanitizedThemePath = sanitizeInput(themePath);
|
|
23
|
+
const themeValidation = filesystem.validatePath(sanitizedThemePath);
|
|
24
|
+
|
|
25
|
+
if (!themeValidation.isValid) {
|
|
26
|
+
throw new AtomixCLIError(
|
|
27
|
+
themeValidation.error,
|
|
28
|
+
'INVALID_PATH',
|
|
29
|
+
[
|
|
30
|
+
'Ensure theme path is within the project directory',
|
|
31
|
+
'Avoid sensitive or absolute system paths'
|
|
32
|
+
]
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Default output directory; may overwrite app build—use -o dist-theme for custom themes.
|
|
37
|
+
const outputDir = options.output ?? './dist';
|
|
38
|
+
const sanitizedOutput = sanitizeInput(outputDir);
|
|
39
|
+
const outputValidation = filesystem.validatePath(sanitizedOutput);
|
|
40
|
+
|
|
41
|
+
if (!outputValidation.isValid) {
|
|
42
|
+
throw new AtomixCLIError(
|
|
43
|
+
outputValidation.error,
|
|
44
|
+
'INVALID_PATH',
|
|
45
|
+
['Use a project-relative directory for output']
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Resolve index.scss
|
|
50
|
+
const indexPath = sanitizedThemePath.endsWith('.scss')
|
|
51
|
+
? resolve(themeValidation.safePath)
|
|
52
|
+
: resolve(themeValidation.safePath, 'index.scss');
|
|
53
|
+
|
|
54
|
+
if (!(await filesystem.exists(indexPath))) {
|
|
55
|
+
throw new AtomixCLIError(
|
|
56
|
+
`Theme file not found: ${indexPath}`,
|
|
57
|
+
'THEME_NOT_FOUND',
|
|
58
|
+
['Check if the file path is correct', 'Ensure the file has a .scss extension']
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const themeName = basename(dirname(indexPath));
|
|
63
|
+
|
|
64
|
+
const performBuild = async () => {
|
|
65
|
+
try {
|
|
66
|
+
await themeCompiler.compile(indexPath, outputValidation.safePath, {
|
|
67
|
+
minify: options.minify,
|
|
68
|
+
sourcemap: options.sourcemap,
|
|
69
|
+
analyze: options.analyze,
|
|
70
|
+
themeName,
|
|
71
|
+
logger: {
|
|
72
|
+
info: (msg) => { spinner.text = msg; },
|
|
73
|
+
debug: (msg) => logger.debug(msg)
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
spinner.succeed(`Theme '${themeName}' built successfully`);
|
|
78
|
+
if (options.watch) {
|
|
79
|
+
logger.info('\n👁️ Watch mode enabled. Rebuilding on changes...');
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
spinner.fail(`Build failed: ${error.message}`);
|
|
83
|
+
if (!options.watch) throw error;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Initial build
|
|
88
|
+
await performBuild();
|
|
89
|
+
|
|
90
|
+
// Watch mode
|
|
91
|
+
if (options.watch) {
|
|
92
|
+
const watcher = chokidar.watch([dirname(indexPath)], {
|
|
93
|
+
ignored: /node_modules/,
|
|
94
|
+
persistent: true,
|
|
95
|
+
ignoreInitial: true
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
watcher.on('all', async (event, path) => {
|
|
99
|
+
logger.debug(`File ${event}: ${path}`);
|
|
100
|
+
spinner.start('Rebuilding theme...');
|
|
101
|
+
await performBuild();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Only allowed direct success exit: clean shutdown in watch mode.
|
|
105
|
+
process.on('SIGINT', () => {
|
|
106
|
+
watcher.close();
|
|
107
|
+
process.exit(0);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
} catch (error) {
|
|
112
|
+
spinner.fail('Operation failed');
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|