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

Fix tree-shaking issues #248

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/demo
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"]
}
]
}
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
}
}
]
}
3 changes: 3 additions & 0 deletions demo/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
plugins: [[require('../lib'), { pure: true }]],
}
12 changes: 12 additions & 0 deletions demo/demoComponents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react'
import styled from 'styled-components'

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

export function MyComponent() {
return React.createElement(Wrapper)
}
MyComponent.displayName = 'FancyName1'
MyComponent.defaultProps = {}
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"
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@
"scripts": {
"clean": "rimraf lib",
"style": "prettier --write src/**/*.js",
"prebuild": "yarn clean",
"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
13 changes: 13 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import syntax from 'babel-plugin-syntax-jsx'
import pureAnnotation from './visitors/pure'
import pureInlineCalculations from './visitors/pureInlineCalculations'
import minify from './visitors/minify'
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 {
Expand All @@ -28,11 +30,22 @@ export default function({ types: t }) {
pureAnnotation(t)(path, state)
},
TaggedTemplateExpression(path, state) {
pureInlineCalculations(t)(path, state)
minify(t)(path, state)
displayNameAndId(t)(path, state)
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)
},
},
}
}
41 changes: 34 additions & 7 deletions src/utils/detectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,38 @@ export const importLocalName = (name, state, bypassCache = false) => {
return localName
}

export const isStyled = t => (tag, state) => {
// cache styled tags that we've already found from previous calls to isStyled()
const visitedStyledTags = new WeakSet()

export const isStyled = t => (tag, state, includeIIFE = false) => {
if (includeIIFE) {
// check to see if this is an IIFE wrapper created by pureWrapStaticProps()
// that replaced what was originally a `styled` call
if (t.isArrowFunctionExpression(tag) && tag.body && tag.body.body[0]) {
const statement = tag.body.body[0]
if (t.isVariableDeclaration(statement)) {
const callee = statement.declarations[0].init.callee
if (callee && isStyled(t)(callee, state)) {
return true
}
}
}
}

if (
t.isCallExpression(tag) &&
t.isMemberExpression(tag.callee) &&
tag.callee.property.name !== 'default' /** ignore default for #93 below */
) {
// styled.something()
return isStyled(t)(tag.callee.object, state)
} else {
return (
(t.isMemberExpression(tag) &&
tag.object.name === importLocalName('default', state)) ||
}
if (visitedStyledTags.has(tag)) {
return true
}
const ret = Boolean(
(t.isMemberExpression(tag) &&
tag.object.name === importLocalName('default', state)) ||
(t.isCallExpression(tag) &&
tag.callee.name === importLocalName('default', state)) ||
/**
Expand All @@ -87,8 +107,11 @@ export const isStyled = t => (tag, state) => {
t.isMemberExpression(tag.callee) &&
tag.callee.property.name === 'default' &&
tag.callee.object.name === state.styledRequired)
)
)
if (ret) {
visitedStyledTags.add(tag)
}
return ret
}

export const isCSSHelper = t => (tag, state) =>
Expand All @@ -108,10 +131,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'
136 changes: 136 additions & 0 deletions src/utils/isFunctionComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* 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'
) {
if (visited) {
return
}
visited = filterFn ? filterFn(path2) : true
}
},
JSXElement(path2) {
if (visited) {
return
}
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 Error('babel-plugin-styled-components: 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 (isStyled(types)(init.callee, state, true /* includeIIFE */)) {
return true
}
}
return false
}
}

return isReturningJSXElement(path, state, filterFn)
}
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
39 changes: 39 additions & 0 deletions src/visitors/pureInlineCalculations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import annotateAsPure from '@babel/helper-annotate-as-pure'

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

/*
Calculations inside a template literal passed to styled-components can break tree-shaking when using terser, e.g.:
`
font-size: ${helper(2)};
width: ${`50${widthUnits}`};
`
So we add PURE comments to any such calls.

NB: This means that any helper functions used inside your styles should be side-effect free.
In practical terms it's probably only an issue if the side-effect affects whether or not the component
should be included in your bundle, but avoiding side effects in helper functions is a good practice
anyhow.

Another note: Assuming the `transpileTemplateLiterals` option is enabled, it seems that rollup's tree-shaking
algorithm works fine without running this function. It seems to only be needed for terser.
*/
export default t => (path, state) => {
if (!usePureAnnotation(state)) {
return
}
if (isStyled(t)(path.node.tag, state)) {
// loop through any ${} expressions inside the template
for (const expr of path.node.quasi.expressions) {
if (
t.isCallExpression(expr) ||
// template literals (which get transpiled to function calls when transpiling to ES5)
// can break tree-shaking too
t.isTemplateLiteral(expr)
) {
annotateAsPure(expr)
}
}
}
}