0.2.0 - Mid migration

This commit is contained in:
Daniel Mason 2022-04-25 14:47:15 +12:00
parent 139e6a915e
commit 7e38fdbd7d
42393 changed files with 5358157 additions and 62 deletions

62
web/node_modules/babel-plugin-emotion/src/css-macro.js generated vendored Normal file
View file

@ -0,0 +1,62 @@
// @flow
import { createMacro } from 'babel-plugin-macros'
import { addDefault, addNamed } from '@babel/helper-module-imports'
import { transformExpressionWithStyles } from './utils'
export const transformCssCallExpression = ({
babel,
state,
path,
sourceMap
}: {
babel: *,
state: *,
path: *,
sourceMap?: string
}) => {
let node = transformExpressionWithStyles({
babel,
state,
path,
shouldLabel: true,
sourceMap
})
if (node) {
path.replaceWith(node)
path.hoist()
} else if (path.isCallExpression()) {
path.addComment('leading', '#__PURE__')
}
}
export default createMacro(({ references, state, babel, isEmotionCall }) => {
if (!isEmotionCall) {
state.emotionSourceMap = true
}
const t = babel.types
if (references.default && references.default.length) {
references.default.reverse().forEach(reference => {
if (!state.cssIdentifier) {
state.cssIdentifier = addDefault(reference, '@emotion/css', {
nameHint: 'css'
})
}
reference.replaceWith(t.cloneDeep(state.cssIdentifier))
transformCssCallExpression({ babel, state, path: reference.parentPath })
})
}
Object.keys(references)
.filter(x => x !== 'default')
.forEach(referenceKey => {
let runtimeNode = addNamed(
state.file.path,
referenceKey,
'@emotion/css',
{ nameHint: referenceKey }
)
references[referenceKey].reverse().forEach(reference => {
reference.replaceWith(t.cloneDeep(runtimeNode))
})
})
})

View file

@ -0,0 +1,50 @@
// @flow
import { transformExpressionWithStyles } from './utils'
import { addNamed } from '@babel/helper-module-imports'
import { createMacro } from 'babel-plugin-macros'
export let createEmotionMacro = (instancePath: string) =>
createMacro(function macro({ references, state, babel, isEmotionCall }) {
if (!isEmotionCall) {
state.emotionSourceMap = true
}
let t = babel.types
Object.keys(references).forEach(referenceKey => {
let isPure = true
let runtimeNode = addNamed(state.file.path, referenceKey, instancePath)
switch (referenceKey) {
case 'injectGlobal': {
isPure = false
}
// eslint-disable-next-line no-fallthrough
case 'css':
case 'keyframes': {
references[referenceKey].reverse().forEach(reference => {
const path = reference.parentPath
reference.replaceWith(t.cloneDeep(runtimeNode))
if (isPure) {
path.addComment('leading', '#__PURE__')
}
let node = transformExpressionWithStyles({
babel,
state,
path,
shouldLabel: true
})
if (node) {
path.node.arguments[0] = node
}
})
break
}
default: {
references[referenceKey].reverse().forEach(reference => {
reference.replaceWith(t.cloneDeep(runtimeNode))
})
}
}
})
})

242
web/node_modules/babel-plugin-emotion/src/index.js generated vendored Normal file
View file

@ -0,0 +1,242 @@
// @flow
import { createEmotionMacro } from './emotion-macro'
import { createStyledMacro } from './styled-macro'
import cssMacro, { transformCssCallExpression } from './css-macro'
import { addNamed } from '@babel/helper-module-imports'
import nodePath from 'path'
import { getSourceMap, getStyledOptions } from './utils'
let webStyledMacro = createStyledMacro({
importPath: '@emotion/styled-base',
originalImportPath: '@emotion/styled',
isWeb: true
})
let nativeStyledMacro = createStyledMacro({
importPath: '@emotion/native',
originalImportPath: '@emotion/native',
isWeb: false
})
let primitivesStyledMacro = createStyledMacro({
importPath: '@emotion/primitives',
originalImportPath: '@emotion/primitives',
isWeb: false
})
export const macros = {
createEmotionMacro,
css: cssMacro,
createStyledMacro
}
export type BabelPath = any
export type EmotionBabelPluginPass = any
let emotionCoreMacroThatsNotARealMacro = ({ references, state, babel }) => {
Object.keys(references).forEach(refKey => {
if (refKey === 'css') {
references[refKey].forEach(path => {
transformCssCallExpression({ babel, state, path: path.parentPath })
})
}
})
}
emotionCoreMacroThatsNotARealMacro.keepImport = true
function getAbsolutePath(instancePath: string, rootPath: string) {
if (instancePath.charAt(0) === '.') {
let absoluteInstancePath = nodePath.resolve(rootPath, instancePath)
return absoluteInstancePath
}
return false
}
function getInstancePathToCompare(instancePath: string, rootPath: string) {
let absolutePath = getAbsolutePath(instancePath, rootPath)
if (absolutePath === false) {
return instancePath
}
return absolutePath
}
export default function(babel: *) {
let t = babel.types
return {
name: 'emotion',
inherits: require('babel-plugin-syntax-jsx'),
visitor: {
ImportDeclaration(path: *, state: *) {
const hasFilepath =
path.hub.file.opts.filename &&
path.hub.file.opts.filename !== 'unknown'
let dirname = hasFilepath
? nodePath.dirname(path.hub.file.opts.filename)
: ''
if (
!state.pluginMacros[path.node.source.value] &&
state.emotionInstancePaths.indexOf(
getInstancePathToCompare(path.node.source.value, dirname)
) !== -1
) {
state.pluginMacros[path.node.source.value] = createEmotionMacro(
path.node.source.value
)
}
let pluginMacros = state.pluginMacros
// most of this is from https://github.com/kentcdodds/babel-plugin-macros/blob/master/src/index.js
if (pluginMacros[path.node.source.value] === 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() {}
})
pluginMacros[path.node.source.value]({
references: referencePathsByImportName,
state,
babel,
isBabelMacrosCall: true,
isEmotionCall: true
})
if (!pluginMacros[path.node.source.value].keepImport) {
path.remove()
}
},
Program(path: *, state: *) {
state.emotionInstancePaths = (state.opts.instances || []).map(
instancePath => getInstancePathToCompare(instancePath, process.cwd())
)
state.pluginMacros = {
'@emotion/css': cssMacro,
'@emotion/styled': webStyledMacro,
'@emotion/core': emotionCoreMacroThatsNotARealMacro,
'@emotion/primitives': primitivesStyledMacro,
'@emotion/native': nativeStyledMacro,
emotion: createEmotionMacro('emotion')
}
if (state.opts.cssPropOptimization === undefined) {
for (const node of path.node.body) {
if (
t.isImportDeclaration(node) &&
node.source.value === '@emotion/core' &&
node.specifiers.some(
x => t.isImportSpecifier(x) && x.imported.name === 'jsx'
)
) {
state.transformCssProp = true
break
}
}
} else {
state.transformCssProp = state.opts.cssPropOptimization
}
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) &&
(t.isObjectExpression(path.node.value.expression) ||
t.isArrayExpression(path.node.value.expression))
) {
let expressionPath = path.get('value.expression')
let sourceMap =
state.emotionSourceMap && path.node.loc !== undefined
? getSourceMap(path.node.loc.start, state)
: ''
expressionPath.replaceWith(
t.callExpression(
// the name of this identifier doesn't really matter at all
// it'll never appear in generated code
t.identifier('___shouldNeverAppearCSS'),
[path.node.value.expression]
)
)
transformCssCallExpression({
babel,
state,
path: expressionPath,
sourceMap
})
if (t.isCallExpression(expressionPath)) {
if (!state.cssIdentifier) {
state.cssIdentifier = addNamed(path, 'css', '@emotion/core', {
nameHint: 'css'
})
}
expressionPath
.get('callee')
.replaceWith(t.cloneDeep(state.cssIdentifier))
}
}
},
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)
}
}
}
}
}
}

View file

@ -0,0 +1,113 @@
// @flow
import { createMacro } from 'babel-plugin-macros'
import { addDefault, addNamed } from '@babel/helper-module-imports'
import { transformExpressionWithStyles, getStyledOptions } from './utils'
export let createStyledMacro = ({
importPath,
originalImportPath = importPath,
isWeb
}: {
importPath: string,
originalImportPath?: string,
isWeb: boolean
}) =>
createMacro(({ references, state, babel, isEmotionCall }) => {
if (!isEmotionCall) {
state.emotionSourceMap = true
}
const t = babel.types
if (references.default && references.default.length) {
let _styledIdentifier
let getStyledIdentifier = () => {
if (_styledIdentifier === undefined) {
_styledIdentifier = addDefault(state.file.path, importPath, {
nameHint: 'styled'
})
}
return t.cloneDeep(_styledIdentifier)
}
let originalImportPathStyledIdentifier
let getOriginalImportPathStyledIdentifier = () => {
if (originalImportPathStyledIdentifier === undefined) {
originalImportPathStyledIdentifier = addDefault(
state.file.path,
originalImportPath,
{
nameHint: 'styled'
}
)
}
return t.cloneDeep(originalImportPathStyledIdentifier)
}
if (importPath === originalImportPath) {
getOriginalImportPathStyledIdentifier = getStyledIdentifier
}
references.default.forEach(reference => {
let isCall = false
if (
t.isMemberExpression(reference.parent) &&
reference.parent.computed === false
) {
isCall = true
if (
// checks if the first character is lowercase
// becasue we don't want to transform the member expression if
// it's in primitives/native
reference.parent.property.name.charCodeAt(0) > 96
) {
reference.parentPath.replaceWith(
t.callExpression(getStyledIdentifier(), [
t.stringLiteral(reference.parent.property.name)
])
)
} else {
reference.replaceWith(getStyledIdentifier())
}
} else if (
reference.parentPath &&
reference.parentPath.parentPath &&
t.isCallExpression(reference.parentPath) &&
reference.parent.callee === reference.node
) {
isCall = true
reference.replaceWith(getStyledIdentifier())
} else {
reference.replaceWith(getOriginalImportPathStyledIdentifier())
}
if (reference.parentPath && reference.parentPath.parentPath) {
const styledCallPath = reference.parentPath.parentPath
let node = transformExpressionWithStyles({
path: styledCallPath,
state,
babel,
shouldLabel: false
})
if (node && isWeb) {
// we know the argument length will be 1 since that's the only time we will have a node since it will be static
styledCallPath.node.arguments[0] = node
}
}
if (isCall) {
reference.addComment('leading', '#__PURE__')
if (isWeb) {
reference.parentPath.node.arguments[1] = getStyledOptions(
t,
reference.parentPath,
state
)
}
}
})
}
Object.keys(references)
.filter(x => x !== 'default')
.forEach(referenceKey => {
let runtimeNode = addNamed(state.file.path, referenceKey, importPath)
references[referenceKey].reverse().forEach(reference => {
reference.replaceWith(t.cloneDeep(runtimeNode))
})
})
})

View file

@ -0,0 +1,54 @@
// @flow
import { getLabelFromPath } from './label'
import { getTargetClassName } from './get-target-class-name'
const getKnownProperties = (t: *, node: *) =>
new Set(
node.properties
.filter(n => t.isObjectProperty(n) && !n.computed)
.map(n => (t.isIdentifier(n.key) ? n.key.name : n.key.value))
)
export let getStyledOptions = (t: *, path: *, state: *) => {
let args = path.node.arguments
let optionsArgument = args.length >= 2 ? args[1] : null
let properties = []
let knownProperties =
optionsArgument && t.isObjectExpression(optionsArgument)
? getKnownProperties(t, optionsArgument)
: new Set()
if (!knownProperties.has('target')) {
properties.push(
t.objectProperty(
t.identifier('target'),
t.stringLiteral(getTargetClassName(state, t))
)
)
}
let label = getLabelFromPath(path, state, t)
if (label && !knownProperties.has('label')) {
properties.push(
t.objectProperty(t.identifier('label'), t.stringLiteral(label))
)
}
if (optionsArgument) {
if (!t.isObjectExpression(optionsArgument)) {
return t.callExpression(state.file.addHelper('extends'), [
t.objectExpression([]),
t.objectExpression(properties),
optionsArgument
])
}
properties.unshift(...optionsArgument.properties)
}
return t.objectExpression(
// $FlowFixMe
properties
)
}

View file

@ -0,0 +1,52 @@
// @flow
import findRoot from 'find-root'
import memoize from '@emotion/memoize'
import nodePath from 'path'
import hashString from '@emotion/hash'
import escapeRegexp from 'escape-string-regexp'
let hashArray = (arr: Array<string>) => hashString(arr.join(''))
const unsafeRequire = require
const getPackageRootPath = memoize(filename => findRoot(filename))
const separator = new RegExp(escapeRegexp(nodePath.sep), 'g')
const normalizePath = path => nodePath.normalize(path).replace(separator, '/')
export function getTargetClassName(state: *, t: *) {
if (state.emotionTargetClassNameCount === undefined) {
state.emotionTargetClassNameCount = 0
}
const hasFilepath =
state.file.opts.filename && state.file.opts.filename !== 'unknown'
const filename = hasFilepath ? state.file.opts.filename : ''
// normalize the file path to ignore folder structure
// outside the current node project and arch-specific delimiters
let moduleName = ''
let rootPath = filename
try {
rootPath = getPackageRootPath(filename)
moduleName = unsafeRequire(rootPath + '/package.json').name
} catch (err) {}
const finalPath =
filename === rootPath ? 'root' : filename.slice(rootPath.length)
const positionInFile = state.emotionTargetClassNameCount++
const stuffToHash = [moduleName]
if (finalPath) {
stuffToHash.push(normalizePath(finalPath))
} else {
stuffToHash.push(state.file.code)
}
const stableClassName = `e${hashArray(stuffToHash)}${positionInFile}`
return stableClassName
}

View file

@ -0,0 +1,11 @@
// @flow
export { getExpressionsFromTemplateLiteral } from './minify'
export { getLabelFromPath } from './label'
export { getSourceMap } from './source-maps'
export { getTargetClassName } from './get-target-class-name'
export { simplifyObject } from './object-to-string'
export {
transformExpressionWithStyles
} from './transform-expression-with-styles'
export { getStyledOptions } from './get-styled-options'
export { appendStringToArguments, joinStringLiterals } from './strings'

View file

@ -0,0 +1,135 @@
// @flow
import nodePath from 'path'
const invalidClassNameCharacters = /[!"#$%&'()*+,./:;<=>?@[\]^`|}~{]/g
const sanitizeLabelPart = (labelPart: string) =>
labelPart.trim().replace(invalidClassNameCharacters, '-')
function getLabel(
identifierName?: string,
autoLabel: boolean,
labelFormat?: string,
filename: string
) {
if (!identifierName || !autoLabel) return null
if (!labelFormat) return sanitizeLabelPart(identifierName)
const parsedPath = nodePath.parse(filename)
let localDirname = nodePath.basename(parsedPath.dir)
let localFilename = parsedPath.name
if (localFilename === 'index') {
localFilename = localDirname
}
return labelFormat
.replace(/\[local\]/gi, sanitizeLabelPart(identifierName))
.replace(/\[filename\]/gi, sanitizeLabelPart(localFilename))
.replace(/\[dirname\]/gi, sanitizeLabelPart(localDirname))
}
export function getLabelFromPath(path: *, state: *, t: *) {
return getLabel(
getIdentifierName(path, t),
state.opts.autoLabel === undefined
? process.env.NODE_ENV !== 'production'
: state.opts.autoLabel,
state.opts.labelFormat,
state.file.opts.filename
)
}
let pascalCaseRegex = /^[A-Z][A-Za-z]+/
function getDeclaratorName(path, t) {
// $FlowFixMe
const parent = path.findParent(
p =>
p.isVariableDeclarator() ||
p.isFunctionDeclaration() ||
p.isFunctionExpression() ||
p.isArrowFunctionExpression() ||
p.isObjectProperty()
)
if (!parent) {
return ''
}
// we probably have a css call assigned to a variable
// so we'll just return the variable name
if (parent.isVariableDeclarator()) {
if (t.isIdentifier(parent.node.id)) {
return parent.node.id.name
}
return ''
}
// we probably have an inline css prop usage
if (parent.isFunctionDeclaration()) {
let { name } = parent.node.id
if (pascalCaseRegex.test(name)) {
return name
}
return ''
}
// we could also have an object property
if (parent.isObjectProperty() && !parent.node.computed) {
return parent.node.key.name
}
let variableDeclarator = path.findParent(p => p.isVariableDeclarator())
if (!variableDeclarator) {
return ''
}
let { name } = variableDeclarator.node.id
if (pascalCaseRegex.test(name)) {
return name
}
return ''
}
function getIdentifierName(path: *, t: *) {
let classOrClassPropertyParent
if (
t.isObjectProperty(path.parentPath) &&
path.parentPath.node.computed === false &&
(t.isIdentifier(path.parentPath.node.key) ||
t.isStringLiteral(path.parentPath.node.key))
) {
return path.parentPath.node.key.name || path.parentPath.node.key.value
}
if (path) {
// $FlowFixMe
classOrClassPropertyParent = path.findParent(
p => t.isClassProperty(p) || t.isClass(p)
)
}
if (classOrClassPropertyParent) {
if (
t.isClassProperty(classOrClassPropertyParent) &&
classOrClassPropertyParent.node.computed === false &&
t.isIdentifier(classOrClassPropertyParent.node.key)
) {
return classOrClassPropertyParent.node.key.name
}
if (
t.isClass(classOrClassPropertyParent) &&
classOrClassPropertyParent.node.id
) {
return t.isIdentifier(classOrClassPropertyParent.node.id)
? classOrClassPropertyParent.node.id.name
: ''
}
}
let declaratorName = getDeclaratorName(path, t)
// if the name starts with _ it was probably generated by babel so we should ignore it
if (declaratorName.charAt(0) === '_') {
return ''
}
return declaratorName
}

View file

@ -0,0 +1,96 @@
// @flow
// babel-plugin-styled-components
// https://github.com/styled-components/babel-plugin-styled-components/blob/8d44acc36f067d60d4e09f9c22ff89695bc332d2/src/minify/index.js
const multilineCommentRegex = /\/\*[^!](.|[\r\n])*?\*\//g
const lineCommentStart = /\/\//g
const symbolRegex = /(\s*[;:{},]\s*)/g
// Counts occurences of substr inside str
const countOccurences = (str, substr) => str.split(substr).length - 1
// Joins substrings until predicate returns true
const reduceSubstr = (substrs, join, predicate) => {
const length = substrs.length
let res = substrs[0]
if (length === 1) {
return res
}
for (let i = 1; i < length; i++) {
if (predicate(res)) {
break
}
res += join + substrs[i]
}
return res
}
// Joins at comment starts when it's inside a string or parantheses
// effectively removing line comments
export const stripLineComment = (line: string) =>
reduceSubstr(
line.split(lineCommentStart),
'//',
str =>
!str.endsWith(':') && // NOTE: This is another guard against urls, if they're not inside strings or parantheses.
countOccurences(str, "'") % 2 === 0 &&
countOccurences(str, '"') % 2 === 0 &&
countOccurences(str, '(') === countOccurences(str, ')')
)
export const compressSymbols = (code: string) =>
code.split(symbolRegex).reduce((str, fragment, index) => {
// Even-indices are non-symbol fragments
if (index % 2 === 0) {
return str + fragment
}
// Only manipulate symbols outside of strings
if (
countOccurences(str, "'") % 2 === 0 &&
countOccurences(str, '"') % 2 === 0
) {
return str + fragment.trim()
}
return str + fragment
}, '')
// Detects lines that are exclusively line comments
const isLineComment = line => line.trim().startsWith('//')
const linebreakRegex = /[\r\n]\s*/g
const spacesAndLinebreakRegex = /\s+|\n+/g
function multilineReplacer(match: string) {
// When we encounter a standard multi-line CSS comment and it contains a '@'
// character, we keep the comment but optimize it into a single line. Some
// Stylis plugins, such as the stylis-rtl via the cssjanus plugin, use this
// special comment syntax to control behavior (such as: /* @noflip */).
// We can do this with standard CSS comments because they will work with
// compression, as opposed to non-standard single-line comments that will
// break compressed CSS. If the comment doesn't contain '@', then we replace
// it with a line break, which effectively removes it from the output.
const keepComment = match.indexOf('@') > -1
if (keepComment) {
return match.replace(spacesAndLinebreakRegex, ' ').trim()
}
return '\n'
}
export const minify = (code: string) => {
const newCode = code
.replace(multilineCommentRegex, multilineReplacer) // If allowed, remove line breaks and extra space from multi-line comments so they appear on one line
.split(linebreakRegex) // Split at newlines
.filter(line => line.length > 0 && !isLineComment(line)) // Removes lines containing only line comments
.map(stripLineComment) // Remove line comments inside text
.join(' ') // Rejoin all lines
return compressSymbols(newCode)
}

View file

@ -0,0 +1,88 @@
// @flow
import { minify } from './minify-utils'
export function getExpressionsFromTemplateLiteral(node: *, t: *): Array<*> {
const raw = createRawStringFromTemplateLiteral(node)
const minified = minify(raw)
return replacePlaceholdersWithExpressions(minified, node.expressions || [], t)
}
const interleave = (strings: Array<*>, interpolations: Array<*>) =>
interpolations.reduce(
(array, interp, i) => array.concat([interp], strings[i + 1]),
[strings[0]]
)
function getDynamicMatches(str: string) {
const re = /xxx(\d+)xxx/gm
let match
const matches = []
while ((match = re.exec(str)) !== null) {
// so that flow doesn't complain
if (match !== null) {
matches.push({
value: match[0],
p1: parseInt(match[1], 10),
index: match.index
})
}
}
return matches
}
function replacePlaceholdersWithExpressions(
str: string,
expressions: Array<*>,
t: *
) {
const matches = getDynamicMatches(str)
if (matches.length === 0) {
if (str === '') {
return []
}
return [t.stringLiteral(str)]
}
const strings = []
const finalExpressions = []
let cursor = 0
matches.forEach(({ value, p1, index }, i) => {
const preMatch = str.substring(cursor, index)
cursor = cursor + preMatch.length + value.length
if (preMatch) {
strings.push(t.stringLiteral(preMatch))
} else if (i === 0) {
strings.push(t.stringLiteral(''))
}
finalExpressions.push(expressions[p1])
if (i === matches.length - 1) {
strings.push(t.stringLiteral(str.substring(index + value.length)))
}
})
return interleave(strings, finalExpressions).filter(
(node: { value: string }) => {
return node.value !== ''
}
)
}
function createRawStringFromTemplateLiteral(quasi: {
quasis: Array<{ value: { cooked: string } }>
}) {
let strs = quasi.quasis.map(x => x.value.cooked)
const src = strs
.reduce((arr, str, i) => {
arr.push(str)
if (i !== strs.length - 1) {
arr.push(`xxx${i}xxx`)
}
return arr
}, [])
.join('')
.trim()
return src
}

View file

@ -0,0 +1,40 @@
// @flow
import { serializeStyles } from '@emotion/serialize'
// to anyone looking at this, this isn't intended to simplify every single case
// it's meant to simplify the most common cases so i don't want to make it especially complex
// also, this will be unnecessary when prepack is ready
export function simplifyObject(node: *, t: Object) {
let finalString = ''
for (let i = 0; i < node.properties.length; i++) {
let property = node.properties[i]
if (
!t.isObjectProperty(property) ||
property.computed ||
(!t.isIdentifier(property.key) && !t.isStringLiteral(property.key)) ||
(!t.isStringLiteral(property.value) &&
!t.isNumericLiteral(property.value) &&
!t.isObjectExpression(property.value))
) {
return node
}
let key = property.key.name || property.key.value
if (key === 'styles') {
return node
}
if (t.isObjectExpression(property.value)) {
let simplifiedChild = simplifyObject(property.value, t)
if (!t.isStringLiteral(simplifiedChild)) {
return node
}
finalString += `${key}{${simplifiedChild.value}}`
continue
}
let value = property.value.value
finalString += serializeStyles([{ [key]: value }]).styles
}
return t.stringLiteral(finalString)
}

View file

@ -0,0 +1,45 @@
// @flow
import { SourceMapGenerator } from 'source-map'
import convert from 'convert-source-map'
function getGeneratorOpts(file) {
return file.opts.generatorOpts ? file.opts.generatorOpts : file.opts
}
export function makeSourceMapGenerator(file: *) {
const generatorOpts = getGeneratorOpts(file)
const filename = generatorOpts.sourceFileName
const generator = new SourceMapGenerator({
file: filename,
sourceRoot: generatorOpts.sourceRoot
})
generator.setSourceContent(filename, file.code)
return generator
}
export function getSourceMap(
offset: {
line: number,
column: number
},
state: *
): string {
const generator = makeSourceMapGenerator(state.file)
const generatorOpts = getGeneratorOpts(state.file)
if (
generatorOpts.sourceFileName &&
generatorOpts.sourceFileName !== 'unknown'
) {
generator.addMapping({
generated: {
line: 1,
column: 0
},
source: generatorOpts.sourceFileName,
original: offset
})
return convert.fromObject(generator).toComment({ multiline: true })
}
return ''
}

View file

@ -0,0 +1,45 @@
// @flow
import {
getTypeScriptMakeTemplateObjectPath,
isTaggedTemplateTranspiledByBabel
} from './transpiled-output-utils'
export const appendStringToArguments = (path: *, string: string, t: *) => {
if (!string) {
return
}
const args = path.node.arguments
if (t.isStringLiteral(args[args.length - 1])) {
args[args.length - 1].value += string
} else {
const makeTemplateObjectCallPath = getTypeScriptMakeTemplateObjectPath(path)
if (makeTemplateObjectCallPath) {
makeTemplateObjectCallPath.get('arguments').forEach(argPath => {
const elements = argPath.get('elements')
const lastElement = elements[elements.length - 1]
lastElement.replaceWith(
t.stringLiteral(lastElement.node.value + string)
)
})
} else if (!isTaggedTemplateTranspiledByBabel(path)) {
args.push(t.stringLiteral(string))
}
}
}
export const joinStringLiterals = (expressions: Array<*>, t: *) => {
return expressions.reduce((finalExpressions, currentExpression, i) => {
if (!t.isStringLiteral(currentExpression)) {
finalExpressions.push(currentExpression)
} else if (
t.isStringLiteral(finalExpressions[finalExpressions.length - 1])
) {
finalExpressions[finalExpressions.length - 1].value +=
currentExpression.value
} else {
finalExpressions.push(currentExpression)
}
return finalExpressions
}, [])
}

View file

@ -0,0 +1,174 @@
// @flow
import { serializeStyles } from '@emotion/serialize'
import { getExpressionsFromTemplateLiteral } from './minify'
import { getLabelFromPath } from './label'
import { getSourceMap } from './source-maps'
import { simplifyObject } from './object-to-string'
import { appendStringToArguments, joinStringLiterals } from './strings'
import {
getTypeScriptMakeTemplateObjectPath,
isTaggedTemplateTranspiledByBabel
} from './transpiled-output-utils'
const CSS_OBJECT_STRINGIFIED_ERROR =
"You have tried to stringify object returned from `css` function. It isn't supposed to be used directly (e.g. as value of the `className` prop), but rather handed to emotion so it can handle it (e.g. as value of `css` prop)."
// with babel@6 fallback
const cloneNode = (t, node) =>
typeof t.cloneNode === 'function' ? t.cloneNode(node) : t.cloneDeep(node)
function createSourceMapConditional(t, production, development) {
return t.conditionalExpression(
t.binaryExpression(
'===',
t.memberExpression(
t.memberExpression(t.identifier('process'), t.identifier('env')),
t.identifier('NODE_ENV')
),
t.stringLiteral('production')
),
production,
development
)
}
export let transformExpressionWithStyles = ({
babel,
state,
path,
shouldLabel,
sourceMap = ''
}: {
babel: *,
state: *,
path: *,
shouldLabel: boolean,
sourceMap?: string
}): * => {
let t = babel.types
if (t.isTaggedTemplateExpression(path)) {
const expressions = getExpressionsFromTemplateLiteral(path.node.quasi, t)
if (state.emotionSourceMap && path.node.quasi.loc !== undefined) {
sourceMap = getSourceMap(path.node.quasi.loc.start, state)
}
path.replaceWith(t.callExpression(path.node.tag, expressions))
}
if (t.isCallExpression(path)) {
const canAppendStrings = path.node.arguments.every(
arg => arg.type !== 'SpreadElement'
)
if (canAppendStrings && shouldLabel) {
const label = getLabelFromPath(path, state, t)
if (label) {
appendStringToArguments(path, `;label:${label};`, t)
}
}
path.get('arguments').forEach(node => {
if (t.isObjectExpression(node)) {
node.replaceWith(simplifyObject(node.node, t))
}
})
path.node.arguments = joinStringLiterals(path.node.arguments, t)
if (
canAppendStrings &&
state.emotionSourceMap &&
!sourceMap &&
path.node.loc !== undefined
) {
sourceMap = getSourceMap(path.node.loc.start, state)
}
if (
path.node.arguments.length === 1 &&
t.isStringLiteral(path.node.arguments[0])
) {
let cssString = path.node.arguments[0].value
let res = serializeStyles([cssString])
let prodNode = t.objectExpression([
t.objectProperty(t.identifier('name'), t.stringLiteral(res.name)),
t.objectProperty(t.identifier('styles'), t.stringLiteral(res.styles))
])
let node = prodNode
if (sourceMap) {
if (!state.emotionStringifiedCssId) {
const uid = state.file.scope.generateUidIdentifier(
'__EMOTION_STRINGIFIED_CSS_ERROR__'
)
state.emotionStringifiedCssId = uid
const cssObjectToString = t.functionDeclaration(
uid,
[],
t.blockStatement([
t.returnStatement(t.stringLiteral(CSS_OBJECT_STRINGIFIED_ERROR))
])
)
cssObjectToString._compact = true
state.file.path.unshiftContainer('body', [cssObjectToString])
}
let devNode = t.objectExpression([
t.objectProperty(t.identifier('name'), t.stringLiteral(res.name)),
t.objectProperty(t.identifier('styles'), t.stringLiteral(res.styles)),
t.objectProperty(t.identifier('map'), t.stringLiteral(sourceMap)),
t.objectProperty(
t.identifier('toString'),
cloneNode(t, state.emotionStringifiedCssId)
)
])
node = createSourceMapConditional(t, prodNode, devNode)
}
return node
}
if (sourceMap) {
let lastIndex = path.node.arguments.length - 1
let last = path.node.arguments[lastIndex]
let sourceMapConditional = createSourceMapConditional(
t,
t.stringLiteral(''),
t.stringLiteral(sourceMap)
)
if (t.isStringLiteral(last)) {
path.node.arguments[lastIndex] = t.binaryExpression(
'+',
last,
sourceMapConditional
)
} else {
const makeTemplateObjectCallPath = getTypeScriptMakeTemplateObjectPath(
path
)
if (makeTemplateObjectCallPath) {
const sourceMapId = state.file.scope.generateUidIdentifier(
'emotionSourceMap'
)
const sourceMapDeclaration = t.variableDeclaration('var', [
t.variableDeclarator(sourceMapId, sourceMapConditional)
])
sourceMapDeclaration._compact = true
state.file.path.unshiftContainer('body', [sourceMapDeclaration])
makeTemplateObjectCallPath.get('arguments').forEach(argPath => {
const elements = argPath.get('elements')
const lastElement = elements[elements.length - 1]
lastElement.replaceWith(
t.binaryExpression(
'+',
lastElement.node,
cloneNode(t, sourceMapId)
)
)
})
} else if (!isTaggedTemplateTranspiledByBabel(path)) {
path.node.arguments.push(sourceMapConditional)
}
}
}
}
}

View file

@ -0,0 +1,80 @@
// @flow
// this only works correctly in modules, but we don't run on scripts anyway, so it's fine
// the difference is that in modules template objects are being cached per call site
export function getTypeScriptMakeTemplateObjectPath(path: *) {
if (path.node.arguments.length === 0) {
return null
}
const firstArgPath = path.get('arguments')[0]
if (
firstArgPath.isLogicalExpression() &&
firstArgPath.get('left').isIdentifier() &&
firstArgPath.get('right').isAssignmentExpression() &&
firstArgPath.get('right.right').isCallExpression() &&
firstArgPath.get('right.right.callee').isIdentifier() &&
firstArgPath.node.right.right.callee.name.includes('makeTemplateObject') &&
firstArgPath.node.right.right.arguments.length === 2
) {
return firstArgPath.get('right.right')
}
return null
}
// this is only used to prevent appending strings/expressions to arguments incorectly
// we could push them to found array expressions, as we do it for TS-transpile output ¯\_(ツ)_/¯
// it seems overly complicated though - mainly because we'd also have to check against existing stuff of a particular type (source maps & labels)
// considering Babel double-transpilation as a valid use case seems rather far-fetched
export function isTaggedTemplateTranspiledByBabel(path: *) {
if (path.node.arguments.length === 0) {
return false
}
const firstArgPath = path.get('arguments')[0]
if (
!firstArgPath.isCallExpression() ||
!firstArgPath.get('callee').isIdentifier()
) {
return false
}
const calleeName = firstArgPath.node.callee.name
if (!calleeName.includes('templateObject')) {
return false
}
const bindingPath = path.scope.getBinding(calleeName).path
if (!bindingPath.isFunction()) {
return false
}
const functionBody = bindingPath.get('body.body')
if (!functionBody[0].isVariableDeclaration()) {
return false
}
const declarationInit = functionBody[0].get('declarations')[0].get('init')
if (!declarationInit.isCallExpression()) {
return false
}
const declarationInitArguments = declarationInit.get('arguments')
if (
declarationInitArguments.length === 0 ||
declarationInitArguments.length > 2 ||
declarationInitArguments.some(argPath => !argPath.isArrayExpression())
) {
return false
}
return true
}