diff --git a/__tests__/server/utils/__snapshots__/loadModules.spec.jsx.snap b/__tests__/server/utils/__snapshots__/loadModules.spec.jsx.snap index cfeee2af6..f1d6501e4 100644 --- a/__tests__/server/utils/__snapshots__/loadModules.spec.jsx.snap +++ b/__tests__/server/utils/__snapshots__/loadModules.spec.jsx.snap @@ -16,10 +16,10 @@ exports[`loadModules updates the client module map cache 1`] = ` "browser": { "modules": { "some-root": { - "baseUrl": "https://example.com/cdn/some-root/2.2.2/", + "baseUrl": "https://example.com/cdn/some-root/1.1.2/", "browser": { "integrity": "nggdfhr34", - "url": "https://example.com/cdn/some-root/2.2.2/some-root.browser.js", + "url": "https://example.com/cdn/some-root/1.1.2/some-root.browser.js", }, }, }, @@ -27,10 +27,10 @@ exports[`loadModules updates the client module map cache 1`] = ` "legacyBrowser": { "modules": { "some-root": { - "baseUrl": "https://example.com/cdn/some-root/2.2.2/", + "baseUrl": "https://example.com/cdn/some-root/1.1.2/", "legacyBrowser": { "integrity": "7567ee", - "url": "https://example.com/cdn/some-root/2.2.2/some-root.legacy.browser.js", + "url": "https://example.com/cdn/some-root/1.1.2/some-root.legacy.browser.js", }, }, }, diff --git a/__tests__/server/utils/loadModules.spec.jsx b/__tests__/server/utils/loadModules.spec.jsx index 77f447c56..4429e810d 100644 --- a/__tests__/server/utils/loadModules.spec.jsx +++ b/__tests__/server/utils/loadModules.spec.jsx @@ -44,47 +44,36 @@ jest.mock('../../../src/server/plugins/csp', () => ({ const RootModule = () => ({}); -describe('loadModules', () => { - const moduleMap = { - modules: { - 'some-root': { - node: { - url: 'https://example.com/cdn/some-root/2.2.2/some-root.node.js', - integrity: '4y45hr', - }, - browser: { - url: 'https://example.com/cdn/some-root/2.2.2/some-root.browser.js', - integrity: 'nggdfhr34', - }, - legacyBrowser: { - url: 'https://example.com/cdn/some-root/2.2.2/some-root.legacy.browser.js', - integrity: '7567ee', - }, +let modules; +let fetchedModuleMap; +function setFetchedRootModuleVersion(version) { + modules = { + 'some-root': { + node: { + url: `https://example.com/cdn/some-root/${version}/some-root.node.js`, + integrity: '4y45hr', + }, + browser: { + url: `https://example.com/cdn/some-root/${version}/some-root.browser.js`, + integrity: 'nggdfhr34', + }, + legacyBrowser: { + url: `https://example.com/cdn/some-root/${version}/some-root.legacy.browser.js`, + integrity: '7567ee', }, }, }; + fetchedModuleMap = { modules }; +} + +describe('loadModules', () => { beforeAll(() => { getModule.mockImplementation(() => RootModule); - updateModuleRegistry.mockImplementation(() => ({ - 'some-root': { - node: { - url: 'https://example.com/cdn/some-root/2.2.2/some-root.node.js', - integrity: '4y45hr', - }, - browser: { - url: 'https://example.com/cdn/some-root/2.2.2/some-root.browser.js', - integrity: 'nggdfhr34', - }, - legacyBrowser: { - url: 'https://example.com/cdn/some-root/2.2.2/some-root.legacy.browser.js', - integrity: '7567ee', - }, - }, - })); + updateModuleRegistry.mockImplementation(() => modules); global.fetch = jest.fn(() => Promise.resolve({ - json: () => Promise.resolve(moduleMap), + json: () => Promise.resolve(fetchedModuleMap), })); }); @@ -94,9 +83,10 @@ describe('loadModules', () => { }); it('updates the holocron module registry', async () => { + setFetchedRootModuleVersion('1.1.1'); await loadModules(); expect(updateModuleRegistry).toHaveBeenCalledWith({ - moduleMap: addBaseUrlToModuleMap(moduleMap), + moduleMap: addBaseUrlToModuleMap(fetchedModuleMap), batchModulesToUpdate: require('../../../src/server/utils/batchModulesToUpdate').default, getModulesToUpdate: require('../../../src/server/utils/getModulesToUpdate').default, onModuleLoad: require('../../../src/server/utils/onModuleLoad').default, @@ -104,11 +94,34 @@ describe('loadModules', () => { }); it('updates the client module map cache', async () => { + setFetchedRootModuleVersion('1.1.2'); await loadModules(); expect(getClientModuleMapCache()).toMatchSnapshot(); }); + it('returns loaded modules', async () => { + setFetchedRootModuleVersion('2.0.0'); + const loadedModules = await loadModules(); + expect(loadedModules).toEqual({ + 'some-root': { + browser: { + integrity: 'nggdfhr34', + url: 'https://example.com/cdn/some-root/2.0.0/some-root.browser.js', + }, + legacyBrowser: { + integrity: '7567ee', + url: 'https://example.com/cdn/some-root/2.0.0/some-root.legacy.browser.js', + }, + node: { + integrity: '4y45hr', + url: 'https://example.com/cdn/some-root/2.0.0/some-root.node.js', + }, + }, + }); + }); + it('doesnt update caches when there are no changes', async () => { + setFetchedRootModuleVersion('1.1.3'); updateModuleRegistry.mockImplementationOnce(() => ({})); await loadModules(); expect(getClientModuleMapCache()).toMatchSnapshot(); @@ -118,12 +131,13 @@ describe('loadModules', () => { RootModule[CONFIGURATION_KEY] = { csp: "default-src 'none';", }; - + setFetchedRootModuleVersion('1.1.4'); await loadModules(); expect(updateCSP).toHaveBeenCalledWith("default-src 'none';"); }); it('calls updateCSP even when csp is not set', async () => { + setFetchedRootModuleVersion('1.1.5'); delete RootModule[CONFIGURATION_KEY].csp; await loadModules(); expect(updateCSP).toHaveBeenCalledWith(undefined); @@ -150,8 +164,25 @@ describe('loadModules', () => { }); it('does not attempt to update CSP', async () => { + setFetchedRootModuleVersion('1.1.6'); await loadModules(); expect(updateCSP).not.toHaveBeenCalled(); }); }); + + describe('when module map does not change', () => { + beforeAll(async () => { + setFetchedRootModuleVersion('1.1.1'); + await loadModules(); + jest.clearAllMocks(); + }); + it('does not updateModuleRegistry', async () => { + await loadModules(); + expect(updateModuleRegistry).not.toHaveBeenCalled(); + }); + it('does not return any modules', async () => { + const loadedModules = await loadModules(); + expect(loadedModules).toEqual({}); + }); + }); }); diff --git a/package-lock.json b/package-lock.json index b66075f0a..2daa0966c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "lean-intl": "^4.2.2", "matcher": "^4.0.0", "node-fetch": "^2.6.7", + "object-hash": "^3.0.0", "opossum": "^7.1.0", "opossum-prometheus": "^0.3.0", "pidusage": "^3.0.2", @@ -11922,6 +11923,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint-loader/node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/eslint-loader/node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -18666,15 +18675,6 @@ "node": ">=10.0.0" } }, - "node_modules/lockfile-lint-api/node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/lockfile-lint/node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -20214,9 +20214,9 @@ } }, "node_modules/object-hash": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "engines": { "node": ">= 6" } diff --git a/package.json b/package.json index 1e2017d54..a5a218965 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "lean-intl": "^4.2.2", "matcher": "^4.0.0", "node-fetch": "^2.6.7", + "object-hash": "^3.0.0", "opossum": "^7.1.0", "opossum-prometheus": "^0.3.0", "pidusage": "^3.0.2", diff --git a/src/server/utils/loadModules.js b/src/server/utils/loadModules.js index 11b66eeb7..0cfc2b182 100644 --- a/src/server/utils/loadModules.js +++ b/src/server/utils/loadModules.js @@ -17,6 +17,7 @@ import { getModule } from 'holocron'; import { updateModuleRegistry } from 'holocron/server'; +import hash from 'object-hash'; import onModuleLoad, { CONFIGURATION_KEY } from './onModuleLoad'; import batchModulesToUpdate from './batchModulesToUpdate'; import getModulesToUpdate from './getModulesToUpdate'; @@ -25,9 +26,17 @@ import { setClientModuleMapCache } from './clientModuleMapCache'; import { updateCSP } from '../plugins/csp'; import addBaseUrlToModuleMap from './addBaseUrlToModuleMap'; +let cashedModuleMapHash; + const loadModules = async () => { const moduleMapResponse = await fetch(process.env.HOLOCRON_MODULE_MAP_URL); const moduleMap = addBaseUrlToModuleMap(await moduleMapResponse.json()); + + const moduleMapHash = hash(moduleMap); + if (cashedModuleMapHash && cashedModuleMapHash === moduleMapHash) { + return {}; + } + cashedModuleMapHash = moduleMapHash; const serverConfig = getServerStateConfig(); const loadedModules = await updateModuleRegistry({