|
| 1 | +#!/usr/bin/env node |
| 2 | + |
| 3 | +/** |
| 4 | + * @viivue/atomic-css — build script |
| 5 | + * |
| 6 | + * How it works: |
| 7 | + * 1. Copies this package's scss/ folder to a unique temp directory. |
| 8 | + * 2. Overwrites _config.scss in that temp directory with your project's config. |
| 9 | + * 3. Compiles from the temp directory — node_modules is never modified. |
| 10 | + * 4. Writes atomic.css and atomic.min.css to your output folder (--output). |
| 11 | + * 5. Cleans up the temp directory in the finally block. |
| 12 | + * |
| 13 | + * Usage (in your project's package.json scripts): |
| 14 | + * node node_modules/@viivue/atomic-css/bin/build.js \ |
| 15 | + * --config path/to/your/_config.scss \ |
| 16 | + * --output path/to/your/output/folder |
| 17 | + */ |
| 18 | + |
| 19 | +'use strict'; |
| 20 | + |
| 21 | +const os = require('os'); |
| 22 | +const path = require('path'); |
| 23 | +const fs = require('fs'); |
| 24 | +const sass = require('sass'); |
| 25 | +const csso = require('csso'); |
| 26 | + |
| 27 | +// ── Read CLI arguments ──────────────────────────────────────────────────────── |
| 28 | +const args = process.argv.slice(2); |
| 29 | + |
| 30 | +function getArg(name){ |
| 31 | + const i = args.indexOf(name); |
| 32 | + const val = i !== -1 ? args[i + 1] : undefined; |
| 33 | + // Reject if the "value" is itself a flag (e.g. --config --output ./out) |
| 34 | + return val && !val.startsWith('--') ? val : null; |
| 35 | +} |
| 36 | + |
| 37 | +const configArg = getArg('--config'); |
| 38 | +const outputArg = getArg('--output'); |
| 39 | + |
| 40 | +// Both --config and --output are required. |
| 41 | +// If called with no args at all (e.g. checking the binary exists), exit silently. |
| 42 | +// If only one arg is missing, tell the user which one and exit with code 1 |
| 43 | +// so npm scripts and CI pipelines correctly detect the failure. |
| 44 | +if(!configArg || !outputArg){ |
| 45 | + if(args.length === 0) process.exit(0); |
| 46 | + if(!configArg) console.error('\n [atomic-css] Missing required argument: --config <path/to/_config.scss>\n'); |
| 47 | + if(!outputArg) console.error('\n [atomic-css] Missing required argument: --output <path/to/output/>\n'); |
| 48 | + process.exit(1); |
| 49 | +} |
| 50 | + |
| 51 | +const configPath = path.resolve(process.cwd(), configArg); |
| 52 | +const outputDir = path.resolve(process.cwd(), outputArg); |
| 53 | + |
| 54 | +if(!fs.existsSync(configPath)){ |
| 55 | + console.error(`[atomic-css] Config not found: ${configPath}`); |
| 56 | + process.exit(1); |
| 57 | +} |
| 58 | + |
| 59 | +// ── Paths inside this package ───────────────────────────────────────────────── |
| 60 | +// __dirname is the bin/ folder of this package (inside node_modules) |
| 61 | +const pkgDir = path.join(__dirname, '..'); |
| 62 | +const configDest = path.join(pkgDir, 'scss', '_config.scss'); // checked for package integrity only |
| 63 | +const buildEntry = path.join(pkgDir, 'scss', '_build.scss'); // main sass entry point |
| 64 | + |
| 65 | +// ── Helpers ─────────────────────────────────────────────────────────────────── |
| 66 | + |
| 67 | +// Read _build.scss and return the list of module names it imports |
| 68 | +function getModules(buildFile){ |
| 69 | + const content = fs.readFileSync(buildFile, 'utf8'); |
| 70 | + return [...content.matchAll(/@use\s+["']([^"']+)["']/g)].map(m => m[1]); |
| 71 | +} |
| 72 | + |
| 73 | +// Extract a short module name from a sass error/warning URL for readable logs |
| 74 | +function moduleFromUrl(url){ |
| 75 | + if(!url) return 'unknown'; |
| 76 | + const file = url.pathname || url.toString(); |
| 77 | + return path.basename(file, '.scss').replace(/^_/, ''); |
| 78 | +} |
| 79 | + |
| 80 | +// ── Build ───────────────────────────────────────────────────────────────────── |
| 81 | + |
| 82 | +// tempDir is declared outside try so finally can always clean it up. |
| 83 | +let tempDir = null; |
| 84 | + |
| 85 | +// buildFailed is set in catch so we can call process.exit(1) AFTER finally |
| 86 | +// has cleaned up the temp directory. |
| 87 | +// Never call process.exit() inside catch — it skips the finally block entirely. |
| 88 | +let buildFailed = false; |
| 89 | + |
| 90 | +try{ |
| 91 | + // Verify package integrity before doing anything |
| 92 | + if(!fs.existsSync(configDest) || !fs.existsSync(buildEntry)){ |
| 93 | + console.error(`[atomic-css] Package files missing in: ${path.join(pkgDir, 'scss')}`); |
| 94 | + console.error(' Try reinstalling @viivue/atomic-css.'); |
| 95 | + process.exit(1); |
| 96 | + } |
| 97 | + |
| 98 | + // Copy the entire scss/ folder to a unique temp directory so node_modules |
| 99 | + // is never modified — safe for read-only environments and concurrent builds. |
| 100 | + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atomic-css-')); |
| 101 | + fs.cpSync(path.join(pkgDir, 'scss'), tempDir, {recursive: true}); |
| 102 | + |
| 103 | + // Overwrite _config.scss in the temp directory with the user's config |
| 104 | + fs.copyFileSync(configPath, path.join(tempDir, '_config.scss')); |
| 105 | + |
| 106 | + const tempBuildEntry = path.join(tempDir, '_build.scss'); |
| 107 | + const modules = getModules(tempBuildEntry); |
| 108 | + |
| 109 | + // Create the output folder if it doesn't exist yet |
| 110 | + fs.mkdirSync(outputDir, {recursive: true}); |
| 111 | + |
| 112 | + const warnings = []; |
| 113 | + |
| 114 | + const {css} = sass.compile(tempBuildEntry, { |
| 115 | + sourceMap: false, |
| 116 | + // Allow sass to resolve package imports (e.g. @use "@viivue/atomic-css/...") |
| 117 | + // from the project's node_modules folder |
| 118 | + loadPaths: [path.join(process.cwd(), 'node_modules')], |
| 119 | + logger: { |
| 120 | + warn(message, {span}){ |
| 121 | + const mod = span?.url ? moduleFromUrl(span.url) : 'unknown'; |
| 122 | + warnings.push(` WARN [${mod}] ${message}`); |
| 123 | + }, |
| 124 | + debug(){ |
| 125 | + }, |
| 126 | + }, |
| 127 | + }); |
| 128 | + |
| 129 | + // Print each compiled module so the user knows what ran |
| 130 | + modules.forEach(m => console.log(` OK ${m}`)); |
| 131 | + |
| 132 | + if(warnings.length){ |
| 133 | + console.log(''); |
| 134 | + warnings.forEach(w => console.warn(w)); |
| 135 | + } |
| 136 | + |
| 137 | + // Minify and write both the readable and minified versions |
| 138 | + const {css: minified} = csso.minify(css); |
| 139 | + |
| 140 | + const cssOutPath = path.join(outputDir, 'atomic.css'); |
| 141 | + const minOutPath = path.join(outputDir, 'atomic.min.css'); |
| 142 | + fs.writeFileSync(cssOutPath, css); |
| 143 | + fs.writeFileSync(minOutPath, minified); |
| 144 | + |
| 145 | + function formatBytes(n){ |
| 146 | + return n >= 1024 ? (n / 1024).toFixed(1) + ' kB' : n + ' B'; |
| 147 | + } |
| 148 | + |
| 149 | + const cssSize = formatBytes(Buffer.byteLength(css)); |
| 150 | + const minSize = formatBytes(Buffer.byteLength(minified)); |
| 151 | + console.log(''); |
| 152 | + console.log(` ✓ atomic.css ${cssSize.padStart(8)} → ${outputDir}`); |
| 153 | + console.log(` ✓ atomic.min.css ${minSize.padStart(8)} → ${outputDir}`); |
| 154 | + |
| 155 | +}catch(err){ |
| 156 | + const mod = err.span?.url ? moduleFromUrl(err.span.url) : 'unknown'; |
| 157 | + // span.start.line is 0-based per the Sass JS API spec, so add 1 for display |
| 158 | + const line = err.span?.start?.line != null ? `:${err.span.start.line + 1}` : ''; |
| 159 | + console.error(''); |
| 160 | + console.error(` ERR [${mod}${line}] ${err.sassMessage || err.message}`); |
| 161 | + buildFailed = true; |
| 162 | + |
| 163 | +}finally{ |
| 164 | + // Clean up the temp directory whether the build succeeded or failed. |
| 165 | + if(tempDir){ |
| 166 | + try{ |
| 167 | + fs.rmSync(tempDir, {recursive: true, force: true}); |
| 168 | + }catch(cleanupErr){ |
| 169 | + console.error(` [atomic-css] WARNING: Could not clean up temp directory: ${cleanupErr.message}`); |
| 170 | + } |
| 171 | + } |
| 172 | +} |
| 173 | + |
| 174 | +// Exit after finally has cleaned up the temp directory. |
| 175 | +if(buildFailed) process.exit(1); |
0 commit comments