Repository: ProjectEvergreen/greenwood (js)

Issue:
frontmatter import attributes
## Summary
For frontmatter imports, one current limitation is that only paths are supported, e.g.
```md
---
imports:
  - /components/my-component/component.js 
---
```


However, there is no way to add attributes to these, like say if we wanted to add an [optimization attribute like `data-opt-gwd="static"`](https://www.greenwoodjs.io/docs/configuration/#optimization)

## Details
It would be nice if this 
```md
---
imports:
  - /components/my-component/component.js foo="bar"
---
```

produced this output
```html
<script src="/components/my-component/component.js" foo="bar"></script>
```

Pull request:
enhancement/issue 1255 frontmatter imports attributes
<!--
## Submitting a Pull Request
We love contributions and appreciate any help you can offer!
-->

## Related Issue
resolves #1255 

## Summary of Changes
1. Support attributes for frontmatter imports
1. Add documentation

> note: previous implementation was forwarding `type="module"` automatically, now we do not assume anything

Patch:
diff --git a/packages/cli/src/lib/layout-utils.js b/packages/cli/src/lib/layout-utils.js
index e3973967..8dbf2819 100644
--- a/packages/cli/src/lib/layout-utils.js
+++ b/packages/cli/src/lib/layout-utils.js
@@ -227,15 +227,29 @@ async function getAppLayout(pageLayoutContents, compilation, customImports = [],
     const mergedStyles = [
       ...appRoot.querySelectorAll('head style'),
       ...[...(pageRoot && pageRoot.querySelectorAll('head style')) || []],
-      ...customImports.filter(resource => resource.split('.').pop() === 'css')
-        .map(resource => `<link rel="stylesheet" href="${resource}"></link>`)
+      ...customImports.filter(resource => resource.split(' ')[0].split('.').pop() === 'css')
+        .map((resource) => {
+          const [href, ...attributes] = resource.split(' ');
+          const attrs = attributes?.length > 0
+            ? attributes.join(' ')
+            : '';
+
+          return `<link rel="stylesheet" href="${href}" ${attrs}></link>`;
+        })
     ].join('\n');
 
     const mergedScripts = [
       ...appRoot.querySelectorAll('head script'),
       ...[...(pageRoot && pageRoot.querySelectorAll('head script')) || []],
-      ...customImports.filter(resource => resource.split('.').pop() === 'js')
-        .map(resource => `<script src="${resource}" type="module"></script>`)
+      ...customImports.filter(resource => resource.split(' ')[0].split('.').pop() === 'js')
+        .map((resource) => {
+          const [src, ...attributes] = resource.split(' ');
+          const attrs = attributes?.length > 0
+            ? attributes.join(' ')
+            : '';
+
+          return `<script src="${src}" ${attrs}></script>`;
+        })
     ].join('\n');
 
     const finalBody = pageLayoutContents
diff --git a/packages/cli/src/lifecycles/bundle.js b/packages/cli/src/lifecycles/bundle.js
index 4ed4cb21..9c963f78 100644
--- a/packages/cli/src/lifecycles/bundle.js
+++ b/packages/cli/src/lifecycles/bundle.js
@@ -105,7 +105,7 @@ async function optimizeStaticPages(compilation, plugins) {
       }
 
       // clean up optimization markers
-      const body = (await response.text()).replace(/data-gwd-opt=".*[a-z]"/g, '');
+      const body = (await response.text()).replace(/data-gwd-opt=".*?[a-z]"/g, '');
 
       await fs.writeFile(new URL(`.${outputPath}`, outputDir), body);
     })
diff --git a/www/pages/docs/front-matter.md b/www/pages/docs/front-matter.md
index 26fe0eb6..c8611413 100644
--- a/www/pages/docs/front-matter.md
+++ b/www/pages/docs/front-matter.md
@@ -27,18 +27,20 @@ label: 'My Blog Post from 3/5/2020'
 ### Imports
 If you want to include files on a _per **page** basis_, you can use the predefined `imports` feature from Greenwood.  This is great for one off use cases where you don't want to ship a third party lib in all your layouts, but just for this one particular page.  This is effectively a naive form of code splitting.  🤓
 
+You can also add attributes by space delimiting them after the path.
+
 #### Example
 ```md
 ---
 imports:
-  - /components/my-component/component.js
+  - /components/my-component/component.js type="module" foo="bar"
   - /components/my-component/component.css
 ---
 ```
 
 You will then see the following emitted for file
 ```html
-<script type="module" src="/components/my-component/component.js"></script>
+<script type="module" src="/components/my-component/component.js" type="module" foo="bar"></script>
 <link rel="stylesheet" href="/components/my-component/component.css"/>
 ```
 


Test patch:
diff --git a/packages/cli/test/cases/build.default.workspace-frontmatter-imports/build.default.workspace-frontmatter-imports.spec.js b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/build.default.workspace-frontmatter-imports.spec.js
index d5f78937..d7c39f9f 100644
--- a/packages/cli/test/cases/build.default.workspace-frontmatter-imports/build.default.workspace-frontmatter-imports.spec.js
+++ b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/build.default.workspace-frontmatter-imports.spec.js
@@ -110,6 +110,16 @@ describe('Build Greenwood With: ', function() {
         it('should output a heading tag from the custom element', function() {
           expect(html).to.contain('<h3>My Counter</h3>');
         });
+
+        it('should have expected attributes on the `<link>` tag from frontmatter imports', async function() {
+          const link = Array.from(dom.window.document
+            .querySelectorAll('head link'))
+            .find(link => link.getAttribute('href')?.startsWith('/components/counter/'));
+
+          expect(link.getAttribute('data-gwd-opt')).to.equal(null);
+          expect(link.getAttribute('foo')).to.equal('bar');
+          expect(link.getAttribute('baz')).to.equal('bar');
+        });
       });
 
       describe('Custom Multihyphen component', () => {
@@ -127,6 +137,15 @@ describe('Build Greenwood With: ', function() {
         it('should have the expected prerendered content', function() {
           expect(html).to.contain('I have multiple hyphens in my tag name!');
         });
+
+        it('should have expected attributes on the `<script>` tag from frontmatter imports', async function() {
+          const script = Array.from(dom.window.document
+            .querySelectorAll('head script'))
+            .find(script => script.getAttribute('src')?.startsWith('/multi-hyphen'));
+
+          expect(script.getAttribute('foo')).to.equal('bar');
+          expect(script.getAttribute('type')).to.equal('module');
+        });
       });
     });
   });
diff --git a/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/pages/examples/demo.md b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/pages/examples/demo.md
index c67290c5..49db0a95 100644
--- a/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/pages/examples/demo.md
+++ b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/pages/examples/demo.md
@@ -2,8 +2,8 @@
 title: Demo Page
 imports:
   - /components/counter/counter.js
-  - /components/counter/counter.css
-  - /components/multi-hyphen/multi-hyphen.js
+  - /components/counter/counter.css data-gwd-opt='none' foo='bar' baz='bar'
+  - /components/multi-hyphen/multi-hyphen.js type="module" foo="bar"
 ---
 
 ## Demo Page Example
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 3fee021d..c19c4c81 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
@@ -167,7 +167,7 @@ describe('Serve Greenwood With: ', function() {
           .filter(tag => !tag.getAttribute('data-gwd'))
           .filter(tag => !tag.getAttribute('type'));
 
-        expect(scripts.length).to.equal(2);
+        expect(scripts.length).to.equal(3);
         expect(scripts[1].textContent).to.contain('console.log');
       });