UNPKG

styled-jsx

Version:

Full, scoped and component-friendly CSS support for JSX (SSR+browser)

159 lines (140 loc) 5.16 kB
import jsx from 'babel-plugin-syntax-jsx' import murmurHash from './lib/murmurhash2' import transform from './lib/style-transform' const STYLE_ATTRIBUTE = 'jsx' const MARKUP_ATTRIBUTE = 'data-jsx' const INJECT_METHOD = '_jsxStyleInject' export default function ({ types: t }) { const findStyles = (children) => ( children.filter((el) => ( t.isJSXElement(el) && el.openingElement.name.name === 'style' && el.openingElement.attributes.some((attr) => ( attr.name.name === STYLE_ATTRIBUTE )) )) ) const getExpressionText = (expr) => ( t.isTemplateLiteral(expr) ? expr.quasis[0].value.raw // assume string literal : expr.value ) return { inherits: jsx, visitor: { JSXOpeningElement (path, state) { if (state.hasJSXStyle) { if (null == state.ignoreClosing) { // this flag has a two-fold purpose: // - ignore the opening tag of the parent element // of the style tag, since we don't want to add // the attribute to that one // - keep a counter of elements inside so that we // can keep track of when we exit the parent // to reset state state.ignoreClosing = 1 return } const el = path.node if (el.name && 'style' !== el.name.name) { 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() } }, JSXElement: { enter (path, state) { if (null == state.hasJSXStyle) { const styles = findStyles(path.node.children) if (styles.length) { state.jsxId = '' state.styles = [] for (const style of styles) { if (style.children.length !== 1) { throw path.buildCodeFrameError(`Expected a child under ` + `JSX Style tag, but got ${style.children.length} ` + `(eg: <style jsx>{\`hi\`}</style>)`) } const child = style.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}`) } 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: <style jsx>{\`some css\`}</style>),` + ` but got ${expression.type}`) } const styleText = getExpressionText(expression) const styleId = '' + murmurHash(styleText) state.styles.push([ styleId, styleText ]) } state.jsxId += murmurHash(state.styles.map((s) => s[1]).join('')) state.hasJSXStyle = true state.file.hasJSXStyle = true // next visit will be: JSXOpeningElement } else { state.hasJSXStyle = false } } else if (state.hasJSXStyle) { const el = path.node.openingElement if (el.name && 'style' === el.name.name) { // we replace styles with the function call const [id, css] = state.styles.shift() path.replaceWith( t.JSXExpressionContainer( t.callExpression( t.identifier(INJECT_METHOD), [ t.stringLiteral(id), t.stringLiteral(transform(id, css)) ] ) ) ) } } }, exit (path, state) { if (state.hasJSXStyle && !--state.ignoreClosing) { state.hasJSXStyle = null } } }, Program: { enter (path, state) { state.file.hasJSXStyle = false }, exit ({ node, scope }, state) { if (!(state.file.hasJSXStyle && !scope.hasBinding(INJECT_METHOD))) { return } const importDeclaration = t.importDeclaration( [t.importDefaultSpecifier(t.identifier(INJECT_METHOD))], t.stringLiteral('styled-jsx/inject') ); node.body.unshift(importDeclaration); } }, } } }