@chassis-ui/docs 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +149 -20
- package/bin/chassis-docs.js +6 -0
- package/package.json +16 -2
- package/src/cli/assets.js +111 -0
- package/src/cli/files.js +97 -0
- package/src/cli/html-validate.js +111 -0
- package/src/cli/main.js +131 -0
- package/src/cli/shared.js +69 -0
- package/src/cli/vnu.js +94 -0
- package/src/components/shortcodes/Code.astro +2 -2
- package/src/components/shortcodes/Example.astro +2 -1
- package/src/components/shortcodes/Icon.astro +3 -1
- package/src/components/shortcodes/ResizableExample.astro +2 -1
- package/src/integration.ts +6 -0
- package/src/js/example-mode.js +3 -2
- package/src/js/search.js +2 -1
- package/src/layouts/BaseLayout.astro +5 -2
- package/src/layouts/footer/Consent.astro +135 -0
- package/src/layouts/footer/Footer.astro +14 -4
- package/src/layouts/head/Favicons.astro +7 -6
- package/src/layouts/head/Head.astro +8 -2
- package/src/layouts/head/Social.astro +5 -2
- package/src/layouts/head/Stylesheet.astro +3 -2
- package/src/layouts/header/Navigation.astro +3 -2
- package/src/libs/docs-path.ts +7 -0
- package/src/libs/highlight.ts +2 -2
- package/src/libs/icon.ts +4 -4
- package/src/libs/markdown.ts +13 -1
- package/src/libs/schema.ts +11 -1
- package/src/scss/_consent.scss +14 -0
- package/src/scss/_footer.scss +11 -1
- package/src/scss/main.scss +1 -1
- package/src/site.ts +9 -1
- package/virtual.d.ts +5 -0
- package/src/layouts/head/Analytics.astro +0 -31
- package/src/scss/fonts.scss +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @chassis-ui/docs
|
|
2
2
|
|
|
3
|
+
## 0.6.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Add the `staticPath` key of `config.yml`: the URL path that the pages load the static files from, `/static` by default. A site that is served under a path of another host, such as `chassis-ui.com/css`, sets it to `/css/static` and rewrites that path to `/static`, so that its files no longer depend on the `Referer` header to be found. The layouts, the `Icon` shortcode and the scripts of the package use it, and `getStaticPath()` of `@chassis-ui/docs/site` returns a URL under it for the site's own files. See [A site under a prefix](https://github.com/chassis-ui/website/tree/main/packages/docs#a-site-under-a-prefix) in the README.
|
|
8
|
+
|
|
9
|
+
## 0.6.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- A code block can take focus, so that a keyboard can scroll one that is wider than the page. The Google Fonts stylesheet is linked from the head instead of imported by the styles, so the browser requests it with the page, not after the stylesheet that imported it.
|
|
14
|
+
- Google Analytics loads only after the visitor accepts. When `analytics.googleId` is set, production builds show a consent banner at the bottom of the page, and nothing is sent to Google and no cookie is set before the visitor accepts. The choice is kept in `localStorage`, so the sites served from one host share it. The footer has a "Cookie settings" button that opens the banner again, and a "Privacy" link to `/privacy/`. Declining stops Google Analytics and deletes its cookies.
|
|
15
|
+
- The head no longer links `safari-pinned-tab.svg` as a `mask-icon`. The docs build of chassis-assets has no such file, so every page asked for one that returned 404. Safari has used the regular favicon for pinned tabs since version 12.
|
|
16
|
+
- The header and the footer link Chassis React: "React" in the navigation of the header, "React Components" among the docs of the footer and "Chassis React" among its GitHub links. The footer lists the projects in the order of the header. The "Privacy" link and the "Cookie settings" button moved from the last column of the footer to below the license.
|
|
17
|
+
- The package installs a `chassis-docs` command with the build steps that every Chassis site shares, so that a site no longer needs its own copies of them. `chassis-docs vendor` builds the `vendor/assets` submodule at the pinned commit. `chassis-docs sync-submodules` moves it to the latest `app/docs` and builds it, and works on a fresh CI checkout too. `chassis-docs html-validate` and `chassis-docs vnu` validate the built HTML. The two validators need `html-validate` and `vnu-jar`, which are optional peer dependencies. See [Commands](https://github.com/chassis-ui/website/tree/main/packages/docs#commands) in the README.
|
|
18
|
+
- The "Skip to main content" link of the header has a target on every page. `BaseLayout` gives its `<main>` the id `content`, which only the docs layout had set, on its title. A page that fills the `main` slot of `BaseLayout` sets the id on an element of its own.
|
|
19
|
+
|
|
3
20
|
## 0.6.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -34,6 +34,8 @@ pnpm add @chassis-ui/docs
|
|
|
34
34
|
}
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
Two more are optional. They are needed by the [commands](#commands) that validate HTML: `html-validate` `^11.0.0` and `vnu-jar` `>=26.0.0`.
|
|
38
|
+
|
|
37
39
|
A site also installs `@astrojs/mdx` and the rest of the Chassis stack, `@chassis-ui/tokens` and `@chassis-ui/icons`.
|
|
38
40
|
|
|
39
41
|
## Setup
|
|
@@ -104,23 +106,24 @@ repo: 'https://github.com/chassis-ui/css'
|
|
|
104
106
|
currentVersion: '0.5.2'
|
|
105
107
|
```
|
|
106
108
|
|
|
107
|
-
| Key | Required | Meaning
|
|
108
|
-
| --------------------------------------------- | -------- |
|
|
109
|
-
| `title`, `subtitle`, `description`, `authors` | yes | Used in the page title and the meta tags
|
|
110
|
-
| `baseURL` | yes | Canonical URL of the site
|
|
111
|
-
| `docsPath` | yes | URL path of the docs pages. Starts with `/`
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
109
|
+
| Key | Required | Meaning |
|
|
110
|
+
| --------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
111
|
+
| `title`, `subtitle`, `description`, `authors` | yes | Used in the page title and the meta tags |
|
|
112
|
+
| `baseURL` | yes | Canonical URL of the site |
|
|
113
|
+
| `docsPath` | yes | URL path of the docs pages. Starts with `/` |
|
|
114
|
+
| `staticPath` | no | URL path that the pages load the static files from. Default `/static`. See [A site under a prefix](#a-site-under-a-prefix) |
|
|
115
|
+
| `repo` | yes | URL of the repository |
|
|
116
|
+
| `currentVersion` | yes | Version that the site documents. Links to source files use the tag `v<currentVersion>` |
|
|
117
|
+
| `githubOrg` | no | GitHub organisation name, not a URL. The header links to it |
|
|
118
|
+
| `figmaHandle` | no | Figma Community handle. The header links to it |
|
|
119
|
+
| `xUsername` | no | X handle. Used in the social meta tags |
|
|
120
|
+
| `analytics.googleId` | no | Google Analytics ID. In production builds, loaded after the visitor accepts in a consent banner |
|
|
121
|
+
| `anchors.min`, `anchors.max` | no | Heading levels that get an anchor link. Default 2 to 5 |
|
|
122
|
+
| `toc.min`, `toc.max` | no | Heading levels in the table of contents. Default 2 to 6 |
|
|
123
|
+
| `sourceDir` | no | Directory that `file` props are relative to, from the site's root, e.g. `"../css"`. Default `"."` |
|
|
124
|
+
| `sourcePath` | no | The same directory from the root of the repository, e.g. `"packages/css"` |
|
|
125
|
+
| `sitePath` | no | The site's root from the root of the repository, e.g. `"packages/site"` |
|
|
126
|
+
| `siteBranch` | no | The branch that "View on GitHub" links to. Default `main` |
|
|
124
127
|
|
|
125
128
|
`file` props are those of `<ScssDocs>`, `<ScssDocsSimple>`, `<JsDocs>` and `<Code filePath>`.
|
|
126
129
|
|
|
@@ -179,7 +182,7 @@ export const collections = {
|
|
|
179
182
|
|
|
180
183
|
### 4. Provide the static files
|
|
181
184
|
|
|
182
|
-
The layouts link to files under `/static/`, which the site puts into its `public` directory:
|
|
185
|
+
The layouts link to files under `/static/`, which the site puts into `static/` of its `public` directory:
|
|
183
186
|
|
|
184
187
|
| URL | From |
|
|
185
188
|
| -------------------------------------------------------------------- | ----------------------------------------- |
|
|
@@ -187,10 +190,33 @@ The layouts link to files under `/static/`, which the site puts into its `public
|
|
|
187
190
|
| `/static/icons/chassis-icons.svg`, `/static/icons/chassis-icons.css` | the `icons` folder of `@chassis-ui/icons` |
|
|
188
191
|
| `/static/images/*`: logo, favicons, social image | the docs build of `chassis-assets` |
|
|
189
192
|
|
|
193
|
+
`chassis-docs vendor` builds the docs build of `chassis-assets` in the `vendor/assets` submodule. See [Commands](#commands).
|
|
194
|
+
|
|
190
195
|
These helpers find the folders, whether `node_modules` is in the site's root or in the root of the repository: `getChassisCSSFsPath()`, `getChassisIconsFsPath()`, `getChassisAssetsFsPath()` and `getChassisTokensFsPath()`. Each takes `{ root, dir }`.
|
|
191
196
|
|
|
192
197
|
A site that type-checks with `astro check` declares the `@chassis-ui/css` module, which has no types: `declare module '@chassis-ui/css'`.
|
|
193
198
|
|
|
199
|
+
#### A site under a prefix
|
|
200
|
+
|
|
201
|
+
A site that is served under a path of another host, such as `chassis-ui.com/css`, cannot use `/static/` there: that path belongs to the host's own site. It loads its static files under a path of its own instead:
|
|
202
|
+
|
|
203
|
+
```yaml
|
|
204
|
+
# config.yml
|
|
205
|
+
staticPath: '/css/static'
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The layouts, the `Icon` shortcode and the scripts of the package then link `/css/static/css/chassis.min.css` and so on. The files do not move: they stay in `static/` of the `public` directory. Two rewrites make the path answer:
|
|
209
|
+
|
|
210
|
+
- on the host, from `/css/static/*` to `/static/*` of the site's deployment. chassis-ui.com has one for each Chassis site.
|
|
211
|
+
- in the site's own `vercel.json`, from `/css/static/(.*)` to `/static/$1`, so that the deployment also works when it is opened directly.
|
|
212
|
+
|
|
213
|
+
Two kinds of URL are the site's own to move:
|
|
214
|
+
|
|
215
|
+
- URLs that the site writes itself, in its pages and components. `getStaticPath('images/hero.png')` of `@chassis-ui/docs/site` returns the URL under `staticPath`.
|
|
216
|
+
- The files that Astro builds, when the site puts them under `static/` with `build.assets`. Astro's `build.assetsPrefix` adds the prefix to them, `'/css'` here.
|
|
217
|
+
|
|
218
|
+
A social image named in the frontmatter as `static/images/…` is read from the `public` directory and linked under `staticPath`.
|
|
219
|
+
|
|
194
220
|
### 5. Use the layouts
|
|
195
221
|
|
|
196
222
|
```astro
|
|
@@ -229,6 +255,7 @@ chassisDocs({
|
|
|
229
255
|
| `getCallout(name)` | An entry of the `callouts` collection |
|
|
230
256
|
| `getSiteRoot()`, `getSiteFsPath(file)` | Absolute path of the site's root, or of a file in it |
|
|
231
257
|
| `getPublicFsPath(file)` | Absolute path of a file in the `public` directory |
|
|
258
|
+
| `getStaticPath(file)` | URL path of a static file, under `staticPath` of the config |
|
|
232
259
|
| `getSourceFsPath(file)` | Absolute path of a file that a `file` prop names |
|
|
233
260
|
| `getSourceUrl(file)` | URL of a source file on GitHub, at the tag of the current version |
|
|
234
261
|
| `getSiteFileUrl(filePath)` | URL of a file of the site on GitHub, on the site's branch |
|
|
@@ -238,6 +265,88 @@ chassisDocs({
|
|
|
238
265
|
|
|
239
266
|
Code that runs while Astro loads its configuration uses the library functions of the root entry instead: `loadConfig()`, `loadData()`, `loadSidebar()` and the path helpers.
|
|
240
267
|
|
|
268
|
+
## Commands
|
|
269
|
+
|
|
270
|
+
The package installs a `chassis-docs` command with the build steps that every Chassis site shares. Call it from the scripts of `package.json`:
|
|
271
|
+
|
|
272
|
+
```json
|
|
273
|
+
{
|
|
274
|
+
"scripts": {
|
|
275
|
+
"vendor": "chassis-docs vendor",
|
|
276
|
+
"sync-submodules": "chassis-docs sync-submodules",
|
|
277
|
+
"lint:html": "chassis-docs html-validate _site",
|
|
278
|
+
"lint:vnu": "chassis-docs vnu _site"
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
| Command | What it does |
|
|
284
|
+
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
285
|
+
| `chassis-docs vendor` | Checks out the `vendor/assets` submodule at the commit that the repository pins, pulls its Git LFS files, installs its dependencies and runs its `pnpm assets:site` |
|
|
286
|
+
| `chassis-docs sync-submodules` | Moves `vendor/assets` to the latest commit of the `app/docs` branch and builds it. Commit the new pointer afterwards |
|
|
287
|
+
| `chassis-docs html-validate [paths]` | Validates the HTML files in `paths` with [html-validate](https://html-validate.org) |
|
|
288
|
+
| `chassis-docs vnu [paths]` | Validates `paths` with the [Nu Html Checker](https://validator.github.io/validator/). Skipped, with a warning, when Java is missing |
|
|
289
|
+
| `chassis-docs help` | Lists the commands and their options |
|
|
290
|
+
|
|
291
|
+
`vendor` and `sync-submodules` work from any folder of the repository. They need Git, Git LFS and pnpm. `sync-submodules --branch <name>`, or the `SUBMODULE_BRANCH` variable, follows another branch.
|
|
292
|
+
|
|
293
|
+
Each command exits with 1 when it fails, so a script or CI job stops there.
|
|
294
|
+
|
|
295
|
+
### Validating HTML
|
|
296
|
+
|
|
297
|
+
`html-validate` and `vnu` take any number of files and folders, `_site` when none is given. Paths are relative to the folder the script runs in, which for a site in `packages/site` is that folder when the script is in its `package.json`, and the repository root when it is in the root one.
|
|
298
|
+
|
|
299
|
+
What a site can change:
|
|
300
|
+
|
|
301
|
+
| To | Use | Validator | Adds to the defaults or replaces them |
|
|
302
|
+
| ----------------------------------- | ------------------------------------- | ------------- | ------------------------------------- |
|
|
303
|
+
| Skip a file or folder | `--ignore <path>`, repeatable | both | adds |
|
|
304
|
+
| Check `static/icons` too | `--no-default-ignore` | both | removes the default |
|
|
305
|
+
| Turn a rule off or change it | `rules` in the file of `--config` | html-validate | overrides the rule |
|
|
306
|
+
| Use more presets or plugins | `extends` in the file of `--config` | html-validate | adds |
|
|
307
|
+
| Allow custom elements or attributes | `elements` in the file of `--config` | html-validate | replaces |
|
|
308
|
+
| Leave out a message | `--filter <regex>` or `--filter-file` | vnu | adds |
|
|
309
|
+
|
|
310
|
+
The defaults:
|
|
311
|
+
|
|
312
|
+
- Both skip `static/icons` in each path, where a site copies `@chassis-ui/icons`, whose preview page is not the site's.
|
|
313
|
+
- `html-validate` extends `html-validate:recommended` and `html-validate:document`. It turns off `void-style`, since Astro writes void elements with a slash, `no-inline-style`, since Shiki writes inline styles, and `require-sri`. `heading-level` lets a page start at `h2`, as the dialogs of the layouts do.
|
|
314
|
+
- `vnu` fails on warnings as well as errors, and leaves out "Trailing slash on void elements", which Astro writes.
|
|
315
|
+
|
|
316
|
+
A site with its own exceptions keeps them in files next to its `package.json`, and names them in its scripts:
|
|
317
|
+
|
|
318
|
+
```json
|
|
319
|
+
{
|
|
320
|
+
"scripts": {
|
|
321
|
+
"lint:html": "chassis-docs html-validate _site --ignore _site/examples --config html-validate.json",
|
|
322
|
+
"lint:vnu": "chassis-docs vnu _site --ignore _site/examples --filter-file vnu-filters.txt"
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
`html-validate.json`:
|
|
328
|
+
|
|
329
|
+
```json
|
|
330
|
+
{
|
|
331
|
+
"rules": {
|
|
332
|
+
"prefer-native-element": ["error", { "exclude": ["region"] }],
|
|
333
|
+
"prefer-button": "off"
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
`vnu-filters.txt`, one regular expression per line, where a line that starts with `#` is a comment:
|
|
339
|
+
|
|
340
|
+
```text
|
|
341
|
+
# The docs show autocomplete on inputs of every type
|
|
342
|
+
Attribute “autocomplete” is only allowed when the input type is.*
|
|
343
|
+
Bad value “heading” for attribute “role” on element “summary”.
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
A filter is matched against the message as the output shows it, curly quotes included: `“role”`. A line copied from the output works.
|
|
347
|
+
|
|
348
|
+
A rule or filter applies to every file that the command checks. To make an exception for one page only, skip the page with `--ignore`, or disable the rule inside the page's markup with a comment of html-validate, such as `<!-- [html-validate-disable-next prefer-button] -->`.
|
|
349
|
+
|
|
241
350
|
## Exports
|
|
242
351
|
|
|
243
352
|
These import paths are the public API.
|
|
@@ -257,7 +366,26 @@ These import paths are the public API.
|
|
|
257
366
|
|
|
258
367
|
No other path resolves: `exports` in `package.json` lists these and nothing else. The layouts and components are listed by name, so the folders inside `layouts/` and `components/` are not reachable.
|
|
259
368
|
|
|
260
|
-
The integration provides
|
|
369
|
+
The integration provides five modules: `virtual:chassis-docs/config`, `virtual:chassis-docs/sidebar`, `virtual:chassis-docs/paths`, `virtual:chassis-docs/static` and `virtual:chassis-docs/styles`. Use the functions of `@chassis-ui/docs/site` instead of importing them.
|
|
370
|
+
|
|
371
|
+
## Compatibility
|
|
372
|
+
|
|
373
|
+
"Requires" is what `package.json` of each version declares. "Built with" is what the website in this repository resolved and built when the version was released. CI also builds the [starter site](starter/) with the newest versions that its ranges accept.
|
|
374
|
+
|
|
375
|
+
| `@chassis-ui/docs` | Requires Astro | Requires `@chassis-ui/css` | Requires Node | Built with |
|
|
376
|
+
| ------------------ | -------------------------- | -------------------------- | ------------- | ------------------------------------ |
|
|
377
|
+
| 0.6.2 | `^7.0.0` | `^0.5.0-0` | `>=22.12.0` | Astro 7.3.5, css 0.5.2, tokens 0.6.0 |
|
|
378
|
+
| 0.6.1 | `^7.0.0` | `^0.5.0-0` | `>=22.12.0` | Astro 7.3.5, css 0.5.2, tokens 0.6.0 |
|
|
379
|
+
| 0.6.0 | `^7.0.0` | `^0.5.0-0` | `>=22.12.0` | Astro 7.3.5, css 0.5.2, tokens 0.5.3 |
|
|
380
|
+
| 0.5.1 | `^7.0.0` | `^0.5.0-0` | `>=22.12.0` | Astro 7.3.5, css 0.5.2, tokens 0.5.3 |
|
|
381
|
+
| 0.5.0 | `^7.0.0` | `^0.5.0-0` | not declared | not recorded |
|
|
382
|
+
| 0.4.0 | `^7.0.0` | `^0.4.0` | not declared | not recorded |
|
|
383
|
+
| 0.3.0 to 0.3.10 | `^7.0.0` | `>=0.3.1` to `>=0.3.4` | not declared | not recorded |
|
|
384
|
+
| 0.1.0 to 0.2.0 | `^5.0.0`, 0.1.0–0.1.1 `^4` | not declared | not declared | not recorded |
|
|
385
|
+
|
|
386
|
+
`@chassis-ui/tokens` is not a dependency of the package. The styles take the design tokens that `@chassis-ui/css` carries: css 0.5 is built on tokens 0.5, css 0.4 on tokens 0.4. A site's own version of `@chassis-ui/tokens` matters only when the site puts it on the Sass load path in place of those. Tokens 0.6 renamed and removed some tokens, listed in its changelog.
|
|
387
|
+
|
|
388
|
+
Before a release, the [canary](https://github.com/chassis-ui/website/blob/main/build/canary.js) builds the site of each Chassis repository whose range accepts the new version.
|
|
261
389
|
|
|
262
390
|
## Versioning
|
|
263
391
|
|
|
@@ -270,10 +398,11 @@ A change is breaking when it changes something that this README documents:
|
|
|
270
398
|
|
|
271
399
|
- the import paths listed under [Exports](#exports), and what they export
|
|
272
400
|
- the options of the integration
|
|
401
|
+
- the commands of `chassis-docs`, their options and their defaults
|
|
273
402
|
- the keys of `config.yml`, and the schemas of the sidebar and the content collections
|
|
274
403
|
- the props and slots of the layouts, components and shortcodes
|
|
275
404
|
- the `[[config:]]` and `[[docsref:]]` syntax
|
|
276
|
-
- the URLs under `/static/` that the layouts link to
|
|
405
|
+
- the URLs under `staticPath`, `/static/` by default, that the layouts link to
|
|
277
406
|
- the supported versions of Node, Astro and `@chassis-ui/css`
|
|
278
407
|
|
|
279
408
|
Everything else may change in a patch release: the rendered HTML, class names, the content of the styles, and every file that is not reachable through a documented import path.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chassis-ui/docs",
|
|
3
3
|
"description": "Shared Astro components and layouts for Chassis documentation sites.",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"design-system"
|
|
24
24
|
],
|
|
25
25
|
"files": [
|
|
26
|
+
"bin",
|
|
26
27
|
"src",
|
|
27
28
|
"index.ts",
|
|
28
29
|
"virtual.d.ts",
|
|
@@ -31,6 +32,9 @@
|
|
|
31
32
|
"LICENSE",
|
|
32
33
|
"CHANGELOG.md"
|
|
33
34
|
],
|
|
35
|
+
"bin": {
|
|
36
|
+
"chassis-docs": "./bin/chassis-docs.js"
|
|
37
|
+
},
|
|
34
38
|
"main": "index.ts",
|
|
35
39
|
"exports": {
|
|
36
40
|
".": "./index.ts",
|
|
@@ -70,10 +74,20 @@
|
|
|
70
74
|
"@pagefind/component-ui": "^1.0.0",
|
|
71
75
|
"@shikijs/transformers": "^4.3.1",
|
|
72
76
|
"astro": "^7.0.0",
|
|
77
|
+
"html-validate": "^11.0.0",
|
|
73
78
|
"htmlparser2": "^12.0.0",
|
|
74
79
|
"rehype-autolink-headings": "^7.0.0",
|
|
75
80
|
"sass": "^1.101.0",
|
|
76
|
-
"shiki": "^4.3.1"
|
|
81
|
+
"shiki": "^4.3.1",
|
|
82
|
+
"vnu-jar": ">=26.0.0"
|
|
83
|
+
},
|
|
84
|
+
"peerDependenciesMeta": {
|
|
85
|
+
"html-validate": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"vnu-jar": {
|
|
89
|
+
"optional": true
|
|
90
|
+
}
|
|
77
91
|
},
|
|
78
92
|
"dependencies": {
|
|
79
93
|
"@types/hast": "^3.0.5",
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { CommandError, log, run as defaultRun } from './shared.js'
|
|
4
|
+
|
|
5
|
+
/** Where every Chassis site has chassis-assets as a submodule. */
|
|
6
|
+
export const ASSETS_PATH = 'vendor/assets'
|
|
7
|
+
|
|
8
|
+
/** The docs build of chassis-assets, inside the submodule. `getChassisAssetsFsPath()` reads it. */
|
|
9
|
+
export const ASSETS_OUTPUT = 'dist/web/docs/chassis'
|
|
10
|
+
|
|
11
|
+
/** The branch of chassis-assets that the docs sites follow. */
|
|
12
|
+
export const DEFAULT_BRANCH = 'app/docs'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The root of the repository that contains the working directory, so that the commands
|
|
16
|
+
* work from a site's folder too.
|
|
17
|
+
* @param {typeof defaultRun} run
|
|
18
|
+
*/
|
|
19
|
+
function repositoryRoot(run) {
|
|
20
|
+
return run('git', ['rev-parse', '--show-toplevel'], { quiet: true }).trim()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Checks out `vendor/assets` at the commit that the repository pins, and builds it.
|
|
25
|
+
* @param {{ root?: string, run?: typeof defaultRun }} [options]
|
|
26
|
+
*/
|
|
27
|
+
export function buildAssets({ run = defaultRun, root = repositoryRoot(run) } = {}) {
|
|
28
|
+
log(`Building ${ASSETS_PATH} at the pinned commit`)
|
|
29
|
+
run('git', ['submodule', 'update', '--init', ASSETS_PATH], { cwd: root })
|
|
30
|
+
buildCheckout(root, run)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Moves `vendor/assets` to the latest commit of `branch`, and builds it. The repository
|
|
35
|
+
* then has a changed submodule pointer to commit.
|
|
36
|
+
* @param {{ root?: string, branch?: string, run?: typeof defaultRun }} [options]
|
|
37
|
+
*/
|
|
38
|
+
export function syncAssets({
|
|
39
|
+
run = defaultRun,
|
|
40
|
+
root = repositoryRoot(run),
|
|
41
|
+
branch = DEFAULT_BRANCH
|
|
42
|
+
} = {}) {
|
|
43
|
+
const dir = path.join(root, ASSETS_PATH)
|
|
44
|
+
const git = (args, quiet = true) => run('git', args, { cwd: dir, quiet })
|
|
45
|
+
|
|
46
|
+
log(`Moving ${ASSETS_PATH} to the latest ${branch}`)
|
|
47
|
+
|
|
48
|
+
if (!fs.existsSync(path.join(dir, '.git'))) {
|
|
49
|
+
run('git', ['submodule', 'update', '--init', ASSETS_PATH], { cwd: root })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (git(['rev-parse', '--abbrev-ref', 'HEAD']).trim() !== branch) {
|
|
53
|
+
if (git(['status', '--porcelain']).trim()) {
|
|
54
|
+
throw new CommandError(
|
|
55
|
+
`${ASSETS_PATH} has uncommitted changes. Commit or discard them, then run the command again.`
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A fresh checkout, as on CI, has the submodule at a detached HEAD, fetched for the
|
|
60
|
+
// pinned commit only: there is neither a local branch nor a remote-tracking one.
|
|
61
|
+
git(['fetch', 'origin', branch])
|
|
62
|
+
|
|
63
|
+
if (hasLocalBranch(git, branch)) {
|
|
64
|
+
git(['checkout', branch])
|
|
65
|
+
} else {
|
|
66
|
+
git(['checkout', '-b', branch, 'FETCH_HEAD'])
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
git(['pull', '--ff-only', 'origin', branch])
|
|
71
|
+
buildCheckout(root, run)
|
|
72
|
+
|
|
73
|
+
const commit = git(['rev-parse', '--short', 'HEAD']).trim()
|
|
74
|
+
const moved = run('git', ['status', '--porcelain', '--', ASSETS_PATH], {
|
|
75
|
+
cwd: root,
|
|
76
|
+
quiet: true
|
|
77
|
+
}).trim()
|
|
78
|
+
|
|
79
|
+
if (moved) {
|
|
80
|
+
log(`${ASSETS_PATH} is at ${commit}. Check the site, then commit ${ASSETS_PATH} on its own.`)
|
|
81
|
+
} else {
|
|
82
|
+
log(`${ASSETS_PATH} is already at the latest ${branch}, ${commit}.`, 'success')
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function hasLocalBranch(git, branch) {
|
|
87
|
+
try {
|
|
88
|
+
git(['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`])
|
|
89
|
+
return true
|
|
90
|
+
} catch {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildCheckout(root, run) {
|
|
96
|
+
const dir = path.join(root, ASSETS_PATH)
|
|
97
|
+
|
|
98
|
+
// The images are stored with Git LFS. Without it they are pointer files.
|
|
99
|
+
run('git', ['lfs', 'install', '--local'], { cwd: dir, quiet: true })
|
|
100
|
+
run('git', ['lfs', 'pull'], { cwd: dir })
|
|
101
|
+
run('pnpm', ['install', '--ignore-workspace'], { cwd: dir })
|
|
102
|
+
run('pnpm', ['assets:site'], { cwd: dir })
|
|
103
|
+
|
|
104
|
+
const output = path.join(dir, ASSETS_OUTPUT)
|
|
105
|
+
|
|
106
|
+
if (!fs.existsSync(output)) {
|
|
107
|
+
throw new CommandError(`The build of ${ASSETS_PATH} wrote nothing to ${ASSETS_OUTPUT}.`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
log(`Built ${path.join(ASSETS_PATH, ASSETS_OUTPUT)}`, 'success')
|
|
111
|
+
}
|
package/src/cli/files.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { CommandError } from './shared.js'
|
|
4
|
+
|
|
5
|
+
/** The default output folder of a Chassis site. */
|
|
6
|
+
export const DEFAULT_PATHS = ['_site']
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The folders to skip when no `--ignore` is given: `static/icons` of each path, the files
|
|
10
|
+
* that a site copies from `@chassis-ui/icons`. They include a preview page of their own.
|
|
11
|
+
* @param {string[]} paths
|
|
12
|
+
*/
|
|
13
|
+
export function defaultIgnores(paths) {
|
|
14
|
+
return paths.map((dir) => path.join(dir, 'static/icons'))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether `file` is one of `ignores` or inside one of them.
|
|
19
|
+
* @param {string} file
|
|
20
|
+
* @param {string[]} ignores
|
|
21
|
+
*/
|
|
22
|
+
export function isIgnored(file, ignores) {
|
|
23
|
+
const target = path.resolve(file)
|
|
24
|
+
|
|
25
|
+
return ignores.some((ignore) => isInside(target, path.resolve(ignore)))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isInside(target, dir) {
|
|
29
|
+
const relative = path.relative(dir, target)
|
|
30
|
+
|
|
31
|
+
return relative === '' || (relative.split(path.sep)[0] !== '..' && !path.isAbsolute(relative))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function checkExists(paths) {
|
|
35
|
+
for (const entry of paths) {
|
|
36
|
+
if (!fs.existsSync(entry)) {
|
|
37
|
+
throw new CommandError(`${entry} does not exist. Build the site first.`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Lists the HTML files in `paths`, which are files or folders, leaving out `ignores` and
|
|
44
|
+
* `node_modules`.
|
|
45
|
+
* @param {string[]} paths
|
|
46
|
+
* @param {string[]} ignores
|
|
47
|
+
* @returns {string[]}
|
|
48
|
+
*/
|
|
49
|
+
export function findHtmlFiles(paths, ignores) {
|
|
50
|
+
const files = []
|
|
51
|
+
|
|
52
|
+
const visit = (entry) => {
|
|
53
|
+
if (isIgnored(entry, ignores)) return
|
|
54
|
+
|
|
55
|
+
if (fs.statSync(entry).isDirectory()) {
|
|
56
|
+
for (const name of fs.readdirSync(entry).sort()) {
|
|
57
|
+
if (name !== 'node_modules') visit(path.join(entry, name))
|
|
58
|
+
}
|
|
59
|
+
} else if (entry.endsWith('.html')) {
|
|
60
|
+
files.push(entry)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
checkExists(paths)
|
|
65
|
+
paths.forEach(visit)
|
|
66
|
+
|
|
67
|
+
return files
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns `paths` with `ignores` taken out. A folder that contains an ignored path is
|
|
72
|
+
* replaced by its entries, so that a checker that walks folders never reaches it.
|
|
73
|
+
* @param {string[]} paths
|
|
74
|
+
* @param {string[]} ignores
|
|
75
|
+
* @returns {string[]}
|
|
76
|
+
*/
|
|
77
|
+
export function expandPaths(paths, ignores) {
|
|
78
|
+
const result = []
|
|
79
|
+
|
|
80
|
+
const visit = (entry) => {
|
|
81
|
+
if (isIgnored(entry, ignores)) return
|
|
82
|
+
|
|
83
|
+
const target = path.resolve(entry)
|
|
84
|
+
const containsIgnored = ignores.some((ignore) => isInside(path.resolve(ignore), target))
|
|
85
|
+
|
|
86
|
+
if (containsIgnored && fs.statSync(entry).isDirectory()) {
|
|
87
|
+
for (const name of fs.readdirSync(entry).sort()) visit(path.join(entry, name))
|
|
88
|
+
} else {
|
|
89
|
+
result.push(entry)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
checkExists(paths)
|
|
94
|
+
paths.forEach(visit)
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import { CommandError, importPeer, log } from './shared.js'
|
|
3
|
+
import { findHtmlFiles } from './files.js'
|
|
4
|
+
|
|
5
|
+
/** What the layouts and shortcodes of the package need. A site adds its own on top. */
|
|
6
|
+
export const defaultConfig = {
|
|
7
|
+
extends: ['html-validate:recommended', 'html-validate:document'],
|
|
8
|
+
rules: {
|
|
9
|
+
// Astro writes void elements with a trailing slash
|
|
10
|
+
'void-style': 'off',
|
|
11
|
+
// Shiki highlights code with inline styles
|
|
12
|
+
'no-inline-style': 'off',
|
|
13
|
+
// The dialogs start at h2, and the `document` preset expects h1 in each sectioning root
|
|
14
|
+
'heading-level': ['error', { minInitialRank: 'h2', sectioningRoots: [] }],
|
|
15
|
+
// The layouts load their files from the site itself
|
|
16
|
+
'require-sri': 'off'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Adds a site's config to the default one: `extends` is appended, `rules` are merged,
|
|
22
|
+
* and `elements` is replaced.
|
|
23
|
+
* @param {Record<string, any>} base
|
|
24
|
+
* @param {Record<string, any>} [extra]
|
|
25
|
+
* @returns {Record<string, any>}
|
|
26
|
+
*/
|
|
27
|
+
export function mergeConfig(base, extra = {}) {
|
|
28
|
+
return {
|
|
29
|
+
...base,
|
|
30
|
+
...extra,
|
|
31
|
+
extends: [...(base.extends ?? []), ...(extra.extends ?? [])],
|
|
32
|
+
rules: { ...base.rules, ...extra.rules }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reads a config file of html-validate.
|
|
38
|
+
* @param {string} file
|
|
39
|
+
*/
|
|
40
|
+
export function readConfig(file) {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new CommandError(`Could not read ${file}: ${error.message}`, { cause: error })
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Validating hundreds of files at once runs out of file descriptors.
|
|
49
|
+
async function mapWithConcurrency(limit, items, fn) {
|
|
50
|
+
const results = []
|
|
51
|
+
const running = new Set()
|
|
52
|
+
|
|
53
|
+
for (const item of items) {
|
|
54
|
+
const promise = fn(item).finally(() => running.delete(promise))
|
|
55
|
+
|
|
56
|
+
running.add(promise)
|
|
57
|
+
results.push(promise)
|
|
58
|
+
|
|
59
|
+
if (running.size >= limit) await Promise.race(running)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Promise.all(results)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validates the HTML files in `paths` with html-validate. Returns the exit code.
|
|
67
|
+
* @param {{ paths: string[], ignores: string[], config?: string }} options
|
|
68
|
+
*/
|
|
69
|
+
export async function htmlValidate({ paths, ignores, config }) {
|
|
70
|
+
const { HtmlValidate, Severity } = await importPeer('html-validate')
|
|
71
|
+
const validator = new HtmlValidate(mergeConfig(defaultConfig, config && readConfig(config)))
|
|
72
|
+
const files = findHtmlFiles(paths, ignores)
|
|
73
|
+
|
|
74
|
+
log(`Validating ${files.length} HTML files with html-validate`)
|
|
75
|
+
|
|
76
|
+
const reports = await mapWithConcurrency(10, files, async (file) => ({
|
|
77
|
+
file,
|
|
78
|
+
report: await validator.validateFile(file)
|
|
79
|
+
}))
|
|
80
|
+
|
|
81
|
+
let issueCount = 0
|
|
82
|
+
let fileCount = 0
|
|
83
|
+
|
|
84
|
+
for (const { file, report } of reports) {
|
|
85
|
+
const messages = (report.results[0]?.messages ?? []).filter(
|
|
86
|
+
(message) => message.severity >= Severity.WARN
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if (messages.length === 0) continue
|
|
90
|
+
|
|
91
|
+
fileCount++
|
|
92
|
+
issueCount += messages.length
|
|
93
|
+
console.error(`\n${file}`)
|
|
94
|
+
|
|
95
|
+
for (const message of messages) {
|
|
96
|
+
const level = message.severity === Severity.ERROR ? 'error' : 'warning'
|
|
97
|
+
|
|
98
|
+
console.error(
|
|
99
|
+
` ${message.line}:${message.column} ${level} ${message.message} (${message.ruleId})`
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (issueCount > 0) {
|
|
105
|
+
log(`\n${issueCount} issues in ${fileCount} files.`, 'error')
|
|
106
|
+
return 1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
log('Every HTML file is valid.', 'success')
|
|
110
|
+
return 0
|
|
111
|
+
}
|