├── .eslintrc ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── flake.lock ├── flake.nix ├── index.js ├── package.json └── test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "extends": [ 6 | "eslint:recommended" 7 | ], 8 | "globals": { 9 | "Promise": true 10 | }, 11 | "rules": { 12 | "semi": [ 13 | 2, 14 | "never" 15 | ], 16 | "no-cond-assign": [ 17 | 2, 18 | "except-parens" 19 | ], 20 | "no-unused-expressions": 2, 21 | "indent": [ 22 | 2, 23 | 2, 24 | { 25 | "SwitchCase": 1 26 | } 27 | ], 28 | "comma-style": 2, 29 | "max-len": [ 30 | 2, 31 | { 32 | "code": 100, 33 | "ignoreComments": true 34 | } 35 | ], 36 | "new-cap": 2, 37 | "strict": 0, 38 | "no-trailing-spaces": 2, 39 | "no-undef": 2, 40 | "no-unused-vars": 2, 41 | "quotes": [ 42 | 2, 43 | "single" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | npm-test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: ['18', '20', '21'] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | 24 | - run: | 25 | npm install 26 | npm test 27 | 28 | - uses: coverallsapp/github-action@master 29 | with: 30 | github-token: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | .nyc_output/ 5 | coverage/ 6 | package-lock.json 7 | yarn.lock 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pid 3 | *.seed 4 | .editorconfig 5 | .eslintrc* 6 | .eslintignore 7 | .gitignore 8 | .grunt 9 | .lock-wscript 10 | .node_repl_history 11 | .stylelintrc* 12 | .travis.yml 13 | .vscode 14 | .nyc_output 15 | appveyor.yml 16 | coverage 17 | gulpfile.js 18 | lib-cov 19 | logs 20 | node_modules 21 | npm-debug.log* 22 | pids 23 | test 24 | test.js 25 | yarn.lock 26 | flake.lock 27 | flake.nix 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Andrey Kuzmin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gulp-postcss 2 | 3 | ![Build Status](https://github.com/postcss/gulp-postcss/actions/workflows/test.yml/badge.svg?branch=main) 4 | [![Coverage Status](https://img.shields.io/coveralls/postcss/gulp-postcss.svg)](https://coveralls.io/r/postcss/gulp-postcss) 5 | 6 | [PostCSS](https://github.com/postcss/postcss) gulp plugin to pipe CSS through 7 | several plugins, but parse CSS only once. 8 | 9 | ## Install 10 | 11 | $ npm install --save-dev postcss gulp-postcss 12 | 13 | Install required [postcss plugins](https://www.npmjs.com/browse/keyword/postcss-plugin) separately. E.g. for autoprefixer, you need to install [autoprefixer](https://github.com/postcss/autoprefixer) package. 14 | 15 | ## Basic usage 16 | 17 | The configuration is loaded automatically from `postcss.config.js` 18 | as [described here](https://www.npmjs.com/package/postcss-load-config), 19 | so you don't have to specify any options. 20 | 21 | ```js 22 | var postcss = require('gulp-postcss'); 23 | var gulp = require('gulp'); 24 | 25 | gulp.task('css', function () { 26 | return gulp.src('./src/*.css') 27 | .pipe(postcss()) 28 | .pipe(gulp.dest('./dest')); 29 | }); 30 | ``` 31 | 32 | ## Passing plugins directly 33 | 34 | ```js 35 | var postcss = require('gulp-postcss'); 36 | var gulp = require('gulp'); 37 | var autoprefixer = require('autoprefixer'); 38 | var cssnano = require('cssnano'); 39 | 40 | gulp.task('css', function () { 41 | var plugins = [ 42 | autoprefixer({browsers: ['last 1 version']}), 43 | cssnano() 44 | ]; 45 | return gulp.src('./src/*.css') 46 | .pipe(postcss(plugins)) 47 | .pipe(gulp.dest('./dest')); 48 | }); 49 | ``` 50 | 51 | ## Using with .pcss extension 52 | 53 | For using gulp-postcss to have input files in .pcss format and get .css output need additional library like gulp-rename. 54 | 55 | ```js 56 | var postcss = require('gulp-postcss'); 57 | var gulp = require('gulp'); 58 | const rename = require('gulp-rename'); 59 | 60 | gulp.task('css', function () { 61 | return gulp.src('./src/*.pcss') 62 | .pipe(postcss()) 63 | .pipe(rename({ 64 | extname: '.css' 65 | })) 66 | .pipe(gulp.dest('./dest')); 67 | }); 68 | ``` 69 | 70 | This is done for more explicit transformation. According to [gulp plugin guidelines](https://github.com/gulpjs/gulp/blob/master/docs/writing-a-plugin/guidelines.md#guidelines) 71 | 72 | > Your plugin should only do one thing, and do it well. 73 | 74 | 75 | ## Passing additional options to PostCSS 76 | 77 | The second optional argument to gulp-postcss is passed to PostCSS. 78 | 79 | This, for instance, may be used to enable custom parser: 80 | 81 | ```js 82 | var gulp = require('gulp'); 83 | var postcss = require('gulp-postcss'); 84 | var nested = require('postcss-nested'); 85 | var sugarss = require('sugarss'); 86 | 87 | gulp.task('default', function () { 88 | var plugins = [nested]; 89 | return gulp.src('in.sss') 90 | .pipe(postcss(plugins, { parser: sugarss })) 91 | .pipe(gulp.dest('out')); 92 | }); 93 | ``` 94 | 95 | If you are using a `postcss.config.js` file, you can pass PostCSS options as the first argument to gulp-postcss. 96 | 97 | This, for instance, will let PostCSS know what the final file destination path is, since it will be unaware of the path given to `gulp.dest()`: 98 | 99 | ```js 100 | var gulp = require('gulp'); 101 | var postcss = require('gulp-postcss'); 102 | 103 | gulp.task('default', function () { 104 | return gulp.src('in.scss') 105 | .pipe(postcss({ to: 'out/in.css' })) 106 | .pipe(gulp.dest('out')); 107 | }); 108 | ``` 109 | 110 | ## Using a custom processor 111 | 112 | ```js 113 | var postcss = require('gulp-postcss'); 114 | var cssnext = require('postcss-cssnext'); 115 | var opacity = function (css, opts) { 116 | css.walkDecls(function(decl) { 117 | if (decl.prop === 'opacity') { 118 | decl.parent.insertAfter(decl, { 119 | prop: '-ms-filter', 120 | value: '"progid:DXImageTransform.Microsoft.Alpha(Opacity=' + (parseFloat(decl.value) * 100) + ')"' 121 | }); 122 | } 123 | }); 124 | }; 125 | 126 | gulp.task('css', function () { 127 | var plugins = [ 128 | cssnext({browsers: ['last 1 version']}), 129 | opacity 130 | ]; 131 | return gulp.src('./src/*.css') 132 | .pipe(postcss(plugins)) 133 | .pipe(gulp.dest('./dest')); 134 | }); 135 | ``` 136 | 137 | ## Source map support 138 | 139 | Source map is disabled by default, to extract map use together 140 | with [gulp-sourcemaps](https://github.com/floridoo/gulp-sourcemaps). 141 | 142 | ```js 143 | return gulp.src('./src/*.css') 144 | .pipe(sourcemaps.init()) 145 | .pipe(postcss(plugins)) 146 | .pipe(sourcemaps.write('.')) 147 | .pipe(gulp.dest('./dest')); 148 | ``` 149 | 150 | ## Advanced usage 151 | 152 | If you want to configure postcss on per-file-basis, you can pass a callback 153 | that receives [vinyl file object](https://github.com/gulpjs/vinyl) and returns 154 | `{ plugins: plugins, options: options }`. For example, when you need to 155 | parse different extensions differntly: 156 | 157 | ```js 158 | var gulp = require('gulp'); 159 | var postcss = require('gulp-postcss'); 160 | 161 | gulp.task('css', function () { 162 | function callback(file) { 163 | return { 164 | plugins: [ 165 | require('postcss-import')({ root: file.dirname }), 166 | require('postcss-modules') 167 | ], 168 | options: { 169 | parser: file.extname === '.sss' ? require('sugarss') : false 170 | } 171 | } 172 | } 173 | return gulp.src('./src/*.css') 174 | .pipe(postcss(callback)) 175 | .pipe(gulp.dest('./dest')); 176 | }); 177 | ``` 178 | 179 | The same result may be achieved with 180 | [`postcss-load-config`](https://www.npmjs.com/package/postcss-load-config), 181 | because it receives `ctx` with the context options and the vinyl file. 182 | 183 | ```js 184 | var gulp = require('gulp'); 185 | var postcss = require('gulp-postcss'); 186 | 187 | gulp.task('css', function () { 188 | var contextOptions = { modules: true }; 189 | return gulp.src('./src/*.css') 190 | .pipe(postcss(contextOptions)) 191 | .pipe(gulp.dest('./dest')); 192 | }); 193 | ``` 194 | 195 | ```js 196 | // postcss.config.js or .postcssrc.js 197 | module.exports = function (ctx) { 198 | var file = ctx.file; 199 | var options = ctx; 200 | return { 201 | parser: file.extname === '.sss' ? : 'sugarss' : false, 202 | plugins: { 203 | 'postcss-import': { root: file.dirname } 204 | 'postcss-modules': options.modules ? {} : false 205 | } 206 | } 207 | }; 208 | ``` 209 | 210 | ## Changelog 211 | 212 | * 10.0.0 213 | * Released with the same changes as 9.1.0 214 | 215 | * 9.1.0 **deprecated, because it breaks semver by dropping support for node <18** 216 | * Bump postcss-load-config to ^5.0.0 217 | * Ensure options are passed to plugins when using postcss.config.js #170 218 | * Update deps 219 | * Drop support for node <18 220 | * Add flake.nix for local dev with `nix develop` 221 | 222 | * 9.0.1 223 | * Bump postcss-load-config to ^3.0.0 224 | 225 | * 9.0.0 226 | * Bump PostCSS to 8.0 227 | * Drop Node 6 support 228 | * PostCSS is now a peer dependency 229 | 230 | * 8.0.0 231 | * Bump PostCSS to 7.0 232 | * Drop Node 4 support 233 | 234 | * 7.0.1 235 | * Drop dependency on gulp-util 236 | 237 | * 7.0.0 238 | * Bump PostCSS to 6.0 239 | * Smaller module size 240 | * Use eslint instead of jshint 241 | 242 | * 6.4.0 243 | * Add more details to `PluginError` object 244 | 245 | * 6.3.0 246 | * Integrated with postcss-load-config 247 | * Added a callback to configure postcss on per-file-basis 248 | * Dropped node 0.10 support 249 | 250 | * 6.2.0 251 | * Fix syntax error message for PostCSS 5.2 compatibility 252 | 253 | * 6.1.1 254 | * Fixed the error output 255 | 256 | * 6.1.0 257 | * Support for `null` files 258 | * Updated dependencies 259 | 260 | * 6.0.1 261 | * Added an example and a test to pass options to PostCSS (e.g. `syntax` option) 262 | * Updated vinyl-sourcemaps-apply to 0.2.0 263 | 264 | * 6.0.0 265 | * Updated PostCSS to version 5.0.0 266 | 267 | * 5.1.10 268 | * Use autoprefixer in README 269 | 270 | * 5.1.9 271 | * Prevent unhandled exception of the following pipes from being suppressed by Promise 272 | 273 | * 5.1.8 274 | * Prevent stream’s unhandled exception from being suppressed by Promise 275 | 276 | * 5.1.7 277 | * Updated direct dependencies 278 | 279 | * 5.1.6 280 | * Updated `CssSyntaxError` check 281 | 282 | * 5.1.4 283 | * Simplified error handling 284 | * Simplified postcss execution with object plugins 285 | 286 | * 5.1.3 Updated travis banner 287 | 288 | * 5.1.2 Transferred repo into postcss org on github 289 | 290 | * 5.1.1 291 | * Allow override of `to` option 292 | 293 | * 5.1.0 PostCSS Runner Guidelines 294 | * Set `from` and `to` processing options 295 | * Don't output js stack trace for `CssSyntaxError` 296 | * Display `result.warnings()` content 297 | 298 | * 5.0.1 299 | * Fix to support object plugins 300 | 301 | * 5.0.0 302 | * Use async API 303 | 304 | * 4.0.3 305 | * Fixed bug with relative source map 306 | 307 | * 4.0.2 308 | * Made PostCSS a simple dependency, because peer dependency is deprecated 309 | 310 | * 4.0.1 311 | * Made PostCSS 4.x a peer dependency 312 | 313 | * 4.0.0 314 | * Updated PostCSS to 4.0 315 | 316 | * 3.0.0 317 | * Updated PostCSS to 3.0 and fixed tests 318 | 319 | * 2.0.1 320 | * Added Changelog 321 | * Added example for a custom processor in README 322 | 323 | * 2.0.0 324 | * Disable source map by default 325 | * Test source map 326 | * Added Travis support 327 | * Use autoprefixer-core in README 328 | 329 | * 1.0.2 330 | * Improved README 331 | 332 | * 1.0.1 333 | * Don't add source map comment if used with gulp-sourcemaps 334 | 335 | * 1.0.0 336 | * Initial release 337 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1701680307, 9 | "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1705084402, 24 | "narHash": "sha256-i+ipI7VgXV+nLi5ZwZ0xCamvD6Iu59vDHe6S2YOoW+Q=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "f65e5ea4794033885e1dafff7e8632e4f8cd1909", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "repo": "nixpkgs", 33 | "type": "github" 34 | } 35 | }, 36 | "root": { 37 | "inputs": { 38 | "flake-utils": "flake-utils", 39 | "nixpkgs": "nixpkgs" 40 | } 41 | }, 42 | "systems": { 43 | "locked": { 44 | "lastModified": 1681028828, 45 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 46 | "owner": "nix-systems", 47 | "repo": "default", 48 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 49 | "type": "github" 50 | }, 51 | "original": { 52 | "owner": "nix-systems", 53 | "repo": "default", 54 | "type": "github" 55 | } 56 | } 57 | }, 58 | "root": "root", 59 | "version": 7 60 | } 61 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | }; 6 | 7 | outputs = { self, nixpkgs, flake-utils }: 8 | flake-utils.lib.eachDefaultSystem (system: 9 | let 10 | pkgs = nixpkgs.legacyPackages.${system}; 11 | in 12 | { 13 | devShell = pkgs.mkShell { 14 | buildInputs = [ pkgs.nodejs_21 ]; 15 | }; 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Stream = require('stream') 2 | var postcss = require('postcss') 3 | var applySourceMap = require('vinyl-sourcemaps-apply') 4 | var fancyLog = require('fancy-log') 5 | var PluginError = require('plugin-error') 6 | var path = require('path') 7 | 8 | 9 | module.exports = withConfigLoader(function (loadConfig) { 10 | 11 | var stream = new Stream.Transform({ objectMode: true }) 12 | 13 | stream._transform = function (file, encoding, cb) { 14 | 15 | if (file.isNull()) { 16 | return cb(null, file) 17 | } 18 | 19 | if (file.isStream()) { 20 | return handleError('Streams are not supported!') 21 | } 22 | 23 | // Protect `from` and `map` if using gulp-sourcemaps 24 | var isProtected = file.sourceMap 25 | ? { from: true, map: true } 26 | : {} 27 | 28 | var options = { 29 | from: file.path, 30 | to: file.path, 31 | // Generate a separate source map for gulp-sourcemaps 32 | map: file.sourceMap ? { annotation: false } : false 33 | } 34 | 35 | loadConfig(file) 36 | .then(function (config) { 37 | var configOpts = config.options || {} 38 | // Extend the default options if not protected 39 | for (var opt in configOpts) { 40 | if (configOpts.hasOwnProperty(opt) && !isProtected[opt]) { 41 | options[opt] = configOpts[opt] 42 | } else { 43 | fancyLog.info( 44 | 'gulp-postcss:', 45 | file.relative + '\nCannot override ' + opt + 46 | ' option, because it is required by gulp-sourcemaps' 47 | ) 48 | } 49 | } 50 | return postcss(config.plugins || []) 51 | .process(file.contents, options) 52 | }) 53 | .then(handleResult, handleError) 54 | 55 | function handleResult (result) { 56 | var map 57 | var warnings = result.warnings().join('\n') 58 | 59 | file.contents = Buffer.from(result.css) 60 | 61 | // Apply source map to the chain 62 | if (file.sourceMap) { 63 | map = result.map.toJSON() 64 | map.file = file.relative 65 | map.sources = [].map.call(map.sources, function (source) { 66 | return path.join(path.dirname(file.relative), source) 67 | }) 68 | applySourceMap(file, map) 69 | } 70 | 71 | if (warnings) { 72 | fancyLog.info('gulp-postcss:', file.relative + '\n' + warnings) 73 | } 74 | 75 | setImmediate(function () { 76 | cb(null, file) 77 | }) 78 | } 79 | 80 | function handleError (error) { 81 | var errorOptions = { fileName: file.path, showStack: true } 82 | if (error.name === 'CssSyntaxError') { 83 | errorOptions.error = error 84 | errorOptions.fileName = error.file || file.path 85 | errorOptions.lineNumber = error.line 86 | errorOptions.showProperties = false 87 | errorOptions.showStack = false 88 | error = error.message + '\n\n' + error.showSourceCode() + '\n' 89 | } 90 | // Prevent stream’s unhandled exception from 91 | // being suppressed by Promise 92 | setImmediate(function () { 93 | cb(new PluginError('gulp-postcss', error, errorOptions)) 94 | }) 95 | } 96 | 97 | } 98 | 99 | return stream 100 | }) 101 | 102 | 103 | function withConfigLoader(cb) { 104 | return function (plugins, options) { 105 | if (Array.isArray(plugins)) { 106 | return cb(function () { 107 | return Promise.resolve({ 108 | plugins: plugins, 109 | options: options 110 | }) 111 | }) 112 | } else if (typeof plugins === 'function') { 113 | return cb(function (file) { 114 | return Promise.resolve(plugins(file)) 115 | }) 116 | } else { 117 | var postcssLoadConfig = require('postcss-load-config') 118 | var contextOptions = plugins || {} 119 | return cb(function(file) { 120 | var configPath 121 | if (contextOptions.config) { 122 | if (path.isAbsolute(contextOptions.config)) { 123 | configPath = contextOptions.config 124 | } else { 125 | configPath = path.join(file.base, contextOptions.config) 126 | } 127 | } else { 128 | configPath = file.dirname 129 | } 130 | // @TODO: The options property is deprecated and should be removed in 10.0.0. 131 | contextOptions.options = Object.assign({}, contextOptions) 132 | contextOptions.file = file 133 | return postcssLoadConfig( 134 | contextOptions, 135 | configPath 136 | ) 137 | }) 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-postcss", 3 | "nyc": { 4 | "lines": 100, 5 | "statements": 100, 6 | "functions": 100, 7 | "branches": 100, 8 | "reporter": [ 9 | "lcov", 10 | "text" 11 | ], 12 | "cache": true, 13 | "all": true, 14 | "check-coverage": true 15 | }, 16 | "version": "10.0.0", 17 | "description": "PostCSS gulp plugin", 18 | "main": "index.js", 19 | "engines": { 20 | "node": ">=18" 21 | }, 22 | "scripts": { 23 | "pretest": "eslint *.js", 24 | "test": "nyc mocha test.js" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/postcss/gulp-postcss.git" 29 | }, 30 | "keywords": [ 31 | "gulpplugin", 32 | "postcss", 33 | "postcss-runner", 34 | "css" 35 | ], 36 | "author": "Andrey Kuzmin ", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/postcss/gulp-postcss/issues" 40 | }, 41 | "homepage": "https://github.com/postcss/gulp-postcss", 42 | "dependencies": { 43 | "fancy-log": "^2.0.0", 44 | "plugin-error": "^2.0.1", 45 | "postcss-load-config": "^5.0.0", 46 | "vinyl-sourcemaps-apply": "^0.2.1" 47 | }, 48 | "devDependencies": { 49 | "eslint": "^5.16.0", 50 | "gulp-sourcemaps": "^2.6.5", 51 | "mocha": "^10.2.0", 52 | "nyc": "^15.1.0", 53 | "postcss": "^8.0.0", 54 | "proxyquire": "^2.1.0", 55 | "sinon": "^6.3.5", 56 | "vinyl": "^2.2.0" 57 | }, 58 | "peerDependencies": { 59 | "postcss": "^8.0.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | /* eslint max-len: ["off"] */ 3 | 4 | var assert = require('assert') 5 | var Vinyl = require('vinyl') 6 | var fancyLog = require('fancy-log') 7 | var PluginError = require('plugin-error') 8 | var sourceMaps = require('gulp-sourcemaps') 9 | var postcss = require('./index') 10 | var proxyquire = require('proxyquire') 11 | var sinon = require('sinon') 12 | var path = require('path') 13 | 14 | it('should pass file when it isNull()', function (cb) { 15 | var stream = postcss([ doubler ]) 16 | var emptyFile = { 17 | isNull: function () { return true } 18 | } 19 | 20 | stream.once('data', function (data) { 21 | assert.equal(data, emptyFile) 22 | cb() 23 | }) 24 | 25 | stream.write(emptyFile) 26 | 27 | stream.end() 28 | }) 29 | 30 | it('should transform css with multiple processors', function (cb) { 31 | 32 | var stream = postcss( 33 | [ asyncDoubler, objectDoubler() ] 34 | ) 35 | 36 | stream.on('data', function (file) { 37 | var result = file.contents.toString('utf8') 38 | var target = 'a { color: black; color: black; color: black; color: black }' 39 | assert.equal( result, target ) 40 | cb() 41 | }) 42 | 43 | stream.write(new Vinyl({ 44 | contents: Buffer.from('a { color: black }') 45 | })) 46 | 47 | stream.end() 48 | 49 | }) 50 | 51 | it('should not transform css with out any processor', function (cb) { 52 | 53 | var css = 'a { color: black }' 54 | 55 | var stream = postcss(function(){ 56 | return {} 57 | }) 58 | 59 | stream.on('data', function (file) { 60 | var result = file.contents.toString('utf8') 61 | var target = css 62 | assert.equal( result, target ) 63 | cb() 64 | }) 65 | 66 | stream.write(new Vinyl({ 67 | contents: Buffer.from(css) 68 | })) 69 | 70 | stream.end() 71 | 72 | }) 73 | 74 | it('should correctly wrap postcss errors', function (cb) { 75 | 76 | var stream = postcss([ doubler ]) 77 | 78 | stream.on('error', function (err) { 79 | assert.ok(err instanceof PluginError) 80 | assert.equal(err.plugin, 'gulp-postcss') 81 | assert.equal(err.column, 1) 82 | assert.equal(err.lineNumber, 1) 83 | assert.equal(err.name, 'CssSyntaxError') 84 | assert.equal(err.reason, 'Unclosed block') 85 | assert.equal(err.showStack, false) 86 | assert.equal(err.source, 'a {') 87 | assert.equal(err.fileName, path.resolve('testpath')) 88 | cb() 89 | }) 90 | 91 | stream.write(new Vinyl({ 92 | contents: Buffer.from('a {'), 93 | path: path.resolve('testpath') 94 | })) 95 | 96 | stream.end() 97 | 98 | }) 99 | 100 | it('should respond with error on stream files', function (cb) { 101 | 102 | var stream = postcss([ doubler ]) 103 | 104 | stream.on('error', function (err) { 105 | assert.ok(err instanceof PluginError) 106 | assert.equal(err.plugin, 'gulp-postcss') 107 | assert.equal(err.showStack, true) 108 | assert.equal(err.message, 'Streams are not supported!') 109 | assert.equal(err.fileName, path.resolve('testpath')) 110 | cb() 111 | }) 112 | 113 | var streamFile = { 114 | isStream: function () { return true }, 115 | isNull: function() { return false }, 116 | path: path.resolve('testpath') 117 | } 118 | 119 | stream.write(streamFile) 120 | 121 | stream.end() 122 | 123 | }) 124 | 125 | it('should generate source maps', function (cb) { 126 | 127 | var init = sourceMaps.init() 128 | var write = sourceMaps.write() 129 | var css = postcss( 130 | [ doubler, asyncDoubler ] 131 | ) 132 | 133 | init 134 | .pipe(css) 135 | .pipe(write) 136 | 137 | write.on('data', function (file) { 138 | assert.equal(file.sourceMap.mappings, 'AAAA,IAAI,YAAW,EAAX,YAAW,EAAX,YAAW,EAAX,aAAa') 139 | assert(/sourceMappingURL=data:application\/json;(?:charset=\w+;)?base64/.test(file.contents.toString())) 140 | cb() 141 | }) 142 | 143 | init.write(new Vinyl({ 144 | base: __dirname, 145 | path: __dirname + '/fixture.css', 146 | contents: Buffer.from('a { color: black }') 147 | })) 148 | 149 | init.end() 150 | 151 | }) 152 | 153 | 154 | it('should correctly generate relative source map', function (cb) { 155 | 156 | var init = sourceMaps.init() 157 | var css = postcss( 158 | [ doubler, doubler ] 159 | ) 160 | 161 | init.pipe(css) 162 | 163 | css.on('data', function (file) { 164 | assert.equal(file.sourceMap.file, 'fixture.css') 165 | assert.deepEqual(file.sourceMap.sources, ['fixture.css']) 166 | cb() 167 | }) 168 | 169 | init.write(new Vinyl({ 170 | base: __dirname + '/src', 171 | path: __dirname + '/src/fixture.css', 172 | contents: Buffer.from('a { color: black }') 173 | })) 174 | 175 | init.end() 176 | 177 | }) 178 | 179 | 180 | describe('PostCSS Guidelines', function () { 181 | 182 | var sandbox = sinon.createSandbox() 183 | var CssSyntaxError = function (message, source) { 184 | this.name = 'CssSyntaxError' 185 | this.message = message 186 | this.source = source 187 | this.showSourceCode = function () { 188 | return this.source 189 | } 190 | this.toString = function(){ 191 | var code = this.showSourceCode() 192 | if ( code ) { 193 | code = '\n\n' + code + '\n' 194 | } 195 | return this.name + ': ' + this.message + code 196 | } 197 | } 198 | var postcssStub = { 199 | use: function () {}, 200 | process: function () {} 201 | } 202 | var postcssLoadConfigStub 203 | var postcss = proxyquire('./index', { 204 | postcss: function (plugins) { 205 | postcssStub.use(plugins) 206 | return postcssStub 207 | }, 208 | 'postcss-load-config': function (ctx, configPath) { 209 | return postcssLoadConfigStub(ctx, configPath) 210 | }, 211 | 'vinyl-sourcemaps-apply': function () { 212 | return {} 213 | } 214 | }) 215 | 216 | beforeEach(function () { 217 | postcssLoadConfigStub = sandbox.stub() 218 | sandbox.stub(postcssStub, 'use') 219 | sandbox.stub(postcssStub, 'process') 220 | }) 221 | 222 | afterEach(function () { 223 | sandbox.restore() 224 | }) 225 | 226 | it('should set `from` and `to` processing options to `file.path`', function (cb) { 227 | 228 | var stream = postcss([ doubler ]) 229 | var cssPath = __dirname + '/src/fixture.css' 230 | postcssStub.process.returns(Promise.resolve({ 231 | css: '', 232 | warnings: function () { 233 | return [] 234 | } 235 | })) 236 | 237 | stream.on('data', function () { 238 | assert.equal(postcssStub.process.getCall(0).args[1].to, cssPath) 239 | assert.equal(postcssStub.process.getCall(0).args[1].from, cssPath) 240 | cb() 241 | }) 242 | 243 | stream.write(new Vinyl({ 244 | contents: Buffer.from('a {}'), 245 | path: cssPath 246 | })) 247 | 248 | stream.end() 249 | 250 | }) 251 | 252 | it('should allow override of `to` processing option', function (cb) { 253 | 254 | var stream = postcss([ doubler ], {to: 'overridden'}) 255 | postcssStub.process.returns(Promise.resolve({ 256 | css: '', 257 | warnings: function () { 258 | return [] 259 | } 260 | })) 261 | 262 | stream.on('data', function () { 263 | assert.equal(postcssStub.process.getCall(0).args[1].to, 'overridden') 264 | cb() 265 | }) 266 | 267 | stream.write(new Vinyl({ 268 | contents: Buffer.from('a {}') 269 | })) 270 | 271 | stream.end() 272 | 273 | }) 274 | 275 | it('should take plugins and options from callback', function (cb) { 276 | 277 | var cssPath = __dirname + '/fixture.css' 278 | var file = new Vinyl({ 279 | contents: Buffer.from('a {}'), 280 | path: cssPath 281 | }) 282 | var plugins = [ doubler ] 283 | var callback = sandbox.stub().returns({ 284 | plugins: plugins, 285 | options: { to: 'overridden' } 286 | }) 287 | var stream = postcss(callback) 288 | 289 | postcssStub.process.returns(Promise.resolve({ 290 | css: '', 291 | warnings: function () { 292 | return [] 293 | } 294 | })) 295 | 296 | stream.on('data', function () { 297 | assert.equal(callback.getCall(0).args[0], file) 298 | assert.equal(postcssStub.use.getCall(0).args[0], plugins) 299 | assert.equal(postcssStub.process.getCall(0).args[1].to, 'overridden') 300 | cb() 301 | }) 302 | 303 | stream.end(file) 304 | 305 | }) 306 | 307 | it('should take plugins and options from postcss-load-config', function (cb) { 308 | 309 | var cssPath = __dirname + '/fixture.css' 310 | var file = new Vinyl({ 311 | contents: Buffer.from('a {}'), 312 | path: cssPath 313 | }) 314 | var stream = postcss({ to: 'initial' }) 315 | var plugins = [ doubler ] 316 | 317 | postcssLoadConfigStub.returns(Promise.resolve({ 318 | plugins: plugins, 319 | options: { to: 'overridden' } 320 | })) 321 | 322 | postcssStub.process.returns(Promise.resolve({ 323 | css: '', 324 | warnings: function () { 325 | return [] 326 | } 327 | })) 328 | 329 | stream.on('data', function () { 330 | assert.deepEqual(postcssLoadConfigStub.getCall(0).args[0], { 331 | file: file, 332 | to: 'initial', 333 | options: { to: 'initial' } 334 | }) 335 | assert.equal(postcssStub.use.getCall(0).args[0], plugins) 336 | assert.equal(postcssStub.process.getCall(0).args[1].to, 'overridden') 337 | cb() 338 | }) 339 | 340 | stream.end(file) 341 | 342 | }) 343 | 344 | it('should point the config location to file directory', function (cb) { 345 | var cssPath = __dirname + '/fixture.css' 346 | var stream = postcss() 347 | postcssLoadConfigStub.returns(Promise.resolve({ plugins: [] })) 348 | postcssStub.process.returns(Promise.resolve({ 349 | css: '', 350 | warnings: function () { 351 | return [] 352 | } 353 | })) 354 | stream.on('data', function () { 355 | assert.deepEqual(postcssLoadConfigStub.getCall(0).args[1], __dirname) 356 | cb() 357 | }) 358 | stream.end(new Vinyl({ 359 | contents: Buffer.from('a {}'), 360 | path: cssPath 361 | })) 362 | }) 363 | 364 | it('should set the config location from option', function (cb) { 365 | var cssPath = __dirname + '/fixture.css' 366 | var stream = postcss({ config: '/absolute/path' }) 367 | postcssLoadConfigStub.returns(Promise.resolve({ plugins: [] })) 368 | postcssStub.process.returns(Promise.resolve({ 369 | css: '', 370 | warnings: function () { 371 | return [] 372 | } 373 | })) 374 | stream.on('data', function () { 375 | assert.deepEqual(postcssLoadConfigStub.getCall(0).args[1], '/absolute/path') 376 | cb() 377 | }) 378 | stream.end(new Vinyl({ 379 | contents: Buffer.from('a {}'), 380 | path: cssPath 381 | })) 382 | }) 383 | 384 | it('should set the config location from option relative to the base dir', function (cb) { 385 | var cssPath = __dirname + '/src/fixture.css' 386 | var stream = postcss({ config: './relative/path' }) 387 | postcssLoadConfigStub.returns(Promise.resolve({ plugins: [] })) 388 | postcssStub.process.returns(Promise.resolve({ 389 | css: '', 390 | warnings: function () { 391 | return [] 392 | } 393 | })) 394 | stream.on('data', function () { 395 | assert.deepEqual(postcssLoadConfigStub.getCall(0).args[1], path.join(__dirname, 'relative/path')) 396 | cb() 397 | }) 398 | stream.end(new Vinyl({ 399 | contents: Buffer.from('a {}'), 400 | path: cssPath, 401 | base: __dirname 402 | })) 403 | }) 404 | 405 | it('should not override `from` and `map` if using gulp-sourcemaps', function (cb) { 406 | var stream = postcss([ doubler ], { from: 'overridden', map: 'overridden' }) 407 | var cssPath = __dirname + '/fixture.css' 408 | postcssStub.process.returns(Promise.resolve({ 409 | css: '', 410 | warnings: function () { 411 | return [] 412 | }, 413 | map: { 414 | toJSON: function () { 415 | return { 416 | sources: [], 417 | file: '' 418 | } 419 | } 420 | } 421 | })) 422 | 423 | sandbox.stub(fancyLog, 'info') 424 | 425 | stream.on('data', function () { 426 | assert.deepEqual(postcssStub.process.getCall(0).args[1].from, cssPath) 427 | assert.deepEqual(postcssStub.process.getCall(0).args[1].map, { annotation: false }) 428 | var firstMessage = fancyLog.info.getCall(0).args[1] 429 | var secondMessage = fancyLog.info.getCall(1).args[1] 430 | assert(firstMessage, '/fixture.css\nCannot override from option, because it is required by gulp-sourcemaps') 431 | assert(secondMessage, '/fixture.css\nCannot override map option, because it is required by gulp-sourcemaps') 432 | cb() 433 | }) 434 | 435 | var file = new Vinyl({ 436 | contents: Buffer.from('a {}'), 437 | path: cssPath 438 | }) 439 | file.sourceMap = {} 440 | stream.end(file) 441 | }) 442 | 443 | it('should not output js stack trace for `CssSyntaxError`', function (cb) { 444 | 445 | var stream = postcss([ doubler ]) 446 | var cssSyntaxError = new CssSyntaxError('messageText', 'sourceCode') 447 | postcssStub.process.returns(Promise.reject(cssSyntaxError)) 448 | 449 | stream.on('error', function (error) { 450 | assert.equal(error.showStack, false) 451 | assert.equal(error.message, 'messageText\n\nsourceCode\n') 452 | assert.equal(error.source, 'sourceCode') 453 | cb() 454 | }) 455 | 456 | stream.write(new Vinyl({ 457 | contents: Buffer.from('a {}') 458 | })) 459 | 460 | stream.end() 461 | 462 | }) 463 | 464 | 465 | it('should display `result.warnings()` content', function (cb) { 466 | 467 | var stream = postcss([ doubler ]) 468 | var cssPath = __dirname + '/src/fixture.css' 469 | function Warning (msg) { 470 | this.toString = function () { 471 | return msg 472 | } 473 | } 474 | 475 | sandbox.stub(fancyLog, 'info') 476 | postcssStub.process.returns(Promise.resolve({ 477 | css: '', 478 | warnings: function () { 479 | return [new Warning('msg1'), new Warning('msg2')] 480 | } 481 | })) 482 | 483 | stream.on('data', function () { 484 | assert(fancyLog.info.calledWith('gulp-postcss:', 'src' + path.sep + 'fixture.css\nmsg1\nmsg2')) 485 | cb() 486 | }) 487 | 488 | stream.write(new Vinyl({ 489 | contents: Buffer.from('a {}'), 490 | path: cssPath 491 | })) 492 | 493 | stream.end() 494 | 495 | }) 496 | 497 | it('should pass options down to PostCSS', function (cb) { 498 | 499 | var customSyntax = function () {} 500 | var options = { 501 | syntax: customSyntax 502 | } 503 | 504 | var stream = postcss([ doubler ], options) 505 | var cssPath = __dirname + '/src/fixture.css' 506 | postcssStub.process.returns(Promise.resolve({ 507 | css: '', 508 | warnings: function () { 509 | return [] 510 | } 511 | })) 512 | 513 | stream.on('data', function () { 514 | var resultOptions = postcssStub.process.getCall(0).args[1] 515 | // remove automatically set options 516 | delete resultOptions.from 517 | delete resultOptions.to 518 | delete resultOptions.map 519 | assert.deepEqual(resultOptions, options) 520 | cb() 521 | }) 522 | 523 | stream.write(new Vinyl({ 524 | contents: Buffer.from('a {}'), 525 | path: cssPath 526 | })) 527 | 528 | stream.end() 529 | 530 | }) 531 | 532 | }) 533 | 534 | 535 | function doubler (css) { 536 | css.walkDecls(function (decl) { 537 | decl.parent.prepend(decl.clone()) 538 | }) 539 | } 540 | 541 | function asyncDoubler (css) { 542 | return new Promise(function (resolve) { 543 | setTimeout(function () { 544 | doubler(css) 545 | resolve() 546 | }) 547 | }) 548 | } 549 | 550 | function objectDoubler () { 551 | var processor = require('postcss')() 552 | processor.use(doubler) 553 | return processor 554 | } 555 | --------------------------------------------------------------------------------