",
11 | "license": "MIT",
12 | "dependencies": {
13 | "adrenaline": "file:../",
14 | "babel-register": "^6.7.2",
15 | "body-parser": "^1.13.3",
16 | "express": "^4.13.3",
17 | "express-graphql": "^0.5.0",
18 | "graphql": "^0.6.0",
19 | "history": "^1.17.0",
20 | "react": "^15.0.0",
21 | "react-dom": "^15.0.0",
22 | "react-router": "^1.0.0",
23 | "whatwg-fetch": "^0.9.0"
24 | },
25 | "devDependencies": {
26 | "babel-core": "^6.7.4",
27 | "babel-loader": "^6.2.4",
28 | "babel-preset-es2015": "^6.6.0",
29 | "babel-preset-react": "^6.5.0",
30 | "babel-preset-stage-0": "^6.5.0",
31 | "expect": "^1.13.4",
32 | "mocha": "^2.3.4",
33 | "webpack": "^1.12.14",
34 | "webpack-dev-server": "^1.11.0"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/example/src/client/components/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export default class App extends Component {
4 | render() {
5 | return (
6 |
7 |
Header
8 | {this.props.children}
9 |
10 | );
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/example/src/client/components/Loader.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export default class Loader extends Component {
4 | render() {
5 | return (
6 | Loading
7 | );
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/example/src/client/components/TodoApp.jsx:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | import React, { Component, PropTypes } from 'react';
4 | import { container } from 'adrenaline';
5 |
6 | import TodoInput from './TodoInput';
7 | import TodoList from './TodoList';
8 | import Loader from './Loader';
9 | import { createTodo } from '../mutations/todo';
10 |
11 | class TodoApp extends Component {
12 | static propTypes = {
13 | viewer: PropTypes.object,
14 | isFetching: PropTypes.bool.isRequired,
15 | mutate: PropTypes.func.isRequired,
16 | }
17 |
18 | createTodo = (args) => {
19 | this.props.mutate({
20 | mutation: createTodo,
21 | variables: { input: args },
22 | });
23 | }
24 |
25 | render() {
26 | const { viewer, isFetching } = this.props;
27 |
28 | if (isFetching) {
29 | return ;
30 | }
31 |
32 | return (
33 |
34 |
35 |
36 |
37 | );
38 | }
39 | }
40 |
41 | export default container({
42 | query: `
43 | query {
44 | viewer {
45 | id,
46 | ${TodoList.getFragment('todos')}
47 | }
48 | }
49 | `,
50 | })(TodoApp);
51 |
--------------------------------------------------------------------------------
/example/src/client/components/TodoInput.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | const ENTER_KEY_CODE = 13;
4 |
5 | export default class TodoInput extends Component {
6 | static propTypes = {
7 | createTodo: PropTypes.func.isRequired,
8 | }
9 |
10 | onEnter = (e) => {
11 | const { createTodo } = this.props;
12 | const input = this._input;
13 |
14 | if (!input.value.length) return;
15 |
16 | if (e.keyCode === ENTER_KEY_CODE) {
17 | createTodo({ text: input.value });
18 | input.value = '';
19 | }
20 | }
21 |
22 | render() {
23 | return (
24 | { this._input = input }}
26 | type="text"
27 | onKeyDown={this.onEnter}
28 | autoFocus="true"
29 | />
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/example/src/client/components/TodoItem.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { presenter } from 'adrenaline';
3 |
4 | class TodoItem extends Component {
5 | static propTypes = {
6 | todo: PropTypes.object.isRequired,
7 | }
8 |
9 | render() {
10 | const { todo } = this.props;
11 |
12 | return (
13 | {todo.text}
14 | );
15 | }
16 | }
17 |
18 | export default presenter({
19 | fragments: {
20 | todo: `
21 | fragment on Todo {
22 | text
23 | }
24 | `,
25 | },
26 | })(TodoItem);
27 |
--------------------------------------------------------------------------------
/example/src/client/components/TodoList.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { presenter } from 'adrenaline';
3 |
4 | import TodoItem from './TodoItem';
5 |
6 | class TodoList extends Component {
7 | static propTypes = {
8 | todos: PropTypes.array,
9 | }
10 |
11 | static defaultProps = {
12 | todos: [],
13 | }
14 |
15 | render() {
16 | const { todos } = this.props;
17 |
18 | return (
19 |
20 | {todos.map(todo =>
21 |
22 | )}
23 |
24 | );
25 | }
26 | }
27 |
28 | export default presenter({
29 | fragments: {
30 | todos: `
31 | fragment on User {
32 | todos {
33 | id,
34 | ${TodoItem.getFragment('todo')}
35 | }
36 | }
37 | `,
38 | },
39 | })(TodoList);
40 |
--------------------------------------------------------------------------------
/example/src/client/components/__tests__/TodoApp.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | import TestUtils from 'adrenaline/lib/test';
3 |
4 | import schema from '../../../server/schema';
5 | import TodoApp from '../TodoApp';
6 |
7 | expect.extend(TestUtils.expect);
8 |
9 | describe('Queries regression', () => {
10 | it('for TodoApp', () => {
11 | expect(TodoApp).toBeValidAgainst(schema);
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/example/src/client/index.js:
--------------------------------------------------------------------------------
1 | import 'whatwg-fetch';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import { Router } from 'react-router';
5 | import { Adrenaline } from 'adrenaline';
6 | import createBrowserHistory from 'history/lib/createBrowserHistory';
7 |
8 | import routes from './routes';
9 |
10 | const history = createBrowserHistory();
11 |
12 | const rootNode = document.getElementById('root');
13 | ReactDOM.render(
14 |
15 |
16 | ,
17 | rootNode
18 | );
19 |
--------------------------------------------------------------------------------
/example/src/client/mutations/todo.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | export const createTodo = `
4 | mutation AppMutation($input: TodoInput) {
5 | createTodo(input: $input) {
6 | id
7 | }
8 | }
9 | `;
10 |
--------------------------------------------------------------------------------
/example/src/client/routes.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route } from 'react-router';
3 | import App from './components/App';
4 | import TodoApp from './components/TodoApp';
5 |
6 | export default (
7 |
8 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/example/src/server/data.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | let idx = 4;
4 | let data = {
5 | users: [
6 | {
7 | id: 'u-1',
8 | name: 'User1',
9 | todos: ['t-1', 't-2', 't-3'],
10 | },
11 | ],
12 | todos: [
13 | {
14 | id: 't-1',
15 | text: 'hey',
16 | owner: 'u-1',
17 | createdAt: (new Date()).toString(),
18 | },
19 | {
20 | id: 't-2',
21 | text: 'ho',
22 | owner: 'u-1',
23 | createdAt: (new Date()).toString(),
24 | },
25 | {
26 | id: 't-3',
27 | text: 'lets go',
28 | owner: 'u-1',
29 | createdAt: (new Date()).toString(),
30 | },
31 | ],
32 | };
33 |
34 | export function findTodoById(id) {
35 | return data.todos.filter(t => t.id === id)[0];
36 | }
37 |
38 | export function findTodo({ count }) {
39 | return count ? data.todos.slice(0, count) : data.todos;
40 | }
41 |
42 | export function createTodo({ text }) {
43 | const todo = {
44 | id: 't-' + idx++,
45 | text: text,
46 | owner: 'u-1',
47 | createdAt: (new Date()).toString(),
48 | };
49 | data.todos.push(todo);
50 | return todo;
51 | }
52 |
53 | export function findUser() {
54 | return data.users[0];
55 | }
56 |
--------------------------------------------------------------------------------
/example/src/server/index.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | import { join } from 'path';
4 | import express from 'express';
5 | import graphqlHTTP from 'express-graphql';
6 |
7 | import schema from './schema';
8 | import * as connection from './data';
9 |
10 |
11 | const app = express();
12 |
13 | const publicPath = join(__dirname, '..', '..', '.tmp');
14 | app.use('/public', express.static(publicPath));
15 |
16 | app.use('/graphql', graphqlHTTP({
17 | schema,
18 | context: { connection },
19 | }));
20 |
21 | app.get('*', (req, res) => {
22 | res.send(`
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | `);
35 | });
36 |
37 | export default app;
38 |
--------------------------------------------------------------------------------
/example/src/server/schema.js:
--------------------------------------------------------------------------------
1 | import {
2 | GraphQLObjectType,
3 | GraphQLID,
4 | GraphQLString,
5 | GraphQLInt,
6 | GraphQLList,
7 | GraphQLNonNull,
8 | GraphQLEnumType,
9 | GraphQLInputObjectType,
10 | GraphQLSchema,
11 | } from 'graphql';
12 |
13 | const todoType = new GraphQLObjectType({
14 | name: 'Todo',
15 | description: 'Todo type',
16 | fields: () => ({
17 | id: {
18 | type: new GraphQLNonNull(GraphQLID),
19 | description: 'Todo id',
20 | },
21 | text: {
22 | type: new GraphQLNonNull(GraphQLString),
23 | description: 'Todo text',
24 | },
25 | owner: {
26 | type: userType,
27 | resolve: (todo, _, { connection }) => {
28 | return connection.findUser();
29 | },
30 | },
31 | createdAt: {
32 | type: new GraphQLNonNull(GraphQLString),
33 | description: 'Todo creation date',
34 | },
35 | }),
36 | });
37 |
38 | const userType = new GraphQLObjectType({
39 | name: 'User',
40 | fields: () => ({
41 | id: {
42 | type: new GraphQLNonNull(GraphQLID),
43 | },
44 | name: {
45 | type: GraphQLString,
46 | },
47 | todos: {
48 | type: new GraphQLList(todoType),
49 | args: {
50 | count: {
51 | name: 'count',
52 | type: GraphQLInt,
53 | },
54 | },
55 | resolve: (user, params, { connection }) => {
56 | return connection.findTodo(params);
57 | },
58 | },
59 | }),
60 | });
61 |
62 | const enumType = new GraphQLEnumType({
63 | name: 'Test',
64 | values: {
65 | ONE: {
66 | value: 1,
67 | },
68 | TWO: {
69 | value: 2,
70 | },
71 | },
72 | });
73 |
74 | const todoInput = new GraphQLInputObjectType({
75 | name: 'TodoInput',
76 | fields: () => ({
77 | text: { type: GraphQLString },
78 | }),
79 | });
80 |
81 | export default new GraphQLSchema({
82 | query: new GraphQLObjectType({
83 | name: 'Query',
84 | fields: () => ({
85 | viewer: {
86 | type: userType,
87 | resolve: (root, args, { connection }) => {
88 | return connection.findUser();
89 | },
90 | },
91 | test: {
92 | type: enumType,
93 | resolve: () => 1,
94 | },
95 | }),
96 | }),
97 | mutation: new GraphQLObjectType({
98 | name: 'Mutation',
99 | fields: () => ({
100 | createTodo: {
101 | type: todoType,
102 | args: {
103 | input: {
104 | type: todoInput,
105 | },
106 | },
107 | resolve: (root, { input }, { connection }) => {
108 | return connection.createTodo(input);
109 | },
110 | },
111 | }),
112 | }),
113 | });
114 |
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | module.exports = {
5 | devtool: 'source-map',
6 | entry: {
7 | bundle: path.join(__dirname, 'src', 'client')
8 | },
9 | output: {
10 | path: path.join(__dirname, '.tmp'),
11 | filename: "bundle.js"
12 | },
13 | resolve: {
14 | extensions: ['', '.js', '.jsx'],
15 | },
16 | module: {
17 | loaders: [
18 | {
19 | test: /\.jsx?$/,
20 | exclude: /node_modules/,
21 | loader: 'babel'
22 | }
23 | ]
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/images/resgression-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gyzerok/adrenaline/2e7692f563caa594612082a3491fb6fa1f7c2aa2/images/resgression-example.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "adrenaline",
3 | "version": "1.0.2",
4 | "description": "Relay-like framework with simplier API",
5 | "main": "./lib/index.js",
6 | "scripts": {
7 | "test": "mocha --compilers js:babel-register $(find ./src -name '*.spec.js')",
8 | "build": "node_modules/.bin/babel src --ignore __tests__ --out-dir lib/ --source-maps",
9 | "prepublish": "npm run build"
10 | },
11 | "author": "Fedor Nezhivoi ",
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/gyzerok/adrenaline.git"
15 | },
16 | "license": "MIT",
17 | "keywords": [
18 | "adrenaline",
19 | "react",
20 | "redux",
21 | "react-redux",
22 | "bindings",
23 | "relay",
24 | "graphql",
25 | "flux"
26 | ],
27 | "dependencies": {
28 | "invariant": "^2.2.1"
29 | },
30 | "peerDependencies": {
31 | "react": "^15.0.0",
32 | "graphql": "^0.6.0"
33 | },
34 | "devDependencies": {
35 | "babel-cli": "^6.6.5",
36 | "babel-eslint": "^6.0.0",
37 | "babel-preset-es2015": "^6.6.0",
38 | "babel-preset-react": "^6.5.0",
39 | "babel-preset-stage-0": "^6.5.0",
40 | "babel-register": "^6.7.2",
41 | "eslint": "^2.4.0",
42 | "eslint-plugin-react": "^4.2.3",
43 | "expect": "^1.14.0",
44 | "mocha": "^2.4.5",
45 | "webpack": "^1.12.2"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/Adrenaline.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | import defaultNetworkLayer from '../network/defaultNetworkLayer';
4 |
5 |
6 | export default class Adrenaline extends Component {
7 | static childContextTypes = {
8 | query: PropTypes.func,
9 | mutate: PropTypes.func,
10 | }
11 |
12 | static propTypes = {
13 | endpoint: PropTypes.string,
14 | networkLayer: PropTypes.object,
15 | children: PropTypes.element.isRequired,
16 | }
17 |
18 | static defaultProps = {
19 | endpoint: '/graphql',
20 | networkLayer: defaultNetworkLayer,
21 | }
22 |
23 | getChildContext() {
24 | const { endpoint, networkLayer } = this.props;
25 |
26 | return {
27 | query: (query, variables) => {
28 | return networkLayer.performQuery(endpoint, query, variables);
29 | },
30 | mutate: (...args) => {
31 | return networkLayer.performMutation(endpoint, ...args);
32 | },
33 | };
34 | }
35 |
36 | render() {
37 | const { children } = this.props;
38 | return children;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/container.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import invariant from 'invariant';
3 |
4 | import getDisplayName from '../utils/getDisplayName';
5 | import shallowEqual from '../utils/shallowEqual';
6 |
7 |
8 | export default function container(specs) {
9 | return DecoratedComponent => {
10 | const displayName = `AdrenalineContainer(${getDisplayName(DecoratedComponent)})`;
11 |
12 | invariant(
13 | specs !== null && specs !== undefined,
14 | `${displayName} requires configuration.`
15 | );
16 |
17 | invariant(
18 | typeof specs.query === 'string',
19 | `You have to define 'query' as a string in ${displayName}.`
20 | );
21 |
22 | invariant(
23 | !specs.variables || typeof specs.variables === 'function',
24 | `You have to define 'variables' as a function in ${displayName}.`
25 | );
26 |
27 | function mapPropsToVariables(props) {
28 | return !!specs.variables ? specs.variables(props) : {};
29 | }
30 |
31 | return class extends Component {
32 | static displayName = displayName
33 | static DecoratedComponent = DecoratedComponent
34 |
35 | static contextTypes = {
36 | query: PropTypes.func,
37 | mutate: PropTypes.func,
38 | }
39 |
40 | static getSpecs() {
41 | return specs;
42 | }
43 |
44 | constructor(props, context) {
45 | super(props, context);
46 |
47 | this.state = {
48 | data: null,
49 | isFetching: true,
50 | };
51 | }
52 |
53 | componentWillMount() {
54 | this.query();
55 | }
56 |
57 | componentWillReceiveProps(nextProps) {
58 | if (!shallowEqual(
59 | mapPropsToVariables(this.props),
60 | mapPropsToVariables(nextProps)
61 | )) {
62 | this.query(nextProps);
63 | }
64 | }
65 |
66 | query = (props = this.props) => {
67 | const { query } = specs;
68 | const variables = mapPropsToVariables(props);
69 |
70 | this.setState({ isFetching: true }, () => {
71 | this.context.query(query, variables)
72 | .catch(err => {
73 | console.error(err); // eslint-disable-line
74 | })
75 | .then(data => this.setState({ data, isFetching: false }));
76 | });
77 | }
78 |
79 | mutate = ({ mutation = '', variables = {}, files = null, invalidate = true }) => {
80 | return this.context.mutate(mutation, variables, files)
81 | .then(() => {
82 | if (invalidate) {
83 | this.query();
84 | }
85 | });
86 | }
87 |
88 | render() {
89 | const { data, isFetching } = this.state;
90 | const variables = mapPropsToVariables(this.props);
91 |
92 | const dataOrDefault = !data ? {} : data;
93 |
94 | return (
95 |
101 | );
102 | }
103 | };
104 | };
105 | }
106 |
--------------------------------------------------------------------------------
/src/components/presenter.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import invariant from 'invariant';
3 |
4 | import getDisplayName from '../utils/getDisplayName';
5 | import isPlainObject from '../utils/isPlainObject';
6 |
7 |
8 | export default function presenter(specs) {
9 | return DecoratedComponent => {
10 | const displayName = `AdrenalinePresenter(${getDisplayName(DecoratedComponent)})`;
11 |
12 | invariant(
13 | specs.hasOwnProperty('fragments'),
14 | '%s have not fragments declared',
15 | displayName
16 | );
17 |
18 | const { fragments } = specs;
19 |
20 | invariant(
21 | isPlainObject(fragments),
22 | 'Fragments have to be declared as object in %s',
23 | displayName
24 | );
25 |
26 | return class extends Component {
27 | static displayName = displayName;
28 | static DecoratedComponent = DecoratedComponent;
29 |
30 | static getFragment(key) {
31 | invariant(
32 | typeof key === 'string',
33 | 'You cant call getFragment(key: string) without string key in %s',
34 | displayName
35 | );
36 |
37 | invariant(
38 | fragments.hasOwnProperty(key),
39 | 'Component %s has no fragment %s',
40 | displayName,
41 | key
42 | );
43 |
44 | return fragments[key]
45 | .replace(/\s+/g, ' ')
46 | .replace('fragment', '...')
47 | .trim();
48 | }
49 |
50 | render() {
51 | return (
52 |
53 | );
54 | }
55 | };
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export Adrenaline from './components/Adrenaline';
2 | export container from './components/container';
3 | export presenter from './components/presenter';
4 |
--------------------------------------------------------------------------------
/src/network/defaultNetworkLayer.js:
--------------------------------------------------------------------------------
1 | export default {
2 | performQuery(endpoint, query, variables) {
3 | const opts = {
4 | method: 'post',
5 | headers: {
6 | 'Accept': 'application/json', //eslint-disable-line
7 | 'Content-Type': 'application/json',
8 | },
9 | credentials: 'same-origin',
10 | body: JSON.stringify({ query, variables }),
11 | };
12 |
13 | return fetch(endpoint, opts)
14 | .then(parseJSON);
15 | },
16 |
17 | performMutation(endpoint, mutation, variables, files) {
18 | if (!files) {
19 | return fetch(endpoint, {
20 | method: 'post',
21 | headers: {
22 | 'Accept': 'application/json', // eslint-disable-line
23 | 'Content-Type': 'application/json',
24 | },
25 | credentials: 'same-origin',
26 | body: JSON.stringify({
27 | query: mutation,
28 | variables,
29 | }),
30 | }).then(parseJSON);
31 | }
32 |
33 | const formData = new FormData();
34 | formData.append('query', mutation);
35 | formData.append('variables', JSON.stringify(variables));
36 | if (files) {
37 | for (const filename in files) {
38 | if (files.hasOwnProperty(filename)) {
39 | formData.append(filename, files[filename]);
40 | }
41 | }
42 | }
43 |
44 | return fetch(endpoint, {
45 | method: 'post',
46 | credentials: 'same-origin',
47 | body: formData,
48 | }).then(parseJSON);
49 | },
50 | };
51 |
52 | function parseJSON(res) {
53 | if (res.status !== 200) {
54 | throw new Error('Invalid request.');
55 | }
56 |
57 | return res.json().then(json => json.data);
58 | }
59 |
--------------------------------------------------------------------------------
/src/test-utils/expect.js:
--------------------------------------------------------------------------------
1 | import { parse, validate } from 'graphql';
2 |
3 | export default {
4 | toBeValidAgainst(schema) {
5 | const specs = this.actual.getSpecs();
6 | const errors = validate(schema, parse(specs.query));
7 |
8 | if (errors.length === 0) return this;
9 |
10 | throw errors[0];
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/src/test.js:
--------------------------------------------------------------------------------
1 | import expect from './test-utils/expect';
2 |
3 | export default { expect };
4 |
--------------------------------------------------------------------------------
/src/utils/__tests__/getDisplayName.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 |
3 | import getDisplayName from '../getDisplayName';
4 |
5 |
6 | describe('utils', () => {
7 | describe('getDisplayName', () => {
8 | it('should return String or Component for empty object', () => {
9 | const actual = [
10 | { displayName: 'hey' },
11 | { name: 'ho' },
12 | {},
13 | ].map(getDisplayName);
14 | const expected = ['hey', 'ho', 'Component'];
15 | expect(actual).toEqual(expected);
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/utils/__tests__/shallowEqual.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 |
3 | import shallowEqual from '../shallowEqual';
4 |
5 |
6 | describe('utils', () => {
7 | describe('shallowEqual', () => {
8 | it('should return true if arguments fields are equal', () => {
9 | expect(
10 | shallowEqual(
11 | { a: 1, b: 2, c: undefined },
12 | { a: 1, b: 2, c: undefined },
13 | )
14 | ).toBe(true);
15 |
16 | expect(
17 | shallowEqual(
18 | { a: 1, b: 2, c: 3 },
19 | { a: 1, b: 2, c: 3 },
20 | )
21 | ).toBe(true);
22 |
23 | const o = {};
24 | expect(
25 | shallowEqual(
26 | { a: 1, b: 2, c: o },
27 | { a: 1, b: 2, c: o },
28 | )
29 | ).toBe(true);
30 | });
31 |
32 | it('should return false if first argument has too many keys', () => {
33 | expect(
34 | shallowEqual(
35 | { a: 1, b: 2, c: 3 },
36 | { a: 1, b: 2 },
37 | )
38 | ).toBe(false);
39 | });
40 |
41 | it('should return false if second argument has too many keys', () => {
42 | expect(
43 | shallowEqual(
44 | { a: 1, b: 2 },
45 | { a: 1, b: 2, c: 3 },
46 | )
47 | ).toBe(false);
48 | });
49 |
50 | it('should return false if arguments have different keys', () => {
51 | expect(
52 | shallowEqual(
53 | { a: 1, b: 2, c: undefined },
54 | { a: 1, bb: 2, c: undefined },
55 | )
56 | ).toBe(false);
57 | });
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/src/utils/getDisplayName.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | export default function getDisplayName(Component) {
4 | return Component.displayName || Component.name || 'Component';
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/isPlainObject.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | export default function isPlainObject(obj) {
4 | return obj
5 | ? typeof obj === 'object'
6 | && Object.getPrototypeOf(obj) === Object.prototype
7 | : false;
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/shallowEqual.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | export default function shallowEqual(objA, objB) {
4 | if (objA === objB) {
5 | return true;
6 | }
7 |
8 | const keysA = Object.keys(objA || {});
9 | const keysB = Object.keys(objB || {});
10 |
11 | if (keysA.length !== keysB.length) {
12 | return false;
13 | }
14 |
15 | // Test for A's keys different from B.
16 | const hasOwn = Object.prototype.hasOwnProperty;
17 | for (let i = 0; i < keysA.length; i++) {
18 | if (!hasOwn.call(objB, keysA[i]) ||
19 | objA[keysA[i]] !== objB[keysA[i]]) {
20 | return false;
21 | }
22 | }
23 |
24 | return true;
25 | }
26 |
--------------------------------------------------------------------------------