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
6 changes: 6 additions & 0 deletions .changeset/sdui-jsx-gate-manifest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@objectstack/lint": minor
"@objectstack/cli": minor
---

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.
15 changes: 14 additions & 1 deletion packages/cli/src/commands/validate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

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

Expand Down
19 changes: 19 additions & 0 deletions packages/lint/src/validate-jsx-pages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,22 @@ describe('validateJsxPages (ADR-0080 build gate)', () => {
expect(validateJsxPages({ pages: [{ name: 'full', kind: 'full', regions: [] }] })).toEqual([]);
});
});

describe('validateJsxPages — full validation with a manifest', () => {
const manifest = {
components: {
flex: { type: 'flex', namespace: 'ui', isContainer: true, inputs: [] },
'object-table': { type: 'object-table', namespace: 'plugin-grid', inputs: [{ name: 'object', type: 'string', required: true }] },
},
};
it('catches unknown components and missing required props', () => {
const stack = { pages: [{ name: 'cc', kind: 'jsx', source: '<flex><object-table /><bogus /></flex>' }] };
const f = validateJsxPages(stack, { manifest } as never);
expect(f.some((x) => x.rule === 'jsx-missing-required-prop')).toBe(true);
expect(f.some((x) => x.rule === 'jsx-forbidden-tag')).toBe(true);
});
it('passes when components + required props are satisfied', () => {
const stack = { pages: [{ name: 'cc', kind: 'jsx', source: '<flex><object-table object="account" /></flex>' }] };
expect(validateJsxPages(stack, { manifest } as never)).toEqual([]);
});
});
8 changes: 5 additions & 3 deletions packages/lint/src/validate-jsx-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// thread it through `compile()` here. Until then this catches the structural
// class of error an AI author is most likely to emit.

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

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

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

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