Repository: ProjectEvergreen/greenwood
Pull request #696 (2021-08-13 17:09:43)

Guides/issue 689 improve docs for theme pack developers
<!--
## Submitting a Pull Request
We love contributions and appreciate any help you can offer!
-->

## Related Issue
resolves #682 

## Summary of Changes
Found a not too bad work around for ensuring theme pack plugin authors can use Greenwood to run all commands when developing a theme pack
1. Update Theme Pack guide with lessons learned from getting [**greenwood-starter-presentation**](https://github.com/thescientist13/greenwood-starter-presentation/pull/33/files)
1. Added test cases specifically from the plugin author perspective (woops)


One observation left over seems to be an odd discrepancy between how [`process.env.__GWD_COMMAND__` is getting handled / set](https://github.com/ProjectEvergreen/greenwood/discussions/682#discussioncomment-1169324)?  🤔 

## TODO
1. [x] Should update FAQ now that there is a solution for this

Anyway, I think is good enough to close this particular issue out at least, and concentrate on the overall experience desired being tracked in #682 .


Patch:
diff --git a/www/pages/guides/theme-packs.md b/www/pages/guides/theme-packs.md
index 4d779ecf..03404485 100644
--- a/www/pages/guides/theme-packs.md
+++ b/www/pages/guides/theme-packs.md
@@ -120,13 +120,13 @@ So using our current example, our final _my-theme-pack.js_ would look like this:
 ```js
 const path = require('path');
 
-module.exports = () => [{
+module.exports = (options = {}) => [{
   type: 'context',
   name: 'my-theme-pack:context',
-  provider: (options = {}) => {
+  provider: compilation) => {
     // you can use other directory names besides templates/ this way!
     const templateLocation = options.__isDevelopment
-      ? path.join(process.cwd(), 'src/layouts')
+      ? path.join(compilation.context.userWorkspace, 'layouts')
       : path.join(__dirname, 'dist/layouts');
 
     return {
@@ -138,7 +138,7 @@ module.exports = () => [{
 }];
 ```
 
-And our final _greenwood.config.js_ would look like this, which add a "one-off" [resource plugin](/plugins/resource/) to tell Greenwood to route requests to your theme pack files away from _node_modules+ and to the location of your projects files for development.  
+And our final _greenwood.config.js_ would look like this, which adds a "one-off" [resource plugin](/plugins/resource/) to tell Greenwood to route requests to your theme pack files away from _node_modules+ and to the location of your projects files for development.  
 
 Additionally, we make sure to pass the flag from above for `__isDevelopment` to our plugin.
 ```js
@@ -155,11 +155,15 @@ class MyThemePackDevelopmentResource extends ResourceInterface {
   }
 
   async shouldResolve(url) {
-    return Promise.resolve(url.indexOf(`/node_modules/${packageName}/`) >= 0);
+    // eslint-disable-next-line no-underscore-dangle
+    return Promise.resolve((process.env.__GWD_COMMAND__ === 'develop') && url.indexOf(`/node_modules/${packageName}/`) >= 0);
   }
 
   async resolve(url) {
-    return Promise.resolve(url.replace(`/node_modules/${packageName}/dist/`, path.join(process.cwd(), '/src/')));
+    const { userWorkspace } = this.compilation.context;
+    const filePath = this.getBareUrlPath(url).split(`/node_modules/${packageName}/dist/`)[1];
+
+    return Promise.resolve(path.join(userWorkspace, filePath));
   }
 }
 
@@ -181,6 +185,20 @@ You should then be able to run `yarn develop` and load `/` in your browser and t
 
 You're all ready for development now! 🙌
 
+### Production Testing
+You can also use Greenwood to test your theme pack using a production build so that you can run `greenwood build` or `greenwood serve` to validate your work.  To do so requires just one additional script to your _package.json_ to put your theme pack files in the _node_modules_ where Greenwood would assume them to be.  Just call this before `build` or `serve`.
+```json
+{
+  "scripts": {
+    
+    "build:pre": "mkdir -pv ./node_modules/greenwood-starter-presentation/dist && rsync -rv --exclude 'pages/' ./src/ ./node_modules/greenwood-starter-presentation/dist",
+
+    "build": "npm run build:pre && greenwood build",
+    "serve": "npm run build:pre && greenwood serve"
+    
+  }
+}
+```
 
 ### Publishing
 When it comes to publishing, it should be fairly straightforward, and you'll just want to do the following:
@@ -237,34 +255,6 @@ Success! 🥳
 
 ### FAQ
 
-#### _I'm getting an (Rollup) error when trying to build or test my theme pack for production_
-If you try and run `yarn build` or `yarn serve` in a repo where you are creating the theme pack, as per the guide here, you may see this error if you reference assets like `<script>`, `<link>`, etc in your templates.  ex:
-
-```shell
-prerendering complete for page /slides/7/.
-prerendering complete for page /slides/6/.
-prerendering complete for page /.
-done prerendering all pages
-Error: ENOENT: no such file or directory, open '/Users/owenbuckley/Workspace/github/repos/greenwood-starter-presentation/node_modules/greenwood-starter-presentation/dist/components/presenter-mode.js'
-    at Object.openSync (fs.js:476:3)
-    at Object.readFileSync (fs.js:377:35)
-    at /Users/owenbuckley/Workspace/github/repos/greenwood-starter-presentation/node_modules/@greenwood/cli/src/config/rollup.config.js:185:35
-    at Array.forEach (<anonymous>)
-    at Object.buildStart (/Users/owenbuckley/Workspace/github/repos/greenwood-starter-presentation/node_modules/@greenwood/cli/src/config/rollup.config.js:171:23)
-    at /Users/owenbuckley/Workspace/github/repos/greenwood-starter-presentation/node_modules/rollup/dist/shared/rollup.js:18870:25
-    at async Promise.all (index 2)
-    at async rollupInternal (/Users/owenbuckley/Workspace/github/repos/greenwood-starter-presentation/node_modules/rollup/dist/shared/rollup.js:20239:9)
-    at async /Users/owenbuckley/Workspace/github/repos/greenwood-starter-presentation/node_modules/@greenwood/cli/src/lifecycles/bundle.js:12:24 {
-  errno: -2,
-  syscall: 'open',
-  code: 'ENOENT',
-  path: '/Users/owenbuckley/Workspace/github/repos/greenwood-starter-presentation/node_modules/greenwood-starter-presentation/dist/components/presenter-mode.js'
-}
-```
-
-Although within your theme pack project you can use `yarn develop` to create a theme pack like any other Greenwood project, there are a couple limitations.  Mainly from your theme pack templates you must explicitely reference _node_modules/<pacakge-name>/path/to/asset/_ as the starting prefix, but we are tracking a solution and `yarn develop` should be sufficient to be able to succesfully develop and publish for now.
-
-
 #### _Can I include pages as part of a theme pack?_
 
 Support for [including pages as part of a theme pack](https://github.com/ProjectEvergreen/greenwood/issues/681) is planned and coming soon, pretty much as soon as we can support [external data sources](https://github.com/ProjectEvergreen/greenwood/issues/21) in the CLI.


Test patch:
diff --git a/packages/cli/test/cases/build.plugins.context/build.plugins.context.spec.js b/packages/cli/test/cases/build.plugins.context/build.plugins.context.spec.js
index 5af76bee..886c70b6 100644
--- a/packages/cli/test/cases/build.plugins.context/build.plugins.context.spec.js
+++ b/packages/cli/test/cases/build.plugins.context/build.plugins.context.spec.js
@@ -1,6 +1,6 @@
 /*
  * Use Case
- * Run Greenwood with a custom context plugin that provides custom templates (app / page) and resources (JS / CSS); aka a "theme pack".
+ * Build with Greenwood when using a custom context plugin (e.g. installed via npm) that provides custom templates (app / page) and resources (JS / CSS); aka a "theme pack".
  *
  * User Result
  * Should generate a bare bones Greenwood build with expected templates being used from node_modules along with JS and CSS.
diff --git a/packages/cli/test/cases/develop.plugins.context/develop.plugins.context.spec.js b/packages/cli/test/cases/develop.plugins.context/develop.plugins.context.spec.js
index 784dd683..df7acb38 100644
--- a/packages/cli/test/cases/develop.plugins.context/develop.plugins.context.spec.js
+++ b/packages/cli/test/cases/develop.plugins.context/develop.plugins.context.spec.js
@@ -1,12 +1,12 @@
 /*
  * Use Case
- * Develop with Greenwood with a custom context plugin that provides custom templates (app / page) and resources (JS / CSS); aka a "theme pack".
+ * Develop with Greenwood when using a custom context plugin (e.g. installed via npm) that provides custom templates (app / page) and resources (JS / CSS); aka a "theme pack".
  *
  * User Result
  * Should start development server with expected templates being used from node_modules along with JS and CSS.
  *
  * User Command
- * greenwood build
+ * greenwood develop
  *
  * User Config
  * Mock Theme Pack Plugin (from fixtures)
diff --git a/packages/cli/test/cases/theme-pack/greenwood.config.js b/packages/cli/test/cases/theme-pack/greenwood.config.js
new file mode 100644
index 00000000..01fca701
--- /dev/null
+++ b/packages/cli/test/cases/theme-pack/greenwood.config.js
@@ -0,0 +1,33 @@
+const myThemePack = require('./my-theme-pack');
+const packageName = require('./package.json').name;
+const path = require('path');
+const { ResourceInterface } = require('@greenwood/cli/src/lib/resource-interface');
+
+class MyThemePackDevelopmentResource extends ResourceInterface {
+  constructor(compilation, options) {
+    super(compilation, options);
+    this.extensions = ['*'];
+  }
+
+  async shouldResolve(url) {
+    // eslint-disable-next-line no-underscore-dangle
+    return Promise.resolve(process.env.__GWD_COMMAND__ === 'develop' && url.indexOf(`/node_modules/${packageName}/`) >= 0);
+  }
+
+  async resolve(url) {
+    return Promise.resolve(this.getBareUrlPath(url).replace(`/node_modules/${packageName}/dist/`, path.join(process.cwd(), '/src/')));
+  }
+}
+
+module.exports = {
+  plugins: [
+    ...myThemePack({
+      __isDevelopment: true
+    }),
+    {
+      type: 'resource',
+      name: `${packageName}:resource`,
+      provider: (compilation, options) => new MyThemePackDevelopmentResource(compilation, options)
+    }
+  ]
+};
\ No newline at end of file
diff --git a/packages/cli/test/cases/theme-pack/my-theme-pack.js b/packages/cli/test/cases/theme-pack/my-theme-pack.js
new file mode 100644
index 00000000..ef539348
--- /dev/null
+++ b/packages/cli/test/cases/theme-pack/my-theme-pack.js
@@ -0,0 +1,18 @@
+const packageJson = require('./package.json');
+const path = require('path');
+
+module.exports = (options = {}) => [{
+  type: 'context',
+  name: `${packageJson.name}:context`,
+  provider: (compilation) => {
+    const templateLocation = options.__isDevelopment // eslint-disable-line no-underscore-dangle
+      ? path.join(compilation.context.userWorkspace, 'layouts')
+      : path.join(__dirname, 'dist/layouts');
+
+    return {
+      templates: [
+        templateLocation
+      ]
+    };
+  }
+}];
\ No newline at end of file
diff --git a/packages/cli/test/cases/theme-pack/package.json b/packages/cli/test/cases/theme-pack/package.json
new file mode 100644
index 00000000..13773b1a
--- /dev/null
+++ b/packages/cli/test/cases/theme-pack/package.json
@@ -0,0 +1,4 @@
+{
+  "name": "my-theme-pack",
+  "main": "my-theme-pack.js"
+}
\ No newline at end of file
diff --git a/packages/cli/test/cases/theme-pack/src/components/header.js b/packages/cli/test/cases/theme-pack/src/components/header.js
new file mode 100644
index 00000000..bc80f746
--- /dev/null
+++ b/packages/cli/test/cases/theme-pack/src/components/header.js
@@ -0,0 +1,18 @@
+const template = document.createElement('template');
+      
+template.innerHTML = `
+  <header>Welcome to my blog!</header>
+`;
+
+class HeaderComponent extends HTMLElement {
+  constructor() {
+    super();
+    this.attachShadow({ mode: 'open' });
+  }
+
+  async connectedCallback() {
+    this.shadowRoot.appendChild(template.content.cloneNode(true));
+  }
+}
+
+customElements.define('x-header', HeaderComponent);
\ No newline at end of file
diff --git a/packages/cli/test/cases/theme-pack/src/layouts/blog-post.html b/packages/cli/test/cases/theme-pack/src/layouts/blog-post.html
new file mode 100644
index 00000000..66790e89
--- /dev/null
+++ b/packages/cli/test/cases/theme-pack/src/layouts/blog-post.html
@@ -0,0 +1,16 @@
+<html>
+
+  <head>
+    <link rel="stylesheet" href="/node_modules/my-theme-pack/dist/styles/theme.css"></link> 
+    <script type="module" src="/node_modules/my-theme-pack/dist/components/header.js"></script> 
+  </head>
+
+  <body>
+    <x-header></x-header>
+
+    <h1>This is the blog post template called from the layouts directory.</h1>
+    <content-outlet></content-outlet>
+
+  </body>
+
+</html>
\ No newline at end of file
diff --git a/packages/cli/test/cases/theme-pack/src/pages/index.md b/packages/cli/test/cases/theme-pack/src/pages/index.md
new file mode 100644
index 00000000..d740afc2
--- /dev/null
+++ b/packages/cli/test/cases/theme-pack/src/pages/index.md
@@ -0,0 +1,7 @@
+---
+template: 'blog-post'
+---
+
+## Title of blog post
+
+Lorum Ipsum, this is a test.
\ No newline at end of file
diff --git a/packages/cli/test/cases/theme-pack/src/styles/main.css b/packages/cli/test/cases/theme-pack/src/styles/main.css
new file mode 100644
index 00000000..8b1fd120
--- /dev/null
+++ b/packages/cli/test/cases/theme-pack/src/styles/main.css
@@ -0,0 +1,7 @@
+@import './theme.css';
+
+:root {
+  color: var(--color-text-primary);
+  font-family: var(--font-family);
+  background-color: var(--color-secondary);
+}
\ No newline at end of file
diff --git a/packages/cli/test/cases/theme-pack/src/styles/theme.css b/packages/cli/test/cases/theme-pack/src/styles/theme.css
new file mode 100644
index 00000000..724c11ab
--- /dev/null
+++ b/packages/cli/test/cases/theme-pack/src/styles/theme.css
@@ -0,0 +1,5 @@
+:root {
+  --color-primary: #135;
+  --color-secondary: #74b238;
+  --font-family: 'Optima', sans-serif;
+}
\ No newline at end of file
diff --git a/packages/cli/test/cases/theme-pack/theme-pack.build.spec.js b/packages/cli/test/cases/theme-pack/theme-pack.build.spec.js
new file mode 100644
index 00000000..e84d317e
--- /dev/null
+++ b/packages/cli/test/cases/theme-pack/theme-pack.build.spec.js
@@ -0,0 +1,153 @@
+/*
+ * Use Case
+ * A theme pack _author_ creating a theme pack and using Greenwood for development and testing
+ * following the guide published on the Greenwood website. (https://www.greenwoodjs.io/guides/theme-packs/)
+ * 
+ * User Result
+ * Should correctly validate the develop and build / serve commands work correctly using tge expected templates 
+ * being resolved correctly per the known work around needs as documented in the FAQ and tracked in a discussion.
+ * https://github.com/ProjectEvergreen/greenwood/discussions/682
+ * 
+ * User Command
+ * greenwood develop
+ *
+ * User Config
+ * Mock Theme Pack Plugin (from fixtures)
+ *
+ * Plugin Author Workspace
+ * src/
+ *   components/
+ *     header.js
+ *   layouts/
+ *     blog-post.html
+ *   pages/
+ *     index.md
+ *   styles/
+ *     theme.css
+ */
+const expect = require('chai').expect;
+const glob = require('glob-promise');
+const { JSDOM } = require('jsdom');
+const path = require('path');
+const { getSetupFiles, getDependencyFiles, getOutputTeardownFiles } = require('../../../../../test/utils');
+const Runner = require('gallinago').Runner;
+const runSmokeTest = require('../../../../../test/smoke-test');
+
+describe('Build Greenwood With: ', function() {
+  const LABEL = 'Developement environment for a heme Pack';
+  const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
+  const outputPath = __dirname;
+  let runner;
+
+  before(function() {
+    this.context = {
+      publicDir: path.join(outputPath, 'public')
+    };
+    runner = new Runner(true);
+  });
+
+  describe(LABEL, function() {
+
+    before(async function() {
+      const themePacktemplates = await getDependencyFiles(
+        `${__dirname}/src/layouts/*.html`,
+        `${outputPath}/node_modules/my-theme-pack/dist/layouts`
+      );
+      const themePackStyles = await getDependencyFiles(
+        `${__dirname}/src/styles/*.css`,
+        `${outputPath}/node_modules/my-theme-pack/dist/styles`
+      );
+      const themePackComponents = await getDependencyFiles(
+        `${__dirname}/src/components/*.js`,
+        `${outputPath}/node_modules/my-theme-pack/dist/components`
+      );
+
+      await runner.setup(outputPath, [
+        ...getSetupFiles(outputPath),
+        ...themePacktemplates,
+        ...themePackStyles,
+        ...themePackComponents
+      ]);
+      await runner.runCommand(cliPath, 'build');
+    });
+
+    runSmokeTest(['public', 'index'], LABEL);
+
+    describe('Custom Default App and Page Templates', function() {
+      let dom;
+
+      before(async function() {
+        dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html'));
+      });
+
+      it('should have expected text from layout/blog-post.html', function() {
+        const pageTemplateHeading = dom.window.document.querySelectorAll('body h1')[0];
+
+        expect(pageTemplateHeading.textContent).to.be.equal('This is the blog post template called from the layouts directory.');
+      });
+
+      it('should have expected text from (test) user workspace pages/index.md', function() {
+        const heading = dom.window.document.querySelectorAll('body h2')[0];
+        const paragraph = dom.window.document.querySelectorAll('body p')[0];
+
+        expect(heading.textContent).to.be.equal('Title of blog post');
+        expect(paragraph.textContent).to.be.equal('Lorum Ipsum, this is a test.');
+      });
+    });
+
+    describe('Custom Theme Pack theme.css in app template', function() {
+      let dom;
+
+      before(async function() {
+        dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html'));
+      });
+
+      it('should output a single CSS file for custom theme pack styles/theme.css', async function() {
+        expect(await glob.promise(path.join(this.context.publicDir, '**/theme.*.css'))).to.have.lengthOf(1);
+      });
+
+      it('should have expected link tag in the head', function() {
+        const linkTag = Array.from(dom.window.document.querySelectorAll('head link'))
+          .filter((linkTag) => {
+            return linkTag.getAttribute('rel') === 'stylesheet'
+              && linkTag.getAttribute('href').indexOf('/theme.') === 0;
+          });
+
+        expect(linkTag.length).to.equal(1);
+      });
+    });
+
+    describe('Custom Theme Pack heading.js in custom title page template', function() {
+      let dom;
+
+      before(async function() {
+        dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'index.html'));
+      });
+
+      it('should output a single JS file for custom theme pack components/heading.js', async function() {
+        expect(await glob.promise(path.join(this.context.publicDir, '**/header.*.js'))).to.have.lengthOf(1);
+      });
+
+      it('should have expected link tag in the head', function() {
+        const scriptTag = Array.from(dom.window.document.querySelectorAll('head script'))
+          .filter((linkTag) => {
+            return linkTag.getAttribute('src').indexOf('/header.') === 0;
+          });
+
+        expect(scriptTag.length).to.equal(1);
+      });
+
+      it('should have expected heading component content', function() {
+        const header = dom.window.document.querySelector('body x-header header');
+
+        expect(header.textContent).to.equal('Welcome to my blog!');
+      });
+    });
+  });
+
+  after(function() {
+    runner.teardown(getOutputTeardownFiles(outputPath));
+  });
+
+});
+
diff --git a/packages/cli/test/cases/theme-pack/theme-pack.develop.spec.js b/packages/cli/test/cases/theme-pack/theme-pack.develop.spec.js
new file mode 100644
index 00000000..ceaf5bb2
--- /dev/null
+++ b/packages/cli/test/cases/theme-pack/theme-pack.develop.spec.js
@@ -0,0 +1,198 @@
+/*
+ * Use Case
+ * A theme pack _author_ creating a theme pack and using Greenwood for development and testing
+ * following the guide published on the Greenwood website. (https://www.greenwoodjs.io/guides/theme-packs/)
+ * 
+ * User Result
+ * Should correctly validate the develop and build / serve commands work correctly using tge expected templates 
+ * being resolved correctly per the known work around needs as documented in the FAQ and tracked in a discussion.
+ * https://github.com/ProjectEvergreen/greenwood/discussions/682
+ * 
+ * User Command
+ * greenwood develop
+ *
+ * User Config
+ * Mock Theme Pack Plugin (from fixtures)
+ *
+ * Plugin Author Workspace
+ * src/
+ *   components/
+ *     header.js
+ *   layouts/
+ *     blog-post.html
+ *   pages/
+ *     index.md
+ *   styles/
+ *     theme.css
+ */
+const expect = require('chai').expect;
+const { JSDOM } = require('jsdom');
+const packageJson = require('./package.json');
+const path = require('path');
+const request = require('request');
+const Runner = require('gallinago').Runner;
+const runSmokeTest = require('../../../../../test/smoke-test');
+
+describe('Develop Greenwood With: ', function() {
+  const LABEL = 'Developement environment for a Theme Pack';
+  const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
+  const outputPath = __dirname;
+  const hostname = 'http://localhost';
+  const port = 1984;
+  let runner;
+
+  before(function() {
+    this.context = {
+      hostname: `${hostname}:${port}`
+    };
+    runner = new Runner();
+  });
+
+  describe(LABEL, function() {
+
+    before(async function() {
+      await runner.setup(outputPath);
+
+      return new Promise(async (resolve) => {
+        setTimeout(() => {
+          resolve();
+        }, 3000);
+
+        await runner.runCommand(cliPath, 'develop');
+      });
+    });
+
+    runSmokeTest(['serve'], LABEL);
+
+    describe('Develop command specific HTML behaviors', function() {
+      let response = {};
+      let dom;
+
+      before(async function() {
+        return new Promise((resolve, reject) => {
+          request.get({
+            url: `http://127.0.0.1:${port}`,
+            headers: {
+              accept: 'text/html'
+            }
+          }, (err, res, body) => {
+            if (err) {
+              reject();
+            }
+
+            response = res;
+            
+            dom = new JSDOM(body);
+            resolve();
+          });
+        });
+      });
+
+      it('should return a 200', function(done) {
+        expect(response.statusCode).to.equal(200);
+        done();
+      });
+
+      it('should have expected text from from a mock package layouts/blog-post.html in the users workspace', function(done) {
+        const pageTemplateHeading = dom.window.document.querySelectorAll('body h1')[0];
+
+        expect(pageTemplateHeading.textContent).to.be.equal('This is the blog post template called from the layouts directory.');
+        done();
+      });
+
+      it('should have expected text from user workspace pages/index.md', function(done) {
+        const heading = dom.window.document.querySelectorAll('body h2')[0];
+        const paragraph = dom.window.document.querySelectorAll('body p')[0];
+
+        expect(heading.textContent).to.be.equal('Title of blog post');
+        expect(paragraph.textContent).to.be.equal('Lorum Ipsum, this is a test.');
+        done();
+      });
+    });
+
+    describe('Custom Theme Pack internal logic for resolving theme.css for local development', function() {
+      let response = {};
+
+      before(async function() {
+        return new Promise((resolve, reject) => {
+          request.get({
+            url: `http://127.0.0.1:${port}/node_modules/${packageJson.name}/dist/styles/theme.css`
+          }, (err, res, body) => {
+            if (err) {
+              reject();
+            }
+
+            response = res;
+            response.body = body;
+
+            resolve();
+          });
+        });
+      });
+
+      it('should return a 200', function(done) {
+        expect(response.statusCode).to.equal(200);
+
+        done();
+      });
+
+      it('should return the correct content type', function(done) {
+        expect(response.headers['content-type']).to.equal('text/css');
+        done();
+      });
+
+      it('should correctly return CSS from the developers local files', function(done) {
+        expect(response.body).to.equal(':root {\n  --color-primary: #135;\n  --color-secondary: #74b238;\n  --font-family: \'Optima\', sans-serif;\n}');
+
+        done();
+      });
+    });
+
+    describe('Custom Theme Pack internal logic for resolving header.js for local development', function() {
+      let response = {};
+
+      before(async function() {
+        return new Promise((resolve, reject) => {
+          request.get({
+            url: `http://127.0.0.1:${port}/node_modules/${packageJson.name}/dist/components/header.js`
+          }, (err, res, body) => {
+            if (err) {
+              reject();
+            }
+
+            response = res;
+            response.body = body;
+
+            resolve();
+          });
+        });
+      });
+
+      it('should return a 200', function(done) {
+        expect(response.statusCode).to.equal(200);
+
+        done();
+      });
+
+      it('should return the correct content type', function(done) {
+        expect(response.headers['content-type']).to.equal('text/javascript');
+        done();
+      });
+
+      it('should correctly return JavaScript from the developers local files', function(done) {
+        expect(response.body).to.contain('customElements.define(\'x-header\', HeaderComponent);');
+
+        done();
+      });
+    });
+  });
+
+  after(function() {
+    runner.stopCommand();
+    runner.teardown([
+      path.join(outputPath, '.greenwood')
+    ]);
+  });
+
+});
+