Repository: ember-cli/ember-cli
Pull request #9595 (2021-08-07 09:58:38)

Add support for `addons.exclude` and `addons.include` options
Implements [RFC 639](https://emberjs.github.io/rfcs/0639-replace-blacklist-whitelist.html).

rwjblue: @bertdeblock - Thanks for picking this up!!! I think if you rebase, it should fix the CI failures...

Patch:
diff --git a/lib/broccoli/ember-app.js b/lib/broccoli/ember-app.js
index 0b52fe442..e5d562226 100644
--- a/lib/broccoli/ember-app.js
+++ b/lib/broccoli/ember-app.js
@@ -84,7 +84,7 @@ class EmberApp {
    - trees, defaults to `{}`
    - jshintrc, defaults to `{}`
    - vendorFiles, defaults to `{}`
-   - addons, defaults to `{ blacklist: [], whitelist: [] }`
+   - addons, defaults to `{ exclude: [], include: [] }`
 
    @class EmberApp
    @constructor
@@ -170,10 +170,12 @@ class EmberApp {
     this._cachedAddonBundles = {};
 
     if (this.project.perBundleAddonCache && this.project.perBundleAddonCache.numProxies > 0) {
-      if (this.options.addons.whitelist && this.options.addons.whitelist.length) {
+      if (this.options.addons.include && this.options.addons.include.length) {
+        let optionKey = this.options.addons.hasWhitelist ? 'whitelist' : 'include';
+
         throw new Error(
           [
-            '[ember-cli] addon bundle caching is disabled for apps that specify an addon `whitelist`',
+            `[ember-cli] addon bundle caching is disabled for apps that specify an addon "${optionKey}"`,
             '',
             'All addons using bundle caching:',
             ...this.project.perBundleAddonCache.getPathsToAddonsOptedIn(),
@@ -181,10 +183,12 @@ class EmberApp {
         );
       }
 
-      if (this.options.addons.blacklist && this.options.addons.blacklist.length) {
+      if (this.options.addons.exclude && this.options.addons.exclude.length) {
+        let optionKey = this.options.addons.hasBlacklist ? 'blacklist' : 'exclude';
+
         throw new Error(
           [
-            '[ember-cli] addon bundle caching is disabled for apps that specify an addon `blacklist`',
+            `[ember-cli] addon bundle caching is disabled for apps that specify an addon "${optionKey}"`,
             '',
             'All addons using bundle caching:',
             ...this.project.perBundleAddonCache.getPathsToAddonsOptedIn(),
@@ -366,6 +370,40 @@ class EmberApp {
 
     this.options = defaultsDeep(options, detectedDefaultOptions, DEFAULT_CONFIG);
 
+    if (this.options.addons.blacklist) {
+      if (this.options.addons.exclude) {
+        throw new Error('Specifying both "blacklist" and "exclude" is not supported. Please use only one.');
+      }
+
+      this.options.addons.hasBlacklist = true;
+      this.options.addons.exclude = this.options.addons.blacklist;
+    }
+
+    Object.defineProperty(this.options.addons, 'blacklist', {
+      get() {
+        console.log(chalk.yellow('Please use "exclude" instead of "blacklist".'));
+
+        return this.exclude;
+      },
+    });
+
+    if (this.options.addons.whitelist) {
+      if (this.options.addons.include) {
+        throw new Error('Specifying both "whitelist" and "include" is not supported. Please use only one.');
+      }
+
+      this.options.addons.hasWhitelist = true;
+      this.options.addons.include = this.options.addons.whitelist;
+    }
+
+    Object.defineProperty(this.options.addons, 'whitelist', {
+      get() {
+        console.log(chalk.yellow('Please use "include" instead of "whitelist".'));
+
+        return this.include;
+      },
+    });
+
     // For now we must disable Babel sourcemaps due to unforeseen
     // performance regressions.
     if (!('sourceMaps' in this.options.babel)) {
@@ -555,24 +593,24 @@ class EmberApp {
 
   /**
     @private
-    @method _addonDisabledByBlacklist
+    @method _addonDisabledByExclude
     @param {Addon} addon
     @return {Boolean}
   */
-  _addonDisabledByBlacklist(addon) {
-    let blacklist = this.options.addons.blacklist;
-    return !!blacklist && blacklist.indexOf(addon.name) !== -1;
+  _addonDisabledByExclude(addon) {
+    let exclude = this.options.addons.exclude;
+    return !!exclude && exclude.indexOf(addon.name) !== -1;
   }
 
   /**
     @private
-    @method _addonDisabledByWhitelist
+    @method _addonDisabledByInclude
     @param {Addon} addon
     @return {Boolean}
   */
-  _addonDisabledByWhitelist(addon) {
-    let whitelist = this.options.addons.whitelist;
-    return !!whitelist && whitelist.indexOf(addon.name) === -1;
+  _addonDisabledByInclude(addon) {
+    let include = this.options.addons.include;
+    return !!include && include.indexOf(addon.name) === -1;
   }
 
   /**
@@ -616,7 +654,7 @@ class EmberApp {
       return false;
     }
 
-    return !this._addonDisabledByBlacklist(addon) && !this._addonDisabledByWhitelist(addon);
+    return !this._addonDisabledByExclude(addon) && !this._addonDisabledByInclude(addon);
   }
 
   /**
@@ -628,18 +666,22 @@ class EmberApp {
   _notifyAddonIncluded() {
     let addonNames = this.project.addons.map((addon) => addon.name);
 
-    if (this.options.addons.blacklist) {
-      this.options.addons.blacklist.forEach((addonName) => {
+    if (this.options.addons.exclude) {
+      let optionKey = this.options.addons.hasBlacklist ? 'blacklist' : 'exclude';
+
+      this.options.addons.exclude.forEach((addonName) => {
         if (addonNames.indexOf(addonName) === -1) {
-          throw new Error(`Addon "${addonName}" defined in blacklist is not found`);
+          throw new Error(`Addon "${addonName}" defined in "${optionKey}" is not found`);
         }
       });
     }
 
-    if (this.options.addons.whitelist) {
-      this.options.addons.whitelist.forEach((addonName) => {
+    if (this.options.addons.include) {
+      let optionKey = this.options.addons.hasWhitelist ? 'whitelist' : 'include';
+
+      this.options.addons.include.forEach((addonName) => {
         if (addonNames.indexOf(addonName) === -1) {
-          throw new Error(`Addon "${addonName}" defined in whitelist is not found`);
+          throw new Error(`Addon "${addonName}" defined in "${optionKey}" is not found`);
         }
       });
     }


Test patch:
diff --git a/tests/unit/broccoli/ember-app-test.js b/tests/unit/broccoli/ember-app-test.js
index 65673a1f8..493f176a7 100644
--- a/tests/unit/broccoli/ember-app-test.js
+++ b/tests/unit/broccoli/ember-app-test.js
@@ -1154,8 +1154,8 @@ describe('EmberApp', function () {
             },
           });
 
-          expect(app._addonDisabledByBlacklist({ name: 'ember-foo-env-addon' })).to.be.true;
-          expect(app._addonDisabledByBlacklist({ name: 'Ember Random Addon' })).to.be.false;
+          expect(app._addonDisabledByExclude({ name: 'ember-foo-env-addon' })).to.be.true;
+          expect(app._addonDisabledByExclude({ name: 'Ember Random Addon' })).to.be.false;
           expect(app.project.addons.length).to.equal(8);
         });
 
@@ -1171,7 +1171,7 @@ describe('EmberApp', function () {
             });
           }
 
-          expect(load).to.throw('Addon "ember-cli-self-troll" defined in blacklist is not found');
+          expect(load).to.throw('Addon "ember-cli-self-troll" defined in "blacklist" is not found');
         });
       });
 
@@ -1186,8 +1186,8 @@ describe('EmberApp', function () {
             },
           });
 
-          expect(app._addonDisabledByWhitelist({ name: 'ember-foo-env-addon' })).to.be.false;
-          expect(app._addonDisabledByWhitelist({ name: 'Ember Random Addon' })).to.be.true;
+          expect(app._addonDisabledByInclude({ name: 'ember-foo-env-addon' })).to.be.false;
+          expect(app._addonDisabledByInclude({ name: 'Ember Random Addon' })).to.be.true;
           expect(app.project.addons.length).to.equal(1);
         });
 
@@ -1202,7 +1202,7 @@ describe('EmberApp', function () {
             });
           }
 
-          expect(load).to.throw('Addon "ember-cli-self-troll" defined in whitelist is not found');
+          expect(load).to.throw('Addon "ember-cli-self-troll" defined in "whitelist" is not found');
         });
       });
 
@@ -1220,6 +1220,122 @@ describe('EmberApp', function () {
           expect(app.project.addons.length).to.equal(0);
         });
       });
+
+      describe('exclude', function () {
+        it('prevents addons to be added to the project', function () {
+          process.env.EMBER_ENV = 'foo';
+
+          let app = new EmberApp({
+            project,
+            addons: {
+              exclude: ['ember-foo-env-addon'],
+            },
+          });
+
+          expect(app._addonDisabledByExclude({ name: 'ember-foo-env-addon' })).to.be.true;
+          expect(app._addonDisabledByExclude({ name: 'Ember Random Addon' })).to.be.false;
+          expect(app.project.addons.length).to.equal(8);
+        });
+
+        it('throws if unavailable addon is specified', function () {
+          function load() {
+            process.env.EMBER_ENV = 'foo';
+
+            new EmberApp({
+              project,
+              addons: {
+                exclude: ['ember-cli-self-troll'],
+              },
+            });
+          }
+
+          expect(load).to.throw('Addon "ember-cli-self-troll" defined in "exclude" is not found');
+        });
+      });
+
+      describe('include', function () {
+        it('prevents non-included addons to be added to the project', function () {
+          process.env.EMBER_ENV = 'foo';
+
+          let app = new EmberApp({
+            project,
+            addons: {
+              include: ['ember-foo-env-addon'],
+            },
+          });
+
+          expect(app._addonDisabledByInclude({ name: 'ember-foo-env-addon' })).to.be.false;
+          expect(app._addonDisabledByInclude({ name: 'Ember Random Addon' })).to.be.true;
+          expect(app.project.addons.length).to.equal(1);
+        });
+
+        it('throws if unavailable addon is specified', function () {
+          function load() {
+            process.env.EMBER_ENV = 'foo';
+
+            new EmberApp({
+              project,
+              addons: {
+                include: ['ember-cli-self-troll'],
+              },
+            });
+          }
+
+          expect(load).to.throw('Addon "ember-cli-self-troll" defined in "include" is not found');
+        });
+      });
+
+      describe('exclude wins over include', function () {
+        it('prevents addon to be added to the project', function () {
+          process.env.EMBER_ENV = 'foo';
+
+          let app = new EmberApp({
+            project,
+            addons: {
+              include: ['ember-foo-env-addon'],
+              exclude: ['ember-foo-env-addon'],
+            },
+          });
+
+          expect(app.project.addons.length).to.equal(0);
+        });
+      });
+
+      describe('blacklist and exclude', function () {
+        it('throws when both are specified', function () {
+          function load() {
+            process.env.EMBER_ENV = 'foo';
+
+            new EmberApp({
+              project,
+              addons: {
+                blacklist: ['ember-foo-env-addon'],
+                exclude: ['ember-foo-env-addon'],
+              },
+            });
+          }
+
+          expect(load).to.throw('Specifying both "blacklist" and "exclude" is not supported. Please use only one.');
+        });
+      });
+
+      describe('whitelist and include', function () {
+        it('throws when both are specified', function () {
+          function load() {
+            process.env.EMBER_ENV = 'foo';
+
+            new EmberApp({
+              project,
+              addons: {
+                whitelist: ['ember-foo-env-addon'],
+                include: ['ember-foo-env-addon'],
+              },
+            });
+          }
+
+          expect(load).to.throw('Specifying both "whitelist" and "include" is not supported. Please use only one.');
+        });
+      });
     });
 
     describe('addon instance bundle caching validation (when used within the project)', function () {
@@ -1256,7 +1372,7 @@ describe('EmberApp', function () {
           });
         }).to.throw(
           [
-            '[ember-cli] addon bundle caching is disabled for apps that specify an addon `whitelist`',
+            '[ember-cli] addon bundle caching is disabled for apps that specify an addon "whitelist"',
             '',
             'All addons using bundle caching:',
             projectWithBundleCaching.addons.find((addon) => addon.name === 'foo').packageRoot,
@@ -1286,7 +1402,67 @@ describe('EmberApp', function () {
           });
         }).to.throw(
           [
-            '[ember-cli] addon bundle caching is disabled for apps that specify an addon `blacklist`',
+            '[ember-cli] addon bundle caching is disabled for apps that specify an addon "blacklist"',
+            '',
+            'All addons using bundle caching:',
+            projectWithBundleCaching.addons.find((addon) => addon.name === 'foo').packageRoot,
+          ].join('\n')
+        );
+      });
+
+      it('throws an error if an addon `include` is specified', function () {
+        fixturifyProject.addInRepoAddon('foo', '1.0.0', { allowCachingPerBundle: true });
+        fixturifyProject.addInRepoAddon('foo-bar', '1.0.0', {
+          callback: (inRepoAddon) => {
+            inRepoAddon.pkg['ember-addon'].paths = ['../foo'];
+          },
+        });
+
+        fixturifyProject.writeSync();
+
+        let projectWithBundleCaching = fixturifyProject.buildProjectModel();
+        projectWithBundleCaching.initializeAddons();
+
+        expect(() => {
+          new EmberApp({
+            project: projectWithBundleCaching,
+            addons: {
+              include: ['foo'],
+            },
+          });
+        }).to.throw(
+          [
+            '[ember-cli] addon bundle caching is disabled for apps that specify an addon "include"',
+            '',
+            'All addons using bundle caching:',
+            projectWithBundleCaching.addons.find((addon) => addon.name === 'foo').packageRoot,
+          ].join('\n')
+        );
+      });
+
+      it('throws an error if an addon `exclude` is specified', function () {
+        fixturifyProject.addInRepoAddon('foo', '1.0.0', { allowCachingPerBundle: true });
+        fixturifyProject.addInRepoAddon('foo-bar', '1.0.0', {
+          callback: (inRepoAddon) => {
+            inRepoAddon.pkg['ember-addon'].paths = ['../foo'];
+          },
+        });
+
+        fixturifyProject.writeSync();
+
+        let projectWithBundleCaching = fixturifyProject.buildProjectModel();
+        projectWithBundleCaching.initializeAddons();
+
+        expect(() => {
+          new EmberApp({
+            project: projectWithBundleCaching,
+            addons: {
+              exclude: ['foo'],
+            },
+          });
+        }).to.throw(
+          [
+            '[ember-cli] addon bundle caching is disabled for apps that specify an addon "exclude"',
             '',
             'All addons using bundle caching:',
             projectWithBundleCaching.addons.find((addon) => addon.name === 'foo').packageRoot,