├── .babelrc ├── CHANGELOG.md ├── .gitignore ├── .travis.yml ├── test ├── mocha.opts └── binary.js ├── .npmignore ├── .editorconfig ├── autoprefixer-cli ├── enable-es6.js ├── README.md ├── package.json ├── LICENSE ├── gulpfile.babel.js ├── .eslintrc └── binary.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "loose": "all" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0 2 | * Initial release from old `autoprefixer` CLI. 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | 4 | node_modules/ 5 | npm-debug.log 6 | 7 | build/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - iojs 5 | - "0.12" 6 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --inline-diffs 2 | --timeout 6000 3 | --require ./enable-es6 4 | --reporter spec 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | 3 | node_modules/ 4 | npm-debug.log 5 | 6 | build 7 | 8 | test 9 | .travis.yml 10 | 11 | gulpfile.babel.js 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{json,yml}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /autoprefixer-cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable no-var, prefer-arrow-callback */ 3 | 4 | require('./enable-es6'); 5 | var Binary = require('./binary'); 6 | 7 | var binary = new Binary(process); 8 | binary.run(function () { 9 | if ( binary.status ) process.exit(binary.status); 10 | }); 11 | -------------------------------------------------------------------------------- /enable-es6.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var, prefer-arrow-callback */ 2 | 3 | var path = require('path'); 4 | 5 | var escape = function (str) { 6 | return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); 7 | }; 8 | 9 | var regexp = ['binary.js', 'test', 'benchmark'].map(function (i) { 10 | var str = path.join(__dirname, i); 11 | if ( i.indexOf('.js') === -1 ) str += path.sep; 12 | return '^' + escape(str); 13 | }).join('|'); 14 | 15 | require('babel-core/register')({ 16 | only: new RegExp('(' + regexp + ')'), 17 | ignore: false 18 | }); 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Autoprefixer CLI [![Build Status][ci-img]][ci] 2 | 3 | CLI tool for [Autoprefixer]. The official way to use Autoprefixer 4 | is [postcss-cli], this tool is for legacy code. 5 | 6 | Install: 7 | 8 | ```sh 9 | npm install --global autoprefixer-cli 10 | ``` 11 | 12 | Usage: 13 | 14 | ```sh 15 | autoprefixer-cli -o main.prefixed.css main.css 16 | ``` 17 | 18 | See full documentation by: 19 | 20 | ```sh 21 | autoprefixer-cli -h 22 | ``` 23 | 24 | [Autoprefixer]: https://github.com/postcss/autoprefixer 25 | [postcss-cli]: https://github.com/code42day/postcss-cli 26 | [ci-img]: https://travis-ci.org/ai/autoprefixer-cli.svg 27 | [ci]: https://travis-ci.org/ai/autoprefixer-cli 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autoprefixer-cli", 3 | "version": "1.0.0", 4 | "description": "CLI for Autoprefixer", 5 | "keywords": [ 6 | "css", 7 | "autoprefixer", 8 | "cli" 9 | ], 10 | "author": "Andrey Sitnik ", 11 | "license": "MIT", 12 | "repository": "ai/autoprefixer-cli.git", 13 | "dependencies": { 14 | "autoprefixer": "^6.2.0", 15 | "babel-core": "5.8.23", 16 | "fs-extra": "^0.24.0", 17 | "postcss": "^5.0.4" 18 | }, 19 | "devDependencies": { 20 | "gulp-json-editor": "2.2.1", 21 | "babel-eslint": "4.1.1", 22 | "gulp-replace": "0.5.4", 23 | "gulp-eslint": "1.0.0", 24 | "gulp-mocha": "2.1.3", 25 | "gulp-babel": "5.2.1", 26 | "mocha": "2.3.0", 27 | "gulp": "3.9.0", 28 | "chai": "3.2.0" 29 | }, 30 | "bin": "./autoprefixer-cli", 31 | "scripts": { 32 | "test": "gulp" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2013 Andrey Sitnik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import path from 'path'; 3 | import fs from 'fs-extra'; 4 | 5 | gulp.task('clean', (done) => { 6 | fs.remove(path.join(__dirname, 'build'), done); 7 | }); 8 | 9 | gulp.task('build:bin', ['clean'], () => { 10 | let replace = require('gulp-replace'); 11 | return gulp.src('autoprefixer-cli') 12 | .pipe(replace(/require\('\.\/enable-es6'\);\n/, '')) 13 | .pipe(gulp.dest('build/')); 14 | }); 15 | 16 | gulp.task('build:lib', ['clean'], () => { 17 | let replace = require('gulp-replace'); 18 | let babel = require('gulp-babel'); 19 | return gulp.src(['binary.js', 'index.js']) 20 | .pipe(replace(/require\('\.\/enable-es6'\);\n/, '')) 21 | .pipe(babel({ loose: 'all' })) 22 | .pipe(gulp.dest('build/')); 23 | }); 24 | 25 | gulp.task('build:docs', ['clean'], () => { 26 | let ignore = require('fs').readFileSync('.npmignore').toString() 27 | .trim().split(/\n+/) 28 | .concat(['*.js', '.npmignore', 'package.json', 'autoprefixer-cli']) 29 | .map( i => '!' + i ); 30 | return gulp.src(['*'].concat(ignore)) 31 | .pipe(gulp.dest('build')); 32 | }); 33 | 34 | gulp.task('build:package', ['clean'], () => { 35 | let editor = require('gulp-json-editor'); 36 | return gulp.src('./package.json') 37 | .pipe(editor( (d) => { 38 | d.devDependencies['babel-core'] = d.dependencies['babel-core']; 39 | delete d.dependencies['babel-core']; 40 | return d; 41 | })) 42 | .pipe(gulp.dest('build')); 43 | }); 44 | 45 | gulp.task('build', ['build:bin', 'build:lib', 'build:docs', 'build:package']); 46 | 47 | gulp.task('lint', () => { 48 | let eslint = require('gulp-eslint'); 49 | return gulp.src(['index.js', 50 | 'binary.js', 51 | 'test/*.js', 52 | 'gulpfile.babel.js', 53 | 'autoprefixer-cli', 54 | 'enable-es6.js']) 55 | .pipe(eslint()) 56 | .pipe(eslint.format()) 57 | .pipe(eslint.failAfterError()); 58 | }); 59 | 60 | gulp.task('test', () => { 61 | require('./enable-es6'); 62 | let mocha = require('gulp-mocha'); 63 | return gulp.src('test/*.js', { read: false }) 64 | .pipe(mocha({ timeout: 6000 })); 65 | }); 66 | 67 | gulp.task('default', ['lint', 'test']); 68 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "space-before-function-paren": [2, { "named": "never" }], 5 | "no-shadow-restricted-names": [2], 6 | "computed-property-spacing": [2], 7 | "no-empty-character-class": [2], 8 | "no-irregular-whitespace": [2], 9 | "no-unexpected-multiline": [2], 10 | "no-multiple-empty-lines": [2], 11 | "space-return-throw-case": [2], 12 | "prefer-arrow-callback": [2], 13 | "no-constant-condition": [2], 14 | "no-dupe-class-members": [2], 15 | "no-extra-boolean-cast": [2], 16 | "no-inner-declarations": [2], 17 | "no-this-before-super": [2], 18 | "no-use-before-define": [2], 19 | "no-array-constructor": [2], 20 | "object-curly-spacing": [2, "always"], 21 | "no-floating-decimal": [2], 22 | "no-warning-comments": [2], 23 | "handle-callback-err": [2], 24 | "no-unneeded-ternary": [2], 25 | "operator-assignment": [2], 26 | "space-before-blocks": [2], 27 | "no-native-reassign": [2], 28 | "no-trailing-spaces": [2], 29 | "operator-linebreak": [2, "after"], 30 | "consistent-return": [2], 31 | "no-duplicate-case": [2], 32 | "no-invalid-regexp": [2], 33 | "no-negated-in-lhs": [2], 34 | "constructor-super": [2], 35 | "no-nested-ternary": [2], 36 | "no-extend-native": [2], 37 | "block-scoped-var": [2], 38 | "no-control-regex": [2], 39 | "no-sparse-arrays": [2], 40 | "no-throw-literal": [2], 41 | "no-return-assign": [2], 42 | "object-shorthand": [2], 43 | "no-const-assign": [2], 44 | "no-class-assign": [2], 45 | "no-regex-spaces": [2], 46 | "no-implied-eval": [2], 47 | "no-useless-call": [2], 48 | "no-self-compare": [2], 49 | "no-octal-escape": [2], 50 | "no-new-wrappers": [2], 51 | "linebreak-style": [2], 52 | "space-infix-ops": [2], 53 | "space-unary-ops": [2], 54 | "no-cond-assign": [2], 55 | "no-func-assign": [2], 56 | "no-unreachable": [2], 57 | "accessor-pairs": [2], 58 | "no-empty-label": [2], 59 | "no-fallthrough": [2], 60 | "no-path-concat": [2], 61 | "no-new-require": [2], 62 | "no-spaced-func": [2], 63 | "no-unused-vars": [2], 64 | "spaced-comment": [2], 65 | "block-spacing": [2], 66 | "no-delete-var": [2], 67 | "comma-spacing": [2], 68 | "no-extra-semi": [2], 69 | "no-extra-bind": [2], 70 | "arrow-spacing": [2], 71 | "prefer-spread": [2], 72 | "no-new-object": [2], 73 | "no-multi-str": [2], 74 | "semi-spacing": [2], 75 | "no-lonely-if": [2], 76 | "dot-notation": [2], 77 | "dot-location": [2, "property"], 78 | "comma-dangle": [2, "never"], 79 | "no-dupe-args": [2], 80 | "no-dupe-keys": [2], 81 | "no-ex-assign": [2], 82 | "no-obj-calls": [2], 83 | "valid-typeof": [2], 84 | "default-case": [2], 85 | "no-redeclare": [2], 86 | "no-div-regex": [2], 87 | "no-sequences": [2], 88 | "no-label-var": [2], 89 | "comma-style": [2], 90 | "brace-style": [2], 91 | "no-debugger": [2], 92 | "quote-props": [2, "as-needed"], 93 | "no-iterator": [2], 94 | "no-new-func": [2], 95 | "key-spacing": [2, { "align": "value" }], 96 | "complexity": [2], 97 | "new-parens": [2], 98 | "no-eq-null": [2], 99 | "no-bitwise": [2], 100 | "wrap-iife": [2], 101 | "no-caller": [2], 102 | "use-isnan": [2], 103 | "no-labels": [2], 104 | "no-shadow": [2], 105 | "camelcase": [2], 106 | "eol-last": [2], 107 | "no-octal": [2], 108 | "no-empty": [2], 109 | "no-alert": [2], 110 | "no-proto": [2], 111 | "no-undef": [2], 112 | "no-eval": [2], 113 | "no-with": [2], 114 | "no-void": [2], 115 | "max-len": [2, 80], 116 | "new-cap": [2], 117 | "eqeqeq": [2], 118 | "no-new": [2], 119 | "quotes": [2, "single"], 120 | "no-var": [2], 121 | "indent": [2, 4], 122 | "semi": [2, "always"], 123 | "yoda": [2, "never"] 124 | }, 125 | "env": { 126 | "mocha": true, 127 | "node": true, 128 | "es6": true 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /test/binary.js: -------------------------------------------------------------------------------- 1 | import Binary from '../binary'; 2 | 3 | import { expect } from 'chai'; 4 | import child from 'child_process'; 5 | import parse from 'postcss/lib/parse'; 6 | import path from 'path'; 7 | import fs from 'fs-extra'; 8 | 9 | class StringBuffer { 10 | constructor() { 11 | this.content = ''; 12 | } 13 | 14 | write(str) { 15 | this.content += str; 16 | } 17 | 18 | resume() { 19 | this.resumed = true; 20 | } 21 | 22 | on(event, callback) { 23 | if ( event === 'data' && this.resumed ) { 24 | callback(this.content); 25 | } else if ( event === 'end' ) { 26 | callback(); 27 | } 28 | } 29 | } 30 | 31 | let tempDir = path.join(__dirname, 'fixtures'); 32 | 33 | function temp(file) { 34 | return path.join(tempDir, file); 35 | } 36 | 37 | function write(file, content) { 38 | if ( !fs.existsSync(tempDir) ) fs.mkdirSync(tempDir); 39 | fs.outputFileSync(temp(file), content); 40 | } 41 | 42 | function read(file) { 43 | return fs.readFileSync(temp(file)).toString(); 44 | } 45 | 46 | function readMap(file) { 47 | return parse(read(file)).source.input.map.consumer(); 48 | } 49 | 50 | function exists(file) { 51 | return fs.existsSync(temp(file)); 52 | } 53 | 54 | let css = 'a { transition: all 1s }'; 55 | let prefixed = 'a { -webkit-transition: all 1s; transition: all 1s }'; 56 | 57 | let stdout, stderr, stdin; 58 | 59 | 60 | function exec(...args) { 61 | let callback = args.pop(); 62 | args = args.map( (arg) => { 63 | if ( arg.match(/\.css/) || arg.match(/\/$/) ) { 64 | return temp(arg); 65 | } else { 66 | return arg; 67 | } 68 | }); 69 | 70 | let argv = ['', ''].concat(args); 71 | let binary = new Binary({ argv, stdin, stdout, stderr }); 72 | 73 | binary.run( () => { 74 | let error; 75 | if ( binary.status === 0 && stderr.content === '' ) { 76 | error = false; 77 | } else { 78 | error = stderr.content; 79 | } 80 | callback(stdout.content, error); 81 | }); 82 | } 83 | 84 | let run = (...args) => { 85 | let callback = args.pop(); 86 | args.push( (out, err) => { 87 | expect(err).to.be.false; 88 | callback(out); 89 | }); 90 | exec(...args); 91 | }; 92 | 93 | let raise = (...args) => { 94 | let done = args.pop(); 95 | let error = args.pop(); 96 | args.push( (out, err) => { 97 | expect(out).to.be.empty; 98 | expect(err).to.match(error); 99 | done(); 100 | }); 101 | exec(...args); 102 | }; 103 | 104 | describe('Binary', () => { 105 | beforeEach( () => { 106 | stdout = new StringBuffer(); 107 | stderr = new StringBuffer(); 108 | stdin = new StringBuffer(); 109 | }); 110 | 111 | afterEach( () => { 112 | if ( fs.existsSync(tempDir) ) fs.removeSync(tempDir); 113 | }); 114 | 115 | it('shows autoprefixer version', (done) => { 116 | run('-v', (out) => { 117 | expect(out).to.match(/^autoprefixer-cli [\d\.]+\n$/); 118 | done(); 119 | }); 120 | }); 121 | 122 | it('shows help instructions', (done) => { 123 | run('-h', (out) => { 124 | expect(out).to.match(/Usage:/); 125 | done(); 126 | }); 127 | }); 128 | 129 | it('shows selected browsers and properties', (done) => { 130 | run('-i', (out) => { 131 | expect(out).to.match(/Browsers:/); 132 | done(); 133 | }); 134 | }); 135 | 136 | it('changes browsers', (done) => { 137 | run('-i', '-b', 'ie 6', (out) => { 138 | expect(out).to.match(/IE: 6/); 139 | done(); 140 | }); 141 | }); 142 | 143 | it('cleans styles', (done) => { 144 | write('a.css', prefixed); 145 | run('-c', 'a.css', (out) => { 146 | expect(out).to.be.empty; 147 | expect(read('a.css')).to.eql(css); 148 | done(); 149 | }); 150 | }); 151 | 152 | it('rewrites several files', (done) => { 153 | write('a.css', css); 154 | write('b.css', css + css); 155 | run('-b', 'chrome 25', 'a.css', 'b.css', (out) => { 156 | expect(out).to.be.empty; 157 | expect(read('a.css')).to.eql(prefixed); 158 | expect(read('b.css')).to.eql(prefixed + prefixed); 159 | done(); 160 | }); 161 | }); 162 | 163 | it('changes output file', (done) => { 164 | write('a.css', css); 165 | run('-b', 'chrome 25', 'a.css', '-o', 'b.css', (out) => { 166 | expect(out).to.be.empty; 167 | expect(read('a.css')).to.eql(css); 168 | expect(read('b.css')).to.eql(prefixed); 169 | done(); 170 | }); 171 | }); 172 | 173 | it('creates dirs for output file', (done) => { 174 | write('a.css', ''); 175 | run('a.css', '-o', 'one/two/b.css', (out) => { 176 | expect(out).to.be.empty; 177 | expect(read('one/two/b.css')).to.be.empty; 178 | done(); 179 | }); 180 | }); 181 | 182 | it('outputs to dir', (done) => { 183 | write('a.css', css); 184 | write('b.css', css + css); 185 | 186 | run('-b', 'chrome 25', 'a.css', 'b.css', '-d', 'out/', (out) => { 187 | expect(out).to.be.empty; 188 | 189 | expect(read('a.css')).to.eql(css); 190 | expect(read('b.css')).to.eql(css + css); 191 | expect(read('out/a.css')).to.eql(prefixed); 192 | expect(read('out/b.css')).to.eql(prefixed + prefixed); 193 | 194 | done(); 195 | }); 196 | }); 197 | 198 | it('outputs to stdout', (done) => { 199 | write('a.css', css); 200 | run('-b', 'chrome 25', '-o', '-', 'a.css', (out) => { 201 | expect(out).to.eql(prefixed + '\n'); 202 | expect(read('a.css')).to.eql(css); 203 | done(); 204 | }); 205 | }); 206 | 207 | it('reads from stdin', (done) => { 208 | stdin.content = css; 209 | run('-b', 'chrome 25', (out) => { 210 | expect(out).to.eql(prefixed + '\n'); 211 | done(); 212 | }); 213 | }); 214 | 215 | it('skip source map by default', (done) => { 216 | write('a.css', css); 217 | run('-o', 'b.css', 'a.css', () => { 218 | expect(exists('b.css.map')).to.be.false; 219 | done(); 220 | }); 221 | }); 222 | 223 | it('inline source map on -m argument', (done) => { 224 | write('a.css', css); 225 | run('-m', '-o', 'b.css', 'a.css', () => { 226 | expect(read('b.css')).to.match(/\n\/\*# sourceMappingURL=/); 227 | expect(exists('b.css.map')).to.be.false; 228 | 229 | let map = readMap('b.css'); 230 | expect(map.file).to.eql('b.css'); 231 | expect(map.sources).to.eql(['a.css']); 232 | 233 | done(); 234 | }); 235 | }); 236 | 237 | it('generates separated source map file', (done) => { 238 | write('a.css', css); 239 | run('--no-inline-map', '-o', 'b.css', 'a.css', () => { 240 | expect(exists('b.css.map')).to.be.true; 241 | done(); 242 | }); 243 | }); 244 | 245 | it('modify source map', (done) => { 246 | write('a.css', css); 247 | run('-m', '-o', 'b.css', 'a.css', () => { 248 | run('-o', 'c.css', 'b.css', () => { 249 | let map = readMap('c.css'); 250 | expect(map.file).to.eql('c.css'); 251 | expect(map.sources).to.eql(['a.css']); 252 | done(); 253 | }); 254 | }); 255 | }); 256 | 257 | it('forces map inline on request', (done) => { 258 | write('a.css', css); 259 | run('--no-inline-map', '-o', 'b.css', 'a.css', () => { 260 | run('-I', '-o', 'c.css', 'b.css', () => { 261 | expect(read('c.css')).to.match(/sourceMappingURL=/); 262 | expect(exists('c.css.map')).to.be.false; 263 | done(); 264 | }); 265 | }); 266 | }); 267 | 268 | it('ignore previous source map on request', (done) => { 269 | write('a.css', css); 270 | run('-m', '-o', 'b.css', 'a.css', () => { 271 | run('--no-map', '-o', 'c.css', 'b.css', () => { 272 | expect(read('c.css')).to.not.match(/sourceMappingURL=/); 273 | done(); 274 | }); 275 | }); 276 | }); 277 | 278 | it('uses cascade by default', (done) => { 279 | write('a.css', 'a {\n' + 280 | ' mask: none }'); 281 | run('-b', 'chrome 25', 'a.css', () => { 282 | expect(read('a.css')).to.eql('a {\n' + 283 | ' -webkit-mask: none;\n' + 284 | ' mask: none }'); 285 | done(); 286 | }); 287 | }); 288 | 289 | it('disables cascade by request', (done) => { 290 | write('a.css', 'a {\n' + 291 | ' mask: none }'); 292 | run('-b', 'chrome 25', '--no-cascade', 'a.css', () => { 293 | expect(read('a.css')).to.eql('a {\n' + 294 | ' -webkit-mask: none;\n' + 295 | ' mask: none }'); 296 | done(); 297 | }); 298 | }); 299 | 300 | it('removes old prefixes by default', (done) => { 301 | write('a.css', 'a {\n' + 302 | ' -webkit-transition: 1s;\n' + 303 | ' transition: 1s }'); 304 | run('-b', 'chrome 38', 'a.css', () => { 305 | expect(read('a.css')).to.eql('a {\n' + 306 | ' transition: 1s }'); 307 | done(); 308 | }); 309 | }); 310 | 311 | it('keeps old prefixes by request', (done) => { 312 | write('a.css', 'a {\n' + 313 | ' -webkit-transition: 1s;\n' + 314 | ' transition: 1s }'); 315 | run('-b', 'chrome 38', '--no-remove', 'a.css', () => { 316 | expect(read('a.css')).to.eql('a {\n' + 317 | ' -webkit-transition: 1s;\n' + 318 | ' transition: 1s }'); 319 | done(); 320 | }); 321 | }); 322 | 323 | it('changes annotation', (done) => { 324 | write('a/a.css', css); 325 | run('--annotation', '../a.map', 'a/a.css', () => { 326 | expect(read('a/a.css')).to.match(/sourceMappingURL=..\/a.map/); 327 | expect(exists('a.map')).to.be.true; 328 | done(); 329 | }); 330 | }); 331 | 332 | it('skips annotation on request', (done) => { 333 | write('a.css', css); 334 | run('-m', '--no-map-annotation', '-o', 'b.css', 'a.css', () => { 335 | expect(read('b.css')).to.not.match(/sourceMappingURL=/); 336 | expect(exists('b.css.map')).to.be.true; 337 | done(); 338 | }); 339 | }); 340 | 341 | it('includes sources content', (done) => { 342 | write('a.css', css); 343 | run('-m', 'a.css', () => { 344 | expect(readMap('a.css').sourcesContent).to.be.instanceof(Array); 345 | done(); 346 | }); 347 | }); 348 | 349 | it('misses sources content on request', (done) => { 350 | write('a.css', css); 351 | run('--no-sources-content', 'a.css', () => { 352 | expect(readMap('a.css').sourcesContent).to.not.exist; 353 | done(); 354 | }); 355 | }); 356 | 357 | it('forces sources content on request', (done) => { 358 | write('a.css', css); 359 | run('--no-sources-content', '-o', 'b.css', 'a.css', () => { 360 | run('--sources-content', '-o', 'c.css', 'b.css', () => { 361 | expect(readMap('c.css').sourcesContent).to.be.instanceof(Array); 362 | done(); 363 | }); 364 | }); 365 | }); 366 | 367 | it('raises an error when files does not exists', (done) => { 368 | raise('not.css', /doesn't exists/, done); 369 | }); 370 | 371 | it('raises on several inputs and one output file', (done) => { 372 | write('a.css', css); 373 | write('b.css', css); 374 | raise('a.css', 'b.css', '-o', 'c.css', 375 | /For several files you can specify only output dir/, done); 376 | }); 377 | 378 | it('raises on STDIN and output dir', (done) => { 379 | raise('-d', 'out/', 380 | /For STDIN input you need to specify output file/, done); 381 | }); 382 | 383 | it('raises file in output dir', (done) => { 384 | write('b.css', ''); 385 | raise('a.css', '-d', 'b.css', /is a file, not directory/, done); 386 | }); 387 | 388 | it('raises an error when unknown arguments are given', (done) => { 389 | raise('-x', /autoprefixer-cli: Unknown argument -x/, done); 390 | }); 391 | 392 | it('prints errors', (done) => { 393 | raise('-b', 'ie', /autoprefixer-cli: Unknown browser query `ie`/, done); 394 | }); 395 | 396 | it('prints parsing errors', (done) => { 397 | stdin.content = 'a {'; 398 | raise(/^autoprefixer-cli::1:1: [\s\S]+a \{/, done); 399 | }); 400 | 401 | it('prints warnings', (done) => { 402 | write('a.css', 'a{background:linear-gradient(top,white,black)}'); 403 | raise('a.css', /1:3: Gradient/, done); 404 | }); 405 | 406 | }); 407 | 408 | describe('autoprefixer-cli', () => { 409 | 410 | it('is an executable', (done) => { 411 | let binary = path.join(__dirname, '../autoprefixer-cli'); 412 | child.execFile(binary, ['-v'], { }, (error, out) => { 413 | expect(error).to.not.exist; 414 | expect(out).to.match(/^autoprefixer-cli [\d\.]+\n$/); 415 | done(); 416 | }); 417 | }); 418 | 419 | }); 420 | -------------------------------------------------------------------------------- /binary.js: -------------------------------------------------------------------------------- 1 | import autoprefixer from 'autoprefixer'; 2 | import postcss from 'postcss'; 3 | import path from 'path'; 4 | import fs from 'fs-extra'; 5 | 6 | export default class Binary { 7 | constructor(process) { 8 | this.arguments = process.argv.slice(2); 9 | this.stdin = process.stdin; 10 | this.stderr = process.stderr; 11 | this.stdout = process.stdout; 12 | 13 | this.status = 0; 14 | this.command = 'compile'; 15 | this.inputFiles = []; 16 | 17 | this.processOptions = { }; 18 | this.pluginOptions = { }; 19 | this.parseArguments(); 20 | } 21 | 22 | // Quick help message 23 | help() { 24 | return ( 25 | `Usage: autoprefixer [OPTION...] FILES 26 | 27 | Parse CSS files and add prefixed properties and values. 28 | 29 | Options: 30 | -b, --browsers BROWSERS add prefixes for selected browsers 31 | -c, --clean remove all known prefixes 32 | -o, --output FILE set output file 33 | -d, --dir DIR set output dir 34 | -m, --map generate source map 35 | --no-map skip source map even if previous map exists 36 | --no-inline-map do not inline maps to data:uri 37 | --inline-map force inline map 38 | --annotation PATH change map location relative from CSS file 39 | --no-map-annotation do not add source map annotation comment in CSS 40 | --no-sources-content remove origin CSS from maps 41 | --sources-content force include origin CSS into map 42 | --no-cascade do not create nice visual cascade of prefixes 43 | --no-remove do not remove outdated prefixes 44 | -i, --info show selected browsers and properties 45 | -h, --help show help text 46 | -v, --version print program version`); 47 | } 48 | 49 | // Options description 50 | desc() { 51 | return ( 52 | `Files: 53 | If you didn't set input files, autoprefixer will read from stdin stream. 54 | 55 | By default, prefixed CSS will rewrite original files. 56 | 57 | You can specify output file or directory by '-o' argument. 58 | For several input files you can specify only output directory by '-d'. 59 | 60 | Output CSS will be written to stdout stream on '-o -' argument or stdin input. 61 | 62 | Source maps: 63 | On '-m' argument Autoprefixer will generate source map for changes near 64 | output CSS (for out/main.css it generates out/main.css.map). 65 | 66 | If previous source map will be near input files (for example, in/main.css 67 | and in/main.css.map) Autoprefixer will apply previous map to output 68 | source map. 69 | 70 | Browsers: 71 | Separate browsers by comma. For example, '-b "> 1%, opera 12"'. 72 | You can set browsers by global usage statictics: '-b \"> 1%\"'. 73 | or last version: '-b "last 2 versions"'.`); 74 | } 75 | 76 | // Print to stdout 77 | print(str) { 78 | str = str.replace(/\n$/, ''); 79 | this.stdout.write(str + '\n'); 80 | } 81 | 82 | // Print to stdout 83 | error(str) { 84 | this.status = 1; 85 | this.stderr.write(str + '\n'); 86 | } 87 | 88 | // Get current version 89 | version() { 90 | return require('autoprefixer/package.json').version; 91 | } 92 | 93 | // Parse arguments 94 | parseArguments() { 95 | let args = this.arguments.slice(); 96 | while ( args.length > 0 ) { 97 | let arg = args.shift(); 98 | 99 | if ( arg === '-h' || arg === '--help' ) { 100 | this.command = 'showHelp'; 101 | 102 | } else if ( arg === '-v' || arg === '--version' ) { 103 | this.command = 'showVersion'; 104 | 105 | } else if ( arg === '-i' || arg === '--info' ) { 106 | this.command = 'info'; 107 | 108 | } else if ( arg === '-m' || arg === '--map' ) { 109 | this.processOptions.map = { }; 110 | 111 | } else if ( arg === '--no-map' ) { 112 | this.processOptions.map = false; 113 | 114 | } else if ( arg === '-I' || arg === '--inline-map' ) { 115 | if ( typeof this.processOptions.map === 'undefined' ) { 116 | this.processOptions.map = { }; 117 | } 118 | this.processOptions.map.inline = true; 119 | 120 | } else if ( arg === '--no-inline-map' ) { 121 | if ( typeof this.processOptions.map === 'undefined' ) { 122 | this.processOptions.map = { }; 123 | } 124 | this.processOptions.map.inline = false; 125 | 126 | } else if ( arg === '--annotation' ) { 127 | if ( typeof this.processOptions.map === 'undefined' ) { 128 | this.processOptions.map = { }; 129 | } 130 | this.processOptions.map.annotation = args.shift(); 131 | 132 | } else if ( arg === '--no-map-annotation' ) { 133 | if ( typeof this.processOptions.map === 'undefined' ) { 134 | this.processOptions.map = { }; 135 | } 136 | this.processOptions.map.annotation = false; 137 | 138 | } else if ( arg === '--sources-content' ) { 139 | if ( typeof this.processOptions.map === 'undefined' ) { 140 | this.processOptions.map = { }; 141 | } 142 | this.processOptions.map.sourcesContent = true; 143 | 144 | } else if ( arg === '--no-sources-content' ) { 145 | if ( typeof this.processOptions.map === 'undefined' ) { 146 | this.processOptions.map = { }; 147 | } 148 | this.processOptions.map.sourcesContent = false; 149 | 150 | } else if ( arg === '--no-cascade' ) { 151 | this.pluginOptions.cascade = false; 152 | 153 | } else if ( arg === '--no-remove' ) { 154 | this.pluginOptions.remove = false; 155 | 156 | } else if ( arg === '-b' || arg === '--browsers' ) { 157 | this.pluginOptions.browsers = args.shift().split(',') 158 | .map( (i) => i.trim() ); 159 | 160 | } else if ( arg === '-c' || arg === '--clean' ) { 161 | this.pluginOptions.browsers = []; 162 | 163 | } else if ( arg === '-o' || arg === '--output' ) { 164 | this.outputFile = args.shift(); 165 | 166 | } else if ( arg === '-d' || arg === '--dir' ) { 167 | this.outputDir = args.shift(); 168 | 169 | } else if ( arg.match(/^-\w$/) || arg.match(/^--\w[\w-]+$/) ) { 170 | this.command = undefined; 171 | 172 | this.error('autoprefixer-cli: Unknown argument ' + arg); 173 | this.error(''); 174 | this.error(this.help()); 175 | 176 | } else { 177 | this.inputFiles.push(arg); 178 | } 179 | } 180 | } 181 | 182 | // Print help 183 | showHelp(done) { 184 | this.print(this.help()); 185 | this.print(''); 186 | this.print(this.desc()); 187 | done(); 188 | } 189 | 190 | // Print version 191 | showVersion(done) { 192 | this.print('autoprefixer-cli ' + this.version()); 193 | done(); 194 | } 195 | 196 | // Print inspect 197 | info(done) { 198 | this.print(autoprefixer(this.pluginOptions).info()); 199 | done(); 200 | } 201 | 202 | // Mark that there is another async work 203 | startWork() { 204 | this.waiting += 1; 205 | } 206 | 207 | // Execute done callback if there is no works 208 | endWork() { 209 | this.waiting -= 1; 210 | if ( this.waiting <= 0 ) this.doneCallback(); 211 | } 212 | 213 | // Write error to stderr and finish work 214 | workError(str) { 215 | this.error(str); 216 | this.endWork(); 217 | } 218 | 219 | // Lazy loading for Autoprefixer instance 220 | compiler() { 221 | if ( !this.compilerCache ) { 222 | this.compilerCache = postcss([ autoprefixer(this.pluginOptions) ]); 223 | } 224 | return this.compilerCache; 225 | } 226 | 227 | // Compile loaded CSS 228 | compileCSS(css, output, input) { 229 | let opts = { }; 230 | for ( let name in this.processOptions ) { 231 | opts[name] = this.processOptions[name]; 232 | } 233 | if ( input ) opts.from = input; 234 | if ( output !== '-' ) opts.to = output; 235 | 236 | this.compiler().process(css, opts) 237 | .catch( (err) => { 238 | if ( err.name === 'BrowserslistError' || err.autoprefixer ) { 239 | this.error('autoprefixer-cli: ' + err.message); 240 | } else if ( err.name === 'CssSyntaxError' ) { 241 | let text = err.message + err.showSourceCode(); 242 | this.error('autoprefixer-cli:' + text); 243 | } else { 244 | this.error('autoprefixer-cli: Internal error\n'); 245 | if ( err.stack ) { 246 | this.error(err.stack); 247 | } else { 248 | this.error(err.message); 249 | } 250 | } 251 | this.endWork(); 252 | }) 253 | .then( (result) => { 254 | result.warnings().forEach( (warn) => { 255 | this.stderr.write(warn.toString() + '\n'); 256 | }); 257 | 258 | if ( output === '-' ) { 259 | this.print(result.css); 260 | this.endWork(); 261 | } else { 262 | fs.outputFile(output, result.css, (err1) => { 263 | if ( err1 ) this.error('autoprefixer-cli: ' + err1); 264 | 265 | if ( result.map ) { 266 | let map; 267 | if ( opts.map && opts.map.annotation ) { 268 | map = path.resolve(path.dirname(output), 269 | opts.map.annotation); 270 | } else { 271 | map = output + '.map'; 272 | } 273 | fs.writeFile(map, result.map, (err2) => { 274 | if ( err2 ) { 275 | this.error('autoprefixer-cli: ' + err2); 276 | } 277 | this.endWork(); 278 | }); 279 | } else { 280 | this.endWork(); 281 | } 282 | }); 283 | } 284 | }); 285 | } 286 | 287 | // Return input and output files array 288 | files() { 289 | if ( this.inputFiles.length === 0 && !this.outputFile ) { 290 | this.outputFile = '-'; 291 | } 292 | 293 | let file; 294 | let list = []; 295 | if ( this.outputDir ) { 296 | if ( this.inputFiles.length === 0 ) { 297 | this.error('autoprefixer-cli: For STDIN input you need ' + 298 | 'to specify output file (by -o FILE),\n ' + 299 | 'not output dir'); 300 | return false; 301 | } 302 | 303 | let dir = this.outputDir; 304 | if ( fs.existsSync(dir) && !fs.statSync(dir).isDirectory() ) { 305 | this.error('autoprefixer-cli: Path ' + dir + 306 | ' is a file, not directory'); 307 | return false; 308 | } 309 | 310 | let output; 311 | for ( file of this.inputFiles ) { 312 | output = path.join(this.outputDir, path.basename(file)); 313 | list.push([file, output]); 314 | } 315 | 316 | } else if ( this.outputFile ) { 317 | if ( this.inputFiles.length > 1 ) { 318 | this.error('autoprefixer-cli: For several files you can ' + 319 | 'specify only output dir (by -d DIR`),\n' + 320 | 'not one output file'); 321 | return false; 322 | } 323 | 324 | for ( file of this.inputFiles ) { 325 | list.push([file, this.outputFile]); 326 | } 327 | 328 | } else { 329 | for ( file of this.inputFiles ) { 330 | list.push([file, file]); 331 | } 332 | } 333 | 334 | return list; 335 | } 336 | 337 | // Compile selected files 338 | compile(done) { 339 | this.waiting = 0; 340 | this.doneCallback = done; 341 | 342 | let files = this.files(); 343 | if ( files === false ) return done(); 344 | 345 | if ( files.length === 0 ) { 346 | this.startWork(); 347 | 348 | let css = ''; 349 | this.stdin.resume(); 350 | this.stdin.on('data', (chunk) => css += chunk); 351 | this.stdin.on('end', () => { 352 | this.compileCSS(css, this.outputFile); 353 | }); 354 | } else { 355 | let i, input, output; 356 | for ( i = 0; i < files.length; i++ ) { 357 | this.startWork(); 358 | } 359 | for ( i = 0; i < files.length; i++ ) { 360 | [input, output] = files[i]; 361 | 362 | if ( !fs.existsSync(input) ) { 363 | this.workError('autoprefixer-cli: File ' + input + ' ' + 364 | 'doesn\'t exists'); 365 | continue; 366 | } 367 | 368 | ((input2, output2) => { 369 | fs.readFile(input2, (error, css) => { 370 | if ( error ) { 371 | this.workError('autoprefixer-cli: ' + 372 | error.message); 373 | } else { 374 | this.compileCSS(css, output2, input2); 375 | } 376 | }); 377 | })(input, output); 378 | } 379 | } 380 | } 381 | 382 | // Execute command selected by arguments 383 | run(done) { 384 | if ( this.command ) { 385 | this[this.command](done); 386 | } else { 387 | done(); 388 | } 389 | } 390 | } 391 | --------------------------------------------------------------------------------