Repository: ember-cli/ember-cli
Pull request #9507 (2021-04-20 04:00:32)

Add a new config, `ember-addon.projectRoot`, to specify the location of the project
This config allows you to have a nested project and to allow running `ember s` from outside of the project's directory; e.g., if you're using workspaces and still want to allow `ember s` from the root of the repo, all you'd have to do is update the top-level `package.json` to have the following config:

```json
{
  "ember-addon": {
    "projectRoot": "./packages/path-to-ember-project"
  }
}
```

rwjblue: Code changes look good. We need to ensure that this is documented as well.

Can you:

* Update the prose here to describe the function properly now?

https://github.com/ember-cli/ember-cli/blob/0a74e73493f82bed69eab637bcbddc7286726d55/lib/models/project.js#L646-L647

* Prepare a PR for https://github.com/ember-learn/cli-guides/blob/master/guides/appendix/configuration.md explaining this new type of configuration

Patch:
diff --git a/lib/models/project.js b/lib/models/project.js
index ec35a9cc0..6ccde5cc0 100644
--- a/lib/models/project.js
+++ b/lib/models/project.js
@@ -643,9 +643,13 @@ class Project {
   }
 
   /**
-    Returns a new project based on the first package.json that is found
+    Returns a new project based on the first `package.json` that is found
     in `pathName`.
 
+    If the above `package.json` specifies `ember-addon.projectRoot`, we load
+    the project based on the relative path between this directory and the
+    specified `projectRoot`.
+
     @private
     @static
     @method closestSync
@@ -659,15 +663,26 @@ class Project {
     let ui = ensureUI(_ui);
 
     let directory = findupPath(pathName);
+    let pkg = fs.readJsonSync(path.join(directory, 'package.json'));
     logger.info('found package.json at %s', directory);
 
+    // allow `package.json` files to specify where the actual project lives
+    if (pkg && pkg['ember-addon'] && typeof pkg['ember-addon'].projectRoot === 'string') {
+      if (fs.existsSync(path.join(directory, 'ember-cli-build.js'))) {
+        throw new Error(
+          `Both \`ember-addon.projectRoot\` and \`ember-cli-build.js\` exist as part of \`${directory}\``
+        );
+      }
+
+      return Project.closestSync(path.join(directory, pkg['ember-addon'].projectRoot), _ui, _cli);
+    }
+
     let relative = path.relative(directory, pathName);
     if (relative.indexOf('tmp') === 0) {
       logger.info('ignoring parent project since we are in the tmp folder of the project');
       return Project.nullProject(_ui, _cli);
     }
 
-    let pkg = fs.readJsonSync(path.join(directory, 'package.json'));
     logger.info('project name: %s', pkg && pkg.name);
 
     if (!isEmberCliProject(pkg)) {


Test patch:
diff --git a/tests/fixtures/app/nested-project/actual-project/app/templates/application.hbs b/tests/fixtures/app/nested-project/actual-project/app/templates/application.hbs
new file mode 100644
index 000000000..41f167ffc
--- /dev/null
+++ b/tests/fixtures/app/nested-project/actual-project/app/templates/application.hbs
@@ -0,0 +1,7 @@
+{{page-title "Actual Project"}}
+
+{{!-- The following component displays Ember's default welcome message. --}}
+<WelcomePage />
+{{!-- Feel free to remove this! --}}
+
+{{outlet}}
diff --git a/tests/fixtures/app/nested-project/actual-project/ember-cli-build.js b/tests/fixtures/app/nested-project/actual-project/ember-cli-build.js
new file mode 100644
index 000000000..48e94e9e4
--- /dev/null
+++ b/tests/fixtures/app/nested-project/actual-project/ember-cli-build.js
@@ -0,0 +1,24 @@
+'use strict';
+
+const EmberApp = require('ember-cli/lib/broccoli/ember-app');
+
+module.exports = function (defaults) {
+  let app = new EmberApp(defaults, {
+    // Add options here
+  });
+
+  // Use `app.import` to add additional libraries to the generated
+  // output files.
+  //
+  // If you need to use different assets in different
+  // environments, specify an object as the first parameter. That
+  // object's keys should be the environment name and the values
+  // should be the asset to use in that environment.
+  //
+  // If the library that you are including contains AMD or ES6
+  // modules that you would like to import into your application
+  // please specify an object with the list of modules as keys
+  // along with the exports of each module as its value.
+
+  return app.toTree();
+};
diff --git a/tests/fixtures/app/nested-project/actual-project/package.json b/tests/fixtures/app/nested-project/actual-project/package.json
new file mode 100644
index 000000000..34a2011b0
--- /dev/null
+++ b/tests/fixtures/app/nested-project/actual-project/package.json
@@ -0,0 +1,70 @@
+{
+  "name": "actual-project",
+  "version": "0.0.0",
+  "private": true,
+  "description": "Small description for actual-project goes here",
+  "repository": "",
+  "license": "MIT",
+  "author": "",
+  "directories": {
+    "doc": "doc",
+    "test": "tests"
+  },
+  "scripts": {
+    "build": "ember build --environment=production",
+    "lint": "npm-run-all --aggregate-output --continue-on-error --parallel \"lint:!(fix)\"",
+    "lint:fix": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*:fix",
+    "lint:hbs": "ember-template-lint .",
+    "lint:hbs:fix": "ember-template-lint . --fix",
+    "lint:js": "eslint . --cache",
+    "lint:js:fix": "eslint . --fix",
+    "start": "ember serve",
+    "test": "npm-run-all lint test:*",
+    "test:ember": "ember test"
+  },
+  "devDependencies": {
+    "@ember/optional-features": "^2.0.0",
+    "@ember/test-helpers": "^2.2.5",
+    "@glimmer/component": "^1.0.4",
+    "@glimmer/tracking": "^1.0.4",
+    "babel-eslint": "^10.1.0",
+    "broccoli-asset-rev": "^3.0.0",
+    "ember-auto-import": "^1.11.2",
+    "ember-cli": "~<%= emberCLIVersion %>",
+    "ember-cli-app-version": "^5.0.0",
+    "ember-cli-babel": "^7.26.3",
+    "ember-cli-dependency-checker": "^3.2.0",
+    "ember-cli-htmlbars": "^5.7.1",
+    "ember-cli-inject-live-reload": "^2.0.2",
+    "ember-cli-sri": "^2.1.1",
+    "ember-cli-terser": "^4.0.1",
+    "ember-data": "~3.27.0-beta.0",
+    "ember-export-application-global": "^2.0.1",
+    "ember-fetch": "^8.0.4",
+    "ember-load-initializers": "^2.1.2",
+    "ember-maybe-import-regenerator": "^0.1.6",
+    "ember-page-title": "^6.2.1",
+    "ember-qunit": "^5.1.4",
+    "ember-resolver": "^8.0.2",
+    "ember-source": "~3.27.0-beta.3",
+    "ember-template-lint": "^3.2.0",
+    "ember-welcome-page": "^4.0.0",
+    "eslint": "^7.24.0",
+    "eslint-config-prettier": "^8.1.0",
+    "eslint-plugin-ember": "^10.3.0",
+    "eslint-plugin-node": "^11.1.0",
+    "eslint-plugin-prettier": "^3.3.1",
+    "eslint-plugin-qunit": "^6.0.0",
+    "loader.js": "^4.7.0",
+    "npm-run-all": "^4.1.5",
+    "prettier": "^2.2.1",
+    "qunit": "^2.14.1",
+    "qunit-dom": "^1.6.0"
+  },
+  "engines": {
+    "node": "10.* || >= 12"
+  },
+  "ember": {
+    "edition": "octane"
+  }
+}
diff --git a/tests/fixtures/app/nested-project/package.json b/tests/fixtures/app/nested-project/package.json
new file mode 100644
index 000000000..1c6ac30bd
--- /dev/null
+++ b/tests/fixtures/app/nested-project/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "nested-project",
+  "ember-addon": {
+    "projectRoot": "./actual-project"
+  }
+}
\ No newline at end of file
diff --git a/tests/fixtures/app/project-root-with-ember-cli-build/ember-cli-build.js b/tests/fixtures/app/project-root-with-ember-cli-build/ember-cli-build.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/fixtures/app/project-root-with-ember-cli-build/package.json b/tests/fixtures/app/project-root-with-ember-cli-build/package.json
new file mode 100644
index 000000000..da66aad83
--- /dev/null
+++ b/tests/fixtures/app/project-root-with-ember-cli-build/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "project-root-with-ember-cli-build",
+  "ember-addon": {
+    "projectRoot": "./actual-project"
+  }
+}
\ No newline at end of file
diff --git a/tests/unit/models/project-test.js b/tests/unit/models/project-test.js
index 49f3037f6..77d262214 100644
--- a/tests/unit/models/project-test.js
+++ b/tests/unit/models/project-test.js
@@ -672,4 +672,22 @@ describe('models/project.js', function () {
       );
     });
   });
+
+  describe('Project.closestSync', function () {
+    it('should use the `actual-project` specified by `ember-addon.projectRoot` in the top-level `package.json`', function () {
+      let cli = new MockCLI();
+      projectPath = path.resolve(__dirname, '../../fixtures/app/nested-project');
+      project = Project.closestSync(projectPath, cli.ui, cli);
+      expect(project.root).to.equal(path.resolve(__dirname, '../../fixtures/app/nested-project/actual-project'));
+    });
+
+    it('should throw if both `ember-addon.projectRoot` and `ember-cli-build.js` exist', function () {
+      let cli = new MockCLI();
+      projectPath = path.resolve(__dirname, '../../fixtures/app/project-root-with-ember-cli-build');
+
+      expect(() => Project.closestSync(projectPath, cli.ui, cli)).to.throw(
+        `Both \`ember-addon.projectRoot\` and \`ember-cli-build.js\` exist as part of \`${projectPath}\``
+      );
+    });
+  });
 });