// @flow import { createEmotionMacro, transformers as vanillaTransformers } from './emotion-macro' import { createStyledMacro, styledTransformer } from './styled-macro' import coreMacro, { transformers as coreTransformers, transformCsslessArrayExpression, transformCsslessObjectExpression } from './core-macro' import { getStyledOptions, createTransformerMacro } from './utils' const getCssExport = (reexported, importSource, mapping) => { const cssExport = Object.keys(mapping).find(localExportName => { const [packageName, exportName] = mapping[localExportName].canonicalImport return packageName === '@emotion/react' && exportName === 'css' }) if (!cssExport) { throw new Error( `You have specified that '${importSource}' re-exports '${reexported}' from '@emotion/react' but it doesn't also re-export 'css' from '@emotion/react', 'css' is necessary for certain optimisations, please re-export it from '${importSource}'` ) } return cssExport } let webStyledMacro = createStyledMacro({ importSource: '@emotion/styled/base', originalImportSource: '@emotion/styled', isWeb: true }) let nativeStyledMacro = createStyledMacro({ importSource: '@emotion/native', originalImportSource: '@emotion/native', isWeb: false }) let primitivesStyledMacro = createStyledMacro({ importSource: '@emotion/primitives', originalImportSource: '@emotion/primitives', isWeb: false }) let vanillaEmotionMacro = createEmotionMacro('@emotion/css') let transformersSource = { '@emotion/css': vanillaTransformers, '@emotion/react': coreTransformers, '@emotion/styled': { default: [ styledTransformer, { styledBaseImport: ['@emotion/styled/base', 'default'], isWeb: true } ] }, '@emotion/primitives': { default: [styledTransformer, { isWeb: false }] }, '@emotion/native': { default: [styledTransformer, { isWeb: false }] } } export const macros = { core: coreMacro, nativeStyled: nativeStyledMacro, primitivesStyled: primitivesStyledMacro, webStyled: webStyledMacro, vanillaEmotion: vanillaEmotionMacro } export type BabelPath = any export type EmotionBabelPluginPass = any const AUTO_LABEL_VALUES = ['dev-only', 'never', 'always'] export default function (babel: *, options: *) { if ( options.autoLabel !== undefined && !AUTO_LABEL_VALUES.includes(options.autoLabel) ) { throw new Error( `The 'autoLabel' option must be undefined, or one of the following: ${AUTO_LABEL_VALUES.map( s => `"${s}"` ).join(', ')}` ) } let t = babel.types return { name: '@emotion', // https://github.com/babel/babel/blob/0c97749e0fe8ad845b902e0b23a24b308b0bf05d/packages/babel-plugin-syntax-jsx/src/index.ts#L9-L18 manipulateOptions(opts: *, parserOpts: *) { const { plugins } = parserOpts if ( plugins.some(p => { const plugin = Array.isArray(p) ? p[0] : p return plugin === 'typescript' || plugin === 'jsx' }) ) { return } plugins.push('jsx') }, visitor: { ImportDeclaration(path: *, state: *) { const macro = state.pluginMacros[path.node.source.value] // most of this is from https://github.com/kentcdodds/babel-plugin-macros/blob/main/src/index.js if (macro === undefined) { return } if (t.isImportNamespaceSpecifier(path.node.specifiers[0])) { return } const imports = path.node.specifiers.map(s => ({ localName: s.local.name, importedName: s.type === 'ImportDefaultSpecifier' ? 'default' : s.imported.name })) let shouldExit = false let hasReferences = false const referencePathsByImportName = imports.reduce( (byName, { importedName, localName }) => { let binding = path.scope.getBinding(localName) if (!binding) { shouldExit = true return byName } byName[importedName] = binding.referencePaths hasReferences = hasReferences || Boolean(byName[importedName].length) return byName }, {} ) if (!hasReferences || shouldExit) { return } /** * Other plugins that run before babel-plugin-macros might use path.replace, where a path is * put into its own replacement. Apparently babel does not update the scope after such * an operation. As a remedy, the whole scope is traversed again with an empty "Identifier" * visitor - this makes the problem go away. * * See: https://github.com/kentcdodds/import-all.macro/issues/7 */ state.file.scope.path.traverse({ Identifier() {} }) macro({ path, references: referencePathsByImportName, state, babel, isEmotionCall: true, isBabelMacrosCall: true }) }, Program(path: *, state: *) { let macros = {} let jsxReactImports: Array<{ importSource: string, export: string, cssExport: string }> = [ { importSource: '@emotion/react', export: 'jsx', cssExport: 'css' } ] state.jsxReactImport = jsxReactImports[0] Object.keys(state.opts.importMap || {}).forEach(importSource => { let value = state.opts.importMap[importSource] let transformers = {} Object.keys(value).forEach(localExportName => { let { canonicalImport, ...options } = value[localExportName] let [packageName, exportName] = canonicalImport if (packageName === '@emotion/react' && exportName === 'jsx') { jsxReactImports.push({ importSource, export: localExportName, cssExport: getCssExport('jsx', importSource, value) }) return } let packageTransformers = transformersSource[packageName] if (packageTransformers === undefined) { throw new Error( `There is no transformer for the export '${exportName}' in '${packageName}'` ) } let extraOptions if (packageName === '@emotion/react' && exportName === 'Global') { // this option is not supposed to be set in importMap extraOptions = { cssExport: getCssExport('Global', importSource, value) } } else if ( packageName === '@emotion/styled' && exportName === 'default' ) { // this is supposed to override defaultOptions value // and let correct value to be set if coming in options extraOptions = { styledBaseImport: undefined } } let [exportTransformer, defaultOptions] = // $FlowFixMe Array.isArray(packageTransformers[exportName]) ? packageTransformers[exportName] : [packageTransformers[exportName]] transformers[localExportName] = [ exportTransformer, { ...defaultOptions, ...extraOptions, ...options } ] }) macros[importSource] = createTransformerMacro(transformers, { importSource }) }) state.pluginMacros = { '@emotion/styled': webStyledMacro, '@emotion/react': coreMacro, '@emotion/primitives': primitivesStyledMacro, '@emotion/native': nativeStyledMacro, '@emotion/css': vanillaEmotionMacro, ...macros } for (const node of path.node.body) { if (t.isImportDeclaration(node)) { let jsxReactImport = jsxReactImports.find( thing => node.source.value === thing.importSource && node.specifiers.some( x => t.isImportSpecifier(x) && x.imported.name === thing.export ) ) if (jsxReactImport) { state.jsxReactImport = jsxReactImport break } } } if (state.opts.cssPropOptimization === false) { state.transformCssProp = false } else { state.transformCssProp = true } if (state.opts.sourceMap === false) { state.emotionSourceMap = false } else { state.emotionSourceMap = true } }, JSXAttribute(path: *, state: *) { if (path.node.name.name !== 'css' || !state.transformCssProp) { return } if (t.isJSXExpressionContainer(path.node.value)) { if (t.isArrayExpression(path.node.value.expression)) { transformCsslessArrayExpression({ state, babel, path }) } else if (t.isObjectExpression(path.node.value.expression)) { transformCsslessObjectExpression({ state, babel, path, cssImport: state.jsxReactImport }) } } }, CallExpression: { exit(path: BabelPath, state: EmotionBabelPluginPass) { try { if ( path.node.callee && path.node.callee.property && path.node.callee.property.name === 'withComponent' ) { switch (path.node.arguments.length) { case 1: case 2: { path.node.arguments[1] = getStyledOptions(t, path, state) } } } } catch (e) { throw path.buildCodeFrameError(e) } } } } } }