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/.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 bfa13bf..a786167 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,7 @@ language: node_js node_js: - - 7 - - 6 - - 5 + - node + - 8 before_install: - npm config set depth 0 notifications: diff --git a/README.md b/README.md index 02e006d..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,9 +9,7 @@ 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! -> ⚠️⚠️⚠️ -> -> Note: +> ## ⚠️⚠️⚠️ 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`. @@ -18,8 +17,7 @@ React CSS Modules implement automatic mapping of CSS modules. Every CSS class is > `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 +> 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) @@ -161,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]' ] } ``` diff --git a/package.json b/package.json index e276aa4..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" @@ -20,7 +20,7 @@ }, "license": "BSD-3-Clause", "dependencies": { - "hoist-non-react-statics": "^1.2.0", + "hoist-non-react-statics": "^2.5.5", "lodash": "^4.16.6", "object-unfreeze": "^1.1.0" }, diff --git a/src/extendReactClass.js b/src/extendReactClass.js index 6eba084..4741bea 100644 --- a/src/extendReactClass.js +++ b/src/extendReactClass.js @@ -19,14 +19,16 @@ export default (Component: Object, defaultStyles: Object, options: Object) => { const hasDefaultstyles = _.isObject(defaultStyles); + let renderResult; + if (this.props.styles || hasDefaultstyles) { const props = Object.assign({}, this.props); - if (this.props.styles) { - styles = this.props.styles; + if (props.styles) { + styles = props.styles; } else if (hasDefaultstyles) { styles = defaultStyles; - delete this.props.styles; + delete props.styles; } Object.defineProperty(props, 'styles', { @@ -36,12 +38,29 @@ export default (Component: Object, defaultStyles: Object, options: Object) => { writable: false }); - this.props = props; + const originalProps = this.props; + + let renderIsSuccessful = false; + + 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(); + renderResult = super.render(); + } if (renderResult) { return linkClass(renderResult, styles, options); diff --git a/src/linkClass.js b/src/linkClass.js index 48142e1..4ed3975 100644 --- a/src/linkClass.js +++ b/src/linkClass.js @@ -13,7 +13,9 @@ const linkArray = (array: Array, styles: Object, configuration: Object) => { // eslint-disable-next-line no-use-before-define array[index] = linkElement(React.Children.only(value), styles, configuration); } else if (_.isArray(value)) { - array[index] = linkArray(value, styles, configuration); + const unfreezedValue = Object.isFrozen(value) ? objectUnfreeze(value) : value; + + array[index] = linkArray(unfreezedValue, styles, configuration); } }); @@ -22,17 +24,26 @@ const linkArray = (array: Array, styles: Object, configuration: Object) => { const linkElement = (element: ReactElement, styles: Object, configuration: Object): ReactElement => { let appendClassName; - let elementIsFrozen; 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); + }); + } + + const elementIsFrozen = Object.isFrozen && Object.isFrozen(elementShallowCopy); + const propsFrozen = Object.isFrozen && Object.isFrozen(elementShallowCopy.props); + const propsNotExtensible = Object.isExtensible && !Object.isExtensible(elementShallowCopy.props); - // https://github.com/facebook/react/blob/v0.13.3/src/classic/element/ReactElement.js#L131 + 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); @@ -41,14 +52,7 @@ const linkElement = (element: ReactElement, styles: Object, configuration: Objec if (React.isValidElement(children)) { elementShallowCopy.props.children = linkElement(React.Children.only(children), styles, configuration); } else if (_.isArray(children) || isIterable(children)) { - elementShallowCopy.props.children = React.Children.map(children, (node) => { - if (React.isValidElement(node)) { - // eslint-disable-next-line no-use-before-define - return linkElement(React.Children.only(node), styles, configuration); - } else { - return node; - } - }); + elementShallowCopy.props.children = linkArray(objectUnfreeze(children), styles, configuration); } _.forEach(restProps, (propValue, propName) => { @@ -76,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/tests/linkClass.js b/tests/linkClass.js index fdda06b..cf7c289 100644 --- a/tests/linkClass.js +++ b/tests/linkClass.js @@ -1,4 +1,4 @@ -/* eslint-disable max-nested-callbacks, react/prefer-stateless-function, class-methods-use-this, no-console */ +/* eslint-disable max-nested-callbacks, react/prefer-stateless-function, class-methods-use-this, no-console, no-unused-expressions */ import chai, { expect @@ -125,6 +125,33 @@ describe('linkClass', () => { expect(subject.props.children[0].props.className).to.equal('foo-1'); expect(subject.props.children[1].props.className).to.equal('bar-1'); }); + it('assigns a generated className to elements inside nested arrays', () => { + let subject; + + subject =
+ {[ + [ +

, +

+ ], + [ +

, +

+ ] + ]} +

; + + subject = linkClass(subject, { + bar: 'bar-1', + foo: 'foo-1' + }); + + expect(subject.props.children[0][0].props.className).to.equal('foo-1'); + expect(subject.props.children[0][1].props.className).to.equal('bar-1'); + + expect(subject.props.children[1][0].props.className).to.equal('foo-1'); + expect(subject.props.children[1][1].props.className).to.equal('bar-1'); + }); it('styleName is deleted from props', () => { let subject; @@ -141,6 +168,22 @@ describe('linkClass', () => { expect(subject.props.children[0].props).not.to.have.property('styleName'); expect(subject.props.children[1].props).not.to.have.property('styleName'); }); + it('preserves original keys', () => { + let subject; + + subject =
+

+

+

; + + subject = linkClass(subject, { + bar: 'bar-1', + foo: 'foo-1' + }); + + expect(subject.props.children[0].key).to.equal('1'); + expect(subject.props.children[1].key).to.equal('2'); + }); }); context('when multiple descendants have styleName and are iterable', () => { it('assigns a generated className', () => { @@ -239,6 +282,76 @@ describe('linkClass', () => { }); }); + context('can\'t write to properties', () => { + context('when the element is frozen', () => { + it('adds className but is still frozen', () => { + let subject; + + subject =
; + + Object.freeze(subject); + subject = linkClass(subject, { + foo: 'foo-1' + }); + + expect(subject).to.be.frozen; + expect(subject.props.className).to.equal('foo-1'); + }); + }); + context('when the element\'s props are frozen', () => { + it('adds className and only props are still frozen', () => { + let subject; + + subject =
; + + Object.freeze(subject.props); + subject = linkClass(subject, { + foo: 'foo-1' + }); + + expect(subject.props).to.be.frozen; + expect(subject.props.className).to.equal('foo-1'); + }); + }); + context('when the element\'s props are not extensible', () => { + it('adds className and props are still not extensible', () => { + let subject; + + subject =
; + + Object.preventExtensions(subject.props); + subject = linkClass(subject, { + foo: 'foo-1' + }); + + expect(subject.props).to.not.be.extensible; + expect(subject.props.className).to.equal('foo-1'); + }); + }); + }); + + context('when element is an array', () => { + it('handles each element individually', () => { + let subject; + + subject = [ +
, +
+

+

+ ]; + + subject = linkClass(subject, { + bar: 'bar-1', + foo: 'foo-1' + }); + + expect(subject).to.be.an('array'); + expect(subject[0].props.className).to.equal('foo-1'); + expect(subject[1].props.children.props.className).to.equal('bar-1'); + }); + }); + describe('options.allowMultiple', () => { context('when multiple module names are used', () => { context('when false', () => {