diff --git a/src/exports.js b/src/exports.js index 2dadd66..33d2180 100644 --- a/src/exports.js +++ b/src/exports.js @@ -1,5 +1,5 @@ /* @flow */ -import {mapObj, hashObject} from './util'; +import {mapObj, hashString} from './util'; import { injectAndGetClassName, reset, startBuffering, flushToString, @@ -20,10 +20,11 @@ export type MaybeSheetDefinition = SheetDefinition | false | null | void const StyleSheet = { create(sheetDefinition /* : SheetDefinition */) { return mapObj(sheetDefinition, ([key, val]) => { + const stringVal = JSON.stringify(val); return [key, { - // TODO(gil): Further minify the -O_o--combined hashes + _len: stringVal.length, _name: process.env.NODE_ENV === 'production' ? - `_${hashObject(val)}` : `${key}_${hashObject(val)}`, + hashString(stringVal) : `${key}_${hashString(stringVal)}`, _definition: val }]; }); diff --git a/src/inject.js b/src/inject.js index 9d17595..45e3168 100644 --- a/src/inject.js +++ b/src/inject.js @@ -3,7 +3,7 @@ import asap from 'asap'; import OrderedElements from './ordered-elements'; import {generateCSS} from './generate'; -import {hashObject} from './util'; +import {hashObject, hashString} from './util'; /* :: import type { SheetDefinition, SheetDefinitions } from './index.js'; @@ -243,6 +243,13 @@ const processStyleDefinitions = ( } }; +// Sum up the lengths of the stringified style definitions (which was saved as _len property) +// and use modulus to return a single byte hash value. +// We append this extra byte to the 32bit hash to decrease the chance of hash collisions. +const getStyleDefinitionsLengthHash = (styleDefinitions /* : any[] */) /* : string */ => ( + styleDefinitions.reduce((length, styleDefinition) => length + styleDefinition._len, 0) % 36 +).toString(36); + /** * Inject styles associated with the passed style definition objects, and return * an associated CSS class name. @@ -269,7 +276,16 @@ export const injectAndGetClassName = ( if (processedStyleDefinitions.classNameBits.length === 0) { return ""; } - const className = processedStyleDefinitions.classNameBits.join("-o_O-"); + + let className; + if (process.env.NODE_ENV === 'production') { + className = processedStyleDefinitions.classNameBits.length === 1 ? + `_${processedStyleDefinitions.classNameBits[0]}` : + `_${hashString(processedStyleDefinitions.classNameBits.join())}${ + getStyleDefinitionsLengthHash(styleDefinitions)}`; + } else { + className = processedStyleDefinitions.classNameBits.join("-o_O-"); + } injectStyleOnce( className, diff --git a/src/util.js b/src/util.js index b12d8e0..587580e 100644 --- a/src/util.js +++ b/src/util.js @@ -123,6 +123,9 @@ export const stringifyAndImportantifyValue = ( prop /* : any */ ) /* : string */ => importantify(stringifyValue(key, prop)); +// Turn a string into a hash string of base-36 values (using letters and numbers) +export const hashString = (string /* : string */) /* string */ => stringHash(string).toString(36); + // Hash a javascript object using JSON.stringify. This is very fast, about 3 // microseconds on my computer for a sample object: // http://jsperf.com/test-hashfnv32a-hash/5 @@ -131,8 +134,7 @@ export const stringifyAndImportantifyValue = ( // this to produce consistent hashes browsers need to have a consistent // ordering of objects. Ben Alpert says that Facebook depends on this, so we // can probably depend on this too. -export const hashObject = (object /* : ObjectMap */) /* : string */ => stringHash(JSON.stringify(object)).toString(36); - +export const hashObject = (object /* : ObjectMap */) /* : string */ => hashString(JSON.stringify(object)); // Given a single style value string like the "b" from "a: b;", adds !important // to generate "b !important". diff --git a/tests/index_test.js b/tests/index_test.js index 8856903..4069a0d 100644 --- a/tests/index_test.js +++ b/tests/index_test.js @@ -252,7 +252,7 @@ describe('StyleSheet.create', () => { }, }); - assert.equal(sheet.test._name, '_j5rvvh'); + assert.equal(sheet.test._name, 'j5rvvh'); }); }) }); diff --git a/tests/inject_test.js b/tests/inject_test.js index 5dde94c..90299ce 100644 --- a/tests/inject_test.js +++ b/tests/inject_test.js @@ -4,10 +4,12 @@ import jsdom from 'jsdom'; import { StyleSheet, css } from '../src/index.js'; import { + injectAndGetClassName, injectStyleOnce, reset, startBuffering, flushToString, flushToStyleTag, - addRenderedClassNames, getRenderedClassNames + addRenderedClassNames, getRenderedClassNames, } from '../src/inject.js'; +import { defaultSelectorHandlers } from '../src/generate'; const sheet = StyleSheet.create({ red: { @@ -34,6 +36,52 @@ describe('injection', () => { global.document = undefined; }); + describe('injectAndGetClassName', () => { + it('uses hashed class name', () => { + const className = injectAndGetClassName(false, [sheet.red], defaultSelectorHandlers); + assert.equal(className, 'red_137u7ef'); + }); + + it('combines class names', () => { + const className = injectAndGetClassName(false, [sheet.red, sheet.blue, sheet.green], defaultSelectorHandlers); + assert.equal(className, 'red_137u7ef-o_O-blue_1tsdo2i-o_O-green_1jzdmtb'); + }); + + describe('process.env.NODE_ENV === \'production\'', () => { + let prodSheet; + beforeEach(() => { + process.env.NODE_ENV = 'production'; + prodSheet = StyleSheet.create({ + red: { + color: 'red', + }, + + blue: { + color: 'blue', + }, + + green: { + color: 'green', + }, + }); + }); + + afterEach(() => { + delete process.env.NODE_ENV; + }); + + it('uses hashed class name (does not re-hash)', () => { + const className = injectAndGetClassName(false, [prodSheet.red], defaultSelectorHandlers); + assert.equal(className, `_${prodSheet.red._name}`); + }); + + it('creates minified combined class name', () => { + const className = injectAndGetClassName(false, [prodSheet.red, prodSheet.blue, prodSheet.green], defaultSelectorHandlers); + assert.equal(className, '_11v1eztc'); + }); + }); + }); + describe('injectStyleOnce', () => { it('causes styles to automatically be added', done => { injectStyleOnce("x", ".x", [{ color: "red" }], false);