Repository: ProjectEvergreen/greenwood (js)

Issue:
frontmatter imports not working for any format supported by a resource plugin 

## Type of Change

<!-- Please select from one of the following options: -->
Feature

## Summary

the imports field in the frontmatter filters out any files that are not .js and not .css,  please support any format supported by any Adapter plugin.  

## Details

I noticed this trying to import a typescript component in a project that has the typescript plugin enabled.  the component works fine if included via layout, and also works fine if included by another component.   If however, I attempt to use the imports field, then the corresponding script tag is not generated and the component does not hydrate, it gets processed as an unknown tag. 

Pull request:
bug/issue 1319 expand support for custom resource plugins like in frontmatter imports
<!--
## Submitting a Pull Request
We love contributions and appreciate any help you can offer!
-->

## Related Issue

resolves #1319 

## Summary of Changes

1. Extend layout utilities to account for custom formats inside frontmatter imports
1. Refined TypeScript to better support multiple use case (though a more thorough review should happen as part of https://github.com/ProjectEvergreen/greenwood/issues/1222)
1. Did some `async` refactoring to ensure predictable results (though a more thorough review should happen as part of https://github.com/ProjectEvergreen/greenwood/issues/823)
1. Addressed an existing issue where custom CSS formats would not get properly bundled (could be its own issue, but seeing as it was directly in line of sight of this PR, just bundled it all up)
1. Fixed an inconsistency where some resources were not getting optimized correctly
1. Add test cases

> _Was thinking of backporting this to `0.30.x` line, but due to the nature of these changes, will withhold this to the `0.31.x` line_

## Documentation 

N / A

## TODO
1.  [x] remove demo code

Patch:
diff --git a/packages/cli/src/config/rollup.config.js b/packages/cli/src/config/rollup.config.js
index 3c6e4310..a4d07608 100644
--- a/packages/cli/src/config/rollup.config.js
+++ b/packages/cli/src/config/rollup.config.js
@@ -309,11 +309,12 @@ function greenwoodImportMetaUrl(compilation) {
         const assetExtension = assetName.split('.').pop();
         const assetContents = await fs.promises.readFile(url, 'utf-8');
         const name = assetName.replace(`.${assetExtension}`, '');
+        const request = new Request(url, { headers: { 'Accept': 'text/javascript' } });
         let bundleExtensions = ['js'];
 
         for (const plugin of customResourcePlugins) {
-          if (plugin.shouldServe && await plugin.shouldServe(url)) {
-            const response = await plugin.serve(url);
+          if (plugin.shouldServe && await plugin.shouldServe(url, request)) {
+            const response = await plugin.serve(url, request);
 
             if (response?.headers?.get('content-type') || ''.indexOf('text/javascript') >= 0) {
               bundleExtensions = [...bundleExtensions, ...plugin.extensions];
diff --git a/packages/cli/src/lib/async-utils.js b/packages/cli/src/lib/async-utils.js
new file mode 100644
index 00000000..191642d8
--- /dev/null
+++ b/packages/cli/src/lib/async-utils.js
@@ -0,0 +1,30 @@
+// https://stackoverflow.com/a/76974728/417806
+async function asyncFilter(arr, cb) {
+  const filtered = [];
+
+  for (const element of arr) {
+    const needAdd = await cb(element);
+
+    if (needAdd) {
+      filtered.push(element);
+    }
+  }
+
+  return filtered;
+}
+
+// https://stackoverflow.com/a/71278238/417806
+async function asyncMap(items, mapper) {
+  const mappedItems = [];
+
+  for (const item of items) {
+    mappedItems.push(await mapper(item));
+  }
+
+  return mappedItems;
+}
+
+export {
+  asyncFilter,
+  asyncMap
+};
\ No newline at end of file
diff --git a/packages/cli/src/lib/layout-utils.js b/packages/cli/src/lib/layout-utils.js
index 1f65f926..08f2acc6 100644
--- a/packages/cli/src/lib/layout-utils.js
+++ b/packages/cli/src/lib/layout-utils.js
@@ -3,6 +3,7 @@ import fs from 'fs/promises';
 import htmlparser from 'node-html-parser';
 import { checkResourceExists } from './resource-utils.js';
 import { Worker } from 'worker_threads';
+import { asyncFilter } from './async-utils.js';
 
 async function getCustomPageLayoutsFromPlugins(compilation, layoutName) {
   const contextPlugins = compilation.config.plugins.filter((plugin) => {
@@ -194,6 +195,11 @@ async function getAppLayout(pageLayoutContents, compilation, customImports = [],
     const pageTitle = pageRoot && pageRoot.querySelector('head title');
     const hasActiveFrontmatterTitle = compilation.config.activeContent && (pageTitle && pageTitle.rawText.indexOf(activeFrontmatterTitleKey) >= 0
       || appTitle && appTitle.rawText.indexOf(activeFrontmatterTitleKey) >= 0);
+    const resourcePlugins = compilation.config.plugins.filter((plugin) => {
+      return plugin.type === 'resource' && !plugin.isGreenwoodDefaultPlugin;
+    }).map((plugin) => {
+      return plugin.provider(compilation);
+    });
     let title;
 
     if (hasActiveFrontmatterTitle) {
@@ -231,7 +237,27 @@ async function getAppLayout(pageLayoutContents, compilation, customImports = [],
     const mergedStyles = [
       ...appRoot.querySelectorAll('head style'),
       ...[...(pageRoot && pageRoot.querySelectorAll('head style')) || []],
-      ...customImports.filter(resource => resource.split(' ')[0].split('.').pop() === 'css')
+      ...(await asyncFilter(customImports, async (resource) => {
+        const [href] = resource.split(' ');
+        const isCssFile = href.split(' ')[0].split('.').pop() === 'css';
+
+        if (isCssFile) {
+          return true;
+        }
+
+        const resourceUrl = new URL(`file://${href}`);
+        const request = new Request(resourceUrl, { headers: { 'Accept': 'text/css' } });
+        let isSupportedCustomFormat = false;
+
+        for (const plugin of resourcePlugins) {
+          if (plugin.shouldServe && await plugin.shouldServe(resourceUrl, request)) {
+            isSupportedCustomFormat = true;
+            break;
+          }
+        }
+
+        return isSupportedCustomFormat;
+      }))
         .map((resource) => {
           const [href, ...attributes] = resource.split(' ');
           const attrs = attributes?.length > 0
@@ -245,7 +271,27 @@ async function getAppLayout(pageLayoutContents, compilation, customImports = [],
     const mergedScripts = [
       ...appRoot.querySelectorAll('head script'),
       ...[...(pageRoot && pageRoot.querySelectorAll('head script')) || []],
-      ...customImports.filter(resource => resource.split(' ')[0].split('.').pop() === 'js')
+      ...(await asyncFilter(customImports, async (resource) => {
+        const [src] = resource.split(' ');
+        const isJavaScriptFile = src.split(' ')[0].split('.').pop() === 'js';
+
+        if (isJavaScriptFile) {
+          return true;
+        }
+
+        const resourceUrl = new URL(`file://${src}`);
+        const request = new Request(resourceUrl, { headers: { 'Accept': 'text/javascript' } });
+        let isSupportedCustomFormat = false;
+
+        for (const plugin of resourcePlugins) {
+          if (plugin.shouldServe && await plugin.shouldServe(resourceUrl, request)) {
+            isSupportedCustomFormat = true;
+            break;
+          }
+        }
+
+        return isSupportedCustomFormat;
+      }))
         .map((resource) => {
           const [src, ...attributes] = resource.split(' ');
           const attrs = attributes?.length > 0
diff --git a/packages/cli/src/lib/resource-utils.js b/packages/cli/src/lib/resource-utils.js
index a6ad7b53..6c21ed78 100644
--- a/packages/cli/src/lib/resource-utils.js
+++ b/packages/cli/src/lib/resource-utils.js
@@ -2,6 +2,7 @@ import fs from 'fs/promises';
 import { hashString } from './hashing-utils.js';
 import { getResolvedHrefFromPathnameShortcut } from '../lib/node-modules-utils.js';
 import htmlparser from 'node-html-parser';
+import { asyncMap } from './async-utils.js';
 
 async function modelResource(context, type, src = undefined, contents = undefined, optimizationAttr = undefined, rawAttributes = undefined) {
   const { scratchDir, userWorkspace, projectDirectory } = context;
@@ -120,37 +121,38 @@ async function trackResourcesForRoute(html, compilation, route) {
   });
 
   // intentionally support <script> tags from the <head> or <body>
-  const scripts = await Promise.all(root.querySelectorAll('script')
+  const scripts = await asyncMap(Array.from(root.querySelectorAll('script'))
     .filter(script => (
       isLocalLink(script.getAttribute('src')) || script.rawText)
       && script.rawAttrs.indexOf('importmap') < 0
       && script.getAttribute('type') !== 'application/json')
-    .map(async(script) => {
-      const src = script.getAttribute('src');
-      const optimizationAttr = script.getAttribute('data-gwd-opt');
-      const { rawAttrs } = script;
-
-      if (src) {
-        // <script src="...."></script>
-        return await modelResource(context, 'script', src, null, optimizationAttr, rawAttrs);
-      } else if (script.rawText) {
-        // <script>...</script>
-        return await modelResource(context, 'script', null, script.rawText, optimizationAttr, rawAttrs);
-      }
-    }));
+  , async (script) => {
+    const src = script.getAttribute('src');
+    const optimizationAttr = script.getAttribute('data-gwd-opt');
+    const { rawAttrs } = script;
+
+    if (src) {
+      // <script src="...."></script>
+      return await modelResource(context, 'script', src, null, optimizationAttr, rawAttrs);
+    } else if (script.rawText) {
+      // <script>...</script>
+      return await modelResource(context, 'script', null, script.rawText, optimizationAttr, rawAttrs);
+    }
+  });
 
-  const styles = await Promise.all(root.querySelectorAll('style')
+  const styles = await asyncMap(Array.from(root.querySelectorAll('style'))
     .filter(style => !(/\$/).test(style.rawText) && !(/<!-- Shady DOM styles for -->/).test(style.rawText)) // filter out Shady DOM <style> tags that happen when using puppeteer
-    .map(async(style) => await modelResource(context, 'style', null, style.rawText, null, style.getAttribute('data-gwd-opt'))));
+  , async(style) => await modelResource(context, 'style', null, style.rawText, null, style.getAttribute('data-gwd-opt')));
 
-  const links = await Promise.all(root.querySelectorAll('head link')
+  const links = await asyncMap(Array.from(root.querySelectorAll('link'))
     .filter(link => {
       // <link rel="stylesheet" href="..."></link>
       return link.getAttribute('rel') === 'stylesheet'
         && link.getAttribute('href') && isLocalLink(link.getAttribute('href'));
-    }).map(async(link) => {
-      return modelResource(context, 'link', link.getAttribute('href'), null, link.getAttribute('data-gwd-opt'), link.rawAttrs);
-    }));
+    })
+  , async(link) => {
+    return await modelResource(context, 'link', link.getAttribute('href'), null, link.getAttribute('data-gwd-opt'), link.rawAttrs);
+  });
 
   const resources = [
     ...scripts,
diff --git a/packages/cli/src/lifecycles/bundle.js b/packages/cli/src/lifecycles/bundle.js
index 814d1b94..d06de1f2 100644
--- a/packages/cli/src/lifecycles/bundle.js
+++ b/packages/cli/src/lifecycles/bundle.js
@@ -150,6 +150,7 @@ async function bundleStyleResources(compilation, resourcePlugins) {
         });
       }
 
+      // keep the original contents, but still hash the filename based on content
       if (compilation.config.optimization === 'none' || optimizationAttr === 'none') {
         optimizedFileContents = contents;
       } else {
@@ -160,6 +161,22 @@ async function bundleStyleResources(compilation, resourcePlugins) {
         const initResponse = new Response(contents, { headers });
 
         let response = await resourcePlugins.reduce(async (responsePromise, plugin) => {
+          const intermediateResponse = await responsePromise;
+          const shouldServe = plugin.shouldServe && await plugin.shouldServe(url, request);
+
+          if (shouldServe) {
+            const currentResponse = await plugin.serve(url, request);
+            const mergedResponse = mergeResponse(intermediateResponse.clone(), currentResponse.clone());
+
+            if (mergedResponse.headers.get('Content-Type').indexOf(contentType) >= 0) {
+              return Promise.resolve(mergedResponse.clone());
+            }
+          }
+
+          return Promise.resolve(responsePromise);
+        }, Promise.resolve(initResponse));
+
+        response = await resourcePlugins.reduce(async (responsePromise, plugin) => {
           const intermediateResponse = await responsePromise;
           const shouldPreIntercept = plugin.shouldPreIntercept && await plugin.shouldPreIntercept(url, request, intermediateResponse.clone());
 
@@ -173,7 +190,7 @@ async function bundleStyleResources(compilation, resourcePlugins) {
           }
 
           return Promise.resolve(responsePromise);
-        }, Promise.resolve(initResponse));
+        }, Promise.resolve(response.clone()));
 
         response = await resourcePlugins.reduce(async (responsePromise, plugin) => {
           const intermediateResponse = await responsePromise;
@@ -346,10 +363,6 @@ const bundleCompilation = async (compilation) => {
         return plugin.type === 'resource';
       }).map((plugin) => {
         return plugin.provider(compilation);
-      }).filter((provider) => {
-        return provider.shouldIntercept && provider.intercept
-          || provider.shouldPreIntercept && provider.preIntercept
-          || provider.shouldOptimize && provider.optimize;
       });
 
       console.info('bundling static assets...');
diff --git a/packages/cli/src/lifecycles/graph.js b/packages/cli/src/lifecycles/graph.js
index d870d26f..b791501d 100644
--- a/packages/cli/src/lifecycles/graph.js
+++ b/packages/cli/src/lifecycles/graph.js
@@ -74,7 +74,7 @@ const generateGraph = async (compilation) => {
             const relativePagePath = filenameUrl.pathname.replace(pagesDir.pathname, './');
             const isApiRoute = relativePagePath.startsWith('./api');
             const req = isApiRoute
-              ? new Request(filenameUrl)
+              ? new Request(filenameUrl, { headers: { 'Accept': 'text/javascript' } })
               : new Request(filenameUrl, { headers: { 'Accept': 'text/html' } });
             let isCustom = null;
 
diff --git a/packages/cli/src/plugins/resource/plugin-standard-css.js b/packages/cli/src/plugins/resource/plugin-standard-css.js
index 43fa5289..75be90ea 100644
--- a/packages/cli/src/plugins/resource/plugin-standard-css.js
+++ b/packages/cli/src/plugins/resource/plugin-standard-css.js
@@ -336,12 +336,9 @@ class StandardCssResource extends ResourceInterface {
   }
 
   async shouldIntercept(url, request, response) {
-    const { pathname } = url;
-    const ext = pathname.split('.').pop();
-
     return url.protocol === 'file:'
-      && ext === this.extensions[0]
-      && (response.headers.get('Content-Type')?.indexOf('text/css') >= 0 || request.headers.get('Accept')?.indexOf('text/javascript') >= 0) || url.searchParams?.get('polyfill') === 'type-css';
+      && (response.headers.get('Content-Type')?.indexOf(this.contentType) >= 0 || request.headers.get('Accept')?.indexOf(this.contentType) >= 0)
+      || (request.headers.get('Accept')?.indexOf('text/javascript') > 0 || url.searchParams?.get('polyfill') === 'type-css');
   }
 
   async intercept(url, request, response) {
diff --git a/packages/cli/src/plugins/resource/plugin-standard-html.js b/packages/cli/src/plugins/resource/plugin-standard-html.js
index 19937907..827cc70b 100644
--- a/packages/cli/src/plugins/resource/plugin-standard-html.js
+++ b/packages/cli/src/plugins/resource/plugin-standard-html.js
@@ -206,7 +206,7 @@ class StandardHtmlResource extends ResourceInterface {
         } else if (type === 'link') {
           const tag = root.querySelectorAll('link').find(link => link.getAttribute('href') === src);
 
-          if (!optimizationAttr && (optimization !== 'none' && optimization !== 'inline')) {
+          if (optimizationAttr !== 'inline' && optimization !== 'inline') {
             const optimizedFilePath = `${basePath}/${optimizedFileName}`;
 
             body = body.replace(src, optimizedFilePath);
diff --git a/packages/plugin-typescript/src/index.js b/packages/plugin-typescript/src/index.js
index 277bf0ae..dbc735f7 100644
--- a/packages/plugin-typescript/src/index.js
+++ b/packages/plugin-typescript/src/index.js
@@ -33,11 +33,13 @@ class TypeScriptResource extends ResourceInterface {
     this.contentType = 'text/javascript';
   }
 
-  async shouldServe(url) {
+  async shouldServe(url, request) {
     const { pathname, protocol } = url;
+    const hasJsHeaders = (request?.headers.get('Accept') ?? '').indexOf(this.contentType) >= 0 || request.headers.get('Sec-Fetch-Dest') === 'script';
+    const serveAsPage = (request?.headers.get('Accept') ?? '').indexOf('text/html') >= 0 && this.options.servePage;
     const isTsFile = protocol === 'file:' && pathname.split('.').pop() === this.extensions[0];
 
-    return isTsFile || isTsFile && url.searchParams.has('type') && url.searchParams.get('type') === this.extensions[0];
+    return (serveAsPage && isTsFile) || (isTsFile && hasJsHeaders) || isTsFile && url.searchParams.has('type') && url.searchParams.get('type') === this.extensions[0];
   }
 
   async serve(url) {


Test patch:
diff --git a/packages/cli/test/cases/build.config.optimization-none/build.config-optimization-none.spec.js b/packages/cli/test/cases/build.config.optimization-none/build.config-optimization-none.spec.js
index 734fc876..def114b6 100644
--- a/packages/cli/test/cases/build.config.optimization-none/build.config-optimization-none.spec.js
+++ b/packages/cli/test/cases/build.config.optimization-none/build.config-optimization-none.spec.js
@@ -64,12 +64,6 @@ describe('Build Greenwood With: ', function() {
         cssFiles = await glob.promise(`${path.join(this.context.publicDir, 'styles')}/theme.*.css`);
       });
 
-      it('should contain no <link> preload tags in the <head>', function() {
-        const preloadTags = dom.window.document.querySelectorAll('head link[rel="preload"]');
-
-        expect(preloadTags.length).to.be.equal(0);
-      });
-
       describe('<script> tag and preloading', function() {
         it('should contain one un-minified javascript file in the output directory', async function() {
           expect(jsFiles).to.have.lengthOf(1);
@@ -95,7 +89,7 @@ describe('Build Greenwood With: ', function() {
         });
       });
 
-      describe('<link> tags should not be preloaded', function() {
+      describe('theme.css', function() {
         it('should contain one style.css in the output directory', async function() {
           expect(cssFiles).to.have.lengthOf(1);
         });
@@ -106,10 +100,10 @@ describe('Build Greenwood With: ', function() {
           expect(css).to.contain('{\n  margin: 0;\n  padding: 0;\n  font-family: \'Comic Sans\', sans-serif;\n}');
         });
 
-        it('should have only one expected <link> tag in the <head>', function() {
+        it('should have two <link> tag in the <head>', function() {
           const linkTags = dom.window.document.querySelectorAll('head link');
 
-          expect(linkTags.length).to.be.equal(1);
+          expect(linkTags.length).to.be.equal(2);
         });
       });
     });
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 17a11828..bea516f0 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
@@ -1,6 +1,6 @@
 /*
  * Use Case
- * Run Greenwood build command with a workspace that uses frontmatter imports.
+ * Run Greenwood build command with a workspace that uses frontmatter imports, including custom formats.
  * Added prerender: true to showcase prerendering WCs in markdown
  *
  * User Result
@@ -11,7 +11,10 @@
  *
  * User Config
  * export default {
- *   prerender: true
+ *   prerender: true,
+ *   plugins: [{
+ *     // naive TS and Sass plugins
+ *   }]
  * }
  *
  * User Workspace
@@ -20,6 +23,11 @@
  *     counter/
  *       counter.js
  *       counter.css
+ *   scripts/
+ *     frontmatter-standard.js
+*      frontmatter-custom.ts
+ *   styles/
+ *     frontmatter-custom.scss
  *   pages/
  *     examples/
  *       counter.md
@@ -70,7 +78,7 @@ describe('Build Greenwood With: ', function() {
         html = await fs.promises.readFile(htmlPath, 'utf-8');
       });
 
-      it('should output a counter.css file from frontmatter import', async function() {
+      it('should output an unoptimized counter.css file from frontmatter import', async function() {
         const cssFiles = await glob.promise(`${this.context.publicDir}**/**/counter.*.css`);
 
         expect(cssFiles).to.have.lengthOf(1);
@@ -114,7 +122,7 @@ describe('Build Greenwood With: ', function() {
         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/'));
+            .find(link => link.getAttribute('href') === '/components/counter/counter.592053089.css' && link.rel !== 'preload');
 
           expect(link.getAttribute('data-gwd-opt')).to.equal(null);
           expect(link.getAttribute('foo')).to.equal('bar');
@@ -147,6 +155,60 @@ describe('Build Greenwood With: ', function() {
           expect(script.getAttribute('type')).to.equal('module');
         });
       });
+
+      describe('Frontmatter JavaScript import', () => {
+        it('should output a transformed frontmatter-standard.js file from custom frontmatter import', async function() {
+          const jsFiles = await glob.promise(`${this.context.publicDir}**/**/frontmatter-standard.*.js`);
+          const contents = await fs.promises.readFile(jsFiles[0], 'utf-8');
+
+          expect(jsFiles).to.have.lengthOf(1);
+          expect(contents).to.contain('console.log({message:"Hello from frontmatter standard"});');
+        });
+
+        it('should have expected attributes on the `<script>` tag from frontmatter imports', function() {
+          const script = Array.from(dom.window.document
+            .querySelectorAll('head script'))
+            .find(script => script.getAttribute('src')?.startsWith('/frontmatter-standard.DoXOcwxl'));
+
+          expect(script.getAttribute('type')).to.equal('module');
+        });
+      });
+
+      describe('Custom Frontmatter JavaScript import format with expected contents transformed', () => {
+        it('should output a transformed frontmatter-custom.js file from custom frontmatter import', async function() {
+          const jsFiles = await glob.promise(`${this.context.publicDir}**/**/frontmatter-custom.*.js`);
+          const contents = await fs.promises.readFile(jsFiles[0], 'utf-8');
+
+          expect(jsFiles).to.have.lengthOf(1);
+          expect(contents).to.contain('console.log({message:"Hello from frontmatter custom"});');
+        });
+
+        it('should have expected attributes on the `<script>` tag from frontmatter imports', function() {
+          const script = Array.from(dom.window.document
+            .querySelectorAll('head script'))
+            .find(script => script.getAttribute('src')?.startsWith('/frontmatter-custom.CqVHikqT.js'));
+
+          expect(script.getAttribute('type')).to.equal('module');
+        });
+      });
+
+      describe('Custom Frontmatter CSS import format with expected contents transformed', () => {
+        it('should output a frontmatter-custom.css file from custom frontmatter import', async function() {
+          const cssFiles = await glob.promise(`${this.context.publicDir}**/**/frontmatter-custom.*.css`);
+          const contents = await fs.promises.readFile(cssFiles[0], 'utf-8');
+
+          expect(cssFiles).to.have.lengthOf(1);
+          expect(contents).to.equal('body{--primary-color:\'red\'}');
+        });
+
+        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') === '/styles/frontmatter-custom.223140015.css');
+
+          expect(link).to.not.be.undefined;
+        });
+      });
     });
   });
 
diff --git a/packages/cli/test/cases/build.default.workspace-frontmatter-imports/greenwood.config.js b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/greenwood.config.js
index 8dc4be46..fec2168e 100644
--- a/packages/cli/test/cases/build.default.workspace-frontmatter-imports/greenwood.config.js
+++ b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/greenwood.config.js
@@ -1,3 +1,67 @@
+import fs from 'fs/promises';
+
+class NaiveTypeScriptResource {
+  constructor(compilation, options) {
+    this.compilation = compilation;
+    this.options = options;
+
+    this.extensions = ['ts'];
+    this.contentType = 'text/javascript';
+  }
+
+  async shouldServe(url, request) {
+    return url.pathname.split('.').pop() === this.extensions[0] && request.headers?.get('Accept').indexOf(this.contentType) >= 0;
+  }
+
+  async serve(url) {
+    let body = await fs.readFile(url, 'utf-8');
+
+    body = body.replace(': string', '');
+
+    return new Response(body, {
+      headers: new Headers({
+        'Content-Type': this.contentType
+      })
+    });
+  }
+}
+
+class NaiveSassResource {
+  constructor(compilation, options) {
+    this.compilation = compilation;
+    this.options = options;
+
+    this.extensions = ['scss'];
+    this.contentType = 'text/css';
+  }
+
+  async shouldServe(url, request) {
+    return url.pathname.split('.').pop() === this.extensions[0] && request.headers?.get('Accept').indexOf(this.contentType) >= 0;
+  }
+
+  async serve(url) {
+    let body = await fs.readFile(url, 'utf-8');
+
+    body = body.replace(/\$my-color: red;/, '');
+    body = body.replace(/\$my-color/, '\'red\'');
+
+    return new Response(body, {
+      headers: new Headers({
+        'Content-Type': this.contentType
+      })
+    });
+  }
+}
+
 export default {
-  prerender: true
+  prerender: true,
+  plugins: [{
+    type: 'resource',
+    name: 'plugin-naive-ts',
+    provider: (compilation) => new NaiveTypeScriptResource(compilation)
+  }, {
+    type: 'resource',
+    name: 'plugin-naive-sass',
+    provider: (compilation) => new NaiveSassResource(compilation)
+  }]
 };
\ No newline at end of file
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 49db0a95..beaf5d63 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
@@ -4,6 +4,9 @@ imports:
   - /components/counter/counter.js
   - /components/counter/counter.css data-gwd-opt='none' foo='bar' baz='bar'
   - /components/multi-hyphen/multi-hyphen.js type="module" foo="bar"
+  - /scripts/frontmatter-custom.ts type="module"
+  - /scripts/frontmatter-standard.js type="module"
+  - /styles/frontmatter-custom.scss
 ---
 
 ## Demo Page Example
diff --git a/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/scripts/custom-script.ts b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/scripts/custom-script.ts
new file mode 100644
index 00000000..7cf9a0ed
--- /dev/null
+++ b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/scripts/custom-script.ts
@@ -0,0 +1,3 @@
+const message: string = 'Hello from custom script';
+
+console.log({ message });
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/scripts/frontmatter-custom.ts b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/scripts/frontmatter-custom.ts
new file mode 100644
index 00000000..16a35635
--- /dev/null
+++ b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/scripts/frontmatter-custom.ts
@@ -0,0 +1,3 @@
+const message: string = 'Hello from frontmatter custom';
+
+console.log({ message });
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/scripts/frontmatter-standard.js b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/scripts/frontmatter-standard.js
new file mode 100644
index 00000000..44507239
--- /dev/null
+++ b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/scripts/frontmatter-standard.js
@@ -0,0 +1,3 @@
+const message = 'Hello from frontmatter standard';
+
+console.log({ message });
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/styles/frontmatter-custom.scss b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/styles/frontmatter-custom.scss
new file mode 100644
index 00000000..943dee61
--- /dev/null
+++ b/packages/cli/test/cases/build.default.workspace-frontmatter-imports/src/styles/frontmatter-custom.scss
@@ -0,0 +1,5 @@
+$my-color: red;
+
+body {
+  --primary-color: $my-color;
+}
\ No newline at end of file
diff --git a/packages/plugin-typescript/test/cases/develop.default/develop.default.spec.js b/packages/plugin-typescript/test/cases/develop.default/develop.default.spec.js
index a2178827..613157ed 100644
--- a/packages/plugin-typescript/test/cases/develop.default/develop.default.spec.js
+++ b/packages/plugin-typescript/test/cases/develop.default/develop.default.spec.js
@@ -60,7 +60,9 @@ describe('Develop Greenwood With: ', function() {
       let data;
 
       before(async function() {
-        response = await fetch(`${hostname}:${port}/main.ts`);
+        response = await fetch(`${hostname}:${port}/main.ts`, { headers: {
+          'Accept': 'text/javascript'
+        } });
         data = await response.text();
       });