You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
412 lines
11 KiB
412 lines
11 KiB
#!/usr/bin/env node |
|
|
|
var Emitter = require('events').EventEmitter, |
|
forEach = require('async-foreach').forEach, |
|
Gaze = require('gaze'), |
|
meow = require('meow'), |
|
util = require('util'), |
|
path = require('path'), |
|
glob = require('glob'), |
|
sass = require('../lib'), |
|
render = require('../lib/render'), |
|
watcher = require('../lib/watcher'), |
|
stdout = require('stdout-stream'), |
|
stdin = require('get-stdin'), |
|
fs = require('fs'); |
|
|
|
/** |
|
* Initialize CLI |
|
*/ |
|
|
|
var cli = meow({ |
|
pkg: '../package.json', |
|
version: sass.info, |
|
help: [ |
|
'Usage:', |
|
' node-sass [options] <input.scss>', |
|
' cat <input.scss> | node-sass [options] > output.css', |
|
'', |
|
'Example: Compile foobar.scss to foobar.css', |
|
' node-sass --output-style compressed foobar.scss > foobar.css', |
|
' cat foobar.scss | node-sass --output-style compressed > foobar.css', |
|
'', |
|
'Example: Watch the sass directory for changes, compile with sourcemaps to the css directory', |
|
' node-sass --watch --recursive --output css', |
|
' --source-map true --source-map-contents sass', |
|
'', |
|
'Options', |
|
' -w, --watch Watch a directory or file', |
|
' -r, --recursive Recursively watch directories or files', |
|
' -o, --output Output directory', |
|
' -x, --omit-source-map-url Omit source map URL comment from output', |
|
' -i, --indented-syntax Treat data from stdin as sass code (versus scss)', |
|
' -q, --quiet Suppress log output except on error', |
|
' -v, --version Prints version info', |
|
' --output-style CSS output style (nested | expanded | compact | compressed)', |
|
' --indent-type Indent type for output CSS (space | tab)', |
|
' --indent-width Indent width; number of spaces or tabs (maximum value: 10)', |
|
' --linefeed Linefeed style (cr | crlf | lf | lfcr)', |
|
' --source-comments Include debug info in output', |
|
' --source-map Emit source map (boolean, or path to output .map file)', |
|
' --source-map-contents Embed include contents in map', |
|
' --source-map-embed Embed sourceMappingUrl as data URI', |
|
' --source-map-root Base path, will be emitted in source-map as is', |
|
' --include-path Path to look for imported files', |
|
' --follow Follow symlinked directories', |
|
' --precision The amount of precision allowed in decimal numbers', |
|
' --error-bell Output a bell character on errors', |
|
' --importer Path to .js file containing custom importer', |
|
' --functions Path to .js file containing custom functions', |
|
' --help Print usage info' |
|
].join('\n') |
|
}, { |
|
boolean: [ |
|
'error-bell', |
|
'follow', |
|
'indented-syntax', |
|
'omit-source-map-url', |
|
'quiet', |
|
'recursive', |
|
'source-map-embed', |
|
'source-map-contents', |
|
'source-comments', |
|
'watch' |
|
], |
|
string: [ |
|
'functions', |
|
'importer', |
|
'include-path', |
|
'indent-type', |
|
'linefeed', |
|
'output', |
|
'output-style', |
|
'precision', |
|
'source-map-root' |
|
], |
|
alias: { |
|
c: 'source-comments', |
|
i: 'indented-syntax', |
|
q: 'quiet', |
|
o: 'output', |
|
r: 'recursive', |
|
x: 'omit-source-map-url', |
|
v: 'version', |
|
w: 'watch' |
|
}, |
|
default: { |
|
'include-path': process.cwd(), |
|
'indent-type': 'space', |
|
'indent-width': 2, |
|
linefeed: 'lf', |
|
'output-style': 'nested', |
|
precision: 5, |
|
quiet: false, |
|
recursive: true |
|
} |
|
}); |
|
|
|
/** |
|
* Is a Directory |
|
* |
|
* @param {String} filePath |
|
* @returns {Boolean} |
|
* @api private |
|
*/ |
|
|
|
function isDirectory(filePath) { |
|
var isDir = false; |
|
try { |
|
var absolutePath = path.resolve(filePath); |
|
isDir = fs.statSync(absolutePath).isDirectory(); |
|
} catch (e) { |
|
isDir = e.code === 'ENOENT'; |
|
} |
|
return isDir; |
|
} |
|
|
|
/** |
|
* Get correct glob pattern |
|
* |
|
* @param {Object} options |
|
* @returns {String} |
|
* @api private |
|
*/ |
|
|
|
function globPattern(options) { |
|
return options.recursive ? '**/*.{sass,scss}' : '*.{sass,scss}'; |
|
} |
|
|
|
/** |
|
* Create emitter |
|
* |
|
* @api private |
|
*/ |
|
|
|
function getEmitter() { |
|
var emitter = new Emitter(); |
|
|
|
emitter.on('error', function(err) { |
|
if (options.errorBell) { |
|
err += '\x07'; |
|
} |
|
console.error(err); |
|
if (!options.watch) { |
|
process.exit(1); |
|
} |
|
}); |
|
|
|
emitter.on('warn', function(data) { |
|
if (!options.quiet) { |
|
console.warn(data); |
|
} |
|
}); |
|
|
|
emitter.on('info', function(data) { |
|
if (!options.quiet) { |
|
console.info(data); |
|
} |
|
}); |
|
|
|
emitter.on('log', stdout.write.bind(stdout)); |
|
|
|
return emitter; |
|
} |
|
|
|
/** |
|
* Construct options |
|
* |
|
* @param {Array} arguments |
|
* @param {Object} options |
|
* @api private |
|
*/ |
|
|
|
function getOptions(args, options) { |
|
var cssDir, sassDir, file, mapDir; |
|
options.src = args[0]; |
|
|
|
if (args[1]) { |
|
options.dest = path.resolve(args[1]); |
|
} else if (options.output) { |
|
options.dest = path.join( |
|
path.resolve(options.output), |
|
[path.basename(options.src, path.extname(options.src)), '.css'].join('')); // replace ext. |
|
} |
|
|
|
if (options.directory) { |
|
sassDir = path.resolve(options.directory); |
|
file = path.relative(sassDir, args[0]); |
|
cssDir = path.resolve(options.output); |
|
options.dest = path.join(cssDir, file).replace(path.extname(file), '.css'); |
|
} |
|
|
|
if (options.sourceMap) { |
|
if(!options.sourceMapOriginal) { |
|
options.sourceMapOriginal = options.sourceMap; |
|
} |
|
|
|
// check if sourceMap path ends with .map to avoid isDirectory false-positive |
|
var sourceMapIsDirectory = options.sourceMapOriginal.indexOf('.map', options.sourceMapOriginal.length - 4) === -1 && isDirectory(options.sourceMapOriginal); |
|
|
|
if (options.sourceMapOriginal === 'true') { |
|
options.sourceMap = options.dest + '.map'; |
|
} else if (!sourceMapIsDirectory) { |
|
options.sourceMap = path.resolve(options.sourceMapOriginal); |
|
} else if (sourceMapIsDirectory) { |
|
if (!options.directory) { |
|
options.sourceMap = path.resolve(options.sourceMapOriginal, path.basename(options.dest) + '.map'); |
|
} else { |
|
sassDir = path.resolve(options.directory); |
|
file = path.relative(sassDir, args[0]); |
|
mapDir = path.resolve(options.sourceMapOriginal); |
|
options.sourceMap = path.join(mapDir, file).replace(path.extname(file), '.css.map'); |
|
} |
|
} |
|
} |
|
|
|
return options; |
|
} |
|
|
|
/** |
|
* Watch |
|
* |
|
* @param {Object} options |
|
* @param {Object} emitter |
|
* @api private |
|
*/ |
|
|
|
function watch(options, emitter) { |
|
var handler = function(files) { |
|
files.added.forEach(function(file) { |
|
var watch = gaze.watched(); |
|
Object.keys(watch).forEach(function (dir) { |
|
if (watch[dir].indexOf(file) !== -1) { |
|
gaze.add(file); |
|
} |
|
}); |
|
}); |
|
|
|
files.changed.forEach(function(file) { |
|
if (path.basename(file)[0] !== '_') { |
|
renderFile(file, options, emitter); |
|
} |
|
}); |
|
|
|
files.removed.forEach(function(file) { |
|
gaze.remove(file); |
|
}); |
|
}; |
|
|
|
var gaze = new Gaze(); |
|
gaze.add(watcher.reset(options)); |
|
gaze.on('error', emitter.emit.bind(emitter, 'error')); |
|
|
|
gaze.on('changed', function(file) { |
|
handler(watcher.changed(file)); |
|
}); |
|
|
|
gaze.on('added', function(file) { |
|
handler(watcher.added(file)); |
|
}); |
|
|
|
gaze.on('deleted', function(file) { |
|
handler(watcher.removed(file)); |
|
}); |
|
} |
|
|
|
/** |
|
* Run |
|
* |
|
* @param {Object} options |
|
* @param {Object} emitter |
|
* @api private |
|
*/ |
|
|
|
function run(options, emitter) { |
|
if (!Array.isArray(options.includePath)) { |
|
options.includePath = [options.includePath]; |
|
} |
|
|
|
if (options.directory) { |
|
if (!options.output) { |
|
emitter.emit('error', 'An output directory must be specified when compiling a directory'); |
|
} |
|
if (!isDirectory(options.output)) { |
|
emitter.emit('error', 'An output directory must be specified when compiling a directory'); |
|
} |
|
} |
|
|
|
if (options.sourceMapOriginal && options.directory && !isDirectory(options.sourceMapOriginal) && options.sourceMapOriginal !== 'true') { |
|
emitter.emit('error', 'The --source-map option must be either a boolean or directory when compiling a directory'); |
|
} |
|
|
|
if (options.importer) { |
|
if ((path.resolve(options.importer) === path.normalize(options.importer).replace(/(.+)([\/|\\])$/, '$1'))) { |
|
options.importer = require(options.importer); |
|
} else { |
|
options.importer = require(path.resolve(options.importer)); |
|
} |
|
} |
|
|
|
if (options.functions) { |
|
if ((path.resolve(options.functions) === path.normalize(options.functions).replace(/(.+)([\/|\\])$/, '$1'))) { |
|
options.functions = require(options.functions); |
|
} else { |
|
options.functions = require(path.resolve(options.functions)); |
|
} |
|
} |
|
|
|
if (options.watch) { |
|
watch(options, emitter); |
|
} else if (options.directory) { |
|
renderDir(options, emitter); |
|
} else { |
|
render(options, emitter); |
|
} |
|
} |
|
|
|
/** |
|
* Render a file |
|
* |
|
* @param {String} file |
|
* @param {Object} options |
|
* @param {Object} emitter |
|
* @api private |
|
*/ |
|
function renderFile(file, options, emitter) { |
|
options = getOptions([path.resolve(file)], options); |
|
if (options.watch && !options.quiet) { |
|
emitter.emit('info', util.format('=> changed: %s', file)); |
|
} |
|
render(options, emitter); |
|
} |
|
|
|
/** |
|
* Render all sass files in a directory |
|
* |
|
* @param {Object} options |
|
* @param {Object} emitter |
|
* @api private |
|
*/ |
|
function renderDir(options, emitter) { |
|
var globPath = path.resolve(options.directory, globPattern(options)); |
|
glob(globPath, { ignore: '**/_*', follow: options.follow }, function(err, files) { |
|
if (err) { |
|
return emitter.emit('error', util.format('You do not have permission to access this path: %s.', err.path)); |
|
} else if (!files.length) { |
|
return emitter.emit('error', 'No input file was found.'); |
|
} |
|
|
|
forEach(files, function(subject) { |
|
emitter.once('done', this.async()); |
|
renderFile(subject, options, emitter); |
|
}, function(successful, arr) { |
|
var outputDir = path.join(process.cwd(), options.output); |
|
if (!options.quiet) { |
|
emitter.emit('info', util.format('Wrote %s CSS files to %s', arr.length, outputDir)); |
|
} |
|
process.exit(); |
|
}); |
|
}); |
|
} |
|
|
|
/** |
|
* Arguments and options |
|
*/ |
|
|
|
var options = getOptions(cli.input, cli.flags); |
|
var emitter = getEmitter(); |
|
|
|
/** |
|
* Show usage if no arguments are supplied |
|
*/ |
|
|
|
if (!options.src && process.stdin.isTTY) { |
|
emitter.emit('error', [ |
|
'Provide a Sass file to render', |
|
'', |
|
'Example: Compile foobar.scss to foobar.css', |
|
' node-sass --output-style compressed foobar.scss > foobar.css', |
|
' cat foobar.scss | node-sass --output-style compressed > foobar.css', |
|
'', |
|
'Example: Watch the sass directory for changes, compile with sourcemaps to the css directory', |
|
' node-sass --watch --recursive --output css', |
|
' --source-map true --source-map-contents sass', |
|
].join('\n')); |
|
} |
|
|
|
/** |
|
* Apply arguments |
|
*/ |
|
|
|
if (options.src) { |
|
if (isDirectory(options.src)) { |
|
options.directory = options.src; |
|
} |
|
run(options, emitter); |
|
} else if (!process.stdin.isTTY) { |
|
stdin(function(data) { |
|
options.data = data; |
|
options.stdin = true; |
|
run(options, emitter); |
|
}); |
|
}
|
|
|