├── .gitignore ├── husky.config.js ├── lint-staged.config.js ├── .eslintrc ├── .editorconfig ├── LICENSE ├── package.json ├── .github └── workflows │ └── nodejs.yml ├── README.md ├── CHANGELOG.md ├── src └── index.js └── test └── index.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | .nyc_output 4 | coverage 5 | node_modules 6 | 7 | -------------------------------------------------------------------------------- /husky.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hooks: { 3 | "pre-commit": "lint-staged", 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*.js": ["eslint --fix", "prettier --write"], 3 | "*.{json,md,yml,css,ts}": ["prettier --write"], 4 | }; 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2018 4 | }, 5 | "env": { 6 | "es6": true, 7 | "node": true, 8 | "jest": true 9 | }, 10 | "extends": ["eslint:recommended", "prettier"] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2015 Mark Dalgleish 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-modules-local-by-default", 3 | "version": "4.2.0", 4 | "description": "A CSS Modules transform to make local scope the default", 5 | "main": "src/index.js", 6 | "author": "Mark Dalgleish", 7 | "license": "MIT", 8 | "files": [ 9 | "src" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/css-modules/postcss-modules-local-by-default.git" 14 | }, 15 | "engines": { 16 | "node": "^10 || ^12 || >= 14" 17 | }, 18 | "keywords": [ 19 | "css-modules", 20 | "postcss", 21 | "css", 22 | "postcss-plugin" 23 | ], 24 | "scripts": { 25 | "prettier": "prettier -l --ignore-path .gitignore .", 26 | "eslint": "eslint --ignore-path .gitignore .", 27 | "lint": "yarn eslint && yarn prettier", 28 | "test:only": "jest", 29 | "test:watch": "jest --watch", 30 | "test:coverage": "jest --coverage --collectCoverageFrom=\"src/**/*\"", 31 | "test": "yarn test:coverage", 32 | "prepublishOnly": "yarn lint && yarn test" 33 | }, 34 | "dependencies": { 35 | "icss-utils": "^5.0.0", 36 | "postcss-selector-parser": "^7.0.0", 37 | "postcss-value-parser": "^4.1.0" 38 | }, 39 | "devDependencies": { 40 | "coveralls": "^3.1.0", 41 | "eslint": "^7.10.0", 42 | "eslint-config-prettier": "^6.12.0", 43 | "husky": "^4.3.0", 44 | "jest": "^26.5.2", 45 | "lint-staged": "^10.4.0", 46 | "postcss": "^8.1.0", 47 | "prettier": "^2.1.2" 48 | }, 49 | "peerDependencies": { 50 | "postcss": "^8.1.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - next 8 | pull_request: 9 | branches: 10 | - master 11 | - next 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | lint: 18 | name: Lint - ${{ matrix.os }} - Node v${{ matrix.node-version }} 19 | 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest] 26 | node-version: ["lts/*"] 27 | 28 | runs-on: ${{ matrix.os }} 29 | 30 | concurrency: 31 | group: lint-${{ matrix.os }}-v${{ matrix.node-version }}-${{ github.ref }} 32 | cancel-in-progress: true 33 | 34 | steps: 35 | - uses: actions/checkout@v4 36 | with: 37 | fetch-depth: 0 38 | 39 | - name: Use Node.js ${{ matrix.node-version }} 40 | uses: actions/setup-node@v4 41 | with: 42 | node-version: ${{ matrix.node-version }} 43 | architecture: "x64" 44 | cache: "yarn" 45 | cache-dependency-path: "yarn.lock" 46 | 47 | - name: Install dependencies 48 | run: yarn --frozen-lockfile 49 | 50 | - name: Lint 51 | run: yarn lint 52 | 53 | test: 54 | name: Test - ${{ matrix.os }} - Node v${{ matrix.node-version }} 55 | 56 | strategy: 57 | matrix: 58 | os: [ubuntu-latest, windows-latest, macos-latest] 59 | node-version: ["10", "12", "14", "16", "18", "20"] 60 | webpack-version: [latest] 61 | 62 | runs-on: ${{ matrix.os }} 63 | 64 | concurrency: 65 | group: test-${{ matrix.os }}-v${{ matrix.node-version }}-${{ github.ref }} 66 | cancel-in-progress: true 67 | 68 | steps: 69 | - name: Setup Git 70 | if: matrix.os == 'windows-latest' 71 | run: git config --global core.autocrlf input 72 | 73 | - uses: actions/checkout@v4 74 | 75 | - name: Use Node.js ${{ matrix.node-version }} 76 | uses: actions/setup-node@v4 77 | with: 78 | node-version: "${{ matrix.node-version }}" 79 | architecture: "x64" 80 | cache: "yarn" 81 | cache-dependency-path: "yarn.lock" 82 | 83 | - name: Install dependencies 84 | run: yarn --frozen-lockfile 85 | 86 | - name: Run tests for webpack version ${{ matrix.webpack-version }} 87 | run: yarn test --ci 88 | 89 | - name: Submit coverage data to codecov 90 | uses: codecov/codecov-action@v3 91 | with: 92 | token: ${{ secrets.CODECOV_TOKEN }} 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status][ci-img]][ci] [![codecov][codecov-img]][codecov] [![npm][npm-img]][npm] 2 | 3 | # CSS Modules: Local by Default 4 | 5 | Transformation examples: 6 | 7 | Selectors (mode `local`, by default):: 8 | 9 | 10 | ```css 11 | .foo { ... } /* => */ :local(.foo) { ... } 12 | 13 | .foo .bar { ... } /* => */ :local(.foo) :local(.bar) { ... } 14 | 15 | /* Shorthand global selector */ 16 | 17 | :global .foo .bar { ... } /* => */ .foo .bar { ... } 18 | 19 | .foo :global .bar { ... } /* => */ :local(.foo) .bar { ... } 20 | 21 | /* Targeted global selector */ 22 | 23 | :global(.foo) .bar { ... } /* => */ .foo :local(.bar) { ... } 24 | 25 | .foo:global(.bar) { ... } /* => */ :local(.foo).bar { ... } 26 | 27 | .foo :global(.bar) .baz { ... } /* => */ :local(.foo) .bar :local(.baz) { ... } 28 | 29 | .foo:global(.bar) .baz { ... } /* => */ :local(.foo).bar :local(.baz) { ... } 30 | ``` 31 | 32 | 33 | Declarations (mode `local`, by default): 34 | 35 | 36 | ```css 37 | .foo { 38 | animation-name: fadeInOut, global(moveLeft300px), local(bounce); 39 | } 40 | 41 | .bar { 42 | animation: rotate 1s, global(spin) 3s, local(fly) 6s; 43 | } 44 | 45 | /* => */ 46 | 47 | :local(.foo) { 48 | animation-name: :local(fadeInOut), moveLeft300px, :local(bounce); 49 | } 50 | 51 | :local(.bar) { 52 | animation: :local(rotate) 1s, spin 3s, :local(fly) 6s; 53 | } 54 | ``` 55 | 56 | 57 | ## Pure Mode 58 | 59 | In pure mode, all selectors must contain at least one local class or id 60 | selector 61 | 62 | To ignore this rule for a specific selector, add the a `/* cssmodules-pure-ignore */` comment in front 63 | of the selector: 64 | 65 | ```css 66 | /* cssmodules-pure-ignore */ 67 | :global(#modal-backdrop) { 68 | ...; 69 | } 70 | ``` 71 | 72 | or by adding a `/* cssmodules-pure-no-check */` comment at the top of a file to disable this check for the whole file: 73 | 74 | ```css 75 | /* cssmodules-pure-no-check */ 76 | 77 | :global(#modal-backdrop) { 78 | ...; 79 | } 80 | 81 | :global(#my-id) { 82 | ...; 83 | } 84 | ``` 85 | 86 | ## Building 87 | 88 | ```bash 89 | $ npm install 90 | $ npm test 91 | ``` 92 | 93 | - Build: [![Build Status][ci-img]][ci] 94 | - Lines: [![coveralls][coveralls-img]][coveralls] 95 | - Statements: [![codecov][codecov-img]][codecov] 96 | 97 | ## Development 98 | 99 | ```bash 100 | $ yarn test:watch 101 | ``` 102 | 103 | ## License 104 | 105 | MIT 106 | 107 | ## With thanks 108 | 109 | - [Tobias Koppers](https://github.com/sokra) 110 | - [Glen Maddern](https://github.com/geelen) 111 | 112 | --- 113 | 114 | Mark Dalgleish, 2015. 115 | 116 | [ci-img]: https://github.com/css-modules/postcss-modules-local-by-default/actions/workflows/nodejs.yml/badge.svg 117 | [ci]: https://github.com/css-modules/postcss-modules-local-by-default/actions/workflows/nodejs.yml 118 | [npm-img]: https://img.shields.io/npm/v/postcss-modules-local-by-default.svg?style=flat-square 119 | [npm]: https://www.npmjs.com/package/postcss-modules-local-by-default 120 | [coveralls-img]: https://img.shields.io/coveralls/css-modules/postcss-modules-local-by-default/master.svg?style=flat-square 121 | [coveralls]: https://coveralls.io/r/css-modules/postcss-modules-local-by-default?branch=master 122 | [codecov-img]: https://img.shields.io/codecov/c/github/css-modules/postcss-modules-local-by-default/master.svg?style=flat-square 123 | [codecov]: https://codecov.io/github/css-modules/postcss-modules-local-by-default?branch=master 124 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [4.2.0](https://github.com/postcss-modules-local-by-default/compare/v4.1.1...v4.2.0) - 2024-12-11 7 | 8 | - feat: add support a `/* cssmodules-pure-no-check */` comment 9 | 10 | ## [4.1.0](https://github.com/postcss-modules-local-by-default/compare/v4.0.5...v4.1.1) - 2024-11-11 11 | 12 | - feat: add `global()` and `local()` for animations 13 | - feat: add pure ignore comment 14 | - fix: css nesting and pure mode 15 | 16 | ## [4.0.5](https://github.com/postcss-modules-local-by-default/compare/v4.0.4...v4.0.5) - 2024-04-03 17 | 18 | ### Fixes 19 | 20 | - don't break the `@scope` at-rule without params 21 | 22 | ## [4.0.4](https://github.com/postcss-modules-local-by-default/compare/v4.0.3...v4.0.4) - 2024-01-17 23 | 24 | ### Fixes 25 | 26 | - handle `@scope` at-rule 27 | - css nesting 28 | - do not tread negative values as identifiers in the animation shorthand 29 | 30 | ## [4.0.3](https://github.com/postcss-modules-local-by-default/compare/v4.0.2...v4.0.3) - 2023-05-23 31 | 32 | ### Fixes 33 | 34 | - fix: do not localize `animation-name` property with var and env functions 35 | 36 | ## [4.0.2](https://github.com/postcss-modules-local-by-default/compare/v4.0.1...v4.0.2) - 2023-05-23 37 | 38 | ### Fixes 39 | 40 | - don't handle identifiers in nested function for the `animation` property 41 | 42 | ## [4.0.1](https://github.com/postcss-modules-local-by-default/compare/v4.0.0...v4.0.1) - 2023-05-19 43 | 44 | ### Fixes 45 | 46 | - don't handle global values in `animation` and `animation-name` properties 47 | - handle all possible identifiers in `animation` and `animation-name` properties 48 | - fix bug with nested `:global` and `:local` in pseudo selectors 49 | 50 | ## [4.0.0](https://github.com/postcss-modules-local-by-default/compare/v4.0.0-rc.4...v4.0.0) - 2020-10-13 51 | 52 | ### Fixes 53 | 54 | - compatibility with plugins other plugins 55 | 56 | ## [4.0.0-rc.4](https://github.com/postcss-modules-local-by-default/compare/v4.0.0-rc.3...v4.0.0-rc.4) - 2020-10-11 57 | 58 | ### Fixes 59 | 60 | - compatibility with plugins other plugins 61 | 62 | ## [4.0.0-rc.3](https://github.com/postcss-modules-local-by-default/compare/v4.0.0-rc.2...v4.0.0-rc.3) - 2020-10-08 63 | 64 | ### Fixes 65 | 66 | - compatibility with plugins other plugins 67 | 68 | ## [4.0.0-rc.2](https://github.com/postcss-modules-local-by-default/compare/v4.0.0-rc.1...v4.0.0-rc.2) - 2020-10-08 69 | 70 | ### BREAKING CHANGE 71 | 72 | - minimum supported `postcss` version is `^8.1.0` 73 | 74 | ### Fixes 75 | 76 | - minimum supported `Node.js` version is `^10 || ^12 || >= 14` 77 | - compatibility with PostCSS 8 78 | 79 | ## [4.0.0-rc.1](https://github.com/postcss-modules-local-by-default/compare/v4.0.0-rc.0...v4.0.0-rc.1) - 2020-09-22 80 | 81 | ### BREAKING CHANGE 82 | 83 | - update `icss-utils` for PostCSS 8 compatibility 84 | 85 | ## [4.0.0-rc.0](https://github.com/postcss-modules-local-by-default/compare/v3.0.3...4.0.0-rc.0) - 2020-09-18 86 | 87 | ### BREAKING CHANGE 88 | 89 | - minimum supported `Node.js` version is `>= 10.13.0 || >= 12.13.0 || >= 14` 90 | - minimum supported `postcss` version is `^8.0.3` 91 | - `postcss` was moved to `peerDependencies`, you need to install `postcss` in your project before use the plugin 92 | 93 | ## [3.0.3](https://github.com/postcss-modules-local-by-default/compare/v3.0.2...v3.0.3) - 2020-07-25 94 | 95 | ### Fixed 96 | 97 | - treat `:import` and `:export` statements as pure 98 | 99 | ## [3.0.2](https://github.com/postcss-modules-local-by-default/compare/v3.0.1...v3.0.2) - 2019-06-05 100 | 101 | ### Fixed 102 | 103 | - better handle invalid syntax 104 | 105 | ## [3.0.1](https://github.com/postcss-modules-local-by-default/compare/v3.0.0...v3.0.1) - 2019-05-16 106 | 107 | ### Fixed 108 | 109 | - adds safety check before accessing "rule parent" 110 | 111 | ## [3.0.0](https://github.com/postcss-modules-local-by-default/compare/v2.0.6...v3.0.0) - 2019-05-07 112 | 113 | ### Features 114 | 115 | - don't localize imported values in selectors 116 | 117 | ### Changes 118 | 119 | - don't localize imported values in selectors 120 | 121 | ## [2.0.6](https://github.com/postcss-modules-local-by-default/compare/v2.0.5...v2.0.6) - 2019-03-05 122 | 123 | ### Fixed 124 | 125 | - handles properly selector with escaping characters (like: `.\31 a2b3c { color: red }`) 126 | 127 | ## [2.0.5](https://github.com/postcss-modules-local-by-default/compare/v2.0.4...v2.0.5) - 2019-02-06 128 | 129 | ### Fixed 130 | 131 | - Path to `index.js` 132 | 133 | ## [2.0.4](https://github.com/postcss-modules-local-by-default/compare/v2.0.3...v2.0.4) - 2019-01-04 134 | 135 | ### Fixed 136 | 137 | - Inappropriate modification of `steps` function arguments 138 | 139 | ## [2.0.3](https://github.com/postcss-modules-local-by-default/compare/v2.0.2...v2.0.3) - 2018-12-21 140 | 141 | ### Fixed 142 | 143 | - Don't modify inappropriate animation keywords 144 | 145 | ## [2.0.2](https://github.com/postcss-modules-local-by-default/compare/v2.0.1...v2.0.2) - 2018-12-05 146 | 147 | ### Fixed 148 | 149 | - Don't break unicode characters. 150 | 151 | ## [2.0.1](https://github.com/postcss-modules-local-by-default/compare/v2.0.0...v2.0.1) - 2018-11-23 152 | 153 | ### Fixed 154 | 155 | - Handle uppercase `keyframes` at rule. 156 | 157 | ## [2.0.0](https://github.com/postcss-modules-local-by-default/compare/v1.3.1...v2.0.0) - 2018-11-23 158 | 159 | ### Changed 160 | 161 | - Drop support `nodejs@4`. 162 | - Update `postcss` version to `7`. 163 | 164 | ## [0.0.11](https://github.com/postcss-modules-local-by-default/compare/v0.0.10...v0.0.11) - 2015-07-19 165 | 166 | ### Fixed 167 | 168 | - Localisation of animation properties. 169 | 170 | ## [0.0.10](https://github.com/postcss-modules-local-by-default/compare/v0.0.9...v0.0.10) - 2015-06-17 171 | 172 | ### Added 173 | 174 | - Localised at-rules. 175 | 176 | ## [0.0.9](https://github.com/postcss-modules-local-by-default/compare/v0.0.8...v0.0.9) - 2015-06-12 177 | 178 | ### Changed 179 | 180 | - Using global selectors outside of a global context no longer triggers warnings. Instead, this functionality will be provided by a CSS Modules linter. 181 | 182 | ### Fixed 183 | 184 | - Keyframe rules. 185 | 186 | ## [0.0.8](https://github.com/postcss-modules-local-by-default/compare/v0.0.7...v0.0.8) - 2015-06-11 187 | 188 | ### Added 189 | 190 | - Pure mode where only local scope is allowed. 191 | 192 | ### Changed 193 | 194 | - Using global selectors outside of a global context now triggers warnings. 195 | 196 | ## [0.0.7](https://github.com/postcss-modules-local-by-default/compare/v0.0.6...v0.0.7) - 2015-05-30 197 | 198 | ### Changed 199 | 200 | - Migrated to `css-selector-tokenizer`. 201 | 202 | ## [0.0.6](https://github.com/postcss-modules-local-by-default/compare/v0.0.5...v0.0.6) - 2015-05-28 203 | 204 | ### Changed 205 | 206 | - Renamed project to `postcss-modules-local-by-default`. 207 | 208 | ## [0.0.5](https://github.com/postcss-modules-local-by-default/compare/v0.0.4...v0.0.5) - 2015-05-22 209 | 210 | ### Added 211 | 212 | - Support for css-loader [inheritance](https://github.com/webpack/css-loader#inheriting) and [local imports](https://github.com/webpack/css-loader#importing-local-class-names). 213 | 214 | ## [0.0.4](https://github.com/postcss-modules-local-by-default/compare/v0.0.3...v0.0.4) - 2015-05-22 215 | 216 | ### Changed 217 | 218 | - Hide global leak detection behind undocumented `lint` option until it's more robust. 219 | 220 | ## [0.0.3](https://github.com/postcss-modules-local-by-default/compare/v0.0.2...v0.0.3) - 2015-05-22 221 | 222 | ### Changed 223 | 224 | - Transformer output now uses the new `:local(.identifier)` syntax. 225 | 226 | ### Added 227 | 228 | - Simple global leak detection. Non-local selectors like `input{}` and `[data-foobar]` now throw when not marked as global. 229 | 230 | ## [0.0.2](https://github.com/postcss-modules-local-by-default/compare/v0.0.1...v0.0.2) - 2015-05-14 231 | 232 | ### Added 233 | 234 | - Support for global selectors appended directly to locals, e.g. `.foo:global(.bar)` 235 | 236 | ## 0.0.1 - 2015-05-12 237 | 238 | ### Added 239 | 240 | - Automatic local classes 241 | - Explicit global selectors with `:global` 242 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const selectorParser = require("postcss-selector-parser"); 4 | const valueParser = require("postcss-value-parser"); 5 | const { extractICSS } = require("icss-utils"); 6 | 7 | const IGNORE_FILE_MARKER = "cssmodules-pure-no-check"; 8 | const IGNORE_NEXT_LINE_MARKER = "cssmodules-pure-ignore"; 9 | 10 | const isSpacing = (node) => node.type === "combinator" && node.value === " "; 11 | 12 | const isPureCheckDisabled = (root) => { 13 | for (const node of root.nodes) { 14 | if (node.type !== "comment") { 15 | return false; 16 | } 17 | if (node.text.trim().startsWith(IGNORE_FILE_MARKER)) { 18 | return true; 19 | } 20 | } 21 | return false; 22 | }; 23 | 24 | function getIgnoreComment(node) { 25 | if (!node.parent) { 26 | return; 27 | } 28 | const indexInParent = node.parent.index(node); 29 | for (let i = indexInParent - 1; i >= 0; i--) { 30 | const prevNode = node.parent.nodes[i]; 31 | if (prevNode.type === "comment") { 32 | if (prevNode.text.trimStart().startsWith(IGNORE_NEXT_LINE_MARKER)) { 33 | return prevNode; 34 | } 35 | } else { 36 | break; 37 | } 38 | } 39 | } 40 | 41 | function normalizeNodeArray(nodes) { 42 | const array = []; 43 | 44 | nodes.forEach((x) => { 45 | if (Array.isArray(x)) { 46 | normalizeNodeArray(x).forEach((item) => { 47 | array.push(item); 48 | }); 49 | } else if (x) { 50 | array.push(x); 51 | } 52 | }); 53 | 54 | if (array.length > 0 && isSpacing(array[array.length - 1])) { 55 | array.pop(); 56 | } 57 | return array; 58 | } 59 | 60 | const isPureSelectorSymbol = Symbol("is-pure-selector"); 61 | 62 | function localizeNode(rule, mode, localAliasMap) { 63 | const transform = (node, context) => { 64 | if (context.ignoreNextSpacing && !isSpacing(node)) { 65 | throw new Error("Missing whitespace after " + context.ignoreNextSpacing); 66 | } 67 | 68 | if (context.enforceNoSpacing && isSpacing(node)) { 69 | throw new Error("Missing whitespace before " + context.enforceNoSpacing); 70 | } 71 | 72 | let newNodes; 73 | 74 | switch (node.type) { 75 | case "root": { 76 | let resultingGlobal; 77 | 78 | context.hasPureGlobals = false; 79 | 80 | newNodes = node.nodes.map((n) => { 81 | const nContext = { 82 | global: context.global, 83 | lastWasSpacing: true, 84 | hasLocals: false, 85 | explicit: false, 86 | }; 87 | 88 | n = transform(n, nContext); 89 | 90 | if (typeof resultingGlobal === "undefined") { 91 | resultingGlobal = nContext.global; 92 | } else if (resultingGlobal !== nContext.global) { 93 | throw new Error( 94 | 'Inconsistent rule global/local result in rule "' + 95 | node + 96 | '" (multiple selectors must result in the same mode for the rule)' 97 | ); 98 | } 99 | 100 | if (!nContext.hasLocals) { 101 | context.hasPureGlobals = true; 102 | } 103 | 104 | return n; 105 | }); 106 | 107 | context.global = resultingGlobal; 108 | 109 | node.nodes = normalizeNodeArray(newNodes); 110 | break; 111 | } 112 | case "selector": { 113 | newNodes = node.map((childNode) => transform(childNode, context)); 114 | 115 | node = node.clone(); 116 | node.nodes = normalizeNodeArray(newNodes); 117 | break; 118 | } 119 | case "combinator": { 120 | if (isSpacing(node)) { 121 | if (context.ignoreNextSpacing) { 122 | context.ignoreNextSpacing = false; 123 | context.lastWasSpacing = false; 124 | context.enforceNoSpacing = false; 125 | return null; 126 | } 127 | context.lastWasSpacing = true; 128 | return node; 129 | } 130 | break; 131 | } 132 | case "pseudo": { 133 | let childContext; 134 | const isNested = !!node.length; 135 | const isScoped = node.value === ":local" || node.value === ":global"; 136 | const isImportExport = 137 | node.value === ":import" || node.value === ":export"; 138 | 139 | if (isImportExport) { 140 | context.hasLocals = true; 141 | // :local(.foo) 142 | } else if (isNested) { 143 | if (isScoped) { 144 | if (node.nodes.length === 0) { 145 | throw new Error(`${node.value}() can't be empty`); 146 | } 147 | 148 | if (context.inside) { 149 | throw new Error( 150 | `A ${node.value} is not allowed inside of a ${context.inside}(...)` 151 | ); 152 | } 153 | 154 | childContext = { 155 | global: node.value === ":global", 156 | inside: node.value, 157 | hasLocals: false, 158 | explicit: true, 159 | }; 160 | 161 | newNodes = node 162 | .map((childNode) => transform(childNode, childContext)) 163 | .reduce((acc, next) => acc.concat(next.nodes), []); 164 | 165 | if (newNodes.length) { 166 | const { before, after } = node.spaces; 167 | 168 | const first = newNodes[0]; 169 | const last = newNodes[newNodes.length - 1]; 170 | 171 | first.spaces = { before, after: first.spaces.after }; 172 | last.spaces = { before: last.spaces.before, after }; 173 | } 174 | 175 | node = newNodes; 176 | 177 | break; 178 | } else { 179 | childContext = { 180 | global: context.global, 181 | inside: context.inside, 182 | lastWasSpacing: true, 183 | hasLocals: false, 184 | explicit: context.explicit, 185 | }; 186 | newNodes = node.map((childNode) => { 187 | const newContext = { 188 | ...childContext, 189 | enforceNoSpacing: false, 190 | }; 191 | 192 | const result = transform(childNode, newContext); 193 | 194 | childContext.global = newContext.global; 195 | childContext.hasLocals = newContext.hasLocals; 196 | 197 | return result; 198 | }); 199 | 200 | node = node.clone(); 201 | node.nodes = normalizeNodeArray(newNodes); 202 | 203 | if (childContext.hasLocals) { 204 | context.hasLocals = true; 205 | } 206 | } 207 | break; 208 | 209 | //:local .foo .bar 210 | } else if (isScoped) { 211 | if (context.inside) { 212 | throw new Error( 213 | `A ${node.value} is not allowed inside of a ${context.inside}(...)` 214 | ); 215 | } 216 | 217 | const addBackSpacing = !!node.spaces.before; 218 | 219 | context.ignoreNextSpacing = context.lastWasSpacing 220 | ? node.value 221 | : false; 222 | 223 | context.enforceNoSpacing = context.lastWasSpacing 224 | ? false 225 | : node.value; 226 | 227 | context.global = node.value === ":global"; 228 | context.explicit = true; 229 | 230 | // because this node has spacing that is lost when we remove it 231 | // we make up for it by adding an extra combinator in since adding 232 | // spacing on the parent selector doesn't work 233 | return addBackSpacing 234 | ? selectorParser.combinator({ value: " " }) 235 | : null; 236 | } 237 | break; 238 | } 239 | case "id": 240 | case "class": { 241 | if (!node.value) { 242 | throw new Error("Invalid class or id selector syntax"); 243 | } 244 | 245 | if (context.global) { 246 | break; 247 | } 248 | 249 | const isImportedValue = localAliasMap.has(node.value); 250 | const isImportedWithExplicitScope = isImportedValue && context.explicit; 251 | 252 | if (!isImportedValue || isImportedWithExplicitScope) { 253 | const innerNode = node.clone(); 254 | innerNode.spaces = { before: "", after: "" }; 255 | 256 | node = selectorParser.pseudo({ 257 | value: ":local", 258 | nodes: [innerNode], 259 | spaces: node.spaces, 260 | }); 261 | 262 | context.hasLocals = true; 263 | } 264 | 265 | break; 266 | } 267 | case "nesting": { 268 | if (node.value === "&") { 269 | context.hasLocals = rule.parent[isPureSelectorSymbol]; 270 | } 271 | } 272 | } 273 | 274 | context.lastWasSpacing = false; 275 | context.ignoreNextSpacing = false; 276 | context.enforceNoSpacing = false; 277 | 278 | return node; 279 | }; 280 | 281 | const rootContext = { 282 | global: mode === "global", 283 | hasPureGlobals: false, 284 | }; 285 | 286 | rootContext.selector = selectorParser((root) => { 287 | transform(root, rootContext); 288 | }).processSync(rule, { updateSelector: false, lossless: true }); 289 | 290 | return rootContext; 291 | } 292 | 293 | function localizeDeclNode(node, context) { 294 | switch (node.type) { 295 | case "word": 296 | if (context.localizeNextItem) { 297 | if (!context.localAliasMap.has(node.value)) { 298 | node.value = ":local(" + node.value + ")"; 299 | context.localizeNextItem = false; 300 | } 301 | } 302 | break; 303 | 304 | case "function": 305 | if ( 306 | context.options && 307 | context.options.rewriteUrl && 308 | node.value.toLowerCase() === "url" 309 | ) { 310 | node.nodes.map((nestedNode) => { 311 | if (nestedNode.type !== "string" && nestedNode.type !== "word") { 312 | return; 313 | } 314 | 315 | let newUrl = context.options.rewriteUrl( 316 | context.global, 317 | nestedNode.value 318 | ); 319 | 320 | switch (nestedNode.type) { 321 | case "string": 322 | if (nestedNode.quote === "'") { 323 | newUrl = newUrl.replace(/(\\)/g, "\\$1").replace(/'/g, "\\'"); 324 | } 325 | 326 | if (nestedNode.quote === '"') { 327 | newUrl = newUrl.replace(/(\\)/g, "\\$1").replace(/"/g, '\\"'); 328 | } 329 | 330 | break; 331 | case "word": 332 | newUrl = newUrl.replace(/("|'|\)|\\)/g, "\\$1"); 333 | break; 334 | } 335 | 336 | nestedNode.value = newUrl; 337 | }); 338 | } 339 | break; 340 | } 341 | return node; 342 | } 343 | 344 | // `none` is special value, other is global values 345 | const specialKeywords = [ 346 | "none", 347 | "inherit", 348 | "initial", 349 | "revert", 350 | "revert-layer", 351 | "unset", 352 | ]; 353 | 354 | function localizeDeclarationValues(localize, declaration, context) { 355 | const valueNodes = valueParser(declaration.value); 356 | 357 | valueNodes.walk((node, index, nodes) => { 358 | if ( 359 | node.type === "function" && 360 | (node.value.toLowerCase() === "var" || node.value.toLowerCase() === "env") 361 | ) { 362 | return false; 363 | } 364 | 365 | if ( 366 | node.type === "word" && 367 | specialKeywords.includes(node.value.toLowerCase()) 368 | ) { 369 | return; 370 | } 371 | 372 | const subContext = { 373 | options: context.options, 374 | global: context.global, 375 | localizeNextItem: localize && !context.global, 376 | localAliasMap: context.localAliasMap, 377 | }; 378 | nodes[index] = localizeDeclNode(node, subContext); 379 | }); 380 | 381 | declaration.value = valueNodes.toString(); 382 | } 383 | 384 | // letter 385 | // An uppercase letter or a lowercase letter. 386 | // 387 | // ident-start code point 388 | // A letter, a non-ASCII code point, or U+005F LOW LINE (_). 389 | // 390 | // ident code point 391 | // An ident-start code point, a digit, or U+002D HYPHEN-MINUS (-). 392 | 393 | // We don't validate `hex digits`, because we don't need it, it is work of linters. 394 | const validIdent = 395 | /^-?([a-z\u0080-\uFFFF_]|(\\[^\r\n\f])|-(?![0-9]))((\\[^\r\n\f])|[a-z\u0080-\uFFFF_0-9-])*$/i; 396 | 397 | /* 398 | The spec defines some keywords that you can use to describe properties such as the timing 399 | function. These are still valid animation names, so as long as there is a property that accepts 400 | a keyword, it is given priority. Only when all the properties that can take a keyword are 401 | exhausted can the animation name be set to the keyword. I.e. 402 | 403 | animation: infinite infinite; 404 | 405 | The animation will repeat an infinite number of times from the first argument, and will have an 406 | animation name of infinite from the second. 407 | */ 408 | const animationKeywords = { 409 | // animation-direction 410 | $normal: 1, 411 | $reverse: 1, 412 | $alternate: 1, 413 | "$alternate-reverse": 1, 414 | // animation-fill-mode 415 | $forwards: 1, 416 | $backwards: 1, 417 | $both: 1, 418 | // animation-iteration-count 419 | $infinite: 1, 420 | // animation-play-state 421 | $paused: 1, 422 | $running: 1, 423 | // animation-timing-function 424 | $ease: 1, 425 | "$ease-in": 1, 426 | "$ease-out": 1, 427 | "$ease-in-out": 1, 428 | $linear: 1, 429 | "$step-end": 1, 430 | "$step-start": 1, 431 | // Special 432 | $none: Infinity, // No matter how many times you write none, it will never be an animation name 433 | // Global values 434 | $initial: Infinity, 435 | $inherit: Infinity, 436 | $unset: Infinity, 437 | $revert: Infinity, 438 | "$revert-layer": Infinity, 439 | }; 440 | 441 | function localizeDeclaration(declaration, context) { 442 | const isAnimation = /animation(-name)?$/i.test(declaration.prop); 443 | 444 | if (isAnimation) { 445 | let parsedAnimationKeywords = {}; 446 | const valueNodes = valueParser(declaration.value).walk((node) => { 447 | // If div-token appeared (represents as comma ','), a possibility of an animation-keywords should be reflesh. 448 | if (node.type === "div") { 449 | parsedAnimationKeywords = {}; 450 | 451 | return; 452 | } else if ( 453 | node.type === "function" && 454 | node.value.toLowerCase() === "local" && 455 | node.nodes.length === 1 456 | ) { 457 | node.type = "word"; 458 | node.value = node.nodes[0].value; 459 | 460 | return localizeDeclNode(node, { 461 | options: context.options, 462 | global: context.global, 463 | localizeNextItem: true, 464 | localAliasMap: context.localAliasMap, 465 | }); 466 | } else if (node.type === "function") { 467 | // replace `animation: global(example)` with `animation-name: example` 468 | if (node.value.toLowerCase() === "global" && node.nodes.length === 1) { 469 | node.type = "word"; 470 | node.value = node.nodes[0].value; 471 | } 472 | 473 | // Do not handle nested functions 474 | return false; 475 | } 476 | // Ignore all except word 477 | else if (node.type !== "word") { 478 | return; 479 | } 480 | 481 | const value = node.type === "word" ? node.value.toLowerCase() : null; 482 | 483 | let shouldParseAnimationName = false; 484 | 485 | if (value && validIdent.test(value)) { 486 | if ("$" + value in animationKeywords) { 487 | parsedAnimationKeywords["$" + value] = 488 | "$" + value in parsedAnimationKeywords 489 | ? parsedAnimationKeywords["$" + value] + 1 490 | : 0; 491 | 492 | shouldParseAnimationName = 493 | parsedAnimationKeywords["$" + value] >= 494 | animationKeywords["$" + value]; 495 | } else { 496 | shouldParseAnimationName = true; 497 | } 498 | } 499 | 500 | return localizeDeclNode(node, { 501 | options: context.options, 502 | global: context.global, 503 | localizeNextItem: shouldParseAnimationName && !context.global, 504 | localAliasMap: context.localAliasMap, 505 | }); 506 | }); 507 | 508 | declaration.value = valueNodes.toString(); 509 | 510 | return; 511 | } 512 | 513 | if (/url\(/i.test(declaration.value)) { 514 | return localizeDeclarationValues(false, declaration, context); 515 | } 516 | } 517 | 518 | const isPureSelector = (context, rule) => { 519 | if (!rule.parent || rule.type === "root") { 520 | return !context.hasPureGlobals; 521 | } 522 | 523 | if (rule.type === "rule" && rule[isPureSelectorSymbol]) { 524 | return rule[isPureSelectorSymbol] || isPureSelector(context, rule.parent); 525 | } 526 | 527 | return !context.hasPureGlobals || isPureSelector(context, rule.parent); 528 | }; 529 | 530 | const isNodeWithoutDeclarations = (rule) => { 531 | if (rule.nodes.length > 0) { 532 | return !rule.nodes.every( 533 | (item) => 534 | item.type === "rule" || 535 | (item.type === "atrule" && !isNodeWithoutDeclarations(item)) 536 | ); 537 | } 538 | 539 | return true; 540 | }; 541 | 542 | module.exports = (options = {}) => { 543 | if ( 544 | options && 545 | options.mode && 546 | options.mode !== "global" && 547 | options.mode !== "local" && 548 | options.mode !== "pure" 549 | ) { 550 | throw new Error( 551 | 'options.mode must be either "global", "local" or "pure" (default "local")' 552 | ); 553 | } 554 | 555 | const pureMode = options && options.mode === "pure"; 556 | const globalMode = options && options.mode === "global"; 557 | 558 | return { 559 | postcssPlugin: "postcss-modules-local-by-default", 560 | prepare() { 561 | const localAliasMap = new Map(); 562 | 563 | return { 564 | Once(root) { 565 | const { icssImports } = extractICSS(root, false); 566 | const enforcePureMode = pureMode && !isPureCheckDisabled(root); 567 | 568 | Object.keys(icssImports).forEach((key) => { 569 | Object.keys(icssImports[key]).forEach((prop) => { 570 | localAliasMap.set(prop, icssImports[key][prop]); 571 | }); 572 | }); 573 | 574 | root.walkAtRules((atRule) => { 575 | if (/keyframes$/i.test(atRule.name)) { 576 | const globalMatch = /^\s*:global\s*\((.+)\)\s*$/.exec( 577 | atRule.params 578 | ); 579 | const localMatch = /^\s*:local\s*\((.+)\)\s*$/.exec( 580 | atRule.params 581 | ); 582 | 583 | let globalKeyframes = globalMode; 584 | 585 | if (globalMatch) { 586 | if (enforcePureMode) { 587 | const ignoreComment = getIgnoreComment(atRule); 588 | if (!ignoreComment) { 589 | throw atRule.error( 590 | "@keyframes :global(...) is not allowed in pure mode" 591 | ); 592 | } else { 593 | ignoreComment.remove(); 594 | } 595 | } 596 | atRule.params = globalMatch[1]; 597 | globalKeyframes = true; 598 | } else if (localMatch) { 599 | atRule.params = localMatch[0]; 600 | globalKeyframes = false; 601 | } else if ( 602 | atRule.params && 603 | !globalMode && 604 | !localAliasMap.has(atRule.params) 605 | ) { 606 | atRule.params = ":local(" + atRule.params + ")"; 607 | } 608 | 609 | atRule.walkDecls((declaration) => { 610 | localizeDeclaration(declaration, { 611 | localAliasMap, 612 | options: options, 613 | global: globalKeyframes, 614 | }); 615 | }); 616 | } else if (/scope$/i.test(atRule.name)) { 617 | if (atRule.params) { 618 | const ignoreComment = pureMode 619 | ? getIgnoreComment(atRule) 620 | : undefined; 621 | 622 | if (ignoreComment) { 623 | ignoreComment.remove(); 624 | } 625 | 626 | atRule.params = atRule.params 627 | .split("to") 628 | .map((item) => { 629 | const selector = item.trim().slice(1, -1).trim(); 630 | const context = localizeNode( 631 | selector, 632 | options.mode, 633 | localAliasMap 634 | ); 635 | 636 | context.options = options; 637 | context.localAliasMap = localAliasMap; 638 | 639 | if ( 640 | enforcePureMode && 641 | context.hasPureGlobals && 642 | !ignoreComment 643 | ) { 644 | throw atRule.error( 645 | 'Selector in at-rule"' + 646 | selector + 647 | '" is not pure ' + 648 | "(pure selectors must contain at least one local class or id)" 649 | ); 650 | } 651 | 652 | return `(${context.selector})`; 653 | }) 654 | .join(" to "); 655 | } 656 | 657 | atRule.nodes.forEach((declaration) => { 658 | if (declaration.type === "decl") { 659 | localizeDeclaration(declaration, { 660 | localAliasMap, 661 | options: options, 662 | global: globalMode, 663 | }); 664 | } 665 | }); 666 | } else if (atRule.nodes) { 667 | atRule.nodes.forEach((declaration) => { 668 | if (declaration.type === "decl") { 669 | localizeDeclaration(declaration, { 670 | localAliasMap, 671 | options: options, 672 | global: globalMode, 673 | }); 674 | } 675 | }); 676 | } 677 | }); 678 | 679 | root.walkRules((rule) => { 680 | if ( 681 | rule.parent && 682 | rule.parent.type === "atrule" && 683 | /keyframes$/i.test(rule.parent.name) 684 | ) { 685 | // ignore keyframe rules 686 | return; 687 | } 688 | 689 | const context = localizeNode(rule, options.mode, localAliasMap); 690 | 691 | context.options = options; 692 | context.localAliasMap = localAliasMap; 693 | 694 | const ignoreComment = enforcePureMode 695 | ? getIgnoreComment(rule) 696 | : undefined; 697 | const isNotPure = enforcePureMode && !isPureSelector(context, rule); 698 | 699 | if ( 700 | isNotPure && 701 | isNodeWithoutDeclarations(rule) && 702 | !ignoreComment 703 | ) { 704 | throw rule.error( 705 | 'Selector "' + 706 | rule.selector + 707 | '" is not pure ' + 708 | "(pure selectors must contain at least one local class or id)" 709 | ); 710 | } else if (ignoreComment) { 711 | ignoreComment.remove(); 712 | } 713 | 714 | if (pureMode) { 715 | rule[isPureSelectorSymbol] = !isNotPure; 716 | } 717 | 718 | rule.selector = context.selector; 719 | 720 | // Less-syntax mixins parse as rules with no nodes 721 | if (rule.nodes) { 722 | rule.nodes.forEach((declaration) => 723 | localizeDeclaration(declaration, context) 724 | ); 725 | } 726 | }); 727 | }, 728 | }; 729 | }, 730 | }; 731 | }; 732 | module.exports.postcss = true; 733 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const postcss = require("postcss"); 4 | const plugin = require("../src"); 5 | const name = require("../package.json").name; 6 | 7 | const tests = [ 8 | { 9 | name: "scope selectors", 10 | input: ".foobar { a_value: some-value; }", 11 | expected: ":local(.foobar) { a_value: some-value; }", 12 | }, 13 | { 14 | name: "scope escaped selectors", 15 | input: ".\\3A \\) {}", 16 | expected: ":local(.\\3A \\)) {}", 17 | }, 18 | { 19 | name: "scope ids", 20 | input: "#foobar { a_value: some-value; }", 21 | expected: ":local(#foobar) { a_value: some-value; }", 22 | }, 23 | { 24 | name: "scope escaped ids", 25 | input: "#\\#test {}", 26 | expected: ":local(#\\#test) {}", 27 | }, 28 | { 29 | name: "scope escaped ids (2)", 30 | input: "#u-m\\00002b {}", 31 | expected: ":local(#u-m\\00002b) {}", 32 | }, 33 | { 34 | name: "scope multiple selectors", 35 | input: ".foo, .baz { a_value: some-value; }", 36 | expected: ":local(.foo), :local(.baz) { a_value: some-value; }", 37 | }, 38 | { 39 | name: "scope sibling selectors", 40 | input: ".foo ~ .baz { a_value: some-value; }", 41 | expected: ":local(.foo) ~ :local(.baz) { a_value: some-value; }", 42 | }, 43 | { 44 | name: "scope psuedo elements", 45 | input: ".foo:after { a_value: some-value; }", 46 | expected: ":local(.foo):after { a_value: some-value; }", 47 | }, 48 | { 49 | name: "scope media queries", 50 | input: "@media only screen { .foo { a_value: some-value; } }", 51 | expected: "@media only screen { :local(.foo) { a_value: some-value; } }", 52 | }, 53 | { 54 | name: "allow narrow global selectors", 55 | input: ":global(.foo .bar) { a_value: some-value; }", 56 | expected: ".foo .bar { a_value: some-value; }", 57 | }, 58 | { 59 | name: "allow narrow local selectors", 60 | input: ":local(.foo .bar) { a_value: some-value; }", 61 | expected: ":local(.foo) :local(.bar) { a_value: some-value; }", 62 | }, 63 | { 64 | name: "allow broad global selectors", 65 | input: ":global .foo .bar { a_value: some-value; }", 66 | expected: ".foo .bar { a_value: some-value; }", 67 | }, 68 | { 69 | name: "allow broad local selectors", 70 | input: ":local .foo .bar { a_value: some-value; }", 71 | expected: ":local(.foo) :local(.bar) { a_value: some-value; }", 72 | }, 73 | { 74 | name: "allow multiple narrow global selectors", 75 | input: ":global(.foo), :global(.bar) { a_value: some-value; }", 76 | expected: ".foo, .bar { a_value: some-value; }", 77 | }, 78 | { 79 | name: "allow multiple broad global selectors", 80 | input: ":global .foo, :global .bar { a_value: some-value; }", 81 | expected: ".foo, .bar { a_value: some-value; }", 82 | }, 83 | { 84 | name: "allow multiple broad local selectors", 85 | input: ":local .foo, :local .bar { a_value: some-value; }", 86 | expected: ":local(.foo), :local(.bar) { a_value: some-value; }", 87 | }, 88 | { 89 | name: "allow narrow global selectors nested inside local styles", 90 | input: ".foo :global(.foo .bar) { a_value: some-value; }", 91 | expected: ":local(.foo) .foo .bar { a_value: some-value; }", 92 | }, 93 | { 94 | name: "allow broad global selectors nested inside local styles", 95 | input: ".foo :global .foo .bar { a_value: some-value; }", 96 | expected: ":local(.foo) .foo .bar { a_value: some-value; }", 97 | }, 98 | { 99 | name: "allow parentheses inside narrow global selectors", 100 | input: ".foo :global(.foo:not(.bar)) { a_value: some-value; }", 101 | expected: ":local(.foo) .foo:not(.bar) { a_value: some-value; }", 102 | }, 103 | { 104 | name: "allow parentheses inside narrow local selectors", 105 | input: ".foo :local(.foo:not(.bar)) { a_value: some-value; }", 106 | expected: 107 | ":local(.foo) :local(.foo):not(:local(.bar)) { a_value: some-value; }", 108 | }, 109 | { 110 | name: "allow narrow global selectors appended to local styles", 111 | input: ".foo:global(.foo.bar) { a_value: some-value; }", 112 | expected: ":local(.foo).foo.bar { a_value: some-value; }", 113 | }, 114 | { 115 | name: "ignore selectors that are already local", 116 | input: ":local(.foobar) { a_value: some-value; }", 117 | expected: ":local(.foobar) { a_value: some-value; }", 118 | }, 119 | { 120 | name: "ignore nested selectors that are already local", 121 | input: ":local(.foo) :local(.bar) { a_value: some-value; }", 122 | expected: ":local(.foo) :local(.bar) { a_value: some-value; }", 123 | }, 124 | { 125 | name: "ignore multiple selectors that are already local", 126 | input: ":local(.foo), :local(.bar) { a_value: some-value; }", 127 | expected: ":local(.foo), :local(.bar) { a_value: some-value; }", 128 | }, 129 | { 130 | name: "ignore sibling selectors that are already local", 131 | input: ":local(.foo) ~ :local(.bar) { a_value: some-value; }", 132 | expected: ":local(.foo) ~ :local(.bar) { a_value: some-value; }", 133 | }, 134 | { 135 | name: "ignore psuedo elements that are already local", 136 | input: ":local(.foo):after { a_value: some-value; }", 137 | expected: ":local(.foo):after { a_value: some-value; }", 138 | }, 139 | { 140 | name: "trim whitespace after empty broad selector", 141 | input: ".bar :global :global { a_value: some-value; }", 142 | expected: ":local(.bar) { a_value: some-value; }", 143 | }, 144 | { 145 | name: "broad global should be limited to selector", 146 | input: 147 | ":global .foo, .bar :global, .foobar :global { a_value: some-value; }", 148 | expected: ".foo, :local(.bar), :local(.foobar) { a_value: some-value; }", 149 | }, 150 | { 151 | name: "broad global should be limited to nested selector", 152 | input: ".foo:not(:global .bar).foobar { a_value: some-value; }", 153 | expected: ":local(.foo):not(.bar):local(.foobar) { a_value: some-value; }", 154 | }, 155 | { 156 | name: "broad global and local should allow switching", 157 | input: 158 | ".foo :global .bar :local .foobar :local .barfoo { a_value: some-value; }", 159 | expected: 160 | ":local(.foo) .bar :local(.foobar) :local(.barfoo) { a_value: some-value; }", 161 | }, 162 | { 163 | name: "localize a single animation-name", 164 | input: ".foo { animation-name: bar; }", 165 | expected: ":local(.foo) { animation-name: :local(bar); }", 166 | }, 167 | { 168 | name: "localize a single animation-name #2", 169 | input: ".foo { animation-name: local(bar); }", 170 | expected: ":local(.foo) { animation-name: :local(bar); }", 171 | }, 172 | { 173 | name: "not localize animation-name in a var function", 174 | input: ".foo { animation-name: var(--bar); }", 175 | expected: ":local(.foo) { animation-name: var(--bar); }", 176 | }, 177 | { 178 | name: "not localize animation-name in a var function #2", 179 | input: ".foo { animation-name: vAr(--bar); }", 180 | expected: ":local(.foo) { animation-name: vAr(--bar); }", 181 | }, 182 | { 183 | name: "not localize animation-name in an env function", 184 | input: ".foo { animation-name: env(bar); }", 185 | expected: ":local(.foo) { animation-name: env(bar); }", 186 | }, 187 | { 188 | name: "not localize animation-name in an global function", 189 | input: ".foo { animation-name: global(bar); }", 190 | expected: ":local(.foo) { animation-name: bar; }", 191 | }, 192 | { 193 | name: "localize and not localize animation-name in mixed case", 194 | input: 195 | ".foo { animation-name: fadeInOut, global(moveLeft300px), local(bounce); }", 196 | expected: 197 | ":local(.foo) { animation-name: :local(fadeInOut), moveLeft300px, :local(bounce); }", 198 | }, 199 | { 200 | name: "localize and not localize animation-name in mixed case #2", 201 | options: { mode: "global" }, 202 | input: 203 | ".foo { animation-name: fadeInOut, global(moveLeft300px), local(bounce); }", 204 | expected: 205 | ".foo { animation-name: fadeInOut, moveLeft300px, :local(bounce); }", 206 | }, 207 | { 208 | name: "localize and not localize animation-name in mixed case #3", 209 | options: { mode: "pure" }, 210 | input: 211 | ".foo { animation-name: fadeInOut, global(moveLeft300px), local(bounce); }", 212 | expected: 213 | ":local(.foo) { animation-name: :local(fadeInOut), moveLeft300px, :local(bounce); }", 214 | }, 215 | { 216 | name: "not localize animation in an global function", 217 | input: ".foo { animation: global(bar); }", 218 | expected: ":local(.foo) { animation: bar; }", 219 | }, 220 | { 221 | name: "not localize a certain animation in an global function", 222 | input: ".foo { animation: global(bar), foo; }", 223 | expected: ":local(.foo) { animation: bar, :local(foo); }", 224 | }, 225 | { 226 | name: "localize and not localize a certain animation in mixed case", 227 | input: ".foo { animation: rotate 1s, global(spin) 3s, local(fly) 6s; }", 228 | expected: 229 | ":local(.foo) { animation: :local(rotate) 1s, spin 3s, :local(fly) 6s; }", 230 | }, 231 | { 232 | name: "localize and not localize a certain animation in mixed case #2", 233 | options: { mode: "global" }, 234 | input: ".foo { animation: rotate 1s, global(spin) 3s, local(fly) 6s; }", 235 | expected: ".foo { animation: rotate 1s, spin 3s, :local(fly) 6s; }", 236 | }, 237 | { 238 | name: "localize and not localize a certain animation in mixed case #2", 239 | options: { mode: "pure" }, 240 | input: ".foo { animation: rotate 1s, global(spin) 3s, local(fly) 6s; }", 241 | expected: 242 | ":local(.foo) { animation: :local(rotate) 1s, spin 3s, :local(fly) 6s; }", 243 | }, 244 | { 245 | name: "not localize animation-name in an env function #2", 246 | input: ".foo { animation-name: eNv(bar); }", 247 | expected: ":local(.foo) { animation-name: eNv(bar); }", 248 | }, 249 | { 250 | name: "not localize a single animation-delay", 251 | input: ".foo { animation-delay: 1s; }", 252 | expected: ":local(.foo) { animation-delay: 1s; }", 253 | }, 254 | { 255 | name: "localize multiple animation-names", 256 | input: ".foo { animation-name: bar, foobar; }", 257 | expected: ":local(.foo) { animation-name: :local(bar), :local(foobar); }", 258 | }, 259 | { 260 | name: "not localize revert", 261 | input: ".foo { animation: revert; }", 262 | expected: ":local(.foo) { animation: revert; }", 263 | }, 264 | { 265 | name: "not localize revert #2", 266 | input: ".foo { animation-name: revert; }", 267 | expected: ":local(.foo) { animation-name: revert; }", 268 | }, 269 | { 270 | name: "not localize revert #3", 271 | input: ".foo { animation-name: revert, foo, none; }", 272 | expected: ":local(.foo) { animation-name: revert, :local(foo), none; }", 273 | }, 274 | { 275 | name: "not localize revert-layer", 276 | input: ".foo { animation: revert-layer; }", 277 | expected: ":local(.foo) { animation: revert-layer; }", 278 | }, 279 | { 280 | name: "not localize revert", 281 | input: ".foo { animation-name: revert-layer; }", 282 | expected: ":local(.foo) { animation-name: revert-layer; }", 283 | }, 284 | { 285 | name: "localize animation using special characters", 286 | input: ".foo { animation: \\@bounce; }", 287 | expected: ":local(.foo) { animation: :local(\\@bounce); }", 288 | }, 289 | { 290 | name: "localize animation using special characters", 291 | input: ".foo { animation: bou\\@nce; }", 292 | expected: ":local(.foo) { animation: :local(bou\\@nce); }", 293 | }, 294 | { 295 | name: "localize animation using special characters", 296 | input: ".foo { animation: \\ as; }", 297 | expected: ":local(.foo) { animation: :local(\\ as); }", 298 | }, 299 | { 300 | name: "localize animation using special characters", 301 | input: ".foo { animation: t\\ t; }", 302 | expected: ":local(.foo) { animation: :local(t\\ t); }", 303 | }, 304 | { 305 | name: "localize animation using special characters", 306 | input: ".foo { animation: -\\a; }", 307 | expected: ":local(.foo) { animation: :local(-\\a); }", 308 | }, 309 | { 310 | name: "localize animation using special characters", 311 | input: ".foo { animation: --\\a; }", 312 | expected: ":local(.foo) { animation: :local(--\\a); }", 313 | }, 314 | { 315 | name: "localize animation using special characters", 316 | input: ".foo { animation: \\a; }", 317 | expected: ":local(.foo) { animation: :local(\\a); }", 318 | }, 319 | { 320 | name: "localize animation using special characters", 321 | input: ".foo { animation: -\\a; }", 322 | expected: ":local(.foo) { animation: :local(-\\a); }", 323 | }, 324 | { 325 | name: "localize animation using special characters", 326 | input: ".foo { animation: --; }", 327 | expected: ":local(.foo) { animation: :local(--); }", 328 | }, 329 | { 330 | name: "localize animation using special characters", 331 | input: ".foo { animation: 😃bounce😃; }", 332 | expected: ":local(.foo) { animation: :local(😃bounce😃); }", 333 | }, 334 | { 335 | name: "not localize revert", 336 | input: ".foo { animation: --foo; }", 337 | expected: ":local(.foo) { animation: :local(--foo); }", 338 | }, 339 | { 340 | name: "not localize name in nested function", 341 | input: ".foo { animation: fade .2s var(--easeOutQuart) .1s forwards }", 342 | expected: 343 | ":local(.foo) { animation: :local(fade) .2s var(--easeOutQuart) .1s forwards }", 344 | }, 345 | { 346 | name: "not localize name in nested function #2", 347 | input: ".foo { animation: fade .2s env(FOO_BAR) .1s forwards, name }", 348 | expected: 349 | ":local(.foo) { animation: :local(fade) .2s env(FOO_BAR) .1s forwards, :local(name) }", 350 | }, 351 | { 352 | name: "not localize name in nested function #3", 353 | input: ".foo { animation: var(--foo-bar) .1s forwards, name }", 354 | expected: 355 | ":local(.foo) { animation: var(--foo-bar) .1s forwards, :local(name) }", 356 | }, 357 | { 358 | name: "not localize name in nested function #3", 359 | input: ".foo { animation: var(--foo-bar) .1s forwards name, name }", 360 | expected: 361 | ":local(.foo) { animation: var(--foo-bar) .1s forwards :local(name), :local(name) }", 362 | }, 363 | { 364 | name: "localize animation", 365 | input: ".foo { animation: a; }", 366 | expected: ":local(.foo) { animation: :local(a); }", 367 | }, 368 | { 369 | name: "localize animation #2", 370 | input: ".foo { animation: bar 5s, foobar; }", 371 | expected: ":local(.foo) { animation: :local(bar) 5s, :local(foobar); }", 372 | }, 373 | { 374 | name: "localize animation #3", 375 | input: ".foo { animation: ease ease; }", 376 | expected: ":local(.foo) { animation: ease :local(ease); }", 377 | }, 378 | { 379 | name: "localize animation #4", 380 | input: ".foo { animation: 0s ease 0s 1 normal none test running; }", 381 | expected: 382 | ":local(.foo) { animation: 0s ease 0s 1 normal none :local(test) running; }", 383 | }, 384 | { 385 | name: "localize animation with vendor prefix", 386 | input: ".foo { -webkit-animation: bar; animation: bar; }", 387 | expected: 388 | ":local(.foo) { -webkit-animation: :local(bar); animation: :local(bar); }", 389 | }, 390 | { 391 | name: "not localize other rules", 392 | input: '.foo { content: "animation: bar;" }', 393 | expected: ':local(.foo) { content: "animation: bar;" }', 394 | }, 395 | { 396 | name: "not localize global rules", 397 | input: ":global .foo { animation: foo; animation-name: bar; }", 398 | expected: ".foo { animation: foo; animation-name: bar; }", 399 | }, 400 | { 401 | name: "handle nested global", 402 | input: ":global .a:not(:global .b) { a_value: some-value; }", 403 | expected: ".a:not(.b) { a_value: some-value; }", 404 | }, 405 | { 406 | name: "handle nested global #1", 407 | input: 408 | ":global .a:not(:global .b:not(:global .c)) { a_value: some-value; }", 409 | expected: ".a:not(.b:not(.c)) { a_value: some-value; }", 410 | }, 411 | { 412 | name: "handle nested global #2", 413 | input: ":local .a:not(:not(:not(:global .c))) { a_value: some-value; }", 414 | expected: ":local(.a):not(:not(:not(.c))) { a_value: some-value; }", 415 | }, 416 | { 417 | name: "handle nested global #3", 418 | input: ":global .a:not(:global .b, :global .c) { a_value: some-value; }", 419 | expected: ".a:not(.b, .c) { a_value: some-value; }", 420 | }, 421 | { 422 | name: "handle nested global #4", 423 | input: ":local .a:not(:global .b, :local .c) { a_value: some-value; }", 424 | expected: ":local(.a):not(.b, :local(.c)) { a_value: some-value; }", 425 | }, 426 | { 427 | name: "handle nested global #5", 428 | input: ":global .a:not(:local .b, :global .c) { a_value: some-value; }", 429 | expected: ".a:not(:local(.b), .c) { a_value: some-value; }", 430 | }, 431 | { 432 | name: "handle nested global #6", 433 | input: ":global .a:not(.b, .c) { a_value: some-value; }", 434 | expected: ".a:not(.b, .c) { a_value: some-value; }", 435 | }, 436 | { 437 | name: "handle nested global #7", 438 | input: ":local .a:not(.b, .c) { a_value: some-value; }", 439 | expected: ":local(.a):not(:local(.b), :local(.c)) { a_value: some-value; }", 440 | }, 441 | { 442 | name: "handle nested global #8", 443 | input: ":global .a:not(:local .b, .c) { a_value: some-value; }", 444 | expected: ".a:not(:local(.b), :local(.c)) { a_value: some-value; }", 445 | }, 446 | { 447 | name: "handle a complex animation rule", 448 | input: 449 | ".foo { animation: foo, bar 5s linear 2s infinite alternate, barfoo 1s; }", 450 | expected: 451 | ":local(.foo) { animation: :local(foo), :local(bar) 5s linear 2s infinite alternate, :local(barfoo) 1s; }", 452 | }, 453 | { 454 | name: "handle animations where the first value is not the animation name", 455 | input: ".foo { animation: 1s foo; }", 456 | expected: ":local(.foo) { animation: 1s :local(foo); }", 457 | }, 458 | { 459 | name: "handle animations where the first value is not the animation name whilst also using keywords", 460 | input: ".foo { animation: 1s normal ease-out infinite foo; }", 461 | expected: 462 | ":local(.foo) { animation: 1s normal ease-out infinite :local(foo); }", 463 | }, 464 | { 465 | name: "not treat animation curve as identifier of animation name even if it separated by comma", 466 | input: 467 | ".foo { animation: slide-right 300ms forwards ease-out, fade-in 300ms forwards ease-out; }", 468 | expected: 469 | ":local(.foo) { animation: :local(slide-right) 300ms forwards ease-out, :local(fade-in) 300ms forwards ease-out; }", 470 | }, 471 | { 472 | name: 'not treat "start" and "end" keywords in steps() function as identifiers', 473 | input: [ 474 | ".foo { animation: spin 1s steps(12, end) infinite; }", 475 | ".foo { animation: spin 1s STEPS(12, start) infinite; }", 476 | ".foo { animation: spin 1s steps(12, END) infinite; }", 477 | ".foo { animation: spin 1s steps(12, START) infinite; }", 478 | ].join("\n"), 479 | expected: [ 480 | ":local(.foo) { animation: :local(spin) 1s steps(12, end) infinite; }", 481 | ":local(.foo) { animation: :local(spin) 1s STEPS(12, start) infinite; }", 482 | ":local(.foo) { animation: :local(spin) 1s steps(12, END) infinite; }", 483 | ":local(.foo) { animation: :local(spin) 1s steps(12, START) infinite; }", 484 | ].join("\n"), 485 | }, 486 | { 487 | name: "handle animations with custom timing functions", 488 | input: 489 | ".foo { animation: 1s normal cubic-bezier(0.25, 0.5, 0.5. 0.75) foo; }", 490 | expected: 491 | ":local(.foo) { animation: 1s normal cubic-bezier(0.25, 0.5, 0.5. 0.75) :local(foo); }", 492 | }, 493 | { 494 | name: "handle animations whose names are keywords", 495 | input: ".foo { animation: 1s infinite infinite; }", 496 | expected: ":local(.foo) { animation: 1s infinite :local(infinite); }", 497 | }, 498 | { 499 | name: 'handle not localize an animation shorthand value of "inherit"', 500 | input: ".foo { animation: inherit; }", 501 | expected: ":local(.foo) { animation: inherit; }", 502 | }, 503 | { 504 | name: 'handle "constructor" as animation name', 505 | input: ".foo { animation: constructor constructor; }", 506 | expected: 507 | ":local(.foo) { animation: :local(constructor) :local(constructor); }", 508 | }, 509 | { 510 | name: "default to global when mode provided", 511 | input: ".foo { a_value: some-value; }", 512 | options: { mode: "global" }, 513 | expected: ".foo { a_value: some-value; }", 514 | }, 515 | { 516 | name: "default to local when mode provided", 517 | input: ".foo { a_value: some-value; }", 518 | options: { mode: "local" }, 519 | expected: ":local(.foo) { a_value: some-value; }", 520 | }, 521 | { 522 | name: "use correct spacing", 523 | input: [ 524 | ".a :local .b {}", 525 | ".a:local.b {}", 526 | ".a:local(.b) {}", 527 | ".a:local( .b ) {}", 528 | ".a :local(.b) {}", 529 | ".a :local( .b ) {}", 530 | ":local(.a).b {}", 531 | ":local( .a ).b {}", 532 | ":local(.a) .b {}", 533 | ":local( .a ) .b {}", 534 | ].join("\n"), 535 | options: { mode: "global" }, 536 | expected: [ 537 | ".a :local(.b) {}", 538 | ".a:local(.b) {}", 539 | ".a:local(.b) {}", 540 | ".a:local(.b) {}", 541 | ".a :local(.b) {}", 542 | ".a :local(.b) {}", 543 | ":local(.a).b {}", 544 | ":local(.a).b {}", 545 | ":local(.a) .b {}", 546 | ":local(.a) .b {}", 547 | ].join("\n"), 548 | }, 549 | { 550 | name: "localize keyframes", 551 | input: 552 | "@keyframes foo { from: { a_value: some-value; } to { a_value: some-value; } }", 553 | expected: 554 | "@keyframes :local(foo) { from: { a_value: some-value; } to { a_value: some-value; } }", 555 | }, 556 | { 557 | name: "localize keyframes starting with special characters", 558 | input: "@keyframes \\@foo { from { color: red; } to { color: blue; } }", 559 | expected: 560 | "@keyframes :local(\\@foo) { from { color: red; } to { color: blue; } }", 561 | }, 562 | { 563 | name: "localize keyframes containing special characters", 564 | input: "@keyframes f\\@oo { from { color: red; } to { color: blue; } }", 565 | expected: 566 | "@keyframes :local(f\\@oo) { from { color: red; } to { color: blue; } }", 567 | }, 568 | { 569 | name: "localize keyframes in global default mode", 570 | input: "@keyframes foo { a_value: some-value; }", 571 | options: { mode: "global" }, 572 | expected: "@keyframes foo { a_value: some-value; }", 573 | }, 574 | { 575 | name: "localize explicit keyframes", 576 | input: 577 | "@keyframes :local(foo) { 0% { color: red; } 33.3% { color: yellow; } 100% { color: blue; } } @-webkit-keyframes :global(bar) { from { color: red; } to { color: blue; } }", 578 | expected: 579 | "@keyframes :local(foo) { 0% { color: red; } 33.3% { color: yellow; } 100% { color: blue; } } @-webkit-keyframes bar { from { color: red; } to { color: blue; } }", 580 | }, 581 | { 582 | name: "ignore :export statements", 583 | input: ":export { foo: __foo; }", 584 | expected: ":export { foo: __foo; }", 585 | }, 586 | { 587 | name: "ignore :import statemtents", 588 | input: ':import("~/lol.css") { foo: __foo; }', 589 | expected: ':import("~/lol.css") { foo: __foo; }', 590 | }, 591 | { 592 | name: "incorrectly handle nested selectors", 593 | input: ".bar:not(:global .foo, .baz) { a_value: some-value; }", 594 | expected: ":local(.bar):not(.foo, .baz) { a_value: some-value; }", 595 | }, 596 | { 597 | name: "compile in pure mode", 598 | input: 599 | ':global(.foo).bar, [type="radio"] ~ .label, :not(.foo), #bar { a_value: some-value; }', 600 | options: { mode: "pure" }, 601 | expected: 602 | '.foo:local(.bar), [type="radio"] ~ :local(.label), :not(:local(.foo)), :local(#bar) { a_value: some-value; }', 603 | }, 604 | { 605 | name: "compile explict global element", 606 | input: ":global(input) { a_value: some-value; }", 607 | expected: "input { a_value: some-value; }", 608 | }, 609 | { 610 | name: "compile explict global attribute", 611 | input: 612 | ':global([type="radio"]), :not(:global [type="radio"]) { a_value: some-value; }', 613 | expected: '[type="radio"], :not([type="radio"]) { a_value: some-value; }', 614 | }, 615 | { 616 | name: "throw on invalid mode", 617 | input: "", 618 | options: { mode: "???" }, 619 | error: /"global", "local" or "pure"/, 620 | }, 621 | { 622 | name: "throw on inconsistent selector result", 623 | input: ":global .foo, .bar { a_value: some-value; }", 624 | error: /Inconsistent/, 625 | }, 626 | { 627 | name: "throw on nested :locals", 628 | input: ":local(:local(.foo)) { a_value: some-value; }", 629 | error: /is not allowed inside/, 630 | }, 631 | { 632 | name: "throw on nested :globals", 633 | input: ":global(:global(.foo)) { a_value: some-value; }", 634 | error: /is not allowed inside/, 635 | }, 636 | { 637 | name: "throw on nested mixed", 638 | input: ":local(:global(.foo)) { a_value: some-value; }", 639 | error: /is not allowed inside/, 640 | }, 641 | { 642 | name: "throw on nested broad :local", 643 | input: ":global(:local .foo) { a_value: some-value; }", 644 | error: /is not allowed inside/, 645 | }, 646 | { 647 | name: "throw on incorrect spacing with broad :global", 648 | input: ".foo :global.bar { a_value: some-value; }", 649 | error: /Missing whitespace after :global/, 650 | }, 651 | { 652 | name: "throw on incorrect spacing with broad :local", 653 | input: ".foo:local .bar { a_value: some-value; }", 654 | error: /Missing whitespace before :local/, 655 | }, 656 | { 657 | name: "throw on not pure selector (global class)", 658 | input: ":global(.foo) { a_value: some-value; }", 659 | options: { mode: "pure" }, 660 | error: /":global\(\.foo\)" is not pure/, 661 | }, 662 | { 663 | name: "throw on not pure selector (with multiple 1)", 664 | input: ".foo, :global(.bar) { a_value: some-value; }", 665 | options: { mode: "pure" }, 666 | error: /".foo, :global\(\.bar\)" is not pure/, 667 | }, 668 | { 669 | name: "throw on not pure selector (with multiple 2)", 670 | input: ":global(.bar), .foo { a_value: some-value; }", 671 | options: { mode: "pure" }, 672 | error: /":global\(\.bar\), .foo" is not pure/, 673 | }, 674 | { 675 | name: "throw on not pure selector (element)", 676 | input: "input { a_value: some-value; }", 677 | options: { mode: "pure" }, 678 | error: /"input" is not pure/, 679 | }, 680 | { 681 | name: "throw on not pure selector (attribute)", 682 | input: '[type="radio"] { a_value: some-value; }', 683 | options: { mode: "pure" }, 684 | error: /"\[type="radio"\]" is not pure/, 685 | }, 686 | { 687 | name: "throw on not pure keyframes", 688 | input: "@keyframes :global(foo) { a_value: some-value; }", 689 | options: { mode: "pure" }, 690 | error: /@keyframes :global\(\.\.\.\) is not allowed in pure mode/, 691 | }, 692 | { 693 | name: "pass through global element", 694 | input: "input { a_value: some-value; }", 695 | expected: "input { a_value: some-value; }", 696 | }, 697 | { 698 | name: "localise class and pass through element", 699 | input: ".foo input { a_value: some-value; }", 700 | expected: ":local(.foo) input { a_value: some-value; }", 701 | }, 702 | { 703 | name: "pass through attribute selector", 704 | input: '[type="radio"] { a_value: some-value; }', 705 | expected: '[type="radio"] { a_value: some-value; }', 706 | }, 707 | { 708 | name: "not modify urls without option", 709 | input: 710 | ".a { background: url(./image.png); }\n" + 711 | ":global .b { background: url(image.png); }\n" + 712 | '.c { background: url("./image.png"); }', 713 | expected: 714 | ":local(.a) { background: url(./image.png); }\n" + 715 | ".b { background: url(image.png); }\n" + 716 | ':local(.c) { background: url("./image.png"); }', 717 | }, 718 | { 719 | name: "rewrite url in local block", 720 | input: 721 | ".a { background: url(./image.png); }\n" + 722 | ":global .b { background: url(image.png); }\n" + 723 | '.c { background: url("./image.png"); }\n' + 724 | ".c { background: url('./image.png'); }\n" + 725 | '.d { background: -webkit-image-set(url("./image.png") 1x, url("./image2x.png") 2x); }\n' + 726 | '@font-face { src: url("./font.woff"); }\n' + 727 | '@-webkit-font-face { src: url("./font.woff"); }\n' + 728 | '@media screen { .a { src: url("./image.png"); } }\n' + 729 | '@keyframes :global(ani1) { 0% { src: url("image.png"); } }\n' + 730 | '@keyframes ani2 { 0% { src: url("./image.png"); } }\n' + 731 | "foo { background: end-with-url(something); }", 732 | options: { 733 | rewriteUrl: function (global, url) { 734 | const mode = global ? "global" : "local"; 735 | return "(" + mode + ")" + url + '"' + mode + '"'; 736 | }, 737 | }, 738 | expected: 739 | ':local(.a) { background: url((local\\)./image.png\\"local\\"); }\n' + 740 | '.b { background: url((global\\)image.png\\"global\\"); }\n' + 741 | ':local(.c) { background: url("(local)./image.png\\"local\\""); }\n' + 742 | ":local(.c) { background: url('(local)./image.png\"local\"'); }\n" + 743 | ':local(.d) { background: -webkit-image-set(url("(local)./image.png\\"local\\"") 1x, url("(local)./image2x.png\\"local\\"") 2x); }\n' + 744 | '@font-face { src: url("(local)./font.woff\\"local\\""); }\n' + 745 | '@-webkit-font-face { src: url("(local)./font.woff\\"local\\""); }\n' + 746 | '@media screen { :local(.a) { src: url("(local)./image.png\\"local\\""); } }\n' + 747 | '@keyframes ani1 { 0% { src: url("(global)image.png\\"global\\""); } }\n' + 748 | '@keyframes :local(ani2) { 0% { src: url("(local)./image.png\\"local\\""); } }\n' + 749 | "foo { background: end-with-url(something); }", 750 | }, 751 | { 752 | name: "not crash on atrule without nodes", 753 | input: '@charset "utf-8";', 754 | expected: '@charset "utf-8";', 755 | }, 756 | { 757 | name: "not crash on a rule without nodes", 758 | input: (function () { 759 | const inner = postcss.rule({ selector: ".b", ruleWithoutBody: true }); 760 | const outer = postcss.rule({ selector: ".a" }).push(inner); 761 | const root = postcss.root().push(outer); 762 | inner.nodes = undefined; 763 | return root; 764 | })(), 765 | // postcss-less's stringify would honor `ruleWithoutBody` and omit the trailing `{}` 766 | expected: ":local(.a) {\n :local(.b) {}\n}", 767 | }, 768 | { 769 | name: "not break unicode characters", 770 | input: '.a { content: "\\2193" }', 771 | expected: ':local(.a) { content: "\\2193" }', 772 | }, 773 | { 774 | name: "not break unicode characters", 775 | input: '.a { content: "\\2193\\2193" }', 776 | expected: ':local(.a) { content: "\\2193\\2193" }', 777 | }, 778 | { 779 | name: "not break unicode characters", 780 | input: '.a { content: "\\2193 \\2193" }', 781 | expected: ':local(.a) { content: "\\2193 \\2193" }', 782 | }, 783 | { 784 | name: "not break unicode characters", 785 | input: '.a { content: "\\2193\\2193\\2193" }', 786 | expected: ':local(.a) { content: "\\2193\\2193\\2193" }', 787 | }, 788 | { 789 | name: "not break unicode characters", 790 | input: '.a { content: "\\2193 \\2193 \\2193" }', 791 | expected: ':local(.a) { content: "\\2193 \\2193 \\2193" }', 792 | }, 793 | { 794 | name: "not ignore custom property set", 795 | input: 796 | ":root { --title-align: center; --sr-only: { position: absolute; } }", 797 | expected: 798 | ":root { --title-align: center; --sr-only: { position: absolute; } }", 799 | }, 800 | /** 801 | * Imported aliases 802 | */ 803 | { 804 | name: "not localize imported alias", 805 | input: ` 806 | :import(foo) { a_value: some-value; } 807 | 808 | .foo > .a_value { a_value: some-value; } 809 | `, 810 | expected: ` 811 | :import(foo) { a_value: some-value; } 812 | 813 | :local(.foo) > .a_value { a_value: some-value; } 814 | `, 815 | }, 816 | { 817 | name: "not localize nested imported alias", 818 | input: ` 819 | :import(foo) { a_value: some-value; } 820 | 821 | .foo > .a_value > .bar { a_value: some-value; } 822 | `, 823 | expected: ` 824 | :import(foo) { a_value: some-value; } 825 | 826 | :local(.foo) > .a_value > :local(.bar) { a_value: some-value; } 827 | `, 828 | }, 829 | 830 | { 831 | name: "ignore imported in explicit local", 832 | input: ` 833 | :import(foo) { a_value: some-value; } 834 | 835 | :local(.a_value) { a_value: some-value; } 836 | `, 837 | expected: ` 838 | :import(foo) { a_value: some-value; } 839 | 840 | :local(.a_value) { a_value: some-value; } 841 | `, 842 | }, 843 | { 844 | name: "escape local context with explict global", 845 | input: ` 846 | :import(foo) { a_value: some-value; } 847 | 848 | :local .foo :global(.a_value) .bar { a_value: some-value; } 849 | `, 850 | expected: ` 851 | :import(foo) { a_value: some-value; } 852 | 853 | :local(.foo) .a_value :local(.bar) { a_value: some-value; } 854 | `, 855 | }, 856 | { 857 | name: "respect explicit local", 858 | input: ` 859 | :import(foo) { a_value: some-value; } 860 | 861 | .a_value :local .a_value .foo :global .a_value { a_value: some-value; } 862 | `, 863 | expected: ` 864 | :import(foo) { a_value: some-value; } 865 | 866 | .a_value :local(.a_value) :local(.foo) .a_value { a_value: some-value; } 867 | `, 868 | }, 869 | { 870 | name: "not localize imported animation-name", 871 | input: ` 872 | :import(file) { a_value: some-value; } 873 | 874 | .foo { animation-name: a_value; } 875 | `, 876 | expected: ` 877 | :import(file) { a_value: some-value; } 878 | 879 | :local(.foo) { animation-name: a_value; } 880 | `, 881 | }, 882 | { 883 | name: "throw on invalid syntax id usage", 884 | input: ". { a_value: some-value; }", 885 | error: /Invalid class or id selector syntax/, 886 | }, 887 | { 888 | name: "throw on invalid syntax class usage", 889 | input: "# { a_value: some-value; }", 890 | error: /Invalid class or id selector syntax/, 891 | }, 892 | { 893 | name: "throw on invalid syntax local class usage", 894 | input: ":local(.) { a_value: some-value; }", 895 | error: /Invalid class or id selector syntax/, 896 | }, 897 | { 898 | name: "throw on invalid syntax local id usage", 899 | input: ":local(#) { a_value: some-value; }", 900 | error: /Invalid class or id selector syntax/, 901 | }, 902 | { 903 | name: "throw on invalid global class usage", 904 | input: ":global(.) { a_value: some-value; }", 905 | error: /Invalid class or id selector syntax/, 906 | }, 907 | { 908 | name: "throw on invalid global class usage", 909 | input: ":global(#) { a_value: some-value; }", 910 | error: /Invalid class or id selector syntax/, 911 | }, 912 | { 913 | name: "throw on invalid global class usage", 914 | input: ":global(.a:not(:global .b, :global .c)) { a_value: some-value; }", 915 | error: /A :global is not allowed inside of a :global/, 916 | }, 917 | { 918 | name: "consider & statements as pure", 919 | input: ".foo { &:hover { a_value: some-value; } }", 920 | options: { mode: "pure" }, 921 | expected: ":local(.foo) { &:hover { a_value: some-value; } }", 922 | }, 923 | { 924 | name: "consider & statements as pure #2", 925 | input: 926 | ".foo { @media screen and (min-width: 900px) { &:hover { a_value: some-value; } } }", 927 | options: { mode: "pure" }, 928 | expected: 929 | ":local(.foo) { @media screen and (min-width: 900px) { &:hover { a_value: some-value; } } }", 930 | }, 931 | { 932 | name: "consider global inside local as pure", 933 | input: ".foo button { a_value: some-value; }", 934 | options: { mode: "pure" }, 935 | expected: ":local(.foo) button { a_value: some-value; }", 936 | }, 937 | { 938 | name: "consider selector & statements as pure", 939 | input: ".foo { html &:hover { a_value: some-value; } }", 940 | options: { mode: "pure" }, 941 | expected: ":local(.foo) { html &:hover { a_value: some-value; } }", 942 | }, 943 | { 944 | name: "consider selector & statements as pure", 945 | input: ".foo { &:global(.bar) { a_value: some-value; } }", 946 | options: { mode: "pure" }, 947 | expected: ":local(.foo) { &.bar { a_value: some-value; } }", 948 | }, 949 | { 950 | name: "throw on nested & selectors without a local selector", 951 | input: ":global(.foo) { &:hover { a_value: some-value; } }", 952 | options: { mode: "pure" }, 953 | error: /is not pure/, 954 | }, 955 | { 956 | name: "should suppress errors for global selectors after ignore comment", 957 | options: { mode: "pure" }, 958 | input: `/* cssmodules-pure-ignore */ 959 | :global(.foo) { color: blue; }`, 960 | expected: `.foo { color: blue; }`, 961 | }, 962 | { 963 | name: "should suppress errors for global selectors after ignore comment #2", 964 | options: { mode: "pure" }, 965 | input: `/* cssmodules-pure-ignore */ 966 | /* another comment */ 967 | :global(.foo) { color: blue; }`, 968 | expected: `/* another comment */ 969 | .foo { color: blue; }`, 970 | }, 971 | { 972 | name: "should suppress errors for global selectors after ignore comment #3", 973 | options: { mode: "pure" }, 974 | input: `/* another comment */ 975 | /* cssmodules-pure-ignore */ 976 | :global(.foo) { color: blue; }`, 977 | expected: `/* another comment */ 978 | .foo { color: blue; }`, 979 | }, 980 | { 981 | name: "should suppress errors for global selectors after ignore comment #4", 982 | options: { mode: "pure" }, 983 | input: `/* cssmodules-pure-ignore */ /* another comment */ 984 | :global(.foo) { color: blue; }`, 985 | expected: `/* another comment */ 986 | .foo { color: blue; }`, 987 | }, 988 | { 989 | name: "should suppress errors for global selectors after ignore comment #5", 990 | options: { mode: "pure" }, 991 | input: `/* another comment */ /* cssmodules-pure-ignore */ 992 | :global(.foo) { color: blue; }`, 993 | expected: `/* another comment */ 994 | .foo { color: blue; }`, 995 | }, 996 | { 997 | name: "should suppress errors for global selectors after ignore comment #6", 998 | options: { mode: "pure" }, 999 | input: `.foo { /* cssmodules-pure-ignore */ :global(.bar) { color: blue }; }`, 1000 | expected: `:local(.foo) { .bar { color: blue }; }`, 1001 | }, 1002 | { 1003 | name: "should suppress errors for global selectors after ignore comment #7", 1004 | options: { mode: "pure" }, 1005 | input: `/* cssmodules-pure-ignore */ :global(.foo) { /* cssmodules-pure-ignore */ :global(.bar) { color: blue } }`, 1006 | expected: `.foo { .bar { color: blue } }`, 1007 | }, 1008 | { 1009 | name: "should suppress errors for global selectors after ignore comment #8", 1010 | options: { mode: "pure" }, 1011 | input: `/* cssmodules-pure-ignore */ :global(.foo) { color: blue; }`, 1012 | expected: `.foo { color: blue; }`, 1013 | }, 1014 | { 1015 | name: "should suppress errors for global selectors after ignore comment #9", 1016 | options: { mode: "pure" }, 1017 | input: `/* 1018 | cssmodules-pure-ignore 1019 | */ :global(.foo) { color: blue; }`, 1020 | expected: `.foo { color: blue; }`, 1021 | }, 1022 | { 1023 | name: "should allow additional text in ignore comment", 1024 | options: { mode: "pure" }, 1025 | input: `/* cssmodules-pure-ignore - needed for third party integration */ 1026 | :global(#foo) { color: blue; }`, 1027 | expected: `#foo { color: blue; }`, 1028 | }, 1029 | { 1030 | name: "should not affect rules after the ignored block", 1031 | options: { mode: "pure" }, 1032 | input: `/* cssmodules-pure-ignore */ 1033 | :global(.foo) { color: blue; } 1034 | :global(.bar) { color: red; }`, 1035 | error: /is not pure/, 1036 | }, 1037 | { 1038 | name: "should work with nested global selectors in ignored block", 1039 | options: { mode: "pure" }, 1040 | input: `/* cssmodules-pure-ignore */ 1041 | :global(.foo) { 1042 | :global(.bar) { color: blue; } 1043 | }`, 1044 | error: /is not pure/, 1045 | }, 1046 | { 1047 | name: "should work with ignored nested global selectors in ignored block", 1048 | options: { mode: "pure" }, 1049 | input: `/* cssmodules-pure-ignore */ 1050 | :global(.foo) { 1051 | /* cssmodules-pure-ignore */ 1052 | :global(.bar) { color: blue; } 1053 | }`, 1054 | expected: `.foo { 1055 | .bar { color: blue; } 1056 | }`, 1057 | }, 1058 | { 1059 | name: "should work with view transitions in ignored block", 1060 | options: { mode: "pure" }, 1061 | input: `/* cssmodules-pure-ignore */ 1062 | ::view-transition-group(modal) { 1063 | animation-duration: 300ms; 1064 | }`, 1065 | expected: `::view-transition-group(modal) { 1066 | animation-duration: 300ms; 1067 | }`, 1068 | }, 1069 | { 1070 | name: "should work with keyframes in ignored block", 1071 | options: { mode: "pure" }, 1072 | input: `/* cssmodules-pure-ignore */ 1073 | @keyframes :global(fadeOut) { 1074 | from { opacity: 1; } 1075 | to { opacity: 0; } 1076 | }`, 1077 | expected: `@keyframes fadeOut { 1078 | from { opacity: 1; } 1079 | to { opacity: 0; } 1080 | }`, 1081 | }, 1082 | { 1083 | name: "should work with scope in ignored block", 1084 | options: { mode: "pure" }, 1085 | input: ` 1086 | /* cssmodules-pure-ignore */ 1087 | @scope (:global(.foo)) to (:global(.bar)) { 1088 | .article-footer { 1089 | border: 5px solid black; 1090 | } 1091 | } 1092 | `, 1093 | expected: ` 1094 | @scope (.foo) to (.bar) { 1095 | :local(.article-footer) { 1096 | border: 5px solid black; 1097 | } 1098 | } 1099 | `, 1100 | }, 1101 | { 1102 | name: "should work with scope in ignored block #2", 1103 | options: { mode: "pure" }, 1104 | input: ` 1105 | /* cssmodules-pure-ignore */ 1106 | @scope (:global(.foo)) 1107 | to (:global(.bar)) { 1108 | .article-footer { 1109 | border: 5px solid black; 1110 | } 1111 | } 1112 | `, 1113 | expected: ` 1114 | @scope (.foo) to (.bar) { 1115 | :local(.article-footer) { 1116 | border: 5px solid black; 1117 | } 1118 | } 1119 | `, 1120 | }, 1121 | { 1122 | name: "should work in media queries", 1123 | options: { mode: "pure" }, 1124 | input: `@media (min-width: 768px) { 1125 | /* cssmodules-pure-ignore */ 1126 | :global(.foo) { color: blue; } 1127 | }`, 1128 | expected: `@media (min-width: 768px) { 1129 | .foo { color: blue; } 1130 | }`, 1131 | }, 1132 | { 1133 | name: "should handle multiple ignore comments", 1134 | options: { mode: "pure" }, 1135 | input: `/* cssmodules-pure-ignore */ 1136 | :global(.foo) { color: blue; } 1137 | .local { color: green; } 1138 | /* cssmodules-pure-ignore */ 1139 | :global(.bar) { color: red; }`, 1140 | expected: `.foo { color: blue; } 1141 | :local(.local) { color: green; } 1142 | .bar { color: red; }`, 1143 | }, 1144 | { 1145 | name: "should work with complex selectors in ignored block", 1146 | options: { mode: "pure" }, 1147 | input: `/* cssmodules-pure-ignore */ 1148 | :global(.foo):hover > :global(.bar) + :global(.baz) { 1149 | color: blue; 1150 | }`, 1151 | expected: `.foo:hover > .bar + .baz { 1152 | color: blue; 1153 | }`, 1154 | }, 1155 | { 1156 | name: "should work with multiple selectors in ignored block", 1157 | options: { mode: "pure" }, 1158 | input: `/* cssmodules-pure-ignore */ 1159 | :global(.foo), 1160 | :global(.bar), 1161 | :global(.baz) { 1162 | color: blue; 1163 | }`, 1164 | expected: `.foo, 1165 | .bar, 1166 | .baz { 1167 | color: blue; 1168 | }`, 1169 | }, 1170 | { 1171 | name: "should work with pseudo-elements in ignored block", 1172 | options: { mode: "pure" }, 1173 | input: `/* cssmodules-pure-ignore */ 1174 | :global(.foo)::before, 1175 | :global(.foo)::after { 1176 | content: ''; 1177 | }`, 1178 | expected: `.foo::before, 1179 | .foo::after { 1180 | content: ''; 1181 | }`, 1182 | }, 1183 | { 1184 | name: "should disable pure mode checks for entire file with no-check comment", 1185 | options: { mode: "pure" }, 1186 | input: `/* cssmodules-pure-no-check */ 1187 | :global(.foo) { border: 1px solid #e2e8f0 } 1188 | :global(.bar) { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1) } 1189 | :global(.baz) { background: #4299e1 }`, 1190 | expected: `/* cssmodules-pure-no-check */ 1191 | .foo { border: 1px solid #e2e8f0 } 1192 | .bar { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1) } 1193 | .baz { background: #4299e1 }`, 1194 | }, 1195 | { 1196 | name: "should disable pure mode checks for nested selectors", 1197 | options: { mode: "pure" }, 1198 | input: `/* cssmodules-pure-no-check */ 1199 | :global(.foo) { 1200 | &:hover { border-color: #cbd5e0 } 1201 | & :global(.bar) { color: blue } 1202 | }`, 1203 | expected: `/* cssmodules-pure-no-check */ 1204 | .foo { 1205 | &:hover { border-color: #cbd5e0 } 1206 | & .bar { color: blue } 1207 | }`, 1208 | }, 1209 | { 1210 | name: "should ignore no-check comment if not at root level", 1211 | options: { mode: "pure" }, 1212 | input: `:global(.bar) { color: blue } 1213 | /* cssmodules-pure-no-check */`, 1214 | error: /is not pure/, 1215 | }, 1216 | { 1217 | name: "should ignore no-check comment if not at root level #2", 1218 | options: { mode: "pure" }, 1219 | input: `/* Some file description */ 1220 | .class { color: red; } 1221 | /* cssmodules-pure-no-check */ 1222 | :global(.foo) { color: blue }`, 1223 | error: /is not pure/, 1224 | }, 1225 | { 1226 | name: "should allow other comments before no-check comment", 1227 | options: { mode: "pure" }, 1228 | input: `/* Some file description */ 1229 | /* cssmodules-pure-no-check */ 1230 | :global(.foo) { color: blue }`, 1231 | expected: `/* Some file description */ 1232 | /* cssmodules-pure-no-check */ 1233 | .foo { color: blue }`, 1234 | }, 1235 | { 1236 | name: "should disable pure mode checks for deep nested selectors", 1237 | options: { mode: "pure" }, 1238 | input: `/* cssmodules-pure-no-check */ 1239 | :global(.foo) { max-width: 600px } 1240 | :global(.bar) { background: #fafafa } 1241 | :global(.baz) { 1242 | :global(.foobar) { 1243 | &::-webkit-scrollbar { width: 8px } 1244 | } 1245 | }`, 1246 | expected: `/* cssmodules-pure-no-check */ 1247 | .foo { max-width: 600px } 1248 | .bar { background: #fafafa } 1249 | .baz { 1250 | .foobar { 1251 | &::-webkit-scrollbar { width: 8px } 1252 | } 1253 | }`, 1254 | }, 1255 | { 1256 | name: "should work with keyframes when no-check is enabled", 1257 | options: { mode: "pure" }, 1258 | input: `/* cssmodules-pure-no-check */ 1259 | @keyframes :global(fadeIn) { 1260 | from { opacity: 0 } 1261 | to { opacity: 1 } 1262 | } 1263 | :global(.animate) { animation: global(fadeIn) 0.3s }`, 1264 | expected: `/* cssmodules-pure-no-check */ 1265 | @keyframes fadeIn { 1266 | from { opacity: 0 } 1267 | to { opacity: 1 } 1268 | } 1269 | .animate { animation: fadeIn 0.3s }`, 1270 | }, 1271 | { 1272 | name: "should allow multiline no-check comment", 1273 | options: { mode: "pure" }, 1274 | input: `/* 1275 | cssmodules-pure-no-check 1276 | */ 1277 | :global(.foo) { color: blue }`, 1278 | expected: `/* 1279 | cssmodules-pure-no-check 1280 | */ 1281 | .foo { color: blue }`, 1282 | }, 1283 | { 1284 | name: "should allow additional text in no-check comment", 1285 | options: { mode: "pure" }, 1286 | input: `/* cssmodules-pure-no-check - needed for styling third-party components */ 1287 | :global(.foo) { color: blue }`, 1288 | expected: `/* cssmodules-pure-no-check - needed for styling third-party components */ 1289 | .foo { color: blue }`, 1290 | }, 1291 | { 1292 | name: "should work with media queries when no-check is enabled", 1293 | options: { mode: "pure" }, 1294 | input: `/* cssmodules-pure-no-check */ 1295 | @media (max-width: 768px) { 1296 | :global(.foo) { position: fixed } 1297 | }`, 1298 | expected: `/* cssmodules-pure-no-check */ 1299 | @media (max-width: 768px) { 1300 | .foo { position: fixed } 1301 | }`, 1302 | }, 1303 | { 1304 | name: "css nesting", 1305 | input: ` 1306 | .foo { 1307 | &.class { 1308 | a_value: some-value; 1309 | } 1310 | 1311 | @media screen and (min-width: 900px) { 1312 | b_value: some-value; 1313 | 1314 | .bar { 1315 | c_value: some-value; 1316 | } 1317 | 1318 | &.baz { 1319 | c_value: some-value; 1320 | } 1321 | } 1322 | }`, 1323 | expected: ` 1324 | :local(.foo) { 1325 | &:local(.class) { 1326 | a_value: some-value; 1327 | } 1328 | 1329 | @media screen and (min-width: 900px) { 1330 | b_value: some-value; 1331 | 1332 | :local(.bar) { 1333 | c_value: some-value; 1334 | } 1335 | 1336 | &:local(.baz) { 1337 | c_value: some-value; 1338 | } 1339 | } 1340 | }`, 1341 | }, 1342 | { 1343 | name: "css nesting #1", 1344 | options: { mode: "global" }, 1345 | input: ` 1346 | :local(.foo) { 1347 | &:local(.class) { 1348 | a_value: some-value; 1349 | } 1350 | 1351 | @media screen and (min-width: 900px) { 1352 | b_value: some-value; 1353 | 1354 | :local(.bar) { 1355 | c_value: some-value; 1356 | } 1357 | 1358 | &:local(.baz) { 1359 | c_value: some-value; 1360 | } 1361 | } 1362 | }`, 1363 | expected: ` 1364 | :local(.foo) { 1365 | &:local(.class) { 1366 | a_value: some-value; 1367 | } 1368 | 1369 | @media screen and (min-width: 900px) { 1370 | b_value: some-value; 1371 | 1372 | :local(.bar) { 1373 | c_value: some-value; 1374 | } 1375 | 1376 | &:local(.baz) { 1377 | c_value: some-value; 1378 | } 1379 | } 1380 | }`, 1381 | }, 1382 | { 1383 | name: "css nesting #2", 1384 | options: { mode: "pure" }, 1385 | input: ` 1386 | .foo { 1387 | &.class { 1388 | a_value: some-value; 1389 | } 1390 | 1391 | @media screen and (min-width: 900px) { 1392 | b_value: some-value; 1393 | 1394 | .bar { 1395 | c_value: some-value; 1396 | } 1397 | 1398 | &.baz { 1399 | c_value: some-value; 1400 | } 1401 | } 1402 | }`, 1403 | expected: ` 1404 | :local(.foo) { 1405 | &:local(.class) { 1406 | a_value: some-value; 1407 | } 1408 | 1409 | @media screen and (min-width: 900px) { 1410 | b_value: some-value; 1411 | 1412 | :local(.bar) { 1413 | c_value: some-value; 1414 | } 1415 | 1416 | &:local(.baz) { 1417 | c_value: some-value; 1418 | } 1419 | } 1420 | }`, 1421 | }, 1422 | { 1423 | name: "css nesting #3", 1424 | input: ".foo { span { a_value: some-value; } }", 1425 | options: { mode: "pure" }, 1426 | expected: ":local(.foo) { span { a_value: some-value; } }", 1427 | }, 1428 | { 1429 | name: "css nesting (unfolded) #3", 1430 | input: ".foo span { a_value: some-value }", 1431 | options: { mode: "pure" }, 1432 | expected: ":local(.foo) span { a_value: some-value }", 1433 | }, 1434 | { 1435 | name: "css nesting #4", 1436 | input: ".foo { span { a { a_value: some-value; } } }", 1437 | options: { mode: "pure" }, 1438 | expected: ":local(.foo) { span { a { a_value: some-value; } } }", 1439 | }, 1440 | { 1441 | name: "css nesting (unfolded) #4", 1442 | input: ".foo span a { a_value: some-value }", 1443 | options: { mode: "pure" }, 1444 | expected: ":local(.foo) span a { a_value: some-value }", 1445 | }, 1446 | { 1447 | name: "css nesting #5", 1448 | input: "html { .foo { a_value: some-value; } }", 1449 | options: { mode: "pure" }, 1450 | expected: "html { :local(.foo) { a_value: some-value; } }", 1451 | }, 1452 | { 1453 | name: "css nesting (unfolded) #5", 1454 | input: "html .foo { a_value: some-value }", 1455 | options: { mode: "pure" }, 1456 | expected: "html :local(.foo) { a_value: some-value }", 1457 | }, 1458 | { 1459 | name: "css nesting #6", 1460 | input: 1461 | "html { @media screen and (min-width: 900px) { .foo { a_value: some-value; } } }", 1462 | options: { mode: "pure" }, 1463 | expected: 1464 | "html { @media screen and (min-width: 900px) { :local(.foo) { a_value: some-value; } } }", 1465 | }, 1466 | { 1467 | name: "css nesting (unfolded) #6", 1468 | input: 1469 | "@media screen and (min-width: 900px) { html .foo { a_value: some-value } }", 1470 | options: { mode: "pure" }, 1471 | expected: 1472 | "@media screen and (min-width: 900px) { html :local(.foo) { a_value: some-value } }", 1473 | }, 1474 | { 1475 | name: "css nesting #7", 1476 | input: 1477 | "html { .foo { a_value: some-value; } .bar { a_value: some-value; } }", 1478 | options: { mode: "pure" }, 1479 | expected: 1480 | "html { :local(.foo) { a_value: some-value; } :local(.bar) { a_value: some-value; } }", 1481 | }, 1482 | { 1483 | name: "css nesting (unfolded) #7", 1484 | input: "html .foo, html .bar { a_value: some-value }", 1485 | options: { mode: "pure" }, 1486 | expected: "html :local(.foo), html :local(.bar) { a_value: some-value }", 1487 | }, 1488 | { 1489 | name: "css nesting #8", 1490 | input: 1491 | ".class { @media screen and (min-width: 900px) { & > span { a_value: some-value; } } }", 1492 | options: { mode: "pure" }, 1493 | expected: 1494 | ":local(.class) { @media screen and (min-width: 900px) { & > span { a_value: some-value; } } }", 1495 | }, 1496 | { 1497 | name: "css nesting (unfolded) #8", 1498 | input: 1499 | "@media screen and (min-width: 900px) { .class > span { a_value: some-value } }", 1500 | options: { mode: "pure" }, 1501 | expected: 1502 | "@media screen and (min-width: 900px) { :local(.class) > span { a_value: some-value } }", 1503 | }, 1504 | { 1505 | name: "css nesting #9", 1506 | input: 1507 | "html { @media screen and (min-width: 900px) { & > .class { a_value: some-value; } } }", 1508 | options: { mode: "pure" }, 1509 | expected: 1510 | "html { @media screen and (min-width: 900px) { & > :local(.class) { a_value: some-value; } } }", 1511 | }, 1512 | { 1513 | name: "css nesting (unfolded) #9", 1514 | input: 1515 | "@media screen and (min-width: 900px) { html > .class { a_value: some-value } }", 1516 | options: { mode: "pure" }, 1517 | expected: 1518 | "@media screen and (min-width: 900px) { html > :local(.class) { a_value: some-value } }", 1519 | }, 1520 | { 1521 | name: "css nesting #10", 1522 | input: 1523 | ".class { @media screen and (min-width: 900px) { & { a_value: some-value; } } }", 1524 | options: { mode: "pure" }, 1525 | expected: 1526 | ":local(.class) { @media screen and (min-width: 900px) { & { a_value: some-value; } } }", 1527 | }, 1528 | { 1529 | name: "css nesting (unfolded) #10", 1530 | input: 1531 | "@media screen and (min-width: 900px) { .class { a_value: some-value } }", 1532 | options: { mode: "pure" }, 1533 | expected: 1534 | "@media screen and (min-width: 900px) { :local(.class) { a_value: some-value } }", 1535 | }, 1536 | { 1537 | name: "css nesting #11", 1538 | input: "html { .foo { span { a_value: some-value; } } }", 1539 | options: { mode: "pure" }, 1540 | expected: "html { :local(.foo) { span { a_value: some-value; } } }", 1541 | }, 1542 | { 1543 | name: "css nesting (unfolded) #11", 1544 | input: "html .foo span { a_value: some-value }", 1545 | options: { mode: "pure" }, 1546 | expected: "html :local(.foo) span { a_value: some-value }", 1547 | }, 1548 | { 1549 | name: "css nesting #12", 1550 | input: "html { button { .foo { div { span { a_value: some-value; } } } } }", 1551 | options: { mode: "pure" }, 1552 | expected: 1553 | "html { button { :local(.foo) { div { span { a_value: some-value; } } } } }", 1554 | }, 1555 | { 1556 | name: "css nesting #13", 1557 | input: ".foo { button { div { div { span { a_value: some-value; } } } } }", 1558 | options: { mode: "pure" }, 1559 | expected: 1560 | ":local(.foo) { button { div { div { span { a_value: some-value; } } } } }", 1561 | }, 1562 | { 1563 | name: "css nesting #14", 1564 | input: "html { button { div { div { .foo { a_value: some-value; } } } } }", 1565 | options: { mode: "pure" }, 1566 | expected: 1567 | "html { button { div { div { :local(.foo) { a_value: some-value; } } } } }", 1568 | }, 1569 | { 1570 | name: "css nesting #15", 1571 | input: 1572 | "html { button { @media screen and (min-width: 900px) { .foo { div { span { a_value: some-value; } } } } } }", 1573 | options: { mode: "pure" }, 1574 | expected: 1575 | "html { button { @media screen and (min-width: 900px) { :local(.foo) { div { span { a_value: some-value; } } } } } }", 1576 | }, 1577 | { 1578 | name: "css nesting #16", 1579 | input: "html { .foo { a_value: some-value; } }", 1580 | options: { mode: "pure" }, 1581 | expected: "html { :local(.foo) { a_value: some-value; } }", 1582 | }, 1583 | { 1584 | name: "css nesting #17", 1585 | input: ".foo { div { a_value: some-value; } }", 1586 | options: { mode: "pure" }, 1587 | expected: ":local(.foo) { div { a_value: some-value; } }", 1588 | }, 1589 | { 1590 | name: "css nesting #18", 1591 | input: 1592 | "@media screen and (min-width: 900px) { html { .foo { a_value: some-value; } } }", 1593 | options: { mode: "pure" }, 1594 | expected: 1595 | "@media screen and (min-width: 900px) { html { :local(.foo) { a_value: some-value; } } }", 1596 | }, 1597 | { 1598 | name: "css nesting #19", 1599 | input: 1600 | "html { @media screen and (min-width: 900px) { .foo { a_value: some-value; } } }", 1601 | options: { mode: "pure" }, 1602 | expected: 1603 | "html { @media screen and (min-width: 900px) { :local(.foo) { a_value: some-value; } } }", 1604 | }, 1605 | { 1606 | name: "css nesting #20", 1607 | input: 1608 | "html { .foo { @media screen and (min-width: 900px) { a_value: some-value; } } }", 1609 | options: { mode: "pure" }, 1610 | expected: 1611 | "html { :local(.foo) { @media screen and (min-width: 900px) { a_value: some-value; } } }", 1612 | }, 1613 | { 1614 | name: "css nesting #21", 1615 | input: 1616 | "@media screen and (min-width: 900px) { .foo { div { a_value: some-value; } } }", 1617 | options: { mode: "pure" }, 1618 | expected: 1619 | "@media screen and (min-width: 900px) { :local(.foo) { div { a_value: some-value; } } }", 1620 | }, 1621 | { 1622 | name: "css nesting #22", 1623 | input: 1624 | ".foo { @media screen and (min-width: 900px) { div { a_value: some-value; } } }", 1625 | options: { mode: "pure" }, 1626 | expected: 1627 | ":local(.foo) { @media screen and (min-width: 900px) { div { a_value: some-value; } } }", 1628 | }, 1629 | { 1630 | name: "css nesting #23", 1631 | input: 1632 | ".foo { div { @media screen and (min-width: 900px) { a_value: some-value; } } }", 1633 | options: { mode: "pure" }, 1634 | expected: 1635 | ":local(.foo) { div { @media screen and (min-width: 900px) { a_value: some-value; } } }", 1636 | }, 1637 | { 1638 | name: "css nesting - throw on mixed parents", 1639 | input: ".foo, html { span { a_value: some-value; } }", 1640 | options: { mode: "pure" }, 1641 | error: /is not pure/, 1642 | }, 1643 | { 1644 | name: "css nesting - throw on &", 1645 | input: "html { & > span { a_value: some-value; } }", 1646 | options: { mode: "pure" }, 1647 | error: /is not pure/, 1648 | }, 1649 | { 1650 | name: "css nesting - throw on & #2", 1651 | input: "html { button { & > span { a_value: some-value; } } }", 1652 | options: { mode: "pure" }, 1653 | error: /is not pure/, 1654 | }, 1655 | { 1656 | name: "css nesting - throw on & #3", 1657 | input: 1658 | "html { @media screen and (min-width: 900px) { & > span { a_value: some-value; } } }", 1659 | options: { mode: "pure" }, 1660 | error: /is not pure/, 1661 | }, 1662 | { 1663 | name: "css nesting - throw on & #4", 1664 | input: "html { button { div { div { & { a_value: some-value; } } } } }", 1665 | options: { mode: "pure" }, 1666 | error: /is not pure/, 1667 | }, 1668 | { 1669 | name: "css nesting - throw", 1670 | input: "html { button { div { div { div { a_value: some-value; } } } } }", 1671 | options: { mode: "pure" }, 1672 | error: /is not pure/, 1673 | }, 1674 | { 1675 | name: "css nesting - throw #2", 1676 | input: "html { button { div { div { div { } } } } }", 1677 | options: { mode: "pure" }, 1678 | error: /is not pure/, 1679 | }, 1680 | { 1681 | name: "css nesting - throw #3", 1682 | input: 1683 | "html { button { @media screen and (min-width: 900px) { div { div { div { } } } } } }", 1684 | options: { mode: "pure" }, 1685 | error: /is not pure/, 1686 | }, 1687 | { 1688 | name: "css nesting - throw #4", 1689 | input: 1690 | "@media screen and (min-width: 900px) { html { button { div { div { div { } } } } } }", 1691 | options: { mode: "pure" }, 1692 | error: /is not pure/, 1693 | }, 1694 | { 1695 | name: "css nesting - throw #5", 1696 | input: 1697 | "html { div { @media screen and (min-width: 900px) { color: red } } }", 1698 | options: { mode: "pure" }, 1699 | error: /is not pure/, 1700 | }, 1701 | { 1702 | name: "css nesting - throw #6", 1703 | input: 1704 | "html { div { @media screen and (min-width: 900px) { @media screen and (min-width: 900px) { color: red } } } }", 1705 | options: { mode: "pure" }, 1706 | error: /is not pure/, 1707 | }, 1708 | { 1709 | name: "css nesting - throw #7", 1710 | input: 1711 | "html { div { @media screen and (min-width: 900px) { .a { } @media screen and (min-width: 900px) { color: red } } } }", 1712 | options: { mode: "pure" }, 1713 | error: /is not pure/, 1714 | }, 1715 | { 1716 | name: "css nesting - throw #7", 1717 | input: 1718 | "html { div { @media screen and (min-width: 900px) { .a { a_value: some-value; } @media screen and (min-width: 900px) { color: red } } } }", 1719 | options: { mode: "pure" }, 1720 | error: /is not pure/, 1721 | }, 1722 | { 1723 | name: "css nesting - throw #8", 1724 | input: ` 1725 | @media screen and (min-width: 900px) { 1726 | .a { a_value: some-value; } 1727 | @media screen and (min-width: 900px) { 1728 | div { 1729 | color: red 1730 | } 1731 | } 1732 | }`, 1733 | options: { mode: "pure" }, 1734 | error: /is not pure/, 1735 | }, 1736 | { 1737 | name: "css nesting - throw on global styles with a local selector", 1738 | input: `html { a_value: some-value; .foo { a_value: some-value; } }`, 1739 | options: { mode: "pure" }, 1740 | error: /is not pure/, 1741 | }, 1742 | { 1743 | name: "css nesting - throw on global styles with a local selector #2", 1744 | input: `html { .foo { a_value: some-value; } a_value: some-value; }`, 1745 | options: { mode: "pure" }, 1746 | error: /is not pure/, 1747 | }, 1748 | { 1749 | name: "css nesting - throw on global styles with a local selector #3", 1750 | input: ` 1751 | html { 1752 | .foo { a_value: some-value; } 1753 | button { 1754 | color: red; 1755 | .bar { a_value: some-value; } 1756 | } 1757 | }`, 1758 | options: { mode: "pure" }, 1759 | error: /is not pure/, 1760 | }, 1761 | { 1762 | name: "css nesting - throw on global styles with a local selector #4", 1763 | input: ` 1764 | html { 1765 | @media screen and (min-width: 900px) { 1766 | button { 1767 | color: red; 1768 | .bar { a_value: some-value; } 1769 | } 1770 | } 1771 | }`, 1772 | options: { mode: "pure" }, 1773 | error: /is not pure/, 1774 | }, 1775 | /* 1776 | Bug in postcss-selector-parser 1777 | { 1778 | name: 'throw on invalid global class usage', 1779 | input: ':global() {}', 1780 | error: /:global\(\) can't be empty/ 1781 | }, 1782 | */ 1783 | { 1784 | name: "consider :import statements pure", 1785 | input: ':import("~/lol.css") { foo: __foo; }', 1786 | options: { mode: "pure" }, 1787 | expected: ':import("~/lol.css") { foo: __foo; }', 1788 | }, 1789 | { 1790 | name: "consider :export statements pure", 1791 | input: ":export { foo: __foo; }", 1792 | options: { mode: "pure" }, 1793 | expected: ":export { foo: __foo; }", 1794 | }, 1795 | { 1796 | name: "handle negative animation-delay in animation shorthand", 1797 | input: ".foo { animation: 1s -500ms; }", 1798 | expected: ":local(.foo) { animation: 1s -500ms; }", 1799 | }, 1800 | { 1801 | name: "handle negative animation-delay in animation shorthand #1", 1802 | input: ".foo { animation: 1s -500.0ms; }", 1803 | expected: ":local(.foo) { animation: 1s -500.0ms; }", 1804 | }, 1805 | { 1806 | name: "handle negative animation-delay in animation shorthand #2", 1807 | input: ".foo { animation: 1s -500.0ms -a_value; }", 1808 | expected: ":local(.foo) { animation: 1s -500.0ms :local(-a_value); }", 1809 | }, 1810 | { 1811 | name: "@scope at-rule", 1812 | input: ` 1813 | .article-header { 1814 | color: red; 1815 | } 1816 | 1817 | .article-body { 1818 | color: blue; 1819 | } 1820 | 1821 | @scope (.article-body) to (.article-header) { 1822 | .article-body { 1823 | border: 5px solid black; 1824 | background-color: goldenrod; 1825 | } 1826 | } 1827 | 1828 | @scope(.article-body)to(.article-header){ 1829 | .article-footer { 1830 | border: 5px solid black; 1831 | } 1832 | } 1833 | 1834 | @scope ( .article-body ) { 1835 | img { 1836 | border: 5px solid black; 1837 | background-color: goldenrod; 1838 | } 1839 | } 1840 | 1841 | @scope { 1842 | :scope { 1843 | color: red; 1844 | } 1845 | } 1846 | `, 1847 | expected: ` 1848 | :local(.article-header) { 1849 | color: red; 1850 | } 1851 | 1852 | :local(.article-body) { 1853 | color: blue; 1854 | } 1855 | 1856 | @scope (:local(.article-body)) to (:local(.article-header)) { 1857 | :local(.article-body) { 1858 | border: 5px solid black; 1859 | background-color: goldenrod; 1860 | } 1861 | } 1862 | 1863 | @scope(:local(.article-body)) to (:local(.article-header)){ 1864 | :local(.article-footer) { 1865 | border: 5px solid black; 1866 | } 1867 | } 1868 | 1869 | @scope (:local(.article-body)) { 1870 | img { 1871 | border: 5px solid black; 1872 | background-color: goldenrod; 1873 | } 1874 | } 1875 | 1876 | @scope { 1877 | :scope { 1878 | color: red; 1879 | } 1880 | } 1881 | `, 1882 | }, 1883 | { 1884 | name: "@scope at-rule #1", 1885 | input: ` 1886 | @scope (.article-body) to (figure) { 1887 | .article-footer { 1888 | border: 5px solid black; 1889 | } 1890 | } 1891 | `, 1892 | expected: ` 1893 | @scope (:local(.article-body)) to (figure) { 1894 | :local(.article-footer) { 1895 | border: 5px solid black; 1896 | } 1897 | } 1898 | `, 1899 | }, 1900 | { 1901 | name: "@scope at-rule #2", 1902 | input: ` 1903 | @scope (:local(.article-body)) to (:global(.class)) { 1904 | .article-footer { 1905 | border: 5px solid black; 1906 | } 1907 | :local(.class-1) { 1908 | color: red; 1909 | } 1910 | :global(.class-2) { 1911 | color: blue; 1912 | } 1913 | } 1914 | `, 1915 | expected: ` 1916 | @scope (:local(.article-body)) to (.class) { 1917 | :local(.article-footer) { 1918 | border: 5px solid black; 1919 | } 1920 | :local(.class-1) { 1921 | color: red; 1922 | } 1923 | .class-2 { 1924 | color: blue; 1925 | } 1926 | } 1927 | `, 1928 | }, 1929 | { 1930 | name: "@scope at-rule #3", 1931 | options: { mode: "global" }, 1932 | input: ` 1933 | @scope (:local(.article-header)) to (:global(.class)) { 1934 | .article-footer { 1935 | border: 5px solid black; 1936 | } 1937 | :local(.class-1) { 1938 | color: red; 1939 | } 1940 | :global(.class-2) { 1941 | color: blue; 1942 | } 1943 | } 1944 | `, 1945 | expected: ` 1946 | @scope (:local(.article-header)) to (.class) { 1947 | .article-footer { 1948 | border: 5px solid black; 1949 | } 1950 | :local(.class-1) { 1951 | color: red; 1952 | } 1953 | .class-2 { 1954 | color: blue; 1955 | } 1956 | } 1957 | `, 1958 | }, 1959 | { 1960 | name: "@scope at-rule #4", 1961 | options: { mode: "pure" }, 1962 | input: ` 1963 | @scope (.article-header) to (.class) { 1964 | .article-footer { 1965 | border: 5px solid black; 1966 | } 1967 | .class-1 { 1968 | color: red; 1969 | } 1970 | .class-2 { 1971 | color: blue; 1972 | } 1973 | } 1974 | `, 1975 | expected: ` 1976 | @scope (:local(.article-header)) to (:local(.class)) { 1977 | :local(.article-footer) { 1978 | border: 5px solid black; 1979 | } 1980 | :local(.class-1) { 1981 | color: red; 1982 | } 1983 | :local(.class-2) { 1984 | color: blue; 1985 | } 1986 | } 1987 | `, 1988 | }, 1989 | { 1990 | name: "@scope at-rule #5", 1991 | input: ` 1992 | @scope (.article-header) to (.class) { 1993 | .article-footer { 1994 | src: url("./font.woff"); 1995 | } 1996 | } 1997 | `, 1998 | options: { 1999 | rewriteUrl: function (global, url) { 2000 | const mode = global ? "global" : "local"; 2001 | return "(" + mode + ")" + url + '"' + mode + '"'; 2002 | }, 2003 | }, 2004 | expected: ` 2005 | @scope (:local(.article-header)) to (:local(.class)) { 2006 | :local(.article-footer) { 2007 | src: url("(local)./font.woff\\"local\\""); 2008 | } 2009 | } 2010 | `, 2011 | }, 2012 | { 2013 | name: "@scope at-rule #6", 2014 | input: ` 2015 | .foo { 2016 | @scope (.article-header) to (.class) { 2017 | :scope { 2018 | background: blue; 2019 | } 2020 | 2021 | .bar { 2022 | color: red; 2023 | } 2024 | } 2025 | } 2026 | `, 2027 | expected: ` 2028 | :local(.foo) { 2029 | @scope (:local(.article-header)) to (:local(.class)) { 2030 | :scope { 2031 | background: blue; 2032 | } 2033 | 2034 | :local(.bar) { 2035 | color: red; 2036 | } 2037 | } 2038 | } 2039 | `, 2040 | }, 2041 | { 2042 | name: "@scope at-rule #7", 2043 | options: { mode: "pure" }, 2044 | input: ` 2045 | @scope (:global(.article-header).foo) to (:global(.class).bar) { 2046 | .bar { 2047 | color: red; 2048 | } 2049 | } 2050 | `, 2051 | expected: ` 2052 | @scope (.article-header:local(.foo)) to (.class:local(.bar)) { 2053 | :local(.bar) { 2054 | color: red; 2055 | } 2056 | } 2057 | `, 2058 | }, 2059 | ]; 2060 | 2061 | function process(css, options) { 2062 | return postcss(plugin(options)).process(css).css; 2063 | } 2064 | 2065 | describe(name, function () { 2066 | it("should use the postcss plugin api", function () { 2067 | expect(plugin().postcssPlugin).toBe(name); 2068 | }); 2069 | 2070 | tests.forEach(function (testCase) { 2071 | it(testCase.name, () => { 2072 | const { options, error, input } = testCase; 2073 | 2074 | if (error) { 2075 | expect(() => { 2076 | process(input, options); 2077 | }).toThrow(testCase.error); 2078 | } else { 2079 | const { expected } = testCase; 2080 | 2081 | expect(expected).toBe(process(input, options)); 2082 | } 2083 | }); 2084 | }); 2085 | }); 2086 | --------------------------------------------------------------------------------