Repository: ember-cli/ember-cli
Pull request #7461 (2017-11-22 23:37:59)

Change when availableOptions are normalized
The normalization of the availableOptions was done during the init step on the parent Command class. However, this meant that if a child class was to extend from Command they could not set availableOptions within their own init or beforeRun steps as the normalization would have already been done. This change moves the normalization to just before it is needed and after init, beforeRun, etc have happened.

Fixes: #7459 


Patch:
diff --git a/lib/models/command.js b/lib/models/command.js
index 1b45c832a..f5647b09f 100644
--- a/lib/models/command.js
+++ b/lib/models/command.js
@@ -155,8 +155,6 @@ let Command = CoreObject.extend({
      * ```
      */
     this.anonymousOptions = this.anonymousOptions || [];
-
-    this.registerOptions();
   },
 
   /**
@@ -543,6 +541,8 @@ let Command = CoreObject.extend({
     let commandOptions = {};
     let parsedOptions;
 
+    this.registerOptions();
+
     let assembleAndValidateOption = function(option) {
       return this.assignOption(option, parsedOptions, commandOptions);
     };
@@ -640,6 +640,8 @@ let Command = CoreObject.extend({
    */
   getJson(options) {
     let json = {};
+
+    this.registerOptions(options);
     this._printableProperties.forEach(key => (json[key] = this[key]));
 
     if (this.addAdditionalJsonForHelp) {


Test patch:
diff --git a/tests/unit/models/command-test.js b/tests/unit/models/command-test.js
index 75a73a2a8..d642653ef 100644
--- a/tests/unit/models/command-test.js
+++ b/tests/unit/models/command-test.js
@@ -214,6 +214,65 @@ describe('models/command.js', function() {
     });
   });
 
+  it('should be able to set availableOptions within init', function() {
+    let AvailableOptionsInitCommand = Command.extend({
+      name: 'available-options-init-command',
+      init() {
+        this._super.apply(this, arguments);
+
+        this.availableOptions = [{
+          name: 'spicy',
+          type: String,
+          default: true,
+        }];
+      },
+      run(options) { return options; },
+    });
+
+    return new AvailableOptionsInitCommand(Object.assign(options, {
+      project: {
+        hasDependencies() { return true; },
+        isEmberCLIProject() { return false; },
+      },
+    })).validateAndRun([]).then(function(commandOptions) {
+      expect(commandOptions).to.deep.equal({ spicy: true });
+    });
+  });
+
+  it('should be able to set availableOptions within beforeRun', function() {
+    let AvailableOptionsInitCommand = Command.extend({
+      name: 'available-options-init-command',
+
+      availableOptions: [{
+        name: 'spicy',
+        type: Boolean,
+        default: true,
+      }],
+
+      beforeRun() {
+        return new Promise(resolve => {
+          resolve(this.availableOptions.push({
+            name: 'foobar',
+            type: String,
+            default: 'bazbaz',
+          }));
+        });
+      },
+      run(options) { return options; },
+    });
+
+    const command = new AvailableOptionsInitCommand(Object.assign(options, {
+      project: {
+        hasDependencies() { return true; },
+        isEmberCLIProject() { return false; },
+      },
+    }));
+
+    return command.beforeRun().then(() => command.validateAndRun([]).then(commandOptions => {
+      expect(commandOptions).to.deep.equal({ spicy: true, foobar: 'bazbaz' });
+    }));
+  });
+
   it('availableOptions with aliases should work.', function() {
     expect(new OptionsAliasCommand(options).parseArgs(['-soft-shell'])).to.deep.equal({
       options: {
@@ -455,11 +514,17 @@ describe('models/command.js', function() {
     };
     let dupe = { name: 'spicy', type: Boolean, default: true, aliases: [{ 'mild': false }] };
     let noAlias = { name: 'reload', type: Boolean, default: false };
-    expect(new OptionsAliasCommand(options).validateOption(option)).to.be.ok;
+    const aliasCommand = new OptionsAliasCommand(options);
+    aliasCommand.registerOptions();
+    expect(aliasCommand.validateOption(option)).to.be.ok;
 
-    expect(new ServeCommand(options).validateOption(noAlias)).to.be.false;
+    const serveCommand = new ServeCommand(options);
+    serveCommand.registerOptions();
+    expect(serveCommand.validateOption(noAlias)).to.be.false;
 
-    expect(new OptionsAliasCommand(options).validateOption(dupe)).to.be.false;
+    const optionsAliasCommand = new OptionsAliasCommand(options);
+    optionsAliasCommand.registerOptions();
+    expect(optionsAliasCommand.validateOption(dupe)).to.be.false;
   });
 
   it('validateOption() should throw an error when option is missing name or type', function() {