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.
444 lines
11 KiB
444 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(` |
|
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 |
|
`, { |
|
version: sass.info, |
|
flags: { |
|
errorBell: { |
|
type: 'boolean', |
|
}, |
|
functions: { |
|
type: 'string', |
|
}, |
|
follow: { |
|
type: 'boolean', |
|
}, |
|
importer: { |
|
type: 'string', |
|
}, |
|
includePath: { |
|
type: 'string', |
|
default: [process.cwd()], |
|
isMultiple: true, |
|
}, |
|
indentType: { |
|
type: 'string', |
|
default: 'space', |
|
}, |
|
indentWidth: { |
|
type: 'number', |
|
default: 2, |
|
}, |
|
indentedSyntax: { |
|
type: 'boolean', |
|
alias: 'i', |
|
}, |
|
linefeed: { |
|
type: 'string', |
|
default: 'lf', |
|
}, |
|
omitSourceMapUrl: { |
|
type: 'boolean', |
|
alias: 'x', |
|
}, |
|
output: { |
|
type: 'string', |
|
alias: 'o', |
|
}, |
|
outputStyle: { |
|
type: 'string', |
|
default: 'nested', |
|
}, |
|
precision: { |
|
type: 'number', |
|
default: 5, |
|
}, |
|
quiet: { |
|
type: 'boolean', |
|
default: false, |
|
alias: 'q', |
|
}, |
|
recursive: { |
|
type: 'boolean', |
|
default: true, |
|
alias: 'r', |
|
}, |
|
sourceMapContents: { |
|
type: 'boolean', |
|
}, |
|
sourceMapEmbed: { |
|
type: 'boolean', |
|
}, |
|
sourceMapRoot: { |
|
type: 'string', |
|
}, |
|
sourceComments: { |
|
type: 'boolean', |
|
alias: 'c', |
|
}, |
|
version: { |
|
type: 'boolean', |
|
alias: 'v', |
|
}, |
|
watch: { |
|
type: 'boolean', |
|
alias: 'w', |
|
}, |
|
}, |
|
}); |
|
|
|
/** |
|
* 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; |
|
} |
|
|
|
if (options.sourceMapOriginal === 'true') { |
|
options.sourceMap = options.dest + '.map'; |
|
} else { |
|
// 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 (!sourceMapIsDirectory) { |
|
options.sourceMap = path.resolve(options.sourceMapOriginal); |
|
} else 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 (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); |
|
}); |
|
}
|
|
|