From 0515fcd9326a83cf5347ef561563e581ecc30447 Mon Sep 17 00:00:00 2001 From: Giuliano Kranevitter Date: Thu, 29 Jun 2023 12:15:46 -0400 Subject: [PATCH] chore: test coverage --- jest.esm.config.js | 1 + .../extendWebpackConfig.spec.js.snap | 2 +- ...able-missing-externals-loader.spec.js.snap | 19 -- ...ted-external-fallbacks-loader.spec.js.snap | 19 ++ .../externals-loader.spec.js.snap | 39 ++- .../validate-externals-loader.spec.js.snap | 8 +- .../loaders/validate-externals-loader.spec.js | 10 + .../generateESBuildOptions.spec.js.snap | 222 ++++++++++++++++++ .../esbuild/generateESBuildOptions.spec.js | 12 +- .../esbuild/plugins/externals-loader.spec.js | 99 ++++++-- .../utils/bundle-external-fallbacks.spec.js | 208 ++++++++++++++++ .../esbuild/generateESBuildOptions.js | 2 +- .../esbuild/plugins/externals-loader.js | 30 +-- .../utils/get-modules-bundler-config.js | 4 +- packages/one-app-dev-bundler/index.js | 3 +- .../utils/bundle-external-fallbacks.js | 10 +- .../utils/dev-build-module.js | 2 +- 17 files changed, 598 insertions(+), 92 deletions(-) delete mode 100644 packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/enable-missing-externals-loader.spec.js.snap 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-dev-bundler/__tests__/utils/bundle-external-fallbacks.spec.js diff --git a/jest.esm.config.js b/jest.esm.config.js index 462bb388..8cca6222 100644 --- a/jest.esm.config.js +++ b/jest.esm.config.js @@ -30,6 +30,7 @@ module.exports = { '!packages/*/test-results/**', // 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/packages/one-app-bundler/__tests__/utils/__snapshots__/extendWebpackConfig.spec.js.snap b/packages/one-app-bundler/__tests__/utils/__snapshots__/extendWebpackConfig.spec.js.snap index 773e2e1f..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 @@ -53,7 +53,7 @@ Object { "test": "/path/src/index", "use": Array [ Object { - "loader": "@americanexpress/one-app-bundler/webpack/loaders/enable-missing-externals-loader", + "loader": "@americanexpress/one-app-bundler/webpack/loaders/enable-unlisted-external-fallbacks-loader", "options": Object { "enableUnlistedExternalFallbacks": true, }, diff --git a/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/enable-missing-externals-loader.spec.js.snap b/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/enable-missing-externals-loader.spec.js.snap deleted file mode 100644 index 729b9c79..00000000 --- a/packages/one-app-bundler/__tests__/webpack/loaders/__snapshots__/enable-missing-externals-loader.spec.js.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`enable-missing-externals-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-missing-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 app compatibility validation"`; - -exports[`enable-missing-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 app compatibility validation"`; - -exports[`enable-missing-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 app compatibility validation"`; 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 d20d18d2..190dfdea 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 @@ -2,18 +2,24 @@ exports[`externals-loader does not use fallback for server 1`] = ` "try { + const Holocron = require(\\"holocron\\"); + const fallbackExternal = Holocron.getExternal({ + name: 'lodash', + version: '4.17.21' + }); const rootModuleExternal = global.getTenantRootModule && global.getTenantRootModule().appConfig.providedExternals['lodash']; - if (rootModuleExternal && require('holocron').validateExternal({ - providedVersion: rootModuleExternal.version, - requestedRange: '^1.0.0' - })) { - module.exports = rootModuleExternal.module; - } else { - This is CJS code - } + + module.exports = fallbackExternal || (rootModuleExternal ? rootModuleExternal.module : () => { + throw new Error('[server][undefined] External not found: lodash'); + }) } catch (error) { - const errorGettingExternal = new Error('Failed to get external lodash from root module on the server', error.message); + const errorGettingExternal = new Error([ + '[server] Failed to get external fallback lodash', + error.message + ].filter(Boolean).join(' :: ')); + errorGettingExternal.shouldBlockModuleReload = false; + throw errorGettingExternal; } " @@ -21,17 +27,24 @@ exports[`externals-loader does not use fallback for server 1`] = ` exports[`externals-loader should ignore the content and get the dependency from the root module 1`] = ` "try { - const fallbackExternal = global.Holocron.getExternal({ + const Holocron = global.Holocron; + const fallbackExternal = Holocron.getExternal({ name: 'lodash', version: '4.17.21' }); const rootModuleExternal = global.getTenantRootModule && global.getTenantRootModule().appConfig.providedExternals['lodash']; + module.exports = fallbackExternal || (rootModuleExternal ? rootModuleExternal.module : () => { - throw new Error('External not found: lodash'); - }); + throw new Error('[undefined][undefined] External not found: lodash'); + }) } catch (error) { - const errorGettingExternal = new Error('Failed to get external fallback lodash'); + const errorGettingExternal = new Error([ + '[undefined] Failed to get external fallback lodash', + error.message + ].filter(Boolean).join(' :: ')); + errorGettingExternal.shouldBlockModuleReload = false; + throw errorGettingExternal; } " 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 9ac08ac2..fa3da0b6 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 @@ -9,14 +9,14 @@ if (!global.BROWSER) { SomeComponent.appConfig = Object.assign({}, SomeComponent.appConfig, { requiredExternals: { \\"ajv\\": { + \\"name\\": \\"ajv\\", \\"version\\": \\"6.12.6\\", - \\"semanticRange\\": \\"^6.7.0\\", - \\"filename\\": \\"ajv.js\\" + \\"semanticRange\\": \\"^6.7.0\\" }, \\"lodash\\": { + \\"name\\": \\"lodash\\", \\"version\\": \\"4.17.21\\", - \\"semanticRange\\": \\"^4.17.20\\", - \\"filename\\": \\"lodash.js\\" + \\"semanticRange\\": \\"^4.17.20\\" } }, }); 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..ada3d4cc 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('fs'); const readPkgUp = require('read-pkg-up'); const validateExternalsLoader = require('../../../webpack/loaders/validate-required-externals-loader'); @@ -23,10 +24,18 @@ jest.mock('read-pkg-up', () => ({ sync: jest.fn(), })); +jest.mock('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(() => '{}'); + describe('validate-required-externals-loader', () => { + beforeEach(() => { + // fs.readFileSync.mockClear(); + }); + it('should add versions for server side validation', () => { const content = `\ import SomeComponent from './SomeComponent'; @@ -34,6 +43,7 @@ import SomeComponent from './SomeComponent'; export default SomeComponent; `; expect(validateExternalsLoader(content)).toMatchSnapshot(); + expect(fs.writeFileSync).toHaveBeenCalled(); }); it('should throw an error when the wrong syntax is used - export from', () => { 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..5d6aef4c 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,78 @@ 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(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 +256,78 @@ 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(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 +425,81 @@ 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(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..668e92e6 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.externalsConfig('browser', 'awesome')).toMatchSnapshot(); + expect(configs.externalsConfig('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.externalsConfig('browser', 'awesome')).toMatchSnapshot(); + expect(configs.externalsConfig('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.externalsConfig('browser', 'awesome')).toMatchSnapshot(); + expect(configs.externalsConfig('server', 'awesome')).toMatchSnapshot(); expect(console).not.toHaveLogs(); expect(console).not.toHaveErrors(); 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..f0b77b8e 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__/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..1755bb5a --- /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 '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(() => ({ + externalsConfig: (env) => ({ + mocked: env, + }), +}))); + +jest.mock('read-pkg-up', () => ({ + readPackageUpSync: jest.fn(() => ({ + packageJson: { + dependencies: {}, + }, + })), +})); + +jest.mock('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', + mocked: 'browser', + outfile: '/path/build/1.0.0/awesome.browser.js', + }); + expect(esbuild.build).toHaveBeenCalledWith({ + entryPoints: [ + '/path/node_modules/awesome', + ], + mocked: 'node', + outfile: '/path/build/1.0.0/awesome.node.js', + }); + + expect(fs.writeFileSync).toHaveBeenCalledTimes(1); + expect(fs.writeFileSync).toHaveBeenCalledWith('/path/build/1.0.0/awesome.browser.js', [ + 'const testing = true;', + 'Holocron.registerExternal({ name: "awesome", version: "1.0.0", module: __holocron_external__awesome__1_0_0});', + ].join('\n')); + }); + + 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', + outfile: '/path/build/1.0.0/awesome.browser.js', + }); + expect(esbuild.build).toHaveBeenCalledWith({ + entryPoints: [ + '/path/node_modules/awesome', + ], + mocked: 'node', + 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 5cbb8d22..917060a1 100644 --- a/packages/one-app-dev-bundler/esbuild/generateESBuildOptions.js +++ b/packages/one-app-dev-bundler/esbuild/generateESBuildOptions.js @@ -148,7 +148,7 @@ const generateESBuildOptions = async ({ watch, useLiveReload }) => { * @returns ESBuild config for externals */ const externalsConfig = (env, externalName) => ({ - ...(env === 'browser' ? browserConfig : nodeConfig), + ...env === 'browser' ? browserConfig : nodeConfig, plugins: [ removeWebpackLoaderSyntax, bundleAssetSizeLimiter(commonConfigPluginOptions), 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 d58817f2..105ec8c4 100644 --- a/packages/one-app-dev-bundler/esbuild/plugins/externals-loader.js +++ b/packages/one-app-dev-bundler/esbuild/plugins/externals-loader.js @@ -20,22 +20,6 @@ import { readPackageUpSync } from 'read-pkg-up'; import { BUNDLE_TYPES } from '../constants/enums.js'; import getModulesBundlerConfig from '../utils/get-modules-bundler-config.js'; -const ignoreUntil = (cond, arr = []) => { - if (arr.length === 0) { - return arr; - } - - const value = arr[0]; - - if (cond(value)) { - return arr; - } - - const [, ...rest] = arr; - - return ignoreUntil(cond, rest); -}; - const externalsLoader = ({ bundleType }) => ({ name: 'externalsLoader', setup(build) { @@ -72,22 +56,12 @@ const externalsLoader = ({ bundleType }) => ({ // your onLoad can then just match .* within that namespace and you guarantee you target // every package you want. build.onLoad({ filter: /.*/, namespace: 'externalsLoader' }, async ({ path: externalName }) => { - const resolveDir = ignoreUntil( - (value) => value === 'node_modules', - require - .resolve(externalName) - .split(path.sep) - .reverse() - ) - .reverse() - .join(path.sep); const version = readPackageUpSync({ cwd: path.resolve(process.cwd(), 'node_modules', externalName), })?.packageJson.version; return { loader: 'js', - // resolveDir, contents: ` try { const Holocron = ${bundleType === BUNDLE_TYPES.SERVER ? 'require("holocron")' : `${globalReferenceString}.Holocron`}; @@ -98,11 +72,11 @@ const externalsLoader = ({ bundleType }) => ({ const rootModuleExternal = ${globalReferenceString}.getTenantRootModule && ${globalReferenceString}.getTenantRootModule().appConfig.providedExternals['${externalName}']; module.exports = fallbackExternal || (rootModuleExternal ? rootModuleExternal.module : () => { - throw new Error('[${bundleType}][${packageJson.name}] External not found: ${externalName}'); + throw new Error('[${bundleType.toString()}][${packageJson.name}] External not found: ${externalName}'); }) } catch (error) { const errorGettingExternal = new Error([ - '[${bundleType}] Failed to get external fallback ${externalName}', + '[${bundleType.toString()}] Failed to get external fallback ${externalName}', error.message ].filter(Boolean).join(' :: ')); 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..d2ab605e 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 != null) { return bundlerConfig && bundlerConfig[configKey]; } diff --git a/packages/one-app-dev-bundler/index.js b/packages/one-app-dev-bundler/index.js index 810b0d5a..9095277a 100644 --- a/packages/one-app-dev-bundler/index.js +++ b/packages/one-app-dev-bundler/index.js @@ -16,7 +16,8 @@ import _devBuildModule from './utils/dev-build-module.js'; -export { devBuildModule } from './utils/dev-build-module.js'; export { bundleExternalFallbacks } from './utils/bundle-external-fallbacks.js'; +export const devBuildModule = _devBuildModule; + export default _devBuildModule; diff --git a/packages/one-app-dev-bundler/utils/bundle-external-fallbacks.js b/packages/one-app-dev-bundler/utils/bundle-external-fallbacks.js index 3bbab8e8..c71c3043 100644 --- a/packages/one-app-dev-bundler/utils/bundle-external-fallbacks.js +++ b/packages/one-app-dev-bundler/utils/bundle-external-fallbacks.js @@ -21,10 +21,6 @@ import generateESBuildOptions from '../esbuild/generateESBuildOptions.js'; const EXTERNAL_PREFIX = '__holocron_external'; -const { packageJson } = readPackageUpSync() || {}; -const { 'one-amex': { bundler = {} } } = packageJson; -const { requiredExternals } = bundler; - const getExternalLibraryName = (name, version) => [EXTERNAL_PREFIX, snakeCase(name), version.replace(/[^\d.]+/g, '').replace(/\.+/g, '_')].filter(Boolean).join('__'); /** @@ -32,6 +28,10 @@ const getExternalLibraryName = (name, version) => [EXTERNAL_PREFIX, snakeCase(na * 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 @@ -72,7 +72,7 @@ export const bundleExternalFallbacks = async () => { ); } }).catch((error) => { - console.error(`Failed to build fallback for external ${externalName}`, error); + console.error(`Failed to build fallback for external ${externalName} for ${env}`, error); }); })) )); diff --git a/packages/one-app-dev-bundler/utils/dev-build-module.js b/packages/one-app-dev-bundler/utils/dev-build-module.js index 76992b8c..8ab0ac3b 100755 --- a/packages/one-app-dev-bundler/utils/dev-build-module.js +++ b/packages/one-app-dev-bundler/utils/dev-build-module.js @@ -21,7 +21,7 @@ import getCliOptions from './get-cli-options.js'; const asyncLocaleBundler = async (watch) => localeBundler(watch); -export const devBuildModule = async () => { +const devBuildModule = async () => { const { watch, useLiveReload,