From e70ed4fef1253938d9ce1bae3771a23715306174 Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Thu, 27 Jul 2017 21:11:24 +1000 Subject: [PATCH 1/9] Use postcss-nested and postcss-safe-parser instead of styled-components fork --- package.json | 3 ++- src/parser.js | 10 ++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index a0190a0e8..2eb093567 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "fbjs": "^0.8.12", "inline-style-prefixer": "^3.0.6", "postcss-js": "^1.0.0", - "styled-components": "2.0.0", + "postcss-nested": "^2.1.0", + "postcss-safe-parser": "^3.0.1", "theming": "^1.0.1", "touch": "^1.0.0" }, diff --git a/src/parser.js b/src/parser.js index c29135c5d..1d6098a3e 100644 --- a/src/parser.js +++ b/src/parser.js @@ -1,14 +1,14 @@ // @flow -import parse from 'styled-components/lib/vendor/postcss-safe-parser/parse' -import postcssNested from 'styled-components/lib/vendor/postcss-nested' -import stringify from 'styled-components/lib/vendor/postcss/stringify' +import parse from 'postcss-safe-parser' +import postcssNested from 'postcss-nested' +import stringify from 'postcss/lib/stringify' import postcssJs from 'postcss-js' import objParse from 'postcss-js/parser' import autoprefixer from 'autoprefixer' import { processStyleName } from './glamor/CSSPropertyOperations' import { objStyle } from './index' -const prefixer = postcssJs.sync([autoprefixer]) +const prefixer = postcssJs.sync([autoprefixer, postcssNested]) type Rule = { parent: { selector: string, nodes: Array }, @@ -41,8 +41,6 @@ export function parseCSS ( let vars = 0 let composes: number = 0 - postcssNested(root) - root.walkDecls((decl: Decl): void => { if (decl.prop === 'composes') { if (!/xxx(\d+)xxx/gm.exec(decl.value)) { From 47e391e4e9808775b263ffaf2481fed6313ba381 Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Thu, 27 Jul 2017 22:15:13 +1000 Subject: [PATCH 2/9] Pass filename to postcss --- src/babel.js | 24 +- src/obj-parse.js | 64 +++ src/parser.js | 9 +- test/browserlist/.browserslistrc | 1 + .../__snapshots__/babel.test.js.snap | 10 + .../__snapshots__/react.test.js.snap | 453 +++++++++++++++++ test/browserlist/babel.test.js | 18 + test/browserlist/react.test.js | 456 ++++++++++++++++++ 8 files changed, 1021 insertions(+), 14 deletions(-) create mode 100644 src/obj-parse.js create mode 100644 test/browserlist/.browserslistrc create mode 100644 test/browserlist/__snapshots__/babel.test.js.snap create mode 100644 test/browserlist/__snapshots__/react.test.js.snap create mode 100644 test/browserlist/babel.test.js create mode 100644 test/browserlist/react.test.js diff --git a/src/babel.js b/src/babel.js index 948992822..ce3d20193 100644 --- a/src/babel.js +++ b/src/babel.js @@ -9,6 +9,10 @@ import { map } from './utils' import cssProps from './css-prop' import ASTObject from './ast-object' +function getFilename (path) { + return path.hub.file.opts.filename === 'unknown' ? '' : path.hub.file.opts.filename +} + export function replaceCssWithCallExpression ( path, identifier, @@ -25,7 +29,7 @@ export function replaceCssWithCallExpression ( ) if (state.extractStatic && !path.node.quasi.expressions.length) { const cssText = staticCSSTextCreator(name, hash, src) - const { staticCSSRules } = parseCSS(cssText, true) + const { staticCSSRules } = parseCSS(cssText, true, getFilename(path)) state.insertStaticRules(staticCSSRules) if (!removePath) { @@ -39,7 +43,7 @@ export function replaceCssWithCallExpression ( return } - const { styles, composesCount } = parseCSS(src, false) + const { styles, composesCount } = parseCSS(src, false, getFilename(path)) if (!removePath) { path.addComment('leading', '#__PURE__') @@ -84,7 +88,7 @@ export function buildStyledCallExpression (identifier, tag, path, state, t) { ) const cssText = `.${name}-${hash} { ${src} }` - const { staticCSSRules } = parseCSS(cssText, true) + const { staticCSSRules } = parseCSS(cssText, true, getFilename(path)) state.insertStaticRules(staticCSSRules) return t.callExpression(identifier, [ @@ -97,7 +101,7 @@ export function buildStyledCallExpression (identifier, tag, path, state, t) { path.addComment('leading', '#__PURE__') - const { styles, composesCount } = parseCSS(src, false) + const { styles, composesCount } = parseCSS(src, false, getFilename(path)) const objs = path.node.quasi.expressions.slice(0, composesCount) const vars = path.node.quasi.expressions.slice(composesCount) @@ -127,24 +131,24 @@ export function buildStyledObjectCallExpression (path, identifier, t) { : t.stringLiteral(path.node.callee.property.name) return t.callExpression(identifier, [ tag, - t.arrayExpression(buildProcessedStylesFromObjectAST(path.node.arguments, t)) + t.arrayExpression(buildProcessedStylesFromObjectAST(path.node.arguments, path, t)) ]) } -function buildProcessedStylesFromObjectAST (objectAST, t) { +function buildProcessedStylesFromObjectAST (objectAST, path, t) { if (t.isObjectExpression(objectAST)) { const astObject = ASTObject.fromAST(objectAST, t) - const { styles } = parseCSS(astObject.obj, false) + const { styles } = parseCSS(astObject.obj, false, getFilename(path)) astObject.obj = styles return astObject.toAST() } if (t.isArrayExpression(objectAST)) { return t.arrayExpression( - buildProcessedStylesFromObjectAST(objectAST.elements, t) + buildProcessedStylesFromObjectAST(objectAST.elements, path, t) ) } if (Array.isArray(objectAST)) { - return map(objectAST, obj => buildProcessedStylesFromObjectAST(obj, t)) + return map(objectAST, obj => buildProcessedStylesFromObjectAST(obj, path, t)) } return objectAST @@ -152,7 +156,7 @@ function buildProcessedStylesFromObjectAST (objectAST, t) { export function replaceCssObjectCallExpression (path, identifier, t) { const argWithStyles = path.node.arguments[0] - const styles = buildProcessedStylesFromObjectAST(argWithStyles, t) + const styles = buildProcessedStylesFromObjectAST(argWithStyles, path, t) path.replaceWith(t.callExpression(identifier, [styles])) } diff --git a/src/obj-parse.js b/src/obj-parse.js new file mode 100644 index 000000000..4d35c6d0b --- /dev/null +++ b/src/obj-parse.js @@ -0,0 +1,64 @@ +import postcss from 'postcss' +import isUnitlessNumber from './glamor/CSSPropertyOperations/CSSProperty' +import { processStyleName } from './glamor/CSSPropertyOperations' + +function decl (parent, name, value) { + if (value === false || value === null) return + + name = processStyleName(name) + if (typeof value === 'number') { + if (value === 0 || isUnitlessNumber[name] === 1) { + value = value.toString() + } else { + value += 'px' + } + } + + if (name === 'css-float') name = 'float' + + parent.push(postcss.decl({ prop: name, value: value })) +} + +function atRule (parent, parts, value) { + var node = postcss.atRule({ name: parts[1], params: parts[3] || '' }) + if (typeof value === 'object') { + node.nodes = [] + parse(value, node) + } + parent.push(node) +} + +function parse (obj, parent) { + var name, value, node, i + for (name in obj) { + if (obj.hasOwnProperty(name)) { + value = obj[name] + if (name[0] === '@') { + var parts = name.match(/@([^\s]+)(\s+([\w\W]*)\s*)?/) + if (Array.isArray(value)) { + for (i = 0; i < value.length; i++) { + atRule(parent, parts, value[i]) + } + } else { + atRule(parent, parts, value) + } + } else if (Array.isArray(value)) { + for (i = 0; i < value.length; i++) { + decl(parent, name, value[i]) + } + } else if (typeof value === 'object' && value !== null) { + node = postcss.rule({ selector: name }) + parse(value, node) + parent.push(node) + } else { + decl(parent, name, value) + } + } + } +} + +export default function (obj, opts) { + var root = postcss.root(opts) + parse(obj, root) + return root +} diff --git a/src/parser.js b/src/parser.js index 1d6098a3e..0bba7dbb6 100644 --- a/src/parser.js +++ b/src/parser.js @@ -3,7 +3,7 @@ import parse from 'postcss-safe-parser' import postcssNested from 'postcss-nested' import stringify from 'postcss/lib/stringify' import postcssJs from 'postcss-js' -import objParse from 'postcss-js/parser' +import objParse from './obj-parse' import autoprefixer from 'autoprefixer' import { processStyleName } from './glamor/CSSPropertyOperations' import { objStyle } from './index' @@ -25,7 +25,8 @@ type Decl = { export function parseCSS ( css: string, - extractStatic: boolean + extractStatic: boolean, + filename: string ): { staticCSSRules: Array, styles: { [string]: any }, @@ -34,9 +35,9 @@ export function parseCSS ( // todo - handle errors let root if (typeof css === 'object') { - root = objParse(css) + root = objParse(css, { from: filename }) } else { - root = parse(css) + root = parse(css, { from: filename }) } let vars = 0 let composes: number = 0 diff --git a/test/browserlist/.browserslistrc b/test/browserlist/.browserslistrc new file mode 100644 index 000000000..c4ac9625a --- /dev/null +++ b/test/browserlist/.browserslistrc @@ -0,0 +1 @@ +chrome 59 \ No newline at end of file diff --git a/test/browserlist/__snapshots__/babel.test.js.snap b/test/browserlist/__snapshots__/babel.test.js.snap new file mode 100644 index 000000000..ed748bd03 --- /dev/null +++ b/test/browserlist/__snapshots__/babel.test.js.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`babel css inline css basic 1`] = ` +" +/*#__PURE__*/css([], [], function createEmotionStyledRules() { + return [{ + \\"display\\": \\"-webkit-box; display: -ms-flexbox; display: flex\\" + }]; +});" +`; diff --git a/test/browserlist/__snapshots__/react.test.js.snap b/test/browserlist/__snapshots__/react.test.js.snap new file mode 100644 index 000000000..33772bb40 --- /dev/null +++ b/test/browserlist/__snapshots__/react.test.js.snap @@ -0,0 +1,453 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`styled basic render 1`] = ` +.glamor-0 { + color: blue; + font-size: 20px; +} + +

+ hello world +

+`; + +exports[`styled basic render with object as style 1`] = ` +.glamor-0 { + font-size: 20px; +} + +

+ hello world +

+`; + +exports[`styled call expression 1`] = ` +.glamor-0 { + font-size: 20px; +} + +

+ hello world +

+`; + +exports[`styled change theme 1`] = ` +.glamor-0 { + color: green; +} + + + + +
+ this will be green then pink +
+
+
+
+`; + +exports[`styled change theme 2`] = ` +.glamor-0 { + color: pink; +} + + + + +
+ this will be green then pink +
+
+
+
+`; + +exports[`styled change theme 3`] = ` + + + +`; + +exports[`styled component as selector 1`] = ` +.glamor-0 { + font-size: 20px; +} + +.glamor-1 { + display: -webkit-box; + display: -ms-flexbox; + display: flex; +} + +
+ hello +

+ This will be green +

+ world +
+`; + +exports[`styled composes 1`] = ` +.glamor-0 { + color: blue; + font-size: 32px; +} + +

+ hello world +

+`; + +exports[`styled composes based on props 1`] = ` +.glamor-0 { + color: blue; +} + +

+ hello world +

+`; + +exports[`styled composes based on props 2`] = ` +.glamor-0 { + color: green; +} + +

+ hello world +

+`; + +exports[`styled composes with objects 1`] = ` +.glamor-0 { + color: #333; + font-size: 32px; + height: 64px; +} + +@media only screen and (-webkit-min-device-pixel-ratio: 1.5), + only screen and (min--moz-device-pixel-ratio: 1.5), + only screen and (-o-min-device-pixel-ratio: 1.5/1), + only screen and (min-resolution: 144dpi), + only screen and (min-resolution: 1.5dppx) { + .glamor-0 { + font-size: 1.4323121856191332em; + } +} + +

+ hello world +

+`; + +exports[`styled composition 1`] = ` +.glamor-0 { + font-size: 13.333333333333334px; +} + +

+ hello world +

+`; + +exports[`styled function in expression 1`] = ` +.glamor-0 { + font-size: 40px; +} + +

+ hello world +

+`; + +exports[`styled handles more than 10 dynamic properties 1`] = ` +.glamor-0 { + -webkit-text-decoration: underline; + text-decoration: underline; + border-right: solid blue 54px; + background: white; + color: black; + display: block; + border-radius: 3px; + padding: 25px; + width: 500px; + z-index: 100; + font-size: 18px; + text-align: center; + border-left: undefined; +} + +

+ hello world +

+`; + +exports[`styled higher order component 1`] = ` +.glamor-0 { + font-size: 20px; + name: onyx; + background-color: '#343a40'; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; +} + +
+`; + +exports[`styled innerRef 1`] = ` +.glamor-0 { + font-size: 12px; +} + +

+ hello world +

+`; + +exports[`styled input placeholder 1`] = ` +.glamor-0::-webkit-input-placeholder { + background-color: green; +} + +.glamor-0:-ms-input-placeholder { + background-color: green; +} + +.glamor-0::placeholder { + background-color: green; +} + + + hello world + +`; + +exports[`styled input placeholder object 1`] = ` +.glamor-0::-webkit-input-placeholder { + background-color: green; +} + +.glamor-0:-ms-input-placeholder { + background-color: green; +} + +.glamor-0::placeholder { + background-color: green; +} + + + hello world + +`; + +exports[`styled name 1`] = ` +.glamor-0 { + name: FancyH1; + font-size: 20px; +} + +

+ hello world +

+`; + +exports[`styled nested 1`] = ` +.glamor-0 { + font-size: 20px; +} + +.glamor-1 { + display: -webkit-box; + display: -ms-flexbox; + display: flex; +} + +.glamor-1 div { + color: green; +} + +.glamor-1 div span { + color: red; +} + +
+ hello +

+ This will be green +

+ world +
+`; + +exports[`styled no dynamic 1`] = ` +.glamor-0 { + font-size: 12px; +} + +

+ hello world +

+`; + +exports[`styled object composition 1`] = ` +.glamor-0 { + border-radius: 50%; + -webkit-transition: -webkit-transform 400ms ease-in-out; + transition: -webkit-transform 400ms ease-in-out; + transition: transform 400ms ease-in-out; + transition: transform 400ms ease-in-out, -webkit-transform 400ms ease-in-out; + border: 3px solid currentColor; + width: 96px; + height: 96px; + color: blue; +} + +.glamor-0:hover { + -webkit-transform: scale(1.2); + transform: scale(1.2); +} + + +`; + +exports[`styled objects 1`] = ` +.glamor-0 { + padding: 10px; + display: flex; +} + +

+ hello world +

+`; + +exports[`styled prefixing 1`] = ` +.glamor-0 { + display: -webkit-box; + display: -ms-flexbox; + display: flex; +} + +
+ hello world +
+`; + +exports[`styled themes 1`] = ` +.glamor-0 { + background-color: #ffd43b; + color: #8c81d8; + height: 64px; + font-size: 32px; +} + + + hello world + +`; + +exports[`styled throws if undefined is passed as the component 1`] = ` +"You are trying to create a styled element with an undefined component. +You may have forgotten to import it." +`; diff --git a/test/browserlist/babel.test.js b/test/browserlist/babel.test.js new file mode 100644 index 000000000..a307ce19f --- /dev/null +++ b/test/browserlist/babel.test.js @@ -0,0 +1,18 @@ +/* eslint-env jest */ +import * as babel from 'babel-core' +import plugin from '../../src/babel' + +describe('babel css', () => { + describe('inline', () => { + test('css basic', () => { + const basic = ` + css\` + display: flex; + \`` + const { code } = babel.transform(basic, { + plugins: [[plugin]] + }) + expect(code).toMatchSnapshot() + }) + }) +}) diff --git a/test/browserlist/react.test.js b/test/browserlist/react.test.js new file mode 100644 index 000000000..7378ee15b --- /dev/null +++ b/test/browserlist/react.test.js @@ -0,0 +1,456 @@ +/* eslint-env jest */ +import React from 'react' +import renderer from 'react-test-renderer' +import serializer from 'jest-glamor-react' +import { css, sheet } from '../../src/index' +import styled from '../../src/react' +import { ThemeProvider } from '../../src/react/theming' +import { mount } from 'enzyme' +import enzymeToJson from 'enzyme-to-json' + +import { lighten, hiDPI, modularScale } from 'polished' + +expect.addSnapshotSerializer(serializer(sheet)) + +describe('styled', () => { + test('no dynamic', () => { + const H1 = styled.h1`font-size: 12px;` + + const tree = renderer.create(

hello world

).toJSON() + + expect(tree).toMatchSnapshot() + }) + + test('basic render', () => { + const fontSize = 20 + const H1 = styled.h1` + color: blue; + font-size: ${fontSize}; + ` + + const tree = renderer.create(

hello world

).toJSON() + + expect(tree).toMatchSnapshot() + }) + + test('basic render with object as style', () => { + const fontSize = 20 + const H1 = styled.h1({ fontSize }) + + const tree = renderer.create(

hello world

).toJSON() + + expect(tree).toMatchSnapshot() + }) + + test('name', () => { + const fontSize = 20 + const H1 = styled.h1` + name: FancyH1; + font-size: ${fontSize}px; + ` + + const tree = renderer.create(

hello world

).toJSON() + + expect(tree).toMatchSnapshot() + }) + + test('call expression', () => { + const fontSize = 20 + const H1 = styled('h1')` + font-size: ${fontSize}px; + ` + + const tree = renderer + .create(

hello world

) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + + test('nested', () => { + const fontSize = '20px' + const H1 = styled.h1`font-size: ${fontSize};` + + const Thing = styled.div` + display: flex; + & div { + color: green; + + & span { + color: red; + } + } + ` + + const tree = renderer + .create( + + hello

This will be green

world +
+ ) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + + test('composition', () => { + const fontSize = 20 + const H1 = styled('h1')` + font-size: ${fontSize + 'px'}; + ` + + const H2 = styled(H1)`font-size: ${fontSize * 2 / 3}` + + const tree = renderer + .create(

hello world

) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + + test('input placeholder', () => { + const Input = styled.input` + ::placeholder { + background-color: green; + } + ` + const tree = renderer + .create(hello world) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + + test('input placeholder object', () => { + const Input = styled('input')({ + '::placeholder': { + backgroundColor: 'green' + } + }) + + const tree = renderer + .create(hello world) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + + test('object composition', () => { + const imageStyles = css({ + width: 96, + height: 96 + }) + + const fakeBlue = css([ + { + color: 'blue' + } + ]) + + const red = css([ + { + color: 'red' + } + ]) + + const blue = css([ + red, + { + color: 'blue' + } + ]) + + const prettyStyles = css([ + { + borderRadius: '50%', + transition: 'transform 400ms ease-in-out', + ':hover': { + transform: 'scale(1.2)' + } + }, + { border: '3px solid currentColor' } + ]) + + const Avatar = styled('img')` + composes: ${prettyStyles} ${imageStyles} ${blue} + ` + + const tree = renderer.create().toJSON() + + expect(tree).toMatchSnapshot() + }) + + test('handles more than 10 dynamic properties', () => { + const H1 = styled('h1')` + text-decoration: ${'underline'}; + border-right: solid blue 54px; + background: ${'white'}; + color: ${'black'}; + display: ${'block'}; + border-radius: ${'3px'}; + padding: ${'25px'}; + width: ${'500px'}; + z-index: ${100}; + font-size: ${'18px'}; + text-align: ${'center'}; + border-left: ${p => p.theme.blue}; + ` + + const tree = renderer + .create( +

+ hello world +

+ ) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + + test('component as selector', () => { + const fontSize = '20px' + const H1 = styled.h1`font-size: ${fontSize};` + + const Thing = styled.div` + display: flex; + .${H1} { + color: green; + } + ` + + const tree = renderer + .create( + + hello

This will be green

world +
+ ) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + + test('function in expression', () => { + const fontSize = 20 + const H1 = styled('h1')` + font-size: ${fontSize + 'px'}; + ` + + const H2 = styled(H1)`font-size: ${({ scale }) => fontSize * scale + 'px'}` + + const tree = renderer + .create( +

+ hello world +

+ ) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + + test('composes', () => { + const fontSize = '20px' + + const cssA = css` + color: blue; + ` + + const cssB = css` + composes: ${cssA} + color: red; + ` + + const BlueH1 = styled('h1')` + composes: ${cssB}; + color: blue; + font-size: ${fontSize}; + ` + + const FinalH2 = styled(BlueH1)`font-size:32px;` + + const tree = renderer + .create( + + hello world + + ) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + + test('composes with objects', () => { + const cssA = { + color: lighten(0.2, '#000'), + fontSize: modularScale(1), + [hiDPI(1.5).replace('\n', ' ').trim()]: { + fontSize: modularScale(1.25) + } + } + + const cssB = css` + composes: ${cssA} + height: 64px; + ` + + const H1 = styled('h1')` + composes: ${cssB} + font-size: ${modularScale(4)}; + ` + + const H2 = styled(H1)`font-size:32px;` + + const tree = renderer + .create( +

+ hello world +

+ ) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + + test('innerRef', () => { + const H1 = styled.h1`font-size: 12px;` + + const refFunction = jest.fn() + + const tree = renderer + .create(

hello world

) + .toJSON() + + expect(tree).toMatchSnapshot() + expect(refFunction).toBeCalled() + }) + + test('themes', () => { + const theme = { + white: '#f8f9fa', + purple: '#8c81d8', + gold: '#ffd43b' + } + + const fontSize = '20px' + + const cssA = css` + color: blue; + ` + + const cssB = css` + composes: ${cssA} + height: 64px; + ` + + const Heading = styled('span')` + background-color: ${p => p.theme.gold}; + ` + + const H1 = styled(Heading)` + composes: ${cssB} + font-size: ${fontSize}; + color: ${p => p.theme.purple} + ` + + const H2 = styled(H1)`font-size:32px;` + const refFunction = jest.fn() + const tree = renderer + .create( + +

+ hello world +

+
+ ) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + + test('higher order component', () => { + const fontSize = 20 + const Content = styled('div')` + font-size: ${fontSize}px; + ` + + const squirtleBlueBackground = css` + name: squirtle-blue-bg; + background-color: #7FC8D6; + ` + + const flexColumn = Component => { + const NewComponent = styled(Component)` + composes: ${squirtleBlueBackground} + name: onyx; + background-color: '#343a40'; + flex-direction: column; + ` + + return NewComponent + } + + const ColumnContent = flexColumn(Content) + + // expect(ColumnContent.displayName).toMatchSnapshotWithGlamor() + + const tree = renderer.create().toJSON() + + expect(tree).toMatchSnapshot() + }) + + test('composes based on props', () => { + const cssA = css` + color: blue; + ` + + const cssB = css` + color: green; + ` + + const H1 = styled('h1')` + composes: ${props => (props.a ? cssA : cssB)} + ` + + const tree = renderer.create(

hello world

).toJSON() + + expect(tree).toMatchSnapshot() + const tree2 = renderer.create(

hello world

).toJSON() + + expect(tree2).toMatchSnapshot() + }) + + test('objects', () => { + const H1 = styled('h1')('some-class', { padding: 10 }, props => ({ + display: props.display + })) + const tree = renderer.create(

hello world

).toJSON() + + expect(tree).toMatchSnapshot() + }) + + test.only('prefixing', () => { + const Div = styled.div` + display: flex; + ` + const tree = renderer.create(
hello world
).toJSON() + + expect(tree).toMatchSnapshot() + }) + test('change theme', () => { + const Div = styled.div` + color: ${props => props.theme.primary} + ` + const TestComponent = (props) => ({props.renderChild ?
this will be green then pink
: null}
) + const wrapper = mount() + expect(enzymeToJson(wrapper)).toMatchSnapshot() + wrapper.setProps({ theme: { primary: 'pink' } }) + expect(enzymeToJson(wrapper)).toMatchSnapshot() + wrapper.setProps({ renderChild: false }) + expect(enzymeToJson(wrapper)).toMatchSnapshot() + }) + test('throws if undefined is passed as the component', () => { + expect( + () => styled(undefined)`display: flex;` + ).toThrowErrorMatchingSnapshot() + }) +}) From 02e1bc01539334119baec6bb2ff7d209a5a596f0 Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Thu, 27 Jul 2017 22:26:55 +1000 Subject: [PATCH 3/9] Change tests --- .../__snapshots__/css.test.js.snap | 29 ++ test/browserlist/css.test.js | 27 ++ test/browserlist/react.test.js | 456 ------------------ 3 files changed, 56 insertions(+), 456 deletions(-) create mode 100644 test/browserlist/__snapshots__/css.test.js.snap create mode 100644 test/browserlist/css.test.js delete mode 100644 test/browserlist/react.test.js diff --git a/test/browserlist/__snapshots__/css.test.js.snap b/test/browserlist/__snapshots__/css.test.js.snap new file mode 100644 index 000000000..9442f6438 --- /dev/null +++ b/test/browserlist/__snapshots__/css.test.js.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`prefixing css 1`] = ` +.glamor-0 { + display: -webkit-box; + display: -ms-flexbox; + display: flex; +} + +
+ hello world +
+`; + +exports[`prefixing styled 1`] = ` +.glamor-0 { + display: -webkit-box; + display: -ms-flexbox; + display: flex; +} + +
+ hello world +
+`; diff --git a/test/browserlist/css.test.js b/test/browserlist/css.test.js new file mode 100644 index 000000000..5f2c2673f --- /dev/null +++ b/test/browserlist/css.test.js @@ -0,0 +1,27 @@ +/* eslint-env jest */ +import React from 'react' +import renderer from 'react-test-renderer' +import serializer from 'jest-glamor-react' +import { css, sheet } from '../../src/index' +import styled from '../../src/react' + +expect.addSnapshotSerializer(serializer(sheet)) + +describe('prefixing', () => { + test('styled', () => { + const Div = styled.div` + display: flex; + ` + const tree = renderer.create(
hello world
).toJSON() + + expect(tree).toMatchSnapshot() + }) + test('css', () => { + const cls1 = css` + display: flex; + ` + const tree = renderer.create(
hello world
).toJSON() + + expect(tree).toMatchSnapshot() + }) +}) diff --git a/test/browserlist/react.test.js b/test/browserlist/react.test.js deleted file mode 100644 index 7378ee15b..000000000 --- a/test/browserlist/react.test.js +++ /dev/null @@ -1,456 +0,0 @@ -/* eslint-env jest */ -import React from 'react' -import renderer from 'react-test-renderer' -import serializer from 'jest-glamor-react' -import { css, sheet } from '../../src/index' -import styled from '../../src/react' -import { ThemeProvider } from '../../src/react/theming' -import { mount } from 'enzyme' -import enzymeToJson from 'enzyme-to-json' - -import { lighten, hiDPI, modularScale } from 'polished' - -expect.addSnapshotSerializer(serializer(sheet)) - -describe('styled', () => { - test('no dynamic', () => { - const H1 = styled.h1`font-size: 12px;` - - const tree = renderer.create(

hello world

).toJSON() - - expect(tree).toMatchSnapshot() - }) - - test('basic render', () => { - const fontSize = 20 - const H1 = styled.h1` - color: blue; - font-size: ${fontSize}; - ` - - const tree = renderer.create(

hello world

).toJSON() - - expect(tree).toMatchSnapshot() - }) - - test('basic render with object as style', () => { - const fontSize = 20 - const H1 = styled.h1({ fontSize }) - - const tree = renderer.create(

hello world

).toJSON() - - expect(tree).toMatchSnapshot() - }) - - test('name', () => { - const fontSize = 20 - const H1 = styled.h1` - name: FancyH1; - font-size: ${fontSize}px; - ` - - const tree = renderer.create(

hello world

).toJSON() - - expect(tree).toMatchSnapshot() - }) - - test('call expression', () => { - const fontSize = 20 - const H1 = styled('h1')` - font-size: ${fontSize}px; - ` - - const tree = renderer - .create(

hello world

) - .toJSON() - - expect(tree).toMatchSnapshot() - }) - - test('nested', () => { - const fontSize = '20px' - const H1 = styled.h1`font-size: ${fontSize};` - - const Thing = styled.div` - display: flex; - & div { - color: green; - - & span { - color: red; - } - } - ` - - const tree = renderer - .create( - - hello

This will be green

world -
- ) - .toJSON() - - expect(tree).toMatchSnapshot() - }) - - test('composition', () => { - const fontSize = 20 - const H1 = styled('h1')` - font-size: ${fontSize + 'px'}; - ` - - const H2 = styled(H1)`font-size: ${fontSize * 2 / 3}` - - const tree = renderer - .create(

hello world

) - .toJSON() - - expect(tree).toMatchSnapshot() - }) - - test('input placeholder', () => { - const Input = styled.input` - ::placeholder { - background-color: green; - } - ` - const tree = renderer - .create(hello world) - .toJSON() - - expect(tree).toMatchSnapshot() - }) - - test('input placeholder object', () => { - const Input = styled('input')({ - '::placeholder': { - backgroundColor: 'green' - } - }) - - const tree = renderer - .create(hello world) - .toJSON() - - expect(tree).toMatchSnapshot() - }) - - test('object composition', () => { - const imageStyles = css({ - width: 96, - height: 96 - }) - - const fakeBlue = css([ - { - color: 'blue' - } - ]) - - const red = css([ - { - color: 'red' - } - ]) - - const blue = css([ - red, - { - color: 'blue' - } - ]) - - const prettyStyles = css([ - { - borderRadius: '50%', - transition: 'transform 400ms ease-in-out', - ':hover': { - transform: 'scale(1.2)' - } - }, - { border: '3px solid currentColor' } - ]) - - const Avatar = styled('img')` - composes: ${prettyStyles} ${imageStyles} ${blue} - ` - - const tree = renderer.create().toJSON() - - expect(tree).toMatchSnapshot() - }) - - test('handles more than 10 dynamic properties', () => { - const H1 = styled('h1')` - text-decoration: ${'underline'}; - border-right: solid blue 54px; - background: ${'white'}; - color: ${'black'}; - display: ${'block'}; - border-radius: ${'3px'}; - padding: ${'25px'}; - width: ${'500px'}; - z-index: ${100}; - font-size: ${'18px'}; - text-align: ${'center'}; - border-left: ${p => p.theme.blue}; - ` - - const tree = renderer - .create( -

- hello world -

- ) - .toJSON() - - expect(tree).toMatchSnapshot() - }) - - test('component as selector', () => { - const fontSize = '20px' - const H1 = styled.h1`font-size: ${fontSize};` - - const Thing = styled.div` - display: flex; - .${H1} { - color: green; - } - ` - - const tree = renderer - .create( - - hello

This will be green

world -
- ) - .toJSON() - - expect(tree).toMatchSnapshot() - }) - - test('function in expression', () => { - const fontSize = 20 - const H1 = styled('h1')` - font-size: ${fontSize + 'px'}; - ` - - const H2 = styled(H1)`font-size: ${({ scale }) => fontSize * scale + 'px'}` - - const tree = renderer - .create( -

- hello world -

- ) - .toJSON() - - expect(tree).toMatchSnapshot() - }) - - test('composes', () => { - const fontSize = '20px' - - const cssA = css` - color: blue; - ` - - const cssB = css` - composes: ${cssA} - color: red; - ` - - const BlueH1 = styled('h1')` - composes: ${cssB}; - color: blue; - font-size: ${fontSize}; - ` - - const FinalH2 = styled(BlueH1)`font-size:32px;` - - const tree = renderer - .create( - - hello world - - ) - .toJSON() - - expect(tree).toMatchSnapshot() - }) - - test('composes with objects', () => { - const cssA = { - color: lighten(0.2, '#000'), - fontSize: modularScale(1), - [hiDPI(1.5).replace('\n', ' ').trim()]: { - fontSize: modularScale(1.25) - } - } - - const cssB = css` - composes: ${cssA} - height: 64px; - ` - - const H1 = styled('h1')` - composes: ${cssB} - font-size: ${modularScale(4)}; - ` - - const H2 = styled(H1)`font-size:32px;` - - const tree = renderer - .create( -

- hello world -

- ) - .toJSON() - - expect(tree).toMatchSnapshot() - }) - - test('innerRef', () => { - const H1 = styled.h1`font-size: 12px;` - - const refFunction = jest.fn() - - const tree = renderer - .create(

hello world

) - .toJSON() - - expect(tree).toMatchSnapshot() - expect(refFunction).toBeCalled() - }) - - test('themes', () => { - const theme = { - white: '#f8f9fa', - purple: '#8c81d8', - gold: '#ffd43b' - } - - const fontSize = '20px' - - const cssA = css` - color: blue; - ` - - const cssB = css` - composes: ${cssA} - height: 64px; - ` - - const Heading = styled('span')` - background-color: ${p => p.theme.gold}; - ` - - const H1 = styled(Heading)` - composes: ${cssB} - font-size: ${fontSize}; - color: ${p => p.theme.purple} - ` - - const H2 = styled(H1)`font-size:32px;` - const refFunction = jest.fn() - const tree = renderer - .create( - -

- hello world -

-
- ) - .toJSON() - - expect(tree).toMatchSnapshot() - }) - - test('higher order component', () => { - const fontSize = 20 - const Content = styled('div')` - font-size: ${fontSize}px; - ` - - const squirtleBlueBackground = css` - name: squirtle-blue-bg; - background-color: #7FC8D6; - ` - - const flexColumn = Component => { - const NewComponent = styled(Component)` - composes: ${squirtleBlueBackground} - name: onyx; - background-color: '#343a40'; - flex-direction: column; - ` - - return NewComponent - } - - const ColumnContent = flexColumn(Content) - - // expect(ColumnContent.displayName).toMatchSnapshotWithGlamor() - - const tree = renderer.create().toJSON() - - expect(tree).toMatchSnapshot() - }) - - test('composes based on props', () => { - const cssA = css` - color: blue; - ` - - const cssB = css` - color: green; - ` - - const H1 = styled('h1')` - composes: ${props => (props.a ? cssA : cssB)} - ` - - const tree = renderer.create(

hello world

).toJSON() - - expect(tree).toMatchSnapshot() - const tree2 = renderer.create(

hello world

).toJSON() - - expect(tree2).toMatchSnapshot() - }) - - test('objects', () => { - const H1 = styled('h1')('some-class', { padding: 10 }, props => ({ - display: props.display - })) - const tree = renderer.create(

hello world

).toJSON() - - expect(tree).toMatchSnapshot() - }) - - test.only('prefixing', () => { - const Div = styled.div` - display: flex; - ` - const tree = renderer.create(
hello world
).toJSON() - - expect(tree).toMatchSnapshot() - }) - test('change theme', () => { - const Div = styled.div` - color: ${props => props.theme.primary} - ` - const TestComponent = (props) => ({props.renderChild ?
this will be green then pink
: null}
) - const wrapper = mount() - expect(enzymeToJson(wrapper)).toMatchSnapshot() - wrapper.setProps({ theme: { primary: 'pink' } }) - expect(enzymeToJson(wrapper)).toMatchSnapshot() - wrapper.setProps({ renderChild: false }) - expect(enzymeToJson(wrapper)).toMatchSnapshot() - }) - test('throws if undefined is passed as the component', () => { - expect( - () => styled(undefined)`display: flex;` - ).toThrowErrorMatchingSnapshot() - }) -}) From efaa83e955837d2181b9c6769643c3801a4c456c Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Thu, 27 Jul 2017 22:31:34 +1000 Subject: [PATCH 4/9] Update snapshots --- .../__snapshots__/react.test.js.snap | 453 ------------------ 1 file changed, 453 deletions(-) delete mode 100644 test/browserlist/__snapshots__/react.test.js.snap diff --git a/test/browserlist/__snapshots__/react.test.js.snap b/test/browserlist/__snapshots__/react.test.js.snap deleted file mode 100644 index 33772bb40..000000000 --- a/test/browserlist/__snapshots__/react.test.js.snap +++ /dev/null @@ -1,453 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`styled basic render 1`] = ` -.glamor-0 { - color: blue; - font-size: 20px; -} - -

- hello world -

-`; - -exports[`styled basic render with object as style 1`] = ` -.glamor-0 { - font-size: 20px; -} - -

- hello world -

-`; - -exports[`styled call expression 1`] = ` -.glamor-0 { - font-size: 20px; -} - -

- hello world -

-`; - -exports[`styled change theme 1`] = ` -.glamor-0 { - color: green; -} - - - - -
- this will be green then pink -
-
-
-
-`; - -exports[`styled change theme 2`] = ` -.glamor-0 { - color: pink; -} - - - - -
- this will be green then pink -
-
-
-
-`; - -exports[`styled change theme 3`] = ` - - - -`; - -exports[`styled component as selector 1`] = ` -.glamor-0 { - font-size: 20px; -} - -.glamor-1 { - display: -webkit-box; - display: -ms-flexbox; - display: flex; -} - -
- hello -

- This will be green -

- world -
-`; - -exports[`styled composes 1`] = ` -.glamor-0 { - color: blue; - font-size: 32px; -} - -

- hello world -

-`; - -exports[`styled composes based on props 1`] = ` -.glamor-0 { - color: blue; -} - -

- hello world -

-`; - -exports[`styled composes based on props 2`] = ` -.glamor-0 { - color: green; -} - -

- hello world -

-`; - -exports[`styled composes with objects 1`] = ` -.glamor-0 { - color: #333; - font-size: 32px; - height: 64px; -} - -@media only screen and (-webkit-min-device-pixel-ratio: 1.5), - only screen and (min--moz-device-pixel-ratio: 1.5), - only screen and (-o-min-device-pixel-ratio: 1.5/1), - only screen and (min-resolution: 144dpi), - only screen and (min-resolution: 1.5dppx) { - .glamor-0 { - font-size: 1.4323121856191332em; - } -} - -

- hello world -

-`; - -exports[`styled composition 1`] = ` -.glamor-0 { - font-size: 13.333333333333334px; -} - -

- hello world -

-`; - -exports[`styled function in expression 1`] = ` -.glamor-0 { - font-size: 40px; -} - -

- hello world -

-`; - -exports[`styled handles more than 10 dynamic properties 1`] = ` -.glamor-0 { - -webkit-text-decoration: underline; - text-decoration: underline; - border-right: solid blue 54px; - background: white; - color: black; - display: block; - border-radius: 3px; - padding: 25px; - width: 500px; - z-index: 100; - font-size: 18px; - text-align: center; - border-left: undefined; -} - -

- hello world -

-`; - -exports[`styled higher order component 1`] = ` -.glamor-0 { - font-size: 20px; - name: onyx; - background-color: '#343a40'; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; -} - -
-`; - -exports[`styled innerRef 1`] = ` -.glamor-0 { - font-size: 12px; -} - -

- hello world -

-`; - -exports[`styled input placeholder 1`] = ` -.glamor-0::-webkit-input-placeholder { - background-color: green; -} - -.glamor-0:-ms-input-placeholder { - background-color: green; -} - -.glamor-0::placeholder { - background-color: green; -} - - - hello world - -`; - -exports[`styled input placeholder object 1`] = ` -.glamor-0::-webkit-input-placeholder { - background-color: green; -} - -.glamor-0:-ms-input-placeholder { - background-color: green; -} - -.glamor-0::placeholder { - background-color: green; -} - - - hello world - -`; - -exports[`styled name 1`] = ` -.glamor-0 { - name: FancyH1; - font-size: 20px; -} - -

- hello world -

-`; - -exports[`styled nested 1`] = ` -.glamor-0 { - font-size: 20px; -} - -.glamor-1 { - display: -webkit-box; - display: -ms-flexbox; - display: flex; -} - -.glamor-1 div { - color: green; -} - -.glamor-1 div span { - color: red; -} - -
- hello -

- This will be green -

- world -
-`; - -exports[`styled no dynamic 1`] = ` -.glamor-0 { - font-size: 12px; -} - -

- hello world -

-`; - -exports[`styled object composition 1`] = ` -.glamor-0 { - border-radius: 50%; - -webkit-transition: -webkit-transform 400ms ease-in-out; - transition: -webkit-transform 400ms ease-in-out; - transition: transform 400ms ease-in-out; - transition: transform 400ms ease-in-out, -webkit-transform 400ms ease-in-out; - border: 3px solid currentColor; - width: 96px; - height: 96px; - color: blue; -} - -.glamor-0:hover { - -webkit-transform: scale(1.2); - transform: scale(1.2); -} - - -`; - -exports[`styled objects 1`] = ` -.glamor-0 { - padding: 10px; - display: flex; -} - -

- hello world -

-`; - -exports[`styled prefixing 1`] = ` -.glamor-0 { - display: -webkit-box; - display: -ms-flexbox; - display: flex; -} - -
- hello world -
-`; - -exports[`styled themes 1`] = ` -.glamor-0 { - background-color: #ffd43b; - color: #8c81d8; - height: 64px; - font-size: 32px; -} - - - hello world - -`; - -exports[`styled throws if undefined is passed as the component 1`] = ` -"You are trying to create a styled element with an undefined component. -You may have forgotten to import it." -`; From 59e92d3909fbaca5300a1331e1d28c3d5f758449 Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Thu, 27 Jul 2017 22:52:57 +1000 Subject: [PATCH 5/9] Rename a test directory --- test/{browserlist => browserslist}/.browserslistrc | 0 .../__snapshots__/babel.test.js.snap | 0 .../__snapshots__/css.test.js.snap | 0 test/{browserlist => browserslist}/babel.test.js | 3 ++- test/{browserlist => browserslist}/css.test.js | 0 5 files changed, 2 insertions(+), 1 deletion(-) rename test/{browserlist => browserslist}/.browserslistrc (100%) rename test/{browserlist => browserslist}/__snapshots__/babel.test.js.snap (100%) rename test/{browserlist => browserslist}/__snapshots__/css.test.js.snap (100%) rename test/{browserlist => browserslist}/babel.test.js (86%) rename test/{browserlist => browserslist}/css.test.js (100%) diff --git a/test/browserlist/.browserslistrc b/test/browserslist/.browserslistrc similarity index 100% rename from test/browserlist/.browserslistrc rename to test/browserslist/.browserslistrc diff --git a/test/browserlist/__snapshots__/babel.test.js.snap b/test/browserslist/__snapshots__/babel.test.js.snap similarity index 100% rename from test/browserlist/__snapshots__/babel.test.js.snap rename to test/browserslist/__snapshots__/babel.test.js.snap diff --git a/test/browserlist/__snapshots__/css.test.js.snap b/test/browserslist/__snapshots__/css.test.js.snap similarity index 100% rename from test/browserlist/__snapshots__/css.test.js.snap rename to test/browserslist/__snapshots__/css.test.js.snap diff --git a/test/browserlist/babel.test.js b/test/browserslist/babel.test.js similarity index 86% rename from test/browserlist/babel.test.js rename to test/browserslist/babel.test.js index a307ce19f..6bb8c3e9a 100644 --- a/test/browserlist/babel.test.js +++ b/test/browserslist/babel.test.js @@ -10,7 +10,8 @@ describe('babel css', () => { display: flex; \`` const { code } = babel.transform(basic, { - plugins: [[plugin]] + plugins: [[plugin]], + filename: __filename }) expect(code).toMatchSnapshot() }) diff --git a/test/browserlist/css.test.js b/test/browserslist/css.test.js similarity index 100% rename from test/browserlist/css.test.js rename to test/browserslist/css.test.js From 07c0cc3c0998d4f5a0df7389a6538bd22879965b Mon Sep 17 00:00:00 2001 From: Kye Hohenberger Date: Fri, 28 Jul 2017 15:55:29 -0600 Subject: [PATCH 6/9] css prop supports objects/arrays support spread operator in object styles --- src/ast-object.js | 244 +++++++++++++++--- src/babel.js | 15 +- src/css-prop.js | 6 +- src/parser.js | 3 +- test/.babelrc | 3 +- test/__snapshots__/css-prop.test.js.snap | 12 + test/__snapshots__/react.test.js.snap | 26 +- .../babel/__snapshots__/css-prop.test.js.snap | 26 +- test/babel/__snapshots__/css.test.js.snap | 6 +- test/babel/__snapshots__/styled.test.js.snap | 23 ++ test/babel/css-prop.test.js | 19 +- test/babel/styled.test.js | 41 +++ .../__snapshots__/babel.test.js.snap | 3 +- test/css-prop.test.js | 9 + test/macro/__snapshots__/react.test.js.snap | 6 +- test/react.test.js | 10 + 16 files changed, 354 insertions(+), 98 deletions(-) diff --git a/src/ast-object.js b/src/ast-object.js index a19a58ce1..773a76522 100644 --- a/src/ast-object.js +++ b/src/ast-object.js @@ -1,37 +1,99 @@ +import { expandCSSFallbacks, prefixer } from './parser' import { forEach, reduce } from './utils' -export default class ASTObject { - obj: { [string]: any } - expressions: Array - composesCount: number - t: any +function prefixAst (args, t) { + function isLiteral (value) { + return ( + t.isStringLiteral(value) || + t.isNumericLiteral(value) || + t.isBooleanLiteral(value) + ) + } - constructor (obj, expressions, composesCount, t) { - this.obj = obj - this.expressions = expressions - this.composesCount = composesCount - this.t = t + if (Array.isArray(args)) { + return args.map(element => prefixAst(element, t)) } - toAST () { - const { obj, t } = this + if (t.isObjectExpression(args)) { + let properties = [] + args.properties.forEach(property => { + // nested objects + if (t.isObjectExpression(property.value)) { + const key = property.computed + ? property.key + : t.isStringLiteral(property.key) + ? t.stringLiteral(property.key.value) + : t.identifier(property.key.name) - const props = [] - for (let key in obj) { - const rawValue = obj[key] - const { computed, composes, ast: keyAST } = this.objKeyToAst(key) - - let valueAST - if (composes) { - // valueAST = t.arrayExpression(expressions.slice(0, composesCount)) - continue + return properties.push( + t.objectProperty(key, prefixAst(property.value, t), property.computed) + ) + + // literal value or array of literal values + } else if ( + isLiteral(property.value) || + (t.isArrayExpression(property.value) && + property.value.elements.every(isLiteral)) + ) { + // bail on computed properties + if (property.computed) { + properties.push(property) + return + } + + // handle array values: { display: ['flex', 'block'] } + const propertyValue = t.isArrayExpression(property.value) + ? property.value.elements.map(element => element.value) + : property.value.value + + const style = { [property.key.name]: propertyValue } + const prefixedStyle = expandCSSFallbacks(prefixer(style)) + + for (let k in prefixedStyle) { + const key = t.isStringLiteral(property.key) + ? t.stringLiteral(k) + : t.identifier(k) + const val = prefixedStyle[k] + let value + + if (typeof val === 'number') { + value = t.numericLiteral(val) + } else if (typeof val === 'string') { + value = t.stringLiteral(val) + } else if (Array.isArray(val)) { + value = t.arrayExpression(val.map(i => t.stringLiteral(i))) + } + + properties.push(t.objectProperty(key, value)) + } + + // expressions } else { - valueAST = this.objValueToAst(rawValue) + properties.push(property) } + }) - props.push(t.objectProperty(keyAST, valueAST, computed)) - } - return t.objectExpression(props) + return t.objectExpression(properties) + } + + if (t.isArrayExpression(args)) { + return t.arrayExpression(prefixAst(args.elements, t)) + } + + return args +} + +export default class ASTObject { + props: Array + expressions: Array + composesCount: number + t: any + + constructor (props, expressions, composesCount, t) { + this.props = props + this.expressions = expressions || [] + this.composesCount = composesCount + this.t = t } objKeyToAst (key): { computed: boolean, ast: any, composes: boolean } { @@ -54,21 +116,26 @@ export default class ASTObject { } objValueToAst (value) { - const { expressions, composesCount, t } = this - + const { composesCount, t } = this if (typeof value === 'string') { const matches = this.getDynamicMatches(value) if (matches.length) { return this.replacePlaceholdersWithExpressions(matches, value) } return t.stringLiteral(value) + } else if (typeof value === 'number') { + return t.numericLiteral(value) } else if (Array.isArray(value)) { // should never hit here + + if (value[0] && (value[0].key || value[0].value || value[0].spread)) { + return this.toAST(value) + } + return t.arrayExpression(value.map(v => this.objValueToAst(v))) } - const obj = new this.constructor(value, expressions, composesCount, t) - return obj.toAST() + return ASTObject.fromJS(value, composesCount, t).toAST() } getDynamicMatches (str) { @@ -109,7 +176,7 @@ export default class ASTObject { // } templateExpressions.push( - expressions + expressions.length ? expressions[p1 - composesCount] : t.identifier(`x${p1 - composesCount}`) ) @@ -131,6 +198,73 @@ export default class ASTObject { return t.templateLiteral(templateElements, templateExpressions) } + toAST (props = this.props) { + return this.t.objectExpression( + props.map(prop => { + if (this.t.isObjectProperty(prop)) { + return prop + } + + // console.log(JSON.stringify(prop, null, 2)) + + const { property, key, value, spread, shorthand } = prop + + if (spread || shorthand) { + return property + } + + const { computed, ast: keyAST } = this.objKeyToAst(key) + const valueAST = this.objValueToAst(value) + + return this.t.objectProperty(keyAST, valueAST, computed) + }) + ) + } + + toJS (props = this.props) { + return props.reduce( + ( + accum, + { property, key, value, computed: isComputedProperty, spread } + ) => { + if (spread) { + return accum + } + + accum[key] = value + return accum + }, + {} + ) + } + + static fromJS (jsObj, composesCount, t) { + const props = [] + for (let key in jsObj) { + // console.log(key) + if (jsObj.hasOwnProperty(key)) { + let value + if (Object.prototype.toString.call(jsObj[key]) === '[object Object]') { + // console.log("what the fuck", jsObj[key]) + // value = ASTObject.fromJS(jsObj[key], composesCount, t) + value = jsObj[key] + } else { + value = jsObj[key] + } + + props.push({ + key: key, + value: value, + computed: false, + spread: false, + property: null + }) + } + } + + return new ASTObject(props, [], composesCount, t) + } + static fromAST (astObj, t) { function isLiteral (value) { return ( @@ -170,12 +304,27 @@ export default class ASTObject { return `xxx${expressions.length - 1}xxx` } - function toObj (astObj) { - let obj = {} + function convertAstToObj (astObj) { + const props = [] - forEach(astObj.properties, property => { - // nested objects + forEach(astObj.properties, (property, i) => { let key + if (t.isSpreadProperty(property)) { + props.push({ + key: null, + value: null, + computed: false, + shorthand: false, + spread: true, + property + }) + return + } + + if (property.shorthand) { + return property + } + if (property.computed) { key = replaceExpressionsWithPlaceholders(property.key) } else { @@ -183,19 +332,34 @@ export default class ASTObject { ? property.key.name : property.key.value } + + // nested objects if (t.isObjectExpression(property.value)) { - obj[key] = toObj(property.value) + props.push({ + key, + value: convertAstToObj(property.value), + computed: property.computed, + spread: false, + shorthand: false, + property + }) } else { - obj[key] = replaceExpressionsWithPlaceholders(property.value) + props.push({ + key, + value: replaceExpressionsWithPlaceholders(property.value), + computed: property.computed, + spread: false, + shorthand: false, + property + }) } }) - - return obj + return props } - const obj = toObj(astObj) + const objectProperties = convertAstToObj(prefixAst(astObj, t)) return new ASTObject( - obj, + objectProperties, expressions, 0, // composesCount: we should support this, t diff --git a/src/babel.js b/src/babel.js index ce3d20193..ad7c87eeb 100644 --- a/src/babel.js +++ b/src/babel.js @@ -3,14 +3,16 @@ import fs from 'fs' import { basename } from 'path' import { touchSync } from 'touch' import { inline } from './inline' -import { parseCSS } from './parser' +import { expandCSSFallbacks, prefixer, parseCSS } from './parser' import { getIdentifierName } from './babel-utils' import { map } from './utils' import cssProps from './css-prop' import ASTObject from './ast-object' function getFilename (path) { - return path.hub.file.opts.filename === 'unknown' ? '' : path.hub.file.opts.filename + return path.hub.file.opts.filename === 'unknown' + ? '' + : path.hub.file.opts.filename } export function replaceCssWithCallExpression ( @@ -61,7 +63,7 @@ export function replaceCssWithCallExpression ( t.blockStatement([ t.returnStatement( t.arrayExpression([ - new ASTObject(styles, false, composesCount, t).toAST() + ASTObject.fromJS(styles, composesCount, t).toAST() ]) ) ]) @@ -115,7 +117,7 @@ export function buildStyledCallExpression (identifier, tag, path, state, t) { t.blockStatement([ t.returnStatement( t.arrayExpression([ - new ASTObject(styles, false, composesCount, t).toAST() + ASTObject.fromJS(styles, composesCount, t).toAST() ]) ) ]) @@ -137,10 +139,7 @@ export function buildStyledObjectCallExpression (path, identifier, t) { function buildProcessedStylesFromObjectAST (objectAST, path, t) { if (t.isObjectExpression(objectAST)) { - const astObject = ASTObject.fromAST(objectAST, t) - const { styles } = parseCSS(astObject.obj, false, getFilename(path)) - astObject.obj = styles - return astObject.toAST() + return ASTObject.fromAST(objectAST, t).toAST() } if (t.isArrayExpression(objectAST)) { return t.arrayExpression( diff --git a/src/css-prop.js b/src/css-prop.js index fc57c52c0..c65662486 100644 --- a/src/css-prop.js +++ b/src/css-prop.js @@ -45,9 +45,9 @@ export default function (path, t) { ) ) } else { - throw path.buildCodeFrameError( - `${cssPropValue.value} is not a string or template literal` - ) + cssTemplateExpression = t.callExpression(t.identifier('css'), [ + cssPropValue + ]) } if (!classNamesValue) { diff --git a/src/parser.js b/src/parser.js index 0bba7dbb6..5e5c87751 100644 --- a/src/parser.js +++ b/src/parser.js @@ -6,9 +6,8 @@ import postcssJs from 'postcss-js' import objParse from './obj-parse' import autoprefixer from 'autoprefixer' import { processStyleName } from './glamor/CSSPropertyOperations' -import { objStyle } from './index' -const prefixer = postcssJs.sync([autoprefixer, postcssNested]) +export const prefixer = postcssJs.sync([autoprefixer, postcssNested]) type Rule = { parent: { selector: string, nodes: Array }, diff --git a/test/.babelrc b/test/.babelrc index 9fbb976c8..24b6795a1 100644 --- a/test/.babelrc +++ b/test/.babelrc @@ -2,7 +2,8 @@ "presets": [ "flow", "env", - "react" + "react", + "stage-2" ], "plugins": [["./babel-plugin-emotion-test", { "inline": true }]] } diff --git a/test/__snapshots__/css-prop.test.js.snap b/test/__snapshots__/css-prop.test.js.snap index 5b5d16412..21b2cc0b1 100644 --- a/test/__snapshots__/css-prop.test.js.snap +++ b/test/__snapshots__/css-prop.test.js.snap @@ -78,6 +78,18 @@ exports[`css prop react kitchen sink 1`] = `
`; +exports[`css prop react objects 1`] = ` +.glamor-0 { + undefined: red; +} + +

+ hello world +

+`; + exports[`css prop react string expression 1`] = ` .glamor-0 { color: red; diff --git a/test/__snapshots__/react.test.js.snap b/test/__snapshots__/react.test.js.snap index 0a1233d60..81433ea3d 100644 --- a/test/__snapshots__/react.test.js.snap +++ b/test/__snapshots__/react.test.js.snap @@ -14,12 +14,8 @@ exports[`styled basic render 1`] = ` `; exports[`styled basic render with object as style 1`] = ` -.glamor-0 { - font-size: 20px; -} -

hello world

@@ -304,14 +300,6 @@ exports[`styled input placeholder 1`] = ` `; exports[`styled input placeholder object 1`] = ` -.glamor-0::-webkit-input-placeholder { - background-color: green; -} - -.glamor-0:-ms-input-placeholder { - background-color: green; -} - .glamor-0::placeholder { background-color: green; } @@ -417,6 +405,18 @@ exports[`styled objects 1`] = ` `; +exports[`styled objects with spread properties 1`] = ` +.glamor-0 { + font-size: 20px; +} + +
+ hello world +
+`; + exports[`styled themes 1`] = ` .glamor-0 { background-color: #ffd43b; diff --git a/test/babel/__snapshots__/css-prop.test.js.snap b/test/babel/__snapshots__/css-prop.test.js.snap index 796edddc9..da908c188 100644 --- a/test/babel/__snapshots__/css-prop.test.js.snap +++ b/test/babel/__snapshots__/css-prop.test.js.snap @@ -8,25 +8,31 @@ exports[`babel css prop StringLiteral css prop value 1`] = ` })}>
;" `; -exports[`babel css prop basic 1`] = ` +exports[`babel css prop basic inline 1`] = ` +"
;" +`; + +exports[`babel css prop basic object 1`] = ` +"
;" +`; + +exports[`babel css prop basic with extractStatic 1`] = ` "import \\"./css-prop.test.emotion.css\\";
;" `; -exports[`babel css prop basic 2`] = ` +exports[`babel css prop basic with extractStatic 2`] = ` ".css-jf1v9l { color: brown }" `; -exports[`babel css prop basic inline 1`] = ` -"
;" -`; - exports[`babel css prop className as expression 1`] = ` "
{ - test('basic', () => { + test('basic with extractStatic', () => { const basic = '(
)' const { code } = babel.transform(basic, { plugins: [[plugin, { extractStatic: true }]], @@ -28,6 +28,14 @@ describe('babel css prop', () => { expect(code).toMatchSnapshot() }) + test('basic object', () => { + const basic = '(
)' + const {code} = babel.transform(basic, { + plugins: [plugin] + }) + expect(code).toMatchSnapshot() + }) + test('dynamic inline', () => { const basic = `(
)` const { code } = babel.transform(basic, { @@ -60,15 +68,6 @@ describe('babel css prop', () => { expect(code).toMatchSnapshot() }) - test('wrong value type', () => { - const basic = '(
)' - expect(() => - babel.transform(basic, { - plugins: [plugin] - }) - ).toThrow() - }) - test('StringLiteral css prop value', () => { const basic = `
` const { code } = babel.transform(basic, { diff --git a/test/babel/styled.test.js b/test/babel/styled.test.js index 148865e72..204fb8329 100644 --- a/test/babel/styled.test.js +++ b/test/babel/styled.test.js @@ -2,6 +2,7 @@ /* eslint-env jest */ import * as babel from 'babel-core' import plugin from '../../src/babel' +import stage2 from 'babel-plugin-transform-object-rest-spread' import * as fs from 'fs' jest.mock('fs') @@ -226,6 +227,46 @@ describe('babel styled component', () => { expect(code).toMatchSnapshot() }) + test('styled. objects with a single spread property', () => { + const basic = ` + const defaultText = { fontSize: 20 } + const Figure = styled.figure({ + ...defaultText + })` + const { code } = babel.transform(basic, { + plugins: [plugin, stage2] + }) + expect(code).toMatchSnapshot() + }) + + test('styled. objects with a multiple spread properties', () => { + const basic = ` + const defaultText = { fontSize: 20 } + const Figure = styled.figure({ + ...defaultText, + ...defaultFigure + })` + const {code} = babel.transform(basic, { + plugins: [plugin, stage2] + }) + expect(code).toMatchSnapshot() + }) + + test('styled. objects with a multiple spread properties and other keys', () => { + const basic = ` + const defaultText = { fontSize: 20 } + const Figure = styled.figure({ + ...defaultText, + fontSize: '20px', + ...defaultFigure, + ...defaultText2 + })` + const {code} = babel.transform(basic, { + plugins: [plugin, stage2] + }) + expect(code).toMatchSnapshot() + }) + test('styled objects prefixed', () => { const basic = ` const H1 = styled.h1({ diff --git a/test/browserslist/__snapshots__/babel.test.js.snap b/test/browserslist/__snapshots__/babel.test.js.snap index ed748bd03..5635c7443 100644 --- a/test/browserslist/__snapshots__/babel.test.js.snap +++ b/test/browserslist/__snapshots__/babel.test.js.snap @@ -1,7 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`babel css inline css basic 1`] = ` -" +"\\"use strict\\"; + /*#__PURE__*/css([], [], function createEmotionStyledRules() { return [{ \\"display\\": \\"-webkit-box; display: -ms-flexbox; display: flex\\" diff --git a/test/css-prop.test.js b/test/css-prop.test.js index aba22a93f..317a300ea 100644 --- a/test/css-prop.test.js +++ b/test/css-prop.test.js @@ -26,6 +26,15 @@ describe('css prop react', () => { expect(tree).toMatchSnapshot() }) + test('objects', () => { + const fontSize = '1px' + const tree = renderer + .create(

hello world

) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + test('kitchen sink', () => { const props = { online: false, error: false, radius: 5 } const huge = 100 diff --git a/test/macro/__snapshots__/react.test.js.snap b/test/macro/__snapshots__/react.test.js.snap index 00201709c..0e9c0da8b 100644 --- a/test/macro/__snapshots__/react.test.js.snap +++ b/test/macro/__snapshots__/react.test.js.snap @@ -13,12 +13,8 @@ exports[`styled basic render 1`] = ` `; exports[`styled basic render with object as style 1`] = ` -.glamor-0 { - font-size: 20px; -} -

hello world

diff --git a/test/react.test.js b/test/react.test.js index 346fb4ae3..b0498dcda 100644 --- a/test/react.test.js +++ b/test/react.test.js @@ -428,6 +428,16 @@ describe('styled', () => { expect(tree).toMatchSnapshot() }) + test('objects with spread properties', () => { + const defaultText = { fontSize: 20 } + const Figure = styled.figure({ + ...defaultText + }) + const tree = renderer.create(
hello world
).toJSON() + + expect(tree).toMatchSnapshot() + }) + test('change theme', () => { const Div = styled.div` color: ${props => props.theme.primary} From d293ae641f9838720f3b4004bc611ee6d47a7b60 Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Sat, 29 Jul 2017 12:42:57 +1000 Subject: [PATCH 7/9] Fix css prop with object importing and prettier --- src/babel.js | 31 +++++++++++++------ src/css-prop.js | 15 ++++----- src/utils.js | 2 +- .../babel/__snapshots__/css-prop.test.js.snap | 27 +++++++++------- 4 files changed, 47 insertions(+), 28 deletions(-) diff --git a/src/babel.js b/src/babel.js index 5e5e2125d..dc8439849 100644 --- a/src/babel.js +++ b/src/babel.js @@ -15,10 +15,6 @@ function getFilename (path) { : path.hub.file.opts.filename } -function getFilename (path) { - return path.hub.file.opts.filename === 'unknown' ? '' : path.hub.file.opts.filename -} - export function replaceCssWithCallExpression ( path, identifier, @@ -137,7 +133,9 @@ export function buildStyledObjectCallExpression (path, identifier, t) { : t.stringLiteral(path.node.callee.property.name) return t.callExpression(identifier, [ tag, - t.arrayExpression(buildProcessedStylesFromObjectAST(path.node.arguments, path, t)) + t.arrayExpression( + buildProcessedStylesFromObjectAST(path.node.arguments, path, t) + ) ]) } @@ -151,7 +149,9 @@ function buildProcessedStylesFromObjectAST (objectAST, path, t) { ) } if (Array.isArray(objectAST)) { - return map(objectAST, obj => buildProcessedStylesFromObjectAST(obj, path, t)) + return map(objectAST, obj => + buildProcessedStylesFromObjectAST(obj, path, t) + ) } return objectAST @@ -210,7 +210,12 @@ export default function (babel) { if (state.cssPropIdentifier) { path.node.body.unshift( t.importDeclaration( - [t.importSpecifier(state.cssPropIdentifier, t.identifier('css'))], + [ + t.importSpecifier( + state.cssPropIdentifier, + t.identifier('css') + ) + ], t.stringLiteral('emotion') ) ) @@ -310,8 +315,16 @@ export default function (babel) { (name, hash, src) => src, true ) - } else if (state.cssPropIdentifier && path.node.tag === state.cssPropIdentifier) { - replaceCssWithCallExpression(path, state.cssPropIdentifier, state, t) + } else if ( + state.cssPropIdentifier && + path.node.tag === state.cssPropIdentifier + ) { + replaceCssWithCallExpression( + path, + state.cssPropIdentifier, + state, + t + ) } } } diff --git a/src/css-prop.js b/src/css-prop.js index 2c6b6c090..800eb9f39 100644 --- a/src/css-prop.js +++ b/src/css-prop.js @@ -45,9 +45,7 @@ export default function (path, state, t) { ) ) } else { - cssTemplateExpression = t.callExpression(t.identifier('css'), [ - cssPropValue - ]) + cssTemplateExpression = t.callExpression(getCssIdentifer(), [cssPropValue]) } if (!classNamesValue) { @@ -90,14 +88,17 @@ export default function (path, state, t) { ) } - function createCssTemplateExpression (templateLiteral) { - let identifier = t.identifier('css') + function getCssIdentifer () { if (state.opts.autoImportCssProp !== false) { if (!state.cssPropIdentifier) { state.cssPropIdentifier = path.scope.generateUidIdentifier('css') } - identifier = state.cssPropIdentifier + return state.cssPropIdentifier + } else { + return t.identifier('css') } - return t.taggedTemplateExpression(identifier, templateLiteral) + } + function createCssTemplateExpression (templateLiteral) { + return t.taggedTemplateExpression(getCssIdentifer(), templateLiteral) } } diff --git a/src/utils.js b/src/utils.js index af5c99c3b..5a924e996 100644 --- a/src/utils.js +++ b/src/utils.js @@ -80,7 +80,7 @@ export const assign: any = function (target) { let i = 1 let length = arguments.length - for (;i < length; i++) { + for (; i < length; i++) { var source = arguments[i] for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { diff --git a/test/babel/__snapshots__/css-prop.test.js.snap b/test/babel/__snapshots__/css-prop.test.js.snap index 53f4a69ee..3d92f1dc5 100644 --- a/test/babel/__snapshots__/css-prop.test.js.snap +++ b/test/babel/__snapshots__/css-prop.test.js.snap @@ -9,27 +9,32 @@ exports[`babel css prop StringLiteral css prop value 1`] = ` })}>
;" `; -exports[`babel css prop basic 1`] = ` +exports[`babel css prop basic inline 1`] = ` +"import { css as _css } from \\"emotion\\"; +
;" +`; + +exports[`babel css prop basic object 1`] = ` +"import { css as _css } from \\"emotion\\"; +
;" +`; + +exports[`babel css prop basic with extractStatic 1`] = ` "import { css as _css } from \\"emotion\\"; import \\"./css-prop.test.emotion.css\\";
;" `; -exports[`babel css prop basic 2`] = ` +exports[`babel css prop basic with extractStatic 2`] = ` ".css-jf1v9l { color: brown }" `; -exports[`babel css prop basic inline 1`] = ` -"import { css as _css } from \\"emotion\\"; -
;" -`; - exports[`babel css prop className as expression 1`] = ` "import { css as _css } from \\"emotion\\";
Date: Fri, 28 Jul 2017 21:35:55 -0600 Subject: [PATCH 8/9] =?UTF-8?q?Only=20run=20the=20spec=20content=20when=20?= =?UTF-8?q?its=20actually=20there=20=F0=9F=A6=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/react/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/react/index.js b/src/react/index.js index 1542fd03b..773290c33 100644 --- a/src/react/index.js +++ b/src/react/index.js @@ -78,7 +78,9 @@ export default function (tag, objs, vars = [], content) { tag.__emotion_spec, (accum, spec) => { push(accum, spec.objs) - push(accum, spec.content.apply(null, map(spec.vars, getValue))) + if (spec.content) { + push(accum, spec.content.apply(null, map(spec.vars, getValue))) + } return accum }, [] From 8b4b2ac8831064c8cd57e8511c708ee91a63705b Mon Sep 17 00:00:00 2001 From: Kye Hohenberger Date: Fri, 28 Jul 2017 21:50:43 -0600 Subject: [PATCH 9/9] Update snapshots --- test/__snapshots__/css-prop.test.js.snap | 3 ++- test/__snapshots__/css.test.js.snap | 22 +--------------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/test/__snapshots__/css-prop.test.js.snap b/test/__snapshots__/css-prop.test.js.snap index 21b2cc0b1..7d1d579c7 100644 --- a/test/__snapshots__/css-prop.test.js.snap +++ b/test/__snapshots__/css-prop.test.js.snap @@ -80,7 +80,8 @@ exports[`css prop react kitchen sink 1`] = ` exports[`css prop react objects 1`] = ` .glamor-0 { - undefined: red; + color: red; + font-size: 1px; }

`; @@ -198,20 +192,6 @@ exports[`css nested at rules 1`] = ` } } -@supports (display: grid) { - .glamor-0 { - display: grid; - } -} - -@supports (display: grid) and ((display: -webkit-box) or (display: flex)) { - .glamor-0 { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - } -} -