Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ jobs:
- name: Lint (anvil)
run: pnpm --filter ./apps/docs run check-anvil

# Each option the catalog lists is in its card page's tables, and each row is still one.
- name: Lint (option tables)
run: pnpm --filter ./apps/docs run check-option-tables

# Once, for both the previews and the site; `code-checks` typechecks the site.
- name: Build packages
run: pnpm run build:packages
Expand Down
3 changes: 3 additions & 0 deletions apps/docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ carries a server changeset.
- **Every example `uses:` is `<owner>/<repo>@<sha> # vX.Y.Z`**, the shape Renovate's custom manager
reads ([#114]).
- **`public/favicon-self-hosted.svg` is `favicon.svg` with blue bars**: change both.
- **`check-option-tables` holds each card page to the catalog**: every option it lists sits in an
`Option` table on the card's page or is named on `customization/common-options.md`, and every
`Option` table row is one the card takes.

## Building and checking

Expand Down
1 change: 1 addition & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"check-cards": "node scripts/generate-cards.ts --check",
"test:live": "node --conditions=@stats/source --env-file-if-exists=../../.env scripts/test-live.ts",
"check-anvil": "node --conditions=@stats/source scripts/check-anvil.ts",
"check-option-tables": "node --conditions=@stats/source scripts/check-option-tables.ts",
"test:e2e": "playwright test",
"generate-themes-page": "node --conditions=@stats/source scripts/generate-themes-page.ts",
"check-themes-page": "node --conditions=@stats/source scripts/generate-themes-page.ts --check",
Expand Down
93 changes: 93 additions & 0 deletions apps/docs/scripts/check-option-tables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env node
/**
* @file Fails when a card's page and the catalog disagree on the options the card takes.
*
* The tables are written by hand for their prose, so nothing else notices one falling behind.
*
* @returns Nothing; the process exits non-zero naming each option out of step.
*/

import { readFileSync } from 'node:fs';

import { cards } from '@stats-forge/github-stats-forge-catalog';

import { CARDS } from '../src/anvil/cards.ts';

const PAGES = new URL('../src/content/docs/docs/', import.meta.url);
const COMMON_PAGE = 'customization/common-options.md';

/** @returns The backticked names in `text`, such as both of `` `from` / `to` ``, or one given a value. */
const namesIn = (text: string): Array<string> =>
[...text.matchAll(/`(?<name>[a-z_]+)(?:=[^`]*)?`/g)].flatMap(
(match) => match.groups?.['name'] ?? [],
);

/** @returns The names in the first column of each table whose first header reads `Option`. */
const optionTableNames = (page: string): Set<string> => {
const names = new Set<string>();
let inOptionTable = false;
let header = true;
for (const line of page.split('\n')) {
if (!line.startsWith('|')) {
header = true;
inOptionTable = false;
continue;
}
const firstCell = line.split('|')[1]?.trim() ?? '';
if (header) {
inOptionTable = firstCell === 'Option';
header = false;
continue;
}
if (inOptionTable) {
for (const name of namesIn(firstCell)) {
names.add(name);
}
}
}
return names;
};

const read = (path: string): string => readFileSync(new URL(path, PAGES), 'utf8');

const common = read(COMMON_PAGE);
const commonNames = new Set(namesIn(common));
const failures: Array<string> = [];

for (const card of cards) {
const docs = CARDS.find((extra) => extra.id === card.id)?.docs;
if (docs === undefined) {
failures.push(`${card.id}: the anvil names no page for it`);
continue;
}
const path = `cards/${docs}.md`;
const documented = optionTableNames(read(path));
const taken = new Set([...card.required, ...card.options].map((option) => option.name));

for (const name of taken) {
if (!documented.has(name) && !commonNames.has(name)) {
failures.push(`${path}: \`${name}\` is not in an Option table, nor on ${COMMON_PAGE}`);
}
}
for (const name of documented) {
if (!taken.has(name)) {
failures.push(
`${path}: \`${name}\` is in an Option table, but the ${card.id} card takes no such option`,
);
}
}
}

const takenByAny = new Set(cards.flatMap((card) => card.options.map((option) => option.name)));
for (const name of optionTableNames(common)) {
if (!takenByAny.has(name)) {
failures.push(`${COMMON_PAGE}: \`${name}\` is in an Option table, but no card takes it`);
}
}

if (failures.length > 0) {
process.stderr.write(`${failures.join('\n')}\n`);
process.exitCode = 1;
} else {
process.stderr.write(` ✓ every option of ${String(cards.length)} cards is documented\n`);
}
1 change: 1 addition & 0 deletions scripts/check-all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const CHECKS: Record<string, Array<string>> = {
'graphql types': ['--filter', './packages/core', 'run', 'check-graphql-types'],
'themes page': ['--filter', './apps/docs', 'run', 'check-themes-page'],
anvil: ['--filter', './apps/docs', 'run', 'check-anvil'],
'option tables': ['--filter', './apps/docs', 'run', 'check-option-tables'],
// after `typecheck`, which leaves the CLI built for it
'card previews': ['--filter', './apps/docs', 'run', 'check-cards'],
'docs site': ['run', 'docs:build'],
Expand Down
Loading