├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .nycrc ├── LICENSE.md ├── README.md ├── circle.yml ├── install-babel-6.sh ├── install-babel-7.sh ├── package.json ├── scripts └── release.sh ├── src ├── index.js ├── options_resolvers │ ├── append.js │ ├── camelCase.js │ ├── createImportedName.js │ ├── devMode.js │ ├── extensions.js │ ├── generateScopedName.js │ ├── hashPrefix.js │ ├── ignore.js │ ├── importPathFormatter.js │ ├── index.js │ ├── mode.js │ ├── prepend.js │ ├── preprocessCss.js │ ├── processCss.js │ ├── processorOpts.js │ ├── resolve.js │ ├── rootDir.js │ └── use.js └── utils │ ├── extractCssFile.js │ ├── index.js │ ├── isBoolean.js │ ├── isFunction.js │ ├── isModulePath.js │ ├── isPlainObject.js │ ├── isRegExp.js │ ├── isString.js │ ├── requireLocalFileOrNodeModule.js │ └── writeCssFile.js ├── test ├── css │ └── child.css ├── extensions.scss ├── fixtures │ ├── append.module.js │ ├── exctractcss.include.js │ ├── exctractcss.main.expected.babel7.js │ ├── exctractcss.main.expected.js │ ├── exctractcss.main.js │ ├── extensions.expected.babel7.js │ ├── extensions.expected.js │ ├── extensions.js │ ├── extractcss.combined.expected.css │ ├── extractcss.css.child.expected.css │ ├── extractcss.parent-combined.expected.css │ ├── extractcss.parent.expected.css │ ├── extractcss.styles.expected.css │ ├── generateScopedName.function.module.js │ ├── generateScopedName.module.js │ ├── global.import.js │ ├── global.require.js │ ├── import.expected.babel7.js │ ├── import.expected.js │ ├── import.js │ ├── keepImport.expected.babel7.js │ ├── keepImport.expected.js │ ├── keepImport.js │ ├── prepend.module.js │ ├── preprocessCss.module.js │ ├── processCss.module.js │ ├── require.expected.babel7.js │ ├── require.expected.js │ ├── require.ignored.expected.babel7.js │ ├── require.ignored.expected.js │ └── require.js ├── index.spec.js ├── mocha.opts ├── options_resolvers │ ├── append.spec.js │ ├── camelCase.spec.js │ ├── devMode.spec.js │ ├── extensions.spec.js │ ├── generateScopedName.spec.js │ ├── hashPrefix.spec.js │ ├── ignore.spec.js │ ├── mode.spec.js │ ├── prepend.spec.js │ ├── preprocessCss.spec.js │ ├── processCss.spec.js │ ├── processorOpts.spec.js │ ├── resolve.spec.js │ ├── rootDir.spec.js │ └── use.spec.js ├── parent.css ├── styles.css └── utils │ ├── isBoolean.spec.js │ ├── isFunction.spec.js │ ├── isModulePath.spec.js │ ├── isPlainObject.spec.js │ ├── isRegExp.spec.js │ ├── isString.spec.js │ └── requireLocalFileOrNodeModule.spec.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "targets": { "node": "6.12" }}] 4 | ], 5 | "plugins": [ 6 | "transform-object-rest-spread" 7 | ], 8 | "env": { 9 | "test": { 10 | "plugins": [ 11 | "istanbul" 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | 5 | # Change these settings to your own preference 6 | indent_style = space 7 | indent_size = 2 8 | 9 | # We recommend you to keep these unchanged 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | test/fixtures 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-lite", 3 | "env": { 4 | "mocha": true, 5 | "node": true 6 | }, 7 | "rules": { 8 | "padded-blocks": 0, 9 | "comma-dangle": [2, "never"], 10 | "indent": [2, 4, {"SwitchCase": 1}] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### JetBrains template 2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 3 | 4 | *.iml 5 | 6 | ## Directory-based project format: 7 | .idea/ 8 | # if you remove the above rule, at least ignore the following: 9 | 10 | # User-specific stuff: 11 | # .idea/workspace.xml 12 | # .idea/tasks.xml 13 | # .idea/dictionaries 14 | 15 | # Sensitive or high-churn files: 16 | # .idea/dataSources.ids 17 | # .idea/dataSources.xml 18 | # .idea/sqlDataSources.xml 19 | # .idea/dynamic.xml 20 | # .idea/uiDesigner.xml 21 | 22 | # Gradle: 23 | # .idea/gradle.xml 24 | # .idea/libraries 25 | 26 | # Mongo Explorer plugin: 27 | # .idea/mongoSettings.xml 28 | 29 | ## File-based project format: 30 | *.ipr 31 | *.iws 32 | 33 | ## Plugin-specific files: 34 | 35 | # IntelliJ 36 | /out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Crashlytics plugin (for Android Studio and IntelliJ) 45 | com_crashlytics_export_strings.xml 46 | crashlytics.properties 47 | crashlytics-build.properties 48 | ### OSX template 49 | .DS_Store 50 | .AppleDouble 51 | .LSOverride 52 | 53 | # Icon must end with two \r 54 | Icon 55 | 56 | # Thumbnails 57 | ._* 58 | 59 | # code coverage 60 | .nyc_output 61 | coverage 62 | 63 | # Files that might appear in the root of a volume 64 | .DocumentRevisions-V100 65 | .fseventsd 66 | .Spotlight-V100 67 | .TemporaryItems 68 | .Trashes 69 | .VolumeIcon.icns 70 | 71 | # Directories potentially created on remote AFP share 72 | .AppleDB 73 | .AppleDesktop 74 | Network Trash Folder 75 | Temporary Items 76 | .apdisk 77 | node_modules 78 | build 79 | 80 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | scripts 4 | .babelrc 5 | .eslintrc 6 | circle.yml 7 | 8 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "require": [ 3 | "babel-register" 4 | ], 5 | "sourceMap": false, 6 | "instrument": false 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Michal Kvasničák 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-css-modules-transform [![Circle CI](https://circleci.com/gh/michalkvasnicak/babel-plugin-css-modules-transform.svg?style=svg)](https://circleci.com/gh/michalkvasnicak/babel-plugin-css-modules-transform) 2 | 3 | **🎉 Babel 6 and Babel 7 compatible** 4 | 5 | **⚠️ Babel 7 compatibility added in 1.4.0** 6 | 7 | This Babel plugin finds all `require`s for css module files and replace them with a hash where keys are class names and values are generated css class names. 8 | 9 | This plugin is based on the fantastic [css-modules-require-hook](https://github.com/css-modules/css-modules-require-hook). 10 | 11 | ## Warning 12 | 13 | This plugin is experimental, pull requests are welcome. 14 | 15 | **Do not run this plugin as part of webpack frontend configuration. This plugin is intended only for backend compilation.** 16 | 17 | ## Example 18 | 19 | ```css 20 | /* test.css */ 21 | 22 | .someClass { 23 | color: red; 24 | } 25 | ``` 26 | 27 | ```js 28 | // component.js 29 | const styles = require('./test.css'); 30 | 31 | console.log(styles.someClass); 32 | 33 | // transformed file 34 | const styles = { 35 | 'someClass': 'Test__someClass___2Frqu' 36 | } 37 | 38 | console.log(styles.someClass); // prints Test__someClass___2Frqu 39 | ``` 40 | 41 | ## Installation 42 | 43 | ```console 44 | npm install --save-dev babel-plugin-css-modules-transform 45 | ``` 46 | 47 | **Include plugin in `.babelrc`** 48 | 49 | ```json 50 | { 51 | "plugins": ["css-modules-transform"] 52 | } 53 | ``` 54 | 55 | **With custom options [css-modules-require-hook options](https://github.com/css-modules/css-modules-require-hook#tuning-options)** 56 | 57 | 58 | ```js 59 | { 60 | "plugins": [ 61 | [ 62 | "css-modules-transform", { 63 | "append": [ 64 | "npm-module-name", 65 | "./path/to/module-exporting-a-function.js" 66 | ], 67 | "camelCase": false, 68 | "createImportedName": "npm-module-name", 69 | "createImportedName": "./path/to/module-exporting-a-function.js", 70 | "devMode": false, 71 | "extensions": [".css", ".scss", ".less"], // list extensions to process; defaults to .css 72 | "generateScopedName": "[name]__[local]___[hash:base64:5]", // in case you don't want to use a function 73 | "generateScopedName": "./path/to/module-exporting-a-function.js", // in case you want to use a function 74 | "generateScopedName": "npm-module-name", 75 | "hashPrefix": "string", 76 | "ignore": "*css", 77 | "ignore": "./path/to/module-exporting-a-function-or-regexp.js", 78 | "preprocessCss": "./path/to/module-exporting-a-function.js", 79 | "preprocessCss": "npm-module-name", 80 | "processCss": "./path/to/module-exporting-a-function.js", 81 | "processCss": "npm-module-name", 82 | "processorOpts": "npm-module-name", 83 | "processorOpts": "./path/to/module/exporting-a-plain-object.js", 84 | "mode": "string", 85 | "prepend": [ 86 | "npm-module-name", 87 | "./path/to/module-exporting-a-function.js" 88 | ], 89 | "extractCss": "./dist/stylesheets/combined.css" 90 | } 91 | ] 92 | ] 93 | } 94 | ``` 95 | 96 | ## Using a preprocessor 97 | 98 | When using this plugin with a preprocessor, you'll need to configure it as such: 99 | 100 | 101 | ```js 102 | // ./path/to/module-exporting-a-function.js 103 | var sass = require('node-sass'); 104 | var path = require('path'); 105 | 106 | module.exports = function processSass(data, filename) { 107 | var result; 108 | result = sass.renderSync({ 109 | data: data, 110 | file: filename 111 | }).css; 112 | return result.toString('utf8'); 113 | }; 114 | ``` 115 | 116 | and then add any relevant extensions to your plugin config: 117 | 118 | ```js 119 | { 120 | "plugins": [ 121 | [ 122 | "css-modules-transform", { 123 | "preprocessCss": "./path/to/module-exporting-a-function.js", 124 | "extensions": [".css", ".scss"] 125 | } 126 | ] 127 | ] 128 | } 129 | 130 | ``` 131 | 132 | ## Extract CSS Files 133 | 134 | When you publish a library, you might want to ship compiled css files as well to 135 | help integration in other projects. 136 | 137 | An more complete alternative is to use 138 | [babel-plugin-webpack-loaders](https://github.com/istarkov/babel-plugin-webpack-loaders) 139 | but be aware that a new webpack instance is run for each css file, this has a 140 | huge overhead. If you do not use fancy stuff, you might consider using 141 | [babel-plugin-css-modules-transform](https://github.com/michalkvasnicak/babel-plugin-css-modules-transform) 142 | instead. 143 | 144 | 145 | To combine all css files in a single file, give its name: 146 | 147 | ```js 148 | { 149 | "plugins": [ 150 | [ 151 | "css-modules-transform", { 152 | "extractCss": "./dist/stylesheets/combined.css" 153 | } 154 | ] 155 | ] 156 | } 157 | ``` 158 | 159 | To extract all files in a single directory, give an object: 160 | 161 | ```js 162 | { 163 | "plugins": [ 164 | [ 165 | "css-modules-transform", { 166 | "extractCss": { 167 | "dir": "./dist/stylesheets/", 168 | "relativeRoot": "./src/", 169 | "filename": "[path]/[name].css" 170 | } 171 | } 172 | ] 173 | ] 174 | } 175 | ``` 176 | 177 | Note that `relativeRoot` is used to resolve relative directory names, available 178 | as `[path]` in `filename` pattern. 179 | 180 | ## Keeping import 181 | 182 | To keep import statements you should set option `keepImport` to *true*. In this way, simultaneously with the converted values, the import will be described as unassigned call expression. 183 | 184 | ```js 185 | // before 186 | const styles = require('./test.css'); 187 | ``` 188 | 189 | ```js 190 | // after 191 | require('./test.css'); 192 | 193 | const styles = { 194 | 'someClass': 'Test__someClass___2Frqu' 195 | } 196 | ``` 197 | 198 | ## Alternatives 199 | 200 | - [babel-plugin-transform-postcss](https://github.com/wbyoung/babel-plugin-transform-postcss) - which supports async plugins and does not depend on `css-modules-require-hook`. 201 | 202 | ## License 203 | 204 | MIT 205 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin" 4 | node: 5 | version: 6.12.3 6 | 7 | dependencies: 8 | cache_directories: 9 | - ~/.cache/yarn 10 | 11 | dependencies: 12 | override: 13 | - yarn 14 | 15 | test: 16 | override: 17 | - yarn test 18 | # test with babel 7 19 | - ./install-babel-7.sh 20 | - BABEL_7=1 yarn test 21 | -------------------------------------------------------------------------------- /install-babel-6.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # fail on error 4 | set -e 5 | 6 | # remove babel core 7 | yarn remove @babel/core 8 | 9 | # install babel6 10 | yarn add -D babel-cli@^6.26.0 babel-core@^6.26.0 babel-plugin-transform-object-rest-spread@^6.26.0 babel-preset-env@^1.6.1 babel-register@^6.26.0 11 | -------------------------------------------------------------------------------- /install-babel-7.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # fail on error 4 | set -e 5 | 6 | # install babel7 deps 7 | yarn add -D babel-cli@^7.0.0-beta.3 babel-core@^7.0.0-beta.3 babel-register@^7.0.0-beta.3 babel-preset-env@^7.0.0-beta.3 @babel/core babel-plugin-transform-object-rest-spread@^7.0.0-beta.3 gulp-babel@7.0.0 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-css-modules-transform", 3 | "version": "1.6.2", 4 | "description": "Transform required css modules so one can use generated class names.", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "build": "babel src --ignore **/*.spec.js -d build", 8 | "lint": "eslint src", 9 | "pretest": "npm run lint", 10 | "test": "cross-env NODE_ENV=test nyc mocha" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/michalkvasnicak/babel-plugin-css-modules-transform.git" 15 | }, 16 | "keywords": [ 17 | "babel", 18 | "css-modules", 19 | "babel-plugin", 20 | "plugin" 21 | ], 22 | "author": "Michal Kvasničák (http://kvasnicak.info)", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/michalkvasnicak/babel-plugin-css-modules-transform/issues" 26 | }, 27 | "homepage": "https://github.com/michalkvasnicak/babel-plugin-css-modules-transform#readme", 28 | "dependencies": { 29 | "css-modules-require-hook": "^4.0.6", 30 | "mkdirp": "^0.5.1" 31 | }, 32 | "devDependencies": { 33 | "babel-cli": "^6.26.0", 34 | "babel-core": "^6.26.0", 35 | "babel-eslint": "^7.1.1", 36 | "babel-plugin-istanbul": "^4.1.3", 37 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 38 | "babel-preset-env": "^1.6.1", 39 | "babel-register": "^6.26.0", 40 | "chai": "^3.4.1", 41 | "cross-env": "^5.0.0", 42 | "eslint": "^1.9.0", 43 | "eslint-config-airbnb-lite": "^1.0.0", 44 | "gulp-babel": "7.0.0", 45 | "gulp-util": "^3.0.7", 46 | "mocha": "^3.4.2", 47 | "nyc": "^10.3.2", 48 | "postcss": "^5.x", 49 | "postcss-modules-extract-imports": "^1.x", 50 | "postcss-modules-local-by-default": "^1.x", 51 | "postcss-modules-scope": "^1.x", 52 | "postcss-modules-values": "^1.x", 53 | "rimraf": "^2.5.4" 54 | }, 55 | "engines": { 56 | "node": ">=4.0.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | update_version() { 4 | echo "$(node -p "p=require('./${1}');p.version='${2}';JSON.stringify(p,null,2)")" > $1 5 | echo "Updated ${1} version to ${2}" 6 | } 7 | 8 | validate_semver() { 9 | if ! [[ $1 =~ ^[0-9]\.[0-9]+\.[0-9](-.+)? ]]; then 10 | echo "Version $1 is not valid! It must be a valid semver string like 1.0.2 or 2.3.0-beta.1" 11 | exit 1 12 | fi 13 | } 14 | 15 | current_version=$(node -p "require('./package').version") 16 | 17 | printf "Next version (current is $current_version)? " 18 | read next_version 19 | 20 | validate_semver $next_version 21 | 22 | next_ref="v$next_version" 23 | 24 | npm test 25 | 26 | update_version 'package.json' $next_version 27 | 28 | npm run build 29 | 30 | git commit -am "Version $next_version" 31 | 32 | git tag $next_ref 33 | git tag latest -f 34 | 35 | git push origin master 36 | git push origin $next_ref 37 | git push origin latest -f 38 | 39 | npm publish 40 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { resolve, dirname, isAbsolute } from 'path'; 2 | 3 | // options resolvers 4 | import * as requireHooksOptions from './options_resolvers'; 5 | 6 | // utils. 7 | import { extractCssFile } from './utils'; 8 | 9 | const defaultOptions = { 10 | generateScopedName: '[name]__[local]___[hash:base64:5]' 11 | }; 12 | 13 | function updateStyleSheetPath(pathStringLiteral, importPathFormatter) { 14 | if (!importPathFormatter) { return pathStringLiteral; } 15 | 16 | return { 17 | ...pathStringLiteral, 18 | value: importPathFormatter(pathStringLiteral.value) 19 | }; 20 | } 21 | 22 | 23 | function findExpressionStatementChild(path, t) { 24 | const parent = path.parentPath; 25 | if (!parent) { 26 | throw new Error('Invalid expression structure'); 27 | } 28 | if ( 29 | t.isExpressionStatement(parent) 30 | || t.isProgram(parent) 31 | || t.isBlockStatement(parent) 32 | ) { 33 | return path; 34 | } 35 | return findExpressionStatementChild(parent, t); 36 | } 37 | 38 | export default function transformCssModules({ types: t }) { 39 | function resolveModulePath(filename) { 40 | const dir = dirname(filename); 41 | if (isAbsolute(dir)) return dir; 42 | if (process.env.PWD) return resolve(process.env.PWD, dir); 43 | return resolve(dir); 44 | } 45 | 46 | /** 47 | * 48 | * @param {String} filepath javascript file path 49 | * @param {String} cssFile requireed css file path 50 | * @returns {Array} array of class names 51 | */ 52 | function requireCssFile(filepath, cssFile) { 53 | let filePathOrModuleName = cssFile; 54 | 55 | // only resolve path to file when we have a file path 56 | if (!/^\w/i.test(filePathOrModuleName)) { 57 | const from = resolveModulePath(filepath); 58 | filePathOrModuleName = resolve(from, filePathOrModuleName); 59 | } 60 | 61 | // css-modules-require-hooks throws if file is ignored 62 | try { 63 | return require(filePathOrModuleName); 64 | } catch (e) { 65 | // As a last resort, require the cssFile itself. This enables loading of CSS files from external deps 66 | try { 67 | return require(cssFile); 68 | } catch (f) { 69 | return {}; // return empty object, this simulates result of ignored stylesheet file 70 | } 71 | } 72 | } 73 | 74 | // is css modules require hook initialized? 75 | let initialized = false; 76 | // are we requiring a module for preprocessCss, processCss, etc? 77 | // we don't want them to be transformed using this plugin 78 | // because it will cause circular dependency in babel-node and babel-register process 79 | let inProcessingFunction = false; 80 | 81 | let matchExtensions = /\.css$/i; 82 | 83 | function matcher(extensions = ['.css']) { 84 | const extensionsPattern = extensions.join('|').replace(/\./g, '\\\.'); 85 | return new RegExp(`(${extensionsPattern})$`, 'i'); 86 | } 87 | 88 | function buildClassNameToScopeNameMap(tokens) { 89 | /* eslint-disable new-cap */ 90 | return t.ObjectExpression( 91 | Object.keys(tokens).map(token => 92 | t.ObjectProperty( 93 | t.StringLiteral(token), 94 | t.StringLiteral(tokens[token]) 95 | ) 96 | ) 97 | ); 98 | } 99 | 100 | const cssMap = new Map(); 101 | let thisPluginOptions = null; 102 | 103 | const pluginApi = { 104 | manipulateOptions(options) { 105 | if (initialized || inProcessingFunction) { 106 | return options; 107 | } 108 | 109 | // find options for this plugin 110 | // we have to use this hack because plugin.key does not have to be 'css-modules-transform' 111 | // so we will identify it by comparing manipulateOptions 112 | if (Array.isArray(options.plugins[0])) { // babel 6 113 | thisPluginOptions = options.plugins.filter( 114 | ([plugin]) => plugin.manipulateOptions === pluginApi.manipulateOptions 115 | )[0][1]; 116 | } else { // babel 7 117 | thisPluginOptions = options.plugins.filter( 118 | (plugin) => plugin.manipulateOptions === pluginApi.manipulateOptions 119 | )[0].options; 120 | } 121 | 122 | const currentConfig = { ...defaultOptions, ...thisPluginOptions }; 123 | // this is not a css-require-ook config 124 | delete currentConfig.extractCss; 125 | delete currentConfig.keepImport; 126 | delete currentConfig.importPathFormatter; 127 | 128 | // match file extensions, speeds up transform by creating one 129 | // RegExp ahead of execution time 130 | matchExtensions = matcher(currentConfig.extensions); 131 | 132 | const pushStylesCreator = (toWrap) => (css, filepath) => { 133 | let processed; 134 | 135 | if (typeof toWrap === 'function') { 136 | processed = toWrap(css, filepath); 137 | } 138 | 139 | if (typeof processed !== 'string') processed = css; 140 | 141 | // set css content only if is new 142 | if (!cssMap.has(filepath) || cssMap.get(filepath) !== processed) { 143 | cssMap.set(filepath, processed); 144 | } 145 | 146 | return processed; 147 | }; 148 | 149 | // resolve options 150 | Object.keys(requireHooksOptions).forEach(key => { 151 | // skip undefined options 152 | if (currentConfig[key] === undefined) { 153 | if (key === 'importPathFormatter' && thisPluginOptions && thisPluginOptions[key]) { 154 | thisPluginOptions[key] = requireHooksOptions[key](thisPluginOptions[key]); 155 | } 156 | return; 157 | } 158 | 159 | inProcessingFunction = true; 160 | currentConfig[key] = requireHooksOptions[key](currentConfig[key], currentConfig); 161 | inProcessingFunction = false; 162 | }); 163 | 164 | // wrap or define processCss function that collect generated css 165 | currentConfig.processCss = pushStylesCreator(currentConfig.processCss); 166 | 167 | require('css-modules-require-hook')(currentConfig); 168 | 169 | initialized = true; 170 | 171 | return options; 172 | }, 173 | post() { 174 | // extract css only if is this option set 175 | if (thisPluginOptions && thisPluginOptions.extractCss) { 176 | // always rewrite file :-/ 177 | extractCssFile( 178 | process.cwd(), 179 | cssMap, 180 | thisPluginOptions.extractCss 181 | ); 182 | } 183 | }, 184 | visitor: { 185 | // import styles from './style.css'; 186 | ImportDefaultSpecifier(path, { file }) { 187 | const { value } = path.parentPath.node.source; 188 | 189 | if (matchExtensions.test(value)) { 190 | const requiringFile = file.opts.filename; 191 | const tokens = requireCssFile(requiringFile, value); 192 | 193 | const varDeclaration = t.variableDeclaration( 194 | 'var', 195 | [ 196 | t.variableDeclarator( 197 | t.identifier(path.node.local.name), 198 | buildClassNameToScopeNameMap(tokens) 199 | ) 200 | ] 201 | ); 202 | 203 | if (thisPluginOptions && thisPluginOptions.keepImport === true) { 204 | path.parentPath.replaceWithMultiple([ 205 | t.expressionStatement( 206 | t.callExpression( 207 | t.identifier('require'), 208 | [updateStyleSheetPath(t.stringLiteral(value), thisPluginOptions.importPathFormatter)] 209 | ) 210 | ), 211 | varDeclaration 212 | ]); 213 | } else { 214 | path.parentPath.replaceWith(varDeclaration); 215 | } 216 | } 217 | }, 218 | 219 | // const styles = require('./styles.css'); 220 | CallExpression(path, { file }) { 221 | const { callee: { name: calleeName }, arguments: args } = path.node; 222 | 223 | if (calleeName !== 'require' || !args.length || !t.isStringLiteral(args[0])) { 224 | return; 225 | } 226 | 227 | const [{ value: stylesheetPath }] = args; 228 | 229 | if (matchExtensions.test(stylesheetPath)) { 230 | const requiringFile = file.opts.filename; 231 | const tokens = requireCssFile(requiringFile, stylesheetPath); 232 | 233 | // if parent expression is not a Program, replace expression with tokens 234 | // Otherwise remove require from file, we just want to get generated css for our output 235 | if (!t.isExpressionStatement(path.parent)) { 236 | path.replaceWith(buildClassNameToScopeNameMap(tokens)); 237 | 238 | // Keeped import will places before closest expression statement child 239 | if (thisPluginOptions && thisPluginOptions.keepImport === true) { 240 | findExpressionStatementChild(path, t).insertBefore( 241 | t.expressionStatement( 242 | t.callExpression( 243 | t.identifier('require'), 244 | [updateStyleSheetPath(t.stringLiteral(stylesheetPath), thisPluginOptions.importPathFormatter)] 245 | ) 246 | ) 247 | ); 248 | } 249 | } else if (!thisPluginOptions || thisPluginOptions.keepImport !== true) { 250 | path.remove(); 251 | } 252 | } 253 | } 254 | } 255 | }; 256 | 257 | return pluginApi; 258 | } 259 | -------------------------------------------------------------------------------- /src/options_resolvers/append.js: -------------------------------------------------------------------------------- 1 | import { isFunction, isModulePath, requireLocalFileOrNodeModule } from '../utils'; 2 | 3 | /** 4 | * Resolves append option for css-modules-require-hook 5 | * 6 | * @param {*} value 7 | * @returns {Function} 8 | */ 9 | export default function append(value/* , currentConfig */) { 10 | if (Array.isArray(value)) { 11 | return value.map((option, index) => { 12 | if (isFunction(option)) { 13 | return option(); 14 | } else if (isModulePath(option)) { 15 | const requiredOption = requireLocalFileOrNodeModule(option); 16 | 17 | if (!isFunction(requiredOption)) { 18 | throw new Error(`Configuration 'append[${index}]' module is not exporting a function`); 19 | } 20 | 21 | return requiredOption(); 22 | } 23 | 24 | throw new Error(`Configuration 'append[${index}]' is not a function or a valid module path`); 25 | }); 26 | } 27 | 28 | throw new Error(`Configuration 'append' is not an array`); 29 | } 30 | -------------------------------------------------------------------------------- /src/options_resolvers/camelCase.js: -------------------------------------------------------------------------------- 1 | import { isBoolean } from '../utils'; 2 | 3 | /** 4 | * Resolves camelCase option for css-modules-require-hook 5 | * 6 | * @param {*} value 7 | * @returns {boolean} 8 | */ 9 | export default function camelCase(value/* , currentConfig */) { 10 | if (!isBoolean(value) && ['dashes', 'dashesOnly', 'only'].indexOf(value) < 0) { 11 | throw new Error( 12 | `Configuration 'camelCase' is not a boolean or one of 'dashes'|'dashesOnly'|'only'` 13 | ); 14 | } 15 | 16 | return value; 17 | } 18 | -------------------------------------------------------------------------------- /src/options_resolvers/createImportedName.js: -------------------------------------------------------------------------------- 1 | import { isFunction, isModulePath, requireLocalFileOrNodeModule } from '../utils'; 2 | 3 | /** 4 | * Resolves createImportedName css-modules-require-hook option 5 | * 6 | * @param {String|Function} value 7 | * @returns {Function} 8 | */ 9 | export default function createImportedName(value/* , currentConfig */) { 10 | if (isFunction(value)) { 11 | return value; 12 | } else if (isModulePath(value)) { 13 | const requiredOption = requireLocalFileOrNodeModule(value); 14 | 15 | if (!isFunction(requiredOption)) { 16 | throw new Error(`Configuration file for 'createImportedName' is not exporting a function`); 17 | } 18 | 19 | return requiredOption; 20 | } 21 | 22 | throw new Error(`Configuration 'createImportedName' is not a function nor a valid module path`); 23 | } 24 | -------------------------------------------------------------------------------- /src/options_resolvers/devMode.js: -------------------------------------------------------------------------------- 1 | import { isBoolean } from '../utils'; 2 | 3 | /** 4 | * Resolves devMode option for css-modules-require-hook 5 | * 6 | * @param {*} value 7 | * @returns {boolean} 8 | */ 9 | export default function devMode(value/* , currentConfig */) { 10 | if (!isBoolean(value)) { 11 | throw new Error(`Configuration 'devMode' is not a boolean`); 12 | } 13 | 14 | return value; 15 | } 16 | -------------------------------------------------------------------------------- /src/options_resolvers/extensions.js: -------------------------------------------------------------------------------- 1 | import { isString } from '../utils'; 2 | 3 | /** 4 | * Resolves extensions for css-modules-require-hook 5 | * 6 | * @param {*} value 7 | * @returns {Array.} 8 | */ 9 | export default function extensions(value/* , currentConfig */) { 10 | if (Array.isArray(value)) { 11 | return value.map((extension, index) => { 12 | if (!isString(extension)) { 13 | throw new Error(`Configuration 'extensions[${index}]' is not a string`); 14 | } 15 | 16 | return extension; 17 | }); 18 | } 19 | 20 | throw new Error(`Configuration 'extensions' is not an array`); 21 | } 22 | -------------------------------------------------------------------------------- /src/options_resolvers/generateScopedName.js: -------------------------------------------------------------------------------- 1 | import { isModulePath, isFunction, isString, requireLocalFileOrNodeModule } from '../utils'; 2 | 3 | /** 4 | * Resolves generateScopedName option for css-modules-require-hook 5 | * 6 | * @param {String|Function} value 7 | * 8 | * @returns {String|Function} 9 | */ 10 | export default function generateScopedName(value/* ,currentConfig */) { 11 | if (isModulePath(value)) { 12 | const requiredModule = requireLocalFileOrNodeModule(value); 13 | 14 | if (isString(requiredModule) || isFunction(requiredModule)) { 15 | return requiredModule; 16 | } 17 | 18 | throw new Error(`Configuration file for 'generateScopedName' is not exporting a string nor a function`); 19 | } else if (isString(value) || isFunction(value)) { 20 | return value; 21 | } else { 22 | throw new Error(`Configuration 'generateScopedName' is not a function, string nor valid path to module`); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/options_resolvers/hashPrefix.js: -------------------------------------------------------------------------------- 1 | import { isString } from '../utils'; 2 | 3 | /** 4 | * Resolves hashPrefix option for css-modules-require-hook 5 | * 6 | * @param {*} value 7 | * @returns {String} 8 | */ 9 | export default function hashPrefix(value/* , currentConfig */) { 10 | if (!isString(value)) { 11 | throw new Error(`Configuration 'hashPrefix' is not a string`); 12 | } 13 | 14 | return value; 15 | } 16 | -------------------------------------------------------------------------------- /src/options_resolvers/ignore.js: -------------------------------------------------------------------------------- 1 | import { isFunction, isModulePath, isRegExp, isString, requireLocalFileOrNodeModule } from '../utils'; 2 | 3 | /** 4 | * Resolves ignore option for css-modules-require-hook 5 | * 6 | * @param {*} value 7 | * @returns {Function|String|RegExp} 8 | */ 9 | export default function ignore(value/* , currentConfig */) { 10 | if (isFunction(value) || isRegExp(value)) { 11 | return value; 12 | } else if (isModulePath(value)) { 13 | const requiredOption = requireLocalFileOrNodeModule(value); 14 | 15 | if (isFunction(requiredOption) || isString(requiredOption) || isRegExp(requiredOption)) { 16 | return requiredOption; 17 | } 18 | 19 | throw new Error(`Configuration file for 'ignore' is not exporting a string nor a function`); 20 | } else if (isString(value)) { 21 | return value; 22 | } else { 23 | throw new Error(`Configuration 'ignore' is not a function, string, RegExp nor valid path to module`); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/options_resolvers/importPathFormatter.js: -------------------------------------------------------------------------------- 1 | import { isFunction, isModulePath, requireLocalFileOrNodeModule } from '../utils'; 2 | 3 | /** 4 | * Resolves importPathFormatter option 5 | * 6 | * @param {String|Function} value 7 | * @returns {Function} 8 | */ 9 | export default function importPathFormatter(value/* , currentConfig */) { 10 | if (isFunction(value)) { 11 | return value; 12 | } else if (isModulePath(value)) { 13 | const requiredOption = requireLocalFileOrNodeModule(value); 14 | 15 | if (!isFunction(requiredOption)) { 16 | throw new Error(`Configuration file for 'importPathFormatter' is not exporting a function`); 17 | } 18 | 19 | return requiredOption; 20 | } 21 | 22 | throw new Error(`Configuration 'importPathFormatter' is not a function nor a valid module path`); 23 | } 24 | -------------------------------------------------------------------------------- /src/options_resolvers/index.js: -------------------------------------------------------------------------------- 1 | export { default as append } from './append'; 2 | export { default as camelCase } from './camelCase'; 3 | export { default as createImportedName } from './createImportedName'; 4 | export { default as devMode } from './devMode'; 5 | export { default as generateScopedName } from './generateScopedName'; 6 | export { default as hashPrefix } from './hashPrefix'; 7 | export { default as ignore } from './ignore'; 8 | export { default as mode } from './mode'; 9 | export { default as prepend } from './prepend'; 10 | export { default as preprocessCss } from './preprocessCss'; 11 | export { default as processCss } from './processCss'; 12 | export { default as processorOpts } from './processorOpts'; 13 | export { default as rootDir } from './rootDir'; 14 | export { default as resolve } from './resolve'; 15 | export { default as use } from './use'; 16 | export { default as importPathFormatter } from './importPathFormatter'; 17 | -------------------------------------------------------------------------------- /src/options_resolvers/mode.js: -------------------------------------------------------------------------------- 1 | import { isString } from '../utils'; 2 | 3 | /** 4 | * Resolves mode option for css-modules-require-hook 5 | * 6 | * @param {*} value 7 | * @returns {String} 8 | */ 9 | export default function mode(value/* , currentConfig */) { 10 | if (!isString(value)) { 11 | throw new Error(`Configuration 'mode' is not a string`); 12 | } 13 | 14 | return value; 15 | } 16 | -------------------------------------------------------------------------------- /src/options_resolvers/prepend.js: -------------------------------------------------------------------------------- 1 | import { isFunction, isModulePath, requireLocalFileOrNodeModule } from '../utils'; 2 | 3 | /** 4 | * Resolves prepend option for css-modules-require-hook 5 | * 6 | * @param {*} value 7 | * @returns {Function} 8 | */ 9 | export default function prepend(value/* , currentConfig */) { 10 | if (Array.isArray(value)) { 11 | return value.map((option, index) => { 12 | if (isFunction(option)) { 13 | return option(); 14 | } else if (isModulePath(option)) { 15 | const requiredOption = requireLocalFileOrNodeModule(option); 16 | 17 | if (!isFunction(requiredOption)) { 18 | throw new Error(`Configuration 'prepend[${index}]' module is not exporting a function`); 19 | } 20 | 21 | return requiredOption(); 22 | } 23 | 24 | throw new Error(`Configuration 'prepend[${index}]' is not a function or a valid module path`); 25 | }); 26 | } 27 | 28 | throw new Error(`Configuration 'prepend' is not an array`); 29 | } 30 | -------------------------------------------------------------------------------- /src/options_resolvers/preprocessCss.js: -------------------------------------------------------------------------------- 1 | import { isModulePath, isFunction, requireLocalFileOrNodeModule } from '../utils'; 2 | 3 | /** 4 | * Resolves preprocessCss option for css-modules-require-hook 5 | * 6 | * @param {String|Function} value 7 | * 8 | * @returns {String|Function} 9 | */ 10 | export default function preprocessCss(value/* ,currentConfig */) { 11 | if (isModulePath(value)) { 12 | const requiredModule = requireLocalFileOrNodeModule(value); 13 | 14 | if (isFunction(requiredModule)) { 15 | return requiredModule; 16 | } 17 | 18 | throw new Error(`Configuration file for 'preprocessCss' is not exporting a function`); 19 | } else if (isFunction(value)) { 20 | return value; 21 | } else { 22 | throw new Error(`Configuration 'preprocessCss' is not a function nor a valid path to module`); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/options_resolvers/processCss.js: -------------------------------------------------------------------------------- 1 | import { isModulePath, isFunction, requireLocalFileOrNodeModule } from '../utils'; 2 | 3 | /** 4 | * Resolves processCss option for css-modules-require-hook 5 | * 6 | * @param {String|Function} value 7 | * 8 | * @returns {String|Function} 9 | */ 10 | export default function processCss(value/* ,currentConfig */) { 11 | if (isModulePath(value)) { 12 | const requiredModule = requireLocalFileOrNodeModule(value); 13 | 14 | if (isFunction(requiredModule)) { 15 | return requiredModule; 16 | } 17 | 18 | throw new Error(`Configuration file for 'processCss' is not exporting a function`); 19 | } else if (isFunction(value)) { 20 | return value; 21 | } else { 22 | throw new Error(`Configuration 'processCss' is not a function nor a valid path to module`); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/options_resolvers/processorOpts.js: -------------------------------------------------------------------------------- 1 | import { isModulePath, isPlainObject, requireLocalFileOrNodeModule } from '../utils'; 2 | 3 | /** 4 | * Resolves processorOpts option for css-modules-require-hook 5 | * 6 | * @param {String|Function} value 7 | * 8 | * @returns {String|Function} 9 | */ 10 | export default function processorOpts(value/* ,currentConfig */) { 11 | if (isModulePath(value)) { 12 | const requiredModule = requireLocalFileOrNodeModule(value); 13 | 14 | if (isPlainObject(requiredModule)) { 15 | return requiredModule; 16 | } 17 | 18 | throw new Error(`Configuration file for 'processorOpts' is not exporting a plain object`); 19 | } else if (isPlainObject(value)) { 20 | return value; 21 | } else { 22 | throw new Error(`Configuration 'processorOpts' is not a plain object nor a valid path to module`); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/options_resolvers/resolve.js: -------------------------------------------------------------------------------- 1 | import { isAbsolute } from 'path'; 2 | import { statSync } from 'fs'; 3 | import { isBoolean, isPlainObject, isString } from '../utils'; 4 | 5 | /** 6 | * Resolves resolve option for css-modules-require-hook 7 | * 8 | * @param {*} value 9 | * @returns {Object} 10 | */ 11 | export default function resolve(value/* , currentConfig */) { 12 | if (!isPlainObject(value)) { 13 | throw new Error(`Configuration 'resolve' is not an object`); 14 | } 15 | 16 | if ('alias' in value && !isPlainObject(value.alias)) { 17 | throw new Error(`Configuration 'resolve.alias' is not an object`); 18 | } 19 | 20 | if ('extensions' in value) { 21 | if (!Array.isArray(value.extensions)) { 22 | throw new Error(`Configuration 'resolve.extensions' is not an array`); 23 | } 24 | 25 | value.extensions.map((option, index) => { 26 | if (!isString(option)) { 27 | throw new Error(`Configuration 'resolve.extensions[${index}]' is not a string`); 28 | } 29 | }); 30 | } 31 | 32 | if ('modules' in value) { 33 | if (!Array.isArray(value.modules)) { 34 | throw new Error(`Configuration 'resolve.modules' is not an array`); 35 | } 36 | 37 | value.modules.map((option, index) => { 38 | if (!isAbsolute(option) || !statSync(option).isDirectory()) { 39 | throw new Error(`Configuration 'resolve.modules[${index}]' is not containing a valid absolute path`); 40 | } 41 | }); 42 | } 43 | 44 | if ('mainFile' in value && !isString(value.mainFile)) { 45 | throw new Error(`Configuration 'resolve.mainFile' is not a string`); 46 | } 47 | 48 | if ('preserveSymlinks' in value && !isBoolean(value.preserveSymlinks)) { 49 | throw new Error(`Configuration 'resolve.preserveSymlinks' is not a boolean`); 50 | } 51 | 52 | return value; 53 | } 54 | -------------------------------------------------------------------------------- /src/options_resolvers/rootDir.js: -------------------------------------------------------------------------------- 1 | import { isAbsolute } from 'path'; 2 | import { statSync } from 'fs'; 3 | import { isString } from '../utils'; 4 | 5 | /** 6 | * Resolves rootDir option for css-modules-require-hook 7 | * 8 | * @param {*} value 9 | * @returns {String} 10 | */ 11 | export default function rootDir(value/* , currentConfig */) { 12 | if (!isString(value)) { 13 | throw new Error(`Configuration 'rootDir' is not a string`); 14 | } 15 | 16 | if (!isAbsolute(value) || !statSync(value).isDirectory()) { 17 | throw new Error(`Configuration 'rootDir' is not containing a valid absolute path`); 18 | } 19 | 20 | return value; 21 | } 22 | -------------------------------------------------------------------------------- /src/options_resolvers/use.js: -------------------------------------------------------------------------------- 1 | import { isFunction, isModulePath, requireLocalFileOrNodeModule } from '../utils'; 2 | 3 | /** 4 | * Resolves use option for css-modules-require-hook 5 | * 6 | * @param {*} value 7 | * @returns {Function} 8 | */ 9 | export default function use(value/* , currentConfig */) { 10 | if (Array.isArray(value)) { 11 | return value.map((option, index) => { 12 | if (isFunction(option)) { 13 | return option(); 14 | } else if (isModulePath(option)) { 15 | const requiredOption = requireLocalFileOrNodeModule(option); 16 | 17 | if (!isFunction(requiredOption)) { 18 | throw new Error(`Configuration 'use[${index}]' module is not exporting a function`); 19 | } 20 | 21 | return requiredOption(); 22 | } 23 | 24 | throw new Error(`Configuration 'use[${index}]' is not a function or a valid module path`); 25 | }); 26 | } 27 | 28 | throw new Error(`Configuration 'use' is not an array`); 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/extractCssFile.js: -------------------------------------------------------------------------------- 1 | import writeCssFile from './writeCssFile'; 2 | import { basename, dirname, extname, join, relative, resolve } from 'path'; 3 | 4 | export const PATH_VARIABLES = ['[path]', '[name]']; 5 | 6 | /** 7 | * Extracts CSS to file 8 | * 9 | * @param {String} cwd 10 | * @param {Map} cssMap 11 | * @param {String|Object} extractCss 12 | * @returns {null} 13 | */ 14 | export default function extractCssFile(cwd, cssMap, extractCss) { 15 | // this is the case where a single extractCss is requested 16 | if (typeof(extractCss) === 'string') { 17 | // check if extractCss contains some from pattern variables, if yes throw! 18 | PATH_VARIABLES.forEach(VARIABLE => { 19 | if (extractCss.indexOf(VARIABLE) !== -1) { 20 | throw new Error('extractCss cannot contain variables'); 21 | } 22 | }); 23 | 24 | const css = Array.from(cssMap.values()).join(''); 25 | 26 | return writeCssFile(extractCss, css); 27 | } 28 | 29 | // This is the case where each css file is written in 30 | // its own file. 31 | const { 32 | dir = 'dist', 33 | filename = '[name].css', 34 | relativeRoot = '' 35 | } = extractCss; 36 | 37 | // check if filename contains at least [name] variable 38 | if (filename.indexOf('[name]') === -1) { 39 | throw new Error('[name] variable has to be used in extractCss.filename option'); 40 | } 41 | 42 | cssMap.forEach((css, filepath) => { 43 | // Make css file name relative to relativeRoot 44 | const relativePath = relative( 45 | resolve(cwd, relativeRoot), 46 | filepath 47 | ); 48 | 49 | const destination = join( 50 | resolve(cwd, dir), 51 | filename 52 | ) 53 | .replace(/\[name]/, basename(filepath, extname(filepath))) 54 | .replace(/\[path]/, dirname(relativePath)); 55 | 56 | writeCssFile(destination, css); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export { default as extractCssFile } from './extractCssFile'; 2 | export { default as isBoolean } from './isBoolean'; 3 | export { default as isFunction } from './isFunction'; 4 | export { default as isModulePath } from './isModulePath'; 5 | export { default as isPlainObject } from './isPlainObject'; 6 | export { default as isRegExp } from './isRegExp'; 7 | export { default as isString } from './isString'; 8 | export { default as requireLocalFileOrNodeModule } from './requireLocalFileOrNodeModule'; 9 | export { default as writeCssFile } from './writeCssFile'; 10 | -------------------------------------------------------------------------------- /src/utils/isBoolean.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Is provided value a boolean? 3 | * 4 | * @param {*} value 5 | * @returns {boolean} 6 | */ 7 | export default function isBoolean(value) { 8 | return value === true || value === false; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/isFunction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Is provided object a function? 3 | * 4 | * @param {*} object 5 | * @returns {boolean} 6 | */ 7 | export default function isFunction(object) { 8 | return typeof object === 'function'; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/isModulePath.js: -------------------------------------------------------------------------------- 1 | import { resolve as resolvePath } from 'path'; 2 | import isString from './isString'; 3 | 4 | /** 5 | * Is given path a valid file or node module path? 6 | * 7 | * @param {String} path 8 | * @returns {boolean} 9 | */ 10 | export default function isModulePath(path) { 11 | if (!isString(path) || path === '') { 12 | return false; 13 | } 14 | 15 | try { 16 | require.resolve(resolvePath(process.cwd(), path)); 17 | return true; 18 | } catch (e) { 19 | try { 20 | require.resolve(path); 21 | return true; 22 | } catch (_e) { 23 | return false; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/isPlainObject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Is provided value an plain object? 3 | * 4 | * @param {*} object 5 | * @returns {boolean} 6 | */ 7 | export default function isPlainObject(object) { 8 | if (object === null || object === undefined) { 9 | return false; 10 | } 11 | 12 | return object.toString() === '[object Object]'; 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/isRegExp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Is Provided object an RegExp? 3 | * 4 | * @param {*} object 5 | * @returns {boolean} 6 | */ 7 | export default function isRegExp(object) { 8 | return object instanceof RegExp; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/isString.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Is provided object a string? 3 | * 4 | * @param {*} object 5 | * @returns {boolean} 6 | */ 7 | export default function isString(object) { 8 | return typeof object === 'string'; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/requireLocalFileOrNodeModule.js: -------------------------------------------------------------------------------- 1 | import { resolve as resolvePath } from 'path'; 2 | 3 | /** 4 | * Require local file or node modules 5 | * 6 | * @param {String} path 7 | * @returns {*} 8 | */ 9 | export default function requireLocalFileOrNodeModule(path) { 10 | const localFile = resolvePath(process.cwd(), path); 11 | 12 | try { 13 | // first try to require local file 14 | return require(localFile); 15 | } catch (e) { 16 | if (e.code === 'MODULE_NOT_FOUND') { 17 | // try to require node_module 18 | return require(path); 19 | } 20 | 21 | throw e; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/writeCssFile.js: -------------------------------------------------------------------------------- 1 | import mkdirp from 'mkdirp'; 2 | import { dirname } from 'path'; 3 | import { appendFileSync, writeFileSync } from 'fs'; 4 | 5 | /** 6 | * Writes css file to given path (and creates directories) 7 | * 8 | * @param {String} path 9 | * @param {String} content 10 | * @param {Boolean} append 11 | */ 12 | export default function writeCssFile(path, content, append = false) { 13 | mkdirp.sync(dirname(path)); 14 | 15 | if (append) { 16 | appendFileSync(path, content); 17 | } else { 18 | writeFileSync(path, content); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/css/child.css: -------------------------------------------------------------------------------- 1 | .line { 2 | display: inline; 3 | } 4 | -------------------------------------------------------------------------------- /test/extensions.scss: -------------------------------------------------------------------------------- 1 | .sassy { 2 | display: block; 3 | color: pink; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/append.module.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/fixtures/exctractcss.include.js: -------------------------------------------------------------------------------- 1 | require('../styles.css'); 2 | -------------------------------------------------------------------------------- /test/fixtures/exctractcss.main.expected.babel7.js: -------------------------------------------------------------------------------- 1 | require('./exctractcss.include.js'); 2 | -------------------------------------------------------------------------------- /test/fixtures/exctractcss.main.expected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./exctractcss.include.js'); 4 | -------------------------------------------------------------------------------- /test/fixtures/exctractcss.main.js: -------------------------------------------------------------------------------- 1 | require('./exctractcss.include.js'); 2 | require('../parent.css'); 3 | -------------------------------------------------------------------------------- /test/fixtures/extensions.expected.babel7.js: -------------------------------------------------------------------------------- 1 | var css = { 2 | "className": "styles__className___385m0 parent__block___33Sxl child__line___3fweh" 3 | }; 4 | var scss = { 5 | "sassy": "extensions__sassy___12Yag" 6 | }; 7 | 8 | var foo = require('something-that-has-css-somewhere-in-the-name'); 9 | 10 | var bar = require('something-that-has-scss-somewhere-in-the-name'); 11 | -------------------------------------------------------------------------------- /test/fixtures/extensions.expected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var css = { 4 | 'className': 'styles__className___385m0 parent__block___33Sxl child__line___3fweh' 5 | }; 6 | var scss = { 7 | 'sassy': 'extensions__sassy___12Yag' 8 | }; 9 | var foo = require('something-that-has-css-somewhere-in-the-name'); 10 | var bar = require('something-that-has-scss-somewhere-in-the-name'); 11 | -------------------------------------------------------------------------------- /test/fixtures/extensions.js: -------------------------------------------------------------------------------- 1 | var css = require('../styles.css'); 2 | var scss = require('../extensions.scss'); 3 | var foo = require('something-that-has-css-somewhere-in-the-name'); 4 | var bar = require('something-that-has-scss-somewhere-in-the-name'); 5 | -------------------------------------------------------------------------------- /test/fixtures/extractcss.combined.expected.css: -------------------------------------------------------------------------------- 1 | .child__line___3fweh { 2 | display: inline; 3 | } 4 | .parent__block___33Sxl { 5 | display: block; 6 | } 7 | .styles__className___385m0 { 8 | color: red; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/extractcss.css.child.expected.css: -------------------------------------------------------------------------------- 1 | .child__line___3fweh { 2 | display: inline; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/extractcss.parent-combined.expected.css: -------------------------------------------------------------------------------- 1 | .child__line___3fweh { 2 | display: inline; 3 | } 4 | .parent__block___33Sxl { 5 | display: block; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/extractcss.parent.expected.css: -------------------------------------------------------------------------------- 1 | .parent__block___33Sxl { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/extractcss.styles.expected.css: -------------------------------------------------------------------------------- 1 | .styles__className___385m0 { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/generateScopedName.function.module.js: -------------------------------------------------------------------------------- 1 | module.exports = function() {}; 2 | -------------------------------------------------------------------------------- /test/fixtures/generateScopedName.module.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/fixtures/global.import.js: -------------------------------------------------------------------------------- 1 | import '../styles.css'; 2 | -------------------------------------------------------------------------------- /test/fixtures/global.require.js: -------------------------------------------------------------------------------- 1 | require('../styles.css'); 2 | -------------------------------------------------------------------------------- /test/fixtures/import.expected.babel7.js: -------------------------------------------------------------------------------- 1 | var styles = { 2 | "className": "styles__className___385m0 parent__block___33Sxl child__line___3fweh" 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/import.expected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var styles = { 4 | 'className': 'styles__className___385m0 parent__block___33Sxl child__line___3fweh' 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/import.js: -------------------------------------------------------------------------------- 1 | import styles from '../styles.css'; 2 | -------------------------------------------------------------------------------- /test/fixtures/keepImport.expected.babel7.js: -------------------------------------------------------------------------------- 1 | require("../styles.css"); 2 | 3 | var css = { 4 | "className": "styles__className___385m0 parent__block___33Sxl child__line___3fweh" 5 | }; 6 | 7 | require("../extensions.scss"); 8 | 9 | var scss = { 10 | "sassy": "extensions__sassy___12Yag" 11 | }; 12 | 13 | require('../extensions.scss'); 14 | 15 | var foo = require('something-that-has-css-somewhere-in-the-name'); 16 | 17 | var bar = require('something-that-has-scss-somewhere-in-the-name'); 18 | -------------------------------------------------------------------------------- /test/fixtures/keepImport.expected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../styles.css'); 4 | 5 | var css = { 6 | 'className': 'styles__className___385m0 parent__block___33Sxl child__line___3fweh' 7 | }; 8 | 9 | require('../extensions.scss'); 10 | 11 | var scss = { 12 | 'sassy': 'extensions__sassy___12Yag' 13 | }; 14 | require('../extensions.scss'); 15 | var foo = require('something-that-has-css-somewhere-in-the-name'); 16 | var bar = require('something-that-has-scss-somewhere-in-the-name'); 17 | -------------------------------------------------------------------------------- /test/fixtures/keepImport.js: -------------------------------------------------------------------------------- 1 | import css from '../styles.css'; 2 | var scss = require('../extensions.scss'); 3 | require('../extensions.scss'); 4 | var foo = require('something-that-has-css-somewhere-in-the-name'); 5 | var bar = require('something-that-has-scss-somewhere-in-the-name'); 6 | -------------------------------------------------------------------------------- /test/fixtures/prepend.module.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/fixtures/preprocessCss.module.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/fixtures/processCss.module.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/fixtures/require.expected.babel7.js: -------------------------------------------------------------------------------- 1 | const styles = { 2 | "className": "styles__className___385m0 parent__block___33Sxl child__line___3fweh" 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/require.expected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const styles = { 4 | 'className': 'styles__className___385m0 parent__block___33Sxl child__line___3fweh' 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/require.ignored.expected.babel7.js: -------------------------------------------------------------------------------- 1 | const styles = {}; 2 | -------------------------------------------------------------------------------- /test/fixtures/require.ignored.expected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const styles = {}; 4 | -------------------------------------------------------------------------------- /test/fixtures/require.js: -------------------------------------------------------------------------------- 1 | const styles = require('../styles.css'); -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { resolve, join, relative, basename, dirname } from 'path'; 3 | import { readFileSync } from 'fs'; 4 | import gulpUtil from 'gulp-util'; 5 | import rimraf from 'rimraf'; 6 | 7 | describe('babel-plugin-css-modules-transform', () => { 8 | function transform(path, configuration = {}) { 9 | // remove css modules transform plugin (simulates clean processes) 10 | delete require.cache[resolve(__dirname, '../src/index.js')]; 11 | const babel = require('babel-core'); 12 | if (configuration && !('devMode' in configuration)) configuration.devMode = true; 13 | 14 | return babel.transformFileSync(resolve(__dirname, path), { 15 | babelrc: false, 16 | presets: [['env', { targets: { node: '6.12' } }]], 17 | plugins: [ 18 | 'transform-object-rest-spread', 19 | ['@babel/../../src/index.js', configuration] 20 | ] 21 | }); 22 | } 23 | 24 | function createBabelStream(configuration = {}) { 25 | // remove css modules transform plugin (simulates clean processes) 26 | delete require.cache[resolve(__dirname, '../src/index.js')]; 27 | const gulpBabel = require('gulp-babel'); 28 | // set css-modules-require-hook in dev to clear cache 29 | if (configuration && !('devMode' in configuration)) configuration.devMode = true; 30 | 31 | return gulpBabel({ 32 | presets: [['env', { targets: { node: '6.12' } }]], 33 | plugins: [ 34 | 'transform-object-rest-spread', 35 | ['@babel/../../src/index.js', configuration] 36 | ] 37 | }); 38 | } 39 | 40 | function readExpected(path) { 41 | let file = path; 42 | 43 | if (process.env.BABEL_7 && /\.js$/.test(file)) { 44 | // we load fixture for babel 7, they changed few things so we need to use different fixture 45 | file = join(dirname(file), `./${basename(file, '.js')}.babel7.js`); 46 | } 47 | // We trim the contents of the file so that we don't have 48 | // to deal with newline issues, since some text editors 49 | // automatically inserts them. It's easier to do this than to 50 | // configure the editors to avoid inserting newlines for these 51 | // particular files. 52 | return readFileSync(resolve(__dirname, file), 'utf8').trim(); 53 | } 54 | 55 | beforeEach((done) => { 56 | rimraf(`${__dirname}/output/`, done); 57 | }); 58 | 59 | after((done) => { 60 | rimraf(`${__dirname}/output/`, done); 61 | }); 62 | 63 | it('should ignore files', () => { 64 | // run this as first case because css-modules-require-hook will be cached with given options 65 | expect(transform('fixtures/require.js', { 66 | ignore: /\.css$/ 67 | }).code).to.be.equal(readExpected('fixtures/require.ignored.expected.js')); 68 | }); 69 | 70 | it('should not throw if we are requiring css module to module scope', () => { 71 | expect(() => transform('fixtures/global.require.js')).to.not.throw(); 72 | 73 | expect(() => transform('fixtures/global.import.js')).to.not.throw(); 74 | }); 75 | 76 | it('should replace require call with hash of class name => css class name', () => { 77 | expect(transform('fixtures/require.js').code).to.be.equal(readExpected('fixtures/require.expected.js')); 78 | expect(transform('fixtures/import.js').code).to.be.equal(readExpected('fixtures/import.expected.js')); 79 | }); 80 | 81 | it('should replace require call with hash of class name => css class name via gulp', (cb) => { 82 | const stream = createBabelStream({}); 83 | 84 | stream.on('data', (file) => { 85 | expect(file.contents.toString()).to.be.equal(readExpected('fixtures/import.expected.js')); 86 | }); 87 | 88 | stream.on('end', cb); 89 | 90 | stream.write(new gulpUtil.File({ 91 | cwd: __dirname, 92 | base: join(__dirname, 'fixtures'), 93 | path: join(__dirname, 'fixtures/import.js'), 94 | contents: readFileSync(join(__dirname, 'fixtures/import.js')) 95 | })); 96 | 97 | stream.end(); 98 | }); 99 | 100 | it('should accept file extensions as an array', () => { 101 | expect( 102 | transform( 103 | 'fixtures/extensions.js', 104 | { 105 | extensions: ['.scss', '.css'] 106 | } 107 | ).code 108 | ).to.be.equal(readExpected('fixtures/extensions.expected.js')); 109 | }); 110 | 111 | it('should write a multiple css files using import', () => { 112 | expect(transform(`${__dirname}/fixtures/import.js`, { 113 | extractCss: { 114 | dir: `${__dirname}/output/`, 115 | filename: '[path]/[name].css', 116 | relativeRoot: __dirname 117 | }, 118 | extensions: ['.scss', '.css'] 119 | }).code).to.be.equal(readExpected('fixtures/import.expected.js')); 120 | 121 | expect(readExpected(`${__dirname}/output/parent.css`)) 122 | .to.be.equal(readExpected('fixtures/extractcss.parent.expected.css')); 123 | expect(readExpected(`${__dirname}/output/styles.css`)) 124 | .to.be.equal(readExpected('fixtures/extractcss.styles.expected.css')); 125 | }); 126 | 127 | it('should write a multiple css files using import preserving directory structure', () => { 128 | expect(transform('fixtures/import.js', { 129 | extractCss: { 130 | dir: `${__dirname}/output/`, 131 | filename: '[path]/[name].css', 132 | relativeRoot: `${__dirname}` 133 | } 134 | }).code).to.be.equal(readExpected('fixtures/import.expected.js')); 135 | 136 | expect(readExpected(`${__dirname}/output/parent.css`)) 137 | .to.be.equal(readExpected('fixtures/extractcss.parent.expected.css')); 138 | expect(readExpected(`${__dirname}/output/styles.css`)) 139 | .to.be.equal(readExpected('fixtures/extractcss.styles.expected.css')); 140 | expect(readExpected(`${__dirname}/output/css/child.css`)) 141 | .to.be.equal(readExpected('fixtures/extractcss.css.child.expected.css')); 142 | }); 143 | 144 | it('should write a multiple css files using require', () => { 145 | expect(transform('fixtures/require.js', { 146 | extractCss: { 147 | dir: `${__dirname}/output/`, 148 | filename: '[name].css', 149 | relativeRoot: `${__dirname}` 150 | } 151 | }).code).to.be.equal(readExpected('fixtures/require.expected.js')); 152 | 153 | expect(readExpected(`${__dirname}/output/parent.css`)) 154 | .to.be.equal(readExpected('fixtures/extractcss.parent.expected.css')); 155 | expect(readExpected(`${__dirname}/output/styles.css`)) 156 | .to.be.equal(readExpected('fixtures/extractcss.styles.expected.css')); 157 | }); 158 | 159 | it('should write a single css file using import', () => { 160 | expect(transform('fixtures/import.js', { 161 | extractCss: `${__dirname}/output/combined.css` 162 | }).code).to.be.equal(readExpected('fixtures/import.expected.js')); 163 | 164 | expect(readExpected(`${__dirname}/output/combined.css`)) 165 | .to.be.equal(readExpected('fixtures/extractcss.combined.expected.css')); 166 | }); 167 | 168 | it('should write a single css file using require', () => { 169 | expect(transform('fixtures/require.js', { 170 | extractCss: `${__dirname}/output/combined.css` 171 | }).code).to.be.equal(readExpected('fixtures/require.expected.js')); 172 | 173 | expect(readExpected(`${__dirname}/output/combined.css`)) 174 | .to.be.equal(readExpected('fixtures/extractcss.combined.expected.css')); 175 | }); 176 | 177 | it('should extract styles with a single input file via gulp', (cb) => { 178 | const stream = createBabelStream({ 179 | extractCss: `${__dirname}/output/combined.css` 180 | }); 181 | 182 | stream.on('data', (file) => { 183 | expect(file.contents.toString()).to.be.equal(readExpected('fixtures/exctractcss.main.expected.js')); 184 | }); 185 | 186 | stream.on('end', (err) => { 187 | if (err) return cb(err); 188 | expect(readExpected(`${__dirname}/output/combined.css`)) 189 | .to.be.equal(readExpected('fixtures/extractcss.parent-combined.expected.css')); 190 | 191 | return cb(); 192 | }); 193 | 194 | stream.write(new gulpUtil.File({ 195 | cwd: __dirname, 196 | base: join(__dirname, 'fixtures'), 197 | path: join(__dirname, 'fixtures/exctractcss.main.js'), 198 | contents: readFileSync(join(__dirname, 'fixtures/exctractcss.main.js')) 199 | })); 200 | 201 | stream.end(); 202 | }); 203 | 204 | it('should extract multiple files via gulp', (cb) => { 205 | const stream = createBabelStream({ 206 | extractCss: { 207 | dir: `${__dirname}/output/`, 208 | filename: '[name].css', 209 | relativeRoot: `${__dirname}` 210 | } 211 | }); 212 | 213 | // it seems that a data function is required 214 | stream.on('data', () => {}); 215 | 216 | stream.on('end', (err) => { 217 | if (err) return cb(err); 218 | 219 | expect(readExpected(`${__dirname}/output/parent.css`)) 220 | .to.be.equal(readExpected('fixtures/extractcss.parent.expected.css')); 221 | expect(readExpected(`${__dirname}/output/styles.css`)) 222 | .to.be.equal(readExpected('fixtures/extractcss.styles.expected.css')); 223 | 224 | return cb(); 225 | }); 226 | 227 | stream.write(new gulpUtil.File({ 228 | cwd: __dirname, 229 | base: join(__dirname, 'fixtures'), 230 | path: join(__dirname, 'fixtures/exctractcss.main.js'), 231 | contents: readFileSync(join(__dirname, 'fixtures/exctractcss.main.js')) 232 | })); 233 | 234 | stream.write(new gulpUtil.File({ 235 | cwd: __dirname, 236 | base: join(__dirname, 'fixtures'), 237 | path: join(__dirname, 'fixtures/exctractcss.include.js'), 238 | contents: readFileSync(join(__dirname, 'fixtures/exctractcss.include.js')) 239 | })); 240 | 241 | stream.end(); 242 | }); 243 | 244 | it('should extract combined files via gulp', (cb) => { 245 | const stream = createBabelStream({ 246 | extractCss: `${__dirname}/output/combined.css` 247 | }); 248 | 249 | // it seems that a data function is required 250 | stream.on('data', () => {}); 251 | 252 | stream.on('end', (err) => { 253 | if (err) return cb(err); 254 | 255 | expect(readExpected(`${__dirname}/output/combined.css`)) 256 | .to.be.equal(readExpected('fixtures/extractcss.combined.expected.css')); 257 | return cb(); 258 | }); 259 | 260 | stream.write(new gulpUtil.File({ 261 | cwd: __dirname, 262 | base: join(__dirname, 'fixtures'), 263 | path: join(__dirname, 'fixtures/exctractcss.main.js'), 264 | contents: readFileSync(join(__dirname, 'fixtures/exctractcss.main.js')) 265 | })); 266 | 267 | stream.write(new gulpUtil.File({ 268 | cwd: __dirname, 269 | base: join(__dirname, 'fixtures'), 270 | path: join(__dirname, 'fixtures/exctractcss.include.js'), 271 | contents: readFileSync(join(__dirname, 'fixtures/exctractcss.include.js')) 272 | })); 273 | 274 | stream.end(); 275 | }); 276 | 277 | it('should call custom preprocess', () => { 278 | const called = []; 279 | expect(transform('fixtures/require.js', { 280 | extractCss: `${__dirname}/output/combined.css`, 281 | processCss(css, filename) { 282 | called.push(relative(__dirname, filename)); 283 | return css; 284 | } 285 | }).code).to.be.equal(readExpected('fixtures/require.expected.js')); 286 | expect(called).to.be.deep.equal([ 287 | 'css/child.css', 288 | 'parent.css', 289 | 'styles.css' 290 | ]); 291 | }); 292 | 293 | describe('keepImport option', () => { 294 | it('keeps requires/imports', () => { 295 | expect(transform('fixtures/keepImport.js', { 296 | keepImport: true, 297 | extensions: ['.scss', '.css'] 298 | }).code).to.be.equal(readExpected('fixtures/keepImport.expected.js')); 299 | }); 300 | }); 301 | 302 | describe('calling without options', () => { 303 | it('keeps requires/imports', () => { 304 | delete require.cache[resolve(__dirname, '../src/index.js')]; 305 | const babel = require('babel-core'); 306 | const result = babel.transformFileSync(resolve(__dirname, 'fixtures/import.js'), { 307 | babelrc: false, 308 | presets: [['env', { targets: { node: '6.12'} }]], 309 | plugins: [ 310 | 'transform-object-rest-spread', 311 | '@babel/../../src/index.js' 312 | ] 313 | }); 314 | 315 | expect(result.code).to.be.equal( 316 | readExpected('fixtures/import.expected.js') 317 | ); 318 | }); 319 | }); 320 | }); 321 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-register 2 | test/**/*.spec.js 3 | -------------------------------------------------------------------------------- /test/options_resolvers/append.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import append from '../../src/options_resolvers/append'; 4 | 5 | describe('options_resolvers/append', () => { 6 | it('should throw if append is not an array', () => { 7 | expect( 8 | () => append({}) 9 | ).to.throw(); 10 | }); 11 | 12 | it('should throw if append does not contain functions', () => { 13 | expect( 14 | () => append(['test/fixtures/append.module.js']) 15 | ).to.throw(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/options_resolvers/camelCase.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import camelCase from '../../src/options_resolvers/camelCase'; 4 | 5 | describe('options_resolvers/camelCase', () => { 6 | it('should throw if camelCase value is not a boolean or is not in enum', () => { 7 | expect( 8 | () => camelCase(null) 9 | ).to.throw(); 10 | 11 | expect( 12 | () => camelCase('unknown') 13 | ).to.throw(); 14 | 15 | expect(camelCase(true)).to.be.equal(true); 16 | expect(camelCase('dashes')).to.be.equal('dashes'); 17 | expect(camelCase('dashesOnly')).to.be.equal('dashesOnly'); 18 | expect(camelCase('only')).to.be.equal('only'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/options_resolvers/devMode.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import devMode from '../../src/options_resolvers/devMode'; 4 | 5 | describe('options_resolvers/devMode', () => { 6 | it('should throw if devMode value is not a boolean', () => { 7 | expect( 8 | () => devMode(null) 9 | ).to.throw(); 10 | 11 | expect(devMode(true)).to.be.equal(true); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/options_resolvers/extensions.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import extensions from '../../src/options_resolvers/extensions'; 4 | 5 | describe('options_resolvers/extensions', () => { 6 | it('should throw if extensions is not an array', () => { 7 | expect( 8 | () => extensions({}) 9 | ).to.throw(); 10 | }); 11 | 12 | it('should throw if append does not strings', () => { 13 | expect( 14 | () => extensions([true]) 15 | ).to.throw(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/options_resolvers/generateScopedName.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import generateScopedName from '../../src/options_resolvers/generateScopedName'; 4 | 5 | describe('options_resolvers/generateScopedName', () => { 6 | it('should throw if generateScopeName is not exporting a function', () => { 7 | expect( 8 | () => generateScopedName('test/fixtures/generateScopedName.module.js') 9 | ).to.throw(); 10 | }); 11 | 12 | it('should not throw if generateScopeName is exporting a function', () => { 13 | expect( 14 | () => generateScopedName('test/fixtures/generateScopedName.function.module.js') 15 | ).to.not.throw(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/options_resolvers/hashPrefix.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import hashPrefix from '../../src/options_resolvers/hashPrefix'; 4 | 5 | describe('options_resolvers/hashPrefix', () => { 6 | it('should throw if hashPrefix value is not a string', () => { 7 | expect( 8 | () => hashPrefix(null) 9 | ).to.throw(); 10 | 11 | expect(hashPrefix('hashPrefix')).to.be.equal('hashPrefix'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/options_resolvers/ignore.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import ignore from '../../src/options_resolvers/ignore'; 4 | 5 | describe('options_resolvers/ignore', () => { 6 | it('should throw if ignore value is not a function, string or RegExp', () => { 7 | expect( 8 | () => ignore(null) 9 | ).to.throw(); 10 | }); 11 | 12 | it('should return string', () => { 13 | expect(ignore('string')).to.be.equal('string'); 14 | }); 15 | 16 | it('should return function', () => { 17 | const func = () => {}; 18 | 19 | expect(ignore(func)).to.be.equal(func); 20 | }); 21 | 22 | it('should return function on module require', () => { 23 | expect(ignore('./test/fixtures/generateScopedName.function.module.js')).to.be.a('function'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/options_resolvers/mode.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import mode from '../../src/options_resolvers/mode'; 4 | 5 | describe('options_resolvers/mode', () => { 6 | it('should throw if mode value is not a string', () => { 7 | expect( 8 | () => mode(null) 9 | ).to.throw(); 10 | 11 | expect(mode('mode')).to.be.equal('mode'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/options_resolvers/prepend.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import prepend from '../../src/options_resolvers/prepend'; 4 | 5 | describe('options_resolvers/prepend', () => { 6 | it('should throw if prepend is not an array', () => { 7 | expect( 8 | () => prepend({}) 9 | ).to.throw(); 10 | }); 11 | 12 | it('should throw if prepend does not contain functions', () => { 13 | expect( 14 | () => prepend(['test/fixtures/append.module.js']) 15 | ).to.throw(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/options_resolvers/preprocessCss.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import preprocessCss from '../../src/options_resolvers/preprocessCss'; 4 | 5 | describe('options_resolvers/preprocessCss', () => { 6 | it('should throw if processCss is not a function', () => { 7 | expect( 8 | () => preprocessCss('test/fixtures/preprocessCss.module.js') 9 | ).to.throw(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/options_resolvers/processCss.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import processCss from '../../src/options_resolvers/processCss'; 4 | 5 | describe('options_resolvers/processCss', () => { 6 | it('should throw if processCss is not a function', () => { 7 | expect( 8 | () => processCss('test/fixtures/processCss.module.js') 9 | ).to.throw(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/options_resolvers/processorOpts.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import processorOpts from '../../src/options_resolvers/processorOpts'; 4 | 5 | describe('options_resolvers/processorOpts', () => { 6 | it('should throw if processorOpts is not an object or valid module path exporting object', () => { 7 | expect( 8 | () => processorOpts('test/fixtures/generateScopedName.function.module.js') 9 | ).to.throw(); 10 | 11 | expect( 12 | () => processorOpts(null) 13 | ).to.throw(); 14 | }); 15 | 16 | it('should return object', () => { 17 | expect(processorOpts({})).to.be.deep.equal({}); 18 | expect(processorOpts('test/fixtures/processCss.module.js')).to.be.deep.equal({}); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/options_resolvers/resolve.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import resolve from '../../src/options_resolvers/resolve'; 4 | 5 | describe('options_resolvers/resolve', () => { 6 | it('should throw if resolve is not an object', () => { 7 | expect( 8 | () => resolve([]) 9 | ).to.throw(); 10 | }); 11 | 12 | it('should throw if resolve.alias is not an object', () => { 13 | expect( 14 | () => resolve({ alias: [] }) 15 | ).to.throw(); 16 | }); 17 | 18 | it('should throw if resolve.extensions is not an array', () => { 19 | expect( 20 | () => resolve({ extensions: {} }) 21 | ).to.throw(); 22 | }); 23 | 24 | it('should throw if resolve.modules is not an array', () => { 25 | expect( 26 | () => resolve({ modules: {} }) 27 | ).to.throw(); 28 | }); 29 | 30 | it('should throw if resolve.modules.* is not an absolute directory or does not exist', () => { 31 | expect( 32 | () => resolve({ modules: ['/test/this/not/exists'] }) 33 | ).to.throw(); 34 | 35 | expect( 36 | () => resolve('./') 37 | ).to.throw(); 38 | }); 39 | 40 | it('should throw if resolve.mainFile is not a string', () => { 41 | expect( 42 | () => resolve({ mainFile: {} }) 43 | ).to.throw(); 44 | }); 45 | 46 | it('should throw if resolve.preserveSymlinks is not a boolean', () => { 47 | expect( 48 | () => resolve({ preserveSymlinks: 1 }) 49 | ).to.throw(); 50 | }); 51 | 52 | it('works if resolve.alias is an object', () => { 53 | expect(() => resolve({ alias: {} })).to.not.throw(); 54 | }); 55 | 56 | it('works if resolve.extensions is an array of strings', () => { 57 | expect(() => resolve({ extensions: ['a', 'b'] })).to.not.throw(); 58 | }); 59 | 60 | it('works if resolve.modules is an array of valid file paths', () => { 61 | expect(() => resolve({ modules: [__dirname] })).to.not.throw(); 62 | }); 63 | 64 | it('works if resolve.mainFile is a string', () => { 65 | expect(() => resolve({ mainFile: 'aa' })).to.not.throw(); 66 | }); 67 | 68 | it('works if resolve.preserveSymlinks is a boolean', () => { 69 | expect(() => resolve({ preserveSymlinks: true })).to.not.throw(); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/options_resolvers/rootDir.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import rootDir from '../../src/options_resolvers/rootDir'; 4 | 5 | describe('options_resolvers/rootDir', () => { 6 | it('should throw if rootDir is not an absolute directory or does not exist', () => { 7 | expect( 8 | () => rootDir('') 9 | ).to.throw(); 10 | 11 | expect(() => rootDir('/test/this/not/exists')).to.throw(); 12 | expect(() => rootDir('./')).to.throw(); 13 | }); 14 | 15 | it('should throw if rootDir is a file path', () => { 16 | expect(() => rootDir(__filename)).to.throw(); 17 | }); 18 | 19 | it('should return rootDir if is valid', () => { 20 | expect(rootDir(__dirname)).to.be.equal(__dirname); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/options_resolvers/use.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import use from '../../src/options_resolvers/use'; 4 | 5 | describe('options_resolvers/use', () => { 6 | it('should throw if use is not an array', () => { 7 | expect( 8 | () => use('test/fixtures/processCss.module.js') 9 | ).to.throw(); 10 | }); 11 | 12 | it('should throw if array is not containing functions or module paths', () => { 13 | expect( 14 | () => use([null]) 15 | ).to.throw(); 16 | 17 | expect( 18 | () => use(['unknown-module']) 19 | ).to.throw(); 20 | }); 21 | 22 | it('should return result of called function', () => { 23 | let called = false; 24 | 25 | const caller = () => { 26 | called = true; 27 | 28 | return 'result'; 29 | }; 30 | 31 | expect(use([caller])).to.be.deep.equal(['result']); 32 | expect(called).to.be.equal(true); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/parent.css: -------------------------------------------------------------------------------- 1 | .block { 2 | composes: line from './css/child.css'; 3 | display: block; 4 | } 5 | -------------------------------------------------------------------------------- /test/styles.css: -------------------------------------------------------------------------------- 1 | .className { 2 | composes: block from './parent.css'; 3 | color: red; 4 | } 5 | -------------------------------------------------------------------------------- /test/utils/isBoolean.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import isBoolean from '../../src/utils/isBoolean'; 4 | 5 | describe('utils/isBoolean', () => { 6 | it('should return true for valid values', () => { 7 | expect(isBoolean(null)).to.be.equal(false); 8 | expect(isBoolean('')).to.be.equal(false); 9 | expect(isBoolean(1)).to.be.equal(false); 10 | expect(isBoolean(false)).to.be.equal(true); 11 | expect(isBoolean(true)).to.be.equal(true); 12 | expect(isBoolean(() => {})).to.be.equal(false); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/utils/isFunction.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import isFunction from '../../src/utils/isFunction'; 4 | 5 | describe('utils/isFunction', () => { 6 | it('should return true for function', () => { 7 | expect(isFunction(null)).to.be.equal(false); 8 | expect(isFunction('')).to.be.equal(false); 9 | expect(isFunction(1)).to.be.equal(false); 10 | expect(isFunction(false)).to.be.equal(false); 11 | expect(isFunction(() => {})).to.be.equal(true); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/utils/isModulePath.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import isModulePath from '../../src/utils/isModulePath'; 4 | 5 | describe('utils/isModulePath', () => { 6 | it('should return true for valid paths', () => { 7 | expect(isModulePath('mkdirp')).to.be.equal(true); 8 | expect(isModulePath('not-global-existing')).to.be.equal(false); 9 | expect(isModulePath('./test/utils/isModulePath.spec.js')).to.be.equal(true); 10 | expect(isModulePath('./test/utils/nonexisting')).to.be.equal(false); 11 | expect(isModulePath('')).to.be.equal(false); 12 | expect(isModulePath(1)).to.be.equal(false); 13 | expect(isModulePath(false)).to.be.equal(false); 14 | expect(isModulePath(() => {})).to.be.equal(false); 15 | expect(isModulePath({})).to.be.equal(false); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/utils/isPlainObject.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import isPlainObject from '../../src/utils/isPlainObject'; 4 | 5 | describe('utils/isPlainObject', () => { 6 | it('should return true for an object', () => { 7 | expect(isPlainObject(null)).to.be.equal(false); 8 | expect(isPlainObject('')).to.be.equal(false); 9 | expect(isPlainObject(1)).to.be.equal(false); 10 | expect(isPlainObject(false)).to.be.equal(false); 11 | expect(isPlainObject(() => {})).to.be.equal(false); 12 | expect(isPlainObject({})).to.be.equal(true); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/utils/isRegExp.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import isRegExp from '../../src/utils/isRegExp'; 4 | 5 | describe('utils/isRegExp', () => { 6 | it('should return true for a RegExp', () => { 7 | expect(isRegExp(null)).to.be.equal(false); 8 | expect(isRegExp('')).to.be.equal(false); 9 | expect(isRegExp(1)).to.be.equal(false); 10 | expect(isRegExp(false)).to.be.equal(false); 11 | expect(isRegExp(() => {})).to.be.equal(false); 12 | expect(isRegExp({})).to.be.equal(false); 13 | expect(isRegExp(/a/)).to.be.equal(true); 14 | expect(isRegExp(new RegExp('a'))).to.be.equal(true); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/utils/isString.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import isString from '../../src/utils/isString'; 4 | 5 | describe('utils/isString', () => { 6 | it('should return true for a string', () => { 7 | expect(isString(null)).to.be.equal(false); 8 | expect(isString('')).to.be.equal(true); 9 | expect(isString('a')).to.be.equal(true); 10 | expect(isString(1)).to.be.equal(false); 11 | expect(isString(false)).to.be.equal(false); 12 | expect(isString(() => {})).to.be.equal(false); 13 | expect(isString({})).to.be.equal(false); 14 | expect(isString(/a/)).to.be.equal(false); 15 | expect(isString(new RegExp('a'))).to.be.equal(false); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/utils/requireLocalFileOrNodeModule.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import requireLocalFileOrNodeModule from '../../src/utils/requireLocalFileOrNodeModule'; 4 | 5 | describe('utils/requireLocalFileOrNodeModule', () => { 6 | it('should return exported value from file', () => { 7 | expect(requireLocalFileOrNodeModule('mkdirp')).to.be.a('function'); 8 | expect(requireLocalFileOrNodeModule('./src/utils/requireLocalFileOrNodeModule')).to.be.a('object'); 9 | }); 10 | }); 11 | --------------------------------------------------------------------------------