diff --git a/.babelrc b/.babelrc index 197c614..3751ba8 100644 --- a/.babelrc +++ b/.babelrc @@ -1,14 +1,19 @@ { - "presets": [ - "es2015", - "stage-0", - "react" + "plugins": [ + "add-module-exports", + "lodash", + "transform-class-properties", + [ + "transform-es2015-classes", + { + "loose": true + } ], - "plugins": [ - "add-module-exports", - "lodash", - "transform-class-properties", - ["transform-es2015-classes", { "loose": true }], - "transform-proto-to-assign" - ] + "transform-proto-to-assign" + ], + "presets": [ + "es2015", + "stage-0", + "react" + ] } diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0f17867 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.eslintrc b/.eslintrc index 40288f8..bcaad1b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,3 +1,6 @@ { - "extends": "canonical" + "extends": [ + "canonical", + "canonical/mocha" + ] } diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..2f093a7 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: gajus +patreon: gajus diff --git a/.gitignore b/.gitignore index d54786c..df978de 100755 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,10 @@ coverage dist *.log .* -!.README +!.babelrc +!.editorconfig +!.eslintrc !.gitignore !.npmignore -!.babelrc +!.README !.travis.yml -!.eslintrc diff --git a/.travis.yml b/.travis.yml index 7eedb8d..a786167 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,14 @@ language: node_js node_js: - - 6 - - 5 - - 4 + - node + - 8 before_install: - npm config set depth 0 - - npm install --global npm@3 notifications: email: false sudo: false script: - npm run test - npm run lint +after_success: + - semantic-release pre && npm publish && semantic-release post diff --git a/README.md b/README.md index 26dd67b..a6b7399 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # React CSS Modules +[![GitSpo Mentions](https://gitspo.com/badges/mentions/gajus/react-css-modules?style=flat-square)](https://gitspo.com/mentions/gajus/react-css-modules) [![Travis build status](http://img.shields.io/travis/gajus/react-css-modules/master.svg?style=flat-square)](https://travis-ci.org/gajus/react-css-modules) [![NPM version](http://img.shields.io/npm/v/react-css-modules.svg?style=flat-square)](https://www.npmjs.org/package/react-css-modules) [![js-canonical-style](https://img.shields.io/badge/code%20style-canonical-blue.svg?style=flat-square)](https://github.com/gajus/canonical) @@ -8,6 +9,16 @@ React CSS Modules implement automatic mapping of CSS modules. Every CSS class is assigned a local-scoped identifier with a global unique name. CSS Modules enable a modular and reusable CSS! +> ## ⚠️⚠️⚠️ DEPRECATION NOTICE ⚠️⚠️⚠️ +> +> If you are considering to use `react-css-modules`, evaluate if [`babel-plugin-react-css-modules`](https://github.com/gajus/babel-plugin-react-css-modules) covers your use case. +> `babel-plugin-react-css-modules` is a lightweight alternative of `react-css-modules`. +> +> `babel-plugin-react-css-modules` is not a drop-in replacement and does not cover all the use cases of `react-css-modules`. +> However, it has a lot smaller performance overhead (0-10% vs +50%; see [Performance](https://github.com/gajus/babel-plugin-react-css-modules#performance)) and a lot smaller size footprint (less than 2kb vs +17kb). +> +> It is easy to get started! See the demo https://github.com/gajus/babel-plugin-react-css-modules/tree/master/demo + - [CSS Modules](#css-modules) - [webpack `css-loader`](#webpack-css-loader) - [What's the Problem?](#whats-the-problem) @@ -24,13 +35,12 @@ React CSS Modules implement automatic mapping of CSS modules. Every CSS class is - [Decorator](#decorator) - [Options](#options) - [`allowMultiple`](#allowmultiple) - - [`errorWhenNotFound`](#errorwhennotfound) + - [`handleNotFoundStyleName`](#handlenotfoundstylename) - [SASS, SCSS, LESS and other CSS Preprocessors](#sass-scss-less-and-other-css-preprocessors) - [Enable Sourcemaps](#enable-sourcemaps) - [Class Composition](#class-composition) - [What Problems does Class Composition Solve?](#what-problems-does-class-composition-solve) - [Class Composition Using CSS Preprocessors](#class-composition-using-css-preprocessors) -- [SASS, SCSS, LESS and other CSS Preprocessors](#sass-scss-less-and-other-css-preprocessors) - [Global CSS](#global-css) - [Multiple CSS Modules](#multiple-css-modules) @@ -115,7 +125,7 @@ Using `react-css-modules`:
``` -* You are warned when `styleName` refers to an undefined CSS Module ([`errorWhenNotFound`](#errorwhennotfound) option). +* You are warned when `styleName` refers to an undefined CSS Module ([`handleNotFoundStyleName`](#handlenotfoundstylename) option). * You can enforce use of a single CSS module per `ReactElement` ([`allowMultiple`](#allowmultiple) option). ## The Implementation @@ -149,8 +159,8 @@ Setup: { test: /\.css$/, loaders: [ - 'style?sourceMap', - 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]' + 'style-loader?sourceMap', + 'css-loader?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]' ] } ``` @@ -199,9 +209,9 @@ Setup: ```js { test: /\.css$/, - loader: ExtractTextPlugin.extract({ - notExtractLoader: 'style-loader', - loader: 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]!resolve-url!postcss', + use: ExtractTextPlugin.extract({ + fallback: 'style-loader', + use: 'css-loader?modules,localIdentName="[name]-[local]-[hash:base64:6]"' }), } ``` @@ -396,7 +406,7 @@ export default CSSModules(CustomList, styles); * @typedef CSSModules~Options * @see {@link https://github.com/gajus/react-css-modules#options} * @property {Boolean} allowMultiple - * @property {Boolean} errorWhenNotFound + * @property {String} handleNotFoundStyleName */ /** @@ -480,11 +490,17 @@ When `false`, the following will cause an error:
``` -#### `errorWhenNotFound` +#### `handleNotFoundStyleName` + +Default: `throw`. + +Defines the desired action when `styleName` cannot be mapped to an existing CSS Module. -Default: `true`. +Available options: -Throws an error when `styleName` cannot be mapped to an existing CSS Module. +* `throw` throws an error +* `log` logs a warning using `console.warn` +* `ignore` silently ignores the missing style name ## SASS, SCSS, LESS and other CSS Preprocessors diff --git a/package.json b/package.json index a5d10f7..00e1ffa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-css-modules", "description": "Seamless mapping of class names to CSS modules inside of React components.", - "main": "./dist/", + "main": "./dist/index.js", "repository": { "type": "git", "url": "https://github.com/gajus/react-css-modules" @@ -12,7 +12,7 @@ "css", "modules" ], - "version": "4.0.0", + "version": "4.3.0", "author": { "name": "Gajus Kuizinas", "email": "gajus@gajus.com", @@ -20,34 +20,36 @@ }, "license": "BSD-3-Clause", "dependencies": { - "hoist-non-react-statics": "^1.0.5", - "lodash": "^4.6.1", - "object-unfreeze": "^1.0.2" + "hoist-non-react-statics": "^2.5.5", + "lodash": "^4.16.6", + "object-unfreeze": "^1.1.0" }, "devDependencies": { - "babel-cli": "^6.10.1", + "babel-cli": "^6.18.0", "babel-plugin-add-module-exports": "^0.2.1", - "babel-plugin-lodash": "^3.2.5", + "babel-plugin-lodash": "^3.2.9", "babel-plugin-transform-proto-to-assign": "^6.9.0", - "babel-preset-es2015": "^6.9.0", - "babel-preset-react": "^6.11.1", - "babel-preset-stage-0": "^6.5.0", - "babel-register": "^6.9.0", - "chai": "^3.5.0", - "eslint": "^3.0.0", - "eslint-config-canonical": "^1.7.12", - "husky": "^0.11.7", - "jsdom": "^9.5.0", - "mocha": "^3.0.2", - "react": "^15.0.0-rc.1", - "react-addons-shallow-compare": "^15.0.0-rc.1", - "react-addons-test-utils": "^15.0.0-rc.1", - "react-dom": "^15.0.0-rc.1" + "babel-preset-es2015": "^6.18.0", + "babel-preset-react": "^6.16.0", + "babel-preset-stage-0": "^6.16.0", + "babel-register": "^6.18.0", + "chai": "^4.0.0-canary.1", + "chai-spies": "^0.7.1", + "eslint": "^3.10.0", + "eslint-config-canonical": "^5.5.0", + "husky": "^0.11.9", + "jsdom": "^9.8.3", + "mocha": "^3.1.2", + "react": "^15.4.0-rc.4", + "react-addons-shallow-compare": "^15.4.0-rc.4", + "react-addons-test-utils": "^15.4.0-rc.4", + "react-dom": "^15.4.0-rc.4", + "semantic-release": "^6.3.2" }, "scripts": { "lint": "eslint ./src ./tests", - "test": "mocha --compilers js:babel-register ./tests/**/*.js", - "build": "babel ./src --out-dir ./dist", - "precommit": "npm run lint && npm run test" + "test": "NODE_ENV=development mocha --compilers js:babel-register ./tests/**/*.js && npm run lint && npm run build", + "build": "NODE_ENV=production babel ./src --out-dir ./dist", + "precommit": "npm run test" } } diff --git a/src/simple-map.js b/src/SimpleMap.js similarity index 72% rename from src/simple-map.js rename to src/SimpleMap.js index 435ae25..7cf618b 100644 --- a/src/simple-map.js +++ b/src/SimpleMap.js @@ -1,4 +1,4 @@ -export class SimpleMap { +export default class { constructor () { this.size = 0; this.keys = []; @@ -19,7 +19,3 @@ export class SimpleMap { return value; } } - -const exportedMap = typeof Map === 'undefined' ? SimpleMap : Map; - -export default exportedMap; diff --git a/src/extendReactClass.js b/src/extendReactClass.js index 0588005..4741bea 100644 --- a/src/extendReactClass.js +++ b/src/extendReactClass.js @@ -1,9 +1,10 @@ /* eslint-disable react/prop-types */ -import React from 'react'; import _ from 'lodash'; +import React from 'react'; import hoistNonReactStatics from 'hoist-non-react-statics'; import linkClass from './linkClass'; +import renderNothing from './renderNothing'; /** * @param {ReactClass} Component @@ -14,44 +15,60 @@ import linkClass from './linkClass'; export default (Component: Object, defaultStyles: Object, options: Object) => { const WrappedComponent = class extends Component { render () { - let propsChanged, - styles; + let styles; + + const hasDefaultstyles = _.isObject(defaultStyles); - propsChanged = false; + let renderResult; - if (this.props.styles) { - styles = this.props.styles; - } else if (_.isObject(defaultStyles)) { + if (this.props.styles || hasDefaultstyles) { const props = Object.assign({}, this.props); + if (props.styles) { + styles = props.styles; + } else if (hasDefaultstyles) { + styles = defaultStyles; + delete props.styles; + } + Object.defineProperty(props, 'styles', { configurable: true, enumerable: false, - value: defaultStyles, + value: styles, writable: false }); - this.props = props; + const originalProps = this.props; + + let renderIsSuccessful = false; - propsChanged = true; - styles = defaultStyles; + try { + this.props = props; + + renderResult = super.render(); + + renderIsSuccessful = true; + } finally { + this.props = originalProps; + } + + // @see https://github.com/facebook/react/issues/14224 + if (!renderIsSuccessful) { + renderResult = super.render(); + } } else { styles = {}; - } - - const renderResult = super.render(); - if (propsChanged) { - delete this.props.styles; + renderResult = super.render(); } if (renderResult) { return linkClass(renderResult, styles, options); } - return React.createElement('noscript'); + return renderNothing(React.version); } - }; + }; return hoistNonReactStatics(WrappedComponent, Component); }; diff --git a/src/generateAppendClassName.js b/src/generateAppendClassName.js index e0fe7df..127aa2a 100644 --- a/src/generateAppendClassName.js +++ b/src/generateAppendClassName.js @@ -1,10 +1,12 @@ -import Map from './simple-map'; +import SimpleMap from './SimpleMap'; -const stylesIndex = new Map(); +const CustomMap = typeof Map === 'undefined' ? SimpleMap : Map; -export default (styles, styleNames: Array, errorWhenNotFound: boolean): string => { - let appendClassName, - stylesIndexMap; +const stylesIndex = new CustomMap(); + +export default (styles, styleNames: Array, handleNotFoundStyleName: "throw" | "log" | "ignore"): string => { + let appendClassName; + let stylesIndexMap; stylesIndexMap = stylesIndex.get(styles); @@ -15,8 +17,8 @@ export default (styles, styleNames: Array, errorWhenNotFound: boolean): return styleNameIndex; } } else { - stylesIndexMap = new Map(); - stylesIndex.set(styles, new Map()); + stylesIndexMap = new CustomMap(); + stylesIndex.set(styles, new CustomMap()); } appendClassName = ''; @@ -27,8 +29,14 @@ export default (styles, styleNames: Array, errorWhenNotFound: boolean): if (className) { appendClassName += ' ' + className; - } else if (errorWhenNotFound === true) { - throw new Error('"' + styleNames[styleName] + '" CSS module is undefined.'); + } else { + if (handleNotFoundStyleName === 'throw') { + throw new Error('"' + styleNames[styleName] + '" CSS module is undefined.'); + } + if (handleNotFoundStyleName === 'log') { + // eslint-disable-next-line no-console + console.warn('"' + styleNames[styleName] + '" CSS module is undefined.'); + } } } } diff --git a/src/linkClass.js b/src/linkClass.js index 64a1636..4ed3975 100644 --- a/src/linkClass.js +++ b/src/linkClass.js @@ -7,37 +7,64 @@ import isIterable from './isIterable'; import parseStyleName from './parseStyleName'; import generateAppendClassName from './generateAppendClassName'; +const linkArray = (array: Array, styles: Object, configuration: Object) => { + _.forEach(array, (value, index) => { + if (React.isValidElement(value)) { + // eslint-disable-next-line no-use-before-define + array[index] = linkElement(React.Children.only(value), styles, configuration); + } else if (_.isArray(value)) { + const unfreezedValue = Object.isFrozen(value) ? objectUnfreeze(value) : value; + + array[index] = linkArray(unfreezedValue, styles, configuration); + } + }); + + return array; +}; + const linkElement = (element: ReactElement, styles: Object, configuration: Object): ReactElement => { - let appendClassName, - elementIsFrozen, - elementShallowCopy; + let appendClassName; + let elementShallowCopy; elementShallowCopy = element; - if (Object.isFrozen && Object.isFrozen(elementShallowCopy)) { - elementIsFrozen = true; + if (Array.isArray(elementShallowCopy)) { + return elementShallowCopy.map((arrayElement) => { + return linkElement(arrayElement, styles, configuration); + }); + } - // https://github.com/facebook/react/blob/v0.13.3/src/classic/element/ReactElement.js#L131 + const elementIsFrozen = Object.isFrozen && Object.isFrozen(elementShallowCopy); + const propsFrozen = Object.isFrozen && Object.isFrozen(elementShallowCopy.props); + const propsNotExtensible = Object.isExtensible && !Object.isExtensible(elementShallowCopy.props); + + if (elementIsFrozen) { + // https://github.com/facebook/react/blob/v0.13.3/src/classic/element/ReactElement.js#L131 elementShallowCopy = objectUnfreeze(elementShallowCopy); elementShallowCopy.props = objectUnfreeze(elementShallowCopy.props); + } else if (propsFrozen || propsNotExtensible) { + elementShallowCopy.props = objectUnfreeze(elementShallowCopy.props); } const styleNames = parseStyleName(elementShallowCopy.props.styleName || '', configuration.allowMultiple); + const {children, ...restProps} = elementShallowCopy.props; - if (React.isValidElement(elementShallowCopy.props.children)) { - elementShallowCopy.props.children = linkElement(React.Children.only(elementShallowCopy.props.children), styles, configuration); - } else if (_.isArray(elementShallowCopy.props.children) || isIterable(elementShallowCopy.props.children)) { - elementShallowCopy.props.children = React.Children.map(elementShallowCopy.props.children, (node) => { - if (React.isValidElement(node)) { - return linkElement(node, styles, configuration); - } else { - return node; - } - }); + if (React.isValidElement(children)) { + elementShallowCopy.props.children = linkElement(React.Children.only(children), styles, configuration); + } else if (_.isArray(children) || isIterable(children)) { + elementShallowCopy.props.children = linkArray(objectUnfreeze(children), styles, configuration); } + _.forEach(restProps, (propValue, propName) => { + if (React.isValidElement(propValue)) { + elementShallowCopy.props[propName] = linkElement(React.Children.only(propValue), styles, configuration); + } else if (_.isArray(propValue)) { + elementShallowCopy.props[propName] = linkArray(propValue, styles, configuration); + } + }); + if (styleNames.length) { - appendClassName = generateAppendClassName(styles, styleNames, configuration.errorWhenNotFound); + appendClassName = generateAppendClassName(styles, styleNames, configuration.handleNotFoundStyleName); if (appendClassName) { if (elementShallowCopy.props.className) { @@ -53,6 +80,12 @@ const linkElement = (element: ReactElement, styles: Object, configuration: Objec if (elementIsFrozen) { Object.freeze(elementShallowCopy.props); Object.freeze(elementShallowCopy); + } else if (propsFrozen) { + Object.freeze(elementShallowCopy.props); + } + + if (propsNotExtensible) { + Object.preventExtensions(elementShallowCopy.props); } return elementShallowCopy; diff --git a/src/makeConfiguration.js b/src/makeConfiguration.js index 0eae15f..cc3169c 100644 --- a/src/makeConfiguration.js +++ b/src/makeConfiguration.js @@ -4,7 +4,7 @@ import _ from 'lodash'; * @typedef CSSModules~Options * @see {@link https://github.com/gajus/react-css-modules#options} * @property {boolean} allowMultiple - * @property {boolean} errorWhenNotFound + * @property {string} handleNotFoundStyleName */ /** @@ -14,7 +14,7 @@ import _ from 'lodash'; export default (userConfiguration = {}) => { const configuration = { allowMultiple: false, - errorWhenNotFound: true + handleNotFoundStyleName: 'throw' }; _.forEach(userConfiguration, (value, name) => { @@ -22,8 +22,12 @@ export default (userConfiguration = {}) => { throw new Error('Unknown configuration property "' + name + '".'); } - if (!_.isBoolean(value)) { - throw new Error('"' + name + '" property value must be a boolean.'); + if (name === 'allowMultiple' && !_.isBoolean(value)) { + throw new Error('"allowMultiple" property value must be a boolean.'); + } + + if (name === 'handleNotFoundStyleName' && !_.includes(['throw', 'log', 'ignore'], value)) { + throw new Error('"handleNotFoundStyleName" property value must be "throw", "log" or "ignore".'); } configuration[name] = value; diff --git a/src/parseStyleName.js b/src/parseStyleName.js index 7b5ee55..937b2d6 100644 --- a/src/parseStyleName.js +++ b/src/parseStyleName.js @@ -8,7 +8,7 @@ export default (styleNamePropertyValue: string, allowMultiple: boolean): Array { const WrappedComponent = (props = {}, ...args) => { - let styles, - useProps; + let styles; + let useProps; + const hasDefaultstyles = _.isObject(defaultStyles); - if (props.styles) { - useProps = props; - styles = props.styles; - } else if (_.isObject(defaultStyles)) { - useProps = _.assign({}, props, { - styles: defaultStyles - }); + if (props.styles || hasDefaultstyles) { + useProps = Object.assign({}, props); + + if (props.styles) { + styles = props.styles; + } else { + styles = defaultStyles; + } Object.defineProperty(useProps, 'styles', { configurable: true, enumerable: false, - value: defaultStyles, + value: styles, writable: false }); - - styles = defaultStyles; } else { useProps = props; styles = {}; @@ -39,7 +40,7 @@ export default (Component: Function, defaultStyles: Object, options: Object): Fu return linkClass(renderResult, styles, options); } - return React.createElement('noscript'); + return renderNothing(React.version); }; _.assign(WrappedComponent, Component); diff --git a/tests/simple-map.js b/tests/SimpleMap.js similarity index 61% rename from tests/simple-map.js rename to tests/SimpleMap.js index a9b7d1a..08ca888 100644 --- a/tests/simple-map.js +++ b/tests/SimpleMap.js @@ -1,12 +1,10 @@ import { expect } from 'chai'; -import { - SimpleMap -} from './../src/simple-map'; +import SimpleMap from './../src/SimpleMap'; -const getTests = (map) => { - return () => { +describe('SimpleMap', () => { + context('simple map with primitive or object as keys', () => { const values = [ [1, 'something'], ['1', 'somethingElse'], @@ -14,6 +12,12 @@ const getTests = (map) => { [null, null] ]; + let map; + + beforeEach(() => { + map = new SimpleMap(); + }); + it('should set', () => { values.forEach(([key, value]) => { map.set(key, value); @@ -22,16 +26,13 @@ const getTests = (map) => { }); it('should get', () => { + values.forEach(([key, value]) => { + map.set(key, value); + }); + values.forEach(([key, value]) => { expect(map.get(key)).to.equal(value); }); }); - }; -}; - -describe('SimpleMap', () => { - context('simple map with primitive or object as keys', getTests(new SimpleMap())); - if (typeof Map !== 'undefined') { - context('sanity - running tests against native Map', getTests(new Map())); - } + }); }); diff --git a/tests/extendReactClass.js b/tests/extendReactClass.js index 1113506..01733ee 100644 --- a/tests/extendReactClass.js +++ b/tests/extendReactClass.js @@ -71,8 +71,8 @@ describe('extendReactClass', () => { TestUtils.renderIntoDocument(); }); it('does not affect pure-render logic', (done) => { - let Component, - rendered; + let Component; + let rendered; rendered = false; @@ -128,7 +128,7 @@ describe('extendReactClass', () => { }); }); context('rendering Component that returns null', () => { - it('generates