diff --git a/.changeset/jsonc-fence-extractor-test-only.md b/.changeset/jsonc-fence-extractor-test-only.md new file mode 100644 index 0000000000..03c7c1f195 --- /dev/null +++ b/.changeset/jsonc-fence-extractor-test-only.md @@ -0,0 +1,7 @@ +--- +--- + +Test-only change in `@object-ui/components`: the skill-guide fence extractor in +`skill-guide-data-table-binding.test.tsx` now reads `jsonc` fences as well as +`json`, so retagging a comment-carrying guide example no longer hides it from +every assertion in that file. No published behaviour changes. diff --git a/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx b/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx index 421bca4d63..76fdb2814e 100644 --- a/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx +++ b/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx @@ -91,10 +91,16 @@ function readGuide(rel: string): string { return fs.readFileSync(path.join(repoRoot, rel), 'utf8'); } -/** Every fenced ```json block body in a markdown file, in document order. */ +/** + * Every fenced ```json OR ```jsonc block body in a markdown file, in document + * order. Both tags are read on purpose: the guides tag their comment-carrying + * examples ```jsonc, and `parseBlock` below already strips those `//` comments + * before parsing. Reading only ```json would let a retag silently hide an + * example from every assertion here — which is a pass, not a failure. + */ function jsonBlocks(md: string): string[] { const out: string[] = []; - const fence = /```json\n([\s\S]*?)```/g; + const fence = /```jsonc?\n([\s\S]*?)```/g; let m: RegExpExecArray | null; while ((m = fence.exec(md)) !== null) out.push(m[1]); return out; diff --git a/skills/objectui/guides/schema-expressions.md b/skills/objectui/guides/schema-expressions.md index caecb6660b..3773413fc1 100644 --- a/skills/objectui/guides/schema-expressions.md +++ b/skills/objectui/guides/schema-expressions.md @@ -29,7 +29,7 @@ full boundary tables -- the evaluated fields, the raw ones, `visible` over -- are in [`rules/protocol.md`](../rules/protocol.md), which is the anchor for this rule. The four cases that decide most schemas: -```json +```jsonc // Evaluated, then dropped -- renders an empty card { "type": "card", "props": { "title": "${data.customer.name}" } } @@ -167,7 +167,7 @@ name, so calls are case-insensitive. Each condition field has two forms — a shorthand and an `On` suffix: -```json +```jsonc { "hidden": true } // static boolean { "hidden": "${data.role !== 'admin'}" } // template expression { "hiddenOn": "data.role !== 'admin'" } // raw expression (no ${} needed) @@ -325,7 +325,7 @@ what reaches the screen. `list` is the component authors reach for first, and it is **data-as-nodes**: the array it renders *is* the node list. It never reads `children`. -```json +```jsonc // ❌ Renders two EMPTY
  • . `children` is not a template — `list` never reads it, // and `${item.name}` would render literally even if it did. { @@ -352,7 +352,7 @@ descriptor: section exists to close: binding `list` to ordinary records produces one empty `
  • ` per record — the right number of bullets, no text in any of them. -```json +```jsonc // ✅ Authored items — `title` and `ordered` are read off the node { "type": "list", @@ -362,7 +362,7 @@ section exists to close: binding `list` to ordinary records produces one empty } ``` -```json +```jsonc // ✅ Bound data, already node-shaped: dataSource = { rows: [{ "content": "Ada" }, { "content": "Linus" }] } { "type": "list", "bind": "rows" } ``` @@ -371,7 +371,7 @@ Only `body` entries go back through `SchemaRenderer`, so they are the one place inside a list where expressions are evaluated at all — against the host scope, never against a current element: -```json +```jsonc // ✅ `${data.*}` works inside `body`; there is still no `${item.*}` { "type": "list", @@ -388,7 +388,7 @@ never against a current element: accessors (`accessorKey`). Cell values are plain property lookups — never expressions — and `table` does not read `bind`. -```json +```jsonc // ✅ `table`: inline rows + column accessors, no per-row scope { "type": "table", @@ -444,7 +444,7 @@ Expressions are compiled once per unique `(expression, variableNames)` pair and **Cause:** The field isn't expression-evaluated. Use `content`. -```json +```jsonc // ❌ Won't evaluate — `value` is read but never templated { "type": "text", "value": "${data.total}" } @@ -460,7 +460,7 @@ Expressions are compiled once per unique `(expression, variableNames)` pair and **Cause 1:** `visible` is also set and takes priority. **Cause 2:** Expression returns a non-boolean truthy value — use explicit comparison. -```json +```jsonc // ❌ Truthy but not boolean { "hidden": "${data.count}" } @@ -476,7 +476,7 @@ is **not** blocked — measured through the schema path, it returns a real `Date`. Prefer a formula function anyway: a `Date` object stringifies into the DOM as a locale-dependent blob. -```json +```jsonc // ✅ Works, but renders "Thu Jan 01 1970 …" { "type": "text", "content": "${new Date(data.timestamp)}" } @@ -489,7 +489,7 @@ the DOM as a locale-dependent blob. ### Object literal in expression -```json +```jsonc // ❌ Object literals not supported { "type": "text", "style": "${{ color: 'red' }}" } diff --git a/skills/objectui/rules/protocol.md b/skills/objectui/rules/protocol.md index 5e6a201e97..5ea19e06fe 100644 --- a/skills/objectui/rules/protocol.md +++ b/skills/objectui/rules/protocol.md @@ -154,7 +154,7 @@ key under `props` never reaches a `ui:*` / `page:*` renderer at all. **❌ FORBIDDEN:** Adding custom properties not defined in `@objectstack/spec`. **Example violation:** -```json +```jsonc { "type": "data-table", "fields": [...], // ❌ spec uses "columns" @@ -163,7 +163,7 @@ key under `props` never reaches a `ui:*` / `page:*` renderer at all. ``` **✅ CORRECT:** -```json +```jsonc { "type": "data-table", "columns": [...] // ✅ declared by DataTableSchema, read off the node @@ -174,7 +174,7 @@ key under `props` never reaches a `ui:*` / `page:*` renderer at all. When the entire string is a single `${expression}`, the result preserves its type: -```json +```jsonc "${data.count}" // → returns number 42, not string "42" "${data.isActive}" // → returns boolean true, not string "true" "Count: ${data.count}" // → returns string "Count: 42" (mixed template) @@ -184,7 +184,7 @@ When the entire string is a single `${expression}`, the result preserves its typ The `bind` field is NOT expression-evaluated. It's a path string resolved by `useDataScope()`, and only a component that calls that hook reads it: -```json +```jsonc { "type": "list", "bind": "customerNames" // Resolved as dataSource.customerNames diff --git a/skills/objectui/rules/styling.md b/skills/objectui/rules/styling.md index fe511a6f52..06654f354a 100644 --- a/skills/objectui/rules/styling.md +++ b/skills/objectui/rules/styling.md @@ -167,7 +167,7 @@ const buttonVariants = cva( **Every component must accept `className` on its schema node** to allow JSON-level style overrides. Like every other key, it is read off the node itself — not out of a `props` envelope, which the renderers never read: -```json +```jsonc { "type": "card", "className": "bg-red-500", // ✅ User can override styles