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); | 
						|
  }); | 
						|
}
 | 
						|
 |