Skip to content
This repository has been archived by the owner on May 3, 2024. It is now read-only.

Commit

Permalink
feat(external-fallbacks): enable modules to have external fallbacks (#…
Browse files Browse the repository at this point in the history
…984)

---------

Co-authored-by: Giuliano Kranevitter <giuliano.kranevitter1@aexp.com>
  • Loading branch information
JAdshead and giulianok authored Sep 12, 2023
1 parent 84838a8 commit 7d51efe
Show file tree
Hide file tree
Showing 13 changed files with 426 additions and 297 deletions.
2 changes: 1 addition & 1 deletion __tests__/client/initClient.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('initClient', () => {
const mockError = new Error('This is a test error!!!');
loadPrerenderScripts.mockImplementationOnce(() => { throw mockError; });

const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
const initClient = require('../../src/client/initClient').default;

await initClient();
Expand Down
360 changes: 276 additions & 84 deletions __tests__/server/plugins/reactHtml/index.spec.jsx

Large diffs are not rendered by default.

10 changes: 0 additions & 10 deletions __tests__/server/utils/__snapshots__/onModuleLoad.spec.jsx.snap
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`onModuleLoad includes messages about all missing or incompatible externals 1`] = `
"dep-a@^2.0.0 is required by my-awesome-module, but the root module provides 3.2.0
dep-b@~5.8.0 is required by my-awesome-module, but the root module provides 5.9.6
External 'dep-d' is required by my-awesome-module, but is not provided by the root module"
`;

exports[`onModuleLoad throws if the root module does not provide the expected external 1`] = `"External 'dep-b' is required by my-awesome-module, but is not provided by the root module"`;

exports[`onModuleLoad throws if the root module provides an incompatible version of a required external 1`] = `"dep-a@^2.1.1 is required by my-awesome-module, but the root module provides 2.1.0"`;

exports[`onModuleLoad throws when the one app version is incompatible 1`] = `"some-module@1.0.2 is not compatible with this version of one-app (4.43.0-0), it requires ~4.41.0."`;

exports[`onModuleLoad validates csp added to the root module 1`] = `"Root module must provide a valid content security policy."`;
2 changes: 1 addition & 1 deletion __tests__/server/utils/getModulesToUpdate.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jest.mock('../../../src/server/utils/stateConfig', () => ({
rootModuleName: 'test-root',
})),
}));
jest.mock('../../../src/server/utils/onModuleLoad', () => ({
jest.mock('holocron', () => ({
getModulesUsingExternals: jest.fn(() => ['first-module', 'another-module']),
}));

Expand Down
171 changes: 50 additions & 121 deletions __tests__/server/utils/onModuleLoad.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import util from 'util';
import React from 'react';
import { preprocessEnvVar } from '@americanexpress/env-config-utils';
import { META_DATA_KEY } from '@americanexpress/one-app-bundler';
import { clearModulesUsingExternals } from 'holocron';

import onModuleLoad, {
setModulesUsingExternals,
getModulesUsingExternals,
CONFIGURATION_KEY,
validateCspIsPresent,
} from '../../../src/server/utils/onModuleLoad';
Expand All @@ -29,14 +29,20 @@ import onModuleLoad, {
import { setStateConfig, getClientStateConfig, getServerStateConfig } from '../../../src/server/utils/stateConfig';
import { setRedirectAllowList } from '../../../src/server/utils/redirectAllowList';
import { setCorsOrigins } from '../../../src/server/plugins/conditionallyAllowCors';
import { extendRestrictedAttributesAllowList, validateSafeRequestRestrictedAttributes } from '../../../src/server/utils/safeRequest';
import {
extendRestrictedAttributesAllowList,
validateSafeRequestRestrictedAttributes,
} from '../../../src/server/utils/safeRequest';
import { setConfigureRequestLog } from '../../../src/server/utils/logging/fastifyPlugin';
import { setCreateSsrFetch } from '../../../src/server/utils/createSsrFetch';
import { getEventLoopDelayThreshold, getEventLoopDelayPercentile } from '../../../src/server/utils/createCircuitBreaker';
import setupDnsCache from '../../../src/server/utils/setupDnsCache';
import { configurePWA } from '../../../src/server/pwa';
import { setErrorPage } from '../../../src/server/plugins/reactHtml/staticErrorPage';

jest.mock('holocron', () => ({
clearModulesUsingExternals: jest.fn(),
}));
jest.mock('../../../src/server/utils/stateConfig', () => ({
setStateConfig: jest.fn(),
getClientStateConfig: jest.fn(),
Expand All @@ -46,7 +52,9 @@ jest.mock('../../../src/server/utils/redirectAllowList', () => ({
setRedirectAllowList: jest.fn(),
}));
jest.mock('@americanexpress/env-config-utils');
jest.mock('../../../src/server/utils/readJsonFile', () => () => ({ buildVersion: '4.43.0-0-38f0178d' }));
jest.mock('../../../src/server/utils/readJsonFile', () => () => ({
buildVersion: '4.43.0-0-38f0178d',
}));
jest.mock('../../../src/server/plugins/conditionallyAllowCors', () => ({
setCorsOrigins: jest.fn(),
}));
Expand All @@ -73,6 +81,7 @@ describe('onModuleLoad', () => {
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => null);

beforeEach(() => {
process.env.ONE_DANGEROUSLY_ACCEPT_BREAKING_EXTERNALS = 'false';
process.env.ONE_DANGEROUSLY_DISABLE_CSP = 'false';
global.getTenantRootModule = () => RootModule;
jest.resetAllMocks();
Expand All @@ -84,42 +93,34 @@ describe('onModuleLoad', () => {
}));
});

afterEach(() => {
setModulesUsingExternals([]);
});

it('does not do anything if there is not an environment variable config', () => {
expect(
() => onModuleLoad({ module: { [META_DATA_KEY]: { version: '1.0.0' } } })
).not.toThrow();
expect(() => onModuleLoad({ module: { [META_DATA_KEY]: { version: '1.0.0' } } })).not.toThrow();
expect(preprocessEnvVar).not.toHaveBeenCalled();
});

it('does not throw when the one app version is compatible', () => {
expect(
() => onModuleLoad({
module: {
[CONFIGURATION_KEY]: {
appCompatibility: '^4.41.0',
},
[META_DATA_KEY]: { version: '1.0.1' },
expect(() => onModuleLoad({
module: {
[CONFIGURATION_KEY]: {
appCompatibility: '^4.41.0',
},
moduleName: 'some-module',
})
[META_DATA_KEY]: { version: '1.0.1' },
},
moduleName: 'some-module',
})
).not.toThrow();
});

it('throws when the one app version is incompatible', () => {
expect(
() => onModuleLoad({
module: {
[CONFIGURATION_KEY]: {
appCompatibility: '~4.41.0',
},
[META_DATA_KEY]: { version: '1.0.2' },
expect(() => onModuleLoad({
module: {
[CONFIGURATION_KEY]: {
appCompatibility: '~4.41.0',
},
moduleName: 'some-module',
})
[META_DATA_KEY]: { version: '1.0.2' },
},
moduleName: 'some-module',
})
).toThrowErrorMatchingSnapshot();
});

Expand Down Expand Up @@ -159,7 +160,8 @@ describe('onModuleLoad', () => {
[META_DATA_KEY]: { version: '1.0.4' },
},
moduleName: 'some-module',
})).not.toThrow();
})
).not.toThrow();
});

it('calls setStateConfig if provideStateConfig is supplied', () => {
Expand Down Expand Up @@ -226,109 +228,29 @@ describe('onModuleLoad', () => {
'dep-c': '>1.0.0',
},
};
expect(() => onModuleLoad({ module: { [CONFIGURATION_KEY]: configuration, [META_DATA_KEY]: { version: '1.0.7' } }, moduleName: 'my-awesome-module' })).not.toThrow();
expect(() => onModuleLoad({
module: { [CONFIGURATION_KEY]: configuration, [META_DATA_KEY]: { version: '1.0.7' } },
moduleName: 'my-awesome-module',
})
).not.toThrow();
});

it('warns if a module that isn\'t the root module attempts to provide externals', () => {
it("warns if a module that isn't the root module attempts to provide externals", () => {
const configuration = {
providedExternals: {
'dep-b': {
version: '1.0.0',
module: () => null,
},
},

};
onModuleLoad({ module: { [CONFIGURATION_KEY]: configuration, [META_DATA_KEY]: { version: '1.0.8' } }, moduleName: 'my-awesome-module' });
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
});

it('throws if the root module does not provide the expected external', () => {
RootModule[CONFIGURATION_KEY] = {
providedExternals: {
'dep-a': { version: '2.1.0', module: () => 0 },
},
};
const configuration = {
requiredExternals: {
'dep-b': '^2.0.0',
},
};
expect(() => onModuleLoad({ module: { [CONFIGURATION_KEY]: configuration, [META_DATA_KEY]: { version: '1.0.9' } }, moduleName: 'my-awesome-module' })).toThrowErrorMatchingSnapshot();
});

it('throws if the root module provides an incompatible version of a required external', () => {
RootModule[CONFIGURATION_KEY].providedExternals = {
'dep-a': { version: '2.1.0', module: () => 0 },
};
const configuration = {
requiredExternals: {
'dep-a': '^2.1.1',
},
};
expect(() => onModuleLoad({ module: { [CONFIGURATION_KEY]: configuration, [META_DATA_KEY]: { version: '1.0.10' } }, moduleName: 'my-awesome-module' })).toThrowErrorMatchingSnapshot();
});

it('includes messages about all missing or incompatible externals', () => {
RootModule[CONFIGURATION_KEY] = {
providedExternals: {
'dep-a': { version: '3.2.0', module: () => 0 },
'dep-b': { version: '5.9.6', module: () => 0 },
'dep-c': { version: '3.0.10', module: () => 0 },
},
};
const configuration = {
requiredExternals: {
'dep-a': '^2.0.0',
'dep-b': '~5.8.0',
'dep-c': '>1.0.0',
'dep-d': '^3.1.2',
},
};
expect(() => onModuleLoad({ module: { [CONFIGURATION_KEY]: configuration, [META_DATA_KEY]: { version: '1.0.11' } }, moduleName: 'my-awesome-module' })).toThrowErrorMatchingSnapshot();
});

it('logs a warning if the root module provides an incompatible version of a required external and ONE_DANGEROUSLY_ACCEPT_BREAKING_EXTERNALS is set to true', () => {
process.env.ONE_DANGEROUSLY_ACCEPT_BREAKING_EXTERNALS = true;
RootModule[CONFIGURATION_KEY].providedExternals = {
'dep-a': { version: '2.1.0', module: () => 0 },
};
const configuration = {
requiredExternals: {
'dep-a': '^2.1.1',
},
};
expect(() => onModuleLoad({ module: { [CONFIGURATION_KEY]: configuration, [META_DATA_KEY]: { version: '1.0.10' } }, moduleName: 'my-awesome-module' })).not.toThrow();
onModuleLoad({
module: { [CONFIGURATION_KEY]: configuration, [META_DATA_KEY]: { version: '1.0.8' } },
moduleName: 'my-awesome-module',
});
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
});

it('keeps track of the externals a module is using', () => {
RootModule[CONFIGURATION_KEY] = {
providedExternals: {
'dep-a': { version: '2.1.0', module: () => 0 },
'dep-b': { version: '5.8.6', module: () => 0 },
'dep-c': { version: '3.0.10', module: () => 0 },
},
};
const configuration = {
requiredExternals: {
'dep-a': '^2.0.0',
'dep-b': '~5.8.0',
},
};
expect(getModulesUsingExternals()).toEqual([]);
onModuleLoad({ module: { [CONFIGURATION_KEY]: configuration, [META_DATA_KEY]: { version: '1.0.12' } }, moduleName: 'my-awesome-module' });
expect(getModulesUsingExternals()).toEqual(['my-awesome-module']);
});

it('clears the modules using externals when loading the root module', () => {
const modulesUsingExternals = ['a', 'b', 'c'];
setModulesUsingExternals(modulesUsingExternals);
expect(getModulesUsingExternals()).toEqual(modulesUsingExternals);
onModuleLoad({ module: { [CONFIGURATION_KEY]: { csp }, [META_DATA_KEY]: { version: '1.0.13' } }, moduleName: 'some-root' });
expect(getModulesUsingExternals()).toEqual([]);
});

it('validates csp added to the root module', () => {
const callOnModuleLoad = () => onModuleLoad({
module: {},
Expand Down Expand Up @@ -454,6 +376,11 @@ describe('onModuleLoad', () => {
expect(util.format(...consoleInfoSpy.mock.calls[0])).toBe('Loaded module not-the-root-module@1.0.16');
});

it('clears the modules using externals when loading the root module', () => {
onModuleLoad({ module: { [CONFIGURATION_KEY]: { csp }, [META_DATA_KEY]: { version: '1.0.13' } }, moduleName: 'some-root' });
expect(clearModulesUsingExternals).toHaveBeenCalled();
});

it('updates allowed safeRequest values from the root module', () => {
onModuleLoad({
module: {
Expand Down Expand Up @@ -502,7 +429,9 @@ describe('onModuleLoad', () => {
expect(setConfigureRequestLog).toHaveBeenCalledWith(configureRequestLog);
});
it('Throws error if csp and ONE_DANGEROUSLY_DISABLE_CSP is not set', () => {
expect(() => validateCspIsPresent(missingCsp)).toThrow('Root module must provide a valid content security policy.');
expect(() => validateCspIsPresent(missingCsp)).toThrow(
'Root module must provide a valid content security policy.'
);
});

it('Does not throw if valid csp is present', () => {
Expand Down
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
"fastify-metrics": "^10.3.0",
"fastify-plugin": "^4.2.0",
"helmet": "^7.0.0",
"holocron": "^1.7.0",
"holocron": "^1.8.1",
"holocron-module-route": "^1.7.0",
"immutable": "^4.1.0",
"ip": "^1.1.8",
Expand Down
4 changes: 3 additions & 1 deletion src/client/initClient.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { browserHistory, Router, matchPromise } from '@americanexpress/one-app-router';
import { setModuleMap } from 'holocron';
import { setModuleMap, setRequiredExternalsRegistry } from 'holocron';

import {
initializeClientStore, loadPrerenderScripts, moveHelmetScripts, loadServiceWorker,
Expand All @@ -29,6 +29,8 @@ export default async function initClient() {
try {
// eslint-disable-next-line no-underscore-dangle
setModuleMap(global.__CLIENT_HOLOCRON_MODULE_MAP__);
// eslint-disable-next-line no-underscore-dangle
setRequiredExternalsRegistry(global.__HOLOCRON_EXTERNALS__);
moveHelmetScripts();

const store = initializeClientStore();
Expand Down
Loading

0 comments on commit 7d51efe

Please sign in to comment.