Conventions, tooling, and workflow for contributing to Devonz.
| Aspect | Tool / Convention |
|---|---|
| Package manager | pnpm 9.14.4 |
| Node version | ≥ 18.18.0 |
| Linter | ESLint (@blitz/eslint-plugin) |
| Formatter | Prettier 3.x |
| Test runner | Vitest 2.x |
| Test utilities | @testing-library/react 16.x |
| Path alias | ~/ → ./app/ |
| CSS framework | UnoCSS (Tailwind-compatible) |
Config lives in eslint.config.mjs. Key rules enforced:
- No relative imports — use
~/path alias instead of../ - Semicolons always
- Curly braces always (even single-line blocks)
- No
eval() - Unix line endings (
LF, notCRLF) - Arrow spacing — spaces before/after
=> - Consistent return — every branch must explicitly return (or none)
- Array brackets — no spaces inside
[] - Naming conventions — enforced on
.tsxfiles via@blitz/eslint-plugin
# Lint the project
pnpm lint
# Lint and auto-fix
pnpm lint:fixIntegrated with ESLint via eslint-config-prettier + eslint-plugin-prettier. Running pnpm lint:fix applies both ESLint fixes and Prettier formatting.
| Pattern | Meaning |
|---|---|
*.client.tsx |
Browser-only component — never runs on the server |
*.server.ts |
Server-only code — never shipped to the client |
*.spec.ts / *.spec.tsx |
Test files (colocated with source) |
api.*.ts |
Remix API route under app/routes/ |
PascalCase.tsx |
React components |
camelCase.ts |
Utilities, services, stores |
// CORRECT — absolute path alias
import { workbenchStore } from '~/lib/stores/workbench';
import { BaseChat } from '~/components/chat/BaseChat';
// WRONG — relative imports are blocked by ESLint
import { workbenchStore } from '../../stores/workbench';The ~/ alias maps to app/ and is configured in both tsconfig.json and Vite.
Use the .client.tsx suffix for components that depend on browser APIs.
// Workbench.client.tsx — uses RuntimeClient, browser-only APIs
export default function Workbench() { /* ... */ }Use createScopedLogger from ~/utils/logger for debug output:
import { createScopedLogger } from '~/utils/logger';
const logger = createScopedLogger('MyComponent');
logger.debug('Initializing...');
logger.error('Something failed', error);- Read stores with
useStore(someAtom)from@nanostores/react - Avoid
useStatefor shared/global state — use nanostores - See STATE-MANAGEMENT.md for patterns
Tests use Vitest with @testing-library/react and @testing-library/jest-dom.
# Run all tests once
pnpm test
# Watch mode
pnpm test:watchimport { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { MyComponent } from '~/components/MyComponent';
describe('MyComponent', () => {
it('renders correctly', () => {
render(<MyComponent />);
expect(screen.getByText('Hello')).toBeInTheDocument();
});
});The project currently has 810 tests across 38 spec files.
Recent test expansion areas include:
- MCP schema sanitization — 11 tests in
mcpService.spec.tscovering JSON Schema cleanup for Google Gemini compatibility (anyOf,oneOf,allOf,additionalPropertiesremoval) - MCP result text extraction — 16 tests in
ToolInvocations.spec.tsverifying correct rendering of MCP tool call results - Auto-approve toggle — tests for the MCP auto-approve UI toggle behaviour
Colocate test files next to source files:
components/
chat/
Markdown.tsx
Markdown.spec.ts
All API routes live in app/routes/ with the api. prefix:
api.chat.ts → POST /api/chat
api.models.$provider.ts → GET /api/models/:provider
Every route handler must be wrapped with withSecurity() from ~/lib/security. This is mandatory for all new routes.
Use Zod for request body validation:
import { z } from 'zod';
const RequestSchema = z.object({
field: z.string().min(1),
});
export async function action({ request }: ActionFunctionArgs) {
const body = await request.json();
const parsed = RequestSchema.safeParse(body);
if (!parsed.success) {
return json({ error: 'Invalid request' }, { status: 400 });
}
// ...
}Credentials come from cookies — never from request bodies or query params:
const cookieHeader = request.headers.get('Cookie') || '';See LLM-PROVIDERS.md — step-by-step guide.
Extended thinking is supported for Anthropic Claude and Google Gemini providers. When adding a new provider, check whether it supports extended/reasoning tokens and wire up the
thinkingBudgetparameter accordingly.
- Create in the appropriate
components/subdirectory - Use
.client.tsxsuffix if browser-only - Use scoped logger for debug output
- Read state from stores, not local state for shared data
- Create in
app/lib/stores/ - Use
atom()ormap()fromnanostores - Guard with
import.meta.hot?.datafor HMR safety - See STATE-MANAGEMENT.md
- Create
api.your-route.tsinapp/routes/ - Export
loader(GET) oraction(POST/PUT/DELETE) - Validate input with Zod
- Read credentials from cookies
- Return
json()responses with proper status codes - Wrap with
withSecurity()— import from~/lib/securityand wrap your handler function
New MCP tools must have JSON Schemas compatible with Google Gemini, which rejects anyOf, oneOf, allOf, and additionalProperties keywords. The _sanitizeJsonSchema() method in mcpService.ts handles this automatically at connection time — it recursively strips unsupported keywords and flattens composite schemas into a single object type. No manual cleanup is needed when adding MCP servers, but be aware of this constraint when debugging schema-related tool call failures.
# Create a feature branch
git checkout -b feature/my-feature
# Make changes, then lint + test
pnpm lint:fix
pnpm test
# Commit with descriptive message
git commit -m "feat: add X to Y"
# Push and open a PR
git push origin feature/my-featureUse conventional commits:
| Prefix | Usage |
|---|---|
feat: |
New feature |
fix: |
Bug fix |
docs: |
Documentation only |
refactor: |
Code restructuring (no behavior change) |
test: |
Adding or updating tests |
chore: |
Tooling, dependencies, config |
| Script | Command | Purpose |
|---|---|---|
dev |
pnpm dev |
Start dev server (Remix + Vite) |
build |
pnpm build |
Production build |
start |
pnpm start |
Start production server |
lint |
pnpm lint |
Run ESLint |
lint:fix |
pnpm lint:fix |
ESLint + Prettier auto-fix |
test |
pnpm test |
Run tests (Vitest) |
test:watch |
pnpm test:watch |
Watch mode tests |
clean |
pnpm clean |
Clean build artifacts |
update |
pnpm run update |
Pull latest and reinstall (git users) |
docker:build |
pnpm docker:build |
Build Docker image |
docker:run |
pnpm docker:run |
Run Docker container |
docker:up |
pnpm docker:up |
Start via Docker Compose |
docker:down |
pnpm docker:down |
Stop Docker Compose |
docker:dev |
pnpm docker:dev |
Docker dev mode |
docker:update |
pnpm docker:update |
Update Docker deployment |
setup |
pnpm run setup |
Interactive setup wizard (init .env.local, prompt for keys) |
preview |
pnpm preview |
Build + serve locally |
test:coverage |
pnpm test:coverage |
Run tests with V8 coverage |
test:integration |
pnpm test:integration |
Run integration tests (LocalRuntime, AgentTools) |