#!/usr/bin/env node /** * Module dependencies. */ var fs = require('fs') , stylus = require('../lib/stylus') , basename = require('path').basename , dirname = require('path').dirname , extname = require('path').extname , resolve = require('path').resolve , join = require('path').join , isWindows = process.platform === 'win32' , semver = require('semver') , mkdirSync = semver.satisfies(process.version, '>=10.12.0') ? fs.mkdirSync : require('mkdirp').sync; /** * Arguments. */ var args = process.argv.slice(2); /** * Compare flag. */ var compare = false; /** * Compress flag. */ var compress = false; /** * CSS conversion flag. */ var convertCSS = false; /** * Line numbers flag. */ var linenos = false; /** * CSS class prefix. */ var prefix = ''; /** * Print to stdout flag. */ var print = false; /** * Firebug flag */ var firebug = false; /** * Quiet flag */ var quiet = false; /** * Sourcemap flag */ var sourcemap = false; /** * Files to processes. */ var files = []; /** * Import paths. */ var paths = []; /** * Destination directory. */ var dest; /** * Watcher hash. */ var watchers; /** * Enable REPL. */ var interactive; /** * Plugins. */ var plugins = []; /** * Optional url() function. */ var urlFunction = false; /** * Include CSS on import. */ var includeCSS = false; /** * Set file imports. */ var imports = []; /** * Resolve relative urls */ var resolveURL = false; /** * Disable cache. */ var disableCache = false; /** * Display dependencies flag. */ var deps = false; /** * Hoist at-rules. */ var hoist = false; /** * Specify custom file extension. */ var ext = '.css'; /** * Usage docs. */ var usage = [ '' , ' Usage: stylus [options] [command] [< in [> out]]' , ' [file|dir ...]' , '' , ' Commands:' , '' , ' help [:] Opens help info at MDN for in' , ' your default browser. Optionally' , ' searches other resources of :' , ' safari opera w3c ms caniuse quirksmode' , '' , ' Options:' , '' , ' -i, --interactive Start interactive REPL' , ' -u, --use Utilize the Stylus plugin at ' , ' -U, --inline Utilize image inlining via data URI support' , ' -w, --watch Watch file(s) for changes and re-compile' , ' -o, --out Output to when passing files' , ' -C, --css [dest] Convert CSS input to Stylus' , ' -I, --include Add to lookup paths' , ' -c, --compress Compress CSS output' , ' -d, --compare Display input along with output' , ' -f, --firebug Emits debug infos in the generated CSS that' , ' can be used by the FireStylus Firebug plugin' , ' -l, --line-numbers Emits comments in the generated CSS' , ' indicating the corresponding Stylus line' , ' -m, --sourcemap Generates a sourcemap in sourcemaps v3 format' , ' -q, --quiet Less noisy output' , ' --sourcemap-inline Inlines sourcemap with full source text in base64 format' , ' --sourcemap-root "sourceRoot" property of the generated sourcemap' , ' --sourcemap-base Base from which sourcemap and all sources are relative' , ' -P, --prefix [prefix] prefix all css classes' , ' -p, --print Print out the compiled CSS' , ' --import Import stylus ' , ' --include-css Include regular CSS on @import' , ' --ext Specify custom file extension for compiled file, default .css' , ' -D, --deps Display dependencies of the compiled file' , ' --disable-cache Disable caching' , ' --hoist-atrules Move @import and @charset to the top' , ' -r, --resolve-url Resolve relative urls inside imports' , ' --resolve-url-nocheck Like --resolve-url but without file existence check' , ' -V, --version Display the version of Stylus' , ' -h, --help Display help information' , '' ].join('\n'); /** * Handle arguments. */ var arg; while (args.length) { arg = args.shift(); switch (arg) { case '-h': case '--help': console.error(usage); return; case '-d': case '--compare': compare = true; break; case '-c': case '--compress': compress = true; break; case '-C': case '--css': convertCSS = true; break; case '-f': case '--firebug': firebug = true; break; case '-l': case '--line-numbers': linenos = true; break; case '-m': case '--sourcemap': sourcemap = {}; break; case '-q': case '--quiet': quiet = true; break; case '--sourcemap-inline': sourcemap = sourcemap || {}; sourcemap.inline = true; break; case '--sourcemap-root': var url = args.shift(); if (!url) throw new Error('--sourcemap-root required'); sourcemap = sourcemap || {}; sourcemap.sourceRoot = url; break; case '--sourcemap-base': var path = args.shift(); if (!path) throw new Error('--sourcemap-base required'); sourcemap = sourcemap || {}; sourcemap.basePath = path; break; case '-P': case '--prefix': prefix = args.shift(); if (!prefix) throw new Error('--prefix required'); break; case '-p': case '--print': print = true; break; case '-V': case '--version': console.log(stylus.version); return; case '-o': case '--out': dest = args.shift(); if (!dest) throw new Error('--out required'); break; case 'help': var name = args.shift() , browser = name.split(':'); if (browser.length > 1) { name = [].slice.call(browser, 1).join(':'); browser = browser[0]; } else { name = browser[0]; browser = ''; } if (!name) throw new Error('help required'); help(name); break; case '--include-css': includeCSS = true; break; case '--ext': ext = args.shift(); if (!ext) throw new Error('--ext required'); break; case '--disable-cache': disableCache = true; break; case '--hoist-atrules': hoist = true; break; case '-i': case '--repl': case '--interactive': interactive = true; break; case '-I': case '--include': var path = args.shift(); if (!path) throw new Error('--include required'); paths.push(path); break; case '-w': case '--watch': watchers = {}; break; case '-U': case '--inline': args.unshift('--use', 'url'); break; case '-u': case '--use': var options; var path = args.shift(); if (!path) throw new Error('--use required'); // options if ('--with' == args[0]) { args.shift(); options = args.shift(); if (!options) throw new Error('--with required'); options = eval('(' + options + ')'); } // url support if ('url' == path) { urlFunction = options || {}; } else { paths.push(dirname(path)); plugins.push({ path: path, options: options }); } break; case '--import': var file = args.shift(); if (!file) throw new Error('--import required'); imports.push(file); break; case '-r': case '--resolve-url': resolveURL = {}; break; case '--resolve-url-nocheck': resolveURL = { nocheck: true }; break; case '-D': case '--deps': deps = true; break; default: files.push(arg); } } // if --watch is used, assume we are // not working with stdio if (watchers && !files.length) { files = fs.readdirSync(process.cwd()) .filter(function(file){ return file.match(/\.styl$/); }); } // --sourcemap flag is not working with stdio if (sourcemap && !files.length) sourcemap = false; /** * Open the default browser to the CSS property `name`. * * @param {String} name */ function help(name) { var url , exec = require('child_process').exec , command; name = encodeURIComponent(name); switch (browser) { case 'safari': case 'webkit': url = 'https://developer.apple.com/library/safari/search/?q=' + name; break; case 'opera': url = 'http://dev.opera.com/search/?term=' + name; break; case 'w3c': url = 'http://www.google.com/search?q=site%3Awww.w3.org%2FTR+' + name; break; case 'ms': url = 'http://social.msdn.microsoft.com/search/en-US/ie?query=' + name + '&refinement=59%2c61'; break; case 'caniuse': url = 'http://caniuse.com/#search=' + name; break; case 'quirksmode': url = 'http://www.google.com/search?q=site%3Awww.quirksmode.org+' + name; break; default: url = 'https://developer.mozilla.org/en/CSS/' + name; } switch (process.platform) { case 'linux': command = 'x-www-browser'; break; default: command = 'open'; } exec(command + ' "' + url + '"', function(){ process.exit(0); }); } // Compilation options if (files.length > 1 && isCSS(dest)) { dest = dirname(dest); } var options = { filename: 'stdin' , compress: compress , firebug: firebug , linenos: linenos , sourcemap: sourcemap , paths: [process.cwd()].concat(paths) , prefix: prefix , dest: dest , 'hoist atrules': hoist }; // Buffer stdin var str = ''; // Convert CSS to Stylus if (convertCSS) { switch (files.length) { case 2: compileCSSFile(files[0], files[1]); break; case 1: var file = files[0]; compileCSSFile(file, join(dirname(file), basename(file, extname(file))) + '.styl'); break; default: var stdin = process.openStdin(); stdin.setEncoding('utf8'); stdin.on('data', function(chunk){ str += chunk; }); stdin.on('end', function(){ var out = stylus.convertCSS(str); console.log(out); }); } } else if (interactive) { repl(); } else if (deps) { // if --deps is used, just display list of the dependencies // not working with stdio and dirs displayDeps(); } else { if (files.length) { compileFiles(files); } else { compileStdio(); } } /** * Start Stylus REPL. */ function repl() { var options = { cache: false, filename: 'stdin', imports: [join(__dirname, '..', 'lib', 'functions')] } , parser = new stylus.Parser('', options) , evaluator = new stylus.Evaluator(parser.parse(), options) , rl = require('readline') , repl = rl.createInterface(process.stdin, process.stdout, autocomplete) , global = evaluator.global.scope; // expose BIFs evaluator.evaluate(); // readline repl.setPrompt('> '); repl.prompt(); // HACK: flat-list auto-complete function autocomplete(line){ var out = process.stdout , keys = Object.keys(global.locals) , len = keys.length , words = line.split(/\s+/) , word = words.pop() , names = [] , name , node , key; // find words that match for (var i = 0; i < len; ++i) { key = keys[i]; if (0 == key.indexOf(word)) { node = global.lookup(key); switch (node.nodeName) { case 'function': names.push(node.toString()); break; default: names.push(key); } } } return [names, line]; }; repl.on('line', function(line){ if (!line.trim().length) return repl.prompt(); parser = new stylus.Parser(line, options); parser.state.push('expression'); evaluator.return = true; try { var expr = parser.parse(); evaluator.root = expr; var ret = evaluator.evaluate(); var node; while (node = ret.nodes.pop()) { if (!node.suppress) { var str = node.toString(); if ('(' == str[0]) str = str.replace(/^\(|\)$/g, ''); console.log('\033[90m=> \033[0m' + highlight(str)); break; } } repl.prompt(); } catch (err) { console.error('\033[31merror: %s\033[0m', err.message || err.stack); repl.prompt(); } }); repl.on('SIGINT', function(){ console.log(); process.exit(0); }); } /** * Highlight the given string of Stylus. */ function highlight(str) { return str .replace(/(#)?(\d+(\.\d+)?)/g, function($0, $1, $2){ return $1 ? $0 : '\033[36m' + $2 + '\033[0m'; }) .replace(/(#[\da-fA-F]+)/g, '\033[33m$1\033[0m') .replace(/('.*?'|".*?")/g, '\033[32m$1\033[0m'); } /** * Convert a CSS file to a Styl file */ function compileCSSFile(file, fileOut) { fs.stat(file, function(err, stat){ if (err) throw err; if (stat.isFile()) { fs.readFile(file, 'utf8', function(err, str){ if (err) throw err; var styl = stylus.convertCSS(str); fs.writeFile(fileOut, styl, function(err){ if (err) throw err; }); }); } }); } /** * Compile with stdio. */ function compileStdio() { process.stdin.setEncoding('utf8'); process.stdin.on('data', function(chunk){ str += chunk; }); process.stdin.on('end', function(){ // Compile to css var style = stylus(str, options); if (includeCSS) style.set('include css', true); if (disableCache) style.set('cache', false); usePlugins(style); importFiles(style); style.render(function(err, css){ if (err) throw err; if (compare) { console.log('\n\x1b[1mInput:\x1b[0m'); console.log(str); console.log('\n\x1b[1mOutput:\x1b[0m'); } console.log(css); if (compare) console.log(); }); }).resume(); } /** * Compile the given files. */ function compileFiles(files) { files.forEach(compileFile); } /** * Display dependencies of the compiled files. */ function displayDeps() { files.forEach(function(file){ // ensure file exists fs.stat(file, function(err, stat){ if (err) throw err; fs.readFile(file, 'utf8', function(err, str){ if (err) throw err; options.filename = file; var style = stylus(str, options); usePlugins(style); importFiles(style); console.log(style.deps().join('\n')); }); }); }); } /** * Compile the given file. */ function compileFile(file) { // ensure file exists fs.stat(file, function(err, stat){ if (err) throw err; // file if (stat.isFile()) { fs.readFile(file, 'utf8', function(err, str){ if (err) throw err; options.filename = file; options._imports = []; var style = stylus(str, options); if (includeCSS) style.set('include css', true); if (disableCache) style.set('cache', false); if (sourcemap) style.set('sourcemap', sourcemap); usePlugins(style); importFiles(style); style.render(function(err, css){ watchImports(file, options._imports); if (err) { if (watchers) { console.error(err.stack || err.message); } else { throw err; } } else { writeFile(file, css); // write sourcemap if (sourcemap && !sourcemap.inline) { writeSourcemap(file, style.sourcemap); } } }); }); // directory } else if (stat.isDirectory()) { fs.readdir(file, function(err, files){ if (err) throw err; files.filter(function(path){ return path.match(/\.styl$/); }).map(function(path){ return join(file, path); }).forEach(compileFile); }); } }); } /** * Write the given CSS output. */ function createPath(file, sourceMap) { var out; if (files.length === 1 && isCSS(dest)) { return [dest, sourceMap ? '.map' : ''].join(''); } // --out support out = [basename(file, extname(file)), sourceMap ? ext + '.map' : ext].join(''); return dest ? join(dest, out) : join(dirname(file), out); } /** * Check if the given path is a CSS file. */ function isCSS(file) { return file && '.css' === extname(file); } function writeFile(file, css) { // --print support if (print) return process.stdout.write(css); var path = createPath(file); mkdirSync(dirname(path), { recursive: true }) fs.writeFile(path, css, function(err){ if (err) throw err; console.log(' \033[90mcompiled\033[0m %s', path); // --watch support watch(file, file); }); } /** * Write the given sourcemap. */ function writeSourcemap(file, sourcemap) { var path = createPath(file, true); mkdirSync(dirname(path), { recursive: true }) fs.writeFile(path, JSON.stringify(sourcemap), function(err){ if (err) throw err; // don't output log message if --print is present if (!print) console.log(' \033[90mgenerated\033[0m %s', path); }); } /** * Watch the given `file` and recompiling `rootFile` when modified. */ function watch(file, rootFile) { // not watching if (!watchers) return; // already watched if (watchers[file]) { watchers[file][rootFile] = true; return; } // watch the file itself watchers[file] = {}; watchers[file][rootFile] = true; if (print) { console.error('Stylus CLI Error: Watch and print cannot be used together'); process.exit(1); } if(!quiet){ console.log(' \033[90mwatching\033[0m %s', file); } // if is windows use fs.watch api instead // TODO: remove watchFile when fs.watch() works on osx etc if (isWindows) { fs.watch(file, compile); } else { fs.watchFile(file, { interval: 300 }, function(curr, prev) { if (curr.mtime > prev.mtime) compile(); }); } function compile() { for (var rootFile in watchers[file]) { compileFile(rootFile); } } } /** * Watch `imports`, re-compiling `file` when they change. */ function watchImports(file, imports) { imports.forEach(function(imported){ if (!imported.path) return; watch(imported.path, file); }); } /** * Utilize plugins. */ function usePlugins(style) { plugins.forEach(function(plugin){ var path = plugin.path; var options = plugin.options; fn = require(/^\.+\//.test(path) ? resolve(path) : path); if ('function' != typeof fn) { throw new Error('plugin ' + path + ' does not export a function'); } style.use(fn(options)); }); if (urlFunction) { style.define('url', stylus.url(urlFunction)); } else if (resolveURL) { style.define('url', stylus.resolver(resolveURL)); } } /** * Imports the indicated files. */ function importFiles(style) { imports.forEach(function(file) { style.import(file); }); }