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
4 changes: 4 additions & 0 deletions .changeset/10088-json-fences-parse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

Docs and test-only change; no published behaviour changes. Every `json` fence under `content/docs` now survives `JSON.parse`, and a fence that annotates JSON with comments is tagged `jsonc`. The one source file touched is a test in `@object-ui/types`: `page-actions-refusal-7926.test.ts` pinned four unparseable `json` fences in `guide/layout.md` as its blind spot, and that count is zero now.
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,8 @@ The repository root also has a `docs/` directory, and it is **not** part of the

**A code block inside one of those records is a SPECIMEN, not an example to copy.** An ADR states what was decided on a date and a dated audit states what was measured on one, so a snippet inside either is part of the record — edited until it compiles, it makes the record say something it never said. Fence such a block `plaintext` (an unhighlighted spelling `scripts/check-doc-fence-languages.mjs` already lists, and one `scripts/check-doc-snippet-types.mjs` does not compile), never `ts` / `tsx` / `typescript`. Code a reader may copy belongs in `content/docs/**` or `skills/objectui/**`, where a gate compiles it and a wrong line can be fixed without falsifying a record. Maintainer ruling 2026-09-08 (objectui#8363); the four records that predate it are not edited — they are named, with their measured counts, in that gate's `UNGATED_DOCS` ledger.

**A `json` fence is what a reader copies into a metadata file, so its body must pass a strict `JSON.parse`.** A block that annotates JSON with `//` or `/* … */` comments or trailing commas is fenced `jsonc` instead, and it must still be one JSON document once those are removed. The tag is how a reader tells a deliberate annotation from a mistake, and a `json` fence that does not parse is always the mistake. The other non-parsing shapes are admitted by no JSON dialect, so rewrite them rather than retag them. A `${…}` expression goes on one line inside its string, because a raw newline in a string is invalid under both tags. A bad/good comparison becomes one fence per document, with its label on the line above. An elision (`...`, `[...]`, a `"..."` member) is removed, which leaves an empty list or the bare node. A counter-example still parses: what is wrong with it belongs in its content, not its syntax. This is the contract `parseJsonFence` in `scripts/check-skill-examples.mjs` enforces on the skills tree's marked fences. ⚠️ Nothing enforces it for `content/docs` today. `scripts/check-doc-expression-carriage.mjs` reads these fences but normalises comments, raw newlines and elisions away by design, and whether a gate should block a `json` fence that does not parse is the open decision objectui#10943.

```bash
# Start the documentation site dev server
pnpm site:dev
Expand Down
2 changes: 1 addition & 1 deletion content/docs/guide/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -qO- http://localhost:80/health.json || exit 1
```

```json
```jsonc
// railway.json (excerpt)
{
"deploy": {
Expand Down
59 changes: 27 additions & 32 deletions content/docs/guide/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ The scope the evaluator builds is what you published, plus two names the rendere

Some components provide scoped data:

```json
```jsonc
{
"type": "list",
"items": "${users}",
Expand Down Expand Up @@ -337,12 +337,7 @@ Available: All standard `Math` functions
```json
{
"type": "text",
"content": "${
status === 'active' ? 'Active' :
status === 'pending' ? 'Pending review' :
status === 'error' ? 'Failed' :
'Unknown'
}"
"content": "${status === 'active' ? 'Active' : status === 'pending' ? 'Pending review' : status === 'error' ? 'Failed' : 'Unknown'}"
}
```

Expand All @@ -351,11 +346,7 @@ Available: All standard `Math` functions
```json
{
"type": "alert",
"visibleOn": "${
(user.role === 'admin' || user.role === 'moderator') &&
user.isActive &&
!user.isSuspended
}"
"visibleOn": "${(user.role === 'admin' || user.role === 'moderator') && user.isActive && !user.isSuspended}"
}
```

Expand All @@ -364,12 +355,7 @@ Available: All standard `Math` functions
```json
{
"type": "text",
"content": "${
users
.filter(u => u.isActive)
.map(u => u.name)
.join(', ')
}"
"content": "${users.filter(u => u.isActive).map(u => u.name).join(', ')}"
}
```

Expand All @@ -380,11 +366,7 @@ Available: All standard `Math` functions
```json
{
"type": "text",
"content": "${
new Date().getHours() < 12 ? 'Good morning' :
new Date().getHours() < 18 ? 'Good afternoon' :
'Good evening'
}, ${user.firstName}!"
"content": "${new Date().getHours() < 12 ? 'Good morning' : new Date().getHours() < 18 ? 'Good afternoon' : 'Good evening'}, ${user.firstName}!"
}
```

Expand Down Expand Up @@ -526,16 +508,17 @@ evaluated on every component type:

Expressions are re-evaluated when data changes. Avoid expensive operations:

❌ Bad: Complex computation in expression

```json
// ❌ Bad: Complex computation in expression
{
"type": "text",
"content": "${users.map(u => expensiveOperation(u)).join(', ')}"
}

// ✅ Good: Pre-compute, and publish the result
```

✅ Good: Pre-compute, and publish the result

<!-- doc-snippet: fragment — the good half of a bad/good contrast: `users` and `expensiveOperation` are the reader's own rows and function, shown only to place the computation outside the expression -->

```tsx
Expand Down Expand Up @@ -670,41 +653,53 @@ language's own. A membership test is written with the array method:

### 1. Keep Expressions Simple

❌ Bad: Too complex

```json
// ❌ Bad: Too complex
{
"content": "${users.filter(u => u.age > 18).map(u => ({...u, isAdult: true})).reduce((acc, u) => acc + u.score, 0)}"
}
```

// ✅ Good: Pre-compute complex logic
✅ Good: Pre-compute complex logic

```json
{
"content": "${adultUsersScore}"
}
```

### 2. Use Meaningful Variable Names

❌ Bad

```json
// ❌ Bad
{
"visibleOn": "${x && y || z}"
}
```

// ✅ Good
✅ Good

```json
{
"visibleOn": "${isAdmin && isActive || isSuperUser}"
}
```

### 3. Handle Null/Undefined

❌ Bad: Might throw error

```json
// ❌ Bad: Might throw error
{
"content": "${user.address.city}"
}
```

// ✅ Good: Safe access
✅ Good: Safe access

```json
{
"content": "${user.address?.city || 'N/A'}"
}
Expand Down
20 changes: 10 additions & 10 deletions content/docs/guide/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,10 @@ Control page content width:
{
"type": "page",
"title": "Settings",
"maxWidth": "lg", // Centered content with max width
"maxWidth": "lg",
"children": {
"type": "form",
"fields": [...]
"fields": []
}
}
```
Expand Down Expand Up @@ -515,17 +515,17 @@ Omit `sidebar` and the content fills the width under the top bar.
{
"label": "General",
"value": "general",
"content": { "type": "form", "fields": [...] }
"content": { "type": "form", "fields": [] }
},
{
"label": "Security",
"value": "security",
"content": { "type": "form", "fields": [...] }
"content": { "type": "form", "fields": [] }
},
{
"label": "Notifications",
"value": "notifications",
"content": { "type": "form", "fields": [...] }
"content": { "type": "form", "fields": [] }
}
]
}
Expand Down Expand Up @@ -660,11 +660,11 @@ Control page content padding:
```json
{
"type": "page",
"padding": false, // Remove default padding
"padding": false,
"children": {
"type": "container",
"className": "p-8", // Custom padding
"children": [...]
"className": "p-8",
"children": []
}
}
```
Expand Down Expand Up @@ -743,10 +743,10 @@ Use constrained width for forms and reading content:
```json
{
"type": "page",
"maxWidth": "lg", // Better for forms
"maxWidth": "lg",
"children": {
"type": "form",
"fields": [...]
"fields": []
}
}
```
Expand Down
6 changes: 1 addition & 5 deletions content/docs/guide/schema-rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,11 +277,7 @@ it on `label`: `text` is not a `BadgeSchema` key.
"title": "Welcome!",
"children": {
"type": "text",
"content": "${
user.isNew ? 'Start with the quick tour.' :
user.tasks.length === 0 ? 'You are all caught up.' :
'You have tasks waiting.'
}"
"content": "${user.isNew ? 'Start with the quick tour.' : user.tasks.length === 0 ? 'You are all caught up.' : 'You have tasks waiting.'}"
}
}
```
Expand Down
15 changes: 11 additions & 4 deletions content/docs/guide/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,14 +366,21 @@ two different fields: the row fetch asks the server for one field while the
grid renders another, so the cell has nothing behind it and the relation is
never expanded.

```jsonc
// ✗ two identities on one column
✗ two identities on one column

```json
{ "field": "account", "name": "account_name" }
```

✗ legacy-only: the renderer shows it, the request used to drop it

// ✗ legacy-only: the renderer shows it, the request used to drop it
```json
{ "name": "account" }
```

✓ canonical

// ✓ canonical
```json
{ "field": "account" }
```

Expand Down
2 changes: 1 addition & 1 deletion content/docs/plugins/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ Now all plugin components are available in your schemas!

All ObjectUI plugins follow the same usage pattern:

```json
```jsonc
{
"type": "plugin-component-name",
"className": "tailwind-classes"
Expand Down
16 changes: 9 additions & 7 deletions content/docs/utilities/runner.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,10 @@ strict pnpm workspace.)
then re-run `pnpm install` from the repo root:

```json
"dependencies": {
"@object-ui/plugin-yourplugin": "workspace:*"
{
"dependencies": {
"@object-ui/plugin-yourplugin": "workspace:*"
}
}
```

Expand Down Expand Up @@ -243,7 +245,7 @@ import '@object-ui/plugin-yourplugin'

5. **Use in schemas:**

```json
```jsonc
{
"type": "your-component"
// Your component's keys go here, on the node itself — renderers read
Expand Down Expand Up @@ -293,9 +295,9 @@ Test how multiple plugins work together:
{
"type": "div",
"children": [
{ "type": "object-kanban", "..." },
{ "type": "bar-chart", "..." },
{ "type": "data-table", "..." }
{ "type": "object-kanban" },
{ "type": "bar-chart" },
{ "type": "data-table" }
]
}
```
Expand Down Expand Up @@ -493,7 +495,7 @@ ComponentRegistry.register('my-component', MyComponent)
Add a page document to `src/app-data/pages/`. The file name is the route: this one is
served at `/my-page`.

```json
```jsonc
// src/app-data/pages/my-page.json
{
"type": "page",
Expand Down
2 changes: 1 addition & 1 deletion content/docs/utilities/vscode-extension.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ Hover over properties to see:

Real-time validation with error messages:

```json
```jsonc
{
"type": "button",
"invalidProp": true // ❌ Error: Unknown property
Expand Down
20 changes: 9 additions & 11 deletions packages/types/src/__tests__/page-actions-refusal-7926.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ describe('objectui#7926 — the guide no longer authors `actions` on a `page` no
try {
doc = JSON.parse(m[1]);
} catch {
doc = null; // elided fragment (`[...]`), counted as blind below
doc = null; // a fence that does not parse, counted as blind below
}
out.push({ line, doc, body: m[1] });
}
Expand Down Expand Up @@ -221,17 +221,15 @@ describe('objectui#7926 — the guide no longer authors `actions` on a `page` no
// requirement, carried into this card by its dispatch). The COUNT is pinned,
// not the line numbers — a line list would redden on every unrelated edit
// above it, and a permanently red pin is one nobody reads.
//
// The count was four — the page's `[...]` elisions — until objectui#10088 made
// every `json` fence on the page parse. It is zero now, so every `page` node
// the guide authors in a `json` fence is judged above, and a new fence that
// does not parse is red here rather than a fifth blind entry. ⚠️ This reader
// visits `json` only: a `jsonc` fence on this page would sit OUTSIDE its
// population, not inside this count.
const blind = fences.filter((f) => f.doc === null);
expect(blind).toHaveLength(4);
// …and each is blind for the DECLARED reason, an author's `[...]` elision —
// never because a real document stopped parsing. That is the half that makes
// the count above mean something.
for (const f of blind) {
expect({ line: f.line, elided: f.body.includes('...') }).toEqual({
line: f.line,
elided: true,
});
}
expect(blind.map((f) => `${GUIDE_PATH}:${f.line}`)).toEqual([]);
});

it('every `page` node the guide teaches declares its content under `children`', () => {
Expand Down
Loading