├── .eslintrc ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── .eslinrc.js ├── .gitignore ├── README.md ├── config │ ├── sassVars.js │ ├── sassVars.json │ └── utils.scss ├── demo.png ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── index.js │ └── styles.scss └── webpack.config.js ├── jest.config.js ├── package-lock.json ├── package.json └── src ├── __mocks__ ├── jsVars1.js ├── jsVars2.js ├── jsonVars1.json ├── jsonVars2.json └── tsVars1.ts ├── __snapshots__ └── sassVarsLoader.test.js.snap ├── sassVarsLoader.js ├── sassVarsLoader.test.js └── utils ├── __snapshots__ └── convertJsToSass.test.js.snap ├── convertJsToSass.js ├── convertJsToSass.test.js ├── isModule.js ├── isModule.test.js ├── readSassFiles.js ├── readVarsFromJSONFiles.js ├── readVarsFromJSONFiles.test.js ├── readVarsFromJavascriptFiles.js ├── readVarsFromJavascriptFiles.test.js ├── readVarsFromTypescriptFiles.js ├── readVarsFromTypescriptFiles.test.js ├── transformKeys.js ├── transformKeys.test.js ├── transformObject.js ├── transformObject.test.js ├── watchFileForChanges.js ├── watchFileForChanges.test.js ├── watchFilesForChanges.js ├── watchFilesForChanges.test.js ├── watchModuleForChanges.js └── watchModuleForChanges.test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:jest/recommended" 4 | ], 5 | "plugins": [ 6 | "jest" 7 | ], 8 | "parserOptions": { 9 | "ecmaVersion": 2018, 10 | "sourceType": "module", 11 | "ecmaFeatures": { 12 | "impliedStrict": true, 13 | "experimentalObjectRestSpread": true 14 | } 15 | }, 16 | "env": { 17 | "jest/globals": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Webstorm 2 | .idea 3 | 4 | # VS Code 5 | .history 6 | .vscode 7 | 8 | # OSX 9 | .DS_Store 10 | 11 | # VIM 12 | *.swp 13 | *.un~ 14 | 15 | # NPM 16 | npm-debug.log 17 | node_modules 18 | .npmrc 19 | *.log 20 | 21 | # YARN 22 | yarn.lock 23 | 24 | # coverage 25 | coverage 26 | 27 | #dist 28 | dist 29 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Webstorm 2 | .idea 3 | 4 | # OSX 5 | .DS_Store 6 | 7 | # VIM 8 | *.swp 9 | *.un~ 10 | 11 | # NPM 12 | npm-debug.log 13 | node_modules 14 | .npmrc 15 | *.log 16 | 17 | # coverage 18 | coverage 19 | 20 | # non dist files 21 | coverage 22 | example 23 | src 24 | .travis.yml 25 | .eslintrc 26 | .nvmrc 27 | .gitignore 28 | 29 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "printWidth": 120, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "es5", 7 | "jsxBracketSameLine": false, 8 | "semi": false, 9 | "rcVerbose": true 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | codecov: true 4 | cache: 5 | directories: 6 | - node_modules 7 | env: 8 | - NODE_ENV=development 9 | matrix: 10 | fast_finish: true 11 | install: 12 | - NODE_ENV=development npm install 13 | script: 14 | - npm run lint 15 | - npm run test -- --runInBand 16 | - npm run coverage 17 | deploy: 18 | skip_cleanup: true 19 | provider: npm 20 | email: epegzz@gmail.com 21 | api_key: 22 | secure: mOTInY08VZzX2wajhu3JCxvLKVqa3RSG2PMy1u8vVfsNzh6oPezYR1FLCTjy5+WgEnrv5QHFzlRvv3yPeod7cqQ1mjQZGyx46jHu0BJq8xFSjtcD7woHWRpssZce/R1SmUr+hX8SNwK2x8c19TKsfn3VOeWevZraXSGXhVfypnDc+SakaPQXzBRzFDjHC2b1Pg+jEb4Z6dhT2bTcsOMD27Sp5ZM2O8OHL+SkVuDoyviWBIP8pJEX6R88zJRmxBHX0rNQ+83EC0EUn/iUbMd60Pi8A4pJXLthhrnKAWWdEDoVbYN/x5IB085zaRALIjM56FbxN019aDq69nKxI+GRx6iTKp07/yARGAHaTqXH6jbfluLXU2fzOYPq52zdzGCzkzny0Wt7m+JuMC2YDiXMTm3BgsfcntiGlQaH88XVUOqbko8x6Ra2/NIMBynLoipaFCLRwtC6dvcukCjMUdwe+/7KFzMWeYX2cLPBCIZiLHhZOuSwN0cKAPJ1woT6NwGzPUrktTZC+cU9yhefVURr7l0L5Wr/evYl2CZ86PawKim6lRKFGfX8EaOSgzfy1Z58C989swu74ICTRfjDvlDE/RpHY4JFrWrA7IZspkaVAAxtQxJeiO2OvgI3fcGr2ouMvmp5d2S1GU1KH4qDN39EgmoDX4ALdlfxZ0fpl3xhkmw= 23 | on: 24 | tags: true 25 | repo: epegzz/sass-vars-loader 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [6.0.0] 2 | ### Changed 3 | - Now respects the order in which the files are specified in the config when loading sass vars. 4 | 5 | ## [5.1.0] 6 | ### Added 7 | - Added `transformKeys` option to transform the variable / key names in Sass. 8 | 9 | ## [5.0.0] 10 | ### Changed 11 | - Strings are not quoted anymore. This might be a breaking change for some: Until now, all 12 | variable values that started with a "0" or a whitespace got automatically quoted. 13 | From now on you need to add the quotes yourself if you need them (i.e. "'0123'"). 14 | 15 | ## [4.4.0] 16 | ### Changed 17 | - Arguments passed to the `files` option are now getting resolved, which allows passing files 18 | that are installed as node_modules without loosing HMR functionality. 19 | 20 | ## [4.0.0] 21 | ### Changed 22 | - Dropping Babel. 23 | Because `sass-vars-loader` is a NodeJS only project, there is not enough justification to use 24 | Babel as a compiler. Therefore, starting with this release, the NPM package will use the source 25 | code directly instead of a compiled version. 26 | The downside: `sass-vars-loader` now requires NodeJS version `8` or greater. 27 | The benefit: No unnecessary polyfills anymore when using a recent Node version. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Daniel Schäfer 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 |

Sass Vars Loader

2 |

Import Sass vars from Webpack config or from JS/JSON files

3 |

4 | 5 | Travis 6 | 7 | 8 | Maintainability 9 | 10 | 11 | Codecov 12 | 13 | 14 | npm version 15 | 16 | 17 | npm installs 18 | 19 | 20 | dependencies 21 | 22 |

23 | 24 | 25 | 26 | 27 | 28 | ##### This loader allows you to use Sass variables defined in: 29 | 30 |
  • ✅ JSON Files
  • 31 |
  • ✅ JavaScript Files
  • 32 |
  • ✅ Inlined in Webpack Config
  • 33 | 34 | 35 | 36 | ##### Supports both syntax types: 37 | 38 |
  • ✅ SASS Syntax
  • 39 |
  • ✅ SCSS Syntax
  • 40 | 41 | 42 | 43 | ##### Supports hot reload: 44 | 45 |
  • ✅ HMR Enabled
  • 46 | 47 |
    48 | 49 | ## Install 50 | 51 | using npm 52 | ```sh 53 | npm install @epegzz/sass-vars-loader --save-dev 54 | ``` 55 | using yarn 56 | ```sh 57 | yarn add @epegzz/sass-vars-loader --dev 58 | ``` 59 | 60 | 61 | ## Usage 62 | 63 | Look at the [Example Webpack Config File](./example/webpack.config.js) to see how to use this 64 | loader in conjunction with [style-loader](https://github.com/webpack-contrib/style-loader) and 65 | [css-loader](https://github.com/webpack-contrib/css-loader) 66 | 67 | ### Option 1: Inline Sass vars in the webpack config 68 | 69 | ```scss 70 | // styles.css: 71 | 72 | .some-class { 73 | background: $greenFromWebpackConfig; 74 | } 75 | ``` 76 | 77 | ```js 78 | // webpack.config.js 79 | 80 | var path = require('path'); 81 | 82 | module.exports = { 83 | entry: './src/index.js', 84 | module: { 85 | rules: [{ 86 | test: /\.scss$/, 87 | use: [ 88 | // Inserts all imported styles into the html document 89 | { loader: "style-loader" }, 90 | 91 | // Translates CSS into CommonJS 92 | { loader: "css-loader" }, 93 | 94 | // Compiles Sass to CSS 95 | { loader: "sass-loader", options: { includePaths: ["app/styles.scss"] } }, 96 | 97 | // Reads Sass vars from files or inlined in the options property 98 | { loader: "@epegzz/sass-vars-loader", options: { 99 | syntax: 'scss', 100 | // Option 1) Specify vars here 101 | vars: { 102 | greenFromWebpackConfig: '#0f0' 103 | } 104 | } 105 | }] 106 | }] 107 | }, 108 | output: { 109 | filename: 'bundle.js', 110 | path: path.resolve(__dirname, 'dist') 111 | } 112 | }; 113 | ``` 114 | 115 | ### Option 2: Load Sass vars from JSON file 116 | 117 | ```js 118 | // config/sassVars.json 119 | 120 | { 121 | "purpleFromJSON": "purple" 122 | } 123 | ``` 124 | 125 | ```scss 126 | // styles.css: 127 | 128 | .some-class { 129 | background: $purpleFromJSON; 130 | } 131 | ``` 132 | 133 | ```js 134 | // webpack.config.js 135 | 136 | var path = require('path'); 137 | 138 | module.exports = { 139 | entry: './src/index.js', 140 | module: { 141 | rules: [{ 142 | test: /\.scss$/, 143 | use: [ 144 | // Inserts all imported styles into the html document 145 | { loader: "style-loader" }, 146 | 147 | // Translates CSS into CommonJS 148 | { loader: "css-loader" }, 149 | 150 | // Compiles Sass to CSS 151 | { loader: "sass-loader", options: { includePaths: ["app/styles.scss"] } }, 152 | 153 | // Reads Sass vars from files or inlined in the options property 154 | { loader: "@epegzz/sass-vars-loader", options: { 155 | syntax: 'scss', 156 | files: [ 157 | // Option 2) Load vars from JSON file 158 | path.resolve(__dirname, 'config/sassVars.json') 159 | ] 160 | } 161 | }] 162 | }] 163 | }, 164 | output: { 165 | filename: 'bundle.js', 166 | path: path.resolve(__dirname, 'dist') 167 | } 168 | }; 169 | ``` 170 | 171 | 172 | ### Option 3: Load Sass vars from JavaScript file 173 | 174 | ```js 175 | // config/sassVars.js 176 | 177 | module.exports = { 178 | blueFromJavaScript: 'blue' 179 | }; 180 | ``` 181 | 182 | ```scss 183 | // styles.css: 184 | 185 | .some-class { 186 | background: $blueFromJavaScript; 187 | } 188 | ``` 189 | 190 | ```js 191 | // webpack.config.js 192 | 193 | var path = require('path'); 194 | 195 | module.exports = { 196 | entry: './src/index.js', 197 | module: { 198 | rules: [{ 199 | test: /\.scss$/, 200 | use: [ 201 | // Inserts all imported styles into the html document 202 | { loader: "style-loader" }, 203 | 204 | // Translates CSS into CommonJS 205 | { loader: "css-loader" }, 206 | 207 | // Compiles Sass to CSS 208 | { loader: "sass-loader", options: { includePaths: ["app/styles.scss"] } }, 209 | 210 | // Reads Sass vars from files or inlined in the options property 211 | { loader: "@epegzz/sass-vars-loader", options: { 212 | syntax: 'scss', 213 | files: [ 214 | // Option 3) Load vars from JavaScript file 215 | path.resolve(__dirname, 'config/sassVars.js') 216 | ] 217 | } 218 | }] 219 | }] 220 | }, 221 | output: { 222 | filename: 'bundle.js', 223 | path: path.resolve(__dirname, 'dist') 224 | } 225 | }; 226 | ``` 227 | 228 | 229 | ### Pro Tip: Using objects as Sass vars! 230 | 231 | Use [map_get](http://sass-lang.com/documentation/Sass/Script/Functions.html#map_get-instance_method) 232 | in order to use objects as Sass vars: 233 | 234 | ```js 235 | // config/sassVars.js 236 | 237 | module.exports = { 238 | lightTheme: { 239 | background: 'white', 240 | color: 'black' 241 | }, 242 | darkTheme: { 243 | background: 'black', 244 | color: 'gray' 245 | } 246 | }; 247 | ``` 248 | 249 | ```scss 250 | // styles.css: 251 | 252 | $theme: $lightTheme; 253 | 254 | .some-class { 255 | background: map_get($theme, background); 256 | color: map_get($theme, color); 257 | } 258 | ``` 259 | -------------------------------------------------------------------------------- /example/.eslinrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceType: "module" 3 | }; 4 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # sass-vars-loader usage example 2 | 3 | This is a simple example project using webpack2 and sass-vars-loader. 4 | 5 | The Sass vars in `src/styles.scss` are read from `webpack.config.js`, `config/sassVars.js` 6 | and `config/sassVars.json`. 7 | 8 | Open those files and play around! :) 9 | 10 | 11 | ## Installing 12 | 13 | ``` 14 | npm install 15 | ``` 16 | 17 | ## Running 18 | 19 | First build the `bundle.js` 20 | ``` 21 | npm run build 22 | ``` 23 | 24 | and then open `index.html` in your browser. 25 | 26 | 27 | It should look like this: 28 | 29 | ![index.html](./demo.png "index.html") -------------------------------------------------------------------------------- /example/config/sassVars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | blueFromJS: 'blue', 3 | } 4 | -------------------------------------------------------------------------------- /example/config/sassVars.json: -------------------------------------------------------------------------------- 1 | { 2 | "purpleFromJSON": "purple" 3 | } -------------------------------------------------------------------------------- /example/config/utils.scss: -------------------------------------------------------------------------------- 1 | @function opaque($color, $opacity) { 2 | @return rgba($color, $opacity); 3 | } -------------------------------------------------------------------------------- /example/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsc8x/sass-vars-loader/71dbe697ca6f212b7fc0df37b5f0fa038c3d739b/example/demo.png -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | sass-vars-loader demo 4 | 5 | 6 | 7 |
    8 | Green from webpack config 9 |
    10 | 11 |
    12 | Purple from JSON file 13 |
    14 | 15 |
    16 | Blue from JS file 17 |
    18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass-vars-loader-demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "webpack" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@epegzz/sass-vars-loader": "file:..", 15 | "@webpack-cli/migrate": "^0.1.2", 16 | "@webpack-cli/serve": "^0.1.2", 17 | "babel-preset-env": "^1.7.0", 18 | "css-loader": "^2.1.0", 19 | "node-sass": "^4.11.0", 20 | "sass-loader": "^7.1.0", 21 | "style-loader": "^0.23.1", 22 | "webpack": "^4.28.2", 23 | "webpack-cli": "^3.1.2" 24 | }, 25 | "dependencies": {} 26 | } 27 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | // Import your Sass file anywhere in your app and webpack will make sure it 2 | // ends up as CSS in your HTML document. 3 | import './styles.scss' 4 | -------------------------------------------------------------------------------- /example/src/styles.scss: -------------------------------------------------------------------------------- 1 | body { 2 | 3 | // Option 1) Load vars from webpack config (from webpack.config.js) 4 | .styles-from-webpack-config { 5 | background: $greenFromWebpackConfig; 6 | } 7 | 8 | // Option 2) Load vars from JSON file (from config/sassVars.json) 9 | .styles-from-json-file { 10 | background: $purpleFromJSON; 11 | } 12 | 13 | // Option 3) Load vars from Javascript file (from config/sassVars.json) 14 | .styles-from-js-file { 15 | background: $blueFromJS; 16 | } 17 | 18 | 19 | 20 | div { 21 | padding: 20px; 22 | margin: 20px; 23 | } 24 | pre { 25 | background: #ffffff3b; 26 | padding: 20px; 27 | } 28 | h3 { 29 | background: #ffffff69; 30 | padding: 10px; 31 | } 32 | } -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | watch: true, 6 | mode: 'production', 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.scss$/, 11 | use: [ 12 | // Inserts all imported styles into the html document 13 | { loader: 'style-loader' }, 14 | 15 | // Translates CSS into CommonJS 16 | { loader: 'css-loader' }, 17 | 18 | // Compiles Sass to CSS 19 | { 20 | loader: 'sass-loader', 21 | options: { includePaths: ['app/styles.scss'] }, 22 | }, 23 | 24 | // Reads Sass vars from files or inlined in the options property 25 | { 26 | loader: '@epegzz/sass-vars-loader', 27 | options: { 28 | // You can specify vars here 29 | vars: { 30 | greenFromWebpackConfig: 'opaque(green, 0.5)', // `opaque` is defined in `config/utils.scss` which gets loaded below 31 | }, 32 | files: [ 33 | // You can include sass files 34 | path.resolve(__dirname, 'config/utils.scss'), 35 | // You can include JSON files 36 | path.resolve(__dirname, 'config/sassVars.json'), 37 | // You can include JavaScript files 38 | path.resolve(__dirname, 'config/sassVars.js'), 39 | ], 40 | }, 41 | }, 42 | ], 43 | }, 44 | ], 45 | }, 46 | output: { 47 | filename: 'bundle.js', 48 | path: path.resolve(__dirname, 'dist'), 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | coverageDirectory: './coverage/', 3 | collectCoverage: true, 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@epegzz/sass-vars-loader", 3 | "version": "6.1.0", 4 | "author": "Daniel Schäfer ", 5 | "description": "A SASS vars loader for Webpack. Load global SASS vars from JS/JSON/Typescript files or from Webpack config.", 6 | "keywords": [ 7 | "scss", 8 | "sass", 9 | "js", 10 | "json", 11 | "vars", 12 | "ts", 13 | "typescript", 14 | "variables", 15 | "webpack", 16 | "loader" 17 | ], 18 | "license": "MIT", 19 | "engines": { 20 | "node": ">=8" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/epegzz/sass-vars-loader" 25 | }, 26 | "engineStrict": true, 27 | "main": "src/sassVarsLoader.js", 28 | "scripts": { 29 | "test": "NODE_ENV=testing jest --verbose", 30 | "precommit": "lint-staged", 31 | "watch-test": "NODE_ENV=testing jest --watch", 32 | "coverage": "NODE_ENV=testing jest && codecov", 33 | "lint": "eslint src", 34 | "format": "prettier-eslint --write \"src/**/*.js\"" 35 | }, 36 | "lint-staged": { 37 | "*.js": [ 38 | "npm run format", 39 | "git add" 40 | ] 41 | }, 42 | "dependencies": { 43 | "loader-utils": "^1.2.3", 44 | "require-from-string": "^2.0.2", 45 | "typescript": "^3.5.3" 46 | }, 47 | "devDependencies": { 48 | "codecov": "^3.5.0", 49 | "eslint": "^6.1.0", 50 | "eslint-plugin-jest": "^22.13.0", 51 | "husky": "^3.0.1", 52 | "jest": "^24.8.0", 53 | "lint-staged": "^9.2.0", 54 | "prettier": "^1.18.2", 55 | "prettier-eslint-cli": "^5.0.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/__mocks__/jsVars1.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | value1FromJs: 'foo', 3 | loadingOrderTest2: 'fromJS', 4 | loadingOrderTest3: 'fromJS', 5 | } 6 | -------------------------------------------------------------------------------- /src/__mocks__/jsVars2.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | value2FromJs: 'foo', 3 | } 4 | -------------------------------------------------------------------------------- /src/__mocks__/jsonVars1.json: -------------------------------------------------------------------------------- 1 | { 2 | "value1FromJson": "foo", 3 | "loadingOrderTest1": "fromJSON", 4 | "loadingOrderTest2": "fromJSON" 5 | } -------------------------------------------------------------------------------- /src/__mocks__/jsonVars2.json: -------------------------------------------------------------------------------- 1 | { 2 | "value2FromJson": "foo" 3 | } -------------------------------------------------------------------------------- /src/__mocks__/tsVars1.ts: -------------------------------------------------------------------------------- 1 | const lol: any = { 2 | value1FromTs: 'tsFoo', 3 | loadingOrderTest4: 'fromTS', 4 | loadingOrderTest5: 'fromTS', 5 | } 6 | 7 | export default lol; 8 | -------------------------------------------------------------------------------- /src/__snapshots__/sassVarsLoader.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`With multi post-processing Returns expected Sass contents 1`] = `"sassFileContents"`; 4 | 5 | exports[`With sass syntax Returns expected Sass contents 1`] = ` 6 | "// Vars from Webpack config 7 | $value1FromWebpack: foo 8 | $nested: (works: (veryWell: true, withoutProblems: indeed)) 9 | 10 | sassFileContents" 11 | `; 12 | 13 | exports[`With single post-processing Returns expected Sass contents 1`] = `"sassFileContents"`; 14 | 15 | exports[`With vars from JSON, JS and config Returns expected Sass contents 1`] = ` 16 | "// Vars from jsonVars1.json 17 | $value1FromJson: foo; 18 | $loadingOrderTest1: fromJSON; 19 | $loadingOrderTest2: fromJSON; 20 | 21 | // Vars from jsVars1.js 22 | $value1FromJs: foo; 23 | $loadingOrderTest2: fromJS; 24 | $loadingOrderTest3: fromJS; 25 | 26 | // Vars from tsVars1.ts 27 | $value1FromTs: tsFoo; 28 | $loadingOrderTest4: fromTS; 29 | $loadingOrderTest5: fromTS; 30 | 31 | // Vars from Webpack config 32 | $loadingOrderTest3: fromConfig; 33 | 34 | sassFileContents" 35 | `; 36 | 37 | exports[`With vars from files Returns expected Sass contents 1`] = ` 38 | "// Vars from jsonVars1.json 39 | $value1FromJson: foo; 40 | $loadingOrderTest1: fromJSON; 41 | $loadingOrderTest2: fromJSON; 42 | 43 | // Vars from jsVars1.js 44 | $value1FromJs: foo; 45 | $loadingOrderTest2: fromJS; 46 | $loadingOrderTest3: fromJS; 47 | 48 | // Vars from tsVars1.ts 49 | $value1FromTs: tsFoo; 50 | $loadingOrderTest4: fromTS; 51 | $loadingOrderTest5: fromTS; 52 | 53 | // Vars from jsonVars2.json 54 | $value2FromJson: foo; 55 | 56 | sassFileContents" 57 | `; 58 | 59 | exports[`With vars from webpack config Returns expected Sass contents 1`] = ` 60 | "// Vars from Webpack config 61 | $value1FromWebpack: foo; 62 | $nested: (works: (veryWell: true, withoutProblems: indeed)); 63 | 64 | sassFileContents" 65 | `; 66 | 67 | exports[`Without options Returns expected Sass contents 1`] = `"sassFileContents"`; 68 | -------------------------------------------------------------------------------- /src/sassVarsLoader.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const loaderUtils = require('loader-utils') 3 | const readVarsFromJSONFiles = require('./utils/readVarsFromJSONFiles') 4 | const readVarsFromJavascriptFiles = require('./utils/readVarsFromJavascriptFiles') 5 | const readVarsFromTypescriptFiles = require('./utils/readVarsFromTypescriptFiles') 6 | const readSassFiles = require('./utils/readSassFiles') 7 | const watchFilesForChanges = require('./utils/watchFilesForChanges') 8 | const convertJsToSass = require('./utils/convertJsToSass') 9 | const transformKeys = require('./utils/transformKeys') 10 | const transformObject = require('./utils/transformObject') 11 | module.exports = async function(content) { 12 | this.cacheable() 13 | const callback = this.async() 14 | try { 15 | const options = loaderUtils.getOptions(this) || {} 16 | 17 | const files = options.files || [] 18 | const syntax = options.syntax || 'scss' 19 | const transformFileContent = options.transformFileContent 20 | let transformKeysCallbacks = options.transformKeys 21 | if (transformKeysCallbacks && !Array.isArray(transformKeysCallbacks)) { 22 | transformKeysCallbacks = [transformKeysCallbacks] 23 | } 24 | 25 | await watchFilesForChanges(this, files) 26 | 27 | const vars = [] 28 | for (const file of files) { 29 | // Javascript 30 | if (file.match(/\.js$/i)) { 31 | vars.push({ file, object: transformObject(readVarsFromJavascriptFiles([file]), transformFileContent) }) 32 | } 33 | 34 | // Typescript 35 | if (file.match(/\.ts$/i)) { 36 | vars.push({ file, object: transformObject(readVarsFromTypescriptFiles([file]), transformFileContent) }) 37 | } 38 | 39 | // JSON 40 | if (file.match(/\.json$/i)) { 41 | vars.push({ file, object: transformObject(readVarsFromJSONFiles([file]), transformFileContent) }) 42 | } 43 | 44 | // Sass/Scss 45 | if (file.match(/\.s[ac]ss$/i)) { 46 | vars.push({ file, string: readSassFiles([file]) }) 47 | } 48 | } 49 | 50 | // Vars from Webpack config 51 | if (options.vars) { 52 | vars.push({ object: transformObject(options.vars, transformFileContent) }) 53 | } 54 | 55 | const varsString = vars.reduce((result, { file, object, string }) => { 56 | if (object) { 57 | if (transformKeysCallbacks) { 58 | object = transformKeysCallbacks.reduce((res, fn) => transformKeys(res, fn), {}) 59 | } 60 | string = convertJsToSass(object, syntax) 61 | } 62 | 63 | if (string && !/^[\s\n]*$/.test(string)) { 64 | const comment = file ? `Vars from ${path.parse(file).base}` : 'Vars from Webpack config' 65 | 66 | return `${result}// ${comment}\n${string}\n\n` 67 | } 68 | 69 | return result 70 | }, '') 71 | 72 | callback(null, `${varsString}${content}`) 73 | } catch (err) { 74 | callback(err) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/sassVarsLoader.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const sassVarsLoader = require('./sassVarsLoader') 3 | 4 | const mockSassFileContents = `sassFileContents` 5 | let result, error, mockOptions 6 | const loaderContext = { 7 | cacheable: jest.fn(), 8 | addDependency: jest.fn(), 9 | resolve: jest.fn((context, file, callback) => { 10 | callback(null, file) 11 | }), 12 | } 13 | 14 | jest.mock('loader-utils', () => ({ 15 | getOptions: () => mockOptions, 16 | })) 17 | 18 | describe('With vars from webpack config', () => { 19 | beforeAll(async () => { 20 | await setup({ 21 | vars: { 22 | value1FromWebpack: 'foo', 23 | nested: { 24 | works: { 25 | veryWell: true, 26 | withoutProblems: 'indeed', 27 | }, 28 | }, 29 | }, 30 | }) 31 | }) 32 | expectCorrectResult() 33 | expectMarksItselfAsCacheable() 34 | }) 35 | 36 | describe('With vars from files', () => { 37 | beforeAll(async () => { 38 | await setup({ 39 | files: [ 40 | path.resolve(__dirname, '__mocks__/jsonVars1.json'), 41 | path.resolve(__dirname, '__mocks__/jsVars1.js'), 42 | path.resolve(__dirname, '__mocks__/tsVars1.ts'), 43 | path.resolve(__dirname, '__mocks__/jsonVars2.json'), 44 | ], 45 | }) 46 | }) 47 | expectCorrectResult() 48 | expectMarksItselfAsCacheable() 49 | expectWatchesFilesForChanges() 50 | }) 51 | 52 | describe('With vars from JSON, JS and config', () => { 53 | beforeAll(async () => { 54 | await setup({ 55 | vars: { 56 | loadingOrderTest3: 'fromConfig', 57 | }, 58 | files: [ 59 | path.resolve(__dirname, '__mocks__/jsonVars1.json'), 60 | path.resolve(__dirname, '__mocks__/jsVars1.js'), 61 | path.resolve(__dirname, '__mocks__/tsVars1.ts'), 62 | ], 63 | }) 64 | }) 65 | expectCorrectResult() 66 | }) 67 | 68 | describe('Without options', () => { 69 | beforeAll(async () => { 70 | await setup() 71 | }) 72 | expectCorrectResult() 73 | expectMarksItselfAsCacheable() 74 | }) 75 | 76 | describe('With sass syntax', () => { 77 | beforeAll(async () => { 78 | await setup({ 79 | syntax: 'sass', 80 | vars: { 81 | value1FromWebpack: 'foo', 82 | nested: { 83 | works: { 84 | veryWell: true, 85 | withoutProblems: 'indeed', 86 | }, 87 | }, 88 | }, 89 | }) 90 | }) 91 | expectCorrectResult() 92 | }) 93 | 94 | describe('With invalid file', () => { 95 | beforeAll(async () => { 96 | await setup({ 97 | syntax: 'sass', 98 | files: ['~invalid~'], 99 | }) 100 | }) 101 | expectError(`Invalid file: "~invalid~". Consider using "path.resolve" in your config.`) 102 | }) 103 | 104 | describe('With single post-processing', () => { 105 | beforeAll(async () => { 106 | await setup({ 107 | transformKeys: key => `transformed-${key}`, 108 | vars: { 109 | 'transformed-valueToTransform': 'foo', 110 | 'transformed-nested': { 111 | 'transformed-works': { 112 | 'transformed-Complete': true, 113 | 'transformed-veryWellResult': true, 114 | 'transformed-withoutProblems': 'indeed', 115 | }, 116 | }, 117 | }, 118 | }) 119 | }) 120 | expectCorrectResult() 121 | expectMarksItselfAsCacheable() 122 | }) 123 | 124 | describe('With multi post-processing', () => { 125 | beforeAll(async () => { 126 | await setup({ 127 | transformKeys: [key => `transformed-${key}`, key => key.toUpperCase()], 128 | vars: { 129 | 'TRANSFORMED-VALUETOTRANSFORM': 'foo', 130 | 'TRANSFORMED-NESTED': { 131 | 'TRANSFORMED-WORKS': { 132 | 'TRANSFORMED-COMPLETE': true, 133 | 'TRANSFORMED-VERYWELLRESULT': true, 134 | 'TRANSFORMED-WITHOUTPROBLEMS': 'indeed', 135 | }, 136 | }, 137 | }, 138 | }) 139 | }) 140 | expectCorrectResult() 141 | expectMarksItselfAsCacheable() 142 | }) 143 | 144 | async function setup(options) { 145 | result = null 146 | error = null 147 | mockOptions = options 148 | loaderContext.addDependency.mockClear() 149 | loaderContext.cacheable.mockClear() 150 | loaderContext.async = () => (err, res) => { 151 | error = err 152 | result = res 153 | } 154 | await sassVarsLoader.call(loaderContext, mockSassFileContents) 155 | } 156 | 157 | function expectCorrectResult() { 158 | it('Returns expected Sass contents', () => { 159 | expect(result).toMatchSnapshot() 160 | }) 161 | } 162 | 163 | function expectError(message) { 164 | it('Returns an error', () => { 165 | expect(error && error.message).toEqual(message) 166 | }) 167 | } 168 | 169 | function expectMarksItselfAsCacheable() { 170 | it('Marks itself as cacheable', () => { 171 | expect(loaderContext.cacheable).toHaveBeenCalled() 172 | }) 173 | } 174 | 175 | function expectWatchesFilesForChanges() { 176 | it('Watches files for changes', () => { 177 | const { files } = mockOptions 178 | expect(loaderContext.addDependency).toHaveBeenCalledTimes(files.length) 179 | }) 180 | } 181 | -------------------------------------------------------------------------------- /src/utils/__snapshots__/convertJsToSass.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`With sass syntax converts array with nested arrays 1`] = `"$it: ((list, 1), (list, 2))"`; 4 | 5 | exports[`With sass syntax converts empty array 1`] = `"$it: ()"`; 6 | 7 | exports[`With sass syntax converts empty object 1`] = `""`; 8 | 9 | exports[`With sass syntax converts multi line string 1`] = ` 10 | "$it: line1 11 | line2" 12 | `; 13 | 14 | exports[`With sass syntax converts multi line string with double quotes 1`] = ` 15 | "$it: \\"line1 16 | line2\\"" 17 | `; 18 | 19 | exports[`With sass syntax converts multi line string with single quotes 1`] = ` 20 | "$it: 'line1 21 | line2'" 22 | `; 23 | 24 | exports[`With sass syntax converts multi value object 1`] = `"$it: (a: 1, b: 2)"`; 25 | 26 | exports[`With sass syntax converts multiple vars 1`] = ` 27 | "$it: value 28 | $also: this" 29 | `; 30 | 31 | exports[`With sass syntax converts nested object with array 1`] = `"$it: (a: 1, b: 2, c: (d: (4, px, em)))"`; 32 | 33 | exports[`With sass syntax converts nested object: nested 1`] = `"$it: (a: 1, b: 2, c: (d: 4))"`; 34 | 35 | exports[`With sass syntax converts number 1`] = `"$it: 5"`; 36 | 37 | exports[`With sass syntax converts object with nested array 1`] = `"$it: (15, px, (nested: (oh, no)))"`; 38 | 39 | exports[`With sass syntax converts simple object 1`] = `"$it: (a: 1)"`; 40 | 41 | exports[`With sass syntax converts simple simple 1`] = `"$it: (1, 2)"`; 42 | 43 | exports[`With sass syntax converts string 1`] = `"$it: value"`; 44 | 45 | exports[`With sass syntax converts string with double quotes 1`] = `"$it: 'value'"`; 46 | 47 | exports[`With sass syntax converts string with single quotes 1`] = `"$it: \\"value\\""`; 48 | 49 | exports[`With scss syntax converts array with nested arrays 1`] = `"$it: ((list, 1), (list, 2));"`; 50 | 51 | exports[`With scss syntax converts empty array 1`] = `"$it: ();"`; 52 | 53 | exports[`With scss syntax converts empty object 1`] = `""`; 54 | 55 | exports[`With scss syntax converts multi line string 1`] = ` 56 | "$it: line1 57 | line2;" 58 | `; 59 | 60 | exports[`With scss syntax converts multi line string with double quotes 1`] = ` 61 | "$it: \\"line1 62 | line2\\";" 63 | `; 64 | 65 | exports[`With scss syntax converts multi line string with single quotes 1`] = ` 66 | "$it: 'line1 67 | line2';" 68 | `; 69 | 70 | exports[`With scss syntax converts multi value object 1`] = `"$it: (a: 1, b: 2);"`; 71 | 72 | exports[`With scss syntax converts multiple vars 1`] = ` 73 | "$it: value; 74 | $also: this;" 75 | `; 76 | 77 | exports[`With scss syntax converts nested object with array 1`] = `"$it: (a: 1, b: 2, c: (d: (4, px, em)));"`; 78 | 79 | exports[`With scss syntax converts nested object: nested 1`] = `"$it: (a: 1, b: 2, c: (d: 4));"`; 80 | 81 | exports[`With scss syntax converts number 1`] = `"$it: 5;"`; 82 | 83 | exports[`With scss syntax converts object with nested array 1`] = `"$it: (15, px, (nested: (oh, no)));"`; 84 | 85 | exports[`With scss syntax converts simple object 1`] = `"$it: (a: 1);"`; 86 | 87 | exports[`With scss syntax converts simple simple 1`] = `"$it: (1, 2);"`; 88 | 89 | exports[`With scss syntax converts string 1`] = `"$it: value;"`; 90 | 91 | exports[`With scss syntax converts string with double quotes 1`] = `"$it: 'value';"`; 92 | 93 | exports[`With scss syntax converts string with single quotes 1`] = `"$it: \\"value\\";"`; 94 | -------------------------------------------------------------------------------- /src/utils/convertJsToSass.js: -------------------------------------------------------------------------------- 1 | function convertJsToSass(obj, syntax) { 2 | const suffix = syntax === 'sass' ? '' : ';' 3 | const keys = Object.keys(obj) 4 | const lines = keys.map(key => `$${key}: ${formatValue(obj[key], syntax)}${suffix}`) 5 | return lines.join('\n') 6 | } 7 | 8 | function formatNestedObject(obj, syntax) { 9 | const keys = Object.keys(obj) 10 | return keys.map(key => `${key}: ${formatValue(obj[key], syntax)}`).join(', ') 11 | } 12 | 13 | function formatValue(value, syntax) { 14 | if (value instanceof Array) { 15 | return `(${value.map(formatValue).join(', ')})` 16 | } 17 | 18 | if (typeof value === 'object') { 19 | return `(${formatNestedObject(value, syntax)})` 20 | } 21 | 22 | if (typeof value === 'string') { 23 | return value 24 | } 25 | 26 | return JSON.stringify(value) 27 | } 28 | 29 | module.exports = convertJsToSass 30 | -------------------------------------------------------------------------------- /src/utils/convertJsToSass.test.js: -------------------------------------------------------------------------------- 1 | const convertJsToSass = require('./convertJsToSass') 2 | 3 | const testCases = [ 4 | { name: 'converts string', input: { it: 'value' } }, 5 | { name: 'converts multi line string', input: { it: 'line1\nline2' } }, 6 | { name: 'converts string with single quotes', input: { it: '"value"' } }, 7 | { name: 'converts string with double quotes', input: { it: "'value'" } }, 8 | { name: 'converts multi line string with double quotes', input: { it: '"line1\nline2"' } }, 9 | { name: 'converts multi line string with single quotes', input: { it: "'line1\nline2'" } }, 10 | { name: 'converts number', input: { it: 5 } }, 11 | { name: 'converts empty array', input: { it: [] } }, 12 | { name: 'converts simple simple', input: { it: [1, 2] } }, 13 | { name: 'converts multiple vars', input: { it: 'value', also: 'this' } }, 14 | { 15 | name: 'converts array with nested arrays', 16 | input: { it: [['list', 1], ['list', 2]] }, 17 | }, 18 | { 19 | name: 'converts object with nested array', 20 | input: { it: [15, 'px', { nested: ['oh', 'no'] }] }, 21 | }, 22 | { name: 'converts empty object', input: {} }, 23 | { name: 'converts simple object', input: { it: { a: 1 } } }, 24 | { name: 'converts multi value object', input: { it: { a: 1, b: 2 } } }, 25 | { 26 | name: 'converts nested object: nested', 27 | input: { it: { a: 1, b: 2, c: { d: 4 } } }, 28 | }, 29 | { 30 | name: 'converts nested object with array', 31 | input: { it: { a: 1, b: 2, c: { d: [4, 'px', 'em'] } } }, 32 | }, 33 | ] 34 | ;['sass', 'scss'].forEach(syntax => 35 | describe(`With ${syntax} syntax`, () => 36 | testCases.forEach(testCase => 37 | it(testCase.name, () => expect(convertJsToSass(testCase.input, syntax)).toMatchSnapshot()) 38 | )) 39 | ) 40 | -------------------------------------------------------------------------------- /src/utils/isModule.js: -------------------------------------------------------------------------------- 1 | const isModule = path => { 2 | try { 3 | require.resolve(path) 4 | return true 5 | } catch (e) { 6 | return false 7 | } 8 | } 9 | 10 | module.exports = isModule 11 | -------------------------------------------------------------------------------- /src/utils/isModule.test.js: -------------------------------------------------------------------------------- 1 | const isModule = require('./isModule') 2 | 3 | describe('isModule', () => { 4 | it('returns true if it is a module', () => { 5 | expect(isModule('fs')).toEqual(true) 6 | }) 7 | it('returns false if it is not a module', () => { 8 | expect(isModule('->definitelyNotAModule')).toEqual(false) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /src/utils/readSassFiles.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | module.exports = function(files) { 4 | return files.reduce((vars, filepath) => { 5 | if (filepath.match(/\.s[ac]ss/)) { 6 | return [vars, fs.readFileSync(filepath, 'utf8')].join('\n') 7 | } 8 | return vars 9 | }, '') 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/readVarsFromJSONFiles.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | module.exports = function(files) { 4 | return files.reduce( 5 | (vars, filepath) => 6 | Object.assign(vars, filepath.endsWith('.json') && JSON.parse(fs.readFileSync(filepath, 'utf8'))), 7 | {} 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/readVarsFromJSONFiles.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const readVarsFromJSONFiles = require('./readVarsFromJSONFiles') 3 | 4 | const files = [ 5 | path.resolve(__dirname, '../__mocks__/jsonVars1.json'), 6 | path.resolve(__dirname, '../__mocks__/jsVars1.js'), 7 | path.resolve(__dirname, '../__mocks__/jsonVars2.json'), 8 | ] 9 | 10 | it('returns a vars object as expected', () => { 11 | expect(readVarsFromJSONFiles(files)).toEqual({ 12 | value1FromJson: 'foo', 13 | value2FromJson: 'foo', 14 | loadingOrderTest1: 'fromJSON', 15 | loadingOrderTest2: 'fromJSON', 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/utils/readVarsFromJavascriptFiles.js: -------------------------------------------------------------------------------- 1 | module.exports = function(files) { 2 | return files.reduce((vars, filepath) => { 3 | if (!filepath.endsWith('.js')) { 4 | return vars 5 | } 6 | delete require.cache[filepath] 7 | return Object.assign(vars, require(filepath)) 8 | }, {}) 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/readVarsFromJavascriptFiles.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const readVarsFromJavascriptFiles = require('./readVarsFromJavascriptFiles') 3 | 4 | const files = [ 5 | path.resolve(__dirname, '../__mocks__/jsVars1.js'), 6 | path.resolve(__dirname, '../__mocks__/jsonVars1.json'), 7 | path.resolve(__dirname, '../__mocks__/jsVars2.js'), 8 | ] 9 | 10 | it('returns a vars object as expected', () => { 11 | expect(readVarsFromJavascriptFiles(files)).toEqual({ 12 | value1FromJs: 'foo', 13 | value2FromJs: 'foo', 14 | loadingOrderTest2: 'fromJS', 15 | loadingOrderTest3: 'fromJS', 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/utils/readVarsFromTypescriptFiles.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const ts = require('typescript') 3 | const requirefs = require('require-from-string') 4 | 5 | module.exports = function(files) { 6 | return files.reduce((vars, filepath) => { 7 | if (!filepath.endsWith('.ts')) { 8 | return vars 9 | } 10 | delete require.cache[filepath] 11 | 12 | const input = fs.readFileSync(filepath, 'utf8') 13 | const transpiledInput = ts.transpileModule(input, { 14 | compilerOptions: { module: ts.ModuleKind.CommonJS }, 15 | }) 16 | const result = requirefs(transpiledInput.outputText) 17 | return Object.assign(vars, result.default) 18 | }, {}) 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/readVarsFromTypescriptFiles.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const readVarsFromTypescriptFiles = require('./readVarsFromTypescriptFiles') 3 | 4 | const files = [ 5 | path.resolve(__dirname, '../__mocks__/jsVars1.js'), 6 | path.resolve(__dirname, '../__mocks__/jsonVars1.json'), 7 | path.resolve(__dirname, '../__mocks__/tsVars1.ts'), 8 | path.resolve(__dirname, '../__mocks__/jsVars2.js'), 9 | ] 10 | 11 | it('returns a vars object as expected', () => { 12 | expect(readVarsFromTypescriptFiles(files)).toEqual({ 13 | value1FromTs: 'tsFoo', 14 | loadingOrderTest4: 'fromTS', 15 | loadingOrderTest5: 'fromTS', 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/utils/transformKeys.js: -------------------------------------------------------------------------------- 1 | function transformKeys(vars, callback) { 2 | return Object.keys(vars).reduce((result, key) => { 3 | const value = vars[key] instanceof Object ? transformKeys(vars[key], callback) : vars[key] 4 | return Object.assign(result, { [callback(key)]: value }) 5 | }, {}) 6 | } 7 | 8 | module.exports = transformKeys 9 | -------------------------------------------------------------------------------- /src/utils/transformKeys.test.js: -------------------------------------------------------------------------------- 1 | const transformKeys = require('./transformKeys') 2 | 3 | const vars = { 4 | topPlainKey: 'hello', 5 | topNestedKey: { 6 | subNestedKey: { 7 | deepPlainKey: true, 8 | }, 9 | }, 10 | } 11 | 12 | describe('transformKeys', () => { 13 | it('Nested adding prefix', () => { 14 | expect(transformKeys(vars, key => `transformed-${key}`)).toStrictEqual({ 15 | 'transformed-topPlainKey': 'hello', 16 | 'transformed-topNestedKey': { 17 | 'transformed-subNestedKey': { 18 | 'transformed-deepPlainKey': true, 19 | }, 20 | }, 21 | }) 22 | }) 23 | 24 | it('Nested camelCase to lower case', () => { 25 | expect(transformKeys(vars, key => key.toLowerCase())).toStrictEqual({ 26 | topplainkey: 'hello', 27 | topnestedkey: { 28 | subnestedkey: { 29 | deepplainkey: true, 30 | }, 31 | }, 32 | }) 33 | }) 34 | 35 | it('Nested camelCase to UPPER CASE', () => { 36 | expect(transformKeys(vars, key => key.toUpperCase())).toStrictEqual({ 37 | TOPPLAINKEY: 'hello', 38 | TOPNESTEDKEY: { 39 | SUBNESTEDKEY: { 40 | DEEPPLAINKEY: true, 41 | }, 42 | }, 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/utils/transformObject.js: -------------------------------------------------------------------------------- 1 | function transformObject(varsObject, transformer) { 2 | if (transformer && {}.toString.call(transformer) === '[object Function]') return transformer(varsObject) 3 | return varsObject 4 | } 5 | 6 | module.exports = transformObject 7 | -------------------------------------------------------------------------------- /src/utils/transformObject.test.js: -------------------------------------------------------------------------------- 1 | const transformObject = require('./transformObject') 2 | 3 | const vars = { 4 | topPlainKey: 'hello', 5 | topNestedKey: { 6 | subNestedKey: { 7 | deepPlainKey: true, 8 | }, 9 | }, 10 | } 11 | 12 | describe('transformObject', () => { 13 | it('Passing a transform function', () => { 14 | expect(transformObject(vars, obj => ({ newObj: true }))).toStrictEqual({ newObj: true }) 15 | }) 16 | 17 | it('Passing a non function value', () => { 18 | expect(transformObject(vars, 'non-function')).toStrictEqual(vars) 19 | }) 20 | 21 | it('Not passing a transformer', () => { 22 | expect(transformObject(vars)).toStrictEqual(vars) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/utils/watchFileForChanges.js: -------------------------------------------------------------------------------- 1 | /** 2 | * watchFileForChanges 3 | * 4 | * Adds a file as loader dependency which will make Webpack watch 5 | * the file in watch-mode and reloads if it changes. 6 | */ 7 | 8 | module.exports = function(loader, file) { 9 | return loader.addDependency(file) 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/watchFileForChanges.test.js: -------------------------------------------------------------------------------- 1 | const watchFileForChanges = require('./watchFileForChanges') 2 | 3 | const mockLoader = { 4 | addDependency: jest.fn(), 5 | } 6 | 7 | describe('watchFileForChanges', () => { 8 | beforeAll(() => { 9 | watchFileForChanges(mockLoader, 'file1') 10 | }) 11 | it('calls `loader.addDependency`', () => { 12 | expect(mockLoader.addDependency).toHaveBeenCalledTimes(1) 13 | expect(mockLoader.addDependency).toHaveBeenCalledWith('file1') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/utils/watchFilesForChanges.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const watchFileForChanges = require('./watchFileForChanges') 3 | const watchModuleForChanges = require('./watchModuleForChanges') 4 | const isModule = require('./isModule') 5 | 6 | /** 7 | * watchFilesForChanges 8 | * 9 | * Adds files as loader dependency which will make Webpack watch 10 | * the files in watch-mode and reload if they change. 11 | */ 12 | 13 | async function watchFilesForChanges(loader, files) { 14 | for (const file of files) { 15 | if (fs.existsSync(file)) { 16 | watchFileForChanges(loader, file) 17 | } else if (isModule(file)) { 18 | await watchModuleForChanges(loader, file) 19 | } else { 20 | throw new Error(`Invalid file: "${file}". Consider using "path.resolve" in your config.`) 21 | } 22 | } 23 | } 24 | 25 | module.exports = watchFilesForChanges 26 | -------------------------------------------------------------------------------- /src/utils/watchFilesForChanges.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const watchFilesForChanges = require('./watchFilesForChanges') 3 | const watchFileForChanges = require('./watchFileForChanges') 4 | const watchModuleForChanges = require('./watchModuleForChanges') 5 | 6 | jest.mock('./watchFileForChanges') 7 | jest.mock('./watchModuleForChanges') 8 | 9 | describe('watchFilesForChanges', () => { 10 | it('watches modules', async () => { 11 | watchFileForChanges.mockClear() 12 | watchModuleForChanges.mockClear() 13 | 14 | let error 15 | try { 16 | await watchFilesForChanges({}, ['fs']) 17 | } catch (e) { 18 | error = e 19 | } 20 | 21 | expect(error).toEqual(undefined) 22 | expect(watchFileForChanges).toHaveBeenCalledTimes(0) 23 | expect(watchModuleForChanges).toHaveBeenCalledTimes(1) 24 | expect(watchModuleForChanges).toHaveBeenCalledWith(expect.anything(), 'fs') 25 | }) 26 | 27 | it('watches files', async () => { 28 | watchFileForChanges.mockClear() 29 | watchModuleForChanges.mockClear() 30 | 31 | let error 32 | const file = path.resolve(__dirname, '../__mocks__/jsVars1.js') 33 | try { 34 | await watchFilesForChanges({}, [file]) 35 | } catch (e) { 36 | error = e 37 | } 38 | 39 | expect(error).toEqual(undefined) 40 | expect(watchModuleForChanges).toHaveBeenCalledTimes(0) 41 | expect(watchFileForChanges).toHaveBeenCalledTimes(1) 42 | expect(watchFileForChanges).toHaveBeenCalledWith(expect.anything(), file) 43 | }) 44 | 45 | it('throws error for invalid file', async () => { 46 | watchFileForChanges.mockClear() 47 | watchModuleForChanges.mockClear() 48 | 49 | let error 50 | const file = '~~invalid~~' 51 | try { 52 | await watchFilesForChanges({}, [file]) 53 | } catch (e) { 54 | error = e 55 | } 56 | 57 | expect(error.message).toEqual(`Invalid file: "${file}". Consider using "path.resolve" in your config.`) 58 | expect(watchModuleForChanges).toHaveBeenCalledTimes(0) 59 | expect(watchFileForChanges).toHaveBeenCalledTimes(0) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /src/utils/watchModuleForChanges.js: -------------------------------------------------------------------------------- 1 | /** 2 | * watchModuleForChanges 3 | * 4 | * Adds a file from a module as loader dependency which will make Webpack watch 5 | * the file in watch-mode and reload if it changes. 6 | */ 7 | 8 | module.exports = async function(loader, file) { 9 | return new Promise((resolve, reject) => { 10 | delete require.cache[require.resolve(file)] 11 | loader.resolve(loader.rootContext, file, (err, resolvedFile) => { 12 | if (err) return reject(err) 13 | loader.addDependency(resolvedFile) 14 | resolve() 15 | }) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/watchModuleForChanges.test.js: -------------------------------------------------------------------------------- 1 | const watchModuleForChanges = require('./watchModuleForChanges') 2 | 3 | let mockError 4 | const mockLoader = { 5 | addDependency: jest.fn(), 6 | resolve: jest.fn((context, file, callback) => { 7 | callback(mockError, file) 8 | }), 9 | } 10 | 11 | describe('watchModuleForChanges', () => { 12 | beforeEach(() => { 13 | mockLoader.addDependency.mockClear() 14 | }) 15 | it('calls `loader.addDependency`', async () => { 16 | let error 17 | try { 18 | await watchModuleForChanges(mockLoader, 'fs') 19 | } catch (e) { 20 | error = e 21 | } 22 | expect(error).toEqual(undefined) 23 | expect(mockLoader.addDependency).toHaveBeenCalledTimes(1) 24 | expect(mockLoader.addDependency).toHaveBeenCalledWith('fs') 25 | }) 26 | 27 | it('rejects if require cannot resolve the module', async () => { 28 | const moduleName = '->definitelyNotAModule' 29 | let error 30 | try { 31 | await watchModuleForChanges(mockLoader, moduleName) 32 | } catch (e) { 33 | error = e 34 | } 35 | expect(error.message).toEqual(`Cannot find module '${moduleName}' from 'watchModuleForChanges.js'`) 36 | expect(mockLoader.addDependency).toHaveBeenCalledTimes(0) 37 | }) 38 | }) 39 | --------------------------------------------------------------------------------