diff --git a/packages/one-app-bundler/README.md b/packages/one-app-bundler/README.md index f2790903..b0b4d67d 100644 --- a/packages/one-app-bundler/README.md +++ b/packages/one-app-bundler/README.md @@ -277,6 +277,22 @@ You can pass only one if you wish to customize a single build target. #### [`purgecss`](https://github.com/FullHuman/purgecss) Options +`purgecss` is an opt-in optimization that can reduce the overall bundle size of your module by +eliminating unused css from your module's bundle. You can enable `purgecss` by setting +`bundler.purgecss.enabled` to `true` in the `one-amex` key in your module's `package.json`: + +```json +{ + "one-amex": { + "bundler": { + "purgecss": { + "enabled": true + } + } + } +} +``` + You may add additional paths for `purgecss` to consider before stripping out unused CSS by adding an array of glob patterns to `bundler.purgecss.paths` under `bundler.purgecss.paths`. The example below illustrates how we would add @@ -287,6 +303,7 @@ under `bundler.purgecss.paths`. The example below illustrates how we would add "one-amex": { "bundler": { "purgecss": { + "enabled": true, "paths": ["node_modules/some-lib/src/**/*.{js,jsx}"] } } @@ -302,6 +319,7 @@ before enabling any of the following: "one-amex": { "bundler": { "purgecss": { + "enabled": true, "paths": ["node_modules/some-lib/src/**/*.{js,jsx}"], "extractors": [{ "extractor": "purgeJs", @@ -329,6 +347,7 @@ before enabling any of the following: "one-amex": { "bundler": { "purgecss": { + "enabled": true, "paths": ["node_modules/some-lib/src/**/*.{js,jsx}"], "extractors": [{ "extractor": "purgeJs", @@ -353,23 +372,6 @@ before enabling any of the following: } ``` -##### Disabling purgecss - -`purgecss` can be disabled for your module by adding -`bundler.purgecss.disabled` as `true`. **Disabling purgecss entirely may increase your module bundle size and decrease performance.** - -```json -{ - "one-amex": { - "bundler": { - "purgecss": { - "disabled": true - } - } - } -} -``` - #### 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. diff --git a/packages/one-app-bundler/package.json b/packages/one-app-bundler/package.json index 63a8740d..05882930 100644 --- a/packages/one-app-bundler/package.json +++ b/packages/one-app-bundler/package.json @@ -42,7 +42,6 @@ "dependencies": { "@americanexpress/one-app-dev-bundler": "^1.7.0", "@americanexpress/one-app-locale-bundler": "^6.6.0", - "@americanexpress/purgecss-loader": "4.0.0", "@babel/core": "^7.22.20", "ajv": "^8.12.0", "assert": "^2.1.0", diff --git a/packages/one-app-bundler/utils/validation/index.js b/packages/one-app-bundler/utils/validation/index.js index 52cf5ff4..b238476f 100644 --- a/packages/one-app-bundler/utils/validation/index.js +++ b/packages/one-app-bundler/utils/validation/index.js @@ -59,6 +59,7 @@ export const purgecssSchema = Joi.object({ blocklist: Joi.array().items(Joi.string().required()), /* eslint-enable inclusive-language/use-inclusive-words -- disables require enables */ disabled: Joi.boolean().strict(), + enabled: Joi.boolean().strict(), }); const optionsSchema = Joi.object({ 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 c70bc489..f3b39d0c 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 @@ -80,6 +80,158 @@ describe('Esbuild plugin stylesLoader', () => { describe('NON-PRODUCTION environment', () => { mockNodeEnv('development'); + it('should transform inputs to default outputs for purged css, browser', async () => { + glob.sync.mockReturnValue(['Test.jsx']); + + getModulesBundlerConfig.mockImplementationOnce(() => ({ + enabled: true, + })); + + expect.assertions(3); + + const plugin = stylesLoader({}, { + bundleType: BUNDLE_TYPES.BROWSER, + }); + const onLoadHook = runSetupAndGetLifeHooks(plugin).onLoad[0].hookFunction; + const additionalMockedFiles = { + 'Test.jsx': `\ + import styles from './index.module.css'; + + const Component = () => { + return ( +
+

Testing

+
+ ); + } + + export default Component`, + }; + + const { + contents, loader, + } = await runOnLoadHook( + onLoadHook, + { + mockFileName: 'index.module.css', + mockFileContent: `\ + .root { + background: white; + } + + .somethingElse { + font-color: lime; + } + + .second { + font-color: black; + }`, + }, + additionalMockedFiles + ); + + expect(sassCompile).toHaveBeenCalledTimes(0); + expect(loader).toEqual('js'); + expect(contents).toMatchInlineSnapshot(` +"const digest = 'be76540996d2256b09af85f09bb93016999ae115235d972319628c011a06d6cc'; +const css = \` ._root_w8zvp_1 { + background: white; + } + + ._second_w8zvp_9 { + font-color: black; + }\`; +(function() { + if ( global.BROWSER && !document.getElementById(digest)) { + var el = document.createElement('style'); + el.id = digest; + el.textContent = css; + document.head.appendChild(el); + } +})(); +export const root = '_root_w8zvp_1'; +export const second = '_second_w8zvp_9'; +export default { root, second }; +export { css, digest };" +`); + }); + + it('should transform inputs to named outputs for purged css, browser', async () => { + glob.sync.mockReturnValue(['Test.jsx']); + + getModulesBundlerConfig.mockImplementationOnce(() => ({ + enabled: true, + })); + + expect.assertions(3); + + const plugin = stylesLoader({}, { + bundleType: BUNDLE_TYPES.BROWSER, + }); + const onLoadHook = runSetupAndGetLifeHooks(plugin).onLoad[0].hookFunction; + const additionalMockedFiles = { + 'Test.jsx': `\ + import { root, second } from './index.module.css'; + + const Component = () => { + return ( +
+

Testing

+
+ ); + } + + export default Component`, + }; + + const { + contents, loader, + } = await runOnLoadHook( + onLoadHook, + { + mockFileName: 'index.module.css', + mockFileContent: `\ + .root { + background: white; + } + + .somethingElse { + font-color: lime; + } + + .second { + font-color: black; + }`, + }, + additionalMockedFiles + ); + + expect(sassCompile).toHaveBeenCalledTimes(0); + expect(loader).toEqual('js'); + expect(contents).toMatchInlineSnapshot(` +"const digest = 'be76540996d2256b09af85f09bb93016999ae115235d972319628c011a06d6cc'; +const css = \` ._root_w8zvp_1 { + background: white; + } + + ._second_w8zvp_9 { + font-color: black; + }\`; +(function() { + if ( global.BROWSER && !document.getElementById(digest)) { + var el = document.createElement('style'); + el.id = digest; + el.textContent = css; + document.head.appendChild(el); + } +})(); +export const root = '_root_w8zvp_1'; +export const second = '_second_w8zvp_9'; +export default { root, second }; +export { css, digest };" +`); + }); + it('should transform inputs to outputs for scss, in the browser', async () => { expect.assertions(4); @@ -507,6 +659,163 @@ export const testClass = 'test-class'; export const nestedClass = 'nested-class'; export default { testClass, nestedClass }; export { css, digest };" +`); + }); + }); + + describe('purgecss', () => { + glob.sync.mockReturnValue(['Test.jsx']); + const additionalMockedFiles = { + 'Test.jsx': `\ + import { root, second } from './index.module.css'; + + const Component = () => { + return ( +
+

Testing

+
+ ); + } + + export default Component;`, + }; + const mockFileNameAndContent = { + mockFileName: 'index.module.css', + mockFileContent: `\ + .root { + background: white; + } + + .somethingElse { + font-color: lime; + } + + .second { + font-color: black; + }`, + }; + + it('should not purge css by default', async () => { + expect.assertions(1); + + const plugin = stylesLoader({}, { + bundleType: BUNDLE_TYPES.BROWSER, + }); + const onLoadHook = runSetupAndGetLifeHooks(plugin).onLoad[0].hookFunction; + const { contents } = await runOnLoadHook( + onLoadHook, + mockFileNameAndContent, + additionalMockedFiles + ); + + expect(contents).toMatchInlineSnapshot(` +"const digest = 'bec209059b0fca9bfe3221e70ce9deb5b73196ef71f2b1171ed73f09d53edbc8'; +const css = \` ._root_18xtd_1 { + background: white; + } + + ._somethingElse_18xtd_5 { + font-color: lime; + } + + ._second_18xtd_9 { + font-color: black; + }\`; +(function() { + if ( global.BROWSER && !document.getElementById(digest)) { + var el = document.createElement('style'); + el.id = digest; + el.textContent = css; + document.head.appendChild(el); + } +})(); +export const root = '_root_18xtd_1'; +export const somethingElse = '_somethingElse_18xtd_5'; +export const second = '_second_18xtd_9'; +export default { root, somethingElse, second }; +export { css, digest };" +`); + }); + + it('should purge css if disabled === false', async () => { + expect.assertions(1); + + getModulesBundlerConfig.mockImplementationOnce(() => ({ + enabled: true, + })); + + const plugin = stylesLoader({}, { + bundleType: BUNDLE_TYPES.BROWSER, + }); + const onLoadHook = runSetupAndGetLifeHooks(plugin).onLoad[0].hookFunction; + const { contents } = await runOnLoadHook( + onLoadHook, + mockFileNameAndContent, + additionalMockedFiles + ); + + expect(contents).toMatchInlineSnapshot(` +"const digest = 'a4b4b7b1b3332cc2e20331d5b11a79c021125e809ed8187e65eeca7756852397'; +const css = \` ._root_18xtd_1 { + background: white; + } + + ._second_18xtd_9 { + font-color: black; + }\`; +(function() { + if ( global.BROWSER && !document.getElementById(digest)) { + var el = document.createElement('style'); + el.id = digest; + el.textContent = css; + document.head.appendChild(el); + } +})(); +export const root = '_root_18xtd_1'; +export const second = '_second_18xtd_9'; +export default { root, second }; +export { css, digest };" +`); + }); + + it('should purge css if enabled === true', async () => { + expect.assertions(1); + + getModulesBundlerConfig.mockImplementationOnce(() => ({ + disabled: false, + })); + + const plugin = stylesLoader({}, { + bundleType: BUNDLE_TYPES.BROWSER, + }); + const onLoadHook = runSetupAndGetLifeHooks(plugin).onLoad[0].hookFunction; + const { contents } = await runOnLoadHook( + onLoadHook, + mockFileNameAndContent, + additionalMockedFiles + ); + + expect(contents).toMatchInlineSnapshot(` +"const digest = 'a4b4b7b1b3332cc2e20331d5b11a79c021125e809ed8187e65eeca7756852397'; +const css = \` ._root_18xtd_1 { + background: white; + } + + ._second_18xtd_9 { + font-color: black; + }\`; +(function() { + if ( global.BROWSER && !document.getElementById(digest)) { + var el = document.createElement('style'); + el.id = digest; + el.textContent = css; + document.head.appendChild(el); + } +})(); +export const root = '_root_18xtd_1'; +export const second = '_second_18xtd_9'; +export default { root, second }; +export { css, digest };" `); }); }); @@ -518,6 +827,10 @@ export { css, digest };" it('should transform inputs to default outputs for purged css, browser', async () => { glob.sync.mockReturnValue(['Test.jsx']); + getModulesBundlerConfig.mockImplementationOnce(() => ({ + enabled: true, + })); + expect.assertions(3); const plugin = stylesLoader({}, { @@ -544,7 +857,7 @@ export { css, digest };" } = await runOnLoadHook( onLoadHook, { - mockFileNAme: 'index.module.css', + mockFileName: 'index.module.css', mockFileContent: `\ .root { background: white; @@ -590,6 +903,10 @@ export { css, digest };" it('should transform inputs to named outputs for purged css, browser', async () => { glob.sync.mockReturnValue(['Test.jsx']); + getModulesBundlerConfig.mockImplementationOnce(() => ({ + enabled: true, + })); + expect.assertions(3); const plugin = stylesLoader({}, { @@ -616,7 +933,7 @@ export { css, digest };" } = await runOnLoadHook( onLoadHook, { - mockFileNAme: 'index.module.css', + mockFileName: 'index.module.css', mockFileContent: `\ .root { background: white; @@ -659,13 +976,9 @@ export { css, digest };" `); }); - it('should transform inputs to outputs for scss, with purge disabled, in the browser', async () => { + it('should transform inputs to outputs for scss, in the browser', async () => { expect.assertions(4); - getModulesBundlerConfig.mockImplementation(() => ({ - disabled: true, - })); - const mockFileName = 'index.scss'; const mockFileContent = `body { background: white; @@ -711,13 +1024,9 @@ export { css, digest };" `); }); - it('should transform inputs to outputs for css, with purge disabled, in the browser', async () => { + it('should transform inputs to outputs for css, in the browser', async () => { expect.assertions(3); - getModulesBundlerConfig.mockImplementation(() => ({ - disabled: true, - })); - const mockFileName = 'index.css'; const mockFileContent = `body { background: white; diff --git a/packages/one-app-dev-bundler/esbuild/utils/load-styles.js b/packages/one-app-dev-bundler/esbuild/utils/load-styles.js index aeb5c7a9..11cc63db 100644 --- a/packages/one-app-dev-bundler/esbuild/utils/load-styles.js +++ b/packages/one-app-dev-bundler/esbuild/utils/load-styles.js @@ -57,9 +57,15 @@ const loadStyles = async ({ // Generate the css modules information let cssModulesJSON = {}; + const purgecssConfig = getModulesBundlerConfig('purgecss') || {}; + if (purgecssConfig.disabled !== undefined) { + console.warn('The `disabled` option in the `purgecss` config is deprecated. Please use `enabled: true` if you would like to use purgecss, or remove the `disabled` option.'); + } + const shouldPurgeCss = purgecssConfig.enabled || purgecssConfig.disabled === false; + const result = await postcss([ - (process.env.NODE_ENV === 'production' && !purgecssConfig.disabled) && purgecss(purgecssConfig), + shouldPurgeCss && purgecss(purgecssConfig), cssModules({ localsConvention, generateScopedName, diff --git a/packages/one-app-server-bundler/utils/validation/index.js b/packages/one-app-server-bundler/utils/validation/index.js index cf20ce48..f6f83559 100644 --- a/packages/one-app-server-bundler/utils/validation/index.js +++ b/packages/one-app-server-bundler/utils/validation/index.js @@ -59,6 +59,7 @@ const purgecssSchema = Joi.object({ blocklist: Joi.array().items(Joi.string().required()), /* eslint-enable inclusive-language/use-inclusive-words -- disables require enables */ disabled: Joi.boolean().strict(), + enabled: Joi.boolean().strict(), }); const optionsSchema = Joi.object({ diff --git a/yarn.lock b/yarn.lock index 202a7444..be4b3c60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31,14 +31,6 @@ prop-types "^15.5.6" warning "^3.0.0" -"@americanexpress/purgecss-loader@4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@americanexpress/purgecss-loader/-/purgecss-loader-4.0.0.tgz#71d87b3d7861b5c9128a3e3aff4d882710129d79" - integrity sha512-PO8swpaOZf28j9r44NLaChOmmV23VUAbrMT9FeB11U+0YtZNL74uwC3HxHejtKP2VP7+dty3tmIKy3ysUt8mkA== - dependencies: - loader-utils "^1.4.2" - purgecss "^4.1.3" - "@americanexpress/vitruvius@^2.0.0": version "2.0.2" resolved "https://registry.yarnpkg.com/@americanexpress/vitruvius/-/vitruvius-2.0.2.tgz#7325ba00e3c57b0993973787ae50b0f7c72c3c38" @@ -4733,7 +4725,7 @@ commander@^7.2.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== -commander@^8.0.0, commander@^8.3.0: +commander@^8.3.0: version "8.3.0" resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== @@ -7319,7 +7311,7 @@ glob@^10.3.4, glob@^10.3.7: minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-scurry "^1.10.1" -glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@^7.2.3: +glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -9237,7 +9229,7 @@ loader-runner@^4.2.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== -loader-utils@^1.2.3, loader-utils@^1.4.2: +loader-utils@^1.2.3: version "1.4.2" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== @@ -11189,7 +11181,7 @@ postcss-reduce-transforms@^6.0.1: dependencies: postcss-value-parser "^4.2.0" -postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.15, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.6: +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.15, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: version "6.0.15" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz#11cc2b21eebc0b99ea374ffb9887174855a01535" integrity sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw== @@ -11225,7 +11217,7 @@ postcss@^7.0.14, postcss@^7.0.23, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0. picocolors "^0.2.1" source-map "^0.6.1" -postcss@^8.2.1, postcss@^8.3.5, postcss@^8.4.21, postcss@^8.4.33: +postcss@^8.2.1, postcss@^8.4.21, postcss@^8.4.33: version "8.4.33" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742" integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg== @@ -11413,16 +11405,6 @@ purgecss@^3.1.3: postcss "^8.2.1" postcss-selector-parser "^6.0.2" -purgecss@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-4.1.3.tgz#683f6a133c8c4de7aa82fe2746d1393b214918f7" - integrity sha512-99cKy4s+VZoXnPxaoM23e5ABcP851nC2y2GROkkjS8eJaJtlciGavd7iYAw2V84WeBqggZ12l8ef44G99HmTaw== - dependencies: - commander "^8.0.0" - glob "^7.1.7" - postcss "^8.3.5" - postcss-selector-parser "^6.0.6" - q@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"