Repository: ember-cli/ember-cli
Pull request #9533 (2021-05-10 18:20:34)

Ensure package-info objects are stable when they represent the same addon
This fixes a bug where it's possible for package-infos representing the same addon to be _different_ throughout the project. This is done by adding a `packageRoot` property to each addon instance; we then use this to read the package as part of `loadAddon`.


Patch:
diff --git a/lib/models/package-info-cache/index.js b/lib/models/package-info-cache/index.js
index 70eb96baa..71f4b1004 100644
--- a/lib/models/package-info-cache/index.js
+++ b/lib/models/package-info-cache/index.js
@@ -244,7 +244,7 @@ class PackageInfoCache {
    * No copy is made.
    */
   loadAddon(addonInstance) {
-    let pkgInfo = this._readPackage(addonInstance.root, addonInstance.pkg);
+    let pkgInfo = this._readPackage(addonInstance.packageRoot, addonInstance.pkg);
 
     // NOTE: the returned pkgInfo may contain errors, or may contain
     // other packages that have errors. We will try to process
diff --git a/lib/models/package-info-cache/package-info.js b/lib/models/package-info-cache/package-info.js
index b3e96b986..34f48e1be 100644
--- a/lib/models/package-info-cache/package-info.js
+++ b/lib/models/package-info-cache/package-info.js
@@ -440,10 +440,12 @@ class PackageInfo {
     if (typeof module === 'function') {
       ctor = module;
       ctor.prototype.root = ctor.prototype.root || mainDir;
+      ctor.prototype.packageRoot = ctor.prototype.packageRoot || this.realPath;
       ctor.prototype.pkg = ctor.prototype.pkg || this.pkg;
     } else {
       const Addon = require('../addon'); // done here because of circular dependency
-      ctor = Addon.extend(Object.assign({ root: mainDir, pkg: this.pkg }, module));
+
+      ctor = Addon.extend(Object.assign({ root: mainDir, packageRoot: this.realPath, pkg: this.pkg }, module));
     }
 
     ctor._meta_ = {


Test patch:
diff --git a/tests/fixtures/addon/simple/lib/ember-super-button/lib/ember-with-addon-main/lib/main.js b/tests/fixtures/addon/simple/lib/ember-super-button/lib/ember-with-addon-main/lib/main.js
new file mode 100644
index 000000000..3ad817af8
--- /dev/null
+++ b/tests/fixtures/addon/simple/lib/ember-super-button/lib/ember-with-addon-main/lib/main.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = {
+  name: require('../package').name,
+};
diff --git a/tests/fixtures/addon/simple/lib/ember-super-button/lib/ember-with-addon-main/package.json b/tests/fixtures/addon/simple/lib/ember-super-button/lib/ember-with-addon-main/package.json
new file mode 100644
index 000000000..6cf2ccf37
--- /dev/null
+++ b/tests/fixtures/addon/simple/lib/ember-super-button/lib/ember-with-addon-main/package.json
@@ -0,0 +1,9 @@
+{
+  "name": "ember-with-addon-main",
+  "keywords": [
+    "ember-addon"
+  ],
+  "ember-addon": {
+    "main": "lib/main.js"
+  }
+}
diff --git a/tests/fixtures/addon/simple/lib/ember-super-button/package.json b/tests/fixtures/addon/simple/lib/ember-super-button/package.json
index d493c4084..a2c50d4bb 100644
--- a/tests/fixtures/addon/simple/lib/ember-super-button/package.json
+++ b/tests/fixtures/addon/simple/lib/ember-super-button/package.json
@@ -4,9 +4,12 @@
     "ember-addon"
   ],
   "ember-addon": {
-    "paths": ["./lib/ember-ng"]
+    "paths": [
+      "./lib/ember-ng",
+      "./lib/ember-with-addon-main"
+    ]
   },
   "dependencies": {
     "ember-yagni": "0"
   }
-}
+}
\ No newline at end of file
diff --git a/tests/fixtures/addon/simple/package.json b/tests/fixtures/addon/simple/package.json
index e1db417e4..ef32076f9 100644
--- a/tests/fixtures/addon/simple/package.json
+++ b/tests/fixtures/addon/simple/package.json
@@ -5,7 +5,10 @@
     "something-else": "latest"
   },
   "ember-addon": {
-    "paths": ["./lib/ember-super-button"]
+    "paths": [
+      "./lib/ember-super-button",
+      "./lib/ember-super-button/lib/ember-with-addon-main"
+    ]
   },
   "devDependencies": {
     "ember-resolver": "^7.0.0",
@@ -20,4 +23,4 @@
     "ember-addon-with-dependencies": "latest",
     "loader.js": "latest"
   }
-}
+}
\ No newline at end of file
diff --git a/tests/unit/broccoli/addon/linting-test.js b/tests/unit/broccoli/addon/linting-test.js
index e999de66e..6d18f159d 100644
--- a/tests/unit/broccoli/addon/linting-test.js
+++ b/tests/unit/broccoli/addon/linting-test.js
@@ -18,6 +18,7 @@ describe('Addon - linting', function () {
     let MockAddon = Addon.extend({
       name: 'first',
       root: input.path(),
+      packageRoot: input.path(),
     });
     lintTrees = [];
     let cli = new MockCLI();
diff --git a/tests/unit/broccoli/addon/module-name-test.js b/tests/unit/broccoli/addon/module-name-test.js
index f265f74da..9a4daf731 100644
--- a/tests/unit/broccoli/addon/module-name-test.js
+++ b/tests/unit/broccoli/addon/module-name-test.js
@@ -18,6 +18,7 @@ describe('Addon - moduleName', function () {
     input = await createTempDir();
     let MockAddon = Addon.extend({
       root: input.path(),
+      packageRoot: input.path(),
       name: 'fake-addon',
       moduleName() {
         return 'totes-not-fake-addon';
diff --git a/tests/unit/broccoli/ember-app-test.js b/tests/unit/broccoli/ember-app-test.js
index 7c51260fb..a982a63a3 100644
--- a/tests/unit/broccoli/ember-app-test.js
+++ b/tests/unit/broccoli/ember-app-test.js
@@ -299,6 +299,7 @@ describe('EmberApp', function () {
 
       let AddonFoo = Addon.extend({
         root: 'foo',
+        packageRoot: 'foo',
         name: 'foo',
       });
       let addonFoo = new AddonFoo(app, project);
@@ -330,6 +331,7 @@ describe('EmberApp', function () {
 
       let AddonFoo = Addon.extend({
         root: 'foo',
+        packageRoot: 'foo',
         name: 'foo',
         treeForStyles() {
           return addonFooStyles.path();
diff --git a/tests/unit/broccoli/template-precompilation-test.js b/tests/unit/broccoli/template-precompilation-test.js
index 1a321a519..c1aeddc89 100644
--- a/tests/unit/broccoli/template-precompilation-test.js
+++ b/tests/unit/broccoli/template-precompilation-test.js
@@ -60,6 +60,7 @@ describe('template preprocessors', function () {
       input = await createTempDir();
       let MockAddon = Addon.extend({
         root: input.path(),
+        packageRoot: input.path(),
         name: 'fake-addon',
       });
       let cli = new MockCLI();
diff --git a/tests/unit/models/addon-test.js b/tests/unit/models/addon-test.js
index 409815053..b77ec380f 100644
--- a/tests/unit/models/addon-test.js
+++ b/tests/unit/models/addon-test.js
@@ -37,6 +37,7 @@ describe('models/addon.js', function () {
       let TheAddon = Addon.extend({
         name: 'such name',
         root: path.resolve(fixturePath, 'simple'),
+        packageRoot: path.resolve(fixturePath, 'simple'),
         _warn(message) {
           warning = `${message}`;
         },
@@ -62,6 +63,7 @@ describe('models/addon.js', function () {
       FirstAddon = Addon.extend({
         name: 'first',
         root: projectPath,
+        packageRoot: projectPath,
 
         init() {
           this._super.apply(this, arguments);
@@ -73,6 +75,7 @@ describe('models/addon.js', function () {
       SecondAddon = Addon.extend({
         name: 'first',
         root: projectPath,
+        packageRoot: projectPath,
 
         init() {
           this._super.apply(this, arguments);
@@ -271,7 +274,7 @@ describe('models/addon.js', function () {
     });
 
     it('must define a `name` property', function () {
-      let Foo = Addon.extend({ root: 'foo' });
+      let Foo = Addon.extend({ root: 'foo', packageRoot: 'foo' });
 
       expect(() => {
         new Foo(project);
@@ -285,6 +288,7 @@ describe('models/addon.js', function () {
         let MyAddon = Addon.extend({
           name: 'test-project',
           root: 'foo',
+          packageRoot: 'foo',
         });
 
         let projectPath = path.resolve(fixturePath, 'simple');
@@ -362,6 +366,7 @@ describe('models/addon.js', function () {
     describe('findOwnAddonByName', function () {
       let ThisAddon = Addon.extend({
         root: 'foo',
+        packageRoot: 'foo',
         name: 'this-addon',
       });
 
@@ -400,6 +405,7 @@ describe('models/addon.js', function () {
         let MyAddon = Addon.extend({
           name: 'test-project',
           root: 'foo',
+          packageRoot: 'foo',
         });
 
         let projectPath = path.resolve(fixturePath, 'simple');
@@ -690,6 +696,7 @@ describe('models/addon.js', function () {
       let AddonTemp = Addon.extend({
         name: 'temp',
         root: 'foo',
+        packageRoot: 'foo',
       });
 
       addon = new AddonTemp(project, project);
@@ -714,6 +721,7 @@ describe('models/addon.js', function () {
       let BaseAddon = Addon.extend({
         name: 'test-project',
         root: projectPath,
+        packageRoot: projectPath,
       });
 
       addon = new BaseAddon(project, project);
@@ -742,6 +750,7 @@ describe('models/addon.js', function () {
       let MyAddon = Addon.extend({
         name: 'test-project',
         root: 'foo',
+        packageRoot: 'foo',
       });
 
       let projectPath = path.resolve(fixturePath, 'simple');
@@ -807,6 +816,7 @@ describe('models/addon.js', function () {
           Addon.extend({
             name: 'test-project',
             root: 'foo',
+            packageRoot: 'foo',
             treeForApp() {},
           })
         );
@@ -819,6 +829,7 @@ describe('models/addon.js', function () {
           Addon.extend({
             name: 'test-project',
             root: 'foo',
+            packageRoot: 'foo',
             compileAddon() {},
           })
         );
@@ -831,6 +842,7 @@ describe('models/addon.js', function () {
           Addon.extend({
             name: 'test-project',
             root: 'foo',
+            packageRoot: 'foo',
             init() {
               this._super && this._super.init.apply(this, arguments);
 
@@ -847,6 +859,7 @@ describe('models/addon.js', function () {
           Addon.extend({
             name: 'test-project',
             root: 'foo',
+            packageRoot: 'foo',
           })
         );
 
@@ -863,6 +876,7 @@ describe('models/addon.js', function () {
           Addon.extend({
             name: 'test-project',
             root: path.join(projectPath, 'node_modules', 'ember-generated-with-export-addon'),
+            packageRoot: path.join(projectPath, 'node_modules', 'ember-generated-with-export-addon'),
             treeForAddon(tree) {
               return tree;
             },
@@ -879,6 +893,7 @@ describe('models/addon.js', function () {
         let addonProto = {
           name: 'test-project',
           root: path.join(projectPath, 'node_modules', 'ember-generated-with-export-addon'),
+          packageRoot: path.join(projectPath, 'node_modules', 'ember-generated-with-export-addon'),
           treeForAddon(tree) {
             return tree;
           },
diff --git a/tests/unit/models/package-info-cache/package-info-cache-test.js b/tests/unit/models/package-info-cache/package-info-cache-test.js
index 3687a48fb..0de1cddc7 100644
--- a/tests/unit/models/package-info-cache/package-info-cache-test.js
+++ b/tests/unit/models/package-info-cache/package-info-cache-test.js
@@ -64,13 +64,13 @@ describe('models/package-info-cache/package-info-cache-test.js', function () {
   });
 
   describe('packageInfo contents tests on valid project', function () {
-    let projectPath, packageJsonPath, packageContents, projectPackageInfo;
+    let project, projectPath, packageJsonPath, packageContents, projectPackageInfo;
 
     beforeEach(function () {
       projectPath = path.resolve(addonFixturePath, 'simple');
       packageJsonPath = path.join(projectPath, 'package.json');
       packageContents = require(packageJsonPath);
-      let project = new Project(projectPath, packageContents, ui, cli);
+      project = new Project(projectPath, packageContents, ui, cli);
       let pic = project.packageInfoCache;
 
       projectPackageInfo = pic.getEntry(projectPath);
@@ -151,12 +151,21 @@ describe('models/package-info-cache/package-info-cache-test.js', function () {
       expect(packageAndErrorNames).to.deep.equal(devDependencyNames);
     });
 
-    it('shows projectPackageInfo has 1 in-repo addon named "ember-super-button"', function () {
+    it('shows projectPackageInfo has 2 in-repo addons', function () {
       let inRepoAddons = projectPackageInfo.inRepoAddons;
+
       expect(inRepoAddons).to.exist;
-      expect(inRepoAddons.length).to.equal(1);
+      expect(inRepoAddons.length).to.equal(2);
+
       expect(inRepoAddons[0].realPath.indexOf(`simple${path.sep}lib${path.sep}ember-super-button`)).to.be.above(0);
       expect(inRepoAddons[0].pkg.name).to.equal('ember-super-button');
+
+      expect(
+        inRepoAddons[1].realPath.indexOf(
+          `simple${path.sep}lib${path.sep}ember-super-button${path.sep}lib${path.sep}ember-with-addon-main`
+        )
+      ).to.be.above(0);
+      expect(inRepoAddons[1].pkg.name).to.equal('ember-with-addon-main');
     });
 
     it('shows projectPackageInfo has 7 internal addon packages', function () {
@@ -172,6 +181,60 @@ describe('models/package-info-cache/package-info-cache-test.js', function () {
       expect(nodeModules.entries).to.exist;
       expect(Object.keys(nodeModules.entries).length).to.equal(9);
     });
+
+    it('returns stable package infos for a package info representing the same addon', function () {
+      project.initializeAddons();
+
+      const findAddonsByName = (projectOrAddon, addonToFind, _foundAddons = []) => {
+        if (!projectOrAddon) {
+          return _foundAddons;
+        }
+
+        projectOrAddon.addons.forEach((addon) => {
+          if (addon.name === addonToFind) {
+            _foundAddons.push(addon);
+          }
+
+          findAddonsByName(addon, addonToFind, _foundAddons);
+        });
+
+        return _foundAddons;
+      };
+
+      const findAllInRepoPackageInfosByPredicate = (packageInfo, predicate, _foundPackageInfos = []) => {
+        if (predicate(packageInfo)) {
+          _foundPackageInfos.push(packageInfo);
+        }
+
+        (packageInfo.inRepoAddons || []).forEach((addonPackageInfo) =>
+          findAllInRepoPackageInfosByPredicate(addonPackageInfo, predicate, _foundPackageInfos)
+        );
+
+        return _foundPackageInfos;
+      };
+
+      let allAddonsWithAddonMain = findAddonsByName(project, 'ember-with-addon-main');
+
+      let projectAddonWithMainPackageInfo = findAllInRepoPackageInfosByPredicate(
+        project._packageInfo,
+        (packageInfo) =>
+          typeof packageInfo.addonMainPath === 'string' &&
+          packageInfo.addonMainPath.endsWith(path.join('ember-with-addon-main', 'lib', 'main.js'))
+      );
+
+      let allPackageInfosForAddonWithMain = [
+        ...allAddonsWithAddonMain.map((addon) => addon._packageInfo),
+        ...projectAddonWithMainPackageInfo,
+      ];
+
+      let areAllPackageInfosEqual = allPackageInfosForAddonWithMain.every(
+        (packageInfo) => packageInfo === allPackageInfosForAddonWithMain[0]
+      );
+
+      expect(allAddonsWithAddonMain.length).to.equal(2);
+      expect(allPackageInfosForAddonWithMain.length).to.equal(4);
+      expect(areAllPackageInfosEqual).to.equal(true);
+    });
   });
 
   describe('packageInfo', function () {
diff --git a/tests/unit/models/project-test.js b/tests/unit/models/project-test.js
index c24f0fbe4..f03b75422 100644
--- a/tests/unit/models/project-test.js
+++ b/tests/unit/models/project-test.js
@@ -347,6 +347,7 @@ describe('models/project.js', function () {
         'ember-non-root-addon',
         'ember-random-addon',
         'ember-super-button',
+        'ember-with-addon-main',
       ];
       expect(Object.keys(project.addonPackages)).to.deep.equal(expected);
     });