CLAUDE.md · git:20260831.ddcd0ba · 2026-08-31 · sha256 a6a53e328d23c150

CLAUDE.md git:20260831.ddcd0baA

Immutable. This exact content is served forever at /api/v1/blob/a6a53e328d23c150.

# 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 */],
  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: { 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`.