diff --git a/CHANGELOG.md b/CHANGELOG.md index 999d6047848..c590b278dbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Change log ### vNEXT +- Make `fetch` implementation configurable [PR #2008](https://github.com/apollographql/apollo-client/pull/2008) ### 1.9.0 - Move to `apollo-link-core` from `apollo-link` to reduce bundle size [PR #1955](https://github.com/apollographql/apollo-client/pull/1955) diff --git a/fetch-mock.typings.d.ts b/fetch-mock.typings.d.ts index e046baed11d..525de2858d5 100644 --- a/fetch-mock.typings.d.ts +++ b/fetch-mock.typings.d.ts @@ -281,6 +281,14 @@ declare namespace FetchMock { lot of array buffers, it can be useful to default to `false` */ configure(opts: Object): void; + /** + * Configure the fetch implementation to be used + */ + setImplementations(implementations: Object): void; + /** + * Return a sandbox + */ + sandbox(): FetchMockStatic; } } diff --git a/fetch-ponyfill.typings.d.ts b/fetch-ponyfill.typings.d.ts new file mode 100644 index 00000000000..1291b12bf7e --- /dev/null +++ b/fetch-ponyfill.typings.d.ts @@ -0,0 +1,8 @@ +// Type definitions for fetch-ponyfill 4.1.0 +// Project: https://github.com/qubyte/fetch-ponyfill + +declare module 'fetch-ponyfill' { + function fetchPonyfill(options?: any): any; + namespace fetchPonyfill {} + export = fetchPonyfill; +} diff --git a/package.json b/package.json index 68859fb2bf0..89d451aa812 100644 --- a/package.json +++ b/package.json @@ -73,12 +73,12 @@ "license": "MIT", "dependencies": { "apollo-link-core": "^0.2.0", + "fetch-ponyfill": "^4.1.0", "graphql": "^0.10.0", "graphql-anywhere": "^3.0.1", "graphql-tag": "^2.0.0", "redux": "^3.4.0", - "symbol-observable": "^1.0.2", - "whatwg-fetch": "^2.0.0" + "symbol-observable": "^1.0.2" }, "devDependencies": { "@types/benchmark": "1.0.30", @@ -104,7 +104,6 @@ "grunt": "1.0.1", "grunt-tslint": "5.0.1", "gzip-size": "3.0.0", - "isomorphic-fetch": "2.2.1", "istanbul": "0.4.5", "lint-staged": "4.0.2", "lodash": "4.17.4", diff --git a/src/transport/batchedNetworkInterface.ts b/src/transport/batchedNetworkInterface.ts index 22ecbf6471c..f5d478b398d 100644 --- a/src/transport/batchedNetworkInterface.ts +++ b/src/transport/batchedNetworkInterface.ts @@ -1,7 +1,5 @@ import { ExecutionResult } from 'graphql'; -import 'whatwg-fetch'; - import { BaseNetworkInterface, HTTPNetworkInterface, @@ -43,13 +41,15 @@ export class HTTPBatchedNetworkInterface extends BaseNetworkInterface { batchInterval = 10, batchMax = 0, fetchOpts, + fetch = undefined, }: { uri: string; batchInterval?: number; batchMax?: number; fetchOpts: RequestInit; + fetch?: any; }) { - super(uri, fetchOpts); + super(uri, fetchOpts, fetch); if (typeof batchInterval !== 'number') { throw new Error(`batchInterval must be a number, got ${batchInterval}`); @@ -236,7 +236,7 @@ export class HTTPBatchedNetworkInterface extends BaseNetworkInterface { return printRequest(request); }); - return fetch(this._uri, { + return this._fetch(this._uri, { ...this._opts, body: JSON.stringify(printedRequests), method: 'POST', @@ -255,6 +255,7 @@ export interface BatchingNetworkInterfaceOptions { batchInterval?: number; batchMax?: number; opts?: RequestInit; + fetch?: any; } export function createBatchingNetworkInterface( @@ -270,5 +271,6 @@ export function createBatchingNetworkInterface( batchInterval: options.batchInterval, batchMax: options.batchMax, fetchOpts: options.opts || {}, + fetch: options.fetch, }); } diff --git a/src/transport/networkInterface.ts b/src/transport/networkInterface.ts index 7c7819b667c..65b4450229f 100644 --- a/src/transport/networkInterface.ts +++ b/src/transport/networkInterface.ts @@ -1,5 +1,3 @@ -import 'whatwg-fetch'; - import { ExecutionResult, DocumentNode } from 'graphql'; import { print } from 'graphql/language/printer'; @@ -12,6 +10,11 @@ import { removeConnectionDirectiveFromDocument } from '../queries/queryTransform import { Observable } from '../util/Observable'; +import * as fetchPonyfill_ from 'fetch-ponyfill'; +// Trick rollup, see: +// https://github.com/rollup/rollup/issues/670#issuecomment-284621537 +const fetchPonyfill = fetchPonyfill_; + /** * This is an interface that describes an GraphQL document to be sent * to the server. @@ -100,8 +103,13 @@ export class BaseNetworkInterface implements NetworkInterface { public _afterwares: AfterwareInterface[] | BatchAfterwareInterface[]; public _uri: string; public _opts: RequestInit; + public _fetch: any; - constructor(uri: string | undefined, opts: RequestInit = {}) { + constructor( + uri: string | undefined, + opts: RequestInit = {}, + fetch: any | undefined, + ) { if (!uri) { throw new Error('A remote endpoint is required for a network layer'); } @@ -112,6 +120,7 @@ export class BaseNetworkInterface implements NetworkInterface { this._uri = uri; this._opts = { ...opts }; + this._fetch = fetch || fetchPonyfill(); this._middlewares = []; this._afterwares = []; @@ -184,7 +193,7 @@ export class HTTPFetchNetworkInterface extends BaseNetworkInterface { request, options, }: RequestAndOptions): Promise { - return fetch(this._uri, { + return this._fetch(this._uri, { ...this._opts, body: JSON.stringify(printRequest(request)), method: 'POST', @@ -277,6 +286,7 @@ export class HTTPFetchNetworkInterface extends BaseNetworkInterface { export interface NetworkInterfaceOptions { uri?: string; opts?: RequestInit; + fetch?: any; } export function createNetworkInterface( @@ -291,6 +301,7 @@ export function createNetworkInterface( let uri: string | undefined; let opts: RequestInit | undefined; + let fetch: any | undefined; // We want to change the API in the future so that you just pass all of the options as one // argument, so even though the internals work with two arguments we're warning here. @@ -299,9 +310,11 @@ export function createNetworkInterface( as of Apollo Client 0.5. Please pass it as the "uri" property of the network interface options.`); opts = secondArgOpts.opts; uri = uriOrInterfaceOpts; + fetch = secondArgOpts.fetch; } else { opts = uriOrInterfaceOpts.opts; uri = uriOrInterfaceOpts.uri; + fetch = uriOrInterfaceOpts.fetch; } - return new HTTPFetchNetworkInterface(uri, opts); + return new HTTPFetchNetworkInterface(uri, opts, fetch); } diff --git a/test/batchedNetworkInterface.ts b/test/batchedNetworkInterface.ts index 75fd2456e0d..d3bc98b5367 100644 --- a/test/batchedNetworkInterface.ts +++ b/test/batchedNetworkInterface.ts @@ -17,10 +17,6 @@ import { ExecutionResult } from 'graphql'; import gql from 'graphql-tag'; -import 'whatwg-fetch'; - -declare var fetch: any; - describe('HTTPBatchedNetworkInterface', () => { // Helper method that tests a roundtrip given a particular set of requests to the // batched network interface and the @@ -41,14 +37,6 @@ describe('HTTPBatchedNetworkInterface', () => { opts?: RequestInit; }) => { const url = 'http://fake.com/graphql'; - const batchedNetworkInterface = new HTTPBatchedNetworkInterface({ - uri: url, - batchInterval: 10, - fetchOpts: opts, - }); - - batchedNetworkInterface.use(middlewares); - batchedNetworkInterface.useAfter(afterwares); const printedRequests: Array = []; const resultList: Array = []; @@ -57,7 +45,7 @@ describe('HTTPBatchedNetworkInterface', () => { resultList.push(result); }); - fetch = + const fetch = fetchFunc || createMockFetch({ url, @@ -75,6 +63,16 @@ describe('HTTPBatchedNetworkInterface', () => { result: createMockedIResponse(resultList), }); + const batchedNetworkInterface = new HTTPBatchedNetworkInterface({ + uri: url, + batchInterval: 10, + fetchOpts: opts, + fetch, + }); + + batchedNetworkInterface.use(middlewares); + batchedNetworkInterface.useAfter(afterwares); + return batchedNetworkInterface .batchQuery(requestResultPairs.map(({ request }) => request)) .then(results => { @@ -459,18 +457,13 @@ describe('HTTPBatchedNetworkInterface', () => { `; const url = 'http://fake.com/graphql'; - const batchedNetworkInterface = new HTTPBatchedNetworkInterface({ - uri: url, - batchInterval: 10, - fetchOpts: {}, - }); const printedRequests: Array = [ printRequest({ query: authorQuery }), ]; const resultList: Array = [authorResult]; - fetch = createMockFetch({ + const fetch = createMockFetch({ url, opts: { body: JSON.stringify(printedRequests), @@ -483,6 +476,13 @@ describe('HTTPBatchedNetworkInterface', () => { result: createMockedIResponse(resultList), }); + const batchedNetworkInterface = new HTTPBatchedNetworkInterface({ + uri: url, + batchInterval: 10, + fetchOpts: {}, + fetch, + }); + return batchedNetworkInterface .batchQuery([{ query: authorQueryWithConnection }]) .then(results => { diff --git a/test/client.ts b/test/client.ts index 6dd374f609a..951083ea3e4 100644 --- a/test/client.ts +++ b/test/client.ts @@ -1,7 +1,6 @@ import * as chai from 'chai'; const { assert } = chai; import * as sinon from 'sinon'; -import * as fetchMock from 'fetch-mock'; import ApolloClient, { printAST } from '../src'; @@ -72,8 +71,6 @@ import { cloneDeep, assign, isEqual } from 'lodash'; import { ApolloLink, Observable } from 'apollo-link-core'; -declare var fetch: any; - // make it easy to assert with promises chai.use(chaiAsPromised); @@ -2348,8 +2345,7 @@ describe('client', () => { }, }; const url = 'http://not-a-real-url.com'; - const oldFetch = fetch; - fetch = createMockFetch({ + const fetch = createMockFetch({ url, opts: { body: JSON.stringify([ @@ -2372,6 +2368,7 @@ describe('client', () => { uri: 'http://not-a-real-url.com', batchInterval: 5, opts: {}, + fetch, }); Promise.all([ networkInterface.query({ query: firstQuery }), @@ -2382,7 +2379,6 @@ describe('client', () => { firstResult, secondResult, ]); - fetch = oldFetch; done(); }) .catch(e => { @@ -2416,8 +2412,7 @@ describe('client', () => { } `; const url = 'http://not-a-real-url.com'; - const oldFetch = fetch; - fetch = createMockFetch({ + const fetch = createMockFetch({ url, opts: { body: JSON.stringify([ @@ -2440,6 +2435,7 @@ describe('client', () => { uri: 'http://not-a-real-url.com', batchInterval: 5, opts: {}, + fetch, }); Promise.all([ networkInterface.query({ query: firstQuery }), @@ -2453,7 +2449,6 @@ describe('client', () => { e.message, 'BatchingNetworkInterface: server response is not an array', ); - fetch = oldFetch; done(); }); }); @@ -2491,8 +2486,7 @@ describe('client', () => { }, }; const url = 'http://not-a-real-url.com'; - const oldFetch = fetch; - fetch = createMockFetch( + const fetch = createMockFetch( { url, opts: { @@ -2530,6 +2524,7 @@ describe('client', () => { uri: 'http://not-a-real-url.com', batchInterval: 5, opts: {}, + fetch, }); Promise.all([ networkInterface.query({ query: firstQuery }), @@ -2545,7 +2540,6 @@ describe('client', () => { firstResult, secondResult, ]); - fetch = oldFetch; done(); }) .catch(e => { @@ -2585,15 +2579,7 @@ describe('client', () => { const url = 'http://not-a-real-url.com'; - const networkInterface = createBatchingNetworkInterface({ - uri: url, - batchInterval: 5, - batchMax: 2, - opts: {}, - }); - - const oldFetch = fetch; - fetch = createMockFetch( + const fetch = createMockFetch( { url, opts: { @@ -2638,6 +2624,14 @@ describe('client', () => { }, ); + const networkInterface = createBatchingNetworkInterface({ + uri: url, + batchInterval: 5, + batchMax: 2, + opts: {}, + fetch, + }); + Promise.all([ networkInterface.query({ query: authorQuery }), networkInterface.query({ query: personQuery }), @@ -2661,7 +2655,6 @@ describe('client', () => { personResult, authorResult, ]); - fetch = oldFetch; done(); }) .catch(e => { @@ -2701,14 +2694,7 @@ describe('client', () => { const url = 'http://not-a-real-url.com'; - const networkInterface = createBatchingNetworkInterface({ - uri: url, - batchInterval: 5, - opts: {}, - }); - - const oldFetch = fetch; - fetch = createMockFetch({ + const fetch = createMockFetch({ url, opts: { body: JSON.stringify([ @@ -2733,6 +2719,13 @@ describe('client', () => { ]), }); + const networkInterface = createBatchingNetworkInterface({ + uri: url, + batchInterval: 5, + opts: {}, + fetch, + }); + Promise.all([ networkInterface.query({ query: authorQuery }), networkInterface.query({ query: personQuery }), @@ -2756,7 +2749,6 @@ describe('client', () => { personResult, authorResult, ]); - fetch = oldFetch; done(); }) .catch(e => { diff --git a/test/mocks/mockFetch.ts b/test/mocks/mockFetch.ts index 3b13f427b7a..4a007887d55 100644 --- a/test/mocks/mockFetch.ts +++ b/test/mocks/mockFetch.ts @@ -1,5 +1,3 @@ -import 'whatwg-fetch'; - // This is an implementation of a mocked window.fetch implementation similar in // structure to the MockedNetworkInterface. diff --git a/test/networkInterface.ts b/test/networkInterface.ts index cd445ff5980..2f35596730e 100644 --- a/test/networkInterface.ts +++ b/test/networkInterface.ts @@ -2,7 +2,15 @@ import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import { assign, isEqual } from 'lodash'; -import * as fetchMock from 'fetch-mock'; +import * as fetchMockModule from 'fetch-mock'; + +import * as fetchPonyfill_ from 'fetch-ponyfill'; +// Trick rollup, see: +// https://github.com/rollup/rollup/issues/670#issuecomment-284621537 +const fetchPonyfill = fetchPonyfill_; + +const fetchMock = fetchMockModule.sandbox(); +fetchMock.setImplementations(fetchPonyfill()); // make it easy to assert with promises chai.use(chaiAsPromised); @@ -257,7 +265,7 @@ describe('network interface', () => { it('should alter the request variables', () => { const testWare1 = TestWare([{ key: 'personNum', val: 1 }]); - const swapi = createNetworkInterface({ uri: swapiUrl }); + const swapi = createNetworkInterface({ uri: swapiUrl, fetch: fetchMock }); swapi.use([testWare1]); // this is a stub for the end user client api const simpleRequest = { @@ -275,7 +283,7 @@ describe('network interface', () => { it('should alter the options but not overwrite defaults', () => { const testWare1 = TestWare([], [{ key: 'planet', val: 'mars' }]); - const swapi = createNetworkInterface({ uri: swapiUrl }); + const swapi = createNetworkInterface({ uri: swapiUrl, fetch: fetchMock }); swapi.use([testWare1]); // this is a stub for the end user client api const simpleRequest = { @@ -299,6 +307,7 @@ describe('network interface', () => { const swapi = createNetworkInterface({ uri: 'http://graphql-swapi.test/', + fetch: fetchMock, }); swapi.use([testWare1]); const simpleRequest = { @@ -327,6 +336,7 @@ describe('network interface', () => { const swapi = createNetworkInterface({ uri: 'http://graphql-swapi.test/', + fetch: fetchMock, }); swapi.use([testWare1, testWare2]); // this is a stub for the end user client api @@ -346,7 +356,7 @@ describe('network interface', () => { const testWare1 = TestWare([{ key: 'personNum', val: 1 }]); const testWare2 = TestWare([{ key: 'filmNum', val: 1 }]); - const swapi = createNetworkInterface({ uri: swapiUrl }); + const swapi = createNetworkInterface({ uri: swapiUrl, fetch: fetchMock }); swapi.use([testWare1]).use([testWare2]); const simpleRequest = { query: complexQueryWithTwoVars, @@ -373,7 +383,10 @@ describe('network interface', () => { describe('afterware', () => { it('should return errors thrown in afterwares', () => { - const networkInterface = createNetworkInterface({ uri: swapiUrl }); + const networkInterface = createNetworkInterface({ + uri: swapiUrl, + fetch: fetchMock, + }); networkInterface.useAfter([ { applyAfterware() { @@ -459,7 +472,7 @@ describe('network interface', () => { }; it('should fetch remote data', () => { - const swapi = createNetworkInterface({ uri: swapiUrl }); + const swapi = createNetworkInterface({ uri: swapiUrl, fetch: fetchMock }); // this is a stub for the end user client api const simpleRequest = { @@ -483,6 +496,7 @@ describe('network interface', () => { it('should throw an error with the response when request is forbidden', () => { const unauthorizedInterface = createNetworkInterface({ uri: unauthorizedUrl, + fetch: fetchMock, }); return unauthorizedInterface.query(doomedToFail).catch(err => { @@ -498,6 +512,7 @@ describe('network interface', () => { it('should throw an error with the response when service is unavailable', () => { const unauthorizedInterface = createNetworkInterface({ uri: serviceUnavailableUrl, + fetch: fetchMock, }); return unauthorizedInterface.query(doomedToFail).catch(err => { @@ -513,7 +528,7 @@ describe('network interface', () => { describe('transforming queries', () => { it('should remove the @connection directive', () => { - const swapi = createNetworkInterface({ uri: swapiUrl }); + const swapi = createNetworkInterface({ uri: swapiUrl, fetch: fetchMock }); const simpleRequestWithConnection = { query: simpleQueryWithConnection, @@ -528,7 +543,7 @@ describe('network interface', () => { }); it('should remove the @connection directive even with no key but warn the user', () => { - const swapi = createNetworkInterface({ uri: swapiUrl }); + const swapi = createNetworkInterface({ uri: swapiUrl, fetch: fetchMock }); const simpleRequestWithConnectionButNoKey = { query: simpleQueryWithConnectionButNoKey, diff --git a/test/tests.ts b/test/tests.ts index 9048229ddfe..391d9d08b7e 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -6,9 +6,8 @@ /// /// -// ensure support for fetch and promise +// ensure support for promise import 'es6-promise'; -import 'isomorphic-fetch'; import { QueryManager } from '../src/core/QueryManager'; diff --git a/tsconfig.json b/tsconfig.json index 68d5e8312d5..319fd8806f6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "node_modules/typescript/lib/lib.es2015.d.ts", "node_modules/typescript/lib/lib.dom.d.ts", "typings.d.ts", + "fetch-ponyfill.typings.d.ts", "fetch-mock.typings.d.ts", "src/index.ts", "test/tests.ts",