Repository: ProjectEvergreen/greenwood (js)

Issue:
fetching any JSON file that is not `graph.json` always returns an ES module
## Type of Change
- [ ] New Feature Request
- [ ] Documentation / Website
- [ ] Improvement / Suggestion
- [x] Bug
- [ ] Other (please clarify below)

## Summary
Just noticed as part of working on #516 that if you try and do something like this
```html
<script type="module">
  fetch('/data.json')
    .then(resp => resp.json())
    .then(resp => console.debug('DATA', resp));
</script>
```

Based on [our current logic](https://github.com/ProjectEvergreen/greenwood/blob/v0.12.0/packages/cli/src/plugins/resource/plugin-standard-json.js#L26), it will always come back as ESM, e.g.
```json
export default { ... }
```

When it should just come back as the exact contents.

## Details
This also uncovers another issue related to how we should be able to support using ESM with non standard module types like JSON and images, since `import` just uses `fetch`, to a browser, a request for JSON, to a browser they both look the same, so there is currently no way for us to distinguish the differences between these two
```html
<script type="module">
  fetch('/data.json')
    .then(resp => resp.json())
    .then(resp => console.debug('DATA', resp));
</script>
```

```js
import json from '/package.json';
```

Will need to setup a discussion for this and drop down to using an internal API for now that can get "hints" along the lines of import assertions to help keep us moving, ex.
```js
import json from '/package.json?type=json';
```

> ~~**_However, this need for query params is dependent on #599 and requires a bit of a work around for now, so needing to make an exception to solve both at the same time._**~~

Pull request:
Bug/issue 604 fix json fetch handling
<!--
## Submitting a Pull Request
We love contributions and appreciate any help you can offer!
-->

resolves #604 

## Summary of Changes
1. For now, have to add an "internal" API to capture `originalUrl` as a header to play nicely with #599 wherein if we have query strings in URLs against the filesystem, then things will break, so need to preserve query params another way
1. Adapted **plugin-standard-json** accordingly, and then the `?type=json` logic of this will get extracted out into #516 

As with anything non standard, this is no "right way" per se to solve this.  So far the best hint I can come up with is this
```js
import json from '/package.json?type=json';
```

Which takes some influence from a potential standard called import assertions
https://github.com/tc39/proposal-import-assertions


## TODO
1. [x] Add some tests
1. [x] Setup a discussion to review best way forward for things like #516 and other assets like CSS and potentially images - https://github.com/ProjectEvergreen/greenwood/discussions/606
1. [x] Clean up test code 

Patch:
diff --git a/packages/cli/src/config/rollup.config.js b/packages/cli/src/config/rollup.config.js
index 22467cf9..c924686d 100644
--- a/packages/cli/src/config/rollup.config.js
+++ b/packages/cli/src/config/rollup.config.js
@@ -122,7 +122,18 @@ function greenwoodHtmlPlugin(compilation) {
     // and other custom user resource types like .ts, .gql, etc
     async load(id) {
       const extension = path.extname(id);
-      
+      const importAsRegex = /\?type=(.*)/;
+
+      // bit of a hack to get these two bugs to play well together
+      // https://github.com/ProjectEvergreen/greenwood/issues/598
+      // https://github.com/ProjectEvergreen/greenwood/issues/604
+      if (importAsRegex.test(id)) {
+        const match = id.match(importAsRegex);
+        const importee = id.replace(match[0], '');
+        
+        return `export {default} from '${importee}';`;
+      }
+
       switch (extension) {
 
         case '.html':
diff --git a/packages/cli/src/lifecycles/serve.js b/packages/cli/src/lifecycles/serve.js
index c7da2e74..6023b63f 100644
--- a/packages/cli/src/lifecycles/serve.js
+++ b/packages/cli/src/lifecycles/serve.js
@@ -59,6 +59,11 @@ function getDevServer(compilation) {
         : Promise.resolve(response);
     }, Promise.resolve(ctx.url));
 
+    // bit of a hack to get these two bugs to play well together
+    // https://github.com/ProjectEvergreen/greenwood/issues/598
+    // https://github.com/ProjectEvergreen/greenwood/issues/604
+    ctx.request.headers.originalUrl = ctx.originalUrl;
+
     await next();
   });
 
diff --git a/packages/cli/src/plugins/resource/plugin-standard-json.js b/packages/cli/src/plugins/resource/plugin-standard-json.js
index 3e083d6d..adea9317 100644
--- a/packages/cli/src/plugins/resource/plugin-standard-json.js
+++ b/packages/cli/src/plugins/resource/plugin-standard-json.js
@@ -6,33 +6,34 @@
  */
 const fs = require('fs');
 const json = require('@rollup/plugin-json');
-const path = require('path');
 const { ResourceInterface } = require('../../lib/resource-interface');
 
 class StandardJsonResource extends ResourceInterface {
   constructor(compilation, options) {
     super(compilation, options);
     this.extensions = ['.json'];
-    this.contentType = 'text/javascript';
+    this.contentType = 'application/json';
   }
 
-  async serve(url) {
+  async serve(url, headers) {
     return new Promise(async (resolve, reject) => {
       try {
-        let body = ''; 
-        let contentType = '';
-        const { context } = this.compilation;
-
-        if (url.indexOf('graph.json') >= 0) {
-          const json = await fs.promises.readFile(path.join(context.scratchDir, 'graph.json'), 'utf-8');
-
-          contentType = 'application/json';
-          body = JSON.parse(json);
-        } else {
-          const json = await fs.promises.readFile(url, 'utf-8');
+        const filePath = url.indexOf('graph.json') >= 0
+          ? url.replace(userWorkspace, scratchDir)
+          : url;
+        const { scratchDir, userWorkspace } = this.compilation.context;
+        const { originalUrl } = headers.request;
+        const contents = await fs.promises.readFile(filePath, 'utf-8');
+        const isJsonInJs = originalUrl && originalUrl.indexOf('?type=json') >= 0;
+        let body;
+        let contentType;
 
+        if (isJsonInJs) {
           contentType = 'text/javascript';
-          body = `export default ${json}`;
+          body = `export default ${contents}`;
+        } else {
+          body = JSON.parse(contents);
+          contentType = this.contentType;
         }
 
         resolve({
diff --git a/www/components/footer/footer.js b/www/components/footer/footer.js
index 0b7e946b..1ca7eeb8 100644
--- a/www/components/footer/footer.js
+++ b/www/components/footer/footer.js
@@ -1,5 +1,5 @@
 import { css, html, LitElement, unsafeCSS } from 'lit-element';
-import json from '../../package.json';
+import json from '../../package.json?type=json';
 import footerCss from './footer.css';
 
 class FooterComponent extends LitElement {


Test patch:
diff --git a/packages/cli/test/cases/build.default.workspace-javascript-css/build.default.workspace-javascript-css.spec.js b/packages/cli/test/cases/build.default.workspace-javascript-css/build.default.workspace-javascript-css.spec.js
index 2b3ee7bc..f9d9db0b 100644
--- a/packages/cli/test/cases/build.default.workspace-javascript-css/build.default.workspace-javascript-css.spec.js
+++ b/packages/cli/test/cases/build.default.workspace-javascript-css/build.default.workspace-javascript-css.spec.js
@@ -13,6 +13,8 @@
  *
  * User Workspace
  * src/
+ *   assets/
+ *     data.json
  *   pages/
  *     index.html
  *   scripts/
@@ -94,7 +96,7 @@ describe('Build Greenwood With: ', function() {
       it('should have two <script> tag with inline script in the <head>', function() {
         const scriptTagInline = dom.window.document.querySelectorAll('head > script:not([src])');
         
-        expect(scriptTagInline.length).to.be.equal(3);
+        expect(scriptTagInline.length).to.be.equal(4);
       });
 
       it('should have the expected inline content from inline <script> tag one in index.html', async function() {
@@ -129,6 +131,20 @@ describe('Build Greenwood With: ', function() {
 
     });
 
+    describe('json fetching and importing', function() {
+      it('should have the expected output from inline <script> tag fetching data.json in index.html', async function() {
+        const scriptTagOneOutput = dom.window.document.querySelector('body > .output-json-fetch');
+
+        expect(scriptTagOneOutput.textContent).to.be.equal('got json via fetch, status is - 200');
+      });
+
+      it('should have the expected output from importing data.json in main.js', async function() {
+        const scriptTagOneOutput = dom.window.document.querySelector('body > .output-json-import');
+
+        expect(scriptTagOneOutput.textContent).to.be.equal('got json via import, status is - 200');
+      });
+    });
+
     describe('<style>...</style> tag in the <head>', function() {
       it('should have one <style> tag in the <head>', function() {
         const styleTags = dom.window.document.querySelectorAll('head > style');
diff --git a/packages/cli/test/cases/build.default.workspace-javascript-css/src/assets/data.json b/packages/cli/test/cases/build.default.workspace-javascript-css/src/assets/data.json
new file mode 100644
index 00000000..ba21e884
--- /dev/null
+++ b/packages/cli/test/cases/build.default.workspace-javascript-css/src/assets/data.json
@@ -0,0 +1,4 @@
+{
+  "status": 200,
+  "message": "got json"
+}
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.default.workspace-javascript-css/src/pages/index.html b/packages/cli/test/cases/build.default.workspace-javascript-css/src/pages/index.html
index e6bd6326..7519b69e 100644
--- a/packages/cli/test/cases/build.default.workspace-javascript-css/src/pages/index.html
+++ b/packages/cli/test/cases/build.default.workspace-javascript-css/src/pages/index.html
@@ -37,6 +37,15 @@
       
       document.getElementsByClassName('output-script-inline-three')[0].innerHTML = three;
     </script>
+
+    <script type="module">
+      fetch('/assets/data.json')
+        .then((resp) => resp.json())
+        .then((resp) => {
+          const msg =  `${resp.message} via fetch, status is - ${resp.status}`;
+          document.getElementsByClassName('output-json-fetch')[0].innerHTML = msg;
+        })
+    </script>
   </head>
 
   <body>
@@ -46,6 +55,8 @@
     <p class="output-link"></p>
     <p class="module-shim">let me tell you about type="module-shim"</p>
     <p class="output-script-inline-two"></p>
+    <p class="output-json-fetch"></p>
+    <p class="output-json-import"></p>
 
     <span>CSS output two</span>
   </body>
diff --git a/packages/cli/test/cases/build.default.workspace-javascript-css/src/scripts/main.js b/packages/cli/test/cases/build.default.workspace-javascript-css/src/scripts/main.js
index c977bde1..95b72a5c 100644
--- a/packages/cli/test/cases/build.default.workspace-javascript-css/src/scripts/main.js
+++ b/packages/cli/test/cases/build.default.workspace-javascript-css/src/scripts/main.js
@@ -1,1 +1,6 @@
+import json from '/assets/data.json?type=json';
+
+const msg = `${json.message} via import, status is - ${json.status}`;
+
+document.getElementsByClassName('output-json-import')[0].innerHTML = msg;
 document.getElementsByClassName('output-script-src')[0].innerHTML = 'script tag module with src';
\ No newline at end of file