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 += ``; -+ } -+ }); -+ -+ // 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

- -

${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/"))))); }); };