Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stylis v4 #1817

Merged
merged 25 commits into from
Jun 22, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
78afccd
Stylis v4 tryout
Andarist Mar 20, 2020
2d95e2c
Move @import rules in test to be first
Andarist Mar 28, 2020
174395a
Improved compat plugin
Andarist Mar 28, 2020
87bd0ba
Add tests for orphanated pseudos
Andarist Mar 28, 2020
4477c6a
orphanated -> orphaned
Andarist Mar 28, 2020
24f0d0d
Upgrade stylis and improve compat plugin
Andarist Apr 14, 2020
3833179
Improve compat plugin - avoid double compilation
Andarist Apr 14, 2020
85b523b
Shorten compat plugin a little bit
Andarist Apr 14, 2020
79a679a
Add guard for global top-level rules in the compat plugin
Andarist Apr 14, 2020
c6a79be
Fix tagged templates minifier (#1836)
Andarist Apr 16, 2020
48629c7
Make compat plugin be always included
Andarist Apr 16, 2020
06dd9c2
move removeLabel into omnipresentPlugins
Andarist Apr 17, 2020
4ab60ed
Stop special-casing @import insertion
Andarist Apr 18, 2020
c45a983
fix getServerStylisCache
Andarist Apr 18, 2020
d6bb08d
Remove outdated docs around stylisPlugins and prefix options
Andarist Apr 18, 2020
faa8163
Add changesets
Andarist Apr 18, 2020
0654b2a
Add note about prefixer being just a plugin to stylisPlugins docs
Andarist Apr 18, 2020
5e39d81
Actually use officially published Stylis v4
Andarist Apr 24, 2020
a1d7173
Improve error message about incorrect @import insertion
Andarist Jun 13, 2020
345827d
Fix flow errors
Andarist Jun 14, 2020
f561e5c
Reword error messages and changeset content
Andarist Jun 14, 2020
35baa12
update snapshots
Andarist Jun 14, 2020
c076015
Remove mention of `@import` not being usable in `css` calls as it was…
Andarist Jun 14, 2020
703b1dd
Add mention to the changeset about where one can find a prefixer
Andarist Jun 14, 2020
2ec872c
Update .changeset/warm-ties-drop.md
emmatown Jun 21, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 57 additions & 91 deletions packages/cache/src/index.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
// @flow
import { StyleSheet } from '@emotion/sheet'
import { type EmotionCache, type SerializedStyles } from '@emotion/utils'
import Stylis from '@emotion/stylis'
import {
serialize,
compile,
middleware,
rulesheet,
stringify,
compat,
prefixer
} from '@emotion/stylis'
import weakMemoize from '@emotion/weak-memoize'
import { Sheet, removeLabel, ruleSheet } from './stylis-plugins'
import { removeLabel, createUnsafeSelectorsAlarm } from './stylis-plugins'
import type { StylisPlugin } from './types'

let isBrowser = typeof document !== 'undefined'

export type PrefixOption =
| boolean
| ((key: string, value: string, context: 1 | 2 | 3) => boolean)
type StylisPlugins = StylisPlugin[] | StylisPlugin

export type Options = {
nonce?: string,
stylisPlugins?: StylisPlugins,
stylisPlugins?: StylisPlugin[],
prefix?: PrefixOption,
Andarist marked this conversation as resolved.
Show resolved Hide resolved
key: string,
container?: HTMLElement,
speedy?: boolean,
prepend?: boolean
}

let rootServerStylisCache = {}

let getServerStylisCache = isBrowser
? undefined
: weakMemoize(() => {
Expand All @@ -42,11 +47,11 @@ let getServerStylisCache = isBrowser
}
})

const defaultStylisPlugins = [compat, prefixer]
Andarist marked this conversation as resolved.
Show resolved Hide resolved
let movedStyles = false

let createCache = (options: Options): EmotionCache => {
let key = options.key
let stylisOptions

if (!key) {
throw new Error(
Expand All @@ -66,13 +71,7 @@ let createCache = (options: Options): EmotionCache => {
})
}

if (options.prefix !== undefined) {
stylisOptions = {
prefix: options.prefix
}
}

let stylis = new Stylis(stylisOptions)
const stylisPlugins = options.stylisPlugins || defaultStylisPlugins

if (process.env.NODE_ENV !== 'production') {
// $FlowFixMe
Expand Down Expand Up @@ -115,46 +114,72 @@ let createCache = (options: Options): EmotionCache => {
) => string | void

if (isBrowser) {
stylis.use(options.stylisPlugins)(ruleSheet)
let currentSheet
const omnipresentPlugins = [
removeLabel,
stringify,
rulesheet(rule => {
currentSheet.insert(rule)
})
]
if (process.env.NODE_ENV !== 'production') {
omnipresentPlugins.unshift(
createUnsafeSelectorsAlarm({
get compat() {
return cache.compat
}
})
)
}
const serializer = middleware(stylisPlugins.concat(omnipresentPlugins))
const stylis = styles => serialize(compile(styles), serializer)

insert = (
selector: string,
serialized: SerializedStyles,
sheet: StyleSheet,
shouldCache: boolean
): void => {
let name = serialized.name
Sheet.current = sheet
currentSheet = sheet
if (
process.env.NODE_ENV !== 'production' &&
serialized.map !== undefined
) {
let map = serialized.map
Sheet.current = {
insert: (rule: string) => {
sheet.insert(rule + map)
currentSheet = {
insert: rule => {
sheet.insert(rule + serialized.map)
}
}
}
stylis(selector, serialized.styles)

stylis(selector ? `${selector}{${serialized.styles}}` : serialized.styles)

if (shouldCache) {
cache.inserted[name] = true
cache.inserted[serialized.name] = true
}
}
} else {
stylis.use(removeLabel)
let serverStylisCache = rootServerStylisCache
if (options.stylisPlugins || options.prefix !== undefined) {
stylis.use(options.stylisPlugins)
// $FlowFixMe
serverStylisCache = getServerStylisCache(
options.stylisPlugins || rootServerStylisCache
)(options.prefix)
const omnipresentPlugins = [removeLabel, stringify]
if (process.env.NODE_ENV !== 'production') {
omnipresentPlugins.unshift(
createUnsafeSelectorsAlarm({
get compat() {
return cache.compat
}
})
)
}
const serializer = middleware(stylisPlugins.concat(omnipresentPlugins))
const stylis = styles => serialize(compile(styles), serializer)

// $FlowFixMe
let serverStylisCache = getServerStylisCache(stylisPlugins)
let getRules = (selector: string, serialized: SerializedStyles): string => {
let name = serialized.name
if (serverStylisCache[name] === undefined) {
serverStylisCache[name] = stylis(selector, serialized.styles)
serverStylisCache[name] = stylis(
selector ? `${selector}{${serialized.styles}}` : serialized.styles
)
}
return serverStylisCache[name]
}
Expand Down Expand Up @@ -200,65 +225,6 @@ let createCache = (options: Options): EmotionCache => {
}
}

if (process.env.NODE_ENV !== 'production') {
// https://esbench.com/bench/5bf7371a4cd7e6009ef61d0a
const commentStart = /\/\*/g
const commentEnd = /\*\//g

stylis.use((context, content) => {
switch (context) {
case -1: {
while (commentStart.test(content)) {
commentEnd.lastIndex = commentStart.lastIndex

if (commentEnd.test(content)) {
commentStart.lastIndex = commentEnd.lastIndex
continue
}

throw new Error(
Andarist marked this conversation as resolved.
Show resolved Hide resolved
'Your styles have an unterminated comment ("/*" without corresponding "*/").'
)
}

commentStart.lastIndex = 0
break
}
}
})

stylis.use((context, content, selectors) => {
switch (context) {
case -1: {
const flag =
'emotion-disable-server-rendering-unsafe-selector-warning-please-do-not-use-this-the-warning-exists-for-a-reason'
const unsafePseudoClasses = content.match(
/(:first|:nth|:nth-last)-child/g
)

if (unsafePseudoClasses && cache.compat !== true) {
unsafePseudoClasses.forEach(unsafePseudoClass => {
const ignoreRegExp = new RegExp(
`${unsafePseudoClass}.*\\/\\* ${flag} \\*\\/`
)
const ignore = ignoreRegExp.test(content)

if (unsafePseudoClass && !ignore) {
console.error(
`The pseudo class "${unsafePseudoClass}" is potentially unsafe when doing server-side rendering. Try changing it to "${
unsafePseudoClass.split('-child')[0]
}-of-type".`
)
}
})
}

break
}
}
})
}

const cache: EmotionCache = {
key,
sheet: new StyleSheet({
Expand Down
120 changes: 39 additions & 81 deletions packages/cache/src/stylis-plugins.js
Original file line number Diff line number Diff line change
@@ -1,89 +1,47 @@
// @flow
// https://github.com/thysultan/stylis.js/tree/master/plugins/rule-sheet
// inlined to avoid umd wrapper and peerDep warnings/installing stylis
// since we use stylis after closure compiler
const last = arr => (arr.length ? arr[arr.length - 1] : null)

import type { StylisPlugin } from './types'

const delimiter = '/*|*/'
const needle = delimiter + '}'

function toSheet(block) {
if (block) {
Sheet.current.insert(block + '}')
export let removeLabel = element => {
if (element.type === 'decl') {
var value = element.value
if (
// charcode for l
value.charCodeAt(0) === 108 &&
// charcode for b
value.charCodeAt(2) === 98
) {
// this ignores label
element.return = ''
element.value = ''
}
}
}

export let Sheet: { current: { +insert: string => void } } = {
current: (null: any)
}
const ignoreFlag =
'emotion-disable-server-rendering-unsafe-selector-warning-please-do-not-use-this-the-warning-exists-for-a-reason'

export let ruleSheet: StylisPlugin = (
context,
content,
selectors,
parents,
line,
column,
length,
ns,
depth,
at
) => {
switch (context) {
// property
case 1: {
switch (content.charCodeAt(0)) {
case 64: {
// @import
Sheet.current.insert(content + ';')
return ''
}
// charcode for l
case 108: {
// charcode for b
// this ignores label
if (content.charCodeAt(2) === 98) {
return ''
}
}
}
break
}
// selector
case 2: {
if (ns === 0) return content + delimiter
break
}
// at-rule
case 3: {
switch (ns) {
// @font-face, @page
case 102:
case 112: {
Sheet.current.insert(selectors[0] + content)
return ''
}
default: {
return content + (at === 0 ? delimiter : '')
}
}
}
case -2: {
content.split(needle).forEach(toSheet)
}
}
}
const isIgnoringComment = element =>
!!element &&
element.type === 'comm' &&
element.children.indexOf(ignoreFlag) > -1

export let removeLabel: StylisPlugin = (context, content) => {
if (
context === 1 &&
// charcode for l
content.charCodeAt(0) === 108 &&
// charcode for b
content.charCodeAt(2) === 98
// this ignores label
) {
return ''
export let createUnsafeSelectorsAlarm = cache => (element, index, children) => {
Andarist marked this conversation as resolved.
Show resolved Hide resolved
if (element.type !== 'rule') return

const unsafePseudoClasses = element.value.match(
/(:first|:nth|:nth-last)-child/g
)

if (unsafePseudoClasses && cache.compat !== true) {
const prevElement = index > 0 ? children[index - 1] : null
if (prevElement && isIgnoringComment(last(prevElement.children))) {
return
}
unsafePseudoClasses.forEach(unsafePseudoClass => {
console.error(
`The pseudo class "${unsafePseudoClass}" is potentially unsafe when doing server-side rendering. Try changing it to "${
unsafePseudoClass.split('-child')[0]
}-of-type".`
)
})
}
}
29 changes: 21 additions & 8 deletions packages/cache/src/types.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
// @flow

export type StylisPlugin = (
context: -2 | -1 | 0 | 1 | 2 | 3,
content: string,
selectors: Array<string>,
parents: Array<string>,
export type StylisElement = {
type: string,
value: string,
props: Array<string>,
root: StylisElement | null,
children: Array<StylisElement>,
line: number,
column: number,
length: number,
at: number,
depth: number
) => mixed
return: string
}
export type StylisPluginCallback = (
element: StylisElement,
index: number,
children: Array<StylisElement>,
callback: StylisPluginCallback
) => string | void

export type StylisPlugin = (
element: StylisElement,
index: number,
children: Array<StylisElement>,
callback: StylisPluginCallback
) => string | void
Loading