diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9feef9c0..ea50fb26 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -223,7 +223,7 @@ The WTR and the full setup is covered in greater detail within [the GreenwoodJS
1. **[Mock Data Requests](https://www.greenwood.dev/guides/ecosystem/web-test-runner/#content-as-data)**
- If using Greenwood's Content as Data feature, mocking `fetch` with mock data is necessary.
+ If using one of Greenwood's Content as Data [Client APIs](/docs/content-as-data/data-client/), mocking `fetch` with mock data is necessary.
#### Examples
@@ -279,38 +279,26 @@ components/
my-component.stories.js
```
-#### _Content as Data_ Stories
-
-When a component requires a `fetch` request for data, the story will need to mock this request before being able to render within the Storybook.
-
-To mock `fetch`, create a `parameter` within the story export object named `fetchMock`. This object contains a `mocks` array with a `matcher` for the localhost network request URL. The `matcher.response` object represents the mocked data to use with the story.
-
-Checkout [the `blog-posts-list.stories.js` story](https://github.com/ProjectEvergreen/www.greenwoodjs.dev/blob/main/src/components/blog-posts-list/blog-posts-list.stories.js) as an example.
+Below is an example of a basic Storybook file:
```js
-import "./my-custom-element.js";
-import pages from "../../stories/mocks/graph.json";
+import "./header.js";
export default {
- title: "Components/My Custom Element",
- parameters: {
- // ...other parameters, if necessary...
- fetchMock: {
- mocks: [
- {
- matcher: {
- url: "http://localhost:1985/graph.json",
- response: {
- body: pages,
- },
- },
- },
- ],
- },
- },
+ title: "Components/Header",
};
+
+const Template = () => " ";
+
+export const Primary = Template.bind({});
```
+#### Content as Data
+
+When a component implements one of Greenwood's Content as Data [Client APIs](/docs/content-as-data/data-client/), the story will need to mock this request before being able to render within the Storybook.
+
+See the [Greenwood Storybook docs](/guides/ecosystem/storybook/#content-as-data) for more information and [the _blog-posts-list.stories.js_ story](https://github.com/ProjectEvergreen/www.greenwoodjs.dev/blob/main/src/components/blog-posts-list/blog-posts-list.stories.js) for an example in this project.
+
## Hosting and Deployment
This project is hosted on Netlify and automatically deploys on each merge into main. Release branches will be curated over the course of a Greenwood release cycle and then merged at the time the new Greenwood release is published to NPM.
diff --git a/eslint.config.js b/eslint.config.js
index 305bdcb5..b2e3b2ea 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -4,6 +4,7 @@ import markdown from "@eslint/markdown";
import json from "@eslint/json";
import js from "@eslint/js";
import globals from "globals";
+import noOnlyTests from "eslint-plugin-no-only-tests";
export default [
{
@@ -39,6 +40,10 @@ export default [
...js.configs.recommended.rules,
// turn this off for Prettier
"no-irregular-whitespace": "off",
+ "no-only-tests/no-only-tests": "error",
+ },
+ plugins: {
+ "no-only-tests": noOnlyTests,
},
},
{
diff --git a/greenwood.config.js b/greenwood.config.js
index 15c054aa..f0760558 100644
--- a/greenwood.config.js
+++ b/greenwood.config.js
@@ -1,8 +1,8 @@
+import { greenwoodPluginCssModules } from "@greenwood/plugin-css-modules";
import { greenwoodPluginImportRaw } from "@greenwood/plugin-import-raw";
-import { greenwoodPluginCssModules } from "./plugin-css-modules.js";
export default {
- activeFrontmatter: true,
+ activeContent: true,
// would be nice if we could customize these plugins, like appending the autolink headings
// https://github.com/ProjectEvergreen/greenwood/issues/1247
diff --git a/package-lock.json b/package-lock.json
index d52fe514..aea0b64a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,7 +7,6 @@
"": {
"name": "www.greenwoodjs.dev",
"version": "0.0.1",
- "hasInstallScript": true,
"license": "MIT",
"dependencies": {
"geist": "^1.2.0",
@@ -23,8 +22,9 @@
"@eslint/json": "^0.5.0",
"@eslint/markdown": "^6.2.0",
"@esm-bundle/chai": "^4.3.4-fix.0",
- "@greenwood/cli": "^0.30.0-alpha.6",
- "@greenwood/plugin-import-raw": "^0.30.0-alpha.6",
+ "@greenwood/cli": "^0.30.0-alpha.7",
+ "@greenwood/plugin-css-modules": "^0.30.0-alpha.7",
+ "@greenwood/plugin-import-raw": "^0.30.0-alpha.7",
"@ls-lint/ls-lint": "^1.10.0",
"@mapbox/rehype-prism": "^0.9.0",
"@storybook/addon-essentials": "^8.0.6",
@@ -37,12 +37,12 @@
"@web/test-runner-junit-reporter": "^0.7.1",
"eslint": "^9.11.1",
"eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-no-only-tests": "^3.3.0",
"globals": "^15.10.0",
"http-server": "^14.1.1",
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"lit": "^3.1.2",
- "patch-package": "^8.0.0",
"prettier": "^3.2.5",
"rehype-autolink-headings": "^4.0.0",
"rehype-slug": "^3.0.0",
@@ -2307,9 +2307,9 @@
"license": "MIT"
},
"node_modules/@greenwood/cli": {
- "version": "0.30.0-alpha.6",
- "resolved": "https://registry.npmjs.org/@greenwood/cli/-/cli-0.30.0-alpha.6.tgz",
- "integrity": "sha512-jQeZknzbvIJyo/M8rp3yFZAv72vuX+yjXES8UEeW2yL1jhvIyTY2Sr2xNp4zM3K/WVCedp+hBcf/QCCr+Ge+Aw==",
+ "version": "0.30.0-alpha.7",
+ "resolved": "https://registry.npmjs.org/@greenwood/cli/-/cli-0.30.0-alpha.7.tgz",
+ "integrity": "sha512-VzUwNic5lqFG4u+RqEPLAHkNnA/gAghOJSC8KoF6Aed66D+zjDW/WcR1VohemPRDAPqq4utorlMQg7yQCQE11Q==",
"dev": true,
"dependencies": {
"@rollup/plugin-commonjs": "^25.0.0",
@@ -2320,7 +2320,7 @@
"acorn-import-attributes": "^1.9.5",
"acorn-walk": "^8.0.0",
"commander": "^2.20.0",
- "css-tree": "^2.2.1",
+ "css-tree": "^3.0.0",
"es-module-shims": "^1.8.3",
"front-matter": "^4.0.2",
"koa": "^2.13.0",
@@ -2335,7 +2335,7 @@
"remark-rehype": "^7.0.0",
"rollup": "^3.29.4",
"unified": "^9.2.0",
- "wc-compiler": "~0.14.0"
+ "wc-compiler": "~0.15.0"
},
"bin": {
"greenwood": "src/index.js"
@@ -2344,10 +2344,65 @@
"node": ">=18.20.0"
}
},
+ "node_modules/@greenwood/cli/node_modules/css-tree": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.0.0.tgz",
+ "integrity": "sha512-o88DVQ6GzsABn1+6+zo2ct801dBO5OASVyxbbvA2W20ue2puSh/VOuqUj90eUeMSX/xqGqBmOKiRQN7tJOuBXw==",
+ "dev": true,
+ "dependencies": {
+ "mdn-data": "2.10.0",
+ "source-map-js": "^1.0.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/@greenwood/cli/node_modules/mdn-data": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.10.0.tgz",
+ "integrity": "sha512-qq7C3EtK3yJXMwz1zAab65pjl+UhohqMOctTgcqjLOWABqmwj+me02LSsCuEUxnst9X1lCBpoE0WArGKgdGDzw==",
+ "dev": true
+ },
+ "node_modules/@greenwood/plugin-css-modules": {
+ "version": "0.30.0-alpha.7",
+ "resolved": "https://registry.npmjs.org/@greenwood/plugin-css-modules/-/plugin-css-modules-0.30.0-alpha.7.tgz",
+ "integrity": "sha512-anJW8+7uTP0XwNMQKjkiwueIxrD9v7d8GD2KrMMZW58ORl/GRlKAuAsGRvy4Uvdx1YU3kbXSTIWleNBJotPJrQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.0.1",
+ "acorn-import-attributes": "^1.9.5",
+ "acorn-walk": "^8.0.0",
+ "css-tree": "^3.0.0",
+ "node-html-parser": "^1.2.21",
+ "sucrase": "^3.35.0"
+ },
+ "peerDependencies": {
+ "@greenwood/cli": "^0.4.0"
+ }
+ },
+ "node_modules/@greenwood/plugin-css-modules/node_modules/css-tree": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.0.0.tgz",
+ "integrity": "sha512-o88DVQ6GzsABn1+6+zo2ct801dBO5OASVyxbbvA2W20ue2puSh/VOuqUj90eUeMSX/xqGqBmOKiRQN7tJOuBXw==",
+ "dev": true,
+ "dependencies": {
+ "mdn-data": "2.10.0",
+ "source-map-js": "^1.0.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/@greenwood/plugin-css-modules/node_modules/mdn-data": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.10.0.tgz",
+ "integrity": "sha512-qq7C3EtK3yJXMwz1zAab65pjl+UhohqMOctTgcqjLOWABqmwj+me02LSsCuEUxnst9X1lCBpoE0WArGKgdGDzw==",
+ "dev": true
+ },
"node_modules/@greenwood/plugin-import-raw": {
- "version": "0.30.0-alpha.6",
- "resolved": "https://registry.npmjs.org/@greenwood/plugin-import-raw/-/plugin-import-raw-0.30.0-alpha.6.tgz",
- "integrity": "sha512-t3KgUOEfbet7NFAMYMz6W/9O4MH+KqMAge/xFyPspyo8kjNaQgmhfOiWBg7MQW5b28do/SODgRB4EZGKUVgOug==",
+ "version": "0.30.0-alpha.7",
+ "resolved": "https://registry.npmjs.org/@greenwood/plugin-import-raw/-/plugin-import-raw-0.30.0-alpha.7.tgz",
+ "integrity": "sha512-5FybNWlzWS36AffjON63gtr7V4SAfh+bIKfYEyx2Tkzeh+tc0oo+p4P/pBiLcaQjE/pGTtOoDJrlWNoqLfzlXw==",
"dev": true,
"peerDependencies": {
"@greenwood/cli": "^0.4.0"
@@ -5492,11 +5547,6 @@
"dev": true,
"license": "0BSD"
},
- "node_modules/@yarnpkg/lockfile": {
- "version": "1.1.0",
- "dev": true,
- "license": "BSD-2-Clause"
- },
"node_modules/accepts": {
"version": "1.3.8",
"dev": true,
@@ -5540,9 +5590,9 @@
}
},
"node_modules/acorn-walk": {
- "version": "8.3.3",
- "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz",
- "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==",
+ "version": "8.3.4",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+ "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"dev": true,
"dependencies": {
"acorn": "^8.11.0"
@@ -5840,14 +5890,6 @@
"tslib": "^2.4.0"
}
},
- "node_modules/at-least-node": {
- "version": "1.0.0",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">= 4.0.0"
- }
- },
"node_modules/autolinker": {
"version": "0.28.1",
"dev": true,
@@ -6595,20 +6637,6 @@
"devtools-protocol": "*"
}
},
- "node_modules/ci-info": {
- "version": "3.9.0",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/sibiraj-s"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/citty": {
"version": "0.1.6",
"dev": true,
@@ -7970,6 +7998,15 @@
"eslint": ">=7.0.0"
}
},
+ "node_modules/eslint-plugin-no-only-tests": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.3.0.tgz",
+ "integrity": "sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=5.0.0"
+ }
+ },
"node_modules/eslint-scope": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz",
@@ -8743,14 +8780,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/find-yarn-workspace-root": {
- "version": "2.0.0",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "micromatch": "^4.0.2"
- }
- },
"node_modules/flat-cache": {
"version": "4.0.1",
"dev": true,
@@ -10753,33 +10782,11 @@
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
- "node_modules/json-stable-stringify": {
- "version": "1.1.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.5",
- "isarray": "^2.0.5",
- "jsonify": "^0.0.1",
- "object-keys": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"dev": true,
"license": "MIT"
},
- "node_modules/json-stable-stringify/node_modules/isarray": {
- "version": "2.0.5",
- "dev": true,
- "license": "MIT"
- },
"node_modules/json5": {
"version": "2.2.3",
"dev": true,
@@ -10802,14 +10809,6 @@
"graceful-fs": "^4.1.6"
}
},
- "node_modules/jsonify": {
- "version": "0.0.1",
- "dev": true,
- "license": "Public Domain",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/keygrip": {
"version": "1.1.0",
"dev": true,
@@ -10840,14 +10839,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/klaw-sync": {
- "version": "6.0.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "graceful-fs": "^4.1.11"
- }
- },
"node_modules/kleur": {
"version": "3.0.3",
"dev": true,
@@ -13609,14 +13600,6 @@
"node": ">=8"
}
},
- "node_modules/os-tmpdir": {
- "version": "1.0.2",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/p-limit": {
"version": "3.1.0",
"dev": true,
@@ -13698,9 +13681,9 @@
}
},
"node_modules/package-json-from-dist": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
- "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true
},
"node_modules/pako": {
@@ -13766,156 +13749,6 @@
"node": ">= 0.8"
}
},
- "node_modules/patch-package": {
- "version": "8.0.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@yarnpkg/lockfile": "^1.1.0",
- "chalk": "^4.1.2",
- "ci-info": "^3.7.0",
- "cross-spawn": "^7.0.3",
- "find-yarn-workspace-root": "^2.0.0",
- "fs-extra": "^9.0.0",
- "json-stable-stringify": "^1.0.2",
- "klaw-sync": "^6.0.0",
- "minimist": "^1.2.6",
- "open": "^7.4.2",
- "rimraf": "^2.6.3",
- "semver": "^7.5.3",
- "slash": "^2.0.0",
- "tmp": "^0.0.33",
- "yaml": "^2.2.2"
- },
- "bin": {
- "patch-package": "index.js"
- },
- "engines": {
- "node": ">=14",
- "npm": ">5"
- }
- },
- "node_modules/patch-package/node_modules/ansi-styles": {
- "version": "4.3.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/patch-package/node_modules/chalk": {
- "version": "4.1.2",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/patch-package/node_modules/fs-extra": {
- "version": "9.1.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "at-least-node": "^1.0.0",
- "graceful-fs": "^4.2.0",
- "jsonfile": "^6.0.1",
- "universalify": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/patch-package/node_modules/has-flag": {
- "version": "4.0.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/patch-package/node_modules/lru-cache": {
- "version": "6.0.0",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/patch-package/node_modules/open": {
- "version": "7.4.2",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-docker": "^2.0.0",
- "is-wsl": "^2.1.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/patch-package/node_modules/rimraf": {
- "version": "2.7.1",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "glob": "^7.1.3"
- },
- "bin": {
- "rimraf": "bin.js"
- }
- },
- "node_modules/patch-package/node_modules/semver": {
- "version": "7.6.0",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/patch-package/node_modules/slash": {
- "version": "2.0.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/patch-package/node_modules/supports-color": {
- "version": "7.2.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/path-exists": {
"version": "4.0.0",
"dev": true,
@@ -16247,9 +16080,9 @@
}
},
"node_modules/sucrase/node_modules/glob": {
- "version": "10.4.2",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz",
- "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==",
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
@@ -16262,24 +16095,18 @@
"bin": {
"glob": "dist/esm/bin.mjs"
},
- "engines": {
- "node": ">=16 || 14 >=14.18"
- },
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sucrase/node_modules/jackspeak": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz",
- "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==",
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
- "engines": {
- "node": ">=14"
- },
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
@@ -16678,17 +16505,6 @@
"node": ">=14.0.0"
}
},
- "node_modules/tmp": {
- "version": "0.0.33",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "os-tmpdir": "~1.0.2"
- },
- "engines": {
- "node": ">=0.6.0"
- }
- },
"node_modules/to-fast-properties": {
"version": "2.0.0",
"dev": true,
@@ -17412,9 +17228,9 @@
}
},
"node_modules/wc-compiler": {
- "version": "0.14.0",
- "resolved": "https://registry.npmjs.org/wc-compiler/-/wc-compiler-0.14.0.tgz",
- "integrity": "sha512-5ouvZ2vDfwKTX9mj6IJWaJSF7239VAb+i8gbFqIyDRMuHqP0Bv9sq9oyZTDAqJM3trEiNWwv3VqI0fW4B8LAtg==",
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/wc-compiler/-/wc-compiler-0.15.0.tgz",
+ "integrity": "sha512-bzRjWEal5QGKrryZAsD3V9abuQ4blu2LP23GdrIM1UFybDRor6hcRhYJwdBLPriw017x/J69yPuRXLgAm2xUPQ==",
"dev": true,
"dependencies": {
"@projectevergreen/acorn-jsx-esm": "~0.1.0",
@@ -17730,17 +17546,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/yaml": {
- "version": "2.4.1",
- "dev": true,
- "license": "ISC",
- "bin": {
- "yaml": "bin.mjs"
- },
- "engines": {
- "node": ">= 14"
- }
- },
"node_modules/yargs": {
"version": "17.7.2",
"dev": true,
diff --git a/package.json b/package.json
index cb6354aa..27a4a072 100644
--- a/package.json
+++ b/package.json
@@ -30,8 +30,7 @@
"lint:css": "stylelint \"./src/**/*.css\"",
"format": "prettier . --write",
"format:check": "prettier . --check",
- "prepare": "husky install",
- "postinstall": "patch-package"
+ "prepare": "husky install"
},
"dependencies": {
"geist": "^1.2.0",
@@ -47,8 +46,9 @@
"@eslint/json": "^0.5.0",
"@eslint/markdown": "^6.2.0",
"@esm-bundle/chai": "^4.3.4-fix.0",
- "@greenwood/cli": "^0.30.0-alpha.6",
- "@greenwood/plugin-import-raw": "^0.30.0-alpha.6",
+ "@greenwood/cli": "^0.30.0-alpha.7",
+ "@greenwood/plugin-css-modules": "^0.30.0-alpha.7",
+ "@greenwood/plugin-import-raw": "^0.30.0-alpha.7",
"@ls-lint/ls-lint": "^1.10.0",
"@mapbox/rehype-prism": "^0.9.0",
"@storybook/addon-essentials": "^8.0.6",
@@ -61,12 +61,12 @@
"@web/test-runner-junit-reporter": "^0.7.1",
"eslint": "^9.11.1",
"eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-no-only-tests": "^3.3.0",
"globals": "^15.10.0",
"http-server": "^14.1.1",
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"lit": "^3.1.2",
- "patch-package": "^8.0.0",
"prettier": "^3.2.5",
"rehype-autolink-headings": "^4.0.0",
"rehype-slug": "^3.0.0",
diff --git a/patches/@greenwood+cli+0.30.0-alpha.6.patch b/patches/@greenwood+cli+0.30.0-alpha.6.patch
deleted file mode 100644
index 47d7c9f1..00000000
--- a/patches/@greenwood+cli+0.30.0-alpha.6.patch
+++ /dev/null
@@ -1,997 +0,0 @@
-diff --git a/node_modules/@greenwood/cli/src/config/rollup.config.js b/node_modules/@greenwood/cli/src/config/rollup.config.js
-index 53734d8..fa565a2 100644
---- a/node_modules/@greenwood/cli/src/config/rollup.config.js
-+++ b/node_modules/@greenwood/cli/src/config/rollup.config.js
-@@ -354,7 +354,7 @@ function greenwoodImportMetaUrl(compilation) {
- for (const entry of compilation.manifest.apis.keys()) {
- const apiRoute = compilation.manifest.apis.get(entry);
-
-- if (normalizedId.endsWith(apiRoute.path)) {
-+ if (normalizedId.endsWith(apiRoute.pagePath.replace('.', ''))) {
- const assets = apiRoute.assets || [];
-
- assets.push(assetUrl.url.href);
-@@ -646,7 +646,7 @@ const getRollupConfigForApiRoutes = async (compilation) => {
- const { outputDir, pagesDir, apisDir } = compilation.context;
-
- return [...compilation.manifest.apis.values()]
-- .map(api => normalizePathnameForWindows(new URL(`.${api.path}`, pagesDir)))
-+ .map(api => normalizePathnameForWindows(new URL(api.pagePath, pagesDir)))
- .map((filepath) => {
- // account for windows pathname shenanigans by "casting" filepath to a URL first
- const ext = filepath.split('.').pop();
-diff --git a/node_modules/@greenwood/cli/src/data/queries.js b/node_modules/@greenwood/cli/src/data/queries.js
-new file mode 100644
-index 0000000..866e296
---- /dev/null
-+++ b/node_modules/@greenwood/cli/src/data/queries.js
-@@ -0,0 +1,22 @@
-+// TODO how to sync host and port with greenwood config
-+const host = 'localhost';
-+const port = 1985;
-+
-+async function getContent() {
-+ return await fetch(`http://${host}:${port}/graph.json`)
-+ .then(resp => resp.json());
-+}
-+
-+async function getContentByCollection(collection = '') {
-+ return (await fetch(`http://${host}:${port}/graph.json`)
-+ .then(resp => resp.json()))
-+ .filter(page => page?.data?.collection === collection);
-+}
-+
-+async function getContentByRoute(route = '') {
-+ return (await fetch(`http://${host}:${port}/graph.json`)
-+ .then(resp => resp.json()))
-+ .filter(page => page?.route.startsWith(route));
-+}
-+
-+export { getContent, getContentByCollection, getContentByRoute };
-\ No newline at end of file
-diff --git a/node_modules/@greenwood/cli/src/lib/layout-utils.js b/node_modules/@greenwood/cli/src/lib/layout-utils.js
-index 8dbf281..a6f252e 100644
---- a/node_modules/@greenwood/cli/src/lib/layout-utils.js
-+++ b/node_modules/@greenwood/cli/src/lib/layout-utils.js
-@@ -30,10 +30,10 @@ async function getCustomPageLayoutsFromPlugins(compilation, layoutName) {
- return customLayoutLocations;
- }
-
--async function getPageLayout(filePath, compilation, layout) {
-+async function getPageLayout(filePath = '', compilation, layout) {
- const { config, context } = compilation;
-- const { layoutsDir, userLayoutsDir, pagesDir, projectDirectory } = context;
-- const filePathUrl = new URL(`${filePath}`, projectDirectory);
-+ const { layoutsDir, userLayoutsDir, pagesDir } = context;
-+ const filePathUrl = new URL(filePath, pagesDir);
- const customPageFormatPlugins = config.plugins
- .filter(plugin => plugin.type === 'resource' && !plugin.isGreenwoodDefaultPlugin)
- .map(plugin => plugin.provider(compilation));
-@@ -43,13 +43,13 @@ async function getPageLayout(filePath, compilation, layout) {
- && await customPageFormatPlugins[0].shouldServe(filePathUrl);
- const customPluginDefaultPageLayouts = await getCustomPageLayoutsFromPlugins(compilation, 'page');
- const customPluginPageLayouts = await getCustomPageLayoutsFromPlugins(compilation, layout);
-- const extension = filePath.split('.').pop();
-- const is404Page = filePath.startsWith('404') && extension === 'html';
-+ const extension = filePath?.split('.')?.pop();
-+ const is404Page = filePath?.endsWith('404.html') && extension === 'html';
- const hasCustomStaticLayout = await checkResourceExists(new URL(`./${layout}.html`, userLayoutsDir));
- const hasCustomDynamicLayout = await checkResourceExists(new URL(`./${layout}.js`, userLayoutsDir));
- const hasPageLayout = await checkResourceExists(new URL('./page.html', userLayoutsDir));
- const hasCustom404Page = await checkResourceExists(new URL('./404.html', pagesDir));
-- const isHtmlPage = extension === 'html' && await checkResourceExists(new URL(`./${filePath}`, projectDirectory));
-+ const isHtmlPage = extension === 'html' && await checkResourceExists(new URL(filePath, pagesDir));
- let contents;
-
- if (layout && (customPluginPageLayouts.length > 0 || hasCustomStaticLayout)) {
-@@ -108,11 +108,11 @@ async function getPageLayout(filePath, compilation, layout) {
- }
-
- /* eslint-disable-next-line complexity */
--async function getAppLayout(pageLayoutContents, compilation, customImports = [], frontmatterTitle) {
-+async function getAppLayout(pageLayoutContents, compilation, customImports = [], matchingRoute) {
-+ const activeFrontmatterTitleKey = '${globalThis.page.title}';
- const enableHud = compilation.config.devServer.hud;
- const { layoutsDir, userLayoutsDir } = compilation.context;
- const userStaticAppLayoutUrl = new URL('./app.html', userLayoutsDir);
-- // TODO support more than just .js files
- const userDynamicAppLayoutUrl = new URL('./app.js', userLayoutsDir);
- const userHasStaticAppLayout = await checkResourceExists(userStaticAppLayoutUrl);
- const userHasDynamicAppLayout = await checkResourceExists(userDynamicAppLayoutUrl);
-@@ -193,20 +193,25 @@ async function getAppLayout(pageLayoutContents, compilation, customImports = [],
- const appBody = appRoot.querySelector('body') ? appRoot.querySelector('body').innerHTML : '';
- const pageBody = pageRoot && pageRoot.querySelector('body') ? pageRoot.querySelector('body').innerHTML : '';
- const pageTitle = pageRoot && pageRoot.querySelector('head title');
-- const hasInterpolatedFrontmatter = pageTitle && pageTitle.rawText.indexOf('${globalThis.page.title}') >= 0
-- || appTitle && appTitle.rawText.indexOf('${globalThis.page.title}') >= 0;
-+ const hasActiveFrontmatterTitle = compilation.config.activeFrontmatter && (pageTitle && pageTitle.rawText.indexOf(activeFrontmatterTitleKey) >= 0
-+ || appTitle && appTitle.rawText.indexOf(activeFrontmatterTitleKey) >= 0);
-+ let title;
-
-- const title = hasInterpolatedFrontmatter // favor frontmatter interpolation first
-- ? pageTitle && pageTitle.rawText
-+ if (hasActiveFrontmatterTitle) {
-+ const text = pageTitle && pageTitle.rawText.indexOf(activeFrontmatterTitleKey) >= 0
- ? pageTitle.rawText
-- : appTitle.rawText
-- : frontmatterTitle // otherwise, work in order of specificity from page -> page layout -> app layout
-- ? frontmatterTitle
-+ : appTitle.rawText;
-+
-+ title = text.replace(activeFrontmatterTitleKey, matchingRoute.title || matchingRoute.label);
-+ } else {
-+ title = matchingRoute.title
-+ ? matchingRoute.title
- : pageTitle && pageTitle.rawText
- ? pageTitle.rawText
- : appTitle && appTitle.rawText
- ? appTitle.rawText
-- : 'My App';
-+ : matchingRoute.label;
-+ }
-
- const mergedHtml = pageRoot && pageRoot.querySelector('html').rawAttrs !== ''
- ? ``
-diff --git a/node_modules/@greenwood/cli/src/lib/walker-package-ranger.js b/node_modules/@greenwood/cli/src/lib/walker-package-ranger.js
-index 5ce30c1..a92bb3d 100644
---- a/node_modules/@greenwood/cli/src/lib/walker-package-ranger.js
-+++ b/node_modules/@greenwood/cli/src/lib/walker-package-ranger.js
-@@ -215,17 +215,33 @@ async function walkPackageJson(packageJson = {}) {
- return importMap;
- }
-
--function mergeImportMap(html = '', map = {}) {
-- // es-modules-shims breaks on dangling commas in an importMap :/
-- const danglingComma = html.indexOf('"imports": {}') > 0 ? '' : ',';
-- const importMap = JSON.stringify(map).replace('}', '').replace('{', '');
--
-- const merged = html.replace('"imports": {', `
-- "imports": {
-- ${importMap}${danglingComma}
-- `);
-+function mergeImportMap(html = '', map = {}, shouldShim = false) {
-+ const importMapType = shouldShim ? 'importmap-shim' : 'importmap';
-+ const hasImportMap = html.indexOf(`script type="${importMapType}"`) > 0;
-+ const danglingComma = hasImportMap ? ',' : '';
-+ const importMap = JSON.stringify(map, null, 2).replace('}', '').replace('{', '');
-+
-+ if (Object.entries(map).length === 0) {
-+ return html;
-+ }
-
-- return merged;
-+ if (hasImportMap) {
-+ return html.replace('"imports": {', `
-+ "imports": {
-+ ${importMap}${danglingComma}
-+ `);
-+ } else {
-+ return html.replace('
', `
-+
-+
-+ `);
-+ }
- }
-
- export {
-diff --git a/node_modules/@greenwood/cli/src/lifecycles/bundle.js b/node_modules/@greenwood/cli/src/lifecycles/bundle.js
-index 9c963f7..ae467bd 100644
---- a/node_modules/@greenwood/cli/src/lifecycles/bundle.js
-+++ b/node_modules/@greenwood/cli/src/lifecycles/bundle.js
-@@ -243,16 +243,14 @@ async function bundleSsrPages(compilation, optimizePlugins) {
- // and before we optimize so that all bundled assets can tracked up front
- // would be nice to see if this can be done in a single pass though...
- for (const page of ssrPages) {
-- const { imports, route, layout, title, relativeWorkspacePagePath } = page;
-- const moduleUrl = new URL(`.${relativeWorkspacePagePath}`, pagesDir);
-+ const { imports, route, layout, pagePath } = page;
-+ const moduleUrl = new URL(pagePath, pagesDir);
- const request = new Request(moduleUrl);
-- // TODO getLayout has to be static (for now?)
-- // https://github.com/ProjectEvergreen/greenwood/issues/955
- const data = await executeRouteModule({ moduleUrl, compilation, page, prerender: false, htmlContents: null, scripts: [], request });
- let staticHtml = '';
-
-- staticHtml = data.layout ? data.layout : await getPageLayout(staticHtml, compilation, layout);
-- staticHtml = await getAppLayout(staticHtml, compilation, imports, title);
-+ staticHtml = data.layout ? data.layout : await getPageLayout(pagePath, compilation, layout);
-+ staticHtml = await getAppLayout(staticHtml, compilation, imports, page);
- staticHtml = await getUserScripts(staticHtml, compilation);
- staticHtml = await (await interceptPage(new URL(`http://localhost:8080${route}`), new Request(new URL(`http://localhost:8080${route}`)), getPluginInstances(compilation), staticHtml)).text();
-
-@@ -268,14 +266,13 @@ async function bundleSsrPages(compilation, optimizePlugins) {
-
- // second pass to link all bundled assets to their resources before optimizing and generating SSR bundles
- for (const page of ssrPages) {
-- const { filename, route, relativeWorkspacePagePath } = page;
-- const entryFileUrl = new URL(`.${relativeWorkspacePagePath}`, scratchDir);
-- const outputPathRootUrl = new URL(`file://${path.dirname(entryFileUrl.pathname)}`);
-+ const { route, pagePath } = page;
-+ const entryFileUrl = new URL(pagePath, pagesDir);
-+ const entryFileOutputUrl = new URL(`file://${entryFileUrl.pathname.replace(pagesDir.pathname, scratchDir.pathname)}`);
-+ const outputPathRootUrl = new URL(`file://${path.dirname(entryFileOutputUrl.pathname)}/`);
- const htmlOptimizer = config.plugins.find(plugin => plugin.name === 'plugin-standard-html').provider(compilation);
- const pagesPathDiff = context.pagesDir.pathname.replace(context.projectDirectory.pathname, '');
-- const relativeDepth = relativeWorkspacePagePath.replace(`/${filename}`, '') === ''
-- ? '../'
-- : '../'.repeat(relativeWorkspacePagePath.replace(`/${filename}`, '').split('/').length);
-+ const relativeDepth = '../'.repeat(pagePath.split('/').length - 1);
-
- let staticHtml = ssrPrerenderPagesRouteMapper[route];
- staticHtml = await (await htmlOptimizer.optimize(new URL(`http://localhost:8080${route}`), new Response(staticHtml))).text();
-@@ -288,10 +285,10 @@ async function bundleSsrPages(compilation, optimizePlugins) {
- }
-
- // better way to write out this inline code?
-- await fs.writeFile(entryFileUrl, `
-+ await fs.writeFile(entryFileOutputUrl, `
- import { executeRouteModule } from '${normalizePathnameForWindows(executeModuleUrl)}';
-
-- const moduleUrl = new URL('${relativeDepth}${pagesPathDiff}${relativeWorkspacePagePath.replace('/', '')}', import.meta.url);
-+ const moduleUrl = new URL('${relativeDepth}${pagesPathDiff}${pagePath.replace('./', '')}', import.meta.url);
-
- export async function handler(request) {
- const compilation = JSON.parse('${JSON.stringify(compilation)}');
-@@ -311,7 +308,7 @@ async function bundleSsrPages(compilation, optimizePlugins) {
- }
- `);
-
-- input.push(normalizePathnameForWindows(entryFileUrl));
-+ input.push(normalizePathnameForWindows(entryFileOutputUrl));
- }
-
- const ssrConfigs = await getRollupConfigForSsrPages(compilation, input);
-diff --git a/node_modules/@greenwood/cli/src/lifecycles/config.js b/node_modules/@greenwood/cli/src/lifecycles/config.js
-index 9c31a23..807a79e 100644
---- a/node_modules/@greenwood/cli/src/lifecycles/config.js
-+++ b/node_modules/@greenwood/cli/src/lifecycles/config.js
-@@ -46,7 +46,7 @@ const defaultConfig = {
- port: 8080,
- basePath: '',
- optimization: optimizations[0],
-- interpolateFrontmatter: false,
-+ activeFrontmatter: false,
- plugins: greenwoodPlugins,
- markdown: { plugins: [], settings: {} },
- prerender: false,
-@@ -82,7 +82,7 @@ const readAndMergeConfig = async() => {
- if (hasConfigFile) {
- const userCfgFile = (await import(configUrl)).default;
- // eslint-disable-next-line max-len
-- const { workspace, devServer, markdown, optimization, plugins, port, prerender, basePath, staticRouter, pagesDirectory, layoutsDirectory, interpolateFrontmatter, isolation, polyfills } = userCfgFile;
-+ const { workspace, devServer, markdown, optimization, plugins, port, prerender, basePath, staticRouter, pagesDirectory, layoutsDirectory, activeFrontmatter, isolation, polyfills } = userCfgFile;
-
- // workspace validation
- if (workspace) {
-@@ -103,11 +103,11 @@ const readAndMergeConfig = async() => {
- reject(`Error: provided optimization "${optimization}" is not supported. Please use one of: ${optimizations.join(', ')}.`);
- }
-
-- if (interpolateFrontmatter) {
-- if (typeof interpolateFrontmatter !== 'boolean') {
-- reject('Error: greenwood.config.js interpolateFrontmatter must be a boolean');
-+ if (activeFrontmatter) {
-+ if (typeof activeFrontmatter !== 'boolean') {
-+ reject('Error: greenwood.config.js activeFrontmatter must be a boolean');
- }
-- customConfig.interpolateFrontmatter = interpolateFrontmatter;
-+ customConfig.activeFrontmatter = activeFrontmatter;
- }
-
- if (plugins && plugins.length > 0) {
-diff --git a/node_modules/@greenwood/cli/src/lifecycles/graph.js b/node_modules/@greenwood/cli/src/lifecycles/graph.js
-index a1d16e5..d73c515 100644
---- a/node_modules/@greenwood/cli/src/lifecycles/graph.js
-+++ b/node_modules/@greenwood/cli/src/lifecycles/graph.js
-@@ -5,13 +5,33 @@ import { checkResourceExists, requestAsObject } from '../lib/resource-utils.js';
- import toc from 'markdown-toc';
- import { Worker } from 'worker_threads';
-
-+function getLabelFromRoute(_route) {
-+ let route = _route;
-+
-+ if (route === '/index/') {
-+ return 'Home';
-+ } else if (route.endsWith('/index/')) {
-+ route = route.replace('index/', '');
-+ }
-+
-+ return route
-+ .split('/')
-+ .filter(part => part !== '')
-+ .pop()
-+ .split('-')
-+ .map((routePart) => {
-+ return `${routePart.charAt(0).toUpperCase()}${routePart.substring(1)}`;
-+ })
-+ .join(' ');
-+}
- const generateGraph = async (compilation) => {
-
- return new Promise(async (resolve, reject) => {
- try {
- const { context, config } = compilation;
- const { basePath } = config;
-- const { pagesDir, projectDirectory, userWorkspace } = context;
-+ const { pagesDir, userWorkspace } = context;
-+ const collections = {};
- const customPageFormatPlugins = config.plugins
- .filter(plugin => plugin.type === 'resource' && !plugin.isGreenwoodDefaultPlugin)
- .map(plugin => plugin.provider(compilation));
-@@ -19,11 +39,10 @@ const generateGraph = async (compilation) => {
- let apis = new Map();
- let graph = [{
- outputPath: '/index.html',
-- filename: 'index.html',
-- path: '/',
-+ pagePath: './src/index.html',
- route: `${basePath}/`,
-- id: 'index',
-- label: 'Index',
-+ label: 'Home',
-+ title: null,
- data: {},
- imports: [],
- resources: [],
-@@ -50,14 +69,14 @@ const generateGraph = async (compilation) => {
- const isCustom = customPageFormatPlugins[0] && customPageFormatPlugins[0].shouldServe && await customPageFormatPlugins[0].shouldServe(filenameUrl, req)
- ? customPageFormatPlugins[0].servePage
- : null;
-- const relativePagePath = filenameUrl.pathname.replace(pagesDir.pathname, '/');
-- const relativeWorkspacePath = directory.pathname.replace(projectDirectory.pathname, '');
-+ const relativePagePath = filenameUrl.pathname.replace(pagesDir.pathname, './');
- const isStatic = isCustom === 'static' || extension === '.md' || extension === '.html';
- const isDynamic = isCustom === 'dynamic' || extension === '.js';
-- const isApiRoute = relativePagePath.startsWith('/api');
- const isPage = isStatic || isDynamic;
-+ let route = `${relativePagePath.replace('.', '').replace(`${extension}`, '')}`;
-+ let fileContents;
-
-- if (isApiRoute) {
-+ if (route.startsWith('/api/')) {
- const req = new Request(filenameUrl);
- const extension = filenameUrl.pathname.split('.').pop();
- const isCustom = customPageFormatPlugins[0] && customPageFormatPlugins[0].shouldServe && await customPageFormatPlugins[0].shouldServe(filenameUrl, req);
-@@ -67,9 +86,7 @@ const generateGraph = async (compilation) => {
- return;
- }
-
-- const relativeApiPath = filenameUrl.pathname.replace(pagesDir.pathname, '/');
-- const route = `${basePath}${relativeApiPath.replace(`.${extension}`, '')}`;
-- // TODO should this be run in isolation like SSR pages?
-+ // should this be run in isolation like SSR pages?
- // https://github.com/ProjectEvergreen/greenwood/issues/991
- const { isolation } = await import(filenameUrl).then(module => module);
-
-@@ -82,21 +99,19 @@ const generateGraph = async (compilation) => {
- * route: URL route for a given page on outputFilePath
- * isolation: if this should be run in isolated mode
- */
-- apiRoutes.set(route, {
-- filename: filename,
-- outputPath: relativeApiPath,
-- path: relativeApiPath,
-- route,
-+ apiRoutes.set(`${basePath}${route}`, {
-+ pagePath: relativePagePath,
-+ outputPath: relativePagePath,
-+ route: `${basePath}${route}`,
- isolation
- });
- } else if (isPage) {
-- let route = relativePagePath.replace(extension, '');
-- let id = filename.split('/')[filename.split('/').length - 1].replace(extension, '');
-+ let root = filename.split('/')[filename.split('/').length - 1].replace(extension, '');
- let layout = extension === '.html' ? null : 'page';
- let title = null;
-+ let label = getLabelFromRoute(`${route}/`);
- let imports = [];
- let customData = {};
-- let filePath;
- let prerender = true;
- let isolation = false;
- let hydration = false;
-@@ -109,9 +124,9 @@ const generateGraph = async (compilation) => {
- * - pages/blog/index.{html,md,js} -> /blog/
- * - pages/blog/some-post.{html,md,js} -> /blog/some-post/
- */
-- if (relativePagePath.lastIndexOf('/') > 0) {
-+ if (relativePagePath.lastIndexOf('/index') > 0) {
- // https://github.com/ProjectEvergreen/greenwood/issues/455
-- route = id === 'index' || route.replace('/index', '') === `/${id}`
-+ route = root === 'index' || route.replace('/index', '') === `/${root}`
- ? route.replace('index', '')
- : `${route}/`;
- } else {
-@@ -121,60 +136,19 @@ const generateGraph = async (compilation) => {
- }
-
- if (isStatic) {
-- const fileContents = await fs.readFile(filenameUrl, 'utf8');
-+ fileContents = await fs.readFile(filenameUrl, 'utf8');
- const { attributes } = fm(fileContents);
-
- layout = attributes.layout || layout;
- title = attributes.title || title;
-- id = attributes.label || id;
-+ label = attributes.label || label;
- imports = attributes.imports || [];
-- filePath = `${relativeWorkspacePath}${filename}`;
-
-- // prune "reserved" attributes that are supported by Greenwood
-- // https://www.greenwoodjs.io/docs/front-matter
- customData = attributes;
--
-- delete customData.label;
-- delete customData.imports;
-- delete customData.title;
-- delete customData.layout;
--
-- /* Menu Query
-- * Custom front matter - Variable Definitions
-- * --------------------------------------------------
-- * menu: the name of the menu in which this item can be listed and queried
-- * index: the index of this list item within a menu
-- * linkheadings: flag to tell us where to add page's table of contents as menu items
-- * tableOfContents: json object containing page's table of contents(list of headings)
-- */
-- // set specific menu to place this page
-- customData.menu = customData.menu || '';
--
-- // set specific index list priority of this item within a menu
-- customData.index = customData.index || '';
--
-- // set flag whether to gather a list of headings on a page as menu items
-- customData.linkheadings = customData.linkheadings || 0;
-- customData.tableOfContents = [];
--
-- if (customData.linkheadings > 0) {
-- // parse markdown for table of contents and output to json
-- customData.tableOfContents = toc(fileContents).json;
-- customData.tableOfContents.shift();
--
-- // parse table of contents for only the pages user wants linked
-- if (customData.tableOfContents.length > 0 && customData.linkheadings > 0) {
-- customData.tableOfContents = customData.tableOfContents
-- .filter((item) => item.lvl === customData.linkheadings);
-- }
-- }
-- /* ---------End Menu Query-------------------- */
- } else if (isDynamic) {
- const routeWorkerUrl = compilation.config.plugins.filter(plugin => plugin.type === 'renderer')[0].provider(compilation).executeModuleUrl;
- let ssrFrontmatter;
-
-- filePath = route;
--
- await new Promise(async (resolve, reject) => {
- const worker = new Worker(new URL('../lib/ssr-route-worker.js', import.meta.url));
- const request = await requestAsObject(new Request(filenameUrl));
-@@ -207,11 +181,8 @@ const generateGraph = async (compilation) => {
- page: JSON.stringify({
- servePage: isCustom,
- route,
-- id,
-- label: id.split('-')
-- .map((idPart) => {
-- return `${idPart.charAt(0).toUpperCase()}${idPart.substring(1)}`;
-- }).join(' ')
-+ root,
-+ label
- }),
- request
- });
-@@ -221,66 +192,97 @@ const generateGraph = async (compilation) => {
- layout = ssrFrontmatter.layout || layout;
- title = ssrFrontmatter.title || title;
- imports = ssrFrontmatter.imports || imports;
-- customData = ssrFrontmatter.data || customData;
--
-- /* Menu Query
-- * Custom front matter - Variable Definitions
-- * --------------------------------------------------
-- * menu: the name of the menu in which this item can be listed and queried
-- * index: the index of this list item within a menu
-- * linkheadings: flag to tell us where to add page's table of contents as menu items
-- * tableOfContents: json object containing page's table of contents(list of headings)
-- */
-- customData.menu = ssrFrontmatter.menu || '';
-- customData.index = ssrFrontmatter.index || '';
-+ label = ssrFrontmatter.label || label;
-+ customData = ssrFrontmatter || customData;
- }
- }
-
- /*
-- * Graph Properties (per page)
-- *----------------------
-- * data: custom page frontmatter
-- * filename: base filename of the page
-- * id: filename without the extension
-- * relativeWorkspacePagePath: the file path relative to the user's workspace directory
-- * label: "pretty" text representation of the filename
-- * imports: per page JS or CSS file imports to be included in HTML output from frontmatter
-- * resources: sum of all resources for the entire page
-- * outputPath: the filename to write to when generating static HTML
-- * path: path to the file relative to the workspace
-- * route: URL route for a given page on outputFilePath
-- * layout: page layout to use as a base for a generated component
-- * title: a default value that can be used for
-- * isSSR: if this is a server side route
-- * prerender: if this should be statically exported
-- * isolation: if this should be run in isolated mode
-- * hydration: if this page needs hydration support
-- * servePage: signal that this is a custom page file type (static | dynamic)
-- */
-- pages.push({
-+ * Custom front matter - Variable Definitions
-+ * --------------------------------------------------
-+ * collection: the name of the collection for the page
-+ * order: the order of this item within the collection
-+ * tocHeading: heading size to use a Table of Contents for a page
-+ * tableOfContents: json object containing page's table of contents (list of headings)
-+ */
-+
-+ // prune "reserved" attributes that are supported by Greenwood
-+ // https://www.greenwoodjs.io/docs/front-matter
-+ delete customData.label;
-+ delete customData.imports;
-+ delete customData.title;
-+ delete customData.layout;
-+
-+ // set flag whether to gather a list of headings on a page as menu items
-+ customData.tocHeading = customData.tocHeading || 0;
-+ customData.tableOfContents = [];
-+
-+ if (fileContents && customData.tocHeading > 0 && customData.tocHeading <= 6) {
-+ // console.log({ route, fileContents, customData });
-+ // console.log('===========================');
-+ // console.log(customData.tocHeading);
-+ // parse markdown for table of contents and output to json
-+ customData.tableOfContents = toc(fileContents).json;
-+ // not sure why we were shiting here?
-+ // customData.tableOfContents.shift();
-+
-+ // parse table of contents for only the pages user wants linked
-+ if (customData.tableOfContents.length > 0 && customData.tocHeading > 0) {
-+ customData.tableOfContents = customData.tableOfContents
-+ .filter((item) => item.lvl === customData.tocHeading);
-+ }
-+ }
-+
-+ /*
-+ * Page Properties
-+ *----------------------
-+ * label: Display text for the page inferred, by default is the value of title
-+ * title: used to customize the tag of the page, inferred from the filename
-+ * route: URL for accessing the page from the browser
-+ * layout: the custom layout of the page
-+ * data: custom page frontmatter
-+ * imports: per page JS or CSS file imports specified from frontmatter
-+ * resources: all script, style, etc resources for the entire page as URLs
-+ * outputPath: the name of the file in the output folder
-+ * isSSR: if this is a server side route
-+ * prerender: if this page should be statically exported
-+ * isolation: if this page should be run in isolated mode
-+ * hydration: if this page needs hydration support
-+ * servePage: signal that this is a custom page file type (static | dynamic)
-+ */
-+ const page = {
-+ label,
-+ title,
-+ route: `${basePath}${route}`,
-+ layout,
- data: customData || {},
-- filename,
-- id,
-- relativeWorkspacePagePath: relativePagePath,
-- label: id.split('-')
-- .map((idPart) => {
-- return `${idPart.charAt(0).toUpperCase()}${idPart.substring(1)}`;
-- }).join(' '),
- imports,
- resources: [],
-+ pagePath: relativePagePath,
- outputPath: route === '/404/'
- ? '/404.html'
- : `${route}index.html`,
-- path: filePath,
-- route: `${basePath}${route}`,
-- layout,
-- title,
- isSSR: !isStatic,
- prerender,
- isolation,
- hydration,
- servePage: isCustom
-- });
-+ };
-+
-+ pages.push(page);
-+
-+ // handle collections
-+ const pageCollection = customData.collection;
-+
-+ if (pageCollection) {
-+ if (!collections[pageCollection]) {
-+ collections[pageCollection] = [];
-+ }
-+
-+ collections[pageCollection].push(page);
-+ }
-+
-+ compilation.collections = collections;
- } else {
- console.debug(`Unhandled extension (${extension}) for route => ${route}`);
- }
-@@ -320,11 +322,11 @@ const generateGraph = async (compilation) => {
- {
- ...oldGraph,
- outputPath: '/404.html',
-- filename: '404.html',
-+ pagePath: './src/404.html',
- route: `${basePath}/404/`,
- path: '404.html',
-- id: '404',
-- label: 'Not Found'
-+ label: 'Not Found',
-+ title: 'Page Not Found'
- }
- ];
- }
-@@ -346,8 +348,7 @@ const generateGraph = async (compilation) => {
- }
-
- graph.push({
-- filename: null,
-- path: null,
-+ pagePath: null,
- data: {},
- imports: [],
- resources: [],
-diff --git a/node_modules/@greenwood/cli/src/lifecycles/serve.js b/node_modules/@greenwood/cli/src/lifecycles/serve.js
-index ae31a62..f0b95a6 100644
---- a/node_modules/@greenwood/cli/src/lifecycles/serve.js
-+++ b/node_modules/@greenwood/cli/src/lifecycles/serve.js
-@@ -337,7 +337,7 @@ async function getHybridServer(compilation) {
- if (matchingRoute.isolation || isolationMode) {
- await new Promise(async (resolve, reject) => {
- const worker = new Worker(new URL('../lib/ssr-route-worker-isolation-mode.js', import.meta.url));
-- // TODO "faux" new Request here, a better way?
-+ // "faux" new Request here, a better way?
- const request = await requestAsObject(new Request(url));
-
- worker.on('message', async (result) => {
-@@ -370,13 +370,13 @@ async function getHybridServer(compilation) {
- ctx.status = 200;
- } else if (isApiRoute) {
- const apiRoute = manifest.apis.get(url.pathname);
-- const entryPointUrl = new URL(`.${apiRoute.outputPath}`, outputDir);
-+ const entryPointUrl = new URL(`./${apiRoute.outputPath}`, outputDir);
- let body, status, headers, statusText;
-
- if (apiRoute.isolation || isolationMode) {
- await new Promise(async (resolve, reject) => {
- const worker = new Worker(new URL('../lib/api-route-worker.js', import.meta.url));
-- // TODO "faux" new Request here, a better way?
-+ // "faux" new Request here, a better way?
- const req = await requestAsObject(request);
-
- worker.on('message', async (result) => {
-diff --git a/node_modules/@greenwood/cli/src/plugins/resource/plugin-api-routes.js b/node_modules/@greenwood/cli/src/plugins/resource/plugin-api-routes.js
-index 20de63b..6e98d05 100644
---- a/node_modules/@greenwood/cli/src/plugins/resource/plugin-api-routes.js
-+++ b/node_modules/@greenwood/cli/src/plugins/resource/plugin-api-routes.js
-@@ -20,7 +20,7 @@ class ApiRoutesResource extends ResourceInterface {
-
- async serve(url, request) {
- const api = this.compilation.manifest.apis.get(url.pathname);
-- const apiUrl = new URL(`.${api.path}`, this.compilation.context.pagesDir);
-+ const apiUrl = new URL(api.pagePath, this.compilation.context.pagesDir);
- const href = apiUrl.href;
-
- if (process.env.__GWD_COMMAND__ === 'develop') { // eslint-disable-line no-underscore-dangle
-diff --git a/node_modules/@greenwood/cli/src/plugins/resource/plugin-content-as-data.js b/node_modules/@greenwood/cli/src/plugins/resource/plugin-content-as-data.js
-new file mode 100644
-index 0000000..36185b1
---- /dev/null
-+++ b/node_modules/@greenwood/cli/src/plugins/resource/plugin-content-as-data.js
-@@ -0,0 +1,54 @@
-+import { mergeImportMap } from '../../lib/walker-package-ranger.js';
-+import { ResourceInterface } from '../../lib/resource-interface.js';
-+
-+const importMap = {
-+ '@greenwood/cli/src/data/queries.js': '/node_modules/@greenwood/cli/src/data/queries.js'
-+};
-+
-+class ContentAsDataResource extends ResourceInterface {
-+ constructor(compilation, options = {}) {
-+ super(compilation, options);
-+
-+ this.contentType = ['text/html'];
-+ }
-+
-+ async shouldIntercept(url, request, response) {
-+ return response.headers.get('Content-Type')?.indexOf(this.contentType[0]) >= 0 && process.env.__GWD_COMMAND__ === 'develop'; // eslint-disable-line no-underscore-dangle
-+ }
-+
-+ async intercept(url, request, response) {
-+ const body = await response.text();
-+ const newBody = mergeImportMap(body, importMap, this.compilation.config.polyfills.importMaps);
-+
-+ // TODO how come we need to forward headers, shouldn't mergeResponse do that for us?
-+ return new Response(newBody, {
-+ headers: response.headers
-+ });
-+ }
-+
-+ // TODO graphql based hydration?
-+ // async shouldOptimize(url, response) {
-+ // return response.headers.get('Content-Type').indexOf(this.contentType[1]) >= 0;
-+ // }
-+
-+ // async optimize(url, response) {
-+ // let body = await response.text();
-+
-+ // body = body.replace('', `
-+ //
-+ //
-+ // `);
-+
-+ // return new Response(body);
-+ // }
-+}
-+
-+const greenwoodPluginContentAsData = {
-+ type: 'resource',
-+ name: 'plugin-content-as-data:resource',
-+ provider: (compilation) => new ContentAsDataResource(compilation)
-+};
-+
-+export { greenwoodPluginContentAsData };
-\ No newline at end of file
-diff --git a/node_modules/@greenwood/cli/src/plugins/resource/plugin-node-modules.js b/node_modules/@greenwood/cli/src/plugins/resource/plugin-node-modules.js
-index 286c2de..bd662ae 100644
---- a/node_modules/@greenwood/cli/src/plugins/resource/plugin-node-modules.js
-+++ b/node_modules/@greenwood/cli/src/plugins/resource/plugin-node-modules.js
-@@ -10,7 +10,7 @@ import replace from '@rollup/plugin-replace';
- import { getNodeModulesLocationForPackage, getPackageJson, getPackageNameFromUrl } from '../../lib/node-modules-utils.js';
- import { resolveForRelativeUrl } from '../../lib/resource-utils.js';
- import { ResourceInterface } from '../../lib/resource-interface.js';
--import { walkPackageJson } from '../../lib/walker-package-ranger.js';
-+import { walkPackageJson, mergeImportMap } from '../../lib/walker-package-ranger.js';
-
- let importMap;
-
-@@ -75,7 +75,6 @@ class NodeModulesResource extends ResourceInterface {
- async intercept(url, request, response) {
- const { context, config } = this.compilation;
- const { importMaps } = config.polyfills;
-- const importMapType = importMaps ? 'importmap-shim' : 'importmap';
- const importMapShimScript = importMaps ? '' : '';
- let body = await response.text();
- const hasHead = body.match(/\(.*)<\/head>/s);
-@@ -97,15 +96,10 @@ class NodeModulesResource extends ResourceInterface {
- ? await walkPackageJson(userPackageJson)
- : importMap || {};
-
-- // apply import map and shim for users
-+ body = mergeImportMap(body, importMap, importMaps);
- body = body.replace('', `
-
- ${importMapShimScript}
--
- `);
-
- return new Response(body);
-diff --git a/node_modules/@greenwood/cli/src/plugins/resource/plugin-standard-html.js b/node_modules/@greenwood/cli/src/plugins/resource/plugin-standard-html.js
-index 06223cf..b372e38 100644
---- a/node_modules/@greenwood/cli/src/plugins/resource/plugin-standard-html.js
-+++ b/node_modules/@greenwood/cli/src/plugins/resource/plugin-standard-html.js
-@@ -5,7 +5,6 @@
- * This is a Greenwood default plugin.
- *
- */
--import frontmatter from 'front-matter';
- import fs from 'fs/promises';
- import rehypeStringify from 'rehype-stringify';
- import rehypeRaw from 'rehype-raw';
-@@ -37,18 +36,16 @@ class StandardHtmlResource extends ResourceInterface {
-
- async serve(url, request) {
- const { config, context } = this.compilation;
-- const { pagesDir, userWorkspace } = context;
-- const { interpolateFrontmatter } = config;
-+ const { projectDirectory, pagesDir } = context;
-+ const { activeFrontmatter } = config;
- const { pathname } = url;
- const isSpaRoute = this.compilation.graph.find(node => node.isSPA);
- const matchingRoute = this.compilation.graph.find((node) => node.route === pathname) || {};
-- const filePath = !matchingRoute.external ? matchingRoute.path : '';
-- const isMarkdownContent = (matchingRoute?.filename || '').split('.').pop() === 'md';
--
-+ const { pagePath } = matchingRoute;
-+ const filePath = !matchingRoute.external ? pagePath : '';
-+ const isMarkdownContent = (filePath || '').split('.').pop() === 'md';
- let body = '';
-- let title = matchingRoute.title || null;
- let layout = matchingRoute.layout || null;
-- let frontMatter = matchingRoute.data || {};
- let customImports = matchingRoute.imports || [];
- let ssrBody;
- let ssrLayout;
-@@ -59,7 +56,7 @@ class StandardHtmlResource extends ResourceInterface {
- }
-
- if (isMarkdownContent) {
-- const markdownContents = await fs.readFile(filePath, 'utf-8');
-+ const markdownContents = await fs.readFile(new URL(pagePath, pagesDir), 'utf-8');
- const rehypePlugins = [];
- const remarkPlugins = [];
-
-@@ -74,7 +71,6 @@ class StandardHtmlResource extends ResourceInterface {
- }
-
- const settings = config.markdown.settings || {};
-- const fm = frontmatter(markdownContents);
-
- processedMarkdown = await unified()
- .use(remarkParse, settings) // parse markdown into AST
-@@ -85,27 +81,10 @@ class StandardHtmlResource extends ResourceInterface {
- .use(rehypePlugins) // apply userland rehype plugins
- .use(rehypeStringify) // convert AST to HTML string
- .process(markdownContents);
--
-- // configure via frontmatter
-- if (fm.attributes) {
-- frontMatter = fm.attributes;
--
-- if (frontMatter.title) {
-- title = frontMatter.title;
-- }
--
-- if (frontMatter.layout) {
-- layout = frontMatter.layout;
-- }
--
-- if (frontMatter.imports) {
-- customImports = frontMatter.imports;
-- }
-- }
- }
-
- if (matchingRoute.isSSR) {
-- const routeModuleLocationUrl = new URL(`.${matchingRoute.relativeWorkspacePagePath}`, pagesDir);
-+ const routeModuleLocationUrl = new URL(pagePath, pagesDir);
- const routeWorkerUrl = this.compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider().executeModuleUrl;
-
- await new Promise(async (resolve, reject) => {
-@@ -139,12 +118,12 @@ class StandardHtmlResource extends ResourceInterface {
- }
-
- if (isSpaRoute) {
-- body = await fs.readFile(new URL(`./${isSpaRoute.filename}`, userWorkspace), 'utf-8');
-+ body = await fs.readFile(new URL(isSpaRoute.pagePath, projectDirectory), 'utf-8');
- } else {
-- body = ssrLayout ? ssrLayout : await getPageLayout(filePath, this.compilation, layout);
-+ body = ssrLayout ? ssrLayout : await getPageLayout(pagePath, this.compilation, layout);
- }
-
-- body = await getAppLayout(body, this.compilation, customImports, title);
-+ body = await getAppLayout(body, this.compilation, customImports, matchingRoute);
- body = await getUserScripts(body, this.compilation);
-
- if (processedMarkdown) {
-@@ -171,15 +150,30 @@ class StandardHtmlResource extends ResourceInterface {
- body = body.replace(/\(.*)<\/content-outlet>/s, `${ssrBody.replace(/\$/g, '$$$')}`);
- }
-
-- if (interpolateFrontmatter) {
-- for (const fm in frontMatter) {
-+ if (activeFrontmatter) {
-+ for (const fm in matchingRoute.data) {
- const interpolatedFrontmatter = '\\$\\{globalThis.page.' + fm + '\\}';
-+ const needle = typeof matchingRoute.data[fm] === 'string' ? matchingRoute.data[fm] : JSON.stringify(matchingRoute.data[fm]).replace(/"/g, '"');
-+
-+ body = body.replace(new RegExp(interpolatedFrontmatter, 'g'), needle);
-+ }
-+
-+ const activeFrontmatterForwardKeys = ['route', 'label', 'title'];
-+
-+ for (const key of activeFrontmatterForwardKeys) {
-+ const interpolatedFrontmatter = '\\$\\{globalThis.page.' + key + '\\}';
-+
-+ body = body.replace(new RegExp(interpolatedFrontmatter, 'g'), matchingRoute[key]);
-+ }
-+
-+ for (const collection in this.compilation.collections) {
-+ const interpolatedFrontmatter = '\\$\\{globalThis.collection.' + collection + '\\}';
-
-- body = body.replace(new RegExp(interpolatedFrontmatter, 'g'), frontMatter[fm]);
-+ body = body.replace(new RegExp(interpolatedFrontmatter, 'g'), JSON.stringify(this.compilation.collections[collection]).replace(/"/g, '"'));
- }
- }
-
-- // clean up placeholder content-outlet
-+ // clean up any empty placeholder content-outlet
- if (body.indexOf(' ') > 0) {
- body = body.replace(' ', '');
- }
-diff --git a/node_modules/@greenwood/cli/src/plugins/resource/plugin-static-router.js b/node_modules/@greenwood/cli/src/plugins/resource/plugin-static-router.js
-index 4145ec5..f039e61 100644
---- a/node_modules/@greenwood/cli/src/plugins/resource/plugin-static-router.js
-+++ b/node_modules/@greenwood/cli/src/plugins/resource/plugin-static-router.js
-@@ -69,7 +69,7 @@ class StaticRouterResource extends ResourceInterface {
- .filter(page => !page.isSSR)
- .filter(page => !page.route.endsWith('/404/'))
- .map((page) => {
-- const layout = page.filename && page.filename.split('.').pop() === this.extensions[0]
-+ const layout = page.pagePath && page.pagePath.split('.').pop() === this.extensions[0]
- ? page.route
- : page.layout;
- const key = page.route === '/'
-diff --git a/node_modules/@greenwood/cli/src/plugins/server/plugin-content.js b/node_modules/@greenwood/cli/src/plugins/server/plugin-content.js
-new file mode 100644
-index 0000000..5023e6a
---- /dev/null
-+++ b/node_modules/@greenwood/cli/src/plugins/server/plugin-content.js
-@@ -0,0 +1,47 @@
-+import Koa from 'koa';
-+import { ServerInterface } from '../../lib/server-interface.js';
-+import { Readable } from 'stream';
-+
-+class ContentServer extends ServerInterface {
-+ constructor(compilation, options = {}) {
-+ super(compilation, options);
-+ }
-+
-+ async start() {
-+ const app = new Koa();
-+
-+ app.use(async (ctx, next) => {
-+ try {
-+ if (ctx.request.path.startsWith('/graph.json')) {
-+ const { graph } = this.compilation;
-+
-+ ctx.body = Readable.from(JSON.stringify(graph));
-+ ctx.status = 200;
-+ ctx.message = 'OK';
-+
-+ ctx.set('Content-Type', 'application/json');
-+ ctx.set('Access-Control-Allow-Origin', '*');
-+ }
-+ } catch (e) {
-+ ctx.status = 500;
-+ console.error(e);
-+ }
-+
-+ await next();
-+ });
-+
-+ // TODO use dev server +1
-+ await app.listen('1985', () => {
-+ console.log('Started content server at => http://localhost:1985');
-+ });
-+ }
-+}
-+
-+// TODO remove graph.json resolution from regular dev server?
-+const greenwoodPluginContentServer = {
-+ type: 'server',
-+ name: 'plugin-content-server',
-+ provider: (compilation) => new ContentServer(compilation)
-+};
-+
-+export { greenwoodPluginContentServer };
-\ No newline at end of file
diff --git a/patches/wc-compiler+0.14.0.patch b/patches/wc-compiler+0.14.0.patch
deleted file mode 100644
index 298e0216..00000000
--- a/patches/wc-compiler+0.14.0.patch
+++ /dev/null
@@ -1,124 +0,0 @@
-diff --git a/node_modules/wc-compiler/src/dom-shim.js b/node_modules/wc-compiler/src/dom-shim.js
-index be289a3..db07eb9 100644
---- a/node_modules/wc-compiler/src/dom-shim.js
-+++ b/node_modules/wc-compiler/src/dom-shim.js
-@@ -83,6 +83,9 @@ class Document extends Node {
- createDocumentFragment(html) {
- return new DocumentFragment(html);
- }
-+
-+ querySelector() { }
-+ querySelectorAll() { }
- }
-
- // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
-@@ -102,6 +105,10 @@ class ShadowRoot extends DocumentFragment {
- super();
- this.mode = options.mode || 'closed';
- this.adoptedStyleSheets = [];
-+ // TODO not sure if this is the right base class for these?
-+ this.querySelector = noop;
-+ this.querySelectorAll = noop;
-+ this.getElementById = noop;
- }
- }
-
-diff --git a/node_modules/wc-compiler/src/wcc.js b/node_modules/wc-compiler/src/wcc.js
-index 35884d4..13e5aaa 100644
---- a/node_modules/wc-compiler/src/wcc.js
-+++ b/node_modules/wc-compiler/src/wcc.js
-@@ -32,16 +32,27 @@ async function renderComponentRoots(tree, definitions) {
- const { tagName } = node;
-
- if (definitions[tagName]) {
-+ // console.log('renderComponentRoots', { tagName });
- const { moduleURL } = definitions[tagName];
-- const elementInstance = await initializeCustomElement(moduleURL, tagName, node.attrs, definitions);
-- const elementHtml = elementInstance.shadowRoot
-+ // console.log({ node });
-+ const elementInstance = await initializeCustomElement(moduleURL, tagName, node, definitions);
-+ const hasShadow = elementInstance.shadowRoot;
-+ const elementHtml = hasShadow
- ? elementInstance.getInnerHTML({ includeShadowRoots: true })
- : elementInstance.innerHTML;
- const elementTree = parseFragment(elementHtml);
-+ const hasLight = elementTree.childNodes > 0;
-
-- node.childNodes = node.childNodes.length === 0
-+ // console.log('elementHtml', { elementHtml });
-+ // console.log('elementTree', { elementTree });
-+ // console.log('elementTree.childNodes', elementTree.childNodes);
-+ // console.log('node.childNodes', node.childNodes);
-+
-+ node.childNodes = node.childNodes.length === 0 && hasLight > 0 && !hasShadow
- ? elementTree.childNodes
-- : [...elementTree.childNodes, ...node.childNodes];
-+ : hasShadow
-+ ? [...elementTree.childNodes, ...node.childNodes]
-+ : elementTree.childNodes;
- } else {
- console.warn(`WARNING: customElement <${tagName}> is not defined. You may not have imported it yet.`);
- }
-@@ -138,7 +149,10 @@ async function getTagName(moduleURL) {
- return tagName;
- }
-
--async function initializeCustomElement(elementURL, tagName, attrs = [], definitions = [], isEntry, props = {}) {
-+async function initializeCustomElement(elementURL, tagName, node = {}, definitions = [], isEntry, props = {}) {
-+ const { attrs = [], childNodes = [] } = node;
-+ // console.log('initializeCustomElement', { node });
-+
- if (!tagName) {
- const depth = isEntry ? 1 : 0;
- registerDependencies(elementURL, definitions, depth);
-@@ -157,6 +171,41 @@ async function initializeCustomElement(elementURL, tagName, attrs = [], definiti
-
- if (element) {
- const elementInstance = new element(data); // eslint-disable-line new-cap
-+ let innerHTML = elementInstance.innerHTML || '';
-+
-+ // TODO
-+ // 1. Needs to be recursive
-+ // 2. ~~Needs to handle attributes~~
-+ // 3. Needs to handle duplicate content
-+ // 4. Needs to handle self closing tags
-+ // 5. handle all node types
-+ childNodes.forEach((child) => {
-+ const { nodeName, attrs = [] } = child;
-+
-+ if (nodeName !== '#text') {
-+ innerHTML += `<${nodeName}`;
-+
-+ if (attrs.length > 0) {
-+ attrs.forEach(attr => {
-+ innerHTML += ` ${attr.name}="${attr.value}"`;
-+ });
-+ }
-+
-+ innerHTML += '>';
-+
-+ child.childNodes.forEach((c) => {
-+ if (c.nodeName === '#text') {
-+ innerHTML += c.value;
-+ }
-+ });
-+
-+ innerHTML += `${nodeName}>`;
-+ }
-+ });
-+
-+ // console.log({ innerHTML });
-+ elementInstance.innerHTML = innerHTML;
-+ // console.log('=================');
-
- attrs.forEach((attr) => {
- elementInstance.setAttribute(attr.name, attr.value);
-@@ -207,7 +256,7 @@ async function renderFromHTML(html, elements = []) {
- const definitions = [];
-
- for (const url of elements) {
-- await initializeCustomElement(url, undefined, undefined, definitions, true);
-+ registerDependencies(url, definitions, 1);
- }
-
- const elementTree = getParse(html)(html);
diff --git a/plugin-css-modules.js b/plugin-css-modules.js
deleted file mode 100644
index a3acd8e6..00000000
--- a/plugin-css-modules.js
+++ /dev/null
@@ -1,348 +0,0 @@
-/*
- *
- * A plugin for enabling CSS Modules. :tm:
- *
- */
-import fs from "fs";
-import htmlparser from "node-html-parser";
-import { parse, walk } from "css-tree";
-import { ResourceInterface } from "@greenwood/cli/src/lib/resource-interface.js";
-import * as acornWalk from "acorn-walk";
-import * as acorn from "acorn";
-import { hashString } from "@greenwood/cli/src/lib/hashing-utils.js";
-import { importAttributes } from "acorn-import-attributes"; // comes from Greenwood
-
-function getCssModulesMap(compilation) {
- const locationUrl = new URL("./__css-modules-map.json", compilation.context.scratchDir);
- let cssModulesMap = {};
-
- if (fs.existsSync(locationUrl.pathname)) {
- cssModulesMap = JSON.parse(fs.readFileSync(locationUrl.pathname));
- }
-
- return cssModulesMap;
-}
-
-function walkAllImportsForCssModules(scriptUrl, sheets, compilation) {
- const scriptContents = fs.readFileSync(scriptUrl, "utf-8");
-
- acornWalk.simple(
- acorn.Parser.extend(importAttributes).parse(scriptContents, {
- ecmaVersion: "2020",
- sourceType: "module",
- }),
- {
- ImportDeclaration(node) {
- const { specifiers = [], source = {} } = node;
- const { value = "" } = source;
-
- // console.log({ value, specifiers });
- // TODO bare specifiers support?
- if (
- value.endsWith(".module.css") &&
- specifiers.length === 1 &&
- specifiers[0].local.name === "styles"
- ) {
- // console.log("WE GOT A WINNER!!!", value);
- const cssModuleUrl = new URL(value, scriptUrl);
- const scope = cssModuleUrl.pathname.split("/").pop().split(".")[0];
- const cssContents = fs.readFileSync(cssModuleUrl, "utf-8");
- const hash = hashString(cssContents);
- const classNameMap = {};
- let scopedCssContents = cssContents;
-
- const ast = parse(cssContents, {
- // positions: true,
- onParseError(error) {
- console.log(error.formattedMessage);
- },
- });
-
- walk(ast, {
- enter: function (node) {
- // drill down from a SelectorList to its first Selector
- // and check its first child to see if it is a ClassSelector
- // and if so, hash that initial class selector
- if (node.type === "SelectorList") {
- if (node.children?.head?.data?.type === "Selector") {
- if (node.children?.head?.data?.children?.head?.data?.type === "ClassSelector") {
- const { name } = node.children.head.data.children.head.data;
- const scopedClassName = `${scope}-${hash}-${name}`;
- classNameMap[name] = scopedClassName;
-
- /*
- * bit of a hacky solution since as we are walking class names one at a time, if we have multiple uses of .heading (for example)
- * then by the end we could have .my-component-111-header.my-component-111-header.etc, since we want to replace all instances (e.g. the g flag in Regex)
- *
- * csstree supports loc so we _could_ target the class replacement down to start / end points, but that unfortunately slows things down a lot
- */
- // TODO this is a pretty ugly find / replace technique...
- // will definitely want to refactor and test this well
- if (
- scopedCssContents.indexOf(`.${scopedClassName} `) < 0 &&
- scopedCssContents.indexOf(`.${scopedClassName} {`) < 0
- ) {
- scopedCssContents = scopedCssContents.replace(
- new RegExp(String.raw`.${name} `, "g"),
- `.${scope}-${hash}-${name} `,
- );
- scopedCssContents = scopedCssContents.replace(
- new RegExp(String.raw`.${name},`, "g"),
- `.${scope}-${hash}-${name},`,
- );
- scopedCssContents = scopedCssContents.replace(
- new RegExp(String.raw`.${name}:`, "g"),
- `.${scope}-${hash}-${name}:`,
- );
- }
- }
- }
- }
- },
- });
-
- // TODO could we convert this module into an instance of CSSStylesheet to grab values?
- // https://web.dev/articles/constructable-stylesheets
- // or just use postcss-modules plugin?
- const cssModulesMap = getCssModulesMap(compilation);
- // console.log("UPDATE MAP!", { cssModulesMap, cssModuleUrl, scriptUrl });
- fs.writeFileSync(
- new URL("./__css-modules-map.json", compilation.context.scratchDir),
- JSON.stringify({
- ...cssModulesMap,
- [`${cssModuleUrl.href}`]: {
- module: classNameMap,
- contents: scopedCssContents,
- importer: scriptUrl,
- },
- }),
- );
- // globalThis.cssModulesMap.set(cssModuleUrl.href, {
- // module: classNameMap,
- // contents: scopedCssContents
- // })
- // console.log(
- // "after update",
- // getCssModulesMap(compilation)
- // );
- // sheets.push(cssContents);
- } else if (node.source.value.endsWith(".js")) {
- // console.log("go recursive for", { scriptUrl, value });
- const recursiveScriptUrl = new URL(value, scriptUrl);
-
- if (fs.existsSync(recursiveScriptUrl)) {
- walkAllImportsForCssModules(recursiveScriptUrl, sheets, compilation);
- }
- }
- },
- },
- );
-}
-
-class CssModulesResource extends ResourceInterface {
- constructor(compilation, options) {
- super(compilation, options);
-
- this.extensions = ["module.css"];
- this.contentType = "text/javascript";
-
- // // console.log('constructor???')
- if (!fs.existsSync(this.compilation.context.scratchDir.pathname)) {
- // // console.log('!!!!!!!!! make it!');
- fs.mkdirSync(this.compilation.context.scratchDir.pathname, { recursive: true });
- fs.writeFileSync(
- new URL("./__css-modules-map.json", this.compilation.context.scratchDir).pathname,
- JSON.stringify({}),
- );
- }
- }
-
- async shouldResolve(url) {
- return url.protocol === "file:" && url.pathname.endsWith("module.css");
- }
-
- async resolve(url) {
- // console.log({ url });
- const { projectDirectory, userWorkspace } = this.compilation.context;
- const { pathname, searchParams } = url;
- const params =
- url.searchParams.size > 0 ? `${searchParams.toString()}&type=css-module` : "type=css-module";
- const root =
- url.protocol === "file:"
- ? new URL(`file://${pathname}`).href
- : pathname.startsWith("/node_modules")
- ? new URL(`.${pathname}`, projectDirectory).href
- : new URL(`.${pathname}`, userWorkspace).href;
-
- // console.log("DOOT DOOT", { root, params });
- const matchedUrl = new URL(`${root}?${params}`);
-
- return new Request(matchedUrl);
- }
-
- // async shouldIntercept(url) {
- // console.log('css modules intercept', { url });
- // const { pathname, protocol } = url;
- // const mapKey = `${protocol}//${pathname}`;
- // // // console.log(this.compilation.context.scratchDir)
- // // // console.log(new URL('./__css-modules-map.json', this.compilation.context.scratchDir));
- // const cssModulesMap = getCssModulesMap(this.compilation);
- // // console.log("shouldServer", { cssModulesMap, url });
- // return protocol === "file:" && pathname.endsWith(this.extensions[0]) && cssModulesMap[mapKey];
- // }
-
- // async intercept(url) {
- // console.log('css modules intercept', { url });
- // const { pathname, protocol } = url;
- // const mapKey = `${protocol}//${pathname}`;
- // const cssModulesMap = getCssModulesMap(this.compilation);
- // // console.log("@@@@@@", { url, cssModulesMap });
- // const cssModule = `export default ${JSON.stringify(cssModulesMap[mapKey].module)}`;
-
- // // console.log("@@@@@@", { cssModule });
- // return new Response(cssModule, {
- // headers: {
- // "Content-Type": this.contentType,
- // },
- // });
- // }
-
- // this happens "first" as the HTML is returned, to find viable references to CSS Modules
- // better way than just checking for /?
- async shouldIntercept(url) {
- const { pathname, protocol } = url;
- const mapKey = `${protocol}//${pathname}`;
- const cssModulesMap = getCssModulesMap(this.compilation);
-
- return (
- url.pathname.endsWith("/") ||
- (protocol === "file:" && pathname.endsWith(this.extensions[0]) && cssModulesMap[mapKey])
- );
- }
-
- async intercept(url, request, response) {
- const { pathname, protocol } = url;
- const mapKey = `${protocol}//${pathname}`;
- const cssModulesMap = getCssModulesMap(this.compilation);
-
- if (url.pathname.endsWith("/")) {
- const body = await response.text();
- const dom = htmlparser.parse(body, { script: true });
- const scripts = dom.querySelectorAll("head script");
- const sheets = []; // TODO use a map here?
-
- for (const script of scripts) {
- const type = script.getAttribute("type");
- const src = script.getAttribute("src");
- if (src && ["module", "module-shim"].includes(type)) {
- // console.log("check this file for CSS Modules", src);
- // await resolveForRelativeUrl(new URL(src, import.meta.url this.compilation.context.userWorkspace)
- const scriptUrl = new URL(
- `./${src.replace(/\.\.\//g, "").replace(/\.\//g, "")}`,
- this.compilation.context.userWorkspace,
- );
- walkAllImportsForCssModules(scriptUrl, sheets, this.compilation);
- }
- }
-
- const cssModulesMap = getCssModulesMap(this.compilation);
- // console.log({ cssModulesMap });
-
- // for(const cssModule of cssModulesMap) {
- // // console.log({ cssModule });
- // }
- Object.keys(cssModulesMap).forEach((key) => {
- sheets.push(cssModulesMap[key].contents);
- });
-
- const newBody = body.replace(
- "",
- `
-
-
- `,
- );
-
- return new Response(newBody);
- } else if (
- url.pathname.endsWith("/") ||
- (protocol === "file:" && pathname.endsWith(this.extensions[0]) && cssModulesMap[mapKey])
- ) {
- const cssModule = `export default ${JSON.stringify(cssModulesMap[mapKey].module)}`;
-
- return new Response(cssModule, {
- headers: {
- "Content-Type": this.contentType,
- },
- });
- }
- }
-
- async shouldOptimize(url, response) {
- const contents = await response.text();
-
- // fuzzy search for now, we'll do a full AST walk through in optimize
- return (
- contents.indexOf("module.css") >= 0 &&
- (response.headers?.get("Content-Type") || "").indexOf("text/javascript") >= 0
- );
- }
-
- async optimize(url, response) {
- const { context } = this.compilation;
- let contents = await response.clone().text();
-
- acornWalk.simple(
- acorn.Parser.extend(importAttributes).parse(contents, {
- ecmaVersion: "2020",
- sourceType: "module",
- }),
- {
- ImportDeclaration(node) {
- const { specifiers = [], source = {}, start, end } = node;
- const { value = "" } = source;
-
- if (
- value.endsWith(".module.css") &&
- specifiers.length === 1 &&
- specifiers[0].local.name === "styles"
- ) {
- // console.log("WE GOT A WINNER!!!", value);
- contents = `${contents.slice(0, start)} \n ${contents.slice(end)}`;
- const cssModulesMap = getCssModulesMap({ context });
-
- Object.values(cssModulesMap).forEach((value) => {
- const { importer, module } = value;
- // console.log("$$$$$$$", { importer, url });
-
- if (importer === url.href) {
- Object.keys(module).forEach((key) => {
- contents = contents.replace(
- new RegExp(String.raw`\$\{styles.${key}\}`, "g"),
- module[key],
- );
- });
- }
- });
- }
- },
- },
- );
-
- return new Response(contents, { headers: response.headers });
- }
-}
-
-const greenwoodPluginCssModules = () => {
- return [
- {
- type: "resource",
- name: "plugin-css-modules",
- provider: (compilation, options) => new CssModulesResource(compilation, options),
- },
- ];
-};
-
-export { greenwoodPluginCssModules };
diff --git a/src/components/blog-posts-list/blog-posts-list.js b/src/components/blog-posts-list/blog-posts-list.js
index ecf6dfe7..663f7a59 100644
--- a/src/components/blog-posts-list/blog-posts-list.js
+++ b/src/components/blog-posts-list/blog-posts-list.js
@@ -1,9 +1,9 @@
-import { getContentByRoute } from "@greenwood/cli/src/data/queries.js";
+import { getContentByRoute } from "@greenwood/cli/src/data/client.js";
import styles from "./blog-posts-list.module.css";
export default class BlogPostsList extends HTMLElement {
async connectedCallback() {
- const posts = (await getContentByRoute("/blog"))
+ const posts = (await getContentByRoute("/blog/"))
.filter((page) => page.data.published)
// we sort in reverse chronologic order, e.g. last in, first out (LIFO)
.sort((a, b) =>
diff --git a/src/components/blog-posts-list/blog-posts-list.stories.js b/src/components/blog-posts-list/blog-posts-list.stories.js
index 7bb5c5b1..f74eb9dc 100644
--- a/src/components/blog-posts-list/blog-posts-list.stories.js
+++ b/src/components/blog-posts-list/blog-posts-list.stories.js
@@ -1,6 +1,8 @@
import "./blog-posts-list.js";
import pages from "../../stories/mocks/graph.json";
+const ROUTE = "/blog/";
+
export default {
title: "Components/Blog Posts List",
parameters: {
@@ -8,9 +10,9 @@ export default {
mocks: [
{
matcher: {
- url: "http://localhost:1985/graph.json",
+ url: "http://localhost:1984/___graph.json",
response: {
- body: pages,
+ body: pages.filter((page) => page.route.startsWith(ROUTE)),
},
},
},
diff --git a/src/components/capabilities/capabilities.js b/src/components/capabilities/capabilities.js
index 35200db3..7644ee83 100644
--- a/src/components/capabilities/capabilities.js
+++ b/src/components/capabilities/capabilities.js
@@ -25,49 +25,52 @@ export default class Capabilities extends HTMLElement {
}
connectedCallback() {
- this.contentItems = globalThis.document?.querySelectorAll(".capabilities-content") || [];
+ // bail of out of SSR entirely
+ if (typeof window !== "undefined") {
+ this.contentItems = globalThis.document?.querySelectorAll(".capabilities-content") || [];
- if (this.contentItems.length > 0) {
- template.innerHTML = `
-
-
-
Go from zero to fullstack with web standards
+ if (this.contentItems.length > 0) {
+ template.innerHTML = `
+
+
+
Go from zero to fullstack with web standards
-
-
- ${Array.from(this.contentItems)
- .map((item, idx) => {
- const title = item.querySelector("span").innerHTML;
- const icon = item.querySelector("i").textContent;
- const isActiveClass = idx === this.index ? " active" : "";
+
+
+ ${Array.from(this.contentItems)
+ .map((item, idx) => {
+ const title = item.querySelector("span").innerHTML;
+ const icon = item.querySelector("i").textContent;
+ const isActiveClass = idx === this.index ? " active" : "";
- return `
-
-
- ${availableIconSVGs[icon]}
- ${title}
-
-
- `;
- })
- .join("")}
-
-
+ return `
+
+
+ ${availableIconSVGs[icon]}
+ ${title}
+
+
+ `;
+ })
+ .join("")}
+
+
-
${this.contentItems[this.index].querySelector("p").innerHTML}
-
${this.contentItems[this.index].querySelector("pre").outerHTML}
+
${this.contentItems[this.index].querySelector("p").innerHTML}
+
${this.contentItems[this.index].querySelector("pre").outerHTML}
+
-
- `;
+ `;
- this.attachShadow({ mode: "open" });
- this.shadowRoot.appendChild(template.content.cloneNode(true));
- this.shadowRoot.adoptedStyleSheets = [themeSheet, sheet];
- this.shadowRoot
- .querySelectorAll(".section")
- .forEach((item) => item.addEventListener("click", this.selectItem.bind(this)));
- } else {
- console.debug("no capabilities content sections detected");
+ this.attachShadow({ mode: "open" });
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
+ this.shadowRoot.adoptedStyleSheets = [themeSheet, sheet];
+ this.shadowRoot
+ .querySelectorAll(".section")
+ .forEach((item) => item.addEventListener("click", this.selectItem.bind(this)));
+ } else {
+ console.debug("no capabilities content sections detected");
+ }
}
}
diff --git a/src/components/copy-to-clipboard/copy-to-clipboard.js b/src/components/copy-to-clipboard/copy-to-clipboard.js
index 7b1d295a..c4669789 100644
--- a/src/components/copy-to-clipboard/copy-to-clipboard.js
+++ b/src/components/copy-to-clipboard/copy-to-clipboard.js
@@ -9,19 +9,20 @@ template.innerHTML = `
export default class CopyToClipboard extends HTMLElement {
connectedCallback() {
- if (!this.shadowRoot) {
+ // bail of out of SSR entirely
+ if (!this.shadowRoot && typeof window !== "undefined") {
this.attachShadow({ mode: "open" });
this.shadowRoot.appendChild(template.content.cloneNode(true));
- }
- this.shadowRoot.adoptedStyleSheets = [sheet];
+ this.shadowRoot.adoptedStyleSheets = [sheet];
- this.shadowRoot.getElementById("icon")?.addEventListener("click", () => {
- const contents = this.getAttribute("content") ?? undefined;
+ this.shadowRoot.getElementById("icon")?.addEventListener("click", () => {
+ const contents = this.getAttribute("content") ?? undefined;
- navigator.clipboard.writeText(contents);
- console.log("copying the following contents to your clipboard =>", contents);
- });
+ navigator.clipboard.writeText(contents);
+ console.log("copying the following contents to your clipboard =>", contents);
+ });
+ }
}
}
diff --git a/src/components/side-nav/side-nav.js b/src/components/side-nav/side-nav.js
index cc2d1a3d..adc08b82 100644
--- a/src/components/side-nav/side-nav.js
+++ b/src/components/side-nav/side-nav.js
@@ -1,4 +1,4 @@
-import { getContentByRoute } from "@greenwood/cli/src/data/queries.js";
+import { getContentByRoute } from "@greenwood/cli/src/data/client.js";
import styles from "./side-nav.module.css";
export default class SideNav extends HTMLElement {
diff --git a/src/components/side-nav/side-nav.spec.js b/src/components/side-nav/side-nav.spec.js
index 6e39b72d..9ed66291 100644
--- a/src/components/side-nav/side-nav.spec.js
+++ b/src/components/side-nav/side-nav.spec.js
@@ -5,7 +5,7 @@ import graph from "../../stories/mocks/graph.json" with { type: "json" };
// https://stackoverflow.com/questions/45425169/intercept-fetch-api-requests-and-responses-in-javascript
window.fetch = function () {
return new Promise((resolve) => {
- resolve(new Response(JSON.stringify(graph)));
+ resolve(new Response(JSON.stringify(graph.filter((page) => page.route.startsWith(ROUTE)))));
});
};
diff --git a/src/components/side-nav/side-nav.stories.js b/src/components/side-nav/side-nav.stories.js
index 9c31f684..539808ae 100644
--- a/src/components/side-nav/side-nav.stories.js
+++ b/src/components/side-nav/side-nav.stories.js
@@ -1,6 +1,8 @@
import "./side-nav.js";
import pages from "../../stories/mocks/graph.json";
+const ROUTE = "/guides/";
+
export default {
title: "Components/Side Nav",
parameters: {
@@ -8,9 +10,9 @@ export default {
mocks: [
{
matcher: {
- url: "http://localhost:1985/graph.json",
+ url: "http://localhost:1984/___graph.json",
response: {
- body: pages,
+ body: pages.filter((page) => page.route.startsWith(ROUTE)),
},
},
},
@@ -20,6 +22,6 @@ export default {
};
const Template = () =>
- "
";
+ `
`;
export const Primary = Template.bind({});
diff --git a/src/components/table-of-contents/table-of-contents.js b/src/components/table-of-contents/table-of-contents.js
index a625eea2..f9bcc417 100644
--- a/src/components/table-of-contents/table-of-contents.js
+++ b/src/components/table-of-contents/table-of-contents.js
@@ -1,4 +1,4 @@
-import { getContent } from "@greenwood/cli/src/data/queries.js";
+import { getContent } from "@greenwood/cli/src/data/client.js";
import styles from "./table-of-contents.module.css";
export default class TableOfContents extends HTMLElement {
@@ -20,7 +20,7 @@ export default class TableOfContents extends HTMLElement {
const { content, slug } = item;
return `
-
${content}
`;
diff --git a/src/components/table-of-contents/table-of-contents.stories.js b/src/components/table-of-contents/table-of-contents.stories.js
index 94f2c937..b5ee46fe 100644
--- a/src/components/table-of-contents/table-of-contents.stories.js
+++ b/src/components/table-of-contents/table-of-contents.stories.js
@@ -8,7 +8,7 @@ export default {
mocks: [
{
matcher: {
- url: "http://localhost:1985/graph.json",
+ url: "http://localhost:1984/___graph.json",
response: {
body: pages,
},
diff --git a/src/pages/guides/ecosystem/storybook.md b/src/pages/guides/ecosystem/storybook.md
index c2bd8025..5ceafde1 100644
--- a/src/pages/guides/ecosystem/storybook.md
+++ b/src/pages/guides/ecosystem/storybook.md
@@ -201,9 +201,11 @@ export default defineConfig({
## Content as Data
-If you are using any of Greenwood's [content as data](/docs/content-as-data/) features, you'll want to configure Storybook for mocking `fetch` calls in your stories.
+If you are using any of Greenwood's Content as Data [Client APIs](/docs/content-as-data/data-client/), you'll want to configure Storybook to mock the HTTP calls Greenwood's data client makes, and provide the desired response needed based on the API being called.
-1. First, install the [**storybook-addon-fetch-mock**](https://storybook.js.org/addons/storybook-addon-fetch-mock) addon
+This can be accomplished with the [**storybook-addon-fetch-mock**](https://storybook.js.org/addons/storybook-addon-fetch-mock) addon and configuring it with the right `matcher.url` and `matcher.response`
+
+1. First, install the **storybook-addon-fetch-mock** addon
```shell
$ npm i -D storybook-addon-fetch-mock
```
@@ -237,9 +239,10 @@ If you are using any of Greenwood's [content as data](/docs/content-as-data/) fe
mocks: [
{
matcher: {
- url: "http://localhost:1985/graph.json",
+ url: "http://localhost:1984/___graph.json",
response: {
- body: pages,
+ // this is an example of mocking out getContentByRoute
+ body: pages.filter((page) => page.route.startsWith("/blog/")),
},
},
},
diff --git a/src/pages/guides/ecosystem/web-test-runner.md b/src/pages/guides/ecosystem/web-test-runner.md
index 235e7bd2..1862b440 100644
--- a/src/pages/guides/ecosystem/web-test-runner.md
+++ b/src/pages/guides/ecosystem/web-test-runner.md
@@ -192,7 +192,9 @@ export default {
## Content as Data
-If you are using any of Greenwood's [content as data](/docs/content-as-data/) features, you'll want to have your tests handle mocking of `fetch` calls. This can be done with a simple override of `window.fetch`
+If you are using any of Greenwood's Content as Data [Client APIs](/docs/content-as-data/data-client/), you'll want to have your tests handle mocking of `fetch` calls.
+
+This can be done by overriding `window.fetch` and providing the desired response needed based on the API being called:
```js
import { expect } from "@esm-bundle/chai";
@@ -202,7 +204,8 @@ import "./blog-posts-list.js";
// override fetch to return a promise that resolves to our mock data
window.fetch = function () {
return new Promise((resolve) => {
- resolve(new Response(JSON.stringify(graph)));
+ // this is an example of mocking out getContentByRoute
+ resolve(new Response(JSON.stringify(graph.filter((page) => page.route.startsWith("/blog/")))));
});
};