Skip to content

Commit

Permalink
added transformations to fix tree-shaking issues when using static pr…
Browse files Browse the repository at this point in the history
…operties (styled-components#245)
  • Loading branch information
mbrowne committed Sep 24, 2019
1 parent ad289a0 commit 62b694c
Show file tree
Hide file tree
Showing 14 changed files with 384 additions and 32 deletions.
17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Run demo",
"preLaunchTask": "npm: build",
"program": "${workspaceFolder}/node_modules/.bin/babel",
"cwd": "${workspaceFolder}/demo",
"args": ["demoComponents.js"]
}
]
}
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}
15 changes: 15 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "build",
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
6 changes: 6 additions & 0 deletions demo/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: [
'@babel/plugin-proposal-class-properties',
[require('../lib'), { pure: true }],
],
}
7 changes: 7 additions & 0 deletions demo/demoComponents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import styled from 'styled-components'

export const Component2 = styled.div`
color: turquoise;
`
// If this line is removed, then tree-shaking works correctly
Component2.displayName = 'FancyName2'
21 changes: 21 additions & 0 deletions demo/demoComponents.js.bak
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react'
import styled from 'styled-components'

const Wrapper = styled.div`
color: blue;
`

export function FunctionComponent() {
return React.createElement(Wrapper)
}
FunctionComponent.displayName = 'FancyName1'
FunctionComponent.defaultProps = {}

export class ClassComponent extends React.Component {
static displayName = 'FancyName2'
static defaultProps = {}

render() {
return React.createElement(Wrapper)
}
}
6 changes: 6 additions & 0 deletions demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "babel-pugin-styled-components-demo",
"scripts": {
"transpile": "../node_modules/.bin/babel demoComponents.js"
}
}
8 changes: 8 additions & 0 deletions demo/tmp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export class ClassComponent extends React.Component {
static displayName = 'FancyName2'
static defaultProps = {}

render() {
return React.createElement(Wrapper)
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"clean": "rimraf lib",
"style": "prettier --write src/**/*.js",
"build": "babel src -d lib",
"watch": "yarn build -w",
"test": "jest",
"test:watch": "npm run test -- --watch",
"prepublish": "npm run clean && npm run build"
Expand Down
35 changes: 24 additions & 11 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,26 @@ import displayNameAndId from './visitors/displayNameAndId'
import templateLiterals from './visitors/templateLiterals'
import assignStyledRequired from './visitors/assignStyledRequired'
import transpileCssProp from './visitors/transpileCssProp'
import pureWrapStaticProps from './visitors/pureWrapStaticProps'

export default function({ types: t }) {
return {
inherits: syntax,
visitor: {
Program(path, state) {
path.traverse(
{
JSXAttribute(path, state) {
transpileCssProp(t)(path, state)
Program: {
enter(path, state) {
path.traverse(
{
JSXAttribute(path, state) {
transpileCssProp(t)(path, state)
},
VariableDeclarator(path, state) {
assignStyledRequired(t)(path, state)
},
},
VariableDeclarator(path, state) {
assignStyledRequired(t)(path, state)
},
},
state
)
state
)
},
},
CallExpression(path, state) {
displayNameAndId(t)(path, state)
Expand All @@ -33,6 +36,16 @@ export default function({ types: t }) {
templateLiterals(t)(path, state)
pureAnnotation(t)(path, state)
},
FunctionDeclaration(path, state) {
// technically this is more like,
// "mark pure if it's a function component that consumes a styled component and also has static properties",
// but that's rather long ;)
pureWrapStaticProps(t)(path, state)
},
VariableDeclarator(path, state) {
// same thing for arrow functions
pureWrapStaticProps(t)(path, state)
},
},
}
}
46 changes: 25 additions & 21 deletions src/utils/detectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,28 +64,28 @@ export const isStyled = t => (tag, state) => {
// styled.something()
return isStyled(t)(tag.callee.object, state)
} else {
return (
return Boolean(
(t.isMemberExpression(tag) &&
tag.object.name === importLocalName('default', state)) ||
(t.isCallExpression(tag) &&
tag.callee.name === importLocalName('default', state)) ||
/**
* #93 Support require()
* styled-components might be imported using a require()
* call and assigned to a variable of any name.
* - styled.default.div``
* - styled.default.something()
*/
(state.styledRequired &&
t.isMemberExpression(tag) &&
t.isMemberExpression(tag.object) &&
tag.object.property.name === 'default' &&
tag.object.object.name === state.styledRequired) ||
(state.styledRequired &&
t.isCallExpression(tag) &&
t.isMemberExpression(tag.callee) &&
tag.callee.property.name === 'default' &&
tag.callee.object.name === state.styledRequired)
(t.isCallExpression(tag) &&
tag.callee.name === importLocalName('default', state)) ||
/**
* #93 Support require()
* styled-components might be imported using a require()
* call and assigned to a variable of any name.
* - styled.default.div``
* - styled.default.something()
*/
(state.styledRequired &&
t.isMemberExpression(tag) &&
t.isMemberExpression(tag.object) &&
tag.object.property.name === 'default' &&
tag.object.object.name === state.styledRequired) ||
(state.styledRequired &&
t.isCallExpression(tag) &&
t.isMemberExpression(tag.callee) &&
tag.callee.property.name === 'default' &&
tag.callee.object.name === state.styledRequired)
)
}
}
Expand All @@ -107,10 +107,14 @@ export const isWithThemeHelper = t => (tag, state) =>
t.isIdentifier(tag) && tag.name === importLocalName('withTheme', state)

export const isHelper = t => (tag, state) =>
isCSSHelper(t)(tag, state) || isKeyframesHelper(t)(tag, state) || isWithThemeHelper(t)(tag, state)
isCSSHelper(t)(tag, state) ||
isKeyframesHelper(t)(tag, state) ||
isWithThemeHelper(t)(tag, state)

export const isPureHelper = t => (tag, state) =>
isCSSHelper(t)(tag, state) ||
isKeyframesHelper(t)(tag, state) ||
isCreateGlobalStyleHelper(t)(tag, state) ||
isWithThemeHelper(t)(tag, state)

export { isFunctionComponent } from './isFunctionComponent'
137 changes: 137 additions & 0 deletions src/utils/isFunctionComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* Adapted from https://github.com/oliviertassinari/babel-plugin-transform-react-remove-prop-types/blob/master/src/isStatelessComponent.js
*/

import { isStyled } from '../utils/detectors'

const traversed = Symbol('traversed')

function isJSXElementOrReactCreateElement(
path,
filterFn = null // optional filter function to match only certain kinds of React elements
) {
let visited = false

path.traverse({
CallExpression(path2) {
const callee = path2.get('callee')

if (
callee.matchesPattern('React.createElement') ||
callee.matchesPattern('React.cloneElement') ||
callee.node.name === 'cloneElement'
) {
visited = filterFn ? filterFn(path2) : true
}
},
JSXElement(path2) {
visited = filterFn ? filterFn(path2) : true
},
})

return visited
}

function isReturningJSXElement(path, state, filterFn = null, iteration = 0) {
// Early exit for ArrowFunctionExpressions, there is no ReturnStatement node.
if (
path.node.init &&
path.node.init.body &&
isJSXElementOrReactCreateElement(path, filterFn)
) {
return true
}

if (iteration > 20) {
throw new Error('transform-react-remove-prop-type: infinite loop detected.')
}

let visited = false

path.traverse({
ReturnStatement(path2) {
// We have already found what we are looking for.
if (visited) {
return
}

const argument = path2.get('argument')

// Nothing is returned
if (!argument.node) {
return
}

if (isJSXElementOrReactCreateElement(path2, filterFn)) {
visited = true
return
}

if (argument.node.type === 'CallExpression') {
const name = argument.get('callee').node.name
const binding = path.scope.getBinding(name)

if (!binding) {
return
}

// Prevents infinite traverse loop.
if (binding.path[traversed]) {
return
}

binding.path[traversed] = true

if (
isReturningJSXElement(binding.path, state, filterFn, iteration + 1)
) {
visited = true
}
}
},
})

return visited
}

/**
* IMPORTANT: This function assumes that the given path is a VariableDeclarator or FunctionDeclaration,
* and will return false positives otherwise. If a more robust version is needed in the future,
* see https://github.com/oliviertassinari/babel-plugin-transform-react-remove-prop-types/blob/master/src/isStatelessComponent.js
*
* Returns true if the given path is a React function component definition
* @param {Path<VariableDeclarator | FunctionDeclaration>} path
*/
export function isFunctionComponent(
path,
state,
types,
mustConsumeStyledComponent = false // only return true if the component might render a styled component
) {
let filterFn
if (mustConsumeStyledComponent) {
filterFn = reactElementPath => {
// look up the component and check if it's a styled component
const componentId = reactElementPath.isJSXElement()
? reactElementPath.node.openingElement.name
: reactElementPath.node.arguments[0]
const binding = reactElementPath.scope.getBinding(componentId.name)
if (binding && binding.path.isVariableDeclarator()) {
const { init } = binding.path.node
if (
types.isCallExpression(init) &&
types.isCallExpression(init.callee) &&
isStyled(types)(init.callee, state)
) {
return true
}
}
return false
}
}

if (isReturningJSXElement(path, state, filterFn)) {
return true
}
return false
}
6 changes: 6 additions & 0 deletions src/visitors/pure.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import annotateAsPure from '@babel/helper-annotate-as-pure'

import { usePureAnnotation } from '../utils/options'
import { isStyled, isPureHelper } from '../utils/detectors'
import pureWrapStaticProps from './pureWrapStaticProps'

export default t => (path, state) => {
if (usePureAnnotation(state)) {
Expand All @@ -15,6 +16,11 @@ export default t => (path, state) => {
path.parent.type === 'TaggedTemplateExpression'
) {
annotateAsPure(path)
if (path.parent.type === 'VariableDeclarator') {
// if static properties were added to the styled component (e.g. `defaultProps`),
// also wrap it in an IIFE and add a PURE comment to the IIFE
pureWrapStaticProps(t)(path.parentPath, state, true)
}
}
}
}
Expand Down

0 comments on commit 62b694c

Please sign in to comment.