From 8958894be6e11e0f162237701bf856bf7a50b66d Mon Sep 17 00:00:00 2001 From: sarayourfriend Date: Thu, 25 Mar 2021 09:08:39 -0700 Subject: [PATCH] api-fetch: Type the rest of the package (#30161) * api-fetch: Type the rest of the package * Update CHANGELOG * Undo spread operator * Fall back to window.location * Use location.href instead of toString and fix comments --- packages/api-fetch/CHANGELOG.md | 2 + packages/api-fetch/src/index.js | 71 ++++++++++++++----- .../src/middlewares/fetch-all-middleware.js | 43 ++++++++--- packages/api-fetch/src/middlewares/nonce.js | 2 +- packages/api-fetch/tsconfig.json | 14 ++-- 5 files changed, 97 insertions(+), 35 deletions(-) diff --git a/packages/api-fetch/CHANGELOG.md b/packages/api-fetch/CHANGELOG.md index 2057f7c78084ed..9d9dc4330caf50 100644 --- a/packages/api-fetch/CHANGELOG.md +++ b/packages/api-fetch/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Publish TypeScript definitions. + ## 3.22.0 (2021-03-17) ## 3.8.1 (2019-04-22) diff --git a/packages/api-fetch/src/index.js b/packages/api-fetch/src/index.js index a77a6295637a96..2b2c6b3508defe 100644 --- a/packages/api-fetch/src/index.js +++ b/packages/api-fetch/src/index.js @@ -23,7 +23,7 @@ import { * Default set of header values which should be sent with every request unless * explicitly provided through apiFetch options. * - * @type {Object} + * @type {Record} */ const DEFAULT_HEADERS = { // The backend uses the Accept header as a condition for considering an @@ -43,6 +43,9 @@ const DEFAULT_OPTIONS = { credentials: 'include', }; +/** + * @type {import('./types').ApiFetchMiddleware[]} + */ const middlewares = [ userLocaleMiddleware, namespaceEndpointMiddleware, @@ -50,10 +53,22 @@ const middlewares = [ fetchAllMiddleware, ]; +/** + * Register a middleware + * + * @param {import('./types').ApiFetchMiddleware} middleware + */ function registerMiddleware( middleware ) { middlewares.unshift( middleware ); } +/** + * Checks the status of a response, throwing the Response as an error if + * it is outside the 200 range. + * + * @param {Response} response + * @return {Response} The response if the status is in the 200 range. + */ const checkStatus = ( response ) => { if ( response.status >= 200 && response.status < 300 ) { return response; @@ -62,6 +77,11 @@ const checkStatus = ( response ) => { throw response; }; +/** @typedef {(options: import('./types').ApiFetchRequestProps) => Promise} FetchHandler*/ + +/** + * @type {FetchHandler} + */ const defaultFetchHandler = ( nextOptions ) => { const { url, path, data, parse = true, ...remainingOptions } = nextOptions; let { body, headers } = nextOptions; @@ -75,12 +95,16 @@ const defaultFetchHandler = ( nextOptions ) => { headers[ 'Content-Type' ] = 'application/json'; } - const responsePromise = window.fetch( url || path, { - ...DEFAULT_OPTIONS, - ...remainingOptions, - body, - headers, - } ); + const responsePromise = window.fetch( + // fall back to explicitly passing `window.location` which is the behavior if `undefined` is passed + url || path || window.location.href, + { + ...DEFAULT_OPTIONS, + ...remainingOptions, + body, + headers, + } + ); return ( responsePromise @@ -107,25 +131,34 @@ const defaultFetchHandler = ( nextOptions ) => { ); }; +/** @type {FetchHandler} */ let fetchHandler = defaultFetchHandler; /** * Defines a custom fetch handler for making the requests that will override * the default one using window.fetch * - * @param {Function} newFetchHandler The new fetch handler + * @param {FetchHandler} newFetchHandler The new fetch handler */ function setFetchHandler( newFetchHandler ) { fetchHandler = newFetchHandler; } +/** + * @template T + * @param {import('./types').ApiFetchRequestProps} options + * @return {Promise} A promise representing the request processed via the registered middlewares. + */ function apiFetch( options ) { // creates a nested function chain that calls all middlewares and finally the `fetchHandler`, // converting `middlewares = [ m1, m2, m3 ]` into: // ``` // opts1 => m1( opts1, opts2 => m2( opts2, opts3 => m3( opts3, fetchHandler ) ) ); // ``` - const enhancedHandler = middlewares.reduceRight( ( next, middleware ) => { + const enhancedHandler = middlewares.reduceRight( ( + /** @type {FetchHandler} */ next, + middleware + ) => { return ( workingOptions ) => middleware( workingOptions, next ); }, fetchHandler ); @@ -135,14 +168,18 @@ function apiFetch( options ) { } // If the nonce is invalid, refresh it and try again. - return window - .fetch( apiFetch.nonceEndpoint ) - .then( checkStatus ) - .then( ( data ) => data.text() ) - .then( ( text ) => { - apiFetch.nonceMiddleware.nonce = text; - return apiFetch( options ); - } ); + return ( + window + // @ts-ignore + .fetch( apiFetch.nonceEndpoint ) + .then( checkStatus ) + .then( ( data ) => data.text() ) + .then( ( text ) => { + // @ts-ignore + apiFetch.nonceMiddleware.nonce = text; + return apiFetch( options ); + } ) + ); } ); } diff --git a/packages/api-fetch/src/middlewares/fetch-all-middleware.js b/packages/api-fetch/src/middlewares/fetch-all-middleware.js index f062f8957f4fa8..a7779c9e925942 100644 --- a/packages/api-fetch/src/middlewares/fetch-all-middleware.js +++ b/packages/api-fetch/src/middlewares/fetch-all-middleware.js @@ -8,17 +8,32 @@ import { addQueryArgs } from '@wordpress/url'; */ import apiFetch from '..'; -// Apply query arguments to both URL and Path, whichever is present. +/** + * Apply query arguments to both URL and Path, whichever is present. + * + * @param {import('../types').ApiFetchRequestProps} props + * @param {Record} queryArgs + * @return {import('../types').ApiFetchRequestProps} The request with the modified query args + */ const modifyQuery = ( { path, url, ...options }, queryArgs ) => ( { ...options, url: url && addQueryArgs( url, queryArgs ), path: path && addQueryArgs( path, queryArgs ), } ); -// Duplicates parsing functionality from apiFetch. +/** + * Duplicates parsing functionality from apiFetch. + * + * @param {Response} response + * @return {Promise} Parsed response json. + */ const parseResponse = ( response ) => response.json ? response.json() : Promise.reject( response ); +/** + * @param {string | null} linkHeader + * @return {{ next?: string }} The parsed link header. + */ const parseLinkHeader = ( linkHeader ) => { if ( ! linkHeader ) { return {}; @@ -31,22 +46,34 @@ const parseLinkHeader = ( linkHeader ) => { : {}; }; +/** + * @param {Response} response + * @return {string | undefined} The next page URL. + */ const getNextPageUrl = ( response ) => { const { next } = parseLinkHeader( response.headers.get( 'link' ) ); return next; }; +/** + * @param {import('../types').ApiFetchRequestProps} options + * @return {boolean} True if the request contains an unbounded query. + */ const requestContainsUnboundedQuery = ( options ) => { const pathIsUnbounded = - options.path && options.path.indexOf( 'per_page=-1' ) !== -1; + !! options.path && options.path.indexOf( 'per_page=-1' ) !== -1; const urlIsUnbounded = - options.url && options.url.indexOf( 'per_page=-1' ) !== -1; + !! options.url && options.url.indexOf( 'per_page=-1' ) !== -1; return pathIsUnbounded || urlIsUnbounded; }; -// The REST API enforces an upper limit on the per_page option. To handle large -// collections, apiFetch consumers can pass `per_page=-1`; this middleware will -// then recursively assemble a full response array from all available pages. +/** + * The REST API enforces an upper limit on the per_page option. To handle large + * collections, apiFetch consumers can pass `per_page=-1`; this middleware will + * then recursively assemble a full response array from all available pages. + * + * @type {import('../types').ApiFetchMiddleware} + */ const fetchAllMiddleware = async ( options, next ) => { if ( options.parse === false ) { // If a consumer has opted out of parsing, do not apply middleware. @@ -81,7 +108,7 @@ const fetchAllMiddleware = async ( options, next ) => { } // Iteratively fetch all remaining pages until no "next" header is found. - let mergedResults = [].concat( results ); + let mergedResults = /** @type {any[]} */ ( [] ).concat( results ); while ( nextPage ) { const nextResponse = await apiFetch( { ...options, diff --git a/packages/api-fetch/src/middlewares/nonce.js b/packages/api-fetch/src/middlewares/nonce.js index 0f11f1729eed0e..703fb0dab981cf 100644 --- a/packages/api-fetch/src/middlewares/nonce.js +++ b/packages/api-fetch/src/middlewares/nonce.js @@ -1,6 +1,6 @@ /** * @param {string} nonce - * @return {import('../types').ApiFetchMiddleware} A middleware to enhance a request with a nonce. + * @return {import('../types').ApiFetchMiddleware & { nonce: string }} A middleware to enhance a request with a nonce. */ function createNonceMiddleware( nonce ) { /** diff --git a/packages/api-fetch/tsconfig.json b/packages/api-fetch/tsconfig.json index 3ad48264a46d28..744a19d13fa97e 100644 --- a/packages/api-fetch/tsconfig.json +++ b/packages/api-fetch/tsconfig.json @@ -5,17 +5,13 @@ "declarationDir": "build-types" }, "references": [ + { "path": "../i18n" }, { "path": "../url" } ], "include": [ - "src/middlewares/http-v1.js", - "src/middlewares/media-upload.js", - "src/middlewares/namespace-endpoint.js", - "src/middlewares/nonce.js", - "src/middlewares/preloading.js", - "src/middlewares/root-url.js", - "src/middlewares/user-locale.js", - "src/utils/**/*", - "src/types.ts" + "src/**/*", + ], + "exclude": [ + "**/test/**/*", ] }