Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/doc-component-types-md-surface.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
---
---

`scripts/check-doc-component-types.mjs` now collects `.md` pages under `content/docs`
alongside `.mdx`, converging on the `DOC_EXTENSIONS` spelling
`scripts/check-doc-snippet-types.mjs` already uses. No published package changes: the
diff is the gate script, its test, its workflow comments, and six documentation pages.
13 changes: 7 additions & 6 deletions .github/workflows/doc-component-types.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,8 +5,9 @@ name: Doc Component Types
# precisely the shape both of those workflows skip. `ci.yml`'s `type-check` job
# decides whether to run its expensive steps with a `git diff` that excludes
# `content/**`, `'**/*.md'`, `docs/**` and `apps/site/**` — so a PR that edits
# only `content/docs/**.mdx` reports the context and runs none of the gates
# inside it. A gate against a wrong `type` in a teaching snippet, wired there,
# only `content/docs/**` reports the context and runs none of the gates inside
# it. Note the `'**/*.md'` term especially: since objectui#5342 this gate reads
# the `.md` guides too, which is exactly the extension that diff excludes. A gate against a wrong `type` in a teaching snippet, wired there,
# would be blind to every change that can introduce one.
#
# This is the fifth instance of the shape in this repo and the reasoning is
Expand All@@ -23,9 +24,9 @@ name: Doc Component Types
# gate, one home.
#
# It needs no install and no build. The script reads the checkout with `node:fs`
# only: 143 mdx files for the snippets, and the `packages/` + `apps/` sources for
# the registered-key universe it compares them against. A few seconds. Keep it
# that way if you add checks to it — the moment this needs `pnpm install` it
# only: 183 pages for the snippets (143 `.mdx` + 40 `.md`, objectui#5342), and
# the `packages/` + `apps/` sources for the registered-key universe it compares
# them against. A few seconds. Keep it that way if you add checks to it — the moment this needs `pnpm install` it
# stops being cheap enough to run unfiltered, and the filter is the hole.

on:
Expand DownExpand Up@@ -65,7 +66,7 @@ jobs:
with:
node-version: '22.x'

# A `type` string in a `content/docs/**.mdx` code block is not rendered,
# A `type` string in a `content/docs/**` code block is not rendered,
# not parsed and not compared against anything, so a snippet can name a
# component that does not exist and every check in the repo stays green —
# while a reader who copies it gets the renderer's red "Unknown component
Expand Down
4 changes: 2 additions & 2 deletions content/docs/api/schema-reference.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -680,7 +680,7 @@ A single-record detail view with grouped fields, actions, and tabs.
"title": "Customer Info",
"fields": [
{ "name": "customer", "label": "Customer", "type": "text" },
{ "name": "email", "label": "Email", "type": "link" },
{ "name": "email", "label": "Email", "type": "email" },
{ "name": "created", "label": "Created", "type": "date", "format": "MMM d, yyyy" }
]
},
Expand DownExpand Up@@ -1109,7 +1109,7 @@ An enhanced detail view for a single record with sections, tabs, related records
"fields": [
{ "name": "firstName", "label": "First Name", "type": "text" },
{ "name": "lastName", "label": "Last Name", "type": "text" },
{ "name": "email", "label": "Email", "type": "link" },
{ "name": "email", "label": "Email", "type": "email" },
{ "name": "avatar", "label": "Photo", "type": "image" }
]
}
Expand Down
10 changes: 5 additions & 5 deletions content/docs/guide/building-crud-app.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,7 +209,7 @@ function App() {
<div className="min-h-screen bg-background p-6">
<SchemaRenderer
schema={{
type: 'ObjectGrid',
type: 'object-grid',
object: 'task',
view: 'all',
data: { objectSchema: TaskSchema },
Expand DownExpand Up@@ -238,15 +238,15 @@ Add a "New Task" button and handle row clicks to open the edit form:

```tsx
<SchemaRenderer
schema={{ type: 'ObjectGrid', object: 'task', view: 'all', data: { objectSchema: TaskSchema } }}
schema={{ type: 'object-grid', object: 'task', view: 'all', data: { objectSchema: TaskSchema } }}
dataSource={dataSource}
onRowClick={(row: any) => { setEditId(row.id); setShowForm(true); }}
/>

{showForm && (
<SchemaRenderer
schema={{
type: 'ObjectForm',
type: 'object-form',
object: 'task',
mode: editId ? 'edit' : 'create',
recordId: editId,
Expand DownExpand Up@@ -290,7 +290,7 @@ const [searchQuery, setSearchQuery] = useState('');
// Grid responds to view and search changes
<SchemaRenderer
schema={{
type: 'ObjectGrid',
type: 'object-grid',
object: 'task',
view: activeView,
data: {
Expand All@@ -317,7 +317,7 @@ function TaskDetail({ taskId, onBack }: { taskId: string; onBack: () => void })
</button>
<SchemaRenderer
schema={{
type: 'ObjectDetail',
type: 'detail-view',
object: 'task',
recordId: taskId,
data: { objectSchema: TaskSchema },
Expand Down
2 changes: 1 addition & 1 deletion content/docs/guide/expressions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -371,7 +371,7 @@ Available: All standard `Math` functions

```json
{
"type": "empty-state",
"type": "empty",
"visibleOn": "${items.length === 0}",
"message": "No items to display",
"description": "Start by adding your first item"
Expand Down
4 changes: 2 additions & 2 deletions content/docs/guide/schema-playground.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -208,7 +208,7 @@ Schemas can be nested to build complex layouts. Here is a dashboard that combine
"type": "page",
"title": "Project Dashboard",
"body": {
"type": "grid-layout",
"type": "grid",
"columns": 3,
"gap": "md",
"items": [
Expand DownExpand Up@@ -280,7 +280,7 @@ const schema = {
- **Start simple** — Begin with a single `button` or `text` schema and add complexity incrementally.
- **Use `className`** — Any schema object accepts a `className` property for Tailwind utility classes.
- **Check the `type`** — If nothing renders, verify the `type` value matches a registered component.
- **Nest schemas** — Use container types like `stack`, `grid-layout`, and `page` to compose multiple components.
- **Nest schemas** — Use container types like `stack`, `grid`, and `page` to compose multiple components.
- **Add expressions** — Use `visibleOn` and `disabledOn` for dynamic behavior: `"visibleOn": "${data.showAdvanced}"`.

## Related Resources
Expand Down
2 changes: 1 addition & 1 deletion content/docs/guide/schema-rendering.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -387,7 +387,7 @@ Always type your schemas for better IDE support and fewer runtime errors.

```json
{
"type": "empty-state",
"type": "empty",
"visibleOn": "${items.length === 0}",
"message": "No items found",
"action": {
Expand Down
86 changes: 83 additions & 3 deletions scripts/__tests__/check-doc-component-types.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,8 +13,8 @@ import { analyze, deriveRegistryKeys, scanDocs } from '../check-doc-component-ty
* objectui#4823 — the test for `scripts/check-doc-component-types.mjs`.
*
* The gate answers one question: does every `type` string literal in a
* `content/docs/**.mdx` code block name a component this repository actually
* registers. Nothing rendered or parsed those snippets before it, so the same
* `content/docs/**` code block — `.mdx` and `.md` alike — name a component this
* repository actually registers. Nothing rendered or parsed those snippets before it, so the same
* defect landed three times (objectui#4786 `stats-card`, objectui#4796
* `plugin:grid` and `plugin:map`) and CI was green through all three.
*
Expand DownExpand Up@@ -253,6 +253,31 @@ describe('the docs scan reads code blocks, in both spellings, and only code bloc
expect(sites).toEqual([]);
});

it('collects `.md` pages as well as `.mdx` — the extension is not a coverage decision', () => {
// objectui#5342. The collector used to walk `.mdx` only, so 40 `.md` guides
// under the SAME tree were neither judged nor declared. This asserts the
// walk, not the verdict: revert `DOC_EXTENSIONS` to `['.mdx']` and the
// `from-md` site disappears while every other test in this file stays green.
const { sites, counters } = withTree((write) => {
write('content/docs/a.mdx', ['```json', '{ "type": "from-mdx" }', '```'].join('\n'));
write('content/docs/guide/b.md', ['```json', '{ "type": "from-md" }', '```'].join('\n'));
// Not a page: the `meta.json` sidecars fumadocs keeps beside the prose.
write('content/docs/meta.json', '{ "pages": ["a"] }');
}, (dir) => scanDocs(dir));
expect(sites.map((s) => s.value).sort()).toEqual(['from-md', 'from-mdx']);
expect(counters.files).toBe(2);
});

it('judges a `.md` page by the same rule, so an unregistered type there is a finding', () => {
const { findings } = withTree((write) => {
write('packages/demo/src/index.tsx', "ComponentRegistry.register('div', C, { namespace: 'ui' });\n");
write('content/docs/guide/b.md', ['```json', '{ "type": "not-a-component" }', '```'].join('\n'));
}, (dir) => analyze(dir, BARE));
const f = findings as Finding[];
expect(f.map((x) => x.reason)).toContain('unregistered-doc-type');
expect(f.find((x) => x.reason === 'unregistered-doc-type')?.site).toBe('content/docs/guide/b.md:2');
});

it('reports an unterminated fence rather than guessing where code stops', () => {
const { findings } = withTree((write) => {
write('content/docs/x.mdx', ['```json', '{ "type": "div" }'].join('\n'));
Expand DownExpand Up@@ -367,6 +392,18 @@ describe('the scan cannot collapse quietly', () => {
expect(counters.registered).toBeGreaterThan(400);
});

it('really walks the `.md` half of this tree — the objectui#5342 widening, pinned', () => {
// A repo-level assertion because the fixture above proves only the
// mechanism. `content/docs` holds 143 `.mdx` and 40 `.md`; a revert to
// `.mdx`-only drops ~323 `type` literals out of the scan and this repository
// stays green while judging none of them.
const { sites, counters } = scanDocs(repoRoot);
const mdFiles = new Set(sites.filter((s: { file: string }) => s.file.endsWith('.md')).map((s: { file: string }) => s.file));
expect(mdFiles.size, 'no `.md` page carries a scanned `type` literal — the collector narrowed').toBeGreaterThan(15);
expect(mdFiles.has('content/docs/api/schema-reference.md')).toBe(true);
expect(counters.files).toBeGreaterThan(170);
});

it('this repository is green', () => {
const findings = analyze(repoRoot).findings as Finding[];
expect(findings.map((f) => `${f.reason} :: ${f.site} :: ${f.value ?? ''}`)).toEqual([]);
Expand DownExpand Up@@ -408,6 +445,49 @@ describe('the three snippets this gate found on its first run stay fixed', () =>

// ── 6. the wiring ────────────────────────────────────────────────────────────

describe('objectui#5342 — the key errors the widened collector found stay fixed', () => {
// Named rather than left to the repo-wide green assertion, for the same reason
// the block above names its three: these are the live specimens the extension
// widening produced, and a revert would otherwise read as an unrelated
// regression somewhere in a 183-file scan. Each one rendered the OBJUI-001
// "Unknown component type" panel for a reader who copied it.
const read = (rel: string) => fs.readFileSync(path.join(repoRoot, rel), 'utf8');

it('the CRUD guide spells the registered keys, not the PascalCase component names', () => {
const body = read('content/docs/guide/building-crud-app.md');
for (const wrong of ['ObjectGrid', 'ObjectForm', 'ObjectDetail']) {
expect(body, `${wrong} is a component NAME; the registry key is lower-kebab`).not.toContain(
`type: '${wrong}'`,
);
}
expect(body).toContain("type: 'object-grid'");
expect(body).toContain("type: 'object-form'");
expect(body).toContain("type: 'detail-view'");
});

it('the two empty-state snippets spell `empty`, the key EmptySchema declares', () => {
// packages/types/src/feedback.ts declares `type: 'empty'` and
// packages/components/src/renderers/feedback/empty.tsx registers it.
for (const rel of ['content/docs/guide/expressions.md', 'content/docs/guide/schema-rendering.md']) {
expect(read(rel), `${rel} still teaches empty-state`).not.toContain('"type": "empty-state"');
expect(read(rel)).toContain('"type": "empty"');
}
});

it('the playground teaches `grid`, in the fenced snippet AND in the prose beside it', () => {
const body = read('content/docs/guide/schema-playground.md');
expect(body, 'nothing registers grid-layout').not.toContain('grid-layout');
expect(body).toContain('"type": "grid"');
});

it('the schema reference gives its Email field a field type that exists', () => {
// `link` is not in fieldWidgetMap; `email` and `url` are.
const body = read('content/docs/api/schema-reference.md');
expect(body).not.toContain('"label": "Email", "type": "link"');
expect(body).toContain('"label": "Email", "type": "email"');
});
});

describe('wiring — the gate is reachable and a docs-only PR starts it', () => {
const workflowDir = path.join(repoRoot, '.github/workflows');
const workflowPath = path.join(workflowDir, 'doc-component-types.yml');
Expand DownExpand Up@@ -444,7 +524,7 @@ describe('wiring — the gate is reachable and a docs-only PR starts it', () =>
it('runs it in NO path-filtered workflow — the change that breaks it is docs-only', () => {
// The whole reason this is its own workflow. `ci.yml`'s type-check job
// excludes `content/**` from the diff that decides whether its gates run, so
// a PR editing only `content/docs/**.mdx` would start this gate nowhere.
// a PR editing only `content/docs/**` would start this gate nowhere.
expect(workflowFiles.length, 'the workflow directory scan returned implausibly few files').toBeGreaterThan(5);
for (const file of workflowFiles) {
const yaml = yamlOf(file);
Expand Down
Loading
Loading