diff --git a/README.md b/README.md index 8b0e1db93..4459f980f 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,4 @@ minimizes the runtime cost of css-in-js dramatically by parsing your styles with - [vue styled](https://github.com/tkh44/emotion/tree/master/docs/vue-styled.md) - [Usage with CSS Modules](https://github.com/tkh44/emotion/tree/master/docs/css-modules.md) +- [Usage with babel-macros](https://github.com/tkh44/emotion/tree/master/docs/babel-macros.md) \ No newline at end of file diff --git a/docs/babel-macros.md b/docs/babel-macros.md new file mode 100644 index 000000000..90161c039 --- /dev/null +++ b/docs/babel-macros.md @@ -0,0 +1,11 @@ +## Usage with babel-macros + +Instead of using the emotion's babel plugin, you can use emotion with [`babel-macros`](https://github.com/kentcdodds/babel-macros). Add `babel-macros` to your babel config and import whatever you want from emotion but add `/macro` to the end. The macro is currently the same as inline mode. Currently every API except for the css prop is supported by the macro. + +```jsx +import styled from 'emotion/react/macro' +import { css, keyframes, fontFace, injectGlobal, flush, hydrate } from 'emotion/macro' +import vueStyled from 'emotion/vue/macro' +``` + +For some context, check out this [issue](https://github.com/facebookincubator/create-react-app/issues/2730). \ No newline at end of file diff --git a/macro.js b/macro.js new file mode 100644 index 000000000..f6c4f6cfa --- /dev/null +++ b/macro.js @@ -0,0 +1 @@ +module.exports = require('./lib/macro') diff --git a/package.json b/package.json index 4797b7632..dae9bdb06 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,9 @@ "lib", "react", "server.js", - "vue.js", - "dist/DO-NOT-USE.min.js" + "vue", + "dist/DO-NOT-USE.min.js", + "macro.js" ], "scripts": { "build": "babel src -d lib", @@ -44,6 +45,7 @@ "babel-core": "^6.24.1", "babel-eslint": "^7.2.3", "babel-jest": "^20.0.3", + "babel-macros": "^0.5.2", "babel-preset-env": "^1.5.1", "babel-preset-flow": "^6.23.0", "babel-preset-react": "^6.24.1", diff --git a/react/macro.js b/react/macro.js new file mode 100644 index 000000000..c0e9257f9 --- /dev/null +++ b/react/macro.js @@ -0,0 +1 @@ +module.exports = require('../lib/react/macro') diff --git a/src/babel-utils.js b/src/babel-utils.js new file mode 100644 index 000000000..95ed92e6f --- /dev/null +++ b/src/babel-utils.js @@ -0,0 +1,51 @@ +import { keys } from './utils' +import forEach from '@arr/foreach' + +export function getIdentifierName (path, t) { + const parent = path.findParent(p => p.isVariableDeclarator()) + return parent && t.isIdentifier(parent.node.id) ? parent.node.id.name : '' +} + +export function getRuntimeImportPath (path, t) { + const binding = path.scope.getBinding(path.node.name) + if (!t.isImportDeclaration(binding.path.parentPath)) { + throw binding.path.buildCodeFrameError( + 'the emotion macro must be imported with es modules' + ) + } + const importPath = binding.path.parentPath.node.source.value + return importPath.match(/(.*)\/macro/)[1] +} + +export function buildMacroRuntimeNode (path, state, importName, t) { + const runtimeImportPath = getRuntimeImportPath(path, t) + if (state.emotionImports === undefined) state.emotionImports = {} + if (state.emotionImports[runtimeImportPath] === undefined) { state.emotionImports[runtimeImportPath] = {} } + if (state.emotionImports[runtimeImportPath][importName] === undefined) { + state.emotionImports[runtimeImportPath][ + importName + ] = path.scope.generateUidIdentifier(path.node.name) + } + return state.emotionImports[runtimeImportPath][importName] +} + +export function addRuntimeImports (state, t) { + if (state.emotionImports === undefined) return + forEach(keys(state.emotionImports), importPath => { + const importSpecifiers = [] + forEach(keys(state.emotionImports[importPath]), importName => { + const identifier = state.emotionImports[importPath][importName] + if (importName === 'default') { + importSpecifiers.push(t.importDefaultSpecifier(identifier)) + } else { + importSpecifiers.push( + t.importSpecifier(identifier, t.identifier(importName)) + ) + } + }) + state.file.path.node.body.unshift( + t.importDeclaration(importSpecifiers, t.stringLiteral(importPath)) + ) + }) + state.emotionImports = undefined +} diff --git a/src/babel.js b/src/babel.js index cb64b4f9d..b638bf808 100644 --- a/src/babel.js +++ b/src/babel.js @@ -4,6 +4,7 @@ import { touchSync } from 'touch' import postcssJs from 'postcss-js' import autoprefixer from 'autoprefixer' import { inline, keyframes, fontFace, injectGlobal } from './inline' +import { getIdentifierName } from './babel-utils' import cssProps from './css-prop' import createAttrExpression from './attrs' @@ -104,84 +105,230 @@ function parseDynamicValues (rules, t, options) { }) } -const visited = Symbol('visited') +export function buildStyledCallExpression (identifier, tag, path, state, t) { + const identifierName = getIdentifierName(path, t) + let { hash, rules, name, hasOtherMatch, composes, hasCssFunction } = inline( + path.node.quasi, + identifierName, + 'css', + state.inline + ) -export default function (babel) { - const { types: t } = babel + // hash will be '0' when no styles are passed so we can just return the original tag + if (hash === '0') { + return tag + } + const inputClasses = [t.stringLiteral(`${name}-${hash}`)] + for (var i = 0; i < composes; i++) { + inputClasses.push(path.node.quasi.expressions.shift()) + } + + const vars = path.node.quasi.expressions + + const dynamicValues = parseDynamicValues(rules, t, { composes, vars }) + const args = [tag, t.arrayExpression(inputClasses), t.arrayExpression(vars)] + if (!hasOtherMatch && !state.inline && !hasCssFunction) { + state.insertStaticRules(rules) + } else if (rules.length !== 0) { + const inlineContentExpr = t.functionExpression( + t.identifier('createEmotionStyledRules'), + vars.map((x, i) => t.identifier(`x${i}`)), + t.blockStatement([t.returnStatement(t.arrayExpression(dynamicValues))]) + ) + args.push(inlineContentExpr) + } + + return t.callExpression(identifier, args) +} + +export function buildStyledObjectCallExpression (path, identifier, t) { + const tag = t.isCallExpression(path.node.callee) + ? path.node.callee.arguments[0] + : t.stringLiteral(path.node.callee.property.name) + return t.callExpression(identifier, [ + tag, + t.arrayExpression(prefixAst(path.node.arguments, t)), + t.arrayExpression() + ]) +} + +export function replaceGlobalWithCallExpression ( + identifier, + processQuasi, + path, + state, + t +) { + const { rules, hasInterpolation } = processQuasi(path.node.quasi) + if (!hasInterpolation && !state.inline) { + state.insertStaticRules(rules) + if (t.isExpressionStatement(path.parent)) { + path.parentPath.remove() + } else { + path.replaceWith(t.identifier('undefined')) + } + } else { + path.replaceWith( + t.callExpression(identifier, [ + t.arrayExpression( + parseDynamicValues(rules, t, { + inputExpressions: path.node.quasi.expressions + }) + ) + ]) + ) + } +} + +function prefixAst (args, t) { const prefixer = postcssJs.sync([autoprefixer]) function isLiteral (value) { return ( - t.isStringLiteral(value) || - t.isNumericLiteral(value) || - t.isBooleanLiteral(value) + t.isStringLiteral(value) || + t.isNumericLiteral(value) || + t.isBooleanLiteral(value) ) } + if (Array.isArray(args)) { + return args.map(element => prefixAst(element, t)) + } - function prefixAst (args) { - if (Array.isArray(args)) { - return args.map(element => prefixAst(element)) - } + if (t.isObjectExpression(args)) { + let properties = [] + args.properties.forEach(property => { + // nested objects + if (t.isObjectExpression(property.value)) { + const key = t.isStringLiteral(property.key) + ? t.stringLiteral(property.key.value) + : t.identifier(property.key.name) + return properties.push( + t.objectProperty(key, prefixAst(property.value, t)) + ) - if (t.isObjectExpression(args)) { - let properties = [] - args.properties.forEach(property => { - // nested objects - if (t.isObjectExpression(property.value)) { - const key = t.isStringLiteral(property.key) - ? t.stringLiteral(property.key.value) - : t.identifier(property.key.name) - return properties.push( - t.objectProperty(key, prefixAst(property.value)) - ) + // literal value or array of literal values + } else if ( + isLiteral(property.value) || + (t.isArrayExpression(property.value) && + property.value.elements.every(isLiteral)) + ) { + // handle array values: { display: ['flex', 'block'] } + const propertyValue = t.isArrayExpression(property.value) + ? property.value.elements.map(element => element.value) + : property.value.value - // literal value or array of literal values - } else if ( - isLiteral(property.value) || - (t.isArrayExpression(property.value) && - property.value.elements.every(isLiteral)) - ) { - // handle array values: { display: ['flex', 'block'] } - const propertyValue = t.isArrayExpression(property.value) - ? property.value.elements.map(element => element.value) - : property.value.value - - const style = { [property.key.name]: propertyValue } - const prefixedStyle = prefixer(style) - - for (var k in prefixedStyle) { - const key = t.isStringLiteral(property.key) - ? t.stringLiteral(k) - : t.identifier(k) - const val = prefixedStyle[k] - let value - - if (typeof val === 'number') { - value = t.numericLiteral(val) - } else if (typeof val === 'string') { - value = t.stringLiteral(val) - } else if (Array.isArray(val)) { - value = t.arrayExpression(val.map(i => t.stringLiteral(i))) - } + const style = { [property.key.name]: propertyValue } + const prefixedStyle = prefixer(style) + + for (var k in prefixedStyle) { + const key = t.isStringLiteral(property.key) + ? t.stringLiteral(k) + : t.identifier(k) + const val = prefixedStyle[k] + let value - properties.push(t.objectProperty(key, value)) + if (typeof val === 'number') { + value = t.numericLiteral(val) + } else if (typeof val === 'string') { + value = t.stringLiteral(val) + } else if (Array.isArray(val)) { + value = t.arrayExpression(val.map(i => t.stringLiteral(i))) } - // expressions - } else { - properties.push(property) + properties.push(t.objectProperty(key, value)) } - }) - return t.objectExpression(properties) - } + // expressions + } else { + properties.push(property) + } + }) + + return t.objectExpression(properties) + } - if (t.isArrayExpression(args)) { - return t.arrayExpression(prefixAst(args.elements)) + if (t.isArrayExpression(args)) { + return t.arrayExpression(prefixAst(args.elements, t)) + } + + return args +} + +export function replaceCssWithCallExpression (path, identifier, state, t) { + try { + const { hash, name, rules, hasVar, composes, hasOtherMatch } = inline( + path.node.quasi, + getIdentifierName(path, t), + 'css', + state.inline + ) + const inputClasses = [t.stringLiteral(`${name}-${hash}`)] + for (var i = 0; i < composes; i++) { + inputClasses.push(path.node.quasi.expressions.shift()) + } + const args = [ + t.arrayExpression(inputClasses), + t.arrayExpression(path.node.quasi.expressions) + ] + if (!hasOtherMatch && !state.inline) { + state.insertStaticRules(rules) + if (!hasVar) { + return path.replaceWith(joinExpressionsWithSpaces(inputClasses, t)) + } + } else if (rules.length !== 0) { + const expressions = path.node.quasi.expressions.map((x, i) => + t.identifier(`x${i}`) + ) + const inlineContentExpr = t.functionExpression( + t.identifier('createEmotionRules'), + expressions, + t.blockStatement([ + t.returnStatement( + t.arrayExpression( + parseDynamicValues(rules, t, { + inputExpressions: expressions, + composes + }) + ) + ) + ]) + ) + args.push(inlineContentExpr) } + path.replaceWith(t.callExpression(identifier, args)) + } catch (e) { + throw path.buildCodeFrameError(e) + } +} - return args +export function replaceKeyframesWithCallExpression (path, identifier, state, t) { + const { hash, name, rules, hasInterpolation } = keyframes( + path.node.quasi, + getIdentifierName(path, t), + 'animation' + ) + const animationName = `${name}-${hash}` + if (!hasInterpolation && !state.inline) { + state.insertStaticRules([`@keyframes ${animationName} ${rules.join('')}`]) + path.replaceWith(t.stringLiteral(animationName)) + } else { + path.replaceWith( + t.callExpression(identifier, [ + t.stringLiteral(animationName), + t.arrayExpression( + parseDynamicValues(rules, t, { + inputExpressions: path.node.quasi.expressions + }) + ) + ]) + ) } +} + +const visited = Symbol('visited') + +export default function (babel) { + const { types: t } = babel return { name: 'emotion', // not required @@ -235,20 +382,14 @@ export default function (babel) { t.isIdentifier(path.node.callee.object) && path.node.callee.object.name === 'styled') ) { - const tag = t.isCallExpression(path.node.callee) - ? path.node.callee.arguments[0] - : t.stringLiteral(path.node.callee.property.name) - path.replaceWith( - t.callExpression(t.identifier('styled'), [ - tag, - t.arrayExpression(prefixAst(path.node.arguments)), - t.arrayExpression() - ]) - ) + const identifier = t.isCallExpression(path.node.callee) + ? path.node.callee.callee + : path.node.callee.object + path.replaceWith(buildStyledObjectCallExpression(path, identifier, t)) } if (t.isCallExpression(path.node) && path.node.callee.name === 'css') { - const prefixedAst = prefixAst(path.node.arguments) + const prefixedAst = prefixAst(path.node.arguments, t) path.replaceWith(t.callExpression(t.identifier('css'), prefixedAst)) } path[visited] = true @@ -264,207 +405,59 @@ export default function (babel) { // color: ${x0}; // height: ${x1}; }`]; // }); - - const parent = path.findParent(p => p.isVariableDeclarator()) - const identifierName = - parent && t.isIdentifier(parent.node.id) ? parent.node.id.name : '' - - function buildCallExpression (identifier, tag, path) { - let { - hash, - rules, - name, - hasOtherMatch, - composes, - hasCssFunction - } = inline(path.node.quasi, identifierName, 'css', state.inline) - - // hash will be '0' when no styles are passed so we can just return the original tag - if (hash === '0') { - return tag - } - const inputClasses = [t.stringLiteral(`${name}-${hash}`)] - for (var i = 0; i < composes; i++) { - inputClasses.push(path.node.quasi.expressions.shift()) - } - - const vars = path.node.quasi.expressions - - const dynamicValues = parseDynamicValues(rules, t, { composes, vars }) - const args = [ - tag, - t.arrayExpression(inputClasses), - t.arrayExpression(vars) - ] - if (!hasOtherMatch && !state.inline && !hasCssFunction) { - state.insertStaticRules(rules) - } else if (rules.length !== 0) { - const inlineContentExpr = t.functionExpression( - t.identifier('createEmotionStyledRules'), - vars.map((x, i) => t.identifier(`x${i}`)), - t.blockStatement([ - t.returnStatement(t.arrayExpression(dynamicValues)) - ]) - ) - args.push(inlineContentExpr) - } - - return t.callExpression(identifier, args) - } - if ( // styled.h1`color:${color};` t.isMemberExpression(path.node.tag) && - path.node.tag.object.name === 'styled' && - t.isTemplateLiteral(path.node.quasi) + path.node.tag.object.name === 'styled' ) { path.replaceWith( - buildCallExpression( + buildStyledCallExpression( path.node.tag.object, t.stringLiteral(path.node.tag.property.name), - path + path, + state, + t ) ) } else if ( // styled('h1')`color:${color};` t.isCallExpression(path.node.tag) && - path.node.tag.callee.name === 'styled' && - t.isTemplateLiteral(path.node.quasi) + path.node.tag.callee.name === 'styled' ) { path.replaceWith( - buildCallExpression( + buildStyledCallExpression( path.node.tag.callee, path.node.tag.arguments[0], - path + path, + state, + t ) ) - } else if ( - t.isIdentifier(path.node.tag) && - path.node.tag.name === 'css' - ) { - try { - const { - hash, - name, - rules, - hasVar, - composes, - hasOtherMatch - } = inline(path.node.quasi, identifierName, 'css', state.inline) - const inputClasses = [t.stringLiteral(`${name}-${hash}`)] - for (var i = 0; i < composes; i++) { - inputClasses.push(path.node.quasi.expressions.shift()) - } - const args = [ - t.arrayExpression(inputClasses), - t.arrayExpression(path.node.quasi.expressions) - ] - if (!hasOtherMatch && !state.inline) { - state.insertStaticRules(rules) - if (!hasVar) { - return path.replaceWith( - joinExpressionsWithSpaces(inputClasses, t) - ) - } - } else if (rules.length !== 0) { - const expressions = path.node.quasi.expressions.map((x, i) => - t.identifier(`x${i}`) - ) - const inlineContentExpr = t.functionExpression( - t.identifier('createEmotionRules'), - expressions, - t.blockStatement([ - t.returnStatement( - t.arrayExpression( - parseDynamicValues(rules, t, { - inputExpressions: expressions, - composes - }) - ) - ) - ]) - ) - args.push(inlineContentExpr) - } - path.replaceWith(t.callExpression(t.identifier('css'), args)) - } catch (e) { - throw path.buildCodeFrameError(e) - } - } else if ( - t.isIdentifier(path.node.tag) && - path.node.tag.name === 'keyframes' - ) { - const { hash, name, rules, hasInterpolation } = keyframes( - path.node.quasi, - identifierName, - 'animation' - ) - const animationName = `${name}-${hash}` - if (!hasInterpolation && !state.inline) { - state.insertStaticRules([ - `@keyframes ${animationName} ${rules.join('')}` - ]) - path.replaceWith(t.stringLiteral(animationName)) - } else { - path.replaceWith( - t.callExpression(t.identifier('keyframes'), [ - t.stringLiteral(animationName), - t.arrayExpression( - parseDynamicValues(rules, t, { - inputExpressions: path.node.quasi.expressions - }) - ) - ]) + } else if (t.isIdentifier(path.node.tag)) { + if (path.node.tag.name === 'css') { + replaceCssWithCallExpression(path, t.identifier('css'), state, t) + } else if (path.node.tag.name === 'keyframes') { + replaceKeyframesWithCallExpression( + path, + t.identifier('keyframes'), + state, + t ) - } - } else if ( - t.isIdentifier(path.node.tag) && - path.node.tag.name === 'fontFace' - ) { - const { rules, hasInterpolation } = fontFace( - path.node.quasi, - state.inline - ) - if (!hasInterpolation && !state.inline) { - state.insertStaticRules(rules) - if (t.isExpressionStatement(path.parent)) { - path.parentPath.remove() - } else { - path.replaceWith(t.identifier('undefined')) - } - } else { - path.replaceWith( - t.callExpression(t.identifier('fontFace'), [ - t.arrayExpression( - parseDynamicValues(rules, t, { - inputExpressions: path.node.quasi.expressions - }) - ) - ]) + } else if (path.node.tag.name === 'fontFace') { + replaceGlobalWithCallExpression( + t.identifier('fontFace'), + fontFace, + path, + state, + t ) - } - } else if ( - t.isIdentifier(path.node.tag) && - path.node.tag.name === 'injectGlobal' && - t.isTemplateLiteral(path.node.quasi) - ) { - const { rules, hasInterpolation } = injectGlobal(path.node.quasi) - if (!hasInterpolation && !state.inline) { - state.insertStaticRules(rules) - if (t.isExpressionStatement(path.parent)) { - path.parentPath.remove() - } else { - path.replaceWith(t.identifier('undefined')) - } - } else { - path.replaceWith( - t.callExpression(t.identifier('injectGlobal'), [ - t.arrayExpression( - parseDynamicValues(rules, t, { - inputExpressions: path.node.quasi.expressions - }) - ) - ]) + } else if (path.node.tag.name === 'injectGlobal') { + replaceGlobalWithCallExpression( + t.identifier('injectGlobal'), + injectGlobal, + path, + state, + t ) } } diff --git a/src/macro-styled.js b/src/macro-styled.js new file mode 100644 index 000000000..ce2926884 --- /dev/null +++ b/src/macro-styled.js @@ -0,0 +1,61 @@ +import { + buildStyledCallExpression, + buildStyledObjectCallExpression +} from './babel' +import { buildMacroRuntimeNode, addRuntimeImports } from './babel-utils' +import forEach from '@arr/foreach' +import { keys } from './utils' + +module.exports = function macro ({ references, state, babel: { types: t } }) { + if (!state.inline) state.inline = true + if (references.default) { + references.default.forEach(styledReference => { + const path = styledReference.parentPath.parentPath + const runtimeNode = buildMacroRuntimeNode( + styledReference, + state, + 'default', + t + ) + if (t.isTemplateLiteral(path.node.quasi)) { + if (t.isMemberExpression(path.node.tag)) { + path.replaceWith( + buildStyledCallExpression( + runtimeNode, + t.stringLiteral(path.node.tag.property.name), + path, + state, + t + ) + ) + } else if (t.isCallExpression(path.node.tag)) { + path.replaceWith( + buildStyledCallExpression( + runtimeNode, + path.node.tag.arguments[0], + path, + state, + t + ) + ) + } + } else if ( + t.isCallExpression(path) && + (t.isCallExpression(path.node.callee) || + t.isIdentifier(path.node.callee.object)) + ) { + path.replaceWith(buildStyledObjectCallExpression(path, runtimeNode, t)) + } + }) + } + forEach(keys(references), (referenceKey) => { + if (referenceKey !== 'default') { + references[referenceKey].forEach(reference => { + reference.replaceWith( + buildMacroRuntimeNode(reference, state, referenceKey, t) + ) + }) + } + }) + addRuntimeImports(state, t) +} diff --git a/src/macro.js b/src/macro.js new file mode 100644 index 000000000..bec4f9564 --- /dev/null +++ b/src/macro.js @@ -0,0 +1,88 @@ +import { + replaceGlobalWithCallExpression, + replaceCssWithCallExpression, + replaceKeyframesWithCallExpression +} from './babel' +import { buildMacroRuntimeNode, addRuntimeImports } from './babel-utils' +import { injectGlobal, fontFace } from './inline' +import forEach from '@arr/foreach' +import { keys } from './utils' + +module.exports = function macro ({ references, state, babel: { types: t } }) { + if (!state.inline) state.inline = true + forEach(keys(references), (referenceKey) => { + if (referenceKey === 'injectGlobal') { + references.injectGlobal.forEach(injectGlobalReference => { + const path = injectGlobalReference.parentPath + if ( + t.isIdentifier(path.node.tag) && + t.isTemplateLiteral(path.node.quasi) + ) { + replaceGlobalWithCallExpression( + buildMacroRuntimeNode( + injectGlobalReference, + state, + 'injectGlobal', + t + ), + injectGlobal, + path, + state, + t + ) + } + }) + } else if (referenceKey === 'fontFace') { + references.fontFace.forEach(fontFaceReference => { + const path = fontFaceReference.parentPath + if ( + t.isIdentifier(path.node.tag) && + t.isTemplateLiteral(path.node.quasi) + ) { + replaceGlobalWithCallExpression( + buildMacroRuntimeNode(fontFaceReference, state, 'fontFace', t), + fontFace, + path, + state, + t + ) + } + }) + } else if (referenceKey === 'css') { + references.css.forEach(cssReference => { + const path = cssReference.parentPath + const runtimeNode = buildMacroRuntimeNode(cssReference, state, 'css', t) + if ( + t.isIdentifier(path.node.tag) && + t.isTemplateLiteral(path.node.quasi) + ) { + replaceCssWithCallExpression(path, runtimeNode, state, t) + } else { + cssReference.replaceWith(runtimeNode) + } + }) + } else if (referenceKey === 'keyframes') { + references.keyframes.forEach(keyframesReference => { + const path = keyframesReference.parentPath + if ( + t.isIdentifier(path.node.tag) && + t.isTemplateLiteral(path.node.quasi) + ) { + replaceKeyframesWithCallExpression( + path, + buildMacroRuntimeNode(keyframesReference, state, 'keyframes', t), + state, + t + ) + } + }) + } else { + references[referenceKey].forEach((reference) => { + reference.replaceWith( + buildMacroRuntimeNode(reference, state, referenceKey, t) + ) + }) + } + }) + addRuntimeImports(state, t) +} diff --git a/src/react/macro.js b/src/react/macro.js new file mode 100644 index 000000000..e098e05c0 --- /dev/null +++ b/src/react/macro.js @@ -0,0 +1 @@ +module.exports = require('../macro-styled') diff --git a/src/vue.js b/src/vue/index.js similarity index 92% rename from src/vue.js rename to src/vue/index.js index 298813cbe..94ac4a13e 100644 --- a/src/vue.js +++ b/src/vue/index.js @@ -1,4 +1,4 @@ -import { css as magic } from './index' +import { css as magic } from '../index' const styled = (tag, cls, vars = [], content) => { return { diff --git a/src/vue/macro.js b/src/vue/macro.js new file mode 100644 index 000000000..e098e05c0 --- /dev/null +++ b/src/vue/macro.js @@ -0,0 +1 @@ +module.exports = require('../macro-styled') diff --git a/test/.babelrc b/test/.babelrc index 26d7926d2..9fbb976c8 100644 --- a/test/.babelrc +++ b/test/.babelrc @@ -2,7 +2,7 @@ "presets": [ "flow", "env", - "react", - ["./babel-preset-emotion-test", { "inline": true }] - ] + "react" + ], + "plugins": [["./babel-plugin-emotion-test", { "inline": true }]] } diff --git a/test/babel-macro-register.js b/test/babel-macro-register.js new file mode 100644 index 000000000..aed4d8e30 --- /dev/null +++ b/test/babel-macro-register.js @@ -0,0 +1,3 @@ +require('babel-register') + +module.exports = require('babel-macros') diff --git a/test/babel-plugin-emotion-test.js b/test/babel-plugin-emotion-test.js new file mode 100644 index 000000000..6b570ec71 --- /dev/null +++ b/test/babel-plugin-emotion-test.js @@ -0,0 +1,3 @@ +require('babel-register') + +module.exports = require('../src/babel') diff --git a/test/babel-preset-emotion-test.js b/test/babel-preset-emotion-test.js deleted file mode 100644 index 25612a045..000000000 --- a/test/babel-preset-emotion-test.js +++ /dev/null @@ -1,5 +0,0 @@ -require('babel-register') - -module.exports = function (context, opts) { - return { plugins: [[require('../src/babel'), opts]] } -} diff --git a/test/babel/__snapshots__/macro.test.js.snap b/test/babel/__snapshots__/macro.test.js.snap new file mode 100644 index 000000000..9edeec7f3 --- /dev/null +++ b/test/babel/__snapshots__/macro.test.js.snap @@ -0,0 +1,149 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`babel macro css 1`] = ` +"import { css as _css } from '../../src'; + +_css(['css-kgvccl'], [widthVar], function createEmotionRules(x0) { + return [\`.css-kgvccl { margin: 12px 48px; + color: #ffffff; + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + -webkit-flex: 1 0 auto; + -ms-flex: 1 0 auto; + flex: 1 0 auto; + color: blue; + width: \${x0}; }\`]; +});" +`; + +exports[`babel macro css object 1`] = ` +"import { css as _css } from '../../src'; + +const cls1 = _css({ display: 'flex' });" +`; + +exports[`babel macro flush 1`] = ` +"import { flush as _flush } from '../../src'; + +const someOtherVar = _flush;" +`; + +exports[`babel macro fontFace 1`] = ` +"import { fontFace as _fontFace } from '../../src'; + +_fontFace([\`@font-face {font-family: MyHelvetica; + src: local(\\"Helvetica Neue Bold\\"), + local(\\"HelveticaNeue-Bold\\"), + url(MgOpenModernaBold.ttf); + font-weight: bold;}\`]);" +`; + +exports[`babel macro hydrate 1`] = ` +"import { hydrate as _hydrate } from '../../src'; + +const someOtherVar = _hydrate;" +`; + +exports[`babel macro injectGlobal 1`] = ` +"import { injectGlobal as _injectGlobal } from '../../src'; + +_injectGlobal([\`body { + margin: 0; + padding: 0; + }\`, \`body > div { + display: none; +}\`, \`html { + background: green; + }\`]);" +`; + +exports[`babel macro keyframes 1`] = ` +"import { keyframes as _keyframes } from '../../src'; + +const rotate360 = _keyframes('animation-rotate360-1e2ipf5', [\`{ from { + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); + } + to { + -webkit-transform: rotate(360deg); + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } }\`]);" +`; + +exports[`babel macro multiple imports 1`] = ` +"import { keyframes as _keyframes, css as _css } from '../../src'; + +const rotate360 = _keyframes('animation-rotate360-1e2ipf5', [\`{ from { + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); + } + to { + -webkit-transform: rotate(360deg); + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } }\`]); +const thing = _css(['css-thing-dn301t'], [widthVar], function createEmotionRules(x0) { + return [\`.css-thing-dn301t { margin: 12px 48px; + color: #ffffff; + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + -webkit-flex: 1 0 auto; + -ms-flex: 1 0 auto; + flex: 1 0 auto; + color: blue; + width: \${x0}; }\`]; +});" +`; + +exports[`babel macro some import that does not exist 1`] = ` +"import { thisDoesNotExist as _thisDoesNotExist } from '../../src'; + +const someOtherVar = _thisDoesNotExist;" +`; + +exports[`babel macro styled object function 1`] = ` +"import _styled from '../../src/react'; + +const SomeComponent = _styled('div', [{ + display: ['-webkit-box', '-ms-flexbox', 'flex'] +}], []);" +`; + +exports[`babel macro styled object member 1`] = ` +"import _styled from '../../src/react'; + +const SomeComponent = _styled('div', [{ + display: ['-webkit-box', '-ms-flexbox', 'flex'] +}], []);" +`; + +exports[`babel macro styled some import that does not exist 1`] = ` +"import { thisDoesNotExist as _thisDoesNotExist } from '../../src/react'; + +const someOtherVar = _thisDoesNotExist;" +`; + +exports[`babel macro styled tagged template literal function 1`] = ` +"import _styled from '../../src/react'; + +const SomeComponent = _styled('div', ['css-SomeComponent-1q8jsgx'], [], function createEmotionStyledRules() { + return [\`.css-SomeComponent-1q8jsgx { display: -webkit-box; display: -moz-box; display: -ms-flexbox; display: -webkit-flex; display: flex; }\`]; +});" +`; + +exports[`babel macro styled tagged template literal member 1`] = ` +"import _styled from '../../src/react'; + +const SomeComponent = _styled('div', ['css-SomeComponent-1q8jsgx'], [], function createEmotionStyledRules() { + return [\`.css-SomeComponent-1q8jsgx { display: -webkit-box; display: -moz-box; display: -ms-flexbox; display: -webkit-flex; display: flex; }\`]; +});" +`; diff --git a/test/babel/macro.test.js b/test/babel/macro.test.js new file mode 100644 index 000000000..2bc8acc5c --- /dev/null +++ b/test/babel/macro.test.js @@ -0,0 +1,239 @@ +/* eslint-disable no-template-curly-in-string */ +/* eslint-env jest */ +import * as babel from 'babel-core' + +describe('babel macro', () => { + describe('styled', () => { + test('tagged template literal member', () => { + const basic = ` + import styled from '../../src/react/macro' + const SomeComponent = styled.div\` + display: flex; + \` + ` + const { code } = babel.transform(basic, { + plugins: ['babel-macros'], + filename: __filename, + babelrc: false + }) + expect(code).toMatchSnapshot() + }) + test('tagged template literal function', () => { + const basic = ` + import styled from '../../src/react/macro' + const SomeComponent = styled('div')\` + display: flex; + \` + ` + const { code } = babel.transform(basic, { + plugins: ['babel-macros'], + filename: __filename, + babelrc: false + }) + expect(code).toMatchSnapshot() + }) + test('object member', () => { + const basic = ` + import styled from '../../src/react/macro' + const SomeComponent = styled.div({ + display: 'flex' + }) + ` + const { code } = babel.transform(basic, { + plugins: ['babel-macros'], + filename: __filename, + babelrc: false + }) + expect(code).toMatchSnapshot() + }) + test('object function', () => { + const basic = ` + import styled from '../../src/react/macro' + const SomeComponent = styled('div')({ + display: 'flex' + }) + ` + const { code } = babel.transform(basic, { + plugins: ['babel-macros'], + filename: __filename, + babelrc: false + }) + expect(code).toMatchSnapshot() + }) + test('some import that does not exist', () => { + const basic = ` + import { thisDoesNotExist } from '../../src/react/macro' + const someOtherVar = thisDoesNotExist + ` + const { code } = babel.transform(basic, { + plugins: ['babel-macros'], + filename: __filename, + babelrc: false + }) + expect(code).toMatchSnapshot() + }) + test('throws correct error when imported with commonjs', () => { + const basic = ` + const styled = require('../../src/react/macro') + const SomeComponent = styled('div')\` + display: flex; + \` + ` + expect(() => babel.transform(basic, { + plugins: ['babel-macros'], + filename: __filename, + babelrc: false + })).toThrowError(/the emotion macro must be imported with es modules/) + }) + }) + test('injectGlobal', () => { + const basic = ` + import { injectGlobal } from '../../src/macro' + injectGlobal\` + body { + margin: 0; + padding: 0; + & > div { + display: none; + } + } + html { + background: green; + } + \`;` + const { code } = babel.transform(basic, { + plugins: ['babel-macros'], + filename: __filename, + babelrc: false + }) + expect(code).toMatchSnapshot() + }) + test('fontFace', () => { + const basic = ` + import { fontFace } from '../../src/macro' + fontFace\` + font-family: MyHelvetica; + src: local("Helvetica Neue Bold"), + local("HelveticaNeue-Bold"), + url(MgOpenModernaBold.ttf); + font-weight: bold; + \`;` + const { code } = babel.transform(basic, { + plugins: ['babel-macros'], + filename: __filename, + babelrc: false + }) + expect(code).toMatchSnapshot() + }) + test('css', () => { + const basic = ` + import { css } from '../../src/macro' + css\` + margin: 12px 48px; + color: #ffffff; + display: flex; + flex: 1 0 auto; + color: blue; + width: \${widthVar}; + \`` + const { code } = babel.transform(basic, { + plugins: ['babel-macros'], + filename: __filename, + babelrc: false + }) + expect(code).toMatchSnapshot() + }) + test('css object', () => { + const basic = ` + import { css } from '../../src/macro' + const cls1 = css({ display: 'flex' }) + ` + const { code } = babel.transform(basic, { + plugins: ['babel-macros'], + filename: __filename, + babelrc: false + }) + expect(code).toMatchSnapshot() + }) + test('hydrate', () => { + const basic = ` + import { hydrate } from '../../src/macro' + const someOtherVar = hydrate + ` + const { code } = babel.transform(basic, { + plugins: ['babel-macros'], + filename: __filename, + babelrc: false + }) + expect(code).toMatchSnapshot() + }) + test('flush', () => { + const basic = ` + import { flush } from '../../src/macro' + const someOtherVar = flush + ` + const { code } = babel.transform(basic, { + plugins: ['babel-macros'], + filename: __filename, + babelrc: false + }) + expect(code).toMatchSnapshot() + }) + test('some import that does not exist', () => { + const basic = ` + import { thisDoesNotExist } from '../../src/macro' + const someOtherVar = thisDoesNotExist + ` + const { code } = babel.transform(basic, { + plugins: ['babel-macros'], + filename: __filename, + babelrc: false + }) + expect(code).toMatchSnapshot() + }) + test('keyframes', () => { + const basic = ` + import { keyframes } from '../../src/macro' + const rotate360 = keyframes\` + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + \`` + const { code } = babel.transform(basic, { + plugins: ['babel-macros'], + filename: __filename, + babelrc: false + }) + expect(code).toMatchSnapshot() + }) + test('multiple imports', () => { + const basic = ` + import { keyframes, css } from '../../src/macro' + const rotate360 = keyframes\` + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + \` + const thing = css\` + margin: 12px 48px; + color: #ffffff; + display: flex; + flex: 1 0 auto; + color: blue; + width: \${widthVar}; +\` + ` + const { code } = babel.transform(basic, { + plugins: ['babel-macros'], + filename: __filename, + babelrc: false + }) + expect(code).toMatchSnapshot() + }) +}) diff --git a/test/extract/.babelrc b/test/extract/.babelrc index 6c8fd804b..2efecbb12 100644 --- a/test/extract/.babelrc +++ b/test/extract/.babelrc @@ -2,8 +2,7 @@ "presets": [ "flow", "env", - "stage-0", - "react", - "../babel-preset-emotion-test" - ] + "react" + ], + "plugins": ["../babel-plugin-emotion-test"] } diff --git a/test/macro/.babelrc b/test/macro/.babelrc new file mode 100644 index 000000000..56f9f957d --- /dev/null +++ b/test/macro/.babelrc @@ -0,0 +1,8 @@ +{ + "presets": [ + "flow", + "env", + "react" + ], + "plugins": ["../babel-macro-register"] +} diff --git a/test/macro/__snapshots__/css.test.js.snap b/test/macro/__snapshots__/css.test.js.snap new file mode 100644 index 000000000..c4a23a783 --- /dev/null +++ b/test/macro/__snapshots__/css.test.js.snap @@ -0,0 +1,83 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`css composes 1`] = `"css-cls1-vyoujf-va5xsk css-cls1-vyoujf"`; + +exports[`css composes 2`] = `"css-cls2-1qb2ovg-1em6aur css-cls2-1qb2ovg css-cls1-vyoujf-va5xsk css-cls1-vyoujf"`; + +exports[`css composes 3`] = ` +".css-cls1-1gi569l-b877w5 { background: white; + color: black; + text-decoration: underline; + display: block; + border-radius: 3px; + padding: 25px; + width: 500px; + z-index: 100; + font-size: 18px; + text-align: center; + border: solid 1px red; }.css-15famh2{}.css-cls2-1qb2ovg-1em6aur { + -webkit-justify-content: center; + -ms-flex-pack: center; + -webkit-box-pack: center; + justify-content: center; }.css-cls1-vyoujf-va5xsk { display: -webkit-box; display: -moz-box; display: -ms-flexbox; display: -webkit-flex; display: flex; }" +`; + +exports[`css composes with objects 1`] = `"css-q8izmm"`; + +exports[`css composes with objects 2`] = `"css-cls2-1qb2ovg-1em6aur css-cls2-1qb2ovg css-q8izmm"`; + +exports[`css composes with objects 3`] = ` +".css-cls1-1gi569l-b877w5 { background: white; + color: black; + text-decoration: underline; + display: block; + border-radius: 3px; + padding: 25px; + width: 500px; + z-index: 100; + font-size: 18px; + text-align: center; + border: solid 1px red; }.css-15famh2{}.css-cls2-1qb2ovg-1em6aur { + -webkit-justify-content: center; + -ms-flex-pack: center; + -webkit-box-pack: center; + justify-content: center; }.css-cls1-vyoujf-va5xsk { display: -webkit-box; display: -moz-box; display: -ms-flexbox; display: -webkit-flex; display: flex; }.css-1fe3owl{display:flex}.css-q8izmm{display:flex;display:block;width:30px;height:calc(40vw - 50px)}.css-q8izmm:hover{color:blue}.css-q8izmm:after{content:\\" \\";color:red}@media(min-width: 420px){.css-q8izmm{color:green}}" +`; + +exports[`css composes with undefined values 1`] = `"css-cls2-1qb2ovg-1em6aur css-cls2-1qb2ovg css-15famh2"`; + +exports[`css composes with undefined values 2`] = ` +".css-cls1-1gi569l-b877w5 { background: white; + color: black; + text-decoration: underline; + display: block; + border-radius: 3px; + padding: 25px; + width: 500px; + z-index: 100; + font-size: 18px; + text-align: center; + border: solid 1px red; }.css-15famh2{}.css-cls2-1qb2ovg-1em6aur { + -webkit-justify-content: center; + -ms-flex-pack: center; + -webkit-box-pack: center; + justify-content: center; }" +`; + +exports[`css handles more than 10 dynamic properties 1`] = `"css-cls1-1gi569l-b877w5 css-cls1-1gi569l"`; + +exports[`css handles more than 10 dynamic properties 2`] = ` +".css-cls1-1gi569l-b877w5 { background: white; + color: black; + text-decoration: underline; + display: block; + border-radius: 3px; + padding: 25px; + width: 500px; + z-index: 100; + font-size: 18px; + text-align: center; + border: solid 1px red; }" +`; + +exports[`css handles objects 1`] = `"css-1fe3owl"`; diff --git a/test/macro/__snapshots__/inject-global.test.js.snap b/test/macro/__snapshots__/inject-global.test.js.snap new file mode 100644 index 000000000..3d8a6aaa3 --- /dev/null +++ b/test/macro/__snapshots__/inject-global.test.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`injectGlobal 1`] = ` +"html { + background: pink; + }html.active { + background: red; + }body { + color: yellow; + margin: 0; + padding: 0; + }" +`; diff --git a/test/macro/__snapshots__/keyframes.test.js.snap b/test/macro/__snapshots__/keyframes.test.js.snap new file mode 100644 index 000000000..832a5245b --- /dev/null +++ b/test/macro/__snapshots__/keyframes.test.js.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`keyframes keyframes with interpolation 1`] = ` +"@keyframes animation-bounce-10a3qiv-cmo0tx { from, 20%, 53%, 80%, to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + -webkit-transform: translate3d(0,0,0); + -ms-transform: translate3d(0,0,0); + transform: translate3d(0,0,0); + } + + 40%, 43% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + -webkit-transform: translate3d(0, -30px, 0); + -ms-transform: translate3d(0, -30px, 0); + transform: translate3d(0, -30px, 0); + } + + 70% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + -webkit-transform: translate3d(0, -15px, 0); + -ms-transform: translate3d(0, -15px, 0); + transform: translate3d(0, -15px, 0); + } + + 90% { + -webkit-transform: translate3d(0,-4px,0); + -ms-transform: translate3d(0,-4px,0); + transform: translate3d(0,-4px,0); + } }.css-H1-o0kcx2-fy8ana { font-size: 20px; + -webkit-animation: animation-bounce-10a3qiv-cmo0tx 2s linear infinite; + animation: animation-bounce-10a3qiv-cmo0tx 2s linear infinite; }@keyframes animation-1fpnjxj-1mmv2re { from { + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); + } + + to { + -webkit-transform: rotate(360deg); + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } }" +`; + +exports[`keyframes renders 1`] = ` +.css-H1-o0kcx2-fy8ana { + font-size: 20px; + -webkit-animation: animation-bounce-10a3qiv-cmo0tx 2s linear infinite; + animation: animation-bounce-10a3qiv-cmo0tx 2s linear infinite; +} + +