diff --git a/package-lock.json b/package-lock.json index 023580f7..67634d44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-storefront", - "version": "7.5.0", + "version": "7.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4562,7 +4562,7 @@ }, "babel-plugin-syntax-object-rest-spread": { "version": "6.13.0", - "resolved": "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=" }, "babel-plugin-transform-define": { @@ -4611,7 +4611,7 @@ }, "babel-plugin-transform-object-rest-spread": { "version": "6.26.0", - "resolved": "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", "requires": { "babel-plugin-syntax-object-rest-spread": "^6.8.0", @@ -5838,7 +5838,7 @@ "dependencies": { "bytes": { "version": "3.0.0", - "resolved": "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", "dev": true }, @@ -6090,7 +6090,7 @@ }, "globby": { "version": "7.1.1", - "resolved": "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz", + "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", "requires": { "array-union": "^1.0.1", @@ -6126,7 +6126,7 @@ }, "slash": { "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" } } @@ -10636,7 +10636,7 @@ }, "is-regexp": { "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=" }, "is-relative": { @@ -13309,7 +13309,7 @@ "dependencies": { "source-map": { "version": "0.5.7", - "resolved": "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true } diff --git a/package.json b/package.json index bcf6051a..e8b95bcb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-storefront", - "version": "7.6.0", + "version": "7.7.0", "description": "Build and deploy e-commerce progressive web apps (PWAs) in record time.", "module": "./index.js", "license": "Apache-2.0", diff --git a/src/props/createLazyProps.js b/src/props/createLazyProps.js index 4eca1583..ffd4e9aa 100644 --- a/src/props/createLazyProps.js +++ b/src/props/createLazyProps.js @@ -49,7 +49,7 @@ * component will be rendered and can display a skeleton while waiting for the promise returned * by `fetchCallback` to resolve */ -export default function createLazyProps(fetchCallback, { timeout = 50 } = {}) { +export default function createLazyProps(fetchCallback, { timeout = 100 } = {}) { return (options /* from getInitialProps */) => { if (typeof window === 'undefined') { // server diff --git a/src/props/fetchFromAPI.js b/src/props/fetchFromAPI.js index 0883562b..920f73ae 100644 --- a/src/props/fetchFromAPI.js +++ b/src/props/fetchFromAPI.js @@ -1,5 +1,4 @@ import fetch from '../fetch' -import isBrowser from '../utils/isBrowser' /** * A convenience function to be used in `getInitialProps` to fetch data for the page from an @@ -24,23 +23,32 @@ import isBrowser from '../utils/isBrowser' * @param {Object} opts The options object provided to `getInitialProps` * @return {Promise} A promise that resolves to the data that the page should display */ -export default function fetchFromAPI({ req, asPath }) { - const server = !isBrowser() - const host = server ? req.headers['host'] : '' - const protocol = server ? (host.startsWith('localhost') ? 'http://' : 'https://') : '' +export default function fetchFromAPI({ req, asPath, pathname }) { + const host = req ? process.env.API_HOST || req.headers['host'] : '' + const protocol = req ? (host.startsWith('localhost') ? 'http://' : 'https://') : '' + const [path, search] = asPath.split('?') - if (asPath === '/') asPath = '' - if (asPath.startsWith('/?')) asPath = asPath.substring(1) + let uri = `/api${path.replace(/\/$/, '')}` - let uri = `/api${asPath}` + if (search) { + uri += `?${search}` + } + + let headers = {} - if (server) { + if (req) { + // on the server if (uri.indexOf('?') === -1) { uri = uri + '?_includeAppData=1' } else { uri = uri + '&_includeAppData=1' } + + headers = { + host: req.headers['host'], + 'x-next-page': `/api${pathname.replace(/\/$/, '')}`, + } } - return fetch(`${protocol}${host}${uri}`).then(res => res.json()) + return fetch(`${protocol}${host}${uri}`, { headers }).then(res => res.json()) } diff --git a/test/props/fetchFromAPI.test.js b/test/props/fetchFromAPI.test.js index 061a93c8..fdbd6169 100644 --- a/test/props/fetchFromAPI.test.js +++ b/test/props/fetchFromAPI.test.js @@ -1,95 +1,124 @@ +import fetchFromAPI from 'react-storefront/props/fetchFromAPI' + describe('fetchFromAPI', () => { - const headers = { - headers: { 'x-rsf-api-version': '1' }, - } + let headers fetchMock.mockResponse(JSON.stringify({})) - let fetchFromAPI, - isBrowser = true - - beforeEach(() => { - jest.isolateModules(() => { - jest.doMock('react-storefront/utils/isBrowser', () => () => isBrowser) - fetchFromAPI = require('react-storefront/props/fetchFromAPI').default - }) - }) - - afterAll(() => { - jest.resetAllMocks() - }) - describe('in the browser', () => { beforeEach(() => { - isBrowser = true + headers = { + 'x-rsf-api-version': '1', + } }) it('should prepend api to the path', () => { fetchFromAPI({ asPath: '/p/1', }) - expect(fetchMock).toHaveBeenCalledWith('/api/p/1', headers) + expect(fetchMock).toHaveBeenCalledWith('/api/p/1', { headers }) }) it('should call /api when the path is /', () => { fetchFromAPI({ asPath: '/', }) - expect(fetchMock).toHaveBeenCalledWith('/api', headers) + expect(fetchMock).toHaveBeenCalledWith('/api', { headers }) }) it('should append query params directly to api if the root with query params is called', () => { fetchFromAPI({ asPath: '/?test=1', }) - expect(fetchMock).toHaveBeenCalledWith('/api?test=1', headers) + expect(fetchMock).toHaveBeenCalledWith('/api?test=1', { headers }) }) }) describe('on the server', () => { beforeEach(() => { - isBrowser = false + headers = { + 'x-rsf-api-version': '1', + host: 'www.domain.com', + } }) it('should include the protocol, domain, and ?_includeAppData=1', () => { fetchFromAPI({ asPath: '/p/1', + pathname: '/p/[productId]', req: { headers: { host: 'www.domain.com', }, }, }) - expect(fetchMock).toHaveBeenCalledWith( - 'https://www.domain.com/api/p/1?_includeAppData=1', - headers, - ) + expect(fetchMock).toHaveBeenCalledWith('https://www.domain.com/api/p/1?_includeAppData=1', { + headers: { + ...headers, + 'x-next-page': '/api/p/[productId]', + }, + }) + }) + + it('should use API_HOST when provided', () => { + process.env.API_HOST = 'localhost:3001' + + fetchFromAPI({ + asPath: '/p/1', + pathname: '/p/[productId]', + req: { + headers: { + host: 'www.domain.com', + }, + }, + }) + + expect(fetchMock).toHaveBeenCalledWith('http://localhost:3001/api/p/1?_includeAppData=1', { + headers: { + ...headers, + 'x-next-page': '/api/p/[productId]', + }, + }) + + delete process.env.API_HOST }) it('should use http:// for localhost', () => { fetchFromAPI({ asPath: '/p/1', + pathname: '/p/[productId]', req: { headers: { + ...headers, host: 'localhost', }, }, }) - expect(fetchMock).toHaveBeenCalledWith('http://localhost/api/p/1?_includeAppData=1', headers) + expect(fetchMock).toHaveBeenCalledWith('http://localhost/api/p/1?_includeAppData=1', { + headers: { + ...headers, + host: 'localhost', + 'x-next-page': '/api/p/[productId]', + }, + }) }) it('should append _includeAppData to the existing query string', () => { fetchFromAPI({ asPath: '/foo?x=1', + pathname: '/foo', req: { - headers: { - host: 'localhost', - }, + headers, }, }) expect(fetchMock).toHaveBeenCalledWith( - 'http://localhost/api/foo?x=1&_includeAppData=1', - headers, + 'https://www.domain.com/api/foo?x=1&_includeAppData=1', + { + headers: { + ...headers, + 'x-next-page': '/api/foo', + }, + }, ) }) })