Skip to content

Commit 62b694c

Browse files
committed
added transformations to fix tree-shaking issues when using static properties (styled-components#245)
1 parent ad289a0 commit 62b694c

14 files changed

+384
-32
lines changed

.vscode/launch.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"type": "node",
9+
"request": "launch",
10+
"name": "Run demo",
11+
"preLaunchTask": "npm: build",
12+
"program": "${workspaceFolder}/node_modules/.bin/babel",
13+
"cwd": "${workspaceFolder}/demo",
14+
"args": ["demoComponents.js"]
15+
}
16+
]
17+
}

.vscode/settings.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"editor.formatOnSave": true
3+
}

.vscode/tasks.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
// See https://go.microsoft.com/fwlink/?LinkId=733558
3+
// for the documentation about the tasks.json format
4+
"version": "2.0.0",
5+
"tasks": [
6+
{
7+
"type": "npm",
8+
"script": "build",
9+
"group": {
10+
"kind": "build",
11+
"isDefault": true
12+
}
13+
}
14+
]
15+
}

demo/babel.config.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
plugins: [
3+
'@babel/plugin-proposal-class-properties',
4+
[require('../lib'), { pure: true }],
5+
],
6+
}

demo/demoComponents.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import styled from 'styled-components'
2+
3+
export const Component2 = styled.div`
4+
color: turquoise;
5+
`
6+
// If this line is removed, then tree-shaking works correctly
7+
Component2.displayName = 'FancyName2'

demo/demoComponents.js.bak

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react'
2+
import styled from 'styled-components'
3+
4+
const Wrapper = styled.div`
5+
color: blue;
6+
`
7+
8+
export function FunctionComponent() {
9+
return React.createElement(Wrapper)
10+
}
11+
FunctionComponent.displayName = 'FancyName1'
12+
FunctionComponent.defaultProps = {}
13+
14+
export class ClassComponent extends React.Component {
15+
static displayName = 'FancyName2'
16+
static defaultProps = {}
17+
18+
render() {
19+
return React.createElement(Wrapper)
20+
}
21+
}

demo/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "babel-pugin-styled-components-demo",
3+
"scripts": {
4+
"transpile": "../node_modules/.bin/babel demoComponents.js"
5+
}
6+
}

demo/tmp.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export class ClassComponent extends React.Component {
2+
static displayName = 'FancyName2'
3+
static defaultProps = {}
4+
5+
render() {
6+
return React.createElement(Wrapper)
7+
}
8+
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"clean": "rimraf lib",
4545
"style": "prettier --write src/**/*.js",
4646
"build": "babel src -d lib",
47+
"watch": "yarn build -w",
4748
"test": "jest",
4849
"test:watch": "npm run test -- --watch",
4950
"prepublish": "npm run clean && npm run build"

src/index.js

+24-11
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,26 @@ import displayNameAndId from './visitors/displayNameAndId'
55
import templateLiterals from './visitors/templateLiterals'
66
import assignStyledRequired from './visitors/assignStyledRequired'
77
import transpileCssProp from './visitors/transpileCssProp'
8+
import pureWrapStaticProps from './visitors/pureWrapStaticProps'
89

910
export default function({ types: t }) {
1011
return {
1112
inherits: syntax,
1213
visitor: {
13-
Program(path, state) {
14-
path.traverse(
15-
{
16-
JSXAttribute(path, state) {
17-
transpileCssProp(t)(path, state)
14+
Program: {
15+
enter(path, state) {
16+
path.traverse(
17+
{
18+
JSXAttribute(path, state) {
19+
transpileCssProp(t)(path, state)
20+
},
21+
VariableDeclarator(path, state) {
22+
assignStyledRequired(t)(path, state)
23+
},
1824
},
19-
VariableDeclarator(path, state) {
20-
assignStyledRequired(t)(path, state)
21-
},
22-
},
23-
state
24-
)
25+
state
26+
)
27+
},
2528
},
2629
CallExpression(path, state) {
2730
displayNameAndId(t)(path, state)
@@ -33,6 +36,16 @@ export default function({ types: t }) {
3336
templateLiterals(t)(path, state)
3437
pureAnnotation(t)(path, state)
3538
},
39+
FunctionDeclaration(path, state) {
40+
// technically this is more like,
41+
// "mark pure if it's a function component that consumes a styled component and also has static properties",
42+
// but that's rather long ;)
43+
pureWrapStaticProps(t)(path, state)
44+
},
45+
VariableDeclarator(path, state) {
46+
// same thing for arrow functions
47+
pureWrapStaticProps(t)(path, state)
48+
},
3649
},
3750
}
3851
}

src/utils/detectors.js

+25-21
Original file line numberDiff line numberDiff line change
@@ -64,28 +64,28 @@ export const isStyled = t => (tag, state) => {
6464
// styled.something()
6565
return isStyled(t)(tag.callee.object, state)
6666
} else {
67-
return (
67+
return Boolean(
6868
(t.isMemberExpression(tag) &&
6969
tag.object.name === importLocalName('default', state)) ||
70-
(t.isCallExpression(tag) &&
71-
tag.callee.name === importLocalName('default', state)) ||
72-
/**
73-
* #93 Support require()
74-
* styled-components might be imported using a require()
75-
* call and assigned to a variable of any name.
76-
* - styled.default.div``
77-
* - styled.default.something()
78-
*/
79-
(state.styledRequired &&
80-
t.isMemberExpression(tag) &&
81-
t.isMemberExpression(tag.object) &&
82-
tag.object.property.name === 'default' &&
83-
tag.object.object.name === state.styledRequired) ||
84-
(state.styledRequired &&
85-
t.isCallExpression(tag) &&
86-
t.isMemberExpression(tag.callee) &&
87-
tag.callee.property.name === 'default' &&
88-
tag.callee.object.name === state.styledRequired)
70+
(t.isCallExpression(tag) &&
71+
tag.callee.name === importLocalName('default', state)) ||
72+
/**
73+
* #93 Support require()
74+
* styled-components might be imported using a require()
75+
* call and assigned to a variable of any name.
76+
* - styled.default.div``
77+
* - styled.default.something()
78+
*/
79+
(state.styledRequired &&
80+
t.isMemberExpression(tag) &&
81+
t.isMemberExpression(tag.object) &&
82+
tag.object.property.name === 'default' &&
83+
tag.object.object.name === state.styledRequired) ||
84+
(state.styledRequired &&
85+
t.isCallExpression(tag) &&
86+
t.isMemberExpression(tag.callee) &&
87+
tag.callee.property.name === 'default' &&
88+
tag.callee.object.name === state.styledRequired)
8989
)
9090
}
9191
}
@@ -107,10 +107,14 @@ export const isWithThemeHelper = t => (tag, state) =>
107107
t.isIdentifier(tag) && tag.name === importLocalName('withTheme', state)
108108

109109
export const isHelper = t => (tag, state) =>
110-
isCSSHelper(t)(tag, state) || isKeyframesHelper(t)(tag, state) || isWithThemeHelper(t)(tag, state)
110+
isCSSHelper(t)(tag, state) ||
111+
isKeyframesHelper(t)(tag, state) ||
112+
isWithThemeHelper(t)(tag, state)
111113

112114
export const isPureHelper = t => (tag, state) =>
113115
isCSSHelper(t)(tag, state) ||
114116
isKeyframesHelper(t)(tag, state) ||
115117
isCreateGlobalStyleHelper(t)(tag, state) ||
116118
isWithThemeHelper(t)(tag, state)
119+
120+
export { isFunctionComponent } from './isFunctionComponent'

src/utils/isFunctionComponent.js

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* Adapted from https://github.com/oliviertassinari/babel-plugin-transform-react-remove-prop-types/blob/master/src/isStatelessComponent.js
3+
*/
4+
5+
import { isStyled } from '../utils/detectors'
6+
7+
const traversed = Symbol('traversed')
8+
9+
function isJSXElementOrReactCreateElement(
10+
path,
11+
filterFn = null // optional filter function to match only certain kinds of React elements
12+
) {
13+
let visited = false
14+
15+
path.traverse({
16+
CallExpression(path2) {
17+
const callee = path2.get('callee')
18+
19+
if (
20+
callee.matchesPattern('React.createElement') ||
21+
callee.matchesPattern('React.cloneElement') ||
22+
callee.node.name === 'cloneElement'
23+
) {
24+
visited = filterFn ? filterFn(path2) : true
25+
}
26+
},
27+
JSXElement(path2) {
28+
visited = filterFn ? filterFn(path2) : true
29+
},
30+
})
31+
32+
return visited
33+
}
34+
35+
function isReturningJSXElement(path, state, filterFn = null, iteration = 0) {
36+
// Early exit for ArrowFunctionExpressions, there is no ReturnStatement node.
37+
if (
38+
path.node.init &&
39+
path.node.init.body &&
40+
isJSXElementOrReactCreateElement(path, filterFn)
41+
) {
42+
return true
43+
}
44+
45+
if (iteration > 20) {
46+
throw new Error('transform-react-remove-prop-type: infinite loop detected.')
47+
}
48+
49+
let visited = false
50+
51+
path.traverse({
52+
ReturnStatement(path2) {
53+
// We have already found what we are looking for.
54+
if (visited) {
55+
return
56+
}
57+
58+
const argument = path2.get('argument')
59+
60+
// Nothing is returned
61+
if (!argument.node) {
62+
return
63+
}
64+
65+
if (isJSXElementOrReactCreateElement(path2, filterFn)) {
66+
visited = true
67+
return
68+
}
69+
70+
if (argument.node.type === 'CallExpression') {
71+
const name = argument.get('callee').node.name
72+
const binding = path.scope.getBinding(name)
73+
74+
if (!binding) {
75+
return
76+
}
77+
78+
// Prevents infinite traverse loop.
79+
if (binding.path[traversed]) {
80+
return
81+
}
82+
83+
binding.path[traversed] = true
84+
85+
if (
86+
isReturningJSXElement(binding.path, state, filterFn, iteration + 1)
87+
) {
88+
visited = true
89+
}
90+
}
91+
},
92+
})
93+
94+
return visited
95+
}
96+
97+
/**
98+
* IMPORTANT: This function assumes that the given path is a VariableDeclarator or FunctionDeclaration,
99+
* and will return false positives otherwise. If a more robust version is needed in the future,
100+
* see https://github.com/oliviertassinari/babel-plugin-transform-react-remove-prop-types/blob/master/src/isStatelessComponent.js
101+
*
102+
* Returns true if the given path is a React function component definition
103+
* @param {Path<VariableDeclarator | FunctionDeclaration>} path
104+
*/
105+
export function isFunctionComponent(
106+
path,
107+
state,
108+
types,
109+
mustConsumeStyledComponent = false // only return true if the component might render a styled component
110+
) {
111+
let filterFn
112+
if (mustConsumeStyledComponent) {
113+
filterFn = reactElementPath => {
114+
// look up the component and check if it's a styled component
115+
const componentId = reactElementPath.isJSXElement()
116+
? reactElementPath.node.openingElement.name
117+
: reactElementPath.node.arguments[0]
118+
const binding = reactElementPath.scope.getBinding(componentId.name)
119+
if (binding && binding.path.isVariableDeclarator()) {
120+
const { init } = binding.path.node
121+
if (
122+
types.isCallExpression(init) &&
123+
types.isCallExpression(init.callee) &&
124+
isStyled(types)(init.callee, state)
125+
) {
126+
return true
127+
}
128+
}
129+
return false
130+
}
131+
}
132+
133+
if (isReturningJSXElement(path, state, filterFn)) {
134+
return true
135+
}
136+
return false
137+
}

src/visitors/pure.js

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import annotateAsPure from '@babel/helper-annotate-as-pure'
22

33
import { usePureAnnotation } from '../utils/options'
44
import { isStyled, isPureHelper } from '../utils/detectors'
5+
import pureWrapStaticProps from './pureWrapStaticProps'
56

67
export default t => (path, state) => {
78
if (usePureAnnotation(state)) {
@@ -15,6 +16,11 @@ export default t => (path, state) => {
1516
path.parent.type === 'TaggedTemplateExpression'
1617
) {
1718
annotateAsPure(path)
19+
if (path.parent.type === 'VariableDeclarator') {
20+
// if static properties were added to the styled component (e.g. `defaultProps`),
21+
// also wrap it in an IIFE and add a PURE comment to the IIFE
22+
pureWrapStaticProps(t)(path.parentPath, state, true)
23+
}
1824
}
1925
}
2026
}

0 commit comments

Comments
 (0)