Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dynamic value support for extract static in the styled function. #279

Closed
wants to merge 14 commits into from
Closed
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -51,7 +51,7 @@
"module-alias": "^2.0.1",
"npm-run-all": "^4.0.2",
"polished": "^1.2.1",
"prettier-eslint-cli": "^4.0.3",
"prettier-eslint-cli": "^4.2.1",
"react": "^15.5.4",
"react-addons-test-utils": "^15.5.1",
"react-dom": "^15.5.4",
Expand Down
27 changes: 23 additions & 4 deletions packages/babel-plugin-emotion/src/index.js
Expand Up @@ -144,21 +144,40 @@ const getComponentId = (state, prefix: string = 'css') => {
export function buildStyledCallExpression(identifier, tag, path, state, t) {
const identifierName = getIdentifierName(path, t)

if (state.extractStatic && !path.node.quasi.expressions.length) {
if (state.extractStatic) {
const { name, hash, src } = inline(
path.node.quasi,
identifierName,
'styled' // we don't want these styles to be merged in css``
)

const cssText = `.${name}-${hash} { ${src} }`
const { staticCSSRules } = parseCSS(cssText, true, getFilename(path))
const { staticCSSRules, composesCount } = parseCSS(
cssText,
true,
getFilename(path)
)

// Help Needed:
// We should read the browser preferences for postcss
// and turn this on only if css vars are supported in their browser targets.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

@ai ai Aug 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detection:

browserslist(undefined, { path: processingFileName }).some(i => i.slice(0, 2) === 'ie')

It will detect IE and IE mobile.

const objs = path.node.quasi.expressions.slice(0, composesCount)

if (objs.length) {
throw new Error('Cannot use composes in extractStatic mode.')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tkh44 Do you think we should just go to inline mode instead of throwing an error. We also need to check if there are any interpolations in other places before going to extract.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that might be a good idea.

}

state.insertStaticRules(staticCSSRules)
const vars = path.node.quasi.expressions.slice(composesCount)
state.insertStaticRules(
staticCSSRules.map(ruleText =>
ruleText.replace(/xxx(\d+)xxx/gm, `var(--${name}-${hash}-$1)`)
)
)
return t.callExpression(identifier, [
tag,
t.stringLiteral(getComponentId(state, name)),
t.arrayExpression([t.stringLiteral(`${name}-${hash}`)])
t.arrayExpression([t.stringLiteral(`${name}-${hash}`)]),
t.arrayExpression(vars)
])
}

Expand Down
10 changes: 4 additions & 6 deletions packages/babel-plugin-emotion/src/parser.js
Expand Up @@ -35,7 +35,7 @@ export function parseCSS(
} else {
root = parse(css, { from: filename })
}
let vars = 0

let composes: number = 0

root.walkDecls((decl: Decl): void => {
Expand All @@ -52,7 +52,6 @@ export function parseCSS(
const composeMatches = decl.value.match(/xxx(\d+)xxx/gm)
const numOfComposes: number = !composeMatches ? 0 : composeMatches.length
composes += numOfComposes
vars += numOfComposes
decl.remove()
}
})
Expand All @@ -63,10 +62,9 @@ export function parseCSS(

return {
styles,
staticCSSRules:
vars === 0 && extractStatic
? stringifyCSSRoot(postcssJs.parse(styles))
: [],
staticCSSRules: extractStatic
? stringifyCSSRoot(postcssJs.parse(styles))
: [],
composesCount: composes
}
}
Expand Down
Expand Up @@ -2,7 +2,7 @@

exports[`babel styled component extract basic 1`] = `
"import \\"./styled.test.emotion.css\\";
const H1 = styled(\\"h1\\", \\"styled-H1-1qcxcvu0\\", [\\"styled-H1-10x82eg\\"]);"
const H1 = styled(\\"h1\\", \\"styled-H1-1qcxcvu0\\", [\\"styled-H1-10x82eg\\"], []);"
`;

exports[`babel styled component extract basic 2`] = `
Expand Down Expand Up @@ -37,11 +37,46 @@ exports[`babel styled component extract basic 2`] = `

exports[`babel styled component extract no use 1`] = `
"import \\"./styled.test.emotion.css\\";
styled(\\"h1\\", \\"styled-yoisj70\\", [\\"styled-0\\"]);"
styled(\\"h1\\", \\"styled-yoisj70\\", [\\"styled-0\\"], []);"
`;

exports[`babel styled component extract no use 2`] = `".styled-0 {}"`;

exports[`babel styled component extract with interpolation 1`] = `
"import \\"./styled.test.emotion.css\\";
const H1 = styled(\\"h1\\", \\"styled-H1-i6cu680\\", [\\"styled-H1-9acweo\\"], [p => p.justify]);"
`;

exports[`babel styled component extract with interpolation 2`] = `
".styled-H1-9acweo {
display: -webkit-box; display: -ms-flexbox; display: flex;
-webkit-box-pack: var(--styled-H1-9acweo-0);
-ms-flex-pack: var(--styled-H1-9acweo-0);
justify-content: var(--styled-H1-9acweo-0);
width: var(--css-hash-0)
}
.styled-H1-9acweo:hover {
background-color: green
}
@media (max-width: 500px) {
.styled-H1-9acweo {
height: var(--css-hash-1);
position: fixed
}
}
@media print {
.styled-H1-9acweo {
display: none
}
}
.styled-H1-9acweo::before {
color: blue;
width: 20px;
height: 20px;
content: 'pseudo'
}"
`;

exports[`babel styled component inline basic 1`] = `
"const H1 = /*#__PURE__*/styled('h1', 'css-H1-uutogn0', [], [fontSize + 'px'], function createEmotionStyledRules(x0) {
return {
Expand Down
63 changes: 59 additions & 4 deletions packages/babel-plugin-emotion/test/styled.test.js
Expand Up @@ -328,6 +328,9 @@ describe('babel styled component', () => {
})

describe('extract', () => {
afterEach(() => {
fs.writeFileSync.clearMock()
})
test('no use', () => {
const basic = 'styled.h1``'
const { code } = babel.transform(basic, {
Expand All @@ -341,17 +344,69 @@ describe('babel styled component', () => {
})

test('basic', () => {
const basic =
"const H1 = styled.h1`display: flex; justify-content: center; width: var(--css-hash-0); &:hover { background-color: green; } @media (max-width: 500px) { height: var(--css-hash-1); position: fixed; } @media print { display: none; } &::before { color: blue; width: 20px; height: 20px; content: 'pseudo' }`"
const basic = `const H1 = styled.h1\`
display: flex;
justify-content: center;
width: var(--css-hash-0);
&:hover {
background-color: green;
}
@media (max-width: 500px) {
height: var(--css-hash-1);
position: fixed;
}
@media print {
display: none;
}
&::before {
color: blue;
width: 20px;
height: 20px;
content: 'pseudo';
}
\``
const { code } = babel.transform(basic, {
plugins: [[plugin, { extractStatic: true }]],
filename: __filename,
babelrc: false
})

expect(code).toMatchSnapshot()
expect(fs.writeFileSync).toHaveBeenCalledTimes(2)
expect(fs.writeFileSync.mock.calls[1][1]).toMatchSnapshot()
expect(fs.writeFileSync).toHaveBeenCalledTimes(1)
expect(fs.writeFileSync.mock.calls[0][1]).toMatchSnapshot()
})

test('with interpolation', () => {
const basic = `const H1 = styled.h1\`
display: flex;
justify-content: \${p => p.justify};
width: var(--css-hash-0);
&:hover {
background-color: green;
}
@media (max-width: 500px) {
height: var(--css-hash-1);
position: fixed;
}
@media print {
display: none;
}
&::before {
color: blue;
width: 20px;
height: 20px;
content: 'pseudo';
}
\``
const { code } = babel.transform(basic, {
plugins: [[plugin, { extractStatic: true }]],
filename: __filename,
babelrc: false
})

expect(code).toMatchSnapshot()
expect(fs.writeFileSync).toHaveBeenCalledTimes(1)
expect(fs.writeFileSync.mock.calls[0][1]).toMatchSnapshot()
})
})
})
34 changes: 33 additions & 1 deletion packages/emotion/src/index.js
Expand Up @@ -9,6 +9,7 @@ import {
clean,
createMarkupForStyles,
hashString as hash,
hashArray,
hashObject
} from 'emotion-utils'

Expand Down Expand Up @@ -82,7 +83,38 @@ export function css(objs: any, vars: Array<any>, content: () => Array<any>) {
return computedClassName.trim()
}

function insertRawRule(css: string) {
type inputVar = string | number

export function customProperties(baseClassName: string, vars: Array<inputVar>) {
const hash = hashArray([baseClassName, ...vars])
const varCls = `css-vars-${hash}`
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this so that users could SSR and extract the custom property values, but I'm not sure if it will trip up in other places.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to write a test for it but I think we have to do something different to get SSR to work.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wrong! it works right now! 🎉

if (inserted[hash]) {
return varCls
}

let src = ''
forEach(vars, (val: inputVar, i: number) => {
src && (src += '; ')
src += `--${baseClassName}-${i}: ${val}`
})

let spec = {
id: hash,
css: `.${varCls} {${src}}`,
type: 'raw'
}

register(spec)

if (!inserted[spec.id]) {
sheet.insert(spec.css)
inserted[spec.id] = true
}

return varCls
}

export function insertRawRule(css: string) {
let spec = {
id: hash(css),
css,
Expand Down
21 changes: 18 additions & 3 deletions packages/emotion/test/extract/__snapshots__/extract.test.js.snap
Expand Up @@ -2,23 +2,35 @@

exports[`styled basic render nested 1`] = `
<h1
className="styled-H1-1f87tx4 styled-H1-1sjwp4j1"
className="styled-H1-1f87tx4 styled-H1-r6dkuc1"
>
hello world
</h1>
`;

exports[`styled name 1`] = `
<h1
className="styled-H1-131pfth styled-H1-1sjwp4j2"
className="styled-H1-131pfth styled-H1-r6dkuc3"
>
hello world
</h1>
`;

exports[`styled no dynamic 1`] = `
<h1
className="styled-H1-ijh7uz styled-H1-1sjwp4j0"
className="styled-H1-ijh7uz styled-H1-r6dkuc0"
>
hello world
</h1>
`;

exports[`styled with expressions 1`] = `
.glamor-0 {
--styled-H1-1t8i2zo-0: 24;
}

<h1
className="styled-H1-1t8i2zo glamor-0 styled-H1-r6dkuc2"
>
hello world
</h1>
Expand All @@ -40,6 +52,9 @@ exports[`styled writes the correct css 1`] = `
.styled-H1-1f87tx4 span:hover:after {
content: \\"after\\"
}
.styled-H1-1t8i2zo {
font-size: var(--styled-H1-1t8i2zo-0)
}
.styled-H1-131pfth {
name: FancyH1;
font-size: 38px
Expand Down
3 changes: 3 additions & 0 deletions packages/emotion/test/extract/extract.test.emotion.css
Expand Up @@ -13,6 +13,9 @@
.styled-H1-1f87tx4 span:hover:after {
content: "after"
}
.styled-H1-1t8i2zo {
font-size: var(--styled-H1-1t8i2zo-0)
}
.styled-H1-131pfth {
name: FancyH1;
font-size: 38px
Expand Down
8 changes: 8 additions & 0 deletions packages/emotion/test/extract/extract.test.js
Expand Up @@ -36,6 +36,14 @@ describe('styled', () => {
expect(tree).toMatchSnapshot()
})

test('with expressions', () => {
const H1 = styled.h1`font-size: ${p => p.fontSize};`

const tree = renderer.create(<H1 fontSize={24}>hello world</H1>).toJSON()

expect(tree).toMatchSnapshot()
})

test('name', () => {
const H1 = styled.h1`
name: FancyH1;
Expand Down
8 changes: 7 additions & 1 deletion packages/react-emotion/src/index.js
@@ -1,5 +1,5 @@
import { createElement as h } from 'react'
import { css } from 'emotion'
import { css, customProperties } from 'emotion'
import { map, reduce, assign, omit } from 'emotion-utils'
import propsRegexString from /* preval */ './props'

Expand All @@ -18,6 +18,8 @@ export default function(tag, cls, objs, vars = [], content) {
)
}

const EXTRACTED_DYNAMIC = vars && vars.length && !content

const componentTag = tag.displayName || tag.name || 'Component'
const spec = {
vars,
Expand Down Expand Up @@ -56,6 +58,10 @@ export default function(tag, cls, objs, vars = [], content) {
push(accum, spec.objs)
if (spec.content) {
accum.push(spec.content.apply(null, map(spec.vars, getValue)))
} else if (spec.vars.length) {
// Dynamic properties for extracted css will have variables
// but no content function
accum.push(customProperties(spec.objs[0], map(spec.vars, getValue)))
}
accum.push(spec.cls)
return accum
Expand Down