Repository: WP-API/node-wpapi
Pull request #407 (2018-12-21 19:01:23)

Reduce bundle size
- Add Webpack Bundle Analyzer and a `npm run build:stats` package script
- Switch from `lodash.uniq` to custom modern JS `Set`-based `unique()` implementation
- Standardize on `li` library for link header parsing
- Remove unused dependencies
- Remove unneeded url transformation and `mergeUrl` method in http transport code (had no effect)
- Switch to `class` syntax instead of using `util.inherits`


Patch:
diff --git a/lib/autodiscovery.js b/lib/autodiscovery.js
index c45c091..67c6862 100644
--- a/lib/autodiscovery.js
+++ b/lib/autodiscovery.js
@@ -6,7 +6,7 @@
  */
 'use strict';
 
-const parseLinkHeader = require( 'parse-link-header' );
+const parseLinkHeader = require( 'li' ).parse;
 
 /**
  * Attempt to locate a `rel="https://api.w.org"` link relation header
@@ -16,19 +16,20 @@ const parseLinkHeader = require( 'parse-link-header' );
  * @returns {String} The URL of the located API root
  */
 function locateAPIRootHeader( response ) {
-	// Define the expected link rel value per http://v2.wp-api.org/guide/discovery/
+	// 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 && apiHeader.url ) {
-		return apiHeader.url;
+	if ( apiHeader ) {
+		return apiHeader;
 	}
 
-	throw new Error( 'No header link found with rel="https://api.w.org/"' );
+	throw new Error( `No header link found with rel="${ rel }"` );
 }
 
 module.exports = {
diff --git a/lib/constructors/wp-request.js b/lib/constructors/wp-request.js
index 61d75eb..d5d9cd0 100644
--- a/lib/constructors/wp-request.js
+++ b/lib/constructors/wp-request.js
@@ -1,12 +1,12 @@
 'use strict';
 
 const qs = require( 'qs' );
-const _unique = require( 'lodash.uniq' );
 
 const alphaNumericSort = require( '../util/alphanumeric-sort' );
 const keyValToObj = require( '../util/key-val-to-obj' );
 const paramSetter = require( '../util/parameter-setter' );
 const objectReduce = require( '../util/object-reduce' );
+const unique = require( '../util/unique' );
 
 /**
  * WPRequest is the base API request object constructor
@@ -401,7 +401,7 @@ WPRequest.prototype.param = function( props, value ) {
 
 		// Arrays should be de-duped and sorted
 		if ( Array.isArray( value ) ) {
-			value = _unique( value ).sort( alphaNumericSort );
+			value = unique( value ).sort( alphaNumericSort );
 		}
 
 		// Set the value
diff --git a/lib/endpoint-request.js b/lib/endpoint-request.js
index eb20b02..0b0950c 100644
--- a/lib/endpoint-request.js
+++ b/lib/endpoint-request.js
@@ -3,7 +3,6 @@
  */
 'use strict';
 
-const inherit = require( 'util' ).inherits;
 const WPRequest = require( './constructors/wp-request' );
 const mixins = require( './mixins' );
 
@@ -21,27 +20,27 @@ const applyMixin = require( './util/apply-mixin' );
 function createEndpointRequest( handlerSpec, resource, namespace ) {
 
 	// Create the constructor function for this endpoint
-	function EndpointRequest( options ) {
-		WPRequest.call( this, options );
+	class EndpointRequest extends WPRequest {
+		constructor( options ) {
+			super( options );
 
-		/**
-		 * Semi-private instance property specifying the available URL path options
-		 * for this endpoint request handler, keyed by ascending whole numbers.
-		 *
-		 * @property _levels
-		 * @type {object}
-		 * @private
-		 */
-		this._levels = handlerSpec._levels;
+			/**
+			 * Semi-private instance property specifying the available URL path options
+			 * for this endpoint request handler, keyed by ascending whole numbers.
+			 *
+			 * @property _levels
+			 * @type {object}
+			 * @private
+			 */
+			this._levels = handlerSpec._levels;
 
-		// Configure handler for this endpoint's root URL path & set namespace
-		this
-			.setPathPart( 0, resource )
-			.namespace( namespace );
+			// Configure handler for this endpoint's root URL path & set namespace
+			this
+				.setPathPart( 0, resource )
+				.namespace( namespace );
+		}
 	}
 
-	inherit( EndpointRequest, WPRequest );
-
 	// Mix in all available shortcut methods for GET request query parameters that
 	// are valid within this endpoint tree
 	if ( typeof handlerSpec._getArgs === 'object' ) {
diff --git a/lib/http-transport.js b/lib/http-transport.js
index 5e8bc52..af5e7fb 100644
--- a/lib/http-transport.js
+++ b/lib/http-transport.js
@@ -5,7 +5,6 @@
 
 const agent = require( 'superagent' );
 const parseLinkHeader = require( 'li' ).parse;
-const url = require( 'url' );
 
 const WPRequest = require( './constructors/wp-request' );
 const checkMethodSupport = require( './util/check-method-support' );
@@ -72,33 +71,6 @@ function _auth( request, options, forceAuthentication ) {
 // Pagination-Related Helpers
 // ==========================
 
-/**
- * Combine the API endpoint root URI and link URI into a valid request URL.
- * Endpoints are generally a full path to the JSON API's root endpoint, such
- * as `website.com/wp-json`: the link headers, however, are returned as root-
- * relative paths. Concatenating these would generate a URL such as
- * `website.com/wp-json/wp-json/posts?page=2`: we must intelligently merge the
- * URI strings in order to generate a valid new request URL.
- *
- * @private
- * @param endpoint {String} The endpoint URL for the REST API root
- * @param linkPath {String} A root-relative link path to an API request
- * @returns {String} The full URL path to the provided link
- */
-function mergeUrl( endpoint, linkPath ) {
-	const request = url.parse( endpoint );
-	linkPath = url.parse( linkPath, true );
-
-	// Overwrite relevant request URL object properties with the link's values:
-	// Setting these three values from the link will ensure proper URL generation
-	request.query = linkPath.query;
-	request.search = linkPath.search;
-	request.pathname = linkPath.pathname;
-
-	// Reassemble and return the merged URL
-	return url.format( request );
-}
-
 /**
  * Extract the body property from the superagent response, or else try to parse
  * the response text to get a JSON object.
@@ -161,7 +133,9 @@ function createPaginationObject( result, options, httpTransport ) {
 	}
 
 	// Decode the link header object
-	const links = result.headers.link ? parseLinkHeader( result.headers.link ) : {};
+	const links = result.headers.link ?
+		parseLinkHeader( result.headers.link ) :
+		{};
 
 	// Store pagination data from response headers on the response collection
 	_paging = {
@@ -170,16 +144,12 @@ function createPaginationObject( result, options, httpTransport ) {
 		links: links,
 	};
 
-	// Re-use any options from the original request, updating only the endpoint
-	// (this ensures that request properties like authentication are preserved)
-	const endpoint = options.endpoint;
-
 	// Create a WPRequest instance pre-bound to the "next" page, if available
 	if ( links.next ) {
 		_paging.next = new WPRequest( {
 			...options,
 			transport: httpTransport,
-			endpoint: mergeUrl( endpoint, links.next ),
+			endpoint: links.next,
 		} );
 	}
 
@@ -188,7 +158,7 @@ function createPaginationObject( result, options, httpTransport ) {
 		_paging.prev = new WPRequest( {
 			...options,
 			transport: httpTransport,
-			endpoint: mergeUrl( endpoint, links.prev ),
+			endpoint: links.prev,
 		} );
 	}
 
diff --git a/lib/mixins/filters.js b/lib/mixins/filters.js
index dacc722..50c61de 100644
--- a/lib/mixins/filters.js
+++ b/lib/mixins/filters.js
@@ -3,10 +3,9 @@
  */
 'use strict';
 
-const _unique = require( 'lodash.uniq' );
-
 const alphaNumericSort = require( '../util/alphanumeric-sort' );
 const keyValToObj = require( '../util/key-val-to-obj' );
+const unique = require( '../util/unique' );
 
 /**
  * Filter methods that can be mixed in to a request constructor's prototype to
@@ -120,7 +119,7 @@ filterMixins.taxonomy = function( taxonomy, term ) {
 		.sort( alphaNumericSort );
 
 	// De-dupe
-	this._taxonomyFilters[ taxonomy ] = _unique( taxonomyTerms, true );
+	this._taxonomyFilters[ taxonomy ] = unique( taxonomyTerms, true );
 
 	return this;
 };
diff --git a/lib/util/unique.js b/lib/util/unique.js
new file mode 100644
index 0000000..886c0c7
--- /dev/null
+++ b/lib/util/unique.js
@@ -0,0 +1,10 @@
+/**
+ * Return an array with all duplicate items removed.
+ *
+ * This functionality was previously provided by lodash.uniq, but this
+ * modern JS solution yields a smaller bundle size.
+ *
+ * @param {Array} arr An array to de-duplicate
+ * @returns {Array} A de-duplicated array
+ */
+module.exports = arr => Array.from( new Set( arr ) );
diff --git a/package.json b/package.json
index 9a00981..f386de0 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
   },
   "scripts": {
     "build": "webpack && webpack --config webpack.config.minified.js",
+    "build:stats": "webpack && webpack --config webpack.config.minified.js --stats",
     "zip": "npm run build && grunt zip",
     "update-default-routes-json": "node build/scripts/update-default-routes-json",
     "predocs": "rimraf documentation/api-reference",
@@ -58,12 +59,9 @@
     "test": "jest --coverage"
   },
   "dependencies": {
-    "li": "^1.0.1",
-    "lodash.uniq": "^4.3.0",
-    "parse-link-header": "^0.4.1",
-    "qs": "^6.2.0",
-    "route-parser": "0.0.4",
-    "superagent": "^3.3.1"
+    "parse-link-header": "^1.0.1",
+    "qs": "^6.6.0",
+    "superagent": "^4.0.0"
   },
   "devDependencies": {
     "@babel/core": "^7.2.2",
@@ -80,6 +78,7 @@
     "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",
@@ -87,6 +86,7 @@
     "prompt": "^1.0.0",
     "rimraf": "^2.6.1",
     "webpack": "^4.28.1",
+    "webpack-bundle-analyzer": "^3.0.3",
     "webpack-cli": "^3.1.2"
   }
 }
diff --git a/webpack.config.minified.js b/webpack.config.minified.js
index 43675b9..62ce81d 100644
--- a/webpack.config.minified.js
+++ b/webpack.config.minified.js
@@ -1,5 +1,7 @@
 'use strict';
 
+const { BundleAnalyzerPlugin } = require( 'webpack-bundle-analyzer' );
+
 const config = require( './webpack.config' );
 
 // Re-use normal Webpack build config, just adding minification
@@ -18,4 +20,13 @@ module.exports = {
 	optimization: {
 		noEmitOnErrors: true,
 	},
+
+	plugins: [
+		...( config.plugins || [] ),
+	],
 };
+
+// Conditionally opt-in to stats reporting UI.
+if ( process.argv.includes( '--stats' ) ) {
+	module.exports.plugins.push( new BundleAnalyzerPlugin() );
+}


Test patch:
diff --git a/tests/integration/autodiscovery.js b/tests/integration/autodiscovery.js
index 7505fe9..ff77726 100644
--- a/tests/integration/autodiscovery.js
+++ b/tests/integration/autodiscovery.js
@@ -17,16 +17,14 @@ const expectedResults = {
 	firstPostTitle: 'Markup: HTML Tags and Formatting',
 };
 
-const noop = () => {};
-
 describe( 'integration: discover()', () => {
 	let apiPromise;
 
 	beforeAll( () => {
 		apiPromise = WPAPI.discover( 'http://wpapi.local' );
 		// Stub warn and error
-		jest.spyOn( global.console, 'warn' ).mockImplementation( noop );
-		jest.spyOn( global.console, 'error' ).mockImplementation( noop );
+		jest.spyOn( global.console, 'warn' ).mockImplementation( () => {} );
+		jest.spyOn( global.console, 'error' ).mockImplementation( () => {} );
 	} );
 
 	it( 'returns a promise', () => {
diff --git a/tests/integration/media.js b/tests/integration/media.js
index 4d32243..3a7848e 100644
--- a/tests/integration/media.js
+++ b/tests/integration/media.js
@@ -1,9 +1,9 @@
 'use strict';
 
 const path = require( 'path' );
-const _unique = require( 'lodash.uniq' );
 const objectReduce = require( '../../lib/util/object-reduce' );
 const httpTestUtils = require( '../helpers/http-test-utils' );
+const unique = require( '../../lib/util/unique' );
 
 const WPAPI = require( '../../' );
 const WPRequest = require( '../../lib/constructors/wp-request.js' );
@@ -307,7 +307,7 @@ describe( 'integration: media()', () => {
 				const sizeURLs = objectReduce( sizes, ( urls, size ) => urls.concat( size.source_url ), [] );
 
 				// Expect all sizes to have different URLs
-				expect( Object.keys( sizes ).length ).toBe( _unique( sizeURLs ).length );
+				expect( Object.keys( sizes ).length ).toBe( unique( sizeURLs ).length );
 				return sizeURLs.reduce( ( previous, sizeURL ) => previous.then( () => (
 					httpTestUtils.expectStatusCode( sizeURL, 200 )
 				) ), Promise.resolve() );
diff --git a/tests/unit/lib/constructors/wp-request.js b/tests/unit/lib/constructors/wp-request.js
index 4c0738d..7af28c9 100644
--- a/tests/unit/lib/constructors/wp-request.js
+++ b/tests/unit/lib/constructors/wp-request.js
@@ -4,6 +4,13 @@ const WPRequest = require( '../../../../lib/constructors/wp-request' );
 const filterMixins = require( '../../../../lib/mixins/filters' );
 const checkMethodSupport = require( '../../../../lib/util/check-method-support' );
 
+const getQueryStr = ( req ) => {
+	const query = req
+		._renderQuery()
+		.replace( /^\?/, '' );
+	return decodeURIComponent( query );
+};
+
 describe( 'WPRequest', () => {
 
 	let request;
@@ -205,16 +212,6 @@ describe( 'WPRequest', () => {
 	} );
 
 	describe( '.param() convenience methods', () => {
-		let getQueryStr;
-
-		beforeEach( () => {
-			getQueryStr = ( req ) => {
-				const query = req
-					._renderQuery()
-					.replace( /^\?/, '' );
-				return decodeURIComponent( query );
-			};
-		} );
 
 		describe( '.context()', () => {
 
diff --git a/tests/unit/lib/mixins/filters.js b/tests/unit/lib/mixins/filters.js
index cdfbd88..f51f2fe 100644
--- a/tests/unit/lib/mixins/filters.js
+++ b/tests/unit/lib/mixins/filters.js
@@ -1,29 +1,22 @@
 'use strict';
 
-const inherit = require( 'util' ).inherits;
-
 const filterMixins = require( '../../../../lib/mixins/filters' );
 const WPRequest = require( '../../../../lib/constructors/wp-request' );
 
+const getQueryStr = ( req ) => {
+	const query = req
+		._renderQuery()
+		.replace( /^\?/, '' );
+	return decodeURIComponent( query );
+};
+
 describe( 'mixins: filter', () => {
 	let Req;
 	let req;
-	let getQueryStr;
 
 	beforeEach( () => {
-		Req = function() {
-			WPRequest.apply( this, arguments );
-		};
-		inherit( Req, WPRequest );
-
+		Req = class extends WPRequest {};
 		req = new Req();
-
-		getQueryStr = ( req ) => {
-			const query = req
-				._renderQuery()
-				.replace( /^\?/, '' );
-			return decodeURIComponent( query );
-		};
 	} );
 
 	describe( '.filter()', () => {
diff --git a/tests/unit/lib/mixins/parameters.js b/tests/unit/lib/mixins/parameters.js
index 1b95cfa..bbe0d9e 100644
--- a/tests/unit/lib/mixins/parameters.js
+++ b/tests/unit/lib/mixins/parameters.js
@@ -1,29 +1,22 @@
 'use strict';
 
-const inherit = require( 'util' ).inherits;
-
 const parameterMixins = require( '../../../../lib/mixins/parameters' );
 const WPRequest = require( '../../../../lib/constructors/wp-request' );
 
+const getQueryStr = ( req ) => {
+	const query = req
+		._renderQuery()
+		.replace( /^\?/, '' );
+	return decodeURIComponent( query );
+};
+
 describe( 'mixins: parameters', () => {
 	let Req;
 	let req;
-	let getQueryStr;
 
 	beforeEach( () => {
-		Req = function() {
-			WPRequest.apply( this, arguments );
-		};
-		inherit( Req, WPRequest );
-
+		Req = class extends WPRequest {};
 		req = new Req();
-
-		getQueryStr = ( req ) => {
-			const query = req
-				._renderQuery()
-				.replace( /^\?/, '' );
-			return decodeURIComponent( query );
-		};
 	} );
 
 	describe( 'date parameters', () => {
diff --git a/tests/unit/lib/util/unique.js b/tests/unit/lib/util/unique.js
new file mode 100644
index 0000000..e568396
--- /dev/null
+++ b/tests/unit/lib/util/unique.js
@@ -0,0 +1,27 @@
+const unique = require( '../../../../lib/util/unique' );
+
+describe( 'unique', () => {
+
+	it( 'is a function', () => {
+		expect( typeof unique ).toBe( 'function' );
+	} );
+
+	it( 'returns an array unchanged if there are no repeated items', () => {
+		expect( unique( [ 1, 2, 3, 4 ] ) ).toEqual( [ 1, 2, 3, 4 ] );
+		expect( unique( [ 'a', 'b', 'c' ] ) ).toEqual( [ 'a', 'b', 'c' ] );
+		const obj1 = {};
+		const obj2 = {};
+		const obj3 = {};
+		expect( unique( [ obj1, obj2, obj3 ] ) ).toEqual( [ obj1, obj2, obj3 ] );
+	} );
+
+	it( 'returns an array with duplicated items removed', () => {
+		expect( unique( [ 1, 2, 3, 4, 1, 2 ] ) ).toEqual( [ 1, 2, 3, 4 ] );
+		expect( unique( [ 'a', 'b', 'c', 'a', 'b' ] ) ).toEqual( [ 'a', 'b', 'c' ] );
+		const obj1 = {};
+		const obj2 = {};
+		const obj3 = {};
+		expect( unique( [ obj1, obj2, obj1, obj3, obj2 ] ) ).toEqual( [ obj1, obj2, obj3 ] );
+	} );
+
+} );