Repository: ProjectEvergreen/greenwood (js)

Issue:
SSR pages with `prerender` configuration are not getting served from static HTML
## Type of Change
Bug

## Summary
After [upgrading to v0.28.0](https://github.com/AnalogStudiosRI/www.tuesdaystunes.tv), noticed that when [introduced API routes](https://github.com/AnalogStudiosRI/www.tuesdaystunes.tv/pull/114) into a "static" project, this combination triggers the hybrid server and now the HTML coming back for the home page is showing a bunch of 404s because it is being served from _index.js_ instead of the static _index.html_ output.  ([which actually revealed another bug](https://github.com/ProjectEvergreen/greenwood/issues/1098)).

![Screen Shot 2023-04-15 at 2 25 03 PM](https://user-images.githubusercontent.com/895923/232247215-38dbcd0c-3e68-4dcf-9dd7-f22eaf5ebeaf.png)

```html
<link rel="stylesheet" href="../styles/main.css" data-gwd-opt="inline">
          
<script type="module" data-gwd-opt="static" src="../components/footer/footer.js"></script>
<script type="module" data-gwd-opt="static" src="../components/header/header.js"></script>
<script type="module" src="../components/nav/nav.js"></script>
<script type="module" src="../components/video-card/video-card.js"></script>
```

> As a note, seems like these shouldn't be returning a `500` though, should be a `404`.

## Details
However, what is key to note about the difference between this and #1098 is that this project has `prerender: true` in its _greenwood.config.js_ which means this page should _not_ be serving from _index.js_.  The output _index.html_ is correctly bundled, and that is what should be being served.

In fact, there's no reason for this to even need a bundled _index.js_ in the first place.
```sh
% tree public
public
├── 2002200397.827098fd.js.map
├── 404
├── 404.html
├── _index.js
├── _templates
│   └── app.html
├── api
│   ├── assets
│   │   └── video-card-8a4d9eb0.js
│   ├── fragment.js
│   └── greeting.js
├── assets
│   ├── fonts
│   │   └── coop-bl.woff2
│   ├── icons
│   │   ├── facebook.svg
│   │   ├── spotify.svg
│   │   └── youtube.svg
│   ├── images
│   │   ├── avatar-dave.webp
│   │   ├── avatar-erika.webp
│   │   ├── avatar-owen.webp
│   │   └── avatar-tunesy.webp
│   ├── tuesdays-tunes-og.png
│   ├── tuesdays-tunes.webp
│   └── tunesy.webp
├── contact
│   └── index.html
├── favicon.ico
├── footer.5d06a040.js.map
├── graph.json
├── header.ebe96d78.js.map
├── index.html
├── index.js
├── manifest.json
├── nav.97858d20.js
├── nav.97858d20.js.map
├── resources.json
├── styles
├── video-card.1048ec31.js
└── video-card.1048ec31.js.map

10 directories, 31 files
```

Pull request description:
don't output SSR pages and templates when `prerender` configuration is enabled
<!--
## Submitting a Pull Request
We love contributions and appreciate any help you can offer!
-->

## Related Issue
resolves #1099 

## Summary of Changes
1. Exclude pre-rendered SSR pages from server rendering and bundling
1. Exclude SSR pages from copying __templates/_ if no SSR bundling needed
1. Add multiple test cases to cover / validate expected behavior

## TODO
1. [x] Add a test case to reproduce / validate the correct behavior
1. [x] In theory, pre-rendered SSR pages shouldn't even have an _index.js_ generated, so should look into that
1. [x] Should also verify if the __templates/_ directory also needs to get cleaned up

Patch:
diff --git a/packages/cli/src/lifecycles/bundle.js b/packages/cli/src/lifecycles/bundle.js
index 4da88ed4..db288148 100644
--- a/packages/cli/src/lifecycles/bundle.js
+++ b/packages/cli/src/lifecycles/bundle.js
@@ -192,92 +192,100 @@ async function bundleSsrPages(compilation) {
     return p;
   });
 
-  for (const page of compilation.graph) {
-    if (page.isSSR && !page.data.static) {
-      const { filename, path: pagePath } = page;
-      const scratchUrl = new URL(`./${filename}`, outputDir);
-
-      // better way to write out inline code like this?
-      await fs.writeFile(scratchUrl, `
-        import { Worker } from 'worker_threads';
-        import { getAppTemplate, getPageTemplate, getUserScripts } from '@greenwood/cli/src/lib/templating-utils.js';
-
-        export async function handler(request, compilation) {
-          const routeModuleLocationUrl = new URL('./_${filename}', '${outputDir}');
-          const routeWorkerUrl = '${compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider().workerUrl}';
-          const htmlOptimizer = compilation.config.plugins.find(plugin => plugin.name === 'plugin-standard-html').provider(compilation);
-          let body = 'Hello from the ${page.id} page!';
-          let html = '';
-          let frontmatter;
-          let template;
-          let templateType = 'page';
-          let title = '';
-          let imports = [];
-
-          await new Promise((resolve, reject) => {
-            const worker = new Worker(new URL(routeWorkerUrl));
-
-            worker.on('message', (result) => {
-              if (result.body) {
-                body = result.body;
-              }
-
-              if (result.template) {
-                template = result.template;
-              }
-
-              if (result.frontmatter) {
-                frontmatter = result.frontmatter;
-
-                if (frontmatter.title) {
-                  title = frontmatter.title;
+  if (!compilation.config.prerender) {
+    for (const page of compilation.graph) {
+      if (page.isSSR && !page.data.static) {
+        const { filename, path: pagePath } = page;
+        const scratchUrl = new URL(`./${filename}`, outputDir);
+
+        // better way to write out inline code like this?
+        await fs.writeFile(scratchUrl, `
+          import { Worker } from 'worker_threads';
+          import { getAppTemplate, getPageTemplate, getUserScripts } from '@greenwood/cli/src/lib/templating-utils.js';
+
+          export async function handler(request, compilation) {
+            const routeModuleLocationUrl = new URL('./_${filename}', '${outputDir}');
+            const routeWorkerUrl = '${compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider().workerUrl}';
+            const htmlOptimizer = compilation.config.plugins.find(plugin => plugin.name === 'plugin-standard-html').provider(compilation);
+            let body = 'Hello from the ${page.id} page!';
+            let html = '';
+            let frontmatter;
+            let template;
+            let templateType = 'page';
+            let title = '';
+            let imports = [];
+
+            await new Promise((resolve, reject) => {
+              const worker = new Worker(new URL(routeWorkerUrl));
+
+              worker.on('message', (result) => {
+                if (result.body) {
+                  body = result.body;
                 }
 
-                if (frontmatter.template) {
-                  templateType = frontmatter.template;
+                if (result.template) {
+                  template = result.template;
                 }
 
-                if (frontmatter.imports) {
-                  imports = imports.concat(frontmatter.imports);
+                if (result.frontmatter) {
+                  frontmatter = result.frontmatter;
+
+                  if (frontmatter.title) {
+                    title = frontmatter.title;
+                  }
+
+                  if (frontmatter.template) {
+                    templateType = frontmatter.template;
+                  }
+
+                  if (frontmatter.imports) {
+                    imports = imports.concat(frontmatter.imports);
+                  }
                 }
-              }
 
-              resolve();
-            });
+                resolve();
+              });
 
-            worker.on('error', reject);
-            worker.on('exit', (code) => {
-              if (code !== 0) {
-                reject(new Error(\`Worker stopped with exit code \${code}\`));
-              }
-            });
+              worker.on('error', reject);
+              worker.on('exit', (code) => {
+                if (code !== 0) {
+                  reject(new Error(\`Worker stopped with exit code \${code}\`));
+                }
+              });
 
-            worker.postMessage({
-              moduleUrl: routeModuleLocationUrl.href,
-              compilation: \`${JSON.stringify({ graph: intermediateGraph })}\`,
-              route: '${pagePath}'
+              worker.postMessage({
+                moduleUrl: routeModuleLocationUrl.href,
+                compilation: \`${JSON.stringify({ graph: intermediateGraph })}\`,
+                route: '${pagePath}'
+              });
             });
-          });
 
-          html = template ? template : await getPageTemplate('', compilation.context, templateType, []);
-          html = await getAppTemplate(html, compilation.context, imports, [], false, title);
-          html = await getUserScripts(html, compilation.context);
-          html = html.replace(\/\<content-outlet>(.*)<\\/content-outlet>\/s, body);
-          html = await (await htmlOptimizer.optimize(new URL(request.url), new Response(html))).text();
+            html = template ? template : await getPageTemplate('', compilation.context, templateType, []);
+            html = await getAppTemplate(html, compilation.context, imports, [], false, title);
+            html = await getUserScripts(html, compilation.context);
+            html = html.replace(\/\<content-outlet>(.*)<\\/content-outlet>\/s, body);
+            html = await (await htmlOptimizer.optimize(new URL(request.url), new Response(html))).text();
 
-          return new Response(html);
-        }
-      `);
+            return new Response(html);
+          }
+        `);
 
-      input.push(normalizePathnameForWindows(new URL(`./${filename}`, pagesDir)));
+        input.push(normalizePathnameForWindows(new URL(`./${filename}`, pagesDir)));
+      }
     }
-  }
 
-  const [rollupConfig] = await getRollupConfigForSsr(compilation, input);
+    const [rollupConfig] = await getRollupConfigForSsr(compilation, input);
 
-  if (rollupConfig.input.length !== 0) {
-    const bundle = await rollup(rollupConfig);
-    await bundle.write(rollupConfig.output);
+    if (rollupConfig.input.length > 0) {
+      const { userTemplatesDir, outputDir } = compilation.context;
+
+      if (await checkResourceExists(userTemplatesDir)) {
+        await fs.cp(userTemplatesDir, new URL('./_templates/', outputDir), { recursive: true });
+      }
+
+      const bundle = await rollup(rollupConfig);
+      await bundle.write(rollupConfig.output);
+    }
   }
 }
 
diff --git a/packages/cli/src/lifecycles/serve.js b/packages/cli/src/lifecycles/serve.js
index f54a1beb..55633971 100644
--- a/packages/cli/src/lifecycles/serve.js
+++ b/packages/cli/src/lifecycles/serve.js
@@ -284,7 +284,7 @@ async function getHybridServer(compilation) {
         headers: ctx.request.header
       });
 
-      if (matchingRoute.isSSR && !matchingRoute.data.static) {
+      if (!config.prerender && matchingRoute.isSSR && !matchingRoute.data.static) {
         const { handler } = await import(new URL(`./${matchingRoute.filename}`, outputDir));
         // TODO passing compilation this way too hacky?
         // https://github.com/ProjectEvergreen/greenwood/issues/1008
diff --git a/packages/cli/src/plugins/copy/plugin-copy-user-templates.js b/packages/cli/src/plugins/copy/plugin-copy-user-templates.js
deleted file mode 100644
index 12c1d15f..00000000
--- a/packages/cli/src/plugins/copy/plugin-copy-user-templates.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { checkResourceExists } from '../../lib/resource-utils.js';
-
-const greenwoodPluginCopyUserTemplates = [{
-  type: 'copy',
-  name: 'plugin-user-templates',
-  provider: async (compilation) => {
-    const { outputDir, userTemplatesDir } = compilation.context;
-    const assets = [];
-
-    if (await checkResourceExists(userTemplatesDir)) {
-      assets.push({
-        from: userTemplatesDir,
-        to: new URL('./_templates/', outputDir)
-      });
-    }
-
-    return assets;
-  }
-}];
-
-export { greenwoodPluginCopyUserTemplates };
\ No newline at end of file


Test patch:
diff --git a/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/greenwood.config.js b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/greenwood.config.js
new file mode 100644
index 00000000..8dc4be46
--- /dev/null
+++ b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/greenwood.config.js
@@ -0,0 +1,3 @@
+export default {
+  prerender: true
+};
\ No newline at end of file
diff --git a/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/serve.default.ssr-prerender-api-hybrid.spec.js b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/serve.default.ssr-prerender-api-hybrid.spec.js
new file mode 100644
index 00000000..c7c289e1
--- /dev/null
+++ b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/serve.default.ssr-prerender-api-hybrid.spec.js
@@ -0,0 +1,181 @@
+/*
+ * Use Case
+ * Run Greenwood with an SSR route that uses prerender configuration and also uses API routes.
+ *
+ * User Result
+ * Should generate a bare bones Greenwood build for hosting a prerender SSR application with API route handling.
+ *
+ * User Command
+ * greenwood build
+ *
+ * User Config
+ * {
+ *   prerender: true
+ * }
+ *
+ * User Workspace
+ *  src/
+ *   api/
+ *     greeting.js
+ *   components/
+ *     footer.js
+ *   pages/
+ *     index.js
+ *   templates/
+ *     app.html
+ */
+import chai from 'chai';
+import glob from 'glob-promise';
+import { JSDOM } from 'jsdom';
+import path from 'path';
+import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js';
+import request from 'request';
+import { runSmokeTest } from '../../../../../test/smoke-test.js';
+import { Runner } from 'gallinago';
+import { fileURLToPath, URL } from 'url';
+
+const expect = chai.expect;
+
+// https://github.com/ProjectEvergreen/greenwood/issues/1099
+describe('Serve Greenwood With: ', function() {
+  const LABEL = 'A Server Rendered Application (SSR) with prerender configuration and API routes';
+  const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
+  const hostname = 'http://127.0.0.1:8080';
+  const outputPath = fileURLToPath(new URL('.', import.meta.url));
+  let runner;
+
+  before(async function() {
+    this.context = {
+      publicDir: path.join(outputPath, 'public'),
+      hostname
+    };
+    runner = new Runner();
+  });
+
+  describe(LABEL, function() {
+
+    before(async function() {
+      await runner.setup(outputPath, getSetupFiles(outputPath));
+      await runner.runCommand(cliPath, 'build');
+
+      return new Promise(async (resolve) => {
+        setTimeout(() => {
+          resolve();
+        }, 10000);
+
+        await runner.runCommand(cliPath, 'serve');
+      });
+    });
+
+    runSmokeTest(['public', 'index', 'serve'], LABEL);
+
+    describe('Serve command that prerenders SSR pages', function() {
+      let dom;
+      let response;
+
+      before(async function() {
+        return new Promise((resolve, reject) => {
+          request.get(`${hostname}/`, (err, res, body) => {
+            if (err) {
+              reject();
+            }
+  
+            response = res;
+            response.body = body;
+            dom = new JSDOM(body);
+  
+            resolve();
+          });
+        });
+      });
+
+      describe('Serve command with HTML response for the home page', function() {
+        it('should return a 200 status', function(done) {
+          expect(response.statusCode).to.equal(200);
+          done();
+        });
+
+        it('should return the correct content type', function(done) {
+          expect(response.headers['content-type']).to.contain('text/html');
+          done();
+        });
+
+        it('should return a response body', function(done) {
+          expect(response.body).to.not.be.undefined;
+          done();
+        });
+
+        it('should have the expected output for the page', function() {
+          const headings = dom.window.document.querySelectorAll('body > h1');
+
+          expect(headings.length).to.equal(1);
+          expect(headings[0].textContent).to.equal('This is the home page.');
+        });
+
+        it('should have no bundled SSR output for the page', async function() {
+          const scriptFiles = (await glob.promise(path.join(this.context.publicDir, '*.js')))
+            .filter(file => file.indexOf('index.js') >= 0);
+
+          expect(scriptFiles.length).to.equal(0);
+        });
+      });
+
+      // TODO no page.js output
+      describe('Serve command for static HTML response with bundled home page <script> tag', function() {
+        it('should have the expected <script> tags in <head>', function(done) {
+          const scripts = dom.window.document.querySelectorAll('head > script');
+          expect(scripts.length).to.equal(1);
+          done();
+        });
+
+        it('should have the expected bundled filename', function(done) {
+          const script = dom.window.document.querySelectorAll('head > script')[0];
+
+          expect(script.getAttribute('src')).to.match(/^\/footer.*.js/);
+          done();
+        });
+      });
+    });
+
+    describe('Serve command with API specific behaviors for a JSON API', function() {
+      const name = 'Greenwood';
+      let response = {};
+
+      before(async function() {
+        return new Promise((resolve, reject) => {
+          request.get(`${hostname}/api/greeting?name=${name}`, (err, res, body) => {
+            if (err) {
+              reject();
+            }
+
+            response = res;
+            response.body = JSON.parse(body);
+
+            resolve();
+          });
+        });
+      });
+
+      it('should return a 200 status', function(done) {
+        expect(response.statusCode).to.equal(200);
+        done();
+      });
+
+      it('should return the correct content type', function(done) {
+        expect(response.headers['content-type']).to.contain('application/json');
+        done();
+      });
+
+      it('should return the correct response body', function(done) {
+        expect(response.body.message).to.equal(`Hello ${name}!!!`);
+        done();
+      });
+    });
+  });
+
+  after(function() {
+    runner.teardown(getOutputTeardownFiles(outputPath));
+    runner.stopCommand();
+  });
+
+});
\ No newline at end of file
diff --git a/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/src/api/greeting.js b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/src/api/greeting.js
new file mode 100644
index 00000000..09666902
--- /dev/null
+++ b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/src/api/greeting.js
@@ -0,0 +1,11 @@
+export async function handler(request) {
+  const params = new URLSearchParams(request.url.slice(request.url.indexOf('?')));
+  const name = params.has('name') ? params.get('name') : 'World';
+  const body = { message: `Hello ${name}!!!` };
+
+  return new Response(JSON.stringify(body), {
+    headers: new Headers({
+      'Content-Type': 'application/json'
+    })
+  });
+}
\ No newline at end of file
diff --git a/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/src/components/footer.js b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/src/components/footer.js
new file mode 100644
index 00000000..80b35600
--- /dev/null
+++ b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/src/components/footer.js
@@ -0,0 +1,16 @@
+const template = document.createElement('template');
+      
+template.innerHTML = '<footer>This is the footer component.</footer>';
+
+export default class FooterComponent extends HTMLElement {
+  constructor() {
+    super();
+    this.attachShadow({ mode: 'open' });
+  }
+
+  connectedCallback() {
+    this.shadowRoot.appendChild(template.content.cloneNode(true));
+  }
+}
+
+customElements.define('app-footer', FooterComponent);
\ No newline at end of file
diff --git a/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/src/pages/index.js b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/src/pages/index.js
new file mode 100644
index 00000000..fa3d8e44
--- /dev/null
+++ b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/src/pages/index.js
@@ -0,0 +1,7 @@
+export default class HomePage extends HTMLElement {
+  connectedCallback() {
+    this.innerHTML = `
+      <h1>This is the home page.</h1>
+    `;
+  }
+}
\ No newline at end of file
diff --git a/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/src/templates/app.html b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/src/templates/app.html
new file mode 100644
index 00000000..939d14bb
--- /dev/null
+++ b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/src/templates/app.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en" prefix="og:http://ogp.me/ns#">
+  <head>
+    <title>Default Title</title>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1"/>
+    <script type="module" src="/components/footer.js"></script>
+  </head>
+  <body>
+    <app-footer></app-footer>
+    <page-outlet></page-outlet>
+  </body>
+</html>
\ No newline at end of file
diff --git a/packages/cli/test/cases/serve.default.ssr-prerender/serve.default.ssr-prerender.spec.js b/packages/cli/test/cases/serve.default.ssr-prerender/serve.default.ssr-prerender.spec.js
index bc9e55c3..3ed198ae 100644
--- a/packages/cli/test/cases/serve.default.ssr-prerender/serve.default.ssr-prerender.spec.js
+++ b/packages/cli/test/cases/serve.default.ssr-prerender/serve.default.ssr-prerender.spec.js
@@ -23,6 +23,7 @@
  *     app.html
  */
 import chai from 'chai';
+import glob from 'glob-promise';
 import { JSDOM } from 'jsdom';
 import path from 'path';
 import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js';
@@ -107,6 +108,19 @@ describe('Serve Greenwood With: ', function() {
           expect(headings.length).to.equal(1);
           expect(headings[0].textContent).to.equal('This is the home page.');
         });
+
+        it('should have no bundled SSR output for the page', async function() {
+          const scriptFiles = (await glob.promise(path.join(this.context.publicDir, '*.js')))
+            .filter(file => file.indexOf('index.js') >= 0);
+
+          expect(scriptFiles.length).to.equal(0);
+        });
+
+        it('should have no _templates/ output directory for the app', async function() {
+          const templateFiles = await glob.promise(path.join(this.context.publicDir, '_templates/*'));
+
+          expect(templateFiles.length).to.equal(0);
+        });
       });
     });
   });
diff --git a/packages/cli/test/cases/serve.default.ssr-static-export/serve.default.ssr-static-export.spec.js b/packages/cli/test/cases/serve.default.ssr-static-export/serve.default.ssr-static-export.spec.js
index 72f1003d..7120ffee 100644
--- a/packages/cli/test/cases/serve.default.ssr-static-export/serve.default.ssr-static-export.spec.js
+++ b/packages/cli/test/cases/serve.default.ssr-static-export/serve.default.ssr-static-export.spec.js
@@ -22,6 +22,7 @@
  *     app.html
  */
 import chai from 'chai';
+import glob from 'glob-promise';
 import { JSDOM } from 'jsdom';
 import path from 'path';
 import { getSetupFiles, getDependencyFiles, getOutputTeardownFiles } from '../../../../../test/utils.js';
@@ -235,6 +236,13 @@ describe('Serve Greenwood With: ', function() {
 
         expect(counterScript.length).to.equal(1);
       });
+
+      it('should have no bundled SSR output for the page', async function() {
+        const scriptFiles = (await glob.promise(path.join(this.context.publicDir, '*.js')))
+          .filter(file => file.indexOf('artists.js') >= 0);
+
+        expect(scriptFiles.length).to.equal(0);
+      });
     });
   });
 
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 aecc6a23..d119d357 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
@@ -27,6 +27,7 @@
  *     app.html
  */
 import chai from 'chai';
+import glob from 'glob-promise';
 import fs from 'fs';
 import { JSDOM } from 'jsdom';
 import path from 'path';
@@ -131,6 +132,19 @@ describe('Serve Greenwood With: ', function() {
         expect(headings.length).to.equal(1);
         expect(headings[0].textContent).to.equal('Hello from the server rendered home page!');
       });
+
+      it('should have the expected bundled SSR output for the page', async function() {
+        const scriptFiles = (await glob.promise(path.join(this.context.publicDir, '*.js')))
+          .filter(file => file.indexOf('index.js') >= 0);
+
+        expect(scriptFiles.length).to.equal(2);
+      });
+
+      it('should have the expected _templates/ output directory for the app', async function() {
+        const templateFiles = await glob.promise(path.join(this.context.publicDir, '_templates/*'));
+
+        expect(templateFiles.length).to.equal(1);
+      });
     });
 
     describe('Serve command with HTML route response for artists page using "get" functions', function() {
@@ -227,6 +241,13 @@ describe('Serve Greenwood With: ', function() {
         expect(artistsPageGraphData.imports[0].src).to.equal(`/components/${componentName}.js`);
         expect(counterScript.length).to.equal(1);
       });
+
+      it('should have the expected bundled SSR output for the page', async function() {
+        const scriptFiles = (await glob.promise(path.join(this.context.publicDir, '*.js')))
+          .filter(file => file.indexOf('artists.js') >= 0);
+
+        expect(scriptFiles.length).to.equal(2);
+      });
     });
 
     describe('Prerender an HTML route response for users page exporting an HTMLElement as default export', function() {
@@ -257,6 +278,13 @@ describe('Serve Greenwood With: ', function() {
         expect(cardScript.length).to.be.equal(1);
         expect(cardScript[0].type).to.be.equal('module');
       });
+
+      it('should have the expected bundled SSR output for the page', async function() {
+        const scriptFiles = (await glob.promise(path.join(this.context.publicDir, '*.js')))
+          .filter(file => file.indexOf('users.js') >= 0);
+
+        expect(scriptFiles.length).to.equal(2);
+      });
     });
   });