Repository: ProjectEvergreen/greenwood (js)

Issue:
default template merging is generating two `<head>` tags
## Type of Change
- [ ] New Feature Request
- [ ] Documentation / Website
- [ ] Improvement / Suggestion
- [x] Bug
- [ ] Other (please clarify below)

## Summary
If you follow the Quick Start or are having templates being merged together, it looks like duplicate `<head>` tags are being output.

```sh
$ mkdir src/pages
$ echo "## hello world" > src/pages/index.md
$ npx @greenwood/cli buid
```

results in 
```html
<!DOCTYPE html>
<html lang="en" prefix="og:http://ogp.me/ns#">

<head>
  <head>
    <style>
      body {
        transition: opacity ease-in 0.2s;
      }

      body[unresolved] {
        opacity: 0;
        display: block;
        overflow: hidden;
        position: relative;
      }
    </style>

    <title>My App</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
  </head>
</head>



<body>
  <h2>hello world</h2>
</body>

</html>
```

## Details 
This is also reproducible using the [default test case](https://github.com/ProjectEvergreen/greenwood/tree/v0.10.1/packages/cli/test/cases/build.default), which is surprising since I made [smoke tests](https://github.com/ProjectEvergreen/greenwood/blob/v0.10.1/test/utils.js) for this?

> _This was likely introduced as part of #512_

Pull request:
fix duplicate head tags and harden smoke tests
<!--
## Submitting a Pull Request
We love contributions and appreciate any help you can offer!
-->

## Related Issue
resolves #564 

## Summary of Changes
1. Found instances where `<head>` tags were being duplicated back into templates
1. Hardened tests to better ensure proper tag matching

Patch:
diff --git a/packages/cli/src/plugins/resource/plugin-node-modules.js b/packages/cli/src/plugins/resource/plugin-node-modules.js
index b030b1a6..70d5b3fb 100644
--- a/packages/cli/src/plugins/resource/plugin-node-modules.js
+++ b/packages/cli/src/plugins/resource/plugin-node-modules.js
@@ -211,11 +211,7 @@ class NodeModulesResource extends ResourceInterface {
           const contents = hasHead[0]
             .replace(/type="module"/g, 'type="module-shim"');
 
-          newContents = newContents.replace(/\<head>(.*)<\/head>/s, `
-            <head>
-              ${contents}
-            </head>
-          `);
+          newContents = newContents.replace(/\<head>(.*)<\/head>/s, contents);
         }
 
         const userPackageJson = fs.existsSync(`${userWorkspace}/package.json`)
diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js
index 827051a9..20339ed2 100644
--- a/packages/cli/src/plugins/resource/plugin-standard-html.js
+++ b/packages/cli/src/plugins/resource/plugin-standard-html.js
@@ -316,11 +316,7 @@ class StandardHtmlResource extends ResourceInterface {
           contents = contents.replace(/<script defer="" src="(.*es-module-shims.js)"><\/script>/, '');
           contents = contents.replace(/type="module-shim"/g, 'type="module"');
 
-          body = body.replace(/\<head>(.*)<\/head>/s, `
-            <head>
-              ${contents}
-            </head>
-          `);
+          body = body.replace(/\<head>(.*)<\/head>/s, contents);
         }
     
         resolve(body);


Test patch:
diff --git a/test/smoke-test.js b/test/smoke-test.js
index 5005afad..e3d3f44e 100644
--- a/test/smoke-test.js
+++ b/test/smoke-test.js
@@ -57,8 +57,8 @@ function defaultIndex(label) {
           expect(htmlTag[0].getAttribute('prefix')).to.be.equal('og:http://ogp.me/ns#');
         });
 
-        it('should have matching opening and closing <head> tags', function() {
-          expect(tagsMatch('<html>', html)).to.be.equal(true);
+        it('should have matching opening and closing <html> tags', function() {
+          expect(tagsMatch('html', html, 1)).to.be.equal(true);
         });
       });
 
@@ -72,7 +72,7 @@ function defaultIndex(label) {
         it('should have matching opening and closing <head> tags in the <head>', function() {
           // add an expclit > here to avoid conflicting with <header>
           // which is used in a lot of test case scaffolding
-          expect(tagsMatch('head>', html)).to.be.equal(true);
+          expect(tagsMatch('head>', html, 1)).to.be.equal(true);
         });
   
         it('should have a <title> tag in the <head>', function() {
@@ -109,6 +109,10 @@ function defaultIndex(label) {
       });
 
       describe('document <body>', function() {
+        it('should have matching opening and closing <body> tags', function() {
+          expect(tagsMatch('body', html, 1)).to.be.equal(true);
+        });
+
         it('should have no <script> tags in the <body>', function() {
           const bodyScripts = dom.window.document.querySelectorAll('body script');
   
diff --git a/test/utils.js b/test/utils.js
index 920fdaf0..303bdc93 100644
--- a/test/utils.js
+++ b/test/utils.js
@@ -1,10 +1,11 @@
-function tagsMatch(tagName, html) {
+function tagsMatch(tagName, html, expected = null) {
   const openTagRegex = new RegExp(`<${tagName}`, 'g');
   const closeTagRegex = new RegExp(`<\/${tagName.replace('>', '')}>`, 'g');
   const openingCount = (html.match(openTagRegex) || []).length;
   const closingCount = (html.match(closeTagRegex) || []).length;
+  const expectedMatches = parseInt(expected, 10) ? expected : openingCount;
   
-  return openingCount === closingCount;
+  return openingCount === closingCount && openingCount === expectedMatches;
 }
 
 module.exports = {