From adc9ce0a23d9ec96f9dce9a51a8c94d2a3a80e1c Mon Sep 17 00:00:00 2001 From: Giuseppe Gurgone Date: Wed, 1 Feb 2017 17:10:57 +0100 Subject: [PATCH 1/4] Transpile external css --- src/babel.js | 66 ++++++++++++++++++----- test/fixtures/external-styles.js | 7 +++ test/fixtures/external-styles.out.js | 6 +++ test/fixtures/with-external-styles.js | 6 +++ test/fixtures/with-external-styles.out.js | 0 test/index.js | 12 +++++ 6 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 test/fixtures/external-styles.js create mode 100644 test/fixtures/external-styles.out.js create mode 100644 test/fixtures/with-external-styles.js create mode 100644 test/fixtures/with-external-styles.out.js diff --git a/src/babel.js b/src/babel.js index 8124b3d0..05acfccb 100644 --- a/src/babel.js +++ b/src/babel.js @@ -21,6 +21,21 @@ 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 isStyledJsx = ({node: el}) => ( t.isJSXElement(el) && el.openingElement.name.name === 'style' && @@ -32,8 +47,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 + } else { + return [] + } } return path.get('children').filter(isStyledJsx) @@ -125,7 +147,7 @@ export default function ({types: t}) { } } - const makeStyledJsxTag = (id, transformedCss, isTemplateLiteral) => { + const getInjectableCss = (transformedCss, isTemplateLiteral) => { let css if (isTemplateLiteral) { // build the expression from transformedCss @@ -143,7 +165,11 @@ export default function ({types: t}) { css = t.stringLiteral(transformedCss) } - return t.JSXElement( + return css + } + + const makeStyledJsxTag = (id, css) => ( + t.JSXElement( t.JSXOpeningElement( t.JSXIdentifier(STYLE_COMPONENT), [ @@ -161,7 +187,7 @@ export default function ({types: t}) { null, [] ) - } + ) return { inherits: jsx, @@ -216,13 +242,13 @@ 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() || @@ -261,9 +287,11 @@ export default function ({types: t}) { ` but got ${expression.type}`) } - // Validate MemberExpressions and Identifiers - // to ensure that are constants not defined in the closest scope - child.get('expression').traverse(validateExpressionVisitor, scope) + if (!isExternal) { + // 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) @@ -282,12 +310,13 @@ 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 } @@ -295,7 +324,7 @@ export default function ({types: t}) { const [id, css, loc] = state.styles.shift() if (isGlobal) { - path.replaceWith(makeStyledJsxTag(id, css.source || css, css.modified)) + path.replaceWith(makeStyledJsxTag(id, getInjectableCss(css.source || css, css.modified))) return } @@ -342,8 +371,21 @@ 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) ) } }, diff --git a/test/fixtures/external-styles.js b/test/fixtures/external-styles.js new file mode 100644 index 00000000..83e5885b --- /dev/null +++ b/test/fixtures/external-styles.js @@ -0,0 +1,7 @@ +const foo = 'red' + +export default ( + +) diff --git a/test/fixtures/external-styles.out.js b/test/fixtures/external-styles.out.js new file mode 100644 index 00000000..e8a8758f --- /dev/null +++ b/test/fixtures/external-styles.out.js @@ -0,0 +1,6 @@ +const foo = 'red'; + +export default { + 'css': `p[data-jsx="3533501187"] {color: ${foo} }`, + 'id': '458985232' +}; diff --git a/test/fixtures/with-external-styles.js b/test/fixtures/with-external-styles.js new file mode 100644 index 00000000..1f862ea4 --- /dev/null +++ b/test/fixtures/with-external-styles.js @@ -0,0 +1,6 @@ +export default () => ( +
+

test

+ +
+) diff --git a/test/fixtures/with-external-styles.out.js b/test/fixtures/with-external-styles.out.js new file mode 100644 index 00000000..e69de29b diff --git a/test/index.js b/test/index.js index 181a2c47..b94f2cdc 100644 --- a/test/index.js +++ b/test/index.js @@ -94,6 +94,18 @@ test('works with non styled-jsx styles', async t => { t.is(code, out.trim()) }) +test('works on external styles', async t => { + const {code} = await transform('./fixtures/external-styles.js') + const out = await read('./fixtures/external-styles.out.js') + t.is(code, out.trim()) +}) + +test('can use external styles', async t => { + const {code} = await transform('./fixtures/with-external-styles.js') + const out = await read('./fixtures/with-external-styles.out.js') + t.is(code, out.trim()) +}) + test('throws when using `props` or constants ' + 'defined in the closest scope', async t => { [1, 2, 3, 4].forEach(i => { From 6e5f76c429cd41062bdbe8a07cbcf50bfa1bb52c Mon Sep 17 00:00:00 2001 From: Giuseppe Gurgone Date: Wed, 1 Feb 2017 22:16:42 +0100 Subject: [PATCH 2/4] Add support for imports --- src/babel.js | 216 ++++++++++++++++------ test/fixtures/with-external-styles.js | 3 +- test/fixtures/with-external-styles.out.js | 7 + 3 files changed, 164 insertions(+), 62 deletions(-) diff --git a/src/babel.js b/src/babel.js index 05acfccb..cf587c16 100644 --- a/src/babel.js +++ b/src/babel.js @@ -36,6 +36,33 @@ export default function ({types: t}) { ) } + const getImportPath = (path) => { + const attr = path + .get('openingElement') + .get('attributes') + .filter( + (path) => path.get('name').node.name === 'href' + )[0] + + return attr && attr.get('value').node.value + } + + const getImport = ([styleText, styleId, importPath]) => ( + 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' && @@ -168,26 +195,38 @@ export default function ({types: t}) { return css } - const makeStyledJsxTag = (id, css) => ( - 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, - [] + 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, + [] + ) ) - ) + } return { inherits: jsx, @@ -249,57 +288,87 @@ export default function ({types: t}) { } state.styles = [] - const 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: )`) - } - const child = children[0] - - if (!t.isJSXExpressionContainer(child)) { - throw path.buildCodeFrameError(`Expected a child of ` + - `type JSXExpressionContainer under JSX Style tag ` + - `(eg: ), got ${child.type}`) - } - - const expression = child.get('expression') + let scope + if (!isExternal) { + scope = (path.findParent(path => ( + path.isFunctionDeclaration() || + path.isArrowFunctionExpression() || + path.isClassMethod() + )) || path).scope + } - 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: ),` + - ` but got ${expression.type}`) + for (const style of styles) { + 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: )`) + } + + child = children[0] + + if (!t.isJSXExpressionContainer(child)) { + throw path.buildCodeFrameError(`Expected a child of ` + + `type JSXExpressionContainer under JSX Style tag ` + + `(eg: ), 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: ),` + + ` but got ${expression.type}`) + } } - if (!isExternal) { + 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 styleText = getExpressionText(expression) - const styleId = hash(styleText.source || styleText) + 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 + } state.styles.push([ styleId, styleText, - expression.node.loc + loc, + importHash ]) } @@ -321,10 +390,22 @@ export default function ({types: t}) { } // 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, getInjectableCss(css.source || css, css.modified))) + path.replaceWith( + makeStyledJsxTag(id, getInjectableCss(css.source || css, css.modified)) + ) return } @@ -376,8 +457,14 @@ export default function ({types: t}) { if (isExternal) { path.replaceWith( t.objectExpression([ - t.objectProperty(t.stringLiteral('css'), transformedCss), - t.objectProperty(t.stringLiteral('id'), t.stringLiteral(String(id))), + t.objectProperty( + t.stringLiteral('css'), + transformedCss + ), + t.objectProperty( + t.stringLiteral('id'), + t.stringLiteral(String(id)) + ), ]) ) state.file.hasJSXStyle = false @@ -394,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))) { @@ -406,6 +494,12 @@ export default function ({types: t}) { ) node.body.unshift(importDeclaration) + + state.externalStyles.forEach(style => ( + node.body.unshift( + getImport(style) + ) + )) } } } diff --git a/test/fixtures/with-external-styles.js b/test/fixtures/with-external-styles.js index 1f862ea4..8760cf55 100644 --- a/test/fixtures/with-external-styles.js +++ b/test/fixtures/with-external-styles.js @@ -1,6 +1,7 @@ export default () => (

test

- + + -