Load React + Emotion from CDN URLs, use globals

Remove @emotion/babel-preset-css-prop and import { jsx } from
@emotion/core directly instead so Rollup can preserve imports order when
bundling (instead of @emotion/core automatically inserting itself as the
first import).
This commit is contained in:
Michael Jackson
2019-01-23 21:27:58 -08:00
parent 6228f0de5c
commit bbb092d974
117 changed files with 109774 additions and 1773 deletions

View File

@ -0,0 +1,135 @@
// @flow
import * as React from 'react'
import { getRegisteredStyles, insertStyles } from '@emotion/utils'
import { serializeStyles } from '@emotion/serialize'
import { withEmotionCache, ThemeContext } from './context'
import { isBrowser } from './utils'
type ClassNameArg =
| string
| boolean
| { [key: string]: boolean }
| Array<ClassNameArg>
| null
| void
let classnames = (args: Array<ClassNameArg>): string => {
let len = args.length
let i = 0
let cls = ''
for (; i < len; i++) {
let arg = args[i]
if (arg == null) continue
let toAdd
switch (typeof arg) {
case 'boolean':
break
case 'object': {
if (Array.isArray(arg)) {
toAdd = classnames(arg)
} else {
toAdd = ''
for (const k in arg) {
if (arg[k] && k) {
toAdd && (toAdd += ' ')
toAdd += k
}
}
}
break
}
default: {
toAdd = arg
}
}
if (toAdd) {
cls && (cls += ' ')
cls += toAdd
}
}
return cls
}
function merge(
registered: Object,
css: (...args: Array<any>) => string,
className: string
) {
const registeredStyles = []
const rawClassName = getRegisteredStyles(
registered,
registeredStyles,
className
)
if (registeredStyles.length < 2) {
return className
}
return rawClassName + css(registeredStyles)
}
type Props = {
children: ({
css: (...args: Array<any>) => string,
cx: (...args: Array<ClassNameArg>) => string,
theme: Object
}) => React.Node
}
export const ClassNames = withEmotionCache<Props>((props, context) => {
return (
<ThemeContext.Consumer>
{theme => {
let rules = ''
let serializedHashes = ''
let hasRendered = false
let css = (...args: Array<any>) => {
if (hasRendered && process.env.NODE_ENV !== 'production') {
throw new Error('css can only be used during render')
}
let serialized = serializeStyles(args, context.registered)
if (isBrowser) {
insertStyles(context, serialized, false)
} else {
let res = insertStyles(context, serialized, false)
if (res !== undefined) {
rules += res
}
}
if (!isBrowser) {
serializedHashes += ` ${serialized.name}`
}
return `${context.key}-${serialized.name}`
}
let cx = (...args: Array<ClassNameArg>) => {
if (hasRendered && process.env.NODE_ENV !== 'production') {
throw new Error('cx can only be used during render')
}
return merge(context.registered, css, classnames(args))
}
let content = { css, cx, theme }
let ele = props.children(content)
hasRendered = true
if (!isBrowser && rules.length !== 0) {
return (
<React.Fragment>
<style
{...{
[`data-emotion-${context.key}`]: serializedHashes.substring(
1
),
dangerouslySetInnerHTML: { __html: rules },
nonce: context.sheet.nonce
}}
/>
{ele}
</React.Fragment>
)
}
return ele
}}
</ThemeContext.Consumer>
)
})

View File

@ -0,0 +1,71 @@
// @flow
import { type EmotionCache } from '@emotion/utils'
import * as React from 'react'
import createCache from '@emotion/cache'
import { isBrowser } from './utils'
let EmotionCacheContext: React.Context<EmotionCache | null> = React.createContext(
isBrowser ? createCache() : null
)
export let ThemeContext = React.createContext<Object>({})
export let CacheProvider = EmotionCacheContext.Provider
let withEmotionCache = function withEmotionCache<Props>(
func: (props: Props, cache: EmotionCache, ref: React.Ref<*>) => React.Node
): React.StatelessFunctionalComponent<Props> {
let render = (props: Props, ref: React.Ref<*>) => {
return (
<EmotionCacheContext.Consumer>
{(
// $FlowFixMe we know it won't be null
cache: EmotionCache
) => {
return func(props, cache, ref)
}}
</EmotionCacheContext.Consumer>
)
}
// $FlowFixMe
return React.forwardRef(render)
}
if (!isBrowser) {
class BasicProvider extends React.Component<
{ children: EmotionCache => React.Node },
{ value: EmotionCache }
> {
state = { value: createCache() }
render() {
return (
<EmotionCacheContext.Provider {...this.state}>
{this.props.children(this.state.value)}
</EmotionCacheContext.Provider>
)
}
}
withEmotionCache = function withEmotionCache<Props>(
func: (props: Props, cache: EmotionCache) => React.Node
): React.StatelessFunctionalComponent<Props> {
return (props: Props) => (
<EmotionCacheContext.Consumer>
{context => {
if (context === null) {
return (
<BasicProvider>
{newContext => {
return func(props, newContext)
}}
</BasicProvider>
)
} else {
return func(props, context)
}
}}
</EmotionCacheContext.Consumer>
)
}
}
export { withEmotionCache }

View File

@ -0,0 +1,144 @@
// @flow
import * as React from 'react'
import { withEmotionCache, ThemeContext } from './context'
import {
type EmotionCache,
type SerializedStyles,
insertStyles
} from '@emotion/utils'
import { isBrowser } from './utils'
import { StyleSheet } from '@emotion/sheet'
import { serializeStyles } from '@emotion/serialize'
type Styles = Object | Array<Object>
type GlobalProps = {
+styles: Styles | (Object => Styles)
}
let warnedAboutCssPropForGlobal = false
export let Global: React.StatelessFunctionalComponent<
GlobalProps
> = /* #__PURE__ */ withEmotionCache((props: GlobalProps, cache) => {
if (
process.env.NODE_ENV !== 'production' &&
!warnedAboutCssPropForGlobal &&
// check for className as well since the user is
// probably using the custom createElement which
// means it will be turned into a className prop
// $FlowFixMe I don't really want to add it to the type since it shouldn't be used
(props.className || props.css)
) {
console.error(
"It looks like you're using the css prop on Global, did you mean to use the styles prop instead?"
)
warnedAboutCssPropForGlobal = true
}
let styles = props.styles
if (typeof styles === 'function') {
return (
<ThemeContext.Consumer>
{theme => {
let serialized = serializeStyles([styles(theme)])
return <InnerGlobal serialized={serialized} cache={cache} />
}}
</ThemeContext.Consumer>
)
}
let serialized = serializeStyles([styles])
return <InnerGlobal serialized={serialized} cache={cache} />
})
type InnerGlobalProps = {
serialized: SerializedStyles,
cache: EmotionCache
}
// maintain place over rerenders.
// initial render from browser, insertBefore context.sheet.tags[0] or if a style hasn't been inserted there yet, appendChild
// initial client-side render from SSR, use place of hydrating tag
class InnerGlobal extends React.Component<InnerGlobalProps> {
sheet: StyleSheet
componentDidMount() {
this.sheet = new StyleSheet({
key: `${this.props.cache.key}-global`,
nonce: this.props.cache.sheet.nonce,
container: this.props.cache.sheet.container
})
// $FlowFixMe
let node: HTMLStyleElement | null = document.querySelector(
`style[data-emotion-${this.props.cache.key}="${
this.props.serialized.name
}"]`
)
if (node !== null) {
this.sheet.tags.push(node)
}
if (this.props.cache.sheet.tags.length) {
this.sheet.before = this.props.cache.sheet.tags[0]
}
this.insertStyles()
}
componentDidUpdate(prevProps) {
if (prevProps.serialized.name !== this.props.serialized.name) {
this.insertStyles()
}
}
insertStyles() {
if (this.props.serialized.next !== undefined) {
// insert keyframes
insertStyles(this.props.cache, this.props.serialized.next, true)
}
if (this.sheet.tags.length) {
// if this doesn't exist then it will be null so the style element will be appended
let element = this.sheet.tags[this.sheet.tags.length - 1]
.nextElementSibling
this.sheet.before = ((element: any): Element | null)
this.sheet.flush()
}
this.props.cache.insert(``, this.props.serialized, this.sheet, false)
}
componentWillUnmount() {
this.sheet.flush()
}
render() {
if (!isBrowser) {
let { serialized } = this.props
let serializedNames = serialized.name
let serializedStyles = serialized.styles
let next = serialized.next
while (next !== undefined) {
serializedNames += ' ' + next.name
serializedStyles += next.styles
next = next.next
}
let rules = this.props.cache.insert(
``,
{ name: serializedNames, styles: serializedStyles },
this.sheet,
false
)
return (
<style
{...{
[`data-emotion-${this.props.cache.key}`]: serializedNames,
dangerouslySetInnerHTML: { __html: rules },
nonce: this.props.cache.sheet.nonce
}}
/>
)
}
return null
}
}

View File

@ -0,0 +1,7 @@
// @flow
export { withEmotionCache, CacheProvider, ThemeContext } from './context'
export { jsx } from './jsx'
export { Global } from './global'
export { keyframes } from './keyframes'
export { ClassNames } from './class-names'
export { default as css } from '@emotion/css'

View File

@ -0,0 +1,166 @@
// @flow
import * as React from 'react'
import { withEmotionCache, ThemeContext } from './context'
import { getRegisteredStyles, insertStyles } from '@emotion/utils'
import { isBrowser } from './utils'
import { serializeStyles } from '@emotion/serialize'
let typePropName = '__EMOTION_TYPE_PLEASE_DO_NOT_USE__'
let labelPropName = '__EMOTION_LABEL_PLEASE_DO_NOT_USE__'
let hasOwnProperty = Object.prototype.hasOwnProperty
let render = (cache, props, theme: null | Object, ref) => {
let type = props[typePropName]
let registeredStyles = []
let className = ''
let cssProp = theme === null ? props.css : props.css(theme)
// so that using `css` from `emotion` and passing the result to the css prop works
// not passing the registered cache to serializeStyles because it would
// make certain babel optimisations not possible
if (typeof cssProp === 'string' && cache.registered[cssProp] !== undefined) {
cssProp = cache.registered[cssProp]
}
registeredStyles.push(cssProp)
if (props.className !== undefined) {
className = getRegisteredStyles(
cache.registered,
registeredStyles,
props.className
)
}
let serialized = serializeStyles(registeredStyles)
if (
process.env.NODE_ENV !== 'production' &&
serialized.name.indexOf('-') === -1
) {
let labelFromStack = props[labelPropName]
if (labelFromStack) {
serialized = serializeStyles([
serialized,
'label:' + labelFromStack + ';'
])
}
}
const rules = insertStyles(cache, serialized, typeof type === 'string')
className += `${cache.key}-${serialized.name}`
const newProps = {}
for (let key in props) {
if (
hasOwnProperty.call(props, key) &&
key !== 'css' &&
key !== typePropName &&
(process.env.NODE_ENV === 'production' || key !== labelPropName)
) {
newProps[key] = props[key]
}
}
newProps.ref = ref
newProps.className = className
const ele = React.createElement(type, newProps)
if (!isBrowser && rules !== undefined) {
let serializedNames = serialized.name
let next = serialized.next
while (next !== undefined) {
serializedNames += ' ' + next.name
next = next.next
}
return (
<React.Fragment>
<style
{...{
[`data-emotion-${cache.key}`]: serializedNames,
dangerouslySetInnerHTML: { __html: rules },
nonce: cache.sheet.nonce
}}
/>
{ele}
</React.Fragment>
)
}
return ele
}
let Emotion = withEmotionCache((props, cache, ref) => {
// use Context.read for the theme when it's stable
if (typeof props.css === 'function') {
return (
<ThemeContext.Consumer>
{theme => render(cache, props, theme, ref)}
</ThemeContext.Consumer>
)
}
return render(cache, props, null, ref)
})
// $FlowFixMe
export const jsx: typeof React.createElement = function(
type: React.ElementType,
props: Object
) {
let args = arguments
if (props == null || props.css == null) {
// $FlowFixMe
return React.createElement.apply(undefined, args)
}
if (
process.env.NODE_ENV !== 'production' &&
typeof props.css === 'string' &&
// check if there is a css declaration
props.css.indexOf(':') !== -1
) {
throw new Error(
`Strings are not allowed as css prop values, please wrap it in a css template literal from '@emotion/css' like this: css\`${
props.css
}\``
)
}
let argsLength = args.length
let createElementArgArray = new Array(argsLength)
createElementArgArray[0] = Emotion
let newProps = {}
for (let key in props) {
if (hasOwnProperty.call(props, key)) {
newProps[key] = props[key]
}
}
newProps[typePropName] = type
if (process.env.NODE_ENV !== 'production') {
let error = new Error()
if (error.stack) {
// chrome
let match = error.stack.match(/at jsx.*\n\s+at ([A-Z][A-Za-z]+) /)
if (!match) {
// safari and firefox
match = error.stack.match(/^.*\n([A-Z][A-Za-z]+)@/)
}
if (match) {
newProps[labelPropName] = match[1]
}
}
}
createElementArgArray[1] = newProps
for (let i = 2; i < argsLength; i++) {
createElementArgArray[i] = args[i]
}
// $FlowFixMe
return React.createElement.apply(null, createElementArgArray)
}

View File

@ -0,0 +1,23 @@
// @flow
import css from '@emotion/css'
type Keyframes = {|
name: string,
styles: string,
anim: 1,
toString: () => string
|} & string
export const keyframes = (...args: *): Keyframes => {
let insertable = css(...args)
const name = `animation-${insertable.name}`
// $FlowFixMe
return {
name,
styles: `@keyframes ${name}{${insertable.styles}}`,
anim: 1,
toString() {
return `_EMO_${this.name}_${this.styles}_EMO_`
}
}
}

View File

@ -0,0 +1,2 @@
// @flow
export let isBrowser = typeof document !== 'undefined'