diff --git a/.babelrc b/.babelrc index 3c078e9..9bbb1e7 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,14 @@ { "presets": [ - "es2015" - ] + [ + "env", + { + "loose": true, + "modules": "commonjs", + "targets": { + "node": 4, + }, + }, + ], + ], } diff --git a/.gitignore b/.gitignore index f638e40..a2587e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store .npm +lib node_modules npm-debug.log* yarn.lock diff --git a/.npmignore b/.npmignore index 6c2d762..f139cd7 100644 --- a/.npmignore +++ b/.npmignore @@ -6,4 +6,6 @@ coverage demo release.sh +scripts +src test diff --git a/.travis.yml b/.travis.yml index b29cf66..dd338dd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,3 +2,4 @@ language: node_js node_js: - "4" - "6" + - "8" diff --git a/README.md b/README.md index 505ec8e..851c195 100644 --- a/README.md +++ b/README.md @@ -264,6 +264,20 @@ Provides additional hash uniqueness. Might be useful for projects with several s Short alias for the [postcss-modules-local-by-default](https://github.com/css-modules/postcss-modules-local-by-default) plugin's option. + +### `resolve` object + +Changes the way the paths of ICSS imports will be resolved (`@value a from './b.css'` and `composes a from './b.css'`). Supports: + +- `resolve.alias` `object` +- `resolve.extensions` `array` — default value is `['.css']`. +- `resolve.modules` `array` +- `resolve.mainFile` `string` — default value is `'index.css'`. +- `resolve.preserveSymlinks` `boolean` — default value is `false`. + +See the detailed description at: https://github.com/css-modules/postcss-modules-resolve-imports#options + + ### `rootDir` string Provides absolute path to the project directory. Providing this will result in better generated class names. It can be obligatory, if you run require hook and build tools (like [css-modulesify](https://github.com/css-modules/css-modulesify)) from different working directories. diff --git a/demo/components/Button/Button.css b/demo/components/Button/Button.css index 9f99289..edf20f9 100644 --- a/demo/components/Button/Button.css +++ b/demo/components/Button/Button.css @@ -1,24 +1,24 @@ +@value primary, primaryShadow from 'Colors.css'; + +.common +{ + composes: common from './Common.css'; +} + .common, .common:active { - display: inline-block; - margin: 0 0 20px; padding: 0 11px; - cursor: pointer; - user-select: none; transition: all .3s; - text-align: center; - white-space: nowrap; - text-decoration: none; color: #fff; border: none; border-radius: 5px; outline: none; - background: #43a047; - box-shadow: 0 5px 0 #2e7d32; + background: primary; + box-shadow: 0 5px 0 primaryShadow; font: normal 20px/38px Roboto; } diff --git a/demo/components/Button/Colors.css b/demo/components/Button/Colors.css new file mode 100644 index 0000000..f3a7bcc --- /dev/null +++ b/demo/components/Button/Colors.css @@ -0,0 +1,2 @@ +@value primary #43a047; +@value primaryShadow #2e7d32; diff --git a/demo/components/Button/Common.css b/demo/components/Button/Common.css new file mode 100644 index 0000000..d2370e3 --- /dev/null +++ b/demo/components/Button/Common.css @@ -0,0 +1,11 @@ +.common +{ + display: inline-block; + + cursor: pointer; + user-select: none; + + text-align: center; + white-space: nowrap; + text-decoration: none; +} diff --git a/demo/package.json b/demo/package.json index c53702d..4780d66 100644 --- a/demo/package.json +++ b/demo/package.json @@ -19,24 +19,24 @@ "author": "Alexey Litvinov", "license": "MIT", "dependencies": { - "css-modules-require-hook": "^4.0.5", + "css-modules-require-hook": "next", "express": "^4.14.0", "react": "^15.3.2", "react-dom": "^15.3.2" }, "devDependencies": { - "autoprefixer": "^6.5.0", - "babel-core": "^6.17.0", - "babel-loader": "^6.2.5", - "babel-preset-es2015": "^6.16.0", - "babel-preset-react": "^6.16.0", - "babel-preset-stage-0": "^6.16.0", - "css-loader": "^0.25.0", - "extract-text-webpack-plugin": "^1.0.1", + "autoprefixer": "^7.2.4", + "babel-core": "^6.26.0", + "babel-loader": "^7.1.2", + "babel-preset-es2015": "^6.24.1", + "babel-preset-react": "^6.24.1", + "babel-preset-stage-0": "^6.24.1", + "css-loader": "0.28.7", + "extract-text-webpack-plugin": "^3.0.2", "npm-install-webpack-plugin": "^4.0.4", - "postcss-font-magician": "^1.4.0", - "postcss-loader": "^0.13.0", - "style-loader": "^0.13.1", - "webpack": "^1.13.2" + "postcss-font-magician": "^2.1.0", + "postcss-loader": "^2.0.10", + "style-loader": "^0.19.1", + "webpack": "^3.10.0" } } diff --git a/demo/postcss.config.js b/demo/postcss.config.js new file mode 100644 index 0000000..f6e339a --- /dev/null +++ b/demo/postcss.config.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + plugins: [ + // small sugar for CSS + require('postcss-font-magician'), + require('autoprefixer'), + ], +}; diff --git a/demo/webpack.config.js b/demo/webpack.config.js index 952b716..a3197ab 100644 --- a/demo/webpack.config.js +++ b/demo/webpack.config.js @@ -15,29 +15,37 @@ module.exports = { }, module: { - loaders: [ + rules: [ { test: /\.js$/i, exclude: /node_modules/, - loader: 'babel?presets[]=es2015,presets[]=react,presets[]=stage-0', + loader: 'babel-loader', + options: { + presets: ['es2015', 'react', 'stage-0'], + }, }, { test: /\.css$/i, - loader: ExtractTextPlugin.extract('style', - `css?modules&localIdentName=${config.css}!postcss`), + use: ExtractTextPlugin.extract({ + fallback: 'style-loader', + use: [ + { + loader: 'css-loader', + options: { + modules: true, + localIdentName: config.css, + }, + }, + 'postcss-loader', + ], + }), }, ], }, - postcss: [ - // small sugar for CSS - require('postcss-font-magician'), - require('autoprefixer'), - ], - plugins: [ new ExtractTextPlugin('common.css', { - allChunks: true + allChunks: true, }), new NpmInstallPlugin({ cacheMin: 999999, @@ -50,5 +58,5 @@ module.exports = { { react: true, }, - ] + ], }; diff --git a/package.json b/package.json index 07afdca..8dff914 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,10 @@ { "name": "css-modules-require-hook", - "version": "4.0.6", + "version": "4.2.3", "description": "A require hook to compile CSS Modules on the fly", "main": "lib/index.js", "engines": { - "node": ">=0.12" - }, - "scripts": { - "test": "npm run test:babel", - "test:babel": "NODE_PATH=$(pwd)/test/tokens/node_modules $npm_package_scripts_test_unit --compilers js:babel-register", - "test:coverage": "NODE_PATH=$(pwd)/test/tokens/node_modules babel-node --presets es2015 `npm bin`/isparta cover --report text --report html `npm bin`/_mocha -- --require test/setup.js --ui tdd test/*/*.js", - "test:node": "NODE_PATH=$(pwd)/test/tokens/node_modules $npm_package_scripts_test_unit", - "test:watch": "NODE_PATH=$(pwd)/test/tokens/node_modules $npm_package_scripts_test_unit --watch", - "test:unit": "mocha --require test/setup.js --ui tdd test/*/*.js" + "node": ">= 4" }, "repository": { "type": "git", @@ -32,28 +24,51 @@ "url": "https://github.com/css-modules/css-modules-require-hook/issues" }, "homepage": "https://github.com/css-modules/css-modules-require-hook", - "pre-commit": [], + "eslintConfig": { + "extends": "@sullenor/eslint-config/node", + "rules": { + "max-len": [ + 2, + 120 + ] + } + }, + "pre-commit": [ + "lint" + ], + "devDependencies": { + "@sullenor/eslint-config": "next", + "babel-preset-env": "^1.6.0", + "babel-register": "^6.26.0", + "eslint": "^4.6.1", + "gulp": "^3.9.1", + "gulp-babel": "^7.0.0", + "gulp-debug": "^3.1.0", + "jest": "^21.0.2", + "mocha": "^3.5.1", + "postcss-less": "^1.1.0", + "pre-commit": "^1.2.2", + "sinon": "^3.2.1" + }, "dependencies": { "debug": "^2.2.0", "generic-names": "^1.0.1", - "glob-to-regexp": "^0.1.0", + "glob-to-regexp": "^0.3.0", "icss-replace-symbols": "^1.0.2", "lodash": "^4.3.0", - "postcss": "^5.0.19", + "postcss": "^6.0.1", "postcss-modules-extract-imports": "^1.0.0", "postcss-modules-local-by-default": "^1.0.1", - "postcss-modules-parser": "^1.1.0", + "postcss-modules-resolve-imports": "^1.3.0", "postcss-modules-scope": "^1.0.0", "postcss-modules-values": "^1.1.1", "seekout": "^1.0.1" }, - "devDependencies": { - "babel-cli": "^6.5.1", - "babel-preset-es2015": "^6.5.0", - "babel-register": "^6.5.2", - "isparta": "^4.0.0", - "mocha": "^2.4.5", - "postcss-less": "^0.2.0", - "sinon": "^1.17.3" + "scripts": { + "lint": "eslint src/**/*.js", + "prepublish": "npm run transpile", + "pretest": "npm run transpile", + "test": "NODE_PATH=$(pwd)/test/tokens/node_modules mocha --require test/setup.js --ui tdd test/*/*.js --compilers js:babel-register", + "transpile": "gulp --cwd . --gulpfile scripts/gulpfile.js transpile" } } diff --git a/release.sh b/release.sh index 9653094..2720a39 100755 --- a/release.sh +++ b/release.sh @@ -22,7 +22,7 @@ case "$1" in esac # test the code -npm run test:node || exit 1 +npm run test || exit 1 # update package version npm --no-git-tag-version version "$versionType" git add package.json @@ -30,8 +30,4 @@ version=`sed -n '/version/p' package.json|cut -d'"' -f4` git commit -m "version $version" git tag "$version" -cli=node_modules/.bin -${cli}/babel lib --out-dir lib || exit 1 -${cli}/babel preset.js --out-file preset.js || exit 1 npm publish -git reset HEAD --hard diff --git a/scripts/gulpfile.js b/scripts/gulpfile.js new file mode 100644 index 0000000..5a0b41f --- /dev/null +++ b/scripts/gulpfile.js @@ -0,0 +1,24 @@ +'use strict'; + +const babel = require('gulp-babel'); +const debug = require('gulp-debug'); +const gulp = require('gulp'); + +gulp.task('transpile', () => + gulp.src('src/*.js') + .pipe(babel({ + presets: [ + [ + 'env', + { + loose: true, + modules: 'commonjs', + targets: { + node: 4, + }, + }, + ], + ], + })) + .pipe(debug({title: 'transpiled'})) + .pipe(gulp.dest('lib'))); diff --git a/lib/attachHook.js b/src/attachHook.js similarity index 100% rename from lib/attachHook.js rename to src/attachHook.js diff --git a/lib/index.js b/src/index.js similarity index 85% rename from lib/index.js rename to src/index.js index 43dacc4..8d4afae 100644 --- a/lib/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ 'use strict'; -const {assign, identity, negate, camelCase: camelCaseFunc, mapKeys} = require('lodash'); +const {assign, identity, negate} = require('lodash'); const {dirname, relative, resolve} = require('path'); const {readFileSync} = require('fs'); const {transformTokens} = require('./transformTokens'); @@ -15,7 +15,7 @@ const Values = require('postcss-modules-values'); const LocalByDefault = require('postcss-modules-local-by-default'); const ExtractImports = require('postcss-modules-extract-imports'); const Scope = require('postcss-modules-scope'); -const Parser = require('postcss-modules-parser'); +const ResolveImports = require('postcss-modules-resolve-imports'); const debugFetch = require('debug')('css-modules:fetch'); const debugSetup = require('debug')('css-modules:setup'); @@ -34,12 +34,14 @@ module.exports = function setupHook({ generateScopedName, hashPrefix, mode, + resolve: resolveOpts, use, rootDir: context = process.cwd(), }) { debugSetup(arguments[0]); validate(arguments[0]); + const exts = toArray(extensions); const tokensByFile = {}; // debug option is preferred NODE_ENV === 'development' @@ -48,18 +50,15 @@ module.exports = function setupHook({ : process.env.NODE_ENV === 'development'; let scopedName; - if (generateScopedName) { + if (generateScopedName) scopedName = typeof generateScopedName !== 'function' ? genericNames(generateScopedName, {context, hashPrefix}) // for example '[name]__[local]___[hash:base64:5]' : generateScopedName; - } else { + else // small fallback - scopedName = (local, filename) => { - return Scope.generateScopedName(local, relative(context, filename)); - }; - } + scopedName = (local, filename) => Scope.generateScopedName(local, relative(context, filename)); - const plugins = (use || [ + const plugins = use || [ ...prepend, Values, mode @@ -69,8 +68,9 @@ module.exports = function setupHook({ ? new ExtractImports({createImportedName}) : ExtractImports, new Scope({generateScopedName: scopedName}), + new ResolveImports({resolve: Object.assign({}, {extensions: exts}, resolveOpts)}), ...append, - ]).concat(new Parser({fetch})); // no pushing in order to avoid the possible mutations; + ]; // https://github.com/postcss/postcss#options const runner = postcss(plugins); @@ -83,7 +83,7 @@ module.exports = function setupHook({ */ function fetch(_to, from) { // getting absolute path to the processing file - const filename = /[^\\/?%*:|"<>\.]/i.test(_to[0]) + const filename = /[^\\/?%*:|"<>.]/i.test(_to[0]) ? require.resolve(_to) : resolve(dirname(from), _to); @@ -102,27 +102,24 @@ module.exports = function setupHook({ // https://github.com/postcss/postcss/blob/master/docs/api.md#lazywarnings lazyResult.warnings().forEach(message => console.warn(message.text)); - tokens = lazyResult.root.tokens; + tokens = lazyResult.root.exports || {}; - if (!debugMode) { + if (!debugMode) // updating cache tokensByFile[filename] = tokens; - } else { + else // clearing cache in development mode delete require.cache[filename]; - } - if (processCss) { + if (processCss) processCss(lazyResult.css, filename); - } debugFetch(`${filename} → fs`); debugFetch(tokens); return tokens; - }; + } - const exts = toArray(extensions); const isException = buildExceptionChecker(ignore); const hook = filename => { @@ -149,13 +146,11 @@ function toArray(option) { * @return {function} */ function buildExceptionChecker(ignore) { - if (ignore instanceof RegExp) { + if (ignore instanceof RegExp) return filepath => ignore.test(filepath); - } - if (typeof ignore === 'string') { + if (typeof ignore === 'string') return filepath => globToRegex(ignore).test(filepath); - } return ignore || negate(identity); } diff --git a/lib/transformTokens.js b/src/transformTokens.js similarity index 88% rename from lib/transformTokens.js rename to src/transformTokens.js index 5d1089f..aadbd1b 100644 --- a/lib/transformTokens.js +++ b/src/transformTokens.js @@ -16,14 +16,14 @@ const camelizeDashedKeys = (acc, value, key) => { const camelizeOnlyKeys = (acc, value, key) => { const camelizedKey = camelCase(key); - if (camelizedKey !== key) acc[camelizedKey] = value + if (camelizedKey !== key) acc[camelizedKey] = value; else acc[key] = value; return acc; }; const camelizeOnlyDashedKeys = (acc, value, key) => { const camelizedKey = camelizeDashes(key); - if (camelizedKey !== key) acc[camelizedKey] = value + if (camelizedKey !== key) acc[camelizedKey] = value; else acc[key] = value; return acc; }; @@ -36,7 +36,7 @@ exports.transformTokens = transformTokens; * @return {string} */ function camelizeDashes(str) { - return str.replace(/-(\w)/g, (m, letter) => letter.toUpperCase()); + return str.replace(/-+(\w)/g, (m, letter) => letter.toUpperCase()); } /** diff --git a/lib/validate.js b/src/validate.js similarity index 50% rename from lib/validate.js rename to src/validate.js index a0dbee3..ba34ca7 100644 --- a/lib/validate.js +++ b/src/validate.js @@ -14,46 +14,44 @@ const { const rules = { // hook - camelCase: 'boolean|string', - devMode: 'boolean', - extensions: 'array|string', - ignore: 'function|regex|string', - preprocessCss: 'function', - processCss: 'function', - processorOpts: 'object', + camelCase: 'boolean|string', + devMode: 'boolean', + extensions: 'array|string', + ignore: 'function|regex|string', + preprocessCss: 'function', + processCss: 'function', + processorOpts: 'object', // plugins - append: 'array', - prepend: 'array', - use: 'array', + append: 'array', + prepend: 'array', + use: 'array', createImportedName: 'function', generateScopedName: 'function|string', - hashPrefix: 'string', - mode: 'string', - rootDir: 'string', + hashPrefix: 'string', + mode: 'string', + resolve: 'object', + rootDir: 'string', }; const tests = { - array: isArray, - boolean: isBoolean, + array: isArray, + boolean: isBoolean, function: isFunction, - object: isPlainObject, - regex: isRegExp, - string: isString, + object: isPlainObject, + regex: isRegExp, + string: isString, }; module.exports = function validate(options) { const unknownOptions = difference(keys(options), keys(rules)); - if (unknownOptions.length) { + if (unknownOptions.length) throw new Error(`unknown arguments: ${unknownOptions.join(', ')}.`); - } forEach(rules, (types, rule) => { - if (typeof options[rule] === 'undefined') { + if (typeof options[rule] === 'undefined') return; - } - if (!types.split('|').some(type => tests[type](options[rule]))) { + if (!types.split('|').some(type => tests[type](options[rule]))) throw new TypeError(`should specify ${types} as ${rule}`); - } }); -} +}; diff --git a/test/api/camelCase.js b/test/api/camelCase.js index 6d5faa7..94194b9 100644 --- a/test/api/camelCase.js +++ b/test/api/camelCase.js @@ -23,7 +23,7 @@ suite('api/camelCase', () => { test('should replace keys with dashes by its camel-cased equivalent', () => { const tokens = require('./fixture/bem.css'); assert.deepEqual(tokens, { - 'block__element-Modifier': '_test_api_fixture_bem__block__element--modifier', + 'block__elementModifier': '_test_api_fixture_bem__block__element--modifier', }); }); diff --git a/test/api/fixture/shortcuts.css b/test/api/fixture/shortcuts.css new file mode 100644 index 0000000..86df41e --- /dev/null +++ b/test/api/fixture/shortcuts.css @@ -0,0 +1,4 @@ +.color +{ + composes: color from 'oceanic'; +} diff --git a/test/api/generateScopedName.js b/test/api/generateScopedName.js index a6d092e..c39faab 100644 --- a/test/api/generateScopedName.js +++ b/test/api/generateScopedName.js @@ -8,7 +8,7 @@ suite('api/generateScopedName', () => { let args; let tokens; - const processor = spy(function (selector, filepath, source) { + const processor = spy((selector, filepath, source) => { args = [selector, filepath, source]; return selector; }); diff --git a/test/api/hashPrefix.js b/test/api/hashPrefix.js index 87fdb7a..a2a5ec4 100644 --- a/test/api/hashPrefix.js +++ b/test/api/hashPrefix.js @@ -2,7 +2,7 @@ const detachHook = require('../sugar').detachHook; const dropCache = require('../sugar').dropCache; suite('api/hashPrefix', () => { - let samples = []; + const samples = []; suite('using string pattern and hashPrefix', () => { let tokens; diff --git a/test/api/resolve.js b/test/api/resolve.js new file mode 100644 index 0000000..5ed85b9 --- /dev/null +++ b/test/api/resolve.js @@ -0,0 +1,23 @@ +const {detachHook, dropCache} = require('../sugar'); +const path = require('path'); + +suite('api/resolve', () => { + test('should be called', () => { + const tokens = require('./fixture/shortcuts.css'); + + assert.deepEqual(tokens, { + color: '_test_api_fixture_shortcuts__color _test_api_fixture_oceanic__color', + }); + }); + + setup(() => { + hook({resolve: { + modules: [path.join(__dirname, 'fixture')], + }}); + }); + + teardown(() => { + detachHook('.css'); + dropCache('./api/fixture/oceanic.css'); + }); +});