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

[wip] Transpile external css #100

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
260 changes: 198 additions & 62 deletions src/babel.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,48 @@ export default function ({types: t}) {
name && name.name === GLOBAL_ATTRIBUTE
))

const isExternalFile = ({parentPath}) => {
if (t.isExportDefaultDeclaration(parentPath.node)) {
return true
}

let path = parentPath.parentPath
if (t.isExpressionStatement(path.node)) {
path = path.get('expression')
}
return (
t.isAssignmentExpression(path.node) &&
path.get('left').getSource() === 'module.exports'
)
}

const getImportPath = path => {
const attr = path
.get('openingElement')
.get('attributes')
.filter(
path => path.get('name').node.name === 'src'
)[0]

return attr && attr.get('value').node.value
}

const getImport = ([styleText, styleId, importPath]) => (
Copy link
Collaborator Author

@giuseppeg giuseppeg Feb 1, 2017

Choose a reason for hiding this comment

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

need to fix this (and maybe call it makeImportExternal) because transpiled (external) stylesheets are exporting a single object (default)

t.importDeclaration(
[
t.importSpecifier(
t.Identifier(styleText),
t.Identifier('css')
),
t.importSpecifier(
t.Identifier(styleId),
t.Identifier('id')
)
],
t.stringLiteral(importPath)
)
)

const isStyledJsx = ({node: el}) => (
t.isJSXElement(el) &&
el.openingElement.name.name === 'style' &&
Expand All @@ -32,8 +74,15 @@ export default function ({types: t}) {
const findStyles = path => {
if (isStyledJsx(path)) {
const {node} = path
return isGlobalEl(node.openingElement) ?
[path] : []
if (isGlobalEl(node.openingElement)) {
return [path]
} else if (isExternalFile(path)) {
const ret = [path]
ret.__isExternal = true
return ret
}

return []
}

return path.get('children').filter(isStyledJsx)
Expand Down Expand Up @@ -125,7 +174,7 @@ export default function ({types: t}) {
}
}

const makeStyledJsxTag = (id, transformedCss, isTemplateLiteral) => {
const getInjectableCss = (transformedCss, isTemplateLiteral) => {
let css
if (isTemplateLiteral) {
// build the expression from transformedCss
Expand All @@ -143,23 +192,39 @@ export default function ({types: t}) {
css = t.stringLiteral(transformedCss)
}

return t.JSXElement(
t.JSXOpeningElement(
t.JSXIdentifier(STYLE_COMPONENT),
[
t.JSXAttribute(
t.JSXIdentifier(STYLE_COMPONENT_ID),
t.JSXExpressionContainer(t.numericLiteral(id))
),
t.JSXAttribute(
t.JSXIdentifier(STYLE_COMPONENT_CSS),
t.JSXExpressionContainer(css)
)
],
true
),
null,
[]
return css
}

const makeStyledJsxTag = (id, css) => {
const isImported = typeof id === 'string'

return (
t.JSXElement(
t.JSXOpeningElement(
t.JSXIdentifier(STYLE_COMPONENT),
[
t.JSXAttribute(
t.JSXIdentifier(STYLE_COMPONENT_ID),
t.JSXExpressionContainer(
isImported ?
t.identifier(id) :
t.numericLiteral(id)
)
),
t.JSXAttribute(
t.JSXIdentifier(STYLE_COMPONENT_CSS),
t.JSXExpressionContainer(
isImported ?
t.identifier(css) :
css
)
)
],
true
),
null,
[]
)
)
}

Expand Down Expand Up @@ -216,62 +281,94 @@ export default function ({types: t}) {
}

const styles = findStyles(path)
const isExternal = styles.__isExternal

if (styles.length === 0) {
return
}

state.styles = []

const scope = (path.findParent(path => (
path.isFunctionDeclaration() ||
path.isArrowFunctionExpression() ||
path.isClassMethod()
)) || path).scope
let scope
if (!isExternal) {
scope = (path.findParent(path => (
path.isFunctionDeclaration() ||
path.isArrowFunctionExpression() ||
path.isClassMethod()
)) || path).scope
}

for (const style of styles) {
// compute children excluding whitespace
const children = style.get('children').filter(c => (
t.isJSXExpressionContainer(c.node) ||
// ignore whitespace around the expression container
(t.isJSXText(c.node) && c.node.value.trim() !== '')
))

if (children.length !== 1) {
throw path.buildCodeFrameError(`Expected one child under ` +
`JSX Style tag, but got ${style.children.length} ` +
`(eg: <style jsx>{\`hi\`}</style>)`)
const importPath = !isExternal && getImportPath(style)
let child
let expression

if (!importPath) {
// compute children excluding whitespace
const children = style.get('children').filter(c => (
t.isJSXExpressionContainer(c.node) ||
// ignore whitespace around the expression container
(t.isJSXText(c.node) && c.node.value.trim() !== '')
))

if (children.length !== 1) {
throw path.buildCodeFrameError(`Expected one child under ` +
`JSX Style tag, but got ${style.children.length} ` +
`(eg: <style jsx>{\`hi\`}</style>)`)
}

child = children[0]

if (!t.isJSXExpressionContainer(child)) {
throw path.buildCodeFrameError(`Expected a child of ` +
`type JSXExpressionContainer under JSX Style tag ` +
`(eg: <style jsx>{\`hi\`}</style>), got ${child.type}`)
}

expression = child.get('expression')

if (!t.isTemplateLiteral(expression) &&
!t.isStringLiteral(expression)) {
throw path.buildCodeFrameError(`Expected a template ` +
`literal or String literal as the child of the ` +
`JSX Style tag (eg: <style jsx>{\`some css\`}</style>),` +
` but got ${expression.type}`)
}
}

const child = children[0]

if (!t.isJSXExpressionContainer(child)) {
throw path.buildCodeFrameError(`Expected a child of ` +
`type JSXExpressionContainer under JSX Style tag ` +
`(eg: <style jsx>{\`hi\`}</style>), got ${child.type}`)
if (!isExternal && !importPath) {
// Validate MemberExpressions and Identifiers
// to ensure that are constants not defined in the closest scope
child.get('expression').traverse(validateExpressionVisitor, scope)
}

const expression = child.get('expression')

if (!t.isTemplateLiteral(expression) &&
!t.isStringLiteral(expression)) {
throw path.buildCodeFrameError(`Expected a template ` +
`literal or String literal as the child of the ` +
`JSX Style tag (eg: <style jsx>{\`some css\`}</style>),` +
` but got ${expression.type}`)
let styleText
let styleId
let loc
let importHash

if (importPath) {
importHash = hash(importPath)
styleText = `_${importHash}_css`
styleId = `_${importHash}_id`
loc = 0

state.externalStyles.push([
styleText,
styleId,
importPath
])
} else {
styleText = getExpressionText(expression)
styleId = hash(styleText.source || styleText)
loc = expression.node.loc
}

// Validate MemberExpressions and Identifiers
// to ensure that are constants not defined in the closest scope
child.get('expression').traverse(validateExpressionVisitor, scope)

const styleText = getExpressionText(expression)
const styleId = hash(styleText.source || styleText)

state.styles.push([
styleId,
styleText,
expression.node.loc
loc,
importHash
])
}

Expand All @@ -282,20 +379,33 @@ export default function ({types: t}) {
},
exit(path, state) {
const isGlobal = isGlobalEl(path.node.openingElement)
const isExternal = isExternalFile(path)

if (state.hasJSXStyle && (!--state.ignoreClosing && !isGlobal)) {
state.hasJSXStyle = null
}

if (!state.hasJSXStyle || !isStyledJsx(path)) {
if (!isExternal && (!state.hasJSXStyle || !isStyledJsx(path))) {
return
}

// we replace styles with the function call
const [id, css, loc] = state.styles.shift()
const [id, css, loc, importHash] = state.styles.shift()

if (importHash) {
path.replaceWith(
makeStyledJsxTag(
id,
css
)
)
return
}

if (isGlobal) {
path.replaceWith(makeStyledJsxTag(id, css.source || css, css.modified))
path.replaceWith(
makeStyledJsxTag(id, getInjectableCss(css.source || css, css.modified))
)
return
}

Expand Down Expand Up @@ -342,8 +452,27 @@ export default function ({types: t}) {
)
}

transformedCss = getInjectableCss(transformedCss, css.modified)

if (isExternal) {
path.replaceWith(
t.objectExpression([
t.objectProperty(
t.stringLiteral('css'),
transformedCss
),
t.objectProperty(
t.stringLiteral('id'),
t.stringLiteral(String(id))
)
])
)
state.file.hasJSXStyle = false
return
}

path.replaceWith(
makeStyledJsxTag(id, transformedCss, css.modified)
makeStyledJsxTag(id, transformedCss)
)
}
},
Expand All @@ -352,6 +481,7 @@ export default function ({types: t}) {
state.hasJSXStyle = null
state.ignoreClosing = null
state.file.hasJSXStyle = false
state.externalStyles = []
},
exit({node, scope}, state) {
if (!(state.file.hasJSXStyle && !scope.hasBinding(STYLE_COMPONENT))) {
Expand All @@ -364,6 +494,12 @@ export default function ({types: t}) {
)

node.body.unshift(importDeclaration)

state.externalStyles.forEach(style => (
node.body.unshift(
getImport(style)
)
))
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions test/fixtures/external-styles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const foo = 'red'

export default (
<style jsx>{`
p { color: ${foo} }
`}</style>
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this syntax mean we can't import others from imported styles ? 🤔

Copy link
Contributor

@arunoda arunoda Feb 4, 2017

Choose a reason for hiding this comment

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

Why don't we simply for a template string like:

const foo = 'red'

export default `
  p { color: ${foo} }
`

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This would be the best api however is not that easy to achieve this because in the consumer file (the one that imports this) we need to hash `p { color: ${foo} }` and once we have the hash we need to transform the css (add the prefix to it etc).

)
6 changes: 6 additions & 0 deletions test/fixtures/external-styles.out.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const foo = 'red';

export default {
'css': `p[data-jsx="3533501187"] {color: ${foo} }`,
'id': '458985232'
};
7 changes: 7 additions & 0 deletions test/fixtures/with-external-styles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default () => (
<div>
<p>test</p>
<style jsx>{`p {color: red }`}</style>
<style jsx src="./external-styles.js" />
</div>
)
7 changes: 7 additions & 0 deletions test/fixtures/with-external-styles.out.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { css as _1290483527_css, id as _1290483527_id } from "./external-styles.js";
import _JSXStyle from "styled-jsx/style";
export default (() => <div data-jsx={2454282849}>
<p data-jsx={2454282849}>test</p>
<_JSXStyle styleId={1046845255} css={"p[data-jsx=\"2454282849\"] {color: red }"} />
<_JSXStyle styleId={_1290483527_id} css={_1290483527_css} />
</div>);
Loading