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
+[](https://gitspo.com/mentions/gajus/react-css-modules)
[](https://travis-ci.org/gajus/react-css-modules)
[](https://www.npmjs.org/package/react-css-modules)
[](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