├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── .jshintrc ├── .npmrc ├── CHANGELOG ├── Gruntfile.js ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── tasks └── postcss.js └── test ├── expected ├── defaults.css ├── diff.css ├── diff.css.diff ├── mapAnnotationPath.css ├── mapInline.css ├── mapSeparate.css ├── mapSeparate.css.map ├── maps │ └── mapAnnotationPath.css.map └── syntax.scss ├── fixtures ├── a.css └── a.scss └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | ; http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.map] 14 | insert_final_newline = false 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 10 8 | versioning-strategy: increase 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: monthly 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | env: 11 | FORCE_COLOR: 2 12 | 13 | jobs: 14 | run: 15 | name: Node ${{ matrix.node }} 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | node: [14, 16, 18, 20, 22] 22 | 23 | steps: 24 | - name: Clone repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Set Node.js version 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: ${{ matrix.node }} 31 | cache: npm 32 | 33 | - name: Install npm dependencies 34 | run: npm ci 35 | 36 | - name: Run tests 37 | run: npm test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .sass-cache 4 | node_modules 5 | npm-debug.log 6 | tmp 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 10, 3 | "asi": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "expr": true, 7 | "immed": true, 8 | "latedef": true, 9 | "newcap": true, 10 | "noarg": true, 11 | "sub": true, 12 | "undef": true, 13 | "boss": true, 14 | "eqnull": true, 15 | "node": true 16 | } 17 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | lockfile-version=2 2 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v3.1.1 2 | date: 03-11-2021 3 | changes: 4 | - Switch to picocolors 5 | - Code improvements 6 | v3.1.0 7 | date: 11-10-2021 8 | changes: 9 | - Move to the grunt logging API 10 | - General code improvements 11 | v3.0.1 12 | date: 13-04-2021 13 | changes: 14 | - Fix ability to catch grunt errors 15 | v3.0.0 16 | date: 19-09-2020 17 | changes: 18 | - Updated to PostCSS 8 (Note PostCSS is now a peerDependency you must install yourself) 19 | - Drop support for NodeJS 8, 11 and 13 20 | v2.0.4 21 | date: 12-05-2020 22 | changes: 23 | - Updated PostCSS to 7.0.31 24 | - Fix logging of source map output (thanks @jorrit) 25 | v2.0.3 26 | date: 12-05-2020 27 | changes: 28 | - Updated PostCSS to 7.0.30 29 | - Remove kleur dependency 30 | - Fix some logs not showing 31 | v2.0.2 32 | date: 15-04-2020 33 | changes: 34 | - Updated PostCSS to 7.0.27 35 | - Updated diff to 4.0.2 36 | v2.0.1 37 | date: 17-12-2019 38 | changes: 39 | - Updated PostCSS to 7.0.24 40 | v2.0.0 41 | date: 08-11-2019 42 | changes: 43 | - Updated PostCSS to 7.0.21 44 | - Updated kleur to 3.0.3 45 | - Requires Node 8.x or above 46 | 47 | v1.0.9 48 | date: 05-07-2019 49 | changes: 50 | - Updated Grunt to 1.0.4 51 | - Updated time-grunt to 4.0.0 52 | - Updated load-grunt-tasks to 5.0.0 53 | - Updated autoprefixer example to prevent deprecated warning 54 | v1.0.8 55 | date: 13-02-2019 56 | changes: 57 | - Updated PostCSS to 7.0.14 58 | - Updated Kleur to 3.0.2 59 | - Updated cssnano to 4.1.9 60 | v1.0.7 61 | date: 18-01-2019 62 | changes: 63 | - Updated PostCSS to 7.0.13 64 | v1.0.6 65 | date: 14-01-2019 66 | changes: 67 | - Updated PostCSS to 7.0.11 68 | - Replaced Chalk dependency with Kleur 69 | v1.0.5 70 | date: 08-01-2019 71 | changes: 72 | - Updated dependencies. Mainly postcss to v7.0.8 73 | v1.0.4 74 | date: 02-01-2019 75 | changes: 76 | - Fix source file size display (thanks @axten) 77 | v1.0.3 78 | date: 05-12-2018 79 | changes: 80 | - Add support to synchronously process CSS files (thanks @VitaliyR) 81 | - Fix documentation (thanks @jorrit) 82 | v1.0.0 - v1.0.2 83 | date: 22-11-2018 84 | changes: 85 | - PostCSS 7.0.6 86 | - Drop NodeJS 0.x and 4.x 87 | v0.9.0 88 | date: 10-09-2017 89 | changes: 90 | - PostCSS 6.0 91 | - Drop nodejs 0.12 support 92 | - `processors` option as a function (PR #99) 93 | v0.8.0 94 | date: 03-03-2016 95 | changes: 96 | - Drop nodejs 0.10 support 97 | v0.7.2 98 | date: 16-02-2016 99 | changes: 100 | - Update Grunt dependency (#pull/86) 101 | v0.7.1 102 | date: 06-11-2015 103 | changes: 104 | - Fix #70 105 | v0.7.0 106 | date: 21-10-2015 107 | changes: 108 | - New `failOnError` option 109 | - New `writeDest` option 110 | - Better npm@3 flat dependencies compatibility 111 | v0.6.0 112 | date: 23-08-2015 113 | changes: 114 | - PostCSS 5.0 115 | v0.5.5 116 | date: 12-07-2015 117 | changes: 118 | - Handle async PostCSS API properly 119 | v0.5.4 120 | date: 28-06-2015 121 | changes: 122 | - Fix annotation URL for Windows 123 | v0.5.3 124 | date: 25-06-2015 125 | changes: 126 | - Fix annotation paths 127 | v0.5.2 128 | date: 20-06-2015 129 | changes: 130 | - Fix `annotation` option 131 | v0.5.1 132 | date: 10-06-2015 133 | changes: 134 | - Fix process hanging when no source files found 135 | v0.5.0 136 | date: 07-06-2015 137 | changes: 138 | - PostCSS `safe` option support 139 | - Less verbose output in standard mode, `silent` option is removed 140 | - Log no source files found, not fail 141 | v0.4.0 142 | date: 04-05-2015 143 | changes: 144 | - Pass PostCSS Runner Guidelines 145 | v0.3.0: 146 | date: 18-01-2015 147 | changes: 148 | - PostCSS 4.0 149 | - Use a new PostCSS instance for each Grunt target (#12) 150 | v0.2.0: 151 | date: 14-11-2014 152 | changes: 153 | - PostCSS 3.0 154 | - Maps now inline and containing sourcesContent by default 155 | v0.1.0: 156 | date: 25-09-2014 157 | changes: 158 | - First release 159 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const processors = [ 4 | require('cssnano'), 5 | ] 6 | 7 | const processorsFn = () => [ 8 | require('cssnano'), 9 | ] 10 | 11 | module.exports = (grunt) => { 12 | require('load-grunt-tasks')(grunt) 13 | require('@lodder/time-grunt')(grunt) 14 | 15 | grunt.initConfig({ 16 | jshint: { 17 | all: [ 18 | 'Gruntfile.js', 19 | 'tasks/*.js', 20 | '<%= nodeunit.tests %>', 21 | ], 22 | options: { 23 | jshintrc: '.jshintrc', 24 | }, 25 | }, 26 | 27 | clean: { 28 | tests: ['tmp'], 29 | }, 30 | 31 | postcss: { 32 | defaults: { 33 | options: { 34 | processors, 35 | }, 36 | src: 'test/fixtures/a.css', 37 | dest: 'tmp/defaults.css', 38 | }, 39 | defaultsFn: { 40 | options: { 41 | processors: processorsFn, 42 | }, 43 | src: 'test/fixtures/a.css', 44 | dest: 'tmp/defaultsFn.css', 45 | }, 46 | mapInline: { 47 | options: { 48 | map: true, 49 | processors, 50 | }, 51 | src: 'test/fixtures/a.css', 52 | dest: 'tmp/mapInline.css', 53 | }, 54 | mapSeparate: { 55 | options: { 56 | map: { 57 | inline: false, 58 | }, 59 | processors, 60 | }, 61 | src: 'test/fixtures/a.css', 62 | dest: 'tmp/mapSeparate.css', 63 | }, 64 | mapAnnotationPath: { 65 | options: { 66 | map: { 67 | inline: false, 68 | annotation: 'tmp/maps/', 69 | }, 70 | processors, 71 | }, 72 | src: 'test/fixtures/a.css', 73 | dest: 'tmp/mapAnnotationPath.css', 74 | }, 75 | diff: { 76 | options: { 77 | diff: true, 78 | processors, 79 | }, 80 | src: 'test/fixtures/a.css', 81 | dest: 'tmp/diff.css', 82 | }, 83 | syntax: { 84 | options: { 85 | syntax: require('postcss-scss'), 86 | processors: [], 87 | }, 88 | src: 'test/fixtures/a.scss', 89 | dest: 'tmp/syntax.scss', 90 | }, 91 | doWriteDest: { 92 | options: { 93 | syntax: require('postcss-scss'), 94 | writeDest: true, 95 | }, 96 | src: 'test/fixtures/a.scss', 97 | dest: 'tmp/doWriteDest.scss', 98 | }, 99 | noWriteDest: { 100 | options: { 101 | syntax: require('postcss-scss'), 102 | writeDest: false, 103 | }, 104 | src: 'test/fixtures/a.scss', 105 | dest: 'tmp/noWriteDest.scss', 106 | }, 107 | sequential: { 108 | options: { 109 | syntax: require('postcss-scss'), 110 | sequential: true, 111 | }, 112 | src: ['test/fixtures/a.scss', 'test/fixtures/a.css'], 113 | dest: 'tmp/sequential.css', 114 | }, 115 | }, 116 | 117 | nodeunit: { 118 | tests: ['test/test.js'], 119 | }, 120 | }) 121 | 122 | grunt.loadTasks('tasks') 123 | 124 | grunt.registerTask('test', ['clean', 'postcss', 'nodeunit']) 125 | grunt.registerTask('default', ['jshint', 'test']) 126 | } 127 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Dmitry Nikitenko 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grunt-postcss 2 | 3 | [![npm Version](https://img.shields.io/npm/v/@lodder/grunt-postcss?logo=npm&logoColor=fff)](https://www.npmjs.com/package/@lodder/grunt-postcss) 4 | [![Build Status](https://img.shields.io/github/actions/workflow/status/C-Lodder/grunt-postcss/test.yml?branch=master&label=CI&logo=github)](https://github.com/C-Lodder/grunt-postcss/actions/workflows/test.yml?query=branch%3Amaster) 5 | 6 | > Apply several post-processors to your CSS using [PostCSS](https://github.com/postcss/postcss). 7 | 8 | ## Getting Started 9 | 10 | This plugin requires Grunt `1.0.3` or above. 11 | 12 | **Note:** As of v3.0.0, Node.js 10.x or above is required. 13 | 14 | If you haven't used [Grunt](https://gruntjs.com/) before, be sure to check out the [Getting Started](https://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](https://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. Once you're familiar with that process, you may install this plugin with this command: 15 | 16 | ```shell 17 | npm i --save-dev postcss @lodder/grunt-postcss 18 | ``` 19 | 20 | Once the plugin has been installed, it may be enabled inside your Gruntfile with this line of JavaScript: 21 | 22 | ```js 23 | grunt.loadNpmTasks('@lodder/grunt-postcss'); 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```shell 29 | npm i @lodder/grunt-postcss autoprefixer cssnano 30 | ``` 31 | 32 | ```js 33 | grunt.initConfig({ 34 | postcss: { 35 | options: { 36 | map: true, // inline sourcemaps 37 | 38 | // or 39 | map: { 40 | inline: false, // save all sourcemaps as separate files... 41 | annotation: 'dist/css/maps/' // ...to the specified directory 42 | }, 43 | 44 | processors: [ 45 | require('autoprefixer')(), 46 | require('cssnano')() // minify the result 47 | ] 48 | }, 49 | dist: { 50 | src: 'css/*.css' 51 | } 52 | } 53 | }); 54 | ``` 55 | 56 | ## Options 57 | 58 | ### Post-processors options 59 | 60 | ```js 61 | require('postcss-plugin')({option: value}) 62 | ``` 63 | 64 | ### Plugin options 65 | 66 | #### options.processors 67 | 68 | * Type: `Array|Function` 69 | * Default value: `[]` 70 | 71 | An array of PostCSS compatible post-processors. You can also use a function that returns an array of PostCSS post-processors. 72 | 73 | #### options.map 74 | 75 | * Type: `Boolean|Object` 76 | * Default value: `false` 77 | 78 | If the `map` option isn't defined or is set to `false`, PostCSS won't create or update sourcemaps. 79 | 80 | If `true` is specified, PostCSS will try to locate a sourcemap from a previous compilation step using an annotation comment (e.g. from Sass) and create a new sourcemap based on the found one (or just create a new inlined sourcemap). The created sourcemap can be either a separate file or an inlined map depending on what the previous sourcemap was. 81 | 82 | You can gain more control over sourcemap generation by assigning an object to the `map` option: 83 | 84 | * `prev` (string or `false`): a path to a directory where a previous sourcemap is (e.g. `path/`). By default, PostCSS will try to find a previous sourcemap using a path from the annotation comment (or using the annotation comment itself if the map is inlined). You can also set this option to `false` to delete the previous sourcemap. 85 | * `inline` (boolean): whether a sourcemap will be inlined or not. By default, it will be the same as a previous sourcemap or inlined. 86 | * `annotation` (boolean or string): by default, PostCSS will always add annotation comments with a path to a sourcemap file unless it is inlined or the input CSS does not have an annotation comment. PostCSS presumes that you want to save sourcemaps next to your output CSS files, but you can override this behavior and set a path to a directory as a string value for the option. 87 | * `sourcesContent` (boolean): whether original file contents (e.g. Sass sources) will be included to a sourcemap. By default, it's `true` unless a sourcemap from a previous compilation step has the original contents missing. 88 | 89 | #### options.diff 90 | 91 | * Type: `Boolean|String` 92 | * Default value: `false` 93 | 94 | Set it to `true` if you want to get a patch file: 95 | 96 | ```js 97 | options: { 98 | diff: true // or 'custom/path/to/file.css.patch' 99 | } 100 | ``` 101 | 102 | You can also specify a path where you want the file to be saved. 103 | 104 | #### options.sequential 105 | 106 | * Type: `Boolean` 107 | * Default value: `false` 108 | 109 | By default grunt-postcss will load all passed CSS files and immediately process them. Set this to `true` if you want files to be processed one by one. 110 | This can help in case when you have a lot of CSS files and processing them causes an `out of memory` error. 111 | 112 | #### options.failOnError 113 | 114 | * Type: `Boolean` 115 | * Default value: `false` 116 | 117 | Set it to `true` if you want grunt to exit with an error on detecting a warning or error. 118 | 119 | #### options.writeDest 120 | 121 | * Type: `Boolean` 122 | * Default value: `true` 123 | 124 | Set it to `false` if you do not want the destination files to be written. This does not affect the processing of the `map` and `diff` options. 125 | 126 | #### options.onError 127 | 128 | * Type: `Function` 129 | * Default value: `null` 130 | 131 | This function is called when an error occurs and passes the error data. 132 | 133 | #### options.syntax, options.parser, options.stringifier 134 | 135 | Options to control [PostCSS custom syntaxes](https://github.com/postcss/postcss#custom-syntaxes). 136 | 137 | ```js 138 | options: { 139 | parser: require('postcss-safe-parser') // instead of a removed `safe` option 140 | } 141 | ``` 142 | 143 | ```js 144 | options: { 145 | syntax: require('postcss-scss') // work with SCSS directly 146 | } 147 | ``` 148 | 149 | ## Why would I use this? 150 | 151 | Unlike the traditional approach with separate plugins, grunt-postcss allows you to parse and save CSS only once applying all post-processors in memory and thus reducing your build time. PostCSS is also a simple tool for writing your own CSS post-processors. 152 | 153 | ## How to migrate from grunt-autoprefixer? 154 | 155 | Autoprefixer is a PostCSS plugin, so first replace `grunt-autoprefixer` with `grunt-postcss` and `autoprefixer` plugin. 156 | 157 | ```shell 158 | npm remove --save-dev grunt-autoprefixer 159 | npm install --save-dev @lodder/grunt-postcss autoprefixer 160 | ``` 161 | 162 | Assuming you have a config like this: 163 | 164 | ```js 165 | autoprefixer: { 166 | options: { 167 | map: true, 168 | browsers: ['last 1 version'] 169 | }, 170 | dist: { 171 | src: '...' 172 | } 173 | } 174 | ``` 175 | 176 | Replace it with: 177 | 178 | ```js 179 | postcss: { 180 | options: { 181 | map: true, 182 | processors: [ 183 | require('autoprefixer')() 184 | ] 185 | }, 186 | dist: { 187 | src: '...' 188 | } 189 | } 190 | ``` 191 | 192 | And add the `browsers` to either your `package.json` or `.browserslistrc` file. 193 | 194 | `browsers`, `cascade` and `remove` options are plugin-specific, so we pass them as an argument while require the plugin. 195 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lodder/grunt-postcss", 3 | "version": "3.1.1", 4 | "description": "Apply several post-processors to your CSS using PostCSS", 5 | "author": { 6 | "name": "Dmitry Nikitenko", 7 | "email": "dima.nikitenko@gmail.com" 8 | }, 9 | "repository": "C-Lodder/grunt-postcss", 10 | "license": "MIT", 11 | "engines": { 12 | "node": ">=14" 13 | }, 14 | "scripts": { 15 | "test": "grunt test" 16 | }, 17 | "keywords": [ 18 | "gruntplugin", 19 | "postcss-runner", 20 | "css", 21 | "postprocessor", 22 | "postcss" 23 | ], 24 | "files": [ 25 | "tasks/*.js" 26 | ], 27 | "dependencies": { 28 | "diff": "^5.2.0", 29 | "maxmin": "^3.0.0", 30 | "picocolors": "^1.1.1" 31 | }, 32 | "devDependencies": { 33 | "@lodder/time-grunt": "^4.0.0", 34 | "cssnano": "^7.0.7", 35 | "grunt": "^1.6.1", 36 | "grunt-contrib-clean": "^2.0.1", 37 | "grunt-contrib-jshint": "^3.2.0", 38 | "grunt-contrib-nodeunit": "^5.0.0", 39 | "load-grunt-tasks": "^5.1.0", 40 | "postcss": "^8.5.4", 41 | "postcss-scss": "^4.0.9" 42 | }, 43 | "peerDependencies": { 44 | "grunt": ">=1.0.4", 45 | "postcss": "^8.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tasks/postcss.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const postcss = require('postcss') 5 | const diff = require('diff') 6 | const maxmin = require('maxmin') 7 | const picocolors = require('picocolors') 8 | 9 | module.exports = (grunt) => { 10 | let options 11 | let processor 12 | let tasks 13 | 14 | /** 15 | * Returns an input map contents if a custom map path was specified 16 | * @param {string} from Input CSS path 17 | * @returns {?string} 18 | */ 19 | function getPrevMap(from) { 20 | if (typeof options.map.prev === 'string') { 21 | const mapPath = `${options.map.prev + path.basename(from)}.map` 22 | 23 | if (grunt.file.exists(mapPath)) { 24 | return grunt.file.read(mapPath) 25 | } 26 | } 27 | } 28 | 29 | /** 30 | * @param {string} to Output CSS path 31 | * @returns {string} 32 | */ 33 | function getSourcemapPath(to) { 34 | return `${path.join(options.map.annotation, path.basename(to))}.map` 35 | } 36 | 37 | /** 38 | * @param {string} to Output CSS path 39 | * @returns {boolean|string} 40 | */ 41 | function getAnnotation(to) { 42 | let annotation = true 43 | 44 | if (typeof options.map.annotation === 'boolean') { 45 | annotation = options.map.annotation 46 | } 47 | 48 | if (typeof options.map.annotation === 'string') { 49 | annotation = path.relative(path.dirname(to), getSourcemapPath(to)).replace(/\\/g, '/') 50 | } 51 | 52 | return annotation 53 | } 54 | 55 | /** 56 | * @param {string} input Input CSS contents 57 | * @param {string} from Input CSS path 58 | * @param {string} to Output CSS path 59 | * @returns {LazyResult} 60 | */ 61 | function process(input, from, to) { 62 | const map = typeof options.map === 'boolean' ? 63 | options.map : { 64 | prev: getPrevMap(from), 65 | inline: typeof options.map.inline === 'boolean' ? options.map.inline : true, 66 | annotation: getAnnotation(to), 67 | sourcesContent: typeof options.map.sourcesContent === 'boolean' ? options.map.sourcesContent : true, 68 | absolute: typeof options.map.absolute === 'boolean' ? options.map.absolute : false, 69 | } 70 | 71 | return processor.process(input, { 72 | map, 73 | from, 74 | to, 75 | parser: options.parser, 76 | stringifier: options.stringifier, 77 | syntax: options.syntax, 78 | }) 79 | } 80 | 81 | /** 82 | * Runs tasks sequentially 83 | * @returns {Promise} 84 | */ 85 | function runSequence() { 86 | if (tasks.length === 0) { 87 | return Promise.resolve() 88 | } 89 | 90 | let currentTask = tasks.shift() 91 | 92 | return process(currentTask.input, currentTask.from, currentTask.to).then((result) => { 93 | currentTask.cb(result) 94 | currentTask = null 95 | return runSequence() 96 | }) 97 | } 98 | 99 | /** 100 | * Creates a task to be processed 101 | * @param {string} input 102 | * @param {string} from 103 | * @param {string} to 104 | * @param {Function} cb 105 | * @returns {Promise|Object} 106 | */ 107 | function createTask(input, from, to, cb) { 108 | return options.sequential ? { 109 | input, 110 | from, 111 | to, 112 | cb, 113 | } : 114 | process(input, from, to).then(cb) 115 | } 116 | 117 | /** 118 | * Runs prepared tasks 119 | * @returns {Promise} 120 | */ 121 | function runTasks() { 122 | return options.sequential ? runSequence() : Promise.all(tasks) 123 | } 124 | 125 | grunt.registerMultiTask('postcss', 'Process CSS files.', function() { 126 | options = this.options({ 127 | processors: [], 128 | map: false, 129 | diff: false, 130 | safe: false, 131 | failOnError: false, 132 | writeDest: true, 133 | sequential: false, 134 | }) 135 | const tally = { 136 | sheets: 0, 137 | maps: 0, 138 | diffs: 0, 139 | issues: 0, 140 | sizeBefore: 0, 141 | sizeAfter: 0, 142 | } 143 | const done = this.async() 144 | tasks = [] 145 | 146 | processor = typeof options.processors === 'function' ? 147 | postcss(options.processors.call()) : 148 | postcss(options.processors) 149 | 150 | for (const f of this.files) { 151 | const src = f.src.filter((filepath) => { 152 | if (!grunt.file.exists(filepath)) { 153 | grunt.log.error(`Source file ${picocolors.cyan(filepath)} not found.`) 154 | 155 | return false 156 | } 157 | 158 | return true 159 | }) 160 | 161 | if (src.length === 0) { 162 | grunt.log.error(picocolors.red('No source files were found.')) 163 | 164 | done() 165 | continue 166 | } 167 | 168 | Array.prototype.push.apply(tasks, src.map((filepath) => { 169 | const dest = f.dest || filepath 170 | const input = grunt.file.read(filepath) 171 | tally.sizeBefore += input.length 172 | 173 | return createTask(input, filepath, dest, (result) => { 174 | const warnings = result.warnings() 175 | 176 | tally.issues += warnings.length 177 | 178 | for (const msg of warnings) { 179 | grunt.log.error(picocolors.red(msg.toString())) 180 | } 181 | 182 | if (options.writeDest) { 183 | tally.sizeAfter += result.css.length 184 | grunt.file.write(dest, result.css) 185 | grunt.log.ok(`File ${picocolors.cyan(dest)} created. ${picocolors.green(maxmin(input.length, result.css.length))}`) 186 | } 187 | 188 | tally.sheets += 1 189 | 190 | if (result.map) { 191 | let mapDest = `${dest}.map` 192 | 193 | if (typeof options.map.annotation === 'string') { 194 | mapDest = getSourcemapPath(dest) 195 | } 196 | 197 | grunt.file.write(mapDest, result.map.toString()) 198 | grunt.log.ok(`File ${picocolors.cyan(`${dest}.map`)} created (source map).`) 199 | 200 | tally.maps += 1 201 | } 202 | 203 | if (options.diff) { 204 | const diffPath = typeof options.diff === 'string' ? options.diff : `${dest}.diff` 205 | 206 | grunt.file.write(diffPath, diff.createPatch(dest, input, result.css)) 207 | grunt.log.ok(`File ${picocolors.cyan(diffPath)} created (diff).`) 208 | 209 | tally.diffs += 1 210 | } 211 | }) 212 | })) 213 | } 214 | 215 | runTasks().then(() => { 216 | if (tally.sheets) { 217 | if (options.writeDest) { 218 | const size = maxmin(tally.sizeBefore, tally.sizeAfter) 219 | grunt.log.ok(`${tally.sheets} processed ${grunt.util.pluralize(tally.sheets, 'stylesheet/stylesheets')} created. ${picocolors.green(size)}`) 220 | } else { 221 | grunt.log.write(`${tally.sheets} ${grunt.util.pluralize(tally.sheets, 'stylesheet/stylesheets')} processed, no files written.`) 222 | } 223 | } 224 | 225 | if (tally.maps) { 226 | grunt.log.ok(`${tally.maps} ${grunt.util.pluralize(tally.maps, 'sourcemap/sourcemaps')} created.`) 227 | } 228 | 229 | if (tally.diffs) { 230 | grunt.log.ok(`${tally.diffs} ${grunt.util.pluralize(tally.diffs, 'diff/diffs')} created.`) 231 | } 232 | 233 | if (tally.issues) { 234 | grunt.log.error(`${tally.issues} ${grunt.util.pluralize(tally.issues, 'issue/issues')} found.`) 235 | 236 | if (options.failOnError) { 237 | return done(false) 238 | } 239 | } 240 | 241 | done() 242 | }).catch((error) => { 243 | if (options.onError !== undefined && typeof options.onError === 'function') { 244 | options.onError(error) 245 | } 246 | 247 | if (error.name === 'CssSyntaxError') { 248 | grunt.fail.fatal(error.message + error.showSourceCode()) 249 | } else { 250 | grunt.fail.fatal(error) 251 | } 252 | 253 | done(error) 254 | }) 255 | }) 256 | } 257 | -------------------------------------------------------------------------------- /test/expected/defaults.css: -------------------------------------------------------------------------------- 1 | a{color:#fff} -------------------------------------------------------------------------------- /test/expected/diff.css: -------------------------------------------------------------------------------- 1 | a{color:#fff} -------------------------------------------------------------------------------- /test/expected/diff.css.diff: -------------------------------------------------------------------------------- 1 | Index: tmp/diff.css 2 | =================================================================== 3 | --- tmp/diff.css 4 | +++ tmp/diff.css 5 | @@ -1,3 +1,1 @@ 6 | -a { 7 | - color: #fff; 8 | -} 9 | +a{color:#fff} 10 | \ No newline at end of file 11 | -------------------------------------------------------------------------------- /test/expected/mapAnnotationPath.css: -------------------------------------------------------------------------------- 1 | a{color:#fff} 2 | /*# sourceMappingURL=maps/mapAnnotationPath.css.map */ -------------------------------------------------------------------------------- /test/expected/mapInline.css: -------------------------------------------------------------------------------- 1 | a{color:#fff} 2 | /*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3Rlc3QvZml4dHVyZXMvYS5jc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsRUFDSSxVQUNKIiwiZmlsZSI6Im1hcElubGluZS5jc3MiLCJzb3VyY2VzQ29udGVudCI6WyJhIHtcbiAgICBjb2xvcjogI2ZmZjtcbn1cbiJdfQ== */ -------------------------------------------------------------------------------- /test/expected/mapSeparate.css: -------------------------------------------------------------------------------- 1 | a{color:#fff} 2 | /*# sourceMappingURL=mapSeparate.css.map */ -------------------------------------------------------------------------------- /test/expected/mapSeparate.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../test/fixtures/a.css"],"names":[],"mappings":"AAAA,EACI,UACJ","file":"mapSeparate.css","sourcesContent":["a {\n color: #fff;\n}\n"]} -------------------------------------------------------------------------------- /test/expected/maps/mapAnnotationPath.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../../test/fixtures/a.css"],"names":[],"mappings":"AAAA,EACI,UACJ","file":"../mapAnnotationPath.css","sourcesContent":["a {\n color: #fff;\n}\n"]} -------------------------------------------------------------------------------- /test/expected/syntax.scss: -------------------------------------------------------------------------------- 1 | $color: #fff; 2 | 3 | a { 4 | color: $color; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/a.css: -------------------------------------------------------------------------------- 1 | a { 2 | color: #fff; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/a.scss: -------------------------------------------------------------------------------- 1 | $color: #fff; 2 | 3 | a { 4 | color: $color; 5 | } 6 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { readFile, access } = require('fs').promises 4 | 5 | /* 6 | ======== A Handy Little Nodeunit Reference ======== 7 | https://github.com/caolan/nodeunit 8 | 9 | Test methods: 10 | test.expect(numAssertions) 11 | test.done() 12 | Test assertions: 13 | test.ok(value, [message]) 14 | test.equal(actual, expected, [message]) 15 | test.notEqual(actual, expected, [message]) 16 | test.deepEqual(actual, expected, [message]) 17 | test.notDeepEqual(actual, expected, [message]) 18 | test.strictEqual(actual, expected, [message]) 19 | test.notStrictEqual(actual, expected, [message]) 20 | test.throws(block, [error], [message]) 21 | test.doesNotThrow(block, [error], [message]) 22 | test.ifError(value) 23 | */ 24 | 25 | const fileExists = async file => { 26 | try { 27 | await access(file) 28 | return true 29 | } catch { 30 | return false 31 | } 32 | } 33 | 34 | exports.gruntPostcss = { 35 | async defaults(test) { 36 | const actual = { 37 | css: await readFile('tmp/defaults.css', 'utf8'), 38 | } 39 | 40 | const expected = { 41 | css: await readFile('test/expected/defaults.css', 'utf8'), 42 | } 43 | 44 | test.strictEqual(actual.css, expected.css) 45 | 46 | const checkExists = await fileExists('tmp/defaults.css.map') 47 | test.ok(!checkExists) 48 | test.done() 49 | }, 50 | 51 | async defaultsFn(test) { 52 | const actual = { 53 | css: await readFile('tmp/defaultsFn.css', 'utf8'), 54 | } 55 | 56 | const expected = { 57 | css: await readFile('test/expected/defaults.css', 'utf8'), 58 | } 59 | 60 | test.strictEqual(actual.css, expected.css) 61 | 62 | const checkExists = await fileExists('tmp/defaultsFn.css.map') 63 | test.ok(!checkExists) 64 | test.done() 65 | }, 66 | 67 | async mapInline(test) { 68 | const actual = { 69 | css: await readFile('tmp/mapInline.css', 'utf8'), 70 | } 71 | 72 | const expected = { 73 | css: await readFile('test/expected/mapInline.css', 'utf8'), 74 | } 75 | 76 | test.strictEqual(actual.css, expected.css) 77 | 78 | const checkExists = await fileExists('tmp/mapInline.css.map') 79 | test.ok(!checkExists) 80 | test.done() 81 | }, 82 | 83 | async mapSeparate(test) { 84 | const actual = { 85 | css: await readFile('tmp/mapSeparate.css', 'utf8'), 86 | map: await readFile('tmp/mapSeparate.css.map', 'utf8'), 87 | } 88 | 89 | const expected = { 90 | css: await readFile('test/expected/mapSeparate.css', 'utf8'), 91 | map: await readFile('test/expected/mapSeparate.css.map', 'utf8'), 92 | } 93 | 94 | test.strictEqual(actual.css, expected.css) 95 | test.strictEqual(actual.map, expected.map) 96 | test.done() 97 | }, 98 | 99 | async mapAnnotationPath(test) { 100 | const actual = { 101 | css: await readFile('tmp/mapAnnotationPath.css', 'utf8'), 102 | map: await readFile('tmp/maps/mapAnnotationPath.css.map', 'utf8'), 103 | } 104 | 105 | const expected = { 106 | css: await readFile('test/expected/mapAnnotationPath.css', 'utf8'), 107 | map: await readFile('test/expected/maps/mapAnnotationPath.css.map', 'utf8'), 108 | } 109 | 110 | test.strictEqual(actual.css, expected.css) 111 | test.strictEqual(actual.map, expected.map) 112 | 113 | const checkExists = await fileExists('tmp/mapAnnotationPath.css.map') 114 | test.ok(!checkExists) 115 | test.done() 116 | }, 117 | 118 | async diff(test) { 119 | const actual = { 120 | css: await readFile('tmp/diff.css', 'utf8'), 121 | map: await readFile('tmp/diff.css.diff', 'utf8'), 122 | } 123 | 124 | const expected = { 125 | css: await readFile('test/expected/diff.css', 'utf8'), 126 | map: await readFile('test/expected/diff.css.diff', 'utf8'), 127 | } 128 | 129 | test.strictEqual(actual.css, expected.css) 130 | test.strictEqual(actual.map, expected.map) 131 | test.done() 132 | }, 133 | 134 | async syntax(test) { 135 | const actual = { 136 | scss: await readFile('tmp/syntax.scss', 'utf8'), 137 | } 138 | 139 | const expected = { 140 | scss: await readFile('test/expected/syntax.scss', 'utf8'), 141 | } 142 | 143 | test.strictEqual(actual.scss, expected.scss) 144 | test.done() 145 | }, 146 | 147 | async writeDest(test) { 148 | const checkExists = await fileExists('tmp/doWriteDest.scss') 149 | test.ok(checkExists) 150 | 151 | const checkNoExists = await fileExists('tmp/noWriteDest.scss') 152 | test.ok(!checkNoExists) 153 | test.done() 154 | }, 155 | 156 | async sequential(test) { 157 | const checkExists = await fileExists('tmp/sequential.css') 158 | test.ok(checkExists) 159 | 160 | const actual = await readFile('tmp/sequential.css', 'utf8') 161 | const expected = await readFile('test/fixtures/a.css', 'utf8') 162 | test.strictEqual(actual, expected) 163 | test.done() 164 | }, 165 | } 166 | --------------------------------------------------------------------------------