diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..734389120b --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules + +# don't commit compiled files +lib +test-lib +typings diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000000..33a9488b16 --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +example diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..be1bae1517 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: node_js +node_js: + - "5" + - "4" +install: + - npm install -g coveralls + - npm install + +script: + - npm test + - npm run coverage + - coveralls < ./coverage/lcov.info || true # ignore coveralls error + +# Allow Travis tests to run in containers. +sudo: false diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..a16ae77315 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Test", + "type": "node", + "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", + "stopOnEntry": false, + "args": ["lib/test/tests.js"], + "cwd": "${workspaceRoot}", + "runtimeExecutable": null + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..68994d6ebf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,16 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "editor.tabSize": 2, + "editor.rulers": [100], + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "editor.wrappingColumn": 100, + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + // "node_modules": true, + "test-lib": true, + "lib": true, + "coverage": true + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..0808d68832 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Ben Newman + +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. + diff --git a/README.md b/README.md index f044c28173..ef7dab95be 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,10 @@ Use your GraphQL server data in your React components, with the Apollo Client. -## API sketch - -I'd like to base this very heavily on the [`react-redux` API](https://github.com/reactjs/react-redux/blob/master/docs/api.md#api) and have the same parts - a `Provider` component and a `connect` function. Ideally, if you are using Apollo and Redux together, you don't have to nest calls to providers and containers - they should just work together. So, here we go! +- [Provider](#provider) +- [connect](#connect) +- [Additional Props](#additional-props) +- [Using in concert with Redux](#using-in-concert-with-redux) ### Provider @@ -54,7 +55,7 @@ ReactDOM.render( ) ``` -Note that this design calls it just `Provider`, which is appropriate because in the base case you can use it instead of the Redux provider. +The wrapper is called `Provider` because in the base case you can use it instead of the Redux provider or you can use it as an Apollo enhanced Redux Provider. ### connect @@ -72,9 +73,9 @@ import { connect } from 'apollo-react'; import Category from '../components/Category'; -function mapQueriesToProps({ watchQuery, ownProps, state }) { +function mapQueriesToProps({ ownProps, state }) { return { - category: watchQuery({ + category: { query: ` query getCategory($categoryId: Int!) { category(id: $categoryId) { @@ -88,44 +89,42 @@ function mapQueriesToProps({ watchQuery, ownProps, state }) { }, forceFetch: false, returnPartialData: true, - }) - } -} + }, + }; +}; -function mapMutationsToProps({ mutate, ownProps, state }) { +function mapMutationsToProps({ ownProps, state }) { return { - onPostReply(raw) { - return mutate({ - mutation: ` - mutation postReply( - $topic_id: ID! - $category_id: ID! - $raw: String! + postReply: (raw) => ({ + mutation: ` + mutation postReply( + $topic_id: ID! + $category_id: ID! + $raw: String! + ) { + createPost( + topic_id: $topic_id + category: $category_id + raw: $raw ) { - createPost( - topic_id: $topic_id - category: $category_id - raw: $raw - ) { - id - cooked - } + id + cooked } - `, - variables: { - // Use the container component's props - topic_id: ownProps.topic_id, + } + `, + variables: { + // Use the container component's props + topic_id: ownProps.topic_id, - // Use the redux state - category_id: state.selectedCategory, + // Use the redux state + category_id: state.selectedCategory, - // Use an argument passed from the callback - raw, - } - }); - } - } -} + // Use an argument passed from the triggering of the mutation + raw, + } + }), + }; +}; const CategoryWithData = connect({ mapQueriesToProps, @@ -135,7 +134,7 @@ const CategoryWithData = connect({ export default CategoryWithData; ``` -Note that `watchQuery` takes the same arguments as [`ApolloClient#watchQuery`](http://docs.apollostack.com/apollo-client/index.html#watchQuery). In this case, the `Category` component will get a prop called `category`, which has the following keys: +Each key on the object returned by mapQueriesToProps should be made up of the same possible arguments as [`ApolloClient#watchQuery`](http://docs.apollostack.com/apollo-client/index.html#watchQuery). In this case, the `Category` component will get a prop called `category`, which has the following keys: ```js { @@ -145,7 +144,23 @@ Note that `watchQuery` takes the same arguments as [`ApolloClient#watchQuery`](h } ``` -Using in concert with Redux: +`mapMutationsToProps` returns an object made up of keys and values that are custom functions to call the mutation. These can be used in children components (for instance, on a event handler) to trigger the mutation. The resulting function must return the same possible arguents as [`ApolloClient#mutate`](http://docs.apollostack.com/apollo-client/index.html#mutate). In this case, the `Category` component will get a prop called `postReply`, which has the following keys: + +```js +{ + loading: boolean, + error: Error, + result: GraphQLResult, +} +``` + +The `Category` component will also get a prop of `mutations` that will have a key of `postReply`. This key is the method that triggers the mutation and can take custom arguments (e.g. `this.props.mutations.postReply('Apollo and React are really great!')`). These arguments are passed to the method that creates the mutation. + +### Additional Props + +Redux's connect will pass `dispatch` as a prop unless action creators are passed using `mapDisptachToProps`. Likewise, the Apollo connect exposes part of the apollo-client api to props under the keys `query` and `mutate`. These correspond to the Apollo methods and can be used for custom needs outside of the ability of the wrapper component. + +### Using in concert with Redux ```js // ... same as above @@ -165,4 +180,4 @@ const CategoryWithData = connect({ export default CategoryWithData; ``` -In this case, `CategoryWithData` gets two props: `category` and `selectedCategory`. +In this case, `CategoryWithData` gets two props: `category` and `selectedCategory`. \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000000..2c4eaf0af6 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,26 @@ +# Test against this version of Node.js +environment: + matrix: + # node.js + - nodejs_version: "5" + - nodejs_version: "4" + +# Install scripts. (runs after repo cloning) +install: + # Get the latest stable version of Node.js or io.js + - ps: Install-Product node $env:nodejs_version + # install modules + - npm install + +# Post-install test scripts. +test_script: + # run tests + - npm test + +# artifacts: +# - path: ./junit/xunit.xml +# - path: ./xunit.xml + +# nothing to compile in this project +build: off +deploy: off diff --git a/package.json b/package.json index 7c641ac759..e43e5b2ef4 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,67 @@ { - "name": "apollo-react", + "name": "react-apollo", "version": "0.0.1", "description": "React data container for Apollo Client", - "main": "index.js", + "main": "./lib/src/index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "pretest": "npm run compile", + "test": "mocha --require ./test/fixtures/setup.js --reporter spec --full-trace --recursive ./lib/test", + "posttest": "npm run lint", + "compile": "tsc", + "watch": "tsc -w", + "prepublish": "npm run compile", + "lint": "tslint src/*.ts* && tslint test/*.ts*", + "coverage": "istanbul cover ./node_modules/mocha/bin/_mocha -- --reporter dot --full-trace lib/test/tests.js", + "postcoverage": "remap-istanbul --input coverage/coverage.json --type lcovonly --output coverage/lcov.info" }, - "author": "", - "license": "ISC" + "repository": { + "type": "git", + "url": "apollostack/react-apollo" + }, + "keywords": [ + "ecmascript", + "es2015", + "jsnext", + "javascript", + "relay", + "npm", + "react" + ], + "author": "James Baxley ", + "license": "MIT", + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0-0", + "redux": "^2.0.0 || ^3.0.0", + "apollo-client": "0.0.5" + }, + "devDependencies": { + "apollo-client": "0.0.5", + "chai": "^3.5.0", + "chai-as-promised": "^5.2.0", + "chai-enzyme": "^0.4.2", + "cheerio": "^0.20.0", + "enzyme": "^2.2.0", + "graphql": "^0.5.0", + "istanbul": "^0.4.2", + "jsdom": "^8.3.1", + "mocha": "^2.3.3", + "react": "^15.0.1", + "react-addons-test-utils": "^15.0.1", + "react-dom": "^0.14.8", + "redux": "^3.4.0", + "remap-istanbul": "^0.5.1", + "source-map-support": "^0.4.0", + "swapi-graphql": "0.0.4", + "tslint": "^3.6.0", + "typescript": "^1.8.9", + "typescript-require": "^0.2.9-1", + "typings": "^0.7.9" + }, + "dependencies": { + "hoist-non-react-statics": "^1.0.5", + "lodash.isequal": "^4.1.1", + "lodash.isobject": "^3.0.2", + "object-assign": "^4.0.1", + "react-redux": "^4.4.4" + } } diff --git a/src/Provider.tsx b/src/Provider.tsx new file mode 100644 index 0000000000..6bea8b2382 --- /dev/null +++ b/src/Provider.tsx @@ -0,0 +1,65 @@ +/// + +import { + Component, + PropTypes, + Children, +} from 'react'; + +import { + Store, +} from 'redux'; + +import ApolloClient from 'apollo-client'; + +export declare interface ProviderProps { + store?: Store; + client: ApolloClient; +} + +export default class Provider extends Component { + static propTypes = { + store: PropTypes.shape({ + subscribe: PropTypes.func.isRequired, + dispatch: PropTypes.func.isRequired, + getState: PropTypes.func.isRequired, + }), + client: PropTypes.object.isRequired, + children: PropTypes.element.isRequired, + }; + + static childContextTypes = { + store: PropTypes.object.isRequired, + client: PropTypes.object.isRequired, + }; + + public store: Store; + public client: ApolloClient; + + constructor(props, context) { + super(props, context); + this.client = props.client; + + if (props.store) { + this.store = props.store; + return; + } + + // intialize the built in store if none is passed in + props.client.initStore(); + this.store = props.client.store; + + } + + getChildContext() { + return { + store: this.store, + client: this.client, + }; + } + + render() { + const { children } = this.props; + return Children.only(children); + } +}; diff --git a/src/connect.tsx b/src/connect.tsx new file mode 100644 index 0000000000..3f202cbbcf --- /dev/null +++ b/src/connect.tsx @@ -0,0 +1,386 @@ +/// + +import { + Component, + createElement, + PropTypes, +} from 'react'; + +// XXX setup type definitions for individual lodash libs +// import isObject from 'lodash.isobject'; +// import isEqual from 'lodash.isequal'; +import { + isEqual, + isObject, +} from 'lodash'; + +// modules don't export ES6 modules +import invariant = require('invariant'); +import assign = require('object-assign'); + +import { + IMapStateToProps, + IMapDispatchToProps, + IConnectOptions, + connect as ReactReduxConnect, +} from 'react-redux'; + +import { + Store, +} from 'redux'; + +import ApolloClient, { readQueryFromStore } from 'apollo-client'; + +import { + GraphQLResult, +} from 'graphql'; + +export declare interface MapQueriesToPropsOptions { + ownProps: any; + state: any; +}; + +export declare interface MapMutationsToPropsOptions { + ownProps: any; + state: any; +}; + +export declare interface ConnectOptions { + mapStateToProps?: IMapStateToProps; + mapDispatchToProps?: IMapDispatchToProps; + options?: IConnectOptions; + mergeProps?(stateProps: any, dispatchProps: any, ownProps: any): any; + mapQueriesToProps?(opts: MapQueriesToPropsOptions): any; // WatchQueryHandle + mapMutationsToProps?(opts: MapMutationsToPropsOptions): any; // Mutation Handle +}; + +const defaultMapQueriesToProps = opts => ({ }); +const defaultMapMutationsToProps = opts => ({ }); +const defaultQueryData = { + loading: true, + error: null, + result: null, +}; +const defaultMutationData = assign({}, defaultQueryData); + +function getDisplayName(WrappedComponent) { + return WrappedComponent.displayName || WrappedComponent.name || 'Component'; +} + +// Helps track hot reloading. +let nextVersion = 0; + +export default function connect(opts?: ConnectOptions) { + + if (!opts) { + opts = {}; + } + + let { mapQueriesToProps, mapMutationsToProps } = opts; + + // clean up the options for passing to redux + delete opts.mapQueriesToProps; + delete opts.mapMutationsToProps; + + /* + + mapQueriesToProps: + + This method returns a object in the form of { [string]: WatchQueryHandle }. Each + key will be mapped to the props passed to the wrapped component. The resulting prop will + be an object with the following keys: + + { + loading: boolean, + error: Error, + result: GraphQLResult, + } + + */ + mapQueriesToProps = mapQueriesToProps ? mapQueriesToProps : defaultMapQueriesToProps; + + /* + + mapMutationsToProps + + */ + mapMutationsToProps = mapMutationsToProps ? mapMutationsToProps : defaultMapMutationsToProps; + + // Helps track hot reloading. + const version = nextVersion++; + + return function wrapWithApolloComponent(WrappedComponent) { + const apolloConnectDisplayName = `Apollo(Connect(${getDisplayName(WrappedComponent)}))`; + + class ApolloConnect extends Component { + static displayName = apolloConnectDisplayName; + static WrappedComponent = WrappedComponent; + static contextTypes = { + store: PropTypes.object.isRequired, + client: PropTypes.object.isRequired, + }; + + // react and react dev tools (HMR) needs + public state: any; // redux state + public props: any; // passed props + public version: number; + + // data storage + private store: Store; + private client: ApolloClient; // apollo client + private data: any; // apollo data + + // request / action storage + private queryHandles: any; + private mutations: any; + + // calculated switches to control rerenders + private haveOwnPropsChanged: boolean; + private hasQueryDataChanged: boolean; + private hasMutationDataChanged: boolean; + + // the element to render + private renderedElement: any; + + constructor(props, context) { + super(props, context); + this.version = version; + this.store = props.store || context.store; + this.client = props.client || context.client; + + invariant(!!this.client, + `Could not find "client" in either the context or ` + + `props of "${apolloConnectDisplayName}". ` + + `Either wrap the root component in a , ` + + `or explicitly pass "client" as a prop to "${apolloConnectDisplayName}".` + ); + + const storeState = this.store.getState(); + this.state = assign({}, storeState); + + this.data = {}; + this.mutations = {}; + } + + componentWillMount() { + const { props, state } = this; + this.subscribeToAllQueries(props, state); + this.createAllMutationHandles(props, state); + } + + // // best practice says make external requests in `componentDidMount` as to + // // not block rendering + // componentDidMount() { + + // } + + componentWillReceiveProps(nextProps, nextState) { + // we got new props, we need to unsubscribe and re-watch all handles + // with the new data + // XXX determine if any of this data is actually used somehow + // to avoid rebinding queries if nothing has changed + if (!isEqual(this.props, nextProps) || !isEqual(this.state, nextState)) { + this.unsubcribeAllQueries(); + this.subscribeToAllQueries(nextProps, nextState); + } + } + + shouldComponentUpdate() { + return this.haveOwnPropsChanged || this.hasQueryDataChanged || this.hasMutationDataChanged; + } + + componentWillUnmount() { + this.unsubcribeAllQueries(); + } + + subscribeToAllQueries(props: any, state: any) { + const { watchQuery, reduxRootKey } = this.client; + const { store } = this; + + const queryHandles = mapQueriesToProps({ + state, + ownProps: props, + }); + + if (isObject && Object.keys(queryHandles).length) { + this.queryHandles = queryHandles; + + for (const key in queryHandles) { + if (!queryHandles.hasOwnProperty(key)) { + continue; + } + + const { query, variables } = queryHandles[key]; + + const handle = watchQuery({ query, variables }); + + // rudimentary way to manually check cache + let queryData = defaultQueryData as any; + try { + const result = readQueryFromStore({ + store: store.getState()[reduxRootKey].data, + query, + variables, + }); + + queryData = { + error: null, + loading: false, + result, + }; + } catch (e) {/* tslint */} + + this.data[key] = queryData; + + this.handleQueryData(handle, key); + } + } + } + + unsubcribeAllQueries() { + if (this.queryHandles) { + for (const key in this.queryHandles) { + if (!this.queryHandles.hasOwnProperty(key)) { + continue; + } + this.queryHandles[key].stop(); + } + } + } + + handleQueryData(handle: any, key: string) { + // bind each handle to updating and rerendering when data + // has been recieved + + const forceRender = ({ error, data }: any) => { + this.data[key] = { + loading: false, + result: data || null, + error, + }; + + this.hasQueryDataChanged = true; + + // update state to latest of redux store + this.setState(this.store.getState()); + }; + + handle.subscribe({ + onResult: forceRender, + onError(error) { forceRender({ error }); }, + }); + } + + createAllMutationHandles(props: any, state: any): void { + const mutations = mapMutationsToProps({ + state, + ownProps: props, + }); + + if (isObject && Object.keys(mutations).length) { + for (const key in mutations) { + if (!mutations.hasOwnProperty(key)) { + continue; + } + + // setup thunk of mutation + const handle = this.createMutationHandle(key, mutations[key]); + + // XXX should we validate we have what we need to prevent errors? + + // bind key to state for updating + this.data[key] = defaultMutationData; + this.mutations[key] = handle; + } + } + } + + createMutationHandle(key: string, method: () => { mutation: string, variables?: any }): () => Promise { + const { mutate } = this.client; + const { store } = this; + + // middleware to update the props to send data to wrapped component + // when the mutation is done + const forceRender = ({ errors, data }: GraphQLResult): GraphQLResult => { + this.data[key] = { + loading: false, + result: data, + error: errors, + }; + + this.hasMutationDataChanged = true; + + // update state to latest of redux store + // this forces a render of children + this.setState(store.getState()); + + return { + errors, + data, + }; + }; + + return (...args) => { + const { mutation, variables } = method.apply(this.client, args); + return mutate({ mutation, variables }) + .then(forceRender) + .catch(error => forceRender({ errors: error })); + }; + } + + + render() { + const { + haveOwnPropsChanged, + hasQueryDataChanged, + hasMutationDataChanged, + renderedElement, + mutations, + props, + data, + } = this; + + this.haveOwnPropsChanged = false; + this.hasQueryDataChanged = false; + this.hasMutationDataChanged = false; + + let clientProps = { + mutate: this.client.mutate, + query: this.client.query, + } as any; + + if (Object.keys(mutations).length) { + clientProps.mutations = mutations; + } + + const mergedPropsAndData = assign({}, props, data, clientProps); + + if ( + !haveOwnPropsChanged && + !hasQueryDataChanged && + !hasMutationDataChanged && + renderedElement + ) { + return renderedElement; + } + + this.renderedElement = createElement(WrappedComponent, mergedPropsAndData); + return this.renderedElement; + } + + } + + // apply react-redux args from original args + const { mapStateToProps, mapDispatchToProps, mergeProps, options } = opts; + return ReactReduxConnect( + mapStateToProps, + mapDispatchToProps, + mergeProps, + options + )(ApolloConnect); + + }; + +}; + + diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000000..eed507c48e --- /dev/null +++ b/src/index.ts @@ -0,0 +1,4 @@ +import Provider from './Provider'; +import connect from './connect'; + +export { Provider, connect }; diff --git a/test/Provider.tsx b/test/Provider.tsx new file mode 100644 index 0000000000..90d46b96bb --- /dev/null +++ b/test/Provider.tsx @@ -0,0 +1,84 @@ +/// + +import * as React from 'react'; +import * as chai from 'chai'; +import { shallow } from 'enzyme'; +// import * as TestUtils from 'react-addons-test-utils'; +import { createStore } from 'redux'; + +declare function require(name: string); +import chaiEnzyme = require('chai-enzyme'); + +chai.use(chaiEnzyme()); // Note the invocation at the end +const { expect } = chai; + +import Provider from '../src/Provider'; + +import ApolloClient from 'apollo-client'; + +describe(' Component', () => { + + class Child extends React.Component { + render() { + return
; + } + }; + + const client = new ApolloClient(); + + // XXX why isn't this working with TestUtils typing? + // Child.contextType = { + // store: React.PropTypes.object.isRequired, + // client: React.PropTypes.object.isRequired, + // }; + + it('should render children components', () => { + const store = createStore(() => ({})); + + const wrapper = shallow( + +
+ + ); + + expect(wrapper.contains(
)).to.equal(true); + }); + + it('should not require a store', () => { + const wrapper = shallow( + +
+ + ); + + expect(wrapper.contains(
)).to.equal(true); + }); + + // it('should throw if rendered without a child component', () => { + // const store = createStore(() => ({})); + + // try { + // shallow( + // + // ); + // } catch (e) { + // expect(e).to.be.instanceof(Error); + // } + + // }); + + // it('should add the store to the child context', () => { + // const store = createStore(() => ({})); + + // const tree = TestUtils.renderIntoDocument( + // + // + // + // ) as React.Component; + + // const child = TestUtils.findRenderedComponentWithType(tree, Child as ComponentClass); + // expect(child.context.store).to.deep.equal(store); + + // }); + +}); diff --git a/test/connect.tsx b/test/connect.tsx new file mode 100644 index 0000000000..af4b67038e --- /dev/null +++ b/test/connect.tsx @@ -0,0 +1,1321 @@ +/// + +import * as React from 'react'; +import * as chai from 'chai'; +import { mount } from 'enzyme'; +import { createStore, combineReducers, applyMiddleware } from 'redux'; +import { connect as ReactReduxConnect } from 'react-redux'; +import assign = require('object-assign'); +// import { spy } from 'sinon'; + +import { + GraphQLResult, + parse, + print, +} from 'graphql'; + +import ApolloClient from 'apollo-client'; + +declare function require(name: string); +import chaiEnzyme = require('chai-enzyme'); + +chai.use(chaiEnzyme()); // Note the invocation at the end +const { expect } = chai; + +import connect from '../src/connect'; + +describe('connect', () => { + + class Passthrough extends React.Component { + render() { + return ; + } + }; + + class ProviderMock extends React.Component { + + static childContextTypes = { + store: React.PropTypes.object.isRequired, + client: React.PropTypes.object.isRequired, + }; + + getChildContext() { + return { + store: this.props.store, + client: this.props.client, + }; + } + + render() { + return React.Children.only(this.props.children); + } + }; + + describe('prop api', () => { + it('should pass `ApolloClient.query` as props.query', () => { + const store = createStore(() => ({ })); + const query = ` + query people { + allPeople(first: 1) { + people { + name + } + } + } + `; + + const data = { + allPeople: { + people: [ + { + name: 'Luke Skywalker', + }, + ], + }, + }; + + const networkInterface = mockNetworkInterface({ + request: { query }, + result: { data }, + }); + + const client = new ApolloClient({ + networkInterface, + }); + + @connect() + class Container extends React.Component { + render() { + return ; + } + }; + + const wrapper = mount( + + + + ); + + + const props = wrapper.find('span').props() as any; + + expect(props.query).to.exist; + expect(props.query({ query })).to.be.instanceof(Promise); + + }); + + it('should pass `ApolloClient.mutate` as props.mutate', () => { + const store = createStore(() => ({ })); + const client = new ApolloClient(); + + @connect() + class Container extends React.Component { + render() { + return ; + } + }; + + const wrapper = mount( + + + + ); + + + const props = wrapper.find('span').props() as any; + + expect(props.mutate).to.exist; + try { + expect(props.mutate()).to.be.instanceof(Promise); + } catch (e) { + expect(e).to.be.instanceof(TypeError); + }; + + }); + + it('should pass mutation methods as props.mutations dictionary', () => { + const store = createStore(() => ({ })); + + function mapMutationsToProps() { + return { + test: () => ({ + mutate: ``, + }), + }; + }; + + @connect({ mapMutationsToProps }) + class Container extends React.Component { + render() { + return ; + } + }; + + const wrapper = mount( + + + + ); + + + const props = wrapper.find('span').props() as any; + + expect(props.mutations).to.exist; + expect(props.mutations.test).to.exist; + expect(props.mutations.test).to.be.instanceof(Function); + + }); + + }); + + describe('redux passthrough', () => { + it('should allow mapStateToProps', () => { + const store = createStore(() => ({ + foo: 'bar', + baz: 42, + hello: 'world', + })); + + const mapStateToProps = ({ foo, baz }) => ({ foo, baz }); + + @ReactReduxConnect(mapStateToProps) + class Container extends React.Component { + render() { + return ; + } + }; + + @connect({mapStateToProps}) + class ApolloContainer extends React.Component { + render() { + return ; + } + }; + + const wrapper = mount( + + + + ); + + const apolloWrapper = mount( + + + + ); + + const reduxProps = assign({}, wrapper.find('span').props(), { + query: undefined, + mutate: undefined, + }); + const apolloProps = apolloWrapper.find('span').props(); + + expect(reduxProps).to.deep.equal(apolloProps); + + }); + + it('should allow mapDispatchToProps', () => { + function doSomething(thing) { + return { + type: 'APPEND', + body: thing, + }; + }; + + const store = createStore(() => ({ + foo: 'bar', + baz: 42, + hello: 'world', + })); + + const mapDispatchToProps = dispatch => ({ + doSomething: (whatever) => dispatch(doSomething(whatever)), + }); + + @ReactReduxConnect(null, mapDispatchToProps) + class Container extends React.Component { + render() { + return ; + } + }; + + @connect({mapDispatchToProps}) + class ApolloContainer extends React.Component { + render() { + return ; + } + }; + + const wrapper = mount( + + + + ); + + const apolloWrapper = mount( + + + + ); + + const reduxProps = assign({}, wrapper.find('span').props(), { + query: () => {/* tslint */}, + mutate: () => {/* tslint */}, + }) as any; + const apolloProps = apolloWrapper.find('span').props() as any; + + expect(reduxProps.doSomething()).to.deep.equal(apolloProps.doSomething()); + + }); + + it('should allow mergeProps', () => { + function doSomething(thing) { + return { + type: 'APPEND', + body: thing, + }; + }; + + const store = createStore(() => ({ + foo: 'bar', + baz: 42, + hello: 'world', + })); + + const mapStateToProps = ({ foo, baz }) => ({ foo, baz }); + + const mapDispatchToProps = dispatch => ({ + doSomething: (whatever) => dispatch(doSomething(whatever)), + }); + + const mergeProps = (stateProps, dispatchProps, ownProps) => { + return { + bar: stateProps.baz + 1, + makeSomething: dispatchProps.doSomething, + hallPass: ownProps.pass, + }; + }; + + @ReactReduxConnect(mapStateToProps, mapDispatchToProps, mergeProps) + class Container extends React.Component { + render() { + return ; + } + }; + + @connect({mapStateToProps, mapDispatchToProps, mergeProps}) + class ApolloContainer extends React.Component { + render() { + return ; + } + }; + + const wrapper = mount( + + + + ); + + const apolloWrapper = mount( + + + + ); + + const reduxProps = assign({}, wrapper.find('span').props(), { + query: () => {/* tslint */}, + mutate: () => {/* tslint */}, + }) as any; + const apolloProps = apolloWrapper.find('span').props() as any; + + expect(reduxProps.makeSomething()).to.deep.equal(apolloProps.makeSomething()); + expect(reduxProps.bar).to.equal(apolloProps.bar); + expect(reduxProps.hallPass).to.equal(apolloProps.hallPass); + + }); + }); + + describe('apollo methods', () => { + describe('queries', () => { + it('binds a query to props', () => { + const store = createStore(() => ({ + foo: 'bar', + baz: 42, + hello: 'world', + })); + + const query = ` + query people { + allPeople(first: 1) { + people { + name + } + } + } + `; + + const data = { + allPeople: { + people: [ + { + name: 'Luke Skywalker', + }, + ], + }, + }; + + const networkInterface = mockNetworkInterface({ + request: { query }, + result: { data }, + }); + + const client = new ApolloClient({ + networkInterface, + }); + + function mapQueriesToProps() { + return { + people: { query }, + }; + }; + + @connect({ mapQueriesToProps }) + class Container extends React.Component { + render() { + return ; + } + }; + + const wrapper = mount( + + + + ); + + const props = wrapper.find('span').props() as any; + + expect(props.people).to.exist; + expect(props.people.loading).to.be.true; + }); + + it('allows variables as part of the request', () => { + const store = createStore(() => ({ + foo: 'bar', + baz: 42, + hello: 'world', + })); + + const query = ` + query people($count: Int) { + allPeople(first: $count) { + people { + name + } + } + } + `; + + const variables = { + count: 1, + }; + + const data = { + allPeople: { + people: [ + { + name: 'Luke Skywalker', + }, + ], + }, + }; + + + const networkInterface = mockNetworkInterface({ + request: { query, variables }, + result: { data }, + }); + + const client = new ApolloClient({ + networkInterface, + }); + + function mapQueriesToProps() { + return { + people: { query, variables }, + }; + }; + + @connect({ mapQueriesToProps }) + class Container extends React.Component { + render() { + return ; + } + }; + + const wrapper = mount( + + + + ); + + const props = wrapper.find('span').props() as any; + + expect(props.people).to.exist; + expect(props.people.loading).to.be.true; + }); + + it('can use passed props as part of the query', () => { + const store = createStore(() => ({ + foo: 'bar', + baz: 42, + hello: 'world', + })); + + const query = ` + query people($count: Int) { + allPeople(first: $count) { + people { + name + } + } + } + `; + + const variables = { + count: 1, + }; + + const data = { + allPeople: { + people: [ + { + name: 'Luke Skywalker', + }, + ], + }, + }; + + + const networkInterface = mockNetworkInterface({ + request: { query, variables }, + result: { data }, + }); + + const client = new ApolloClient({ + networkInterface, + }); + + function mapQueriesToProps({ ownProps }) { + expect(ownProps.passedCountProp).to.equal(2); + return { + people: { + query, + variables: { + count: ownProps.passedCountProp, + }, + }, + }; + }; + + @connect({ mapQueriesToProps }) + class Container extends React.Component { + render() { + return ; + } + }; + + const wrapper = mount( + + + + ); + + const props = wrapper.find('span').props() as any; + + expect(props.people).to.exist; + expect(props.people.loading).to.be.true; + }); + + it('can use the redux state as part of the query', () => { + const store = createStore(() => ({ + foo: 'bar', + baz: 42, + hello: 'world', + })); + + const query = ` + query people($count: Int) { + allPeople(first: $count) { + people { + name + } + } + } + `; + + const variables = { + count: 1, + }; + + const data = { + allPeople: { + people: [ + { + name: 'Luke Skywalker', + }, + ], + }, + }; + + + const networkInterface = mockNetworkInterface({ + request: { query, variables }, + result: { data }, + }); + + const client = new ApolloClient({ + networkInterface, + }); + + function mapQueriesToProps({ state }) { + expect(state.hello).to.equal('world'); + return { + people: { + query, + variables: { + count: 1, + }, + }, + }; + }; + + @connect({ mapQueriesToProps }) + class Container extends React.Component { + render() { + return ; + } + }; + + const wrapper = mount( + + + + ); + + const props = wrapper.find('span').props() as any; + + expect(props.people).to.exist; + expect(props.people.loading).to.be.true; + }); + + it('allows for multiple queries', () => { + const store = createStore(() => ({ + foo: 'bar', + baz: 42, + hello: 'world', + })); + + const peopleQuery = ` + query people($count: Int) { + allPeople(first: $count) { + people { + name + } + } + } + `; + + const peopleData = { + allPeople: { + people: [ + { + name: 'Luke Skywalker', + }, + ], + }, + }; + + // const shipData = { + // allStarships: { + // starships: [ + // { + // name: 'CR90 corvette', + // }, + // ], + // }, + // }; + + const shipQuery = ` + query starships($count: Int) { + allStarships(first: $count) { + starships { + name + } + } + } + `; + + const variables = { count: 1 }; + + const networkInterface = mockNetworkInterface({ + request: { query: peopleQuery, variables }, + result: { data: peopleData }, + }); + + const client = new ApolloClient({ + networkInterface, + }); + + function mapQueriesToProps() { + return { + people: { query: peopleQuery, variables }, + ships: { query: shipQuery, variables }, + }; + }; + + @connect({ mapQueriesToProps }) + class Container extends React.Component { + render() { + return ; + } + }; + + const wrapper = mount( + + + + ); + + const props = wrapper.find('span').props() as any; + + expect(props.people).to.exist; + expect(props.people.loading).to.be.true; + + expect(props.ships).to.exist; + expect(props.ships.loading).to.be.true; + }); + + + it('should update the props of the child component when data is returned', (done) => { + const store = createStore(() => ({ })); + + const query = ` + query people { + luke: allPeople(first: 1) { + people { + name + } + } + } + `; + + const data = { + luke: { + people: [ + { + name: 'Luke Skywalker', + }, + ], + }, + }; + + const networkInterface = mockNetworkInterface({ + request: { query }, + result: { data }, + }); + + const client = new ApolloClient({ + networkInterface, + }); + + function mapQueriesToProps() { + return { + luke: { query }, + }; + }; + + @connect({ mapQueriesToProps }) + class Container extends React.Component { + componentDidUpdate(prevProps) { + expect(prevProps.luke.loading).to.be.true; + expect(this.props.luke.result).to.deep.equal(data); + done(); + } + render() { + return ; + } + }; + + mount( + + + + ); + }); + + it('should prefill any data already in the store', (done) => { + + const query = ` + query people { + allPeople(first: 1) { + people { + name + } + } + } + `; + + const data = { + allPeople: { + people: [ + { + name: 'Luke Skywalker', + }, + ], + }, + }; + + const networkInterface = mockNetworkInterface({ + request: { query }, + result: { data }, + }); + + const client = new ApolloClient({ + networkInterface, + }); + + const reducer = client.reducer() as any; + + const store = createStore( + combineReducers({ + apollo: reducer, + }), + applyMiddleware(client.middleware()) + ); + + // we prefill the store with a query + client.query({ query }) + .then(() => { + function mapQueriesToProps() { + return { + people: { query }, + }; + }; + + @connect({ mapQueriesToProps }) + class Container extends React.Component { + render() { + return ; + } + }; + + const wrapper = mount( + + + + ); + + const props = wrapper.find('span').props() as any; + + expect(props.people).to.exist; + expect(props.people.loading).to.be.false; + expect(props.people.result).to.deep.equal(data); + done(); + }); + }); + }); + + describe('mutations', () => { + it('should bind mutation data to props', () => { + const store = createStore(() => ({ + foo: 'bar', + baz: 42, + hello: 'world', + })); + + const mutation = ` + mutation makeListPrivate($listId: ID!) { + makeListPrivate(id: $listId) + } + `; + + const variables = { + listId: '1', + }; + + const data = { + makeListPrivate: true, + }; + + const networkInterface = mockNetworkInterface({ + request: { query: mutation, variables }, + result: { data }, + }); + + const client = new ApolloClient({ + networkInterface, + }); + + function mapMutationsToProps() { + return { + makeListPrivate: () => ({ + mutation, + variables, + }), + }; + }; + + @connect({ mapMutationsToProps }) + class Container extends React.Component { + render() { + return ; + } + }; + + const wrapper = mount( + + + + ); + + const props = wrapper.find('span').props() as any; + + expect(props.makeListPrivate).to.exist; + expect(props.makeListPrivate.loading).to.be.true; + }); + + it('should bind multiple mutation keys to props', () => { + const store = createStore(() => ({ + foo: 'bar', + baz: 42, + hello: 'world', + })); + + const mutation1 = ` + mutation makeListPrivate($listId: ID!) { + makeListPrivate(id: $listId) + } + `; + + const mutation2 = ` + mutation makeListReallyPrivate($listId: ID!) { + makeListReallyPrivate(id: $listId) + } + `; + + const data = { + makeListPrivate: true, + }; + + const networkInterface = mockNetworkInterface({ + request: { query: mutation1 }, + result: { data }, + }); + + const client = new ApolloClient({ + networkInterface, + }); + + function mapMutationsToProps() { + return { + makeListPrivate: () => ({ + mutation1, + }), + makeListReallyPrivate: () => ({ + mutation2, + }), + }; + }; + + @connect({ mapMutationsToProps }) + class Container extends React.Component { + render() { + return ; + } + }; + + const wrapper = mount( + + + + ); + + const props = wrapper.find('span').props() as any; + + expect(props.makeListPrivate).to.exist; + expect(props.makeListPrivate.loading).to.be.true; + expect(props.makeListReallyPrivate).to.exist; + expect(props.makeListReallyPrivate.loading).to.be.true; + }); + + it('should bind mutation handler to `props.mutations[key]`', () => { + const store = createStore(() => ({ + foo: 'bar', + baz: 42, + hello: 'world', + })); + + const mutation = ` + mutation makeListPrivate($listId: ID!) { + makeListPrivate(id: $listId) + } + `; + + const variables = { + listId: '1', + }; + + const data = { + makeListPrivate: true, + }; + + const networkInterface = mockNetworkInterface({ + request: { query: mutation, variables }, + result: { data }, + }); + + const client = new ApolloClient({ + networkInterface, + }); + + function mapMutationsToProps() { + return { + makeListPrivate: () => ({ + mutation, + }), + }; + }; + + @connect({ mapMutationsToProps }) + class Container extends React.Component { + render() { + return ; + } + }; + + const wrapper = mount( + + + + ); + + const props = wrapper.find('span').props() as any; + + expect(props.makeListPrivate).to.exist; + expect(props.makeListPrivate.loading).to.be.true; + + expect(props.mutations).to.exist; + expect(props.mutations.makeListPrivate).to.exist; + expect(props.mutations.makeListPrivate).to.be.instanceof(Function); + }); + + it('should update the props of the child component when data is returned', (done) => { + const store = createStore(() => ({ })); + + const mutation = ` + mutation makeListPrivate($listId: ID!) { + makeListPrivate(id: $listId) + } + `; + + const variables = { + listId: '1', + }; + + const data = { + makeListPrivate: true, + }; + + const networkInterface = mockNetworkInterface({ + request: { query: mutation, variables }, + result: { data }, + }); + + const client = new ApolloClient({ + networkInterface, + }); + + function mapMutationsToProps() { + return { + makeListPrivate: () => ({ + mutation, + variables, + }), + }; + }; + + @connect({ mapMutationsToProps }) + class Container extends React.Component { + componentDidMount() { + // call the muation + this.props.mutations.makeListPrivate(); + } + + componentDidUpdate(prevProps) { + expect(prevProps.makeListPrivate.loading).to.be.true; + expect(this.props.makeListPrivate.result).to.deep.equal(data); + done(); + } + + render() { + return ; + } + }; + + mount( + + + + ); + }); + + it('can use passed props as part of the mutation', (done) => { + const store = createStore(() => ({ + foo: 'bar', + baz: 42, + hello: 'world', + })); + + const mutation = ` + mutation makeListPrivate($listId: ID!) { + makeListPrivate(id: $listId) + } + `; + + const variables = { + listId: '1', + }; + + const data = { + makeListPrivate: true, + }; + + const networkInterface = mockNetworkInterface({ + request: { query: mutation, variables }, + result: { data }, + }); + + const client = new ApolloClient({ + networkInterface, + }); + + function mapMutationsToProps({ ownProps }) { + expect(ownProps.listId).to.equal('1'); + return { + makeListPrivate: () => { + return { + mutation, + variables: { + listId: ownProps.listId, + }, + }; + }, + }; + }; + + @connect({ mapMutationsToProps }) + class Container extends React.Component { + componentDidMount() { + // call the muation + this.props.mutations.makeListPrivate(); + } + + componentDidUpdate(prevProps) { + expect(prevProps.makeListPrivate.loading).to.be.true; + expect(this.props.makeListPrivate.result).to.deep.equal(data); + done(); + } + + render() { + return ; + } + }; + + mount( + + + + ); + }); + + it('can use the redux store as part of the mutation', (done) => { + const store = createStore(() => ({ + foo: 'bar', + baz: 42, + listId: '1', + })); + + const mutation = ` + mutation makeListPrivate($listId: ID!) { + makeListPrivate(id: $listId) + } + `; + + const variables = { + listId: '1', + }; + + const data = { + makeListPrivate: true, + }; + + const networkInterface = mockNetworkInterface({ + request: { query: mutation, variables }, + result: { data }, + }); + + const client = new ApolloClient({ + networkInterface, + }); + + function mapMutationsToProps({ state }) { + expect(state.listId).to.equal('1'); + return { + makeListPrivate: () => { + return { + mutation, + variables: { + listId: state.listId, + }, + }; + }, + }; + }; + + @connect({ mapMutationsToProps }) + class Container extends React.Component { + componentDidMount() { + // call the muation + this.props.mutations.makeListPrivate(); + } + + componentDidUpdate(prevProps) { + expect(prevProps.makeListPrivate.loading).to.be.true; + expect(this.props.makeListPrivate.result).to.deep.equal(data); + done(); + } + + render() { + return ; + } + }; + + mount( + + + + ); + }); + + it('should allow passing custom arugments to mutation handle', (done) => { + const store = createStore(() => ({ })); + + const mutation = ` + mutation makeListPrivate($listId: ID!) { + makeListPrivate(id: $listId) + } + `; + + const variables = { + listId: '1', + }; + + const data = { + makeListPrivate: true, + }; + + const networkInterface = mockNetworkInterface({ + request: { query: mutation, variables }, + result: { data }, + }); + + const client = new ApolloClient({ + networkInterface, + }); + + function mapMutationsToProps() { + return { + makeListPrivate: (listId) => { + // expect(listId).to.equal('1'); + return { + mutation, + variables: { + listId, + }, + }; + }, + }; + }; + + @connect({ mapMutationsToProps }) + class Container extends React.Component { + componentDidMount() { + // call the muation + this.props.mutations.makeListPrivate('1'); + } + + componentDidUpdate(prevProps) { + expect(prevProps.makeListPrivate.loading).to.be.true; + expect(this.props.makeListPrivate.result).to.deep.equal(data); + done(); + } + + render() { + return ; + } + }; + + mount( + + + + ); + }); + + }); + }); +}); + +function mockNetworkInterface( + mockedRequest: { + request: any, + result: GraphQLResult, + } +) { + const requestToResultMap: any = {}; + const { request, result } = mockedRequest; + + // Populate set of mocked requests + requestToResultMap[requestToKey(request)] = result as GraphQLResult; + + // A mock for the query method + const queryMock = (req: Request) => { + return new Promise((resolve, reject) => { + // network latency + const resultData = requestToResultMap[requestToKey(req)]; + if (!resultData) { + throw new Error(`Passed request that wasn't mocked: ${requestToKey(req)}`); + } + resolve(resultData); + + }); + }; + + return { + query: queryMock, + _uri: 'mock', + _opts: {}, + _middlewares: [], + use() { return; }, + }; +} + + +function requestToKey(request: any): string { + const query = request.query && print(parse(request.query)); + + return JSON.stringify({ + variables: request.variables, + query, + }); +} diff --git a/test/fixtures/setup.js b/test/fixtures/setup.js new file mode 100644 index 0000000000..9338e05e25 --- /dev/null +++ b/test/fixtures/setup.js @@ -0,0 +1,18 @@ +/* setup.js */ +// github.com/airbnb/enzyme/blob/699ec7e39560a68c198ecf80b59d177d003fc869/docs/guides/jsdom.md +var jsdom = require('jsdom').jsdom; + +var exposedProperties = ['window', 'navigator', 'document']; + +global.document = jsdom(''); +global.window = document.defaultView; +Object.keys(document.defaultView).forEach((property) => { + if (typeof global[property] === 'undefined') { + exposedProperties.push(property); + global[property] = document.defaultView[property]; + } +}); + +global.navigator = { + userAgent: 'node.js' +}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..763cf3228f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "declaration": true, + "noImplicitAny": false, + "rootDir": ".", + "outDir": "lib", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "pretty": true, + "removeComments": true, + "jsx": "react" + }, + "exclude": [ + "typings", + "node_modules", + "dist", + "lib", + "test/fixtures" + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000000..f125713739 --- /dev/null +++ b/tslint.json @@ -0,0 +1,142 @@ +{ + "rules": { + "align": [ + false, + "parameters", + "arguments", + "statements" + ], + "ban": false, + "class-name": true, + "curly": true, + "eofline": true, + "forin": true, + "indent": [ + true, + "spaces" + ], + "interface-name": false, + "jsdoc-format": true, + "label-position": true, + "label-undefined": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + "public-before-private", + "static-before-instance", + "variables-before-functions" + ], + "no-any": false, + "no-arg": true, + "no-bitwise": true, + "no-conditional-assignment": true, + "no-consecutive-blank-lines": false, + "no-console": [ + true, + "log", + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-constructor-vars": true, + "no-debugger": true, + "no-duplicate-key": true, + "no-duplicate-variable": true, + "no-empty": true, + "no-eval": true, + "no-inferrable-types": false, + "no-internal-module": true, + "no-null-keyword": false, + "no-require-imports": false, + "no-shadowed-variable": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unreachable": true, + "no-unused-expression": true, + "no-unused-variable": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "no-var-requires": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-finally", + "check-whitespace" + ], + "quotemark": [ + true, + "single", + "avoid-escape" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "switch-default": true, + "trailing-comma": [ + true, + { + "multiline": "always", + "singleline": "never" + } + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef": [ + false, + "call-signature", + "parameter", + "arrow-parameter", + "property-declaration", + "variable-declaration", + "member-variable-declaration" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + }, + { + "call-signature": "space", + "index-signature": "space", + "parameter": "space", + "property-declaration": "space", + "variable-declaration": "space" + } + ], + "use-strict": [ + false + ], + "variable-name": [ + false, + "check-format", + "allow-leading-underscore", + "ban-keywords" + ], + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } +} diff --git a/typings.json b/typings.json new file mode 100644 index 0000000000..408601c8cc --- /dev/null +++ b/typings.json @@ -0,0 +1,25 @@ +{ + "ambientDependencies": { + "enzyme": "registry:dt/enzyme#1.2.0+20160324040416", + "mocha": "registry:dt/mocha#2.2.5+20160317120654", + "react": "registry:dt/react#0.14.0+20160319053454", + "react-addons-test-utils": "registry:dt/react-addons-test-utils#0.14.0+20160316155526", + "react-dom": "registry:dt/react-dom#0.14.0+20160316155526", + "graphql": "github:nitintutlani/typed-graphql#38bdf66576693dfe808a34876295e989f9199115", + "isomorphic-fetch": "registry:dt/isomorphic-fetch#0.0.0+20160316155526", + "es6-promise": "registry:dt/es6-promise#0.0.0+20160317120654", + "node": "registry:dt/node#4.0.0+20160319033040" + }, + "devDependencies": { + "chai": "registry:npm/chai#3.5.0+20160328084239", + "chai-enzyme": "registry:npm/chai-enzyme#0.4.0+20160310114720", + "sinon": "registry:npm/sinon#1.16.0+20160309002336" + }, + "dependencies": { + "apollo-client": "file:node_modules/apollo-client/lib/src/index.d.ts", + "invariant": "registry:npm/invariant#2.0.0+20160211003958", + "lodash": "registry:npm/lodash#4.0.0+20160412191219", + "object-assign": "registry:npm/object-assign#4.0.1+20160301180549", + "react-redux": "registry:npm/react-redux#4.4.0+20160207114942" + } +}