You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
301 lines
9.4 KiB
301 lines
9.4 KiB
|
3 years ago
|
// @flow
|
||
|
|
import syntaxJsx from '@babel/plugin-syntax-jsx'
|
||
|
|
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',
|
||
|
|
inherits: syntaxJsx,
|
||
|
|
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)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|