Repository: ProjectEvergreen/greenwood (js)

Issue:
not found static assets are returning 500 status code when running the `serve` command
## Type of Change
Bug

## Summary
Noticing that when running `greenwood serve` and accessing a static asset that doesn't exist, the browser is getting a `500` error instead of a `404`.

![Screen Shot 2023-04-29 at 10 41 26 AM](https://user-images.githubusercontent.com/895923/235308620-eb8c077e-f28c-4a14-bfa7-798ac2fbe60d.png)

## Details
In the terminal there is also an ugly error as well
```sh
[Error: ENOENT: no such file or directory, open '/Users/analogstudios/Workspace/analogstudios/repos/www.blissfestri.com/public/foo.png'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: '/Users/analogstudios/Workspace/analogstudios/repos/www.blissfestri.com/public/foo.png'
}
```

Pull request description:
Bug/issue 1102 not found static assets returning 500 for serve command
<!--
## Submitting a Pull Request
We love contributions and appreciate any help you can offer!
-->

## Related Issue
resolves #1102 

## Summary of Changes
1. Check resource exists before serving it
1. Update test cases to validate expected 404 handling

Patch:
diff --git a/packages/cli/src/lifecycles/serve.js b/packages/cli/src/lifecycles/serve.js
index 5e3da896..f23338e1 100644
--- a/packages/cli/src/lifecycles/serve.js
+++ b/packages/cli/src/lifecycles/serve.js
@@ -1,7 +1,7 @@
 import fs from 'fs/promises';
 import { hashString } from '../lib/hashing-utils.js';
 import Koa from 'koa';
-import { mergeResponse } from '../lib/resource-utils.js';
+import { checkResourceExists, mergeResponse } from '../lib/resource-utils.js';
 import { Readable } from 'stream';
 import { ResourceInterface } from '../lib/resource-interface.js';
 
@@ -227,34 +227,37 @@ async function getStaticServer(compilation, composable) {
   app.use(async (ctx, next) => {
     try {
       const url = new URL(`.${ctx.url}`, outputDir.href);
-      const resourcePlugins = standardResourcePlugins
-        .filter((plugin) => plugin.isStandardStaticResource)
-        .map((plugin) => {
-          return plugin.provider(compilation);
-        });
 
-      const request = new Request(url.href, {
-        headers: new Headers(ctx.request.header)
-      });
-      const initResponse = new Response(ctx.body, {
-        status: ctx.response.status,
-        headers: new Headers(ctx.response.header)
-      });
-      const response = await resourcePlugins.reduce(async (responsePromise, plugin) => {
-        return plugin.shouldServe && await plugin.shouldServe(url, request)
-          ? Promise.resolve(await plugin.serve(url, request))
-          : responsePromise;
-      }, Promise.resolve(initResponse));
+      if (await checkResourceExists(url)) {
+        const resourcePlugins = standardResourcePlugins
+          .filter((plugin) => plugin.isStandardStaticResource)
+          .map((plugin) => {
+            return plugin.provider(compilation);
+          });
 
-      if (response.ok) {
-        ctx.body = Readable.from(response.body);
-        ctx.type = response.headers.get('Content-Type');
-        ctx.status = response.status;
+        const request = new Request(url.href, {
+          headers: new Headers(ctx.request.header)
+        });
+        const initResponse = new Response(ctx.body, {
+          status: ctx.response.status,
+          headers: new Headers(ctx.response.header)
+        });
+        const response = await resourcePlugins.reduce(async (responsePromise, plugin) => {
+          return plugin.shouldServe && await plugin.shouldServe(url, request)
+            ? Promise.resolve(await plugin.serve(url, request))
+            : responsePromise;
+        }, Promise.resolve(initResponse));
 
-        // TODO automatically loop and apply all custom headers to Koa response, include Content-Type below
-        // https://github.com/ProjectEvergreen/greenwood/issues/1048
-        if (response.headers.has('Content-Length')) {
-          ctx.set('Content-Length', response.headers.get('Content-Length'));
+        if (response.ok) {
+          ctx.body = Readable.from(response.body);
+          ctx.type = response.headers.get('Content-Type');
+          ctx.status = response.status;
+
+          // TODO automatically loop and apply all custom headers to Koa response, include Content-Type below
+          // https://github.com/ProjectEvergreen/greenwood/issues/1048
+          if (response.headers.has('Content-Length')) {
+            ctx.set('Content-Length', response.headers.get('Content-Length'));
+          }
         }
       }
     } catch (e) {


Test patch:
diff --git a/packages/cli/test/cases/serve.default.api/serve.default.api.spec.js b/packages/cli/test/cases/serve.default.api/serve.default.api.spec.js
index 23cd8b30..ed6f8389 100644
--- a/packages/cli/test/cases/serve.default.api/serve.default.api.spec.js
+++ b/packages/cli/test/cases/serve.default.api/serve.default.api.spec.js
@@ -128,6 +128,30 @@ describe('Serve Greenwood With: ', function() {
         done();
       });
     });
+
+    describe('Serve command with API 404 not found behavior', function() {
+      let response = {};
+
+      before(async function() {
+        return new Promise((resolve, reject) => {
+          request.get(`${hostname}/api/foo`, (err, res, body) => {
+            if (err) {
+              reject();
+            }
+
+            response = res;
+            response.body = body;
+
+            resolve();
+          });
+        });
+      });
+
+      it('should return a 404 status', function(done) {
+        expect(response.statusCode).to.equal(404);
+        done();
+      });
+    });
   });
 
   after(function() {
diff --git a/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js b/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js
index 596967e9..d59b93c4 100644
--- a/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js
+++ b/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js
@@ -305,6 +305,30 @@ describe('Serve Greenwood With: ', function() {
         expect(scriptFiles.length).to.equal(2);
       });
     });
+
+    describe('Serve command with 404 not found behavior', function() {
+      let response = {};
+
+      before(async function() {
+        return new Promise((resolve, reject) => {
+          request.get(`${hostname}/foo`, (err, res, body) => {
+            if (err) {
+              reject();
+            }
+
+            response = res;
+            response.body = body;
+
+            resolve();
+          });
+        });
+      });
+
+      it('should return a 404 status', function(done) {
+        expect(response.statusCode).to.equal(404);
+        done();
+      });
+    });
   });
 
   after(function() {
diff --git a/packages/cli/test/cases/serve.default/serve.default.spec.js b/packages/cli/test/cases/serve.default/serve.default.spec.js
index f2a15e55..9db03782 100644
--- a/packages/cli/test/cases/serve.default/serve.default.spec.js
+++ b/packages/cli/test/cases/serve.default/serve.default.spec.js
@@ -432,6 +432,30 @@ describe('Serve Greenwood With: ', function() {
       });
     });
 
+    describe('Serve command with 404 not found behavior', function() {
+      let response = {};
+
+      before(async function() {
+        return new Promise((resolve, reject) => {
+          request.get(`${hostname}/foo.png`, (err, res, body) => {
+            if (err) {
+              reject();
+            }
+
+            response = res;
+            response.body = body;
+
+            resolve();
+          });
+        });
+      });
+
+      it('should return a 404 status', function(done) {
+        expect(response.statusCode).to.equal(404);
+        done();
+      });
+    });
+
   });
 
   after(function() {