├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json └── postcss-runner.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dominik Ferber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jest-transform-css 2 | 3 | A Jest transformer which enables importing CSS into Jest's `jsdom`. 4 | 5 | **If you are not here for Visual Regression Testing, but just want to make your tests work with CSS Modules, then you are likley looking for https://github.com/keyanzhang/identity-obj-proxy/.** 6 | 7 | **This package is no longer maintained** 8 | 9 | ## Description 10 | 11 | When you want to do Visual Regression Testing in Jest, it is important that the CSS of components is available to the test setup. So far, CSS was not part of tests as it was mocked away by using `moduleNameMapper` like a file-mock or `identity-obj-proxy`. 12 | 13 | `jest-transform-css` is intended to be used in an `jsdom` environment. When any component imports CSS in the test environment, then the loaded CSS will get added to `jsdom` using [`style-inject`](https://github.com/egoist/style-inject) - just like the Webpack CSS loader would do in a production environment. This means the full styles are added to `jsdom`. 14 | 15 | This doesn't make much sense at first, as `jsdom` is headless (non-visual). However, we can copy the resulting document markup ("the HTML") of `jsdom` and copy it to a [`puppeteer`](https://github.com/googlechrome/puppeteer/) instance. We can let the markup render there and take a screenshot there. The [`jsdom-screenshot`](https://github.com/dferber90/jsdom-screenshot) package does exactly this. 16 | 17 | Once we obtained a screenshot, we can compare it to the last version of that screenshot we took, and make tests fail in case they did. The [`jest-image-snapshot`](https://github.com/americanexpress/jest-image-snapshot) plugin does that. 18 | 19 | ## Setup 20 | 21 | ### Installation 22 | 23 | ```bash 24 | yarn add jest-transform-css --dev 25 | ``` 26 | 27 | The old setup of CSS in jest needs to be removed, and the new setup needs to be added next. 28 | 29 | ### Removing module name mapping 30 | 31 | If your project is using plain CSS imported in the components, then you're likely using a mock file. You can remove that configuration. 32 | 33 | ```diff 34 | // in the Jest config 35 | "moduleNameMapper": { 36 | - "\\.(css|less)$": "/__mocks__/styleMock.js" 37 | }, 38 | ``` 39 | 40 | If your project is using CSS Modules, then it's likely that `identity-obj-proxy` is configured. It needs to be removed in order for the styles of the `jest-transform-css` to apply. 41 | 42 | So, remove these lines from `jest.config.js`: 43 | 44 | ```diff 45 | // in the Jest config 46 | "moduleNameMapper": { 47 | - "\\.(s?css|less)$": "identity-obj-proxy" 48 | }, 49 | ``` 50 | 51 | ### Adding `transform` 52 | 53 | Open `jest.config.js` and modify the `transform`: 54 | 55 | ``` 56 | // in the Jest config 57 | transform: { 58 | "^.+\\.js$": "babel-jest", 59 | "^.+\\.css$": "jest-transform-css" 60 | } 61 | ``` 62 | 63 | > Notice that `babel-jest` gets added as well. 64 | > 65 | > The `babel-jest` code preprocessor is enabled by default, when no other preprocessors are added. As `jest-transform-css` is a code preprocessor, `babel-jest` gets disabled when `jest-transform-css` is added. 66 | > 67 | > So it needs to be added again manually. 68 | > 69 | > See https://github.com/facebook/jest/tree/master/packages/babel-jest#setup 70 | 71 | ### Enabling CSS modules 72 | 73 | By default, `jest-transform-css` will treat every file it transforms as a regular CSS file. 74 | 75 | You need to opt into css-modules mode by specifying it in the configuration. 76 | Add `{ modules: true }` option to `jest-transform-css` in `jest.config.js`: 77 | 78 | ```diff 79 | // in the Jest config 80 | transform: { 81 | - "^.+\\.css$": "jest-transform-css" 82 | + "^.+\\.css$": ["jest-transform-css", { modules: true }] 83 | } 84 | ``` 85 | 86 | This will enable CSS module transformation by `jest-transform-css` for all CSS files matching the pattern. 87 | 88 | The config also supports `generateScopedName` property to customize the generated class names. Helpful when using Jest Snapshots and not wanting unnecessary noise from hash generated classnames. 89 | 90 | ``` 91 | // in the Jest config 92 | transform: { 93 | "^.+\\.css$": ["jest-transform-css", { 94 | modules: true, 95 | generateScopedName: "[path]_[name]_[local]" 96 | // Default value is: '[path][local]-[hash:base64:10]' 97 | }] 98 | } 99 | ``` 100 | 101 | Link to all available [placeholder tokens](https://github.com/webpack/loader-utils#interpolatename) \*Note not all placeholders are working and must be tested. 102 | 103 | ## Further setup 104 | 105 | There are many ways to set up styles in a project (CSS modules, global styles, external global styles, local global styles, CSS in JS, LESS, SASS just to name a few). How to continue from here depends on your project. 106 | 107 | ### PostCSS 108 | 109 | If your setup is using `PostCSS` then you should add a `postcss.config.js` at the root of your folder. 110 | 111 | You can apply certain plugins only when `process.env.NODE_ENV === 'test'`. Ensure that valid CSS can be generated. 112 | 113 | > `jest-transform-css` is likley not flexible enough yet to support more sophisticated PostCSS configurations. However, we should be able to add this functionality by extending the configuration file. Feel free to open an issue with your setup and we'll try to support it. 114 | 115 | ### css-loader 116 | 117 | If your setup is using `css-loader` only, without PostCSS then you should be fine. 118 | If you have `modules: true` enabled in `css-loader`, you need to also enable it for `jest-transform-css` (see "Enabling CSS modules"). When components import CSS modules in the test environment, then the CSS is transformed through PostCSS's `cssModules` plugin to generate the classnames. It also injects the styles into `jsdom`. 119 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const crypto = require("crypto"); 3 | const crossSpawn = require("cross-spawn"); 4 | const path = require("path"); 5 | const stripIndent = require("common-tags/lib/stripIndent"); 6 | const THIS_FILE = fs.readFileSync(__filename); 7 | 8 | module.exports = { 9 | getCacheKey: (fileData, filename, configString, options) => { 10 | // Jest 27 passes a single options bag which contains `configString` rather than as a separate argument 11 | if (!options) { 12 | options = configString; 13 | configString = options.configString; 14 | } 15 | const { instrument } = options; 16 | return ( 17 | crypto 18 | .createHash("md5") 19 | .update(THIS_FILE) 20 | .update("\0", "utf8") 21 | .update(fileData) 22 | .update("\0", "utf8") 23 | .update(filename) 24 | .update("\0", "utf8") 25 | .update(configString) 26 | .update("\0", "utf8") 27 | .update(JSON.stringify(options.transformerConfig)) 28 | // TODO load postcssrc (the config) sync and make it part of the cache 29 | // key 30 | // .update("\0", "utf8") 31 | // .update(getPostCssConfig(filename)) 32 | .update("\0", "utf8") 33 | .update(instrument ? "instrument" : "") 34 | .digest("hex") 35 | ); 36 | }, 37 | 38 | process: (src, filename, config, options) => { 39 | // skip when plain CSS is used 40 | // You can pass config to the transformer in jest.config.js like so: 41 | // "^.+\\.css$": ["jest-transform-css", { modules: true }] 42 | // to enable css module transformation. 43 | const useModules = 44 | config && 45 | config.transformerConfig && 46 | ((typeof config.transformerConfig.modules === "boolean" && 47 | config.transformerConfig.modules)); 48 | if (!useModules) { 49 | return { 50 | code: stripIndent` 51 | const styleInject = require('style-inject'); 52 | 53 | styleInject(${JSON.stringify(src)}); 54 | module.exports = {}; 55 | `, 56 | }; 57 | } 58 | 59 | // The "process" function of this Jest transform must be sync, 60 | // but postcss is async. So we spawn a sync process to do an sync 61 | // transformation! 62 | // https://twitter.com/kentcdodds/status/1043194634338324480 63 | const postcssRunner = JSON.stringify(path.join(__dirname, "postcss-runner.js")); 64 | const result = crossSpawn.sync("node", [ 65 | "-e", 66 | stripIndent` 67 | require(${postcssRunner})( 68 | ${JSON.stringify({ 69 | src, 70 | filename, 71 | transformConfig: config.transformerConfig, 72 | // options 73 | })} 74 | ) 75 | .then(out => { console.log(JSON.stringify(out)) }) 76 | `, 77 | ]); 78 | 79 | // check for errors of postcss-runner.js 80 | const error = result.stderr.toString(); 81 | if (result.status !== 0 && error) throw error; 82 | 83 | // read results of postcss-runner.js from stdout 84 | let css; 85 | let tokens; 86 | try { 87 | // we likely logged something to the console from postcss-runner 88 | // in order to debug, and hence the parsing fails! 89 | const parsed = JSON.parse(result.stdout.toString()); 90 | css = parsed.css; 91 | tokens = parsed.tokens; 92 | if (Array.isArray(parsed.warnings)) 93 | parsed.warnings.forEach((warning) => { 94 | console.warn(warning); 95 | }); 96 | } catch (error) { 97 | // we forward the logs and return no mappings 98 | console.error(result.stderr.toString()); 99 | console.log(result.stdout.toString()); 100 | return { 101 | code: stripIndent` 102 | console.error("transform-css: Failed to load '${filename}'"); 103 | module.exports = {}; 104 | `, 105 | }; 106 | } 107 | 108 | // Finally, inject the styles to the document 109 | return { 110 | code: stripIndent` 111 | const styleInject = require('style-inject'); 112 | 113 | styleInject(${JSON.stringify(css)}); 114 | module.exports = ${JSON.stringify(tokens)}; 115 | `, 116 | }; 117 | }, 118 | }; 119 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-transform-css", 3 | "version": "6.0.3", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "jest-transform-css", 9 | "version": "6.0.3", 10 | "license": "MIT", 11 | "dependencies": { 12 | "common-tags": "1.8.2", 13 | "cross-spawn": "^7.0.6", 14 | "postcss-load-config": "4.0.1", 15 | "postcss-modules": "4.3.1", 16 | "style-inject": "0.3.0" 17 | }, 18 | "devDependencies": { 19 | "postcss": "^8.4.12" 20 | }, 21 | "peerDependencies": { 22 | "postcss": "^8.4.12" 23 | } 24 | }, 25 | "node_modules/common-tags": { 26 | "version": "1.8.2", 27 | "license": "MIT", 28 | "engines": { 29 | "node": ">=4.0.0" 30 | } 31 | }, 32 | "node_modules/cross-spawn": { 33 | "version": "7.0.6", 34 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 35 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 36 | "dependencies": { 37 | "path-key": "^3.1.0", 38 | "shebang-command": "^2.0.0", 39 | "which": "^2.0.1" 40 | }, 41 | "engines": { 42 | "node": ">= 8" 43 | } 44 | }, 45 | "node_modules/cssesc": { 46 | "version": "3.0.0", 47 | "license": "MIT", 48 | "bin": { 49 | "cssesc": "bin/cssesc" 50 | }, 51 | "engines": { 52 | "node": ">=4" 53 | } 54 | }, 55 | "node_modules/generic-names": { 56 | "version": "4.0.0", 57 | "license": "MIT", 58 | "dependencies": { 59 | "loader-utils": "^3.2.0" 60 | } 61 | }, 62 | "node_modules/icss-replace-symbols": { 63 | "version": "1.1.0", 64 | "license": "ISC" 65 | }, 66 | "node_modules/icss-utils": { 67 | "version": "5.1.0", 68 | "license": "ISC", 69 | "engines": { 70 | "node": "^10 || ^12 || >= 14" 71 | }, 72 | "peerDependencies": { 73 | "postcss": "^8.1.0" 74 | } 75 | }, 76 | "node_modules/isexe": { 77 | "version": "2.0.0", 78 | "license": "ISC" 79 | }, 80 | "node_modules/lilconfig": { 81 | "version": "2.0.6", 82 | "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", 83 | "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", 84 | "engines": { 85 | "node": ">=10" 86 | } 87 | }, 88 | "node_modules/loader-utils": { 89 | "version": "3.2.1", 90 | "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", 91 | "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", 92 | "engines": { 93 | "node": ">= 12.13.0" 94 | } 95 | }, 96 | "node_modules/lodash.camelcase": { 97 | "version": "4.3.0", 98 | "license": "MIT" 99 | }, 100 | "node_modules/nanoid": { 101 | "version": "3.3.8", 102 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", 103 | "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", 104 | "funding": [ 105 | { 106 | "type": "github", 107 | "url": "https://github.com/sponsors/ai" 108 | } 109 | ], 110 | "bin": { 111 | "nanoid": "bin/nanoid.cjs" 112 | }, 113 | "engines": { 114 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 115 | } 116 | }, 117 | "node_modules/path-key": { 118 | "version": "3.1.1", 119 | "license": "MIT", 120 | "engines": { 121 | "node": ">=8" 122 | } 123 | }, 124 | "node_modules/picocolors": { 125 | "version": "1.1.1", 126 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 127 | "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" 128 | }, 129 | "node_modules/postcss": { 130 | "version": "8.4.49", 131 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", 132 | "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", 133 | "funding": [ 134 | { 135 | "type": "opencollective", 136 | "url": "https://opencollective.com/postcss/" 137 | }, 138 | { 139 | "type": "tidelift", 140 | "url": "https://tidelift.com/funding/github/npm/postcss" 141 | }, 142 | { 143 | "type": "github", 144 | "url": "https://github.com/sponsors/ai" 145 | } 146 | ], 147 | "dependencies": { 148 | "nanoid": "^3.3.7", 149 | "picocolors": "^1.1.1", 150 | "source-map-js": "^1.2.1" 151 | }, 152 | "engines": { 153 | "node": "^10 || ^12 || >=14" 154 | } 155 | }, 156 | "node_modules/postcss-load-config": { 157 | "version": "4.0.1", 158 | "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", 159 | "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", 160 | "dependencies": { 161 | "lilconfig": "^2.0.5", 162 | "yaml": "^2.1.1" 163 | }, 164 | "engines": { 165 | "node": ">= 14" 166 | }, 167 | "funding": { 168 | "type": "opencollective", 169 | "url": "https://opencollective.com/postcss/" 170 | }, 171 | "peerDependencies": { 172 | "postcss": ">=8.0.9", 173 | "ts-node": ">=9.0.0" 174 | }, 175 | "peerDependenciesMeta": { 176 | "postcss": { 177 | "optional": true 178 | }, 179 | "ts-node": { 180 | "optional": true 181 | } 182 | } 183 | }, 184 | "node_modules/postcss-modules": { 185 | "version": "4.3.1", 186 | "license": "MIT", 187 | "dependencies": { 188 | "generic-names": "^4.0.0", 189 | "icss-replace-symbols": "^1.1.0", 190 | "lodash.camelcase": "^4.3.0", 191 | "postcss-modules-extract-imports": "^3.0.0", 192 | "postcss-modules-local-by-default": "^4.0.0", 193 | "postcss-modules-scope": "^3.0.0", 194 | "postcss-modules-values": "^4.0.0", 195 | "string-hash": "^1.1.1" 196 | }, 197 | "peerDependencies": { 198 | "postcss": "^8.0.0" 199 | } 200 | }, 201 | "node_modules/postcss-modules-extract-imports": { 202 | "version": "3.0.0", 203 | "license": "ISC", 204 | "engines": { 205 | "node": "^10 || ^12 || >= 14" 206 | }, 207 | "peerDependencies": { 208 | "postcss": "^8.1.0" 209 | } 210 | }, 211 | "node_modules/postcss-modules-local-by-default": { 212 | "version": "4.0.0", 213 | "license": "MIT", 214 | "dependencies": { 215 | "icss-utils": "^5.0.0", 216 | "postcss-selector-parser": "^6.0.2", 217 | "postcss-value-parser": "^4.1.0" 218 | }, 219 | "engines": { 220 | "node": "^10 || ^12 || >= 14" 221 | }, 222 | "peerDependencies": { 223 | "postcss": "^8.1.0" 224 | } 225 | }, 226 | "node_modules/postcss-modules-scope": { 227 | "version": "3.0.0", 228 | "license": "ISC", 229 | "dependencies": { 230 | "postcss-selector-parser": "^6.0.4" 231 | }, 232 | "engines": { 233 | "node": "^10 || ^12 || >= 14" 234 | }, 235 | "peerDependencies": { 236 | "postcss": "^8.1.0" 237 | } 238 | }, 239 | "node_modules/postcss-modules-values": { 240 | "version": "4.0.0", 241 | "license": "ISC", 242 | "dependencies": { 243 | "icss-utils": "^5.0.0" 244 | }, 245 | "engines": { 246 | "node": "^10 || ^12 || >= 14" 247 | }, 248 | "peerDependencies": { 249 | "postcss": "^8.1.0" 250 | } 251 | }, 252 | "node_modules/postcss-selector-parser": { 253 | "version": "6.0.10", 254 | "license": "MIT", 255 | "dependencies": { 256 | "cssesc": "^3.0.0", 257 | "util-deprecate": "^1.0.2" 258 | }, 259 | "engines": { 260 | "node": ">=4" 261 | } 262 | }, 263 | "node_modules/postcss-value-parser": { 264 | "version": "4.2.0", 265 | "license": "MIT" 266 | }, 267 | "node_modules/shebang-command": { 268 | "version": "2.0.0", 269 | "license": "MIT", 270 | "dependencies": { 271 | "shebang-regex": "^3.0.0" 272 | }, 273 | "engines": { 274 | "node": ">=8" 275 | } 276 | }, 277 | "node_modules/shebang-regex": { 278 | "version": "3.0.0", 279 | "license": "MIT", 280 | "engines": { 281 | "node": ">=8" 282 | } 283 | }, 284 | "node_modules/source-map-js": { 285 | "version": "1.2.1", 286 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 287 | "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 288 | "engines": { 289 | "node": ">=0.10.0" 290 | } 291 | }, 292 | "node_modules/string-hash": { 293 | "version": "1.1.3", 294 | "license": "CC0-1.0" 295 | }, 296 | "node_modules/style-inject": { 297 | "version": "0.3.0", 298 | "license": "MIT" 299 | }, 300 | "node_modules/util-deprecate": { 301 | "version": "1.0.2", 302 | "license": "MIT" 303 | }, 304 | "node_modules/which": { 305 | "version": "2.0.2", 306 | "license": "ISC", 307 | "dependencies": { 308 | "isexe": "^2.0.0" 309 | }, 310 | "bin": { 311 | "node-which": "bin/node-which" 312 | }, 313 | "engines": { 314 | "node": ">= 8" 315 | } 316 | }, 317 | "node_modules/yaml": { 318 | "version": "2.7.0", 319 | "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", 320 | "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", 321 | "bin": { 322 | "yaml": "bin.mjs" 323 | }, 324 | "engines": { 325 | "node": ">= 14" 326 | } 327 | } 328 | }, 329 | "dependencies": { 330 | "common-tags": { 331 | "version": "1.8.2" 332 | }, 333 | "cross-spawn": { 334 | "version": "7.0.6", 335 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 336 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 337 | "requires": { 338 | "path-key": "^3.1.0", 339 | "shebang-command": "^2.0.0", 340 | "which": "^2.0.1" 341 | } 342 | }, 343 | "cssesc": { 344 | "version": "3.0.0" 345 | }, 346 | "generic-names": { 347 | "version": "4.0.0", 348 | "requires": { 349 | "loader-utils": "^3.2.0" 350 | } 351 | }, 352 | "icss-replace-symbols": { 353 | "version": "1.1.0" 354 | }, 355 | "icss-utils": { 356 | "version": "5.1.0", 357 | "requires": {} 358 | }, 359 | "isexe": { 360 | "version": "2.0.0" 361 | }, 362 | "lilconfig": { 363 | "version": "2.0.6", 364 | "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", 365 | "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==" 366 | }, 367 | "loader-utils": { 368 | "version": "3.2.1", 369 | "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", 370 | "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==" 371 | }, 372 | "lodash.camelcase": { 373 | "version": "4.3.0" 374 | }, 375 | "nanoid": { 376 | "version": "3.3.8", 377 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", 378 | "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==" 379 | }, 380 | "path-key": { 381 | "version": "3.1.1" 382 | }, 383 | "picocolors": { 384 | "version": "1.1.1", 385 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 386 | "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" 387 | }, 388 | "postcss": { 389 | "version": "8.4.49", 390 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", 391 | "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", 392 | "requires": { 393 | "nanoid": "^3.3.7", 394 | "picocolors": "^1.1.1", 395 | "source-map-js": "^1.2.1" 396 | } 397 | }, 398 | "postcss-load-config": { 399 | "version": "4.0.1", 400 | "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", 401 | "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", 402 | "requires": { 403 | "lilconfig": "^2.0.5", 404 | "yaml": "^2.1.1" 405 | } 406 | }, 407 | "postcss-modules": { 408 | "version": "4.3.1", 409 | "requires": { 410 | "generic-names": "^4.0.0", 411 | "icss-replace-symbols": "^1.1.0", 412 | "lodash.camelcase": "^4.3.0", 413 | "postcss-modules-extract-imports": "^3.0.0", 414 | "postcss-modules-local-by-default": "^4.0.0", 415 | "postcss-modules-scope": "^3.0.0", 416 | "postcss-modules-values": "^4.0.0", 417 | "string-hash": "^1.1.1" 418 | } 419 | }, 420 | "postcss-modules-extract-imports": { 421 | "version": "3.0.0", 422 | "requires": {} 423 | }, 424 | "postcss-modules-local-by-default": { 425 | "version": "4.0.0", 426 | "requires": { 427 | "icss-utils": "^5.0.0", 428 | "postcss-selector-parser": "^6.0.2", 429 | "postcss-value-parser": "^4.1.0" 430 | } 431 | }, 432 | "postcss-modules-scope": { 433 | "version": "3.0.0", 434 | "requires": { 435 | "postcss-selector-parser": "^6.0.4" 436 | } 437 | }, 438 | "postcss-modules-values": { 439 | "version": "4.0.0", 440 | "requires": { 441 | "icss-utils": "^5.0.0" 442 | } 443 | }, 444 | "postcss-selector-parser": { 445 | "version": "6.0.10", 446 | "requires": { 447 | "cssesc": "^3.0.0", 448 | "util-deprecate": "^1.0.2" 449 | } 450 | }, 451 | "postcss-value-parser": { 452 | "version": "4.2.0" 453 | }, 454 | "shebang-command": { 455 | "version": "2.0.0", 456 | "requires": { 457 | "shebang-regex": "^3.0.0" 458 | } 459 | }, 460 | "shebang-regex": { 461 | "version": "3.0.0" 462 | }, 463 | "source-map-js": { 464 | "version": "1.2.1", 465 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 466 | "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" 467 | }, 468 | "string-hash": { 469 | "version": "1.1.3" 470 | }, 471 | "style-inject": { 472 | "version": "0.3.0" 473 | }, 474 | "util-deprecate": { 475 | "version": "1.0.2" 476 | }, 477 | "which": { 478 | "version": "2.0.2", 479 | "requires": { 480 | "isexe": "^2.0.0" 481 | } 482 | }, 483 | "yaml": { 484 | "version": "2.7.0", 485 | "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", 486 | "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==" 487 | } 488 | } 489 | } 490 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-transform-css", 3 | "description": "Jest transformer to import CSS into Jest's `jsdom`", 4 | "version": "6.0.3", 5 | "main": "index.js", 6 | "author": "Dominik Ferber (http://dferber.de/)", 7 | "license": "MIT", 8 | "dependencies": { 9 | "common-tags": "1.8.2", 10 | "cross-spawn": "^7.0.6", 11 | "postcss-load-config": "4.0.1", 12 | "postcss-modules": "4.3.1", 13 | "style-inject": "0.3.0" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/dferber90/jest-transform-css.git" 18 | }, 19 | "devDependencies": { 20 | "postcss": "^8.4.12" 21 | }, 22 | "peerDependencies": { 23 | "postcss": "^8.4.12" 24 | }, 25 | "keywords": [ 26 | "postcss-runner", 27 | "jest", 28 | "css" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /postcss-runner.js: -------------------------------------------------------------------------------- 1 | const postcss = require("postcss"); 2 | const postcssrc = require("postcss-load-config"); 3 | const cssModules = require("postcss-modules"); 4 | 5 | // This script is essentially a PostCSS Runner 6 | // https://github.com/postcss/postcss/blob/master/docs/guidelines/runner.md#postcss-runner-guidelines 7 | module.exports = ({ src, filename, transformConfig }) => { 8 | const ctx = { 9 | // Not sure whether the map is useful or not. 10 | // Disabled for now. We can always enable it once it becomes clear. 11 | map: false, 12 | // To ensure that PostCSS generates source maps and displays better syntax 13 | // errors, runners must specify the from and to options. If your runner does 14 | // not handle writing to disk (for example, a gulp transform), you should 15 | // set both options to point to the same file" 16 | // https://github.com/postcss/postcss/blob/master/docs/guidelines/runner.md#21-set-from-and-to-processing-options 17 | from: filename, 18 | to: filename 19 | }; 20 | let tokens = {}; 21 | return postcssrc(ctx) 22 | .then( 23 | config => ({ ...config, plugins: config.plugins || [] }), 24 | error => { 25 | // Support running without postcss.config.js 26 | // This is useful in case the webpack setup of the consumer does not 27 | // use PostCSS at all and simply uses css-loader in modules mode. 28 | if (error.message.startsWith("No PostCSS Config found in:")) { 29 | return { plugins: [], options: { from: filename, to: filename } }; 30 | } 31 | throw error; 32 | } 33 | ) 34 | .then(({ plugins, options }) => { 35 | return postcss([ 36 | cssModules({ 37 | // Add 'generateScopedName' property to 'jesttransformcss.config.js' for custom name generation. 38 | // List of available placeholder tokens: https://github.com/webpack/loader-utils#interpolatename 39 | // *Note some placeholder tokens appear to not be working 40 | generateScopedName: 41 | transformConfig && transformConfig.generateScopedName || 42 | "[path][local]-[hash:base64:10]", 43 | getJSON: (cssFileName, exportedTokens, outputFileName) => { 44 | tokens = exportedTokens; 45 | } 46 | }), 47 | ...plugins 48 | ]) 49 | .process(src, options) 50 | .then( 51 | result => ({ 52 | css: result.css, 53 | tokens, 54 | // Display result.warnings() 55 | // PostCSS runners must output warnings from result.warnings() 56 | // https://github.com/postcss/postcss/blob/master/docs/guidelines/runner.md#32-display-resultwarnings 57 | warnings: result.warnings().map(warn => warn.toString()) 58 | }), 59 | // Don’t show JS stack for CssSyntaxError 60 | // PostCSS runners must not show a stack trace for CSS syntax errors, 61 | // as the runner can be used by developers who are not familiar with 62 | // JavaScript. Instead, handle such errors gracefully: 63 | // https://github.com/postcss/postcss/blob/master/docs/guidelines/runner.md#31-dont-show-js-stack-for-csssyntaxerror 64 | error => { 65 | if (error.name === "CssSyntaxError") { 66 | process.stderr.write(error.message + error.showSourceCode()); 67 | } else { 68 | throw error; 69 | } 70 | } 71 | ); 72 | }); 73 | }; 74 | --------------------------------------------------------------------------------