Skip to content
5 changes: 2 additions & 3 deletions packages/serialize/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "serialization utils for emotion",
"main": "dist/emotion-serialize.cjs.js",
"module": "dist/emotion-serialize.esm.js",
"types": "types/index.d.ts",
"types": "dist/emotion-serialize.cjs.d.ts",
"license": "MIT",
"repository": "https://github.com/emotion-js/emotion/tree/main/packages/serialize",
"publishConfig": {
Expand All @@ -25,8 +25,7 @@
},
"files": [
"src",
"dist",
"types/*.d.ts"
"dist"
],
"browser": {
"./dist/emotion-serialize.cjs.js": "./dist/emotion-serialize.browser.cjs.js",
Expand Down
200 changes: 146 additions & 54 deletions packages/serialize/src/index.js → packages/serialize/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,71 @@
/* import type {
Interpolation,
SerializedStyles,
RegisteredCache
} from '@emotion/utils' */
import hashString from '@emotion/hash'
import unitless from '@emotion/unitless'
import memoize from '@emotion/memoize'
import { RegisteredCache, SerializedStyles } from '@emotion/utils'
import * as CSS from 'csstype'

type Props = Record<string, unknown>
type Cursor = {
name: string
styles: string
next?: Cursor
}

export type CSSProperties = CSS.PropertiesFallback<number | string>
export type CSSPropertiesWithMultiValues = {
[K in keyof CSSProperties]:
| CSSProperties[K]
| Array<Extract<CSSProperties[K], string>>
}

export type CSSPseudos = { [K in CSS.Pseudos]?: CSSObject }

export interface ArrayCSSInterpolation extends Array<CSSInterpolation> {}

export type InterpolationPrimitive =
| null
| undefined
| boolean
| number
| string
| ComponentSelector
| Keyframes
| SerializedStyles
| CSSObject

export type CSSInterpolation = InterpolationPrimitive | ArrayCSSInterpolation

export interface CSSOthersObject {
[propertiesName: string]: CSSInterpolation
}

export interface CSSObject
extends CSSPropertiesWithMultiValues,
CSSPseudos,
CSSOthersObject {}

export interface ComponentSelector {
__emotion_styles: any
}

export type Keyframes = {
name: string
styles: string
anim: number
toString: () => string
} & string

export interface ArrayInterpolation<Props>
extends Array<Interpolation<Props>> {}

export interface FunctionInterpolation<Props> {
(props: Props): Interpolation<Props>
}

export type Interpolation<Props> =
| InterpolationPrimitive
| ArrayInterpolation<Props>
| FunctionInterpolation<Props>

const ILLEGAL_ESCAPE_SEQUENCE_ERROR = `You have illegal escape sequence in your template literal, most likely inside content's property value.
Because you write your CSS inside a JavaScript string you actually have to do double escaping, so for example "content: '\\00d7';" should become "content: '\\\\00d7';".
Expand All @@ -18,9 +78,30 @@ const UNDEFINED_AS_OBJECT_KEY_ERROR =
let hyphenateRegex = /[A-Z]|^ms/g
let animationRegex = /_EMO_([^_]+?)_([^]*?)_EMO_/g

const isCustomProperty = (property /*: string */) =>
property.charCodeAt(1) === 45
const isProcessableValue = value => value != null && typeof value !== 'boolean'
const isCustomProperty = (property: string) => property.charCodeAt(1) === 45
const isProcessableValue = <Props>(value: Interpolation<Props>) =>
value != null && typeof value !== 'boolean'

const isComponentSelector = (
interpolation: any
): interpolation is ComponentSelector =>
interpolation !== null &&
typeof interpolation === 'object' &&
'__emotion_styles' in interpolation

const isKeyframes = (interpolation: any): interpolation is Keyframes =>
interpolation !== null &&
typeof interpolation === 'object' &&
'anim' in interpolation &&
interpolation.anim === 1

const isSerializedStyles = (
interpolation: any
): interpolation is SerializedStyles =>
interpolation !== null &&
typeof interpolation === 'object' &&
'styles' in interpolation &&
interpolation.styles !== undefined

const processStyleName = /* #__PURE__ */ memoize((styleName /*: string */) =>
isCustomProperty(styleName)
Expand All @@ -29,9 +110,9 @@ const processStyleName = /* #__PURE__ */ memoize((styleName /*: string */) =>
)

let processStyleValue = (
key /*: string */,
value /*: string | number */
) /*: string | number */ => {
key: string,
value: string | number
): string | number => {
switch (key) {
case 'animation':
case 'animationName': {
Expand All @@ -49,7 +130,7 @@ let processStyleValue = (
}

if (
unitless[key] !== 1 &&
unitless[key as keyof typeof unitless] !== 1 &&
!isCustomProperty(key) &&
typeof value === 'number' &&
value !== 0
Expand All @@ -69,9 +150,9 @@ if (process.env.NODE_ENV !== 'production') {
let msPattern = /^-ms-/
let hyphenPattern = /-(.)/g

let hyphenatedCache = {}
let hyphenatedCache: Record<string, boolean | undefined> = {}

processStyleValue = (key /*: string */, value /*: string */) => {
processStyleValue = (key: string, value: string | number) => {
if (key === 'content') {
if (
typeof value !== 'string' ||
Expand Down Expand Up @@ -106,15 +187,15 @@ if (process.env.NODE_ENV !== 'production') {
}
}

function handleInterpolation(
mergedProps /*: void | Object */,
registered /*: RegisteredCache | void */,
interpolation /*: Interpolation */
) /*: string | number */ {
function handleInterpolation<Props>(
mergedProps: Props | undefined,
registered: RegisteredCache | undefined,
interpolation: Interpolation<Props> | TemplateStringsArray
): string | number {
if (interpolation == null) {
return ''
}
if (interpolation.__emotion_styles !== undefined) {
if (isComponentSelector(interpolation)) {
if (
process.env.NODE_ENV !== 'production' &&
interpolation.toString() === 'NO_COMPONENT_SELECTOR'
Expand All @@ -123,15 +204,14 @@ function handleInterpolation(
'Component selectors can only be used in conjunction with @emotion/babel-plugin.'
)
}
return interpolation
}

switch (typeof interpolation) {
case 'boolean': {
return ''
}
case 'object': {
if (interpolation.anim === 1) {
if (isKeyframes(interpolation)) {
cursor = {
name: interpolation.name,
styles: interpolation.styles,
Expand All @@ -140,7 +220,7 @@ function handleInterpolation(

return interpolation.name
}
if (interpolation.styles !== undefined) {
if (isSerializedStyles(interpolation)) {
let next = interpolation.next
if (next !== undefined) {
// not the most efficient thing ever but this is a pretty rare case
Expand Down Expand Up @@ -187,10 +267,10 @@ function handleInterpolation(
}
case 'string':
if (process.env.NODE_ENV !== 'production') {
const matched = []
const matched: string[] = []
const replaced = interpolation.replace(
animationRegex,
(match, p1, p2) => {
(_match, _p1, p2) => {
const fakeVarName = `animation${matched.length}`
matched.push(
`const ${fakeVarName} = keyframes\`${p2.replace(
Expand All @@ -203,30 +283,39 @@ function handleInterpolation(
)
if (matched.length) {
console.error(
'`keyframes` output got interpolated into plain string, please wrap it with `css`.\n\n' +
'Instead of doing this:\n\n' +
[...matched, `\`${replaced}\``].join('\n') +
'\n\nYou should wrap it with `css` like this:\n\n' +
`css\`${replaced}\``
`\`keyframes\` output got interpolated into plain string, please wrap it with \`css\`.

Instead of doing this:

${[...matched, `\`${replaced}\``].join('\n')}

You should wrap it with \`css\` like this:

css\`${replaced}\``
Comment on lines +274 to +282
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rewrote this string interpolation as semantic coloring broke on VSCode due to the complex ' and ` usage
Screen Shot 2021-11-08 at 17 10 03

)
}
}
break
}

// finalize string values (regular strings and functions interpolated into css calls)
const asString = interpolation as string
if (registered == null) {
return interpolation
return asString
}
const cached = registered[interpolation]
return cached !== undefined ? cached : interpolation
const cached = registered[asString]
return cached !== undefined ? cached : asString
}

function createStringFromObject(
mergedProps /*: void | Object */,
registered /*: RegisteredCache | void */,
obj /*: { [key: string]: Interpolation } */
) /*: string */ {
function createStringFromObject<Props>(
mergedProps: Props | undefined,
registered: RegisteredCache | undefined,
obj:
| ArrayInterpolation<Props>
| CSSObject
| ComponentSelector
| TemplateStringsArray
): string {
let string = ''

if (Array.isArray(obj)) {
Expand All @@ -235,8 +324,12 @@ function createStringFromObject(
}
} else {
for (let key in obj) {
let value = obj[key]
if (typeof value !== 'object') {
let value = (obj as any)[key] as Interpolation<Props>
if (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'symbol'
) {
if (registered != null && registered[value] !== undefined) {
string += `${key}{${registered[value]}}`
} else if (isProcessableValue(value)) {
Expand All @@ -260,7 +353,7 @@ function createStringFromObject(
if (isProcessableValue(value[i])) {
string += `${processStyleName(key)}:${processStyleValue(
key,
value[i]
value[i] as string | number
)};`
}
}
Expand Down Expand Up @@ -296,34 +389,33 @@ function createStringFromObject(

let labelPattern = /label:\s*([^\s;\n{]+)\s*(;|$)/g

let sourceMapPattern
let sourceMapPattern: RegExp | undefined
if (process.env.NODE_ENV !== 'production') {
sourceMapPattern =
/\/\*#\ssourceMappingURL=data:application\/json;\S+\s+\*\//g
}

// this is the cursor for keyframes
// keyframes are stored on the SerializedStyles object as a linked list
let cursor
let cursor: Cursor | undefined

export const serializeStyles = function (
args /*: Array<Interpolation> */,
registered /*: RegisteredCache | void */,
mergedProps /*: void | Object */
) /*: SerializedStyles */ {
export const serializeStyles = function <Props>(
args: Array<TemplateStringsArray | Interpolation<Props>>,
registered: RegisteredCache,
mergedProps?: Props
): SerializedStyles {
if (
args.length === 1 &&
typeof args[0] === 'object' &&
args[0] !== null &&
args[0].styles !== undefined
!Array.isArray(args[0]) &&
isSerializedStyles(args[0])
) {
return args[0]
return args[0] as SerializedStyles
}
let stringMode = true
let styles = ''

cursor = undefined
let strings = args[0]
let strings = args[0] as TemplateStringsArray
if (strings == null || strings.raw === undefined) {
stringMode = false
styles += handleInterpolation(mergedProps, registered, strings)
Expand All @@ -345,7 +437,7 @@ export const serializeStyles = function (
}
let sourceMap

if (process.env.NODE_ENV !== 'production') {
if (process.env.NODE_ENV !== 'production' && sourceMapPattern !== undefined) {
styles = styles.replace(sourceMapPattern, match => {
sourceMap = match
return ''
Expand Down
Loading