Skip to content

Commit 39cb71b

Browse files
committed
chore: verify component index freshness in CI
1 parent d53eaca commit 39cb71b

3 files changed

Lines changed: 91 additions & 10 deletions

File tree

.github/workflows/checks.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
secrets: inherit
2323
with:
2424
cacheKey: ${{ github.sha }}
25-
checkout_paths: packages apps scripts .github
25+
checkout_paths: packages apps scripts .github documentation
2626
packages:
2727
name: Process packages
2828
runs-on: ubuntu-latest
@@ -48,6 +48,11 @@ jobs:
4848
- name: Fallback install (cache miss)
4949
if: steps.setup-cache.outputs.cache-hit != 'true'
5050
run: pnpm install
51+
# Must run BEFORE the build: `prebuild` regenerates the index in the
52+
# workspace, which would mask a stale committed file.
53+
- name: Check AI component index freshness
54+
id: check-component-index
55+
run: pnpm run generate:component-index --check
5156
- name: Build packages
5257
id: build-packages
5358
run: pnpm run build

documentation/AI-COMPONENT-INDEX.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ This file is the canonical "what already exists" reference for AI coding assista
99

1010
- Source path: `packages/eds-core-react/src/components/next/<Component>/`
1111
- Top-level barrel: `packages/eds-core-react/src/components/next/index.ts`
12-
- Components: 22 • Props documented: 93 • With JSDoc description: 1
12+
- Components: 23 • Props documented: 96 • With JSDoc description: 1
1313

1414
## Components
1515

@@ -22,6 +22,7 @@ This file is the canonical "what already exists" reference for AI coding assista
2222
| Button || asChild, icon, multiline, round, size, tone, variant ||| active |
2323
| Checkbox || disabled, helperMessage, indeterminate, indicator, label ||| active |
2424
| Chip || dropdown, onDelete, selected, tone, variant ||| active |
25+
| Dialog || onOpenChange, open, scrim | Dialog.Header, Dialog.Title, Dialog.Content, Dialog.Actions || active |
2526
| Divider ||||| active |
2627
| Field || disabled, position | Field.Label, Field.Description, Field.HelperMessage || active |
2728
| Icon | Icon component for EDS 2.0 | color, data, size, title ||| active |
@@ -41,7 +42,7 @@ This file is the canonical "what already exists" reference for AI coding assista
4142
## Field reference
4243

4344
- **Description** — first paragraph of the JSDoc above the component's `forwardRef` declaration (also tries `<Name>Component` / `<Name>Root` for compound components), with the `<Name>Props` type alias as fallback. `` means no JSDoc is present yet; consider adding one.
44-
- **Props** — EDS-defined props only, from the `<Name>Props` type literal. Intersected HTML attributes (`HTMLAttributes<...>`, `InputHTMLAttributes<...>`, etc.) and React-conventional props (`children`, `className`, `style`, `ref`, `key`) are NOT listed — assume the standard DOM props for the underlying element are available.
45+
- **Props** — EDS-defined props only, from the `<Name>Props` type literal. Intersected HTML attributes (`HTMLAttributes<...>`, `InputHTMLAttributes<...>`, etc.) and React-conventional props (`children`, `className`, `style`, `ref`, `key`) are NOT listed — assume the standard DOM props for the underlying element are available. Known limitation: only inline type literals and intersections are walked — props coming from a referenced local type alias (`type FooProps = SharedBase & {...}`) or a union type are not expanded.
4546
- **Sub-components** — compound sub-components attached via the `Compound<Name>` type pattern (e.g. `Field.Label`, `Banner.Icon`). Standalone components exported from the same directory (e.g. `MenuItem` from `./Menu`) appear as their own rows.
4647
- **asChild**`` if the component supports the `asChild` polymorphism pattern (see `Slot/README.md`).
4748
- **Status**`active` unless the props type carries an `@deprecated` JSDoc tag.

scripts/generate-component-index.js

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@
99
* (the top-level barrel — what consumers can actually import).
1010
*
1111
* Re-run via `pnpm run generate:component-index`. Also runs as part of
12-
* `prebuild`, so any `pnpm run build` keeps the file fresh.
12+
* `prebuild`, so a root `pnpm run build` keeps the file fresh (note:
13+
* `pnpm run build:core-react` does NOT — pre-hooks only fire for the exact
14+
* script name `build`).
15+
*
16+
* `--check` regenerates in memory and exits 1 if the committed file is
17+
* stale, naming the affected components. Wired into the Checks workflow so
18+
* a PR that changes the /next API surface without regenerating fails CI.
1319
*/
1420

1521
const fs = require('fs')
@@ -272,7 +278,7 @@ function renderTable(rows) {
272278
return [header, sep, ...body].join('\n')
273279
}
274280

275-
function generate() {
281+
function buildContent() {
276282
const publicComponents = discoverPublicComponents()
277283
const rows = publicComponents
278284
.map(extractComponentData)
@@ -301,7 +307,7 @@ ${renderTable(rows)}
301307
## Field reference
302308
303309
- **Description** — first paragraph of the JSDoc above the component's \`forwardRef\` declaration (also tries \`<Name>Component\` / \`<Name>Root\` for compound components), with the \`<Name>Props\` type alias as fallback. \`—\` means no JSDoc is present yet; consider adding one.
304-
- **Props** — EDS-defined props only, from the \`<Name>Props\` type literal. Intersected HTML attributes (\`HTMLAttributes<...>\`, \`InputHTMLAttributes<...>\`, etc.) and React-conventional props (\`children\`, \`className\`, \`style\`, \`ref\`, \`key\`) are NOT listed — assume the standard DOM props for the underlying element are available.
310+
- **Props** — EDS-defined props only, from the \`<Name>Props\` type literal. Intersected HTML attributes (\`HTMLAttributes<...>\`, \`InputHTMLAttributes<...>\`, etc.) and React-conventional props (\`children\`, \`className\`, \`style\`, \`ref\`, \`key\`) are NOT listed — assume the standard DOM props for the underlying element are available. Known limitation: only inline type literals and intersections are walked — props coming from a referenced local type alias (\`type FooProps = SharedBase & {...}\`) or a union type are not expanded.
305311
- **Sub-components** — compound sub-components attached via the \`Compound<Name>\` type pattern (e.g. \`Field.Label\`, \`Banner.Icon\`). Standalone components exported from the same directory (e.g. \`MenuItem\` from \`./Menu\`) appear as their own rows.
306312
- **asChild** — \`✓\` if the component supports the \`asChild\` polymorphism pattern (see \`Slot/README.md\`).
307313
- **Status** — \`active\` unless the props type carries an \`@deprecated\` JSDoc tag.
@@ -314,12 +320,81 @@ ${renderTable(rows)}
314320
- Storybook links
315321
`
316322

323+
return { content, rows }
324+
}
325+
326+
/**
327+
* Extract `componentName -> full table row` from generated markdown, so a
328+
* failed --check can say which components are affected instead of just
329+
* "files differ".
330+
*/
331+
function tableRowsByName(markdown) {
332+
const rows = new Map()
333+
for (const line of markdown.split('\n')) {
334+
const match = /^\| ([A-Za-z0-9]+) \| /.exec(line)
335+
if (match && match[1] !== 'Component') {
336+
rows.set(match[1], line)
337+
}
338+
}
339+
return rows
340+
}
341+
342+
function reportStaleness(committed, generated) {
343+
const committedRows = tableRowsByName(committed)
344+
const generatedRows = tableRowsByName(generated)
345+
346+
const missing = [...generatedRows.keys()].filter(
347+
(name) => !committedRows.has(name),
348+
)
349+
const removed = [...committedRows.keys()].filter(
350+
(name) => !generatedRows.has(name),
351+
)
352+
const changed = [...generatedRows.keys()].filter(
353+
(name) =>
354+
committedRows.has(name) &&
355+
committedRows.get(name) !== generatedRows.get(name),
356+
)
357+
358+
if (missing.length > 0) {
359+
console.error(` Missing from the index: ${missing.join(', ')}`)
360+
}
361+
if (removed.length > 0) {
362+
console.error(` No longer exported: ${removed.join(', ')}`)
363+
}
364+
if (changed.length > 0) {
365+
console.error(` Rows out of date: ${changed.join(', ')}`)
366+
}
367+
if (missing.length + removed.length + changed.length === 0) {
368+
console.error(' Table rows match — header, stats, or prose changed.')
369+
}
370+
}
371+
372+
function main() {
373+
const check = process.argv.includes('--check')
374+
const { content, rows } = buildContent()
375+
const relativePath = path.relative(rootDir, outputPath)
376+
377+
if (check) {
378+
const committed = fs.existsSync(outputPath)
379+
? fs.readFileSync(outputPath, 'utf8')
380+
: ''
381+
if (committed === content) {
382+
console.log(`${relativePath} is up to date (${rows.length} components).`)
383+
return
384+
}
385+
console.error(`${relativePath} is stale.`)
386+
reportStaleness(committed, content)
387+
console.error(
388+
' Run `pnpm run generate:component-index` and commit the result.',
389+
)
390+
process.exitCode = 1
391+
return
392+
}
393+
317394
fs.mkdirSync(path.dirname(outputPath), { recursive: true })
318395
fs.writeFileSync(outputPath, content)
319396

320-
console.log(
321-
`Wrote ${path.relative(rootDir, outputPath)} (${rows.length} components).`,
322-
)
397+
console.log(`Wrote ${relativePath} (${rows.length} components).`)
323398
}
324399

325-
generate()
400+
main()

0 commit comments

Comments
 (0)