diff --git a/package.json b/package.json index 2eb093567..bf2a7df71 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "enzyme-to-json": "^1.5.1", "jest": "^20.0.4", "jest-cli": "^20.0.4", - "jest-glamor-react": "^3.0.0", + "jest-glamor-react": "^3.1.0", "npm-run-all": "^4.0.2", "polished": "^1.2.1", "prettier-eslint-cli": "^4.0.3", diff --git a/src/glamor/CSSPropertyOperations/CSSProperty.js b/src/glamor/CSSPropertyOperations/CSSProperty.js index 359065d40..dd4482cc2 100644 --- a/src/glamor/CSSPropertyOperations/CSSProperty.js +++ b/src/glamor/CSSPropertyOperations/CSSProperty.js @@ -79,7 +79,7 @@ let prefixes = ['Webkit', 'ms', 'Moz', 'O'] // Using Object.keys here, or else the vanilla for-in loop makes IE8 go into an // infinite loop, because it iterates over the newly added props too. -keys(isUnitlessNumber).forEach(function (prop) { +forEach(keys(isUnitlessNumber), function (prop) { forEach(prefixes, function (prefix) { isUnitlessNumber[prefixKey(prefix, prop)] = 1 }) diff --git a/src/glamor/CSSPropertyOperations/index.js b/src/glamor/CSSPropertyOperations/index.js index 4de00151c..9a16c49c7 100644 --- a/src/glamor/CSSPropertyOperations/index.js +++ b/src/glamor/CSSPropertyOperations/index.js @@ -10,10 +10,13 @@ */ import dangerousStyleValue from './dangerousStyleValue' -import hyphenateStyleName from 'fbjs/lib/hyphenateStyleName' import memoizeStringOnly from 'fbjs/lib/memoizeStringOnly' -export const processStyleName = memoizeStringOnly(hyphenateStyleName) +const hyphenateRegex = /[A-Z]|^ms/g + +export const processStyleName = memoizeStringOnly(styleName => + styleName.replace(hyphenateRegex, '-$&').toLowerCase() +) if (process.env.NODE_ENV !== 'production') { const warning = require('fbjs/lib/warning') diff --git a/src/glamor/clean.js b/src/glamor/clean.js index 0e645f0fe..99b7b01f2 100644 --- a/src/glamor/clean.js +++ b/src/glamor/clean.js @@ -16,7 +16,7 @@ function cleanObject (object) { let acc = {} let hasFalsy = false - forEach(keys(object), (value) => { + forEach(keys(object), value => { const filteredValue = clean(value) if (filteredValue === null || filteredValue !== value) { hasFalsy = true diff --git a/src/index.js b/src/index.js index 1fdb73d04..9f552b54d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,22 +1,16 @@ // @flow import { StyleSheet } from './sheet' -import { forEach, map, reduce } from './utils' +import { forEach, map, reduce, keys, assign } from './utils' import { hashString as hash, hashObject } from './hash' import { createMarkupForStyles } from './glamor/CSSPropertyOperations' import clean from './glamor/clean.js' -const IS_DEV = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV - export const sheet = new StyleSheet() // 🚀 sheet.inject() export let inserted: { [string | number]: boolean | void } = {} -type inputVar = string | number - -type vars = Array - export function flush () { sheet.flush() inserted = {} @@ -45,27 +39,15 @@ function _getRegistered (rule) { return rule } -// The idea on how to merge object class names come from glamorous -// 💄 -// https://github.com/paypal/glamorous/blob/master/src/get-glamor-classname.js -function getEmotionStylesFromClassName (className) { - const id = className.trim().slice('css-'.length) - if (sheet.registered[id]) { - return sheet.registered[id].style - } else { - return [] - } -} - function buildStyles (objs) { let computedClassName = '' let objectStyles = [] - // This needs to be moved into the core forEach(objs, (cls): void => { if (typeof cls === 'string') { - if (cls.trim().indexOf('css-') === 0) { - objectStyles.push(getEmotionStylesFromClassName(cls)) + const match = emotionClassRegex.exec(cls) + if (match) { + objectStyles.push(ruleCache[match[1]]) } else { computedClassName && (computedClassName += ' ') computedClassName += cls @@ -118,7 +100,7 @@ export function injectGlobal ( // injectGlobal is flattened by postcss // we don't support nested selectors on objects forEach(combined, obj => { - forEach(Object.keys(obj), selector => { + forEach(keys(obj), selector => { insertRawRule(`${selector} {${createMarkupForStyles(obj[selector])}}`) }) }) @@ -131,7 +113,7 @@ export function fontFace ( ) { const combined = reduce( content ? objs.concat(content.apply(null, vars)) : objs, - (accum, item, i) => Object.assign(accum, item), + (accum, item, i) => assign(accum, item), {} ) @@ -141,7 +123,7 @@ export function fontFace ( function insertKeyframe (spec) { if (!inserted[spec.id]) { const inner = map( - Object.keys(spec.keyframes), + keys(spec.keyframes), kf => `${kf} {${createMarkupForStyles(spec.keyframes[kf])}}` ).join('') @@ -181,16 +163,12 @@ type EmotionRule = { [string]: any } type CSSRuleList = Array -type EmotionClassName = { - [string]: any -} - -let cachedCss: (rules: CSSRuleList) => EmotionClassName = +let cachedCss: (rules: CSSRuleList) => EmotionRule = typeof WeakMap !== 'undefined' ? multiIndexCache(_css) : _css // 🍩 // https://github.com/threepointone/glamor -export function objStyle (...rules: CSSRuleList): EmotionClassName { +export function objStyle (...rules: CSSRuleList): EmotionRule { rules = clean(rules) if (!rules) { return nullrule @@ -211,46 +189,52 @@ function _css (rules) { return toRule(spec) } +const emotionClassRegex = /css-([a-zA-Z0-9]+)/ + // of shape { 'data-css-': '' } export function isLikeRule (rule: EmotionRule) { - let keys = Object.keys(rule).filter(x => x !== 'toString') - if (keys.length !== 1) { + const ruleKeys = keys(rule) + if (ruleKeys.length !== 1) { return false } - return !!/css-([a-zA-Z0-9]+)/.exec(keys[0]) + return !!emotionClassRegex.exec(ruleKeys[0]) } // extracts id from a { 'css-': ''} like object export function idFor (rule: EmotionRule) { - let keys = Object.keys(rule).filter(x => x !== 'toString') - if (keys.length !== 1) throw new Error('not a rule') - let regex = /css-([a-zA-Z0-9]+)/ - let match = regex.exec(keys[0]) + const ruleKeys = keys(rule) + if (ruleKeys.length !== 1) throw new Error('not a rule') + let match = emotionClassRegex.exec(ruleKeys[0]) if (!match) throw new Error('not a rule') return match[1] } +const parentSelectorRegex = /&/gm + function selector (id: string, path: string = '') { if (!id) { - return path.replace(/&/g, '') + return path.replace(parentSelectorRegex, '') } if (!path) return `.css-${id}` - let x = path - .split(',') - .map( - x => - x.indexOf('&') >= 0 ? x.replace(/&/gm, `.css-${id}`) : `.css-${id}${x}` - ) - .join(',') + let x = map( + path.split(','), + x => + x.indexOf('&') >= 0 + ? x.replace(parentSelectorRegex, `.css-${id}`) + : `.css-${id}${x}` + ).join(',') return x } function deconstruct (style) { // we can be sure it's not infinitely nested here - let plain, selects, medias, supports - Object.keys(style).forEach(key => { + let plain + let selects + let medias + let supports + forEach(keys(style), key => { if (key.indexOf('&') >= 0) { selects = selects || {} selects[key] = style[key] @@ -276,17 +260,17 @@ function deconstructedStyleToCSS (id, style) { css.push(`${selector(id)}{${createMarkupForStyles(plain)}}`) } if (selects) { - Object.keys(selects).forEach((key: string) => + forEach(keys(selects), (key: string) => css.push(`${selector(id, key)}{${createMarkupForStyles(selects[key])}}`) ) } if (medias) { - Object.keys(medias).forEach(key => + forEach(keys(medias), key => css.push(`${key}{${deconstructedStyleToCSS(id, medias[key]).join('')}}`) ) } if (supports) { - Object.keys(supports).forEach(key => + forEach(keys(supports), key => css.push(`${key}{${deconstructedStyleToCSS(id, supports[key]).join('')}}`) ) } @@ -298,7 +282,7 @@ function insert (spec) { if (!inserted[spec.id]) { inserted[spec.id] = true let deconstructed = deconstruct(spec.style) - deconstructedStyleToCSS(spec.id, deconstructed).map(cssRule => + map(deconstructedStyleToCSS(spec.id, deconstructed), cssRule => sheet.insert(cssRule) ) } @@ -342,9 +326,11 @@ function joinSelectors (a, b) { let as = map(a.split(','), a => (!(a.indexOf('&') >= 0) ? '&' + a : a)) let bs = map(b.split(','), b => (!(b.indexOf('&') >= 0) ? '&' + b : b)) - return bs - .reduce((arr, b) => arr.concat(as.map(a => b.replace(/&/g, a))), []) - .join(',') + return reduce( + bs, + (arr, b) => arr.concat(map(as, a => b.replace(parentSelectorRegex, a))), + [] + ).join(',') } function joinMediaQueries (a, b) { @@ -366,10 +352,11 @@ function joinSupports (a, b) { // flatten a nested array function flatten (inArr) { let arr = [] - for (let i = 0; i < inArr.length; i++) { - if (Array.isArray(inArr[i])) arr = arr.concat(flatten(inArr[i])) - else arr = arr.concat(inArr[i]) - } + forEach(inArr, val => { + if (Array.isArray(val)) arr = arr.concat(flatten(val)) + else arr = arr.concat(val) + }) + return arr } @@ -380,7 +367,7 @@ function build (dest, { selector = '', mq = '', supp = '', src = {} }) { } src = flatten(src) - src.forEach(_src => { + forEach(src, _src => { if (isLikeRule(_src)) { let reg = _getRegistered(_src) if (reg.type !== 'css') { @@ -392,7 +379,7 @@ function build (dest, { selector = '', mq = '', supp = '', src = {} }) { if (_src && _src.composes) { build(dest, { selector, mq, supp, src: _src.composes }) } - Object.keys(_src || {}).forEach(key => { + forEach(keys(_src || {}), key => { if (isSelector(key)) { build(dest, { selector: joinSelectors(selector, key), @@ -478,8 +465,8 @@ function multiIndexCache (fn) { } let value = fn(args) if (inputCaches[args.length]) { - let ctr = 0, - coi = inputCaches[args.length] + let ctr = 0 + let coi = inputCaches[args.length] while (ctr < args.length - 1) { coi = coi.get(args[ctr]) ctr++ @@ -487,7 +474,10 @@ function multiIndexCache (fn) { try { coi.set(args[ctr], value) } catch (err) { - if (IS_DEV && !warnedWeakMapError) { + if ( + (process.env.NODE_ENV === 'development' || !process.env.NODE_ENV) && + !warnedWeakMapError + ) { warnedWeakMapError = true console.warn('failed setting the WeakMap cache for args:', ...args) // eslint-disable-line no-console console.warn( diff --git a/src/parser.js b/src/parser.js index 0bba7dbb6..0febea3ce 100644 --- a/src/parser.js +++ b/src/parser.js @@ -1,9 +1,8 @@ // @flow import parse from 'postcss-safe-parser' import postcssNested from 'postcss-nested' -import stringify from 'postcss/lib/stringify' import postcssJs from 'postcss-js' -import objParse from './obj-parse' +import objParse from 'postcss-js/parser' import autoprefixer from 'autoprefixer' import { processStyleName } from './glamor/CSSPropertyOperations' import { objStyle } from './index' @@ -71,13 +70,7 @@ export function parseCSS ( } function stringifyCSSRoot (root) { - return root.nodes.map((node, i) => { - let str = '' - stringify(node, x => { - str += x - }) - return str - }) + return root.nodes.map(node => node.toString()) } export function expandCSSFallbacks (style: { [string]: any }) { diff --git a/src/react/index.js b/src/react/index.js index bee792882..1542fd03b 100644 --- a/src/react/index.js +++ b/src/react/index.js @@ -1,7 +1,7 @@ import { Component, createElement as h } from 'react' import PropTypes from 'prop-types' import { css } from '../index' -import { map, omit, reduce } from '../utils' +import { map, omit, reduce, assign } from '../utils' import { CHANNEL } from './constants' export { @@ -50,10 +50,9 @@ export default function (tag, objs, vars = [], content) { render () { const { props, state, context } = this - const mergedProps = { - ...props, + const mergedProps = assign({}, props, { theme: state.theme - } + }) const getValue = v => { if (v && typeof v === 'function') { @@ -102,11 +101,10 @@ export default function (tag, objs, vars = [], content) { return h( tag, omit( - { - ...mergedProps, + assign({}, mergedProps, { ref: mergedProps.innerRef, className - }, + }), ['innerRef', 'theme'] ) ) diff --git a/src/sheet.js b/src/sheet.js index 34508bb66..586ad86cb 100644 --- a/src/sheet.js +++ b/src/sheet.js @@ -22,6 +22,8 @@ styleSheet.flush() */ +import { forEach } from './utils' + function last (arr) { return arr[arr.length - 1] } @@ -61,23 +63,22 @@ function makeStyleTag () { return tag } -export function StyleSheet ( - { - speedy = !isDev && !isTest, - maxLength = isBrowser && oldIE ? 4000 : 65000 - }: { speedy: boolean, maxLength: number } = {} -) { - this.isSpeedy = speedy // the big drawback here is that the css won't be editable in devtools - this.sheet = undefined - this.tags = [] - this.maxLength = maxLength - this.ctr = 0 -} - -Object.assign(StyleSheet.prototype, { +export class StyleSheet { + constructor ( + { + speedy = !isDev && !isTest, + maxLength = isBrowser && oldIE ? 4000 : 65000 + }: { speedy: boolean, maxLength: number } = {} + ) { + this.isSpeedy = speedy // the big drawback here is that the css won't be editable in devtools + this.sheet = undefined + this.tags = [] + this.maxLength = maxLength + this.ctr = 0 + } getSheet () { return sheetForTag(last(this.tags)) - }, + } inject () { if (this.injected) { throw new Error('already injected!') @@ -96,14 +97,14 @@ Object.assign(StyleSheet.prototype, { } } this.injected = true - }, + } speedy (bool) { if (this.ctr !== 0) { // cannot change speedy mode after inserting any rule to sheet. Either call speedy(${bool}) earlier in your app, or call flush() before speedy(${bool}) throw new Error(`cannot change speedy now`) } this.isSpeedy = !!bool - }, + } _insert (rule) { // this weirdness for perf, and chrome's weird bug // https://stackoverflow.com/questions/20007992/chrome-suddenly-stopped-accepting-insertrule @@ -119,7 +120,7 @@ Object.assign(StyleSheet.prototype, { console.warn('illegal rule', rule) // eslint-disable-line no-console } } - }, + } insert (rule) { if (isBrowser) { // this is the ultrafast version, works across browsers @@ -150,14 +151,14 @@ Object.assign(StyleSheet.prototype, { this.tags.push(makeStyleTag()) } return this.ctr - 1 - }, + } delete (index) { // we insert a blank rule when 'deleting' so previously returned indexes remain stable return this.replace(index, '') - }, + } flush () { if (isBrowser) { - this.tags.forEach(tag => tag.parentNode.removeChild(tag)) + forEach(this.tags, tag => tag.parentNode.removeChild(tag)) this.tags = [] this.sheet = null this.ctr = 0 @@ -167,15 +168,15 @@ Object.assign(StyleSheet.prototype, { this.sheet.cssRules = [] } this.injected = false - }, + } rules () { if (!isBrowser) { return this.sheet.cssRules } let arr = [] - this.tags.forEach(tag => + forEach(this.tags, tag => arr.splice(arr.length, 0, ...Array.from(sheetForTag(tag).cssRules)) ) return arr } -}) +} diff --git a/src/utils.js b/src/utils.js index 00febe4ac..af5c99c3b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,4 +1,4 @@ -// @flow +// @flow weak export function omit (obj: { [string]: any }, keys: Array) { let target: { [string]: any } = {} let i: string @@ -74,3 +74,19 @@ export function reduce ( return out } + +export const assign: any = + Object.assign || + function (target) { + let i = 1 + let length = arguments.length + for (;i < length; i++) { + var source = arguments[i] + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key] + } + } + } + return target + } diff --git a/test/__snapshots__/css.test.js.snap b/test/__snapshots__/css.test.js.snap index dfaec41dc..e9d05a07e 100644 --- a/test/__snapshots__/css.test.js.snap +++ b/test/__snapshots__/css.test.js.snap @@ -1,5 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`css @supports 1`] = ` +@supports (display: grid) { + .glamor-0 { + display: grid; + } +} + +
+`; + exports[`css composes 1`] = ` .glamor-0 { display: -webkit-box; @@ -60,6 +72,30 @@ exports[`css composes with undefined values 1`] = ` /> `; +exports[`css composition stuff 1`] = ` +.glamor-0 { + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; +} + +
+`; + +exports[`css composition stuff 2`] = ` +.glamor-0 { + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; +} + +
+`; + exports[`css computed key is only dynamic 1`] = ` .glamor-0 { font-size: 10px; @@ -71,6 +107,17 @@ exports[`css computed key is only dynamic 1`] = ` /> `; +exports[`css css variables 1`] = ` +.glamor-0 { + --some-var: 1px; + width: var(--some-var); +} + +
+`; + exports[`css flushes correctly 1`] = ` .glamor-0 { display: -webkit-box; @@ -126,6 +173,50 @@ exports[`css handles objects 1`] = ` /> `; +exports[`css nested array 1`] = ` +.glamor-0 { + display: -webkit-box; + display: -ms-flexbox; + display: flex; +} + +
+`; + +exports[`css nested at rules 1`] = ` +@media (min-width: 420px) { + .glamor-0 { + color: pink; + } +} + +@media (min-width: 420px) and (max-width: 500px) { + .glamor-0 { + color: hotpink; + } +} + +@supports (display: grid) { + .glamor-0 { + display: grid; + } +} + +@supports (display: grid) and ((display: -webkit-box) or (display: flex)) { + .glamor-0 { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + } +} + +
+`; + exports[`css null rule 1`] = `
{ \`` const { code } = babel.transform(basic, { plugins: [[plugin]], - filename: __filename + filename: __filename, + babelrc: false }) expect(code).toMatchSnapshot() }) diff --git a/test/css.test.js b/test/css.test.js index f48d81e17..c6b7edf3b 100644 --- a/test/css.test.js +++ b/test/css.test.js @@ -90,12 +90,64 @@ describe('css', () => { const tree = renderer.create(
).toJSON() expect(tree).toMatchSnapshot() }) + test('@supports', () => { + const cls1 = css` + @supports (display: grid) { + display: grid; + } + ` + const tree = renderer.create(
).toJSON() + expect(tree).toMatchSnapshot() + }) + test('nested at rules', () => { + const cls1 = css` + @supports (display: grid) { + display: grid; + @supports (display: flex) { + display: flex; + } + } + @media (min-width: 420px) { + color: pink; + @media (max-width: 500px) { + color: hotpink; + } + } + ` + const tree = renderer.create(
).toJSON() + expect(tree).toMatchSnapshot() + }) + test('nested array', () => { + const cls1 = css([[{ + display: 'flex' + }]]) + const tree = renderer.create(
).toJSON() + expect(tree).toMatchSnapshot() + }) + test('composition stuff', () => { + const cls1 = css({ + justifyContent: 'center' + }) + const cls2 = css([cls1]) + const tree = renderer.create(
).toJSON() + expect(tree).toMatchSnapshot() + const tree2 = renderer.create(
).toJSON() + expect(tree2).toMatchSnapshot() + }) test('null rule', () => { const cls1 = css() const tree = renderer.create(
).toJSON() expect(tree).toMatchSnapshot() }) + test('css variables', () => { + const cls1 = css` + --some-var: 1px; + width: var(--some-var); + ` + const tree = renderer.create(
).toJSON() + expect(tree).toMatchSnapshot() + }) test('flushes correctly', () => { const cls1 = css` display: flex;