CLAUDE.md · diff
git:20260830.7229219 to git:20260831.ddcd0ba
2 added, 2 removed. Audit A to A.
# CLAUDE.md — KickJS Development Guide
> **Read [`.agents/AGENTS.md`](./.agents/AGENTS.md) first.** It is the canonical,
> multi-agent reference for this monorepo (Claude, Copilot, Codex, Gemini, etc.).
> This file mirrors the same project context distilled for Claude, plus
> Claude-specific notes. When the two disagree on anything substantive, treat
> `.agents/AGENTS.md` as authoritative and flag the discrepancy.
## Project Overview
KickJS is a decorator-driven Node.js framework for TypeScript. The HTTP engine is pluggable — it runs on **Express (default), Fastify, or h3**, selected via `bootstrap({ runtime })`; controllers/modules/DI/context decorators are engine-neutral. Monorepo managed with pnpm workspaces and Turbo.
> The real framework package is `packages/kickjs` (`@forinda/kickjs`) — it holds the DI core, HTTP layer, RequestContext, and the runtime seam (`src/http/runtimes/{express,fastify,h3}.ts`). The `packages/core` + `packages/http` split described in older parts of this doc is **stale**; treat `packages/kickjs/src/{core,http}` as the source of truth. Fastify/h3 runtimes, the h3 v2 web-standard entries (`./h3-web`, `./web` — edge/Bun/Deno), and cross-engine uploads all ship in the **stable** release.
**30+ workspace packages** (published under `@forinda/kickjs*` unless private), CLI with generators + a fullstack template, typed client, Prisma/Drizzle/kick-db support. Runnable example apps live in [forinda/kickjs-examples-archive](https://github.com/forinda/kickjs-examples-archive); `examples/` in-repo holds only test fixtures.
## Quick Commands
```bash
pnpm build # Build all packages
pnpm test # Run all tests
pnpm format # Fix formatting
pnpm format:check # Check formatting
pnpm docs:dev # Dev docs server
pnpm docs:build # Build docs
pnpm changeset # Add a changeset to the current PR
pnpm changeset:status # Preview pending bumps
```
## Repository Structure
```
packages/ # Workspace packages (@forinda/kickjs* on npm unless private)
kickjs/ # THE framework — DI core, decorators, http layer,
# RequestContext, runtimes (express/fastify/h3/h3-web),
# web fetch entry (src/web.ts → @forinda/kickjs/web)
cli/ # kick new (rest|minimal|fullstack), generators, typegen,
# kick add / doctor / agents
client/ # @forinda/kickjs-client — typed fetch client (frontend)
vite/ # Vite HMR plugin + typegen watcher
schema/ # Schema-agnostic validation (Zod/Valibot/Yup/StdSchema)
swagger/ # OpenAPI from decorators + declared response schemas
db/ # kick/db code-first database family — dialects ship as
# subpaths (@forinda/kickjs-db/{pg,mysql,sqlite})
testing/ # createTestApp, createTestModule, plugin harness
devtools/ devtools-kit/ # /_debug dashboard + adapter tab kit
grpc/ # Connect RPC — gRPC-Web/Connect on the shared HTTP port
ai/ mcp/ graphql/ ws/ queue/ cron/ mailer/ otel/
notifications/ multi-tenant/ auth/ prisma/ drizzle/
cli-kit/ lint/ vscode-extension/
core/ http/ config/ # LEGACY split — superseded by packages/kickjs; do not edit
examples/ # Test fixtures only (typegen-test). Runnable apps live in
# github.com/forinda/kickjs-examples-archive
articles/ # Blog articles (dev.to)
scripts/ # release.js (versioning)
docs/ # VitePress documentation site
```
## Package Manager & Build
- **pnpm** — always use `pnpm`, never npm/yarn
- **Turbo** — orchestrates builds with dependency-aware caching
- **Vite 8** — builds each package in library mode (ESM, esbuild minify, node20 target)
- **tsc** — generates `.d.ts` via `tsconfig.build.json` (`emitDeclarationOnly`)
- **Vitest** — test runner with SWC for decorator support
## Code Style
- **Prettier** — no semicolons, single quotes, trailing commas, 100 char width
- **No ESLint** — relies on TypeScript strict mode + Prettier
- **Pre-commit hook** — runs `build → test → format:check` via husky
- Format before committing: `pnpm format`
## Key Patterns
### Adding Middleware (to `packages/kickjs`)
1. Create `packages/kickjs/src/http/middleware/<name>.ts`
2. Add entry to `packages/kickjs/tsdown.config.ts` `entry` object
3. Add export map entry to `packages/kickjs/package.json`
4. Add re-export to `packages/kickjs/src/http/index.ts` (and `src/index.ts` if it's public API — the root index is selective, not `export *`)
### Adding a Package
1. Create `packages/<name>/` with `package.json`, `tsconfig.json`, `tsdown.config.ts`, `vitest.config.ts` (copy `packages/schema/` as the template — tsdown is the repo standard; the old vite-library recipe is retired)
2. Name it `@forinda/kickjs-<name>`, start at `0.0.0` — changesets will set the first published version on the next release PR
3. Use `workspace:*` for internal deps
4. tsdown config: `format: ['esm']`, `dts: true`, all runtime deps in `external`
5. Scripts: `"build": "tsdown"`, `"typecheck": "tsc --noEmit"` — and CHECK the exports map matches what tsdown emits (`dist/index.js` + `dist/index.d.ts`; a mismatch shipped an unresolvable package once)
### Adding an Adapter
Implement `AppAdapter` from `@forinda/kickjs/adapter`:
- `name: string`
- `beforeMount?({ app }: AdapterContext)`, `beforeStart?({ container }: AdapterContext)`, `afterStart?({ server }: AdapterContext)`
- `shutdown?(): Promise<void>`
- `middleware?(): AdapterMiddleware[]`
### Adding an Example App
Use the built CLI to scaffold — never create files manually.
```bash
# 1. Build CLI first (if not already built)
pnpm build
# 2. Scaffold from examples/ directory
cd examples
# Fastest — name + --yes picks all defaults (template=minimal, repo=inmemory,
# no extras, git+install on, pm resolved from corepack/lockfile)
node ../packages/cli/bin.js new my-example-api --yes --no-install --force
# Or specify each flag explicitly when you want a non-default template/repo
node ../packages/cli/bin.js new my-example-api \
--template rest --pm pnpm --repo postgres --no-git --no-install --force
```
`--yes` (alias `--non-interactive`) bypasses every prompt with safe defaults; explicit flags override individual answers. Without `--yes`, every unset flag prompts interactively.
Available flags: `--template rest|minimal|fullstack`, `--pm pnpm|npm|yarn|bun`, `--repo inmemory|<any-name>`, `--packages auth,swagger,...`, `--no-git`, `--no-install`, `--force`, `-y / --yes / --non-interactive`.
3. Update generated `package.json`:
- Rename to `@forinda/kickjs-example-<name>`
- Set `"private": true`
- Replace published `@forinda/kickjs*` deps with `workspace:*` references
4. `pnpm-workspace.yaml` already includes `examples/*` — no change needed
5. Add row to Example Apps table in `README.md`
6. Run `pnpm install && pnpm build` from root to verify
### Decorators
```ts
@Controller('/path') // Route prefix
@Get('/'), @Post('/'), @Put('/'), @Delete('/'), @Patch('/')
@Service() // DI-registered singleton
@Repository() // DI-registered singleton (semantic)
@Autowired() // Property injection
@Inject('token') // Token-based injection
@Value('ENV_VAR') // Config value injection
@Middleware(fn) // Attach middleware
@Public() // Opt out of auth
@Roles('admin') // Role-based access
@Cron('0 * * * *') // Cron schedule
```
### Context Contributors (#107)
Typed, ordered, declarative way to populate `ctx.set('key', value)` before a handler runs. Use this **instead of `@Middleware()`** when the only job is to compute a value other code reads off `ctx`.
```ts
const LoadTenant = defineContextDecorator({
key: 'tenant',
deps: { repo: TENANT_REPO }, // typed DI
resolve: (ctx, { repo }) => repo.findById(ctx.req.headers['x-tenant-id'] as string),
})
const LoadProject = defineContextDecorator({
key: 'project',
dependsOn: ['tenant'], // topo-sorted at startup; cycles fail boot
resolve: (ctx) => projectsRepo.find(ctx.get('tenant')!.id, ctx.params.id),
})
@LoadTenant
@LoadProject
@Get('/projects/:id')
getProject(ctx: RequestContext) { ctx.json(ctx.get('project')) }
```
Five registration sites, precedence high→low: **method > class > module > adapter > global**. Apply via `@`-decorator (method/class), `AppModule.contributors?()`, `AppAdapter.contributors?()`, or `bootstrap({ contributors })`. Full guide at `docs/guide/context-decorators.md`. Do NOT use this for short-circuiting responses, response-stream mutation, or pre-route-matching middleware — keep using `@Middleware()` / global middleware for those.
### RequestContext
Every controller method receives `ctx: RequestContext` with:
- `ctx.body`, `ctx.params`, `ctx.query`, `ctx.headers`
- `ctx.requestId`, `ctx.session`, `ctx.file`, `ctx.files`
- `ctx.qs(fieldConfig)` — parsed query with filters/sort/pagination
- `ctx.paginate(handler, config)` — auto-paginated response
- `ctx.json(data)`, `ctx.created(data)`, `ctx.noContent()`, `ctx.notFound()`
### Built-in Middleware
```ts
import express from 'express'
import { bootstrap, helmet, cors, requestId, requestLogger, csrf, rateLimit } from '@forinda/kickjs'
bootstrap({
modules: [/* your modules */],
- middleware: [
+ middlewares: [
helmet(), // Security headers (X-Frame-Options, HSTS, etc.)
cors({ origin: ['https://app.example.com'] }), // CORS with spec-correct behavior
requestId(), // X-Request-Id generation/propagation
requestLogger(), // Pino-based request logging (method, URL, status, duration)
csrf(), // CSRF protection (double-submit cookie)
rateLimit(), // Rate limiting with pluggable store
express.json(), // Body parsing
],
})
```
Also available: `validate()` (Zod body/query/params), `upload()` (multer file handling), `session()` (cookie sessions).
### Git Workflow
Use feature branches — never commit directly to `main` or `dev`:
- **Stable work** → branch from `main`, PR to `main`
- **Experimental work** → branch from `dev`, PR to `dev`
- **Promote** → PR `dev` → `main` when stable
```bash
git checkout main && git pull origin main
git checkout -b feat/my-feature
# ... make changes ...
git commit -m "feat: description (#issue)"
git push -u origin feat/my-feature
gh pr create --base main
```
### PR / issue bodies with markdown — always via a temp file
Anything richer than a single-line description (code fences, lists, tables, backticks, `$`, multi-paragraph) goes through a temp file. **Never** inline a multi-line body in `gh pr create --body "$(cat <<'EOF' ... EOF)"` or `gh pr edit --body "..."` — the shell escapes backticks and dollar signs, fenced code blocks lose their language hint, and the body lands on GitHub with literal `\`` everywhere.
```bash
# Right — write the body to a file, then pass it:
cat > /tmp/pr-body.md <<'EOF'
## Why
…full markdown, no escape gymnastics…
EOF
gh pr create --base main --title "…" --body-file /tmp/pr-body.md
# For edits to existing PRs, gh pr edit can also take --body-file:
gh pr edit 123 --body-file /tmp/pr-body.md
# If `gh pr edit` exits non-zero (often: projects-classic deprecation),
# fall back to the API:
gh api -X PATCH /repos/<owner>/<repo>/pulls/123 -F body=@/tmp/pr-body.md
```
Same rule for `gh issue create`, `gh release create`, comment posts (`gh pr comment 123 --body-file …`), and changeset bodies committed to disk.
## CLI Architecture
The CLI (`packages/cli/`) is structured as:
```
src/
cli.ts # Entry point, registers all commands
config.ts # KickConfig, ModuleConfig, defineConfig, resolveModuleConfig
commands/
generate.ts # kick g module/controller/service/...
remove.ts # kick rm module
init.ts # kick new
run.ts # kick dev/build/start
add.ts # kick add <package>
generators/
module.ts # generateModule orchestrator
remove-module.ts # removeModule + index.ts cleanup
patterns/ # Pattern-specific generators
rest.ts, minimal.ts
types.ts # ModuleContext interface
templates/ # Code template functions
types.ts # TemplateContext interface
repository.ts # inmemory + custom repo generators
drizzle/index.ts # Drizzle-specific templates
prisma/index.ts # Prisma-specific templates
controller.ts, dtos.ts, domain.ts, ...
```
### Key CLI Config (kick.config.ts)
```ts
export default defineConfig({
pattern: 'rest',
modules: {
dir: 'src/modules',
- repo: 'postgres', // 'inmemory' (built-in) | any name → custom stub
+ repo: { name: 'postgres' }, // 'inmemory' (built-in) | { name } → custom stub
pluralize: true,
schemaDir: 'src/db/schema',
},
commands: [...],
})
```
### Template Functions
All template generators accept `TemplateContext`:
```ts
interface TemplateContext {
pascal: string // PascalCase name
kebab: string // kebab-case name
plural?: string // Pluralized kebab
pluralPascal?: string // Pluralized Pascal
repoPrefix?: string // Repository import prefix
dtoPrefix?: string // DTO import prefix
repoType?: string // Custom repo type name
}
```
## Prisma Adapter
- `PrismaAdapter` — registers client in DI, supports Prisma 5/6/7
- `PrismaModelDelegate` — typed CRUD interface for cast-free repos
- `PrismaQueryAdapter` — translates ParsedQuery to findMany args
- `PrismaQueryConfig<TModel>` — generic validates searchColumns against model fields
- Logging: `$on` for v5/6, `$extends` for v7 (auto-detected)
## Linking the CLI Locally
```bash
pnpm build
cd packages/cli && pnpm link --global
```
Now `kick` uses your latest local code. After changes, just `pnpm build` — no re-link needed.
## Testing
- Tests live in `tests/` at root and `packages/*/src/**/*.test.ts`
- Use `Container.reset()` in `beforeEach` to isolate DI state
- Run: `pnpm test`
## Releasing
Versions are **per-package independent** via [Changesets](https://github.com/changesets/changesets); publish is automated via [npm trusted publishers](https://docs.npmjs.com/trusted-publishers/) (OIDC, no `NPM_TOKEN`).
```bash
pnpm changeset # add a changeset describing your PR's changes
pnpm changeset:status # preview pending bumps
pnpm release:enter:alpha # enter pre-release mode (alpha/beta/rc)
pnpm release:exit:pre # exit pre-release mode
```
`.github/workflows/release.yml` does the rest: it opens a "Version Packages" PR when changesets are pending, then publishes when that PR merges. See `RELEASE.md` for the full flow + npm trusted-publisher one-time setup per package.
**Old `scripts/release.js` (lockstep) is removed** — `@forinda/kickjs@5.3.0` may pair with `@forinda/kickjs-cli@5.2.1`. Each changeset picks its own bump per package.
## CI/CD
- **ci.yml** — build, typecheck, test, format on push to main/dev and PRs
- **deploy-docs.yml** — build and deploy VitePress on push to main
- **release.yml** — runs on push to `main` (or `workflow_dispatch`): opens / updates a "Version Packages" PR when changesets are pending, then publishes via npm trusted-publisher OIDC when that PR merges
## Commit Conventions
```
feat: description # New feature → minor bump
fix: description # Bug fix → patch bump
docs: description # Documentation only
chore: description # Maintenance
refactor: description # Code restructuring
ci: description # CI/CD changes
test: description # Test changes
```
## Important Notes
- Decorators fire at class definition time — tests need `Container.reset()` + re-registration
- Don't manually publish — the changesets workflow does it via OIDC. Examples are private, and changesets v3 skips private packages by default — no `ignore` entry needed.
- All internal links in docs must be **relative** (for versioning/i18n support)
- The `kick` CLI binary comes from `packages/cli/src/cli.ts`
- Vite configs: `minify: 'esbuild'`, all runtime deps in `rollupOptions.external`
- `@prisma/client` peer dep is optional (Prisma 7 generates client locally)
- Old top-level config fields (`modulesDir`, `defaultRepo`, etc.) are deprecated — use `modules` block
- **Env wiring**: `src/env.ts` must call `loadEnv(envSchema)` as a side effect AND be imported from `src/index.ts` (`import './env'`) before `bootstrap()` runs. Otherwise `ConfigService.get('CUSTOM_KEY')` returns `undefined` while `@Value('CUSTOM_KEY')` _appears_ to work via its `process.env` fallback. The CLI generators wire both halves automatically; manual upgrades must add both. See `docs/guide/configuration.md#wiring-the-schema-at-startup`.