Thank you for your interest in contributing to Object UI! This document provides guidelines and instructions for contributing to the project.
- Getting Started
- Development Setup
- Development Workflow
- Architecture Overview
- Writing Tests
- Code Style
- Commit Guidelines
- Pull Request Process
- Documentation
- Adding Components
- Questions & Support
- Node.js 18.0 or higher
- pnpm (recommended package manager)
- Git for version control
- Basic knowledge of React, TypeScript, and Tailwind CSS
- Fork the repository on GitHub
- Clone your fork:
git clone https://github.com/YOUR_USERNAME/objectui.git cd objectui - Add upstream remote:
git remote add upstream https://github.com/objectstack-ai/objectui.git
# Install pnpm if you haven't
npm install -g pnpm
# Install project dependencies
pnpm installTo prevent merge conflicts in pnpm-lock.yaml, configure the custom merge driver:
# Set the merge driver name
git config merge.pnpm-merge.name "pnpm-lock.yaml merge driver"
# Set the merge driver command
git config merge.pnpm-merge.driver "pnpm install"This configuration allows Git to automatically resolve conflicts in pnpm-lock.yaml by regenerating the lockfile using pnpm install instead of attempting a manual merge.
# Sync with upstream
git fetch upstream
git checkout main
git merge upstream/main
# Create a feature branch
git checkout -b feature/your-feature-name# Run the console app — the main dev playground (app shell + the full plugin set,
# including the visual designer, which mounts here rather than in a demo of its own)
pnpm dev
# Run an example app (console-starter and byo-backend-console are the two that ship
# a dev server; hello-world has none, and schema-catalog is a data package)
pnpm --filter @object-ui/example-console-starter dev
# Run the documentation site
pnpm site:dev# Build all packages
pnpm build
# Build specific package
cd packages/core && pnpm build# Run all tests
pnpm test
# Run tests in watch mode
pnpm test:watch
# Run tests with UI (interactive)
pnpm test:ui
# Generate coverage report
pnpm test:coverage# Lint all packages
pnpm lint
# Lint specific package
cd packages/react && pnpm lintObject UI follows a modular monorepo architecture:
packages/
├── types/ # TypeScript type definitions (Zero dependencies)
├── core/ # Core logic, validation, registry (Zero React)
├── react/ # React bindings and SchemaRenderer
├── components/ # UI components (Tailwind + Shadcn)
├── designer/ # Visual schema editor
├── plugin-charts/ # Chart components plugin
└── plugin-editor/ # Rich text editor plugin
- Protocol Agnostic: Core never depends on specific backends
- Tailwind Native: All styling via Tailwind utility classes
- Type Safety: Strict TypeScript everywhere
- Tree Shakable: Modular imports, no monolithic bundles
- Zero React in Core: Core package has no React dependencies
See the Architecture Overview guide for details.
All tests should be placed in __tests__ directories within the source code. We use Vitest and React Testing Library.
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { MyComponent } from './MyComponent'
describe('MyComponent', () => {
it('should render correctly', () => {
render(<MyComponent />)
expect(screen.getByText('Expected Text')).toBeInTheDocument()
})
it('should handle user interaction', async () => {
const { user } = render(<MyComponent />)
await user.click(screen.getByRole('button'))
expect(screen.getByText('Clicked')).toBeInTheDocument()
})
})- Write tests for all new features
- Test user interactions, not implementation details
- Use meaningful test descriptions
- Maintain or improve code coverage (current thresholds: 63% lines, 43% functions, 40% branches, 62% statements)
- Aim to gradually increase coverage toward the long-term goal of 80%+ across all metrics
- Test edge cases and error states
// ✅ Good: Explicit types, clear naming
interface UserData {
id: string
name: string
email: string
}
function getUserById(id: string): UserData | null {
// implementation
}
// ❌ Bad: Implicit any, unclear naming
function get(x) {
// implementation
}// ✅ Good: TypeScript, named exports, clear props
interface ButtonProps {
label: string
onClick: () => void
variant?: 'primary' | 'secondary'
}
export function Button({ label, onClick, variant = 'primary' }: ButtonProps) {
return (
<button
onClick={onClick}
className={cn(
'px-4 py-2 rounded',
variant === 'primary' ? 'bg-blue-500' : 'bg-gray-500'
)}
>
{label}
</button>
)
}
// ❌ Bad: No types, default export, inline styles
export default function Button(props) {
return <button style={{ color: 'blue' }}>{props.label}</button>
}- Always use Tailwind: Never use inline styles or CSS modules
- Use
cn()utility: For conditional classes - Extract repeated classes: Create reusable class combinations
- Follow Shadcn patterns: Match the style of existing components
// ✅ Good
<div className={cn(
'flex items-center gap-2 p-4',
isActive && 'bg-blue-50',
className
)}>
{children}
</div>
// ❌ Bad
<div style={{ display: 'flex', padding: '16px' }}>
{children}
</div>- Use meaningful variable and function names
- Keep functions small and focused (< 50 lines)
- Add JSDoc comments for public APIs
- Avoid deep nesting (max 3 levels)
- Use early returns to reduce complexity
We follow Conventional Commits:
feat:- New featuresfix:- Bug fixesdocs:- Documentation changestest:- Adding or updating testschore:- Maintenance tasks (deps, config)refactor:- Code refactoring (no behavior change)perf:- Performance improvementsstyle:- Code style changes (formatting)
feat: add date picker component
fix: resolve schema validation error
docs: update installation guide
test: add tests for SchemaRenderer
chore: update dependencies
refactor: simplify expression evaluator<type>: <subject>
<body (optional)>
<footer (optional)>
-
Update from upstream:
git fetch upstream git rebase upstream/main
-
Create a changeset (for package changes):
pnpm changeset
This will prompt you to:
- Select which packages have changed
- Choose the version bump type (major, minor, patch)
- Write a summary of the changes
Learn more about changesets in the Versioning and Releases section.
-
Ensure tests pass:
pnpm test -
Ensure build succeeds:
pnpm build
-
Update documentation if needed
-
Add tests for new features
-
Push your branch:
git push origin feature/your-feature-name
-
Go to GitHub and create a Pull Request
-
Fill in the PR template:
- Clear description of changes
- Link to related issues
- Screenshots for UI changes
- Breaking changes (if any)
- Keep PRs focused (one feature/fix per PR)
- Write clear, descriptive PR titles
- Include before/after screenshots for UI changes
- Respond to review comments promptly
- Keep commits clean and meaningful
Our repository includes several automated GitHub workflows that will run when you create a PR:
- Linting: Checks code style and quality
- Type Checking: Validates TypeScript types
- Tests: Runs unit and integration tests
- Build: Ensures all packages build successfully
- Matrix Testing: Tests on Node.js 18.x and 20.x
- Coverage Thresholds: Enforces minimum test coverage (see below)
The project enforces minimum test coverage thresholds to maintain code quality:
- Lines: 63% (target: gradually increase to 80%)
- Functions: 43% (target: gradually increase to 80%)
- Branches: 40% (target: gradually increase to 75%)
- Statements: 62% (target: gradually increase to 80%)
These thresholds are intentionally set just below current coverage levels to prevent CI failures from minor fluctuations while we improve test coverage. New code should aim for higher coverage than these minimums.
- CodeQL: Scans for security vulnerabilities in code
- Dependency Scanning: Checks for known vulnerabilities in dependencies
- Auto-labeling: Automatically labels PRs based on changed files
- Bundle Size: Reports bundle size changes in PR comments
- PR Checks: Validates PR requirements and posts status
- All checks must pass before merging
- Failed checks will show detailed error messages
- Some workflows (like auto-labeling) run automatically
- Review the check results and fix any issues
- Run
pnpm lintbefore committing - Run
pnpm testto catch test failures early - Run
pnpm buildto ensure successful builds - Keep dependencies up to date
- Follow TypeScript strict mode requirements
We use fumadocs for documentation. The published pages live in content/docs/** — that root is declared once, in apps/site/source.config.ts (dir: '../../content/docs') — and a file's path below it becomes its route under /docs. New and updated documentation pages go there.
The repository root also has a docs/ directory, and it is not part of the site: it holds internal engineering material (ADRs, audits, architecture notes) that the fumadocs collection never reads, so none of it is rendered or reachable at a /docs/... route. A page filed there never reaches the site.
# Start the documentation site dev server
pnpm site:dev
# Build the documentation site
pnpm site:build- Use clear, concise language
- Provide code examples for all concepts
- Include both JSON schemas and React code
- Use TypeScript for code examples
- Add practical, real-world examples
- Link to related documentation
IMPORTANT: When adding internal links in documentation, follow these conventions to avoid 404 errors:
<!-- Correct - internal documentation links MUST include /docs/ prefix -->
[Quick Start](/docs/guide/quick-start)
[Components](/docs/components)
[API Reference](/docs/api/schema-reference)
[App Schema](/docs/core/app-schema)
[Architecture](/docs/guide/architecture)<!-- Wrong - missing /docs/ prefix -->
[Quick Start](/guide/quick-start) <!-- ❌ Should be /docs/guide/quick-start -->
[Components](/components) <!-- ❌ Should be /docs/components -->
<!-- Wrong - top-level section that does not exist under content/docs/ -->
[API Reference](/reference/api/core) <!-- ❌ No /reference/ section - use /docs/api/schema-reference -->
[Architecture](/architecture/component) <!-- ❌ No /architecture/ section - use /docs/guide/architecture -->
[Spec](/spec/app) <!-- ❌ No /spec/ section - use /docs/core/app-schema -->The example links in the two fenced blocks above are invisible to the link gate — check-doc-links.mjs blanks fenced code and inline code spans before it scans (it has to: markdown link syntax quoted inside code is not a link), so whenever you edit these examples, verify each route by hand against the pages actually present in content/docs/.
Fumadocs is configured with baseUrl: '/docs', which means all documentation pages are served under the /docs route in Next.js. Internal links must include the /docs/ prefix to match the actual URL structure where the pages are accessible.
Two separate checks with two different jobs — knowing which is which saves a wasted debugging round:
-
Internal links are the PR gate.
scripts/check-doc-links.mjs, run by.github/workflows/docs-links.ymlon every pull request tomain/develop(and on pushes to them). It resolves each/docs/...route and each relative path against the files in the checkout — reading the tree and nothing else, no network — so it is deterministic, and a failure blocks the merge. The workflow deliberately carries nopathsfilter, so a docs-only PR is checked too. Run the same script locally before you push:pnpm docs:check-links
-
External URLs are swept out of band, and gate nothing. lychee, run by
.github/workflows/check-links.yml, makes real network requests and is wired to a weekly cron plus manualworkflow_dispatchonly — never topull_request. That is a deliberate tradeoff (#3213): one third-party 502, rate-limit or anti-scraping response would otherwise turn an unrelated PR red with nothing its author could do about it. The job does fail its own scheduled run on a broken external link; it just never fails yours.
Which files each check reads is declared in the tools themselves — the SCAN_ROOTS table at the top of scripts/check-doc-links.mjs, and the args list in check-links.yml. Both surfaces get extended over time, so read them there instead of trusting a list copied into prose.
We use Changesets for version management and automated releases.
Changesets is a tool that helps us:
- Track changes: Each PR includes a changeset file describing what changed
- Automate versioning: Automatically determine version bumps based on changesets
- Generate changelogs: Create comprehensive changelogs from changeset descriptions
- Coordinate releases: Release multiple packages together in our monorepo
If your PR changes the src/ of a package the release covers, it must add a .changeset/*.md —
this is enforced by CI (changeset-presence.yml, objectui#3387). "The release covers it" means
the package is named in the fixed group of .changeset/config.json, which includes
@object-ui/console at apps/console as well as everything under packages/; @object-ui/site
and the examples are in ignore and are not gated.
What the gate asks for is a declaration, not a release:
-
✅ Score a bump (
patch/minor— nevermajor, see below) for:- New features
- Bug fixes
- Breaking changes (scored
minor, with the break described in the body) - Performance improvements
- API changes
-
✅ Declare that it releases nothing — a changeset with an empty frontmatter — for:
- Internal refactoring with no user-facing change
- Test-only changes under a package's
src/ - Dead-code removal
--- --- Removed the orphaned SystemObjectViewPage; no published behaviour changes.
This is a first-class pass, not a workaround. It costs one line and it puts the reason in the repository, where the next reader finds it — which is the whole point: three user-visible fixes shipped with no changeset and therefore appear in no CHANGELOG, version number or release note anywhere (objectui#3387).
-
❌ No changeset is needed at all for changes that touch no released package's
src/: documentation, CI configuration, repo-level scripts, the examples,apps/site.
Run node scripts/check-changeset-presence.mjs locally to get the same answer CI will give,
including for a changeset you have written but not yet committed.
-
Run the changeset command:
pnpm changeset
-
Select packages: Use arrow keys and spacebar to select which packages changed
🦋 Which packages would you like to include? ◯ @object-ui/core ◉ @object-ui/react ◯ @object-ui/components -
Choose version bump type:
- Major (x.0.0): Breaking changes
- Minor (0.x.0): New features (backwards compatible)
- Patch (0.0.x): Bug fixes and minor updates
-
Write a summary: Describe what changed
Summary: Add support for custom validation rules in forms -
Commit the changeset file:
git add .changeset/*.md git commit -m "chore: add changeset"
Write clear, user-facing descriptions:
✅ Good:
- Add support for custom date formats in DatePicker
- Fix validation error in nested form fields
- Improve performance of large data grids by 50%
❌ Bad:
- Updated code
- Fixed bug
- Changes to validationThe release process is automated:
- Create PR with changes → Include a changeset file
- PR is merged → Changeset bot creates/updates a "Version Packages" PR
- Version PR is merged → Packages are automatically published to npm
You don't need to manually:
- Update version numbers
- Update CHANGELOGs
- Create Git tags
- Publish to npm
Everything is handled by the changeset automation!
# 1. Create a feature branch
git checkout -b feat/add-date-picker
# 2. Make your changes
# ... edit files ...
# 3. Create a changeset
pnpm changeset
# Select @object-ui/components
# Choose "minor" (new feature)
# Summary: "Add DatePicker component with calendar popup"
# 4. Commit everything
git add .
git commit -m "feat: add DatePicker component"
# 5. Push and create PR
git push origin feat/add-date-picker-
Define the schema in
packages/types/:export interface MyComponentSchema extends BaseSchema { type: 'my-component' title: string content: string }
-
Implement the component in
packages/components/:export function MyComponent(props: { schema: MyComponentSchema }) { return ( <div className={cn('p-4', props.schema.className)}> <h3>{props.schema.title}</h3> <p>{props.schema.content}</p> </div> ) }
-
Register the component:
registry.register('my-component', MyComponent)
-
Add tests:
describe('MyComponent', () => { it('should render title and content', () => { const schema = { type: 'my-component', title: 'Test', content: 'Content' } render(<SchemaRenderer schema={schema} />) expect(screen.getByText('Test')).toBeInTheDocument() }) })
-
Add documentation in
docs/components/my-component.md
- GitHub Discussions - General questions and ideas
- GitHub Issues - Bug reports and feature requests
- Email - hello@objectui.org
- Check if the bug is already reported
- Create a new issue with:
- Clear title and description
- Steps to reproduce
- Expected vs actual behavior
- Code examples (minimal reproduction)
- Environment details (OS, Node version, etc.)
- Check if it's already requested
- Open a discussion to gather feedback
- If approved, create an issue with detailed spec
By contributing, you agree that your contributions will be licensed under the MIT License.
Thank you for contributing to Object UI! 🎉