Repository: ProjectEvergreen/greenwood (js)

Issue:
prerendering breaks optimization configuration with multi-line formatted `<script>` / `<style>` / `<link>` tags
## Summary
So this was an interesting development encountered while working on the website, wherein when using Prettier, it broke up a lengthy `<script>` tag over multiple lines
```html
<script
  type="module"
  src="../components/latest-post/latest-post.js"
  data-gwd-opt="static"
></script>
```

This resulted in the script, marked as `static` _not_ being removed from the final page output, thus showing a 404 in the browser console.
![Screenshot 2024-06-05 at 8 40 42 AM](https://github.com/ProjectEvergreen/greenwood/assets/895923/8a6ad218-11fa-455a-86d1-6f3cc6553e92)

## Details
Digging into it, this is because of how the optimizing logic works in Greenwood (I will be the first to admit it is a bit of naive implementation 😅 ) in that when searching for tags that need optimization applied, it basically just looks to match on the existing formatting, to find / replace against that.

Below is just one example for how `<script>` tags are managed.
https://github.com/ProjectEvergreen/greenwood/blob/master/packages/cli/src/plugins/resource/plugin-standard-html.js#L279
```js
if (type === 'script') {
  if (optimizationAttr === 'static' || optimization === 'static') {
    body = body.replace(`<script ${rawAttributes}>${contents.replace(/\.\//g, '/').replace(/\$/g, '$$$')}</script>`, '');
  } else if (optimizationAttr === 'none') {
    // ...
  }
}
```

And so the issue becomes, if the formatting changes at all from the original authored HTML to what eventually gets to Greenwood by this stage, that matching will fail.  And so in this case, with `prerender` enabled, after going through WCC, the HTML is now "formatted" and now there are no line breaks anymore, and thus the matching fails.
```html
<script type="module" src="../components/latest-post/latest-post.js" data-gwd-opt="static"></script>
```
![Screenshot 2024-06-05 at 8 40 20 AM](https://github.com/ProjectEvergreen/greenwood/assets/895923/8fb6cc95-390d-4963-b517-15107e7e9e50)

----

While this issue is going to be scoped just to fixing the immediate issue for optimization handling, this is probably a good sign we should audit the rest of our code and try and adopt a more programmatic option using our [HTML parsing library](https://www.npmjs.com/package/node-html-parser), which I did try, but couldn't get something like this to work (e.g. likey just a skill issue 🙃 ).
```js
let body = await response.text();

const root = htmlparser.parse(body, {
  script: true,
  style: true
});

const tag = root.querySelectorAll('script').find(script => script.getAttribute('src') === src);
const optimized = '';

root.replace(tag, optimized);

console.log(root.outerHTML); // should now have the <script> tag removed!  
```

Will open a new issue for this specifically though.

Pull request description:
bug/issue 1241 prerendering formatting breaks bundle mapping optimization
<!--
## Submitting a Pull Request
We love contributions and appreciate any help you can offer!
-->

## Related Issue
resolves #1241 

## Summary of Changes
1. refactor bundle mapping for `<script>`, `<link>` and `<style>` tags
1. Validated through a test case

## TODO
1. [x] finish script bundle mapping
1. [x] style bundle mapping refactoring
1. [x] link bundle mapping refactoring
1. [x] open up an issue tracking a more programmatic way to do this throughout greenwood - already existed, so added there
1. [x] can we solve this in `modelResource`? - issue is that `modelResource` happens before any prerendering, so won't be formatted in time 😞 

Patch:
diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js
index a2e5e869..456a5244 100644
--- a/packages/cli/src/plugins/resource/plugin-standard-html.js
+++ b/packages/cli/src/plugins/resource/plugin-standard-html.js
@@ -17,6 +17,7 @@ import { getUserScripts, getPageLayout, getAppLayout } from '../../lib/layout-ut
 import { requestAsObject } from '../../lib/resource-utils.js';
 import unified from 'unified';
 import { Worker } from 'worker_threads';
+import htmlparser from 'node-html-parser';
 
 class StandardHtmlResource extends ResourceInterface {
   constructor(compilation, options) {
@@ -200,12 +201,19 @@ class StandardHtmlResource extends ResourceInterface {
     const pageResources = this.compilation.graph.find(page => page.outputPath === pathname || page.route === pathname).resources;
     let body = await response.text();
 
+    const root = htmlparser.parse(body, {
+      script: true,
+      style: true
+    });
+
     for (const pageResource of pageResources) {
       const keyedResource = this.compilation.resources.get(pageResource);
-      const { contents, src, type, optimizationAttr, optimizedFileContents, optimizedFileName, rawAttributes } = keyedResource;
+      const { contents, src, type, optimizationAttr, optimizedFileContents, optimizedFileName } = keyedResource;
 
       if (src) {
         if (type === 'script') {
+          const tag = root.querySelectorAll('script').find(script => script.getAttribute('src') === src);
+
           if (!optimizationAttr && optimization === 'default') {
             const optimizedFilePath = `${basePath}/${optimizedFileName}`;
 
@@ -215,17 +223,19 @@ class StandardHtmlResource extends ResourceInterface {
               <link rel="modulepreload" href="${optimizedFilePath}" as="script">
             `);
           } else if (optimizationAttr === 'inline' || optimization === 'inline') {
-            const isModule = rawAttributes.indexOf('type="module') >= 0 ? ' type="module"' : '';
+            const isModule = tag.rawAttrs.indexOf('type="module') >= 0 ? ' type="module"' : '';
 
-            body = body.replace(`<script ${rawAttributes}></script>`, `
+            body = body.replace(`<script ${tag.rawAttrs}></script>`, `
               <script ${isModule}>
                 ${optimizedFileContents.replace(/\.\//g, `${basePath}/`).replace(/\$/g, '$$$')}
               </script>
             `);
           } else if (optimizationAttr === 'static' || optimization === 'static') {
-            body = body.replace(`<script ${rawAttributes}></script>`, '');
+            body = body.replace(`<script ${tag.rawAttrs}></script>`, '');
           }
         } else if (type === 'link') {
+          const tag = root.querySelectorAll('link').find(link => link.getAttribute('href') === src);
+
           if (!optimizationAttr && (optimization !== 'none' && optimization !== 'inline')) {
             const optimizedFilePath = `${basePath}/${optimizedFileName}`;
 
@@ -239,11 +249,11 @@ class StandardHtmlResource extends ResourceInterface {
             // when pre-rendering, puppeteer normalizes everything to <link .../>
             // but if not using pre-rendering, then it could come out as <link ...></link>
             // not great, but best we can do for now until #742
-            body = body.replace(`<link ${rawAttributes}>`, `
+            body = body.replace(`<link ${tag.rawAttrs}>`, `
               <style>
                 ${optimizedFileContents}
               </style>
-            `).replace(`<link ${rawAttributes}/>`, `
+            `).replace(`<link ${tag.rawAttrs}/>`, `
               <style>
                 ${optimizedFileContents}
               </style>
@@ -252,8 +262,10 @@ class StandardHtmlResource extends ResourceInterface {
         }
       } else {
         if (type === 'script') {
+          const tag = root.querySelectorAll('script').find(script => script.innerHTML === contents);
+
           if (optimizationAttr === 'static' || optimization === 'static') {
-            body = body.replace(`<script ${rawAttributes}>${contents.replace(/\.\//g, '/').replace(/\$/g, '$$$')}</script>`, '');
+            body = body.replace(`<script ${tag.rawAttrs}>${contents.replace(/\.\//g, '/').replace(/\$/g, '$$$')}</script>`, '');
           } else if (optimizationAttr === 'none') {
             body = body.replace(contents, contents.replace(/\.\//g, `${basePath}/`).replace(/\$/g, '$$$'));
           } else {


Test patch:
diff --git a/packages/cli/test/cases/build.config.optimization-overrides/greenwood.config.js b/packages/cli/test/cases/build.config.optimization-overrides/greenwood.config.js
new file mode 100644
index 00000000..da3dd4d9
--- /dev/null
+++ b/packages/cli/test/cases/build.config.optimization-overrides/greenwood.config.js
@@ -0,0 +1,5 @@
+// https://github.com/ProjectEvergreen/greenwood/issues/1241
+// to ensure formatting does not break bundle optimization linking
+export default {
+  prerender: true
+};
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.config.optimization-overrides/src/pages/index.html b/packages/cli/test/cases/build.config.optimization-overrides/src/pages/index.html
index f60ba23a..dd97780c 100644
--- a/packages/cli/test/cases/build.config.optimization-overrides/src/pages/index.html
+++ b/packages/cli/test/cases/build.config.optimization-overrides/src/pages/index.html
@@ -2,9 +2,23 @@
 <html lang="en" prefix="og:http://ogp.me/ns#">
 
   <head>
-    <script type="module" src="/components/header.js" data-gwd-opt="static"></script>
     <script type="module" src="/components/footer.js" data-gwd-opt="inline"></script>
-    <link rel="stylesheet" href="/styles/theme.css" data-gwd-opt="inline"/>
+    <script
+      type="module"
+      src="../components/header.js"
+      data-gwd-opt="static"
+    ></script>
+    <link
+      rel="stylesheet"
+      href="/styles/theme.css"
+      data-gwd-opt="inline"
+    />
+    <script
+      data-gwd-opt="static"
+      type="module"
+    >
+      console.log('I should not be here');
+    </script>
   </head>
 
   <body>