Skip to content

Commit

Permalink
Make the suffix of class names deterministic based on their values.
Browse files Browse the repository at this point in the history
Summary:
Use a hash of the styles for each class name to determine the suffix
added to the name. This means that if multiple styles are created in different
places with the same values, they will use the same class. This is mostly
useful for ensuring that the suffixes are generated the same on both servers
and clients.

I'm still not 100% sure that `JSON.stringify` necessarily produces the same
results on all clients. It looks like ES2015 compliant browser engines have
defined property order that we can count on, but I'm not sure if there are any
of those yet? But Ben Alpert has made a convincing case that Facebook would
break if browsers didn't have consistent ordering so....

Test Plan: - `npm run test`

Reviewers: jlfwong, csilvers

Reviewed By: jlfwong, csilvers

Subscribers: alpert, csilvers, john

Differential Revision: https://phabricator.khanacademy.org/D24493
  • Loading branch information
xymostech committed Jan 12, 2016
1 parent 00f431d commit f45f87d
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 33 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,17 @@ Aphrodite will ensure that the global `@font-face` rule for this font is only in
- [js-next/react-style](https://github.com/js-next/react-style)
- [dowjones/react-inline-style](https://github.com/dowjones/react-inline-style)
- [martinandert/react-inline](https://github.com/martinandert/react-inline)

# License (MIT)

Copyright (c) 2016 Khan Academy

Includes works from https://github.com/garycourt/murmurhash-js, which is MIT licensed with the following copyright:

Copyright (c) 2011 Gary Court

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
106 changes: 91 additions & 15 deletions dist/aphrodite.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,10 @@ module.exports =
var key = _ref2[0];
var val = _ref2[1];

// TODO(jlfwong): Figure out a way (probably an AST transform) to
// make the ID stable here to enable server -> client rehydration.
// Probably just use a large random number (but one that's
// determined at build time instead of runtime).
return [key, {
_name: key + '_' + (0, _util.nextID)(),
// TODO(emily): Make a 'production' mode which doesn't prepend
// the class name here, to make the generated CSS smaller.
_name: key + '_' + (0, _util.hashObject)(val),
_definition: val
}];
});
Expand Down Expand Up @@ -294,16 +292,6 @@ module.exports =
};

exports.kebabifyStyleName = kebabifyStyleName;
// Return a monotonically increasing counter
var nextID = (function () {
var x = 0;
return function () {
x += 1;
return x;
};
})();

exports.nextID = nextID;
var recursiveMerge = function recursiveMerge(a, b) {
// TODO(jlfwong): Handle malformed input where a and b are not the same
// type.
Expand Down Expand Up @@ -363,6 +351,34 @@ module.exports =
strokeWidth: true
};

/**
* Taken from React's CSSProperty.js
*
* @param {string} prefix vendor-specific prefix, eg: Webkit
* @param {string} key style name, eg: transitionDuration
* @return {string} style name prefixed with `prefix`, properly camelCased, eg:
* WebkitTransitionDuration
*/
function prefixKey(prefix, key) {
return prefix + key.charAt(0).toUpperCase() + key.substring(1);
}

/**
* Support style names that may come passed in prefixed by adding permutations
* of vendor prefixes.
* Taken from React's CSSProperty.js
*/
var 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.
// Taken from React's CSSProperty.js
Object.keys(isUnitlessNumber).forEach(function (prop) {
prefixes.forEach(function (prefix) {
isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop];
});
});

var stringifyValue = function stringifyValue(key, prop, stringHandlers) {
// If a handler exists for this particular key, let it interpret
// that value first before continuing
Expand All @@ -380,7 +396,67 @@ module.exports =
return prop;
}
};

exports.stringifyValue = stringifyValue;
/**
* JS Implementation of MurmurHash2
*
* @author <a href="mailto:gary.court@gmail.com">Gary Court</a>
* @see http://github.com/garycourt/murmurhash-js
* @author <a href="mailto:aappleby@gmail.com">Austin Appleby</a>
* @see http://sites.google.com/site/murmurhash/
*
* @param {string} str ASCII only
* @return {string} Base 36 encoded hash result
*/
function murmurhash2_32_gc(str) {
var l = str.length;
var h = l;
var i = 0;
var k = undefined;

while (l >= 4) {
k = str.charCodeAt(i) & 0xff | (str.charCodeAt(++i) & 0xff) << 8 | (str.charCodeAt(++i) & 0xff) << 16 | (str.charCodeAt(++i) & 0xff) << 24;

k = (k & 0xffff) * 0x5bd1e995 + (((k >>> 16) * 0x5bd1e995 & 0xffff) << 16);
k ^= k >>> 24;
k = (k & 0xffff) * 0x5bd1e995 + (((k >>> 16) * 0x5bd1e995 & 0xffff) << 16);

h = (h & 0xffff) * 0x5bd1e995 + (((h >>> 16) * 0x5bd1e995 & 0xffff) << 16) ^ k;

l -= 4;
++i;
}

switch (l) {
case 3:
h ^= (str.charCodeAt(i + 2) & 0xff) << 16;
case 2:
h ^= (str.charCodeAt(i + 1) & 0xff) << 8;
case 1:
h ^= str.charCodeAt(i) & 0xff;
h = (h & 0xffff) * 0x5bd1e995 + (((h >>> 16) * 0x5bd1e995 & 0xffff) << 16);
}

h ^= h >>> 13;
h = (h & 0xffff) * 0x5bd1e995 + (((h >>> 16) * 0x5bd1e995 & 0xffff) << 16);
h ^= h >>> 15;

return (h >>> 0).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
//
// Note that this uses JSON.stringify to stringify the objects so in order for
// 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.
var hashObject = function hashObject(object) {
return murmurhash2_32_gc(JSON.stringify(object));
};
exports.hashObject = hashObject;

/***/ }
/******/ ]);
10 changes: 4 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {generateCSS} from './generate';
import {mapObj, nextID} from './util';
import {mapObj, hashObject} from './util';

const injectStyles = (cssContents) => {
// Taken from
Expand Down Expand Up @@ -56,12 +56,10 @@ const injectStyleOnce = (key, selector, definitions, useImportant) => {
const StyleSheet = {
create(sheetDefinition) {
return mapObj(sheetDefinition, ([key, val]) => {
// TODO(jlfwong): Figure out a way (probably an AST transform) to
// make the ID stable here to enable server -> client rehydration.
// Probably just use a large random number (but one that's
// determined at build time instead of runtime).
return [key, {
_name: `${key}_${nextID()}`,
// TODO(emily): Make a 'production' mode which doesn't prepend
// the class name here, to make the generated CSS smaller.
_name: `${key}_${hashObject(val)}`,
_definition: val
}];
});
Expand Down
66 changes: 57 additions & 9 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,6 @@ const MS_RE = /^ms-/;
const kebabify = (string) => string.replace(UPPERCASE_RE, '-$1').toLowerCase();
export const kebabifyStyleName = (string) => kebabify(string).replace(MS_RE, '-ms-');

// Return a monotonically increasing counter
export const nextID = (function() {
let x = 0;
return () => {
x += 1;
return x;
};
})();

export const recursiveMerge = (a, b) => {
// TODO(jlfwong): Handle malformed input where a and b are not the same
// type.
Expand Down Expand Up @@ -130,3 +121,60 @@ export const stringifyValue = (key, prop, stringHandlers) => {
return prop;
}
};

/**
* JS Implementation of MurmurHash2
*
* @author <a href="mailto:gary.court@gmail.com">Gary Court</a>
* @see http://github.com/garycourt/murmurhash-js
* @author <a href="mailto:aappleby@gmail.com">Austin Appleby</a>
* @see http://sites.google.com/site/murmurhash/
*
* @param {string} str ASCII only
* @return {string} Base 36 encoded hash result
*/
function murmurhash2_32_gc(str) {
let l = str.length;
let h = l;
let i = 0;
let k;

while (l >= 4) {
k = ((str.charCodeAt(i) & 0xff)) |
((str.charCodeAt(++i) & 0xff) << 8) |
((str.charCodeAt(++i) & 0xff) << 16) |
((str.charCodeAt(++i) & 0xff) << 24);

k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
k ^= k >>> 24;
k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));

h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k;

l -= 4;
++i;
}

switch (l) {
case 3: h ^= (str.charCodeAt(i + 2) & 0xff) << 16;
case 2: h ^= (str.charCodeAt(i + 1) & 0xff) << 8;
case 1: h ^= (str.charCodeAt(i) & 0xff);
h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
}

h ^= h >>> 13;
h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
h ^= h >>> 15;

return (h >>> 0).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
//
// Note that this uses JSON.stringify to stringify the objects so in order for
// 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) => murmurhash2_32_gc(JSON.stringify(object));
50 changes: 47 additions & 3 deletions tests/index_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,63 @@ describe('create', () => {
it('assign different names to two different create calls', () => {
const sheet1 = StyleSheet.create({
red: {
color: 'red',
}
color: 'blue',
},
});

const sheet2 = StyleSheet.create({
red: {
color: 'red',
}
},
});

assert.notEqual(sheet1.red._name, sheet2.red._name);
});

it('assigns the same name to identical styles from different create calls', () => {
const sheet1 = StyleSheet.create({
red: {
color: 'red',
height: 20,

':hover': {
color: 'blue',
width: 40,
},
},
});

const sheet2 = StyleSheet.create({
red: {
color: 'red',
height: 20,

':hover': {
color: 'blue',
width: 40,
},
},
});

assert.equal(sheet1.red._name, sheet2.red._name);
});

it('hashes style names correctly', () => {
const sheet = StyleSheet.create({
test: {
color: 'red',
height: 20,

':hover': {
color: 'blue',
width: 40,
},
},
});

assert.equal(sheet.test._name, 'test_y60qhp');
});

it('works for empty stylesheets and styles', () => {
const emptySheet = StyleSheet.create({});

Expand Down

0 comments on commit f45f87d

Please sign in to comment.