Repository: WP-API/node-wpapi (js)

Issue:
V1.2.0 does not work with React-Native
After updating `wpapi` from `1.1.2` to `1.2.0`, my RN app does not run anymore and the packager throw following error:

```bundling failed: Error: Unable to resolve module `li` from `PROJECT_PATH/node_modules/wpapi/lib/autodiscovery.js`: Module `li` does not exist in the Haste module map```

As packager suggested, I did remove the `node_modules` folder, clean the cache and reinstall node modules and didn't help.
But running `yarn add li` and restarting the packager did help and fixed the issue.
However I don't see such note on this repo.

So either it's a bug in `1.2.0` or the readme file should be updated.

FYI, pretty much the same issue was happening with `url` module with version `1.1.2`. So with every new version of `node-wpapi` some dependencies are missing out of box 😄!

Pull request:
Fix missing module `li`
Fix #414

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 457a307..855b33d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,9 @@
 # Changelog
 
-## v2.0.0 _Second Toughest in the Infants_
+## v2.0.0 [**alpha**] _Second Toughest in the Infants_
 
-- **BREAKING**: `wpapi` module no longer includes HTTP methods. Install superagent as a peer dependency and `require( 'wpapi/superagent' )` in order to make HTTP requests.
+- **BREAKING**: The module exported as `wpapi` no longer includes HTTP methods. Install `superagent` as a peer dependency and `require( 'wpapi/superagent' )` in order to make HTTP requests.
+- **BREAKING**: Autodiscovery now either succeeds or fails; a WPAPI instance configured with default routes will no longer be returned.
 
 
 ## v1.2.0 _Space Is Only Noise_
diff --git a/lib/autodiscovery.js b/lib/autodiscovery.js
deleted file mode 100644
index 67c6862..0000000
--- a/lib/autodiscovery.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Utility methods used when querying a site in order to discover its available
- * API endpoints
- *
- * @module autodiscovery
- */
-'use strict';
-
-const parseLinkHeader = require( 'li' ).parse;
-
-/**
- * Attempt to locate a `rel="https://api.w.org"` link relation header
- *
- * @method locateAPIRootHeader
- * @param {Object} response A response object with a link or headers property
- * @returns {String} The URL of the located API root
- */
-function locateAPIRootHeader( response ) {
-	// See https://developer.wordpress.org/rest-api/using-the-rest-api/discovery/
-	const rel = 'https://api.w.org/';
-
-	// Extract & parse the response link headers
-	const link = response.link || ( response.headers && response.headers.link );
-	const headers = parseLinkHeader( link );
-
-	const apiHeader = headers && headers[ rel ];
-
-	if ( apiHeader ) {
-		return apiHeader;
-	}
-
-	throw new Error( `No header link found with rel="${ rel }"` );
-}
-
-module.exports = {
-	locateAPIRootHeader: locateAPIRootHeader,
-};
diff --git a/package.json b/package.json
index f021a5d..9f91dbf 100644
--- a/package.json
+++ b/package.json
@@ -59,6 +59,7 @@
     "test": "jest --coverage"
   },
   "dependencies": {
+    "li": "^1.3.0",
     "parse-link-header": "^1.0.1",
     "qs": "^6.6.0"
   },
@@ -80,7 +81,6 @@
     "jest": "^23.6.0",
     "jsdoc": "^3.4.3",
     "kramed": "^0.5.6",
-    "li": "^1.3.0",
     "load-grunt-tasks": "^3.5.0",
     "lodash.reduce": "^4.6.0",
     "minami": "^1.2.3",
diff --git a/wpapi.js b/wpapi.js
index 7859d0d..7e8d10f 100644
--- a/wpapi.js
+++ b/wpapi.js
@@ -30,9 +30,6 @@ let defaultEndpointFactories;
 // Constant used to detect first-party WordPress REST API routes
 const apiDefaultNamespace = 'wp/v2';
 
-// Pull in autodiscovery methods
-const autodiscovery = require( './lib/autodiscovery' );
-
 // Pull in base module constructors
 const WPRequest = require( './lib/constructors/wp-request' );
 
@@ -410,46 +407,17 @@ WPAPI.prototype.namespace = function( namespace ) {
  * library is unable to parse the provided endpoint.
  */
 WPAPI.discover = ( url ) => {
-	// local placeholder for API root URL
-	let endpoint;
-
-	// Try HEAD request first, for smaller payload: use WPAPI.site to produce
-	// a request that utilizes the defined HTTP transports
-	const req = WPAPI.site( url ).root();
-	return req.headers()
-		.catch( () => {
-			// On the hypothesis that any error here is related to the HEAD request
-			// failing, provisionally try again using GET because that method is
-			// more widely supported
-			return req.get();
-		} )
-		// Inspect response to find API location header
-		.then( autodiscovery.locateAPIRootHeader )
-		.then( ( apiRootURL ) => {
-			// Set the function-scope variable that will be used to instantiate
-			// the bound WPAPI instance,
-			endpoint = apiRootURL;
-
-			// then GET the API root JSON object
-			return WPAPI.site( apiRootURL ).root().get();
-		} )
-		.then( ( apiRootJSON ) => {
-			// Instantiate & bootstrap with the discovered methods
-			return new WPAPI( {
-				endpoint: endpoint,
-				routes: apiRootJSON.routes,
-			} );
-		} )
-		.catch( ( err ) => {
-			/* eslint-disable no-console */
-			console.error( err );
-			if ( endpoint ) {
-				console.warn( 'Endpoint detected, proceeding despite error...' );
-				console.warn( 'Binding to ' + endpoint + ' and assuming default routes' );
-				return new WPAPI.site( endpoint );
-			}
-			throw new Error( 'Autodiscovery failed' );
+	// Use WPAPI.site to make a request using the defined transport
+	const req = WPAPI.site( url ).root().param( 'rest_route', '/' );
+	return req.get().then( ( apiRootJSON ) => {
+		const routes = apiRootJSON.routes;
+		return new WPAPI( {
+			// Derive the endpoint from the self link for the / root
+			endpoint: routes['/']._links.self,
+			// Bootstrap returned WPAPI instance with the discovered routes
+			routes: routes,
 		} );
+	} );
 };
 
 module.exports = WPAPI;


Test patch:
diff --git a/superagent/tests/unit/superagent.js b/superagent/tests/unit/superagent.js
index 80c3f6b..e1339b6 100644
--- a/superagent/tests/unit/superagent.js
+++ b/superagent/tests/unit/superagent.js
@@ -106,51 +106,12 @@ describe( 'WPAPI', () => {
 	} );
 
 	describe( '.discover() constructor method', () => {
-		let responses;
 
 		beforeEach( () => {
-			responses = {
-				head: {},
-				get: {},
-			};
-			responses.head.withLink = {
-				'content-type': 'text/html; charset=UTF-8',
-				link: '<http://mozarts.house/wp-json/>; rel="https://api.w.org/"',
-			};
-			responses.head.withoutLink = {
-				'content-type': 'text/html; charset=utf-8',
-			};
-			responses.get.withLink = {
-				headers: {
-					'content-type': 'text/html; charset=UTF-8',
-					link: '<http://mozarts.house/wp-json/>; rel="https://api.w.org/"',
-				},
-			};
-			responses.get.withoutLink = {
-				headers: {
-					'content-type': 'text/html; charset=UTF-8',
-				},
-			};
-			responses.apiRoot = {
-				name: 'Skip Beats',
-				descrition: 'Just another WordPress weblog',
-				routes: {
-					'list': {},
-					'of': {},
-					'routes': {},
-				},
-			};
-			// Stub HTTP methods
-			jest.spyOn( httpTransport, 'head' ).mockImplementation( () => {} );
 			jest.spyOn( httpTransport, 'get' ).mockImplementation( () => {} );
-			// Stub warn and error
-			jest.spyOn( global.console, 'warn' ).mockImplementation( () => {} );
-			jest.spyOn( global.console, 'error' ).mockImplementation( () => {} );
 		} );
 
 		afterEach( () => {
-			// Restore HTTP methods
-			httpTransport.head.mockRestore();
 			httpTransport.get.mockRestore();
 		} );
 
@@ -159,93 +120,40 @@ describe( 'WPAPI', () => {
 			expect( typeof WPAPI.discover ).toBe( 'function' );
 		} );
 
-		it( 'throws an error if no API endpoint can be discovered', () => {
-			const url = 'http://we.made.it/to/mozarts/house';
-			httpTransport.head.mockImplementationOnce( () => Promise.reject() );
-			httpTransport.get.mockImplementationOnce( () => Promise.reject( 'Some error' ) );
-			const prom = WPAPI.discover( url )
-				.catch( ( err ) => {
-					expect( global.console.error ).toHaveBeenCalledWith( 'Some error' );
-					expect( err.message ).toBe( 'Autodiscovery failed' );
-					return SUCCESS;
-				} );
-			return expect( prom ).resolves.toBe( SUCCESS );
-		} );
-
-		it( 'discovers the API root with a HEAD request', () => {
-			const url = 'http://mozarts.house';
-			httpTransport.head.mockImplementation( () => Promise.resolve( responses.head.withLink ) );
-			httpTransport.get.mockImplementation( () => Promise.resolve( responses.apiRoot ) );
-			const prom = WPAPI.discover( url )
-				.then( ( result ) => {
-					expect( result ).toBeInstanceOf( WPAPI );
-					expect( httpTransport.head ).toBeCalledTimes( 1 );
-					expect( httpTransport.get ).toBeCalledTimes( 1 );
-					expect( result.root().toString() ).toBe( 'http://mozarts.house/wp-json/' );
-					return SUCCESS;
-				} );
-			return expect( prom ).resolves.toBe( SUCCESS );
-		} );
-
-		it( 'throws an error if HEAD succeeds but no link is present', () => {
-			const url = 'http://we.made.it/to/mozarts/house';
-			httpTransport.head.mockImplementationOnce( () => Promise.resolve( responses.head.withoutLink ) );
-			const prom = WPAPI.discover( url )
-				.catch( ( err ) => {
-					expect( global.console.error ).toHaveBeenCalledWith(
-						new Error( 'No header link found with rel="https://api.w.org/"' )
-					);
-					expect( err.message ).toBe( 'Autodiscovery failed' );
-					return SUCCESS;
-				} );
-			return expect( prom ).resolves.toBe( SUCCESS );
-		} );
-
-		it( 'retries the initial site request as a GET if HEAD fails', () => {
+		it( 'discovers the API root with a GET request', () => {
 			const url = 'http://mozarts.house';
-			httpTransport.head.mockImplementation( () => Promise.reject() );
-			httpTransport.get.mockImplementationOnce( () => Promise.resolve( responses.get.withLink ) );
-			httpTransport.get.mockImplementationOnce( () => Promise.resolve( responses.apiRoot ) );
+			httpTransport.get.mockImplementation( () => Promise.resolve( {
+				name: 'Skip Beats',
+				descrition: 'Just another WordPress weblog',
+				routes: {
+					'/': {
+						_links: {
+							self: 'http://mozarts.house/wp-json/',
+						},
+					},
+					'list': {},
+					'of': {},
+					'routes': {},
+				},
+			} ) );
 			const prom = WPAPI.discover( url )
 				.then( ( result ) => {
 					expect( result ).toBeInstanceOf( WPAPI );
-					expect( httpTransport.head ).toBeCalledTimes( 1 );
-					expect( httpTransport.get ).toBeCalledTimes( 2 );
 					expect( result.root().toString() ).toBe( 'http://mozarts.house/wp-json/' );
+					expect( httpTransport.get ).toBeCalledTimes( 1 );
+					const indexRequestObject = httpTransport.get.mock.calls[0][0];
+					expect( indexRequestObject.toString() ).toBe( 'http://mozarts.house/?rest_route=%2F' );
 					return SUCCESS;
 				} );
 			return expect( prom ).resolves.toBe( SUCCESS );
 		} );
 
-		it( 'throws an error if GET retry succeeds but no link is present', () => {
+		it( 'throws an error if no API endpoint can be discovered', () => {
 			const url = 'http://we.made.it/to/mozarts/house';
-			httpTransport.head.mockImplementation( () => Promise.reject() );
-			httpTransport.get.mockImplementationOnce( () => Promise.resolve( responses.get.withoutLink ) );
+			httpTransport.get.mockImplementationOnce( () => Promise.reject( 'Some error' ) );
 			const prom = WPAPI.discover( url )
 				.catch( ( err ) => {
-					expect( global.console.error ).toHaveBeenCalledWith(
-						new Error( 'No header link found with rel="https://api.w.org/"' )
-					);
-					expect( err.message ).toBe( 'Autodiscovery failed' );
-					return SUCCESS;
-				} );
-			return expect( prom ).resolves.toBe( SUCCESS );
-		} );
-
-		it( 'returns WPAPI instance bound to discovered root even when route request errors', () => {
-			const url = 'http://mozarts.house';
-			httpTransport.head.mockImplementation( () => Promise.reject() );
-			httpTransport.get
-				.mockImplementationOnce( () => Promise.resolve( responses.get.withLink ) )
-				.mockImplementationOnce( () => Promise.reject( 'Some error' ) );
-			const prom = WPAPI.discover( url )
-				.then( ( result ) => {
-					expect( result ).toBeInstanceOf( WPAPI );
-					expect( httpTransport.head ).toBeCalledTimes( 1 );
-					expect( httpTransport.get ).toBeCalledTimes( 2 );
-					expect( global.console.error ).toHaveBeenCalledWith( 'Some error' );
-					expect( global.console.warn ).toHaveBeenCalledWith( 'Endpoint detected, proceeding despite error...' );
-					expect( result.root().toString() ).toBe( 'http://mozarts.house/wp-json/' );
+					expect( err ).toBe( 'Some error' );
 					return SUCCESS;
 				} );
 			return expect( prom ).resolves.toBe( SUCCESS );
diff --git a/tests/integration/autodiscovery.js b/tests/integration/autodiscovery.js
index 8e781be..9e38aad 100644
--- a/tests/integration/autodiscovery.js
+++ b/tests/integration/autodiscovery.js
@@ -53,6 +53,24 @@ describe.each( [
 		return expect( prom ).resolves.toBe( SUCCESS );
 	} );
 
+	it( 'resolves to correct endpoint if discovery targets /wp-json', () => {
+		const prom = WPAPI.discover( 'http://wpapi.local/wp-json' )
+			.then( ( site ) => {
+				expect( site.posts().toString() ).toBe( 'http://wpapi.local/wp-json/wp/v2/posts' );
+				return SUCCESS;
+			} );
+		return expect( prom ).resolves.toBe( SUCCESS );
+	} );
+
+	it( 'resolves to correct endpoint if discovery targets ?rest_route=/', () => {
+		const prom = WPAPI.discover( 'http://wpapi.local/?rest_route=/' )
+			.then( ( site ) => {
+				expect( site.posts().toString() ).toBe( 'http://wpapi.local/wp-json/wp/v2/posts' );
+				return SUCCESS;
+			} );
+		return expect( prom ).resolves.toBe( SUCCESS );
+	} );
+
 	it( 'can correctly instantiate requests against the detected and bound site', () => {
 		const prom = apiPromise
 			.then( site => site.posts() )
diff --git a/tests/unit/lib/autodiscovery.js b/tests/unit/lib/autodiscovery.js
deleted file mode 100644
index 9bbcf89..0000000
--- a/tests/unit/lib/autodiscovery.js
+++ /dev/null
@@ -1,37 +0,0 @@
-'use strict';
-
-const autodiscovery = require( '../../../lib/autodiscovery' );
-
-describe( 'autodiscovery helper methods', () => {
-
-	describe( '.locateAPIRootHeader()', () => {
-		let locateAPIRootHeader;
-
-		beforeEach( () => {
-			locateAPIRootHeader = autodiscovery.locateAPIRootHeader;
-		} );
-
-		it( 'is a function', () => {
-			expect( typeof locateAPIRootHeader ).toBe( 'function' );
-		} );
-
-		it( 'throws an error if no link header is found', () => {
-			expect( () => {
-				locateAPIRootHeader( {
-					headers: {},
-				} );
-			} ).toThrow( 'No header link found with rel="https://api.w.org/"' );
-		} );
-
-		it( 'parsed and returns the header with the rel for the REST API endpoint', () => {
-			const result = locateAPIRootHeader( {
-				headers: {
-					link: '<http://wpapi.local/wp-json/>; rel="https://api.w.org/"',
-				},
-			} );
-			expect( result ).toBe( 'http://wpapi.local/wp-json/' );
-		} );
-
-	} );
-
-} );