apexcss-cli 0.1.0

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/README.md ADDED
@@ -0,0 +1,284 @@
1
+ # @apexcss/cli
2
+
3
+ ApexCSS CLI - A powerful build tool for with automatic framework detection and seamless integration.
4
+
5
+ ## What is ApexCSS CLI?
6
+
7
+ ApexCSS CLI helps you build and customize your own CSS utility framework. It provides:
8
+
9
+ - **Automatic Framework Detection** - Detects your project setup (React, Vue, Next.js, etc.) and configures accordingly
10
+ - **Layered CSS Generation** - Build only what you need (base, utilities, themes)
11
+ - **Watch Mode** - Automatically rebuild on configuration changes
12
+ - **Smart Configuration** - TypeScript/JavaScript config files with validation
13
+ - **Framework Integration** - Automatically adds CSS imports to your framework's entry file
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install -g @apexcss/cli
19
+ # or use without installing
20
+ npx @apexcss/cli <command>
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```bash
26
+ # 1. Initialize with automatic framework detection
27
+ apexcss init
28
+
29
+ # 2. Build your CSS
30
+ apexcss build
31
+
32
+ # 3. During development, watch for changes
33
+ apexcss watch
34
+ ```
35
+
36
+ ## Automatic Framework Detection
37
+
38
+ ApexCSS CLI automatically detects your project framework and configures the integration:
39
+
40
+ | Framework | Detection Method | Auto-Import |
41
+ |-----------|-----------------|-------------|
42
+ | Next.js | `next` in dependencies | ✅ Added to globals.css |
43
+ | React | `react` in dependencies | ✅ Added to main entry file |
44
+ | Vue | `vue` in dependencies | ✅ Added to main.ts/js |
45
+ | Angular | `@angular/core` in dependencies | ✅ Added to styles.css |
46
+ | Svelte | `svelte` in dependencies | ✅ Added to main entry file |
47
+ | Astro | `astro` in dependencies | ✅ Added to Layout.astro |
48
+ | Nuxt | `nuxt` in dependencies | ✅ Instructions for nuxt.config.ts |
49
+ | Vanilla | Default fallback | ✅ Added to main.js/ts |
50
+
51
+ Run `apexcss doctor` to see what was detected in your project.
52
+
53
+ ## Usage
54
+
55
+ ### Initialize Configuration
56
+
57
+ ```bash
58
+ # Interactive mode with prompts
59
+ apexcss init
60
+
61
+ # Skip interactive prompts
62
+ apexcss init --interactive=false
63
+
64
+ # Specify framework explicitly
65
+ apexcss init --framework=react
66
+
67
+ # Custom output directory
68
+ apexcss init --output=./src/styles
69
+ ```
70
+
71
+ ### Build CSS
72
+
73
+ ```bash
74
+ # Build complete CSS (base + utilities + themes)
75
+ apexcss build
76
+
77
+ # Build specific layers only
78
+ apexcss build --layer base
79
+ apexcss build --layer utilities
80
+ apexcss build --layer themes
81
+ apexcss build --layer base,themes
82
+
83
+ # Production build with minification
84
+ apexcss build --minify
85
+
86
+ # Generate source maps
87
+ apexcss build --sourcemap
88
+
89
+ # Output as SCSS instead of CSS
90
+ apexcss build --format=scss
91
+ ```
92
+
93
+ ### Watch Mode
94
+
95
+ ```bash
96
+ # Watch config file for changes and auto-rebuild
97
+ apexcss watch
98
+
99
+ # Watch with custom config path
100
+ apexcss watch --config=./custom.config.js
101
+ ```
102
+
103
+ ### Diagnostics
104
+
105
+ ```bash
106
+ # Run system diagnostics
107
+ apexcss doctor
108
+ ```
109
+
110
+ ## CLI Options
111
+
112
+ | Option | Description | Default |
113
+ |--------|-------------|---------|
114
+ | `-c, --config <path>` | Config file path | `./apex.config.js` |
115
+ | `-o, --output <dir>` | Output directory | `./dist/` |
116
+ | `-l, --layer <layers>` | Build specific layers | `all` |
117
+ | `--minify` | Minify output CSS | `false` |
118
+ | `--sourcemap` | Generate source maps | `false` |
119
+ | `--format <format>` | Output format | `css` |
120
+ | `--framework` | Specify framework | `auto-detect` |
121
+ | `--interactive` | Interactive prompts | `true` |
122
+ | `--import` | Add imports to entry files | `true` |
123
+
124
+ ## Configuration
125
+
126
+ Create an `apex.config.js` file in your project root:
127
+
128
+ ```javascript
129
+ export default {
130
+ // Feature toggles - enable/disable utility categories
131
+ features: {
132
+ display: true,
133
+ flexbox: true,
134
+ grid: true,
135
+ positioning: true,
136
+ visibility: true,
137
+ spacing: true,
138
+ typography: true,
139
+ colors: true,
140
+ backgrounds: true,
141
+ borders: true,
142
+ shadows: true,
143
+ opacity: true,
144
+ transitions: true,
145
+ transforms: true,
146
+ animations: true
147
+ },
148
+
149
+ // Breakpoints
150
+ breakpoints: {
151
+ sm: '320px',
152
+ md: '768px',
153
+ lg: '1024px',
154
+ xl: '1280px'
155
+ },
156
+
157
+ // Custom colors using OKLCH color space
158
+ colors: {
159
+ primary: {
160
+ hue: 250,
161
+ chroma: 0.2,
162
+ lightnessScale: {
163
+ 50: 96, 100: 90, 200: 85, 300: 78, 400: 70,
164
+ 500: 65, 600: 55, 700: 45, 800: 35, 900: 25, 950: 18
165
+ }
166
+ },
167
+ secondary: {
168
+ hue: 180,
169
+ chroma: 0.15,
170
+ lightnessScale: {
171
+ 50: 96, 100: 90, 200: 85, 300: 78, 400: 70,
172
+ 500: 65, 600: 55, 700: 45, 800: 35, 900: 25, 950: 18
173
+ }
174
+ }
175
+ },
176
+
177
+ // Spacing scale
178
+ spacing: {
179
+ '0': '0px',
180
+ '1': '0.25rem',
181
+ '2': '0.5rem',
182
+ '4': '1rem',
183
+ '8': '2rem',
184
+ '16': '4rem'
185
+ }
186
+ };
187
+ ```
188
+
189
+ ## Peer Dependencies
190
+
191
+ This CLI requires `apexcss` to be installed in your project:
192
+
193
+ ```bash
194
+ npm install apexcss
195
+ ```
196
+
197
+ Optional peer dependencies:
198
+ - `vite` - For building CSS (recommended)
199
+ - `sass` - For SCSS support
200
+
201
+ ## Development
202
+
203
+ ### Setup
204
+
205
+ ```bash
206
+ # Clone the repository
207
+ git clone https://github.com/chris-briddock/apex-cli.git
208
+ cd apex-cli
209
+
210
+ # Install dependencies
211
+ npm install
212
+ ```
213
+
214
+ ### Testing
215
+
216
+ ```bash
217
+ # Run all tests
218
+ npm test
219
+
220
+ # Run tests with coverage (LCOV report)
221
+ npm run test:coverage
222
+
223
+ # View coverage as text in terminal
224
+ npm run test:coverage:text
225
+
226
+ # Generate HTML coverage report
227
+ npm run test:coverage:html
228
+ # Then open coverage/index.html in your browser
229
+
230
+ # Run tests in watch mode
231
+ npm run test:watch
232
+
233
+ # Run unit tests only
234
+ npm run test:unit
235
+ ```
236
+
237
+ ### Code Quality
238
+
239
+ ```bash
240
+ # Run ESLint
241
+ npm run lint
242
+
243
+ # Fix auto-fixable linting issues
244
+ npm run lint:fix
245
+ ```
246
+
247
+ ### Coverage Summary
248
+
249
+ Current test coverage:
250
+
251
+ | Module | Statements | Branches | Functions | Lines |
252
+ |--------|-----------|----------|-----------|-------|
253
+ | cli/utils/config-loader.js | 99% | 95% | 100% | 99% |
254
+ | cli/utils/logger.js | 100% | 100% | 100% | 100% |
255
+ | cli/utils/framework-detector.js | 95% | 94% | 100% | 95% |
256
+ | cli/commands/doctor.js | 82% | 79% | 100% | 82% |
257
+ | cli/commands/watch.js | 67% | 100% | 83% | 67% |
258
+ | cli/commands/build.js | 54% | 100% | 67% | 54% |
259
+ | cli/commands/init.js | 41% | 96% | 50% | 41% |
260
+
261
+ **Total: 76% statements, 92% branches, 86% functions**
262
+
263
+ ## How It Works
264
+
265
+ 1. **Initialization** (`apexcss init`):
266
+ - Detects your project framework from package.json
267
+ - Creates a starter config file (apex.config.js)
268
+ - Optionally adds CSS import to your framework's entry file
269
+ - Sets up .gitignore for output directory
270
+
271
+ 2. **Build Process** (`apexcss build`):
272
+ - Reads your configuration
273
+ - Generates SCSS based on enabled features
274
+ - Uses Vite to compile CSS (if available)
275
+ - Outputs minified CSS (with optional source maps)
276
+
277
+ 3. **Watch Mode** (`apexcss watch`):
278
+ - Monitors your config file for changes
279
+ - Automatically rebuilds on change
280
+ - Handles concurrent changes gracefully
281
+
282
+ ## License
283
+
284
+ MIT
package/bin/apexcss.js ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ApexCSS CLI
5
+ *
6
+ * Usage:
7
+ * apexcss init Initialize configuration
8
+ * apexcss build Build custom CSS
9
+ * apexcss watch Watch for changes
10
+ * apexcss doctor Check system setup
11
+ *
12
+ * Options:
13
+ * -c, --config <path> Config file path (default: ./apex.config.js)
14
+ * -o, --output <dir> Output directory (default: ./src/apexcss/)
15
+ * --minify Minify output CSS
16
+ * --sourcemap Generate source maps
17
+ * -v, --version Show version
18
+ * -h, --help Show help
19
+ */
20
+
21
+ import { cli } from '../cli/index.js';
22
+
23
+ cli(process.argv);
@@ -0,0 +1,376 @@
1
+ /**
2
+ * Build command - Generate custom CSS from configuration
3
+ */
4
+
5
+ import { writeFileSync, existsSync, mkdirSync, cpSync, rmSync, readFileSync } from 'node:fs';
6
+ import { resolve, dirname } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { loadConfig } from '../utils/config-loader.js';
9
+ import { logger } from '../utils/logger.js';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+
14
+ // Import the config builder
15
+ import { generateSCSS } from '../utils/config-builder.js';
16
+
17
+ /**
18
+ * Valid layer names
19
+ */
20
+ const VALID_LAYERS = ['base', 'utilities', 'themes'];
21
+
22
+ /**
23
+ * Parse layers option and return array of layers to build
24
+ * @param {string} layersOption - Comma-separated layer names or 'all'
25
+ * @returns {string[]} - Array of layer names
26
+ */
27
+ export function parseLayers(layersOption) {
28
+ if (!layersOption || layersOption === 'all') {
29
+ return ['base', 'utilities', 'themes'];
30
+ }
31
+
32
+ const requested = layersOption.split(',').map(l => l.trim().toLowerCase());
33
+ const invalid = requested.filter(l => !VALID_LAYERS.includes(l));
34
+
35
+ if (invalid.length > 0) {
36
+ throw new Error(`Invalid layer(s): ${invalid.join(', ')}. Valid options: ${VALID_LAYERS.join(', ')}`);
37
+ }
38
+
39
+ return requested;
40
+ }
41
+
42
+ /**
43
+ * Generate entry SCSS content based on selected layers
44
+ * @param {string[]} layers - Array of layer names to include
45
+ * @returns {string} - SCSS content for entry file
46
+ */
47
+ export function generateLayerEntry(layers) {
48
+ const lines = [
49
+ '// ============================================================================',
50
+ '// ApexCSS - Layered Build Entry Point',
51
+ '// ============================================================================',
52
+ '// Auto-generated based on --layer option',
53
+ '// ============================================================================',
54
+ ''
55
+ ];
56
+
57
+ // Always include config
58
+ lines.push('@use \'config\';');
59
+
60
+ // Include selected layers
61
+ for (const layer of layers) {
62
+ lines.push(`@use '${layer}';`);
63
+ }
64
+
65
+ lines.push('', '// ============================================================================', '// End of Entry Point', '// ============================================================================', '');
66
+
67
+ return lines.join('\n');
68
+ }
69
+
70
+ /**
71
+ * Get source directories needed for selected layers
72
+ * @param {string[]} layers - Array of layer names
73
+ * @returns {string[]} - Array of source directory names
74
+ */
75
+ export function getSourceEntriesForLayers(layers) {
76
+ const entries = new Set(['config']);
77
+
78
+ for (const layer of layers) {
79
+ entries.add(layer);
80
+ switch (layer) {
81
+ case 'utilities':
82
+ entries.add('mixins');
83
+ entries.add('plugins');
84
+ break;
85
+ case 'base':
86
+ entries.add('mixins');
87
+ break;
88
+ case 'themes':
89
+ entries.add('mixins');
90
+ break;
91
+ }
92
+ }
93
+
94
+ return Array.from(entries);
95
+ }
96
+
97
+ /**
98
+ * Setup build environment
99
+ * @param {string} outputDir - Output directory path
100
+ * @returns {string} - Temp directory path
101
+ */
102
+ export function setupBuildEnvironment(outputDir) {
103
+ const tempDir = resolve(outputDir, '.apexcss-build');
104
+ if (existsSync(tempDir)) {
105
+ rmSync(tempDir, { recursive: true });
106
+ }
107
+ mkdirSync(tempDir, { recursive: true });
108
+ return tempDir;
109
+ }
110
+
111
+ /**
112
+ * Determine source directory
113
+ * @param {string} cwd - Current working directory
114
+ * @returns {string} - Source directory path
115
+ */
116
+ export function determineSourceDir(cwd) {
117
+ // Look for apexcss source in user's node_modules
118
+ const nodeModulesSrcDir = resolve(cwd, 'node_modules', 'apexcss', 'src');
119
+
120
+ if (!existsSync(nodeModulesSrcDir)) {
121
+ throw new Error(
122
+ 'Could not find ApexCSS source files. ' +
123
+ 'Please ensure apexcss is installed: npm install apexcss'
124
+ );
125
+ }
126
+
127
+ return nodeModulesSrcDir;
128
+ }
129
+
130
+ /**
131
+ * Write configuration files to temp directory
132
+ * @param {string} tempDir - Temp directory path
133
+ * @param {string} scssContent - SCSS content to write
134
+ */
135
+ export function writeConfigFiles(tempDir, scssContent) {
136
+ const configDir = resolve(tempDir, 'config');
137
+ mkdirSync(configDir, { recursive: true });
138
+ writeFileSync(resolve(configDir, '_custom-config.scss'), scssContent);
139
+ writeFileSync(
140
+ resolve(configDir, '_index.scss'),
141
+ '// Auto-generated config entry\n@forward \'custom-config\';\n'
142
+ );
143
+ }
144
+
145
+ /**
146
+ * Find generated CSS file in temp directory
147
+ * @param {string} tempDir - Temp directory path
148
+ * @returns {string | undefined} - CSS file path or undefined
149
+ */
150
+ export function findGeneratedCss(tempDir) {
151
+ const candidates = ['apex.css', 'style.css'];
152
+ for (const file of candidates) {
153
+ const fullPath = resolve(tempDir, file);
154
+ if (existsSync(fullPath)) {
155
+ return fullPath;
156
+ }
157
+ }
158
+ return undefined;
159
+ }
160
+
161
+ /**
162
+ * Copy source map if generated
163
+ * @param {string} tempDir - Temp directory path
164
+ * @param {string} outputDir - Output directory path
165
+ */
166
+ export function copySourceMap(tempDir, outputDir) {
167
+ const mapPath = resolve(tempDir, 'apex.css.map');
168
+ if (existsSync(mapPath)) {
169
+ const mapContent = readFileSync(mapPath, 'utf-8');
170
+ writeFileSync(resolve(outputDir, 'apex.css.map'), mapContent);
171
+ logger.success('Source map generated');
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Build CSS from configuration
177
+ * @param {object} options - Command options
178
+ * @returns {Promise<void>}
179
+ */
180
+ export async function buildCommand(options) {
181
+ const cwd = process.cwd();
182
+ const startTime = Date.now();
183
+
184
+ logger.header('Building ApexCSS');
185
+ logger.newline();
186
+
187
+ // Load configuration
188
+ logger.info('Loading configuration...');
189
+ const config = loadConfig(options.configPath);
190
+
191
+ // Resolve output directory
192
+ const outputDir = resolve(cwd, options.outputDir);
193
+
194
+ // Ensure output directory exists
195
+ if (!existsSync(outputDir)) {
196
+ mkdirSync(outputDir, { recursive: true });
197
+ }
198
+
199
+ // Generate SCSS from config
200
+ logger.info('Generating SCSS configuration...');
201
+ const scssContent = generateSCSS(config);
202
+
203
+ // Determine source directory
204
+ const sourceDir = determineSourceDir(cwd);
205
+
206
+ if (!existsSync(sourceDir)) {
207
+ throw new Error(
208
+ 'Could not find ApexCSS source files. ' +
209
+ 'Please ensure apexcss is installed: npm install apexcss'
210
+ );
211
+ }
212
+
213
+ // Parse layers option
214
+ const layers = parseLayers(options.layers);
215
+ logger.info(`Building layers: ${layers.join(', ')}`);
216
+
217
+ // Setup build environment
218
+ const tempDir = setupBuildEnvironment(outputDir);
219
+
220
+ // Copy source files to temp directory based on selected layers
221
+ logger.info('Preparing build environment...');
222
+ copySourceFiles(sourceDir, tempDir, layers);
223
+
224
+ // Write custom config
225
+ writeConfigFiles(tempDir, scssContent);
226
+
227
+ // Generate layer-specific entry file
228
+ const entryContent = generateLayerEntry(layers);
229
+ writeFileSync(resolve(tempDir, 'apex-entry.scss'), entryContent);
230
+
231
+ // Build using Vite programmatically
232
+ logger.info('Compiling CSS...');
233
+
234
+ try {
235
+ await runViteBuild(tempDir, options, outputDir, layers, scssContent);
236
+
237
+ const duration = Date.now() - startTime;
238
+ logger.newline();
239
+ logger.success(`Build completed in ${duration}ms`);
240
+
241
+ } catch (error) {
242
+ // Clean up temp directory on error
243
+ if (existsSync(tempDir)) {
244
+ rmSync(tempDir, { recursive: true });
245
+ }
246
+ throw error;
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Run Vite build
252
+ * @param {string} tempDir - Temp directory path
253
+ * @param {object} options - Build options
254
+ * @param {string} outputDir - Output directory path
255
+ * @param {string[]} layers - Array of layer names
256
+ * @param {string} scssContent - SCSS content
257
+ * @returns {Promise<void>}
258
+ */
259
+ async function runViteBuild(tempDir, options, outputDir, layers, scssContent) {
260
+ const { build } = await import('vite');
261
+ const cwd = process.cwd();
262
+
263
+ // Look for postcss config in user's project
264
+ const userPostcssConfig = resolve(cwd, 'postcss.config.js');
265
+ const apexcssPostcssConfig = resolve(cwd, 'node_modules', 'apexcss', 'postcss.config.js');
266
+
267
+ let postcssConfig;
268
+ if (existsSync(userPostcssConfig)) {
269
+ postcssConfig = userPostcssConfig;
270
+ } else if (existsSync(apexcssPostcssConfig)) {
271
+ postcssConfig = apexcssPostcssConfig;
272
+ } else {
273
+ postcssConfig = undefined;
274
+ }
275
+
276
+ await build({
277
+ configFile: false,
278
+ css: {
279
+ postcss: postcssConfig
280
+ },
281
+ build: {
282
+ cssCodeSplit: false,
283
+ sourcemap: options.sourcemap,
284
+ minify: options.minify ? 'esbuild' : false,
285
+ outDir: tempDir,
286
+ rollupOptions: {
287
+ input: {
288
+ apex: resolve(tempDir, 'apex-entry.scss')
289
+ },
290
+ output: {
291
+ entryFileNames: '[name].js',
292
+ assetFileNames: () => '[name][extname]'
293
+ }
294
+ }
295
+ }
296
+ });
297
+
298
+ // Get output filenames based on layers
299
+ const { filename, description } = getOutputFilenames(layers);
300
+
301
+ // Copy CSS from temp to output
302
+ const generatedCssPath = findGeneratedCss(tempDir);
303
+ const finalCssPath = resolve(outputDir, `${filename}.css`);
304
+
305
+ if (!generatedCssPath) {
306
+ throw new Error('CSS file was not generated');
307
+ }
308
+
309
+ const cssContent = readFileSync(generatedCssPath, 'utf-8');
310
+ writeFileSync(finalCssPath, cssContent);
311
+
312
+ // Calculate file size
313
+ const contentBytes = Buffer.byteLength(cssContent, 'utf8');
314
+ const sizeKB = (contentBytes / 1024).toFixed(2);
315
+
316
+ const filePath = logger.path(`${filename}.css`);
317
+ logger.success(`Built: ${filePath} (${sizeKB} KB) [${description}]`);
318
+
319
+ // Copy source map if generated
320
+ if (options.sourcemap) {
321
+ const mapSource = resolve(tempDir, 'apex.css.map');
322
+ const mapDest = resolve(outputDir, `${filename}.css.map`);
323
+ if (existsSync(mapSource)) {
324
+ const mapContent = readFileSync(mapSource, 'utf-8');
325
+ writeFileSync(mapDest, mapContent);
326
+ logger.success('Source map generated');
327
+ }
328
+ }
329
+
330
+ // Output SCSS if requested
331
+ if (options.format === 'scss' || options.format === 'both') {
332
+ writeFileSync(resolve(outputDir, `${filename}.scss`), scssContent);
333
+ const scssPath = logger.path(`${filename}.scss`);
334
+ logger.success(`Generated: ${scssPath}`);
335
+ }
336
+
337
+ // Clean up temp directory
338
+ rmSync(tempDir, { recursive: true });
339
+ }
340
+
341
+ /**
342
+ * Copy source files to temp directory
343
+ * @param {string} sourceDir - Source directory
344
+ * @param {string} tempDir - Temp directory
345
+ * @param {string[]} layers - Array of layer names to include
346
+ */
347
+ function copySourceFiles(sourceDir, tempDir, layers) {
348
+ const entries = getSourceEntriesForLayers(layers);
349
+
350
+ for (const entry of entries) {
351
+ const sourcePath = resolve(sourceDir, entry);
352
+ const destPath = resolve(tempDir, entry);
353
+
354
+ if (existsSync(sourcePath)) {
355
+ cpSync(sourcePath, destPath, { recursive: true });
356
+ }
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Generate filenames based on selected layers
362
+ * @param {string[]} layers - Array of layer names
363
+ * @returns {object} - Object with filename and description
364
+ */
365
+ function getOutputFilenames(layers) {
366
+ if (layers.length === 3) {
367
+ return { filename: 'apex', description: 'complete framework' };
368
+ }
369
+
370
+ if (layers.length === 1) {
371
+ return { filename: layers[0], description: `${layers[0]} layer` };
372
+ }
373
+
374
+ const layerSuffix = layers.join('-');
375
+ return { filename: layerSuffix, description: `${layers.join(' + ')} layers` };
376
+ }