Instruction file imported from furystack/boilerplate (
.cursor/rules/CODE_STYLE.mdc). Copyright stays with the author.
Code Style
Prettier + ESLint enforced. Run yarn prettier and yarn lint. Generate code that satisfies @furystack/eslint-plugin from the start — don't rely on the linter to catch violations.
Naming
| Kind | Convention | Example |
|---|---|---|
| File | kebab-case .ts / .tsx |
session.ts, theme-switch.tsx |
| Spec | co-located, .spec.ts / .spec.tsx |
session.spec.ts |
| E2E spec | e2e/<flow>.e2e.spec.ts |
e2e/page.spec.ts |
| Class / type / component / service token | PascalCase | SessionService, BoilerplateApi |
| Function / variable | camelCase, verb-led | getCurrentUser, validateEmail |
| Constant | UPPER_SNAKE_CASE | MAX_RETRY_COUNT |
| Boolean | is* / has* / should* / can* prefix |
isAuthenticated |
| Event handler (internal) | handle* |
handleSubmit |
| Event prop callback | on* |
onSave |
| Type alias | prefer type over interface. No I prefix, no Type suffix |
type User = { ... } |
| Component props | PascalCase + Props suffix |
type LoginFormProps |
| Shade element name | kebab-case, unique, namespaced | customElementName: 'shade-login' |
Imports
Order, blank line between groups:
- Node built-ins
- External deps
@furystack/*packages- Workspace packages (
common) - Relative imports
- Type-only imports last in each group
import { join } from 'path'
import { defineService } from '@furystack/inject'
import { Shade } from '@furystack/shades'
import type { BoilerplateApi } from 'common'
import { SessionService } from '../services/session.js'
import type { LoginFormProps } from './types.js'
Group same-package imports together. Sort alphabetically when many.
Workspace layout
boilerplate/
├── common/ # shared types + API contract
│ └── src/
│ ├── boilerplate-api.ts # RestApi interface
│ └── models/
├── frontend/ # Shades app
│ └── src/
│ ├── components/<x>.tsx # leaf components
│ ├── components/<x>/index.tsx + assets
│ ├── pages/<x>.tsx # route targets
│ └── services/<x>.ts # frontend services (defineService)
├── service/ # REST backend
│ └── src/
│ ├── service.ts # entry
│ ├── setup-rest-api.ts # endpoint registration
│ ├── setup-store.ts # store + DataSet wiring
│ └── seed.ts
└── e2e/ # Playwright specs
common re-exports through common/src/index.ts. Use .js import suffix on relative paths (NodeNext ESM).
File structure
// 1. imports
import { defineService } from '@furystack/inject'
// 2. types
type SessionState = 'initializing' | 'unauthenticated' | 'authenticated'
// 3. constants
const DEFAULT_TIMEOUT_MS = 5000
// 4. helpers
const formatError = (err: unknown): string =>
err instanceof Error ? err.message : 'Unknown error'
// 5. main export
export const SessionService = defineService({
name: 'app/SessionService',
lifetime: 'singleton',
factory: ({ inject, onDispose }) => { /* ... */ },
})
Service / component structure
Component render order: services → host props → refs → observable subscriptions → local state → handlers → JSX.
export const UserProfile = Shade<UserProfileProps>({
customElementName: 'user-profile',
render: ({ props, injector, useObservable, useState }) => {
const session = injector.get(SessionService)
const [user] = useObservable('user', session.currentUser)
const [draft, setDraft] = useState('draft', '')
const handleSave = () => { /* ... */ }
return <div>{/* ... */}</div>
},
})
JSX
Multi-line for >2 attrs. Single-line for trivial cases. No nested ternaries — use early returns for >2 branches.
// ✅
<button type="button" onclick={handleClick} style={{ padding: '8px' }}>
Save
</button>
// ❌ nested ternary
{isLoading ? <Loading /> : error ? <Err /> : <Content />}
JSDoc
Required on exported services / components / endpoints. For internal symbols, JsDoc is optional — only add it when it passes the value test below.
Value test
Before writing or keeping JsDoc, strip the type signature mentally and read the JsDoc alone. If it still tells the reader something the type does not, keep it. If it just narrates the type, delete it.
Keep when JsDoc explains:
- Intent — why this exists, what problem it solves
- Trade-offs — design choices that have alternatives
- Constraints — runtime invariants, ordering, lifecycle, side effects
- Non-obvious usage — patterns the type allows but discourages, or shapes the caller is expected to follow
Delete when JsDoc:
- Restates the function name (
isUser→ "Runtime type guard forUser.") - Restates parameter or return types already in the signature
- Says "Service token for the session" when the type is literally
Token<SessionService, 'singleton'>
@example
Include when usage is not obvious from the signature. Examples must be copy-paste compilable — if you copy the block into a fresh .ts file and add the imports shown, tsc --noEmit passes. Drift is the main failure mode; treat examples as code, not prose.
Cross-link instead of duplicating. Write See {@link SessionService} for session lifecycle rather than re-explaining it on every consumer.
Cross-file {@link} references
When a {@link Symbol} references a symbol declared in another file, add a type-only import for that symbol at the top of the referencing file:
import type { SessionService } from '../services/session.js'
TypeScript recognizes JsDoc references as usage (no noUnusedLocals complaint) and IDE navigation works. Type-only imports are erased under verbatimModuleSyntax: true, so circular type-only imports between two files are fine.
Example — value test in practice
// ✅ keeps — explains intent + trade-off, irreplaceable by the type
/**
* Sessions are tracked in-memory only; restart loses state.
* Acceptable trade-off vs. introducing a Redis dependency at this scale.
*/
const sessionStore = new Map<string, Session>()
// ❌ deletes — narrates the type, no extra signal
/**
* Runtime type guard for {@link User}.
*/
export const isUser = (value: unknown): value is User => { /* ... */ }
Comments
Only for non-obvious intent / trade-offs / constraints. Never narrate what code does.
// ✅
// Exponential backoff: 1s, 2s, 4s, 8s, capped at 30s
const delay = Math.min(1000 * 2 ** attempt, 30000)
// ❌
// Set count to 0
const count = 0