From 1987a6534f1d0ed373f25fd2b07150aa3a41d50b Mon Sep 17 00:00:00 2001 From: Zachary Trabookis Date: Sun, 14 Apr 2024 15:07:10 -0400 Subject: [PATCH] feat: handle absolute URIs in PUBLIC_PATH (#568) Webpack allows `publicPath` to be an absolute URI (https://webpack.js.org/configuration/output/#outputpublicpath), in order to support deployments to a CDN. However, the browser history module expects a relative path for `basename` but uses the same environment variable (`PUBLIC_PATH`) to configure it. Since the path is always expected to be be suffixed to the absolute URI, we avoid introducing another configuration variable (thus maintaining full backwards compatibility) by extracting it at initialization time. --- src/index.js | 2 ++ src/initialize.js | 3 +- src/initialize.test.js | 11 ++++++ src/utils.js | 32 +++++++++++++++++ src/utils.test.js | 78 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 125 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index d0499f9e3..9a4137740 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,8 @@ export { convertKeyNames, getQueryParameters, ensureDefinedConfig, + parseURL, + getPath, } from './utils'; export { APP_TOPIC, diff --git a/src/initialize.js b/src/initialize.js index e60343605..61665d8cd 100644 --- a/src/initialize.js +++ b/src/initialize.js @@ -46,6 +46,7 @@ */ import { createBrowserHistory, createMemoryHistory } from 'history'; +import { getPath } from './utils'; import { publish, } from './pubSub'; @@ -89,7 +90,7 @@ import { */ export const history = (typeof window !== 'undefined') ? createBrowserHistory({ - basename: getConfig().PUBLIC_PATH, + basename: getPath(getConfig().PUBLIC_PATH), }) : createMemoryHistory(); /** diff --git a/src/initialize.test.js b/src/initialize.test.js index 869b15c03..413e3e234 100644 --- a/src/initialize.test.js +++ b/src/initialize.test.js @@ -1,4 +1,5 @@ import PubSub from 'pubsub-js'; +import { createBrowserHistory } from 'history'; import { APP_PUBSUB_INITIALIZED, APP_CONFIG_INITIALIZED, @@ -37,6 +38,7 @@ jest.mock('./auth'); jest.mock('./analytics'); jest.mock('./i18n'); jest.mock('./auth/LocalForageCache'); +jest.mock('history'); let config = null; // const newConfig = { @@ -355,3 +357,12 @@ describe('initialize', () => { // expect(logError).not.toHaveBeenCalled(); // }); }); + +describe('history', () => { + it('browser history called by default path', async () => { + // import history from initialize; + expect(createBrowserHistory).toHaveBeenCalledWith({ + basename: '/', + }); + }); +}); diff --git a/src/utils.js b/src/utils.js index be16cff22..091a46fb0 100644 --- a/src/utils.js +++ b/src/utils.js @@ -122,6 +122,38 @@ export function convertKeyNames(object, nameMap) { return modifyObjectKeys(object, transformer); } +/** + * Given a string URL return an element that has been parsed via href. + * This element has the possibility to return different part of the URL. + parser.protocol; // => "http:" + parser.hostname; // => "example.com" + parser.port; // => "3000" + parser.pathname; // => "/pathname/" + parser.search; // => "?search=test" + parser.hash; // => "#hash" + parser.host; // => "example.com:3000" + * https://gist.github.com/jlong/2428561 + * + * @param {string} + * @returns {Object} + */ +export function parseURL(url) { + const parser = document.createElement('a'); + parser.href = url; + return parser; +} + +/** + * Given a string URL return the path of the URL + * + * + * @param {string} + * @returns {string} + */ +export function getPath(url) { + return parseURL(url).pathname; +} + /** * *Deprecated*: A method which converts the supplied query string into an object of * key-value pairs and returns it. Defaults to the current query string - should perform like diff --git a/src/utils.test.js b/src/utils.test.js index 016129040..cd5e6eac3 100644 --- a/src/utils.test.js +++ b/src/utils.test.js @@ -3,6 +3,8 @@ import { camelCaseObject, snakeCaseObject, convertKeyNames, + parseURL, + getPath, getQueryParameters, } from '.'; @@ -113,3 +115,79 @@ describe('getQueryParameters', () => { }); }); }); + +describe('ParseURL', () => { + const testURL = 'http://example.com:3000/pathname/?search=test#hash'; + const parsedURL = parseURL(testURL); + it('String URL is correctly parsed', () => { + expect(parsedURL.toString()).toEqual(testURL); + expect(parsedURL.href).toEqual(testURL); + expect(typeof (parsedURL)).toEqual('object'); + }); + + it('should return protocol from URL', () => { + expect(parsedURL.protocol).toEqual('http:'); + }); + + it('should return hostname from URL', () => { + expect(parsedURL.hostname).toEqual('example.com'); + }); + + it('should return port from URL', () => { + expect(parsedURL.port).toEqual('3000'); + }); + + it('should return pathname from URL', () => { + expect(parsedURL.pathname).toEqual('/pathname/'); + }); + + it('should return search rom URL', () => { + expect(parsedURL.search).toEqual('?search=test'); + }); + + it('should return hash from URL', () => { + expect(parsedURL.hash).toEqual('#hash'); + }); + + it('should return host from URL', () => { + expect(parsedURL.host).toEqual('example.com:3000'); + }); +}); + +describe('getPath', () => { + it('Path is retrieved with full url', () => { + const testURL = 'http://example.com:3000/pathname/?search=test#hash'; + + expect(getPath(testURL)).toEqual('/pathname/'); + }); + + it('Path is retrieved with only path', () => { + const testURL = '/learning/'; + + expect(getPath(testURL)).toEqual('/learning/'); + }); + + it('Path is retrieved without protocol', () => { + const testURL = '//example.com:3000/accounts/'; + + expect(getPath(testURL)).toEqual('/accounts/'); + }); + + it('Path is retrieved with base `/`', () => { + const testURL = '/'; + + expect(getPath(testURL)).toEqual('/'); + }); + + it('Path is retrieved without port', () => { + const testURL = 'https://example.com/accounts/'; + + expect(getPath(testURL)).toEqual('/accounts/'); + }); + + it('Path is retrieved without CDN shape', () => { + const testURL = 'https://d20blt6w1kfasr.cloudfront.net/learning/'; + + expect(getPath(testURL)).toEqual('/learning/'); + }); +});