| #!/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); |
| }); |
| } |