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
9 changes: 9 additions & 0 deletions .changeset/spec-15-ui-schema-reexports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@object-ui/types': major
---

Adopt `@objectstack/spec` 15 across the workspace and drop the value-erased `…Schema` re-exports from `@object-ui/types` (#2561).

**Removed exports.** `packages/types` re-exported the `@objectstack/spec/ui` surface inside `export type { … }` blocks, and those blocks included the zod validators (`DndConfigSchema`, `SpecFormViewSchema`, `ThemeModeSchema`, … 84 names in total). Under `export type` a zod value is erased, so importing any of them as a value from `@object-ui/types` silently yielded `undefined` at runtime. Per the #2561 decision (option a) the schema names are removed from the public surface instead of being converted to value re-exports — consumers that need the runtime validators import them from `@objectstack/spec/ui` directly. The inferred types (`DndConfig`, `SpecFormView`, …) are unchanged, and the genuine value re-exports (`defineStack`, `ObjectStackSchema`, `SpecReportSchema`, …) keep working. `BreakpointColumnMapSchema` / `BreakpointOrderMapSchema` are dropped without a type replacement (the spec exports no companion inferred type). A guardrail test (`spec-ui-schema-reexports.test.ts`) pins the contract.

**Spec 15.** Every workspace package now depends on `@objectstack/spec` ^15.1.1. The `/ui` export-name set is identical to 14.6; the spec-level breaking change is ADR-0089 D3a — `FormFieldSchema` / `FormSectionSchema` / `PageComponentSchema` are `.strict()` and reject undeclared keys, which the workspace test suite passes under. The floor is 15.1.1 (not 15.0.0) because D3a's `.strict().transform(…)` pipes crashed `z.toJSONSchema` over spec's lazySchema proxies (`Cannot set properties of undefined (setting 'ref')`), breaking Studio's spec-derived Page/View inspector schemas; fixed upstream in framework#3021, which shipped in spec 15.1.1. New `view-schema.test.ts` pins the View-inspector derivation (previously untested — it degraded silently).
2 changes: 1 addition & 1 deletion apps/console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
"@object-ui/react": "workspace:*",
"@object-ui/types": "workspace:*",
"@objectstack/client": "^14.6.0",
"@objectstack/spec": "^15.1.0",
"@objectstack/spec": "^15.1.1",
"@tailwindcss/postcss": "^4.3.2",
"@tailwindcss/typography": "^0.5.20",
"@testing-library/jest-dom": "^6.9.1",
Expand Down
2 changes: 1 addition & 1 deletion apps/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@object-ui/plugin-view": "workspace:*",
"@object-ui/react": "workspace:*",
"@object-ui/types": "workspace:*",
"@objectstack/spec": "^15.1.0",
"@objectstack/spec": "^15.1.1",
"fumadocs-core": "16.11.3",
"fumadocs-mdx": "15.0.13",
"fumadocs-ui": "16.11.3",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"devDependencies": {
"@changesets/cli": "^2.31.0",
"@eslint/js": "^10.0.1",
"@objectstack/spec": "^15.1.0",
"@objectstack/spec": "^15.1.1",
"@playwright/test": "^1.61.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/app-shell/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"@object-ui/types": "workspace:*",
"@objectstack/formula": "^14.6.0",
"@objectstack/lint": "^14.6.0",
"@objectstack/spec": "^15.1.0",
"@objectstack/spec": "^15.1.1",
"@sentry/react": "^10.65.0",
"jsonc-parser": "^3.3.1",
"lucide-react": "^1.24.0",
Expand Down
49 changes: 49 additions & 0 deletions packages/app-shell/src/views/metadata-admin/view-schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { describe, it, expect } from 'vitest';
import {
getListVariantSchema,
getFormVariantSchema,
getListColumnSchema,
getViewForm,
} from './view-schema';

/**
* Regression for objectui#2561: the View inspector's spec-derived JSONSchema
* had no test, so when spec 15 (ADR-0089 D3a) turned FormFieldSchema into a
* `.strict().transform(…)` pipe and crashed `z.toJSONSchema` over the
* lazySchema proxy (fixed upstream in framework#3021), the inspector degraded
* silently. Pin the derivation like page-schema.test.ts does for pages.
*/
describe('view-schema (spec-derived View authoring metadata)', () => {
it('derives the list-variant JSONSchema from the spec ViewSchema', () => {
const schema = getListVariantSchema();
expect(schema).toBeTruthy();
expect(JSON.stringify(schema)).toContain('kanban');
});

it('derives the form-variant JSONSchema (recursive strict FormFieldSchema)', () => {
const schema = getFormVariantSchema();
expect(schema).toBeTruthy();
expect(JSON.stringify(schema)).toContain('sections');
});

it('derives the ListColumn JSONSchema', () => {
expect(getListColumnSchema()).toBeTruthy();
});

it('prunes inspector-owned fields from the authoring form', () => {
const form = getViewForm();
if (form) {
const declared = new Set<string>();
for (const s of form.sections ?? []) {
for (const f of s.fields ?? []) {
declared.add(typeof f === 'string' ? f : (f as any)?.field);
}
}
for (const owned of ['columns', 'data', 'name']) {
expect(declared.has(owned)).toBe(false);
}
}
});
});
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"dependencies": {
"@object-ui/types": "workspace:*",
"@objectstack/formula": "^14.6.0",
"@objectstack/spec": "^15.1.0",
"@objectstack/spec": "^15.1.1",
"lodash": "^4.18.1",
"zod": "^4.4.3"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-detail/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
},
"dependencies": {
"@object-ui/i18n": "workspace:*",
"@objectstack/spec": "^15.1.0",
"@objectstack/spec": "^15.1.1",
"lucide-react": "^1.24.0"
},
"peerDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-form/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"@object-ui/permissions": "workspace:*",
"@object-ui/react": "workspace:*",
"@object-ui/types": "workspace:*",
"@objectstack/spec": "^15.1.0",
"@objectstack/spec": "^15.1.1",
"lucide-react": "^1.24.0"
},
"peerDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-gantt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@object-ui/plugin-detail": "workspace:*",
"@object-ui/react": "workspace:*",
"@object-ui/types": "workspace:*",
"@objectstack/spec": "^15.1.0",
"@objectstack/spec": "^15.1.1",
"lucide-react": "^1.24.0",
"qrcode": "^1.5.4",
"sonner": "^2.0.7"
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-map/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"@object-ui/core": "workspace:*",
"@object-ui/react": "workspace:*",
"@object-ui/types": "workspace:*",
"@objectstack/spec": "^15.1.0",
"@objectstack/spec": "^15.1.1",
"lucide-react": "^1.24.0",
"maplibre-gl": "^5.24.0",
"react-map-gl": "^8.1.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-timeline/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"@object-ui/mobile": "workspace:*",
"@object-ui/react": "workspace:*",
"@object-ui/types": "workspace:*",
"@objectstack/spec": "^15.1.0",
"@objectstack/spec": "^15.1.1",
"class-variance-authority": "^0.7.1",
"zod": "^4.4.3"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-tree/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"@object-ui/core": "workspace:*",
"@object-ui/react": "workspace:*",
"@object-ui/types": "workspace:*",
"@objectstack/spec": "^15.1.0",
"@objectstack/spec": "^15.1.1",
"lucide-react": "^1.24.0"
},
"peerDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-view/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"@object-ui/plugin-grid": "workspace:*",
"@object-ui/react": "workspace:*",
"@object-ui/types": "workspace:*",
"@objectstack/spec": "^15.1.0",
"@objectstack/spec": "^15.1.1",
"class-variance-authority": "^0.7.1",
"lucide-react": "^1.24.0"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@object-ui/data-objectstack": "workspace:*",
"@object-ui/i18n": "workspace:*",
"@object-ui/types": "workspace:*",
"@objectstack/spec": "^15.1.0",
"@objectstack/spec": "^15.1.1",
"react-hook-form": "^7.81.0"
},
"peerDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions packages/types/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pnpm add @object-ui/types
Object UI follows a strict **"Protocol First"** approach with a clear inheritance hierarchy:

```
@objectstack/spec (v4.0.x) ← The "Highest Law" - Universal protocol
@objectstack/spec (v15.x) ← The "Highest Law" - Universal protocol
UIComponent ← Base interface for all UI components
Expand Down Expand Up @@ -307,7 +307,7 @@ We follow these constraints for this package:

- **Node.js:** ≥ 18
- **TypeScript:** ≥ 5.0 (strict mode)
- **`@objectstack/spec`:** ^4.0.4
- **`@objectstack/spec`:** ^15.1.1
- **`@objectstack/client`:** ^3.3.0
- **Tailwind CSS:** ≥ 3.4 (for packages with UI)

Expand Down
2 changes: 1 addition & 1 deletion packages/types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"directory": "packages/types"
},
"dependencies": {
"@objectstack/spec": "^15.1.0",
"@objectstack/spec": "^15.1.1",
"zod": "^4.4.3"
},
"devDependencies": {
Expand Down
184 changes: 184 additions & 0 deletions packages/types/src/__tests__/spec-ui-schema-reexports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/**
* Guardrail for #2561 — decision (a): the zod validators (`…Schema`) of
* `@objectstack/spec/ui` are NOT part of @object-ui/types' public surface.
*
* Background: index.ts re-exports the spec UI surface inside
* `export type { … }` blocks. A zod schema listed in such a block is
* value-erased — a consumer importing it as a value silently got `undefined`
* at runtime. Decision (a) drops those names instead of converting them to
* value re-exports; consumers needing the runtime validators import
* `@objectstack/spec/ui` directly.
*
* Note: dual type+value names (`Dashboard`, `DensityMode`, `ThemeMode`, …)
* are intentionally re-exported type-only — only `…Schema`-suffixed zod
* values are governed by this contract.
*/
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import * as SpecUI from '@objectstack/spec/ui';
import * as Types from '../index';

const SRC_DIR = join(dirname(fileURLToPath(import.meta.url)), '..');

/**
* The public names dropped in #2561 (previously value-erased `export type`
* re-exports of spec/ui zod schemas, listed under their exported alias).
*/
const DROPPED_SCHEMA_EXPORTS = [
// Drag and Drop
'DndConfigSchema',
'DragItemSchema',
'DropZoneSchema',
'DragConstraintSchema',
'DragHandleSchema',
'DropEffectSchema',
// Focus & Keyboard Navigation
'FocusManagementSchema',
'FocusTrapConfigSchema',
'KeyboardNavigationConfigSchema',
'KeyboardShortcutSchema',
// Animation & Motion
'ComponentAnimationSchema',
'AnimationTriggerSchema',
'MotionConfigSchema',
'TransitionConfigSchema',
'TransitionPresetSchema',
'EasingFunctionSchema',
// Notifications
'NotificationSchema',
'NotificationConfigSchema',
'NotificationActionSchema',
'NotificationPositionSchema',
'NotificationSeveritySchema',
'NotificationTypeSchema',
// Gestures & Touch
'SpecGestureConfigSchema',
'SpecGestureTypeSchema',
'SwipeGestureConfigSchema',
'SwipeDirectionSchema',
'PinchGestureConfigSchema',
'LongPressGestureConfigSchema',
'TouchInteractionSchema',
'TouchTargetConfigSchema',
// Offline & Sync
'SpecOfflineConfigSchema',
'OfflineCacheConfigSchema',
'OfflineStrategySchema',
'SyncConfigSchema',
'ConflictResolutionSchema',
'PersistStorageSchema',
'EvictionPolicySchema',
// View Enhancements
'ColumnSummarySchema',
'GalleryConfigSchema',
'GroupingConfigSchema',
'RowColorConfigSchema',
'RowHeightSchema',
'DensityModeSchema',
'TimelineConfigSchema',
'NavigationConfigSchema',
'ViewSharingSchema',
// Dashboard
'SpecDashboardSchema',
'SpecDashboardWidgetSchema',
'SpecDashboardHeaderSchema',
'SpecDashboardHeaderActionSchema',
'SpecGlobalFilterSchema',
'GlobalFilterOptionsFromSchema',
'WidgetColorVariantSchema',
// Sharing & Embedding
'SharingConfigSchema',
'EmbedConfigSchema',
// View Configuration
'AddRecordConfigSchema',
'AppearanceConfigSchema',
'UserActionsConfigSchema',
'ViewTabSchema',
// View Filter Rules
'ViewFilterRuleSchema',
// Form View
'SpecFormViewSchema',
'SpecFormSectionSchema',
'SpecFormFieldSchema',
// ListView
'SpecListViewSchema',
'SpecListColumnSchema',
// Page
'SpecPageSchema',
'SpecPageComponentSchema',
'SpecPageRegionSchema',
'SpecPageTypeSchema',
'SpecPageVariableSchema',
// Performance & Page Transitions
'PerformanceConfigSchema',
'PageTransitionSchema',
// Accessibility
'AriaPropsSchema',
'WcagContrastLevelSchema',
// I18n
'I18nLabelSchema',
'I18nObjectSchema',
'LocaleConfigSchema',
'PluralRuleSchema',
'DateFormatSchema',
'NumberFormatSchema',
// Responsive Design
'SpecResponsiveConfigSchema',
'BreakpointColumnMapSchema',
'BreakpointOrderMapSchema',
// theme.ts
'ThemeModeSchema',
];

describe('spec/ui …Schema re-exports (#2561, decision (a))', () => {
it('does not runtime-export the dropped …Schema names', () => {
for (const name of DROPPED_SCHEMA_EXPORTS) {
expect(
name in Types,
`'${name}' must not be exported from @object-ui/types — #2561 dropped ` +
`the spec/ui zod-schema re-exports; import it from '@objectstack/spec/ui'`,
).toBe(false);
}
});

it('keeps the genuine value re-exports intact', () => {
expect(typeof Types.defineStack).toBe('function');
expect(Types.ObjectStackSchema).toBeDefined();
expect(Types.SpecReportSchema).toBeDefined();
expect(Types.SpecReportColumnSchema).toBeDefined();
expect(Types.SpecReportTypeEnum).toBeDefined();
expect(Types.ACTION_LOCATIONS).toBeDefined();
expect(Types.ActionLocationSchema).toBeDefined();
});

it('source: no spec/ui zod value hides inside an `export type` block', () => {
for (const file of ['index.ts', 'theme.ts']) {
const src = readFileSync(join(SRC_DIR, file), 'utf8');
const blocks = src.matchAll(
/export type \{([^}]*)\} from '@objectstack\/spec\/ui'/g,
);
for (const [, body] of blocks) {
const sourceNames = body
.replace(/\/\/[^\n]*/g, '')
.replace(/\/\*[\s\S]*?\*\//g, '')
.split(',')
.map((entry) => entry.trim())
.filter(Boolean)
.map((entry) => entry.split(/\s+as\s+/)[0].trim());
for (const name of sourceNames) {
const isValueErasedSchema =
name.endsWith('Schema') &&
(SpecUI as Record<string, unknown>)[name] !== undefined;
expect(
isValueErasedSchema,
`${file}: '${name}' is a zod value in @objectstack/spec/ui but sits ` +
`in an 'export type' block, so it would be value-erased (#2561) — ` +
`drop it or re-export it as a value deliberately`,
).toBe(false);
}
}
}
});
});
Loading
Loading