diff --git a/packages/cli/src/config/rollup.config.js b/packages/cli/src/config/rollup.config.js
index a434c3bc8..b0bad6bfd 100644
--- a/packages/cli/src/config/rollup.config.js
+++ b/packages/cli/src/config/rollup.config.js
@@ -62,17 +62,19 @@ function greenwoodResourceLoader (compilation, browser = false) {
};
// filter first for any bare specifiers
- if (await checkResourceExists(idUrl) && extension !== 'js') {
- for (const plugin of resourcePlugins) {
- if (plugin.shouldResolve && await plugin.shouldResolve(idUrl)) {
- idUrl = new URL((await plugin.resolve(idUrl)).url);
+ if (await checkResourceExists(idUrl) && !id.startsWith('\x00')) {
+ if (extension !== 'js') {
+ for (const plugin of resourcePlugins) {
+ if (plugin.shouldResolve && await plugin.shouldResolve(idUrl)) {
+ idUrl = new URL((await plugin.resolve(idUrl)).url);
+ }
}
}
const request = new Request(idUrl, {
headers
});
- let response = new Response('');
+ let response = new Response('', { headers: { 'Content-Type': 'text/javascript' } });
for (const plugin of resourcePlugins) {
if (plugin.shouldServe && await plugin.shouldServe(idUrl, request)) {
diff --git a/packages/cli/src/lifecycles/prerender.js b/packages/cli/src/lifecycles/prerender.js
index d67ffbce4..785c95d61 100644
--- a/packages/cli/src/lifecycles/prerender.js
+++ b/packages/cli/src/lifecycles/prerender.js
@@ -1,5 +1,5 @@
import fs from 'fs/promises';
-import { checkResourceExists, trackResourcesForRoute } from '../lib/resource-utils.js';
+import { checkResourceExists, trackResourcesForRoute, mergeResponse } from '../lib/resource-utils.js';
import os from 'os';
import { WorkerPool } from '../lib/threadpool.js';
@@ -30,12 +30,12 @@ async function interceptPage(url, request, plugins, body) {
});
for (const plugin of plugins) {
- if (plugin.shouldPreIntercept && await plugin.shouldPreIntercept(url, request, response)) {
- response = await plugin.preIntercept(url, request, response);
+ if (plugin.shouldPreIntercept && await plugin.shouldPreIntercept(url, request, response.clone())) {
+ response = mergeResponse(response, await plugin.preIntercept(url, request, response.clone()));
}
- if (plugin.shouldIntercept && await plugin.shouldIntercept(url, request, response)) {
- response = await plugin.intercept(url, request, response);
+ if (plugin.shouldIntercept && await plugin.shouldIntercept(url, request, response.clone())) {
+ response = mergeResponse(response, await plugin.intercept(url, request, response.clone()));
}
}
diff --git a/packages/cli/test/cases/build.default.workspace-layouts-page/build.default.workspace-layouts-page.spec.js b/packages/cli/test/cases/build.default.workspace-layouts-page/build.default.workspace-layouts-page.spec.js
index 7b658a80d..8eb405d04 100644
--- a/packages/cli/test/cases/build.default.workspace-layouts-page/build.default.workspace-layouts-page.spec.js
+++ b/packages/cli/test/cases/build.default.workspace-layouts-page/build.default.workspace-layouts-page.spec.js
@@ -133,7 +133,6 @@ describe('Build Greenwood With: ', function() {
});
});
});
-
});
after(function() {
diff --git a/packages/plugin-css-modules/README.md b/packages/plugin-css-modules/README.md
new file mode 100644
index 000000000..8cabc12ae
--- /dev/null
+++ b/packages/plugin-css-modules/README.md
@@ -0,0 +1,126 @@
+# @greenwood/plugin-css-modules
+
+## Overview
+
+A Greenwood plugin for authoring [**CSS Modules ™️**](https://github.com/css-modules/css-modules). It is a modest implementation of [the specification](https://github.com/css-modules/icss). 🙂
+
+This is NOT to be confused with [CSS Module _Scripts_](https://web.dev/articles/css-module-scripts), which Greenwood already supports.
+
+> This package assumes you already have `@greenwood/cli` installed.
+
+## Installation
+
+You can use your favorite JavaScript package manager to install this package.
+
+_examples:_
+```bash
+# npm
+npm i -D @greenwood/plugin-css-modules
+
+# yarn
+yarn add @greenwood/plugin-css-modules --dev
+```
+
+## Usage
+
+Add this plugin to your _greenwood.config.js_.
+
+```javascript
+import { greenwoodPluginCssModules } from '@greenwood/plugin-css-modules';
+
+export default {
+ ...
+
+ plugins: [
+ greenwoodPluginCssModules()
+ ]
+}
+```
+
+Now you can create a CSS file that ends in _.module.css_
+
+```css
+/* header.module.css */
+.container {
+ display: flex;
+ justify-content: space-between;
+}
+
+.navBarMenu {
+ border: 1px solid #020202;
+}
+
+.navBarMenuItem {
+ & a {
+ text-decoration: none;
+ color: #020202;
+ }
+}
+
+@media screen and (min-width: 768px) {
+ .container {
+ padding: 10px 20px;
+ }
+}
+```
+
+
+And reference that in your (Light DOM) HTML based Web Component
+
+```js
+// header.js
+import styles from './header.module.css';
+
+export default class Header extends HTMLElement {
+ connectedCallback() {
+ this.innerHTML = `
+
+ `;
+ }
+}
+
+customElements.define('app-header', Header);
+```
+
+From there, Greenwood will scope your CSS by prefixing with the filename and a hash, and inline that into a `
+
+ `
+ );
+
+ return new Response(newBody);
+ } else if (protocol === 'file:' && pathname.endsWith(this.extensions[0]) && cssModulesMap[mapKey]) {
+ // handle this primarily for SSR / prerendering use case
+ const cssModule = `export default ${JSON.stringify(cssModulesMap[mapKey].module)}`;
+
+ return new Response(cssModule, {
+ headers: {
+ 'Content-Type': this.contentType
+ }
+ });
+ }
+ }
+}
+
+// this process all files that have CssModules content used
+// and strip out the `import` and replace all the references in class attributes with static values
+class StripCssModulesResource extends ResourceInterface {
+ constructor(compilation, options) {
+ super(compilation, options);
+
+ this.extensions = ['module.css'];
+ this.contentType = 'text/javascript';
+ }
+
+ async shouldIntercept(url) {
+ const cssModulesMap = getCssModulesMap(this.compilation);
+
+ for (const [, value] of Object.entries(cssModulesMap)) {
+ if (url.href === value.importer) {
+ return true;
+ }
+ }
+ }
+
+ async intercept(url, request, response) {
+ const { context } = this.compilation;
+ let contents = await response.text();
+
+ acornWalk.simple(
+ acorn.Parser.extend(importAttributes).parse(contents, {
+ ecmaVersion: 'latest',
+ sourceType: 'module'
+ }),
+ {
+ ImportDeclaration(node) {
+ const { specifiers = [], source = {}, start, end } = node;
+ const { value = '' } = source;
+
+ if (
+ value.endsWith('.module.css') &&
+ specifiers.length === 1
+ ) {
+ contents = `${contents.slice(0, start)} \n ${contents.slice(end)}`;
+ const cssModulesMap = getCssModulesMap({ context });
+
+ Object.values(cssModulesMap).forEach((value) => {
+ const { importer, module, identifier } = value;
+
+ if (importer === url.href) {
+ Object.keys(module).forEach((key) => {
+ const literalUsageRegex = new RegExp(String.raw`\$\{${identifier}.${key}\}`, 'g');
+ // https://stackoverflow.com/a/20851557/417806
+ const expressionUsageRegex = new RegExp(String.raw`(((? \n\r\b]))${identifier}\.${key}((?![-\w\d\W])|(?=[ <.,:;!?\n\r\b])))`, 'g');
+
+ if (literalUsageRegex.test(contents)) {
+ contents = contents.replace(literalUsageRegex, module[key]);
+ } else if (expressionUsageRegex.test(contents)) {
+ contents = contents.replace(expressionUsageRegex, `'${module[key]}'`);
+ }
+ });
+ }
+ });
+ }
+ }
+ }
+ );
+
+ return new Response(contents);
+ }
+}
+
+const greenwoodPluginCssModules = () => {
+ return [{
+ type: 'resource',
+ name: 'plugin-css-modules:scan',
+ provider: (compilation, options) => new ScanForCssModulesResource(compilation, options)
+ }, {
+ type: 'resource',
+ name: 'plugin-css-modules-strip-modules',
+ provider: (compilation, options) => new StripCssModulesResource(compilation, options)
+ }];
+};
+
+export { greenwoodPluginCssModules };
\ No newline at end of file
diff --git a/packages/plugin-css-modules/test/cases/build.default/build.default.spec.js b/packages/plugin-css-modules/test/cases/build.default/build.default.spec.js
new file mode 100644
index 000000000..9800f9f16
--- /dev/null
+++ b/packages/plugin-css-modules/test/cases/build.default/build.default.spec.js
@@ -0,0 +1,211 @@
+/*
+ * Use Case
+ * Run Greenwood build with CSS Modules plugin.
+ *
+ * User Result
+ * Should generate a Greenwood project with CSS Modules properly transformed.
+ *
+ * User Command
+ * greenwood build
+ *
+ * User Config
+ * import { greenwoodPluginCssModules } import '@greenwood/plugin-css-modules';
+ *
+ * {
+ * plugins: [
+ * greenwoodPluginCssModules()
+ * ]
+ * }
+ *
+ * User Workspace
+ * src/
+ * components/
+ * header/
+ * header.js
+ * header.module.css
+ * logo/
+ * logo.js
+ * logo.module.css
+ * index.html
+ */
+import chai from 'chai';
+import { JSDOM } from 'jsdom';
+import fs from 'fs';
+import glob from 'glob-promise';
+import path from 'path';
+import { parse, walk } from 'css-tree';
+import { runSmokeTest } from '../../../../../test/smoke-test.js';
+import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js';
+import { Runner } from 'gallinago';
+import { fileURLToPath, URL } from 'url';
+import { implementation } from 'jsdom/lib/jsdom/living/nodes/HTMLStyleElement-impl.js';
+
+const expect = chai.expect;
+
+describe('Build Greenwood With: ', function() {
+ const LABEL = 'Default Configuration for CSS Modules';
+ const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
+ const outputPath = fileURLToPath(new URL('.', import.meta.url));
+ let runner;
+ let updateAStyleBlockRef;
+
+ before(function() {
+ this.context = {
+ publicDir: path.join(outputPath, 'public')
+ };
+ runner = new Runner(false, true);
+ });
+
+ describe(LABEL, function() {
+
+ before(function() {
+ // JSDOM doesn't support CSS nesting and kind of blows up in the console as it tries to parse it automatically
+ // https://github.com/jsdom/jsdom/issues/2005#issuecomment-2397495853
+ updateAStyleBlockRef = implementation.prototype._updateAStyleBlock; // eslint-disable-line no-underscore-dangle
+ implementation.prototype._updateAStyleBlock = () => {}; // eslint-disable-line no-underscore-dangle
+
+ runner.setup(outputPath, getSetupFiles(outputPath));
+ runner.runCommand(cliPath, 'build');
+ });
+
+ runSmokeTest(['public', 'index'], LABEL);
+
+ describe('index.html with expected CSS and SSR contents', function() {
+ const EXPECTED_HEADER_CLASS_NAMES = 8;
+ let dom;
+ let headerSourceCss;
+ let expectedHeaderCss;
+
+ before(async function() {
+ dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html'));
+ headerSourceCss = await fs.promises.readFile(new URL('./src/components/header/header.module.css', import.meta.url), 'utf-8');
+ expectedHeaderCss = await fs.promises.readFile(new URL('./expected.header.css', import.meta.url), 'utf-8');
+ });
+
+ describe('Header component with CSS Modules', () => {
+ it('should have the expected scoped CSS inlined in a