Skip to content

Commit

Permalink
fix: composes on wrong class (#471)
Browse files Browse the repository at this point in the history
fixes #365
  • Loading branch information
jquense committed Nov 5, 2019
1 parent 391e661 commit afdb371
Show file tree
Hide file tree
Showing 54 changed files with 489 additions and 87 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"loader-utils": "^1.2.3",
"lodash": "^4.17.15",
"memory-fs": "^0.5.0",
"postcss-atroot": "^0.1.3",
"postcss-loader": "^3.0.0",
"postcss-nested": "^4.1.1",
"resolve": "^1.11.1"
Expand Down
3 changes: 2 additions & 1 deletion src/css-loader.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
const cssLoader = require('css-loader');
const postcssLoader = require('postcss-loader');
const postcssNested = require('postcss-nested');
const postcssAtroot = require('postcss-atroot');

function postcss(loader, css, map, meta, cb) {
const ctx = { ...loader };
ctx.async = () => cb;
ctx.loaderIndex++;
ctx.query = {
plugins: [postcssNested()],
plugins: [postcssNested(), postcssAtroot()],
};

postcssLoader.call(ctx, css, map, meta);
Expand Down
31 changes: 11 additions & 20 deletions src/utils/wrapInClass.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,30 @@ import { stripIndents } from 'common-tags';

export default function wrapInClass(text) {
const imports = [];
const composes = [];

let match;
const rImports = /@import.*?(?:$|;)/g;
const rComposes = /\b(?:composes\s*?:\s*(.*?)(?:from\s(.+?))?(?=[}\n\r]))/gim;

// eslint-disable-next-line no-cond-assign
while ((match = rImports.exec(text))) {
imports.push(match[0]);
}
match = null;
// eslint-disable-next-line no-cond-assign
while ((match = rComposes.exec(text))) {
composes.push(match[0]);
}

text = text.replace(rImports, '').replace(rComposes, '');
text = text.replace(rImports, '');

// Components need two css classes, the actual style declarations and a hook class.
// We need both so that that interpolations have a class that is _only_
// the single class, e.g. no additional classes composed in so that it can be used
// as a selector
//
// comment prevents Sass from removing the empty class
let val = stripIndents`
.cls1 {
${text.trim() || '/*!*/'}
}`;
.cls1 { /*!*/ }
.cls2 {
composes: cls1;
// Components need two css classes, the first being the single root
// class with styles and the second is the exported name used in the Styled helper
// We need both so that that interpolations have a class that is _only_
// the single class, e.g. no additional classes composed in so that it can be used
// as a selecto
if (composes.length) {
composes.unshift(`composes: cls1;`);
val += `\n.cls2 {\n${composes.join('\n')}\n}`;
}
${text.trim()}
}`;

if (imports.length) val = `${imports.join('\n')}\n${val}`;
return val;
Expand Down
211 changes: 211 additions & 0 deletions test/__file_snapshots__/issue-365-js.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["main"],{

/***/ "./src/runtime/styled.js":
/*!*******************************!*\
!*** ./src/runtime/styled.js ***!
\*******************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

const React = __webpack_require__(/*! react */ "./node_modules/react/index.js"); // eslint-disable-line import/no-extraneous-dependencies

// eslint-disable-next-line no-control-regex
const reWords = /[A-Z\xc0-\xd6\xd8-\xde]?[a-z\xdf-\xf6\xf8-\xff]+(?:['’](?:d|ll|m|re|s|t|ve))?(?=[\xac\xb1\xd7\xf7\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\xbf\u2000-\u206f \t\x0b\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000]|[A-Z\xc0-\xd6\xd8-\xde]|$)|(?:[A-Z\xc0-\xd6\xd8-\xde]|[^\ud800-\udfff\xac\xb1\xd7\xf7\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\xbf\u2000-\u206f \t\x0b\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\d+\u2700-\u27bfa-z\xdf-\xf6\xf8-\xffA-Z\xc0-\xd6\xd8-\xde])+(?:['’](?:D|LL|M|RE|S|T|VE))?(?=[\xac\xb1\xd7\xf7\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\xbf\u2000-\u206f \t\x0b\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000]|[A-Z\xc0-\xd6\xd8-\xde](?:[a-z\xdf-\xf6\xf8-\xff]|[^\ud800-\udfff\xac\xb1\xd7\xf7\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\xbf\u2000-\u206f \t\x0b\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\d+\u2700-\u27bfa-z\xdf-\xf6\xf8-\xffA-Z\xc0-\xd6\xd8-\xde])|$)|[A-Z\xc0-\xd6\xd8-\xde]?(?:[a-z\xdf-\xf6\xf8-\xff]|[^\ud800-\udfff\xac\xb1\xd7\xf7\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\xbf\u2000-\u206f \t\x0b\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\d+\u2700-\u27bfa-z\xdf-\xf6\xf8-\xffA-Z\xc0-\xd6\xd8-\xde])+(?:['’](?:d|ll|m|re|s|t|ve))?|[A-Z\xc0-\xd6\xd8-\xde]+(?:['’](?:D|LL|M|RE|S|T|VE))?|\d*(?:1ST|2ND|3RD|(?![123])\dTH)(?=\b|[a-z_])|\d*(?:1st|2nd|3rd|(?![123])\dth)(?=\b|[A-Z_])|\d+|(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe2f\u20d0-\u20ff]|\ud83c[\udffb-\udfff])?(?:\u200d(?:[^\ud800-\udfff]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])[\ufe0e\ufe0f]?(?:[\u0300-\u036f\ufe20-\ufe2f\u20d0-\u20ff]|\ud83c[\udffb-\udfff])?)*/g;

const camelCase = str =>
(str.match(reWords) || []).reduce(
(acc, next) => acc + (!acc ? next : next[0].toUpperCase() + next.slice(1)),
'',
);

function varsToStyles(props, vars) {
if (!vars || !vars.length) return props.style;
const style = { ...props.style };
vars.forEach(([id, value, unit = '']) => {
const result = typeof value === 'function' ? value(props) : value;
style[`--${id}`] = `${result}${unit}`;
});
return style;
}

function propsToStyles(props, styles, hasModifiers) {
const componentClassName = styles.cls2 || styles.cls1;
let className = props.className
? `${props.className} ${componentClassName}`
: componentClassName;

if (hasModifiers) {
Object.keys(props).forEach(propName => {
const propValue = props[propName];
const typeOf = typeof propValue;

if (typeOf === 'boolean' || propValue == null) {
if (styles[propName]) {
if (propValue) {
className += ` ${styles[propName]}`;
}

delete props[propName];
} else {
const camelPropName = camelCase(propName);

if (styles[camelPropName]) {
if (propValue) {
className += ` ${styles[camelPropName]}`;
}
delete props[propName];
}
}
} else if (typeOf === 'string' || typeOf === 'number') {
const propKey = `${propName}-${propValue}`;

if (styles[propKey]) {
className += ` ${styles[propKey]}`;
delete props[propName];
} else {
const camelPropKey = camelCase(propKey);

if (styles[camelPropKey]) {
className += ` ${styles[camelPropKey]}`;
delete props[propName];
}
}
}
});
}
return className;
}

function styled(type, options, settings) {
if (__DEV__) {
if (Array.isArray(type))
throw new Error(
'This styled() template tag was mistakenly evaluated at runtime. ' +
'Make sure astroturf is properly configured to compile this file',
);
if (typeof settings === 'string')
throw new Error(
'It looks like you have incompatible astroturf versions in your app. ' +
'This runtime expects styles compiled with a newer version of astroturf, ' +
'ensure that your versions are properly deduped and upgraded. ',
);
}
const { displayName, attrs, vars, styles } = settings;

options = options || { allowAs: typeof type === 'string' };

// always passthrough if the type is a styled component
const allowAs = type.isAstroturf ? false : options.allowAs;

const hasModifiers = Object.keys(styles).some(
className => className !== (styles.cls2 || styles.cls1),
);

function Styled(rawProps, ref) {
const props = attrs ? attrs(rawProps) : rawProps;
const childProps = { ...props, ref };

if (allowAs) delete childProps.as;
childProps.style = varsToStyles(childProps, vars);
childProps.className = propsToStyles(childProps, styles, hasModifiers);

return React.createElement(
allowAs && props.as ? props.as : type,
childProps,
);
}

const decorated = React.forwardRef
? React.forwardRef(Styled)
: props => Styled(props, null);

decorated.displayName = displayName;

decorated.withComponent = nextType => styled(nextType, options, settings);

decorated.isAstroturf = true;

return decorated;
}

function jsx(type, props, ...children) {
if (props && props.css) {
const { css, ...childProps } = props;
childProps.style = varsToStyles(childProps, css[1]);
childProps.className = propsToStyles(childProps, css[0] || css, true);
props = childProps;
}
return React.createElement(type, props, ...children);
}

module.exports = styled;
module.exports.styled = styled;
module.exports.jsx = jsx;
module.exports.F = React.Fragment;

if (__DEV__) {
module.exports.css = () => {
throw new Error(
'css template literal evaluated at runtime. ' +
'Make sure astroturf is properly configured to compile this file',
);
};
}


/***/ }),

/***/ "./test/integration/issue-365-BlockStyled.css":
/*!****************************************************!*\
!*** ./test/integration/issue-365-BlockStyled.css ***!
\****************************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

// extracted by mini-css-extract-plugin
module.exports = {"cls1":"issue-365-BlockStyled__cls1","active":"issue-365-BlockStyled__active issue-365-mixins__show","cls2":"issue-365-BlockStyled__cls2 issue-365-BlockStyled__cls1"};

/***/ }),

/***/ "./test/integration/issue-365-mixins.css":
/*!***********************************************!*\
!*** ./test/integration/issue-365-mixins.css ***!
\***********************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

// extracted by mini-css-extract-plugin
module.exports = {"show":"issue-365-mixins__show"};

/***/ }),

/***/ "./test/integration/issue-365.js":
/*!***************************************!*\
!*** ./test/integration/issue-365.js ***!
\***************************************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var astroturf__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! astroturf */ "./src/runtime/styled.js");
/* harmony import */ var astroturf__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(astroturf__WEBPACK_IMPORTED_MODULE_0__);


const mixins = __webpack_require__(/*! ./issue-365-mixins.css */ "./test/integration/issue-365-mixins.css");

const Block = React.createElement(BlockStyled, {
active: false
});
const BlockStyled =
/*#__PURE__*/
astroturf__WEBPACK_IMPORTED_MODULE_0___default()("div", null, {
displayName: "BlockStyled",
styles: __webpack_require__(/*! ./issue-365-BlockStyled.css */ "./test/integration/issue-365-BlockStyled.css"),
attrs: null,
vars: []
});

/***/ })

},[["./test/integration/issue-365.js","runtime~main","vendors~main~vendor"]]]);
12 changes: 12 additions & 0 deletions test/__file_snapshots__/issue-365-styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

.issue-365-mixins__show {
display: block;
}

.issue-365-BlockStyled__cls1 { /*!*/ }
.issue-365-BlockStyled__active {
}
.issue-365-BlockStyled__cls2 {

color: red
}
5 changes: 4 additions & 1 deletion test/__file_snapshots__/loader__css-prop-CssProp1_button.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.cls1 {
.cls1 { /*!*/ }
.cls2 {
composes: cls1;

color: blue;
}
5 changes: 4 additions & 1 deletion test/__file_snapshots__/loader__css-prop-CssProp2_button.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.cls1 {
.cls1 { /*!*/ }
.cls2 {
composes: cls1;

color: violet;
}
5 changes: 4 additions & 1 deletion test/__file_snapshots__/loader__css-prop-CssProp3_button.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.cls1 {
.cls1 { /*!*/ }
.cls2 {
composes: cls1;

color: orange;
}
5 changes: 4 additions & 1 deletion test/__file_snapshots__/loader__css-prop-CssProp4_button.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.cls1 {
.cls1 { /*!*/ }
.cls2 {
composes: cls1;

color: var(--a1qka8js);
}
5 changes: 4 additions & 1 deletion test/__file_snapshots__/loader__css-prop-CssProp5_span.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.cls1 {
.cls1 { /*!*/ }
.cls2 {
composes: cls1;

width: 3rem;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
.cls1 {
.cls1 { /*!*/ }
.cls2 {
composes: cls1;

color: red;
width: 75px;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
.cls1 {
.cls1 { /*!*/ }
.cls2 {
composes: cls1;

color: red;
width: 75px;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
.cls1 {
.cls1 { /*!*/ }
.cls2 {
composes: cls1;

color: red;
width: 75px;

Expand Down
5 changes: 4 additions & 1 deletion test/__file_snapshots__/loader__pass-options-FancierBox.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.cls1 {
.cls1 { /*!*/ }
.cls2 {
composes: cls1;

color: ultra-red;
}
5 changes: 4 additions & 1 deletion test/__file_snapshots__/loader__pass-options-FancyBox.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
.cls1 {
.cls1 { /*!*/ }
.cls2 {
composes: cls1;

color: red;
width: 75px;

Expand Down
Loading

0 comments on commit afdb371

Please sign in to comment.