Initial commit 🎉

fbshipit-source-id: b6fc29740c6875d2e78953b8a7123890a67930f2
Co-authored-by: Sebastian McKenzie <sebmck@fb.com>
Co-authored-by: John Knox <jknox@fb.com>
Co-authored-by: Emil Sjölander <emilsj@fb.com>
Co-authored-by: Pritesh Nandgaonkar <prit91@fb.com>
This commit is contained in:
Daniel Büchele
2018-04-13 08:38:06 -07:00
committed by Daniel Buchele
commit fbbf8cf16b
659 changed files with 87130 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import type {Tracker} from '../index.js';
import {GarbageCollector} from '../gc.js';
import {StyleSheet} from '../sheet.js';
function createGC(): {|
gc: GarbageCollector,
tracker: Tracker,
|} {
const sheet = new StyleSheet();
const tracker = new Map();
const rulesToClass = new WeakMap();
const gc = new GarbageCollector(sheet, tracker, rulesToClass);
return {gc, tracker};
}
test('register classes to be garbage collected when no references exist', () => {
const {gc} = createGC();
gc.registerClassUse('foo');
expect(gc.getCollectionQueue()).toEqual([]);
gc.deregisterClassUse('foo');
expect(gc.getCollectionQueue()).toEqual(['foo']);
});
test('cancel garbage collection for classes used before actual collection happens', () => {
const {gc} = createGC();
gc.registerClassUse('foo');
expect(gc.getCollectionQueue()).toEqual([]);
gc.deregisterClassUse('foo');
expect(gc.getCollectionQueue()).toEqual(['foo']);
gc.registerClassUse('foo');
expect(gc.getCollectionQueue()).toEqual([]);
});
test('garbage collector removes unreferenced classes', () => {
const {gc, tracker} = createGC();
tracker.set('foo', {
displayName: 'foo',
namespace: '',
selector: '',
style: {},
rules: {},
});
gc.registerClassUse('foo');
expect(gc.getCollectionQueue()).toEqual([]);
gc.deregisterClassUse('foo');
expect(gc.getCollectionQueue()).toEqual(['foo']);
expect(gc.hasQueuedCollection()).toBe(true);
expect(tracker.has('foo')).toBe(true);
gc.collectGarbage();
expect(gc.hasQueuedCollection()).toBe(false);
expect(gc.getCollectionQueue()).toEqual([]);
expect(tracker.has('foo')).toBe(false);
});
test('properly tracks reference counts', () => {
const {gc} = createGC();
gc.registerClassUse('foo');
gc.registerClassUse('foo');
gc.registerClassUse('bar');
expect(gc.getReferenceCount('foo')).toBe(2);
expect(gc.getReferenceCount('bar')).toBe(1);
gc.deregisterClassUse('bar');
expect(gc.getReferenceCount('bar')).toBe(0);
gc.deregisterClassUse('foo');
expect(gc.getReferenceCount('foo')).toBe(1);
gc.deregisterClassUse('foo');
expect(gc.getReferenceCount('foo')).toBe(0);
});
test("gracefully handle deregistering classes we don't have a count for", () => {
const {gc} = createGC();
gc.deregisterClassUse('not-tracking');
});
test('only halt garbage collection if there is nothing left in the queue', () => {
const {gc} = createGC();
gc.registerClassUse('foo');
expect(gc.hasQueuedCollection()).toBe(false);
gc.deregisterClassUse('foo');
expect(gc.hasQueuedCollection()).toBe(true);
gc.registerClassUse('bar');
expect(gc.hasQueuedCollection()).toBe(true);
gc.deregisterClassUse('bar');
expect(gc.hasQueuedCollection()).toBe(true);
gc.registerClassUse('bar');
expect(gc.hasQueuedCollection()).toBe(true);
gc.registerClassUse('foo');
expect(gc.hasQueuedCollection()).toBe(false);
});
test('ensure garbage collection happens', () => {
const {gc} = createGC();
gc.registerClassUse('foo');
gc.deregisterClassUse('foo');
expect(gc.hasQueuedCollection()).toBe(true);
expect(gc.getCollectionQueue()).toEqual(['foo']);
jest.runAllTimers();
expect(gc.hasQueuedCollection()).toBe(false);
expect(gc.getCollectionQueue()).toEqual([]);
});
test('flush', () => {
const {gc} = createGC();
gc.registerClassUse('bar');
gc.deregisterClassUse('bar');
expect(gc.getCollectionQueue()).toEqual(['bar']);
expect(gc.getReferenceCount('bar')).toBe(0);
gc.registerClassUse('foo');
expect(gc.getReferenceCount('foo')).toBe(1);
gc.flush();
expect(gc.getCollectionQueue()).toEqual([]);
expect(gc.getReferenceCount('foo')).toBe(0);
});

View File

@@ -0,0 +1,14 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import hash from '../hash.js';
test('hash', () => {
expect(hash('f')).toBe('1xwd1rk');
expect(hash('foobar')).toBe('slolri');
expect(hash('foobar2')).toBe('34u6r4');
});

View File

@@ -0,0 +1,387 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import styled, {buildKeyframes, flush, gc, tracker} from '../index.js';
const ReactTestRenderer = require('react-test-renderer');
const invariant = require('invariant');
const React = require('react'); // eslint-disable-line
const BasicComponent = styled.view({
color: 'red',
});
const DynamicComponent = styled.view({
color: props => props.color,
});
test('can create a basic component without any errors', () => {
let component;
try {
component = ReactTestRenderer.create(<BasicComponent />);
component.toJSON();
component.unmount();
gc.flush();
} finally {
if (component) {
component.unmount();
}
gc.flush();
}
});
test('can create a basic component and garbage collect', () => {
let component;
try {
component = ReactTestRenderer.create(<BasicComponent />);
const tree = component.toJSON();
expect(tree.type).toBe('div');
const className = tree.props.className;
expect(gc.hasQueuedCollection()).toBe(false);
expect(gc.getReferenceCount(className)).toBe(1);
component.unmount();
expect(gc.getReferenceCount(className)).toBe(0);
expect(gc.hasQueuedCollection()).toBe(true);
} finally {
if (component) {
component.unmount();
}
gc.flush();
}
});
test('remove outdated classes when updating component', () => {
let component;
try {
component = ReactTestRenderer.create(<DynamicComponent color="red" />);
const tree = component.toJSON();
const className = tree.props.className;
expect(gc.hasQueuedCollection()).toBe(false);
expect(gc.getReferenceCount(className)).toBe(1);
// updating with the same props should generate the same style and not trigger a collection
component.update(<DynamicComponent color="red" />);
expect(gc.hasQueuedCollection()).toBe(false);
expect(gc.getReferenceCount(className)).toBe(1);
// change style
component.update(<DynamicComponent color="blue" />);
expect(gc.hasQueuedCollection()).toBe(true);
expect(gc.getReferenceCount(className)).toBe(0);
} finally {
if (component) {
component.unmount();
}
gc.flush();
}
});
test('extra class names should be preserved', () => {
let component;
try {
component = ReactTestRenderer.create(<BasicComponent className="foo" />);
const tree = component.toJSON();
expect(tree.props.className.split(' ').includes('foo')).toBe(true);
} finally {
if (component) {
component.unmount();
}
gc.flush();
}
});
test('should inherit component when passed as first arg to styled', () => {
let component;
try {
const InheritComponent = BasicComponent.extends({
backgroundColor: 'black',
});
component = ReactTestRenderer.create(<InheritComponent />);
const tree = component.toJSON();
const rules = tracker.get(tree.props.className);
invariant(rules, 'expected rules');
expect(rules.style).toEqual({
'background-color': 'black',
color: 'red',
});
} finally {
if (component) {
component.unmount();
}
gc.flush();
}
});
test("when passed class name of another styled component it's rules should be inherited", () => {
let component;
try {
class BaseComponent extends styled.StylableComponent<{
className: string,
}> {
render() {
return <BasicComponent className={this.props.className} />;
}
}
const InheritComponent = BaseComponent.extends({
backgroundColor: 'black',
});
component = ReactTestRenderer.create(<InheritComponent />);
const tree = component.toJSON();
const rules = tracker.get(tree.props.className);
invariant(rules, 'expected rules');
expect(rules.style).toEqual({
'background-color': 'black',
color: 'red',
});
} finally {
if (component) {
component.unmount();
}
gc.flush();
}
});
test('supports pseudo selectors', () => {
let component;
try {
const Component = styled.view({
'&:hover': {
color: 'red',
},
});
component = ReactTestRenderer.create(<Component />);
const tree = component.toJSON();
const rules = tracker.get(tree.props.className);
invariant(rules, 'expected rules');
expect(rules.style).toEqual({
color: 'red',
});
} finally {
if (component) {
component.unmount();
}
gc.flush();
}
});
test('supports multiple pseudo selectors', () => {
let component;
try {
const Component = styled.view({
'&:active': {
color: 'blue',
},
'&:hover': {
color: 'red',
},
});
component = ReactTestRenderer.create(<Component />);
const tree = component.toJSON();
const classes = tree.props.className.split(' ');
expect(classes.length).toBe(2);
const hoverRules = tracker.get(classes[1]);
invariant(hoverRules, 'expected hoverRules');
expect(hoverRules.style).toEqual({
color: 'red',
});
expect(hoverRules.namespace).toBe('&:hover');
expect(hoverRules.selector.endsWith(':hover')).toBe(true);
const activeRules = tracker.get(classes[0]);
invariant(activeRules, 'expected activeRules');
expect(activeRules.style).toEqual({
color: 'blue',
});
expect(activeRules.namespace).toBe('&:active');
expect(activeRules.selector.endsWith(':active')).toBe(true);
} finally {
if (component) {
component.unmount();
}
gc.flush();
}
});
test('supports child selectors', () => {
let component;
try {
const Component = styled.view({
'> li': {
color: 'red',
},
});
component = ReactTestRenderer.create(<Component />);
const tree = component.toJSON();
const classes = tree.props.className.split(' ');
expect(classes.length).toBe(1);
const rules = tracker.get(classes[0]);
invariant(rules, 'expected rules');
expect(rules.style).toEqual({
color: 'red',
});
expect(rules.namespace).toBe('> li');
expect(rules.selector.endsWith(' > li')).toBe(true);
} finally {
if (component) {
component.unmount();
}
gc.flush();
}
});
test('flush', () => {
flush();
});
test('innerRef works on styled components', () => {
let component;
try {
const Component = styled.view({});
let called = false;
const innerRef = ref => {
called = true;
};
ReactTestRenderer.create(<Component innerRef={innerRef} />);
expect(called).toBe(true);
} finally {
if (component) {
component.unmount();
}
gc.flush();
}
});
test('ignoreAttributes', () => {
let component;
try {
const Component = styled.view(
{
color: props => props.color,
},
{
ignoreAttributes: ['color'],
},
);
component = ReactTestRenderer.create(<Component color="red" />);
const tree = component.toJSON();
expect(tree.props.color).toBe(undefined);
const rules = tracker.get(tree.props.className);
invariant(rules, 'expected rules');
expect(rules.style).toEqual({
color: 'red',
});
} finally {
if (component) {
component.unmount();
}
gc.flush();
}
});
test('buildKeyframes', () => {
const css = buildKeyframes({
'0%': {
opacity: 0,
},
'50%': {
height: 50,
opacity: 0.8,
},
'100%': {
opacity: 1,
},
});
expect(css).toBe(
[
' 0% {',
' opacity: 0;',
' }',
' 50% {',
' height: 50px;',
' opacity: 0.8;',
' }',
' 100% {',
' opacity: 1;',
' }',
].join('\n'),
);
});
test('keyframes', () => {
const className = styled.keyframes({
'0%': {
opacity: 0,
},
'50%': {
opacity: 0.8,
},
'100%': {
opacity: 1,
},
});
expect(typeof className).toBe('string');
});
test('buildKeyframes only accepts string property values', () => {
expect(() => {
buildKeyframes({
// $FlowFixMe: ignore
'0%': {
fn: () => {},
},
});
}).toThrow('Keyframe objects must only have strings values');
});
test('buildKeyframes only accepts object specs', () => {
expect(() => {
buildKeyframes({
// $FlowFixMe: ignore
'0%': () => {
return '';
},
});
}).toThrow('Keyframe spec must only have objects');
});

View File

@@ -0,0 +1,76 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import {buildRules, normaliseRules} from '../rules.js';
describe('normaliseRules', () => {
test('ensure top level values are expanded', () => {
const normalisedRules = normaliseRules({height: '4px'});
expect(normalisedRules['&'].height).toBe('4px');
});
test('ensure keys are dashed', () => {
const normalisedRules = normaliseRules({
// $FlowFixMe: ignore
'&:hover': {
lineHeight: '4px',
WebkitAppRegion: 'drag',
},
});
const hoverRules = normalisedRules['&:hover'];
expect(Object.keys(hoverRules).length).toBe(2);
expect(hoverRules['line-height']).toBe('4px');
expect(hoverRules['-webkit-app-region']).toBe('drag');
});
test('exclude empty objects', () => {
const normalisedRules = normaliseRules({
'&:hover': {},
});
expect(normalisedRules['&:hover']).toBe(undefined);
});
});
describe('buildRules', () => {
test('ensure null values are left out', () => {
const builtRules = buildRules({height: (null: any)}, {}, {});
expect('height' in builtRules).toBe(false);
const builtRules2 = buildRules(
{
height() {
return (null: any);
},
},
{},
{},
);
expect('height' in builtRules2).toBe(false);
});
test('ensure numbers are appended with px', () => {
expect(buildRules({height: 40}, {}, {}).height).toBe('40px');
});
test("ensure unitless numbers aren't appended with px", () => {
expect(buildRules({'z-index': 4}, {}, {})['z-index']).toBe('4');
});
test('ensure functions are called with props', () => {
const thisProps = {};
expect(
buildRules(
{
border: props => (props === thisProps ? 'foo' : 'bar'),
},
thisProps,
{},
).border,
).toBe('foo');
});
});

View File

@@ -0,0 +1,74 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import {StyleSheet} from '../sheet.js';
describe('flush', () => {
test('should remove all rules', () => {
const sheet = new StyleSheet();
expect(sheet.getRuleCount()).toBe(0);
sheet.insert('foo', 'div {color: red;}');
expect(sheet.getRuleCount()).toBe(1);
sheet.flush();
expect(sheet.getRuleCount()).toBe(0);
});
});
describe('inject', () => {
test("throw's an error when already injected", () => {
const sheet = new StyleSheet();
expect(() => {
sheet.inject();
sheet.inject();
}).toThrow('already injected stylesheet!');
});
});
describe('insert', () => {
test('non-speedy', () => {
const sheet = new StyleSheet();
expect(sheet.getRuleCount()).toBe(0);
sheet.insert('foo', 'div {color: red;}');
expect(sheet.getRuleCount()).toBe(1);
});
test('speedy', () => {
const sheet = new StyleSheet(true);
expect(sheet.getRuleCount()).toBe(0);
sheet.insert('foo', 'div {color: red;}');
expect(sheet.getRuleCount()).toBe(1);
});
});
describe('delete', () => {
test('non-speedy', () => {
const sheet = new StyleSheet();
expect(sheet.getRuleCount()).toBe(0);
sheet.insert('foo', 'div {color: red;}');
expect(sheet.getRuleCount()).toBe(1);
sheet.delete('foo');
expect(sheet.getRuleCount()).toBe(0);
});
test('speedy', () => {
const sheet = new StyleSheet(true);
expect(sheet.getRuleCount()).toBe(0);
sheet.insert('foo', 'div {color: red;}');
expect(sheet.getRuleCount()).toBe(1);
sheet.delete('foo');
expect(sheet.getRuleCount()).toBe(0);
});
});

114
src/ui/styled/gc.js Normal file
View File

@@ -0,0 +1,114 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import type {Tracker, RulesToClass} from './index.js';
import type {StyleSheet} from './sheet.js';
const invariant = require('invariant');
export class GarbageCollector {
constructor(sheet: StyleSheet, tracker: Tracker, rulesToClass: RulesToClass) {
this.sheet = sheet;
this.tracker = tracker;
// used to keep track of what classes are actively in use
this.usedClasses = new Map();
// classes to be removed, we put this in a queue and perform it in bulk rather than straight away
// since by the time the next tick happens this style could have been reinserted
this.classRemovalQueue = new Set();
this.rulesToClass = rulesToClass;
}
tracker: Tracker;
sheet: StyleSheet;
usedClasses: Map<string, number>;
garbageTimer: ?TimeoutID;
classRemovalQueue: Set<string>;
rulesToClass: RulesToClass;
hasQueuedCollection(): boolean {
return Boolean(this.garbageTimer);
}
getReferenceCount(key: string): number {
return this.usedClasses.get(key) || 0;
}
// component has been mounted so make sure it's being depended on
registerClassUse(name: string) {
const count = this.usedClasses.get(name) || 0;
this.usedClasses.set(name, count + 1);
if (this.classRemovalQueue.has(name)) {
this.classRemovalQueue.delete(name);
if (this.classRemovalQueue.size === 0) {
this.haltGarbage();
}
}
}
// component has been unmounted so remove it's dependencies
deregisterClassUse(name: string) {
let count = this.usedClasses.get(name);
if (count == null) {
return;
}
count--;
this.usedClasses.set(name, count);
if (count === 0) {
this.classRemovalQueue.add(name);
this.scheduleGarbage();
}
}
scheduleGarbage() {
if (this.garbageTimer != null) {
return;
}
this.garbageTimer = setTimeout(() => {
this.collectGarbage();
}, 2000);
}
haltGarbage() {
if (this.garbageTimer) {
clearTimeout(this.garbageTimer);
this.garbageTimer = null;
}
}
getCollectionQueue() {
return Array.from(this.classRemovalQueue);
}
collectGarbage() {
this.haltGarbage();
for (const name of this.classRemovalQueue) {
const trackerInfo = this.tracker.get(name);
invariant(trackerInfo != null, 'trying to remove unknown class');
const {rules} = trackerInfo;
this.rulesToClass.delete(rules);
this.sheet.delete(name);
this.tracker.delete(name);
this.usedClasses.delete(name);
}
this.classRemovalQueue.clear();
}
flush() {
this.haltGarbage();
this.classRemovalQueue.clear();
this.usedClasses.clear();
}
}

74
src/ui/styled/hash.js Normal file
View File

@@ -0,0 +1,74 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
export default function hash(str: string): string {
const m = 0x5bd1e995;
const r = 24;
let h = str.length;
let length = str.length;
let currentIndex = 0;
while (length >= 4) {
let k = UInt32(str, currentIndex);
k = Umul32(k, m);
k ^= k >>> r;
k = Umul32(k, m);
h = Umul32(h, m);
h ^= k;
currentIndex += 4;
length -= 4;
}
switch (length) {
case 3:
h ^= UInt16(str, currentIndex);
h ^= str.charCodeAt(currentIndex + 2) << 16;
h = Umul32(h, m);
break;
case 2:
h ^= UInt16(str, currentIndex);
h = Umul32(h, m);
break;
case 1:
h ^= str.charCodeAt(currentIndex);
h = Umul32(h, m);
break;
}
h ^= h >>> 13;
h = Umul32(h, m);
h ^= h >>> 15;
return (h >>> 0).toString(36);
}
function UInt32(str: string, pos: number): number {
return (
str.charCodeAt(pos++) +
(str.charCodeAt(pos++) << 8) +
(str.charCodeAt(pos++) << 16) +
(str.charCodeAt(pos) << 24)
);
}
function UInt16(str: string, pos: number): number {
return str.charCodeAt(pos++) + (str.charCodeAt(pos++) << 8);
}
function Umul32(n: number, m: number): number {
n |= 0;
m |= 0;
const nlo = n & 0xffff;
const nhi = n >>> 16;
const res = (nlo * m + (((nhi * m) & 0xffff) << 16)) | 0;
return res;
}

440
src/ui/styled/index.js Normal file
View File

@@ -0,0 +1,440 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import type {BaseRules, KeyframeRules, RawRules} from './rules.js';
import {buildKeyframeRules, buildRules, normaliseRules} from './rules.js';
import assignDeep from '../../utils/assignDeep.js';
import * as performance from '../../utils/performance.js';
import {GarbageCollector} from './gc.js';
import {StyleSheet} from './sheet.js';
import hash from './hash.js';
const React = require('react');
export type Tracker = Map<
string,
{
displayName: ?string,
namespace: string,
rules: BaseRules,
selector: string,
style: Object,
},
>;
export type RulesToClass = WeakMap<BaseRules, string>;
// map of inserted classes and metadata about them
export const tracker: Tracker = new Map();
// map of rules to their class
const rulesToClass: RulesToClass = new WeakMap();
export const sheet = new StyleSheet(process.env.NODE_ENV === 'production');
export const gc = new GarbageCollector(sheet, tracker, rulesToClass);
function addRules(
displayName: string,
rules: BaseRules,
namespace,
props: Object,
context: Object,
) {
// if these rules have been cached to a className then retrieve it
const cachedClass = rulesToClass.get(rules);
if (cachedClass != null) {
return cachedClass;
}
//
const declarations = [];
const style = buildRules(rules, props, context);
// generate css declarations based on the style object
for (const key in style) {
const val = style[key];
declarations.push(` ${key}: ${val};`);
}
const css = declarations.join('\n');
// build the class name with the display name of the styled component and a unique id based on the css and namespace
const className = displayName + '__' + hash(namespace + css);
// this is the first time we've found this className
if (!tracker.has(className)) {
// build up the correct selector, explode on commas to allow multiple selectors
const selector = namespace
.split(', ')
.map(part => {
if (part[0] === '&') {
return '.' + className + part.slice(1);
} else {
return '.' + className + ' ' + part;
}
})
.join(', ');
// insert the new style text
tracker.set(className, {displayName, namespace, rules, selector, style});
sheet.insert(className, `${selector} {\n${css}\n}`);
// if there's no dynamic rules then cache this
if (hasDynamicRules(rules) === false) {
rulesToClass.set(rules, className);
}
}
return className;
}
// remove all styhles
export function flush() {
gc.flush();
tracker.clear();
sheet.flush();
}
export type TagName = string | Function;
type StyledComponentState = {|
extraClassNames: Array<string>,
classNames: Array<string>,
lastBuiltRules: ?Object,
lastBuiltRulesIsDynamic: boolean,
|};
export class StylableComponent<
Props = void,
State = void,
> extends React.Component<Props, State> {
static extends(
rules: RawRules,
opts?: StyledComponentOpts,
): StyledComponent<any> {
return createStyledComponent(this, rules, opts);
}
}
class StylablePureComponent<
Props = void,
State = void,
> extends React.PureComponent<Props, State> {
static extends(
rules: RawRules,
opts?: StyledComponentOpts,
): StyledComponent<any> {
return createStyledComponent(this, rules, opts);
}
}
class StyledComponentBase<Props> extends React.PureComponent<
Props,
StyledComponentState,
> {
constructor(props: Props, context: Object): void {
super(props, context);
this.state = {
classNames: [],
extraClassNames: [],
lastBuiltRulesIsDynamic: false,
lastBuiltRules: null,
};
}
static defaultProps: ?$Shape<Props>;
static STYLED_CONFIG: {|
tagName: TagName,
ignoreAttributes: ?Array<string>,
builtRules: any,
|};
static extends(
rules: RawRules,
opts?: StyledComponentOpts,
): StyledComponent<any> {
return createStyledComponent(this, rules, opts);
}
componentWillMount(): void {
this.generateClassnames(this.props, null);
}
componentWillReceiveProps(nextProps: Props): void {
this.generateClassnames(nextProps, this.props);
}
componentWillUnmount(): void {
for (const name of this.state.classNames) {
gc.deregisterClassUse(name);
}
}
generateClassnames(props: Props, prevProps: ?Props): void {
throw new Error('unimplemented');
}
}
function hasDynamicRules(rules: Object): boolean {
for (const key in rules) {
const val = rules[key];
if (typeof val === 'function') {
return true;
} else if (typeof val === 'object' && hasDynamicRules(val)) {
return true;
}
}
return false;
}
function hasEquivProps(props: Object, nextProps: Object): boolean {
// check if the props are equivalent
for (const key in props) {
// ignore `children` since we do that check later
if (key === 'children') {
continue;
}
// check strict equality of prop value
if (nextProps[key] !== props[key]) {
return false;
}
}
// check if nextProps has any values that props doesn't
for (const key in nextProps) {
if (!(key in props)) {
return false;
}
}
// check if the boolean equality of children is equivalent
if (Boolean(props.children) !== Boolean(nextProps.children)) {
return false;
}
return true;
}
export type StyledComponent<Props> = Class<StyledComponentBase<Props>>;
type StyledComponentOpts = {
displayName?: string,
contextTypes?: Object,
ignoreAttributes?: Array<string>,
};
function createStyledComponent(
tagName: TagName,
rules: RawRules,
opts?: StyledComponentOpts = {},
): StyledComponent<any> {
let {contextTypes = {}, ignoreAttributes} = opts;
// build up rules
let builtRules = normaliseRules(rules);
// if inheriting from another styled component then take all of it's properties
if (typeof tagName === 'function' && tagName.STYLED_CONFIG) {
// inherit context types
if (tagName.contextTypes) {
contextTypes = {...contextTypes, ...tagName.contextTypes};
}
const parentConfig = tagName.STYLED_CONFIG;
// inherit tagname
tagName = parentConfig.tagName;
// inherit ignoreAttributes
if (parentConfig.ignoreAttributes) {
if (ignoreAttributes) {
ignoreAttributes = ignoreAttributes.concat(
parentConfig.ignoreAttributes,
);
} else {
ignoreAttributes = parentConfig.ignoreAttributes;
}
}
// inherit rules
builtRules = assignDeep({}, parentConfig.builtRules, builtRules);
}
const displayName: string =
opts.displayName == null ? 'StyledComponent' : opts.displayName;
const isDOM = typeof tagName === 'string';
class Constructor<Props: Object> extends StyledComponentBase<Props> {
generateClassnames(props: Props, prevProps: ?Props) {
// if this is a secondary render then check if the props are essentially equivalent
// NOTE: hasEquivProps is not a standard shallow equality test
if (prevProps != null && hasEquivProps(props, prevProps)) {
return;
}
const debugId = performance.mark();
const extraClassNames = [];
let myBuiltRules = builtRules;
// if passed any classes from another styled component, ignore that class and merge in their
// resolved styles
if (props.className) {
const propClassNames = props.className.trim().split(/[\s]+/g);
for (const className of propClassNames) {
const classInfo = tracker.get(className);
if (classInfo) {
const {namespace, style} = classInfo;
myBuiltRules = assignDeep({}, myBuiltRules, {[namespace]: style});
} else {
extraClassNames.push(className);
}
}
}
// if we had the exact same rules as last time and they weren't dynamic then we can bail out here
if (
myBuiltRules !== this.state.lastBuiltRules ||
this.state.lastBuiltRulesIsDynamic !== false
) {
const prevClasses = this.state.classNames;
const classNames = [];
// add rules
for (const namespace in myBuiltRules) {
const className = addRules(
displayName,
myBuiltRules[namespace],
namespace,
props,
this.context,
);
classNames.push(className);
// if this is the first mount render or we didn't previously have this class then add it as new
if (prevProps == null || !prevClasses.includes(className)) {
gc.registerClassUse(className);
}
}
// check what classNames have been removed if this is a secondary render
if (prevProps != null) {
for (const className of prevClasses) {
// if this previous class isn't in the current classes then deregister it
if (!classNames.includes(className)) {
gc.deregisterClassUse(className);
}
}
}
this.setState({
classNames,
lastBuiltRules: myBuiltRules,
lastBuiltRulesIsDynamic: hasDynamicRules(myBuiltRules),
extraClassNames,
});
}
performance.measure(
debugId,
`🚀 ${this.constructor.name} [style calculate]`,
);
}
render() {
const {children, innerRef, ...props} = this.props;
if (ignoreAttributes) {
for (const key of ignoreAttributes) {
delete props[key];
}
}
// build class names
const className = this.state.classNames
.concat(this.state.extraClassNames)
.join(' ');
if (props.is) {
props.class = className;
} else {
props.className = className;
}
//
if (innerRef) {
if (isDOM) {
// dom ref
props.ref = innerRef;
} else {
// probably another styled component so pass it down
props.innerRef = innerRef;
}
}
return React.createElement(tagName, props, children);
}
}
Constructor.STYLED_CONFIG = {
builtRules,
ignoreAttributes,
tagName,
};
Constructor.contextTypes = {
...contextTypes,
};
Object.defineProperty(Constructor, 'name', {
value: displayName,
});
return Constructor;
}
export function buildKeyframes(spec: KeyframeRules) {
let css = [];
const builtRules = buildKeyframeRules(spec);
for (const key in builtRules) {
const declarations = [];
const rules = builtRules[key];
for (const key in rules) {
declarations.push(` ${key}: ${String(rules[key])};`);
}
css.push(` ${key} {`);
css = css.concat(declarations);
css.push(' }');
}
css = css.join('\n');
return css;
}
function createKeyframes(spec: KeyframeRules): string {
const body = buildKeyframes(spec);
const className = `animation-${hash(body)}`;
const css = `@keyframes ${className} {\n${body}\n}`;
sheet.insert(className, css);
return className;
}
type StyledComponentFactory = (
rules: RawRules,
opts?: StyledComponentOpts,
) => StyledComponent<any>;
function createStyledComponentFactory(tagName: string): StyledComponentFactory {
return (rules: RawRules, opts?: StyledComponentOpts) => {
return createStyledComponent(tagName, rules, opts);
};
}
export default {
image: createStyledComponentFactory('img'),
view: createStyledComponentFactory('div'),
text: createStyledComponentFactory('span'),
textInput: createStyledComponentFactory('input'),
customHTMLTag: createStyledComponent,
keyframes: createKeyframes,
StylableComponent,
StylablePureComponent,
};

180
src/ui/styled/rules.js Normal file
View File

@@ -0,0 +1,180 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import type {CSSPropertySet, CSSPropertyValue} from './types.js';
const dashify = require('dashify');
export type NormalisedRules = {
[namespace: string]: BaseRules,
};
export type BaseRules = {
[key: string]: CSSPropertyValue<string | number>,
};
export type PlainRules = {
[key: string]: string,
};
export type NormalisedKeyframeRules = {
[key: string]: PlainRules,
};
export type KeyframeRules = {
[key: string]: CSSPropertySet,
};
export type RawRules = {
...CSSPropertySet,
[key: string]: CSSPropertySet,
};
const unitlessNumberProperties = new Set([
'animation-iteration-count',
'border-image-outset',
'border-image-slice',
'border-image-width',
'column-count',
'flex',
'flex-grow',
'flex-positive',
'flex-shrink',
'flex-order',
'grid-row',
'grid-column',
'font-weight',
'line-clamp',
'line-height',
'opacity',
'order',
'orphans',
'tab-size',
'widows',
'z-index',
'zoom',
'fill-opacity',
'flood-opacity',
'stop-opacity',
'stroke-dasharray',
'stroke-dashoffset',
'stroke-miterlimit',
'stroke-opacity',
'stroke-width',
]);
// put top level styles into an '&' object
function expandRules(rules: RawRules): NormalisedRules {
const expandedRules = {};
const rootRules = {};
for (const key in rules) {
const val = rules[key];
if (typeof val === 'object') {
expandedRules[key] = val;
} else {
rootRules[key] = val;
}
}
if (Object.keys(rootRules).length) {
expandedRules['&'] = rootRules;
}
return expandedRules;
}
function shouldAppendPixel(key: string, val: mixed): boolean {
return (
typeof val === 'number' && !unitlessNumberProperties.has(key) && !isNaN(val)
);
}
export function normaliseRules(rules: RawRules): NormalisedRules {
const expandedRules = expandRules(rules);
const builtRules = {};
for (const key in expandedRules) {
const rules = expandedRules[key];
const myRules = {};
for (const key in rules) {
const val = rules[key];
let dashedKey = dashify(key);
if (/[A-Z]/.test(key[0])) {
dashedKey = `-${dashedKey}`;
}
myRules[dashedKey] = val;
}
if (Object.keys(myRules).length) {
builtRules[key] = myRules;
}
}
return builtRules;
}
export function buildKeyframeRules(
rules: KeyframeRules,
): NormalisedKeyframeRules {
const spec = {};
for (const selector in rules) {
const newRules = {};
const rules2 = rules[selector];
if (!rules2 || typeof rules2 !== 'object') {
throw new Error('Keyframe spec must only have objects');
}
for (const key in rules2) {
let val = rules2[key];
if (shouldAppendPixel(key, val)) {
val += 'px';
} else if (typeof val === 'number') {
val = String(val);
}
if (typeof val !== 'string') {
throw new Error('Keyframe objects must only have strings values');
}
newRules[key] = val;
}
spec[selector] = newRules;
}
return spec;
}
export function buildRules(
rules: BaseRules,
props: NormalisedRules,
context: Object,
): PlainRules {
const style = {};
for (const key in rules) {
let val = rules[key];
if (typeof val === 'function') {
val = val(props, context);
}
if (val != null && shouldAppendPixel(key, val)) {
val += 'px';
}
if (val != null) {
style[key] = String(val);
}
}
return style;
}

92
src/ui/styled/sheet.js Normal file
View File

@@ -0,0 +1,92 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
const invariant = require('invariant');
function makeStyleTag(): HTMLStyleElement {
const tag = document.createElement('style');
tag.type = 'text/css';
tag.appendChild(document.createTextNode(''));
const {head} = document;
invariant(head, 'expected head');
head.appendChild(tag);
return tag;
}
export class StyleSheet {
constructor(isSpeedy?: boolean) {
this.injected = false;
this.isSpeedy = Boolean(isSpeedy);
this.flush();
this.inject();
}
ruleIndexes: Array<string>;
injected: boolean;
isSpeedy: boolean;
tag: HTMLStyleElement;
getRuleCount(): number {
return this.ruleIndexes.length;
}
flush() {
this.ruleIndexes = [];
if (this.tag) {
this.tag.innerHTML = '';
}
}
inject() {
if (this.injected) {
throw new Error('already injected stylesheet!');
}
this.tag = makeStyleTag();
this.injected = true;
}
delete(key: string) {
const index = this.ruleIndexes.indexOf(key);
if (index < 0) {
// TODO maybe error
return;
}
this.ruleIndexes.splice(index, 1);
const tag = this.tag;
if (this.isSpeedy) {
const sheet = tag.sheet;
invariant(sheet, 'expected sheet');
// $FlowFixMe: sheet is actually CSSStylesheet
sheet.deleteRule(index);
} else {
tag.removeChild(tag.childNodes[index + 1]);
}
}
insert(key: string, rule: string) {
const tag = this.tag;
if (this.isSpeedy) {
const sheet = tag.sheet;
invariant(sheet, 'expected sheet');
// $FlowFixMe: sheet is actually CSSStylesheet
sheet.insertRule(rule, sheet.cssRules.length);
} else {
tag.appendChild(document.createTextNode(rule));
}
this.ruleIndexes.push(key);
}
}

1280
src/ui/styled/types.js Normal file

File diff suppressed because it is too large Load Diff