├── .eslintrc ├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── validate.js ├── dist ├── .gitignore ├── .npmignore └── test │ ├── csstree-validator.cjs │ └── csstree-validator.js ├── fixtures ├── css │ ├── bar │ │ ├── not.a.css.file │ │ └── style.css │ ├── foo │ │ └── style.css │ ├── style.css │ └── style.validate-result ├── custom-reporter │ ├── custom-reporter.cjs │ ├── custom-reporter.js │ ├── node_modules │ │ ├── commonjs │ │ │ ├── lib │ │ │ │ └── index.js │ │ │ └── package.json │ │ ├── dual │ │ │ ├── lib │ │ │ │ ├── index.cjs │ │ │ │ └── index.js │ │ │ └── package.json │ │ └── esm │ │ │ ├── lib │ │ │ └── index.js │ │ │ └── package.json │ └── style.css └── reporter │ ├── checkstyle │ ├── console │ ├── gnu │ └── json ├── lib ├── bundle.js ├── cli.js ├── helpers.js ├── index.js ├── reporter │ ├── checkstyle.js │ ├── console.js │ ├── gnu.js │ ├── index.js │ └── json.js ├── validate.js └── version.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── scripts ├── bundle.js └── esm-to-cjs.js └── test ├── cli.js ├── loc.js ├── reporters.js ├── validate.js └── validators.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "mocha": true, 5 | "es6": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 2020, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "no-duplicate-case": 2, 13 | "no-undef": 2, 14 | "no-unused-vars": [ 15 | 2, 16 | { 17 | "vars": "all", 18 | "args": "after-used" 19 | } 20 | ], 21 | "no-empty": [ 22 | 2, 23 | { 24 | "allowEmptyCatch": true 25 | } 26 | ], 27 | "no-implicit-coercion": [ 28 | 2, 29 | { 30 | "boolean": true, 31 | "string": true, 32 | "number": true 33 | } 34 | ], 35 | "no-with": 2, 36 | "brace-style": 2, 37 | "no-mixed-spaces-and-tabs": 2, 38 | "no-multiple-empty-lines": 2, 39 | "no-multi-str": 2, 40 | "dot-location": [ 41 | 2, 42 | "property" 43 | ], 44 | "operator-linebreak": [ 45 | 2, 46 | "after", 47 | { 48 | "overrides": { 49 | "?": "before", 50 | ":": "before" 51 | } 52 | } 53 | ], 54 | "key-spacing": [ 55 | 2, 56 | { 57 | "beforeColon": false, 58 | "afterColon": true 59 | } 60 | ], 61 | "space-unary-ops": [ 62 | 2, 63 | { 64 | "words": true, 65 | "nonwords": false 66 | } 67 | ], 68 | "no-spaced-func": 2, 69 | "space-before-function-paren": [ 70 | 2, 71 | { 72 | "anonymous": "ignore", 73 | "named": "never" 74 | } 75 | ], 76 | "array-bracket-spacing": [ 77 | 2, 78 | "never" 79 | ], 80 | "space-in-parens": [ 81 | 2, 82 | "never" 83 | ], 84 | "comma-dangle": [ 85 | 2, 86 | "never" 87 | ], 88 | "no-trailing-spaces": 2, 89 | "yoda": [ 90 | 2, 91 | "never" 92 | ], 93 | "camelcase": [ 94 | 2, 95 | { 96 | "properties": "never" 97 | } 98 | ], 99 | "comma-style": [ 100 | 2, 101 | "last" 102 | ], 103 | "curly": [ 104 | 2, 105 | "all" 106 | ], 107 | "dot-notation": 2, 108 | "eol-last": 2, 109 | "one-var": [ 110 | 2, 111 | "never" 112 | ], 113 | "wrap-iife": 2, 114 | "space-infix-ops": 2, 115 | "keyword-spacing": [ 116 | 2, 117 | { 118 | "overrides": { 119 | "else": { 120 | "before": true 121 | }, 122 | "while": { 123 | "before": true 124 | }, 125 | "catch": { 126 | "before": true 127 | }, 128 | "finally": { 129 | "before": true 130 | } 131 | } 132 | } 133 | ], 134 | "spaced-comment": [ 135 | 2, 136 | "always" 137 | ], 138 | "space-before-blocks": [ 139 | 2, 140 | "always" 141 | ], 142 | "semi": [ 143 | 2, 144 | "always" 145 | ], 146 | "indent": [ 147 | 2, 148 | 4, 149 | { 150 | "SwitchCase": 1 151 | } 152 | ], 153 | "linebreak-style": [ 154 | 2, 155 | "unix" 156 | ], 157 | "quotes": [ 158 | 2, 159 | "single", 160 | { 161 | "avoidEscape": true 162 | } 163 | ] 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | env: 8 | PRIMARY_NODEJS_VERSION: 22 9 | REPORTER: "min" 10 | 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Setup node ${{ env.PRIMARY_NODEJS_VERSION }} 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: ${{ env.PRIMARY_NODEJS_VERSION }} 21 | cache: "npm" 22 | - run: npm ci 23 | - run: npm run lint 24 | 25 | test-bundle: 26 | name: Test bundle 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | - name: Setup node ${{ env.PRIMARY_NODEJS_VERSION }} 31 | uses: actions/setup-node@v2 32 | with: 33 | node-version: ${{ env.PRIMARY_NODEJS_VERSION }} 34 | cache: "npm" 35 | - run: npm ci 36 | - run: npm run bundle-and-test 37 | 38 | unit-tests: 39 | name: Unit tests 40 | runs-on: ubuntu-latest 41 | 42 | strategy: 43 | matrix: 44 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 45 | node_version: 46 | - 12.20.0 47 | - 14.13.0 48 | - 16 49 | - 20 50 | - 22 51 | 52 | steps: 53 | - uses: actions/checkout@v2 54 | - name: Setup node ${{ matrix.node_version }} 55 | uses: actions/setup-node@v2 56 | with: 57 | node-version: ${{ matrix.node_version }} 58 | cache: "npm" 59 | - run: npm ci 60 | - run: npm run test 61 | - run: npm run esm-to-cjs-and-test 62 | 63 | - run: npm run coverage 64 | if: ${{ matrix.node_version == env.PRIMARY_NODEJS_VERSION }} 65 | - name: Coveralls parallel 66 | if: ${{ matrix.node_version == env.PRIMARY_NODEJS_VERSION }} 67 | uses: coverallsapp/github-action@1.1.3 68 | with: 69 | github-token: ${{ secrets.GITHUB_TOKEN }} 70 | flag-name: node-${{ matrix.node_version }} 71 | parallel: true 72 | 73 | send-to-coveralls: 74 | name: Send coverage to Coveralls 75 | needs: unit-tests 76 | runs-on: ubuntu-latest 77 | steps: 78 | - name: Send coverage to Coveralls 79 | uses: coverallsapp/github-action@1.1.3 80 | with: 81 | github-token: ${{ secrets.GITHUB_TOKEN }} 82 | parallel-finished: true 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | /cjs/ 3 | /cjs-test/ 4 | /node_modules/ 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 4.0.1 (October 11, 2024) 2 | 3 | - Fixed location properties on validation errors; all errors (excluding parse errors) now include `offset`, `start`, `end`, and `loc` properties 4 | 5 | ## 4.0.0 (October 10, 2024) 6 | 7 | - Bumped `csstree` to [^3.0.0](https://github.com/csstree/csstree/releases/tag/v3.0.0) 8 | - Added default reporters into bundle entry points 9 | - Fixed the resolution of a path to a reporter by employing `enhanced-resolve`, which now considers the `exports` field in `package.json` 10 | - Fixed `package.json` for bundling for browser environments 11 | 12 | ## 3.0.0 (December 13, 2021) 13 | 14 | - Added custom reporters support in CLI, e.g. `csstree-validator --reporter path/to/reporter.js` or `csstree-validator --reporter reporter-package` 15 | - Added `Symbol.iterator` for `validateString()`, `validateDictionary()`, `validateFile()`, `validatePathList()` and `validatePath()` result value, i.e. it now can be used with `for ... of` for example `for (const [filename, errors] of result) ...` 16 | - Bumped `csstree` to [2.0](https://github.com/csstree/csstree/releases/tag/v2.0.0) 17 | - Package 18 | - Changed supported versions of Node.js to `^12.20.0`, `^14.13.0` and `>=15.0.0` 19 | - Converted to ES modules. However, CommonJS is supported as well (dual module) 20 | - Added bundle `dist/csstree-validator.esm.js` as ES module 21 | 22 | ## 2.0.1 (March 31, 2021) 23 | 24 | - Fixed wrong `require()` in CLI that causes to crash 25 | - Bumped `csstree` to [1.1.3](https://github.com/csstree/csstree/releases/tag/v1.1.1) to fix the issue with parsing that causes to a failure on a value with function/brackets and `!important` (#18) 26 | 27 | ## 2.0.0 (November 18, 2020) 28 | 29 | - Droped support for Nodejs < 8 30 | - Bumped `csstree` to [1.1.1](https://github.com/csstree/csstree/releases/tag/v1.1.1) 31 | - CLI exits with code `1` and outputs messages to `stderr` when errors (#12) 32 | - Added built version for browsers: `dist/csstree-validator.js` (#11) 33 | - Added at-rule validation for name, prelude and descriptor 34 | - Added `validateAtrule`, `validateAtrulePrelude`, `validateAtruleDescriptor`, `validateRule` and `validateDeclaration` methods 35 | 36 | ## 1.6.0 (October 27, 2020) 37 | 38 | - Bumped `csstree` to [1.0.0](https://github.com/csstree/csstree/releases/tag/v1.0.0) 39 | 40 | ## 1.5.1 (October 7, 2019) 41 | 42 | - Updated `csstree` to [1.0.0-alpha.34](https://github.com/csstree/csstree/releases/tag/v1.0.0-alpha.34) 43 | 44 | ## 1.5.0 (July 11, 2019) 45 | 46 | - Updated `csstree` to [1.0.0-alpha.32](https://github.com/csstree/csstree/releases/tag/v1.0.0-alpha.32) 47 | 48 | ## 1.4.0 (May 30, 2018) 49 | 50 | - Updated `csstree` to [1.0.0-alpha.29](https://github.com/csstree/csstree/releases/tag/v1.0.0-alpha.29) 51 | 52 | ## 1.3.1 (February 19, 2018) 53 | 54 | - Updated `csstree` to 1.0.0-alpha.28 55 | 56 | ## 1.3.0 (November 12, 2017) 57 | 58 | - Added `gnu` reporter (@sideshowbarker, #8) 59 | - Updated `csstree` to 1.0.0-alpha.26 60 | 61 | ## 1.2.1 (September 14, 2017) 62 | 63 | - Updated `csstree` to 1.0.0-alpha24 (minor bug fixes) 64 | 65 | ## 1.2.0 (September 4, 2017) 66 | 67 | - Updated `csstree` to [1.0.0-alpha21](https://github.com/csstree/csstree/releases/tag/v1.0.0-alpha21) 68 | - Use tolerant mode to parse a CSS. Since now a single parse error doesn't prevent validation of a whole CSS. 69 | 70 | ## 1.1.0 (August 28, 2017) 71 | 72 | - Updated `csstree` to [1.0.0-alpha20](https://github.com/csstree/csstree/releases/tag/v1.0.0-alpha20) 73 | - Changed validate function to always contain a list of errors (no single error on parse error) 74 | - Added `validateDictionary()` that validate a dictionary, where key is a filename and value is a CSS as string 75 | - Changed `validateFile()`, `validatePath()` and `validatePathList()` to handle possible file system exceptions (such errors will be stored as regular errors) 76 | - Added second argument for `validatePath()` and `validatePathList()` to rule which file should be validated. Functions validate files with `.css` extension only, when second parameter is not passed. 77 | - Fixed minor issues in reporters output 78 | 79 | ## 1.0.8 (January 19, 2017) 80 | 81 | - Added `loc` to mismatch errors when possible 82 | - Fixed wrong `source` in node `loc` 83 | - Updated `csstree` to `1.0.0-alpha13` 84 | 85 | ## 1.0.7 (January 19, 2017) 86 | 87 | - Updated `csstree` to `1.0.0-alpha12` 88 | 89 | ## 1.0.6 (December 23, 2016) 90 | 91 | - Updated `csstree` to `1.0.0-alpha9` 92 | 93 | ## 1.0.5 (November 11, 2016) 94 | 95 | - Updated `csstree` to `1.0.0-alpha8` 96 | 97 | ## 1.0.4 (October 8, 2016) 98 | 99 | - Updated `csstree` to `1.0.0-alpha7` 100 | 101 | ## 1.0.3 (September 23, 2016) 102 | 103 | - Updated `csstree` to `1.0.0-alpha6` (it was not updated by mistake) 104 | 105 | ## 1.0.2 (September 23, 2016) 106 | 107 | - Updated `csstree` to `1.0.0-alpha6` 108 | - Use syntax validation error line and column when possible to more accurately indicate problem location 109 | - Improved message output for default reporter 110 | - Fixed CSS parse error output (or any other exception during validate) 111 | 112 | ## 1.0.0 (September 17, 2016) 113 | 114 | - Initial implementation 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016-2024 by Roman Dvornov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM Version](https://img.shields.io/npm/v/csstree-validator.svg)](https://www.npmjs.com/package/csstree-validator) 2 | [![Build Status](https://github.com/csstree/validator/actions/workflows/build.yml/badge.svg)](https://github.com/csstree/validator/actions/workflows/build.yml) 3 | [![Coverage Status](https://coveralls.io/repos/github/csstree/validator/badge.svg?branch=master)](https://coveralls.io/github/csstree/validator?branch=master) 4 | 5 | # CSSTree Validator 6 | 7 | CSS Validator built on [CSSTree](https://github.com/csstree/csstree). 8 | 9 | Technically, the package utilizes the capabilities of CSSTree to match CSS syntaxes to various parts of your code and generates a list of errors, if any. 10 | 11 | > **Note:** If `csstree-validator` produces false positives or false negatives, such as unknown properties or invalid values for a property, please report the issue to the [CSSTree issue tracker](https://github.com/csstree/csstree/issues). 12 | 13 | > **Note:** CSSTree currently doesn't support selector syntax matching; therefore, `csstree-validator` doesn't support it either. Support for selector validation will be added once it is available in CSSTree. 14 | 15 | ## Installation 16 | 17 | Install the package via npm: 18 | 19 | ```bash 20 | npm install csstree-validator 21 | ``` 22 | 23 | ## Usage 24 | 25 | You can validate a CSS string or a [CSSTree AST](https://github.com/csstree/csstree/blob/master/docs/ast.md): 26 | 27 | ```js 28 | import { validate } from 'csstree-validator'; 29 | // For CommonJS: 30 | // const { validate } = require('csstree-validator'); 31 | 32 | const filename = 'demo/example.css'; 33 | const css = '.class { pading: 10px; border: 1px super red }'; 34 | 35 | console.log(validate(css, filename)); 36 | // Output: 37 | // [ 38 | // SyntaxError [SyntaxReferenceError]: Unknown property `pading` { 39 | // reference: 'pading', 40 | // property: 'pading', 41 | // offset: 9, 42 | // line: 1, 43 | // column: 10 44 | // }, 45 | // SyntaxError [SyntaxMatchError]: Mismatch { 46 | // message: 'Invalid value for `border` property', 47 | // rawMessage: 'Mismatch', 48 | // syntax: ' || || ', 49 | // css: '1px super red', 50 | // mismatchOffset: 4, 51 | // mismatchLength: 5, 52 | // offset: 35, 53 | // line: 1, 54 | // column: 36, 55 | // loc: { source: 'demo/example.css', start: [Object], end: [Object] }, 56 | // property: 'border', 57 | // details: 'Mismatch\n' + 58 | // ' syntax: || || \n' + 59 | // ' value: 1px super red\n' + 60 | // ' ------------^' 61 | // } 62 | // ] 63 | ``` 64 | 65 | Alternatively, you can use [helper functions](#helpers) to validate a file or directory and utilize one of the built-in [reporters](#reporters): 66 | 67 | ```js 68 | import { validateFile, reporters } from 'csstree-validator'; 69 | 70 | const result = validateFile('./path/to/style.css'); 71 | console.log(reporters.checkstyle(result)); 72 | ``` 73 | 74 | ### Validation Methods 75 | 76 | - `validate(css, filename)` 77 | - `validateAtrule(node)` 78 | - `validateAtrulePrelude(atrule, prelude, preludeLoc)` 79 | - `validateAtruleDescriptor(atrule, descriptor, value, descriptorLoc)` 80 | - `validateDeclaration(property, value, valueLoc)` 81 | - `validateRule(node)` 82 | 83 | ## Helpers 84 | 85 | > **Note:** Helpers are not available in browser environments as they rely on Node.js APIs. 86 | 87 | All helper functions return an object where the key is the path to a file and the value is an array of errors. The result object is iterable (has `Symbol.iterator`) and can be used with `for...of` loops or the spread operator. 88 | 89 | Example: 90 | 91 | ```js 92 | const result = validateFile('path/to/file.css'); 93 | 94 | for (const [filename, errors] of result) { 95 | // Process errors 96 | } 97 | ``` 98 | 99 | Available helper functions: 100 | 101 | - `validateString(css, filename)` 102 | - `validateDictionary(dictionary)` 103 | - `validateFile(filename)` 104 | - `validatePath(searchPath, filter)` 105 | - `validatePathList(pathList, filter)` 106 | 107 | ## Reporters 108 | 109 | CSSTree Validator provides several built-in reporters to convert validation results into different formats: 110 | 111 | - `console` – Human-readable text suitable for console output. 112 | - `json` – Converts errors into a unified JSON array of objects: 113 | 114 | ```ts 115 | type ErrorEntry = { 116 | name: string; // Filename 117 | line: number; 118 | column: number; 119 | atrule?: string; 120 | descriptor?: string; 121 | property?: string; 122 | message: string; 123 | details?: any; 124 | } 125 | ``` 126 | 127 | - `checkstyle` – [Checkstyle](https://checkstyle.sourceforge.io/) XML report format: 128 | 129 | ```xml 130 | 131 | 132 | 133 | 134 | 135 | 136 | ``` 137 | 138 | - `gnu` – GNU error log format: 139 | 140 | ``` 141 | "FILENAME":LINE.COLUMN: error: MESSAGE 142 | "FILENAME":START_LINE.COLUMN-END_LINE.COLUMN: error: MESSAGE 143 | ``` 144 | 145 | Example usage: 146 | 147 | ```js 148 | import { validate, reporters } from 'csstree-validator'; 149 | 150 | const css = '.class { padding: 10px; color: red; }'; 151 | const result = validate(css, 'example.css'); 152 | 153 | console.log(reporters.json(result)); 154 | // Output: 155 | // [ 156 | // { "name": 'example.css', ... }, 157 | // { "name": 'example.css', ... }, 158 | // ... 159 | // ] 160 | ``` 161 | 162 | ## Browser Usage 163 | 164 | CSSTree Validator can be used in browser environments using the available bundles: 165 | 166 | - **IIFE Bundle (`dist/csstree-validator.js`)** – Minified IIFE with `csstreeValidator` as a global variable. 167 | 168 | ```html 169 | 170 | 173 | ``` 174 | 175 | - **ES Module (`dist/csstree-validator.esm.js`)** – Minified ES module. 176 | 177 | ```html 178 | 183 | ``` 184 | 185 | You can also use a CDN service like `unpkg` or `jsDelivr`. By default, the ESM version is exposed for short paths. For the IIFE version, specify the full path to the bundle: 186 | 187 | ```html 188 | 189 | 194 | 195 | 196 | 197 | 198 | 199 | ``` 200 | 201 | **Note:** Helpers are not available in the browser version. 202 | 203 | ## Command-Line Interface (CLI) 204 | 205 | Install globally via npm: 206 | 207 | ```bash 208 | npm install -g csstree-validator 209 | ``` 210 | 211 | Run the validator on a CSS file: 212 | 213 | ```bash 214 | csstree-validator /path/to/style.css 215 | ``` 216 | 217 | Display help: 218 | 219 | ```bash 220 | csstree-validator -h 221 | ``` 222 | 223 | ``` 224 | Usage: 225 | 226 | csstree-validator [fileOrDir] [options] 227 | 228 | Options: 229 | 230 | -h, --help Output usage information 231 | -r, --reporter Output formatter: console (default), checkstyle, json, gnu 232 | or 233 | -v, --version Output version 234 | ``` 235 | 236 | ### Custom Reporters 237 | 238 | In addition to the built-in reporters, you can specify a custom reporter by providing the path to a module or package. The module should export a single function that takes the validation result object and returns a string: 239 | 240 | ```js 241 | export default function(result) { 242 | let output = ''; 243 | 244 | for (const [filename, errors] of result) { 245 | // Generate custom output 246 | } 247 | 248 | return output; 249 | } 250 | 251 | // For CommonJS: 252 | // module.exports = function(result) { ... } 253 | ``` 254 | 255 | The `reporter` option accepts: 256 | 257 | - **ESM Module** – Full path to a file with a `.js` extension. 258 | - **CommonJS Module** – Full path to a file with a `.cjs` extension. 259 | - **ESM Package** – Package name or full path to a module within the package. 260 | - **CommonJS Package** – Package name or path to a module within the package. 261 | - **Dual Package** – Package name or full path to a module within the package. 262 | 263 | The resolution algorithm checks the `reporter` value in the following order: 264 | 265 | 1. If it's a path to a file (relative to `process.cwd()`), use it as a module. 266 | 2. If it's a path to a package module (relative to `process.cwd()`), use the package's module. 267 | 3. Otherwise, the value should be the name of one of the predefined reporters, or an error will be raised. 268 | 269 | ## Integrations 270 | 271 | Plugins that use `csstree-validator`: 272 | 273 | - [VS Code Plugin](https://github.com/csstree/vscode-plugin) 274 | 275 | ## License 276 | 277 | MIT 278 | -------------------------------------------------------------------------------- /bin/validate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { run, isCliError } from '../lib/cli.js'; 4 | 5 | async function perform() { 6 | try { 7 | await run(); 8 | } catch (e) { 9 | // output user frendly message if cli error 10 | if (isCliError(e)) { 11 | console.error(e.message || e); 12 | process.exit(1); 13 | } 14 | 15 | // otherwise re-throw exception 16 | throw e; 17 | } 18 | }; 19 | 20 | perform(); 21 | -------------------------------------------------------------------------------- /dist/.gitignore: -------------------------------------------------------------------------------- 1 | *.cjs 2 | *.js 3 | !test/csstree-validator.cjs 4 | !test/csstree-validator.js 5 | !.gitignore 6 | !.npmignore 7 | -------------------------------------------------------------------------------- /dist/.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !csstree-validator.js 3 | !csstree-validator.esm.js 4 | !version.cjs 5 | !version.js 6 | -------------------------------------------------------------------------------- /dist/test/csstree-validator.cjs: -------------------------------------------------------------------------------- 1 | /* global csstreeValidator */ 2 | const assert = require('assert'); 3 | const fs = require('fs'); 4 | 5 | it('csstree-validator.js', () => { 6 | eval(fs.readFileSync('dist/csstree-validator.js', 'utf8')); 7 | const actual = csstreeValidator.validate('.test { color: gren; colo: #ff0000; }'); 8 | 9 | assert.deepStrictEqual( 10 | actual.map(error => error.message), 11 | [ 12 | 'Invalid value for `color` property', 13 | 'Unknown property `colo`' 14 | ] 15 | ); 16 | }); 17 | -------------------------------------------------------------------------------- /dist/test/csstree-validator.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { validate } from '../csstree-validator.esm.js'; 3 | 4 | it('csstree-validator.esm.js', () => { 5 | const actual = validate('.test { color: gren; colo: #ff0000; }'); 6 | 7 | assert.deepStrictEqual( 8 | actual.map(error => error.message), 9 | [ 10 | 'Invalid value for `color` property', 11 | 'Unknown property `colo`' 12 | ] 13 | ); 14 | }); 15 | -------------------------------------------------------------------------------- /fixtures/css/bar/not.a.css.file: -------------------------------------------------------------------------------- 1 | test { 2 | unknown: 1; 3 | color: bad; 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/css/bar/style.css: -------------------------------------------------------------------------------- 1 | test { 2 | unknown: 1; 3 | color: bad; 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/css/foo/style.css: -------------------------------------------------------------------------------- 1 | test { 2 | unknown: 1; 3 | color: bad; 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/css/style.css: -------------------------------------------------------------------------------- 1 | test { 2 | unknown: 1; 3 | color: bad; 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/css/style.validate-result: -------------------------------------------------------------------------------- 1 | # fixtures/css/style.css 2 | * Unknown property `unknown` 3 | * Invalid value for `color` property 4 | syntax: 5 | value: bad 6 | --------^ 7 | -------------------------------------------------------------------------------- /fixtures/custom-reporter/custom-reporter.cjs: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return 'OK'; 3 | }; 4 | -------------------------------------------------------------------------------- /fixtures/custom-reporter/custom-reporter.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | return 'OK'; 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/custom-reporter/node_modules/commonjs/lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return 'OK'; 3 | } -------------------------------------------------------------------------------- /fixtures/custom-reporter/node_modules/commonjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commonjs", 3 | "main": "lib/index.js" 4 | } -------------------------------------------------------------------------------- /fixtures/custom-reporter/node_modules/dual/lib/index.cjs: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return 'OK'; 3 | } -------------------------------------------------------------------------------- /fixtures/custom-reporter/node_modules/dual/lib/index.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | return 'OK'; 3 | } -------------------------------------------------------------------------------- /fixtures/custom-reporter/node_modules/dual/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commonjs", 3 | "type": "module", 4 | "main": "lib/index.cjs", 5 | "exports": { 6 | ".": { 7 | "require": "./lib/index.cjs", 8 | "import": "./lib/index.js" 9 | }, 10 | "./lib/index.cjs": "./lib/index.cjs", 11 | "./lib/index.js": "./lib/index.js" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /fixtures/custom-reporter/node_modules/esm/lib/index.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | return 'OK'; 3 | } -------------------------------------------------------------------------------- /fixtures/custom-reporter/node_modules/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commonjs", 3 | "type": "module", 4 | "main": "lib/index.js" 5 | } -------------------------------------------------------------------------------- /fixtures/custom-reporter/style.css: -------------------------------------------------------------------------------- 1 | test { 2 | unknown: 1; 3 | color: bad; 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/reporter/checkstyle: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /fixtures/reporter/console: -------------------------------------------------------------------------------- 1 | # match.css 2 | * Invalid value for `color` property 3 | syntax: 4 | value: 123 5 | --------^ 6 | * Invalid value for `border` property 7 | syntax: || || 8 | value: 1px unknown red 9 | ------------^ 10 | * Unknown property `unknown` 11 | 12 | # parse.css 13 | [ERROR] Colon is expected 14 | * Invalid value for `color` property 15 | syntax: 16 | value: red green 17 | ------------^ 18 | -------------------------------------------------------------------------------- /fixtures/reporter/gnu: -------------------------------------------------------------------------------- 1 | "match.css":1.16-1.19: error: Invalid value for `color` property: `123`; allowed: 2 | "match.css":1.33-1.40: error: Invalid value for `border` property: `1px unknown red`; allowed: || || 3 | "match.css":1.46-1.53: error: Unknown property `unknown` 4 | "parse.css":1.11: error: Colon is expected 5 | "parse.css":1.32-1.37: error: Invalid value for `color` property: `red green`; allowed: 6 | -------------------------------------------------------------------------------- /fixtures/reporter/json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "match.css", 4 | "line": 1, 5 | "column": 16, 6 | "property": "color", 7 | "message": "Invalid value for `color` property", 8 | "details": "Mismatch\n syntax: \n value: 123\n --------^" 9 | }, 10 | { 11 | "name": "match.css", 12 | "line": 1, 13 | "column": 33, 14 | "property": "border", 15 | "message": "Invalid value for `border` property", 16 | "details": "Mismatch\n syntax: || || \n value: 1px unknown red\n ------------^" 17 | }, 18 | { 19 | "name": "match.css", 20 | "line": 1, 21 | "column": 46, 22 | "property": "unknown", 23 | "message": "Unknown property `unknown`", 24 | "details": null 25 | }, 26 | { 27 | "name": "parse.css", 28 | "line": 1, 29 | "column": 11, 30 | "message": "Colon is expected", 31 | "details": null 32 | }, 33 | { 34 | "name": "parse.css", 35 | "line": 1, 36 | "column": 32, 37 | "property": "color", 38 | "message": "Invalid value for `color` property", 39 | "details": "Mismatch\n syntax: \n value: red green\n ------------^" 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /lib/bundle.js: -------------------------------------------------------------------------------- 1 | import * as reporters from './reporter/index.js'; 2 | 3 | export * from './version.js'; 4 | export * from './validate.js'; 5 | export { reporters }; 6 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import resolve from 'enhanced-resolve'; 4 | import { command as createCommand, Error as CliError } from 'clap'; 5 | import * as reporters from './reporter/index.js'; 6 | import { validatePath, validateString } from './helpers.js'; 7 | import { version } from './version.js'; 8 | 9 | async function readStdin() { 10 | const buffer = []; 11 | 12 | for await (const chunk of process.stdin) { 13 | buffer.push(chunk); 14 | } 15 | 16 | return buffer.join(''); 17 | } 18 | 19 | function printResult(result, reporter) { 20 | const output = reporter(result); 21 | 22 | if (Object.keys(result).length > 0) { 23 | console.error(output); 24 | process.exit(1); 25 | } 26 | 27 | if (output) { 28 | console.log(output); 29 | } 30 | } 31 | 32 | const nodeResolver = resolve.create.sync({ 33 | conditionNames: ['node', 'import'] 34 | }); 35 | 36 | const command = createCommand('csstree-validate [fileOrDir]') 37 | .version(version) 38 | .option( 39 | '-r, --reporter ', 40 | 'Output formatter: console (default), checkstyle, json, gnu or ', 41 | (nameOrFile) => { 42 | const modulePath = path.resolve(process.cwd(), nameOrFile); 43 | 44 | if (fs.existsSync(modulePath)) { 45 | return import(modulePath); 46 | } 47 | 48 | if (!hasOwnProperty.call(reporters, nameOrFile)) { 49 | try { 50 | const resolvedPath = nodeResolver(process.cwd(), nameOrFile); 51 | return import(resolvedPath); 52 | } catch (e) {} 53 | 54 | throw new CliError('Wrong value for reporter: ' + nameOrFile); 55 | } 56 | 57 | return nameOrFile; 58 | }, 59 | 'console' 60 | ) 61 | .action(async ({ options, args }) => { 62 | const inputPath = args[0]; 63 | const reporter = typeof options.reporter === 'string' 64 | ? reporters[options.reporter] 65 | : (await options.reporter).default; 66 | 67 | if (process.stdin.isTTY && !inputPath) { 68 | command.run(['--help']); 69 | return; 70 | } 71 | 72 | if (!inputPath) { 73 | readStdin().then(input => 74 | printResult(validateString(input, ''), reporter) 75 | ); 76 | } else { 77 | if (!fs.existsSync(inputPath)) { 78 | throw new CliError(`ERROR! No such file or directory: ${inputPath}`); 79 | } 80 | 81 | printResult(validatePath(args[0]), reporter); 82 | } 83 | }); 84 | 85 | export const run = command.run.bind(command); 86 | export function isCliError(err) { 87 | return err instanceof CliError; 88 | } 89 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | import { statSync, readdirSync, readFileSync } from 'fs'; 2 | import { extname, join } from 'path'; 3 | import { validate } from './validate.js'; 4 | 5 | function createResult() { 6 | const result = Object.create(null); 7 | 8 | result[Symbol.iterator] = function*() { 9 | for (const [filename, errors] of Object.entries(this)) { 10 | yield [filename, errors]; 11 | } 12 | }; 13 | 14 | return result; 15 | } 16 | 17 | function defaultShouldBeValidated(filename) { 18 | return extname(filename) === '.css'; 19 | } 20 | 21 | function collectFiles(testPath, shouldBeValidated) { 22 | try { 23 | if (statSync(testPath).isDirectory()) { 24 | return [].concat(...readdirSync(testPath).map(dirFilename => 25 | collectFiles(join(testPath, dirFilename), shouldBeValidated) 26 | )).sort(); 27 | } else { 28 | return shouldBeValidated(testPath) ? [testPath] : []; 29 | } 30 | } catch (e) { 31 | return [testPath]; 32 | } 33 | } 34 | 35 | export function validateDictionary(dictionary) { 36 | const result = createResult(); 37 | 38 | for (const filename of Object.keys(dictionary).sort()) { 39 | result[filename] = validate(dictionary[filename], filename); 40 | } 41 | 42 | return result; 43 | } 44 | 45 | export function validateString(css, filename) { 46 | const result = createResult(); 47 | 48 | if (!filename) { 49 | filename = ''; 50 | } 51 | 52 | result[filename] = validate(css, filename); 53 | 54 | return result; 55 | } 56 | 57 | export function validateFile(filename) { 58 | const result = createResult(); 59 | let css; 60 | 61 | try { 62 | css = readFileSync(filename, 'utf-8'); 63 | result[filename] = validate(css, filename); 64 | } catch (e) { 65 | result[filename] = [e]; 66 | } 67 | 68 | return result; 69 | } 70 | 71 | function validateFileList(list) { 72 | const result = createResult(); 73 | 74 | for (const filename of list) { 75 | const res = validateFile(filename)[filename]; 76 | 77 | if (res && res.length !== 0) { 78 | result[filename] = res; 79 | } 80 | } 81 | 82 | return result; 83 | } 84 | 85 | export function validatePathList(pathList, filter) { 86 | if (typeof filter !== 'function') { 87 | filter = defaultShouldBeValidated; 88 | } 89 | 90 | const fileList = new Set([].concat(...pathList.map(path => 91 | collectFiles(path, filter) 92 | ))); 93 | 94 | return validateFileList([...fileList].sort()); 95 | } 96 | 97 | export function validatePath(searchPath, filter) { 98 | if (typeof filter !== 'function') { 99 | filter = defaultShouldBeValidated; 100 | } 101 | 102 | return validateFileList(collectFiles(searchPath, filter)); 103 | } 104 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import * as reporters from './reporter/index.js'; 2 | 3 | export * from './version.js'; 4 | export * from './helpers.js'; 5 | export * from './validate.js'; 6 | export { reporters }; 7 | -------------------------------------------------------------------------------- /lib/reporter/checkstyle.js: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 4 | // 5 | // 6 | // 7 | export default function(result) { 8 | const output = [ 9 | '', 10 | '' 11 | ]; 12 | 13 | for (const [filename, errors] of result) { 14 | output.push( 15 | '\t', 16 | errors.map(entry => 17 | '\t\t' 22 | ).join('\n'), 23 | '\t' 24 | ); 25 | } 26 | 27 | output.push(''); 28 | 29 | return output.join('\n'); 30 | } 31 | -------------------------------------------------------------------------------- /lib/reporter/console.js: -------------------------------------------------------------------------------- 1 | export default function(result) { 2 | const output = []; 3 | 4 | for (const [filename, errors] of result) { 5 | output.push('# ' + filename); 6 | output.push(...errors.map(function(error) { 7 | if (error.name === 'SyntaxError') { 8 | return ' [ERROR] ' + error.message; 9 | } 10 | 11 | return ' * ' + 12 | String(error.details) 13 | .replace(/^[^\n]+/, error.message) 14 | .replace(/\n/g, '\n '); 15 | })); 16 | output.push(''); 17 | } 18 | 19 | return output.join('\n'); 20 | } 21 | -------------------------------------------------------------------------------- /lib/reporter/gnu.js: -------------------------------------------------------------------------------- 1 | // "FILENAME":LINE.COLUMN: error: MESSAGE 2 | // "FILENAME":START_LINE.COLUMN-END_LINE.COLUMN: error: MESSAGE 3 | export default function(result) { 4 | const output = []; 5 | 6 | for (const [filename, errors] of result) { 7 | output.push(errors.map((error) => { 8 | const line = error.line || -1; 9 | const column = error.column || -1; 10 | const message = error.message; 11 | const value = error.css ? ': `' + error.css + '`' : ''; 12 | const allowed = error.syntax ? '; allowed: ' + error.syntax : ''; 13 | let position = line + '.' + column; 14 | 15 | if (error.loc) { 16 | position += '-' + 17 | error.loc.end.line + '.' + 18 | error.loc.end.column; 19 | } 20 | 21 | return '"' + 22 | filename + '":' + 23 | position + ': ' + 24 | 'error: ' + 25 | message + 26 | value + 27 | allowed; 28 | }).join('\n')); 29 | } 30 | 31 | return output.join('\n'); 32 | } 33 | -------------------------------------------------------------------------------- /lib/reporter/index.js: -------------------------------------------------------------------------------- 1 | export { default as json } from './json.js'; 2 | export { default as console } from './console.js'; 3 | export { default as checkstyle } from './checkstyle.js'; 4 | export { default as gnu } from './gnu.js'; 5 | -------------------------------------------------------------------------------- /lib/reporter/json.js: -------------------------------------------------------------------------------- 1 | // [{ "name": {file}, "line": {line}, "column": {column}, "property": {property}, "message": {error}, "details": {details} }] 2 | export default function(result) { 3 | const output = []; 4 | 5 | for (const [filename, errors] of result) { 6 | output.push(...errors.map((entry) => { 7 | const error = entry.error || entry; 8 | 9 | return { 10 | name: filename, 11 | line: entry.line || 1, 12 | column: entry.column || 1, 13 | atrule: entry.atrule, 14 | descriptor: entry.descriptor, 15 | property: entry.property, 16 | message: entry.message, 17 | details: error.details || (error.rawMessage ? error.message : null) 18 | }; 19 | })); 20 | } 21 | 22 | return JSON.stringify(output, null, 4); 23 | } 24 | -------------------------------------------------------------------------------- /lib/validate.js: -------------------------------------------------------------------------------- 1 | import { lexer, parse, walk, property as propertyName } from 'css-tree'; 2 | 3 | const syntax = lexer; 4 | 5 | function isTargetError(error) { 6 | if (!error) { 7 | return null; 8 | } 9 | 10 | if (error.name !== 'SyntaxError' && 11 | error.name !== 'SyntaxMatchError' && 12 | error.name !== 'SyntaxReferenceError') { 13 | return null; 14 | } 15 | 16 | return error; 17 | } 18 | 19 | function locFromIdentStart(ident, loc) { 20 | if (!loc) { 21 | return null; 22 | } 23 | 24 | const { source, start } = loc; 25 | 26 | return { 27 | source, 28 | start, 29 | end: { 30 | offset: start.offset + ident.length, 31 | line: start.line, 32 | column: start.column + ident.length 33 | } 34 | }; 35 | } 36 | 37 | function makeErrorEntry(error, override) { 38 | Object.assign(error, override); 39 | 40 | if (error.loc) { 41 | const { source, start, end } = error.loc; 42 | 43 | // recreate loc to ensure shape and avoid sharing 44 | error.loc = { 45 | source, 46 | start: { 47 | offset: start.offset, 48 | line: start.line, 49 | column: start.column 50 | }, 51 | end: { 52 | offset: end.offset, 53 | line: end.line, 54 | column: end.column 55 | } 56 | }; 57 | 58 | // map loc.start values on error 59 | Object.assign(error, error.loc.start); 60 | } 61 | 62 | return error; 63 | } 64 | 65 | export function validateAtrule(node) { 66 | const atrule = node.name; 67 | const errors = []; 68 | let error; 69 | 70 | if (error = isTargetError(syntax.checkAtruleName(atrule))) { 71 | errors.push(makeErrorEntry(error, { 72 | atrule, 73 | loc: locFromIdentStart('@' + atrule, node.loc) 74 | })); 75 | 76 | return errors; 77 | } 78 | 79 | errors.push(...validateAtrulePrelude( 80 | atrule, 81 | node.prelude, 82 | node.loc 83 | )); 84 | 85 | if (node.block && node.block.children) { 86 | node.block.children.forEach(child => { 87 | if (child.type === 'Declaration') { 88 | errors.push(...validateAtruleDescriptor( 89 | atrule, 90 | child.property, 91 | child.value, 92 | child.loc 93 | )); 94 | } 95 | }); 96 | } 97 | 98 | return errors; 99 | } 100 | 101 | export function validateAtrulePrelude(atrule, prelude, atruleLoc) { 102 | const errors = []; 103 | let error; 104 | 105 | if (error = isTargetError(syntax.checkAtrulePrelude(atrule, prelude))) { 106 | errors.push(makeErrorEntry(error, { 107 | atrule, 108 | loc: prelude ? prelude.loc : locFromIdentStart('@' + atrule, atruleLoc) 109 | })); 110 | } else if (error = isTargetError(syntax.matchAtrulePrelude(atrule, prelude).error)) { 111 | errors.push(makeErrorEntry(error, { 112 | atrule, 113 | ...error.rawMessage === 'Mismatch' && 114 | { details: error.message, message: 'Invalid value for `@' + atrule + '` prelude' } 115 | })); 116 | } 117 | 118 | return errors; 119 | } 120 | 121 | export function validateAtruleDescriptor(atrule, descriptor, value, descriptorLoc) { 122 | const errors = []; 123 | let error; 124 | 125 | if (error = isTargetError(syntax.checkAtruleDescriptorName(atrule, descriptor))) { 126 | errors.push(makeErrorEntry(error, { 127 | atrule, 128 | descriptor, 129 | loc: locFromIdentStart(descriptor, descriptorLoc) 130 | })); 131 | } else { 132 | if (error = isTargetError(syntax.matchAtruleDescriptor(atrule, descriptor, value).error)) { 133 | errors.push(makeErrorEntry(error, { 134 | atrule, 135 | descriptor, 136 | ...error.rawMessage === 'Mismatch' && 137 | { details: error.message, message: 'Invalid value for `' + descriptor + '` descriptor' } 138 | })); 139 | } 140 | } 141 | 142 | return errors; 143 | } 144 | 145 | export function validateDeclaration(property, value, declarationLoc) { 146 | const errors = []; 147 | let error; 148 | 149 | if (propertyName(property).custom) { 150 | return errors; 151 | } 152 | 153 | if (error = isTargetError(syntax.checkPropertyName(property))) { 154 | errors.push(makeErrorEntry(error, { 155 | property, 156 | loc: locFromIdentStart(property, declarationLoc) 157 | })); 158 | } else if (error = isTargetError(syntax.matchProperty(property, value).error)) { 159 | errors.push(makeErrorEntry(error, { 160 | property, 161 | ...error.rawMessage === 'Mismatch' && 162 | { details: error.message, message: 'Invalid value for `' + property + '` property' } 163 | })); 164 | } 165 | 166 | return errors; 167 | } 168 | 169 | export function validateRule(node) { 170 | const errors = []; 171 | 172 | if (node.block && node.block.children) { 173 | node.block.children.forEach(child => { 174 | if (child.type === 'Declaration') { 175 | errors.push(...validateDeclaration( 176 | child.property, 177 | child.value, 178 | child.loc 179 | )); 180 | } 181 | }); 182 | } 183 | 184 | return errors; 185 | } 186 | 187 | export function validate(css, filename) { 188 | const errors = []; 189 | const ast = typeof css !== 'string' 190 | ? css 191 | : parse(css, { 192 | filename, 193 | positions: true, 194 | parseAtrulePrelude: false, 195 | parseRulePrelude: false, 196 | parseValue: false, 197 | parseCustomProperty: false, 198 | onParseError(error) { 199 | errors.push(error); 200 | } 201 | }); 202 | 203 | walk(ast, { 204 | visit: 'Atrule', 205 | enter(node) { 206 | errors.push(...validateAtrule(node)); 207 | } 208 | }); 209 | 210 | walk(ast, { 211 | visit: 'Rule', 212 | enter(node) { 213 | errors.push(...validateRule(node)); 214 | } 215 | }); 216 | 217 | return errors; 218 | }; 219 | -------------------------------------------------------------------------------- /lib/version.js: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'module'; 2 | 3 | const require = createRequire(import.meta.url); 4 | 5 | export const { version } = require('../package.json'); 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csstree-validator", 3 | "version": "4.0.1", 4 | "description": "CSS validator built on csstree", 5 | "author": "Roman Dvornov ", 6 | "license": "MIT", 7 | "repository": "csstree/validator", 8 | "keywords": [ 9 | "css", 10 | "syntax", 11 | "validator", 12 | "checker" 13 | ], 14 | "bin": { 15 | "csstree-validator": "./bin/validate.js" 16 | }, 17 | "type": "module", 18 | "main": "./cjs/index.cjs", 19 | "module": "./lib/index.js", 20 | "exports": { 21 | ".": { 22 | "browser": { 23 | "import": "./lib/bundle.js", 24 | "require": "./cjs/bundle.cjs" 25 | }, 26 | "import": "./lib/index.js", 27 | "require": "./cjs/index.cjs" 28 | }, 29 | "./package.json": "./package.json" 30 | }, 31 | "unpkg": "dist/csstree-validator.esm.js", 32 | "jsdelivr": "dist/csstree-validator.esm.js", 33 | "browser": { 34 | "./cjs/index.cjs": "./cjs/bundle.cjs", 35 | "./lib/index.js": "./lib/bundle.js", 36 | "./cjs/version.cjs": "./dist/version.cjs", 37 | "./lib/version.js": "./dist/version.js" 38 | }, 39 | "scripts": { 40 | "lint-and-test": "npm run lint && npm test", 41 | "lint": "eslint lib test", 42 | "test": "mocha test --reporter ${REPORTER:-progress}", 43 | "test:cjs": "mocha cjs-test --reporter ${REPORTER:-progress}", 44 | "test:dist": "mocha dist/test --reporter ${REPORTER:-progress}", 45 | "build": "npm run bundle && npm run esm-to-cjs", 46 | "build-and-test": "npm run build && npm run test:dist && npm run test:cjs", 47 | "bundle": "node scripts/bundle", 48 | "bundle-and-test": "npm run bundle && npm run test:dist", 49 | "esm-to-cjs": "node scripts/esm-to-cjs", 50 | "esm-to-cjs-and-test": "npm run esm-to-cjs && npm run test:cjs", 51 | "coverage": "c8 --reporter=lcovonly npm test", 52 | "prepublishOnly": "npm run lint-and-test && npm run build-and-test" 53 | }, 54 | "dependencies": { 55 | "clap": "^3.0.0", 56 | "css-tree": "^3.0.0", 57 | "enhanced-resolve": "^5.16.0" 58 | }, 59 | "devDependencies": { 60 | "c8": "^7.10.0", 61 | "esbuild": "^0.21.0", 62 | "eslint": "^8.4.1", 63 | "mocha": "^9.1.3", 64 | "rollup": "^2.79.2" 65 | }, 66 | "engines": { 67 | "node": "^12.20.0 || ^14.13.0 || >=15.0.0", 68 | "npm": ">=7.0.0" 69 | }, 70 | "files": [ 71 | "bin", 72 | "cjs", 73 | "dist", 74 | "lib" 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const resolve = require('@rollup/plugin-node-resolve'); 2 | const commonjs = require('@rollup/plugin-commonjs'); 3 | const json = require('@rollup/plugin-json'); 4 | const { terser } = require('rollup-plugin-terser'); 5 | 6 | module.exports = { 7 | input: 'lib/validate.js', 8 | output: [ 9 | { name: 'csstreeValidator', format: 'umd', file: 'dist/csstree-validator.js' } 10 | ], 11 | plugins: [ 12 | resolve({ browser: true }), 13 | commonjs(), 14 | json(), 15 | terser() 16 | ] 17 | }; 18 | -------------------------------------------------------------------------------- /scripts/bundle.js: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'fs'; 2 | import path from 'path'; 3 | import esbuild from 'esbuild'; 4 | import { createRequire } from 'module'; 5 | 6 | const { version } = createRequire(import.meta.url)('../package.json'); 7 | 8 | async function build() { 9 | const genModules = { 10 | 'version.js': `export const version = "${version}";`, 11 | 'version.cjs': `exports.version = "${version}";` 12 | }; 13 | const genModulesFilter = new RegExp('lib[\\\\/](' + Object.keys(genModules).join('|').replace(/\./g, '\\.') + ')$'); 14 | const plugins = [{ 15 | name: 'replace', 16 | setup({ onLoad }) { 17 | onLoad({ filter: genModulesFilter }, args => ({ 18 | contents: genModules[path.basename(args.path)] 19 | })); 20 | } 21 | }]; 22 | 23 | await Promise.all([ 24 | esbuild.build({ 25 | entryPoints: ['lib/bundle.js'], 26 | outfile: 'dist/csstree-validator.js', 27 | format: 'iife', 28 | globalName: 'csstreeValidator', 29 | bundle: true, 30 | minify: true, 31 | logLevel: 'info', 32 | plugins 33 | }), 34 | 35 | esbuild.build({ 36 | entryPoints: ['lib/bundle.js'], 37 | outfile: 'dist/csstree-validator.esm.js', 38 | format: 'esm', 39 | bundle: true, 40 | minify: true, 41 | logLevel: 'info', 42 | plugins 43 | }) 44 | ]); 45 | 46 | for (const [key, value] of Object.entries(genModules)) { 47 | const fn = path.basename(key); 48 | 49 | writeFileSync(`dist/${fn}`, value); 50 | } 51 | } 52 | 53 | build(); 54 | -------------------------------------------------------------------------------- /scripts/esm-to-cjs.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { rollup } from 'rollup'; 4 | 5 | const external = [ 6 | 'fs', 7 | 'path', 8 | 'assert', 9 | 'child_process', 10 | 'clap', 11 | 'enhanced-resolve', 12 | 'css-tree', 13 | 'csstree-validator' 14 | ]; 15 | 16 | function removeCreateRequire(id) { 17 | return fs.readFileSync(id, 'utf8') 18 | .replace(/import .+ from 'module';/, '') 19 | .replace(/const require = .+;/, ''); 20 | } 21 | 22 | function replaceContent(map) { 23 | return { 24 | name: 'file-content-replacement', 25 | load(id) { 26 | const key = path.relative('', id); 27 | 28 | if (map.hasOwnProperty(key)) { 29 | return map[key](id); 30 | } 31 | } 32 | }; 33 | } 34 | 35 | function readDir(dir) { 36 | return fs.readdirSync(dir) 37 | .filter(fn => fn.endsWith('.js')) 38 | .map(fn => `${dir}/${fn}`); 39 | } 40 | 41 | async function build(outputDir, ...entryPoints) { 42 | const startTime = Date.now(); 43 | 44 | console.log(); 45 | console.log(`Convert ESM to CommonJS (output: ${outputDir})`); 46 | 47 | const res = await rollup({ 48 | external, 49 | input: entryPoints, 50 | plugins: [ 51 | replaceContent({ 52 | 'lib/version.js': removeCreateRequire 53 | }) 54 | ] 55 | }); 56 | await res.write({ 57 | dir: outputDir, 58 | entryFileNames: '[name].cjs', 59 | format: 'cjs', 60 | exports: 'auto', 61 | preserveModules: true, 62 | interop: false, 63 | esModule: false, 64 | generatedCode: { 65 | constBindings: true 66 | } 67 | }); 68 | await res.close(); 69 | 70 | console.log(`Done in ${Date.now() - startTime}ms`); 71 | } 72 | 73 | async function buildAll() { 74 | await build('./cjs', 'lib/bundle.js', 'lib/cli.js', 'lib/index.js'); 75 | await build('./cjs-test', ...readDir('test')); 76 | } 77 | 78 | buildAll(); 79 | -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- 1 | import assert, { deepStrictEqual, strictEqual } from 'assert'; 2 | import path from 'path'; 3 | import { readFileSync } from 'fs'; 4 | import { spawn } from 'child_process'; 5 | 6 | const { version } = JSON.parse(readFileSync('./package.json')); 7 | const cmd = path.resolve('./bin/validate.js'); 8 | 9 | function fixturePath(filepath) { 10 | return path.join(path.resolve('fixtures'), filepath); 11 | } 12 | 13 | function fixtureContent(filepath) { 14 | return readFileSync(fixturePath(filepath), 'utf-8').trim(); 15 | } 16 | 17 | function assertOutput(actual, expected) { 18 | if (typeof expected === 'function') { 19 | expected(actual.trim()); 20 | } else if (typeof expected === 'string') { 21 | strictEqual(actual.trim(), expected); 22 | } else if (expected instanceof RegExp) { 23 | assert.match(actual.trim(), expected); 24 | } else { 25 | deepStrictEqual(JSON.parse(actual), expected); 26 | } 27 | } 28 | 29 | function run(...cliArgs) { 30 | let stderr = ''; 31 | let stdout = ''; 32 | const options = typeof cliArgs[0] === 'object' ? cliArgs.shift() : null; 33 | const args = [cmd, ...cliArgs]; 34 | const child = spawn('node', args, { stdio: 'pipe', ...options }); 35 | const wrapper = new Promise((resolve, reject) => { 36 | child.once('close', (code) => 37 | code > 1 ? reject(new Error(stderr)) : resolve({ stdout, stderr }) 38 | ); 39 | }); 40 | 41 | child.stderr.on('data', chunk => stderr += chunk); 42 | child.stdout.on('data', chunk => stdout += chunk); 43 | 44 | wrapper.stdin = (data) => { 45 | child.stdin.write(data); 46 | child.stdin.end(); 47 | return wrapper; 48 | }; 49 | wrapper.stdout = expected => 50 | Object.assign(wrapper.then(({ stdout: actual }) => 51 | assertOutput(actual, expected) 52 | ), { stderr: wrapper.stderr }); 53 | wrapper.stderr = expected => 54 | Object.assign(wrapper.then(({ stderr: actual }) => 55 | assertOutput(actual, expected) 56 | ), { stdout: wrapper.stdout }); 57 | 58 | return wrapper; 59 | } 60 | 61 | describe('cli', () => { 62 | it('should output version', () => 63 | run('-v') 64 | .stdout(version) 65 | ); 66 | 67 | it('should output help', () => 68 | run('-h') 69 | .stdout(/Usage:/) 70 | ); 71 | 72 | it('should read content from stdin if no file specified', () => 73 | run() 74 | .stdin(fixtureContent('css/style.css')) 75 | .stderr(fixtureContent('css/style.validate-result') 76 | .replace(/^#.+\n/, '# \n') 77 | ) 78 | ); 79 | 80 | it('should read from file', () => 81 | run(path.relative(process.cwd(), fixturePath('css/style.css'))) 82 | .stderr(fixtureContent('css/style.validate-result')) 83 | ); 84 | 85 | it('should error when wrong reporter', () => 86 | run(path.relative(process.cwd(), fixturePath('css/style.css')), '--reporter', 'bad-value') 87 | .stderr('Wrong value for reporter: bad-value') 88 | ); 89 | 90 | it('should error when file doesn\'t exist', () => 91 | run('not/exists.css') 92 | .stderr('ERROR! No such file or directory: not/exists.css') 93 | ); 94 | 95 | describe('custom reporter', () => { 96 | const cwd = path.resolve('fixtures/custom-reporter'); 97 | const tests = { 98 | // module 99 | 'ESM module': 'custom-reporter.js', 100 | 'commonjs module': 'custom-reporter.cjs', 101 | 102 | // package 103 | 'commonjs package': 'commonjs', 104 | 'commonjs package (path to dir)': 'commonjs/lib', 105 | 'commonjs package (full path)': 'commonjs/lib/index.js', 106 | 'esm package': 'esm', 107 | 'esm package (full path)': 'esm/lib/index.js', 108 | 'dual package': 'dual', 109 | 'dual package (full path)': 'dual/lib/index.js', 110 | 'dual package (full path to cjs)': 'dual/lib/index.cjs' 111 | }; 112 | 113 | for (const [title, reporter] of Object.entries(tests)) { 114 | it(title, () => 115 | run({ cwd }, 'style.css', '-r', reporter) 116 | .stderr('OK') 117 | ); 118 | } 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /test/loc.js: -------------------------------------------------------------------------------- 1 | import { strictEqual, deepStrictEqual } from 'assert'; 2 | import { validateString } from 'csstree-validator'; 3 | 4 | describe('locations', () => { 5 | it('result should contain correct parse error location', () => { 6 | const error = validateString('.broken {\n a;\n}', 'test').test[0]; 7 | 8 | strictEqual(error.line, 2, 'line'); 9 | strictEqual(error.column, 4, 'column'); 10 | strictEqual(error.offset, 13, 'offset'); 11 | }); 12 | 13 | it('result should contain correct location of unknown property', () => { 14 | const error = validateString('.broken {\n abc: 1;\n}', 'test').test[0]; 15 | 16 | strictEqual(error.message, 'Unknown property `abc`'); 17 | strictEqual(error.line, 2); 18 | strictEqual(error.column, 3); 19 | }); 20 | 21 | it('result should contain correct location of mismatch', () => { 22 | const error = validateString('.broken {\n color: rgb(1, green, 3);\n}', 'test').test[0]; 23 | 24 | strictEqual(error.message, 'Invalid value for `color` property'); 25 | strictEqual(error.line, 2); 26 | strictEqual(error.column, 17); 27 | }); 28 | 29 | it('result should contain correct location of uncomplete mismatch', () => { 30 | const error = validateString('.broken {\n border: red 1xx solid;\n}', 'test').test[0]; 31 | 32 | strictEqual(error.message, 'Invalid value for `border` property'); 33 | strictEqual(error.line, 2); 34 | strictEqual(error.column, 15); 35 | }); 36 | 37 | it('should not warn on custom properties', () => { 38 | const error = validateString('.broken { --foo: 123 }', 'test').test; 39 | 40 | deepStrictEqual(error, []); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/reporters.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { strictEqual } from 'assert'; 3 | import { validateDictionary, reporters } from 'csstree-validator'; 4 | 5 | const input = { 6 | 'parse.css': 'foo { boom! } bar { color: red green; }', 7 | 'match.css': '.warn { color: 123; border: 1px unknown red; unknown: yep; --custom: property }' 8 | }; 9 | 10 | function createReporterTest(name, reporter) { 11 | it(name, () => { 12 | const expected = readFileSync('./fixtures/reporter/' + name, 'utf8').trim(); 13 | const actual = reporter(validateDictionary(input)).trim(); 14 | 15 | strictEqual(actual, expected); 16 | }); 17 | } 18 | 19 | describe('test reporter output', () => { 20 | for (const name in reporters) { 21 | createReporterTest(name, reporters[name]); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /test/validate.js: -------------------------------------------------------------------------------- 1 | import { strictEqual, deepStrictEqual } from 'assert'; 2 | import { parse } from 'css-tree'; 3 | import { validate } from 'csstree-validator'; 4 | 5 | function assertIsObject(value, message) { 6 | strictEqual(typeof value === 'object' && value !== null, true, message); 7 | } 8 | 9 | function assertPosition(actual, expected, type) { 10 | assertIsObject(actual, `${type} on error must be an object`); 11 | deepStrictEqual(actual, expected, `${type} on error`); 12 | } 13 | 14 | function assertError(css, expectedLoc, expectedMsg, locIsOptional) { 15 | const res = validate(css); 16 | 17 | strictEqual(Array.isArray(res), true, 'should return an array of errors'); 18 | strictEqual(res.length > 0, true, 'should return errors'); 19 | 20 | const entry = res[0]; 21 | 22 | deepStrictEqual(entry.message, expectedMsg); 23 | 24 | if (typeof expectedLoc === 'string') { 25 | const { offset, line, column } = entry; 26 | const locMatch = expectedLoc.match(/\S+/); 27 | const expectedStartOffset = locMatch.index - (css.slice(0, expectedLoc.length - 1).match(/\n/g) || []).length; 28 | const expectedEndOffset = Math.min(expectedStartOffset + locMatch[0].length, css.length); 29 | const startLines = css.slice(0, expectedStartOffset).split(/\n/); 30 | const middleLines = css.slice(expectedStartOffset, expectedEndOffset).split(/\n/); 31 | const expectedStart = { 32 | offset: expectedStartOffset, 33 | line: startLines.length, 34 | column: startLines.pop().length + 1 35 | }; 36 | const expectedEndLine = expectedStart.line + middleLines.length - 1; 37 | const expectedEnd = { 38 | offset: expectedEndOffset, 39 | line: expectedEndLine, 40 | column: (expectedEndLine === expectedStart.line ? expectedStart.column : -1) + 41 | middleLines.pop().length 42 | }; 43 | // const expected 44 | 45 | // console.log(entry); 46 | deepStrictEqual({ offset, line, column }, expectedStart, 'offset/line/column on error'); 47 | 48 | if (!locIsOptional || entry.loc) { 49 | assertIsObject(entry.loc, 'loc on error must be an object'); 50 | strictEqual(typeof entry.loc.source, 'string', 'loc.source must be a string'); 51 | assertPosition(entry.loc.start, expectedStart, 'start'); 52 | assertPosition(entry.loc.end, expectedEnd, 'end'); 53 | } 54 | } else { 55 | const { offset, line, column } = entry; 56 | 57 | deepStrictEqual({ offset, line, column }, expectedLoc); 58 | } 59 | } 60 | 61 | function assertOk(css) { 62 | deepStrictEqual(validate(css), []); 63 | } 64 | 65 | describe('validate functions', function() { 66 | it('validate() should take AST', () => { 67 | const ast = parse('.a {\n foo: 123;\n}', { positions: true }); 68 | 69 | assertError( 70 | ast, 71 | { offset: 7, line: 2, column: 3 }, 72 | 'Unknown property `foo`' 73 | ); 74 | }); 75 | 76 | describe('parse error', () => { 77 | it('somewhere in the input', () => { 78 | assertError( 79 | 'foo { boom! }', 80 | ' ^', 81 | 'Colon is expected', 82 | true 83 | ); 84 | }); 85 | it('at end of the input', () => { 86 | assertError( 87 | 'a', 88 | ' ^', 89 | '"{" is expected', 90 | true 91 | ); 92 | }); 93 | }); 94 | 95 | describe('declaration', () => { 96 | it('unknown property', () => 97 | assertError( 98 | '.a {\n foo: 123;\n}', 99 | ' ~~~', 100 | 'Unknown property `foo`' 101 | ) 102 | ); 103 | 104 | it('bad value', () => 105 | assertError( 106 | '.a {\n color: 123;\n}', 107 | ' ~~~', 108 | 'Invalid value for `color` property' 109 | ) 110 | ); 111 | 112 | it('bad value #2', () => 113 | assertError( 114 | '.a {\n color: red green;\n}', 115 | ' ~~~~~', 116 | 'Invalid value for `color` property' 117 | ) 118 | ); 119 | 120 | it('bad value #3', () => 121 | assertError( 122 | '.a {\n border: 1px unknown red;\n}', 123 | ' ~~~~~~~', 124 | 'Invalid value for `border` property' 125 | ) 126 | ); 127 | 128 | it('ok', () => 129 | assertOk('.a {\n color: green;\n width: calc(1px + 1%)!important\n}') 130 | ); 131 | }); 132 | 133 | describe('atrule', () => { 134 | it('unknown at-rule', () => 135 | assertError( 136 | '@a { color: green }', 137 | '~~', 138 | 'Unknown at-rule `@a`' 139 | ) 140 | ); 141 | 142 | it('at-rule should has no prelude', () => 143 | assertError( 144 | '@font-face xxx { color: green }', 145 | ' ~~~', 146 | 'At-rule `@font-face` should not contain a prelude' 147 | ) 148 | ); 149 | 150 | it('at-rule should has no prelude #2', () => 151 | assertError( 152 | '@font-face xxx yyy { color: green }', 153 | ' ~~~~~~~', 154 | 'At-rule `@font-face` should not contain a prelude' 155 | ) 156 | ); 157 | 158 | it('at-rule should has a prelude', () => 159 | assertError( 160 | '@document { color: green }', 161 | '~~~~~~~~~', 162 | 'At-rule `@document` should contain a prelude' 163 | ) 164 | ); 165 | 166 | it('bad value for at-rule prelude', () => 167 | assertError( 168 | '@document domain( foo /***/) { }', 169 | ' ~~~', 170 | 'Invalid value for `@document` prelude' 171 | ) 172 | ); 173 | 174 | it('ok at-rule prelude', () => 175 | assertOk('@document url(foo) { }') 176 | ); 177 | 178 | it('bad at-rule descriptor', () => 179 | assertError( 180 | '@font-face { color: green }', 181 | ' ~~~~~', 182 | 'Unknown at-rule descriptor `color`' 183 | ) 184 | ); 185 | 186 | it('bad at-rule descriptor value', () => 187 | assertError( 188 | '@font-face { font-display: foo bar }', 189 | ' ~~~', 190 | 'Invalid value for `font-display` descriptor' 191 | ) 192 | ); 193 | 194 | it('ok at-rule descriptor', () => 195 | assertOk('@font-face { font-display: swap }') 196 | ); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /test/validators.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { strictEqual, deepStrictEqual } from 'assert'; 3 | import { 4 | validateString, 5 | validateDictionary, 6 | validateFile, 7 | validatePath, 8 | validatePathList 9 | } from 'csstree-validator'; 10 | 11 | const fixturePath = './fixtures/css'; 12 | 13 | describe('validators', () => { 14 | describe('validateString', () => { 15 | it('should validate errors in CSS string', () => { 16 | const errors = validateString('foo { a: 1; color: bad; }', 'test'); 17 | 18 | strictEqual(Array.isArray(errors.test), true); 19 | strictEqual(errors.test.length, 2); 20 | }); 21 | 22 | it('filename should be optional', () => { 23 | const errors = validateString('foo {}'); 24 | 25 | deepStrictEqual(Object.keys(errors), ['']); 26 | }); 27 | }); 28 | 29 | it('validateDictionary', () => { 30 | const errors = validateDictionary({ 31 | 'foo': 'foo { a: 1; color: bad; }', 32 | 'bar': 'valid {}' 33 | }); 34 | 35 | deepStrictEqual(Object.keys(errors).sort(), ['bar', 'foo']); 36 | strictEqual(Array.isArray(errors.foo), true); 37 | strictEqual(errors.foo.length, 2); 38 | strictEqual(Array.isArray(errors.bar), true); 39 | strictEqual(errors.bar.length, 0); 40 | }); 41 | 42 | describe('validateFile', () => { 43 | it('should validate file content', () => { 44 | const filename = path.join(fixturePath, 'style.css'); 45 | const errors = validateFile(filename); 46 | 47 | deepStrictEqual(Object.keys(errors), [filename]); 48 | strictEqual(errors[filename].length, 2); 49 | deepStrictEqual( 50 | errors[filename].map(error => error.name), 51 | ['SyntaxReferenceError', 'SyntaxMatchError'] 52 | ); 53 | }); 54 | 55 | it('should not fail when file not found', () => { 56 | const filename = String(Math.random()); 57 | const errors = validateFile(filename); 58 | 59 | deepStrictEqual(Object.keys(errors), [filename]); 60 | strictEqual(errors[filename].length, 1); 61 | strictEqual(errors[filename][0].name, 'Error'); 62 | }); 63 | }); 64 | 65 | describe('validatePath', () => { 66 | it('should validate all files with .css extension on path', () => { 67 | const errors = validatePath(fixturePath); 68 | 69 | deepStrictEqual( 70 | Object.keys(errors) 71 | .map(filename => path.relative(fixturePath, filename)) 72 | .sort(), 73 | ['bar/style.css', 'foo/style.css', 'style.css'] 74 | ); 75 | 76 | Object.keys(errors).forEach((filename) => { 77 | strictEqual(errors[filename].length, 2); 78 | deepStrictEqual( 79 | errors[filename].map((error) => error.name), 80 | ['SyntaxReferenceError', 'SyntaxMatchError'] 81 | ); 82 | }); 83 | }); 84 | 85 | it('should validate all files that match shouldBeValidated on path', () => { 86 | const errors = validatePath( 87 | fixturePath, 88 | filename => path.basename(filename) === 'not.a.css.file' 89 | ); 90 | 91 | deepStrictEqual( 92 | Object.keys(errors) 93 | .map(filename => path.relative(fixturePath, filename)) 94 | .sort(), 95 | ['bar/not.a.css.file'] 96 | ); 97 | 98 | Object.keys(errors).forEach((filename) => { 99 | strictEqual(errors[filename].length, 2); 100 | deepStrictEqual( 101 | errors[filename].map((error) => error.name), 102 | ['SyntaxReferenceError', 'SyntaxMatchError'] 103 | ); 104 | }); 105 | }); 106 | 107 | it('should not fail when path is invalid', () => { 108 | const path = String(Math.random()); 109 | const errors = validatePath(path); 110 | 111 | deepStrictEqual(Object.keys(errors), [path]); 112 | strictEqual(errors[path].length, 1); 113 | strictEqual(errors[path][0].name, 'Error'); 114 | }); 115 | }); 116 | 117 | describe('validatePathList', () => { 118 | it('should validate all files with .css extension on paths', () => { 119 | const errors = validatePathList([ 120 | path.join(fixturePath, 'bar'), 121 | path.join(fixturePath, 'foo') 122 | ]); 123 | 124 | deepStrictEqual( 125 | Object.keys(errors) 126 | .map(filename => path.relative(fixturePath, filename)) 127 | .sort(), 128 | ['bar/style.css', 'foo/style.css'] 129 | ); 130 | 131 | Object.keys(errors).forEach((filename) => { 132 | strictEqual(errors[filename].length, 2); 133 | deepStrictEqual( 134 | errors[filename].map((error) => error.name), 135 | ['SyntaxReferenceError', 'SyntaxMatchError'] 136 | ); 137 | }); 138 | }); 139 | 140 | it('should validate all files that match shouldBeValidated on path', () => { 141 | const errors = validatePathList([ 142 | path.join(fixturePath, 'bar'), 143 | path.join(fixturePath, 'foo') 144 | ], filename => path.basename(filename) === 'not.a.css.file'); 145 | 146 | deepStrictEqual( 147 | Object.keys(errors) 148 | .map((filename) => path.relative(fixturePath, filename)) 149 | .sort(), 150 | ['bar/not.a.css.file'] 151 | ); 152 | 153 | Object.keys(errors).forEach((filename) => { 154 | strictEqual(errors[filename].length, 2); 155 | deepStrictEqual( 156 | errors[filename].map((error) => error.name), 157 | ['SyntaxReferenceError', 'SyntaxMatchError'] 158 | ); 159 | }); 160 | }); 161 | 162 | it('should not fail when path is invalid', () => { 163 | const validPath = path.join(fixturePath, 'bar'); 164 | const invalidPath = Math.random(); 165 | const errors = validatePathList([ 166 | validPath, 167 | invalidPath 168 | ]); 169 | 170 | deepStrictEqual(Object.keys(errors), [ 171 | String(invalidPath), 172 | path.join(validPath, 'style.css') 173 | ]); 174 | strictEqual(errors[path.join(validPath, 'style.css')].length, 2); 175 | strictEqual(errors[invalidPath].length, 1); 176 | strictEqual(errors[invalidPath][0].name, 'TypeError'); 177 | }); 178 | }); 179 | }); 180 | --------------------------------------------------------------------------------