UNPKG

11.6 kBPlain TextView Raw
1#!/usr/bin/env node
2
3var Emitter = require('events').EventEmitter,
4 forEach = require('async-foreach').forEach,
5 Gaze = require('gaze'),
6 meow = require('meow'),
7 util = require('util'),
8 path = require('path'),
9 glob = require('glob'),
10 sass = require('../lib'),
11 render = require('../lib/render'),
12 watcher = require('../lib/watcher'),
13 stdout = require('stdout-stream'),
14 stdin = require('get-stdin'),
15 fs = require('fs');
16
17/**
18 * Initialize CLI
19 */
20
21var cli = meow(`
22 Usage:
23 node-sass [options] <input.scss>
24 cat <input.scss> | node-sass [options] > output.css
25
26 Example: Compile foobar.scss to foobar.css
27 node-sass --output-style compressed foobar.scss > foobar.css
28 cat foobar.scss | node-sass --output-style compressed > foobar.css
29
30 Example: Watch the sass directory for changes, compile with sourcemaps to the css directory
31 node-sass --watch --recursive --output css
32 --source-map true --source-map-contents sass
33
34 Options
35 -w, --watch Watch a directory or file
36 -r, --recursive Recursively watch directories or files
37 -o, --output Output directory
38 -x, --omit-source-map-url Omit source map URL comment from output
39 -i, --indented-syntax Treat data from stdin as sass code (versus scss)
40 -q, --quiet Suppress log output except on error
41 -v, --version Prints version info
42 --output-style CSS output style (nested | expanded | compact | compressed)
43 --indent-type Indent type for output CSS (space | tab)
44 --indent-width Indent width; number of spaces or tabs (maximum value: 10)
45 --linefeed Linefeed style (cr | crlf | lf | lfcr)
46 --source-comments Include debug info in output
47 --source-map Emit source map (boolean, or path to output .map file)
48 --source-map-contents Embed include contents in map
49 --source-map-embed Embed sourceMappingUrl as data URI
50 --source-map-root Base path, will be emitted in source-map as is
51 --include-path Path to look for imported files
52 --follow Follow symlinked directories
53 --precision The amount of precision allowed in decimal numbers
54 --error-bell Output a bell character on errors
55 --importer Path to .js file containing custom importer
56 --functions Path to .js file containing custom functions
57 --help Print usage info
58`, {
59 version: sass.info,
60 flags: {
61 errorBell: {
62 type: 'boolean',
63 },
64 functions: {
65 type: 'string',
66 },
67 follow: {
68 type: 'boolean',
69 },
70 importer: {
71 type: 'string',
72 },
73 includePath: {
74 type: 'string',
75 default: [process.cwd()],
76 isMultiple: true,
77 },
78 indentType: {
79 type: 'string',
80 default: 'space',
81 },
82 indentWidth: {
83 type: 'number',
84 default: 2,
85 },
86 indentedSyntax: {
87 type: 'boolean',
88 alias: 'i',
89 },
90 linefeed: {
91 type: 'string',
92 default: 'lf',
93 },
94 omitSourceMapUrl: {
95 type: 'boolean',
96 alias: 'x',
97 },
98 output: {
99 type: 'string',
100 alias: 'o',
101 },
102 outputStyle: {
103 type: 'string',
104 default: 'nested',
105 },
106 precision: {
107 type: 'number',
108 default: 5,
109 },
110 quiet: {
111 type: 'boolean',
112 default: false,
113 alias: 'q',
114 },
115 recursive: {
116 type: 'boolean',
117 default: true,
118 alias: 'r',
119 },
120 sourceMapContents: {
121 type: 'boolean',
122 },
123 sourceMapEmbed: {
124 type: 'boolean',
125 },
126 sourceMapRoot: {
127 type: 'string',
128 },
129 sourceComments: {
130 type: 'boolean',
131 alias: 'c',
132 },
133 version: {
134 type: 'boolean',
135 alias: 'v',
136 },
137 watch: {
138 type: 'boolean',
139 alias: 'w',
140 },
141 },
142});
143
144/**
145 * Is a Directory
146 *
147 * @param {String} filePath
148 * @returns {Boolean}
149 * @api private
150 */
151
152function isDirectory(filePath) {
153 var isDir = false;
154 try {
155 var absolutePath = path.resolve(filePath);
156 isDir = fs.statSync(absolutePath).isDirectory();
157 } catch (e) {
158 isDir = e.code === 'ENOENT';
159 }
160 return isDir;
161}
162
163/**
164 * Get correct glob pattern
165 *
166 * @param {Object} options
167 * @returns {String}
168 * @api private
169 */
170
171function globPattern(options) {
172 return options.recursive ? '**/*.{sass,scss}' : '*.{sass,scss}';
173}
174
175/**
176 * Create emitter
177 *
178 * @api private
179 */
180
181function getEmitter() {
182 var emitter = new Emitter();
183
184 emitter.on('error', function(err) {
185 if (options.errorBell) {
186 err += '\x07';
187 }
188 console.error(err);
189 if (!options.watch) {
190 process.exit(1);
191 }
192 });
193
194 emitter.on('warn', function(data) {
195 if (!options.quiet) {
196 console.warn(data);
197 }
198 });
199
200 emitter.on('info', function(data) {
201 if (!options.quiet) {
202 console.info(data);
203 }
204 });
205
206 emitter.on('log', stdout.write.bind(stdout));
207
208 return emitter;
209}
210
211/**
212 * Construct options
213 *
214 * @param {Array} arguments
215 * @param {Object} options
216 * @api private
217 */
218
219function getOptions(args, options) {
220 var cssDir, sassDir, file, mapDir;
221 options.src = args[0];
222
223 if (args[1]) {
224 options.dest = path.resolve(args[1]);
225 } else if (options.output) {
226 options.dest = path.join(
227 path.resolve(options.output),
228 [path.basename(options.src, path.extname(options.src)), '.css'].join('')); // replace ext.
229 }
230
231 if (options.directory) {
232 sassDir = path.resolve(options.directory);
233 file = path.relative(sassDir, args[0]);
234 cssDir = path.resolve(options.output);
235 options.dest = path.join(cssDir, file).replace(path.extname(file), '.css');
236 }
237
238 if (options.sourceMap) {
239 if(!options.sourceMapOriginal) {
240 options.sourceMapOriginal = options.sourceMap;
241 }
242
243 if (options.sourceMapOriginal === 'true') {
244 options.sourceMap = options.dest + '.map';
245 } else {
246 // check if sourceMap path ends with .map to avoid isDirectory false-positive
247 var sourceMapIsDirectory = options.sourceMapOriginal.indexOf('.map', options.sourceMapOriginal.length - 4) === -1 && isDirectory(options.sourceMapOriginal);
248
249 if (!sourceMapIsDirectory) {
250 options.sourceMap = path.resolve(options.sourceMapOriginal);
251 } else if (!options.directory) {
252 options.sourceMap = path.resolve(options.sourceMapOriginal, path.basename(options.dest) + '.map');
253 } else {
254 sassDir = path.resolve(options.directory);
255 file = path.relative(sassDir, args[0]);
256 mapDir = path.resolve(options.sourceMapOriginal);
257 options.sourceMap = path.join(mapDir, file).replace(path.extname(file), '.css.map');
258 }
259 }
260 }
261
262 return options;
263}
264
265/**
266 * Watch
267 *
268 * @param {Object} options
269 * @param {Object} emitter
270 * @api private
271 */
272
273function watch(options, emitter) {
274 var handler = function(files) {
275 files.added.forEach(function(file) {
276 var watch = gaze.watched();
277 Object.keys(watch).forEach(function (dir) {
278 if (watch[dir].indexOf(file) !== -1) {
279 gaze.add(file);
280 }
281 });
282 });
283
284 files.changed.forEach(function(file) {
285 if (path.basename(file)[0] !== '_') {
286 renderFile(file, options, emitter);
287 }
288 });
289
290 files.removed.forEach(function(file) {
291 gaze.remove(file);
292 });
293 };
294
295 var gaze = new Gaze();
296 gaze.add(watcher.reset(options));
297 gaze.on('error', emitter.emit.bind(emitter, 'error'));
298
299 gaze.on('changed', function(file) {
300 handler(watcher.changed(file));
301 });
302
303 gaze.on('added', function(file) {
304 handler(watcher.added(file));
305 });
306
307 gaze.on('deleted', function(file) {
308 handler(watcher.removed(file));
309 });
310}
311
312/**
313 * Run
314 *
315 * @param {Object} options
316 * @param {Object} emitter
317 * @api private
318 */
319
320function run(options, emitter) {
321 if (options.directory) {
322 if (!options.output) {
323 emitter.emit('error', 'An output directory must be specified when compiling a directory');
324 }
325 if (!isDirectory(options.output)) {
326 emitter.emit('error', 'An output directory must be specified when compiling a directory');
327 }
328 }
329
330 if (options.sourceMapOriginal && options.directory && !isDirectory(options.sourceMapOriginal) && options.sourceMapOriginal !== 'true') {
331 emitter.emit('error', 'The --source-map option must be either a boolean or directory when compiling a directory');
332 }
333
334 if (options.importer) {
335 if ((path.resolve(options.importer) === path.normalize(options.importer).replace(/(.+)([/|\\])$/, '$1'))) {
336 options.importer = require(options.importer);
337 } else {
338 options.importer = require(path.resolve(options.importer));
339 }
340 }
341
342 if (options.functions) {
343 if ((path.resolve(options.functions) === path.normalize(options.functions).replace(/(.+)([/|\\])$/, '$1'))) {
344 options.functions = require(options.functions);
345 } else {
346 options.functions = require(path.resolve(options.functions));
347 }
348 }
349
350 if (options.watch) {
351 watch(options, emitter);
352 } else if (options.directory) {
353 renderDir(options, emitter);
354 } else {
355 render(options, emitter);
356 }
357}
358
359/**
360 * Render a file
361 *
362 * @param {String} file
363 * @param {Object} options
364 * @param {Object} emitter
365 * @api private
366 */
367function renderFile(file, options, emitter) {
368 options = getOptions([path.resolve(file)], options);
369 if (options.watch && !options.quiet) {
370 emitter.emit('info', util.format('=> changed: %s', file));
371 }
372 render(options, emitter);
373}
374
375/**
376 * Render all sass files in a directory
377 *
378 * @param {Object} options
379 * @param {Object} emitter
380 * @api private
381 */
382function renderDir(options, emitter) {
383 var globPath = path.resolve(options.directory, globPattern(options));
384 glob(globPath, { ignore: '**/_*', follow: options.follow }, function(err, files) {
385 if (err) {
386 return emitter.emit('error', util.format('You do not have permission to access this path: %s.', err.path));
387 } else if (!files.length) {
388 return emitter.emit('error', 'No input file was found.');
389 }
390
391 forEach(files, function(subject) {
392 emitter.once('done', this.async());
393 renderFile(subject, options, emitter);
394 }, function(successful, arr) {
395 var outputDir = path.join(process.cwd(), options.output);
396 if (!options.quiet) {
397 emitter.emit('info', util.format('Wrote %s CSS files to %s', arr.length, outputDir));
398 }
399 process.exit();
400 });
401 });
402}
403
404/**
405 * Arguments and options
406 */
407
408var options = getOptions(cli.input, cli.flags);
409var emitter = getEmitter();
410
411/**
412 * Show usage if no arguments are supplied
413 */
414
415if (!options.src && process.stdin.isTTY) {
416 emitter.emit('error', [
417 'Provide a Sass file to render',
418 '',
419 'Example: Compile foobar.scss to foobar.css',
420 ' node-sass --output-style compressed foobar.scss > foobar.css',
421 ' cat foobar.scss | node-sass --output-style compressed > foobar.css',
422 '',
423 'Example: Watch the sass directory for changes, compile with sourcemaps to the css directory',
424 ' node-sass --watch --recursive --output css',
425 ' --source-map true --source-map-contents sass',
426 ].join('\n'));
427}
428
429/**
430 * Apply arguments
431 */
432
433if (options.src) {
434 if (isDirectory(options.src)) {
435 options.directory = options.src;
436 }
437 run(options, emitter);
438} else if (!process.stdin.isTTY) {
439 stdin(function(data) {
440 options.data = data;
441 options.stdin = true;
442 run(options, emitter);
443 });
444}