Skip to content

Instantly share code, notes, and snippets.

@roc
Created February 6, 2013 12:59
Show Gist options
  • Save roc/4722375 to your computer and use it in GitHub Desktop.
Save roc/4722375 to your computer and use it in GitHub Desktop.
You want stylus -w my/dir to watch files and recursively build when an @imported file is modified. This doesn't happen by default so I'm writing this as a reference to the fix listed here: https://github.com/LearnBoost/stylus/issues/486 Thanks @Jonnywonny!
#!/usr/bin/env node
/**
* Module dependencies.
*/
var fs = require('fs')
, stylus = require('../lib/stylus')
, basename = require('path').basename
, dirname = require('path').dirname
, resolve = require('path').resolve
, join = require('path').join
, isWindows = process.platform === 'win32';
/**
* 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;
/**
* Firebug flag
*/
var firebug = 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;
/**
* Usage docs.
*/
var usage = [
''
, ' Usage: stylus [options] [command] [< in [> out]]'
, ' [file|dir ...]'
, ''
, ' Commands:'
, ''
, ' help [<type>:]<prop> Opens help info at MDC for <prop> in'
, ' your default browser. Optionally'
, ' searches other resources of <type>:'
, ' safari opera w3c ms caniuse quirksmode'
, ''
, ' Options:'
, ''
, ' -i, --interactive Start interactive REPL'
, ' -u, --use <path> Utilize the stylus plugin at <path>'
, ' -U, --inline Utilize image inlining via data uri support'
, ' -w, --watch Watch file(s) for changes and re-compile'
, ' -o, --out <dir> Output to <dir> when passing files'
, ' -C, --css <src> [dest] Convert css input to stylus'
, ' -I, --include <path> Add <path> 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'
, ' --include-css Include regular css on @import'
, ' -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);
process.exit(1);
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 '-V':
case '--version':
console.log(stylus.version);
process.exit(0);
break;
case '-o':
case '--out':
dest = args.shift();
if (!dest) throw new Error('--out <dir> 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 <property> required');
help(name);
break;
case '--include-css':
includeCSS = 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 <path> 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 <path> required');
// options
if ('--with' == args[0]) {
args.shift();
options = args.shift();
if (!options) throw new Error('--with <options> required');
options = eval('(' + options + ')');
}
// url support
if ('url' == path) {
urlFunction = options || {};
} else {
paths.push(dirname(path));
plugins.push({ path: path, options: options });
}
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$/);
});
}
/**
* 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
var options = {
filename: 'stdin'
, compress: compress
, firebug: firebug
, linenos: linenos
, paths: [process.cwd()].concat(paths)
};
// Buffer stdin
var str = '';
// Convert css to stylus
if (convertCSS) {
switch (files.length) {
case 2:
compileCSSFile(files[0], files[1]);
break;
case 1:
compileCSSFile(files[0], files[0].replace('.css', '.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 (files.length) {
compileFiles(files);
} else {
compileStdio();
}
}
/**
* Start stylus REPL.
*/
function repl() {
var options = { 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();
var ret = evaluator.visit(expr);
ret = ret.nodes[ret.nodes.length - 1];
ret = ret.toString();
if ('(' == ret[0]) ret = ret.replace(/^\(|\)$/g, '');
console.log('\033[90m=> \033[0m' + highlight(ret));
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.lstat(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);
usePlugins(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);
}
/**
* Compile the given file.
*/
function compileFile(file) {
// ensure file exists
fs.lstat(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);
usePlugins(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);
}
});
});
// 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 writeFile(file, css) {
// --out support
var path = dest
? join(dest, basename(file, '.styl') + '.css')
: file.replace('.styl', '.css');
fs.writeFile(path, css, function(err){
if (err) throw err;
console.log(' \033[90mcompiled\033[0m %s', path);
// --watch support
watch(file, compileFile);
});
}
// /**
// * Watch the given `file` and invoke `fn` when modified.
// */
// function watch(file, fn) {
// // not watching
// if (!watchers) return;
// // already watched
// if (watchers[file]) return;
// // watch the file itself
// watchers[file] = true;
// 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, function(event) {
// if (event === 'change') fn(file);
// });
// } else {
// fs.watchFile(file, { interval: 50 }, function(curr, prev) {
// if (curr.mtime > prev.mtime) fn(file);
// });
// }
// }
var parentFilePairs = {};
function watch(file, fn, parentFile) {
// not watching
if (!watchers) return;
if (parentFile && parentFilePairs[file + '|' + parentFile]) return;
parentFilePairs[file + '|' + parentFile] = true;
// already watched
if (watchers[file]) { // jonnywonny: I changed this
watchers[file].push(fn);
return;
}
// watch the file itself
watchers[file] = [fn];
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, function(event) {
if (event === 'change') { // jonnywonny: and this
for (var i = 0; i < watchers[file].length; ++ i) {
var watcher = watchers[file][i];
watcher(file);
}
}
});
} else {
fs.watchFile(file, { interval: 50 }, function(curr, prev) {
if (curr.mtime > prev.mtime) { // jonnywonny: and this
for (var i = 0; i < watchers[file].length; ++ i) {
var watcher = watchers[file][i];
watcher(file);
}
}
});
}
}
/**
* Watch `imports`, re-compiling `file` when they change.
*/
function watchImports(file, imports) {
imports.forEach(function(imported){
if (!imported.path) return;
watch(imported.path, function(){
compileFile(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));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment