@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
|
@@ -3,10 +3,65 @@
|
|
|
3
3
|
* Utilities for safe file and path operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { resolve, normalize, isAbsolute, relative } from 'path';
|
|
7
|
-
import { access } from 'fs/promises';
|
|
6
|
+
import { resolve, normalize, isAbsolute, relative, dirname } from 'path';
|
|
7
|
+
import { access, writeFile, mkdir, readFile } from 'fs/promises';
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import {
|
|
11
|
+
validateSecurePath,
|
|
12
|
+
createBackup,
|
|
13
|
+
retryWithBackoff
|
|
14
|
+
} from '../utils/security.js';
|
|
8
15
|
|
|
9
16
|
export const filesystem = {
|
|
17
|
+
/**
|
|
18
|
+
* Safe file write with dry-run support, backup, and retry mechanism
|
|
19
|
+
* @param {string} path - Path to write to
|
|
20
|
+
* @param {string} content - Content to write
|
|
21
|
+
* @param {object} options - Options
|
|
22
|
+
*/
|
|
23
|
+
async writeFile(path, content, options = {}) {
|
|
24
|
+
if (process.env.ATOMIX_DRY_RUN === 'true') {
|
|
25
|
+
logger.info(`${chalk.cyan('[DRY RUN]')} Would write file: ${chalk.bold(path)}`);
|
|
26
|
+
if (options.debug) {
|
|
27
|
+
logger.debug('Content:', content);
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Validate path security
|
|
33
|
+
const pathValidation = validateSecurePath(path);
|
|
34
|
+
if (!pathValidation.isValid) {
|
|
35
|
+
throw new Error(`Security validation failed for path ${path}: ${pathValidation.error}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const safePath = pathValidation.safePath;
|
|
39
|
+
|
|
40
|
+
const writeOperation = async () => {
|
|
41
|
+
try {
|
|
42
|
+
const dir = dirname(safePath);
|
|
43
|
+
await mkdir(dir, { recursive: true });
|
|
44
|
+
|
|
45
|
+
// Create backup if file exists and backup is enabled
|
|
46
|
+
if (options.backup !== false && await this.exists(safePath)) {
|
|
47
|
+
try {
|
|
48
|
+
await createBackup(safePath);
|
|
49
|
+
} catch (backupError) {
|
|
50
|
+
logger.warn(`Backup failed for ${safePath}: ${backupError.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await writeFile(safePath, content, options);
|
|
55
|
+
return true;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new Error(`Failed to write file ${safePath}: ${error.message}`);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Use retry mechanism for file operations
|
|
62
|
+
return retryWithBackoff(writeOperation, options.maxRetries || 2, options.retryDelay || 100);
|
|
63
|
+
},
|
|
64
|
+
|
|
10
65
|
/**
|
|
11
66
|
* Validates and resolves a path within the project directory
|
|
12
67
|
* @param {string} inputPath - The path to validate
|
|
@@ -54,5 +109,50 @@ export const filesystem = {
|
|
|
54
109
|
} catch {
|
|
55
110
|
return false;
|
|
56
111
|
}
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create directory recursively with dry-run support
|
|
116
|
+
* @param {string} path - Directory path to create
|
|
117
|
+
* @param {object} options - Options
|
|
118
|
+
* @returns {Promise<boolean>} Success status
|
|
119
|
+
*/
|
|
120
|
+
async createDirectory(path, options = {}) {
|
|
121
|
+
if (process.env.ATOMIX_DRY_RUN === 'true') {
|
|
122
|
+
logger.info(`${chalk.cyan('[DRY RUN]')} Would create directory: ${chalk.bold(path)}`);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const pathValidation = validateSecurePath(path);
|
|
127
|
+
if (!pathValidation.isValid) {
|
|
128
|
+
throw new Error(`Security validation failed for path ${path}: ${pathValidation.error}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const safePath = pathValidation.safePath;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await mkdir(safePath, { recursive: true });
|
|
135
|
+
logger.debug(`Created directory: ${safePath}`);
|
|
136
|
+
return true;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw new Error(`Failed to create directory ${safePath}: ${error.message}`);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Read file content with encoding
|
|
144
|
+
* @param {string} path - File path
|
|
145
|
+
* @param {string} encoding - File encoding (default: utf8)
|
|
146
|
+
* @returns {Promise<string>} File content
|
|
147
|
+
*/
|
|
148
|
+
async readFile(path, encoding = 'utf8') {
|
|
149
|
+
try {
|
|
150
|
+
const content = await readFile(path, encoding);
|
|
151
|
+
return content;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw new Error(`Failed to read file ${path}: ${error.message}`);
|
|
154
|
+
}
|
|
57
155
|
}
|
|
58
156
|
};
|
|
157
|
+
|
|
158
|
+
export default filesystem;
|
|
@@ -3,16 +3,26 @@
|
|
|
3
3
|
* Core logic for scaffolding components and assets
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { readFile } from 'fs/promises';
|
|
7
7
|
import { existsSync } from 'fs';
|
|
8
8
|
import { join } from 'path';
|
|
9
|
-
import {
|
|
9
|
+
import { templateEngine, COMPLEXITY_LEVELS } from './template-engine.js';
|
|
10
|
+
import { detectFramework } from '../utils/detector.js';
|
|
11
|
+
import { filesystem } from './filesystem.js';
|
|
12
|
+
import { aiEngine } from './ai-engine.js';
|
|
13
|
+
import {
|
|
14
|
+
sanitizeInput,
|
|
15
|
+
validateComponentNameSecure,
|
|
16
|
+
RateLimiter
|
|
17
|
+
} from '../utils/security.js';
|
|
18
|
+
import { AtomixCLIError } from '../utils/error.js';
|
|
19
|
+
import { tokenProvider } from './tokens/token-provider.js';
|
|
20
|
+
import { tokenValidator } from './tokens/token-validator.js';
|
|
21
|
+
import { componentValidator } from './component-validator.js';
|
|
22
|
+
import { generateComponentStylesPackage } from './itcss-generator.js';
|
|
23
|
+
import { generateHookFile } from './hook-generator.js';
|
|
10
24
|
|
|
11
|
-
export
|
|
12
|
-
SIMPLE: { name: 'simple', template: 'simple' },
|
|
13
|
-
MEDIUM: { name: 'medium', template: 'medium' },
|
|
14
|
-
COMPLEX: { name: 'complex', template: 'complex' }
|
|
15
|
-
};
|
|
25
|
+
export { COMPLEXITY_LEVELS };
|
|
16
26
|
|
|
17
27
|
export const COMPONENT_FEATURES = {
|
|
18
28
|
TYPESCRIPT: { name: 'typescript', default: true },
|
|
@@ -23,9 +33,16 @@ export const COMPONENT_FEATURES = {
|
|
|
23
33
|
ACCESSIBILITY: { name: 'accessibility', default: true }
|
|
24
34
|
};
|
|
25
35
|
|
|
36
|
+
// Global rate limiter for AI operations
|
|
37
|
+
const aiRateLimiter = new RateLimiter(5, 60000); // 5 requests per minute
|
|
38
|
+
|
|
26
39
|
export const generator = {
|
|
27
40
|
/**
|
|
28
41
|
* Generates component files based on options
|
|
42
|
+
* @param {string} name - Component name
|
|
43
|
+
* @param {Object} options - Generation options
|
|
44
|
+
* @returns {Promise<string>} Path to generated component
|
|
45
|
+
* @throws {AtomixCLIError} If generation fails
|
|
29
46
|
*/
|
|
30
47
|
async generateComponent(name, options = {}) {
|
|
31
48
|
const {
|
|
@@ -35,52 +52,338 @@ export const generator = {
|
|
|
35
52
|
logger
|
|
36
53
|
} = options;
|
|
37
54
|
|
|
38
|
-
|
|
39
|
-
|
|
55
|
+
// Sanitize and validate component name
|
|
56
|
+
const sanitizedName = sanitizeInput(name, 'componentName');
|
|
57
|
+
const validation = validateComponentNameSecure(sanitizedName);
|
|
58
|
+
if (!validation.isValid) {
|
|
59
|
+
throw new AtomixCLIError(
|
|
60
|
+
`Component name validation failed: ${validation.error}`,
|
|
61
|
+
'INVALID_COMPONENT_NAME',
|
|
62
|
+
[
|
|
63
|
+
'Use PascalCase (e.g., MyComponent, Button)',
|
|
64
|
+
'Start with a letter (not numbers)',
|
|
65
|
+
'Avoid special characters except letters and numbers'
|
|
66
|
+
]
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Detect framework
|
|
71
|
+
let framework;
|
|
72
|
+
try {
|
|
73
|
+
framework = await detectFramework();
|
|
74
|
+
if (logger) logger.debug(`Detected framework: ${framework}`);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw new AtomixCLIError(
|
|
77
|
+
`Framework detection failed: ${error.message}`,
|
|
78
|
+
'FRAMEWORK_DETECTION_FAILED',
|
|
79
|
+
[
|
|
80
|
+
'Ensure package.json exists in project root',
|
|
81
|
+
'Check for React, Next.js, or vanilla project structure',
|
|
82
|
+
'Run `atomix doctor` to diagnose environment issues'
|
|
83
|
+
]
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Load design tokens if available
|
|
88
|
+
let availableTokens = {};
|
|
89
|
+
try {
|
|
90
|
+
const tokenPaths = [
|
|
91
|
+
'./design-tokens/tokens.json',
|
|
92
|
+
'./src/design-tokens/tokens.json',
|
|
93
|
+
'./tokens.json'
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
for (const tokenPath of tokenPaths) {
|
|
97
|
+
if (existsSync(join(process.cwd(), tokenPath))) {
|
|
98
|
+
availableTokens = await tokenProvider.loadTokens(tokenPath);
|
|
99
|
+
if (logger) logger.debug(`Loaded design tokens from ${tokenPath}`);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (logger) logger.debug(`Token loading skipped: ${error.message}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const componentPath = join(outputPath, sanitizedName);
|
|
40
108
|
|
|
41
109
|
// 1. Generate Component File
|
|
42
|
-
|
|
43
|
-
let
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
110
|
+
let content;
|
|
111
|
+
let ext = '.tsx';
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
if (framework === 'vanilla') {
|
|
115
|
+
const templateFn = templateEngine.selectTemplate('vanilla', complexity, 'component');
|
|
116
|
+
content = templateEngine.render(templateFn, sanitizedName);
|
|
117
|
+
ext = '.html';
|
|
118
|
+
} else if (framework === 'next') {
|
|
119
|
+
const templateFn = templateEngine.selectTemplate('next', complexity, 'component');
|
|
120
|
+
content = templateEngine.render(templateFn, sanitizedName);
|
|
121
|
+
} else {
|
|
122
|
+
// Default to React
|
|
123
|
+
const templateFn = templateEngine.selectTemplate('react', complexity, 'component');
|
|
124
|
+
content = templateEngine.render(templateFn, sanitizedName);
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof AtomixCLIError) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
throw new AtomixCLIError(
|
|
131
|
+
`Failed to select template: ${error.message}`,
|
|
132
|
+
'TEMPLATE_SELECTION_FAILED',
|
|
133
|
+
[
|
|
134
|
+
`Check if complexity level '${complexity}' is valid`,
|
|
135
|
+
`Verify framework '${framework}' is supported`,
|
|
136
|
+
'Run `atomix doctor` to check template availability'
|
|
137
|
+
]
|
|
138
|
+
);
|
|
50
139
|
}
|
|
51
140
|
|
|
52
|
-
|
|
53
|
-
|
|
141
|
+
// Validate generated component against design system rules
|
|
142
|
+
const componentValidation = tokenValidator.validateComponent(content, availableTokens);
|
|
143
|
+
if (!componentValidation.valid && logger) {
|
|
144
|
+
logger.debug(`Component validation: ${componentValidation.issues.length} issues found`);
|
|
145
|
+
}
|
|
54
146
|
|
|
55
|
-
|
|
56
|
-
|
|
147
|
+
await filesystem.writeFile(join(componentPath, `${sanitizedName}${ext}`), content, 'utf8');
|
|
148
|
+
if (logger) logger.debug(`Created ${sanitizedName}${ext}`);
|
|
149
|
+
|
|
150
|
+
// 2. Index File (only for React/Next)
|
|
151
|
+
if (framework !== 'vanilla') {
|
|
152
|
+
try {
|
|
153
|
+
const indexTemplateFn = templateEngine.selectTemplate(framework, complexity, 'index');
|
|
154
|
+
const indexContent = templateEngine.render(indexTemplateFn, sanitizedName);
|
|
155
|
+
await filesystem.writeFile(join(componentPath, 'index.ts'), indexContent, 'utf8');
|
|
156
|
+
if (logger) logger.debug(`Created index.ts`);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
throw new AtomixCLIError(
|
|
159
|
+
`Failed to generate index file: ${error.message}`,
|
|
160
|
+
'INDEX_GENERATION_FAILED',
|
|
161
|
+
[
|
|
162
|
+
'Check index template exists for framework',
|
|
163
|
+
'Verify template exports in template files',
|
|
164
|
+
'Try generating without index feature'
|
|
165
|
+
]
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
57
169
|
|
|
58
170
|
// 3. Optional Features
|
|
59
171
|
if (features.includes('storybook')) {
|
|
60
|
-
|
|
172
|
+
try {
|
|
173
|
+
const storyTemplateFn = templateEngine.selectTemplate(framework, complexity, 'story');
|
|
174
|
+
const storyContent = templateEngine.render(storyTemplateFn, sanitizedName);
|
|
175
|
+
await filesystem.writeFile(join(componentPath, `${sanitizedName}.stories.tsx`), storyContent, 'utf8');
|
|
176
|
+
if (logger) logger.debug(`Created ${sanitizedName}.stories.tsx`);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
throw new AtomixCLIError(
|
|
179
|
+
`Failed to generate Storybook story: ${error.message}`,
|
|
180
|
+
'STORYBOOK_GENERATION_FAILED',
|
|
181
|
+
[
|
|
182
|
+
'Check storybook template exists',
|
|
183
|
+
'Verify story feature is supported for this framework',
|
|
184
|
+
'Try generating without --storybook flag'
|
|
185
|
+
]
|
|
186
|
+
);
|
|
187
|
+
}
|
|
61
188
|
}
|
|
62
189
|
|
|
63
190
|
if (features.includes('tests')) {
|
|
64
|
-
|
|
191
|
+
try {
|
|
192
|
+
const testTemplateFn = templateEngine.selectTemplate(framework, complexity, 'test');
|
|
193
|
+
const testContent = templateEngine.render(testTemplateFn, sanitizedName);
|
|
194
|
+
await filesystem.writeFile(join(componentPath, `${sanitizedName}.test.tsx`), testContent, 'utf8');
|
|
195
|
+
if (logger) logger.debug(`Created ${sanitizedName}.test.tsx`);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
throw new AtomixCLIError(
|
|
198
|
+
`Failed to generate test file: ${error.message}`,
|
|
199
|
+
'TEST_GENERATION_FAILED',
|
|
200
|
+
[
|
|
201
|
+
'Check test template exists',
|
|
202
|
+
'Verify test feature is supported for this framework',
|
|
203
|
+
'Try generating without --tests flag'
|
|
204
|
+
]
|
|
205
|
+
);
|
|
206
|
+
}
|
|
65
207
|
}
|
|
66
208
|
|
|
67
|
-
if (features.includes('hook')) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
209
|
+
if (features.includes('hook') && framework !== 'vanilla') {
|
|
210
|
+
try {
|
|
211
|
+
const hookDir = join(outputPath, '..', 'lib', 'composables');
|
|
212
|
+
const hookTemplateFn = templateEngine.selectTemplate(framework, complexity, 'hook');
|
|
213
|
+
const hookContent = templateEngine.render(hookTemplateFn, sanitizedName);
|
|
214
|
+
await filesystem.writeFile(join(hookDir, `use${sanitizedName}.ts`), hookContent, 'utf8');
|
|
215
|
+
if (logger) logger.debug(`Created use${sanitizedName}.ts`);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
throw new AtomixCLIError(
|
|
218
|
+
`Failed to generate composable hook: ${error.message}`,
|
|
219
|
+
'HOOK_GENERATION_FAILED',
|
|
220
|
+
[
|
|
221
|
+
'Check hook template exists',
|
|
222
|
+
'Verify hook feature is supported for this framework',
|
|
223
|
+
'Try generating without --hook flag'
|
|
224
|
+
]
|
|
225
|
+
);
|
|
226
|
+
}
|
|
71
227
|
}
|
|
72
228
|
|
|
73
|
-
// 4. Styles (ITCSS)
|
|
229
|
+
// 4. Styles (ITCSS) - Enhanced with auto-generation
|
|
74
230
|
if (features.includes('styles')) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
231
|
+
try {
|
|
232
|
+
const stylesResult = await generateComponentStylesPackage(sanitizedName, process.cwd(), {
|
|
233
|
+
force: options.force || false
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (logger && stylesResult.created.length > 0) {
|
|
237
|
+
logger.debug(`Created ${stylesResult.created.length} ITCSS style files`);
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
throw new AtomixCLIError(
|
|
241
|
+
`Failed to generate ITCSS styles: ${error.message}`,
|
|
242
|
+
'STYLE_GENERATION_FAILED',
|
|
243
|
+
[
|
|
244
|
+
'Check SCSS templates exist',
|
|
245
|
+
'Verify styles directory structure',
|
|
246
|
+
'Try generating without --styles flag'
|
|
247
|
+
]
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 5. Composable Hook - Enhanced generation
|
|
253
|
+
if (features.includes('hook') && framework !== 'vanilla') {
|
|
254
|
+
try {
|
|
255
|
+
const hookResult = await generateHookFile(sanitizedName, process.cwd(), {
|
|
256
|
+
force: options.force || false
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (logger && hookResult.created.length > 0) {
|
|
260
|
+
logger.debug(`Created ${hookResult.created.length} composable hook files`);
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
throw new AtomixCLIError(
|
|
264
|
+
`Failed to generate composable hook: ${error.message}`,
|
|
265
|
+
'HOOK_GENERATION_FAILED',
|
|
266
|
+
[
|
|
267
|
+
'Check hook template exists',
|
|
268
|
+
'Verify hook feature is supported for this framework',
|
|
269
|
+
'Try generating without --hook flag'
|
|
270
|
+
]
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return componentPath;
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Generates component files using AI based on a prompt
|
|
280
|
+
* @param {string} name - Component name
|
|
281
|
+
* @param {string} prompt - AI prompt
|
|
282
|
+
* @param {Object} options - Generation options
|
|
283
|
+
* @returns {Promise<string>} Path to generated component
|
|
284
|
+
* @throws {AtomixCLIError} If generation fails
|
|
285
|
+
*/
|
|
286
|
+
async generateAIComponent(name, prompt, options = {}) {
|
|
287
|
+
const { outputPath, logger } = options;
|
|
288
|
+
|
|
289
|
+
// Apply rate limiting for AI operations
|
|
290
|
+
const userId = process.env.USER || 'anonymous';
|
|
291
|
+
if (!aiRateLimiter.checkLimit(userId)) {
|
|
292
|
+
throw new AtomixCLIError(
|
|
293
|
+
`Rate limit exceeded. Please wait before generating more AI components. Remaining: ${aiRateLimiter.getRemaining(userId)} seconds`,
|
|
294
|
+
'RATE_LIMIT_EXCEEDED',
|
|
295
|
+
[
|
|
296
|
+
'Wait for rate limit to reset (60 seconds)',
|
|
297
|
+
'Reduce frequency of AI component generation',
|
|
298
|
+
'Use regular generation instead of AI for simple components'
|
|
299
|
+
]
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Sanitize inputs
|
|
304
|
+
const sanitizedName = sanitizeInput(name, 'componentName');
|
|
305
|
+
sanitizeInput(prompt, 'prompt'); // Sanitize but use original
|
|
306
|
+
|
|
307
|
+
const validation = validateComponentNameSecure(sanitizedName);
|
|
308
|
+
if (!validation.isValid) {
|
|
309
|
+
throw new AtomixCLIError(
|
|
310
|
+
`Component name validation failed: ${validation.error}`,
|
|
311
|
+
'INVALID_COMPONENT_NAME',
|
|
312
|
+
[
|
|
313
|
+
'Use PascalCase (e.g., MyComponent, Button)',
|
|
314
|
+
'Start with a letter (not numbers)',
|
|
315
|
+
'Avoid special characters except letters and numbers'
|
|
316
|
+
]
|
|
317
|
+
);
|
|
318
|
+
}
|
|
80
319
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
320
|
+
const componentPath = join(outputPath, sanitizedName);
|
|
321
|
+
|
|
322
|
+
// Call AI Engine
|
|
323
|
+
let generated;
|
|
324
|
+
try {
|
|
325
|
+
generated = await aiEngine.generateComponent(name, prompt);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
throw new AtomixCLIError(
|
|
328
|
+
`AI generation failed: ${error.message}`,
|
|
329
|
+
'AI_GENERATION_FAILED',
|
|
330
|
+
[
|
|
331
|
+
'Check your internet connection',
|
|
332
|
+
'Verify AI engine credentials are configured',
|
|
333
|
+
'Try again in a few moments',
|
|
334
|
+
'Use regular generation as fallback'
|
|
335
|
+
]
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Write component file
|
|
340
|
+
try {
|
|
341
|
+
await filesystem.writeFile(join(componentPath, `${sanitizedName}.tsx`), generated.component, 'utf8');
|
|
342
|
+
if (logger) logger.debug(`Created ${sanitizedName}.tsx (AI)`);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
throw new AtomixCLIError(
|
|
345
|
+
`Failed to write AI-generated component: ${error.message}`,
|
|
346
|
+
'FILE_WRITE_FAILED',
|
|
347
|
+
[
|
|
348
|
+
'Check you have write permissions for the target directory',
|
|
349
|
+
'Ensure the path is valid and within project root',
|
|
350
|
+
'Verify disk space is available'
|
|
351
|
+
]
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Index file
|
|
356
|
+
try {
|
|
357
|
+
const indexTemplateFn = templateEngine.selectTemplate('react', 'medium', 'index');
|
|
358
|
+
const indexContent = templateEngine.render(indexTemplateFn, sanitizedName);
|
|
359
|
+
await filesystem.writeFile(join(componentPath, 'index.ts'), indexContent, 'utf8');
|
|
360
|
+
} catch (error) {
|
|
361
|
+
throw new AtomixCLIError(
|
|
362
|
+
`Failed to write index file: ${error.message}`,
|
|
363
|
+
'FILE_WRITE_FAILED',
|
|
364
|
+
[
|
|
365
|
+
'Check index template exists',
|
|
366
|
+
'Verify write permissions',
|
|
367
|
+
'AI generation may have incomplete templates'
|
|
368
|
+
]
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Optional files from AI
|
|
373
|
+
if (generated.styles) {
|
|
374
|
+
await filesystem.writeFile(join(componentPath, `${sanitizedName}.scss`), generated.styles, 'utf8');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (generated.tests) {
|
|
378
|
+
await filesystem.writeFile(join(componentPath, `${sanitizedName}.test.tsx`), generated.tests, 'utf8');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (generated.stories) {
|
|
382
|
+
await filesystem.writeFile(join(componentPath, `${sanitizedName}.stories.tsx`), generated.stories, 'utf8');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (generated.readme) {
|
|
386
|
+
await filesystem.writeFile(join(componentPath, 'README.md'), generated.readme, 'utf8');
|
|
84
387
|
}
|
|
85
388
|
|
|
86
389
|
return componentPath;
|
|
@@ -88,6 +391,9 @@ export const generator = {
|
|
|
88
391
|
|
|
89
392
|
/**
|
|
90
393
|
* Validates a generated component
|
|
394
|
+
* @param {string} name - Component name
|
|
395
|
+
* @param {string} componentPath - Path to component directory
|
|
396
|
+
* @returns {Promise<Object>} { valid: boolean, issues: string[] }
|
|
91
397
|
*/
|
|
92
398
|
async validate(name, componentPath) {
|
|
93
399
|
const issues = [];
|
|
@@ -99,11 +405,25 @@ export const generator = {
|
|
|
99
405
|
}
|
|
100
406
|
|
|
101
407
|
const content = await readFile(componentFile, 'utf8');
|
|
102
|
-
|
|
103
|
-
|
|
408
|
+
|
|
409
|
+
// Use new component validator for comprehensive checks
|
|
410
|
+
const validationResults = componentValidator.validate(content, name);
|
|
411
|
+
|
|
412
|
+
// Add all issues from component validator
|
|
413
|
+
for (const issue of validationResults.issues) {
|
|
414
|
+
issues.push(`[${issue.rule}] ${issue.message}${issue.suggestion ? ' - ' + issue.suggestion : ''}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Legacy checks (keep for backward compatibility)
|
|
418
|
+
// 1. Check for displayName (already covered by componentValidator)
|
|
419
|
+
// 2. Check for JSDoc documentation (already covered)
|
|
420
|
+
// 3. Check for TypeScript type definitions (already covered)
|
|
421
|
+
// 4. Check for forwardRef usage (already covered)
|
|
422
|
+
// 5. Check for Accessibility attributes (already covered)
|
|
423
|
+
// 6. Check for hardcoded colors (already covered)
|
|
104
424
|
|
|
105
425
|
return {
|
|
106
|
-
valid:
|
|
426
|
+
valid: validationResults.valid,
|
|
107
427
|
issues
|
|
108
428
|
};
|
|
109
429
|
}
|