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 (
+
+ );
+ }
+
+ 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 (
+
+ );
+ }
+
+ 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 (
+
+ );
+ }
+
+ 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"