From 0eeee499ee9fda2eac6b2663c754c6840053d05a Mon Sep 17 00:00:00 2001 From: Edd Hurst Date: Tue, 5 Sep 2023 16:49:53 +0100 Subject: [PATCH 1/6] feat(devBundler): aggregate styles into dependencies and local (#557) * feat(devBundler): aggregate styles into dependencies and local overrides * chore(Test): replace expect statement * chore(Aggregator): reset digest Set with clear instead of reinitializing Co-authored-by: Matthew Mallimo * chore(Lint): update Set to be const * chore(Comment): adjust comment to reflect correct output * chore(devBundler): remove package file changes, unintended change --------- Co-authored-by: Matthew Mallimo --- jest.esm.config.js | 1 + .../plugins/server-styles-dispatcher.spec.js | 7 ++- .../esbuild/plugins/styles-loader.spec.js | 16 +++---- .../utils/server-style-aggregator.spec.js | 44 ++++++++++++++----- .../plugins/server-styles-dispatcher.js | 5 ++- .../esbuild/plugins/styles-loader.js | 5 ++- .../esbuild/utils/server-style-aggregator.js | 32 +++++++++++--- packages/one-app-dev-bundler/jest.config.cjs | 26 +++++++++++ .../one-app-dev-bundler/jest.esm.setup.js | 19 ++++++++ 9 files changed, 125 insertions(+), 30 deletions(-) create mode 100644 packages/one-app-dev-bundler/jest.config.cjs create mode 100644 packages/one-app-dev-bundler/jest.esm.setup.js diff --git a/jest.esm.config.js b/jest.esm.config.js index 462bb388..547826a2 100644 --- a/jest.esm.config.js +++ b/jest.esm.config.js @@ -28,6 +28,7 @@ module.exports = { '!**/node_modules/**', '!**/build/**', '!packages/*/test-results/**', + '!packages/*/jest.esm.setup.js', // Despite it not being in the root, coverage reports see this package '!packages/one-app-locale-bundler/**', ], diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/server-styles-dispatcher.spec.js b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/server-styles-dispatcher.spec.js index 25501b73..8e100bf7 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/server-styles-dispatcher.spec.js +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/server-styles-dispatcher.spec.js @@ -45,7 +45,7 @@ describe('server styles dispatcher', () => { serverStylesDispatcher({ bundleType: BUNDLE_TYPES.SERVER }) ); const onEnd = hooks.onEnd[0]; - addStyle('body{background: white;}body > p{font-color: black;}'); + addStyle('digestMock', 'body{background: white;}body > p{font-color: black;}'); /* onEnd test start */ // mock the bundle using mockFs @@ -64,7 +64,10 @@ describe('server styles dispatcher', () => { expect(actualBundleContent).toMatchInlineSnapshot(` "const mock = \\"JavaScript Content\\"; ;module.exports.ssrStyles = { - getFullSheet: () => \\"body{background: white;}body > p{font-color: black;}\\", + aggregatedStyles: [{\\"css\\":\\"body{background: white;}body > p{font-color: black;}\\",\\"digest\\":\\"digestMock\\"}], + getFullSheet: function getFullSheet() { + return this.aggregatedStyles.reduce((acc, { css }) => acc + css, ''); +}, };" `); diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/styles-loader.spec.js b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/styles-loader.spec.js index 1c893611..c8228855 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/styles-loader.spec.js +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/styles-loader.spec.js @@ -107,7 +107,7 @@ describe('Esbuild plugin stylesLoader', () => { expect(loader).toEqual('js'); expect(contents).toMatchInlineSnapshot(` -"const digest = 'c0c0c0be320475d1514fe8e0c023d2780b6e23c2adab14a438a0ee2ef98369ba'; +"const digest = '5e9583e668d7632ccabf75f612a320b29f5f48cd7a7e86489c7b0f8f5fdcdbbe'; const css = \`body { background: white; } @@ -152,7 +152,7 @@ body > p { expect(sassCompile).toHaveBeenCalledTimes(0); expect(loader).toEqual('js'); expect(contents).toMatchInlineSnapshot(` -"const digest = '83279c4025e8b1107c3f376acaaac5656a3b68d0066ab70f2ceeb3c065a5751f'; +"const digest = '5e9583e668d7632ccabf75f612a320b29f5f48cd7a7e86489c7b0f8f5fdcdbbe'; const css = \`body { background: white; } @@ -200,7 +200,7 @@ body > p { expect(loader).toEqual('js'); expect(contents).toMatchInlineSnapshot(` -"const digest = 'c0c0c0be320475d1514fe8e0c023d2780b6e23c2adab14a438a0ee2ef98369ba'; +"const digest = '11e1fda0219a10c2de0ad6b28c1c6519985965cbef3f5b8f8f119d16f1bafff3'; const css = \`body { background: white; } @@ -239,7 +239,7 @@ body > p { expect(sassCompile).toHaveBeenCalledTimes(0); expect(loader).toEqual('js'); expect(contents).toMatchInlineSnapshot(` -"const digest = '83279c4025e8b1107c3f376acaaac5656a3b68d0066ab70f2ceeb3c065a5751f'; +"const digest = '5e9583e668d7632ccabf75f612a320b29f5f48cd7a7e86489c7b0f8f5fdcdbbe'; const css = \`body { background: white; } @@ -306,7 +306,7 @@ export { css, digest };" expect(sassCompile).toHaveBeenCalledTimes(0); expect(loader).toEqual('js'); expect(contents).toMatchInlineSnapshot(` -"const digest = '786f696ae19422021e0f17df7c6dd6eb43f92c9c101f7d0649b341165dda1b31'; +"const digest = 'f85b3a3cf0c00eb3fd23e6d440b10077d7493cf7f127538acb994cade5bce451'; const css = \` ._root_1vf0l_1 { background: white; } @@ -378,7 +378,7 @@ export { css, digest };" expect(sassCompile).toHaveBeenCalledTimes(0); expect(loader).toEqual('js'); expect(contents).toMatchInlineSnapshot(` -"const digest = '786f696ae19422021e0f17df7c6dd6eb43f92c9c101f7d0649b341165dda1b31'; +"const digest = 'f85b3a3cf0c00eb3fd23e6d440b10077d7493cf7f127538acb994cade5bce451'; const css = \` ._root_1vf0l_1 { background: white; } @@ -432,7 +432,7 @@ export { css, digest };" expect(loader).toEqual('js'); expect(contents).toMatchInlineSnapshot(` -"const digest = 'c0c0c0be320475d1514fe8e0c023d2780b6e23c2adab14a438a0ee2ef98369ba'; +"const digest = '5e9583e668d7632ccabf75f612a320b29f5f48cd7a7e86489c7b0f8f5fdcdbbe'; const css = \`body { background: white; } @@ -481,7 +481,7 @@ body > p { expect(sassCompile).toHaveBeenCalledTimes(0); expect(loader).toEqual('js'); expect(contents).toMatchInlineSnapshot(` -"const digest = '83279c4025e8b1107c3f376acaaac5656a3b68d0066ab70f2ceeb3c065a5751f'; +"const digest = '5e9583e668d7632ccabf75f612a320b29f5f48cd7a7e86489c7b0f8f5fdcdbbe'; const css = \`body { background: white; } diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/utils/server-style-aggregator.spec.js b/packages/one-app-dev-bundler/__tests__/esbuild/utils/server-style-aggregator.spec.js index 951c4c02..220c805a 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/utils/server-style-aggregator.spec.js +++ b/packages/one-app-dev-bundler/__tests__/esbuild/utils/server-style-aggregator.spec.js @@ -14,30 +14,50 @@ * permissions and limitations under the License. */ -import { getAggregatedStyles, emptyAggregatedStyles, addStyle } from '../../../esbuild/utils/server-style-aggregator'; +import { + getAggregatedStyles, + emptyAggregatedStyles, + addStyle, +} from '../../../esbuild/utils/server-style-aggregator'; -describe('serverSideAggirgator utilities', () => { +describe('serverSideAggregatorStyles utilities', () => { beforeEach(() => { emptyAggregatedStyles(); - jest.clearAllMocks(); }); describe('addStyle', () => { - it('should append given perameter to aggregatedStyles', () => { - addStyle('testing string'); - expect(getAggregatedStyles()).toBe('testing string'); + it('should append styles to the aggregatedStyles as an object with a digest and css key', () => { + addStyle('digestMock', 'cssMock', false); + expect(getAggregatedStyles()).toBe('[{"css":"cssMock","digest":"digestMock"}]'); + }); + + it('should deduplicate styles added that share the same digest', () => { + addStyle('digestMock', 'cssMock', false); + addStyle('digestMock', 'cssMock', false); + addStyle('digestMock', 'cssMock', false); + addStyle('digestMock', 'cssMock', false); + expect(getAggregatedStyles()).toBe('[{"css":"cssMock","digest":"digestMock"}]'); }); }); - describe('getAggrigatedStyles', () => { - it('should return string of any and all collected styles', () => { - expect(getAggregatedStyles()).toBe(''); + + describe('getAggregatedStyles', () => { + it('should return a stringified empty array when no styles have been added', () => { + expect(getAggregatedStyles()).toBe('[]'); + }); + + it('should return a stringified array with dependency styles declared first and local styles declared last', () => { + addStyle('digestLocalMock', 'cssLocalMock', false); + addStyle('digestDepsMock', 'cssDepsMock', true); + expect(getAggregatedStyles()).toBe('[{"css":"cssDepsMock","digest":"digestDepsMock"},{"css":"cssLocalMock","digest":"digestLocalMock"}]'); }); }); - describe('emptyAggrigatedStyles', () => { + + describe('emptyAggregatedStyles', () => { it('should return emptied string', () => { - addStyle('testing string'); + addStyle('digestMock', 'cssMock', false); + expect(getAggregatedStyles()).toBe('[{"css":"cssMock","digest":"digestMock"}]'); emptyAggregatedStyles(); - expect(getAggregatedStyles()).toBe(''); + expect(getAggregatedStyles()).toBe('[]'); }); }); }); diff --git a/packages/one-app-dev-bundler/esbuild/plugins/server-styles-dispatcher.js b/packages/one-app-dev-bundler/esbuild/plugins/server-styles-dispatcher.js index a913b928..34dbb436 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/server-styles-dispatcher.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/server-styles-dispatcher.js @@ -32,7 +32,10 @@ const serverStylesDispatcher = ({ bundleType }) => ({ const initialContent = await fs.promises.readFile(fileName, 'utf8'); const outputContent = `${initialContent} ;module.exports.ssrStyles = { - getFullSheet: () => ${JSON.stringify(getAggregatedStyles())}, + aggregatedStyles: ${getAggregatedStyles()}, + getFullSheet: function getFullSheet() { + return this.aggregatedStyles.reduce((acc, { css }) => acc + css, ''); +}, };`; await fs.promises.writeFile(fileName, outputContent, 'utf8'); })); diff --git a/packages/one-app-dev-bundler/esbuild/plugins/styles-loader.js b/packages/one-app-dev-bundler/esbuild/plugins/styles-loader.js index c04d5f8b..09b17025 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/styles-loader.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/styles-loader.js @@ -67,7 +67,7 @@ const stylesLoader = (cssModulesOptions = {}, { bundleType } = {}) => ({ }); const hash = crypto.createHash('sha256'); - hash.update(args.path); + hash.update(result.css); const digest = hash.copy().digest('hex'); let injectedCode = ''; @@ -84,7 +84,8 @@ const stylesLoader = (cssModulesOptions = {}, { bundleType } = {}) => ({ })();`; } else { // For SSR, aggregate all styles, then inject them once at the end - addStyle(result.css); + const isDependencyFile = args.path.indexOf('/node_modules/') >= 0; + addStyle(digest, result.css, isDependencyFile); } // provide useful values to the importer of this file, most importantly, the classnames diff --git a/packages/one-app-dev-bundler/esbuild/utils/server-style-aggregator.js b/packages/one-app-dev-bundler/esbuild/utils/server-style-aggregator.js index ee934978..27c4d1b5 100644 --- a/packages/one-app-dev-bundler/esbuild/utils/server-style-aggregator.js +++ b/packages/one-app-dev-bundler/esbuild/utils/server-style-aggregator.js @@ -14,14 +14,36 @@ * permissions and limitations under the License. */ -let aggregatedStyles = ''; +let aggregatedStyles = { + deps: [], + local: [], +}; -export function addStyle(style) { - aggregatedStyles += style; +const sheetDigests = new Set(); + +export function addStyle(digest, css, isDependencyFile) { + if (!sheetDigests.has(digest)) { + sheetDigests.add(digest); + + aggregatedStyles[isDependencyFile ? 'deps' : 'local'].push({ + css, + digest, + }); + } } -export const getAggregatedStyles = () => aggregatedStyles; +/** + * Returns aggregated styles object from all parsed CSS files with dependencies listed first + * @returns {string} + */ +export const getAggregatedStyles = () => JSON.stringify( + [...aggregatedStyles.deps, ...aggregatedStyles.local] +); export function emptyAggregatedStyles() { - aggregatedStyles = ''; + aggregatedStyles = { + deps: [], + local: [], + }; + sheetDigests.clear(); } diff --git a/packages/one-app-dev-bundler/jest.config.cjs b/packages/one-app-dev-bundler/jest.config.cjs new file mode 100644 index 00000000..c181dd27 --- /dev/null +++ b/packages/one-app-dev-bundler/jest.config.cjs @@ -0,0 +1,26 @@ +/* + * Copyright 2023 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const jestConfig = require('../../jest.esm.config'); + +module.exports = { + ...jestConfig, + collectCoverageFrom: [ + ...jestConfig.collectCoverageFrom, + '**/*.{mjs,js,jsx}', + ], + roots: ['./'] +}; diff --git a/packages/one-app-dev-bundler/jest.esm.setup.js b/packages/one-app-dev-bundler/jest.esm.setup.js new file mode 100644 index 00000000..80bbef75 --- /dev/null +++ b/packages/one-app-dev-bundler/jest.esm.setup.js @@ -0,0 +1,19 @@ +/* + * Copyright 2023 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +const jestSetup = require('../../jest.esm.setup'); + +module.exports = jestSetup; From c64b3eea1743db92ac60f22838878b7e97863b46 Mon Sep 17 00:00:00 2001 From: Danny Date: Wed, 6 Sep 2023 10:49:49 -0400 Subject: [PATCH 2/6] docs(typescript): add section on typescript (#558) Co-authored-by: Jonny Adshead --- packages/one-app-bundler/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/one-app-bundler/README.md b/packages/one-app-bundler/README.md index 619c3a11..ef9ec43e 100644 --- a/packages/one-app-bundler/README.md +++ b/packages/one-app-bundler/README.md @@ -337,6 +337,9 @@ before enabling any of the following: } } ``` +### TypeScript + +TypeScript in One App modules needs no extra configuration within `one-app-bundler` to work. `one-app-bundler` is set up to ignore `TypeScript` features leaving `tsc` to focus on typechecking only. #### Specify what version of One App your module is compatible with From 8443accdb78b60a91ec9c864cd95163319168607 Mon Sep 17 00:00:00 2001 From: Andrew Curtis Date: Thu, 7 Sep 2023 10:29:02 -0400 Subject: [PATCH 3/6] fix(one-app-runner): apply anonymous ip to debugger (#559) Co-authored-by: Jonny Adshead --- .../__tests__/src/__snapshots__/startApp.spec.js.snap | 4 ++-- packages/one-app-runner/src/startApp.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/one-app-runner/__tests__/src/__snapshots__/startApp.spec.js.snap b/packages/one-app-runner/__tests__/src/__snapshots__/startApp.spec.js.snap index 02f4eb0b..fee45539 100644 --- a/packages/one-app-runner/__tests__/src/__snapshots__/startApp.spec.js.snap +++ b/packages/one-app-runner/__tests__/src/__snapshots__/startApp.spec.js.snap @@ -4,9 +4,9 @@ exports[`startApp Displays an error if createNetwork fails: create network calls exports[`startApp Passes the container name to the docker --name flag 1`] = `"docker pull one-app:5.0.0 && docker run -t -p 3000:3000 -p 3001:3001 -p 3002:3002 -p 3005:3005 -p 9229:9229 -e NODE_ENV=development --name=one-app-at-test -v \\"/path/to/module-a:/opt/module-workspace/module-a\\" one-app:5.0.0 /bin/sh -c \\"npm run serve-module '/opt/module-workspace/module-a' && node lib/server/index.js --root-module-name=frank-lloyd-root --module-map-url=https://example.com/module-map.json \\""`; -exports[`startApp applies inspect mode to node process when useDebug is passed 1`] = `"docker pull one-app:5.0.0 && docker run -t -p 3000:3000 -p 3001:3001 -p 3002:3002 -p 3005:3005 -p 9229:9229 -e NODE_ENV=development -v \\"/path/to/module-a:/opt/module-workspace/module-a\\" one-app:5.0.0 /bin/sh -c \\"npm run serve-module '/opt/module-workspace/module-a' && node --inspect=127.0.0.1:9229 lib/server/index.js --root-module-name=frank-lloyd-root --module-map-url=https://example.com/module-map.json \\""`; +exports[`startApp applies inspect mode to node process when useDebug is passed 1`] = `"docker pull one-app:5.0.0 && docker run -t -p 3000:3000 -p 3001:3001 -p 3002:3002 -p 3005:3005 -p 9229:9229 -e NODE_ENV=development -v \\"/path/to/module-a:/opt/module-workspace/module-a\\" one-app:5.0.0 /bin/sh -c \\"npm run serve-module '/opt/module-workspace/module-a' && node --inspect=0.0.0.0:9229 lib/server/index.js --root-module-name=frank-lloyd-root --module-map-url=https://example.com/module-map.json \\""`; -exports[`startApp applies inspect mode to with custom port node process when useDebug and env var 1`] = `"docker pull one-app:5.0.0 && docker run -t -p 3000:3000 -p 3001:3001 -p 3002:3002 -p 3005:3005 -p 9221:9221 -e NODE_ENV=development -v \\"/path/to/module-a:/opt/module-workspace/module-a\\" one-app:5.0.0 /bin/sh -c \\"npm run serve-module '/opt/module-workspace/module-a' && node --inspect=127.0.0.1:9221 lib/server/index.js --root-module-name=frank-lloyd-root --module-map-url=https://example.com/module-map.json \\""`; +exports[`startApp applies inspect mode to with custom port node process when useDebug and env var 1`] = `"docker pull one-app:5.0.0 && docker run -t -p 3000:3000 -p 3001:3001 -p 3002:3002 -p 3005:3005 -p 9221:9221 -e NODE_ENV=development -v \\"/path/to/module-a:/opt/module-workspace/module-a\\" one-app:5.0.0 /bin/sh -c \\"npm run serve-module '/opt/module-workspace/module-a' && node --inspect=0.0.0.0:9221 lib/server/index.js --root-module-name=frank-lloyd-root --module-map-url=https://example.com/module-map.json \\""`; exports[`startApp bypasses docker pull when the offline flag is passed 1`] = `" docker run -t -p 3000:3000 -p 3001:3001 -p 3002:3002 -p 3005:3005 -p 9229:9229 -e NODE_ENV=development -v \\"/path/to/module-a:/opt/module-workspace/module-a\\" one-app:5.0.0 /bin/sh -c \\"npm run serve-module '/opt/module-workspace/module-a' && node lib/server/index.js --root-module-name=frank-lloyd-root --module-map-url=https://example.com/module-map.json \\""`; diff --git a/packages/one-app-runner/src/startApp.js b/packages/one-app-runner/src/startApp.js index 455f4e9b..e57e8658 100644 --- a/packages/one-app-runner/src/startApp.js +++ b/packages/one-app-runner/src/startApp.js @@ -106,7 +106,7 @@ module.exports = async function startApp({ const generateModuleMap = () => (moduleMapUrl ? `--module-map-url=${moduleMapUrl}` : ''); - const generateDebug = (port) => (useDebug ? `--inspect=127.0.0.1:${port}` : ''); + const generateDebug = (port) => (useDebug ? `--inspect=0.0.0.0:${port}` : ''); if (createDockerNetwork) { if (!dockerNetworkToJoin) { From 523898deb9a1a4bcce6ba43915c852b02b7bb3a5 Mon Sep 17 00:00:00 2001 From: Giuliano Kranevitter Date: Wed, 13 Sep 2023 12:46:58 -0400 Subject: [PATCH 4/6] feat: external fallbacks (#536) --- jest.esm.config.js | 1 + package.json | 4 +- .../src/middleware/parrot-scenarios.spec.js | 2 +- .../src/utils/language-packs.spec.js | 2 +- .../__tests__/src/utils/paths.spec.js | 2 +- .../src/webpack/configs/fragments.spec.js | 2 +- .../src/middleware/parrot-scenarios.js | 2 +- .../holocron-dev-server/src/utils/config.js | 2 +- .../src/utils/language-packs.js | 2 +- .../holocron-dev-server/src/utils/paths.js | 2 +- .../holocron-dev-server/src/utils/statics.js | 2 +- .../src/utils/virtual-file-system.js | 4 +- .../holocron-dev-server/src/utils/watcher.js | 2 +- .../src/webpack/configs/fragments.js | 2 +- packages/one-app-bundler/README.md | 61 ++++- packages/one-app-bundler/__mocks__/node:fs.js | 77 ++++++ .../__tests__/bin/bundle-module.spec.js | 23 +- .../__tests__/bin/drop-module.spec.js | 6 +- .../bin/webpack-bundle-module.spec.js | 5 + .../one-app-bundler/__tests__/package.spec.js | 4 +- .../extendWebpackConfig.spec.js.snap | 73 +++++- .../utils/extendWebpackConfig.spec.js | 26 ++ .../__tests__/utils/getConfigOptions.spec.js | 5 + .../webpack/app/webpack.client.spec.js | 2 +- ...ted-external-fallbacks-loader.spec.js.snap | 19 ++ .../externals-loader.spec.js.snap | 45 +++- .../provided-externals-loader.spec.js.snap | 58 ++++- .../validate-externals-loader.spec.js.snap | 2 +- ...unlisted-external-fallbacks-loader.spec.js | 61 +++++ .../webpack/loaders/externals-loader.spec.js | 59 ++++- .../loaders/provided-externals-loader.spec.js | 19 ++ .../loaders/validate-externals-loader.spec.js | 38 ++- .../webpack/module/webpack.client.spec.js | 2 +- .../ssr-css-loader/index-style-loader.spec.js | 2 +- .../webpack/ssr-css-loader/index.spec.js | 2 +- packages/one-app-bundler/bin/bundle-module.js | 36 ++- packages/one-app-bundler/bin/drop-module.js | 4 +- .../bin/generateIntegrityManifest.js | 4 +- .../bin/postProcessOneAppBundle.js | 4 +- .../bin/webpack-bundle-module.js | 6 +- .../one-app-bundler/bin/webpackCallback.js | 4 +- packages/one-app-bundler/package.json | 1 + .../utils/extendWebpackConfig.js | 33 ++- .../one-app-bundler/utils/getConfigOptions.js | 14 +- .../one-app-bundler/utils/validation/index.js | 14 +- .../webpack/app/webpack.client.js | 2 +- .../one-app-bundler/webpack/loaders/common.js | 2 +- ...able-unlisted-external-fallbacks-loader.js | 35 +++ .../webpack/loaders/externals-loader.js | 31 ++- .../loaders/provided-externals-loader.js | 31 ++- .../ssr-css-loader/index-style-loader.js | 2 +- .../webpack/loaders/ssr-css-loader/index.js | 2 +- .../validate-required-externals-loader.js | 31 ++- .../webpack/module/webpack.client.js | 3 +- .../webpack/module/webpack.server.js | 2 +- .../generateESBuildOptions.spec.js.snap | 225 ++++++++++++++++++ .../esbuild/generateESBuildOptions.spec.js | 12 +- .../one-app-index-loader/index.input.jsx | 2 +- .../index_browser-not-watching.output.jsx | 24 +- .../index_browser-watching-live.output.jsx | 24 +- ...index_browser-watching-not-live.output.jsx | 24 +- .../index_server-not-watching.output.jsx | 35 ++- .../index_server-watching-live.output.jsx | 35 ++- .../index_server-watching-not-live.output.jsx | 35 ++- .../cjs-compatibility-hotpatch.spec.js | 4 +- .../esbuild/plugins/externals-loader.spec.js | 99 ++++++-- .../generate-integrity-manifest.spec.js | 4 +- .../esbuild/plugins/legacy-bundler.spec.js | 4 +- .../app-compatibility-injector.spec.js | 8 +- ...nlisted-external-fallback-injector.spec.js | 70 ++++++ .../holocron-module-register-injector.spec.js | 2 +- .../module-metadata-injector.spec.js | 3 +- ...dev-live-reloader-injector_output.spec.jsx | 4 +- .../provided-externals-injector.spec.js | 73 ++++-- .../plugins/one-app-index-loader.spec.js | 15 +- .../plugins/restrict-runtime-symbols.spec.js | 4 +- .../plugins/server-styles-dispatcher.spec.js | 2 +- .../esbuild/plugins/time-build.spec.js | 4 +- .../__tests__/utils/analyze-bundles.spec.js | 6 +- .../__tests__/utils/build-module.spec.js | 2 +- .../utils/bundle-external-fallbacks.spec.js | 208 ++++++++++++++++ .../esbuild/generateESBuildOptions.js | 34 ++- .../plugins/cjs-compatibility-hotpatch.js | 2 +- .../esbuild/plugins/externals-loader.js | 55 ++++- .../esbuild/plugins/font-loader.js | 2 +- .../plugins/generate-integrity-manifest.js | 2 +- .../esbuild/plugins/image-loader.js | 2 +- .../esbuild/plugins/legacy-bundler.js | 2 +- .../app-compatibility-injector.js | 8 +- ...ble-unlisted-external-fallback-injector.js | 37 +++ .../holocron-module-register-injector.js | 2 +- .../module-metadata-injector.js | 3 +- .../provided-externals-injector.js | 37 ++- .../esbuild/plugins/one-app-index-loader.js | 6 +- .../plugins/remove-webpack-loader-syntax.js | 2 +- .../plugins/restrict-runtime-symbols.js | 2 +- .../plugins/server-styles-dispatcher.js | 2 +- .../esbuild/plugins/styles-loader.js | 6 +- .../esbuild/plugins/time-build.js | 2 +- .../utils/get-modules-bundler-config.js | 4 +- .../utils/get-modules-webpack-config.js | 2 +- .../esbuild/utils/purgecss.js | 4 +- packages/one-app-dev-bundler/index.js | 8 +- packages/one-app-dev-bundler/package.json | 3 +- .../utils/analyze-bundles.js | 4 +- .../utils/bundle-external-fallbacks.js | 75 ++++++ .../src/compileModuleLocales.spec.js | 2 +- packages/one-app-locale-bundler/index.js | 2 +- .../src/compileModuleLocales.js | 4 +- .../src/promisified-fs.js | 2 +- .../__tests__/bin/one-app-runner.spec.js | 2 +- .../__tests__/src/startApp.spec.js | 4 +- packages/one-app-runner/bin/one-app-runner.js | 2 +- packages/one-app-runner/src/startApp.js | 4 +- yarn.lock | 12 +- 115 files changed, 1839 insertions(+), 310 deletions(-) create mode 100644 packages/one-app-bundler/__mocks__/node:fs.js create mode 100644 packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/enable-unlisted-external-fallbacks-loader.spec.js.snap create mode 100644 packages/one-app-bundler/__tests__/webpack/loaders/enable-unlisted-external-fallbacks-loader.spec.js create mode 100644 packages/one-app-bundler/webpack/loaders/enable-unlisted-external-fallbacks-loader.js create mode 100644 packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/enable-unlisted-external-fallback-injector.spec.js create mode 100644 packages/one-app-dev-bundler/__tests__/utils/bundle-external-fallbacks.spec.js create mode 100644 packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/enable-unlisted-external-fallback-injector.js create mode 100644 packages/one-app-dev-bundler/utils/bundle-external-fallbacks.js diff --git a/jest.esm.config.js b/jest.esm.config.js index 547826a2..97419297 100644 --- a/jest.esm.config.js +++ b/jest.esm.config.js @@ -31,6 +31,7 @@ module.exports = { '!packages/*/jest.esm.setup.js', // Despite it not being in the root, coverage reports see this package '!packages/one-app-locale-bundler/**', + '!packages/one-app-dev-bundler/index.js', ], roots: [ 'packages/one-app-dev-bundler', diff --git a/package.json b/package.json index a8f4677a..7e9a73f0 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "cross-env": "^7.0.2", "enzyme": "^3.11.0", "enzyme-to-json": "^3.2.2", - "eslint": "8", + "eslint": "8.29.0", "eslint-config-amex": "^15.2.1", "eslint-plugin-jest": "^25.3.4", "eslint-plugin-jest-dom": "^4.0.1", @@ -52,4 +52,4 @@ "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" } } -} +} \ No newline at end of file diff --git a/packages/holocron-dev-server/__tests__/src/middleware/parrot-scenarios.spec.js b/packages/holocron-dev-server/__tests__/src/middleware/parrot-scenarios.spec.js index 0d8d7f19..0770f3b6 100644 --- a/packages/holocron-dev-server/__tests__/src/middleware/parrot-scenarios.spec.js +++ b/packages/holocron-dev-server/__tests__/src/middleware/parrot-scenarios.spec.js @@ -12,7 +12,7 @@ * under the License. */ -import path from 'path'; +import path from 'node:path'; import parrot from 'parrot-middleware'; import createMocksMiddleware, { diff --git a/packages/holocron-dev-server/__tests__/src/utils/language-packs.spec.js b/packages/holocron-dev-server/__tests__/src/utils/language-packs.spec.js index 4b3bb07c..6bc77d76 100644 --- a/packages/holocron-dev-server/__tests__/src/utils/language-packs.spec.js +++ b/packages/holocron-dev-server/__tests__/src/utils/language-packs.spec.js @@ -12,7 +12,7 @@ * under the License. */ -import path from 'path'; +import path from 'node:path'; import { loadLanguagePacks, loadModuleLanguagePack, diff --git a/packages/holocron-dev-server/__tests__/src/utils/paths.spec.js b/packages/holocron-dev-server/__tests__/src/utils/paths.spec.js index 02c55ee0..e3a29e45 100644 --- a/packages/holocron-dev-server/__tests__/src/utils/paths.spec.js +++ b/packages/holocron-dev-server/__tests__/src/utils/paths.spec.js @@ -12,7 +12,7 @@ * under the License. */ -import path from 'path'; +import path from 'node:path'; import { modulesBundleName } from '../../../src/constants'; import { diff --git a/packages/holocron-dev-server/__tests__/src/webpack/configs/fragments.spec.js b/packages/holocron-dev-server/__tests__/src/webpack/configs/fragments.spec.js index 606cbdd3..927de5b3 100644 --- a/packages/holocron-dev-server/__tests__/src/webpack/configs/fragments.spec.js +++ b/packages/holocron-dev-server/__tests__/src/webpack/configs/fragments.spec.js @@ -12,7 +12,7 @@ * under the License. */ -import path from 'path'; +import path from 'node:path'; import { createResolverConfigFragment, diff --git a/packages/holocron-dev-server/src/middleware/parrot-scenarios.js b/packages/holocron-dev-server/src/middleware/parrot-scenarios.js index 5dd9f447..8348585e 100644 --- a/packages/holocron-dev-server/src/middleware/parrot-scenarios.js +++ b/packages/holocron-dev-server/src/middleware/parrot-scenarios.js @@ -12,7 +12,7 @@ * under the License. */ -import path from 'path'; +import path from 'node:path'; import express from 'express'; import parrot from 'parrot-middleware'; diff --git a/packages/holocron-dev-server/src/utils/config.js b/packages/holocron-dev-server/src/utils/config.js index d31ae568..46db631c 100644 --- a/packages/holocron-dev-server/src/utils/config.js +++ b/packages/holocron-dev-server/src/utils/config.js @@ -12,7 +12,7 @@ * under the License. */ -import path from 'path'; +import path from 'node:path'; import readPkgUp from 'read-pkg-up'; import { defaultLogLevel, errorReportingUrlFragment, oneAppDockerImageName } from '../constants'; diff --git a/packages/holocron-dev-server/src/utils/language-packs.js b/packages/holocron-dev-server/src/utils/language-packs.js index da471e21..313491b3 100644 --- a/packages/holocron-dev-server/src/utils/language-packs.js +++ b/packages/holocron-dev-server/src/utils/language-packs.js @@ -12,7 +12,7 @@ * under the License. */ -import path from 'path'; +import path from 'node:path'; import jsonParse from 'json-parse-context'; import { volume, ufs } from './virtual-file-system'; diff --git a/packages/holocron-dev-server/src/utils/paths.js b/packages/holocron-dev-server/src/utils/paths.js index ca81cfcc..eac9f555 100644 --- a/packages/holocron-dev-server/src/utils/paths.js +++ b/packages/holocron-dev-server/src/utils/paths.js @@ -12,7 +12,7 @@ * under the License. */ -import path from 'path'; +import path from 'node:path'; import { modulesBundleName } from '../constants'; diff --git a/packages/holocron-dev-server/src/utils/statics.js b/packages/holocron-dev-server/src/utils/statics.js index a039cf7a..d02a54a4 100644 --- a/packages/holocron-dev-server/src/utils/statics.js +++ b/packages/holocron-dev-server/src/utils/statics.js @@ -12,7 +12,7 @@ * under the License. */ -import path from 'path'; +import path from 'node:path'; import { execSync, execFileSync, spawnSync } from 'child_process'; import { ufs } from './virtual-file-system'; diff --git a/packages/holocron-dev-server/src/utils/virtual-file-system.js b/packages/holocron-dev-server/src/utils/virtual-file-system.js index 4b67eeb5..24b661ac 100644 --- a/packages/holocron-dev-server/src/utils/virtual-file-system.js +++ b/packages/holocron-dev-server/src/utils/virtual-file-system.js @@ -12,8 +12,8 @@ * under the License. */ -import fs from 'fs'; -import path from 'path'; +import fs from 'node:fs'; +import path from 'node:path'; import { Union } from 'unionfs'; import { Volume, createFsFromVolume } from 'memfs'; diff --git a/packages/holocron-dev-server/src/utils/watcher.js b/packages/holocron-dev-server/src/utils/watcher.js index df7269de..6d08925b 100644 --- a/packages/holocron-dev-server/src/utils/watcher.js +++ b/packages/holocron-dev-server/src/utils/watcher.js @@ -12,7 +12,7 @@ * under the License. */ -import path from 'path'; +import path from 'node:path'; import chokidar from 'chokidar'; import { logLocaleAction, logLocaleModuleNamesBeingWatched, warnOnLocaleWatchError } from './logs'; diff --git a/packages/holocron-dev-server/src/webpack/configs/fragments.js b/packages/holocron-dev-server/src/webpack/configs/fragments.js index 801f8de9..907ed012 100644 --- a/packages/holocron-dev-server/src/webpack/configs/fragments.js +++ b/packages/holocron-dev-server/src/webpack/configs/fragments.js @@ -12,7 +12,7 @@ * under the License. */ -import path from 'path'; +import path from 'node:path'; import merge from 'webpack-merge'; import { diff --git a/packages/one-app-bundler/README.md b/packages/one-app-bundler/README.md index ef9ec43e..b4feb84f 100644 --- a/packages/one-app-bundler/README.md +++ b/packages/one-app-bundler/README.md @@ -113,18 +113,40 @@ In order to avoid duplicate code in your One App instance, you may want to share a dependency across all your modules that is not already provided by One App. These dependencies can be provided to your modules by your root module. The root module should include in its configuration -`providedExternals`, which is an array of external dependencies to be bundled -with it and provided to other modules. +`providedExternals`. This will include and make the listed dependencies +available to be consumed by child modules. Child modules will use `requiredExternals` +to consume dependencies provided by the root modules `providedExternals`, this will also remove +the dependency from the child modules bundle. Modules shouldn't configure both `providedExternals` and `requiredExternals`. -Remember `providedExternals` are dependencies which your root module will make available to child modules. `requiredExternals` are a list of dependencies the child module will need to be made available by the root module. +Remember `providedExternals` are dependencies which your root module will make available to child modules. +`requiredExternals` are a list of dependencies the child module will need to be made available by the root module. -All modules `requiredExternals` are validated at runtime against the root modules list of `providedExternals`. If the external dependency is not provided One App will throw an error. This will either result in the One App server not starting or, if it is already running, One App will not load that module. For example, if your child module requires `^2.1.0` of a dependency but your root module provides `2.0.0`, this will result in One App not loading that child module as the provided dependencies version does not satisfy the required semantic range. +All modules `requiredExternals` are validated at runtime against the root modules list of `providedExternals`. +By default if the external dependency is not provided One App will throw an error. This will either result in the +One App server not starting or, if it is already running, One App will not load that module. For example, if your +child module requires `^2.1.0` of a dependency but your root module provides `2.0.0`, this will result in One App +not loading that child module as the provided dependencies version does not satisfy the required semantic range. -This ensures that all of the listed dependencies features potentially required by the child module to work will be provided which could result in hard to debug bugs. +This ensures that all of the listed dependencies features, potentially required by the child module to work, will be provided which could result in hard to debug bugs. If you attempt to include one of the [dependencies](https://github.com/americanexpress/one-app-cli/blob/main/packages/one-app-bundler/webpack/webpack.common.js#L102-L155) provided by One App in your `providedExternals` or `requiredExternals`, your build will fail. + +##### Externals Fallbacks + +External fallbacks were added to help reduce the impact of some of the cons listed below. +For each dependency listed in `requiredExternals` fallback bundles(browser and server) will +be created. If the root module permits these fallbacks will be used to enable that child +module to load when there is no valid provided external dependency. This can be helpful when +transitioning between major versions of a externals dependency. + +To enable fallbacks the root module will need to set the `fallbackEnabled` option to `true` for each +provided external and the `enableUnlistedExternalFallbacks` to allow fallbacks for unlisted +dependencies. + +##### Usage + First make sure to add your dependency to your module's `package.json`: ```bash @@ -137,7 +159,14 @@ Then configure `one-app-bundler` to provide that dependency (and any others) as { "one-amex": { "bundler": { - "providedExternals": ["some-dependency", "another-dependency"] + "providedExternals": { + "some-dependency": { + "fallbackEnabled": true + }, + "another-dependency": { + "fallbackEnabled": false + } + } } } } @@ -179,6 +208,24 @@ npm install some-dependency * Couples your child and root module together * Increases complexity when managing updates to the provided and required dependency + +#### `enableUnlistedExternalFallbacks` + +To allow child modules to load when a `requiredExternal` is not listed as a `providedExternal` use the `enableUnlistedExternalFallbacks` option. +The child module must provide a fallback bundle for the missing required external to load when this option is set. + +`enableUnlistedExternalFallbacks` defaults to false if unset. + +```json +{ + "one-amex": { + "bundler": { + "enableUnlistedExternalFallbacks": true + } + } +} +``` + #### `performanceBudget` Set a custom [performance budget](https://webpack.js.org/configuration/performance/#performancemaxassetsize) @@ -326,7 +373,7 @@ before enabling any of the following: #### Legacy browser support `disableDevelopmentLegacyBundle` can be added to your bundler config and set to *true* to opt out of bundling the `legacy` assets. This will reduce bundle size and build times. This is only configured to be removed when in `development`. `production` builds will not skip the `legacy` build. -**Caution as this will remove legacy browser support from your module.** +**Caution as this will remove legacy browser support from your module.** ```json { diff --git a/packages/one-app-bundler/__mocks__/node:fs.js b/packages/one-app-bundler/__mocks__/node:fs.js new file mode 100644 index 00000000..8d1c6931 --- /dev/null +++ b/packages/one-app-bundler/__mocks__/node:fs.js @@ -0,0 +1,77 @@ +/* + * Copyright 2019 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations + * under the License. + */ + +let files = {}; + +const fs = { + _: { + setFiles: (configuredFiles) => { + files = configuredFiles || {}; + }, + + getFiles: () => files, + }, + + accessSync: jest.fn((filePath) => { + if (!files[filePath]) { + throw new Error(`Couldn't access file ${filePath}`); + } + }), + + readFileSync: jest.fn((filePath) => { + if (!files[filePath]) { + throw new Error(`Couldn't read file ${filePath}`); + } + return files[filePath]; + }), + + readFile: jest.fn((filePath, cb) => { + if (!files[filePath]) { + cb(new Error(`Couldn't read file ${filePath}`)); + } else { + cb(undefined, files[filePath]); + } + }), + + writeFileSync: jest.fn((filePath, content) => { + files[filePath] = content; + }), + + rmdirSync: jest.fn((dirPath) => { + if (!files[dirPath]) { + throw new Error(`Couldn't delete dir ${dirPath}`); + } + delete files[dirPath]; + }), + + mkdirSync: jest.fn(), + + symlinkSync: jest.fn(), + + unlinkSync: jest.fn(), + + closeSync: jest.fn(), + + stat: jest.fn(), + + statSync: jest.fn(), + + readlink: jest.fn(), + + readlinkSync: jest.fn(), + + readdirSync: jest.fn(), +}; + +module.exports = fs; diff --git a/packages/one-app-bundler/__tests__/bin/bundle-module.spec.js b/packages/one-app-bundler/__tests__/bin/bundle-module.spec.js index c3c4bb9a..bd6d82d0 100644 --- a/packages/one-app-bundler/__tests__/bin/bundle-module.spec.js +++ b/packages/one-app-bundler/__tests__/bin/bundle-module.spec.js @@ -15,7 +15,10 @@ /* eslint-disable global-require -- testing `on import` functionality needs 'require' in every tests */ -jest.mock('@americanexpress/one-app-dev-bundler', () => jest.fn(async () => {})); +jest.mock('@americanexpress/one-app-dev-bundler', () => ({ + devBuildModule: async () => undefined, + bundleExternalFallbacks: async () => undefined, +})); jest.mock('../../bin/webpack-bundle-module', () => jest.fn()); jest.spyOn(console, 'info'); @@ -40,9 +43,19 @@ describe('bundle-module', () => { process.env.NODE_ENV = nodeEnv; }); - it('should call the webpack bundler with no args', () => { + // bundleModule has async side effects, use this + // to have expectations on the next cycle of the event loop + const waitForNextEventLoopIteration = () => new Promise((resolve) => { + setImmediate(() => { + resolve(); + }); + }); + + it('should call the webpack bundler with no args', async () => { process.argv = []; + require('../../bin/bundle-module'); + await waitForNextEventLoopIteration(); // Since this is testing on-require behaviour, and there is a dynamic import, it's not possible // to directly assert the correct bundler was called, so instead just assert that the @@ -54,7 +67,10 @@ describe('bundle-module', () => { it('should call the dev bundler when passed --dev in NODE_ENV=development', async () => { process.argv = ['--dev']; process.env.NODE_ENV = 'development'; + require('../../bin/bundle-module'); + await waitForNextEventLoopIteration(); + expect(console.info).toHaveBeenCalledTimes(1); expect(console.info).toHaveBeenCalledWith('Running dev bundler'); }); @@ -62,7 +78,10 @@ describe('bundle-module', () => { it('should call the webpack bundler when passed --dev in NODE_ENV=production, and inform the user this has happened', async () => { process.argv = ['--dev']; process.env.NODE_ENV = 'production'; + require('../../bin/bundle-module'); + await waitForNextEventLoopIteration(); + expect(console.info).toHaveBeenCalledTimes(2); expect(console.info).toHaveBeenNthCalledWith(1, 'Ignoring `--dev` flag for NODE_ENV=production'); expect(console.info).toHaveBeenNthCalledWith(2, 'Running production bundler'); diff --git a/packages/one-app-bundler/__tests__/bin/drop-module.spec.js b/packages/one-app-bundler/__tests__/bin/drop-module.spec.js index ce6f2dab..f9a524d6 100644 --- a/packages/one-app-bundler/__tests__/bin/drop-module.spec.js +++ b/packages/one-app-bundler/__tests__/bin/drop-module.spec.js @@ -15,9 +15,9 @@ /* eslint-disable global-require -- testing `on import` functionality needs 'require' in every tests */ -let fs = require('fs'); +let fs = require('node:fs'); -jest.mock('fs'); +jest.mock('node:fs'); jest.mock('yargs', () => ({ argv: { _: ['my-module-name'] }, })); @@ -28,7 +28,7 @@ describe('drop-module', () => { beforeEach(() => { jest.resetModules(); jest.clearAllMocks(); - fs = require('fs'); + fs = require('node:fs'); }); it('should throw an error if it cannot access the module map', () => { diff --git a/packages/one-app-bundler/__tests__/bin/webpack-bundle-module.spec.js b/packages/one-app-bundler/__tests__/bin/webpack-bundle-module.spec.js index 8766f53f..e0555fb8 100644 --- a/packages/one-app-bundler/__tests__/bin/webpack-bundle-module.spec.js +++ b/packages/one-app-bundler/__tests__/bin/webpack-bundle-module.spec.js @@ -56,6 +56,7 @@ describe('bundle-module', () => { it('should bundle the module for the server', () => { process.argv = []; require('../../bin/webpack-bundle-module'); + expect(webpack).toHaveBeenCalledTimes(3); expect(webpack).toHaveBeenCalledWith(serverConfig, 'cb(node, true)'); expect(webpack.mock.calls[0][0]).not.toHaveProperty('watch'); @@ -65,6 +66,7 @@ describe('bundle-module', () => { it('should bundle the module for modern browsers', () => { process.argv = []; require('../../bin/webpack-bundle-module'); + expect(webpack).toHaveBeenCalledTimes(3); expect(webpack).toHaveBeenCalledWith(clientConfig('modern'), 'cb(browser, true)'); expect(webpack.mock.calls[1][0]).not.toHaveProperty('watch'); @@ -74,6 +76,7 @@ describe('bundle-module', () => { it('should bundle the module for legacy browsers', () => { process.argv = []; require('../../bin/webpack-bundle-module'); + expect(webpack).toHaveBeenCalledTimes(3); expect(webpack).toHaveBeenCalledWith(clientConfig('legacy'), 'cb(legacyBrowser, true)'); expect(webpack.mock.calls[2][0]).not.toHaveProperty('watch'); @@ -91,6 +94,7 @@ describe('bundle-module', () => { jest.mock('../../utils/getConfigOptions', () => jest.fn(() => ({ disableDevelopmentLegacyBundle: false }))); process.argv = []; require('../../bin/webpack-bundle-module'); + expect(webpack).toHaveBeenCalledTimes(3); expect(webpack).toHaveBeenCalledWith(clientConfig('legacy'), 'cb(legacyBrowser, true)'); }); @@ -100,6 +104,7 @@ describe('bundle-module', () => { jest.mock('../../utils/getConfigOptions', () => jest.fn(() => ({ disableDevelopmentLegacyBundle: true }))); process.argv = []; require('../../bin/webpack-bundle-module'); + expect(webpack).toHaveBeenCalledTimes(2); expect(webpack).not.toHaveBeenCalledWith(clientConfig('legacy'), 'cb(legacyBrowser, true)'); }); diff --git a/packages/one-app-bundler/__tests__/package.spec.js b/packages/one-app-bundler/__tests__/package.spec.js index dc28205c..c23ece57 100644 --- a/packages/one-app-bundler/__tests__/package.spec.js +++ b/packages/one-app-bundler/__tests__/package.spec.js @@ -12,8 +12,8 @@ * under the License. */ -const fs = require('fs'); -const path = require('path'); +const fs = require('node:fs'); +const path = require('node:path'); const PACKAGE_DIR_PATH = path.resolve(path.join(__dirname, '../')); diff --git a/packages/one-app-bundler/__tests__/utils/__snapshots__/extendWebpackConfig.spec.js.snap b/packages/one-app-bundler/__tests__/utils/__snapshots__/extendWebpackConfig.spec.js.snap index afc4451a..af360b35 100644 --- a/packages/one-app-bundler/__tests__/utils/__snapshots__/extendWebpackConfig.spec.js.snap +++ b/packages/one-app-bundler/__tests__/utils/__snapshots__/extendWebpackConfig.spec.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports['extendWebpackConfig should bundle requiredExternals designated by providedExternals 1'] = ` +exports[`extendWebpackConfig should bundle requiredExternals designated by providedExternals 1`] = ` Object { "test": "/path/src/index", "use": Array [ @@ -8,24 +8,68 @@ Object { "loader": "@americanexpress/one-app-bundler/webpack/loaders/provided-externals-loader", "options": Object { "moduleName": "test-root-module", - "providedExternals": Array [ - "ajv", - "chalk", - "lodash", - ], + "providedExternals": Object { + "ajv": Object { + "enableFallback": false, + }, + "chalk": Object { + "enableFallback": false, + }, + "lodash": Object { + "enableFallback": false, + }, + }, + }, + }, + ], +} +`; + +exports[`extendWebpackConfig should bundle requiredExternals designated by providedExternals with custom configuration 1`] = ` +Object { + "test": "/path/src/index", + "use": Array [ + Object { + "loader": "@americanexpress/one-app-bundler/webpack/loaders/provided-externals-loader", + "options": Object { + "moduleName": "test-root-module", + "providedExternals": Object { + "ajv": Object { + "enableFallback": true, + }, + "chalk": Object { + "enableFallback": false, + }, + "lodash": Object {}, + }, + }, + }, + ], +} +`; + +exports[`extendWebpackConfig should enable missing external fallbacks 1`] = ` +Object { + "test": "/path/src/index", + "use": Array [ + Object { + "loader": "@americanexpress/one-app-bundler/webpack/loaders/enable-unlisted-external-fallbacks-loader", + "options": Object { + "enableUnlistedExternalFallbacks": true, }, }, ], } `; -exports['extendWebpackConfig should use the correct trailing slash on windows 1'] = ` +exports[`extendWebpackConfig should use the correct trailing slash on windows 1`] = ` Object { "test": StringMatching /ajv\\\\\\\\\\$/, "use": Array [ Object { "loader": "@americanexpress/one-app-bundler/webpack/loaders/externals-loader", "options": Object { + "bundleTarget": undefined, "externalName": "ajv", }, }, @@ -33,13 +77,14 @@ Object { } `; -exports['extendWebpackConfig should use the correct trailing slash on windows 2'] = ` +exports[`extendWebpackConfig should use the correct trailing slash on windows 2`] = ` Object { "test": StringMatching /lodash\\\\\\\\\\$/, "use": Array [ Object { "loader": "@americanexpress/one-app-bundler/webpack/loaders/externals-loader", "options": Object { + "bundleTarget": undefined, "externalName": "lodash", }, }, @@ -47,7 +92,7 @@ Object { } `; -exports['extendWebpackConfig should use the correct trailing slash on windows 3'] = ` +exports[`extendWebpackConfig should use the correct trailing slash on windows 3`] = ` Object { "test": "/path/src/index", "use": Array [ @@ -64,13 +109,14 @@ Object { } `; -exports['extendWebpackConfig should use the provided requiredExternals configured 1'] = ` +exports[`extendWebpackConfig should use the provided requiredExternals configured 1`] = ` Object { "test": StringMatching /ajv\\\\/\\$/, "use": Array [ Object { "loader": "@americanexpress/one-app-bundler/webpack/loaders/externals-loader", "options": Object { + "bundleTarget": undefined, "externalName": "ajv", }, }, @@ -78,13 +124,14 @@ Object { } `; -exports['extendWebpackConfig should use the provided requiredExternals configured 2'] = ` +exports[`extendWebpackConfig should use the provided requiredExternals configured 2`] = ` Object { "test": StringMatching /lodash\\\\/\\$/, "use": Array [ Object { "loader": "@americanexpress/one-app-bundler/webpack/loaders/externals-loader", "options": Object { + "bundleTarget": undefined, "externalName": "lodash", }, }, @@ -92,7 +139,7 @@ Object { } `; -exports['extendWebpackConfig should use the provided requiredExternals configured 3'] = ` +exports[`extendWebpackConfig should use the provided requiredExternals configured 3`] = ` Object { "test": "/path/src/index", "use": Array [ @@ -109,7 +156,7 @@ Object { } `; -exports['extendWebpackConfig should validate the one app version 1'] = ` +exports[`extendWebpackConfig should validate the one app version 1`] = ` Object { "test": "/path/src/index", "use": Array [ diff --git a/packages/one-app-bundler/__tests__/utils/extendWebpackConfig.spec.js b/packages/one-app-bundler/__tests__/utils/extendWebpackConfig.spec.js index 1a3dbe24..69c1d36f 100644 --- a/packages/one-app-bundler/__tests__/utils/extendWebpackConfig.spec.js +++ b/packages/one-app-bundler/__tests__/utils/extendWebpackConfig.spec.js @@ -168,6 +168,25 @@ describe('extendWebpackConfig', () => { expect(rules[rules.length - 1]).toMatchSnapshot(); }); + it('should bundle requiredExternals designated by providedExternals with custom configuration', () => { + getConfigOptions.mockReturnValueOnce({ + providedExternals: { + ajv: { + enableFallback: true, + }, + chalk: { + enableFallback: false, + }, + lodash: {}, + }, + moduleName: 'test-root-module', + }); + const result = extendWebpackConfig(originalWebpackConfig); + const { rules } = result.module; + expect(rules).toHaveLength(originalWebpackConfig.module.rules.length + 1); + expect(rules[rules.length - 1]).toMatchSnapshot(); + }); + it('should use the provided requiredExternals configured', () => { getConfigOptions.mockReturnValueOnce({ requiredExternals: ['ajv', 'lodash'] }); const result = extendWebpackConfig(originalWebpackConfig); @@ -199,6 +218,13 @@ describe('extendWebpackConfig', () => { expect(rules[rules.length - 1]).toMatchSnapshot(); }); + it('should enable missing external fallbacks', () => { + getConfigOptions.mockReturnValueOnce({ enableUnlistedExternalFallbacks: true }); + const result = extendWebpackConfig(originalWebpackConfig); + const { rules } = result.module; + expect(rules[rules.length - 1]).toMatchSnapshot(); + }); + it('should validate the one app version', () => { getConfigOptions.mockReturnValueOnce({ appCompatibility: '^4.41.0' }); const result = extendWebpackConfig(originalWebpackConfig); diff --git a/packages/one-app-bundler/__tests__/utils/getConfigOptions.spec.js b/packages/one-app-bundler/__tests__/utils/getConfigOptions.spec.js index c3d9dc69..5c9c0231 100644 --- a/packages/one-app-bundler/__tests__/utils/getConfigOptions.spec.js +++ b/packages/one-app-bundler/__tests__/utils/getConfigOptions.spec.js @@ -67,6 +67,11 @@ describe('getConfigOptions', () => { expect(() => require('../../utils/getConfigOptions')).not.toThrow(); }); + it('should allow a user to include requiredExternals, providedExternals can also be an object', () => { + readPkgUp.sync.mockReturnValueOnce({ packageJson: { 'one-amex': { bundler: { providedExternals: { b: {} } } } } }); + expect(() => require('../../utils/getConfigOptions')).not.toThrow(); + }); + it('should throw when a user attempts to provide an app provided external', () => { readPkgUp.sync.mockReturnValueOnce({ packageJson: { 'one-amex': { bundler: { providedExternals: ['@americanexpress/one-app-ducks'] } } } }); expect(() => require('../../utils/getConfigOptions')).toThrowErrorMatchingSnapshot(); diff --git a/packages/one-app-bundler/__tests__/webpack/app/webpack.client.spec.js b/packages/one-app-bundler/__tests__/webpack/app/webpack.client.spec.js index bfa10a1e..eae7e293 100644 --- a/packages/one-app-bundler/__tests__/webpack/app/webpack.client.spec.js +++ b/packages/one-app-bundler/__tests__/webpack/app/webpack.client.spec.js @@ -102,7 +102,7 @@ describe('webpack/app', () => { const modernWebpackConfig = configGenerator('modern'); const legacyWebpackConfig = configGenerator('legacy'); expect(legacyWebpackConfig.entry.vendors.length - modernWebpackConfig.entry.vendors.length) - .toBeGreaterThan(20); + .toBeGreaterThan(5); }); it('does not transpile node_modules when DANGEROUSLY_DISABLE_DEPENDENCY_TRANSPILATION true', () => { diff --git a/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/enable-unlisted-external-fallbacks-loader.spec.js.snap b/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/enable-unlisted-external-fallbacks-loader.spec.js.snap new file mode 100644 index 00000000..aea09f32 --- /dev/null +++ b/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/enable-unlisted-external-fallbacks-loader.spec.js.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`enable-unlisted-external-fallbacks-loader should append the enableUnlistedExternalFallbacks to the default export 1`] = ` +"import MyComponent from './components/MyComponent'; +export default MyComponent; +; +if (!global.BROWSER) { + MyComponent.appConfig = Object.assign({}, MyComponent.appConfig, { + enableUnlistedExternalFallbacks: \\"true\\", + }); +} +" +`; + +exports[`enable-unlisted-external-fallbacks-loader should throw an error when the wrong syntax is used - export default hoc() 1`] = `"@americanexpress/one-app-bundler: Module must use \`export default VariableName\` in index syntax to use app compatibility validation"`; + +exports[`enable-unlisted-external-fallbacks-loader should throw an error when the wrong syntax is used - export from 1`] = `"@americanexpress/one-app-bundler: Module must use \`export default VariableName\` in index syntax to use app compatibility validation"`; + +exports[`enable-unlisted-external-fallbacks-loader should throw an error when the wrong syntax is used - module.exports 1`] = `"@americanexpress/one-app-bundler: Module must use \`export default VariableName\` in index syntax to use app compatibility validation"`; diff --git a/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/externals-loader.spec.js.snap b/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/externals-loader.spec.js.snap index 4b6b36db..a5eaa1c1 100644 --- a/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/externals-loader.spec.js.snap +++ b/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/externals-loader.spec.js.snap @@ -1,11 +1,50 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports['externals-loader should ignore the content and get the dependency from the root module 1'] = ` +exports[`externals-loader when bundleTarget is browser does not include content, gets the dependency from root module 1`] = ` "try { - module.exports = global.getTenantRootModule().appConfig.providedExternals['my-dependency'].module; + const Holocron = global.Holocron; + const fallbackExternal = Holocron.getExternal && Holocron.getExternal({ + name: 'my-dependency', + version: '1.2.3' + }); + const rootModuleExternal = global.getTenantRootModule && global.getTenantRootModule().appConfig.providedExternals['my-dependency']; + + module.exports = fallbackExternal || (rootModuleExternal ? rootModuleExternal.module : () => { + throw new Error('[browser][undefined] External not found: my-dependency'); + }) +} catch (error) { + const errorGettingExternal = new Error([ + '[browser] Failed to get external fallback my-dependency', + error.message + ].filter(Boolean).join(' :: ')); + + errorGettingExternal.shouldBlockModuleReload = false; + + throw errorGettingExternal; +} +" +`; + +exports[`externals-loader when bundleTarget is server does not include content, gets the dependency from root module 1`] = ` +"try { + const Holocron = require(\\"holocron\\"); + const fallbackExternal = Holocron.getExternal && Holocron.getExternal({ + name: 'my-dependency', + version: '1.2.3' + }); + const rootModuleExternal = global.getTenantRootModule && global.getTenantRootModule().appConfig.providedExternals['my-dependency']; + + module.exports = fallbackExternal || (rootModuleExternal ? rootModuleExternal.module : () => { + throw new Error('[server][undefined] External not found: my-dependency'); + }) } catch (error) { - const errorGettingExternal = new Error('Failed to get external my-dependency from root module'); + const errorGettingExternal = new Error([ + '[server] Failed to get external fallback my-dependency', + error.message + ].filter(Boolean).join(' :: ')); + errorGettingExternal.shouldBlockModuleReload = false; + throw errorGettingExternal; } " diff --git a/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/provided-externals-loader.spec.js.snap b/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/provided-externals-loader.spec.js.snap index 527d00c4..db10b43e 100644 --- a/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/provided-externals-loader.spec.js.snap +++ b/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/provided-externals-loader.spec.js.snap @@ -1,13 +1,59 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports['provided-externals-loader should append the providedExternals to the default export 1'] = ` +exports[`provided-externals-loader accepts an object 1`] = ` "import MyComponent from './components/MyComponent'; export default MyComponent; ; MyComponent.appConfig = Object.assign({}, MyComponent.appConfig, { providedExternals: { - 'ajv': { version: '6.12.6', module: require('ajv')}, - 'lodash': { version: '4.17.21', module: require('lodash')}, + + 'ajv': { + ...{ + \\"fallbackEnabled\\": false +}, + version: '6.12.6', + module: require('ajv'), + }, + + 'lodash': { + ...{ + \\"fallbackEnabled\\": false +}, + version: '4.17.21', + module: require('lodash'), + } + }, +}); + +if(global.getTenantRootModule === undefined || (global.rootModuleName && global.rootModuleName === 'undefined')){ +global.getTenantRootModule = () => MyComponent; +global.rootModuleName = 'undefined'; +} +" +`; + +exports[`provided-externals-loader should append the providedExternals to the default export 1`] = ` +"import MyComponent from './components/MyComponent'; +export default MyComponent; +; +MyComponent.appConfig = Object.assign({}, MyComponent.appConfig, { + providedExternals: { + + 'ajv': { + ...{ + \\"fallbackEnabled\\": false +}, + version: '6.12.6', + module: require('ajv'), + }, + + 'lodash': { + ...{ + \\"fallbackEnabled\\": false +}, + version: '4.17.21', + module: require('lodash'), + } }, }); @@ -18,8 +64,8 @@ global.rootModuleName = 'undefined'; " `; -exports['provided-externals-loader should throw an error when the wrong syntax is used - export default hoc() 1'] = '"@americanexpress/one-app-bundler: Module must use `export default VariableName` in index syntax to use providedExternals"'; +exports[`provided-externals-loader should throw an error when the wrong syntax is used - export default hoc() 1`] = `"@americanexpress/one-app-bundler: Module must use \`export default VariableName\` in index syntax to use providedExternals"`; -exports['provided-externals-loader should throw an error when the wrong syntax is used - export from 1'] = '"@americanexpress/one-app-bundler: Module must use `export default VariableName` in index syntax to use providedExternals"'; +exports[`provided-externals-loader should throw an error when the wrong syntax is used - export from 1`] = `"@americanexpress/one-app-bundler: Module must use \`export default VariableName\` in index syntax to use providedExternals"`; -exports['provided-externals-loader should throw an error when the wrong syntax is used - module.exports 1'] = '"@americanexpress/one-app-bundler: Module must use `export default VariableName` in index syntax to use providedExternals"'; +exports[`provided-externals-loader should throw an error when the wrong syntax is used - module.exports 1`] = `"@americanexpress/one-app-bundler: Module must use \`export default VariableName\` in index syntax to use providedExternals"`; diff --git a/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/validate-externals-loader.spec.js.snap b/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/validate-externals-loader.spec.js.snap index 82e6cd3f..edc7497f 100644 --- a/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/validate-externals-loader.spec.js.snap +++ b/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/validate-externals-loader.spec.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`validate-required-externals-loader should add versions for server side validation 1`] = ` +exports[`validate-required-externals-loader should add versions for legacy server side validation on appConfig 1`] = ` "import SomeComponent from './SomeComponent'; export default SomeComponent; diff --git a/packages/one-app-bundler/__tests__/webpack/loaders/enable-unlisted-external-fallbacks-loader.spec.js b/packages/one-app-bundler/__tests__/webpack/loaders/enable-unlisted-external-fallbacks-loader.spec.js new file mode 100644 index 00000000..d130c6f8 --- /dev/null +++ b/packages/one-app-bundler/__tests__/webpack/loaders/enable-unlisted-external-fallbacks-loader.spec.js @@ -0,0 +1,61 @@ +/* + * Copyright 2023 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations + * under the License. + */ + +const loaderUtils = require('loader-utils'); +const enableUnlistedExternalFallbacksLoader = require('../../../webpack/loaders/enable-unlisted-external-fallbacks-loader'); + +jest.mock('loader-utils', () => ({ + getOptions: jest.fn(() => ({ enableUnlistedExternalFallbacks: true })), +})); + +describe('enable-unlisted-external-fallbacks-loader', () => { + it('should append the enableUnlistedExternalFallbacks to the default export', () => { + loaderUtils.getOptions.mockReturnValueOnce({ enableUnlistedExternalFallbacks: true }); + + const content = `\ +import MyComponent from './components/MyComponent'; +export default MyComponent; +`; + expect(enableUnlistedExternalFallbacksLoader(content)).toMatchSnapshot(); + }); + + it('should throw an error when the wrong syntax is used - export from', () => { + loaderUtils.getOptions.mockReturnValueOnce({ enableUnlistedExternalFallbacks: true }); + + const content = `\ +export default from './components/MyComponent'; +`; + expect(() => enableUnlistedExternalFallbacksLoader(content)).toThrowErrorMatchingSnapshot(); + }); + + it('should throw an error when the wrong syntax is used - module.exports', () => { + loaderUtils.getOptions.mockReturnValueOnce({ enableUnlistedExternalFallbacks: true }); + + const content = `\ +module.exports = require('./components/MyComponent'); +`; + expect(() => enableUnlistedExternalFallbacksLoader(content)).toThrowErrorMatchingSnapshot(); + }); + + it('should throw an error when the wrong syntax is used - export default hoc()', () => { + loaderUtils.getOptions.mockReturnValueOnce({ enableUnlistedExternalFallbacks: true }); + + const content = `\ +import SomeComponent from './SomeComponent'; + +export default hocChain(SomeComponent); +`; + expect(() => enableUnlistedExternalFallbacksLoader(content)).toThrowErrorMatchingSnapshot(); + }); +}); diff --git a/packages/one-app-bundler/__tests__/webpack/loaders/externals-loader.spec.js b/packages/one-app-bundler/__tests__/webpack/loaders/externals-loader.spec.js index 5f61493d..03b449e2 100644 --- a/packages/one-app-bundler/__tests__/webpack/loaders/externals-loader.spec.js +++ b/packages/one-app-bundler/__tests__/webpack/loaders/externals-loader.spec.js @@ -12,14 +12,67 @@ * under the License. */ +const loaderUtils = require('loader-utils'); const externalsLoader = require('../../../webpack/loaders/externals-loader'); jest.mock('loader-utils', () => ({ - getOptions: jest.fn(() => ({ externalName: 'my-dependency' })), + getOptions: jest.fn(() => ({ + externalName: 'my-dependency', + bundleTarget: 'browser', + })), })); +jest.mock('read-pkg-up', () => ({ + sync: () => ({ + packageJson: { + dependencies: { + 'my-dependency': '^1.0.0', + }, + }, + }), +})); + +// mock for the fake external dependency package.json +jest.mock('my-dependency/package.json', () => ({ + version: '1.2.3', +}), { virtual: true }); + describe('externals-loader', () => { - it('should ignore the content and get the dependency from the root module', () => { - expect(externalsLoader('This is some content!')).toMatchSnapshot(); + describe('when bundleTarget is browser', () => { + it('does not include content, gets the dependency from root module', () => { + const content = 'This is some content!'; + const loadedExternalContent = externalsLoader(content); + expect(content).toMatch(/This is some content/); + expect(loadedExternalContent).not.toMatch(/This is some content/); + expect(loadedExternalContent).toMatchSnapshot(); + }); + + it('uses global.Holocron', () => { + const loadedExternalContent = externalsLoader('This is some content!'); + expect(loadedExternalContent).toMatch(/global\.Holocron/); + }); + }); + + describe('when bundleTarget is server', () => { + it('does not include content, gets the dependency from root module', () => { + loaderUtils.getOptions.mockReturnValueOnce({ + externalName: 'my-dependency', + bundleTarget: 'server', + }); + const content = 'This is some content!'; + const loadedExternalContent = externalsLoader(content); + expect(content).toMatch(/This is some content/); + expect(loadedExternalContent).not.toMatch(/This is some content/); + expect(loadedExternalContent).toMatchSnapshot(); + }); + + it('requires Holocron', () => { + loaderUtils.getOptions.mockReturnValueOnce({ + externalName: 'my-dependency', + bundleTarget: 'server', + }); + const loadedExternalContent = externalsLoader('This is some content!'); + expect(loadedExternalContent).toMatch(/require\("holocron"\)/); + }); }); }); diff --git a/packages/one-app-bundler/__tests__/webpack/loaders/provided-externals-loader.spec.js b/packages/one-app-bundler/__tests__/webpack/loaders/provided-externals-loader.spec.js index f0546367..fdf18055 100644 --- a/packages/one-app-bundler/__tests__/webpack/loaders/provided-externals-loader.spec.js +++ b/packages/one-app-bundler/__tests__/webpack/loaders/provided-externals-loader.spec.js @@ -12,6 +12,7 @@ * under the License. */ +const loaderUtils = require('loader-utils'); const providedExternalsLoader = require('../../../webpack/loaders/provided-externals-loader'); jest.mock('loader-utils', () => ({ @@ -20,6 +21,18 @@ jest.mock('loader-utils', () => ({ describe('provided-externals-loader', () => { it('should append the providedExternals to the default export', () => { + loaderUtils.getOptions.mockReturnValueOnce({ providedExternals: ['ajv', 'lodash'] }); + + const content = `\ +import MyComponent from './components/MyComponent'; +export default MyComponent; +`; + expect(providedExternalsLoader(content)).toMatchSnapshot(); + }); + + it('accepts an object', () => { + loaderUtils.getOptions.mockReturnValueOnce({ providedExternals: { ajv: {}, lodash: {} } }); + const content = `\ import MyComponent from './components/MyComponent'; export default MyComponent; @@ -28,6 +41,8 @@ export default MyComponent; }); it('should throw an error when the wrong syntax is used - export from', () => { + loaderUtils.getOptions.mockReturnValueOnce({ providedExternals: ['ajv', 'lodash'] }); + const content = `\ export default from './components/MyComponent'; `; @@ -35,6 +50,8 @@ export default from './components/MyComponent'; }); it('should throw an error when the wrong syntax is used - module.exports', () => { + loaderUtils.getOptions.mockReturnValueOnce({ providedExternals: ['ajv', 'lodash'] }); + const content = `\ module.exports = require('./components/MyComponent'); `; @@ -42,6 +59,8 @@ module.exports = require('./components/MyComponent'); }); it('should throw an error when the wrong syntax is used - export default hoc()', () => { + loaderUtils.getOptions.mockReturnValueOnce({ providedExternals: ['ajv', 'lodash'] }); + const content = `\ import SomeComponent from './SomeComponent'; diff --git a/packages/one-app-bundler/__tests__/webpack/loaders/validate-externals-loader.spec.js b/packages/one-app-bundler/__tests__/webpack/loaders/validate-externals-loader.spec.js index 783f6942..54e9070d 100644 --- a/packages/one-app-bundler/__tests__/webpack/loaders/validate-externals-loader.spec.js +++ b/packages/one-app-bundler/__tests__/webpack/loaders/validate-externals-loader.spec.js @@ -12,6 +12,7 @@ * under the License. */ +const fs = require('node:fs'); const readPkgUp = require('read-pkg-up'); const validateExternalsLoader = require('../../../webpack/loaders/validate-required-externals-loader'); @@ -23,11 +24,19 @@ jest.mock('read-pkg-up', () => ({ sync: jest.fn(), })); +jest.mock('node:fs'); + // eslint-disable-next-line global-require -- mocking readPkgUp needs us to require a json file readPkgUp.sync.mockImplementation(() => ({ packageJson: require('../../../package.json') })); +fs.readFileSync = jest.fn(() => '{}'); +fs.writeFileSync = jest.fn(); + describe('validate-required-externals-loader', () => { - it('should add versions for server side validation', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should add versions for legacy server side validation on appConfig', () => { const content = `\ import SomeComponent from './SomeComponent'; @@ -36,6 +45,33 @@ export default SomeComponent; expect(validateExternalsLoader(content)).toMatchSnapshot(); }); + it('adds modules required externals to module-config.json file', () => { + const content = `\ +import SomeComponent from './SomeComponent'; + +export default SomeComponent; +`; + validateExternalsLoader(content); + const [moduleConfigPath, moduleConfig] = fs.writeFileSync.mock.calls[0]; + expect(moduleConfigPath).toMatch('module-config.json'); + expect(moduleConfig).toMatchInlineSnapshot(` +"{ + \\"requiredExternals\\": { + \\"ajv\\": { + \\"name\\": \\"ajv\\", + \\"version\\": \\"6.12.6\\", + \\"semanticRange\\": \\"^6.7.0\\" + }, + \\"lodash\\": { + \\"name\\": \\"lodash\\", + \\"version\\": \\"4.17.21\\", + \\"semanticRange\\": \\"^4.17.20\\" + } + } +}" +`); + }); + it('should throw an error when the wrong syntax is used - export from', () => { const content = `\ export default from './components/MyComponent'; diff --git a/packages/one-app-bundler/__tests__/webpack/module/webpack.client.spec.js b/packages/one-app-bundler/__tests__/webpack/module/webpack.client.spec.js index 3aead170..fb8561af 100644 --- a/packages/one-app-bundler/__tests__/webpack/module/webpack.client.spec.js +++ b/packages/one-app-bundler/__tests__/webpack/module/webpack.client.spec.js @@ -13,7 +13,7 @@ */ const { sync } = require('read-pkg-up'); -const path = require('path'); +const path = require('node:path'); const { validateWebpackConfig } = require('../../../test-utils'); const getConfigOptions = require('../../../utils/getConfigOptions'); const configGenerator = require('../../../webpack/module/webpack.client'); diff --git a/packages/one-app-bundler/__tests__/webpack/ssr-css-loader/index-style-loader.spec.js b/packages/one-app-bundler/__tests__/webpack/ssr-css-loader/index-style-loader.spec.js index c0a0247a..58af436b 100644 --- a/packages/one-app-bundler/__tests__/webpack/ssr-css-loader/index-style-loader.spec.js +++ b/packages/one-app-bundler/__tests__/webpack/ssr-css-loader/index-style-loader.spec.js @@ -14,7 +14,7 @@ const indexStyleLoader = require('../../../webpack/loaders/ssr-css-loader/index-style-loader'); -jest.mock('path', () => ({ +jest.mock('node:path', () => ({ resolve: (dir, filename) => `/path/to/one-app-bundler/webpack/ssr-css-loader/${filename}`, })); diff --git a/packages/one-app-bundler/__tests__/webpack/ssr-css-loader/index.spec.js b/packages/one-app-bundler/__tests__/webpack/ssr-css-loader/index.spec.js index a5ee666e..73681a87 100644 --- a/packages/one-app-bundler/__tests__/webpack/ssr-css-loader/index.spec.js +++ b/packages/one-app-bundler/__tests__/webpack/ssr-css-loader/index.spec.js @@ -14,7 +14,7 @@ const SSRCSSLoader = require('../../../webpack/loaders/ssr-css-loader'); -jest.mock('path', () => ({ +jest.mock('node:path', () => ({ resolve: (dir, filename) => `/path/to/one-app-bundler/webpack/ssr-css-loader/${filename}`, })); diff --git a/packages/one-app-bundler/bin/bundle-module.js b/packages/one-app-bundler/bin/bundle-module.js index 810f91ef..ea11163d 100755 --- a/packages/one-app-bundler/bin/bundle-module.js +++ b/packages/one-app-bundler/bin/bundle-module.js @@ -13,22 +13,32 @@ * under the License. */ -if (process.env.NODE_ENV !== 'production' && process.argv.includes('--dev')) { - console.info('Running dev bundler'); - import('@americanexpress/one-app-dev-bundler').then(({ default: esbuildBundleModule }) => { - if (esbuildBundleModule) { - esbuildBundleModule().catch((error) => { +const bundleModule = async () => { + const { + devBuildModule, + bundleExternalFallbacks, + } = await import('@americanexpress/one-app-dev-bundler'); + + await bundleExternalFallbacks(); + + if (process.env.NODE_ENV !== 'production' && process.argv.includes('--dev')) { + console.info('Running dev bundler'); + + if (devBuildModule) { + devBuildModule().catch((error) => { console.error(`Build failed with error ${error.message}`); console.error(error); throw error; }); } - }); -} else { - if (process.argv.includes('--dev')) { - console.info('Ignoring `--dev` flag for NODE_ENV=production'); + } else { + if (process.argv.includes('--dev')) { + console.info('Ignoring `--dev` flag for NODE_ENV=production'); + } + console.info('Running production bundler'); + // eslint-disable-next-line global-require -- Only require the bundler when it runs + require('./webpack-bundle-module'); } - console.info('Running production bundler'); - // eslint-disable-next-line global-require -- Only require the bundler when it runs - require('./webpack-bundle-module'); -} +}; + +bundleModule(); diff --git a/packages/one-app-bundler/bin/drop-module.js b/packages/one-app-bundler/bin/drop-module.js index bb74e032..1c684317 100755 --- a/packages/one-app-bundler/bin/drop-module.js +++ b/packages/one-app-bundler/bin/drop-module.js @@ -13,8 +13,8 @@ * under the License. */ -const fs = require('fs'); -const path = require('path'); +const fs = require('node:fs'); +const path = require('node:path'); const { argv } = require('yargs'); const publicPath = path.join(process.cwd(), 'static'); diff --git a/packages/one-app-bundler/bin/generateIntegrityManifest.js b/packages/one-app-bundler/bin/generateIntegrityManifest.js index c0a29ecc..0f138904 100644 --- a/packages/one-app-bundler/bin/generateIntegrityManifest.js +++ b/packages/one-app-bundler/bin/generateIntegrityManifest.js @@ -13,8 +13,8 @@ */ const ssri = require('ssri'); -const fs = require('fs'); -const path = require('path'); +const fs = require('node:fs'); +const path = require('node:path'); const generateIntegrityManifest = (label, pathToBundle) => { const integrity = ssri.fromData( diff --git a/packages/one-app-bundler/bin/postProcessOneAppBundle.js b/packages/one-app-bundler/bin/postProcessOneAppBundle.js index 0d0dbf4c..3648f6a7 100644 --- a/packages/one-app-bundler/bin/postProcessOneAppBundle.js +++ b/packages/one-app-bundler/bin/postProcessOneAppBundle.js @@ -15,8 +15,8 @@ /* eslint-disable import/no-dynamic-require, global-require -- we need to load generated assets at runtime */ -const path = require('path'); -const fs = require('fs'); +const path = require('node:path'); +const fs = require('node:fs'); const { hashElement } = require('folder-hash'); const readPkgUp = require('read-pkg-up'); const generateIntegrityManifest = require('./generateIntegrityManifest'); diff --git a/packages/one-app-bundler/bin/webpack-bundle-module.js b/packages/one-app-bundler/bin/webpack-bundle-module.js index 493dffd6..faf9397e 100644 --- a/packages/one-app-bundler/bin/webpack-bundle-module.js +++ b/packages/one-app-bundler/bin/webpack-bundle-module.js @@ -13,8 +13,8 @@ */ const webpack = require('webpack'); -const path = require('path'); -const fs = require('fs'); +const path = require('node:path'); +const fs = require('node:fs'); const localeBundler = require('@americanexpress/one-app-locale-bundler'); const getConfigOptions = require('../utils/getConfigOptions'); @@ -27,7 +27,9 @@ const modernClientConfig = clientConfig('modern'); const legacyClientConfig = clientConfig('legacy'); fs.writeFileSync(path.join(process.cwd(), 'bundle.integrity.manifest.json'), JSON.stringify({})); + localeBundler(watch); + webpack(serverConfig, getWebpackCallback('node', true)); webpack(modernClientConfig, getWebpackCallback('browser', true)); diff --git a/packages/one-app-bundler/bin/webpackCallback.js b/packages/one-app-bundler/bin/webpackCallback.js index 25f299a6..98527507 100644 --- a/packages/one-app-bundler/bin/webpackCallback.js +++ b/packages/one-app-bundler/bin/webpackCallback.js @@ -13,8 +13,8 @@ */ const chalk = require('chalk'); -const fs = require('fs'); -const path = require('path'); +const fs = require('node:fs'); +const path = require('node:path'); const generateIntegrityManifest = require('./generateIntegrityManifest'); module.exports = function getWebpackCallback(label, isModuleBuild) { diff --git a/packages/one-app-bundler/package.json b/packages/one-app-bundler/package.json index 4d758f3a..a41811da 100644 --- a/packages/one-app-bundler/package.json +++ b/packages/one-app-bundler/package.json @@ -75,6 +75,7 @@ "webpack-custom-chunk-id-plugin": "^1.0.3", "webpack-dynamic-public-path": "^1.0.4", "webpack-merge": "^4.2.1", + "webpack-sources": "^3.2.3", "webpack-subresource-integrity": "^1.3.4", "yargs": "^16.2.0" }, diff --git a/packages/one-app-bundler/utils/extendWebpackConfig.js b/packages/one-app-bundler/utils/extendWebpackConfig.js index 4058a7c4..a304c68c 100644 --- a/packages/one-app-bundler/utils/extendWebpackConfig.js +++ b/packages/one-app-bundler/utils/extendWebpackConfig.js @@ -12,7 +12,7 @@ * under the License. */ -const path = require('path'); +const path = require('node:path'); const merge = require('webpack-merge'); const uniqBy = require('lodash/uniqBy'); const getConfigOptions = require('./getConfigOptions'); @@ -29,6 +29,16 @@ function getCustomWebpackConfigPath(options, bundleTarget) { return false; } +function parseProvidedExternals(providedExternals) { + return Array.isArray(providedExternals) + ? providedExternals.reduce((obj, externalName) => ({ + ...obj, + [externalName]: { + enableFallback: false, + }, + }), {}) : providedExternals; +} + function extendWebpackConfig(webpackConfig, bundleTarget) { const configOptions = getConfigOptions(); const cliOptions = getCliOptions(); @@ -39,6 +49,7 @@ function extendWebpackConfig(webpackConfig, bundleTarget) { requiredExternals, providedExternals, moduleName, + enableUnlistedExternalFallbacks, } = configOptions; const { watch } = cliOptions; @@ -53,6 +64,7 @@ function extendWebpackConfig(webpackConfig, bundleTarget) { const indexPath = path.join(process.cwd(), 'src', 'index'); if (providedExternals) { + const parsedProvidedExternals = parseProvidedExternals(providedExternals); customWebpackConfig = merge(customWebpackConfig, { module: { rules: [{ @@ -60,7 +72,7 @@ function extendWebpackConfig(webpackConfig, bundleTarget) { use: [{ loader: '@americanexpress/one-app-bundler/webpack/loaders/provided-externals-loader', options: { - providedExternals, + providedExternals: parsedProvidedExternals, moduleName, }, }], @@ -78,6 +90,7 @@ function extendWebpackConfig(webpackConfig, bundleTarget) { loader: '@americanexpress/one-app-bundler/webpack/loaders/externals-loader', options: { externalName, + bundleTarget, }, }], })), { @@ -93,6 +106,22 @@ function extendWebpackConfig(webpackConfig, bundleTarget) { }); } + if (enableUnlistedExternalFallbacks) { + customWebpackConfig = merge(customWebpackConfig, { + module: { + rules: [{ + test: indexPath, + use: [{ + loader: '@americanexpress/one-app-bundler/webpack/loaders/enable-unlisted-external-fallbacks-loader', + options: { + enableUnlistedExternalFallbacks, + }, + }], + }], + }, + }); + } + if (appCompatibility) { customWebpackConfig = merge(customWebpackConfig, { module: { diff --git a/packages/one-app-bundler/utils/getConfigOptions.js b/packages/one-app-bundler/utils/getConfigOptions.js index 31fdf8b5..0ed3beda 100644 --- a/packages/one-app-bundler/utils/getConfigOptions.js +++ b/packages/one-app-bundler/utils/getConfigOptions.js @@ -25,10 +25,16 @@ function validateOptions(options) { if (options.requiredExternals || options.providedExternals) { const intersection = Object.keys(commonConfig.externals) - .filter((externalName) => - // eslint-disable-next-line implicit-arrow-linebreak -- without the linebreak the line fails max-len - (options.requiredExternals || options.providedExternals).includes(externalName) - ); + .filter((externalName) => { + if (options.providedExternals) { + const providedExternals = Array.isArray(options.providedExternals) + ? options.providedExternals + : Object.keys(options.providedExternals); + return providedExternals.includes(externalName); + } + return options.requiredExternals.includes(externalName); + }); + if (intersection.length > 0) { throw new Error(`@americanexpress/one-app-bundler: Attempted to bundle ${intersection.join(', ')}, but modules cannot provide externals that One App includes.`); } diff --git a/packages/one-app-bundler/utils/validation/index.js b/packages/one-app-bundler/utils/validation/index.js index 3c580731..a35ae08c 100644 --- a/packages/one-app-bundler/utils/validation/index.js +++ b/packages/one-app-bundler/utils/validation/index.js @@ -15,11 +15,14 @@ */ const Joi = require('joi'); -const externalsSchema = Joi.array().items(Joi.string().required()).messages({ - 'array.base': 'Externals must be an array', - 'array.includesRequiredUnknowns': 'Externals must have at least one entry', - 'string.base': 'Externals must contain strings', -}); +const externalsSchema = Joi.alternatives().try( + Joi.array().items(Joi.string().required()).messages({ + 'array.base': 'Externals must be an array', + 'array.includesRequiredUnknowns': 'Externals must have at least one entry', + 'string.base': 'Externals must contain strings', + }), + Joi.object().pattern(/^/, Joi.object({ fallbackEnabled: Joi.boolean() })) +); const webpackConfigSchema = Joi.string().messages({ 'string.base': 'Webpack Configs must be a string', @@ -59,6 +62,7 @@ const purgecssSchema = Joi.object({ }); const optionsSchema = Joi.object({ + enableUnlistedExternalFallbacks: Joi.boolean(), providedExternals: externalsSchema, requiredExternals: externalsSchema, performanceBudget: performanceBudgetSchema, diff --git a/packages/one-app-bundler/webpack/app/webpack.client.js b/packages/one-app-bundler/webpack/app/webpack.client.js index 4f45d20a..86f5b448 100644 --- a/packages/one-app-bundler/webpack/app/webpack.client.js +++ b/packages/one-app-bundler/webpack/app/webpack.client.js @@ -14,7 +14,7 @@ const webpack = require('webpack'); const merge = require('webpack-merge'); -const path = require('path'); +const path = require('node:path'); const TerserPlugin = require('terser-webpack-plugin'); const coreJsCompat = require('core-js-compat'); const coreJsEntries = require('core-js-compat/entries'); diff --git a/packages/one-app-bundler/webpack/loaders/common.js b/packages/one-app-bundler/webpack/loaders/common.js index 6008c416..19052950 100644 --- a/packages/one-app-bundler/webpack/loaders/common.js +++ b/packages/one-app-bundler/webpack/loaders/common.js @@ -12,7 +12,7 @@ * under the License. */ -const path = require('path'); +const path = require('node:path'); const dartSass = require('sass'); const getConfigOptions = require('../../utils/getConfigOptions'); diff --git a/packages/one-app-bundler/webpack/loaders/enable-unlisted-external-fallbacks-loader.js b/packages/one-app-bundler/webpack/loaders/enable-unlisted-external-fallbacks-loader.js new file mode 100644 index 00000000..fbb1a0cf --- /dev/null +++ b/packages/one-app-bundler/webpack/loaders/enable-unlisted-external-fallbacks-loader.js @@ -0,0 +1,35 @@ +/* + * Copyright 2023 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations + * under the License. + */ + +const loaderUtils = require('loader-utils'); + +function enableUnlistedExternalFallbacksLoader(content) { + const { enableUnlistedExternalFallbacks } = loaderUtils.getOptions(this); + const match = content.match(/export\s+default\s+(?!from)(\w+);$/m); + + if (match) { + const newContent = `${content}; +if (!global.BROWSER) { + ${match[1]}.appConfig = Object.assign({}, ${match[1]}.appConfig, { + enableUnlistedExternalFallbacks: "${enableUnlistedExternalFallbacks}", + }); +} +`; + return newContent; + } + + throw new Error('@americanexpress/one-app-bundler: Module must use `export default VariableName` in index syntax to use app compatibility validation'); +} + +module.exports = enableUnlistedExternalFallbacksLoader; diff --git a/packages/one-app-bundler/webpack/loaders/externals-loader.js b/packages/one-app-bundler/webpack/loaders/externals-loader.js index 77555364..9ac42976 100644 --- a/packages/one-app-bundler/webpack/loaders/externals-loader.js +++ b/packages/one-app-bundler/webpack/loaders/externals-loader.js @@ -11,20 +11,39 @@ * or implied. See the License for the specific language governing permissions and limitations * under the License. */ - const loaderUtils = require('loader-utils'); +const readPkgUp = require('read-pkg-up'); + +const { packageJson } = readPkgUp.sync(); + +function externalsLoader() { + const { externalName, bundleTarget } = loaderUtils.getOptions(this); + // eslint-disable-next-line global-require, import/no-dynamic-require -- need to require a package.json at runtime + const { version } = require(`${externalName}/package.json`); -function requiredExternalsLoader() { - const options = loaderUtils.getOptions(this); return `\ try { - module.exports = global.getTenantRootModule().appConfig.providedExternals['${options.externalName}'].module; + const Holocron = ${bundleTarget === 'server' ? 'require("holocron")' : 'global.Holocron'}; + const fallbackExternal = Holocron.getExternal && Holocron.getExternal({ + name: '${externalName}', + version: '${version}' + }); + const rootModuleExternal = global.getTenantRootModule && global.getTenantRootModule().appConfig.providedExternals['${externalName}']; + + module.exports = fallbackExternal || (rootModuleExternal ? rootModuleExternal.module : () => { + throw new Error('[${bundleTarget}][${packageJson.name}] External not found: ${externalName}'); + }) } catch (error) { - const errorGettingExternal = new Error('Failed to get external ${options.externalName} from root module'); + const errorGettingExternal = new Error([ + '[${bundleTarget}] Failed to get external fallback ${externalName}', + error.message + ].filter(Boolean).join(' :: ')); + errorGettingExternal.shouldBlockModuleReload = false; + throw errorGettingExternal; } `; } -module.exports = requiredExternalsLoader; +module.exports = externalsLoader; diff --git a/packages/one-app-bundler/webpack/loaders/provided-externals-loader.js b/packages/one-app-bundler/webpack/loaders/provided-externals-loader.js index 6fcc8ef7..cf836ec7 100644 --- a/packages/one-app-bundler/webpack/loaders/provided-externals-loader.js +++ b/packages/one-app-bundler/webpack/loaders/provided-externals-loader.js @@ -15,25 +15,40 @@ const loaderUtils = require('loader-utils'); function providedExternalsLoader(content) { - const options = loaderUtils.getOptions(this); - const providedExternals = options.providedExternals.map((externalName) => { + const { moduleName, providedExternals } = loaderUtils.getOptions(this); + + const extendedProvidedExternals = (Array.isArray(providedExternals) + ? providedExternals : Object.keys(providedExternals)).map((externalName) => { // eslint-disable-next-line global-require, import/no-dynamic-require -- need to require a package.json at runtime - const { version } = require(`${externalName}/package.json`); - return `'${externalName}': { version: '${version}', module: require('${externalName}')}`; - }); + const externalPkg = require(`${externalName}/package.json`); + + return ` + '${externalName}': { + ...${JSON.stringify({ + fallbackEnabled: false, + ...providedExternals[externalName], + }, null, 2)}, + version: '${externalPkg.version}', + module: require('${externalName}'), + }`; + }, {}); + const match = content.match(/export\s+default\s+(?!from)(\w+);$/m); if (match) { return `${content}; ${match[1]}.appConfig = Object.assign({}, ${match[1]}.appConfig, { providedExternals: { - ${providedExternals.join(',\n ')}, + ${ + // NOTE: We need to use 'join' instead of JSON.stringify because it performs some escaping. + extendedProvidedExternals.join(', \n') +} }, }); -if(global.getTenantRootModule === undefined || (global.rootModuleName && global.rootModuleName === '${options.moduleName}')){ +if(global.getTenantRootModule === undefined || (global.rootModuleName && global.rootModuleName === '${moduleName}')){ global.getTenantRootModule = () => ${match[1]}; -global.rootModuleName = '${options.moduleName}'; +global.rootModuleName = '${moduleName}'; } `; } diff --git a/packages/one-app-bundler/webpack/loaders/ssr-css-loader/index-style-loader.js b/packages/one-app-bundler/webpack/loaders/ssr-css-loader/index-style-loader.js index 7d1f2bae..f5f4e4d9 100644 --- a/packages/one-app-bundler/webpack/loaders/ssr-css-loader/index-style-loader.js +++ b/packages/one-app-bundler/webpack/loaders/ssr-css-loader/index-style-loader.js @@ -12,7 +12,7 @@ * under the License. */ -const path = require('path'); +const path = require('node:path'); // stringify handles win32 path slashes too // so `C:\path\node_modules` doesn't turn into something with a newline diff --git a/packages/one-app-bundler/webpack/loaders/ssr-css-loader/index.js b/packages/one-app-bundler/webpack/loaders/ssr-css-loader/index.js index 5f2b75f0..48f875f7 100644 --- a/packages/one-app-bundler/webpack/loaders/ssr-css-loader/index.js +++ b/packages/one-app-bundler/webpack/loaders/ssr-css-loader/index.js @@ -12,7 +12,7 @@ * under the License. */ -const path = require('path'); +const path = require('node:path'); // stringify handles win32 path slashes too // so `C:\path\node_modules` doesn't turn into something with a newline diff --git a/packages/one-app-bundler/webpack/loaders/validate-required-externals-loader.js b/packages/one-app-bundler/webpack/loaders/validate-required-externals-loader.js index bf31c75b..beca66de 100644 --- a/packages/one-app-bundler/webpack/loaders/validate-required-externals-loader.js +++ b/packages/one-app-bundler/webpack/loaders/validate-required-externals-loader.js @@ -12,17 +12,38 @@ * under the License. */ +const fs = require('node:fs'); +const path = require('node:path'); const loaderUtils = require('loader-utils'); const readPkgUp = require('read-pkg-up'); function validateRequiredExternalsLoader(content) { const options = loaderUtils.getOptions(this); const { packageJson } = readPkgUp.sync(); + const integrityManifest = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'bundle.integrity.manifest.json'), 'utf-8')); - const requiredExternals = options.requiredExternals.map((externalName) => { + const requiredExternals = options.requiredExternals.reduce((obj, externalName) => { + // eslint-disable-next-line global-require, import/no-dynamic-require -- need to require a package.json at runtime + const { version } = require(`${externalName}/package.json`); + const semanticRange = packageJson.dependencies[externalName]; + + return { + ...obj, + [externalName]: { + name: externalName, + version, + semanticRange, + integrity: integrityManifest[externalName], + }, + }; + }, {}); + + // NOTE: This is required to keep backwards compatibility with older versions of one-app + const legacyRequiredExternals = options.requiredExternals.map((externalName) => { const version = packageJson.dependencies[externalName]; return `'${externalName}': '${version}'`; }); + const match = content.match(/export\s+default\s+(?!from)(\w+);$/m); if (match) { @@ -30,12 +51,18 @@ function validateRequiredExternalsLoader(content) { if (!global.BROWSER) { ${match[1]}.appConfig = Object.assign({}, ${match[1]}.appConfig, { requiredExternals: { - ${requiredExternals.join(',\n ')}, + ${legacyRequiredExternals.join(',\n ')}, }, }); } `; + // NOTE: This is temporary. Since we only need 'requiredExternals' in module-config.json + // we create, for now, the file right here with the data. + fs.writeFileSync(path.resolve(process.cwd(), 'build', packageJson.version, 'module-config.json'), JSON.stringify({ + requiredExternals, + }, null, 2)); + return newContent; } diff --git a/packages/one-app-bundler/webpack/module/webpack.client.js b/packages/one-app-bundler/webpack/module/webpack.client.js index 4e809a76..d5c1f1e9 100644 --- a/packages/one-app-bundler/webpack/module/webpack.client.js +++ b/packages/one-app-bundler/webpack/module/webpack.client.js @@ -13,7 +13,7 @@ */ const webpack = require('webpack'); -const path = require('path'); +const path = require('node:path'); const merge = require('webpack-merge'); const readPkgUp = require('read-pkg-up'); const WebpackDynamicPublicPathPlugin = require('webpack-dynamic-public-path'); @@ -39,6 +39,7 @@ const { version, name } = packageJson; const holocronModuleName = `holocronModule_${name.replace(/-/g, '_')}`; module.exports = (babelEnv) => { const configOptions = getConfigOptions(); + return extendWebpackConfig(merge( commonConfig, { diff --git a/packages/one-app-bundler/webpack/module/webpack.server.js b/packages/one-app-bundler/webpack/module/webpack.server.js index a2c2f06d..48150ef3 100644 --- a/packages/one-app-bundler/webpack/module/webpack.server.js +++ b/packages/one-app-bundler/webpack/module/webpack.server.js @@ -12,7 +12,7 @@ * under the License. */ -const path = require('path'); +const path = require('node:path'); const webpack = require('webpack'); const merge = require('webpack-merge'); const readPkgUp = require('read-pkg-up'); diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/__snapshots__/generateESBuildOptions.spec.js.snap b/packages/one-app-dev-bundler/__tests__/esbuild/__snapshots__/generateESBuildOptions.spec.js.snap index 58ffeae5..377d4df7 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/__snapshots__/generateESBuildOptions.spec.js.snap +++ b/packages/one-app-dev-bundler/__tests__/esbuild/__snapshots__/generateESBuildOptions.spec.js.snap @@ -92,6 +92,79 @@ Object { } `; +exports[`The generateESBuildOptions function should return the correct values for all build targets when not watching 3`] = ` +Object { + "bundle": true, + "define": Object { + "global": "globalThis", + "global.BROWSER": "true", + }, + "entryPoints": Array [ + "./src/index.js", + ], + "metafile": true, + "minify": false, + "outfile": "/path/to/package/mock/build/versionMock/packageNameMock.browser.js", + "plugins": Array [ + "esbuild_plugin_for(remove-webpack-loader-syntax)", + "esbuild_plugin_for(bundle-asset-size-limiter)([{\\"watch\\":false,\\"useLiveReload\\":false,\\"severity\\":\\"SymbolMock(WARNING)\\"}])", + "esbuild_plugin_for(legacy-bundler)([\\"awesome\\"])", + "esbuild_plugin_for(generate-integrity-manifest)([{\\"bundleName\\":\\"awesome\\"}])", + "esbuild_plugin_for(restrict-runtime-symbols)([{\\"watch\\":false,\\"useLiveReload\\":false,\\"severity\\":\\"SymbolMock(WARNING)\\",\\"bundleType\\":\\"SymbolMock(BROWSER)\\"}])", + "esbuild_plugin_for(time-build)([{\\"bundleName\\":\\"awesome\\",\\"watch\\":false}])", + ], + "sourcemap": true, + "target": Array [ + "chrome58", + "firefox57", + "safari11", + ], + "treeShaking": true, +} +`; + +exports[`The generateESBuildOptions function should return the correct values for all build targets when not watching 4`] = ` +Object { + "bundle": true, + "define": Object { + "global.BROWSER": "false", + }, + "entryPoints": Array [ + "./src/index.js", + ], + "external": Array [ + "@americanexpress/one-app-router", + "create-shared-react-context", + "holocron", + "react", + "react-dom", + "redux", + "react-redux", + "reselect", + "immutable", + "@americanexpress/one-app-ducks", + "holocron-module-route", + "prop-types", + "react-helmet", + ], + "metafile": true, + "minify": false, + "outfile": "/path/to/package/mock/build/versionMock/packageNameMock.node.js", + "platform": "node", + "plugins": Array [ + "esbuild_plugin_for(remove-webpack-loader-syntax)", + "esbuild_plugin_for(bundle-asset-size-limiter)([{\\"watch\\":false,\\"useLiveReload\\":false,\\"severity\\":\\"SymbolMock(WARNING)\\"}])", + "esbuild_plugin_for(generate-integrity-manifest)([{\\"bundleName\\":\\"awesome\\"}])", + "esbuild_plugin_for(restrict-runtime-symbols)([{\\"watch\\":false,\\"useLiveReload\\":false,\\"severity\\":\\"SymbolMock(WARNING)\\",\\"bundleType\\":\\"SymbolMock(BROWSER)\\"}])", + "esbuild_plugin_for(time-build)([{\\"bundleName\\":\\"awesome\\",\\"watch\\":false}])", + ], + "target": Array [ + "node12", + ], + "treeShaking": true, +} +`; + exports[`The generateESBuildOptions function should return the correct values for all build targets when on prod mode 1`] = ` Object { "bundle": true, @@ -184,6 +257,79 @@ Object { } `; +exports[`The generateESBuildOptions function should return the correct values for all build targets when on prod mode 3`] = ` +Object { + "bundle": true, + "define": Object { + "global": "globalThis", + "global.BROWSER": "true", + }, + "entryPoints": Array [ + "./src/index.js", + ], + "metafile": true, + "minify": true, + "outfile": "/path/to/package/mock/build/versionMock/packageNameMock.browser.js", + "plugins": Array [ + "esbuild_plugin_for(remove-webpack-loader-syntax)", + "esbuild_plugin_for(bundle-asset-size-limiter)([{\\"watch\\":false,\\"useLiveReload\\":false,\\"severity\\":\\"SymbolMock(ERROR)\\"}])", + "esbuild_plugin_for(legacy-bundler)([\\"awesome\\"])", + "esbuild_plugin_for(generate-integrity-manifest)([{\\"bundleName\\":\\"awesome\\"}])", + "esbuild_plugin_for(restrict-runtime-symbols)([{\\"watch\\":false,\\"useLiveReload\\":false,\\"severity\\":\\"SymbolMock(ERROR)\\",\\"bundleType\\":\\"SymbolMock(BROWSER)\\"}])", + "esbuild_plugin_for(time-build)([{\\"bundleName\\":\\"awesome\\",\\"watch\\":false}])", + ], + "sourcemap": false, + "target": Array [ + "chrome58", + "firefox57", + "safari11", + ], + "treeShaking": true, +} +`; + +exports[`The generateESBuildOptions function should return the correct values for all build targets when on prod mode 4`] = ` +Object { + "bundle": true, + "define": Object { + "global.BROWSER": "false", + }, + "entryPoints": Array [ + "./src/index.js", + ], + "external": Array [ + "@americanexpress/one-app-router", + "create-shared-react-context", + "holocron", + "react", + "react-dom", + "redux", + "react-redux", + "reselect", + "immutable", + "@americanexpress/one-app-ducks", + "holocron-module-route", + "prop-types", + "react-helmet", + ], + "metafile": true, + "minify": true, + "outfile": "/path/to/package/mock/build/versionMock/packageNameMock.node.js", + "platform": "node", + "plugins": Array [ + "esbuild_plugin_for(remove-webpack-loader-syntax)", + "esbuild_plugin_for(bundle-asset-size-limiter)([{\\"watch\\":false,\\"useLiveReload\\":false,\\"severity\\":\\"SymbolMock(ERROR)\\"}])", + "esbuild_plugin_for(generate-integrity-manifest)([{\\"bundleName\\":\\"awesome\\"}])", + "esbuild_plugin_for(restrict-runtime-symbols)([{\\"watch\\":false,\\"useLiveReload\\":false,\\"severity\\":\\"SymbolMock(ERROR)\\",\\"bundleType\\":\\"SymbolMock(BROWSER)\\"}])", + "esbuild_plugin_for(time-build)([{\\"bundleName\\":\\"awesome\\",\\"watch\\":false}])", + ], + "target": Array [ + "node12", + ], + "treeShaking": true, +} +`; + exports[`The generateESBuildOptions function should return the correct values for all build targets when watching 1`] = ` Object { "bundle": true, @@ -281,3 +427,82 @@ Object { }, } `; + +exports[`The generateESBuildOptions function should return the correct values for all build targets when watching 3`] = ` +Object { + "bundle": true, + "define": Object { + "global": "globalThis", + "global.BROWSER": "true", + }, + "entryPoints": Array [ + "./src/index.js", + ], + "metafile": true, + "minify": false, + "outfile": "/path/to/package/mock/build/versionMock/packageNameMock.browser.js", + "plugins": Array [ + "esbuild_plugin_for(remove-webpack-loader-syntax)", + "esbuild_plugin_for(bundle-asset-size-limiter)([{\\"watch\\":true,\\"useLiveReload\\":false,\\"severity\\":\\"SymbolMock(WARNING)\\"}])", + "esbuild_plugin_for(legacy-bundler)([\\"awesome\\"])", + "esbuild_plugin_for(generate-integrity-manifest)([{\\"bundleName\\":\\"awesome\\"}])", + "esbuild_plugin_for(restrict-runtime-symbols)([{\\"watch\\":true,\\"useLiveReload\\":false,\\"severity\\":\\"SymbolMock(WARNING)\\",\\"bundleType\\":\\"SymbolMock(BROWSER)\\"}])", + "esbuild_plugin_for(time-build)([{\\"bundleName\\":\\"awesome\\",\\"watch\\":true}])", + ], + "sourcemap": true, + "target": Array [ + "chrome58", + "firefox57", + "safari11", + ], + "treeShaking": true, + "watch": Object { + "onRebuild": [Function], + }, +} +`; + +exports[`The generateESBuildOptions function should return the correct values for all build targets when watching 4`] = ` +Object { + "bundle": true, + "define": Object { + "global.BROWSER": "false", + }, + "entryPoints": Array [ + "./src/index.js", + ], + "external": Array [ + "@americanexpress/one-app-router", + "create-shared-react-context", + "holocron", + "react", + "react-dom", + "redux", + "react-redux", + "reselect", + "immutable", + "@americanexpress/one-app-ducks", + "holocron-module-route", + "prop-types", + "react-helmet", + ], + "metafile": true, + "minify": false, + "outfile": "/path/to/package/mock/build/versionMock/packageNameMock.node.js", + "platform": "node", + "plugins": Array [ + "esbuild_plugin_for(remove-webpack-loader-syntax)", + "esbuild_plugin_for(bundle-asset-size-limiter)([{\\"watch\\":true,\\"useLiveReload\\":false,\\"severity\\":\\"SymbolMock(WARNING)\\"}])", + "esbuild_plugin_for(generate-integrity-manifest)([{\\"bundleName\\":\\"awesome\\"}])", + "esbuild_plugin_for(restrict-runtime-symbols)([{\\"watch\\":true,\\"useLiveReload\\":false,\\"severity\\":\\"SymbolMock(WARNING)\\",\\"bundleType\\":\\"SymbolMock(BROWSER)\\"}])", + "esbuild_plugin_for(time-build)([{\\"bundleName\\":\\"awesome\\",\\"watch\\":true}])", + ], + "target": Array [ + "node12", + ], + "treeShaking": true, + "watch": Object { + "onRebuild": [Function], + }, +} +`; diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/generateESBuildOptions.spec.js b/packages/one-app-dev-bundler/__tests__/esbuild/generateESBuildOptions.spec.js index fd186dd9..d901715b 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/generateESBuildOptions.spec.js +++ b/packages/one-app-dev-bundler/__tests__/esbuild/generateESBuildOptions.spec.js @@ -95,7 +95,7 @@ describe('The generateESBuildOptions function', () => { }); it('should return the correct values for all build targets when not watching', async () => { - expect.assertions(4); + expect.assertions(6); const configs = await generateESBuildOptions(mockOptions); // When assessing these snapshots ask yourself at-least the following questions: // Should these changes have impacted all three configs, just two, or only one? @@ -105,25 +105,29 @@ describe('The generateESBuildOptions function', () => { // Are new plugins mocked to properly show the params they are passed. Are these params correct? expect(configs.browserConfig).toMatchSnapshot(); expect(configs.nodeConfig).toMatchSnapshot(); + expect(configs.buildExternalsConfig('browser', 'awesome')).toMatchSnapshot(); + expect(configs.buildExternalsConfig('server', 'awesome')).toMatchSnapshot(); expect(console).not.toHaveLogs(); expect(console).not.toHaveErrors(); }); it('should return the correct values for all build targets when watching', async () => { - expect.assertions(4); + expect.assertions(6); const configs = await generateESBuildOptions({ ...mockOptions, watch: true }); // As well as asking the questions from the previous test ask your self these questions: // Are these changes relevant to the 'watch' flow, which should be as performant as possible? expect(configs.browserConfig).toMatchSnapshot(); expect(configs.nodeConfig).toMatchSnapshot(); + expect(configs.buildExternalsConfig('browser', 'awesome')).toMatchSnapshot(); + expect(configs.buildExternalsConfig('server', 'awesome')).toMatchSnapshot(); expect(console).not.toHaveLogs(); expect(console).not.toHaveErrors(); }); it('should return the correct values for all build targets when on prod mode', async () => { - expect.assertions(4); + expect.assertions(6); process.env.NODE_ENV = 'production'; const configs = await generateESBuildOptions(mockOptions); // As well as asking the questions from the first test ask your self these questions: @@ -133,6 +137,8 @@ describe('The generateESBuildOptions function', () => { // If they do, consider that this bundler change might require a one app major version? expect(configs.browserConfig).toMatchSnapshot(); expect(configs.nodeConfig).toMatchSnapshot(); + expect(configs.buildExternalsConfig('browser', 'awesome')).toMatchSnapshot(); + expect(configs.buildExternalsConfig('server', 'awesome')).toMatchSnapshot(); expect(console).not.toHaveLogs(); expect(console).not.toHaveErrors(); diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index.input.jsx b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index.input.jsx index 750bc9d6..3c3dd2e6 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index.input.jsx +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index.input.jsx @@ -14,7 +14,7 @@ * permissions and limitations under the License. */ -const ModuleRootComponent = ({children}) => ( +const ModuleRootComponent = ({ children }) => (
{children}
diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_browser-not-watching.output.jsx b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_browser-not-watching.output.jsx index bfd55da0..57613952 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_browser-not-watching.output.jsx +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_browser-not-watching.output.jsx @@ -14,24 +14,32 @@ * permissions and limitations under the License. */ -const ModuleRootComponent = ({children}) => ( +const ModuleRootComponent = ({ children }) => (
{children}
); export default ModuleRootComponent; -; + ModuleRootComponent.appConfig = Object.assign({}, ModuleRootComponent.appConfig, { providedExternals: { - 'external-package-1': { version: '1.2.3', module: require('external-package-1')}, - 'external-package-2': { version: '4.5.6', module: require('external-package-2')}, + 'external-package-1': { + ...{"fallbackEnabled":false}, + version: '1.2.3', + module: require('external-package-1') + }, + 'external-package-2': { + ...{"fallbackEnabled":false}, + version: '4.5.6', + module: require('external-package-2') + }, }, }); if(globalThis.getTenantRootModule === undefined || (globalThis.rootModuleName && globalThis.rootModuleName === 'axp-mock-module-name')){ -globalThis.getTenantRootModule = () => ModuleRootComponent; -globalThis.rootModuleName = 'axp-mock-module-name'; -} -; + globalThis.getTenantRootModule = () => ModuleRootComponent; + globalThis.rootModuleName = 'axp-mock-module-name'; +}; + Holocron.registerModule("axp-mock-module-name", ModuleRootComponent); diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_browser-watching-live.output.jsx b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_browser-watching-live.output.jsx index 4c3e9ff6..9c43b021 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_browser-watching-live.output.jsx +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_browser-watching-live.output.jsx @@ -14,7 +14,7 @@ * permissions and limitations under the License. */ -const ModuleRootComponent = ({children}) => ( +const ModuleRootComponent = ({ children }) => (
{children}
@@ -114,17 +114,25 @@ LiveModuleContainer.LiveWrappedModule = ModuleRootComponent; // finally export the LiveModuleContainer instead of the ModuleContainer export default LiveModuleContainer; -; + LiveModuleContainer.appConfig = Object.assign({}, LiveModuleContainer.appConfig, { providedExternals: { - 'external-package-1': { version: '1.2.3', module: require('external-package-1')}, - 'external-package-2': { version: '4.5.6', module: require('external-package-2')}, + 'external-package-1': { + ...{"fallbackEnabled":false}, + version: '1.2.3', + module: require('external-package-1') + }, + 'external-package-2': { + ...{"fallbackEnabled":false}, + version: '4.5.6', + module: require('external-package-2') + }, }, }); if(globalThis.getTenantRootModule === undefined || (globalThis.rootModuleName && globalThis.rootModuleName === 'axp-mock-module-name')){ -globalThis.getTenantRootModule = () => LiveModuleContainer; -globalThis.rootModuleName = 'axp-mock-module-name'; -} -; + globalThis.getTenantRootModule = () => LiveModuleContainer; + globalThis.rootModuleName = 'axp-mock-module-name'; +}; + Holocron.registerModule("axp-mock-module-name", LiveModuleContainer); diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_browser-watching-not-live.output.jsx b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_browser-watching-not-live.output.jsx index bfd55da0..57613952 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_browser-watching-not-live.output.jsx +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_browser-watching-not-live.output.jsx @@ -14,24 +14,32 @@ * permissions and limitations under the License. */ -const ModuleRootComponent = ({children}) => ( +const ModuleRootComponent = ({ children }) => (
{children}
); export default ModuleRootComponent; -; + ModuleRootComponent.appConfig = Object.assign({}, ModuleRootComponent.appConfig, { providedExternals: { - 'external-package-1': { version: '1.2.3', module: require('external-package-1')}, - 'external-package-2': { version: '4.5.6', module: require('external-package-2')}, + 'external-package-1': { + ...{"fallbackEnabled":false}, + version: '1.2.3', + module: require('external-package-1') + }, + 'external-package-2': { + ...{"fallbackEnabled":false}, + version: '4.5.6', + module: require('external-package-2') + }, }, }); if(globalThis.getTenantRootModule === undefined || (globalThis.rootModuleName && globalThis.rootModuleName === 'axp-mock-module-name')){ -globalThis.getTenantRootModule = () => ModuleRootComponent; -globalThis.rootModuleName = 'axp-mock-module-name'; -} -; + globalThis.getTenantRootModule = () => ModuleRootComponent; + globalThis.rootModuleName = 'axp-mock-module-name'; +}; + Holocron.registerModule("axp-mock-module-name", ModuleRootComponent); diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_server-not-watching.output.jsx b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_server-not-watching.output.jsx index 4f00d5f6..c9bc9eca 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_server-not-watching.output.jsx +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_server-not-watching.output.jsx @@ -14,30 +14,41 @@ * permissions and limitations under the License. */ -const ModuleRootComponent = ({children}) => ( +const ModuleRootComponent = ({ children }) => (
{children}
); export default ModuleRootComponent; -; - ModuleRootComponent.appConfig = Object.assign({}, ModuleRootComponent.appConfig, { - appCompatibility: "mockAppCompatibility", - }); -; + +ModuleRootComponent.appConfig = Object.assign({}, ModuleRootComponent.appConfig, { + appCompatibility: "mockAppCompatibility", +}); + ModuleRootComponent.appConfig = Object.assign({}, ModuleRootComponent.appConfig, { providedExternals: { - 'external-package-1': { version: '1.2.3', module: require('external-package-1')}, - 'external-package-2': { version: '4.5.6', module: require('external-package-2')}, + 'external-package-1': { + ...{"fallbackEnabled":false}, + version: '1.2.3', + module: require('external-package-1') + }, + 'external-package-2': { + ...{"fallbackEnabled":false}, + version: '4.5.6', + module: require('external-package-2') + }, }, }); if(global.getTenantRootModule === undefined || (global.rootModuleName && global.rootModuleName === 'axp-mock-module-name')){ -global.getTenantRootModule = () => ModuleRootComponent; -global.rootModuleName = 'axp-mock-module-name'; -} -; + global.getTenantRootModule = () => ModuleRootComponent; + global.rootModuleName = 'axp-mock-module-name'; +}; + +ModuleRootComponent.appConfig = Object.assign({}, ModuleRootComponent.appConfig, { + enableUnlistedExternalFallbacks: "false", +}); ModuleRootComponent.__holocron_module_meta_data__ = { version: 'mockModuleVersion', diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_server-watching-live.output.jsx b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_server-watching-live.output.jsx index 4f00d5f6..c9bc9eca 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_server-watching-live.output.jsx +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_server-watching-live.output.jsx @@ -14,30 +14,41 @@ * permissions and limitations under the License. */ -const ModuleRootComponent = ({children}) => ( +const ModuleRootComponent = ({ children }) => (
{children}
); export default ModuleRootComponent; -; - ModuleRootComponent.appConfig = Object.assign({}, ModuleRootComponent.appConfig, { - appCompatibility: "mockAppCompatibility", - }); -; + +ModuleRootComponent.appConfig = Object.assign({}, ModuleRootComponent.appConfig, { + appCompatibility: "mockAppCompatibility", +}); + ModuleRootComponent.appConfig = Object.assign({}, ModuleRootComponent.appConfig, { providedExternals: { - 'external-package-1': { version: '1.2.3', module: require('external-package-1')}, - 'external-package-2': { version: '4.5.6', module: require('external-package-2')}, + 'external-package-1': { + ...{"fallbackEnabled":false}, + version: '1.2.3', + module: require('external-package-1') + }, + 'external-package-2': { + ...{"fallbackEnabled":false}, + version: '4.5.6', + module: require('external-package-2') + }, }, }); if(global.getTenantRootModule === undefined || (global.rootModuleName && global.rootModuleName === 'axp-mock-module-name')){ -global.getTenantRootModule = () => ModuleRootComponent; -global.rootModuleName = 'axp-mock-module-name'; -} -; + global.getTenantRootModule = () => ModuleRootComponent; + global.rootModuleName = 'axp-mock-module-name'; +}; + +ModuleRootComponent.appConfig = Object.assign({}, ModuleRootComponent.appConfig, { + enableUnlistedExternalFallbacks: "false", +}); ModuleRootComponent.__holocron_module_meta_data__ = { version: 'mockModuleVersion', diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_server-watching-not-live.output.jsx b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_server-watching-not-live.output.jsx index 4f00d5f6..c9bc9eca 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_server-watching-not-live.output.jsx +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/__test_fixtures__/one-app-index-loader/index_server-watching-not-live.output.jsx @@ -14,30 +14,41 @@ * permissions and limitations under the License. */ -const ModuleRootComponent = ({children}) => ( +const ModuleRootComponent = ({ children }) => (
{children}
); export default ModuleRootComponent; -; - ModuleRootComponent.appConfig = Object.assign({}, ModuleRootComponent.appConfig, { - appCompatibility: "mockAppCompatibility", - }); -; + +ModuleRootComponent.appConfig = Object.assign({}, ModuleRootComponent.appConfig, { + appCompatibility: "mockAppCompatibility", +}); + ModuleRootComponent.appConfig = Object.assign({}, ModuleRootComponent.appConfig, { providedExternals: { - 'external-package-1': { version: '1.2.3', module: require('external-package-1')}, - 'external-package-2': { version: '4.5.6', module: require('external-package-2')}, + 'external-package-1': { + ...{"fallbackEnabled":false}, + version: '1.2.3', + module: require('external-package-1') + }, + 'external-package-2': { + ...{"fallbackEnabled":false}, + version: '4.5.6', + module: require('external-package-2') + }, }, }); if(global.getTenantRootModule === undefined || (global.rootModuleName && global.rootModuleName === 'axp-mock-module-name')){ -global.getTenantRootModule = () => ModuleRootComponent; -global.rootModuleName = 'axp-mock-module-name'; -} -; + global.getTenantRootModule = () => ModuleRootComponent; + global.rootModuleName = 'axp-mock-module-name'; +}; + +ModuleRootComponent.appConfig = Object.assign({}, ModuleRootComponent.appConfig, { + enableUnlistedExternalFallbacks: "false", +}); ModuleRootComponent.__holocron_module_meta_data__ = { version: 'mockModuleVersion', diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/cjs-compatibility-hotpatch.spec.js b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/cjs-compatibility-hotpatch.spec.js index 86ec7b12..3e7a1b19 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/cjs-compatibility-hotpatch.spec.js +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/cjs-compatibility-hotpatch.spec.js @@ -14,11 +14,11 @@ * permissions and limitations under the License. */ -import fs from 'fs'; +import fs from 'node:fs'; import cjsCompatibilityHotpatch from '../../../esbuild/plugins/cjs-compatibility-hotpatch'; import { runSetupAndGetLifeHooks } from './__plugin-testing-utils__'; -jest.mock('fs', () => ({ +jest.mock('node:fs', () => ({ promises: { readFile: jest.fn(() => 'const mock = "JavaScript Content";'), writeFile: jest.fn(), diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/externals-loader.spec.js b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/externals-loader.spec.js index 35d0d775..b90ee75b 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/externals-loader.spec.js +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/externals-loader.spec.js @@ -14,6 +14,7 @@ * permissions and limitations under the License. */ +import { readPackageUpSync } from 'read-pkg-up'; import externalsLoader from '../../../esbuild/plugins/externals-loader'; import { runSetupAndGetLifeHooks, runOnLoadHook } from './__plugin-testing-utils__'; import getModulesBundlerConfig from '../../../esbuild/utils/get-modules-bundler-config'; @@ -27,6 +28,14 @@ jest.mock('../../../esbuild/utils/get-modules-bundler-config', () => jest.fn((ke return null; })); +jest.mock('read-pkg-up', () => ({ + readPackageUpSync: jest.fn(() => ({ + packageJson: { + dependencies: {}, + }, + })), +})); + describe('Esbuild plugin externalsLoader', () => { beforeEach(() => { jest.clearAllMocks(); @@ -95,14 +104,29 @@ describe('Esbuild plugin externalsLoader', () => { expect(loader).toEqual('js'); expect(contents).toMatchInlineSnapshot(` -"try { - module.exports = globalThis.getTenantRootModule().appConfig.providedExternals['mock/path/to/file/mock-package-name'].module; -} catch (error) { - const errorGettingExternal = new Error('Failed to get external mock/path/to/file/mock-package-name from root module'); - errorGettingExternal.shouldBlockModuleReload = false; - throw errorGettingExternal; -} " + try { + const Holocron = globalThis.Holocron; + const fallbackExternal = Holocron.getExternal({ + name: 'mock/path/to/file/mock-package-name', + version: 'undefined' + }); + const rootModuleExternal = globalThis.getTenantRootModule && globalThis.getTenantRootModule().appConfig.providedExternals['mock/path/to/file/mock-package-name']; + + module.exports = fallbackExternal || (rootModuleExternal ? rootModuleExternal.module : () => { + throw new Error('[Symbol(BUNDLE_TYPES-BROWSER)][undefined] External not found: mock/path/to/file/mock-package-name'); + }) + } catch (error) { + const errorGettingExternal = new Error([ + '[Symbol(BUNDLE_TYPES-BROWSER)] Failed to get external fallback mock/path/to/file/mock-package-name', + error.message + ].filter(Boolean).join(' :: ')); + + errorGettingExternal.shouldBlockModuleReload = false; + + throw errorGettingExternal; + } + " `); }); it('should transform inputs to outputs for server', async () => { @@ -121,16 +145,63 @@ describe('Esbuild plugin externalsLoader', () => { expect(loader).toEqual('js'); expect(contents).toMatchInlineSnapshot(` -"try { - module.exports = global.getTenantRootModule().appConfig.providedExternals['mock/path/to/file/mock-package-name'].module; -} catch (error) { - const errorGettingExternal = new Error('Failed to get external mock/path/to/file/mock-package-name from root module'); - errorGettingExternal.shouldBlockModuleReload = false; - throw errorGettingExternal; -} " + try { + const Holocron = require(\\"holocron\\"); + const fallbackExternal = Holocron.getExternal({ + name: 'mock/path/to/file/mock-package-name', + version: 'undefined' + }); + const rootModuleExternal = global.getTenantRootModule && global.getTenantRootModule().appConfig.providedExternals['mock/path/to/file/mock-package-name']; + + module.exports = fallbackExternal || (rootModuleExternal ? rootModuleExternal.module : () => { + throw new Error('[Symbol(BUNDLE_TYPES-SERVER)][undefined] External not found: mock/path/to/file/mock-package-name'); + }) + } catch (error) { + const errorGettingExternal = new Error([ + '[Symbol(BUNDLE_TYPES-SERVER)] Failed to get external fallback mock/path/to/file/mock-package-name', + error.message + ].filter(Boolean).join(' :: ')); + + errorGettingExternal.shouldBlockModuleReload = false; + + throw errorGettingExternal; + } + " `); }); }); }); + + describe('readPackageUpSync', () => { + it('function call could return a nullable value', () => { + readPackageUpSync.mockImplementationOnce(() => undefined); + + const plugin = externalsLoader({ bundleType: BUNDLE_TYPES.BROWSER }); + + expect(() => runSetupAndGetLifeHooks(plugin)).toThrowError("Missing 'package.json'"); + }); + + it('throws an error when package json is falsy', () => { + readPackageUpSync.mockImplementationOnce(() => ({ + packageJson: undefined, + })); + + const plugin = externalsLoader({ bundleType: BUNDLE_TYPES.BROWSER }); + + expect(() => runSetupAndGetLifeHooks(plugin)).toThrowError("Missing 'package.json'"); + }); + + it('throws an error when "dependencies" is falsy', () => { + readPackageUpSync.mockImplementationOnce(() => ({ + packageJson: { + dependencies: undefined, + }, + })); + + const plugin = externalsLoader({ bundleType: BUNDLE_TYPES.BROWSER }); + + expect(() => runSetupAndGetLifeHooks(plugin)).toThrowError("'package.json' does not have 'dependencies' key"); + }); + }); }); diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/generate-integrity-manifest.spec.js b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/generate-integrity-manifest.spec.js index 97b8ae86..13671b96 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/generate-integrity-manifest.spec.js +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/generate-integrity-manifest.spec.js @@ -14,11 +14,11 @@ * permissions and limitations under the License. */ -import fs from 'fs'; +import fs from 'node:fs'; import generateIntegrityManifest from '../../../esbuild/plugins/generate-integrity-manifest'; import { runSetupAndGetLifeHooks } from './__plugin-testing-utils__'; -jest.mock('fs', () => ({ +jest.mock('node:fs', () => ({ promises: { readFile: jest.fn(() => 'mockFileContent'), writeFile: jest.fn(() => 'mockWriteFileResponse'), diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/legacy-bundler.spec.js b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/legacy-bundler.spec.js index de9a20d3..6c617578 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/legacy-bundler.spec.js +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/legacy-bundler.spec.js @@ -14,12 +14,12 @@ * permissions and limitations under the License. */ -import fs from 'fs'; +import fs from 'node:fs'; import swc from '@swc/core'; import legacyBundler from '../../../esbuild/plugins/legacy-bundler'; import { runSetupAndGetLifeHooks } from './__plugin-testing-utils__'; -jest.mock('fs', () => ({ +jest.mock('node:fs', () => ({ promises: { readFile: jest.fn(() => 'mockFileContent'), writeFile: jest.fn(() => 'mockWriteFileResponse'), diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/app-compatibility-injector.spec.js b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/app-compatibility-injector.spec.js index 62e81602..a071dc55 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/app-compatibility-injector.spec.js +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/app-compatibility-injector.spec.js @@ -53,10 +53,10 @@ describe('The AppCompatibilityInjector', () => { const finalContent = await serverInjector.inject(mockContent, { rootComponentName: 'rootComponentNameMock' }); expect(finalContent).toMatchInlineSnapshot(` -"mockContent; - rootComponentNameMock.appConfig = Object.assign({}, rootComponentNameMock.appConfig, { - appCompatibility: \\"appCompatibilityMock\\", - }); +"mockContent +rootComponentNameMock.appConfig = Object.assign({}, rootComponentNameMock.appConfig, { + appCompatibility: \\"appCompatibilityMock\\", +}); " `); }); diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/enable-unlisted-external-fallback-injector.spec.js b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/enable-unlisted-external-fallback-injector.spec.js new file mode 100644 index 00000000..3cd679fc --- /dev/null +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/enable-unlisted-external-fallback-injector.spec.js @@ -0,0 +1,70 @@ +/* + * Copyright 2023 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import UnlistedExternalFallbackInjector + from '../../../../esbuild/plugins/one-app-index-loader-injectors/enable-unlisted-external-fallback-injector'; +import { BUNDLE_TYPES } from '../../../../esbuild/constants/enums.js'; +import getModulesBundlerConfig from '../../../../esbuild/utils/get-modules-bundler-config.js'; + +jest.mock('../../../../esbuild/utils/get-modules-bundler-config.js', () => jest.fn()); + +describe('UnlistedExternalFallbackInjector', () => { + describe('server targeted bundle', () => { + it('adds enableUnlistedExternalFallbacks:false by default', async () => { + // getModulesBundlerConfig.mockImplementationOnce(jest.fn()); + const serverInjector = new UnlistedExternalFallbackInjector( + { bundleType: BUNDLE_TYPES.SERVER } + ); + const mockedContent = 'mockedContent'; + const finalContent = await serverInjector.inject(mockedContent, { rootComponentName: 'rootComponentNameMock' }); + expect(finalContent).toMatchInlineSnapshot(` +"mockedContent +rootComponentNameMock.appConfig = Object.assign({}, rootComponentNameMock.appConfig, { + enableUnlistedExternalFallbacks: \\"false\\", +}); +" +`); + }); + + it('does not override enableUnlistedExternalFallbacks', async () => { + getModulesBundlerConfig.mockImplementationOnce(() => true); + const serverInjector = new UnlistedExternalFallbackInjector( + { bundleType: BUNDLE_TYPES.SERVER } + ); + const mockedContent = 'mockedContent'; + const finalContent = await serverInjector.inject(mockedContent, { rootComponentName: 'rootComponentNameMock' }); + expect(finalContent).toMatchInlineSnapshot(` +"mockedContent +rootComponentNameMock.appConfig = Object.assign({}, rootComponentNameMock.appConfig, { + enableUnlistedExternalFallbacks: \\"true\\", +}); +" +`); + }); + }); + + describe('browser targeted bundle', () => { + it('does not include enableUnlistedExternalFallbacks', async () => { + getModulesBundlerConfig.mockImplementationOnce(() => true); + const browserInjector = new UnlistedExternalFallbackInjector( + { bundleType: BUNDLE_TYPES.BROWSER } + ); + const mockedContent = 'mockedContent'; + const finalContent = await browserInjector.inject(mockedContent, { rootComponentName: 'rootComponentNameMock' }); + expect(finalContent).toMatchInlineSnapshot('"mockedContent"'); + }); + }); +}); diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/holocron-module-register-injector.spec.js b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/holocron-module-register-injector.spec.js index c5212280..9ac84ea2 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/holocron-module-register-injector.spec.js +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/holocron-module-register-injector.spec.js @@ -36,7 +36,7 @@ describe('The HolocronModuleRegisterInjector', () => { const finalContent = await browserInjector.inject(mockContent, { rootComponentName: 'rootComponentNameMock' }); expect(finalContent).toMatchInlineSnapshot(` -"mockContent; +"mockContent Holocron.registerModule(\\"packageNameMock\\", rootComponentNameMock); " `); diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/module-metadata-injector.spec.js b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/module-metadata-injector.spec.js index 01fcae05..1a63d285 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/module-metadata-injector.spec.js +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/module-metadata-injector.spec.js @@ -51,8 +51,7 @@ describe('The ModuleMetadataInjector', () => { const finalContent = await serverInjector.inject(mockContent, { rootComponentName: 'rootComponentNameMock' }); expect(finalContent).toMatchInlineSnapshot(` -"mockContent; - +"mockContent rootComponentNameMock.__holocron_module_meta_data__ = { version: 'versionMock', }; diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/output-tests/dev-live-reloader-injector_output.spec.jsx b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/output-tests/dev-live-reloader-injector_output.spec.jsx index 44fbdae9..f2a7b747 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/output-tests/dev-live-reloader-injector_output.spec.jsx +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/output-tests/dev-live-reloader-injector_output.spec.jsx @@ -3,8 +3,8 @@ */ import { act, render, screen } from '@testing-library/react'; import { fromJS, Map as iMap } from 'immutable'; -import path from 'path'; -import fs from 'fs'; +import path from 'node:path'; +import fs from 'node:fs'; import React from 'react'; import { BUNDLE_TYPES } from '../../../../../esbuild/constants/enums'; import DevLiveReloaderInjector diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/provided-externals-injector.spec.js b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/provided-externals-injector.spec.js index 1f609583..5bf723f7 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/provided-externals-injector.spec.js +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader-injectors/provided-externals-injector.spec.js @@ -19,7 +19,14 @@ import ProvidedExternalsInjector import getModulesBundlerConfig from '../../../../esbuild/utils/get-modules-bundler-config'; import { BUNDLE_TYPES } from '../../../../esbuild/constants/enums.js'; -jest.mock('../../../../esbuild/utils/get-modules-bundler-config.js', () => jest.fn(() => ['mockProvidedExternal'])); +jest.mock('../../../../esbuild/utils/get-modules-bundler-config.js', () => jest.fn(() => ({ + mockProvidedExternal: { + fallbackEnabled: false, + }, + mockProvidedExternal2: { + fallbackEnabled: true, + }, +}))); jest.mock('../../../../esbuild/utils/get-meta-url.mjs', () => () => 'metaUrlMock'); @@ -48,26 +55,33 @@ describe('The ProvidedExternalsInjector', () => { const finalContent = await browserInjector.inject(mockContent, { rootComponentName: 'rootComponentNameMock' }); expect(finalContent).toMatchInlineSnapshot(` -"mockContent; +"mockContent rootComponentNameMock.appConfig = Object.assign({}, rootComponentNameMock.appConfig, { providedExternals: { - 'mockProvidedExternal': { version: 'mockVersion', module: require('mockProvidedExternal')}, + 'mockProvidedExternal': { + ...{\\"fallbackEnabled\\":false}, + version: 'mockVersion', + module: require('mockProvidedExternal') + }, + 'mockProvidedExternal2': { + ...{\\"fallbackEnabled\\":true}, + version: 'mockVersion', + module: require('mockProvidedExternal2') + }, }, }); if(globalThis.getTenantRootModule === undefined || (globalThis.rootModuleName && globalThis.rootModuleName === 'packageNameMock')){ -globalThis.getTenantRootModule = () => rootComponentNameMock; -globalThis.rootModuleName = 'packageNameMock'; -} + globalThis.getTenantRootModule = () => rootComponentNameMock; + globalThis.rootModuleName = 'packageNameMock'; +}; " `); }); it('should inject nothing in the browser if there are no externals', async () => { expect.assertions(1); - getModulesBundlerConfig.mockImplementationOnce(() => ({ - providedExternals: [], - })); + getModulesBundlerConfig.mockImplementationOnce(() => []); const browserInjector = new ProvidedExternalsInjector( { bundleType: BUNDLE_TYPES.BROWSER, packageJson } ); @@ -80,7 +94,7 @@ globalThis.rootModuleName = 'packageNameMock'; it('should inject nothing in the browser if the externals key does not exist', async () => { expect.assertions(1); - getModulesBundlerConfig.mockImplementationOnce(() => ({})); + getModulesBundlerConfig.mockImplementationOnce(() => undefined); const browserInjector = new ProvidedExternalsInjector( { bundleType: BUNDLE_TYPES.BROWSER, packageJson } ); @@ -101,26 +115,33 @@ globalThis.rootModuleName = 'packageNameMock'; const finalContent = await browserInjector.inject(mockContent, { rootComponentName: 'rootComponentNameMock' }); expect(finalContent).toMatchInlineSnapshot(` -"mockContent; +"mockContent rootComponentNameMock.appConfig = Object.assign({}, rootComponentNameMock.appConfig, { providedExternals: { - 'mockProvidedExternal': { version: 'mockVersion', module: require('mockProvidedExternal')}, + 'mockProvidedExternal': { + ...{\\"fallbackEnabled\\":false}, + version: 'mockVersion', + module: require('mockProvidedExternal') + }, + 'mockProvidedExternal2': { + ...{\\"fallbackEnabled\\":true}, + version: 'mockVersion', + module: require('mockProvidedExternal2') + }, }, }); if(global.getTenantRootModule === undefined || (global.rootModuleName && global.rootModuleName === 'packageNameMock')){ -global.getTenantRootModule = () => rootComponentNameMock; -global.rootModuleName = 'packageNameMock'; -} + global.getTenantRootModule = () => rootComponentNameMock; + global.rootModuleName = 'packageNameMock'; +}; " `); }); it('should inject nothing in the server if there are no externals', async () => { expect.assertions(1); - getModulesBundlerConfig.mockImplementationOnce(() => ({ - providedExternals: [], - })); + getModulesBundlerConfig.mockImplementationOnce(() => []); const browserInjector = new ProvidedExternalsInjector( { bundleType: BUNDLE_TYPES.SERVER, packageJson } ); @@ -130,4 +151,20 @@ global.rootModuleName = 'packageNameMock'; expect(finalContent).toBe(mockContent); }); + + it('adds fallbackEnabled to legacy provided external api', async () => { + expect.assertions(1); + getModulesBundlerConfig.mockImplementationOnce(() => jest.fn(() => [ + 'mockProvidedExternal', + ])); + + const browserInjector = new ProvidedExternalsInjector( + { bundleType: BUNDLE_TYPES.SERVER, packageJson } + ); + const mockContent = 'mockContent'; + + const finalContent = await browserInjector.inject(mockContent, { rootComponentName: 'rootComponentNameMock' }); + + expect(finalContent).toMatchInlineSnapshot('"mockContent"'); + }); }); diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader.spec.js b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader.spec.js index d17ca6f3..141430dc 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader.spec.js +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/one-app-index-loader.spec.js @@ -14,7 +14,7 @@ * permissions and limitations under the License. */ -import fs from 'fs'; +import fs from 'node:fs'; import oneAppIndexLoader from '../../../esbuild/plugins/one-app-index-loader'; import { runOnLoadHook, runSetupAndGetLifeHooks } from './__plugin-testing-utils__'; import { BUNDLE_TYPES } from '../../../esbuild/constants/enums.js'; @@ -221,16 +221,21 @@ describe('Esbuild plugin oneAppIndexLoader', () => { )).rejects.toThrow('one-app-bundler: Module must use `export default VariableName` syntax in index'); }); - it('should throw an exception if an injector removes the default export', async () => { - expect.assertions(1); - + it('should throws an exception if an injector removes the default export', async () => { // it doesn't matter which injector is mocked here. + jest.resetModules(); jest.doMock('../../../esbuild/plugins/one-app-index-loader-injectors/module-metadata-injector', () => class BadInjector { // eslint-disable-next-line class-methods-use-this -- it doesnt' matter if 'this' is used for this mock inject = async () => 'not a default export!'; }); - const plugin = oneAppIndexLoader({ bundleType: BUNDLE_TYPES.BROWSER, watch: true }); + // eslint-disable-next-line global-require -- required to use mocked injector + const oneAppIndexLoaderWithMockedInjector = require('../../../esbuild/plugins/one-app-index-loader').default; + + const plugin = oneAppIndexLoaderWithMockedInjector({ + bundleType: BUNDLE_TYPES.BROWSER, watch: true, + }); + const onLoadHook = runSetupAndGetLifeHooks(plugin).onLoad[0].hookFunction; await expect(async () => runOnLoadHook(onLoadHook, diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/restrict-runtime-symbols.spec.js b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/restrict-runtime-symbols.spec.js index 235fcd1f..86bfefb2 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/restrict-runtime-symbols.spec.js +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/restrict-runtime-symbols.spec.js @@ -14,13 +14,13 @@ * permissions and limitations under the License. */ -import fs from 'fs'; +import fs from 'node:fs'; import restrictRuntimeSymbols, { getParentNode } from '../../../esbuild/plugins/restrict-runtime-symbols'; import { logWarnings, logErrors } from '../../../esbuild/utils/colorful-logging'; import { runSetupAndGetLifeHooks } from './__plugin-testing-utils__'; import { BUNDLE_TYPES, SEVERITY } from '../../../esbuild/constants/enums'; -jest.mock('fs', () => ({ +jest.mock('node:fs', () => ({ promises: { readFile: jest.fn(() => 'mockFileContent'), writeFile: jest.fn(() => 'mockWriteFileResponse'), diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/server-styles-dispatcher.spec.js b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/server-styles-dispatcher.spec.js index 8e100bf7..eb24fbc1 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/server-styles-dispatcher.spec.js +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/server-styles-dispatcher.spec.js @@ -15,7 +15,7 @@ */ import mockFs from 'mock-fs'; -import fs from 'fs'; +import fs from 'node:fs'; import serverStylesDispatcher from '../../../esbuild/plugins/server-styles-dispatcher'; import { runSetupAndGetLifeHooks } from './__plugin-testing-utils__'; import { emptyAggregatedStyles, addStyle } from '../../../esbuild/utils/server-style-aggregator'; diff --git a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/time-build.spec.js b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/time-build.spec.js index f3d9348b..4d605380 100644 --- a/packages/one-app-dev-bundler/__tests__/esbuild/plugins/time-build.spec.js +++ b/packages/one-app-dev-bundler/__tests__/esbuild/plugins/time-build.spec.js @@ -14,14 +14,14 @@ * permissions and limitations under the License. */ -import fs from 'fs'; +import fs from 'node:fs'; import timeBuild from '../../../esbuild/plugins/time-build'; import { runSetupAndGetLifeHooks } from './__plugin-testing-utils__'; jest.spyOn(process.hrtime, 'bigint'); jest.spyOn(console, 'log'); -jest.mock('fs', () => ({ +jest.mock('node:fs', () => ({ promises: { writeFile: jest.fn(), }, diff --git a/packages/one-app-dev-bundler/__tests__/utils/analyze-bundles.spec.js b/packages/one-app-dev-bundler/__tests__/utils/analyze-bundles.spec.js index 8fd0acf6..ff18a31f 100644 --- a/packages/one-app-dev-bundler/__tests__/utils/analyze-bundles.spec.js +++ b/packages/one-app-dev-bundler/__tests__/utils/analyze-bundles.spec.js @@ -14,12 +14,12 @@ * permissions and limitations under the License. */ -import fs from 'fs'; -import path from 'path'; +import fs from 'node:fs'; +import path from 'node:path'; import analyzeBundles from '../../utils/analyze-bundles.js'; -jest.mock('fs', () => ({ +jest.mock('node:fs', () => ({ promises: { readFile: jest.fn(), writeFile: jest.fn(), diff --git a/packages/one-app-dev-bundler/__tests__/utils/build-module.spec.js b/packages/one-app-dev-bundler/__tests__/utils/build-module.spec.js index 6e200599..4971b8f1 100644 --- a/packages/one-app-dev-bundler/__tests__/utils/build-module.spec.js +++ b/packages/one-app-dev-bundler/__tests__/utils/build-module.spec.js @@ -38,7 +38,7 @@ jest.mock('../../esbuild/generateESBuildOptions', () => jest.fn(() => ({ nodeConfig: 'mockNodeConfig', }))); -// eslint-disable-next-line react/display-name -- eslint incorrectly thinks the next line is a react component +// eslint-disable-next-line react/display-name -- not react component jest.mock('../../esbuild/utils/get-modules-bundler-config', () => () => null); jest.mock('../../utils/get-cli-options', () => jest.fn(() => { })); diff --git a/packages/one-app-dev-bundler/__tests__/utils/bundle-external-fallbacks.spec.js b/packages/one-app-dev-bundler/__tests__/utils/bundle-external-fallbacks.spec.js new file mode 100644 index 00000000..66a493f4 --- /dev/null +++ b/packages/one-app-dev-bundler/__tests__/utils/bundle-external-fallbacks.spec.js @@ -0,0 +1,208 @@ +/* + * Copyright 2023 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import fs from 'node:fs'; +import esbuild from 'esbuild'; +import { readPackageUpSync } from 'read-pkg-up'; + +import { bundleExternalFallbacks } from '../../utils/bundle-external-fallbacks.js'; + +jest.mock('esbuild', () => ({ + build: jest.fn(() => Promise.resolve({ + metafile: { + outputs: { + 'build/1.0.0/root-module.node.js': { + bytes: 5955, + }, + }, + }, + })), +})); + +jest.mock('../../esbuild/generateESBuildOptions', () => jest.fn(() => ({ + buildExternalsConfig: (env) => ({ + mocked: env, + }), +}))); + +jest.mock('read-pkg-up', () => ({ + readPackageUpSync: jest.fn(() => ({ + packageJson: { + dependencies: {}, + }, + })), +})); + +jest.mock('node:fs'); + +jest.spyOn(process, 'cwd').mockImplementation(() => '/path/'); + +describe('bundle-external-fallbacks', () => { + beforeAll(() => { + jest.spyOn(console, 'info').mockImplementation(() => null); + jest.spyOn(console, 'error').mockImplementation(() => null); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('no required externals', () => { + it('does not have required externals', async () => { + readPackageUpSync.mockImplementationOnce(() => ({ + packageJson: { + 'one-amex': {}, + }, + })); + + await bundleExternalFallbacks(); + + expect(esbuild.build).not.toHaveBeenCalled(); + }); + + it('required externals is not an array', async () => { + readPackageUpSync.mockImplementationOnce(() => ({ + packageJson: { + 'one-amex': { + bundler: { + requiredExternals: {}, + }, + }, + }, + })); + + await bundleExternalFallbacks(); + + expect(esbuild.build).not.toHaveBeenCalled(); + }); + + it('required externals is an empty array', async () => { + readPackageUpSync.mockImplementationOnce(() => ({ + packageJson: { + 'one-amex': { + bundler: { + requiredExternals: [], + }, + }, + }, + })); + + await bundleExternalFallbacks(); + + expect(esbuild.build).not.toHaveBeenCalled(); + }); + }); + + it('bundles the external for browser and node environments', async () => { + readPackageUpSync.mockImplementationOnce(() => ({ + packageJson: { + version: '1.0.0', + 'one-amex': { + bundler: { + requiredExternals: ['awesome'], + }, + }, + }, + })); + + const readPackageUpSyncMock = jest.fn(() => ({ + packageJson: { + version: '1.0.0', + }, + })); + + readPackageUpSync.mockImplementationOnce(readPackageUpSyncMock); + + fs.readFileSync.mockImplementationOnce(() => 'const testing = true;'); + + await bundleExternalFallbacks(); + + expect(console.info).toHaveBeenCalledWith('Bundling External Fallbacks'); + expect(esbuild.build).toHaveBeenCalledTimes(2); + expect(esbuild.build).toHaveBeenCalledWith({ + entryPoints: [ + '/path/node_modules/awesome', + ], + globalName: '__holocron_external__awesome__1_0_0', + footer: { + js: 'Holocron.registerExternal({ name: "awesome", version: "1.0.0", module: __holocron_external__awesome__1_0_0});', + }, + mocked: 'browser', + outfile: '/path/build/1.0.0/awesome.browser.js', + }); + expect(esbuild.build).toHaveBeenCalledWith({ + entryPoints: [ + '/path/node_modules/awesome', + ], + footer: {}, + mocked: 'node', + outfile: '/path/build/1.0.0/awesome.node.js', + }); + }); + + it('bundle fails', async () => { + readPackageUpSync.mockImplementationOnce(() => ({ + packageJson: { + version: '1.0.0', + 'one-amex': { + bundler: { + requiredExternals: ['awesome'], + }, + }, + }, + })); + + const readPackageUpSyncMock = jest.fn(() => ({ + packageJson: { + version: '1.0.0', + }, + })); + + readPackageUpSync.mockImplementationOnce(readPackageUpSyncMock); + + fs.readFileSync.mockImplementationOnce(() => 'const testing = true;'); + + const error = new Error('Testing'); + + esbuild.build.mockImplementation(() => Promise.reject(error)); + + await bundleExternalFallbacks(); + + expect(esbuild.build).toHaveBeenCalledTimes(2); + expect(esbuild.build).toHaveBeenCalledWith({ + entryPoints: [ + '/path/node_modules/awesome', + ], + globalName: '__holocron_external__awesome__1_0_0', + mocked: 'browser', + footer: { js: 'Holocron.registerExternal({ name: "awesome", version: "1.0.0", module: __holocron_external__awesome__1_0_0});' }, + outfile: '/path/build/1.0.0/awesome.browser.js', + }); + expect(esbuild.build).toHaveBeenCalledWith({ + entryPoints: [ + '/path/node_modules/awesome', + ], + mocked: 'node', + footer: {}, + outfile: '/path/build/1.0.0/awesome.node.js', + }); + + expect(fs.writeFileSync).not.toHaveBeenCalled(); + expect(console.error).toHaveBeenCalledTimes(2); + expect(console.error).toHaveBeenCalledWith('Failed to build fallback for external awesome for browser', error); + expect(console.error).toHaveBeenCalledWith('Failed to build fallback for external awesome for node', error); + }); +}); diff --git a/packages/one-app-dev-bundler/esbuild/generateESBuildOptions.js b/packages/one-app-dev-bundler/esbuild/generateESBuildOptions.js index 39c1101a..57973b8a 100644 --- a/packages/one-app-dev-bundler/esbuild/generateESBuildOptions.js +++ b/packages/one-app-dev-bundler/esbuild/generateESBuildOptions.js @@ -18,7 +18,7 @@ import { globalExternals } from '@fal-works/esbuild-plugin-global-externals'; import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill'; import { NodeModulesPolyfillPlugin } from '@esbuild-plugins/node-modules-polyfill'; import { readPackageUpSync } from 'read-pkg-up'; -import path from 'path'; +import path from 'node:path'; import { BUNDLE_TYPES, SEVERITY } from './constants/enums.js'; import stylesLoader from './plugins/styles-loader.js'; import timeBuild from './plugins/time-build.js'; @@ -39,6 +39,17 @@ import serverStylesDispatcher from './plugins/server-styles-dispatcher.js'; const { browserGlobals, nodeExternals } = getOneAppExternals(); +/** + * Generates ESBuild Options + * @param {object} options build options + * @param {boolean} options.watch enables watch mode. Defaults to `false` + * @param {boolean} options.useLiveReload enables live reload. Defaults to `false` + * @returns {Promise.<{ + * nodeConfig: object, + * browserConfig: object, + * buildExternalsConfig: (env: string, externalName: string) => object + * }>} NodeJS, Browser, and Externals ESBuild configs + */ const generateESBuildOptions = async ({ watch, useLiveReload }) => { const { packageJson, path: packageJsonPath } = readPackageUpSync(); const { version, name } = packageJson; @@ -130,6 +141,24 @@ const generateESBuildOptions = async ({ watch, useLiveReload }) => { external: nodeExternals, }; + /** + * Generates externals config based on the provided external name + * @param {string} env Either "browser" or "node" + * @param {string} externalName External name that's being bundled/transpiled + * @returns ESBuild config for externals + */ + const buildExternalsConfig = (env, externalName) => ({ + ...env === 'browser' ? browserConfig : nodeConfig, + plugins: [ + removeWebpackLoaderSyntax, + bundleAssetSizeLimiter(commonConfigPluginOptions), + env === 'browser' && legacyBundler(externalName), + generateIntegrityManifest({ bundleName: externalName }), + restrictRuntimeSymbols(browserConfigPluginOptions), + timeBuild({ bundleName: externalName, watch }), + ].filter(Boolean), + }); + if (watch) { let reloadBrowser; if (useLiveReload) { @@ -156,8 +185,9 @@ const generateESBuildOptions = async ({ watch, useLiveReload }) => { } return { - browserConfig, nodeConfig, + browserConfig, + buildExternalsConfig, }; }; diff --git a/packages/one-app-dev-bundler/esbuild/plugins/cjs-compatibility-hotpatch.js b/packages/one-app-dev-bundler/esbuild/plugins/cjs-compatibility-hotpatch.js index 342498b0..4bb5165a 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/cjs-compatibility-hotpatch.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/cjs-compatibility-hotpatch.js @@ -14,7 +14,7 @@ * permissions and limitations under the License. */ -import fs from 'fs'; +import fs from 'node:fs'; import { getJsFilenamesFromKeys } from '../utils/get-js-filenames-from-keys.js'; // TODO: (When this bundler is being assessed for production bundling use) diff --git a/packages/one-app-dev-bundler/esbuild/plugins/externals-loader.js b/packages/one-app-dev-bundler/esbuild/plugins/externals-loader.js index 8f799400..50bb207f 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/externals-loader.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/externals-loader.js @@ -14,6 +14,9 @@ * permissions and limitations under the License. */ +import path from 'node:path'; +import { readPackageUpSync } from 'read-pkg-up'; + import { BUNDLE_TYPES } from '../constants/enums.js'; import getModulesBundlerConfig from '../utils/get-modules-bundler-config.js'; @@ -23,7 +26,19 @@ const externalsLoader = ({ bundleType }) => ({ const requiredExternalNames = getModulesBundlerConfig('requiredExternals'); if (!Array.isArray(requiredExternalNames) || requiredExternalNames.length === 0) { - return; // this module does not require any externals, so dont register the hooks + return; // this module does not require any externals, so don't register the hooks + } + + const { packageJson } = readPackageUpSync() || {}; + + if (!packageJson) { + throw new Error("Missing 'package.json'"); + } + + const { dependencies } = packageJson; + + if (!dependencies) { + throw new Error("'package.json' does not have 'dependencies' key"); } const globalReferenceString = bundleType === BUNDLE_TYPES.BROWSER ? 'globalThis' : 'global'; @@ -40,20 +55,36 @@ const externalsLoader = ({ bundleType }) => ({ // return a namespace. (see this above) // your onLoad can then just match .* within that namespace and you guarantee you target // every package you want. - build.onLoad({ filter: /.*/, namespace: 'externalsLoader' }, async (args) => { - const jsContent = `\ -try { - module.exports = ${globalReferenceString}.getTenantRootModule().appConfig.providedExternals['${args.path}'].module; -} catch (error) { - const errorGettingExternal = new Error('Failed to get external ${args.path} from root module'); - errorGettingExternal.shouldBlockModuleReload = false; - throw errorGettingExternal; -} -`; + build.onLoad({ filter: /.*/, namespace: 'externalsLoader' }, async ({ path: externalName }) => { + const version = readPackageUpSync({ + cwd: path.resolve(process.cwd(), 'node_modules', externalName), + })?.packageJson.version; return { - contents: jsContent, loader: 'js', + contents: ` + try { + const Holocron = ${bundleType === BUNDLE_TYPES.SERVER ? 'require("holocron")' : `${globalReferenceString}.Holocron`}; + const fallbackExternal = Holocron.getExternal({ + name: '${externalName}', + version: '${version}' + }); + const rootModuleExternal = ${globalReferenceString}.getTenantRootModule && ${globalReferenceString}.getTenantRootModule().appConfig.providedExternals['${externalName}']; + + module.exports = fallbackExternal || (rootModuleExternal ? rootModuleExternal.module : () => { + throw new Error('[${bundleType.toString()}][${packageJson.name}] External not found: ${externalName}'); + }) + } catch (error) { + const errorGettingExternal = new Error([ + '[${bundleType.toString()}] Failed to get external fallback ${externalName}', + error.message + ].filter(Boolean).join(' :: ')); + + errorGettingExternal.shouldBlockModuleReload = false; + + throw errorGettingExternal; + } + `, }; }); }, diff --git a/packages/one-app-dev-bundler/esbuild/plugins/font-loader.js b/packages/one-app-dev-bundler/esbuild/plugins/font-loader.js index 38c422f4..582793a4 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/font-loader.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/font-loader.js @@ -14,7 +14,7 @@ * permissions and limitations under the License. */ -import fs from 'fs'; +import fs from 'node:fs'; const fontLoader = { name: 'fontLoader', diff --git a/packages/one-app-dev-bundler/esbuild/plugins/generate-integrity-manifest.js b/packages/one-app-dev-bundler/esbuild/plugins/generate-integrity-manifest.js index ca47ac03..6dfe01b4 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/generate-integrity-manifest.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/generate-integrity-manifest.js @@ -15,7 +15,7 @@ */ import ssri from 'ssri'; -import fs from 'fs'; +import fs from 'node:fs'; import { getJsFilenamesFromKeys } from '../utils/get-js-filenames-from-keys.js'; async function writeIntegrityFragment(bundleName, integrityString, fileName) { diff --git a/packages/one-app-dev-bundler/esbuild/plugins/image-loader.js b/packages/one-app-dev-bundler/esbuild/plugins/image-loader.js index 8c4d51cc..09f56f11 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/image-loader.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/image-loader.js @@ -14,7 +14,7 @@ * permissions and limitations under the License. */ -import fs from 'fs'; +import fs from 'node:fs'; const imageLoader = { name: 'imageLoader', diff --git a/packages/one-app-dev-bundler/esbuild/plugins/legacy-bundler.js b/packages/one-app-dev-bundler/esbuild/plugins/legacy-bundler.js index 02e54137..b9786daf 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/legacy-bundler.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/legacy-bundler.js @@ -14,7 +14,7 @@ * permissions and limitations under the License. */ -import fs from 'fs'; +import fs from 'node:fs'; import swc from '@swc/core'; import { getJsFilenamesFromKeys } from '../utils/get-js-filenames-from-keys.js'; diff --git a/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/app-compatibility-injector.js b/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/app-compatibility-injector.js index 07162ccb..eebbe79a 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/app-compatibility-injector.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/app-compatibility-injector.js @@ -29,10 +29,10 @@ export default class AppCompatibilityInjector { return content; } - return `${content}; - ${rootComponentName}.appConfig = Object.assign({}, ${rootComponentName}.appConfig, { - appCompatibility: "${this.appCompatibility}", - }); + return `${content} +${rootComponentName}.appConfig = Object.assign({}, ${rootComponentName}.appConfig, { + appCompatibility: "${this.appCompatibility}", +}); `; }; } diff --git a/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/enable-unlisted-external-fallback-injector.js b/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/enable-unlisted-external-fallback-injector.js new file mode 100644 index 00000000..2a85607d --- /dev/null +++ b/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/enable-unlisted-external-fallback-injector.js @@ -0,0 +1,37 @@ +/* + * Copyright 2023 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import getModulesBundlerConfig from '../../utils/get-modules-bundler-config.js'; +import { BUNDLE_TYPES } from '../../constants/enums.js'; + +export default class UnlistedExternalFallbackInjector { + constructor({ bundleType }) { + this.willInject = bundleType === BUNDLE_TYPES.SERVER; + this.enableUnlistedExternalFallbacks = !!getModulesBundlerConfig('enableUnlistedExternalFallbacks'); + } + + inject = async (content, { rootComponentName }) => { + if (!this.willInject) { + return content; + } + + return `${content} +${rootComponentName}.appConfig = Object.assign({}, ${rootComponentName}.appConfig, { + enableUnlistedExternalFallbacks: "${this.enableUnlistedExternalFallbacks}", +}); +`; + }; +} diff --git a/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/holocron-module-register-injector.js b/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/holocron-module-register-injector.js index 57c294bf..e0b1018a 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/holocron-module-register-injector.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/holocron-module-register-injector.js @@ -27,7 +27,7 @@ export default class HolocronModuleRegisterInjector { return content; } - return `${content}; + return `${content} Holocron.registerModule("${this.moduleName}", ${rootComponentName}); `; }; diff --git a/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/module-metadata-injector.js b/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/module-metadata-injector.js index f9095c15..d03413c7 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/module-metadata-injector.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/module-metadata-injector.js @@ -29,8 +29,7 @@ export default class ModuleMetadataInjector { return content; } - return `${content}; - + return `${content} ${rootComponentName}.${META_DATA_KEY} = { version: '${this.version}', }; diff --git a/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/provided-externals-injector.js b/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/provided-externals-injector.js index ba5b07f9..c0583dd0 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/provided-externals-injector.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader-injectors/provided-externals-injector.js @@ -19,27 +19,40 @@ import getModulesBundlerConfig from '../../utils/get-modules-bundler-config.js'; import getMetaUrl from '../../utils/get-meta-url.mjs'; import { BUNDLE_TYPES } from '../../constants/enums.js'; +const getLength = (arg) => (Array.isArray(arg) ? arg.length : Object.keys(arg).length); + export default class ProvidedExternalsInjector { constructor({ bundleType, packageJson }) { this.moduleName = packageJson.name; const require = createRequire(getMetaUrl()); - const providedExternalNames = getModulesBundlerConfig('providedExternals'); + const providedExternals = getModulesBundlerConfig('providedExternals'); // no need to inject if there are no provided externals - this.willInject = Array.isArray(providedExternalNames) && providedExternalNames.length > 0; - + this.willInject = !!providedExternals && typeof providedExternals === 'object' && getLength(providedExternals) > 0; if (!this.willInject) { return; } - this.providedExternalsString = providedExternalNames.map((externalName) => { - // eslint-disable-next-line import/no-dynamic-require -- dynamic require is needed here - const { version } = require(`${externalName}/package.json`); - return `'${externalName}': { version: '${version}', module: require('${externalName}')}`; - }).join(',\n '); + const extendedProvidedExternals = (Array.isArray(providedExternals) + ? providedExternals : Object.keys(providedExternals)).map((externalName) => { + // eslint-disable-next-line import/no-dynamic-require -- need to require a package.json at runtime + const externalPkg = require(`${externalName}/package.json`); + + const externalOpts = JSON.stringify({ + fallbackEnabled: false, + ...providedExternals[externalName], + }, null, 0); + return `'${externalName}': { + ...${externalOpts}, + version: '${externalPkg.version}', + module: require('${externalName}') + }`; + }, {}); + + this.providedExternalsString = extendedProvidedExternals.join(',\n '); this.globalReferenceString = bundleType === BUNDLE_TYPES.BROWSER ? 'globalThis' : 'global'; } @@ -48,7 +61,7 @@ export default class ProvidedExternalsInjector { return content; } - return `${content}; + return `${content} ${rootComponentName}.appConfig = Object.assign({}, ${rootComponentName}.appConfig, { providedExternals: { ${this.providedExternalsString}, @@ -56,9 +69,9 @@ ${rootComponentName}.appConfig = Object.assign({}, ${rootComponentName}.appConfi }); if(${this.globalReferenceString}.getTenantRootModule === undefined || (${this.globalReferenceString}.rootModuleName && ${this.globalReferenceString}.rootModuleName === '${this.moduleName}')){ -${this.globalReferenceString}.getTenantRootModule = () => ${rootComponentName}; -${this.globalReferenceString}.rootModuleName = '${this.moduleName}'; -} + ${this.globalReferenceString}.getTenantRootModule = () => ${rootComponentName}; + ${this.globalReferenceString}.rootModuleName = '${this.moduleName}'; +}; `; }; } diff --git a/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader.js b/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader.js index bf2c1112..ded637d2 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/one-app-index-loader.js @@ -14,8 +14,8 @@ * permissions and limitations under the License. */ -import path from 'path'; -import fs from 'fs'; +import path from 'node:path'; +import fs from 'node:fs'; import { readPackageUpSync } from 'read-pkg-up'; import HolocronModuleRegisterInjector from './one-app-index-loader-injectors/holocron-module-register-injector.js'; @@ -23,6 +23,7 @@ import ModuleMetadataInjector from './one-app-index-loader-injectors/module-meta import ProvidedExternalsInjector from './one-app-index-loader-injectors/provided-externals-injector.js'; import AppCompatibilityInjector from './one-app-index-loader-injectors/app-compatibility-injector.js'; import DevLiveReloaderInjector from './one-app-index-loader-injectors/dev-live-reloader-injector.js'; +import UnlistedExternalFallbackInjector from './one-app-index-loader-injectors/enable-unlisted-external-fallback-injector.js'; // This loader is responsible for injecting everything into the index file that needs to be there // It is designed to be run against the index.js file of all modules for all bundles @@ -42,6 +43,7 @@ const oneAppIndexLoader = (options) => ({ const injectors = [ new HolocronModuleRegisterInjector(injectorOptions), new ModuleMetadataInjector(injectorOptions), + new UnlistedExternalFallbackInjector(injectorOptions), new ProvidedExternalsInjector(injectorOptions), new AppCompatibilityInjector(injectorOptions), new DevLiveReloaderInjector(injectorOptions), diff --git a/packages/one-app-dev-bundler/esbuild/plugins/remove-webpack-loader-syntax.js b/packages/one-app-dev-bundler/esbuild/plugins/remove-webpack-loader-syntax.js index 537a519f..06d4035c 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/remove-webpack-loader-syntax.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/remove-webpack-loader-syntax.js @@ -14,7 +14,7 @@ * permissions and limitations under the License. */ -import path from 'path'; +import path from 'node:path'; const removeWebpackLoaderSyntax = { name: 'removeWebpackLoaderSyntax', diff --git a/packages/one-app-dev-bundler/esbuild/plugins/restrict-runtime-symbols.js b/packages/one-app-dev-bundler/esbuild/plugins/restrict-runtime-symbols.js index f7e43ceb..cea031e2 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/restrict-runtime-symbols.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/restrict-runtime-symbols.js @@ -14,7 +14,7 @@ * permissions and limitations under the License. */ -import fs from 'fs'; +import fs from 'node:fs'; import * as acorn from 'acorn'; import * as astWalker from 'acorn-walk'; import { getJsFilenamesFromKeys } from '../utils/get-js-filenames-from-keys.js'; diff --git a/packages/one-app-dev-bundler/esbuild/plugins/server-styles-dispatcher.js b/packages/one-app-dev-bundler/esbuild/plugins/server-styles-dispatcher.js index 34dbb436..635dda4f 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/server-styles-dispatcher.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/server-styles-dispatcher.js @@ -14,7 +14,7 @@ * permissions and limitations under the License. */ -import fs from 'fs'; +import fs from 'node:fs'; import { getJsFilenamesFromKeys } from '../utils/get-js-filenames-from-keys.js'; import { getAggregatedStyles, emptyAggregatedStyles } from '../utils/server-style-aggregator.js'; import { BUNDLE_TYPES } from '../constants/enums.js'; diff --git a/packages/one-app-dev-bundler/esbuild/plugins/styles-loader.js b/packages/one-app-dev-bundler/esbuild/plugins/styles-loader.js index 09b17025..820851f3 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/styles-loader.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/styles-loader.js @@ -14,11 +14,11 @@ * permissions and limitations under the License. */ -import { compile as sassCompile } from 'sass'; +import * as sass from 'sass'; import postcss from 'postcss'; import cssModules from 'postcss-modules'; import crypto from 'crypto'; -import fs from 'fs'; +import fs from 'node:fs'; import cssnano from 'cssnano'; import purgecss from '../utils/purgecss.js'; import getModulesBundlerConfig from '../utils/get-modules-bundler-config.js'; @@ -40,7 +40,7 @@ const stylesLoader = (cssModulesOptions = {}, { bundleType } = {}) => ({ // Compile scss to css let cssContent; if (args.path.endsWith('scss')) { - cssContent = sassCompile(args.path, { loadPaths: ['./node_modules'] }).css.toString(); + cssContent = sass.compile(args.path, { loadPaths: ['./node_modules'] }).css.toString(); } else { cssContent = await fs.promises.readFile(args.path, 'utf8'); } diff --git a/packages/one-app-dev-bundler/esbuild/plugins/time-build.js b/packages/one-app-dev-bundler/esbuild/plugins/time-build.js index 3b29fb4c..315711e5 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/time-build.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/time-build.js @@ -14,7 +14,7 @@ * permissions and limitations under the License. */ -import fs from 'fs'; +import fs from 'node:fs'; const timeBuild = ({ bundleName, watch }) => ({ name: 'timeBuild', diff --git a/packages/one-app-dev-bundler/esbuild/utils/get-modules-bundler-config.js b/packages/one-app-dev-bundler/esbuild/utils/get-modules-bundler-config.js index ebfdcf8c..8c4ad71d 100644 --- a/packages/one-app-dev-bundler/esbuild/utils/get-modules-bundler-config.js +++ b/packages/one-app-dev-bundler/esbuild/utils/get-modules-bundler-config.js @@ -16,11 +16,11 @@ import { readPackageUpSync } from 'read-pkg-up'; -const getModulesBundlerConfig = (configKey = undefined) => { +const getModulesBundlerConfig = (configKey) => { const { packageJson } = readPackageUpSync(); const bundlerConfig = packageJson && packageJson['one-amex'] && packageJson['one-amex'].bundler; - if (configKey !== undefined) { + if (configKey) { return bundlerConfig && bundlerConfig[configKey]; } diff --git a/packages/one-app-dev-bundler/esbuild/utils/get-modules-webpack-config.js b/packages/one-app-dev-bundler/esbuild/utils/get-modules-webpack-config.js index 196761e9..607a00b4 100644 --- a/packages/one-app-dev-bundler/esbuild/utils/get-modules-webpack-config.js +++ b/packages/one-app-dev-bundler/esbuild/utils/get-modules-webpack-config.js @@ -15,7 +15,7 @@ */ import { createRequire } from 'module'; -import path from 'path'; +import path from 'node:path'; import getModulesBundlerConfig from './get-modules-bundler-config.js'; import getMetaUrl from './get-meta-url.mjs'; diff --git a/packages/one-app-dev-bundler/esbuild/utils/purgecss.js b/packages/one-app-dev-bundler/esbuild/utils/purgecss.js index 9b473208..d7503dea 100644 --- a/packages/one-app-dev-bundler/esbuild/utils/purgecss.js +++ b/packages/one-app-dev-bundler/esbuild/utils/purgecss.js @@ -14,8 +14,8 @@ * permissions and limitations under the License. */ -import fs from 'fs'; -import path from 'path'; +import fs from 'node:fs'; +import path from 'node:path'; import glob from 'glob-all'; import postcssPurgeCSS from '@fullhuman/postcss-purgecss'; diff --git a/packages/one-app-dev-bundler/index.js b/packages/one-app-dev-bundler/index.js index 4a7f7303..9095277a 100644 --- a/packages/one-app-dev-bundler/index.js +++ b/packages/one-app-dev-bundler/index.js @@ -14,6 +14,10 @@ * permissions and limitations under the License. */ -import devBuildModule from './utils/dev-build-module.js'; +import _devBuildModule from './utils/dev-build-module.js'; -export default devBuildModule; +export { bundleExternalFallbacks } from './utils/bundle-external-fallbacks.js'; + +export const devBuildModule = _devBuildModule; + +export default _devBuildModule; diff --git a/packages/one-app-dev-bundler/package.json b/packages/one-app-dev-bundler/package.json index a39d809a..13246013 100644 --- a/packages/one-app-dev-bundler/package.json +++ b/packages/one-app-dev-bundler/package.json @@ -39,6 +39,7 @@ "esbuild-plugin-svgr": "^1.0.1", "filesize": "^9.0.1", "glob-all": "^3.3.0", + "lodash.snakecase": "^4.1.1", "ms": "^2.1.3", "postcss": "^7.0.32", "postcss-modules": "^3.2.2", @@ -80,4 +81,4 @@ "publishConfig": { "access": "public" } -} +} \ No newline at end of file diff --git a/packages/one-app-dev-bundler/utils/analyze-bundles.js b/packages/one-app-dev-bundler/utils/analyze-bundles.js index a7dda81a..ebe04800 100644 --- a/packages/one-app-dev-bundler/utils/analyze-bundles.js +++ b/packages/one-app-dev-bundler/utils/analyze-bundles.js @@ -14,9 +14,9 @@ * permissions and limitations under the License. */ -import fs from 'fs'; +import fs from 'node:fs'; import ms from 'ms'; -import path from 'path'; +import path from 'node:path'; import chalk from 'chalk'; import filesize from 'filesize'; diff --git a/packages/one-app-dev-bundler/utils/bundle-external-fallbacks.js b/packages/one-app-dev-bundler/utils/bundle-external-fallbacks.js new file mode 100644 index 00000000..2101dd4f --- /dev/null +++ b/packages/one-app-dev-bundler/utils/bundle-external-fallbacks.js @@ -0,0 +1,75 @@ +/* + * Copyright 2023 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations + * under the License. + */ + +import path from 'node:path'; +import { readPackageUpSync } from 'read-pkg-up'; +import esbuild from 'esbuild'; +import snakeCase from 'lodash.snakecase'; +import generateESBuildOptions from '../esbuild/generateESBuildOptions.js'; + +const EXTERNAL_PREFIX = '__holocron_external'; + +const getExternalLibraryName = (name, version) => [EXTERNAL_PREFIX, snakeCase(name), version.replace(/[^\d.]+/g, '').replace(/\.+/g, '_')].filter(Boolean).join('__'); + +/** + * Transpiles Requires Externals as individual files. + * It's completely independent from bundling module's code. + */ +export const bundleExternalFallbacks = async () => { + const { packageJson } = readPackageUpSync(); + const { 'one-amex': { bundler = {} } } = packageJson; + const { requiredExternals } = bundler; + + if ( + requiredExternals && Array.isArray(requiredExternals) + && requiredExternals.length > 0 + ) { + console.info('Bundling External Fallbacks'); + + const { + buildExternalsConfig, + } = await generateESBuildOptions({ watch: false, useLiveReload: false }); + + await Promise.all(['browser', 'node'].map((env) => Promise.all(requiredExternals.map(async (externalName) => { + const indexPath = path.resolve(process.cwd(), 'node_modules', externalName); + const buildPath = path.resolve(process.cwd(), 'build', packageJson.version); + const externalFilename = `${externalName}.${env}.js`; + const outfile = path.resolve(buildPath, externalFilename); + const version = readPackageUpSync({ + cwd: path.resolve(process.cwd(), 'node_modules', externalName), + })?.packageJson.version; + const envConfig = env === 'browser' ? { + globalName: getExternalLibraryName(externalName, version), + } : {}; + + const footer = env === 'browser' ? { + js: `Holocron.registerExternal({ name: "${externalName}", version: "${version}", module: ${getExternalLibraryName(externalName, version)}});`, + } : {}; + + try { + await esbuild.build({ + ...buildExternalsConfig(env, externalName), + entryPoints: [indexPath], + outfile, + footer, + ...envConfig, + }); + } catch (error) { + console.error(`Failed to build fallback for external ${externalName} for ${env}`, error); + } + })))); + } +}; + +export default bundleExternalFallbacks; diff --git a/packages/one-app-locale-bundler/__tests__/src/compileModuleLocales.spec.js b/packages/one-app-locale-bundler/__tests__/src/compileModuleLocales.spec.js index c18d3045..13ac30dc 100644 --- a/packages/one-app-locale-bundler/__tests__/src/compileModuleLocales.spec.js +++ b/packages/one-app-locale-bundler/__tests__/src/compileModuleLocales.spec.js @@ -12,7 +12,7 @@ * under the License. */ -const fs = require('fs'); +const fs = require('node:fs'); const mockFs = require('mock-fs'); diff --git a/packages/one-app-locale-bundler/index.js b/packages/one-app-locale-bundler/index.js index f841df17..03b9e8b0 100644 --- a/packages/one-app-locale-bundler/index.js +++ b/packages/one-app-locale-bundler/index.js @@ -12,7 +12,7 @@ * under the License. */ -const path = require('path'); +const path = require('node:path'); const chokidar = require('chokidar'); const compileModuleLocales = require('./src/compileModuleLocales'); diff --git a/packages/one-app-locale-bundler/src/compileModuleLocales.js b/packages/one-app-locale-bundler/src/compileModuleLocales.js index 806b46be..d30de102 100644 --- a/packages/one-app-locale-bundler/src/compileModuleLocales.js +++ b/packages/one-app-locale-bundler/src/compileModuleLocales.js @@ -30,8 +30,8 @@ [locale] should be BCP-47 compliant */ -const fs = require('fs'); -const path = require('path'); +const fs = require('node:fs'); +const path = require('node:path'); const { promisify } = require('util'); const jsonParseContext = require('json-parse-context'); diff --git a/packages/one-app-locale-bundler/src/promisified-fs.js b/packages/one-app-locale-bundler/src/promisified-fs.js index f0fa851c..3bde5d7d 100644 --- a/packages/one-app-locale-bundler/src/promisified-fs.js +++ b/packages/one-app-locale-bundler/src/promisified-fs.js @@ -12,7 +12,7 @@ * under the License. */ -const fs = require('fs'); +const fs = require('node:fs'); const { promisify } = require('util'); diff --git a/packages/one-app-runner/__tests__/bin/one-app-runner.spec.js b/packages/one-app-runner/__tests__/bin/one-app-runner.spec.js index 5b8efb23..695022f8 100644 --- a/packages/one-app-runner/__tests__/bin/one-app-runner.spec.js +++ b/packages/one-app-runner/__tests__/bin/one-app-runner.spec.js @@ -15,7 +15,7 @@ /* eslint-disable global-require -- testing `on import` behaviour needs requires directly in tests */ // explicitly requiring within each test needed in order to have independent mocks -const path = require('path'); +const path = require('node:path'); const originalProcessExit = process.exit; const originalProcessArgv = process.argv; diff --git a/packages/one-app-runner/__tests__/src/startApp.spec.js b/packages/one-app-runner/__tests__/src/startApp.spec.js index a2a78bba..c2f28b70 100644 --- a/packages/one-app-runner/__tests__/src/startApp.spec.js +++ b/packages/one-app-runner/__tests__/src/startApp.spec.js @@ -13,8 +13,8 @@ */ // explicitly requiring within each test needed in order to have independent mocks -const path = require('path'); -const fs = require('fs'); +const path = require('node:path'); +const fs = require('node:fs'); const childProcess = require('child_process'); const Docker = require('dockerode'); const makeMockSpawn = require('mock-spawn'); diff --git a/packages/one-app-runner/bin/one-app-runner.js b/packages/one-app-runner/bin/one-app-runner.js index 63e3ca93..f090841e 100755 --- a/packages/one-app-runner/bin/one-app-runner.js +++ b/packages/one-app-runner/bin/one-app-runner.js @@ -14,7 +14,7 @@ * the License. */ -const path = require('path'); +const path = require('node:path'); const pkgUp = require('pkg-up'); const yargs = require('yargs'); const startApp = require('../src/startApp'); diff --git a/packages/one-app-runner/src/startApp.js b/packages/one-app-runner/src/startApp.js index e57e8658..784e6d27 100644 --- a/packages/one-app-runner/src/startApp.js +++ b/packages/one-app-runner/src/startApp.js @@ -13,8 +13,8 @@ */ const { spawn } = require('child_process'); -const path = require('path'); -const fs = require('fs'); +const path = require('node:path'); +const fs = require('node:fs'); const Docker = require('dockerode'); module.exports = async function startApp({ diff --git a/yarn.lock b/yarn.lock index 9f519f3a..df7d29ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6987,7 +6987,7 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@8: +eslint@8.29.0: version "8.29.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.29.0.tgz#d74a88a20fb44d59c51851625bc4ee8d0ec43f87" integrity sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg== @@ -9957,6 +9957,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.snakecase@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" + integrity sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw== + lodash.template@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" @@ -15067,6 +15072,11 @@ webpack-sources@^2.2.0: source-list-map "^2.0.1" source-map "^0.6.1" +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + webpack-subresource-integrity@^1.3.4: version "1.5.2" resolved "https://registry.yarnpkg.com/webpack-subresource-integrity/-/webpack-subresource-integrity-1.5.2.tgz#e40b6578d3072e2d24104975249c52c66e9a743e" From ae3c0659f83fd29abc890d833166b340825f8d6b Mon Sep 17 00:00:00 2001 From: Matthew Mallimo Date: Wed, 13 Sep 2023 17:40:10 -0400 Subject: [PATCH 5/6] chore(dep): fix webpack version to 4.46.0 (#560) --- packages/one-app-bundler/package.json | 5 ++++- yarn.lock | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/one-app-bundler/package.json b/packages/one-app-bundler/package.json index a41811da..0f97f03d 100644 --- a/packages/one-app-bundler/package.json +++ b/packages/one-app-bundler/package.json @@ -71,7 +71,7 @@ "style-loader": "^1.0.0", "terser-webpack-plugin": "^4.2.3", "url-loader": "^4.1.0", - "webpack": "^4.29.0", + "webpack": "4.46.0", "webpack-custom-chunk-id-plugin": "^1.0.3", "webpack-dynamic-public-path": "^1.0.4", "webpack-merge": "^4.2.1", @@ -98,5 +98,8 @@ }, "publishConfig": { "access": "public" + }, + "overrides": { + "webpack": "4.46.0" } } diff --git a/yarn.lock b/yarn.lock index df7d29ce..1d597fcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15084,7 +15084,7 @@ webpack-subresource-integrity@^1.3.4: dependencies: webpack-sources "^1.3.0" -webpack@^4.29.0, webpack@^4.29.5, webpack@^4.46.0: +webpack@4.46.0, webpack@^4.29.5, webpack@^4.46.0: version "4.46.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.46.0.tgz#bf9b4404ea20a073605e0a011d188d77cb6ad542" integrity sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q== From 6476fb376a1f081e71ed371c7262ffd23b2f236d Mon Sep 17 00:00:00 2001 From: Matthew Mallimo Date: Thu, 14 Sep 2023 10:30:31 -0400 Subject: [PATCH 6/6] chore(release): publish packages - @americanexpress/holocron-dev-server@0.2.0 - @americanexpress/one-app-bundler@6.21.0 - @americanexpress/one-app-dev-bundler@1.5.0 - @americanexpress/one-app-locale-bundler@6.6.0 - @americanexpress/one-app-runner@6.15.0 --- packages/holocron-dev-server/CHANGELOG.md | 11 +++++++++++ packages/holocron-dev-server/package.json | 4 ++-- packages/one-app-bundler/CHANGELOG.md | 11 +++++++++++ packages/one-app-bundler/package.json | 6 +++--- packages/one-app-dev-bundler/CHANGELOG.md | 17 +++++++++++++++++ packages/one-app-dev-bundler/package.json | 6 +++--- packages/one-app-locale-bundler/CHANGELOG.md | 11 +++++++++++ packages/one-app-locale-bundler/package.json | 2 +- packages/one-app-runner/CHANGELOG.md | 18 ++++++++++++++++++ packages/one-app-runner/package.json | 2 +- 10 files changed, 78 insertions(+), 10 deletions(-) diff --git a/packages/holocron-dev-server/CHANGELOG.md b/packages/holocron-dev-server/CHANGELOG.md index 3276ec6b..2a1a660b 100644 --- a/packages/holocron-dev-server/CHANGELOG.md +++ b/packages/holocron-dev-server/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.2.0](https://github.com/americanexpress/one-app-cli/compare/@americanexpress/holocron-dev-server@0.1.14...@americanexpress/holocron-dev-server@0.2.0) (2023-09-14) + + +### Features + +* external fallbacks ([#536](https://github.com/americanexpress/one-app-cli/issues/536)) ([523898d](https://github.com/americanexpress/one-app-cli/commit/523898deb9a1a4bcce6ba43915c852b02b7bb3a5)) + + + + + ## [0.1.14](https://github.com/americanexpress/one-app-cli/compare/@americanexpress/holocron-dev-server@0.1.13...@americanexpress/holocron-dev-server@0.1.14) (2023-06-13) **Note:** Version bump only for package @americanexpress/holocron-dev-server diff --git a/packages/holocron-dev-server/package.json b/packages/holocron-dev-server/package.json index 6085f4a0..6762a4aa 100644 --- a/packages/holocron-dev-server/package.json +++ b/packages/holocron-dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@americanexpress/holocron-dev-server", - "version": "0.1.14", + "version": "0.2.0", "description": "A micro-frontend dev server for Holocron Modules", "license": "Apache-2.0", "keywords": [ @@ -38,7 +38,7 @@ "module": "src/index.js", "dependencies": { "@americanexpress/fetch-enhancers": "^1.0.3", - "@americanexpress/one-app-bundler": "^6.20.0", + "@americanexpress/one-app-bundler": "^6.21.0", "@americanexpress/one-app-ducks": "^4.1.1", "@americanexpress/one-app-router": "^1.1.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.0-beta.1", diff --git a/packages/one-app-bundler/CHANGELOG.md b/packages/one-app-bundler/CHANGELOG.md index 7777b395..deab69ca 100644 --- a/packages/one-app-bundler/CHANGELOG.md +++ b/packages/one-app-bundler/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [6.21.0](https://github.com/americanexpress/one-app-cli/compare/@americanexpress/one-app-bundler@6.20.0...@americanexpress/one-app-bundler@6.21.0) (2023-09-14) + + +### Features + +* external fallbacks ([#536](https://github.com/americanexpress/one-app-cli/issues/536)) ([523898d](https://github.com/americanexpress/one-app-cli/commit/523898deb9a1a4bcce6ba43915c852b02b7bb3a5)) + + + + + # [6.20.0](https://github.com/americanexpress/one-app-cli/compare/@americanexpress/one-app-bundler@6.19.1...@americanexpress/one-app-bundler@6.20.0) (2023-06-13) diff --git a/packages/one-app-bundler/package.json b/packages/one-app-bundler/package.json index 0f97f03d..5c5d340e 100644 --- a/packages/one-app-bundler/package.json +++ b/packages/one-app-bundler/package.json @@ -1,6 +1,6 @@ { "name": "@americanexpress/one-app-bundler", - "version": "6.20.0", + "version": "6.21.0", "description": "A command line interface(CLI) tool for bundling One App and its modules.", "main": "index.js", "bin": { @@ -39,8 +39,8 @@ "license": "Apache-2.0", "dependencies": { "@americanexpress/eslint-plugin-one-app": "^6.13.5", - "@americanexpress/one-app-dev-bundler": "^1.4.2", - "@americanexpress/one-app-locale-bundler": "^6.5.11", + "@americanexpress/one-app-dev-bundler": "^1.5.0", + "@americanexpress/one-app-locale-bundler": "^6.6.0", "@americanexpress/purgecss-loader": "4.0.0", "@babel/core": "^7.17.5", "@babel/eslint-parser": "^7.17.0", diff --git a/packages/one-app-dev-bundler/CHANGELOG.md b/packages/one-app-dev-bundler/CHANGELOG.md index e04909d5..777f60f2 100644 --- a/packages/one-app-dev-bundler/CHANGELOG.md +++ b/packages/one-app-dev-bundler/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.5.0](https://github.com/americanexpress/one-app-cli/compare/@americanexpress/one-app-dev-bundler@1.4.2...@americanexpress/one-app-dev-bundler@1.5.0) (2023-09-14) + + +### Bug Fixes + +* **dev-bundler:** use semver ranges for all deps ([#550](https://github.com/americanexpress/one-app-cli/issues/550)) ([991ea3b](https://github.com/americanexpress/one-app-cli/commit/991ea3be4bea76f9a80200255a2f2e22a1180929)) + + +### Features + +* **devBundler:** aggregate styles into dependencies and local ([#557](https://github.com/americanexpress/one-app-cli/issues/557)) ([0eeee49](https://github.com/americanexpress/one-app-cli/commit/0eeee499ee9fda2eac6b2663c754c6840053d05a)) +* external fallbacks ([#536](https://github.com/americanexpress/one-app-cli/issues/536)) ([523898d](https://github.com/americanexpress/one-app-cli/commit/523898deb9a1a4bcce6ba43915c852b02b7bb3a5)) + + + + + ## [1.4.2](https://github.com/americanexpress/one-app-cli/compare/@americanexpress/one-app-dev-bundler@1.4.1...@americanexpress/one-app-dev-bundler@1.4.2) (2023-06-13) diff --git a/packages/one-app-dev-bundler/package.json b/packages/one-app-dev-bundler/package.json index 13246013..d9fc5466 100644 --- a/packages/one-app-dev-bundler/package.json +++ b/packages/one-app-dev-bundler/package.json @@ -1,6 +1,6 @@ { "name": "@americanexpress/one-app-dev-bundler", - "version": "1.4.2", + "version": "1.5.0", "description": "A development bundler focussed on speed and modern features.", "main": "index.js", "bin": { @@ -25,7 +25,7 @@ "module": "true", "type": "module", "dependencies": { - "@americanexpress/one-app-locale-bundler": "^6.5.11", + "@americanexpress/one-app-locale-bundler": "^6.6.0", "@esbuild-plugins/node-globals-polyfill": "^0.1.1", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@fal-works/esbuild-plugin-global-externals": "^2.1.2", @@ -81,4 +81,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/packages/one-app-locale-bundler/CHANGELOG.md b/packages/one-app-locale-bundler/CHANGELOG.md index 535e394e..150f3dc6 100644 --- a/packages/one-app-locale-bundler/CHANGELOG.md +++ b/packages/one-app-locale-bundler/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [6.6.0](https://github.com/americanexpress/one-app-cli/compare/@americanexpress/one-app-locale-bundler@6.5.11...@americanexpress/one-app-locale-bundler@6.6.0) (2023-09-14) + + +### Features + +* external fallbacks ([#536](https://github.com/americanexpress/one-app-cli/issues/536)) ([523898d](https://github.com/americanexpress/one-app-cli/commit/523898deb9a1a4bcce6ba43915c852b02b7bb3a5)) + + + + + ## [6.5.11](https://github.com/americanexpress/one-app-cli/compare/@americanexpress/one-app-locale-bundler@6.5.10...@americanexpress/one-app-locale-bundler@6.5.11) (2023-06-13) **Note:** Version bump only for package @americanexpress/one-app-locale-bundler diff --git a/packages/one-app-locale-bundler/package.json b/packages/one-app-locale-bundler/package.json index ece9b1ab..b9e83587 100644 --- a/packages/one-app-locale-bundler/package.json +++ b/packages/one-app-locale-bundler/package.json @@ -1,6 +1,6 @@ { "name": "@americanexpress/one-app-locale-bundler", - "version": "6.5.11", + "version": "6.6.0", "description": "A command line interface(CLI) tool for bundling the locale files.", "bin": { "bundle-module-locale": "./bin/bundle-module-locale.js" diff --git a/packages/one-app-runner/CHANGELOG.md b/packages/one-app-runner/CHANGELOG.md index e3bdda20..516366e7 100644 --- a/packages/one-app-runner/CHANGELOG.md +++ b/packages/one-app-runner/CHANGELOG.md @@ -3,6 +3,24 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [6.15.0](https://github.com/americanexpress/one-app-cli/compare/@americanexpress/one-app-runner@6.14.7...@americanexpress/one-app-runner@6.15.0) (2023-09-14) + + +### Bug Fixes + +* argument escaping ([#555](https://github.com/americanexpress/one-app-cli/issues/555)) ([902395c](https://github.com/americanexpress/one-app-cli/commit/902395c9e5c59da73399c2f3ba35063378fbf5d9)) +* **one-app-runner:** apply anonymous ip to debugger ([#559](https://github.com/americanexpress/one-app-cli/issues/559)) ([8443acc](https://github.com/americanexpress/one-app-cli/commit/8443accdb78b60a91ec9c864cd95163319168607)) +* **one-app-runner:** move placement of --inspect flag ([#556](https://github.com/americanexpress/one-app-cli/issues/556)) ([3370f3e](https://github.com/americanexpress/one-app-cli/commit/3370f3e3afb3238112a47156008bb3a701eaf900)) + + +### Features + +* external fallbacks ([#536](https://github.com/americanexpress/one-app-cli/issues/536)) ([523898d](https://github.com/americanexpress/one-app-cli/commit/523898deb9a1a4bcce6ba43915c852b02b7bb3a5)) + + + + + ## [6.14.7](https://github.com/americanexpress/one-app-cli/compare/@americanexpress/one-app-runner@6.14.6...@americanexpress/one-app-runner@6.14.7) (2023-06-13) diff --git a/packages/one-app-runner/package.json b/packages/one-app-runner/package.json index 19caf6d2..0c1a1ae6 100644 --- a/packages/one-app-runner/package.json +++ b/packages/one-app-runner/package.json @@ -1,6 +1,6 @@ { "name": "@americanexpress/one-app-runner", - "version": "6.14.7", + "version": "6.15.0", "description": "CLI for running One App locally", "license": "Apache-2.0", "contributors": [