@shohojdhara/atomix 0.4.6 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/atomix.css +61 -56
- 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 +93 -109
- package/dist/charts.js +141 -233
- package/dist/charts.js.map +1 -1
- package/dist/core.js +51 -46
- package/dist/core.js.map +1 -1
- package/dist/forms.js +51 -46
- package/dist/forms.js.map +1 -1
- package/dist/heavy.js +51 -46
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +6 -22
- package/dist/index.esm.js +141 -234
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +144 -237
- 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 +1 -1
- package/scripts/atomix-cli.js +40 -1875
- package/scripts/cli/commands/build-theme.js +112 -0
- package/scripts/cli/commands/generate.js +97 -0
- package/scripts/cli/commands/init.js +46 -0
- package/scripts/cli/internal/compiler.js +114 -0
- package/scripts/cli/internal/filesystem.js +58 -0
- package/scripts/cli/internal/generator.js +110 -0
- package/scripts/cli/internal/wizard.js +61 -0
- package/scripts/cli/utils/error.js +47 -0
- package/scripts/cli/utils/helpers.js +43 -0
- package/scripts/cli/utils/logger.js +75 -0
- package/scripts/cli/utils/validation.js +71 -0
- package/src/components/AtomixGlass/AtomixGlass.test.tsx +37 -3
- package/src/components/AtomixGlass/AtomixGlass.tsx +41 -29
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +4 -19
- package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +216 -0
- package/src/components/Chart/BubbleChart.tsx +6 -2
- package/src/components/Chart/Chart.stories.tsx +108 -96
- package/src/components/Chart/ChartToolbar.tsx +6 -4
- package/src/components/Chart/ChartTooltip.tsx +5 -4
- package/src/components/Chart/GaugeChart.tsx +20 -12
- package/src/components/Chart/HeatmapChart.tsx +53 -23
- package/src/components/Chart/TreemapChart.tsx +44 -15
- package/src/components/Chart/index.ts +0 -2
- package/src/components/Chart/types.ts +4 -4
- package/src/components/index.ts +0 -1
- package/src/lib/composables/useAtomixGlass.ts +4 -1
- package/src/lib/composables/useAtomixGlassStyles.ts +9 -7
- package/src/lib/constants/components.ts +7 -7
- package/src/styles/01-settings/_settings.chart.scss +13 -13
- package/src/styles/06-components/_components.atomix-glass.scss +17 -21
- package/src/styles/06-components/_components.chart.scss +23 -5
- 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.tooltip.scss +9 -5
- package/scripts/cli/component-generator.js +0 -564
- package/scripts/cli/interactive-init.js +0 -357
- package/scripts/cli/utils.js +0 -359
- package/src/components/Chart/AnimatedChart.tsx +0 -230
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interactive Init Wizard for Atomix Design System
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import inquirer from 'inquirer';
|
|
6
|
-
import chalk from 'chalk';
|
|
7
|
-
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
8
|
-
import { join, dirname, basename } from 'path';
|
|
9
|
-
import { existsSync } from 'fs';
|
|
10
|
-
import boxen from 'boxen';
|
|
11
|
-
import ora from 'ora';
|
|
12
|
-
|
|
13
|
-
import { projectTemplates, configTemplates } from './templates.js';
|
|
14
|
-
import { commonTemplates } from './templates/common-templates.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Run the interactive init wizard
|
|
18
|
-
*/
|
|
19
|
-
export async function runInitWizard() {
|
|
20
|
-
console.log(boxen(
|
|
21
|
-
chalk.bold.cyan('🎨 Atomix Design System Setup Wizard\n\n') +
|
|
22
|
-
chalk.gray('Let\'s set up your design system project!'),
|
|
23
|
-
{
|
|
24
|
-
padding: 1,
|
|
25
|
-
margin: 1,
|
|
26
|
-
borderStyle: 'round',
|
|
27
|
-
borderColor: 'cyan'
|
|
28
|
-
}
|
|
29
|
-
));
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
// Step 1: Project type
|
|
33
|
-
const { projectType } = await inquirer.prompt([
|
|
34
|
-
{
|
|
35
|
-
type: 'list',
|
|
36
|
-
name: 'projectType',
|
|
37
|
-
message: 'What type of project are you building?',
|
|
38
|
-
choices: [
|
|
39
|
-
{ name: 'React Application', value: 'react' },
|
|
40
|
-
{ name: 'Next.js Application', value: 'nextjs' },
|
|
41
|
-
{ name: 'Vanilla JavaScript/HTML', value: 'vanilla' },
|
|
42
|
-
{ name: 'Custom Setup', value: 'custom' }
|
|
43
|
-
]
|
|
44
|
-
}
|
|
45
|
-
]);
|
|
46
|
-
|
|
47
|
-
// Step 2: Theme selection
|
|
48
|
-
const { themeChoice } = await inquirer.prompt([
|
|
49
|
-
{
|
|
50
|
-
type: 'list',
|
|
51
|
-
name: 'themeChoice',
|
|
52
|
-
message: 'How would you like to handle theming?',
|
|
53
|
-
choices: [
|
|
54
|
-
{ name: 'Use a pre-built theme', value: 'prebuilt' },
|
|
55
|
-
{ name: 'Create a custom theme', value: 'custom' },
|
|
56
|
-
{ name: 'Start with default styles', value: 'default' },
|
|
57
|
-
{ name: 'I\'ll configure this later', value: 'skip' }
|
|
58
|
-
]
|
|
59
|
-
}
|
|
60
|
-
]);
|
|
61
|
-
|
|
62
|
-
let prebuiltThemeName = null;
|
|
63
|
-
if (themeChoice === 'prebuilt') {
|
|
64
|
-
const { theme } = await inquirer.prompt([
|
|
65
|
-
{
|
|
66
|
-
type: 'input',
|
|
67
|
-
name: 'theme',
|
|
68
|
-
message: 'Enter the name of the pre-built theme:',
|
|
69
|
-
default: 'default'
|
|
70
|
-
}
|
|
71
|
-
]);
|
|
72
|
-
prebuiltThemeName = (theme && String(theme).trim()) || 'default';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Step 3: Features
|
|
76
|
-
const { features } = await inquirer.prompt([
|
|
77
|
-
{
|
|
78
|
-
type: 'checkbox',
|
|
79
|
-
name: 'features',
|
|
80
|
-
message: 'Select additional features:',
|
|
81
|
-
choices: [
|
|
82
|
-
{ name: 'TypeScript support', value: 'typescript', checked: true },
|
|
83
|
-
{ name: 'Storybook integration', value: 'storybook' },
|
|
84
|
-
{ name: 'Testing setup (Vitest)', value: 'testing' },
|
|
85
|
-
{ name: 'ESLint & Prettier', value: 'linting' },
|
|
86
|
-
{ name: 'Git hooks (Husky)', value: 'githooks' },
|
|
87
|
-
{ name: 'CI/CD workflows', value: 'cicd' }
|
|
88
|
-
]
|
|
89
|
-
}
|
|
90
|
-
]);
|
|
91
|
-
|
|
92
|
-
// Step 4: Configuration
|
|
93
|
-
const { configType } = await inquirer.prompt([
|
|
94
|
-
{
|
|
95
|
-
type: 'list',
|
|
96
|
-
name: 'configType',
|
|
97
|
-
message: 'Configuration file format:',
|
|
98
|
-
choices: [
|
|
99
|
-
{ name: 'JSON (.atomixrc.json)', value: 'json' },
|
|
100
|
-
{ name: 'JavaScript (atomix.config.js)', value: 'js' },
|
|
101
|
-
{ name: 'No configuration file', value: 'none' }
|
|
102
|
-
]
|
|
103
|
-
}
|
|
104
|
-
]);
|
|
105
|
-
|
|
106
|
-
// Step 5: Installation
|
|
107
|
-
const { shouldInstall } = await inquirer.prompt([
|
|
108
|
-
{
|
|
109
|
-
type: 'confirm',
|
|
110
|
-
name: 'shouldInstall',
|
|
111
|
-
message: 'Install dependencies now?',
|
|
112
|
-
default: true
|
|
113
|
-
}
|
|
114
|
-
]);
|
|
115
|
-
|
|
116
|
-
// Create project structure
|
|
117
|
-
if (projectType !== 'custom') {
|
|
118
|
-
const template = projectTemplates[projectType];
|
|
119
|
-
|
|
120
|
-
// Step 6: Create/Update package.json
|
|
121
|
-
const packageJsonPath = join(process.cwd(), 'package.json');
|
|
122
|
-
let packageJson = {};
|
|
123
|
-
|
|
124
|
-
if (existsSync(packageJsonPath)) {
|
|
125
|
-
packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
|
126
|
-
} else {
|
|
127
|
-
// Create basic package.json
|
|
128
|
-
packageJson = {
|
|
129
|
-
name: basename(process.cwd()),
|
|
130
|
-
version: '0.1.0',
|
|
131
|
-
private: true,
|
|
132
|
-
scripts: {},
|
|
133
|
-
dependencies: {},
|
|
134
|
-
devDependencies: {}
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Merge dependencies
|
|
139
|
-
const packageJsonVersion = packageJson.version || '0.1.0';
|
|
140
|
-
|
|
141
|
-
template.dependencies.forEach(dep => {
|
|
142
|
-
if (!packageJson.dependencies[dep]) {
|
|
143
|
-
// Use a default compatible version if not specified
|
|
144
|
-
const defaultVersions = {
|
|
145
|
-
'react': '^18.0.0',
|
|
146
|
-
'react-dom': '^18.0.0',
|
|
147
|
-
'next': '^14.0.0',
|
|
148
|
-
'sass': '^1.70.0'
|
|
149
|
-
};
|
|
150
|
-
packageJson.dependencies[dep] = defaultVersions[dep] || 'latest';
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
template.devDependencies.forEach(dep => {
|
|
155
|
-
if (!packageJson.devDependencies[dep]) {
|
|
156
|
-
const defaultDevVersions = {
|
|
157
|
-
'typescript': '^5.0.0',
|
|
158
|
-
'vite': '^5.0.0',
|
|
159
|
-
'@shohojdhara/atomix': `^${packageJsonVersion}` // Self-reference for templates
|
|
160
|
-
};
|
|
161
|
-
packageJson.devDependencies[dep] = defaultDevVersions[dep] || 'latest';
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Merge scripts carefully
|
|
166
|
-
const themePath = themeChoice === 'prebuilt' && prebuiltThemeName
|
|
167
|
-
? `themes/${prebuiltThemeName}`
|
|
168
|
-
: 'themes/custom';
|
|
169
|
-
const newScripts = {
|
|
170
|
-
'dev': projectType === 'nextjs' ? 'next dev' : 'vite',
|
|
171
|
-
'build': projectType === 'nextjs' ? 'next build' : 'vite build',
|
|
172
|
-
'build:theme': `atomix build-theme ${themePath}`,
|
|
173
|
-
'generate:component': 'atomix generate component',
|
|
174
|
-
'validate': 'atomix validate --tokens --theme'
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
if (features.includes('storybook')) {
|
|
178
|
-
newScripts['storybook'] = 'storybook dev -p 6006';
|
|
179
|
-
newScripts['build:storybook'] = 'storybook build';
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (features.includes('testing')) {
|
|
183
|
-
newScripts['test'] = 'vitest';
|
|
184
|
-
newScripts['test:watch'] = 'vitest --watch';
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Add new scripts without overwriting user's existing scripts
|
|
188
|
-
for (const [key, value] of Object.entries(newScripts)) {
|
|
189
|
-
if (!packageJson.scripts[key]) {
|
|
190
|
-
packageJson.scripts[key] = value;
|
|
191
|
-
} else if (packageJson.scripts[key] !== value) {
|
|
192
|
-
// Suggest renamed script if conflict exists
|
|
193
|
-
const suggestedKey = `atomix:${key}`;
|
|
194
|
-
if (!packageJson.scripts[suggestedKey]) {
|
|
195
|
-
packageJson.scripts[suggestedKey] = value;
|
|
196
|
-
console.log(chalk.yellow(` ⚠️ Script conflict for "${key}". Added as "${suggestedKey}" instead.`));
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8');
|
|
202
|
-
console.log(chalk.green(' ✓ Updated package.json'));
|
|
203
|
-
|
|
204
|
-
// Create directories
|
|
205
|
-
await mkdir('src', { recursive: true });
|
|
206
|
-
if (projectType === 'nextjs') {
|
|
207
|
-
await mkdir('src/pages', { recursive: true });
|
|
208
|
-
} else if (projectType === 'react') {
|
|
209
|
-
await mkdir('src/components', { recursive: true });
|
|
210
|
-
} else if (projectType === 'vanilla') {
|
|
211
|
-
await mkdir('src/styles', { recursive: true });
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Write template files
|
|
215
|
-
const spinner = ora('Creating project files...').start();
|
|
216
|
-
let filesCreated = 0;
|
|
217
|
-
const totalFiles = Object.keys(template.files).length;
|
|
218
|
-
|
|
219
|
-
for (const [path, content] of Object.entries(template.files)) {
|
|
220
|
-
const filePath = join(process.cwd(), path);
|
|
221
|
-
const dir = dirname(filePath);
|
|
222
|
-
|
|
223
|
-
if (!existsSync(dir)) {
|
|
224
|
-
await mkdir(dir, { recursive: true });
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
await writeFile(filePath, content, 'utf8');
|
|
228
|
-
filesCreated++;
|
|
229
|
-
spinner.text = `Creating project files... (${filesCreated}/${totalFiles})`;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
spinner.succeed(chalk.green(`✓ Created ${filesCreated} files`));
|
|
233
|
-
|
|
234
|
-
// Generate README
|
|
235
|
-
const readmeSpinner = ora('Generating README...').start();
|
|
236
|
-
const projectName = basename(process.cwd());
|
|
237
|
-
const readmeTemplate = projectType === 'react'
|
|
238
|
-
? commonTemplates.readme.react(projectName)
|
|
239
|
-
: projectType === 'nextjs'
|
|
240
|
-
? commonTemplates.readme.nextjs(projectName)
|
|
241
|
-
: commonTemplates.readme.vanilla(projectName);
|
|
242
|
-
|
|
243
|
-
await writeFile(
|
|
244
|
-
join(process.cwd(), 'README.md'),
|
|
245
|
-
readmeTemplate,
|
|
246
|
-
'utf8'
|
|
247
|
-
);
|
|
248
|
-
readmeSpinner.succeed(chalk.green('✓ Generated README.md'));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Create configuration file
|
|
252
|
-
if (configType !== 'none') {
|
|
253
|
-
const configTemplate = configType === 'json'
|
|
254
|
-
? configTemplates.basic
|
|
255
|
-
: configTemplates.advanced;
|
|
256
|
-
const themePathForConfig = themeChoice === 'prebuilt' && prebuiltThemeName
|
|
257
|
-
? `themes/${prebuiltThemeName}`
|
|
258
|
-
: 'themes/custom';
|
|
259
|
-
|
|
260
|
-
for (const [filename, content] of Object.entries(configTemplate)) {
|
|
261
|
-
let configContent = typeof content === 'object'
|
|
262
|
-
? JSON.stringify(content, null, 2)
|
|
263
|
-
: content;
|
|
264
|
-
|
|
265
|
-
if (themeChoice === 'prebuilt' && prebuiltThemeName) {
|
|
266
|
-
if (typeof content === 'object' && content.theme) {
|
|
267
|
-
const merged = { ...content, theme: { ...content.theme, path: themePathForConfig } };
|
|
268
|
-
configContent = JSON.stringify(merged, null, 2);
|
|
269
|
-
} else if (typeof content === 'string') {
|
|
270
|
-
configContent = content.replace(/path:\s*['"]themes\/custom['"]/g, `path: '${themePathForConfig}'`);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
await writeFile(
|
|
275
|
-
join(process.cwd(), filename),
|
|
276
|
-
configContent,
|
|
277
|
-
'utf8'
|
|
278
|
-
);
|
|
279
|
-
console.log(chalk.green(` ✓ Created ${filename}`));
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Create custom theme if selected
|
|
284
|
-
if (themeChoice === 'custom') {
|
|
285
|
-
await mkdir('themes/custom', { recursive: true });
|
|
286
|
-
|
|
287
|
-
const themeContent = `// Custom Theme
|
|
288
|
-
// Generated by Atomix CLI
|
|
289
|
-
|
|
290
|
-
@use '@shohojdhara/atomix/scss/settings' as * with (
|
|
291
|
-
// Your custom token overrides
|
|
292
|
-
$primary-500: #7AFFD7,
|
|
293
|
-
$secondary-500: #FF5733,
|
|
294
|
-
|
|
295
|
-
// Add more overrides as needed
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
// Import Atomix components
|
|
299
|
-
@use '@shohojdhara/atomix/scss/components';
|
|
300
|
-
|
|
301
|
-
// Your custom styles
|
|
302
|
-
.custom-component {
|
|
303
|
-
// Custom component styles
|
|
304
|
-
}`;
|
|
305
|
-
|
|
306
|
-
await writeFile(
|
|
307
|
-
join(process.cwd(), 'themes/custom/index.scss'),
|
|
308
|
-
themeContent,
|
|
309
|
-
'utf8'
|
|
310
|
-
);
|
|
311
|
-
console.log(chalk.green(' ✓ Created custom theme'));
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Success message
|
|
315
|
-
console.log(boxen(
|
|
316
|
-
chalk.bold.green('✨ Setup Complete!\n\n') +
|
|
317
|
-
chalk.cyan('Your Atomix project is ready.\n\n') +
|
|
318
|
-
chalk.gray('Next steps:\n') +
|
|
319
|
-
(shouldInstall ? '' : chalk.white('1. Install dependencies: npm install\n')) +
|
|
320
|
-
chalk.white(`${shouldInstall ? '1' : '2'}. Start development: npm run dev\n`) +
|
|
321
|
-
chalk.white(`${shouldInstall ? '2' : '3'}. Build your theme: npm run build:theme\n`) +
|
|
322
|
-
chalk.white(`${shouldInstall ? '3' : '4'}. Generate components: npm run generate:component\n\n`) +
|
|
323
|
-
chalk.gray('Documentation: https://github.com/shohojdhara/atomix'),
|
|
324
|
-
{
|
|
325
|
-
padding: 1,
|
|
326
|
-
margin: 1,
|
|
327
|
-
borderStyle: 'round',
|
|
328
|
-
borderColor: 'green'
|
|
329
|
-
}
|
|
330
|
-
));
|
|
331
|
-
|
|
332
|
-
// Install dependencies if requested
|
|
333
|
-
if (shouldInstall) {
|
|
334
|
-
console.log(chalk.cyan('\n📥 Installing dependencies...\n'));
|
|
335
|
-
const { execSync } = await import('child_process');
|
|
336
|
-
|
|
337
|
-
try {
|
|
338
|
-
execSync('npm install', { stdio: 'inherit' });
|
|
339
|
-
console.log(chalk.green('\n✅ Dependencies installed successfully!'));
|
|
340
|
-
} catch (error) {
|
|
341
|
-
console.error(chalk.red('\n❌ Failed to install dependencies'));
|
|
342
|
-
console.log(chalk.yellow('Please run: npm install'));
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
} catch (error) {
|
|
347
|
-
if (error.isTTYError) {
|
|
348
|
-
console.error(chalk.red('This environment doesn\'t support interactive prompts'));
|
|
349
|
-
console.log(chalk.yellow('Please use manual setup commands instead'));
|
|
350
|
-
} else {
|
|
351
|
-
console.error(chalk.red('Setup failed:'), error.message);
|
|
352
|
-
}
|
|
353
|
-
process.exit(1);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
export default runInitWizard;
|
package/scripts/cli/utils.js
DELETED
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI Utility Functions
|
|
3
|
-
* Provides common utilities for the Atomix CLI including security, validation, and helpers
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { resolve, relative, isAbsolute, normalize } from 'path';
|
|
7
|
-
import { access } from 'fs/promises';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Enhanced Error Class for CLI
|
|
11
|
-
*/
|
|
12
|
-
export class AtomixCLIError extends Error {
|
|
13
|
-
constructor(message, code, suggestions = []) {
|
|
14
|
-
super(message);
|
|
15
|
-
this.name = 'AtomixCLIError';
|
|
16
|
-
this.code = code;
|
|
17
|
-
this.suggestions = suggestions;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Validates and sanitizes file paths to prevent directory traversal attacks
|
|
23
|
-
* @param {string} inputPath - The path to validate
|
|
24
|
-
* @param {string} basePath - The base directory (defaults to process.cwd())
|
|
25
|
-
* @returns {Object} { isValid: boolean, safePath: string, error?: string }
|
|
26
|
-
*/
|
|
27
|
-
export function validatePath(inputPath, basePath = process.cwd()) {
|
|
28
|
-
try {
|
|
29
|
-
// Normalize the paths to remove any '..' or '.' segments
|
|
30
|
-
const normalizedBase = normalize(resolve(basePath));
|
|
31
|
-
const normalizedInput = normalize(isAbsolute(inputPath)
|
|
32
|
-
? inputPath
|
|
33
|
-
: resolve(basePath, inputPath));
|
|
34
|
-
|
|
35
|
-
// Check if the resolved path is within the base directory
|
|
36
|
-
const relativePath = relative(normalizedBase, normalizedInput);
|
|
37
|
-
|
|
38
|
-
// If the relative path starts with '..', it's outside the base directory
|
|
39
|
-
if (relativePath.startsWith('..')) {
|
|
40
|
-
return {
|
|
41
|
-
isValid: false,
|
|
42
|
-
safePath: null,
|
|
43
|
-
error: 'Path is outside the project directory'
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Additional checks for sensitive paths
|
|
48
|
-
const sensitivePatterns = [
|
|
49
|
-
/^\.git/,
|
|
50
|
-
/node_modules/,
|
|
51
|
-
/^\.env/,
|
|
52
|
-
/\.pem$/,
|
|
53
|
-
/\.key$/,
|
|
54
|
-
/private/i,
|
|
55
|
-
/secret/i
|
|
56
|
-
];
|
|
57
|
-
|
|
58
|
-
for (const pattern of sensitivePatterns) {
|
|
59
|
-
if (pattern.test(relativePath)) {
|
|
60
|
-
return {
|
|
61
|
-
isValid: false,
|
|
62
|
-
safePath: null,
|
|
63
|
-
error: `Access to sensitive path is restricted: ${pattern}`
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
isValid: true,
|
|
70
|
-
safePath: normalizedInput,
|
|
71
|
-
error: null
|
|
72
|
-
};
|
|
73
|
-
} catch (error) {
|
|
74
|
-
return {
|
|
75
|
-
isValid: false,
|
|
76
|
-
safePath: null,
|
|
77
|
-
error: `Invalid path: ${error.message}`
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Validates component names according to PascalCase convention
|
|
84
|
-
* @param {string} name - The component name to validate
|
|
85
|
-
* @returns {Object} { isValid: boolean, error?: string }
|
|
86
|
-
*/
|
|
87
|
-
export function validateComponentName(name) {
|
|
88
|
-
if (!name || typeof name !== 'string') {
|
|
89
|
-
return {
|
|
90
|
-
isValid: false,
|
|
91
|
-
error: 'Component name must be a non-empty string'
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Check PascalCase: starts with uppercase, only contains letters and numbers
|
|
96
|
-
if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
|
|
97
|
-
return {
|
|
98
|
-
isValid: false,
|
|
99
|
-
error: 'Component name must be in PascalCase (e.g., Button, CardHeader)'
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Check for reserved words
|
|
104
|
-
const reservedWords = [
|
|
105
|
-
'Component', 'React', 'Fragment', 'Suspense', 'StrictMode',
|
|
106
|
-
'Error', 'Loading', 'App', 'Root', 'Document', 'Html'
|
|
107
|
-
];
|
|
108
|
-
|
|
109
|
-
if (reservedWords.includes(name)) {
|
|
110
|
-
return {
|
|
111
|
-
isValid: false,
|
|
112
|
-
error: `"${name}" is a reserved word. Please choose a different name.`
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Check minimum length
|
|
117
|
-
if (name.length < 2) {
|
|
118
|
-
return {
|
|
119
|
-
isValid: false,
|
|
120
|
-
error: 'Component name must be at least 2 characters long'
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return { isValid: true };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Validates theme names according to kebab-case convention
|
|
129
|
-
* @param {string} name - The theme name to validate
|
|
130
|
-
* @returns {Object} { isValid: boolean, error?: string }
|
|
131
|
-
*/
|
|
132
|
-
export function validateThemeName(name) {
|
|
133
|
-
if (!name || typeof name !== 'string') {
|
|
134
|
-
return {
|
|
135
|
-
isValid: false,
|
|
136
|
-
error: 'Theme name must be a non-empty string'
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Check kebab-case: lowercase letters, numbers, and hyphens
|
|
141
|
-
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
142
|
-
return {
|
|
143
|
-
isValid: false,
|
|
144
|
-
error: 'Theme name must be lowercase and use hyphens (e.g., dark-theme)'
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Check for consecutive hyphens
|
|
149
|
-
if (/--/.test(name)) {
|
|
150
|
-
return {
|
|
151
|
-
isValid: false,
|
|
152
|
-
error: 'Theme name cannot contain consecutive hyphens'
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Check for trailing hyphen
|
|
157
|
-
if (name.endsWith('-')) {
|
|
158
|
-
return {
|
|
159
|
-
isValid: false,
|
|
160
|
-
error: 'Theme name cannot end with a hyphen'
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return { isValid: true };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Sanitizes user input to prevent injection attacks
|
|
169
|
-
* @param {string} input - The user input to sanitize
|
|
170
|
-
* @returns {string} Sanitized input
|
|
171
|
-
*/
|
|
172
|
-
export function sanitizeInput(input) {
|
|
173
|
-
if (typeof input !== 'string') {
|
|
174
|
-
return String(input);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Remove any shell metacharacters that could be dangerous
|
|
178
|
-
// Added single and double quotes to the blacklist to prevent shell injection
|
|
179
|
-
return input
|
|
180
|
-
.replace(/[;&|`$<>\\"']/g, '')
|
|
181
|
-
.replace(/\0/g, '') // Remove null bytes
|
|
182
|
-
.trim();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Checks if a file exists and is accessible
|
|
187
|
-
* @param {string} filePath - Path to check
|
|
188
|
-
* @returns {Promise<boolean>}
|
|
189
|
-
*/
|
|
190
|
-
export async function fileExists(filePath) {
|
|
191
|
-
try {
|
|
192
|
-
await access(filePath);
|
|
193
|
-
return true;
|
|
194
|
-
} catch {
|
|
195
|
-
return false;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Checks if running in CI environment
|
|
201
|
-
* @returns {boolean}
|
|
202
|
-
*/
|
|
203
|
-
export function isCI() {
|
|
204
|
-
return !!(
|
|
205
|
-
process.env.CI ||
|
|
206
|
-
process.env.CONTINUOUS_INTEGRATION ||
|
|
207
|
-
process.env.GITHUB_ACTIONS ||
|
|
208
|
-
process.env.GITLAB_CI ||
|
|
209
|
-
process.env.CIRCLECI ||
|
|
210
|
-
process.env.TRAVIS ||
|
|
211
|
-
process.env.JENKINS_URL
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Checks if running in debug mode
|
|
217
|
-
* @returns {boolean}
|
|
218
|
-
*/
|
|
219
|
-
export function isDebug() {
|
|
220
|
-
return process.env.ATOMIX_DEBUG === 'true' ||
|
|
221
|
-
process.argv.includes('--debug') ||
|
|
222
|
-
process.argv.includes('-d');
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Formats file size in human readable format
|
|
227
|
-
* @param {number} bytes - File size in bytes
|
|
228
|
-
* @returns {string} Formatted size
|
|
229
|
-
*/
|
|
230
|
-
export function formatFileSize(bytes) {
|
|
231
|
-
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
232
|
-
if (bytes === 0) return '0 B';
|
|
233
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
234
|
-
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Debounce function for watch mode
|
|
239
|
-
* @param {Function} func - Function to debounce
|
|
240
|
-
* @param {number} wait - Wait time in milliseconds
|
|
241
|
-
* @returns {Function} Debounced function
|
|
242
|
-
*/
|
|
243
|
-
export function debounce(func, wait) {
|
|
244
|
-
let timeout;
|
|
245
|
-
return function executedFunction(...args) {
|
|
246
|
-
const later = () => {
|
|
247
|
-
clearTimeout(timeout);
|
|
248
|
-
func(...args);
|
|
249
|
-
};
|
|
250
|
-
clearTimeout(timeout);
|
|
251
|
-
timeout = setTimeout(later, wait);
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Creates a safe file path for cross-platform compatibility
|
|
257
|
-
* @param {...string} segments - Path segments
|
|
258
|
-
* @returns {string} Safe file path
|
|
259
|
-
*/
|
|
260
|
-
export function safePath(...segments) {
|
|
261
|
-
// Filter out empty segments and join with proper separator
|
|
262
|
-
return segments
|
|
263
|
-
.filter(Boolean)
|
|
264
|
-
.join('/')
|
|
265
|
-
.replace(/\/+/g, '/') // Remove duplicate slashes
|
|
266
|
-
.replace(/\\/g, '/'); // Convert Windows backslashes
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Validates SCSS/CSS color values
|
|
271
|
-
* @param {string} color - Color value to validate
|
|
272
|
-
* @returns {boolean}
|
|
273
|
-
*/
|
|
274
|
-
export function isValidColor(color) {
|
|
275
|
-
const patterns = [
|
|
276
|
-
/^#[0-9A-F]{3}$/i, // #RGB
|
|
277
|
-
/^#[0-9A-F]{4}$/i, // #RGBA
|
|
278
|
-
/^#[0-9A-F]{6}$/i, // #RRGGBB
|
|
279
|
-
/^#[0-9A-F]{8}$/i, // #RRGGBBAA
|
|
280
|
-
/^rgb\(/i, // rgb()
|
|
281
|
-
/^rgba\(/i, // rgba()
|
|
282
|
-
/^hsl\(/i, // hsl()
|
|
283
|
-
/^hsla\(/i, // hsla()
|
|
284
|
-
/^var\(--/ // CSS custom property
|
|
285
|
-
];
|
|
286
|
-
|
|
287
|
-
return patterns.some(pattern => pattern.test(color));
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Extracts and validates npm scripts from package.json
|
|
292
|
-
* @param {Object} packageJson - Parsed package.json content
|
|
293
|
-
* @param {Array<string>} requiredScripts - List of required script names
|
|
294
|
-
* @returns {Object} { valid: boolean, missing: Array<string> }
|
|
295
|
-
*/
|
|
296
|
-
export function validateNpmScripts(packageJson, requiredScripts = []) {
|
|
297
|
-
const scripts = packageJson.scripts || {};
|
|
298
|
-
const missing = requiredScripts.filter(script => !scripts[script]);
|
|
299
|
-
|
|
300
|
-
return {
|
|
301
|
-
valid: missing.length === 0,
|
|
302
|
-
missing
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Generates a unique ID for components/themes
|
|
308
|
-
* @param {string} prefix - Prefix for the ID
|
|
309
|
-
* @returns {string} Unique ID
|
|
310
|
-
*/
|
|
311
|
-
export function generateId(prefix = 'atomix') {
|
|
312
|
-
const timestamp = Date.now().toString(36);
|
|
313
|
-
const random = Math.random().toString(36).substring(2, 7);
|
|
314
|
-
return `${prefix}-${timestamp}-${random}`;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Checks Node.js version compatibility
|
|
319
|
-
* @param {string} requiredVersion - Minimum required version (e.g., '18.0.0')
|
|
320
|
-
* @returns {Object} { compatible: boolean, current: string, required: string }
|
|
321
|
-
*/
|
|
322
|
-
export function checkNodeVersion(requiredVersion = '18.0.0') {
|
|
323
|
-
const currentVersion = process.version.substring(1); // Remove 'v' prefix
|
|
324
|
-
const current = currentVersion.split('.').map(Number);
|
|
325
|
-
const required = requiredVersion.split('.').map(Number);
|
|
326
|
-
|
|
327
|
-
let compatible = true;
|
|
328
|
-
for (let i = 0; i < required.length; i++) {
|
|
329
|
-
if (current[i] < required[i]) {
|
|
330
|
-
compatible = false;
|
|
331
|
-
break;
|
|
332
|
-
} else if (current[i] > required[i]) {
|
|
333
|
-
break;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return {
|
|
338
|
-
compatible,
|
|
339
|
-
current: currentVersion,
|
|
340
|
-
required: requiredVersion
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
export default {
|
|
345
|
-
validatePath,
|
|
346
|
-
validateComponentName,
|
|
347
|
-
validateThemeName,
|
|
348
|
-
sanitizeInput,
|
|
349
|
-
fileExists,
|
|
350
|
-
isCI,
|
|
351
|
-
isDebug,
|
|
352
|
-
formatFileSize,
|
|
353
|
-
debounce,
|
|
354
|
-
safePath,
|
|
355
|
-
isValidColor,
|
|
356
|
-
validateNpmScripts,
|
|
357
|
-
generateId,
|
|
358
|
-
checkNodeVersion
|
|
359
|
-
};
|