Repository: ember-cli/ember-cli (js)

Issue:
v3.15 w/ Node 14+ crashes consistently when postBuild hook throws
Discovered during some investigative work by @dfreeman over here https://github.com/typed-ember/ember-cli-typescript/issues/1439#issuecomment-878244006

tldr: 
 - postBuild returns a rejected promise, ember-cli crashes
 - rejected promises need to be caught from postBuild



Pull request description:
Handle rebuild failures without exiting
When a rebuild fails, mark it as a builder failure so the watcher doesn't re-throw it, causing it to be an unhandled error that results in the process exiting.

Fixes #9584 #9404

Patch:
diff --git a/lib/models/builder.js b/lib/models/builder.js
index 16b86a417..12e756203 100644
--- a/lib/models/builder.js
+++ b/lib/models/builder.js
@@ -221,6 +221,10 @@ class Builder extends CoreObject {
     } catch (error) {
       await this.processAddonBuildSteps('buildError', error);
 
+      // Mark this as a builder error so the watcher knows it has been handled
+      // and won't re-throw it
+      error.isBuilderError = true;
+
       throw error;
     } finally {
       clearInterval(uiProgressIntervalID);
diff --git a/lib/tasks/server/middleware/history-support/index.js b/lib/tasks/server/middleware/history-support/index.js
index 3d31dc14a..0635fa461 100644
--- a/lib/tasks/server/middleware/history-support/index.js
+++ b/lib/tasks/server/middleware/history-support/index.js
@@ -49,7 +49,16 @@ class HistorySupportAddon {
 
     app.use(async (req, _, next) => {
       try {
-        const results = await watcher;
+        let results;
+        try {
+          results = await watcher;
+        } catch (e) {
+          // This means there was a build error, so we won't actually be serving
+          // index.html, and we have nothing to do. We have to catch it here,
+          // though, or it will go uncaught and cause the process to exit.
+          return;
+        }
+
         if (this.shouldHandleRequest(req, options)) {
           let assetPath = req.path.slice(baseURL.length);
           let isFile = false;


Test patch:
diff --git a/tests/unit/models/builder-test.js b/tests/unit/models/builder-test.js
index f00f5f180..d92af8566 100644
--- a/tests/unit/models/builder-test.js
+++ b/tests/unit/models/builder-test.js
@@ -478,6 +478,20 @@ describe('models/builder.js', function () {
       await expect(builder.build()).to.be.rejected;
       expect(hooksCalled).to.deep.equal(['preBuild', 'build', 'postBuild', 'outputReady', 'buildError']);
     });
+
+    it('sets `isBuilderError` on handled addon errors', async function () {
+      addon.postBuild = function () {
+        return Promise.reject(new Error('preBuild Error'));
+      };
+
+      let error;
+      try {
+        await builder.build();
+      } catch (e) {
+        error = e;
+      }
+      expect(error).to.haveOwnProperty('isBuilderError', true);
+    });
   });
 
   describe('fallback from broccoli 2 to broccoli-builder', function () {