diff --git a/src/babel.js b/src/babel.js index 431cc3c..40deaf9 100644 --- a/src/babel.js +++ b/src/babel.js @@ -31,161 +31,177 @@ export default function ({types: t}) { expr.value ) + const makeStyledJsxTag = transformedCss => ( + t.JSXElement( + t.JSXOpeningElement( + t.JSXIdentifier(STYLE_COMPONENT), + [t.JSXAttribute( + t.JSXIdentifier(STYLE_COMPONENT_CSS), + t.JSXExpressionContainer(t.stringLiteral(transformedCss)) + )], + true + ), + null, + [] + ) + ) + return { inherits: jsx, visitor: { JSXOpeningElement(path, state) { - if (state.hasJSXStyle) { - if (state.ignoreClosing == null) { - // we keep a counter of elements inside so that we - // can keep track of when we exit the parent to reset state - // note: if we wished to add an option to turn off - // selectors to reach parent elements, it would suffice to - // set this to `1` and do an early return instead - state.ignoreClosing = 0 - } + if (!state.hasJSXStyle) { + return + } + + if (state.ignoreClosing === null) { + // we keep a counter of elements inside so that we + // can keep track of when we exit the parent to reset state + // note: if we wished to add an option to turn off + // selectors to reach parent elements, it would suffice to + // set this to `1` and do an early return instead + state.ignoreClosing = 0 + } - const el = path.node + const el = path.node - if (el.name && el.name.name !== 'style') { - for (const attr of el.attributes) { - if (attr.name === MARKUP_ATTRIBUTE) { - // avoid double attributes - return - } + if (el.name && el.name.name !== 'style') { + for (const attr of el.attributes) { + if (attr.name === MARKUP_ATTRIBUTE) { + // avoid double attributes + return } - - const attr = t.jSXAttribute( - t.JSXIdentifier(MARKUP_ATTRIBUTE), - t.JSXExpressionContainer(t.stringLiteral(state.jsxId)) - ) - el.attributes.push(attr) } - state.ignoreClosing++ - // next visit will be: JSXElement exit() + const attr = t.jSXAttribute( + t.JSXIdentifier(MARKUP_ATTRIBUTE), + t.JSXExpressionContainer(t.stringLiteral(state.jsxId)) + ) + el.attributes.push(attr) } + + state.ignoreClosing++ + // next visit will be: JSXElement exit() }, JSXElement: { enter(path, state) { - if (state.hasJSXStyle == null) { - const styles = findStyles(path.node.children) - - if (styles.length > 0) { - state.jsxId = '' - state.styles = [] - - for (const style of styles) { - // compute children excluding whitespace - const children = style.children.filter(c => ( - t.isJSXExpressionContainer(c) || - // ignore whitespace around the expression container - (t.isJSXText(c) && c.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.expression - - if (!t.isTemplateLiteral(child.expression) && - !t.isStringLiteral(child.expression)) { - throw path.buildCodeFrameError(`Expected a template ` + - `literal or String literal as the child of the ` + - `JSX Style tag (eg: ),` + - ` but got ${expression.type}`) - } - - const styleText = getExpressionText(expression) - const styleId = String(hash(styleText)) - - state.styles.push([ - styleId, - styleText, - expression.loc - ]) - } - - state.jsxId += hash(state.styles.map(s => s[1]).join('')) - state.hasJSXStyle = true - state.file.hasJSXStyle = true - // next visit will be: JSXOpeningElement - } else { - state.hasJSXStyle = false + if (state.hasJSXStyle !== null) { + return + } + + const styles = findStyles(path.node.children) + + if (styles.length === 0) { + state.hasJSXStyle = false + return + } + + state.jsxId = '' + state.styles = [] + + for (const style of styles) { + // compute children excluding whitespace + const children = style.children.filter(c => ( + t.isJSXExpressionContainer(c) || + // ignore whitespace around the expression container + (t.isJSXText(c) && c.value.trim() !== '') + )) + + if (children.length !== 1) { + throw path.buildCodeFrameError(`Expected one child under ` + + `JSX Style tag, but got ${style.children.length} ` + + `(eg: )`) } - } else if (state.hasJSXStyle) { - const el = path.node.openingElement - - if (el.name && el.name.name === 'style') { - // we replace styles with the function call - const [id, css, loc] = state.styles.shift() - - const skipTransform = el.attributes.some(attr => ( - attr.name.name === GLOBAL_ATTRIBUTE - )) - - const useSourceMaps = Boolean(state.file.opts.sourceMaps) - let transformedCss = css - - if (!skipTransform) { - if (useSourceMaps) { - const filename = state.file.opts.sourceFileName - const generator = new SourceMapGenerator({ - file: filename, - sourceRoot: state.file.opts.sourceRoot - }) - generator.setSourceContent(filename, state.file.code) - transformedCss = [ - transform(id, css, generator, loc.start, filename), - convert - .fromObject(generator) - .toComment({multiline: true}), - `/*@ sourceURL=${filename} */` - ].join('\n') - } else { - transformedCss = transform(id, css) - } - } - - path.replaceWith( - t.JSXElement( - t.JSXOpeningElement( - t.JSXIdentifier(STYLE_COMPONENT), - [ - t.JSXAttribute( - t.JSXIdentifier(STYLE_COMPONENT_CSS), - t.JSXExpressionContainer(t.stringLiteral(transformedCss)) - ) - ], - true - ), - null, - [] - ) - ) + + 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.expression + + if (!t.isTemplateLiteral(child.expression) && + !t.isStringLiteral(child.expression)) { + throw path.buildCodeFrameError(`Expected a template ` + + `literal or String literal as the child of the ` + + `JSX Style tag (eg: ),` + + ` but got ${expression.type}`) + } + + const styleText = getExpressionText(expression) + const styleId = String(hash(styleText)) + + state.styles.push([ + styleId, + styleText, + expression.loc + ]) } + + state.jsxId += hash(state.styles.map(s => s[1]).join('')) + state.hasJSXStyle = true + state.file.hasJSXStyle = true + // next visit will be: JSXOpeningElement }, exit(path, state) { if (state.hasJSXStyle && !--state.ignoreClosing) { state.hasJSXStyle = null state.skipTransform = false } + + if (!state.hasJSXStyle) { + return + } + + const el = path.node.openingElement + + if (!el.name || el.name.name !== 'style') { + return + } + + // we replace styles with the function call + const [id, css, loc] = state.styles.shift() + + const skipTransform = el.attributes.some(attr => ( + attr.name.name === GLOBAL_ATTRIBUTE + )) + + if (skipTransform) { + path.replaceWith(makeStyledJsxTag(css)) + return + } + + const useSourceMaps = Boolean(state.file.opts.sourceMaps) + let transformedCss + + if (useSourceMaps) { + const filename = state.file.opts.sourceFileName + const generator = new SourceMapGenerator({ + file: filename, + sourceRoot: state.file.opts.sourceRoot + }) + generator.setSourceContent(filename, state.file.code) + transformedCss = [ + transform(id, css, generator, loc.start, filename), + convert + .fromObject(generator) + .toComment({multiline: true}), + `/*@ sourceURL=${filename} */` + ].join('\n') + } else { + transformedCss = transform(id, css) + } + + path.replaceWith(makeStyledJsxTag(transformedCss)) } }, Program: { enter(path, state) { + state.hasJSXStyle = null + state.ignoreClosing = null state.file.hasJSXStyle = false }, exit({node, scope}, state) {