Skip to content

Commit 21c37d8

Browse files
os-zhuangclaude
andauthored
feat(lint,cli): JSX gate does full validation when a manifest is present (ADR-0080 M3b①) (#2446)
validateJsxPages accepts an optional component manifest -> compile()-based full validation (unknown component / missing-required / enum / type); without it, parse-level. os validate loads sdui.manifest.json from the project root when present. Generating+shipping that manifest from the registry public tier stays a build/CI step. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8ea1f4f commit 21c37d8

4 files changed

Lines changed: 44 additions & 4 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@objectstack/lint": minor
3+
"@objectstack/cli": minor
4+
---
5+
6+
ADR-0080 M3b① (consumption seam): the `os build` / `os validate` JSX gate now does **full component/prop validation** (unknown component, missing/wrong prop, bad enum, bindings) when a `sdui.manifest.json` is present at the project root — falling back to parse-level otherwise. `validateJsxPages` accepts an optional manifest; the validate command loads the file when present. Generating + shipping that manifest from the registry's public tier remains a build/CI step.

packages/cli/src/commands/validate.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { Args, Command, Flags } from '@oclif/core';
4+
import { existsSync, readFileSync } from 'node:fs';
5+
import { join } from 'node:path';
46
import chalk from 'chalk';
57
import { ZodError } from 'zod';
68
import { ObjectStackDefinitionSchema, normalizeStackInput } from '@objectstack/spec';
@@ -170,7 +172,18 @@ export default class Validate extends Command {
170172
// time. Parse it now so malformed source fails loudly (ADR-0078)
171173
// instead of being stored and breaking only at render.
172174
if (!flags.json) printStep('Checking JSX-source pages (ADR-0080)...');
173-
const jsxFindings = validateJsxPages(result.data as Record<string, unknown>);
175+
// Optional component manifest (ADR-0080): if the project ships a
176+
// `sdui.manifest.json` (generated from the registry's public tier), the
177+
// gate does full component/prop validation; otherwise parse-level.
178+
let sduiManifest: unknown;
179+
try {
180+
const mp = join(process.cwd(), 'sdui.manifest.json');
181+
if (existsSync(mp)) sduiManifest = JSON.parse(readFileSync(mp, 'utf8'));
182+
} catch { /* fall back to parse-level */ }
183+
const jsxFindings = validateJsxPages(
184+
result.data as Record<string, unknown>,
185+
sduiManifest ? { manifest: sduiManifest as never } : {},
186+
);
174187
const jsxErrors = jsxFindings.filter((f) => f.severity === 'error');
175188
const jsxWarnings = jsxFindings.filter((f) => f.severity === 'warning');
176189

packages/lint/src/validate-jsx-pages.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,22 @@ describe('validateJsxPages (ADR-0080 build gate)', () => {
2424
expect(validateJsxPages({ pages: [{ name: 'full', kind: 'full', regions: [] }] })).toEqual([]);
2525
});
2626
});
27+
28+
describe('validateJsxPages — full validation with a manifest', () => {
29+
const manifest = {
30+
components: {
31+
flex: { type: 'flex', namespace: 'ui', isContainer: true, inputs: [] },
32+
'object-table': { type: 'object-table', namespace: 'plugin-grid', inputs: [{ name: 'object', type: 'string', required: true }] },
33+
},
34+
};
35+
it('catches unknown components and missing required props', () => {
36+
const stack = { pages: [{ name: 'cc', kind: 'jsx', source: '<flex><object-table /><bogus /></flex>' }] };
37+
const f = validateJsxPages(stack, { manifest } as never);
38+
expect(f.some((x) => x.rule === 'jsx-missing-required-prop')).toBe(true);
39+
expect(f.some((x) => x.rule === 'jsx-forbidden-tag')).toBe(true);
40+
});
41+
it('passes when components + required props are satisfied', () => {
42+
const stack = { pages: [{ name: 'cc', kind: 'jsx', source: '<flex><object-table object="account" /></flex>' }] };
43+
expect(validateJsxPages(stack, { manifest } as never)).toEqual([]);
44+
});
45+
});

packages/lint/src/validate-jsx-pages.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// thread it through `compile()` here. Until then this catches the structural
1515
// class of error an AI author is most likely to emit.
1616

17-
import { parseJsx } from '@objectstack/sdui-parser';
17+
import { parseJsx, compile, type Manifest } from '@objectstack/sdui-parser';
1818

1919
export type JsxPageSeverity = 'error' | 'warning';
2020

@@ -32,7 +32,7 @@ export interface JsxPageFinding {
3232
type AnyRec = Record<string, unknown>;
3333
const asArray = (v: unknown): AnyRec[] => (Array.isArray(v) ? (v as AnyRec[]) : []);
3434

35-
export function validateJsxPages(stack: AnyRec): JsxPageFinding[] {
35+
export function validateJsxPages(stack: AnyRec, opts: { manifest?: Manifest } = {}): JsxPageFinding[] {
3636
const findings: JsxPageFinding[] = [];
3737
const pages = asArray(stack.pages);
3838
for (let p = 0; p < pages.length; p++) {
@@ -52,7 +52,9 @@ export function validateJsxPages(stack: AnyRec): JsxPageFinding[] {
5252
});
5353
continue;
5454
}
55-
const { diagnostics } = parseJsx(source);
55+
// With a component manifest, do full validation (unknown component, missing/
56+
// wrong prop, bad enum, bindings); without it, parse-level (syntax/structure).
57+
const { diagnostics } = opts.manifest ? compile(source, opts.manifest) : parseJsx(source);
5658
for (const d of diagnostics) {
5759
findings.push({
5860
severity: d.severity,

0 commit comments

Comments
 (0)