stylus 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847
  1. #!/usr/bin/env node
  2. /**
  3. * Module dependencies.
  4. */
  5. var fs = require('fs')
  6. , stylus = require('../lib/stylus')
  7. , basename = require('path').basename
  8. , dirname = require('path').dirname
  9. , extname = require('path').extname
  10. , resolve = require('path').resolve
  11. , join = require('path').join
  12. , isWindows = process.platform === 'win32'
  13. , semver = require('semver')
  14. , mkdirSync = semver.satisfies(process.version, '>=10.12.0') ? fs.mkdirSync : require('mkdirp').sync;
  15. /**
  16. * Arguments.
  17. */
  18. var args = process.argv.slice(2);
  19. /**
  20. * Compare flag.
  21. */
  22. var compare = false;
  23. /**
  24. * Compress flag.
  25. */
  26. var compress = false;
  27. /**
  28. * CSS conversion flag.
  29. */
  30. var convertCSS = false;
  31. /**
  32. * Line numbers flag.
  33. */
  34. var linenos = false;
  35. /**
  36. * CSS class prefix.
  37. */
  38. var prefix = '';
  39. /**
  40. * Print to stdout flag.
  41. */
  42. var print = false;
  43. /**
  44. * Firebug flag
  45. */
  46. var firebug = false;
  47. /**
  48. * Quiet flag
  49. */
  50. var quiet = false;
  51. /**
  52. * Sourcemap flag
  53. */
  54. var sourcemap = false;
  55. /**
  56. * Files to processes.
  57. */
  58. var files = [];
  59. /**
  60. * Import paths.
  61. */
  62. var paths = [];
  63. /**
  64. * Destination directory.
  65. */
  66. var dest;
  67. /**
  68. * Watcher hash.
  69. */
  70. var watchers;
  71. /**
  72. * Enable REPL.
  73. */
  74. var interactive;
  75. /**
  76. * Plugins.
  77. */
  78. var plugins = [];
  79. /**
  80. * Optional url() function.
  81. */
  82. var urlFunction = false;
  83. /**
  84. * Include CSS on import.
  85. */
  86. var includeCSS = false;
  87. /**
  88. * Set file imports.
  89. */
  90. var imports = [];
  91. /**
  92. * Resolve relative urls
  93. */
  94. var resolveURL = false;
  95. /**
  96. * Disable cache.
  97. */
  98. var disableCache = false;
  99. /**
  100. * Display dependencies flag.
  101. */
  102. var deps = false;
  103. /**
  104. * Hoist at-rules.
  105. */
  106. var hoist = false;
  107. /**
  108. * Specify custom file extension.
  109. */
  110. var ext = '.css';
  111. /**
  112. * Usage docs.
  113. */
  114. var usage = [
  115. ''
  116. , ' Usage: stylus [options] [command] [< in [> out]]'
  117. , ' [file|dir ...]'
  118. , ''
  119. , ' Commands:'
  120. , ''
  121. , ' help [<type>:]<prop> Opens help info at MDN for <prop> in'
  122. , ' your default browser. Optionally'
  123. , ' searches other resources of <type>:'
  124. , ' safari opera w3c ms caniuse quirksmode'
  125. , ''
  126. , ' Options:'
  127. , ''
  128. , ' -i, --interactive Start interactive REPL'
  129. , ' -u, --use <path> Utilize the Stylus plugin at <path>'
  130. , ' -U, --inline Utilize image inlining via data URI support'
  131. , ' -w, --watch Watch file(s) for changes and re-compile'
  132. , ' -o, --out <dir> Output to <dir> when passing files'
  133. , ' -C, --css <src> [dest] Convert CSS input to Stylus'
  134. , ' -I, --include <path> Add <path> to lookup paths'
  135. , ' -c, --compress Compress CSS output'
  136. , ' -d, --compare Display input along with output'
  137. , ' -f, --firebug Emits debug infos in the generated CSS that'
  138. , ' can be used by the FireStylus Firebug plugin'
  139. , ' -l, --line-numbers Emits comments in the generated CSS'
  140. , ' indicating the corresponding Stylus line'
  141. , ' -m, --sourcemap Generates a sourcemap in sourcemaps v3 format'
  142. , ' -q, --quiet Less noisy output'
  143. , ' --sourcemap-inline Inlines sourcemap with full source text in base64 format'
  144. , ' --sourcemap-root <url> "sourceRoot" property of the generated sourcemap'
  145. , ' --sourcemap-base <path> Base <path> from which sourcemap and all sources are relative'
  146. , ' -P, --prefix [prefix] prefix all css classes'
  147. , ' -p, --print Print out the compiled CSS'
  148. , ' --import <file> Import stylus <file>'
  149. , ' --include-css Include regular CSS on @import'
  150. , ' --ext Specify custom file extension for compiled file, default .css'
  151. , ' -D, --deps Display dependencies of the compiled file'
  152. , ' --disable-cache Disable caching'
  153. , ' --hoist-atrules Move @import and @charset to the top'
  154. , ' -r, --resolve-url Resolve relative urls inside imports'
  155. , ' --resolve-url-nocheck Like --resolve-url but without file existence check'
  156. , ' -V, --version Display the version of Stylus'
  157. , ' -h, --help Display help information'
  158. , ''
  159. ].join('\n');
  160. /**
  161. * Handle arguments.
  162. */
  163. var arg;
  164. while (args.length) {
  165. arg = args.shift();
  166. switch (arg) {
  167. case '-h':
  168. case '--help':
  169. console.error(usage);
  170. return;
  171. case '-d':
  172. case '--compare':
  173. compare = true;
  174. break;
  175. case '-c':
  176. case '--compress':
  177. compress = true;
  178. break;
  179. case '-C':
  180. case '--css':
  181. convertCSS = true;
  182. break;
  183. case '-f':
  184. case '--firebug':
  185. firebug = true;
  186. break;
  187. case '-l':
  188. case '--line-numbers':
  189. linenos = true;
  190. break;
  191. case '-m':
  192. case '--sourcemap':
  193. sourcemap = {};
  194. break;
  195. case '-q':
  196. case '--quiet':
  197. quiet = true;
  198. break;
  199. case '--sourcemap-inline':
  200. sourcemap = sourcemap || {};
  201. sourcemap.inline = true;
  202. break;
  203. case '--sourcemap-root':
  204. var url = args.shift();
  205. if (!url) throw new Error('--sourcemap-root <url> required');
  206. sourcemap = sourcemap || {};
  207. sourcemap.sourceRoot = url;
  208. break;
  209. case '--sourcemap-base':
  210. var path = args.shift();
  211. if (!path) throw new Error('--sourcemap-base <path> required');
  212. sourcemap = sourcemap || {};
  213. sourcemap.basePath = path;
  214. break;
  215. case '-P':
  216. case '--prefix':
  217. prefix = args.shift();
  218. if (!prefix) throw new Error('--prefix <prefix> required');
  219. break;
  220. case '-p':
  221. case '--print':
  222. print = true;
  223. break;
  224. case '-V':
  225. case '--version':
  226. console.log(stylus.version);
  227. return;
  228. case '-o':
  229. case '--out':
  230. dest = args.shift();
  231. if (!dest) throw new Error('--out <dir> required');
  232. break;
  233. case 'help':
  234. var name = args.shift()
  235. , browser = name.split(':');
  236. if (browser.length > 1) {
  237. name = [].slice.call(browser, 1).join(':');
  238. browser = browser[0];
  239. } else {
  240. name = browser[0];
  241. browser = '';
  242. }
  243. if (!name) throw new Error('help <property> required');
  244. help(name);
  245. break;
  246. case '--include-css':
  247. includeCSS = true;
  248. break;
  249. case '--ext':
  250. ext = args.shift();
  251. if (!ext) throw new Error('--ext <ext> required');
  252. break;
  253. case '--disable-cache':
  254. disableCache = true;
  255. break;
  256. case '--hoist-atrules':
  257. hoist = true;
  258. break;
  259. case '-i':
  260. case '--repl':
  261. case '--interactive':
  262. interactive = true;
  263. break;
  264. case '-I':
  265. case '--include':
  266. var path = args.shift();
  267. if (!path) throw new Error('--include <path> required');
  268. paths.push(path);
  269. break;
  270. case '-w':
  271. case '--watch':
  272. watchers = {};
  273. break;
  274. case '-U':
  275. case '--inline':
  276. args.unshift('--use', 'url');
  277. break;
  278. case '-u':
  279. case '--use':
  280. var options;
  281. var path = args.shift();
  282. if (!path) throw new Error('--use <path> required');
  283. // options
  284. if ('--with' == args[0]) {
  285. args.shift();
  286. options = args.shift();
  287. if (!options) throw new Error('--with <options> required');
  288. options = eval('(' + options + ')');
  289. }
  290. // url support
  291. if ('url' == path) {
  292. urlFunction = options || {};
  293. } else {
  294. paths.push(dirname(path));
  295. plugins.push({ path: path, options: options });
  296. }
  297. break;
  298. case '--import':
  299. var file = args.shift();
  300. if (!file) throw new Error('--import <file> required');
  301. imports.push(file);
  302. break;
  303. case '-r':
  304. case '--resolve-url':
  305. resolveURL = {};
  306. break;
  307. case '--resolve-url-nocheck':
  308. resolveURL = { nocheck: true };
  309. break;
  310. case '-D':
  311. case '--deps':
  312. deps = true;
  313. break;
  314. default:
  315. files.push(arg);
  316. }
  317. }
  318. // if --watch is used, assume we are
  319. // not working with stdio
  320. if (watchers && !files.length) {
  321. files = fs.readdirSync(process.cwd())
  322. .filter(function(file){
  323. return file.match(/\.styl$/);
  324. });
  325. }
  326. // --sourcemap flag is not working with stdio
  327. if (sourcemap && !files.length) sourcemap = false;
  328. /**
  329. * Open the default browser to the CSS property `name`.
  330. *
  331. * @param {String} name
  332. */
  333. function help(name) {
  334. var url
  335. , exec = require('child_process').exec
  336. , command;
  337. name = encodeURIComponent(name);
  338. switch (browser) {
  339. case 'safari':
  340. case 'webkit':
  341. url = 'https://developer.apple.com/library/safari/search/?q=' + name;
  342. break;
  343. case 'opera':
  344. url = 'http://dev.opera.com/search/?term=' + name;
  345. break;
  346. case 'w3c':
  347. url = 'http://www.google.com/search?q=site%3Awww.w3.org%2FTR+' + name;
  348. break;
  349. case 'ms':
  350. url = 'http://social.msdn.microsoft.com/search/en-US/ie?query=' + name + '&refinement=59%2c61';
  351. break;
  352. case 'caniuse':
  353. url = 'http://caniuse.com/#search=' + name;
  354. break;
  355. case 'quirksmode':
  356. url = 'http://www.google.com/search?q=site%3Awww.quirksmode.org+' + name;
  357. break;
  358. default:
  359. url = 'https://developer.mozilla.org/en/CSS/' + name;
  360. }
  361. switch (process.platform) {
  362. case 'linux': command = 'x-www-browser'; break;
  363. default: command = 'open';
  364. }
  365. exec(command + ' "' + url + '"', function(){
  366. process.exit(0);
  367. });
  368. }
  369. // Compilation options
  370. if (files.length > 1 && isCSS(dest)) {
  371. dest = dirname(dest);
  372. }
  373. var options = {
  374. filename: 'stdin'
  375. , compress: compress
  376. , firebug: firebug
  377. , linenos: linenos
  378. , sourcemap: sourcemap
  379. , paths: [process.cwd()].concat(paths)
  380. , prefix: prefix
  381. , dest: dest
  382. , 'hoist atrules': hoist
  383. };
  384. // Buffer stdin
  385. var str = '';
  386. // Convert CSS to Stylus
  387. if (convertCSS) {
  388. switch (files.length) {
  389. case 2:
  390. compileCSSFile(files[0], files[1]);
  391. break;
  392. case 1:
  393. var file = files[0];
  394. compileCSSFile(file, join(dirname(file), basename(file, extname(file))) + '.styl');
  395. break;
  396. default:
  397. var stdin = process.openStdin();
  398. stdin.setEncoding('utf8');
  399. stdin.on('data', function(chunk){ str += chunk; });
  400. stdin.on('end', function(){
  401. var out = stylus.convertCSS(str);
  402. console.log(out);
  403. });
  404. }
  405. } else if (interactive) {
  406. repl();
  407. } else if (deps) {
  408. // if --deps is used, just display list of the dependencies
  409. // not working with stdio and dirs
  410. displayDeps();
  411. } else {
  412. if (files.length) {
  413. compileFiles(files);
  414. } else {
  415. compileStdio();
  416. }
  417. }
  418. /**
  419. * Start Stylus REPL.
  420. */
  421. function repl() {
  422. var options = { cache: false, filename: 'stdin', imports: [join(__dirname, '..', 'lib', 'functions')] }
  423. , parser = new stylus.Parser('', options)
  424. , evaluator = new stylus.Evaluator(parser.parse(), options)
  425. , rl = require('readline')
  426. , repl = rl.createInterface(process.stdin, process.stdout, autocomplete)
  427. , global = evaluator.global.scope;
  428. // expose BIFs
  429. evaluator.evaluate();
  430. // readline
  431. repl.setPrompt('> ');
  432. repl.prompt();
  433. // HACK: flat-list auto-complete
  434. function autocomplete(line){
  435. var out = process.stdout
  436. , keys = Object.keys(global.locals)
  437. , len = keys.length
  438. , words = line.split(/\s+/)
  439. , word = words.pop()
  440. , names = []
  441. , name
  442. , node
  443. , key;
  444. // find words that match
  445. for (var i = 0; i < len; ++i) {
  446. key = keys[i];
  447. if (0 == key.indexOf(word)) {
  448. node = global.lookup(key);
  449. switch (node.nodeName) {
  450. case 'function':
  451. names.push(node.toString());
  452. break;
  453. default:
  454. names.push(key);
  455. }
  456. }
  457. }
  458. return [names, line];
  459. };
  460. repl.on('line', function(line){
  461. if (!line.trim().length) return repl.prompt();
  462. parser = new stylus.Parser(line, options);
  463. parser.state.push('expression');
  464. evaluator.return = true;
  465. try {
  466. var expr = parser.parse();
  467. evaluator.root = expr;
  468. var ret = evaluator.evaluate();
  469. var node;
  470. while (node = ret.nodes.pop()) {
  471. if (!node.suppress) {
  472. var str = node.toString();
  473. if ('(' == str[0]) str = str.replace(/^\(|\)$/g, '');
  474. console.log('\033[90m=> \033[0m' + highlight(str));
  475. break;
  476. }
  477. }
  478. repl.prompt();
  479. } catch (err) {
  480. console.error('\033[31merror: %s\033[0m', err.message || err.stack);
  481. repl.prompt();
  482. }
  483. });
  484. repl.on('SIGINT', function(){
  485. console.log();
  486. process.exit(0);
  487. });
  488. }
  489. /**
  490. * Highlight the given string of Stylus.
  491. */
  492. function highlight(str) {
  493. return str
  494. .replace(/(#)?(\d+(\.\d+)?)/g, function($0, $1, $2){
  495. return $1 ? $0 : '\033[36m' + $2 + '\033[0m';
  496. })
  497. .replace(/(#[\da-fA-F]+)/g, '\033[33m$1\033[0m')
  498. .replace(/('.*?'|".*?")/g, '\033[32m$1\033[0m');
  499. }
  500. /**
  501. * Convert a CSS file to a Styl file
  502. */
  503. function compileCSSFile(file, fileOut) {
  504. fs.stat(file, function(err, stat){
  505. if (err) throw err;
  506. if (stat.isFile()) {
  507. fs.readFile(file, 'utf8', function(err, str){
  508. if (err) throw err;
  509. var styl = stylus.convertCSS(str);
  510. fs.writeFile(fileOut, styl, function(err){
  511. if (err) throw err;
  512. });
  513. });
  514. }
  515. });
  516. }
  517. /**
  518. * Compile with stdio.
  519. */
  520. function compileStdio() {
  521. process.stdin.setEncoding('utf8');
  522. process.stdin.on('data', function(chunk){ str += chunk; });
  523. process.stdin.on('end', function(){
  524. // Compile to css
  525. var style = stylus(str, options);
  526. if (includeCSS) style.set('include css', true);
  527. if (disableCache) style.set('cache', false);
  528. usePlugins(style);
  529. importFiles(style);
  530. style.render(function(err, css){
  531. if (err) throw err;
  532. if (compare) {
  533. console.log('\n\x1b[1mInput:\x1b[0m');
  534. console.log(str);
  535. console.log('\n\x1b[1mOutput:\x1b[0m');
  536. }
  537. console.log(css);
  538. if (compare) console.log();
  539. });
  540. }).resume();
  541. }
  542. /**
  543. * Compile the given files.
  544. */
  545. function compileFiles(files) {
  546. files.forEach(compileFile);
  547. }
  548. /**
  549. * Display dependencies of the compiled files.
  550. */
  551. function displayDeps() {
  552. files.forEach(function(file){
  553. // ensure file exists
  554. fs.stat(file, function(err, stat){
  555. if (err) throw err;
  556. fs.readFile(file, 'utf8', function(err, str){
  557. if (err) throw err;
  558. options.filename = file;
  559. var style = stylus(str, options);
  560. usePlugins(style);
  561. importFiles(style);
  562. console.log(style.deps().join('\n'));
  563. });
  564. });
  565. });
  566. }
  567. /**
  568. * Compile the given file.
  569. */
  570. function compileFile(file) {
  571. // ensure file exists
  572. fs.stat(file, function(err, stat){
  573. if (err) throw err;
  574. // file
  575. if (stat.isFile()) {
  576. fs.readFile(file, 'utf8', function(err, str){
  577. if (err) throw err;
  578. options.filename = file;
  579. options._imports = [];
  580. var style = stylus(str, options);
  581. if (includeCSS) style.set('include css', true);
  582. if (disableCache) style.set('cache', false);
  583. if (sourcemap) style.set('sourcemap', sourcemap);
  584. usePlugins(style);
  585. importFiles(style);
  586. style.render(function(err, css){
  587. watchImports(file, options._imports);
  588. if (err) {
  589. if (watchers) {
  590. console.error(err.stack || err.message);
  591. } else {
  592. throw err;
  593. }
  594. } else {
  595. writeFile(file, css);
  596. // write sourcemap
  597. if (sourcemap && !sourcemap.inline) {
  598. writeSourcemap(file, style.sourcemap);
  599. }
  600. }
  601. });
  602. });
  603. // directory
  604. } else if (stat.isDirectory()) {
  605. fs.readdir(file, function(err, files){
  606. if (err) throw err;
  607. files.filter(function(path){
  608. return path.match(/\.styl$/);
  609. }).map(function(path){
  610. return join(file, path);
  611. }).forEach(compileFile);
  612. });
  613. }
  614. });
  615. }
  616. /**
  617. * Write the given CSS output.
  618. */
  619. function createPath(file, sourceMap) {
  620. var out;
  621. if (files.length === 1 && isCSS(dest)) {
  622. return [dest, sourceMap ? '.map' : ''].join('');
  623. }
  624. // --out support
  625. out = [basename(file, extname(file)), sourceMap ? ext + '.map' : ext].join('');
  626. return dest
  627. ? join(dest, out)
  628. : join(dirname(file), out);
  629. }
  630. /**
  631. * Check if the given path is a CSS file.
  632. */
  633. function isCSS(file) {
  634. return file && '.css' === extname(file);
  635. }
  636. function writeFile(file, css) {
  637. // --print support
  638. if (print) return process.stdout.write(css);
  639. var path = createPath(file);
  640. mkdirSync(dirname(path), { recursive: true })
  641. fs.writeFile(path, css, function(err){
  642. if (err) throw err;
  643. console.log(' \033[90mcompiled\033[0m %s', path);
  644. // --watch support
  645. watch(file, file);
  646. });
  647. }
  648. /**
  649. * Write the given sourcemap.
  650. */
  651. function writeSourcemap(file, sourcemap) {
  652. var path = createPath(file, true);
  653. mkdirSync(dirname(path), { recursive: true })
  654. fs.writeFile(path, JSON.stringify(sourcemap), function(err){
  655. if (err) throw err;
  656. // don't output log message if --print is present
  657. if (!print) console.log(' \033[90mgenerated\033[0m %s', path);
  658. });
  659. }
  660. /**
  661. * Watch the given `file` and recompiling `rootFile` when modified.
  662. */
  663. function watch(file, rootFile) {
  664. // not watching
  665. if (!watchers) return;
  666. // already watched
  667. if (watchers[file]) {
  668. watchers[file][rootFile] = true;
  669. return;
  670. }
  671. // watch the file itself
  672. watchers[file] = {};
  673. watchers[file][rootFile] = true;
  674. if (print) {
  675. console.error('Stylus CLI Error: Watch and print cannot be used together');
  676. process.exit(1);
  677. }
  678. if(!quiet){
  679. console.log(' \033[90mwatching\033[0m %s', file);
  680. }
  681. // if is windows use fs.watch api instead
  682. // TODO: remove watchFile when fs.watch() works on osx etc
  683. if (isWindows) {
  684. fs.watch(file, compile);
  685. } else {
  686. fs.watchFile(file, { interval: 300 }, function(curr, prev) {
  687. if (curr.mtime > prev.mtime) compile();
  688. });
  689. }
  690. function compile() {
  691. for (var rootFile in watchers[file]) {
  692. compileFile(rootFile);
  693. }
  694. }
  695. }
  696. /**
  697. * Watch `imports`, re-compiling `file` when they change.
  698. */
  699. function watchImports(file, imports) {
  700. imports.forEach(function(imported){
  701. if (!imported.path) return;
  702. watch(imported.path, file);
  703. });
  704. }
  705. /**
  706. * Utilize plugins.
  707. */
  708. function usePlugins(style) {
  709. plugins.forEach(function(plugin){
  710. var path = plugin.path;
  711. var options = plugin.options;
  712. fn = require(/^\.+\//.test(path) ? resolve(path) : path);
  713. if ('function' != typeof fn) {
  714. throw new Error('plugin ' + path + ' does not export a function');
  715. }
  716. style.use(fn(options));
  717. });
  718. if (urlFunction) {
  719. style.define('url', stylus.url(urlFunction));
  720. } else if (resolveURL) {
  721. style.define('url', stylus.resolver(resolveURL));
  722. }
  723. }
  724. /**
  725. * Imports the indicated files.
  726. */
  727. function importFiles(style) {
  728. imports.forEach(function(file) {
  729. style.import(file);
  730. });
  731. }