diff --git a/.agents/skills/turborepo/SKILL.md b/.agents/skills/turborepo/SKILL.md new file mode 100644 index 00000000..f6238d30 --- /dev/null +++ b/.agents/skills/turborepo/SKILL.md @@ -0,0 +1,914 @@ +--- +name: turborepo +description: | + Turborepo monorepo build system guidance. Triggers on: turbo.json, task pipelines, + dependsOn, caching, remote cache, the "turbo" CLI, --filter, --affected, CI optimization, environment + variables, internal packages, monorepo structure/best practices, and boundaries. + + Use when user: configures tasks/workflows/pipelines, creates packages, sets up + monorepo, shares code between apps, runs changed/affected packages, debugs cache, + or has apps/packages directories. +metadata: + version: 2.8.13-canary.12 +--- + +# Turborepo Skill + +Build system for JavaScript/TypeScript monorepos. Turborepo caches task outputs and runs tasks in parallel based on dependency graph. + +## IMPORTANT: Package Tasks, Not Root Tasks + +**DO NOT create Root Tasks. ALWAYS create package tasks.** + +When creating tasks/scripts/pipelines, you MUST: + +1. Add the script to each relevant package's `package.json` +2. Register the task in root `turbo.json` +3. Root `package.json` only delegates via `turbo run ` + +**DO NOT** put task logic in root `package.json`. This defeats Turborepo's parallelization. + +```json +// DO THIS: Scripts in each package +// apps/web/package.json +{ "scripts": { "build": "next build", "lint": "eslint .", "test": "vitest" } } + +// apps/api/package.json +{ "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } } + +// packages/ui/package.json +{ "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } } +``` + +```json +// turbo.json - register tasks +{ + "tasks": { + "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, + "lint": {}, + "test": { "dependsOn": ["build"] } + } +} +``` + +```json +// Root package.json - ONLY delegates, no task logic +{ + "scripts": { + "build": "turbo run build", + "lint": "turbo run lint", + "test": "turbo run test" + } +} +``` + +```json +// DO NOT DO THIS - defeats parallelization +// Root package.json +{ + "scripts": { + "build": "cd apps/web && next build && cd ../api && tsc", + "lint": "eslint apps/ packages/", + "test": "vitest" + } +} +``` + +Root Tasks (`//#taskname`) are ONLY for tasks that truly cannot exist in packages (rare). + +## Secondary Rule: `turbo run` vs `turbo` + +**Always use `turbo run` when the command is written into code:** + +```json +// package.json - ALWAYS "turbo run" +{ + "scripts": { + "build": "turbo run build" + } +} +``` + +```yaml +# CI workflows - ALWAYS "turbo run" +- run: turbo run build --affected +``` + +**The shorthand `turbo ` is ONLY for one-off terminal commands** typed directly by humans or agents. Never write `turbo build` into package.json, CI, or scripts. + +## Quick Decision Trees + +### "I need to configure a task" + +``` +Configure a task? +├─ Define task dependencies → references/configuration/tasks.md +├─ Lint/check-types (parallel + caching) → Use Transit Nodes pattern (see below) +├─ Specify build outputs → references/configuration/tasks.md#outputs +├─ Handle environment variables → references/environment/RULE.md +├─ Set up dev/watch tasks → references/configuration/tasks.md#persistent +├─ Package-specific config → references/configuration/RULE.md#package-configurations +└─ Global settings (cacheDir, daemon) → references/configuration/global-options.md +``` + +### "My cache isn't working" + +``` +Cache problems? +├─ Tasks run but outputs not restored → Missing `outputs` key +├─ Cache misses unexpectedly → references/caching/gotchas.md +├─ Need to debug hash inputs → Use --summarize or --dry +├─ Want to skip cache entirely → Use --force or cache: false +├─ Remote cache not working → references/caching/remote-cache.md +└─ Environment causing misses → references/environment/gotchas.md +``` + +### "I want to run only changed packages" + +``` +Run only what changed? +├─ Changed packages + dependents (RECOMMENDED) → turbo run build --affected +├─ Custom base branch → --affected --affected-base=origin/develop +├─ Manual git comparison → --filter=...[origin/main] +└─ See all filter options → references/filtering/RULE.md +``` + +**`--affected` is the primary way to run only changed packages.** It automatically compares against the default branch and includes dependents. + +### "I want to filter packages" + +``` +Filter packages? +├─ Only changed packages → --affected (see above) +├─ By package name → --filter=web +├─ By directory → --filter=./apps/* +├─ Package + dependencies → --filter=web... +├─ Package + dependents → --filter=...web +└─ Complex combinations → references/filtering/patterns.md +``` + +### "Environment variables aren't working" + +``` +Environment issues? +├─ Vars not available at runtime → Strict mode filtering (default) +├─ Cache hits with wrong env → Var not in `env` key +├─ .env changes not causing rebuilds → .env not in `inputs` +├─ CI variables missing → references/environment/gotchas.md +└─ Framework vars (NEXT_PUBLIC_*) → Auto-included via inference +``` + +### "I need to set up CI" + +``` +CI setup? +├─ GitHub Actions → references/ci/github-actions.md +├─ Vercel deployment → references/ci/vercel.md +├─ Remote cache in CI → references/caching/remote-cache.md +├─ Only build changed packages → --affected flag +├─ Skip unnecessary builds → turbo-ignore (references/cli/commands.md) +└─ Skip container setup when no changes → turbo-ignore +``` + +### "I want to watch for changes during development" + +``` +Watch mode? +├─ Re-run tasks on change → turbo watch (references/watch/RULE.md) +├─ Dev servers with dependencies → Use `with` key (references/configuration/tasks.md#with) +├─ Restart dev server on dep change → Use `interruptible: true` +└─ Persistent dev tasks → Use `persistent: true` +``` + +### "I need to create/structure a package" + +``` +Package creation/structure? +├─ Create an internal package → references/best-practices/packages.md +├─ Repository structure → references/best-practices/structure.md +├─ Dependency management → references/best-practices/dependencies.md +├─ Best practices overview → references/best-practices/RULE.md +├─ JIT vs Compiled packages → references/best-practices/packages.md#compilation-strategies +└─ Sharing code between apps → references/best-practices/RULE.md#package-types +``` + +### "How should I structure my monorepo?" + +``` +Monorepo structure? +├─ Standard layout (apps/, packages/) → references/best-practices/RULE.md +├─ Package types (apps vs libraries) → references/best-practices/RULE.md#package-types +├─ Creating internal packages → references/best-practices/packages.md +├─ TypeScript configuration → references/best-practices/structure.md#typescript-configuration +├─ ESLint configuration → references/best-practices/structure.md#eslint-configuration +├─ Dependency management → references/best-practices/dependencies.md +└─ Enforce package boundaries → references/boundaries/RULE.md +``` + +### "I want to enforce architectural boundaries" + +``` +Enforce boundaries? +├─ Check for violations → turbo boundaries +├─ Tag packages → references/boundaries/RULE.md#tags +├─ Restrict which packages can import others → references/boundaries/RULE.md#rule-types +└─ Prevent cross-package file imports → references/boundaries/RULE.md +``` + +## Critical Anti-Patterns + +### Using `turbo` Shorthand in Code + +**`turbo run` is recommended in package.json scripts and CI pipelines.** The shorthand `turbo ` is intended for interactive terminal use. + +```json +// WRONG - using shorthand in package.json +{ + "scripts": { + "build": "turbo build", + "dev": "turbo dev" + } +} + +// CORRECT +{ + "scripts": { + "build": "turbo run build", + "dev": "turbo run dev" + } +} +``` + +```yaml +# WRONG - using shorthand in CI +- run: turbo build --affected + +# CORRECT +- run: turbo run build --affected +``` + +### Root Scripts Bypassing Turbo + +Root `package.json` scripts MUST delegate to `turbo run`, not run tasks directly. + +```json +// WRONG - bypasses turbo entirely +{ + "scripts": { + "build": "bun build", + "dev": "bun dev" + } +} + +// CORRECT - delegates to turbo +{ + "scripts": { + "build": "turbo run build", + "dev": "turbo run dev" + } +} +``` + +### Using `&&` to Chain Turbo Tasks + +Don't chain turbo tasks with `&&`. Let turbo orchestrate. + +```json +// WRONG - turbo task not using turbo run +{ + "scripts": { + "changeset:publish": "bun build && changeset publish" + } +} + +// CORRECT +{ + "scripts": { + "changeset:publish": "turbo run build && changeset publish" + } +} +``` + +### `prebuild` Scripts That Manually Build Dependencies + +Scripts like `prebuild` that manually build other packages bypass Turborepo's dependency graph. + +```json +// WRONG - manually building dependencies +{ + "scripts": { + "prebuild": "cd ../../packages/types && bun run build && cd ../utils && bun run build", + "build": "next build" + } +} +``` + +**However, the fix depends on whether workspace dependencies are declared:** + +1. **If dependencies ARE declared** (e.g., `"@repo/types": "workspace:*"` in package.json), remove the `prebuild` script. Turbo's `dependsOn: ["^build"]` handles this automatically. + +2. **If dependencies are NOT declared**, the `prebuild` exists because `^build` won't trigger without a dependency relationship. The fix is to: + - Add the dependency to package.json: `"@repo/types": "workspace:*"` + - Then remove the `prebuild` script + +```json +// CORRECT - declare dependency, let turbo handle build order +// package.json +{ + "dependencies": { + "@repo/types": "workspace:*", + "@repo/utils": "workspace:*" + }, + "scripts": { + "build": "next build" + } +} + +// turbo.json +{ + "tasks": { + "build": { + "dependsOn": ["^build"] + } + } +} +``` + +**Key insight:** `^build` only runs build in packages listed as dependencies. No dependency declaration = no automatic build ordering. + +### Overly Broad `globalDependencies` + +`globalDependencies` affects ALL tasks in ALL packages. Be specific. + +```json +// WRONG - heavy hammer, affects all hashes +{ + "globalDependencies": ["**/.env.*local"] +} + +// BETTER - move to task-level inputs +{ + "globalDependencies": [".env"], + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**"] + } + } +} +``` + +### Repetitive Task Configuration + +Look for repeated configuration across tasks that can be collapsed. Turborepo supports shared configuration patterns. + +```json +// WRONG - repetitive env and inputs across tasks +{ + "tasks": { + "build": { + "env": ["API_URL", "DATABASE_URL"], + "inputs": ["$TURBO_DEFAULT$", ".env*"] + }, + "test": { + "env": ["API_URL", "DATABASE_URL"], + "inputs": ["$TURBO_DEFAULT$", ".env*"] + }, + "dev": { + "env": ["API_URL", "DATABASE_URL"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "cache": false, + "persistent": true + } + } +} + +// BETTER - use globalEnv and globalDependencies for shared config +{ + "globalEnv": ["API_URL", "DATABASE_URL"], + "globalDependencies": [".env*"], + "tasks": { + "build": {}, + "test": {}, + "dev": { + "cache": false, + "persistent": true + } + } +} +``` + +**When to use global vs task-level:** + +- `globalEnv` / `globalDependencies` - affects ALL tasks, use for truly shared config +- Task-level `env` / `inputs` - use when only specific tasks need it + +### NOT an Anti-Pattern: Large `env` Arrays + +A large `env` array (even 50+ variables) is **not** a problem. It usually means the user was thorough about declaring their build's environment dependencies. Do not flag this as an issue. + +### Using `--parallel` Flag + +The `--parallel` flag bypasses Turborepo's dependency graph. If tasks need parallel execution, configure `dependsOn` correctly instead. + +```bash +# WRONG - bypasses dependency graph +turbo run lint --parallel + +# CORRECT - configure tasks to allow parallel execution +# In turbo.json, set dependsOn appropriately (or use transit nodes) +turbo run lint +``` + +### Package-Specific Task Overrides in Root turbo.json + +When multiple packages need different task configurations, use **Package Configurations** (`turbo.json` in each package) instead of cluttering root `turbo.json` with `package#task` overrides. + +```json +// WRONG - root turbo.json with many package-specific overrides +{ + "tasks": { + "test": { "dependsOn": ["build"] }, + "@repo/web#test": { "outputs": ["coverage/**"] }, + "@repo/api#test": { "outputs": ["coverage/**"] }, + "@repo/utils#test": { "outputs": [] }, + "@repo/cli#test": { "outputs": [] }, + "@repo/core#test": { "outputs": [] } + } +} + +// CORRECT - use Package Configurations +// Root turbo.json - base config only +{ + "tasks": { + "test": { "dependsOn": ["build"] } + } +} + +// packages/web/turbo.json - package-specific override +{ + "extends": ["//"], + "tasks": { + "test": { "outputs": ["coverage/**"] } + } +} + +// packages/api/turbo.json +{ + "extends": ["//"], + "tasks": { + "test": { "outputs": ["coverage/**"] } + } +} +``` + +**Benefits of Package Configurations:** + +- Keeps configuration close to the code it affects +- Root turbo.json stays clean and focused on base patterns +- Easier to understand what's special about each package +- Works with `$TURBO_EXTENDS$` to inherit + extend arrays + +**When to use `package#task` in root:** + +- Single package needs a unique dependency (e.g., `"deploy": { "dependsOn": ["web#build"] }`) +- Temporary override while migrating + +See `references/configuration/RULE.md#package-configurations` for full details. + +### Using `../` to Traverse Out of Package in `inputs` + +Don't use relative paths like `../` to reference files outside the package. Use `$TURBO_ROOT$` instead. + +```json +// WRONG - traversing out of package +{ + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", "../shared-config.json"] + } + } +} + +// CORRECT - use $TURBO_ROOT$ for repo root +{ + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/shared-config.json"] + } + } +} +``` + +### Missing `outputs` for File-Producing Tasks + +**Before flagging missing `outputs`, check what the task actually produces:** + +1. Read the package's script (e.g., `"build": "tsc"`, `"test": "vitest"`) +2. Determine if it writes files to disk or only outputs to stdout +3. Only flag if the task produces files that should be cached + +```json +// WRONG: build produces files but they're not cached +{ + "tasks": { + "build": { + "dependsOn": ["^build"] + } + } +} + +// CORRECT: build outputs are cached +{ + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] + } + } +} +``` + +Common outputs by framework: + +- Next.js: `[".next/**", "!.next/cache/**"]` +- Vite/Rollup: `["dist/**"]` +- tsc: `["dist/**"]` or custom `outDir` + +**TypeScript `--noEmit` can still produce cache files:** + +When `incremental: true` in tsconfig.json, `tsc --noEmit` writes `.tsbuildinfo` files even without emitting JS. Check the tsconfig before assuming no outputs: + +```json +// If tsconfig has incremental: true, tsc --noEmit produces cache files +{ + "tasks": { + "typecheck": { + "outputs": ["node_modules/.cache/tsbuildinfo.json"] // or wherever tsBuildInfoFile points + } + } +} +``` + +To determine correct outputs for TypeScript tasks: + +1. Check if `incremental` or `composite` is enabled in tsconfig +2. Check `tsBuildInfoFile` for custom cache location (default: alongside `outDir` or in project root) +3. If no incremental mode, `tsc --noEmit` produces no files + +### `^build` vs `build` Confusion + +```json +{ + "tasks": { + // ^build = run build in DEPENDENCIES first (other packages this one imports) + "build": { + "dependsOn": ["^build"] + }, + // build (no ^) = run build in SAME PACKAGE first + "test": { + "dependsOn": ["build"] + }, + // pkg#task = specific package's task + "deploy": { + "dependsOn": ["web#build"] + } + } +} +``` + +### Environment Variables Not Hashed + +```json +// WRONG: API_URL changes won't cause rebuilds +{ + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} + +// CORRECT: API_URL changes invalidate cache +{ + "tasks": { + "build": { + "outputs": ["dist/**"], + "env": ["API_URL", "API_KEY"] + } + } +} +``` + +### `.env` Files Not in Inputs + +Turbo does NOT load `.env` files - your framework does. But Turbo needs to know about changes: + +```json +// WRONG: .env changes don't invalidate cache +{ + "tasks": { + "build": { + "env": ["API_URL"] + } + } +} + +// CORRECT: .env file changes invalidate cache +{ + "tasks": { + "build": { + "env": ["API_URL"], + "inputs": ["$TURBO_DEFAULT$", ".env", ".env.*"] + } + } +} +``` + +### Root `.env` File in Monorepo + +A `.env` file at the repo root is an anti-pattern — even for small monorepos or starter templates. It creates implicit coupling between packages and makes it unclear which packages depend on which variables. + +``` +// WRONG - root .env affects all packages implicitly +my-monorepo/ +├── .env # Which packages use this? +├── apps/ +│ ├── web/ +│ └── api/ +└── packages/ + +// CORRECT - .env files in packages that need them +my-monorepo/ +├── apps/ +│ ├── web/ +│ │ └── .env # Clear: web needs DATABASE_URL +│ └── api/ +│ └── .env # Clear: api needs API_KEY +└── packages/ +``` + +**Problems with root `.env`:** + +- Unclear which packages consume which variables +- All packages get all variables (even ones they don't need) +- Cache invalidation is coarse-grained (root .env change invalidates everything) +- Security risk: packages may accidentally access sensitive vars meant for others +- Bad habits start small — starter templates should model correct patterns + +**If you must share variables**, use `globalEnv` to be explicit about what's shared, and document why. + +### Strict Mode Filtering CI Variables + +By default, Turborepo filters environment variables to only those in `env`/`globalEnv`. CI variables may be missing: + +```json +// If CI scripts need GITHUB_TOKEN but it's not in env: +{ + "globalPassThroughEnv": ["GITHUB_TOKEN", "CI"], + "tasks": { ... } +} +``` + +Or use `--env-mode=loose` (not recommended for production). + +### Shared Code in Apps (Should Be a Package) + +``` +// WRONG: Shared code inside an app +apps/ + web/ + shared/ # This breaks monorepo principles! + utils.ts + +// CORRECT: Extract to a package +packages/ + utils/ + src/utils.ts +``` + +### Accessing Files Across Package Boundaries + +```typescript +// WRONG: Reaching into another package's internals +import { Button } from "../../packages/ui/src/button"; + +// CORRECT: Install and import properly +import { Button } from "@repo/ui/button"; +``` + +### Too Many Root Dependencies + +```json +// WRONG: App dependencies in root +{ + "dependencies": { + "react": "^18", + "next": "^14" + } +} + +// CORRECT: Only repo tools in root +{ + "devDependencies": { + "turbo": "latest" + } +} +``` + +## Common Task Configurations + +### Standard Build Pipeline + +```json +{ + "$schema": "https://v2-8-13-canary-12.turborepo.dev/schema.json", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", ".next/**", "!.next/cache/**"] + }, + "dev": { + "cache": false, + "persistent": true + } + } +} +``` + +Add a `transit` task if you have tasks that need parallel execution with cache invalidation (see below). + +### Dev Task with `^dev` Pattern (for `turbo watch`) + +A `dev` task with `dependsOn: ["^dev"]` and `persistent: false` in root turbo.json may look unusual but is **correct for `turbo watch` workflows**: + +```json +// Root turbo.json +{ + "tasks": { + "dev": { + "dependsOn": ["^dev"], + "cache": false, + "persistent": false // Packages have one-shot dev scripts + } + } +} + +// Package turbo.json (apps/web/turbo.json) +{ + "extends": ["//"], + "tasks": { + "dev": { + "persistent": true // Apps run long-running dev servers + } + } +} +``` + +**Why this works:** + +- **Packages** (e.g., `@acme/db`, `@acme/validators`) have `"dev": "tsc"` — one-shot type generation that completes quickly +- **Apps** override with `persistent: true` for actual dev servers (Next.js, etc.) +- **`turbo watch`** re-runs the one-shot package `dev` scripts when source files change, keeping types in sync + +**Intended usage:** Run `turbo watch dev` (not `turbo run dev`). Watch mode re-executes one-shot tasks on file changes while keeping persistent tasks running. + +**Alternative pattern:** Use a separate task name like `prepare` or `generate` for one-shot dependency builds to make the intent clearer: + +```json +{ + "tasks": { + "prepare": { + "dependsOn": ["^prepare"], + "outputs": ["dist/**"] + }, + "dev": { + "dependsOn": ["prepare"], + "cache": false, + "persistent": true + } + } +} +``` + +### Transit Nodes for Parallel Tasks with Cache Invalidation + +Some tasks can run in parallel (don't need built output from dependencies) but must invalidate cache when dependency source code changes. + +**The problem with `dependsOn: ["^taskname"]`:** + +- Forces sequential execution (slow) + +**The problem with `dependsOn: []` (no dependencies):** + +- Allows parallel execution (fast) +- But cache is INCORRECT - changing dependency source won't invalidate cache + +**Transit Nodes solve both:** + +```json +{ + "tasks": { + "transit": { "dependsOn": ["^transit"] }, + "my-task": { "dependsOn": ["transit"] } + } +} +``` + +The `transit` task creates dependency relationships without matching any actual script, so tasks run in parallel with correct cache invalidation. + +**How to identify tasks that need this pattern:** Look for tasks that read source files from dependencies but don't need their build outputs. + +### With Environment Variables + +```json +{ + "globalEnv": ["NODE_ENV"], + "globalDependencies": [".env"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"], + "env": ["API_URL", "DATABASE_URL"] + } + } +} +``` + +## Reference Index + +### Configuration + +| File | Purpose | +| ------------------------------------------------------------------------------- | -------------------------------------------------------- | +| [configuration/RULE.md](./references/configuration/RULE.md) | turbo.json overview, Package Configurations | +| [configuration/tasks.md](./references/configuration/tasks.md) | dependsOn, outputs, inputs, env, cache, persistent | +| [configuration/global-options.md](./references/configuration/global-options.md) | globalEnv, globalDependencies, cacheDir, daemon, envMode | +| [configuration/gotchas.md](./references/configuration/gotchas.md) | Common configuration mistakes | + +### Caching + +| File | Purpose | +| --------------------------------------------------------------- | -------------------------------------------- | +| [caching/RULE.md](./references/caching/RULE.md) | How caching works, hash inputs | +| [caching/remote-cache.md](./references/caching/remote-cache.md) | Vercel Remote Cache, self-hosted, login/link | +| [caching/gotchas.md](./references/caching/gotchas.md) | Debugging cache misses, --summarize, --dry | + +### Environment Variables + +| File | Purpose | +| ------------------------------------------------------------- | ----------------------------------------- | +| [environment/RULE.md](./references/environment/RULE.md) | env, globalEnv, passThroughEnv | +| [environment/modes.md](./references/environment/modes.md) | Strict vs Loose mode, framework inference | +| [environment/gotchas.md](./references/environment/gotchas.md) | .env files, CI issues | + +### Filtering + +| File | Purpose | +| ----------------------------------------------------------- | ------------------------ | +| [filtering/RULE.md](./references/filtering/RULE.md) | --filter syntax overview | +| [filtering/patterns.md](./references/filtering/patterns.md) | Common filter patterns | + +### CI/CD + +| File | Purpose | +| --------------------------------------------------------- | ------------------------------- | +| [ci/RULE.md](./references/ci/RULE.md) | General CI principles | +| [ci/github-actions.md](./references/ci/github-actions.md) | Complete GitHub Actions setup | +| [ci/vercel.md](./references/ci/vercel.md) | Vercel deployment, turbo-ignore | +| [ci/patterns.md](./references/ci/patterns.md) | --affected, caching strategies | + +### CLI + +| File | Purpose | +| ----------------------------------------------- | --------------------------------------------- | +| [cli/RULE.md](./references/cli/RULE.md) | turbo run basics | +| [cli/commands.md](./references/cli/commands.md) | turbo run flags, turbo-ignore, other commands | + +### Best Practices + +| File | Purpose | +| ----------------------------------------------------------------------------- | --------------------------------------------------------------- | +| [best-practices/RULE.md](./references/best-practices/RULE.md) | Monorepo best practices overview | +| [best-practices/structure.md](./references/best-practices/structure.md) | Repository structure, workspace config, TypeScript/ESLint setup | +| [best-practices/packages.md](./references/best-practices/packages.md) | Creating internal packages, JIT vs Compiled, exports | +| [best-practices/dependencies.md](./references/best-practices/dependencies.md) | Dependency management, installing, version sync | + +### Watch Mode + +| File | Purpose | +| ------------------------------------------- | ----------------------------------------------- | +| [watch/RULE.md](./references/watch/RULE.md) | turbo watch, interruptible tasks, dev workflows | + +### Boundaries (Experimental) + +| File | Purpose | +| ----------------------------------------------------- | ----------------------------------------------------- | +| [boundaries/RULE.md](./references/boundaries/RULE.md) | Enforce package isolation, tag-based dependency rules | + +## Source Documentation + +This skill is based on the official Turborepo documentation at: + +- Source: `apps/docs/content/docs/` in the Turborepo repository +- Live: https://turborepo.dev/docs diff --git a/.agents/skills/turborepo/command/turborepo.md b/.agents/skills/turborepo/command/turborepo.md new file mode 100644 index 00000000..8323edcb --- /dev/null +++ b/.agents/skills/turborepo/command/turborepo.md @@ -0,0 +1,70 @@ +--- +description: Load Turborepo skill for creating workflows, tasks, and pipelines in monorepos. Use when users ask to "create a workflow", "make a task", "generate a pipeline", or set up build orchestration. +--- + +Load the Turborepo skill and help with monorepo task orchestration: creating workflows, configuring tasks, setting up pipelines, and optimizing builds. + +## Workflow + +### Step 1: Load turborepo skill + +``` +skill({ name: 'turborepo' }) +``` + +### Step 2: Identify task type from user request + +Analyze $ARGUMENTS to determine: + +- **Topic**: configuration, caching, filtering, environment, CI, or CLI +- **Task type**: new setup, debugging, optimization, or implementation + +Use decision trees in SKILL.md to select the relevant reference files. + +### Step 3: Read relevant reference files + +Based on task type, read from `references//`: + +| Task | Files to Read | +| -------------------- | ------------------------------------------------------- | +| Configure turbo.json | `configuration/RULE.md` + `configuration/tasks.md` | +| Debug cache issues | `caching/gotchas.md` | +| Set up remote cache | `caching/remote-cache.md` | +| Filter packages | `filtering/RULE.md` + `filtering/patterns.md` | +| Environment problems | `environment/gotchas.md` + `environment/modes.md` | +| Set up CI | `ci/RULE.md` + `ci/github-actions.md` or `ci/vercel.md` | +| CLI usage | `cli/commands.md` | + +### Step 4: Execute task + +Apply Turborepo-specific patterns from references to complete the user's request. + +**CRITICAL - When creating tasks/scripts/pipelines:** + +1. **DO NOT create Root Tasks** - Always create package tasks +2. Add scripts to each relevant package's `package.json` (e.g., `apps/web/package.json`, `packages/ui/package.json`) +3. Register the task in root `turbo.json` +4. Root `package.json` only contains `turbo run ` - never actual task logic + +**Other things to verify:** + +- `outputs` defined for cacheable tasks +- `dependsOn` uses correct syntax (`^task` vs `task`) +- Environment variables in `env` key +- `.env` files in `inputs` if used +- Use `turbo run` (not `turbo`) in package.json and CI + +### Step 5: Summarize + +``` +=== Turborepo Task Complete === + +Topic: +Files referenced: + + +``` + + +$ARGUMENTS + diff --git a/.agents/skills/turborepo/references/best-practices/RULE.md b/.agents/skills/turborepo/references/best-practices/RULE.md new file mode 100644 index 00000000..4870784d --- /dev/null +++ b/.agents/skills/turborepo/references/best-practices/RULE.md @@ -0,0 +1,241 @@ +# Monorepo Best Practices + +Essential patterns for structuring and maintaining a healthy Turborepo monorepo. + +## Repository Structure + +### Standard Layout + +``` +my-monorepo/ +├── apps/ # Application packages (deployable) +│ ├── web/ +│ ├── docs/ +│ └── api/ +├── packages/ # Library packages (shared code) +│ ├── ui/ +│ ├── utils/ +│ └── config-*/ # Shared configs (eslint, typescript, etc.) +├── package.json # Root package.json (minimal deps) +├── turbo.json # Turborepo configuration +├── pnpm-workspace.yaml # (pnpm) or workspaces in package.json +└── pnpm-lock.yaml # Lockfile (required) +``` + +### Key Principles + +1. **`apps/` for deployables**: Next.js sites, APIs, CLIs - things that get deployed +2. **`packages/` for libraries**: Shared code consumed by apps or other packages +3. **One purpose per package**: Each package should do one thing well +4. **No nested packages**: Don't put packages inside packages + +## Package Types + +### Application Packages (`apps/`) + +- **Deployable**: These are the "endpoints" of your package graph +- **Not installed by other packages**: Apps shouldn't be dependencies of other packages +- **No shared code**: If code needs sharing, extract to `packages/` + +```json +// apps/web/package.json +{ + "name": "web", + "private": true, + "dependencies": { + "@repo/ui": "workspace:*", + "next": "latest" + } +} +``` + +### Library Packages (`packages/`) + +- **Shared code**: Utilities, components, configs +- **Namespaced names**: Use `@repo/` or `@yourorg/` prefix +- **Clear exports**: Define what the package exposes + +```json +// packages/ui/package.json +{ + "name": "@repo/ui", + "exports": { + "./button": "./src/button.tsx", + "./card": "./src/card.tsx" + } +} +``` + +## Package Compilation Strategies + +### Just-in-Time (Simplest) + +Export TypeScript directly; let the app's bundler compile it. + +```json +{ + "name": "@repo/ui", + "exports": { + "./button": "./src/button.tsx" + } +} +``` + +**Pros**: Zero build config, instant changes +**Cons**: Can't cache builds, requires app bundler support + +### Compiled (Recommended for Libraries) + +Package compiles itself with `tsc` or bundler. + +```json +{ + "name": "@repo/ui", + "exports": { + "./button": { + "types": "./src/button.tsx", + "default": "./dist/button.js" + } + }, + "scripts": { + "build": "tsc" + } +} +``` + +**Pros**: Cacheable by Turborepo, works everywhere +**Cons**: More configuration + +## Dependency Management + +### Install Where Used + +Install dependencies in the package that uses them, not the root. + +```bash +# Good: Install in the package that needs it +pnpm add lodash --filter=@repo/utils + +# Avoid: Installing everything at root +pnpm add lodash -w # Only for repo-level tools +``` + +### Root Dependencies + +Only these belong in root `package.json`: + +- `turbo` - The build system +- `husky`, `lint-staged` - Git hooks +- Repository-level tooling + +### Internal Dependencies + +Use workspace protocol for internal packages: + +```json +// pnpm/bun +{ "@repo/ui": "workspace:*" } + +// npm/yarn +{ "@repo/ui": "*" } +``` + +## Exports Best Practices + +### Use `exports` Field (Not `main`) + +```json +{ + "exports": { + ".": "./src/index.ts", + "./button": "./src/button.tsx", + "./utils": "./src/utils.ts" + } +} +``` + +### Avoid Barrel Files + +Don't create `index.ts` files that re-export everything: + +```typescript +// BAD: packages/ui/src/index.ts +export * from './button'; +export * from './card'; +export * from './modal'; +// ... imports everything even if you need one thing + +// GOOD: Direct exports in package.json +{ + "exports": { + "./button": "./src/button.tsx", + "./card": "./src/card.tsx" + } +} +``` + +### Namespace Your Packages + +```json +// Good +{ "name": "@repo/ui" } +{ "name": "@acme/utils" } + +// Avoid (conflicts with npm registry) +{ "name": "ui" } +{ "name": "utils" } +``` + +## Common Anti-Patterns + +### Accessing Files Across Package Boundaries + +```typescript +// BAD: Reaching into another package +import { Button } from "../../packages/ui/src/button"; + +// GOOD: Install and import properly +import { Button } from "@repo/ui/button"; +``` + +### Shared Code in Apps + +``` +// BAD +apps/ + web/ + shared/ # This should be a package! + utils.ts + +// GOOD +packages/ + utils/ # Proper shared package + src/utils.ts +``` + +### Too Many Root Dependencies + +```json +// BAD: Root has app dependencies +{ + "dependencies": { + "react": "^18", + "next": "^14", + "lodash": "^4" + } +} + +// GOOD: Root only has repo tools +{ + "devDependencies": { + "turbo": "latest", + "husky": "latest" + } +} +``` + +## See Also + +- [structure.md](./structure.md) - Detailed repository structure patterns +- [packages.md](./packages.md) - Creating and managing internal packages +- [dependencies.md](./dependencies.md) - Dependency management strategies diff --git a/.agents/skills/turborepo/references/best-practices/dependencies.md b/.agents/skills/turborepo/references/best-practices/dependencies.md new file mode 100644 index 00000000..90902e29 --- /dev/null +++ b/.agents/skills/turborepo/references/best-practices/dependencies.md @@ -0,0 +1,246 @@ +# Dependency Management + +Best practices for managing dependencies in a Turborepo monorepo. + +## Core Principle: Install Where Used + +Dependencies belong in the package that uses them, not the root. + +```bash +# Good: Install in specific package +pnpm add react --filter=@repo/ui +pnpm add next --filter=web + +# Avoid: Installing in root +pnpm add react -w # Only for repo-level tools! +``` + +## Benefits of Local Installation + +### 1. Clarity + +Each package's `package.json` lists exactly what it needs: + +```json +// packages/ui/package.json +{ + "dependencies": { + "react": "^18.0.0", + "class-variance-authority": "^0.7.0" + } +} +``` + +### 2. Flexibility + +Different packages can use different versions when needed: + +```json +// packages/legacy-ui/package.json +{ "dependencies": { "react": "^17.0.0" } } + +// packages/ui/package.json +{ "dependencies": { "react": "^18.0.0" } } +``` + +### 3. Better Caching + +Installing in root changes workspace lockfile, invalidating all caches. + +### 4. Pruning Support + +`turbo prune` can remove unused dependencies for Docker images. + +## What Belongs in Root + +Only repository-level tools: + +```json +// Root package.json +{ + "devDependencies": { + "turbo": "latest", + "husky": "^8.0.0", + "lint-staged": "^15.0.0" + } +} +``` + +**NOT** application dependencies: + +- react, next, express +- lodash, axios, zod +- Testing libraries (unless truly repo-wide) + +## Installing Dependencies + +### Single Package + +```bash +# pnpm +pnpm add lodash --filter=@repo/utils + +# npm +npm install lodash --workspace=@repo/utils + +# yarn +yarn workspace @repo/utils add lodash + +# bun +cd packages/utils && bun add lodash +``` + +### Multiple Packages + +```bash +# pnpm +pnpm add jest --save-dev --filter=web --filter=@repo/ui + +# npm +npm install jest --save-dev --workspace=web --workspace=@repo/ui + +# yarn (v2+) +yarn workspaces foreach -R --from '{web,@repo/ui}' add jest --dev +``` + +### Internal Packages + +```bash +# pnpm +pnpm add @repo/ui --filter=web + +# This updates package.json: +{ + "dependencies": { + "@repo/ui": "workspace:*" + } +} +``` + +## Keeping Versions in Sync + +### Option 1: Tooling + +```bash +# syncpack - Check and fix version mismatches +npx syncpack list-mismatches +npx syncpack fix-mismatches + +# manypkg - Similar functionality +npx @manypkg/cli check +npx @manypkg/cli fix + +# sherif - Rust-based, very fast +npx sherif +``` + +### Option 2: Package Manager Commands + +```bash +# pnpm - Update everywhere +pnpm up --recursive typescript@latest + +# npm - Update in all workspaces +npm install typescript@latest --workspaces +``` + +### Option 3: pnpm Catalogs (pnpm 9.5+) + +```yaml +# pnpm-workspace.yaml +packages: + - "apps/*" + - "packages/*" + +catalog: + react: ^18.2.0 + typescript: ^5.3.0 +``` + +```json +// Any package.json +{ + "dependencies": { + "react": "catalog:" // Uses version from catalog + } +} +``` + +## Internal vs External Dependencies + +### Internal (Workspace) + +```json +// pnpm/bun +{ "@repo/ui": "workspace:*" } + +// npm/yarn +{ "@repo/ui": "*" } +``` + +Turborepo understands these relationships and orders builds accordingly. + +### External (npm Registry) + +```json +{ "lodash": "^4.17.21" } +``` + +Standard semver versioning from npm. + +## Peer Dependencies + +For library packages that expect the consumer to provide dependencies: + +```json +// packages/ui/package.json +{ + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "devDependencies": { + "react": "^18.0.0", // For development/testing + "react-dom": "^18.0.0" + } +} +``` + +## Common Issues + +### "Module not found" + +1. Check the dependency is installed in the right package +2. Run `pnpm install` / `npm install` to update lockfile +3. Check exports are defined in the package + +### Version Conflicts + +Packages can use different versions - this is a feature, not a bug. But if you need consistency: + +1. Use tooling (syncpack, manypkg) +2. Use pnpm catalogs +3. Create a lint rule + +### Hoisting Issues + +Some tools expect dependencies in specific locations. Use package manager config: + +```yaml +# .npmrc (pnpm) +public-hoist-pattern[]=*eslint* +public-hoist-pattern[]=*prettier* +``` + +## Lockfile + +**Required** for: + +- Reproducible builds +- Turborepo dependency analysis +- Cache correctness + +```bash +# Commit your lockfile! +git add pnpm-lock.yaml # or package-lock.json, yarn.lock +``` diff --git a/.agents/skills/turborepo/references/best-practices/packages.md b/.agents/skills/turborepo/references/best-practices/packages.md new file mode 100644 index 00000000..85cdf040 --- /dev/null +++ b/.agents/skills/turborepo/references/best-practices/packages.md @@ -0,0 +1,335 @@ +# Creating Internal Packages + +How to create and structure internal packages in your monorepo. + +## Package Creation Checklist + +1. Create directory in `packages/` +2. Add `package.json` with name and exports +3. Add source code in `src/` +4. Add `tsconfig.json` if using TypeScript +5. Install as dependency in consuming packages +6. Run package manager install to update lockfile + +## Package Compilation Strategies + +### Just-in-Time (JIT) + +Export TypeScript directly. The consuming app's bundler compiles it. + +```json +// packages/ui/package.json +{ + "name": "@repo/ui", + "exports": { + "./button": "./src/button.tsx", + "./card": "./src/card.tsx" + }, + "scripts": { + "lint": "eslint .", + "check-types": "tsc --noEmit" + } +} +``` + +**When to use:** + +- Apps use modern bundlers (Turbopack, webpack, Vite) +- You want minimal configuration +- Build times are acceptable without caching + +**Limitations:** + +- No Turborepo cache for the package itself +- Consumer must support TypeScript compilation +- Can't use TypeScript `paths` (use Node.js subpath imports instead) + +### Compiled + +Package handles its own compilation. + +```json +// packages/ui/package.json +{ + "name": "@repo/ui", + "exports": { + "./button": { + "types": "./src/button.tsx", + "default": "./dist/button.js" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch" + } +} +``` + +```json +// packages/ui/tsconfig.json +{ + "extends": "@repo/typescript-config/library.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} +``` + +**When to use:** + +- You want Turborepo to cache builds +- Package will be used by non-bundler tools +- You need maximum compatibility + +**Remember:** Add `dist/**` to turbo.json outputs! + +## Defining Exports + +### Multiple Entrypoints + +```json +{ + "exports": { + ".": "./src/index.ts", // @repo/ui + "./button": "./src/button.tsx", // @repo/ui/button + "./card": "./src/card.tsx", // @repo/ui/card + "./hooks": "./src/hooks/index.ts" // @repo/ui/hooks + } +} +``` + +### Conditional Exports (Compiled) + +```json +{ + "exports": { + "./button": { + "types": "./src/button.tsx", + "import": "./dist/button.mjs", + "require": "./dist/button.cjs", + "default": "./dist/button.js" + } + } +} +``` + +## Installing Internal Packages + +### Add to Consuming Package + +```json +// apps/web/package.json +{ + "dependencies": { + "@repo/ui": "workspace:*" // pnpm/bun + // "@repo/ui": "*" // npm/yarn + } +} +``` + +### Run Install + +```bash +pnpm install # Updates lockfile with new dependency +``` + +### Import and Use + +```typescript +// apps/web/src/page.tsx +import { Button } from '@repo/ui/button'; + +export default function Page() { + return ; +} +``` + +## One Purpose Per Package + +### Good Examples + +``` +packages/ +├── ui/ # Shared UI components +├── utils/ # General utilities +├── auth/ # Authentication logic +├── database/ # Database client/schemas +├── eslint-config/ # ESLint configuration +├── typescript-config/ # TypeScript configuration +└── api-client/ # Generated API client +``` + +### Avoid Mega-Packages + +``` +// BAD: One package for everything +packages/ +└── shared/ + ├── components/ + ├── utils/ + ├── hooks/ + ├── types/ + └── api/ + +// GOOD: Separate by purpose +packages/ +├── ui/ # Components +├── utils/ # Utilities +├── hooks/ # React hooks +├── types/ # Shared TypeScript types +└── api-client/ # API utilities +``` + +## Config Packages + +### TypeScript Config + +```json +// packages/typescript-config/package.json +{ + "name": "@repo/typescript-config", + "exports": { + "./base.json": "./base.json", + "./nextjs.json": "./nextjs.json", + "./library.json": "./library.json" + } +} +``` + +### ESLint Config + +```json +// packages/eslint-config/package.json +{ + "name": "@repo/eslint-config", + "exports": { + "./base": "./base.js", + "./next": "./next.js" + }, + "dependencies": { + "eslint": "^8.0.0", + "eslint-config-next": "latest" + } +} +``` + +## Common Mistakes + +### Forgetting to Export + +```json +// BAD: No exports defined +{ + "name": "@repo/ui" +} + +// GOOD: Clear exports +{ + "name": "@repo/ui", + "exports": { + "./button": "./src/button.tsx" + } +} +``` + +### Wrong Workspace Syntax + +```json +// pnpm/bun +{ "@repo/ui": "workspace:*" } // Correct + +// npm/yarn +{ "@repo/ui": "*" } // Correct +{ "@repo/ui": "workspace:*" } // Wrong for npm/yarn! +``` + +### Missing from turbo.json Outputs + +```json +// Package builds to dist/, but turbo.json doesn't know +{ + "tasks": { + "build": { + "outputs": [".next/**"] // Missing dist/**! + } + } +} + +// Correct +{ + "tasks": { + "build": { + "outputs": [".next/**", "dist/**"] + } + } +} +``` + +## TypeScript Best Practices + +### Use Node.js Subpath Imports (Not `paths`) + +TypeScript `compilerOptions.paths` breaks with JIT packages. Use Node.js subpath imports instead (TypeScript 5.4+). + +**JIT Package:** + +```json +// packages/ui/package.json +{ + "imports": { + "#*": "./src/*" + } +} +``` + +```typescript +// packages/ui/button.tsx +import { MY_STRING } from "#utils.ts"; // Uses .ts extension +``` + +**Compiled Package:** + +```json +// packages/ui/package.json +{ + "imports": { + "#*": "./dist/*" + } +} +``` + +```typescript +// packages/ui/button.tsx +import { MY_STRING } from "#utils.js"; // Uses .js extension +``` + +### Use `tsc` for Internal Packages + +For internal packages, prefer `tsc` over bundlers. Bundlers can mangle code before it reaches your app's bundler, causing hard-to-debug issues. + +### Enable Go-to-Definition + +For Compiled Packages, enable declaration maps: + +```json +// tsconfig.json +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true + } +} +``` + +This creates `.d.ts` and `.d.ts.map` files for IDE navigation. + +### No Root tsconfig.json Needed + +Each package should have its own `tsconfig.json`. A root one causes all tasks to miss cache when changed. Only use root `tsconfig.json` for non-package scripts. + +### Avoid TypeScript Project References + +They add complexity and another caching layer. Turborepo handles dependencies better. diff --git a/.agents/skills/turborepo/references/best-practices/structure.md b/.agents/skills/turborepo/references/best-practices/structure.md new file mode 100644 index 00000000..5f0b08d8 --- /dev/null +++ b/.agents/skills/turborepo/references/best-practices/structure.md @@ -0,0 +1,270 @@ +# Repository Structure + +Detailed guidance on structuring a Turborepo monorepo. + +## Workspace Configuration + +### pnpm (Recommended) + +```yaml +# pnpm-workspace.yaml +packages: + - "apps/*" + - "packages/*" +``` + +### npm/yarn/bun + +```json +// package.json +{ + "workspaces": ["apps/*", "packages/*"] +} +``` + +## Root package.json + +```json +{ + "name": "my-monorepo", + "private": true, + "packageManager": "pnpm@9.0.0", + "scripts": { + "build": "turbo run build", + "dev": "turbo run dev", + "lint": "turbo run lint", + "test": "turbo run test" + }, + "devDependencies": { + "turbo": "latest" + } +} +``` + +Key points: + +- `private: true` - Prevents accidental publishing +- `packageManager` - Enforces consistent package manager version +- **Scripts only delegate to `turbo run`** - No actual build logic here! +- Minimal devDependencies (just turbo and repo tools) + +## Always Prefer Package Tasks + +**Always use package tasks. Only use Root Tasks if you cannot succeed with package tasks.** + +```json +// packages/web/package.json +{ + "scripts": { + "build": "next build", + "lint": "eslint .", + "test": "vitest", + "typecheck": "tsc --noEmit" + } +} + +// packages/api/package.json +{ + "scripts": { + "build": "tsc", + "lint": "eslint .", + "test": "vitest", + "typecheck": "tsc --noEmit" + } +} +``` + +Package tasks enable Turborepo to: + +1. **Parallelize** - Run `web#lint` and `api#lint` simultaneously +2. **Cache individually** - Each package's task output is cached separately +3. **Filter precisely** - Run `turbo run test --filter=web` for just one package + +**Root Tasks are a fallback** for tasks that truly cannot run per-package: + +```json +// AVOID unless necessary - sequential, not parallelized, can't filter +{ + "scripts": { + "lint": "eslint apps/web && eslint apps/api && eslint packages/ui" + } +} +``` + +## Root turbo.json + +```json +{ + "$schema": "https://v2-8-13-canary-12.turborepo.dev/schema.json", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", ".next/**", "!.next/cache/**"] + }, + "lint": {}, + "test": { + "dependsOn": ["build"] + }, + "dev": { + "cache": false, + "persistent": true + } + } +} +``` + +## Directory Organization + +### Grouping Packages + +You can group packages by adding more workspace paths: + +```yaml +# pnpm-workspace.yaml +packages: + - "apps/*" + - "packages/*" + - "packages/config/*" # Grouped configs + - "packages/features/*" # Feature packages +``` + +This allows: + +``` +packages/ +├── ui/ +├── utils/ +├── config/ +│ ├── eslint/ +│ ├── typescript/ +│ └── tailwind/ +└── features/ + ├── auth/ + └── payments/ +``` + +### What NOT to Do + +```yaml +# BAD: Nested wildcards cause ambiguous behavior +packages: + - "packages/**" # Don't do this! +``` + +## Package Anatomy + +### Minimum Required Files + +``` +packages/ui/ +├── package.json # Required: Makes it a package +├── src/ # Source code +│ └── button.tsx +└── tsconfig.json # TypeScript config (if using TS) +``` + +### package.json Requirements + +```json +{ + "name": "@repo/ui", // Unique, namespaced name + "version": "0.0.0", // Version (can be 0.0.0 for internal) + "private": true, // Prevents accidental publishing + "exports": { + // Entry points + "./button": "./src/button.tsx" + } +} +``` + +## TypeScript Configuration + +### Shared Base Config + +Create a shared TypeScript config package: + +``` +packages/ +└── typescript-config/ + ├── package.json + ├── base.json + ├── nextjs.json + └── library.json +``` + +```json +// packages/typescript-config/base.json +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "bundler", + "module": "ESNext", + "target": "ES2022" + } +} +``` + +### Extending in Packages + +```json +// packages/ui/tsconfig.json +{ + "extends": "@repo/typescript-config/library.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} +``` + +### No Root tsconfig.json + +You likely don't need a `tsconfig.json` in the workspace root. Each package should have its own config extending from the shared config package. + +## ESLint Configuration + +### Shared Config Package + +``` +packages/ +└── eslint-config/ + ├── package.json + ├── base.js + ├── next.js + └── library.js +``` + +```json +// packages/eslint-config/package.json +{ + "name": "@repo/eslint-config", + "exports": { + "./base": "./base.js", + "./next": "./next.js", + "./library": "./library.js" + } +} +``` + +### Using in Packages + +```js +// apps/web/.eslintrc.js +module.exports = { + extends: ["@repo/eslint-config/next"] +}; +``` + +## Lockfile + +A lockfile is **required** for: + +- Reproducible builds +- Turborepo to understand package dependencies +- Cache correctness + +Without a lockfile, you'll see unpredictable behavior. diff --git a/.agents/skills/turborepo/references/boundaries/RULE.md b/.agents/skills/turborepo/references/boundaries/RULE.md new file mode 100644 index 00000000..3deb0a41 --- /dev/null +++ b/.agents/skills/turborepo/references/boundaries/RULE.md @@ -0,0 +1,126 @@ +# Boundaries + +**Experimental feature** - See [RFC](https://github.com/vercel/turborepo/discussions/9435) + +Full docs: https://turborepo.dev/docs/reference/boundaries + +Boundaries enforce package isolation by detecting: + +1. Imports of files outside the package's directory +2. Imports of packages not declared in `package.json` dependencies + +## Usage + +```bash +turbo boundaries +``` + +Run this to check for workspace violations across your monorepo. + +## Tags + +Tags allow you to create rules for which packages can depend on each other. + +### Adding Tags to a Package + +```json +// packages/ui/turbo.json +{ + "tags": ["internal"] +} +``` + +### Configuring Tag Rules + +Rules go in root `turbo.json`: + +```json +// turbo.json +{ + "boundaries": { + "tags": { + "public": { + "dependencies": { + "deny": ["internal"] + } + } + } + } +} +``` + +This prevents `public`-tagged packages from importing `internal`-tagged packages. + +### Rule Types + +**Allow-list approach** (only allow specific tags): + +```json +{ + "boundaries": { + "tags": { + "public": { + "dependencies": { + "allow": ["public"] + } + } + } + } +} +``` + +**Deny-list approach** (block specific tags): + +```json +{ + "boundaries": { + "tags": { + "public": { + "dependencies": { + "deny": ["internal"] + } + } + } + } +} +``` + +**Restrict dependents** (who can import this package): + +```json +{ + "boundaries": { + "tags": { + "private": { + "dependents": { + "deny": ["public"] + } + } + } + } +} +``` + +### Using Package Names + +Package names work in place of tags: + +```json +{ + "boundaries": { + "tags": { + "private": { + "dependents": { + "deny": ["@repo/my-pkg"] + } + } + } + } +} +``` + +## Key Points + +- Rules apply transitively (dependencies of dependencies) +- Helps enforce architectural boundaries at scale +- Catches violations before runtime/build errors diff --git a/.agents/skills/turborepo/references/caching/RULE.md b/.agents/skills/turborepo/references/caching/RULE.md new file mode 100644 index 00000000..fe6388e2 --- /dev/null +++ b/.agents/skills/turborepo/references/caching/RULE.md @@ -0,0 +1,107 @@ +# How Turborepo Caching Works + +Turborepo's core principle: **never do the same work twice**. + +## The Cache Equation + +``` +fingerprint(inputs) → stored outputs +``` + +If inputs haven't changed, restore outputs from cache instead of re-running the task. + +## What Determines the Cache Key + +### Global Hash Inputs + +These affect ALL tasks in the repo: + +- `package-lock.json` / `yarn.lock` / `pnpm-lock.yaml` +- Files listed in `globalDependencies` +- Environment variables in `globalEnv` +- `turbo.json` configuration + +```json +{ + "globalDependencies": [".env", "tsconfig.base.json"], + "globalEnv": ["CI", "NODE_ENV"] +} +``` + +### Task Hash Inputs + +These affect specific tasks: + +- All files in the package (unless filtered by `inputs`) +- `package.json` contents +- Environment variables in task's `env` key +- Task configuration (command, outputs, dependencies) +- Hashes of dependent tasks (`dependsOn`) + +```json +{ + "tasks": { + "build": { + "dependsOn": ["^build"], + "inputs": ["src/**", "package.json", "tsconfig.json"], + "env": ["API_URL"] + } + } +} +``` + +## What Gets Cached + +1. **File outputs** - files/directories specified in `outputs` +2. **Task logs** - stdout/stderr for replay on cache hit + +```json +{ + "tasks": { + "build": { + "outputs": ["dist/**", ".next/**"] + } + } +} +``` + +## Local Cache Location + +``` +.turbo/cache/ +├── .tar.zst # compressed outputs +├── .tar.zst +└── ... +``` + +Add `.turbo` to `.gitignore`. + +## Cache Restoration + +On cache hit, Turborepo: + +1. Extracts archived outputs to their original locations +2. Replays the logged stdout/stderr +3. Reports the task as cached (shows `FULL TURBO` in output) + +## Example Flow + +```bash +# First run - executes build, caches result +turbo build +# → packages/ui: cache miss, executing... +# → packages/web: cache miss, executing... + +# Second run - same inputs, restores from cache +turbo build +# → packages/ui: cache hit, replaying output +# → packages/web: cache hit, replaying output +# → FULL TURBO +``` + +## Key Points + +- Cache is content-addressed (based on input hash, not timestamps) +- Empty `outputs` array means task runs but nothing is cached +- Tasks without `outputs` key cache nothing (use `"outputs": []` to be explicit) +- Cache is invalidated when ANY input changes diff --git a/.agents/skills/turborepo/references/caching/gotchas.md b/.agents/skills/turborepo/references/caching/gotchas.md new file mode 100644 index 00000000..695c783e --- /dev/null +++ b/.agents/skills/turborepo/references/caching/gotchas.md @@ -0,0 +1,169 @@ +# Debugging Cache Issues + +## Diagnostic Tools + +### `--summarize` + +Generates a JSON file with all hash inputs. Compare two runs to find differences. + +```bash +turbo build --summarize +# Creates .turbo/runs/.json +``` + +The summary includes: + +- Global hash and its inputs +- Per-task hashes and their inputs +- Environment variables that affected the hash + +**Comparing runs:** + +```bash +# Run twice, compare the summaries +diff .turbo/runs/.json .turbo/runs/.json +``` + +### `--dry` / `--dry=json` + +See what would run without executing anything: + +```bash +turbo build --dry +turbo build --dry=json # machine-readable output +``` + +Shows cache status for each task without running them. + +### `--force` + +Skip reading cache, re-execute all tasks: + +```bash +turbo build --force +``` + +Useful to verify tasks actually work (not just cached results). + +## Unexpected Cache Misses + +**Symptom:** Task runs when you expected a cache hit. + +### Environment Variable Changed + +Check if an env var in the `env` key changed: + +```json +{ + "tasks": { + "build": { + "env": ["API_URL", "NODE_ENV"] + } + } +} +``` + +Different `API_URL` between runs = cache miss. + +### .env File Changed + +`.env` files aren't tracked by default. Add to `inputs`: + +```json +{ + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", ".env", ".env.local"] + } + } +} +``` + +Or use `globalDependencies` for repo-wide env files: + +```json +{ + "globalDependencies": [".env"] +} +``` + +### Lockfile Changed + +Installing/updating packages changes the global hash. + +### Source Files Changed + +Any file in the package (or in `inputs`) triggers a miss. + +### turbo.json Changed + +Config changes invalidate the global hash. + +## Incorrect Cache Hits + +**Symptom:** Cached output is stale/wrong. + +### Missing Environment Variable + +Task uses an env var not listed in `env`: + +```javascript +// build.js +const apiUrl = process.env.API_URL; // not tracked! +``` + +Fix: add to task config: + +```json +{ + "tasks": { + "build": { + "env": ["API_URL"] + } + } +} +``` + +### Missing File in Inputs + +Task reads a file outside default inputs: + +```json +{ + "tasks": { + "build": { + "inputs": [ + "$TURBO_DEFAULT$", + "../../shared-config.json" // file outside package + ] + } + } +} +``` + +## Useful Flags + +```bash +# Only show output for cache misses +turbo build --output-logs=new-only + +# Show output for everything (debugging) +turbo build --output-logs=full + +# See why tasks are running +turbo build --verbosity=2 +``` + +## Quick Checklist + +Cache miss when expected hit: + +1. Run with `--summarize`, compare with previous run +2. Check env vars with `--dry=json` +3. Look for lockfile/config changes in git + +Cache hit when expected miss: + +1. Verify env var is in `env` array +2. Verify file is in `inputs` array +3. Check if file is outside package directory diff --git a/.agents/skills/turborepo/references/caching/remote-cache.md b/.agents/skills/turborepo/references/caching/remote-cache.md new file mode 100644 index 00000000..da76458b --- /dev/null +++ b/.agents/skills/turborepo/references/caching/remote-cache.md @@ -0,0 +1,127 @@ +# Remote Caching + +Share cache artifacts across your team and CI pipelines. + +## Benefits + +- Team members get cache hits from each other's work +- CI gets cache hits from local development (and vice versa) +- Dramatically faster CI runs after first build +- No more "works on my machine" rebuilds + +## Vercel Remote Cache + +Free, zero-config when deploying on Vercel. For local dev and other CI: + +### Local Development Setup + +```bash +# Authenticate with Vercel +npx turbo login + +# Link repo to your Vercel team +npx turbo link +``` + +This creates `.turbo/config.json` with your team info (gitignored by default). + +### CI Setup + +Set these environment variables: + +```bash +TURBO_TOKEN= +TURBO_TEAM= +``` + +Get your token from Vercel dashboard → Settings → Tokens. + +**GitHub Actions example:** + +```yaml +- name: Build + run: npx turbo build + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} +``` + +## Configuration in turbo.json + +```json +{ + "remoteCache": { + "enabled": true, + "signature": false + } +} +``` + +Options: + +- `enabled`: toggle remote cache (default: true when authenticated) +- `signature`: require artifact signing (default: false) + +## Artifact Signing + +Verify cache artifacts haven't been tampered with: + +```bash +# Set a secret key (use same key across all environments) +export TURBO_REMOTE_CACHE_SIGNATURE_KEY="your-secret-key" +``` + +Enable in config: + +```json +{ + "remoteCache": { + "signature": true + } +} +``` + +Signed artifacts can only be restored if the signature matches. + +## Self-Hosted Options + +Community implementations for running your own cache server: + +- **turbo-remote-cache** (Node.js) - supports S3, GCS, Azure +- **turborepo-remote-cache** (Go) - lightweight, S3-compatible +- **ducktape** (Rust) - high-performance option + +Configure with environment variables: + +```bash +TURBO_API=https://your-cache-server.com +TURBO_TOKEN=your-auth-token +TURBO_TEAM=your-team +``` + +## Cache Behavior Control + +```bash +# Disable remote cache for a run +turbo build --remote-cache-read-only # read but don't write +turbo build --no-cache # skip cache entirely + +# Environment variable alternative +TURBO_REMOTE_ONLY=true # only use remote, skip local +``` + +## Debugging Remote Cache + +```bash +# Verbose output shows cache operations +turbo build --verbosity=2 + +# Check if remote cache is configured +turbo config +``` + +Look for: + +- "Remote caching enabled" in output +- Upload/download messages during runs +- "cache hit, replaying output" with remote cache indicator diff --git a/.agents/skills/turborepo/references/ci/RULE.md b/.agents/skills/turborepo/references/ci/RULE.md new file mode 100644 index 00000000..f331c2cf --- /dev/null +++ b/.agents/skills/turborepo/references/ci/RULE.md @@ -0,0 +1,79 @@ +# CI/CD with Turborepo + +General principles for running Turborepo in continuous integration environments. + +## Core Principles + +### Always Use `turbo run` in CI + +**Never use the `turbo ` shorthand in CI or scripts.** Always use `turbo run`: + +```bash +# CORRECT - Always use in CI, package.json, scripts +turbo run build test lint + +# WRONG - Shorthand is only for one-off terminal commands +turbo build test lint +``` + +The shorthand `turbo ` is only for one-off invocations typed directly in terminal by humans or agents. Anywhere the command is written into code (CI, package.json, scripts), use `turbo run`. + +### Enable Remote Caching + +Remote caching dramatically speeds up CI by sharing cached artifacts across runs. + +Required environment variables: + +```bash +TURBO_TOKEN=your_vercel_token +TURBO_TEAM=your_team_slug +``` + +### Use --affected for PR Builds + +The `--affected` flag only runs tasks for packages changed since the base branch: + +```bash +turbo run build test --affected +``` + +This requires Git history to compute what changed. + +## Git History Requirements + +### Fetch Depth + +`--affected` needs access to the merge base. Shallow clones break this. + +```yaml +# GitHub Actions +- uses: actions/checkout@v4 + with: + fetch-depth: 2 # Minimum for --affected + # Use 0 for full history if merge base is far +``` + +### Why Shallow Clones Break --affected + +Turborepo compares the current HEAD to the merge base with `main`. If that commit isn't fetched, `--affected` falls back to running everything. + +For PRs with many commits, consider: + +```yaml +fetch-depth: 0 # Full history +``` + +## Environment Variables Reference + +| Variable | Purpose | +| ------------------- | ------------------------------------ | +| `TURBO_TOKEN` | Vercel access token for remote cache | +| `TURBO_TEAM` | Your Vercel team slug | +| `TURBO_REMOTE_ONLY` | Skip local cache, use remote only | +| `TURBO_LOG_ORDER` | Set to `grouped` for cleaner CI logs | + +## See Also + +- [github-actions.md](./github-actions.md) - GitHub Actions setup +- [vercel.md](./vercel.md) - Vercel deployment +- [patterns.md](./patterns.md) - CI optimization patterns diff --git a/.agents/skills/turborepo/references/ci/github-actions.md b/.agents/skills/turborepo/references/ci/github-actions.md new file mode 100644 index 00000000..1cdb34f3 --- /dev/null +++ b/.agents/skills/turborepo/references/ci/github-actions.md @@ -0,0 +1,162 @@ +# GitHub Actions + +Complete setup guide for Turborepo with GitHub Actions. + +## Basic Workflow Structure + +```yaml +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: Build and Test + run: turbo run build test lint +``` + +## Package Manager Setup + +### pnpm + +```yaml +- uses: pnpm/action-setup@v3 + with: + version: 9 + +- uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" + +- run: pnpm install --frozen-lockfile +``` + +### Yarn + +```yaml +- uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "yarn" + +- run: yarn install --frozen-lockfile +``` + +### Bun + +```yaml +- uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + +- run: bun install --frozen-lockfile +``` + +## Remote Cache Setup + +### 1. Create Vercel Access Token + +1. Go to [Vercel Dashboard](https://vercel.com/account/tokens) +2. Create a new token with appropriate scope +3. Copy the token value + +### 2. Add Secrets and Variables + +In your GitHub repository settings: + +**Secrets** (Settings > Secrets and variables > Actions > Secrets): + +- `TURBO_TOKEN`: Your Vercel access token + +**Variables** (Settings > Secrets and variables > Actions > Variables): + +- `TURBO_TEAM`: Your Vercel team slug + +### 3. Add to Workflow + +```yaml +jobs: + build: + runs-on: ubuntu-latest + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} +``` + +## Alternative: actions/cache + +If you can't use remote cache, cache Turborepo's local cache directory: + +```yaml +- uses: actions/cache@v4 + with: + path: .turbo + key: turbo-${{ runner.os }}-${{ hashFiles('**/turbo.json', '**/package-lock.json') }} + restore-keys: | + turbo-${{ runner.os }}- +``` + +Note: This is less effective than remote cache since it's per-branch. + +## Complete Example + +```yaml +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - uses: pnpm/action-setup@v3 + with: + version: 9 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: turbo run build --affected + + - name: Test + run: turbo run test --affected + + - name: Lint + run: turbo run lint --affected +``` diff --git a/.agents/skills/turborepo/references/ci/patterns.md b/.agents/skills/turborepo/references/ci/patterns.md new file mode 100644 index 00000000..447509a1 --- /dev/null +++ b/.agents/skills/turborepo/references/ci/patterns.md @@ -0,0 +1,145 @@ +# CI Optimization Patterns + +Strategies for efficient CI/CD with Turborepo. + +## PR vs Main Branch Builds + +### PR Builds: Only Affected + +Test only what changed in the PR: + +```yaml +- name: Test (PR) + if: github.event_name == 'pull_request' + run: turbo run build test --affected +``` + +### Main Branch: Full Build + +Ensure complete validation on merge: + +```yaml +- name: Test (Main) + if: github.ref == 'refs/heads/main' + run: turbo run build test +``` + +## Custom Git Ranges with --filter + +For advanced scenarios, use `--filter` with git refs: + +```bash +# Changes since specific commit +turbo run test --filter="...[abc123]" + +# Changes between refs +turbo run test --filter="...[main...HEAD]" + +# Changes in last 3 commits +turbo run test --filter="...[HEAD~3]" +``` + +## Caching Strategies + +### Remote Cache (Recommended) + +Best performance - shared across all CI runs and developers: + +```yaml +env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} +``` + +### actions/cache Fallback + +When remote cache isn't available: + +```yaml +- uses: actions/cache@v4 + with: + path: .turbo + key: turbo-${{ runner.os }}-${{ github.sha }} + restore-keys: | + turbo-${{ runner.os }}-${{ github.ref }}- + turbo-${{ runner.os }}- +``` + +Limitations: + +- Cache is branch-scoped +- PRs restore from base branch cache +- Less efficient than remote cache + +## Matrix Builds + +Test across Node versions: + +```yaml +strategy: + matrix: + node: [18, 20, 22] + +steps: + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + + - run: turbo run test +``` + +## Parallelizing Across Jobs + +Split tasks into separate jobs: + +```yaml +jobs: + lint: + runs-on: ubuntu-latest + steps: + - run: turbo run lint --affected + + test: + runs-on: ubuntu-latest + steps: + - run: turbo run test --affected + + build: + runs-on: ubuntu-latest + needs: [lint, test] + steps: + - run: turbo run build +``` + +### Cache Considerations + +When parallelizing: + +- Each job has separate cache writes +- Remote cache handles this automatically +- With actions/cache, use unique keys per job to avoid conflicts + +```yaml +- uses: actions/cache@v4 + with: + path: .turbo + key: turbo-${{ runner.os }}-${{ github.job }}-${{ github.sha }} +``` + +## Conditional Tasks + +Skip expensive tasks on draft PRs: + +```yaml +- name: E2E Tests + if: github.event.pull_request.draft == false + run: turbo run test:e2e --affected +``` + +Or require label for full test: + +```yaml +- name: Full Test Suite + if: contains(github.event.pull_request.labels.*.name, 'full-test') + run: turbo run test +``` diff --git a/.agents/skills/turborepo/references/ci/vercel.md b/.agents/skills/turborepo/references/ci/vercel.md new file mode 100644 index 00000000..f21d41ac --- /dev/null +++ b/.agents/skills/turborepo/references/ci/vercel.md @@ -0,0 +1,103 @@ +# Vercel Deployment + +Turborepo integrates seamlessly with Vercel for monorepo deployments. + +## Remote Cache + +Remote caching is **automatically enabled** when deploying to Vercel. No configuration needed - Vercel detects Turborepo and enables caching. + +This means: + +- No `TURBO_TOKEN` or `TURBO_TEAM` setup required on Vercel +- Cache is shared across all deployments +- Preview and production builds benefit from cache + +## turbo-ignore + +Skip unnecessary builds when a package hasn't changed using `turbo-ignore`. + +### Installation + +```bash +npx turbo-ignore +``` + +Or install globally in your project: + +```bash +pnpm add -D turbo-ignore +``` + +### Setup in Vercel + +1. Go to your project in Vercel Dashboard +2. Navigate to Settings > Git > Ignored Build Step +3. Select "Custom" and enter: + +```bash +npx turbo-ignore +``` + +### How It Works + +`turbo-ignore` checks if the current package (or its dependencies) changed since the last successful deployment: + +1. Compares current commit to last deployed commit +2. Uses Turborepo's dependency graph +3. Returns exit code 0 (skip) if no changes +4. Returns exit code 1 (build) if changes detected + +### Options + +```bash +# Check specific package +npx turbo-ignore web + +# Use specific comparison ref +npx turbo-ignore --fallback=HEAD~1 + +# Verbose output +npx turbo-ignore --verbose +``` + +## Environment Variables + +Set environment variables in Vercel Dashboard: + +1. Go to Project Settings > Environment Variables +2. Add variables for each environment (Production, Preview, Development) + +Common variables: + +- `DATABASE_URL` +- `API_KEY` +- Package-specific config + +## Monorepo Root Directory + +For monorepos, set the root directory in Vercel: + +1. Project Settings > General > Root Directory +2. Set to the package path (e.g., `apps/web`) + +Vercel automatically: + +- Installs dependencies from monorepo root +- Runs build from the package directory +- Detects framework settings + +## Build Command + +Vercel auto-detects `turbo run build` when `turbo.json` exists at root. + +Override if needed: + +```bash +turbo run build --filter=web +``` + +Or for production-only optimizations: + +```bash +turbo run build --filter=web --env-mode=strict +``` diff --git a/.agents/skills/turborepo/references/cli/RULE.md b/.agents/skills/turborepo/references/cli/RULE.md new file mode 100644 index 00000000..63f6f34d --- /dev/null +++ b/.agents/skills/turborepo/references/cli/RULE.md @@ -0,0 +1,100 @@ +# turbo run + +The primary command for executing tasks across your monorepo. + +## Basic Usage + +```bash +# Full form (use in CI, package.json, scripts) +turbo run + +# Shorthand (only for one-off terminal invocations) +turbo +``` + +## When to Use `turbo run` vs `turbo` + +**Always use `turbo run` when the command is written into code:** + +- `package.json` scripts +- CI/CD workflows (GitHub Actions, etc.) +- Shell scripts +- Documentation +- Any static/committed configuration + +**Only use `turbo` (shorthand) for:** + +- One-off commands typed directly in terminal +- Ad-hoc invocations by humans or agents + +```json +// package.json - ALWAYS use "turbo run" +{ + "scripts": { + "build": "turbo run build", + "dev": "turbo run dev", + "lint": "turbo run lint", + "test": "turbo run test" + } +} +``` + +```yaml +# CI workflow - ALWAYS use "turbo run" +- run: turbo run build --affected +- run: turbo run test --affected +``` + +```bash +# Terminal one-off - shorthand OK +turbo build --filter=web +``` + +## Running Tasks + +Tasks must be defined in `turbo.json` before running. + +```bash +# Single task +turbo build + +# Multiple tasks +turbo run build lint test + +# See available tasks (run without arguments) +turbo run +``` + +## Passing Arguments to Scripts + +Use `--` to pass arguments through to the underlying package scripts: + +```bash +turbo run build -- --sourcemap +turbo test -- --watch +turbo lint -- --fix +``` + +Everything after `--` goes directly to the task's script. + +## Package Selection + +By default, turbo runs tasks in all packages. Use `--filter` to narrow scope: + +```bash +turbo build --filter=web +turbo test --filter=./apps/* +``` + +See `filtering/` for complete filter syntax. + +## Quick Reference + +| Goal | Command | +| ------------------- | -------------------------- | +| Build everything | `turbo build` | +| Build one package | `turbo build --filter=web` | +| Multiple tasks | `turbo build lint test` | +| Pass args to script | `turbo build -- --arg` | +| Preview run | `turbo build --dry` | +| Force rebuild | `turbo build --force` | diff --git a/.agents/skills/turborepo/references/cli/commands.md b/.agents/skills/turborepo/references/cli/commands.md new file mode 100644 index 00000000..c1eb6b2f --- /dev/null +++ b/.agents/skills/turborepo/references/cli/commands.md @@ -0,0 +1,297 @@ +# turbo run Flags Reference + +Full docs: https://turborepo.dev/docs/reference/run + +## Package Selection + +### `--filter` / `-F` + +Select specific packages to run tasks in. + +```bash +turbo build --filter=web +turbo build -F=@repo/ui -F=@repo/utils +turbo test --filter=./apps/* +``` + +See `filtering/` for complete syntax (globs, dependencies, git ranges). + +### Task Identifier Syntax (v2.2.4+) + +Run specific package tasks directly: + +```bash +turbo run web#build # Build web package +turbo run web#build docs#lint # Multiple specific tasks +``` + +### `--affected` + +Run only in packages changed since the base branch. + +```bash +turbo build --affected +turbo test --affected --filter=./apps/* # combine with filter +``` + +**How it works:** + +- Default: compares `main...HEAD` +- In GitHub Actions: auto-detects `GITHUB_BASE_REF` +- Override base: `TURBO_SCM_BASE=development turbo build --affected` +- Override head: `TURBO_SCM_HEAD=your-branch turbo build --affected` + +**Requires git history** - shallow clones may fall back to running all tasks. + +## Execution Control + +### `--dry` / `--dry=json` + +Preview what would run without executing. + +```bash +turbo build --dry # human-readable +turbo build --dry=json # machine-readable +``` + +### `--force` + +Ignore all cached artifacts, re-run everything. + +```bash +turbo build --force +``` + +### `--concurrency` + +Limit parallel task execution. + +```bash +turbo build --concurrency=4 # max 4 tasks +turbo build --concurrency=50% # 50% of CPU cores +``` + +### `--continue` + +Keep running other tasks when one fails. + +```bash +turbo build test --continue +``` + +### `--only` + +Run only the specified task, skip its dependencies. + +```bash +turbo build --only # skip running dependsOn tasks +``` + +### `--parallel` (Discouraged) + +Ignores task graph dependencies, runs all tasks simultaneously. **Avoid using this flag**—if tasks need to run in parallel, configure `dependsOn` correctly instead. Using `--parallel` bypasses Turborepo's dependency graph, which can cause race conditions and incorrect builds. + +## Cache Control + +### `--cache` + +Fine-grained cache behavior control. + +```bash +# Default: read/write both local and remote +turbo build --cache=local:rw,remote:rw + +# Read-only local, no remote +turbo build --cache=local:r,remote: + +# Disable local, read-only remote +turbo build --cache=local:,remote:r + +# Disable all caching +turbo build --cache=local:,remote: +``` + +## Output & Debugging + +### `--graph` + +Generate task graph visualization. + +```bash +turbo build --graph # opens in browser +turbo build --graph=graph.svg # SVG file +turbo build --graph=graph.png # PNG file +turbo build --graph=graph.json # JSON data +turbo build --graph=graph.mermaid # Mermaid diagram +``` + +### `--summarize` + +Generate JSON run summary for debugging. + +```bash +turbo build --summarize +# creates .turbo/runs/.json +``` + +### `--output-logs` + +Control log output verbosity. + +```bash +turbo build --output-logs=full # all logs (default) +turbo build --output-logs=new-only # only cache misses +turbo build --output-logs=errors-only # only failures +turbo build --output-logs=none # silent +``` + +### `--profile` + +Generate Chrome tracing profile for performance analysis. + +```bash +turbo build --profile=profile.json +# open chrome://tracing and load the file +``` + +### `--verbosity` / `-v` + +Control turbo's own log level. + +```bash +turbo build -v # verbose +turbo build -vv # more verbose +turbo build -vvv # maximum verbosity +``` + +## Environment + +### `--env-mode` + +Control environment variable handling. + +```bash +turbo build --env-mode=strict # only declared env vars (default) +turbo build --env-mode=loose # include all env vars in hash +``` + +## UI + +### `--ui` + +Select output interface. + +```bash +turbo build --ui=tui # interactive terminal UI (default in TTY) +turbo build --ui=stream # streaming logs (default in CI) +``` + +--- + +# turbo-ignore + +Full docs: https://turborepo.dev/docs/reference/turbo-ignore + +Skip CI work when nothing relevant changed. Useful for skipping container setup. + +## Basic Usage + +```bash +# Check if build is needed for current package (uses Automatic Package Scoping) +npx turbo-ignore + +# Check specific package +npx turbo-ignore web + +# Check specific task +npx turbo-ignore --task=test +``` + +## Exit Codes + +- `0`: No changes detected - skip CI work +- `1`: Changes detected - proceed with CI + +## CI Integration Example + +```yaml +# GitHub Actions +- name: Check for changes + id: turbo-ignore + run: npx turbo-ignore web + continue-on-error: true + +- name: Build + if: steps.turbo-ignore.outcome == 'failure' # changes detected + run: pnpm build +``` + +## Comparison Depth + +Default: compares to parent commit (`HEAD^1`). + +```bash +# Compare to specific commit +npx turbo-ignore --fallback=abc123 + +# Compare to branch +npx turbo-ignore --fallback=main +``` + +--- + +# Other Commands + +## turbo boundaries + +Check workspace violations (experimental). + +```bash +turbo boundaries +``` + +See `references/boundaries/` for configuration. + +## turbo watch + +Re-run tasks on file changes. + +```bash +turbo watch build test +``` + +See `references/watch/` for details. + +## turbo prune + +Create sparse checkout for Docker. + +```bash +turbo prune web --docker +``` + +## turbo link / unlink + +Connect/disconnect Remote Cache. + +```bash +turbo link # connect to Vercel Remote Cache +turbo unlink # disconnect +``` + +## turbo login / logout + +Authenticate with Remote Cache provider. + +```bash +turbo login # authenticate +turbo logout # log out +``` + +## turbo generate + +Scaffold new packages. + +```bash +turbo generate +``` diff --git a/.agents/skills/turborepo/references/configuration/RULE.md b/.agents/skills/turborepo/references/configuration/RULE.md new file mode 100644 index 00000000..6c5c0860 --- /dev/null +++ b/.agents/skills/turborepo/references/configuration/RULE.md @@ -0,0 +1,211 @@ +# turbo.json Configuration Overview + +Configuration reference for Turborepo. Full docs: https://turborepo.dev/docs/reference/configuration + +## File Location + +Root `turbo.json` lives at repo root, sibling to root `package.json`: + +``` +my-monorepo/ +├── turbo.json # Root configuration +├── package.json +└── packages/ + └── web/ + ├── turbo.json # Package Configuration (optional) + └── package.json +``` + +## Always Prefer Package Tasks Over Root Tasks + +**Always use package tasks. Only use Root Tasks if you cannot succeed with package tasks.** + +Package tasks enable parallelization, individual caching, and filtering. Define scripts in each package's `package.json`: + +```json +// packages/web/package.json +{ + "scripts": { + "build": "next build", + "lint": "eslint .", + "test": "vitest", + "typecheck": "tsc --noEmit" + } +} + +// packages/api/package.json +{ + "scripts": { + "build": "tsc", + "lint": "eslint .", + "test": "vitest", + "typecheck": "tsc --noEmit" + } +} +``` + +```json +// Root package.json - delegates to turbo +{ + "scripts": { + "build": "turbo run build", + "lint": "turbo run lint", + "test": "turbo run test", + "typecheck": "turbo run typecheck" + } +} +``` + +When you run `turbo run lint`, Turborepo finds all packages with a `lint` script and runs them **in parallel**. + +**Root Tasks are a fallback**, not the default. Only use them for tasks that truly cannot run per-package (e.g., repo-level CI scripts, workspace-wide config generation). + +```json +// AVOID: Task logic in root defeats parallelization +{ + "scripts": { + "lint": "eslint apps/web && eslint apps/api && eslint packages/ui" + } +} +``` + +## Basic Structure + +```json +{ + "$schema": "https://v2-8-13-canary-12.turborepo.dev/schema.json", + "globalEnv": ["CI"], + "globalDependencies": ["tsconfig.json"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] + }, + "dev": { + "cache": false, + "persistent": true + } + } +} +``` + +The `$schema` key enables IDE autocompletion and validation. + +## Configuration Sections + +**Global options** - Settings affecting all tasks: + +- `globalEnv`, `globalDependencies`, `globalPassThroughEnv` +- `cacheDir`, `daemon`, `envMode`, `ui`, `remoteCache` + +**Task definitions** - Per-task settings in `tasks` object: + +- `dependsOn`, `outputs`, `inputs`, `env` +- `cache`, `persistent`, `interactive`, `outputLogs` + +## Package Configurations + +Use `turbo.json` in individual packages to override root settings: + +```json +// packages/web/turbo.json +{ + "extends": ["//"], + "tasks": { + "build": { + "outputs": [".next/**", "!.next/cache/**"] + } + } +} +``` + +The `"extends": ["//"]` is required - it references the root configuration. + +**When to use Package Configurations:** + +- Framework-specific outputs (Next.js, Vite, etc.) +- Package-specific env vars +- Different caching rules for specific packages +- Keeping framework config close to the framework code + +### Extending from Other Packages + +You can extend from config packages instead of just root: + +```json +// packages/web/turbo.json +{ + "extends": ["//", "@repo/turbo-config"] +} +``` + +### Adding to Inherited Arrays with `$TURBO_EXTENDS$` + +By default, array fields in Package Configurations **replace** root values. Use `$TURBO_EXTENDS$` to **append** instead: + +```json +// Root turbo.json +{ + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} +``` + +```json +// packages/web/turbo.json +{ + "extends": ["//"], + "tasks": { + "build": { + // Inherits "dist/**" from root, adds ".next/**" + "outputs": ["$TURBO_EXTENDS$", ".next/**", "!.next/cache/**"] + } + } +} +``` + +Without `$TURBO_EXTENDS$`, outputs would only be `[".next/**", "!.next/cache/**"]`. + +**Works with:** + +- `dependsOn` +- `env` +- `inputs` +- `outputs` +- `passThroughEnv` +- `with` + +### Excluding Tasks from Packages + +Use `extends: false` to exclude a task from a package: + +```json +// packages/ui/turbo.json +{ + "extends": ["//"], + "tasks": { + "e2e": { + "extends": false // UI package doesn't have e2e tests + } + } +} +``` + +## `turbo.jsonc` for Comments + +Use `turbo.jsonc` extension to add comments with IDE support: + +```jsonc +// turbo.jsonc +{ + "tasks": { + "build": { + // Next.js outputs + "outputs": [".next/**", "!.next/cache/**"] + } + } +} +``` diff --git a/.agents/skills/turborepo/references/configuration/global-options.md b/.agents/skills/turborepo/references/configuration/global-options.md new file mode 100644 index 00000000..73ff1c98 --- /dev/null +++ b/.agents/skills/turborepo/references/configuration/global-options.md @@ -0,0 +1,183 @@ +# Global Options Reference + +Options that affect all tasks. Full docs: https://turborepo.dev/docs/reference/configuration + +## globalEnv + +Environment variables affecting all task hashes. + +```json +{ + "globalEnv": ["CI", "NODE_ENV", "VERCEL_*"] +} +``` + +Use for variables that should invalidate all caches when changed. + +## globalDependencies + +Files that affect all task hashes. + +```json +{ + "globalDependencies": ["tsconfig.json", ".env", "pnpm-lock.yaml"] +} +``` + +Lockfile is included by default. Add shared configs here. + +## globalPassThroughEnv + +Variables available to tasks but not included in hash. + +```json +{ + "globalPassThroughEnv": ["AWS_SECRET_KEY", "GITHUB_TOKEN"] +} +``` + +Use for credentials that shouldn't affect cache keys. + +## cacheDir + +Custom cache location. Default: `node_modules/.cache/turbo`. + +```json +{ + "cacheDir": ".turbo/cache" +} +``` + +## daemon + +**Deprecated**: The daemon is no longer used for `turbo run` and this option will be removed in version 3.0. The daemon is still used by `turbo watch` and the Turborepo LSP. + +## envMode + +How unspecified env vars are handled. Default: `"strict"`. + +```json +{ + "envMode": "strict" // Only specified vars available + // or + "envMode": "loose" // All vars pass through +} +``` + +Strict mode catches missing env declarations. + +## ui + +Terminal UI mode. Default: `"stream"`. + +```json +{ + "ui": "tui" // Interactive terminal UI + // or + "ui": "stream" // Traditional streaming logs +} +``` + +TUI provides better UX for parallel tasks. + +## remoteCache + +Configure remote caching. + +```json +{ + "remoteCache": { + "enabled": true, + "signature": true, + "timeout": 30, + "uploadTimeout": 60 + } +} +``` + +| Option | Default | Description | +| --------------- | ---------------------- | ------------------------------------------------------ | +| `enabled` | `true` | Enable/disable remote caching | +| `signature` | `false` | Sign artifacts with `TURBO_REMOTE_CACHE_SIGNATURE_KEY` | +| `preflight` | `false` | Send OPTIONS request before cache requests | +| `timeout` | `30` | Timeout in seconds for cache operations | +| `uploadTimeout` | `60` | Timeout in seconds for uploads | +| `apiUrl` | `"https://vercel.com"` | Remote cache API endpoint | +| `loginUrl` | `"https://vercel.com"` | Login endpoint | +| `teamId` | - | Team ID (must start with `team_`) | +| `teamSlug` | - | Team slug for querystring | + +See https://turborepo.dev/docs/core-concepts/remote-caching for setup. + +## concurrency + +Default: `"10"` + +Limit parallel task execution. + +```json +{ + "concurrency": "4" // Max 4 tasks at once + // or + "concurrency": "50%" // 50% of available CPUs +} +``` + +## futureFlags + +Enable experimental features that will become default in future versions. + +```json +{ + "futureFlags": { + "errorsOnlyShowHash": true + } +} +``` + +### `errorsOnlyShowHash` + +When using `outputLogs: "errors-only"`, show task hashes on start/completion: + +- Cache miss: `cache miss, executing (only logging errors)` +- Cache hit: `cache hit, replaying logs (no errors) ` + +## noUpdateNotifier + +Disable update notifications when new turbo versions are available. + +```json +{ + "noUpdateNotifier": true +} +``` + +## dangerouslyDisablePackageManagerCheck + +Bypass the `packageManager` field requirement. Use for incremental migration. + +```json +{ + "dangerouslyDisablePackageManagerCheck": true +} +``` + +**Warning**: Unstable lockfiles can cause unpredictable behavior. + +## Git Worktree Cache Sharing + +When working in Git worktrees, Turborepo automatically shares local cache between the main worktree and linked worktrees. + +**How it works:** + +- Detects worktree configuration +- Redirects cache to main worktree's `.turbo/cache` +- Works alongside Remote Cache + +**Benefits:** + +- Cache hits across branches +- Reduced disk usage +- Faster branch switching + +**Disabled by**: Setting explicit `cacheDir` in turbo.json. diff --git a/.agents/skills/turborepo/references/configuration/gotchas.md b/.agents/skills/turborepo/references/configuration/gotchas.md new file mode 100644 index 00000000..225bd397 --- /dev/null +++ b/.agents/skills/turborepo/references/configuration/gotchas.md @@ -0,0 +1,348 @@ +# Configuration Gotchas + +Common mistakes and how to fix them. + +## #1 Root Scripts Not Using `turbo run` + +Root `package.json` scripts for turbo tasks MUST use `turbo run`, not direct commands. + +```json +// WRONG - bypasses turbo, no parallelization or caching +{ + "scripts": { + "build": "bun build", + "dev": "bun dev" + } +} + +// CORRECT - delegates to turbo +{ + "scripts": { + "build": "turbo run build", + "dev": "turbo run dev" + } +} +``` + +**Why this matters:** Running `bun build` or `npm run build` at root bypasses Turborepo entirely - no parallelization, no caching, no dependency graph awareness. + +## #2 Using `&&` to Chain Turbo Tasks + +Don't use `&&` to chain tasks that turbo should orchestrate. + +```json +// WRONG - changeset:publish chains turbo task with non-turbo command +{ + "scripts": { + "changeset:publish": "bun build && changeset publish" + } +} + +// CORRECT - use turbo run, let turbo handle dependencies +{ + "scripts": { + "changeset:publish": "turbo run build && changeset publish" + } +} +``` + +If the second command (`changeset publish`) depends on build outputs, the turbo task should run through turbo to get caching and parallelization benefits. + +## #3 Overly Broad globalDependencies + +`globalDependencies` affects hash for ALL tasks in ALL packages. Be specific. + +```json +// WRONG - affects all hashes +{ + "globalDependencies": ["**/.env.*local"] +} + +// CORRECT - move to specific tasks that need it +{ + "globalDependencies": [".env"], + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**"] + } + } +} +``` + +**Why this matters:** `**/.env.*local` matches .env files in ALL packages, causing unnecessary cache invalidation. Instead: + +- Use `globalDependencies` only for truly global files (root `.env`) +- Use task-level `inputs` for package-specific .env files with `$TURBO_DEFAULT$` to preserve default behavior + +## #4 Repetitive Task Configuration + +Look for repeated configuration across tasks that can be collapsed. + +```json +// WRONG - repetitive env and inputs across tasks +{ + "tasks": { + "build": { + "env": ["API_URL", "DATABASE_URL"], + "inputs": ["$TURBO_DEFAULT$", ".env*"] + }, + "test": { + "env": ["API_URL", "DATABASE_URL"], + "inputs": ["$TURBO_DEFAULT$", ".env*"] + } + } +} + +// BETTER - use globalEnv and globalDependencies +{ + "globalEnv": ["API_URL", "DATABASE_URL"], + "globalDependencies": [".env*"], + "tasks": { + "build": {}, + "test": {} + } +} +``` + +**When to use global vs task-level:** + +- `globalEnv` / `globalDependencies` - affects ALL tasks, use for truly shared config +- Task-level `env` / `inputs` - use when only specific tasks need it + +## #5 Using `../` to Traverse Out of Package in `inputs` + +Don't use relative paths like `../` to reference files outside the package. Use `$TURBO_ROOT$` instead. + +```json +// WRONG - traversing out of package +{ + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", "../shared-config.json"] + } + } +} + +// CORRECT - use $TURBO_ROOT$ for repo root +{ + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/shared-config.json"] + } + } +} +``` + +## #6 MOST COMMON MISTAKE: Creating Root Tasks + +**DO NOT create Root Tasks. ALWAYS create package tasks.** + +When you need to create a task (build, lint, test, typecheck, etc.): + +1. Add the script to **each relevant package's** `package.json` +2. Register the task in root `turbo.json` +3. Root `package.json` only contains `turbo run ` + +```json +// WRONG - DO NOT DO THIS +// Root package.json with task logic +{ + "scripts": { + "build": "cd apps/web && next build && cd ../api && tsc", + "lint": "eslint apps/ packages/", + "test": "vitest" + } +} + +// CORRECT - DO THIS +// apps/web/package.json +{ "scripts": { "build": "next build", "lint": "eslint .", "test": "vitest" } } + +// apps/api/package.json +{ "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } } + +// packages/ui/package.json +{ "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } } + +// Root package.json - ONLY delegates +{ "scripts": { "build": "turbo run build", "lint": "turbo run lint", "test": "turbo run test" } } + +// turbo.json - register tasks +{ + "tasks": { + "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, + "lint": {}, + "test": {} + } +} +``` + +**Why this matters:** + +- Package tasks run in **parallel** across all packages +- Each package's output is cached **individually** +- You can **filter** to specific packages: `turbo run test --filter=web` + +Root Tasks (`//#taskname`) defeat all these benefits. Only use them for tasks that truly cannot exist in any package (extremely rare). + +## #7 Tasks That Need Parallel Execution + Cache Invalidation + +Some tasks can run in parallel (don't need built output from dependencies) but must still invalidate cache when dependency source code changes. Using `dependsOn: ["^taskname"]` forces sequential execution. Using no dependencies breaks cache invalidation. + +**Use Transit Nodes for these tasks:** + +```json +// WRONG - forces sequential execution (SLOW) +"my-task": { + "dependsOn": ["^my-task"] +} + +// ALSO WRONG - no dependency awareness (INCORRECT CACHING) +"my-task": {} + +// CORRECT - use Transit Nodes for parallel + correct caching +{ + "tasks": { + "transit": { "dependsOn": ["^transit"] }, + "my-task": { "dependsOn": ["transit"] } + } +} +``` + +**Why Transit Nodes work:** + +- `transit` creates dependency relationships without matching any actual script +- Tasks that depend on `transit` gain dependency awareness +- Since `transit` completes instantly (no script), tasks run in parallel +- Cache correctly invalidates when dependency source code changes + +**How to identify tasks that need this pattern:** Look for tasks that read source files from dependencies but don't need their build outputs. + +## Missing outputs for File-Producing Tasks + +**Before flagging missing `outputs`, check what the task actually produces:** + +1. Read the package's script (e.g., `"build": "tsc"`, `"test": "vitest"`) +2. Determine if it writes files to disk or only outputs to stdout +3. Only flag if the task produces files that should be cached + +```json +// WRONG - build produces files but they're not cached +"build": { + "dependsOn": ["^build"] +} + +// CORRECT - outputs are cached +"build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] +} +``` + +No `outputs` key is fine for stdout-only tasks. For file-producing tasks, missing `outputs` means Turbo has nothing to cache. + +## Forgetting ^ in dependsOn + +```json +// WRONG - looks for "build" in SAME package (infinite loop or missing) +"build": { + "dependsOn": ["build"] +} + +// CORRECT - runs dependencies' build first +"build": { + "dependsOn": ["^build"] +} +``` + +The `^` means "in dependency packages", not "in this package". + +## Missing persistent on Dev Tasks + +```json +// WRONG - dependent tasks hang waiting for dev to "finish" +"dev": { + "cache": false +} + +// CORRECT +"dev": { + "cache": false, + "persistent": true +} +``` + +## Package Config Missing extends + +```json +// WRONG - packages/web/turbo.json +{ + "tasks": { + "build": { "outputs": [".next/**"] } + } +} + +// CORRECT +{ + "extends": ["//"], + "tasks": { + "build": { "outputs": [".next/**"] } + } +} +``` + +Without `"extends": ["//"]`, Package Configurations are invalid. + +## Root Tasks Need Special Syntax + +To run a task defined only in root `package.json`: + +```bash +# WRONG +turbo run format + +# CORRECT +turbo run //#format +``` + +And in dependsOn: + +```json +"build": { + "dependsOn": ["//#codegen"] // Root package's codegen +} +``` + +## Overwriting Default Inputs + +```json +// WRONG - only watches test files, ignores source changes +"test": { + "inputs": ["tests/**"] +} + +// CORRECT - extends defaults, adds test files +"test": { + "inputs": ["$TURBO_DEFAULT$", "tests/**"] +} +``` + +Without `$TURBO_DEFAULT$`, you replace all default file watching. + +## Caching Tasks with Side Effects + +```json +// WRONG - deploy might be skipped on cache hit +"deploy": { + "dependsOn": ["build"] +} + +// CORRECT +"deploy": { + "dependsOn": ["build"], + "cache": false +} +``` + +Always disable cache for deploy, publish, or mutation tasks. diff --git a/.agents/skills/turborepo/references/configuration/tasks.md b/.agents/skills/turborepo/references/configuration/tasks.md new file mode 100644 index 00000000..a529b513 --- /dev/null +++ b/.agents/skills/turborepo/references/configuration/tasks.md @@ -0,0 +1,285 @@ +# Task Configuration Reference + +Full docs: https://turborepo.dev/docs/reference/configuration#tasks + +## dependsOn + +Controls task execution order. + +```json +{ + "tasks": { + "build": { + "dependsOn": [ + "^build", // Dependencies' build tasks first + "codegen", // Same package's codegen task first + "shared#build" // Specific package's build task + ] + } + } +} +``` + +| Syntax | Meaning | +| ---------- | ------------------------------------ | +| `^task` | Run `task` in all dependencies first | +| `task` | Run `task` in same package first | +| `pkg#task` | Run specific package's task first | + +The `^` prefix is crucial - without it, you're referencing the same package. + +### Transit Nodes for Parallel Tasks + +For tasks like `lint` and `check-types` that can run in parallel but need dependency-aware caching: + +```json +{ + "tasks": { + "transit": { "dependsOn": ["^transit"] }, + "lint": { "dependsOn": ["transit"] }, + "check-types": { "dependsOn": ["transit"] } + } +} +``` + +**DO NOT use `dependsOn: ["^lint"]`** - this forces sequential execution. +**DO NOT use `dependsOn: []`** - this breaks cache invalidation. + +The `transit` task creates dependency relationships without running anything (no matching script), so tasks run in parallel with correct caching. + +## outputs + +Glob patterns for files to cache. **If omitted, nothing is cached.** + +```json +{ + "tasks": { + "build": { + "outputs": ["dist/**", "build/**"] + } + } +} +``` + +**Framework examples:** + +```json +// Next.js +"outputs": [".next/**", "!.next/cache/**"] + +// Vite +"outputs": ["dist/**"] + +// TypeScript (tsc) +"outputs": ["dist/**", "*.tsbuildinfo"] + +// No file outputs (lint, typecheck) +"outputs": [] +``` + +Use `!` prefix to exclude patterns from caching. + +## inputs + +Files considered when calculating task hash. Defaults to all tracked files in package. + +```json +{ + "tasks": { + "test": { + "inputs": ["src/**", "tests/**", "vitest.config.ts"] + } + } +} +``` + +**Special values:** + +| Value | Meaning | +| --------------------- | --------------------------------------- | +| `$TURBO_DEFAULT$` | Include default inputs, then add/remove | +| `$TURBO_ROOT$/` | Reference files from repo root | + +```json +{ + "tasks": { + "build": { + "inputs": [ + "$TURBO_DEFAULT$", + "!README.md", + "$TURBO_ROOT$/tsconfig.base.json" + ] + } + } +} +``` + +## env + +Environment variables to include in task hash. + +```json +{ + "tasks": { + "build": { + "env": [ + "API_URL", + "NEXT_PUBLIC_*", // Wildcard matching + "!DEBUG" // Exclude from hash + ] + } + } +} +``` + +Variables listed here affect cache hits - changing the value invalidates cache. + +## cache + +Enable/disable caching for a task. Default: `true`. + +```json +{ + "tasks": { + "dev": { "cache": false }, + "deploy": { "cache": false } + } +} +``` + +Disable for: dev servers, deploy commands, tasks with side effects. + +## persistent + +Mark long-running tasks that don't exit. Default: `false`. + +```json +{ + "tasks": { + "dev": { + "cache": false, + "persistent": true + } + } +} +``` + +Required for dev servers - without it, dependent tasks wait forever. + +## interactive + +Allow task to receive stdin input. Default: `false`. + +```json +{ + "tasks": { + "login": { + "cache": false, + "interactive": true + } + } +} +``` + +## outputLogs + +Control when logs are shown. Options: `full`, `hash-only`, `new-only`, `errors-only`, `none`. + +```json +{ + "tasks": { + "build": { + "outputLogs": "new-only" // Only show logs on cache miss + } + } +} +``` + +## with + +Run tasks alongside this task. For long-running tasks that need runtime dependencies. + +```json +{ + "tasks": { + "dev": { + "with": ["api#dev"], + "persistent": true, + "cache": false + } + } +} +``` + +Unlike `dependsOn`, `with` runs tasks concurrently (not sequentially). Use for dev servers that need other services running. + +## interruptible + +Allow `turbo watch` to restart the task on changes. Default: `false`. + +```json +{ + "tasks": { + "dev": { + "persistent": true, + "interruptible": true, + "cache": false + } + } +} +``` + +Use for dev servers that don't automatically detect dependency changes. + +## description + +Human-readable description of the task. + +```json +{ + "tasks": { + "build": { + "description": "Compiles the application for production deployment" + } + } +} +``` + +For documentation only - doesn't affect execution or caching. + +## passThroughEnv + +Environment variables available at runtime but NOT included in cache hash. + +```json +{ + "tasks": { + "build": { + "passThroughEnv": ["AWS_SECRET_KEY", "GITHUB_TOKEN"] + } + } +} +``` + +**Warning**: Changes to these vars won't cause cache misses. Use `env` if changes should invalidate cache. + +## extends (Package Configuration only) + +Control task inheritance in Package Configurations. + +```json +// packages/ui/turbo.json +{ + "extends": ["//"], + "tasks": { + "lint": { + "extends": false // Exclude from this package + } + } +} +``` + +| Value | Behavior | +| ---------------- | -------------------------------------------------------------- | +| `true` (default) | Inherit from root turbo.json | +| `false` | Exclude task from package, or define fresh without inheritance | diff --git a/.agents/skills/turborepo/references/environment/RULE.md b/.agents/skills/turborepo/references/environment/RULE.md new file mode 100644 index 00000000..30f2a28d --- /dev/null +++ b/.agents/skills/turborepo/references/environment/RULE.md @@ -0,0 +1,96 @@ +# Environment Variables in Turborepo + +Turborepo provides fine-grained control over which environment variables affect task hashing and runtime availability. + +## Configuration Keys + +### `env` - Task-Specific Variables + +Variables that affect a specific task's hash. When these change, only that task rebuilds. + +```json +{ + "tasks": { + "build": { + "env": ["DATABASE_URL", "API_KEY"] + } + } +} +``` + +### `globalEnv` - Variables Affecting All Tasks + +Variables that affect EVERY task's hash. When these change, all tasks rebuild. + +```json +{ + "globalEnv": ["CI", "NODE_ENV"] +} +``` + +### `passThroughEnv` - Runtime-Only Variables (Not Hashed) + +Variables available at runtime but NOT included in hash. **Use with caution** - changes won't trigger rebuilds. + +```json +{ + "tasks": { + "deploy": { + "passThroughEnv": ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"] + } + } +} +``` + +### `globalPassThroughEnv` - Global Runtime Variables + +Same as `passThroughEnv` but for all tasks. + +```json +{ + "globalPassThroughEnv": ["GITHUB_TOKEN"] +} +``` + +## Wildcards and Negation + +### Wildcards + +Match multiple variables with `*`: + +```json +{ + "env": ["MY_API_*", "FEATURE_FLAG_*"] +} +``` + +This matches `MY_API_URL`, `MY_API_KEY`, `FEATURE_FLAG_DARK_MODE`, etc. + +### Negation + +Exclude variables (useful with framework inference): + +```json +{ + "env": ["!NEXT_PUBLIC_ANALYTICS_ID"] +} +``` + +## Complete Example + +```json +{ + "$schema": "https://v2-8-13-canary-12.turborepo.dev/schema.json", + "globalEnv": ["CI", "NODE_ENV"], + "globalPassThroughEnv": ["GITHUB_TOKEN", "NPM_TOKEN"], + "tasks": { + "build": { + "env": ["DATABASE_URL", "API_*"], + "passThroughEnv": ["SENTRY_AUTH_TOKEN"] + }, + "test": { + "env": ["TEST_DATABASE_URL"] + } + } +} +``` diff --git a/.agents/skills/turborepo/references/environment/gotchas.md b/.agents/skills/turborepo/references/environment/gotchas.md new file mode 100644 index 00000000..d6e420bc --- /dev/null +++ b/.agents/skills/turborepo/references/environment/gotchas.md @@ -0,0 +1,141 @@ +# Environment Variable Gotchas + +Common mistakes and how to fix them. + +## .env Files Must Be in `inputs` + +Turbo does NOT read `.env` files. Your framework (Next.js, Vite, etc.) or `dotenv` loads them. But Turbo needs to know when they change. + +**Wrong:** + +```json +{ + "tasks": { + "build": { + "env": ["DATABASE_URL"] + } + } +} +``` + +**Right:** + +```json +{ + "tasks": { + "build": { + "env": ["DATABASE_URL"], + "inputs": ["$TURBO_DEFAULT$", ".env", ".env.local", ".env.production"] + } + } +} +``` + +## Strict Mode Filters CI Variables + +In strict mode, CI provider variables (GITHUB_TOKEN, GITLAB_CI, etc.) are filtered unless explicitly listed. + +**Symptom:** Task fails with "authentication required" or "permission denied" in CI. + +**Solution:** + +```json +{ + "globalPassThroughEnv": ["GITHUB_TOKEN", "GITLAB_CI", "CI"] +} +``` + +## passThroughEnv Doesn't Affect Hash + +Variables in `passThroughEnv` are available at runtime but changes WON'T trigger rebuilds. + +**Dangerous example:** + +```json +{ + "tasks": { + "build": { + "passThroughEnv": ["API_URL"] + } + } +} +``` + +If `API_URL` changes from staging to production, Turbo may serve a cached build pointing to the wrong API. + +**Use passThroughEnv only for:** + +- Auth tokens that don't affect output (SENTRY_AUTH_TOKEN) +- CI metadata (GITHUB_RUN_ID) +- Variables consumed after build (deploy credentials) + +## Runtime-Created Variables Are Invisible + +Turbo captures env vars at startup. Variables created during execution aren't seen. + +**Won't work:** + +```bash +# In package.json scripts +"build": "export API_URL=$COMPUTED_VALUE && next build" +``` + +**Solution:** Set vars before invoking turbo: + +```bash +API_URL=$COMPUTED_VALUE turbo run build +``` + +## Different .env Files for Different Environments + +If you use `.env.development` and `.env.production`, both should be in inputs. + +```json +{ + "tasks": { + "build": { + "inputs": [ + "$TURBO_DEFAULT$", + ".env", + ".env.local", + ".env.development", + ".env.development.local", + ".env.production", + ".env.production.local" + ] + } + } +} +``` + +## Complete Next.js Example + +```json +{ + "$schema": "https://v2-8-13-canary-12.turborepo.dev/schema.json", + "globalEnv": ["CI", "NODE_ENV", "VERCEL"], + "globalPassThroughEnv": ["GITHUB_TOKEN", "VERCEL_URL"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "env": ["DATABASE_URL", "NEXT_PUBLIC_*", "!NEXT_PUBLIC_ANALYTICS_ID"], + "passThroughEnv": ["SENTRY_AUTH_TOKEN"], + "inputs": [ + "$TURBO_DEFAULT$", + ".env", + ".env.local", + ".env.production", + ".env.production.local" + ], + "outputs": [".next/**", "!.next/cache/**"] + } + } +} +``` + +This config: + +- Hashes DATABASE*URL and NEXT_PUBLIC*\* vars (except analytics) +- Passes through SENTRY_AUTH_TOKEN without hashing +- Includes all .env file variants in the hash +- Makes CI tokens available globally diff --git a/.agents/skills/turborepo/references/environment/modes.md b/.agents/skills/turborepo/references/environment/modes.md new file mode 100644 index 00000000..2e655331 --- /dev/null +++ b/.agents/skills/turborepo/references/environment/modes.md @@ -0,0 +1,101 @@ +# Environment Modes + +Turborepo supports different modes for handling environment variables during task execution. + +## Strict Mode (Default) + +Only explicitly configured variables are available to tasks. + +**Behavior:** + +- Tasks only see vars listed in `env`, `globalEnv`, `passThroughEnv`, or `globalPassThroughEnv` +- Unlisted vars are filtered out +- Tasks fail if they require unlisted variables + +**Benefits:** + +- Guarantees cache correctness +- Prevents accidental dependencies on system vars +- Reproducible builds across machines + +```bash +# Explicit (though it's the default) +turbo run build --env-mode=strict +``` + +## Loose Mode + +All system environment variables are available to tasks. + +```bash +turbo run build --env-mode=loose +``` + +**Behavior:** + +- Every system env var is passed through +- Only vars in `env`/`globalEnv` affect the hash +- Other vars are available but NOT hashed + +**Risks:** + +- Cache may restore incorrect results if unhashed vars changed +- "Works on my machine" bugs +- CI vs local environment mismatches + +**Use case:** Migrating legacy projects or debugging strict mode issues. + +## Framework Inference (Automatic) + +Turborepo automatically detects frameworks and includes their conventional env vars. + +### Inferred Variables by Framework + +| Framework | Pattern | +| ---------------- | ------------------- | +| Next.js | `NEXT_PUBLIC_*` | +| Vite | `VITE_*` | +| Create React App | `REACT_APP_*` | +| Gatsby | `GATSBY_*` | +| Nuxt | `NUXT_*`, `NITRO_*` | +| Expo | `EXPO_PUBLIC_*` | +| Astro | `PUBLIC_*` | +| SvelteKit | `PUBLIC_*` | +| Remix | `REMIX_*` | +| Redwood | `REDWOOD_ENV_*` | +| Sanity | `SANITY_STUDIO_*` | +| Solid | `VITE_*` | + +### Disabling Framework Inference + +Globally via CLI: + +```bash +turbo run build --framework-inference=false +``` + +Or exclude specific patterns in config: + +```json +{ + "tasks": { + "build": { + "env": ["!NEXT_PUBLIC_*"] + } + } +} +``` + +### Why Disable? + +- You want explicit control over all env vars +- Framework vars shouldn't bust the cache (e.g., analytics IDs) +- Debugging unexpected cache misses + +## Checking Environment Mode + +Use `--dry` to see which vars affect each task: + +```bash +turbo run build --dry=json | jq '.tasks[].environmentVariables' +``` diff --git a/.agents/skills/turborepo/references/filtering/RULE.md b/.agents/skills/turborepo/references/filtering/RULE.md new file mode 100644 index 00000000..04e19cc8 --- /dev/null +++ b/.agents/skills/turborepo/references/filtering/RULE.md @@ -0,0 +1,148 @@ +# Turborepo Filter Syntax Reference + +## Running Only Changed Packages: `--affected` + +**The primary way to run only changed packages is `--affected`:** + +```bash +# Run build/test/lint only in changed packages and their dependents +turbo run build test lint --affected +``` + +This compares your current branch to the default branch (usually `main` or `master`) and runs tasks in: + +1. Packages with file changes +2. Packages that depend on changed packages (dependents) + +### Why Include Dependents? + +If you change `@repo/ui`, packages that import `@repo/ui` (like `apps/web`) need to re-run their tasks to verify they still work with the changes. + +### Customizing --affected + +```bash +# Use a different base branch +turbo run build --affected --affected-base=origin/develop + +# Use a different head (current state) +turbo run build --affected --affected-head=HEAD~5 +``` + +### Common CI Pattern + +```yaml +# .github/workflows/ci.yml +- run: turbo run build test lint --affected +``` + +This is the most efficient CI setup - only run tasks for what actually changed. + +--- + +## Manual Git Comparison with --filter + +For more control, use `--filter` with git comparison syntax: + +```bash +# Changed packages + dependents (same as --affected) +turbo run build --filter=...[origin/main] + +# Only changed packages (no dependents) +turbo run build --filter=[origin/main] + +# Changed packages + dependencies (packages they import) +turbo run build --filter=[origin/main]... + +# Changed since last commit +turbo run build --filter=...[HEAD^1] + +# Changed between two commits +turbo run build --filter=[a1b2c3d...e4f5g6h] +``` + +### Comparison Syntax + +| Syntax | Meaning | +| ------------- | ------------------------------------- | +| `[ref]` | Packages changed since `ref` | +| `...[ref]` | Changed packages + their dependents | +| `[ref]...` | Changed packages + their dependencies | +| `...[ref]...` | Dependencies, changed, AND dependents | + +--- + +## Other Filter Types + +Filters select which packages to include in a `turbo run` invocation. + +### Basic Syntax + +```bash +turbo run build --filter= +turbo run build -F +``` + +Multiple filters combine as a union (packages matching ANY filter run). + +### By Package Name + +```bash +--filter=web # exact match +--filter=@acme/* # scope glob +--filter=*-app # name glob +``` + +### By Directory + +```bash +--filter=./apps/* # all packages in apps/ +--filter=./packages/ui # specific directory +``` + +### By Dependencies/Dependents + +| Syntax | Meaning | +| ----------- | -------------------------------------- | +| `pkg...` | Package AND all its dependencies | +| `...pkg` | Package AND all its dependents | +| `...pkg...` | Dependencies, package, AND dependents | +| `^pkg...` | Only dependencies (exclude pkg itself) | +| `...^pkg` | Only dependents (exclude pkg itself) | + +### Negation + +Exclude packages with `!`: + +```bash +--filter=!web # exclude web +--filter=./apps/* --filter=!admin # apps except admin +``` + +### Task Identifiers + +Run a specific task in a specific package: + +```bash +turbo run web#build # only web's build task +turbo run web#build api#test # web build + api test +``` + +### Combining Filters + +Multiple `--filter` flags create a union: + +```bash +turbo run build --filter=web --filter=api # runs in both +``` + +--- + +## Quick Reference: Changed Packages + +| Goal | Command | +| ---------------------------------- | ----------------------------------------------------------- | +| Changed + dependents (recommended) | `turbo run build --affected` | +| Custom base branch | `turbo run build --affected --affected-base=origin/develop` | +| Only changed (no dependents) | `turbo run build --filter=[origin/main]` | +| Changed + dependencies | `turbo run build --filter=[origin/main]...` | +| Since last commit | `turbo run build --filter=...[HEAD^1]` | diff --git a/.agents/skills/turborepo/references/filtering/patterns.md b/.agents/skills/turborepo/references/filtering/patterns.md new file mode 100644 index 00000000..17b9f1c5 --- /dev/null +++ b/.agents/skills/turborepo/references/filtering/patterns.md @@ -0,0 +1,152 @@ +# Common Filter Patterns + +Practical examples for typical monorepo scenarios. + +## Single Package + +Run task in one package: + +```bash +turbo run build --filter=web +turbo run test --filter=@acme/api +``` + +## Package with Dependencies + +Build a package and everything it depends on: + +```bash +turbo run build --filter=web... +``` + +Useful for: ensuring all dependencies are built before the target. + +## Package Dependents + +Run in all packages that depend on a library: + +```bash +turbo run test --filter=...ui +``` + +Useful for: testing consumers after changing a shared package. + +## Dependents Only (Exclude Target) + +Test packages that depend on ui, but not ui itself: + +```bash +turbo run test --filter=...^ui +``` + +## Changed Packages + +Run only in packages with file changes since last commit: + +```bash +turbo run lint --filter=[HEAD^1] +``` + +Since a specific branch point: + +```bash +turbo run lint --filter=[main...HEAD] +``` + +## Changed + Dependents (PR Builds) + +Run in changed packages AND packages that depend on them: + +```bash +turbo run build test --filter=...[HEAD^1] +``` + +Or use the shortcut: + +```bash +turbo run build test --affected +``` + +## Directory-Based + +Run in all apps: + +```bash +turbo run build --filter=./apps/* +``` + +Run in specific directories: + +```bash +turbo run build --filter=./apps/web --filter=./apps/api +``` + +## Scope-Based + +Run in all packages under a scope: + +```bash +turbo run build --filter=@acme/* +``` + +## Exclusions + +Run in all apps except admin: + +```bash +turbo run build --filter=./apps/* --filter=!admin +``` + +Run everywhere except specific packages: + +```bash +turbo run lint --filter=!legacy-app --filter=!deprecated-pkg +``` + +## Complex Combinations + +Apps that changed, plus their dependents: + +```bash +turbo run build --filter=...[HEAD^1] --filter=./apps/* +``` + +All packages except docs, but only if changed: + +```bash +turbo run build --filter=[main...HEAD] --filter=!docs +``` + +## Debugging Filters + +Use `--dry` to see what would run without executing: + +```bash +turbo run build --filter=web... --dry +``` + +Use `--dry=json` for machine-readable output: + +```bash +turbo run build --filter=...[HEAD^1] --dry=json +``` + +## CI/CD Patterns + +PR validation (most common): + +```bash +turbo run build test lint --affected +``` + +Deploy only changed apps: + +```bash +turbo run deploy --filter=./apps/* --filter=[main...HEAD] +``` + +Full rebuild of specific app and deps: + +```bash +turbo run build --filter=production-app... +``` diff --git a/.agents/skills/turborepo/references/watch/RULE.md b/.agents/skills/turborepo/references/watch/RULE.md new file mode 100644 index 00000000..44bcf13e --- /dev/null +++ b/.agents/skills/turborepo/references/watch/RULE.md @@ -0,0 +1,99 @@ +# turbo watch + +Full docs: https://turborepo.dev/docs/reference/watch + +Re-run tasks automatically when code changes. Dependency-aware. + +```bash +turbo watch [tasks] +``` + +## Basic Usage + +```bash +# Watch and re-run build task when code changes +turbo watch build + +# Watch multiple tasks +turbo watch build test lint +``` + +Tasks re-run in order configured in `turbo.json` when source files change. + +## With Persistent Tasks + +Persistent tasks (`"persistent": true`) won't exit, so they can't be depended on. They work the same in `turbo watch` as `turbo run`. + +### Dependency-Aware Persistent Tasks + +If your tool has built-in watching (like `next dev`), use its watcher: + +```json +{ + "tasks": { + "dev": { + "persistent": true, + "cache": false + } + } +} +``` + +### Non-Dependency-Aware Tools + +For tools that don't detect dependency changes, use `interruptible`: + +```json +{ + "tasks": { + "dev": { + "persistent": true, + "interruptible": true, + "cache": false + } + } +} +``` + +`turbo watch` will restart interruptible tasks when dependencies change. + +## Limitations + +### Caching + +Caching is experimental with watch mode: + +```bash +turbo watch your-tasks --experimental-write-cache +``` + +### Task Outputs in Source Control + +If tasks write files tracked by git, watch mode may loop infinitely. Watch mode uses file hashes to prevent this but it's not foolproof. + +**Recommendation**: Remove task outputs from git. + +## vs turbo run + +| Feature | `turbo run` | `turbo watch` | +| ----------------- | ----------- | ------------- | +| Runs once | Yes | No | +| Re-runs on change | No | Yes | +| Caching | Full | Experimental | +| Use case | CI, one-off | Development | + +## Common Patterns + +### Development Workflow + +```bash +# Run dev servers and watch for build changes +turbo watch dev build +``` + +### Type Checking During Development + +```bash +# Watch and re-run type checks +turbo watch check-types +``` diff --git a/.agents/skills/workleap-web-configs/SKILL.md b/.agents/skills/workleap-web-configs/SKILL.md index 8d3a699a..44634fbc 100644 --- a/.agents/skills/workleap-web-configs/SKILL.md +++ b/.agents/skills/workleap-web-configs/SKILL.md @@ -1,17 +1,17 @@ --- name: workleap-web-configs description: | - Guide for Workleap's shared web configuration packages: @workleap/eslint-configs, @workleap/typescript-configs, @workleap/rsbuild-configs, @workleap/rslib-configs, @workleap/stylelint-configs, and @workleap/browserslist-config. - - Use this skill when: - (1) Setting up or modifying shared web tooling configs in a Workleap project - (2) ESLint config with @workleap/eslint-configs (defineWebApplicationConfig, defineReactLibraryConfig, defineTypeScriptLibraryConfig, defineMonorepoWorkspaceConfig) - (3) TypeScript config with @workleap/typescript-configs (web-application, library, monorepo-workspace) - (4) Rsbuild config with @workleap/rsbuild-configs (defineDevConfig, defineBuildConfig, defineStorybookConfig) - (5) Rslib config with @workleap/rslib-configs for libraries - (6) Stylelint and Browserslist shared configs - (7) Extending configs or monorepo (Turborepo) vs polyrepo strategies - (8) Troubleshooting wl-web-configs, ESM/ESNext constraints, or Storybook with Rsbuild/Rslib + Workleap's shared web configuration packages (@workleap/eslint-configs, @workleap/typescript-configs, @workleap/rsbuild-configs, @workleap/rslib-configs, @workleap/stylelint-configs, @workleap/browserslist-config). + + Use when: + (1) Setting up or customizing shared web tooling configs in a Workleap project + (2) Configuring ESLint by project type (web app, React library, TS library, monorepo) + (3) Configuring TypeScript by project type (web-application, library, monorepo-workspace) + (4) Configuring Rsbuild or Rslib bundling (dev, build, Storybook) + (5) Configuring Stylelint, Browserslist, or monorepo (Turborepo) vs polyrepo strategies + (6) Extending or customizing shared configs, troubleshooting ESM/ESNext constraints +metadata: + version: 1.2 --- # wl-web-configs @@ -71,56 +71,15 @@ IE 11 last 2 OperaMobile 12.1 versions ``` -## Detailed Documentation +## Reference Guide -For comprehensive setup guides and options, read the appropriate reference file: +For comprehensive setup guides, options, and examples, read the appropriate reference file: -- **ESLint**: See [references/eslint.md](references/eslint.md) -- **TypeScript**: See [references/typescript.md](references/typescript.md) -- **Rsbuild**: See [references/rsbuild.md](references/rsbuild.md) -- **Rslib**: See [references/rslib.md](references/rslib.md) -- **Stylelint**: See [references/stylelint.md](references/stylelint.md) - -## Common Patterns - -### Customizing Default Rules - -All `define*` functions accept a second argument for customization: - -```ts -// ESLint example -export default defineWebApplicationConfig(import.meta.dirname, { - core: { "no-var": "off" }, - typescript: { "@stylistic/quote-props": "off" } -}); -``` - -```json -// TypeScript example - tsconfig.json -{ - "extends": ["@workleap/typescript-configs/web-application.json"], - "compilerOptions": { "strict": false }, - "exclude": ["dist", "node_modules"] -} -``` - -### Configuration Transformers (Advanced) - -For full control over Rsbuild/Rslib configs: - -```ts -import { defineDevConfig, type RsbuildConfigTransformer } from "@workleap/rsbuild-configs"; - -const customTransformer: RsbuildConfigTransformer = (config) => { - config.tools = config.tools ?? {}; - // modify config - return config; -}; - -export default defineDevConfig({ - transformers: [customTransformer] -}); -``` +- **ESLint** — [references/eslint.md](references/eslint.md): Installation, `define*Config` functions, rule categories, customization, and VS Code integration +- **TypeScript** — [references/typescript.md](references/typescript.md): Config files by project type, compiler option overrides, path mappings, and CLI scripts +- **Rsbuild** — [references/rsbuild.md](references/rsbuild.md): Dev/build/Storybook configs, predefined options, transformers, and Turborepo setup +- **Rslib** — [references/rslib.md](references/rslib.md): Library build/dev/Storybook configs, bundleless vs bundle, transformers, and type declarations +- **Stylelint** — [references/stylelint.md](references/stylelint.md): Installation, `.stylelintrc.json` setup, Prettier integration, and VS Code settings ## Critical Rules diff --git a/.changeset/major-hornets-sleep.md b/.changeset/major-hornets-sleep.md new file mode 100644 index 00000000..45e88ae8 --- /dev/null +++ b/.changeset/major-hornets-sleep.md @@ -0,0 +1,9 @@ +--- +"@workleap/common-room": patch +"@workleap/honeycomb": patch +"@workleap/logrocket": patch +"@workleap/telemetry": patch +"@workleap/mixpanel": patch +--- + +Updated packages to use range version for workspace dependency versions. diff --git a/.claude/skills/vitest/GENERATION.md b/.claude/skills/vitest/GENERATION.md new file mode 100644 index 00000000..9bc76640 --- /dev/null +++ b/.claude/skills/vitest/GENERATION.md @@ -0,0 +1,5 @@ +# Generation Info + +- **Source:** `sources/vitest` +- **Git SHA:** `4a7321e10672f00f0bb698823a381c2cc245b8f7` +- **Generated:** 2026-01-28 diff --git a/.claude/skills/vitest/SKILL.md b/.claude/skills/vitest/SKILL.md new file mode 100644 index 00000000..0578bdcf --- /dev/null +++ b/.claude/skills/vitest/SKILL.md @@ -0,0 +1,52 @@ +--- +name: vitest +description: Vitest fast unit testing framework powered by Vite with Jest-compatible API. Use when writing tests, mocking, configuring coverage, or working with test filtering and fixtures. +metadata: + author: Anthony Fu + version: "2026.1.28" + source: Generated from https://github.com/vitest-dev/vitest, scripts located at https://github.com/antfu/skills +--- + +Vitest is a next-generation testing framework powered by Vite. It provides a Jest-compatible API with native ESM, TypeScript, and JSX support out of the box. Vitest shares the same config, transformers, resolvers, and plugins with your Vite app. + +**Key Features:** +- Vite-native: Uses Vite's transformation pipeline for fast HMR-like test updates +- Jest-compatible: Drop-in replacement for most Jest test suites +- Smart watch mode: Only reruns affected tests based on module graph +- Native ESM, TypeScript, JSX support without configuration +- Multi-threaded workers for parallel test execution +- Built-in coverage via V8 or Istanbul +- Snapshot testing, mocking, and spy utilities + +> The skill is based on Vitest 3.x, generated at 2026-01-28. + +## Core + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Configuration | Vitest and Vite config integration, defineConfig usage | [core-config](references/core-config.md) | +| CLI | Command line interface, commands and options | [core-cli](references/core-cli.md) | +| Test API | test/it function, modifiers like skip, only, concurrent | [core-test-api](references/core-test-api.md) | +| Describe API | describe/suite for grouping tests and nested suites | [core-describe](references/core-describe.md) | +| Expect API | Assertions with toBe, toEqual, matchers and asymmetric matchers | [core-expect](references/core-expect.md) | +| Hooks | beforeEach, afterEach, beforeAll, afterAll, aroundEach | [core-hooks](references/core-hooks.md) | + +## Features + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Mocking | Mock functions, modules, timers, dates with vi utilities | [features-mocking](references/features-mocking.md) | +| Snapshots | Snapshot testing with toMatchSnapshot and inline snapshots | [features-snapshots](references/features-snapshots.md) | +| Coverage | Code coverage with V8 or Istanbul providers | [features-coverage](references/features-coverage.md) | +| Test Context | Test fixtures, context.expect, test.extend for custom fixtures | [features-context](references/features-context.md) | +| Concurrency | Concurrent tests, parallel execution, sharding | [features-concurrency](references/features-concurrency.md) | +| Filtering | Filter tests by name, file patterns, tags | [features-filtering](references/features-filtering.md) | + +## Advanced + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Vi Utilities | vi helper: mock, spyOn, fake timers, hoisted, waitFor | [advanced-vi](references/advanced-vi.md) | +| Environments | Test environments: node, jsdom, happy-dom, custom | [advanced-environments](references/advanced-environments.md) | +| Type Testing | Type-level testing with expectTypeOf and assertType | [advanced-type-testing](references/advanced-type-testing.md) | +| Projects | Multi-project workspaces, different configs per project | [advanced-projects](references/advanced-projects.md) | diff --git a/.claude/skills/vitest/references/advanced-environments.md b/.claude/skills/vitest/references/advanced-environments.md new file mode 100644 index 00000000..25a1d5b0 --- /dev/null +++ b/.claude/skills/vitest/references/advanced-environments.md @@ -0,0 +1,264 @@ +--- +name: test-environments +description: Configure environments like jsdom, happy-dom for browser APIs +--- + +# Test Environments + +## Available Environments + +- `node` (default) - Node.js environment +- `jsdom` - Browser-like with DOM APIs +- `happy-dom` - Faster alternative to jsdom +- `edge-runtime` - Vercel Edge Runtime + +## Configuration + +```ts +// vitest.config.ts +defineConfig({ + test: { + environment: 'jsdom', + + // Environment-specific options + environmentOptions: { + jsdom: { + url: 'http://localhost', + }, + }, + }, +}) +``` + +## Installing Environment Packages + +```bash +# jsdom +npm i -D jsdom + +# happy-dom (faster, fewer APIs) +npm i -D happy-dom +``` + +## Per-File Environment + +Use magic comment at top of file: + +```ts +// @vitest-environment jsdom + +import { expect, test } from 'vitest' + +test('DOM test', () => { + const div = document.createElement('div') + expect(div).toBeInstanceOf(HTMLDivElement) +}) +``` + +## jsdom Environment + +Full browser environment simulation: + +```ts +// @vitest-environment jsdom + +test('DOM manipulation', () => { + document.body.innerHTML = '
' + + const app = document.getElementById('app') + app.textContent = 'Hello' + + expect(app.textContent).toBe('Hello') +}) + +test('window APIs', () => { + expect(window.location.href).toBeDefined() + expect(localStorage).toBeDefined() +}) +``` + +### jsdom Options + +```ts +defineConfig({ + test: { + environmentOptions: { + jsdom: { + url: 'http://localhost:3000', + html: '', + userAgent: 'custom-agent', + resources: 'usable', + }, + }, + }, +}) +``` + +## happy-dom Environment + +Faster but fewer APIs: + +```ts +// @vitest-environment happy-dom + +test('basic DOM', () => { + const el = document.createElement('div') + el.className = 'test' + expect(el.className).toBe('test') +}) +``` + +## Multiple Environments per Project + +Use projects for different environments: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + environment: 'node', + }, + }, + { + test: { + name: 'dom', + include: ['tests/dom/**/*.test.ts'], + environment: 'jsdom', + }, + }, + ], + }, +}) +``` + +## Custom Environment + +Create custom environment package: + +```ts +// vitest-environment-custom/index.ts +import type { Environment } from 'vitest/runtime' + +export default { + name: 'custom', + viteEnvironment: 'ssr', // or 'client' + + setup() { + // Setup global state + globalThis.myGlobal = 'value' + + return { + teardown() { + delete globalThis.myGlobal + }, + } + }, +} +``` + +Use with: + +```ts +defineConfig({ + test: { + environment: 'custom', + }, +}) +``` + +## Environment with VM + +For full isolation: + +```ts +export default { + name: 'isolated', + viteEnvironment: 'ssr', + + async setupVM() { + const vm = await import('node:vm') + const context = vm.createContext() + + return { + getVmContext() { + return context + }, + teardown() {}, + } + }, + + setup() { + return { teardown() {} } + }, +} +``` + +## Browser Mode (Separate from Environments) + +For real browser testing, use Vitest Browser Mode: + +```ts +defineConfig({ + test: { + browser: { + enabled: true, + name: 'chromium', // or 'firefox', 'webkit' + provider: 'playwright', + }, + }, +}) +``` + +## CSS and Assets + +In jsdom/happy-dom, configure CSS handling: + +```ts +defineConfig({ + test: { + css: true, // Process CSS + + // Or with options + css: { + include: /\.module\.css$/, + modules: { + classNameStrategy: 'non-scoped', + }, + }, + }, +}) +``` + +## Fixing External Dependencies + +If external deps fail with CSS/asset errors: + +```ts +defineConfig({ + test: { + server: { + deps: { + inline: ['problematic-package'], + }, + }, + }, +}) +``` + +## Key Points + +- Default is `node` - no browser APIs +- Use `jsdom` for full browser simulation +- Use `happy-dom` for faster tests with basic DOM +- Per-file environment via `// @vitest-environment` comment +- Use projects for multiple environment configurations +- Browser Mode is for real browser testing, not environment + + diff --git a/.claude/skills/vitest/references/advanced-projects.md b/.claude/skills/vitest/references/advanced-projects.md new file mode 100644 index 00000000..57b9a735 --- /dev/null +++ b/.claude/skills/vitest/references/advanced-projects.md @@ -0,0 +1,300 @@ +--- +name: projects-workspaces +description: Multi-project configuration for monorepos and different test types +--- + +# Projects + +Run different test configurations in the same Vitest process. + +## Basic Projects Setup + +```ts +// vitest.config.ts +defineConfig({ + test: { + projects: [ + // Glob patterns for config files + 'packages/*', + + // Inline config + { + test: { + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + environment: 'node', + }, + }, + { + test: { + name: 'integration', + include: ['tests/integration/**/*.test.ts'], + environment: 'jsdom', + }, + }, + ], + }, +}) +``` + +## Monorepo Pattern + +```ts +defineConfig({ + test: { + projects: [ + // Each package has its own vitest.config.ts + 'packages/core', + 'packages/cli', + 'packages/utils', + ], + }, +}) +``` + +Package config: + +```ts +// packages/core/vitest.config.ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + name: 'core', + include: ['src/**/*.test.ts'], + environment: 'node', + }, +}) +``` + +## Different Environments + +Run same tests in different environments: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'happy-dom', + root: './shared-tests', + environment: 'happy-dom', + setupFiles: ['./setup.happy-dom.ts'], + }, + }, + { + test: { + name: 'node', + root: './shared-tests', + environment: 'node', + setupFiles: ['./setup.node.ts'], + }, + }, + ], + }, +}) +``` + +## Browser + Node Projects + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + environment: 'node', + }, + }, + { + test: { + name: 'browser', + include: ['tests/browser/**/*.test.ts'], + browser: { + enabled: true, + name: 'chromium', + provider: 'playwright', + }, + }, + }, + ], + }, +}) +``` + +## Shared Configuration + +```ts +// vitest.shared.ts +export const sharedConfig = { + testTimeout: 10000, + setupFiles: ['./tests/setup.ts'], +} + +// vitest.config.ts +import { sharedConfig } from './vitest.shared' + +defineConfig({ + test: { + projects: [ + { + test: { + ...sharedConfig, + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + }, + }, + { + test: { + ...sharedConfig, + name: 'e2e', + include: ['tests/e2e/**/*.test.ts'], + }, + }, + ], + }, +}) +``` + +## Project-Specific Dependencies + +Each project can have different dependencies inlined: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'project-a', + server: { + deps: { + inline: ['package-a'], + }, + }, + }, + }, + ], + }, +}) +``` + +## Running Specific Projects + +```bash +# Run specific project +vitest --project unit +vitest --project integration + +# Multiple projects +vitest --project unit --project e2e + +# Exclude project +vitest --project.ignore browser +``` + +## Providing Values to Projects + +Share values from config to tests: + +```ts +// vitest.config.ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'staging', + provide: { + apiUrl: 'https://staging.api.com', + debug: true, + }, + }, + }, + { + test: { + name: 'production', + provide: { + apiUrl: 'https://api.com', + debug: false, + }, + }, + }, + ], + }, +}) + +// In tests, use inject +import { inject } from 'vitest' + +test('uses correct api', () => { + const url = inject('apiUrl') + expect(url).toContain('api.com') +}) +``` + +## With Fixtures + +```ts +const test = base.extend({ + apiUrl: ['/default', { injected: true }], +}) + +test('uses injected url', ({ apiUrl }) => { + // apiUrl comes from project's provide config +}) +``` + +## Project Isolation + +Each project runs in its own thread pool by default: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'isolated', + isolate: true, // Full isolation + pool: 'forks', + }, + }, + ], + }, +}) +``` + +## Global Setup per Project + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'with-db', + globalSetup: ['./tests/db-setup.ts'], + }, + }, + ], + }, +}) +``` + +## Key Points + +- Projects run in same Vitest process +- Each project can have different environment, config +- Use glob patterns for monorepo packages +- Run specific projects with `--project` flag +- Use `provide` to inject config values into tests +- Projects inherit from root config unless overridden + + diff --git a/.claude/skills/vitest/references/advanced-type-testing.md b/.claude/skills/vitest/references/advanced-type-testing.md new file mode 100644 index 00000000..f67a034e --- /dev/null +++ b/.claude/skills/vitest/references/advanced-type-testing.md @@ -0,0 +1,237 @@ +--- +name: type-testing +description: Test TypeScript types with expectTypeOf and assertType +--- + +# Type Testing + +Test TypeScript types without runtime execution. + +## Setup + +Type tests use `.test-d.ts` extension: + +```ts +// math.test-d.ts +import { expectTypeOf } from 'vitest' +import { add } from './math' + +test('add returns number', () => { + expectTypeOf(add).returns.toBeNumber() +}) +``` + +## Configuration + +```ts +defineConfig({ + test: { + typecheck: { + enabled: true, + + // Only type check + only: false, + + // Checker: 'tsc' or 'vue-tsc' + checker: 'tsc', + + // Include patterns + include: ['**/*.test-d.ts'], + + // tsconfig to use + tsconfig: './tsconfig.json', + }, + }, +}) +``` + +## expectTypeOf API + +```ts +import { expectTypeOf } from 'vitest' + +// Basic type checks +expectTypeOf().toBeString() +expectTypeOf().toBeNumber() +expectTypeOf().toBeBoolean() +expectTypeOf().toBeNull() +expectTypeOf().toBeUndefined() +expectTypeOf().toBeVoid() +expectTypeOf().toBeNever() +expectTypeOf().toBeAny() +expectTypeOf().toBeUnknown() +expectTypeOf().toBeObject() +expectTypeOf().toBeFunction() +expectTypeOf<[]>().toBeArray() +expectTypeOf().toBeSymbol() +``` + +## Value Type Checking + +```ts +const value = 'hello' +expectTypeOf(value).toBeString() + +const obj = { name: 'test', count: 42 } +expectTypeOf(obj).toMatchTypeOf<{ name: string }>() +expectTypeOf(obj).toHaveProperty('name') +``` + +## Function Types + +```ts +function greet(name: string): string { + return `Hello, ${name}` +} + +expectTypeOf(greet).toBeFunction() +expectTypeOf(greet).parameters.toEqualTypeOf<[string]>() +expectTypeOf(greet).returns.toBeString() + +// Parameter checking +expectTypeOf(greet).parameter(0).toBeString() +``` + +## Object Types + +```ts +interface User { + id: number + name: string + email?: string +} + +expectTypeOf().toHaveProperty('id') +expectTypeOf().toHaveProperty('name').toBeString() + +// Check shape +expectTypeOf({ id: 1, name: 'test' }).toMatchTypeOf() +``` + +## Equality vs Matching + +```ts +interface A { x: number } +interface B { x: number; y: string } + +// toMatchTypeOf - subset matching +expectTypeOf().toMatchTypeOf() // B extends A + +// toEqualTypeOf - exact match +expectTypeOf().not.toEqualTypeOf() // Not exact match +expectTypeOf().toEqualTypeOf<{ x: number }>() // Exact match +``` + +## Branded Types + +```ts +type UserId = number & { __brand: 'UserId' } +type PostId = number & { __brand: 'PostId' } + +expectTypeOf().not.toEqualTypeOf() +expectTypeOf().not.toEqualTypeOf() +``` + +## Generic Types + +```ts +function identity(value: T): T { + return value +} + +expectTypeOf(identity).returns.toBeString() +expectTypeOf(identity).returns.toBeNumber() +``` + +## Nullable Types + +```ts +type MaybeString = string | null | undefined + +expectTypeOf().toBeNullable() +expectTypeOf().not.toBeNullable() +``` + +## assertType + +Assert a value matches a type (no assertion at runtime): + +```ts +import { assertType } from 'vitest' + +function getUser(): User | null { + return { id: 1, name: 'test' } +} + +test('returns user', () => { + const result = getUser() + + // @ts-expect-error - should fail type check + assertType(result) + + // Correct type + assertType(result) +}) +``` + +## Using @ts-expect-error + +Test that code produces type error: + +```ts +test('rejects wrong types', () => { + function requireString(s: string) {} + + // @ts-expect-error - number not assignable to string + requireString(123) +}) +``` + +## Running Type Tests + +```bash +# Run type tests +vitest typecheck + +# Run alongside unit tests +vitest --typecheck + +# Type tests only +vitest --typecheck.only +``` + +## Mixed Test Files + +Combine runtime and type tests: + +```ts +// user.test.ts +import { describe, expect, expectTypeOf, test } from 'vitest' +import { createUser } from './user' + +describe('createUser', () => { + test('runtime: creates user', () => { + const user = createUser('John') + expect(user.name).toBe('John') + }) + + test('types: returns User type', () => { + expectTypeOf(createUser).returns.toMatchTypeOf<{ name: string }>() + }) +}) +``` + +## Key Points + +- Use `.test-d.ts` for type-only tests +- `expectTypeOf` for type assertions +- `toMatchTypeOf` for subset matching +- `toEqualTypeOf` for exact type matching +- Use `@ts-expect-error` to test type errors +- Run with `vitest typecheck` or `--typecheck` + + diff --git a/.claude/skills/vitest/references/advanced-vi.md b/.claude/skills/vitest/references/advanced-vi.md new file mode 100644 index 00000000..57a47842 --- /dev/null +++ b/.claude/skills/vitest/references/advanced-vi.md @@ -0,0 +1,249 @@ +--- +name: vi-utilities +description: vi helper for mocking, timers, utilities +--- + +# Vi Utilities + +The `vi` helper provides mocking and utility functions. + +```ts +import { vi } from 'vitest' +``` + +## Mock Functions + +```ts +// Create mock +const fn = vi.fn() +const fnWithImpl = vi.fn((x) => x * 2) + +// Check if mock +vi.isMockFunction(fn) // true + +// Mock methods +fn.mockReturnValue(42) +fn.mockReturnValueOnce(1) +fn.mockResolvedValue(data) +fn.mockRejectedValue(error) +fn.mockImplementation(() => 'result') +fn.mockImplementationOnce(() => 'once') + +// Clear/reset +fn.mockClear() // Clear call history +fn.mockReset() // Clear history + implementation +fn.mockRestore() // Restore original (for spies) +``` + +## Spying + +```ts +const obj = { method: () => 'original' } + +const spy = vi.spyOn(obj, 'method') +obj.method() + +expect(spy).toHaveBeenCalled() + +// Mock implementation +spy.mockReturnValue('mocked') + +// Spy on getter/setter +vi.spyOn(obj, 'prop', 'get').mockReturnValue('value') +``` + +## Module Mocking + +```ts +// Hoisted to top of file +vi.mock('./module', () => ({ + fn: vi.fn(), +})) + +// Partial mock +vi.mock('./module', async (importOriginal) => ({ + ...(await importOriginal()), + specificFn: vi.fn(), +})) + +// Spy mode - keep implementation +vi.mock('./module', { spy: true }) + +// Import actual module inside mock +const actual = await vi.importActual('./module') + +// Import as mock +const mocked = await vi.importMock('./module') +``` + +## Dynamic Mocking + +```ts +// Not hoisted - use with dynamic imports +vi.doMock('./config', () => ({ key: 'value' })) +const config = await import('./config') + +// Unmock +vi.doUnmock('./config') +vi.unmock('./module') // Hoisted +``` + +## Reset Modules + +```ts +// Clear module cache +vi.resetModules() + +// Wait for dynamic imports +await vi.dynamicImportSettled() +``` + +## Fake Timers + +```ts +vi.useFakeTimers() + +setTimeout(() => console.log('done'), 1000) + +// Advance time +vi.advanceTimersByTime(1000) +vi.advanceTimersByTimeAsync(1000) // For async callbacks +vi.advanceTimersToNextTimer() +vi.advanceTimersToNextFrame() // requestAnimationFrame + +// Run all timers +vi.runAllTimers() +vi.runAllTimersAsync() +vi.runOnlyPendingTimers() + +// Clear timers +vi.clearAllTimers() + +// Check state +vi.getTimerCount() +vi.isFakeTimers() + +// Restore +vi.useRealTimers() +``` + +## Mock Date/Time + +```ts +vi.setSystemTime(new Date('2024-01-01')) +expect(new Date().getFullYear()).toBe(2024) + +vi.getMockedSystemTime() // Get mocked date +vi.getRealSystemTime() // Get real time (ms) +``` + +## Global/Env Mocking + +```ts +// Stub global +vi.stubGlobal('fetch', vi.fn()) +vi.unstubAllGlobals() + +// Stub environment +vi.stubEnv('API_KEY', 'test') +vi.stubEnv('NODE_ENV', 'test') +vi.unstubAllEnvs() +``` + +## Hoisted Code + +Run code before imports: + +```ts +const mock = vi.hoisted(() => vi.fn()) + +vi.mock('./module', () => ({ + fn: mock, // Can reference hoisted variable +})) +``` + +## Waiting Utilities + +```ts +// Wait for callback to succeed +await vi.waitFor(async () => { + const el = document.querySelector('.loaded') + expect(el).toBeTruthy() +}, { timeout: 5000, interval: 100 }) + +// Wait for truthy value +const element = await vi.waitUntil( + () => document.querySelector('.loaded'), + { timeout: 5000 } +) +``` + +## Mock Object + +Mock all methods of an object: + +```ts +const original = { + method: () => 'real', + nested: { fn: () => 'nested' }, +} + +const mocked = vi.mockObject(original) +mocked.method() // undefined (mocked) +mocked.method.mockReturnValue('mocked') + +// Spy mode +const spied = vi.mockObject(original, { spy: true }) +spied.method() // 'real' +expect(spied.method).toHaveBeenCalled() +``` + +## Test Configuration + +```ts +vi.setConfig({ + testTimeout: 10_000, + hookTimeout: 10_000, +}) + +vi.resetConfig() +``` + +## Global Mock Management + +```ts +vi.clearAllMocks() // Clear all mock call history +vi.resetAllMocks() // Reset + clear implementation +vi.restoreAllMocks() // Restore originals (spies) +``` + +## vi.mocked Type Helper + +TypeScript helper for mocked values: + +```ts +import { myFn } from './module' +vi.mock('./module') + +// Type as mock +vi.mocked(myFn).mockReturnValue('typed') + +// Deep mocking +vi.mocked(myModule, { deep: true }) + +// Partial mock typing +vi.mocked(fn, { partial: true }).mockResolvedValue({ ok: true }) +``` + +## Key Points + +- `vi.mock` is hoisted - use `vi.doMock` for dynamic mocking +- `vi.hoisted` lets you reference variables in mock factories +- Use `vi.spyOn` to spy on existing methods +- Fake timers require explicit setup and teardown +- `vi.waitFor` retries until assertion passes + + diff --git a/.claude/skills/vitest/references/core-cli.md b/.claude/skills/vitest/references/core-cli.md new file mode 100644 index 00000000..7a05c049 --- /dev/null +++ b/.claude/skills/vitest/references/core-cli.md @@ -0,0 +1,166 @@ +--- +name: vitest-cli +description: Command line interface commands and options +--- + +# Command Line Interface + +## Commands + +### `vitest` + +Start Vitest in watch mode (dev) or run mode (CI): + +```bash +vitest # Watch mode in dev, run mode in CI +vitest foobar # Run tests containing "foobar" in path +vitest basic/foo.test.ts:10 # Run specific test by file and line number +``` + +### `vitest run` + +Run tests once without watch mode: + +```bash +vitest run +vitest run --coverage +``` + +### `vitest watch` + +Explicitly start watch mode: + +```bash +vitest watch +``` + +### `vitest related` + +Run tests that import specific files (useful with lint-staged): + +```bash +vitest related src/index.ts src/utils.ts --run +``` + +### `vitest bench` + +Run only benchmark tests: + +```bash +vitest bench +``` + +### `vitest list` + +List all matching tests without running them: + +```bash +vitest list # List test names +vitest list --json # Output as JSON +vitest list --filesOnly # List only test files +``` + +### `vitest init` + +Initialize project setup: + +```bash +vitest init browser # Set up browser testing +``` + +## Common Options + +```bash +# Configuration +--config # Path to config file +--project # Run specific project + +# Filtering +--testNamePattern, -t # Run tests matching pattern +--changed # Run tests for changed files +--changed HEAD~1 # Tests for last commit changes + +# Reporters +--reporter # default, verbose, dot, json, html +--reporter=html --outputFile=report.html + +# Coverage +--coverage # Enable coverage +--coverage.provider v8 # Use v8 provider +--coverage.reporter text,html + +# Execution +--shard / # Split tests across machines +--bail # Stop after n failures +--retry # Retry failed tests n times +--sequence.shuffle # Randomize test order + +# Watch mode +--no-watch # Disable watch mode +--standalone # Start without running tests + +# Environment +--environment # jsdom, happy-dom, node +--globals # Enable global APIs + +# Debugging +--inspect # Enable Node inspector +--inspect-brk # Break on start + +# Output +--silent # Suppress console output +--no-color # Disable colors +``` + +## Package.json Scripts + +```json +{ + "scripts": { + "test": "vitest", + "test:run": "vitest run", + "test:ui": "vitest --ui", + "coverage": "vitest run --coverage" + } +} +``` + +## Sharding for CI + +Split tests across multiple machines: + +```bash +# Machine 1 +vitest run --shard=1/3 --reporter=blob + +# Machine 2 +vitest run --shard=2/3 --reporter=blob + +# Machine 3 +vitest run --shard=3/3 --reporter=blob + +# Merge reports +vitest --merge-reports --reporter=junit +``` + +## Watch Mode Keyboard Shortcuts + +In watch mode, press: +- `a` - Run all tests +- `f` - Run only failed tests +- `u` - Update snapshots +- `p` - Filter by filename pattern +- `t` - Filter by test name pattern +- `q` - Quit + +## Key Points + +- Watch mode is default in dev, run mode in CI (when `process.env.CI` is set) +- Use `--run` flag to ensure single run (important for lint-staged) +- Both camelCase (`--testTimeout`) and kebab-case (`--test-timeout`) work +- Boolean options can be negated with `--no-` prefix + + diff --git a/.claude/skills/vitest/references/core-config.md b/.claude/skills/vitest/references/core-config.md new file mode 100644 index 00000000..76002a58 --- /dev/null +++ b/.claude/skills/vitest/references/core-config.md @@ -0,0 +1,174 @@ +--- +name: vitest-configuration +description: Configure Vitest with vite.config.ts or vitest.config.ts +--- + +# Configuration + +Vitest reads configuration from `vitest.config.ts` or `vite.config.ts`. It shares the same config format as Vite. + +## Basic Setup + +```ts +// vitest.config.ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + // test options + }, +}) +``` + +## Using with Existing Vite Config + +Add Vitest types reference and use the `test` property: + +```ts +// vite.config.ts +/// +import { defineConfig } from 'vite' + +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + }, +}) +``` + +## Merging Configs + +If you have separate config files, use `mergeConfig`: + +```ts +// vitest.config.ts +import { defineConfig, mergeConfig } from 'vitest/config' +import viteConfig from './vite.config' + +export default mergeConfig(viteConfig, defineConfig({ + test: { + environment: 'jsdom', + }, +})) +``` + +## Common Options + +```ts +defineConfig({ + test: { + // Enable global APIs (describe, it, expect) without imports + globals: true, + + // Test environment: 'node', 'jsdom', 'happy-dom' + environment: 'node', + + // Setup files to run before each test file + setupFiles: ['./tests/setup.ts'], + + // Include patterns for test files + include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'], + + // Exclude patterns + exclude: ['**/node_modules/**', '**/dist/**'], + + // Test timeout in ms + testTimeout: 5000, + + // Hook timeout in ms + hookTimeout: 10000, + + // Enable watch mode by default + watch: true, + + // Coverage configuration + coverage: { + provider: 'v8', // or 'istanbul' + reporter: ['text', 'html'], + include: ['src/**/*.ts'], + }, + + // Run tests in isolation (each file in separate process) + isolate: true, + + // Pool for running tests: 'threads', 'forks', 'vmThreads' + pool: 'threads', + + // Number of threads/processes + poolOptions: { + threads: { + maxThreads: 4, + minThreads: 1, + }, + }, + + // Automatically clear mocks between tests + clearMocks: true, + + // Restore mocks between tests + restoreMocks: true, + + // Retry failed tests + retry: 0, + + // Stop after first failure + bail: 0, + }, +}) +``` + +## Conditional Configuration + +Use `mode` or `process.env.VITEST` for test-specific config: + +```ts +export default defineConfig(({ mode }) => ({ + plugins: mode === 'test' ? [] : [myPlugin()], + test: { + // test options + }, +})) +``` + +## Projects (Monorepos) + +Run different configurations in the same Vitest process: + +```ts +defineConfig({ + test: { + projects: [ + 'packages/*', + { + test: { + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + environment: 'node', + }, + }, + { + test: { + name: 'integration', + include: ['tests/integration/**/*.test.ts'], + environment: 'jsdom', + }, + }, + ], + }, +}) +``` + +## Key Points + +- Vitest uses Vite's transformation pipeline - same `resolve.alias`, plugins work +- `vitest.config.ts` takes priority over `vite.config.ts` +- Use `--config` flag to specify a custom config path +- `process.env.VITEST` is set to `true` when running tests +- Test config uses `test` property, rest is Vite config + + diff --git a/.claude/skills/vitest/references/core-describe.md b/.claude/skills/vitest/references/core-describe.md new file mode 100644 index 00000000..3f7f3fe1 --- /dev/null +++ b/.claude/skills/vitest/references/core-describe.md @@ -0,0 +1,193 @@ +--- +name: describe-api +description: describe/suite for grouping tests into logical blocks +--- + +# Describe API + +Group related tests into suites for organization and shared setup. + +## Basic Usage + +```ts +import { describe, expect, test } from 'vitest' + +describe('Math', () => { + test('adds numbers', () => { + expect(1 + 1).toBe(2) + }) + + test('subtracts numbers', () => { + expect(3 - 1).toBe(2) + }) +}) + +// Alias: suite +import { suite } from 'vitest' +suite('equivalent to describe', () => {}) +``` + +## Nested Suites + +```ts +describe('User', () => { + describe('when logged in', () => { + test('shows dashboard', () => {}) + test('can update profile', () => {}) + }) + + describe('when logged out', () => { + test('shows login page', () => {}) + }) +}) +``` + +## Suite Options + +```ts +// All tests inherit options +describe('slow tests', { timeout: 30_000 }, () => { + test('test 1', () => {}) // 30s timeout + test('test 2', () => {}) // 30s timeout +}) +``` + +## Suite Modifiers + +### Skip Suites + +```ts +describe.skip('skipped suite', () => { + test('wont run', () => {}) +}) + +// Conditional +describe.skipIf(process.env.CI)('not in CI', () => {}) +describe.runIf(!process.env.CI)('only local', () => {}) +``` + +### Focus Suites + +```ts +describe.only('only this suite runs', () => { + test('runs', () => {}) +}) +``` + +### Todo Suites + +```ts +describe.todo('implement later') +``` + +### Concurrent Suites + +```ts +// All tests run in parallel +describe.concurrent('parallel tests', () => { + test('test 1', async ({ expect }) => {}) + test('test 2', async ({ expect }) => {}) +}) +``` + +### Sequential in Concurrent + +```ts +describe.concurrent('parallel', () => { + test('concurrent 1', async () => {}) + + describe.sequential('must be sequential', () => { + test('step 1', async () => {}) + test('step 2', async () => {}) + }) +}) +``` + +### Shuffle Tests + +```ts +describe.shuffle('random order', () => { + test('test 1', () => {}) + test('test 2', () => {}) + test('test 3', () => {}) +}) + +// Or with option +describe('random', { shuffle: true }, () => {}) +``` + +## Parameterized Suites + +### describe.each + +```ts +describe.each([ + { name: 'Chrome', version: 100 }, + { name: 'Firefox', version: 90 }, +])('$name browser', ({ name, version }) => { + test('has version', () => { + expect(version).toBeGreaterThan(0) + }) +}) +``` + +### describe.for + +```ts +describe.for([ + ['Chrome', 100], + ['Firefox', 90], +])('%s browser', ([name, version]) => { + test('has version', () => { + expect(version).toBeGreaterThan(0) + }) +}) +``` + +## Hooks in Suites + +```ts +describe('Database', () => { + let db + + beforeAll(async () => { + db = await createDb() + }) + + afterAll(async () => { + await db.close() + }) + + beforeEach(async () => { + await db.clear() + }) + + test('insert works', async () => { + await db.insert({ name: 'test' }) + expect(await db.count()).toBe(1) + }) +}) +``` + +## Modifier Combinations + +All modifiers can be chained: + +```ts +describe.skip.concurrent('skipped concurrent', () => {}) +describe.only.shuffle('only and shuffled', () => {}) +describe.concurrent.skip('equivalent', () => {}) +``` + +## Key Points + +- Top-level tests belong to an implicit file suite +- Nested suites inherit parent's options (timeout, retry, etc.) +- Hooks are scoped to their suite and nested suites +- Use `describe.concurrent` with context's `expect` for snapshots +- Shuffle order depends on `sequence.seed` config + + diff --git a/.claude/skills/vitest/references/core-expect.md b/.claude/skills/vitest/references/core-expect.md new file mode 100644 index 00000000..91de00a6 --- /dev/null +++ b/.claude/skills/vitest/references/core-expect.md @@ -0,0 +1,219 @@ +--- +name: expect-api +description: Assertions with matchers, asymmetric matchers, and custom matchers +--- + +# Expect API + +Vitest uses Chai assertions with Jest-compatible API. + +## Basic Assertions + +```ts +import { expect, test } from 'vitest' + +test('assertions', () => { + // Equality + expect(1 + 1).toBe(2) // Strict equality (===) + expect({ a: 1 }).toEqual({ a: 1 }) // Deep equality + + // Truthiness + expect(true).toBeTruthy() + expect(false).toBeFalsy() + expect(null).toBeNull() + expect(undefined).toBeUndefined() + expect('value').toBeDefined() + + // Numbers + expect(10).toBeGreaterThan(5) + expect(10).toBeGreaterThanOrEqual(10) + expect(5).toBeLessThan(10) + expect(0.1 + 0.2).toBeCloseTo(0.3, 5) + + // Strings + expect('hello world').toMatch(/world/) + expect('hello').toContain('ell') + + // Arrays + expect([1, 2, 3]).toContain(2) + expect([{ a: 1 }]).toContainEqual({ a: 1 }) + expect([1, 2, 3]).toHaveLength(3) + + // Objects + expect({ a: 1, b: 2 }).toHaveProperty('a') + expect({ a: 1, b: 2 }).toHaveProperty('a', 1) + expect({ a: { b: 1 } }).toHaveProperty('a.b', 1) + expect({ a: 1 }).toMatchObject({ a: 1 }) + + // Types + expect('string').toBeTypeOf('string') + expect(new Date()).toBeInstanceOf(Date) +}) +``` + +## Negation + +```ts +expect(1).not.toBe(2) +expect({ a: 1 }).not.toEqual({ a: 2 }) +``` + +## Error Assertions + +```ts +// Sync errors - wrap in function +expect(() => throwError()).toThrow() +expect(() => throwError()).toThrow('message') +expect(() => throwError()).toThrow(/pattern/) +expect(() => throwError()).toThrow(CustomError) + +// Async errors - use rejects +await expect(asyncThrow()).rejects.toThrow('error') +``` + +## Promise Assertions + +```ts +// Resolves +await expect(Promise.resolve(1)).resolves.toBe(1) +await expect(fetchData()).resolves.toEqual({ data: true }) + +// Rejects +await expect(Promise.reject('error')).rejects.toBe('error') +await expect(failingFetch()).rejects.toThrow() +``` + +## Spy/Mock Assertions + +```ts +const fn = vi.fn() +fn('arg1', 'arg2') +fn('arg3') + +expect(fn).toHaveBeenCalled() +expect(fn).toHaveBeenCalledTimes(2) +expect(fn).toHaveBeenCalledWith('arg1', 'arg2') +expect(fn).toHaveBeenLastCalledWith('arg3') +expect(fn).toHaveBeenNthCalledWith(1, 'arg1', 'arg2') + +expect(fn).toHaveReturned() +expect(fn).toHaveReturnedWith(value) +``` + +## Asymmetric Matchers + +Use inside `toEqual`, `toHaveBeenCalledWith`, etc: + +```ts +expect({ id: 1, name: 'test' }).toEqual({ + id: expect.any(Number), + name: expect.any(String), +}) + +expect({ a: 1, b: 2, c: 3 }).toEqual( + expect.objectContaining({ a: 1 }) +) + +expect([1, 2, 3, 4]).toEqual( + expect.arrayContaining([1, 3]) +) + +expect('hello world').toEqual( + expect.stringContaining('world') +) + +expect('hello world').toEqual( + expect.stringMatching(/world$/) +) + +expect({ value: null }).toEqual({ + value: expect.anything() // Matches anything except null/undefined +}) + +// Negate with expect.not +expect([1, 2]).toEqual( + expect.not.arrayContaining([3]) +) +``` + +## Soft Assertions + +Continue test after failure: + +```ts +expect.soft(1).toBe(2) // Marks test failed but continues +expect.soft(2).toBe(3) // Also runs +// All failures reported at end +``` + +## Poll Assertions + +Retry until passes: + +```ts +await expect.poll(() => fetchStatus()).toBe('ready') + +await expect.poll( + () => document.querySelector('.element'), + { interval: 100, timeout: 5000 } +).toBeTruthy() +``` + +## Assertion Count + +```ts +test('async assertions', async () => { + expect.assertions(2) // Exactly 2 assertions must run + + await doAsync((data) => { + expect(data).toBeDefined() + expect(data.id).toBe(1) + }) +}) + +test('at least one', () => { + expect.hasAssertions() // At least 1 assertion must run +}) +``` + +## Extending Matchers + +```ts +expect.extend({ + toBeWithinRange(received, floor, ceiling) { + const pass = received >= floor && received <= ceiling + return { + pass, + message: () => + `expected ${received} to be within range ${floor} - ${ceiling}`, + } + }, +}) + +test('custom matcher', () => { + expect(100).toBeWithinRange(90, 110) +}) +``` + +## Snapshot Assertions + +```ts +expect(data).toMatchSnapshot() +expect(data).toMatchInlineSnapshot(`{ "id": 1 }`) +await expect(result).toMatchFileSnapshot('./expected.json') + +expect(() => throw new Error('fail')).toThrowErrorMatchingSnapshot() +``` + +## Key Points + +- Use `toBe` for primitives, `toEqual` for objects/arrays +- `toStrictEqual` checks undefined properties and array sparseness +- Always `await` async assertions (`resolves`, `rejects`, `poll`) +- Use context's `expect` in concurrent tests for correct tracking +- `toThrow` requires wrapping sync code in a function + + diff --git a/.claude/skills/vitest/references/core-hooks.md b/.claude/skills/vitest/references/core-hooks.md new file mode 100644 index 00000000..d0c2bfa0 --- /dev/null +++ b/.claude/skills/vitest/references/core-hooks.md @@ -0,0 +1,244 @@ +--- +name: lifecycle-hooks +description: beforeEach, afterEach, beforeAll, afterAll, and around hooks +--- + +# Lifecycle Hooks + +## Basic Hooks + +```ts +import { afterAll, afterEach, beforeAll, beforeEach, test } from 'vitest' + +beforeAll(async () => { + // Runs once before all tests in file/suite + await setupDatabase() +}) + +afterAll(async () => { + // Runs once after all tests in file/suite + await teardownDatabase() +}) + +beforeEach(async () => { + // Runs before each test + await clearTestData() +}) + +afterEach(async () => { + // Runs after each test + await cleanupMocks() +}) +``` + +## Cleanup Return Pattern + +Return cleanup function from `before*` hooks: + +```ts +beforeAll(async () => { + const server = await startServer() + + // Returned function runs as afterAll + return async () => { + await server.close() + } +}) + +beforeEach(async () => { + const connection = await connect() + + // Runs as afterEach + return () => connection.close() +}) +``` + +## Scoped Hooks + +Hooks apply to current suite and nested suites: + +```ts +describe('outer', () => { + beforeEach(() => console.log('outer before')) + + test('test 1', () => {}) // outer before → test + + describe('inner', () => { + beforeEach(() => console.log('inner before')) + + test('test 2', () => {}) // outer before → inner before → test + }) +}) +``` + +## Hook Timeout + +```ts +beforeAll(async () => { + await slowSetup() +}, 30_000) // 30 second timeout +``` + +## Around Hooks + +Wrap tests with setup/teardown context: + +```ts +import { aroundEach, test } from 'vitest' + +// Wrap each test in database transaction +aroundEach(async (runTest) => { + await db.beginTransaction() + await runTest() // Must be called! + await db.rollback() +}) + +test('insert user', async () => { + await db.insert({ name: 'Alice' }) + // Automatically rolled back after test +}) +``` + +### aroundAll + +Wrap entire suite: + +```ts +import { aroundAll, test } from 'vitest' + +aroundAll(async (runSuite) => { + console.log('before all tests') + await runSuite() // Must be called! + console.log('after all tests') +}) +``` + +### Multiple Around Hooks + +Nested like onion layers: + +```ts +aroundEach(async (runTest) => { + console.log('outer before') + await runTest() + console.log('outer after') +}) + +aroundEach(async (runTest) => { + console.log('inner before') + await runTest() + console.log('inner after') +}) + +// Order: outer before → inner before → test → inner after → outer after +``` + +## Test Hooks + +Inside test body: + +```ts +import { onTestFailed, onTestFinished, test } from 'vitest' + +test('with cleanup', () => { + const db = connect() + + // Runs after test finishes (pass or fail) + onTestFinished(() => db.close()) + + // Only runs if test fails + onTestFailed(({ task }) => { + console.log('Failed:', task.result?.errors) + }) + + db.query('SELECT * FROM users') +}) +``` + +### Reusable Cleanup Pattern + +```ts +function useTestDb() { + const db = connect() + onTestFinished(() => db.close()) + return db +} + +test('query users', () => { + const db = useTestDb() + expect(db.query('SELECT * FROM users')).toBeDefined() +}) + +test('query orders', () => { + const db = useTestDb() // Fresh connection, auto-closed + expect(db.query('SELECT * FROM orders')).toBeDefined() +}) +``` + +## Concurrent Test Hooks + +For concurrent tests, use context's hooks: + +```ts +test.concurrent('concurrent', ({ onTestFinished }) => { + const resource = allocate() + onTestFinished(() => resource.release()) +}) +``` + +## Extended Test Hooks + +With `test.extend`, hooks are type-aware: + +```ts +const test = base.extend<{ db: Database }>({ + db: async ({}, use) => { + const db = await createDb() + await use(db) + await db.close() + }, +}) + +// These hooks know about `db` fixture +test.beforeEach(({ db }) => { + db.seed() +}) + +test.afterEach(({ db }) => { + db.clear() +}) +``` + +## Hook Execution Order + +Default order (stack): +1. `beforeAll` (in order) +2. `beforeEach` (in order) +3. Test +4. `afterEach` (reverse order) +5. `afterAll` (reverse order) + +Configure with `sequence.hooks`: + +```ts +defineConfig({ + test: { + sequence: { + hooks: 'list', // 'stack' (default), 'list', 'parallel' + }, + }, +}) +``` + +## Key Points + +- Hooks are not called during type checking +- Return cleanup function from `before*` to avoid `after*` duplication +- `aroundEach`/`aroundAll` must call `runTest()`/`runSuite()` +- `onTestFinished` always runs, even if test fails +- Use context hooks for concurrent tests + + diff --git a/.claude/skills/vitest/references/core-test-api.md b/.claude/skills/vitest/references/core-test-api.md new file mode 100644 index 00000000..1f3c9323 --- /dev/null +++ b/.claude/skills/vitest/references/core-test-api.md @@ -0,0 +1,233 @@ +--- +name: test-api +description: test/it function for defining tests with modifiers +--- + +# Test API + +## Basic Test + +```ts +import { expect, test } from 'vitest' + +test('adds numbers', () => { + expect(1 + 1).toBe(2) +}) + +// Alias: it +import { it } from 'vitest' + +it('works the same', () => { + expect(true).toBe(true) +}) +``` + +## Async Tests + +```ts +test('async test', async () => { + const result = await fetchData() + expect(result).toBeDefined() +}) + +// Promises are automatically awaited +test('returns promise', () => { + return fetchData().then(result => { + expect(result).toBeDefined() + }) +}) +``` + +## Test Options + +```ts +// Timeout (default: 5000ms) +test('slow test', async () => { + // ... +}, 10_000) + +// Or with options object +test('with options', { timeout: 10_000, retry: 2 }, async () => { + // ... +}) +``` + +## Test Modifiers + +### Skip Tests + +```ts +test.skip('skipped test', () => { + // Won't run +}) + +// Conditional skip +test.skipIf(process.env.CI)('not in CI', () => {}) +test.runIf(process.env.CI)('only in CI', () => {}) + +// Dynamic skip via context +test('dynamic skip', ({ skip }) => { + skip(someCondition, 'reason') + // ... +}) +``` + +### Focus Tests + +```ts +test.only('only this runs', () => { + // Other tests in file are skipped +}) +``` + +### Todo Tests + +```ts +test.todo('implement later') + +test.todo('with body', () => { + // Not run, shows in report +}) +``` + +### Failing Tests + +```ts +test.fails('expected to fail', () => { + expect(1).toBe(2) // Test passes because assertion fails +}) +``` + +### Concurrent Tests + +```ts +// Run tests in parallel +test.concurrent('test 1', async ({ expect }) => { + // Use context.expect for concurrent tests + expect(await fetch1()).toBe('result') +}) + +test.concurrent('test 2', async ({ expect }) => { + expect(await fetch2()).toBe('result') +}) +``` + +### Sequential Tests + +```ts +// Force sequential in concurrent context +test.sequential('must run alone', async () => {}) +``` + +## Parameterized Tests + +### test.each + +```ts +test.each([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +])('add(%i, %i) = %i', (a, b, expected) => { + expect(a + b).toBe(expected) +}) + +// With objects +test.each([ + { a: 1, b: 1, expected: 2 }, + { a: 1, b: 2, expected: 3 }, +])('add($a, $b) = $expected', ({ a, b, expected }) => { + expect(a + b).toBe(expected) +}) + +// Template literal +test.each` + a | b | expected + ${1} | ${1} | ${2} + ${1} | ${2} | ${3} +`('add($a, $b) = $expected', ({ a, b, expected }) => { + expect(a + b).toBe(expected) +}) +``` + +### test.for + +Preferred over `.each` - doesn't spread arrays: + +```ts +test.for([ + [1, 1, 2], + [1, 2, 3], +])('add(%i, %i) = %i', ([a, b, expected], { expect }) => { + // Second arg is TestContext + expect(a + b).toBe(expected) +}) +``` + +## Test Context + +First argument provides context utilities: + +```ts +test('with context', ({ expect, skip, task }) => { + console.log(task.name) // Test name + skip(someCondition) // Skip dynamically + expect(1).toBe(1) // Context-bound expect +}) +``` + +## Custom Test with Fixtures + +```ts +import { test as base } from 'vitest' + +const test = base.extend({ + db: async ({}, use) => { + const db = await createDb() + await use(db) + await db.close() + }, +}) + +test('query', async ({ db }) => { + const users = await db.query('SELECT * FROM users') + expect(users).toBeDefined() +}) +``` + +## Retry Configuration + +```ts +test('flaky test', { retry: 3 }, async () => { + // Retries up to 3 times on failure +}) + +// Advanced retry options +test('with delay', { + retry: { + count: 3, + delay: 1000, + condition: /timeout/i, // Only retry on timeout errors + }, +}, async () => {}) +``` + +## Tags + +```ts +test('database test', { tags: ['db', 'slow'] }, async () => {}) + +// Run with: vitest --tags db +``` + +## Key Points + +- Tests with no body are marked as `todo` +- `test.only` throws in CI unless `allowOnly: true` +- Use context's `expect` for concurrent tests and snapshots +- Function name is used as test name if passed as first arg + + diff --git a/.claude/skills/vitest/references/features-concurrency.md b/.claude/skills/vitest/references/features-concurrency.md new file mode 100644 index 00000000..412f60d8 --- /dev/null +++ b/.claude/skills/vitest/references/features-concurrency.md @@ -0,0 +1,250 @@ +--- +name: concurrency-parallelism +description: Concurrent tests, parallel execution, and sharding +--- + +# Concurrency & Parallelism + +## File Parallelism + +By default, Vitest runs test files in parallel across workers: + +```ts +defineConfig({ + test: { + // Run files in parallel (default: true) + fileParallelism: true, + + // Number of worker threads + maxWorkers: 4, + minWorkers: 1, + + // Pool type: 'threads', 'forks', 'vmThreads' + pool: 'threads', + }, +}) +``` + +## Concurrent Tests + +Run tests within a file in parallel: + +```ts +// Individual concurrent tests +test.concurrent('test 1', async ({ expect }) => { + expect(await fetch1()).toBe('result') +}) + +test.concurrent('test 2', async ({ expect }) => { + expect(await fetch2()).toBe('result') +}) + +// All tests in suite concurrent +describe.concurrent('parallel suite', () => { + test('test 1', async ({ expect }) => {}) + test('test 2', async ({ expect }) => {}) +}) +``` + +**Important:** Use `{ expect }` from context for concurrent tests. + +## Sequential in Concurrent Context + +Force sequential execution: + +```ts +describe.concurrent('mostly parallel', () => { + test('parallel 1', async () => {}) + test('parallel 2', async () => {}) + + test.sequential('must run alone 1', async () => {}) + test.sequential('must run alone 2', async () => {}) +}) + +// Or entire suite +describe.sequential('sequential suite', () => { + test('first', () => {}) + test('second', () => {}) +}) +``` + +## Max Concurrency + +Limit concurrent tests: + +```ts +defineConfig({ + test: { + maxConcurrency: 5, // Max concurrent tests per file + }, +}) +``` + +## Isolation + +Each file runs in isolated environment by default: + +```ts +defineConfig({ + test: { + // Disable isolation for faster runs (less safe) + isolate: false, + }, +}) +``` + +## Sharding + +Split tests across machines: + +```bash +# Machine 1 +vitest run --shard=1/3 + +# Machine 2 +vitest run --shard=2/3 + +# Machine 3 +vitest run --shard=3/3 +``` + +### CI Example (GitHub Actions) + +```yaml +jobs: + test: + strategy: + matrix: + shard: [1, 2, 3] + steps: + - run: vitest run --shard=${{ matrix.shard }}/3 --reporter=blob + + merge: + needs: test + steps: + - run: vitest --merge-reports --reporter=junit +``` + +### Merge Reports + +```bash +# Each shard outputs blob +vitest run --shard=1/3 --reporter=blob --coverage +vitest run --shard=2/3 --reporter=blob --coverage + +# Merge all blobs +vitest --merge-reports --reporter=json --coverage +``` + +## Test Sequence + +Control test order: + +```ts +defineConfig({ + test: { + sequence: { + // Run tests in random order + shuffle: true, + + // Seed for reproducible shuffle + seed: 12345, + + // Hook execution order + hooks: 'stack', // 'stack', 'list', 'parallel' + + // All tests concurrent by default + concurrent: true, + }, + }, +}) +``` + +## Shuffle Tests + +Randomize to catch hidden dependencies: + +```ts +// Via CLI +vitest --sequence.shuffle + +// Per suite +describe.shuffle('random order', () => { + test('test 1', () => {}) + test('test 2', () => {}) + test('test 3', () => {}) +}) +``` + +## Pool Options + +### Threads (Default) + +```ts +defineConfig({ + test: { + pool: 'threads', + poolOptions: { + threads: { + maxThreads: 8, + minThreads: 2, + isolate: true, + }, + }, + }, +}) +``` + +### Forks + +Better isolation, slower: + +```ts +defineConfig({ + test: { + pool: 'forks', + poolOptions: { + forks: { + maxForks: 4, + isolate: true, + }, + }, + }, +}) +``` + +### VM Threads + +Full VM isolation per file: + +```ts +defineConfig({ + test: { + pool: 'vmThreads', + }, +}) +``` + +## Bail on Failure + +Stop after first failure: + +```bash +vitest --bail 1 # Stop after 1 failure +vitest --bail # Stop on first failure (same as --bail 1) +``` + +## Key Points + +- Files run in parallel by default +- Use `.concurrent` for parallel tests within file +- Always use context's `expect` in concurrent tests +- Sharding splits tests across CI machines +- Use `--merge-reports` to combine sharded results +- Shuffle tests to find hidden dependencies + + diff --git a/.claude/skills/vitest/references/features-context.md b/.claude/skills/vitest/references/features-context.md new file mode 100644 index 00000000..a9db0a1f --- /dev/null +++ b/.claude/skills/vitest/references/features-context.md @@ -0,0 +1,238 @@ +--- +name: test-context-fixtures +description: Test context, custom fixtures with test.extend +--- + +# Test Context & Fixtures + +## Built-in Context + +Every test receives context as first argument: + +```ts +test('context', ({ task, expect, skip }) => { + console.log(task.name) // Test name + expect(1).toBe(1) // Context-bound expect + skip() // Skip test dynamically +}) +``` + +### Context Properties + +- `task` - Test metadata (name, file, etc.) +- `expect` - Expect bound to this test (important for concurrent tests) +- `skip(condition?, message?)` - Skip the test +- `onTestFinished(fn)` - Cleanup after test +- `onTestFailed(fn)` - Run on failure only + +## Custom Fixtures with test.extend + +Create reusable test utilities: + +```ts +import { test as base } from 'vitest' + +// Define fixture types +interface Fixtures { + db: Database + user: User +} + +// Create extended test +export const test = base.extend({ + // Fixture with setup/teardown + db: async ({}, use) => { + const db = await createDatabase() + await use(db) // Provide to test + await db.close() // Cleanup + }, + + // Fixture depending on another fixture + user: async ({ db }, use) => { + const user = await db.createUser({ name: 'Test' }) + await use(user) + await db.deleteUser(user.id) + }, +}) +``` + +Using fixtures: + +```ts +test('query user', async ({ db, user }) => { + const found = await db.findUser(user.id) + expect(found).toEqual(user) +}) +``` + +## Fixture Initialization + +Fixtures only initialize when accessed: + +```ts +const test = base.extend({ + expensive: async ({}, use) => { + console.log('initializing') // Only runs if test uses it + await use('value') + }, +}) + +test('no fixture', () => {}) // expensive not called +test('uses fixture', ({ expensive }) => {}) // expensive called +``` + +## Auto Fixtures + +Run fixture for every test: + +```ts +const test = base.extend({ + setup: [ + async ({}, use) => { + await globalSetup() + await use() + await globalTeardown() + }, + { auto: true } // Always run + ], +}) +``` + +## Scoped Fixtures + +### File Scope + +Initialize once per file: + +```ts +const test = base.extend({ + connection: [ + async ({}, use) => { + const conn = await connect() + await use(conn) + await conn.close() + }, + { scope: 'file' } + ], +}) +``` + +### Worker Scope + +Initialize once per worker: + +```ts +const test = base.extend({ + sharedResource: [ + async ({}, use) => { + await use(globalResource) + }, + { scope: 'worker' } + ], +}) +``` + +## Injected Fixtures (from Config) + +Override fixtures per project: + +```ts +// test file +const test = base.extend({ + apiUrl: ['/default', { injected: true }], +}) + +// vitest.config.ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'prod', + provide: { apiUrl: 'https://api.prod.com' }, + }, + }, + ], + }, +}) +``` + +## Scoped Values per Suite + +Override fixture for specific suite: + +```ts +const test = base.extend({ + environment: 'development', +}) + +describe('production tests', () => { + test.scoped({ environment: 'production' }) + + test('uses production', ({ environment }) => { + expect(environment).toBe('production') + }) +}) + +test('uses default', ({ environment }) => { + expect(environment).toBe('development') +}) +``` + +## Extended Test Hooks + +Type-aware hooks with fixtures: + +```ts +const test = base.extend<{ db: Database }>({ + db: async ({}, use) => { + const db = await createDb() + await use(db) + await db.close() + }, +}) + +// Hooks know about fixtures +test.beforeEach(({ db }) => { + db.seed() +}) + +test.afterEach(({ db }) => { + db.clear() +}) +``` + +## Composing Fixtures + +Extend from another extended test: + +```ts +// base-test.ts +export const test = base.extend<{ db: Database }>({ + db: async ({}, use) => { /* ... */ }, +}) + +// admin-test.ts +import { test as dbTest } from './base-test' + +export const test = dbTest.extend<{ admin: User }>({ + admin: async ({ db }, use) => { + const admin = await db.createAdmin() + await use(admin) + }, +}) +``` + +## Key Points + +- Use `{ }` destructuring to access fixtures +- Fixtures are lazy - only initialize when accessed +- Return cleanup function from fixtures +- Use `{ auto: true }` for setup fixtures +- Use `{ scope: 'file' }` for expensive shared resources +- Fixtures compose - extend from extended tests + + diff --git a/.claude/skills/vitest/references/features-coverage.md b/.claude/skills/vitest/references/features-coverage.md new file mode 100644 index 00000000..aaf44cfb --- /dev/null +++ b/.claude/skills/vitest/references/features-coverage.md @@ -0,0 +1,207 @@ +--- +name: code-coverage +description: Code coverage with V8 or Istanbul providers +--- + +# Code Coverage + +## Setup + +```bash +# Run tests with coverage +vitest run --coverage +``` + +## Configuration + +```ts +// vitest.config.ts +defineConfig({ + test: { + coverage: { + // Provider: 'v8' (default, faster) or 'istanbul' (more compatible) + provider: 'v8', + + // Enable coverage + enabled: true, + + // Reporters + reporter: ['text', 'json', 'html'], + + // Files to include + include: ['src/**/*.{ts,tsx}'], + + // Files to exclude + exclude: [ + 'node_modules/', + 'tests/', + '**/*.d.ts', + '**/*.test.ts', + ], + + // Report uncovered files + all: true, + + // Thresholds + thresholds: { + lines: 80, + functions: 80, + branches: 80, + statements: 80, + }, + }, + }, +}) +``` + +## Providers + +### V8 (Default) + +```bash +npm i -D @vitest/coverage-v8 +``` + +- Faster, no pre-instrumentation +- Uses V8's native coverage +- Recommended for most projects + +### Istanbul + +```bash +npm i -D @vitest/coverage-istanbul +``` + +- Pre-instruments code +- Works in any JS runtime +- More overhead but widely compatible + +## Reporters + +```ts +coverage: { + reporter: [ + 'text', // Terminal output + 'text-summary', // Summary only + 'json', // JSON file + 'html', // HTML report + 'lcov', // For CI tools + 'cobertura', // XML format + ], + reportsDirectory: './coverage', +} +``` + +## Thresholds + +Fail tests if coverage is below threshold: + +```ts +coverage: { + thresholds: { + // Global thresholds + lines: 80, + functions: 75, + branches: 70, + statements: 80, + + // Per-file thresholds + perFile: true, + + // Auto-update thresholds (for gradual improvement) + autoUpdate: true, + }, +} +``` + +## Ignoring Code + +### V8 + +```ts +/* v8 ignore next -- @preserve */ +function ignored() { + return 'not covered' +} + +/* v8 ignore start -- @preserve */ +// All code here ignored +/* v8 ignore stop -- @preserve */ +``` + +### Istanbul + +```ts +/* istanbul ignore next -- @preserve */ +function ignored() {} + +/* istanbul ignore if -- @preserve */ +if (condition) { + // ignored +} +``` + +Note: `@preserve` keeps comments through esbuild. + +## Package.json Scripts + +```json +{ + "scripts": { + "test": "vitest", + "test:coverage": "vitest run --coverage", + "test:coverage:watch": "vitest --coverage" + } +} +``` + +## Vitest UI Coverage + +Enable HTML coverage in Vitest UI: + +```ts +coverage: { + enabled: true, + reporter: ['text', 'html'], +} +``` + +Run with `vitest --ui` to view coverage visually. + +## CI Integration + +```yaml +# GitHub Actions +- name: Run tests with coverage + run: npm run test:coverage + +- name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./coverage/lcov.info +``` + +## Coverage with Sharding + +Merge coverage from sharded runs: + +```bash +vitest run --shard=1/3 --coverage --reporter=blob +vitest run --shard=2/3 --coverage --reporter=blob +vitest run --shard=3/3 --coverage --reporter=blob + +vitest --merge-reports --coverage --reporter=json +``` + +## Key Points + +- V8 is faster, Istanbul is more compatible +- Use `--coverage` flag or `coverage.enabled: true` +- Include `all: true` to see uncovered files +- Set thresholds to enforce minimum coverage +- Use `@preserve` comment to keep ignore hints + + diff --git a/.claude/skills/vitest/references/features-filtering.md b/.claude/skills/vitest/references/features-filtering.md new file mode 100644 index 00000000..24a41cb5 --- /dev/null +++ b/.claude/skills/vitest/references/features-filtering.md @@ -0,0 +1,211 @@ +--- +name: test-filtering +description: Filter tests by name, file patterns, and tags +--- + +# Test Filtering + +## CLI Filtering + +### By File Path + +```bash +# Run files containing "user" +vitest user + +# Multiple patterns +vitest user auth + +# Specific file +vitest src/user.test.ts + +# By line number +vitest src/user.test.ts:25 +``` + +### By Test Name + +```bash +# Tests matching pattern +vitest -t "login" +vitest --testNamePattern "should.*work" + +# Regex patterns +vitest -t "/user|auth/" +``` + +## Changed Files + +```bash +# Uncommitted changes +vitest --changed + +# Since specific commit +vitest --changed HEAD~1 +vitest --changed abc123 + +# Since branch +vitest --changed origin/main +``` + +## Related Files + +Run tests that import specific files: + +```bash +vitest related src/utils.ts src/api.ts --run +``` + +Useful with lint-staged: + +```js +// .lintstagedrc.js +export default { + '*.{ts,tsx}': 'vitest related --run', +} +``` + +## Focus Tests (.only) + +```ts +test.only('only this runs', () => {}) + +describe.only('only this suite', () => { + test('runs', () => {}) +}) +``` + +In CI, `.only` throws error unless configured: + +```ts +defineConfig({ + test: { + allowOnly: true, // Allow .only in CI + }, +}) +``` + +## Skip Tests + +```ts +test.skip('skipped', () => {}) + +// Conditional +test.skipIf(process.env.CI)('not in CI', () => {}) +test.runIf(!process.env.CI)('local only', () => {}) + +// Dynamic skip +test('dynamic', ({ skip }) => { + skip(someCondition, 'reason') +}) +``` + +## Tags + +Filter by custom tags: + +```ts +test('database test', { tags: ['db'] }, () => {}) +test('slow test', { tags: ['slow', 'integration'] }, () => {}) +``` + +Run tagged tests: + +```bash +vitest --tags db +vitest --tags "db,slow" # OR +vitest --tags db --tags slow # OR +``` + +Configure allowed tags: + +```ts +defineConfig({ + test: { + tags: ['db', 'slow', 'integration'], + strictTags: true, // Fail on unknown tags + }, +}) +``` + +## Include/Exclude Patterns + +```ts +defineConfig({ + test: { + // Test file patterns + include: ['**/*.{test,spec}.{ts,tsx}'], + + // Exclude patterns + exclude: [ + '**/node_modules/**', + '**/e2e/**', + '**/*.skip.test.ts', + ], + + // Include source for in-source testing + includeSource: ['src/**/*.ts'], + }, +}) +``` + +## Watch Mode Filtering + +In watch mode, press: +- `p` - Filter by filename pattern +- `t` - Filter by test name pattern +- `a` - Run all tests +- `f` - Run only failed tests + +## Projects Filtering + +Run specific project: + +```bash +vitest --project unit +vitest --project integration --project e2e +``` + +## Environment-based Filtering + +```ts +const isDev = process.env.NODE_ENV === 'development' +const isCI = process.env.CI + +describe.skipIf(isCI)('local only tests', () => {}) +describe.runIf(isDev)('dev tests', () => {}) +``` + +## Combining Filters + +```bash +# File pattern + test name + changed +vitest user -t "login" --changed + +# Related files + run mode +vitest related src/auth.ts --run +``` + +## List Tests Without Running + +```bash +vitest list # Show all test names +vitest list -t "user" # Filter by name +vitest list --filesOnly # Show only file paths +vitest list --json # JSON output +``` + +## Key Points + +- Use `-t` for test name pattern filtering +- `--changed` runs only tests affected by changes +- `--related` runs tests importing specific files +- Tags provide semantic test grouping +- Use `.only` for debugging, but configure CI to reject it +- Watch mode has interactive filtering + + diff --git a/.claude/skills/vitest/references/features-mocking.md b/.claude/skills/vitest/references/features-mocking.md new file mode 100644 index 00000000..e351efef --- /dev/null +++ b/.claude/skills/vitest/references/features-mocking.md @@ -0,0 +1,265 @@ +--- +name: mocking +description: Mock functions, modules, timers, and dates with vi utilities +--- + +# Mocking + +## Mock Functions + +```ts +import { expect, vi } from 'vitest' + +// Create mock function +const fn = vi.fn() +fn('hello') + +expect(fn).toHaveBeenCalled() +expect(fn).toHaveBeenCalledWith('hello') + +// With implementation +const add = vi.fn((a, b) => a + b) +expect(add(1, 2)).toBe(3) + +// Mock return values +fn.mockReturnValue(42) +fn.mockReturnValueOnce(1).mockReturnValueOnce(2) +fn.mockResolvedValue({ data: true }) +fn.mockRejectedValue(new Error('fail')) + +// Mock implementation +fn.mockImplementation((x) => x * 2) +fn.mockImplementationOnce(() => 'first call') +``` + +## Spying on Objects + +```ts +const cart = { + getTotal: () => 100, +} + +const spy = vi.spyOn(cart, 'getTotal') +cart.getTotal() + +expect(spy).toHaveBeenCalled() + +// Mock implementation +spy.mockReturnValue(200) +expect(cart.getTotal()).toBe(200) + +// Restore original +spy.mockRestore() +``` + +## Module Mocking + +```ts +// vi.mock is hoisted to top of file +vi.mock('./api', () => ({ + fetchUser: vi.fn(() => ({ id: 1, name: 'Mock' })), +})) + +import { fetchUser } from './api' + +test('mocked module', () => { + expect(fetchUser()).toEqual({ id: 1, name: 'Mock' }) +}) +``` + +### Partial Mock + +```ts +vi.mock('./utils', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + specificFunction: vi.fn(), + } +}) +``` + +### Auto-mock with Spy + +```ts +// Keep implementation but spy on calls +vi.mock('./calculator', { spy: true }) + +import { add } from './calculator' + +test('spy on module', () => { + const result = add(1, 2) // Real implementation + expect(result).toBe(3) + expect(add).toHaveBeenCalledWith(1, 2) +}) +``` + +### Manual Mocks (__mocks__) + +``` +src/ + __mocks__/ + axios.ts # Mocks 'axios' + api/ + __mocks__/ + client.ts # Mocks './client' + client.ts +``` + +```ts +// Just call vi.mock with no factory +vi.mock('axios') +vi.mock('./api/client') +``` + +## Dynamic Mocking (vi.doMock) + +Not hoisted - use for dynamic imports: + +```ts +test('dynamic mock', async () => { + vi.doMock('./config', () => ({ + apiUrl: 'http://test.local', + })) + + const { apiUrl } = await import('./config') + expect(apiUrl).toBe('http://test.local') + + vi.doUnmock('./config') +}) +``` + +## Mock Timers + +```ts +import { afterEach, beforeEach, vi } from 'vitest' + +beforeEach(() => { + vi.useFakeTimers() +}) + +afterEach(() => { + vi.useRealTimers() +}) + +test('timers', () => { + const fn = vi.fn() + setTimeout(fn, 1000) + + expect(fn).not.toHaveBeenCalled() + + vi.advanceTimersByTime(1000) + expect(fn).toHaveBeenCalled() +}) + +// Other timer methods +vi.runAllTimers() // Run all pending timers +vi.runOnlyPendingTimers() // Run only currently pending +vi.advanceTimersToNextTimer() // Advance to next timer +``` + +### Async Timer Methods + +```ts +test('async timers', async () => { + vi.useFakeTimers() + + let resolved = false + setTimeout(() => Promise.resolve().then(() => { resolved = true }), 100) + + await vi.advanceTimersByTimeAsync(100) + expect(resolved).toBe(true) +}) +``` + +## Mock Dates + +```ts +vi.setSystemTime(new Date('2024-01-01')) +expect(new Date().getFullYear()).toBe(2024) + +vi.useRealTimers() // Restore +``` + +## Mock Globals + +```ts +vi.stubGlobal('fetch', vi.fn(() => + Promise.resolve({ json: () => ({ data: 'mock' }) }) +)) + +// Restore +vi.unstubAllGlobals() +``` + +## Mock Environment Variables + +```ts +vi.stubEnv('API_KEY', 'test-key') +expect(import.meta.env.API_KEY).toBe('test-key') + +// Restore +vi.unstubAllEnvs() +``` + +## Clearing Mocks + +```ts +const fn = vi.fn() +fn() + +fn.mockClear() // Clear call history +fn.mockReset() // Clear history + implementation +fn.mockRestore() // Restore original (for spies) + +// Global +vi.clearAllMocks() +vi.resetAllMocks() +vi.restoreAllMocks() +``` + +## Config Auto-Reset + +```ts +// vitest.config.ts +defineConfig({ + test: { + clearMocks: true, // Clear before each test + mockReset: true, // Reset before each test + restoreMocks: true, // Restore after each test + unstubEnvs: true, // Restore env vars + unstubGlobals: true, // Restore globals + }, +}) +``` + +## Hoisted Variables for Mocks + +```ts +const mockFn = vi.hoisted(() => vi.fn()) + +vi.mock('./module', () => ({ + getData: mockFn, +})) + +import { getData } from './module' + +test('hoisted mock', () => { + mockFn.mockReturnValue('test') + expect(getData()).toBe('test') +}) +``` + +## Key Points + +- `vi.mock` is hoisted - called before imports +- Use `vi.doMock` for dynamic, non-hoisted mocking +- Always restore mocks to avoid test pollution +- Use `{ spy: true }` to keep implementation but track calls +- `vi.hoisted` lets you reference variables in mock factories + + diff --git a/.claude/skills/vitest/references/features-snapshots.md b/.claude/skills/vitest/references/features-snapshots.md new file mode 100644 index 00000000..6868fb13 --- /dev/null +++ b/.claude/skills/vitest/references/features-snapshots.md @@ -0,0 +1,207 @@ +--- +name: snapshot-testing +description: Snapshot testing with file, inline, and file snapshots +--- + +# Snapshot Testing + +Snapshot tests capture output and compare against stored references. + +## Basic Snapshot + +```ts +import { expect, test } from 'vitest' + +test('snapshot', () => { + const result = generateOutput() + expect(result).toMatchSnapshot() +}) +``` + +First run creates `.snap` file: + +```js +// __snapshots__/test.spec.ts.snap +exports['snapshot 1'] = ` +{ + "id": 1, + "name": "test" +} +` +``` + +## Inline Snapshots + +Stored directly in test file: + +```ts +test('inline snapshot', () => { + const data = { foo: 'bar' } + expect(data).toMatchInlineSnapshot() +}) +``` + +Vitest updates the test file: + +```ts +test('inline snapshot', () => { + const data = { foo: 'bar' } + expect(data).toMatchInlineSnapshot(` + { + "foo": "bar", + } + `) +}) +``` + +## File Snapshots + +Compare against explicit file: + +```ts +test('render html', async () => { + const html = renderComponent() + await expect(html).toMatchFileSnapshot('./expected/component.html') +}) +``` + +## Snapshot Hints + +Add descriptive hints: + +```ts +test('multiple snapshots', () => { + expect(header).toMatchSnapshot('header') + expect(body).toMatchSnapshot('body content') + expect(footer).toMatchSnapshot('footer') +}) +``` + +## Object Shape Matching + +Match partial structure: + +```ts +test('shape snapshot', () => { + const data = { + id: Math.random(), + created: new Date(), + name: 'test' + } + + expect(data).toMatchSnapshot({ + id: expect.any(Number), + created: expect.any(Date), + }) +}) +``` + +## Error Snapshots + +```ts +test('error message', () => { + expect(() => { + throw new Error('Something went wrong') + }).toThrowErrorMatchingSnapshot() +}) + +test('inline error', () => { + expect(() => { + throw new Error('Bad input') + }).toThrowErrorMatchingInlineSnapshot(`[Error: Bad input]`) +}) +``` + +## Updating Snapshots + +```bash +# Update all snapshots +vitest -u +vitest --update + +# In watch mode, press 'u' to update failed snapshots +``` + +## Custom Serializers + +Add custom snapshot formatting: + +```ts +expect.addSnapshotSerializer({ + test(val) { + return val && typeof val.toJSON === 'function' + }, + serialize(val, config, indentation, depth, refs, printer) { + return printer(val.toJSON(), config, indentation, depth, refs) + }, +}) +``` + +Or via config: + +```ts +// vitest.config.ts +defineConfig({ + test: { + snapshotSerializers: ['./my-serializer.ts'], + }, +}) +``` + +## Snapshot Format Options + +```ts +defineConfig({ + test: { + snapshotFormat: { + printBasicPrototype: false, // Don't print Array/Object prototypes + escapeString: false, + }, + }, +}) +``` + +## Concurrent Test Snapshots + +Use context's expect: + +```ts +test.concurrent('concurrent 1', async ({ expect }) => { + expect(await getData()).toMatchSnapshot() +}) + +test.concurrent('concurrent 2', async ({ expect }) => { + expect(await getOther()).toMatchSnapshot() +}) +``` + +## Snapshot File Location + +Default: `__snapshots__/.snap` + +Customize: + +```ts +defineConfig({ + test: { + resolveSnapshotPath: (testPath, snapExtension) => { + return testPath.replace('__tests__', '__snapshots__') + snapExtension + }, + }, +}) +``` + +## Key Points + +- Commit snapshot files to version control +- Review snapshot changes in code review +- Use hints for multiple snapshots in one test +- Use `toMatchFileSnapshot` for large outputs (HTML, JSON) +- Inline snapshots auto-update in test file +- Use context's `expect` for concurrent tests + + diff --git a/.claude/skills/workleap-web-configs/SKILL.md b/.claude/skills/workleap-web-configs/SKILL.md new file mode 100644 index 00000000..44634fbc --- /dev/null +++ b/.claude/skills/workleap-web-configs/SKILL.md @@ -0,0 +1,90 @@ +--- +name: workleap-web-configs +description: | + Workleap's shared web configuration packages (@workleap/eslint-configs, @workleap/typescript-configs, @workleap/rsbuild-configs, @workleap/rslib-configs, @workleap/stylelint-configs, @workleap/browserslist-config). + + Use when: + (1) Setting up or customizing shared web tooling configs in a Workleap project + (2) Configuring ESLint by project type (web app, React library, TS library, monorepo) + (3) Configuring TypeScript by project type (web-application, library, monorepo-workspace) + (4) Configuring Rsbuild or Rslib bundling (dev, build, Storybook) + (5) Configuring Stylelint, Browserslist, or monorepo (Turborepo) vs polyrepo strategies + (6) Extending or customizing shared configs, troubleshooting ESM/ESNext constraints +metadata: + version: 1.2 +--- + +# wl-web-configs + +Workleap's shared configuration library for web tooling. Provides pre-configured packages for ESLint, TypeScript, Rsbuild, Rslib, Stylelint, and Browserslist. + +## Philosophy + +- **No lock-in**: Default configurations can always be extended or overridden +- **By project type**: Configurations are composed internally and offered per project type for simplicity +- **ESM/ESNext by default**: Targets modern JavaScript environments +- **Distributed via NPM**: Easy to adopt new features by bumping package versions + +## Supported Tools (Active) + +| Tool | Package | Purpose | +|------|---------|---------| +| Browserslist | `@workleap/browserslist-config` | Browser targets for apps | +| ESLint | `@workleap/eslint-configs` | Code linting | +| Stylelint | `@workleap/stylelint-configs` | CSS linting | +| TypeScript | `@workleap/typescript-configs` | Type checking (linting only) | +| Rsbuild | `@workleap/rsbuild-configs` | Web application bundling | +| Rslib | `@workleap/rslib-configs` | Library bundling | + +**In maintenance mode** (do not recommend): PostCSS, SWC, webpack, tsup + +## Quick Reference + +### Which Configuration to Use? + +| Project Type | ESLint | TypeScript | Bundler | +|--------------|--------|------------|---------| +| Web app with React | `defineWebApplicationConfig` | `web-application.json` | `@workleap/rsbuild-configs` | +| React library | `defineReactLibraryConfig` | `library.json` | `@workleap/rslib-configs` | +| TypeScript library (no React) | `defineTypeScriptLibraryConfig` | `library.json` | `@workleap/rslib-configs` | +| Monorepo workspace root | `defineMonorepoWorkspaceConfig` | `monorepo-workspace.json` | N/A | + +### Browserslist (Apps Only) + +```bash +pnpm add -D @workleap/browserslist-config browserslist +``` + +```text +# .browserslistrc +extends @workleap/browserslist-config +``` + +Only for projects emitting application bundles. Libraries should NOT include Browserslist. + +To add custom browser targets while still using the shared config: + +```text +# .browserslistrc +extends @workleap/browserslist-config +IE 11 +last 2 OperaMobile 12.1 versions +``` + +## Reference Guide + +For comprehensive setup guides, options, and examples, read the appropriate reference file: + +- **ESLint** — [references/eslint.md](references/eslint.md): Installation, `define*Config` functions, rule categories, customization, and VS Code integration +- **TypeScript** — [references/typescript.md](references/typescript.md): Config files by project type, compiler option overrides, path mappings, and CLI scripts +- **Rsbuild** — [references/rsbuild.md](references/rsbuild.md): Dev/build/Storybook configs, predefined options, transformers, and Turborepo setup +- **Rslib** — [references/rslib.md](references/rslib.md): Library build/dev/Storybook configs, bundleless vs bundle, transformers, and type declarations +- **Stylelint** — [references/stylelint.md](references/stylelint.md): Installation, `.stylelintrc.json` setup, Prettier integration, and VS Code settings + +## Critical Rules + +1. **Never invent APIs**: Only suggest documented options and patterns +2. **Respect maintenance mode**: Do not recommend PostCSS, SWC, webpack, or tsup configs +3. **ESM by default**: All configs target ESM/ESNext unless migrating legacy code +4. **Browserslist for apps only**: Libraries should not include Browserslist config +5. **TypeScript for linting**: The shared TypeScript configs focus on linting; bundlers handle transpilation diff --git a/.claude/skills/workleap-web-configs/references/eslint.md b/.claude/skills/workleap-web-configs/references/eslint.md new file mode 100644 index 00000000..e1540aee --- /dev/null +++ b/.claude/skills/workleap-web-configs/references/eslint.md @@ -0,0 +1,243 @@ +# ESLint Configuration + +## Overview + +`@workleap/eslint-configs` provides ESLint 9+ flat config configurations by project type. Uses ESLint for stylistic rules (not Prettier). + +**Target environment**: ESM, ESNext, Node + +## Available Configurations + +| Function | Use Case | +|----------|----------| +| `defineWebApplicationConfig` | React + TypeScript web applications | +| `defineReactLibraryConfig` | TypeScript libraries WITH React | +| `defineTypeScriptLibraryConfig` | TypeScript libraries WITHOUT React | +| `defineMonorepoWorkspaceConfig` | Monorepo workspace root | + +## Installation + +### Polyrepo + +```bash +pnpm add -D @workleap/eslint-configs @eslint/js @typescript-eslint/parser @types/node eslint typescript-eslint +``` + +### Turborepo (Workspace Root) + +```bash +pnpm add -D @workleap/eslint-configs @eslint/js @typescript-eslint/parser @types/node eslint typescript-eslint turbo +``` + +### Turborepo (Project) + +```bash +pnpm add -D @workleap/eslint-configs @eslint/js @typescript-eslint/parser @types/node eslint typescript-eslint +``` + +## Configuration Examples + +### Web Application + +```ts +// eslint.config.ts +import { defineWebApplicationConfig } from "@workleap/eslint-configs"; + +export default defineWebApplicationConfig(import.meta.dirname); +``` + +### React Library + +```ts +// eslint.config.ts +import { defineReactLibraryConfig } from "@workleap/eslint-configs"; + +export default defineReactLibraryConfig(import.meta.dirname); +``` + +With React Compiler: + +```ts +export default defineReactLibraryConfig(import.meta.dirname, { + react: { compiler: true } +}); +``` + +### TypeScript Library (No React) + +```ts +// eslint.config.ts +import { defineTypeScriptLibraryConfig } from "@workleap/eslint-configs"; + +export default defineTypeScriptLibraryConfig(import.meta.dirname); +``` + +### Monorepo Workspace Root + +```ts +// eslint.config.ts +import { defineMonorepoWorkspaceConfig } from "@workleap/eslint-configs"; + +export default defineMonorepoWorkspaceConfig(import.meta.dirname); +``` + +## Customization + +### Disable a Rule + +```ts +export default defineWebApplicationConfig(import.meta.dirname, { + core: { "no-var": "off" } +}); +``` + +### Change Rule Severity + +```ts +export default defineWebApplicationConfig(import.meta.dirname, { + jsxAlly: { "jsx-a11y/alt-text": "error" } +}); +``` + +### Use Jest Instead of Vitest + +```ts +export default defineWebApplicationConfig(import.meta.dirname, { + testFramework: "jest" +}); +``` + +### Add a Plugin + +```ts +import { defineWebApplicationConfig } from "@workleap/eslint-configs"; +import { defineConfig } from "eslint/config"; +import myPlugin from "eslint-plugin-myplugin"; + +export default defineConfig([ + { + plugins: { myPlugin }, + rules: { "myPlugin/rule": "warn" } + }, + defineWebApplicationConfig(import.meta.dirname) +]); +``` + +### Ignore Files + +```ts +import { defineWebApplicationConfig } from "@workleap/eslint-configs"; +import { defineConfig, globalIgnores } from "eslint/config"; + +export default defineConfig([ + globalIgnores(["packages", "samples", "docs"]), + defineWebApplicationConfig(import.meta.dirname) +]); +``` + +Default ignored: `node_modules`, `dist`, `storybook-static`, `.git`, `.turbo` + +## Rule Categories + +| Category | Description | +|----------|-------------| +| `core` | ESLint recommended + eslint-plugin-import | +| `jest` | eslint-plugin-jest rules | +| `json` | eslint-plugin-jsonc rules | +| `jsxAlly` | eslint-plugin-jsx-a11y rules | +| `packageJson` | eslint-plugin-package-json rules | +| `react` | eslint-plugin-react + hooks + @stylistic JSX | +| `storybook` | eslint-plugin-storybook rules | +| `testingLibrary` | eslint-plugin-testing-library rules | +| `typescript` | typescript-eslint + @stylistic rules | +| `vitest` | @vitest/eslint-plugin rules | +| `yaml` | eslint-plugin-yaml rules | + +## CLI Scripts + +### Polyrepo + +```json +{ + "lint:eslint": "eslint . --max-warnings=0 --cache --cache-location node_modules/.cache/eslint" +} +``` + +### Turborepo Workspace + +```json +{ + "lint": "turbo run lint --continue", + "eslint": "eslint . --max-warnings=0 --cache --cache-location node_modules/.cache/eslint" +} +``` + +### Turborepo Project + +```json +{ + "eslint": "eslint . --max-warnings=0 --cache --cache-location node_modules/.cache/eslint" +} +``` + +## Turborepo Configuration + +```json +// turbo.json +{ + "$schema": "https://turbo.build/schema.json", + "ui": "tui", + "tasks": { + "lint": { "dependsOn": ["eslint"] }, + "//#eslint": { "outputs": ["node_modules/.cache/eslint"] }, + "eslint": { "outputs": ["node_modules/.cache/eslint"] } + } +} +``` + +## VS Code Integration + +Install `dbaeumer.vscode-eslint` extension. + +```json +// .vscode/settings.json +{ + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.sortImports": "explicit" + }, + "editor.formatOnSave": true, + "typescript.format.enable": false, + "javascript.format.enable": false, + "json.format.enable": false, + "eslint.probe": ["javascript", "javascriptreact", "typescript", "typescriptreact", "json", "jsonc", "yaml"] +} +``` + +## EditorConfig (Recommended) + +```ini +# .editorconfig +root = true + +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false +``` + +Install `EditorConfig.EditorConfig` VS Code extension. + +## Troubleshooting + +### "File not found by the project service" + +The file is not included in `tsconfig.json`. Either: +1. Add the file pattern to the `include` field +2. Remove the `include` field entirely (TypeScript includes all files by default) diff --git a/.claude/skills/workleap-web-configs/references/rsbuild.md b/.claude/skills/workleap-web-configs/references/rsbuild.md new file mode 100644 index 00000000..6ebd1674 --- /dev/null +++ b/.claude/skills/workleap-web-configs/references/rsbuild.md @@ -0,0 +1,334 @@ +# Rsbuild Configuration + +## Overview + +`@workleap/rsbuild-configs` provides Rsbuild configurations for web applications. Rsbuild is a high-performance bundler powered by Rspack (Rust-based webpack port). + +**Features**: JavaScript, TypeScript, CSS (modules + LightningCSS), React, SVG as components, PNG/JPEG/GIF, HMR/Fast Refresh, source maps, minification. + +**Target environment**: As per Browserslist configuration. + +## Available Functions + +| Function | Use Case | +|----------|----------| +| `defineDevConfig` | Development server | +| `defineBuildConfig` | Production build | +| `defineStorybookConfig` | Storybook integration | + +## Installation + +```bash +pnpm add -D @workleap/rsbuild-configs @workleap/browserslist-config @rsbuild/core @rspack/core browserslist +``` + +For Storybook, also add: + +```bash +pnpm add -D storybook-react-rsbuild +``` + +## Setup Requirements + +### 1. HTML Template + +Create `public/index.html`: + +```html + + + + + + +
+ + +``` + +### 2. Browserslist + +Create `.browserslistrc`: + +```text +extends @workleap/browserslist-config +``` + +## Development Configuration + +### Basic Setup + +```ts +// rsbuild.dev.ts +import { defineDevConfig } from "@workleap/rsbuild-configs"; + +export default defineDevConfig(); +``` + +### Predefined Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `entry` | object | `{ index: "./src/index.tsx" }` | Entry points | +| `https` | boolean/object | `false` | Enable HTTPS | +| `host` | string | `localhost` | Dev server host | +| `port` | number | `8080` | Dev server port | +| `assetPrefix` | string | `/` | Asset URL prefix | +| `plugins` | array | `[]` | Rsbuild plugins | +| `html` | false/function | default template | HTML template config | +| `lazyCompilation` | boolean | `false` | Enable lazy compilation | +| `hmr` | boolean | `true` | Enable HMR | +| `fastRefresh` | boolean | `true` | Enable Fast Refresh | +| `sourceMap` | false/object | `{ js: "cheap-module-source-map", css: true }` | Source maps | +| `overlay` | false | undefined | Error overlay | +| `writeToDisk` | true | undefined | Write output to disk | +| `react` | false/function | enabled | React transformation | +| `svgr` | false/function | enabled | SVG as React components | +| `verbose` | boolean | `false` | Verbose logging | +| `environmentVariables` | object | `{}` | App environment variables | + +### Examples + +HTTPS with custom port: + +```ts +export default defineDevConfig({ + https: true, + port: 3000 +}); +``` + +Custom entry: + +```ts +export default defineDevConfig({ + entry: { index: "./src/main.tsx" } +}); +``` + +Environment variables: + +```ts +export default defineDevConfig({ + environmentVariables: { + "DEBUG": process.env.DEBUG === "true" + } +}); +``` + +Disable React transformation: + +```ts +export default defineDevConfig({ + react: false +}); +``` + +## Build Configuration + +### Basic Setup + +```ts +// rsbuild.build.ts +import { defineBuildConfig } from "@workleap/rsbuild-configs"; + +export default defineBuildConfig(); +``` + +### Predefined Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `entry` | object | `{ index: "./src/index.tsx" }` | Entry points | +| `distPath` | string | `dist` | Output directory | +| `assetPrefix` | string | `/` | Asset URL prefix | +| `plugins` | array | `[]` | Rsbuild plugins | +| `html` | false/function | default template | HTML template config | +| `minify` | false/object | `true` | Code minification | +| `optimize` | boolean/"readable" | `true` | Production optimization | +| `sourceMap` | false/object | `{ js: "source-map", css: true }` | Source maps | +| `react` | false/function | enabled | React transformation | +| `svgr` | false/function | enabled | SVG as React components | +| `compressImage` | false/function | enabled | Image compression | +| `verbose` | boolean | `false` | Verbose logging | +| `environmentVariables` | object | `{}` | App environment variables | + +### Examples + +Custom output path: + +```ts +import path from "node:path"; + +export default defineBuildConfig({ + distPath: path.resolve("./build") +}); +``` + +Disable minification (debugging): + +```ts +export default defineBuildConfig({ + minify: false +}); +``` + +Readable output (debugging): + +```ts +export default defineBuildConfig({ + optimize: "readable" +}); +``` + +## Storybook Configuration + +### Setup + +```ts +// .storybook/rsbuild.config.ts +import { defineStorybookConfig } from "@workleap/rsbuild-configs"; + +export default defineStorybookConfig(); +``` + +```ts +// .storybook/main.ts +import type { StorybookConfig } from "storybook-react-rsbuild"; + +const storybookConfig: StorybookConfig = { + framework: "storybook-react-rsbuild", + stories: ["../../src/**/*.stories.(tsx|mdx)"] +}; + +export default storybookConfig; +``` + +### Predefined Options + +| Option | Type | Default | +|--------|------|---------| +| `plugins` | array | `[]` | +| `lazyCompilation` | boolean | `false` | +| `sourceMap` | false/object | `{ js: "cheap-module-source-map", css: true }` | +| `react` | false/function | enabled | +| `svgr` | false/function | enabled | +| `verbose` | boolean | `false` | +| `environmentVariables` | object | `{}` | + +## Configuration Transformers + +For full control, use transformers: + +```ts +import { defineDevConfig, type RsbuildConfigTransformer } from "@workleap/rsbuild-configs"; +import type { RsbuildConfig } from "@rsbuild/core"; + +const customTransformer: RsbuildConfigTransformer = (config: RsbuildConfig, context) => { + if (context.environment === "dev") { + config.tools = config.tools ?? {}; + // modify config + } + return config; +}; + +export default defineDevConfig({ + transformers: [customTransformer] +}); +``` + +Context properties: `environment` ("dev" | "build" | "storybook"), `verbose` (boolean) + +## CLI Scripts + +### Development + +```json +{ "dev": "rsbuild dev --config ./rsbuild.dev.ts" } +``` + +### Build + +```json +{ "build": "rsbuild build --config rsbuild.build.ts" } +``` + +### Storybook + +```json +{ + "dev": "storybook dev -p 6006", + "build": "storybook build" +} +``` + +## Type Declarations + +### SVG Files + +Create `src/env.d.ts`: + +```ts +declare module '*.svg' { + export const ReactComponent: React.FunctionComponent>; +} +declare module '*.svg?react' { + const ReactComponent: React.FunctionComponent>; + export default ReactComponent; +} +``` + +### CSS Modules + +```ts +// src/env.d.ts +/// +``` + +For monorepos, include env.d.ts from dependencies: + +```json +// tsconfig.json +{ + "include": [".", "../**/src/env.d.ts"] +} +``` + +## SVG Import + +```tsx +import { ReactComponent as Logo } from "./logo.svg"; + +export const App = () => ; +``` + +## Turborepo Setup + +### turbo.json + +```json +{ + "$schema": "https://turbo.build/schema.json", + "ui": "tui", + "tasks": { + "dev": { + "dependsOn": ["^build"], + "cache": false, + "persistent": true + }, + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", "storybook-static/**"] + } + } +} +``` + +### Workspace Scripts + +```json +{ + "dev-app": "turbo run dev --filter=./apps/app", + "build-app": "turbo run build --filter=./apps/app" +} +``` diff --git a/.claude/skills/workleap-web-configs/references/rslib.md b/.claude/skills/workleap-web-configs/references/rslib.md new file mode 100644 index 00000000..dc3649ca --- /dev/null +++ b/.claude/skills/workleap-web-configs/references/rslib.md @@ -0,0 +1,324 @@ +# Rslib Configuration + +## Overview + +`@workleap/rslib-configs` provides Rslib configurations for library projects. Rslib is powered by Rsbuild and Rspack. + +**Features**: JavaScript, TypeScript, CSS (modules + LightningCSS), React, SVG as components, declaration files (.d.ts), watch mode, source maps. + +**Target environment**: ESM, ESNext + +## Available Functions + +| Function | Use Case | +|----------|----------| +| `defineDevConfig` | Watch mode for development | +| `defineBuildConfig` | Production build for publication | +| `defineStorybookConfig` | Storybook integration | + +## Installation + +```bash +pnpm add -D @workleap/rslib-configs @rslib/core +``` + +For Storybook, also add: + +```bash +pnpm add -D storybook-react-rsbuild storybook-addon-rslib +``` + +## Setup Requirements + +### tsconfig.build.json + +Required for bundleless output (default). The `include` must point to source files: + +```json +// tsconfig.build.json +{ + "extends": "@workleap/typescript-configs/library.json", + "include": ["src"], + "exclude": ["dist", "node_modules"] +} +``` + +### package.json (for publishing) + +Include source files for debugging with source maps: + +```json +{ + "files": ["src", "dist", "CHANGELOG.md", "README.md"] +} +``` + +## Build Configuration + +### Basic Setup + +```ts +// rslib.build.ts +import { defineBuildConfig } from "@workleap/rslib-configs"; +import path from "node:path"; + +export default defineBuildConfig({ + tsconfigPath: path.resolve("./tsconfig.build.json") +}); +``` + +**Note**: `tsconfigPath` is required when using bundleless output (the default). It tells Rslib which files to include based on the tsconfig's `include` field. + +### Predefined Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `entry` | object | `{ index: "./src/**" }` (bundleless) or `{ index: ["./src/index.ts", "./src/index.js"] }` (bundle) | Entry points | +| `syntax` | string | `esnext` | Output syntax target | +| `bundle` | boolean | `false` | Single bundle vs bundleless | +| `tsconfigPath` | string | undefined | Path to tsconfig (required for bundleless) | +| `dts` | object/false | `true` | Generate .d.ts files | +| `target` | string | `web` | Output target | +| `distPath` | string | `dist` | Output directory | +| `plugins` | array | `[]` | Rsbuild plugins | +| `sourceMap` | false/object | `{ js: "source-map", css: true }` | Source maps | +| `react` | true/function | disabled | React transformation | +| `svgr` | true/function | disabled | SVG as React components | + +### Examples + +React library: + +```ts +import { defineBuildConfig } from "@workleap/rslib-configs"; +import path from "node:path"; + +export default defineBuildConfig({ + tsconfigPath: path.resolve("./tsconfig.build.json"), + react: true, + svgr: true +}); +``` + +Bundled output: + +```ts +export default defineBuildConfig({ + bundle: true, + react: true +}); +``` + +Node target: + +```ts +export default defineBuildConfig({ + target: "node", + tsconfigPath: path.resolve("./tsconfig.build.json") +}); +``` + +Custom syntax: + +```ts +export default defineBuildConfig({ + syntax: "es2024", + tsconfigPath: path.resolve("./tsconfig.build.json") +}); +``` + +Disable declaration files: + +```ts +export default defineBuildConfig({ + dts: false, + tsconfigPath: path.resolve("./tsconfig.build.json") +}); +``` + +## Development Configuration + +### Basic Setup + +```ts +// rslib.dev.ts +import { defineDevConfig } from "@workleap/rslib-configs"; +import path from "node:path"; + +export default defineDevConfig({ + tsconfigPath: path.resolve("./tsconfig.build.json") +}); +``` + +**Note**: `tsconfigPath` is required when using bundleless output (the default). + +### Predefined Options + +Same as build config, except: + +| Option | Default Difference | +|--------|-------------------| +| `dts` | `false` (disabled in dev) | +| `sourceMap` | `{ js: "cheap-module-source-map", css: true }` | + +### Example + +```ts +import { defineDevConfig } from "@workleap/rslib-configs"; +import path from "node:path"; + +export default defineDevConfig({ + tsconfigPath: path.resolve("./tsconfig.build.json"), + react: true +}); +``` + +## Storybook Configuration + +### Setup + +```ts +// .storybook/rslib.config.ts +import { defineStorybookConfig } from "@workleap/rslib-configs"; + +export default defineStorybookConfig(); +``` + +```ts +// .storybook/main.ts +import type { StorybookConfig } from "storybook-react-rsbuild"; + +const storybookConfig: StorybookConfig = { + framework: "storybook-react-rsbuild", + addons: ["storybook-addon-rslib"], + stories: ["../../src/**/*.stories.(tsx|mdx)"] +}; + +export default storybookConfig; +``` + +### Predefined Options + +| Option | Type | Default | +|--------|------|---------| +| `plugins` | array | `[]` | +| `sourceMap` | false/object | `{ js: "cheap-module-source-map", css: true }` | +| `react` | false/function | enabled | +| `svgr` | false/function | enabled | + +## Configuration Transformers + +```ts +import { defineBuildConfig, type RslibConfigTransformer } from "@workleap/rslib-configs"; +import type { RslibConfig } from "@rslib/core"; + +const customTransformer: RslibConfigTransformer = (config: RslibConfig, context) => { + if (context.environment === "build") { + // modify config + } + return config; +}; + +export default defineBuildConfig({ + transformers: [customTransformer] +}); +``` + +Context: `environment` ("dev" | "build" | "storybook") + +## CLI Scripts + +### Development (Watch Mode) + +```json +{ "dev": "rslib build -w -c ./rslib.dev.ts" } +``` + +### Build + +```json +{ "build": "rslib build -c rslib.build.ts" } +``` + +### Storybook + +```json +{ + "dev": "storybook dev -p 6006", + "build": "storybook build" +} +``` + +## Type Declarations + +### SVG Files + +```ts +// src/env.d.ts +declare module '*.svg' { + export const ReactComponent: React.FunctionComponent>; +} +declare module '*.svg?react' { + const ReactComponent: React.FunctionComponent>; + export default ReactComponent; +} +``` + +### CSS Modules + +```ts +// src/env.d.ts +/// +``` + +Note: Reference `@rsbuild/core`, not `@rslib/core`. + +## Bundleless vs Bundle + +**Bundleless (default, recommended)**: Outputs individual files matching source structure. Better for tree-shaking by consuming bundlers. + +**Bundle**: Outputs a single file. Bundlers must tree-shake within the file, which is less predictable. + +```ts +// Bundleless (default) +export default defineBuildConfig({ + bundle: false, // default + tsconfigPath: path.resolve("./tsconfig.build.json") +}); + +// Bundle +export default defineBuildConfig({ + bundle: true +}); +``` + +## Turborepo Setup + +### turbo.json + +```json +{ + "$schema": "https://turbo.build/schema.json", + "ui": "tui", + "tasks": { + "dev": { + "dependsOn": ["^build"], + "cache": false, + "persistent": true + }, + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", "storybook-static/**"] + } + } +} +``` + +### Workspace Scripts + +```json +{ + "dev-lib": "turbo run dev --filter=./packages/lib", + "build-lib": "turbo run build --filter=./packages/lib" +} +``` diff --git a/.claude/skills/workleap-web-configs/references/stylelint.md b/.claude/skills/workleap-web-configs/references/stylelint.md new file mode 100644 index 00000000..092a83f2 --- /dev/null +++ b/.claude/skills/workleap-web-configs/references/stylelint.md @@ -0,0 +1,185 @@ +# Stylelint Configuration + +## Overview + +`@workleap/stylelint-configs` provides Stylelint configurations for CSS linting. Uses Prettier for stylistic rules via stylelint-prettier plugin. + +**No SCSS support**: The library does not provide Sass rules. Use native CSS nesting instead. + +## Installation + +### Polyrepo + +```bash +pnpm add -D @workleap/stylelint-configs stylelint prettier +``` + +### Turborepo (Workspace Root) + +```bash +pnpm add -D @workleap/stylelint-configs stylelint prettier turbo +``` + +### Turborepo (Project) + +```bash +pnpm add -D @workleap/stylelint-configs +``` + +## Configuration + +### .stylelintrc.json + +```json +{ + "$schema": "https://json.schemastore.org/stylelintrc", + "extends": "@workleap/stylelint-configs" +} +``` + +### .stylelintignore + +```text +**/dist/* +node_modules +storybook-static +!.storybook +``` + +### .prettierignore (Required) + +Ignore everything except CSS to prevent conflicts with ESLint: + +```text +* +!**/*.css +``` + +## Customization + +### Disable a Rule + +```json +{ + "$schema": "https://json.schemastore.org/stylelintrc", + "extends": "@workleap/stylelint-configs", + "rules": { + "color-hex-length": null + } +} +``` + +### Change Rule Severity + +```json +{ + "$schema": "https://json.schemastore.org/stylelintrc", + "extends": "@workleap/stylelint-configs", + "rules": { + "max-nesting-depth": [2, { "severity": "error" }] + } +} +``` + +### Change Rule Value + +```json +{ + "$schema": "https://json.schemastore.org/stylelintrc", + "extends": "@workleap/stylelint-configs", + "rules": { + "unit-allowed-list": ["rem"] + } +} +``` + +### Add a Plugin + +```json +{ + "$schema": "https://json.schemastore.org/stylelintrc", + "plugins": ["stylelint-order"], + "extends": "@workleap/stylelint-configs", + "rules": { + "order/properties-order": ["width", "height"] + } +} +``` + +## CLI Scripts + +### Polyrepo + +```json +{ + "lint:stylelint": "stylelint \"**/*.css\" --cache --cache-location node_modules/.cache/stylelint" +} +``` + +### Turborepo Workspace + +```json +{ + "lint": "turbo run lint --continue" +} +``` + +### Turborepo Project + +```json +{ + "stylelint": "stylelint \"**/*.css\" --allow-empty-input --cache --cache-location node_modules/.cache/stylelint --max-warnings=0" +} +``` + +## Turborepo Configuration + +```json +// turbo.json +{ + "$schema": "https://turbo.build/schema.json", + "ui": "tui", + "tasks": { + "lint": { "dependsOn": ["stylelint"] }, + "stylelint": { "outputs": ["node_modules/.cache/stylelint"] } + } +} +``` + +## EditorConfig (Recommended) + +For consistent indentation across Prettier and VS Code: + +```ini +# .editorconfig +root = true + +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false +``` + +Install `EditorConfig.EditorConfig` VS Code extension. + +## VS Code Integration + +Install `stylelint.vscode-stylelint` extension. + +```json +// .vscode/settings.json +{ + "editor.codeActionsOnSave": { + "source.fixAll": "explicit" + }, + "editor.formatOnSave": true, + "css.validate": false, + "scss.validate": false +} +``` diff --git a/.claude/skills/workleap-web-configs/references/typescript.md b/.claude/skills/workleap-web-configs/references/typescript.md new file mode 100644 index 00000000..6f3a0a6c --- /dev/null +++ b/.claude/skills/workleap-web-configs/references/typescript.md @@ -0,0 +1,209 @@ +# TypeScript Configuration + +## Overview + +`@workleap/typescript-configs` provides TypeScript configurations by project type. Focuses on **linting only**; bundlers handle transpilation. + +**Target environment**: ESM, ESNext + +## Available Configurations + +| Config | Use Case | +|--------|----------| +| `web-application.json` | React web applications | +| `library.json` | Libraries (with or without React) | +| `monorepo-workspace.json` | Monorepo workspace root | + +### Advanced Composition Pieces + +| Config | Description | +|--------|-------------| +| `core.json` | Basic rules shared by all configs | +| `react.json` | React-specific rules | + +## Installation + +### Polyrepo + +```bash +pnpm add -D @workleap/typescript-configs typescript +``` + +### Turborepo (Workspace + Projects) + +```bash +pnpm add -D @workleap/typescript-configs typescript turbo +``` + +## Configuration Examples + +### Web Application + +```json +// tsconfig.json +{ + "extends": ["@workleap/typescript-configs/web-application.json"], + "exclude": ["dist", "node_modules"] +} +``` + +With Storybook: + +```json +{ + "extends": ["@workleap/typescript-configs/web-application.json"], + "include": ["**/*", ".storybook/*"], + "exclude": ["dist", "node_modules"] +} +``` + +### Library + +```json +// tsconfig.json +{ + "extends": ["@workleap/typescript-configs/library.json"], + "exclude": ["dist", "node_modules"] +} +``` + +With Storybook: + +```json +{ + "extends": ["@workleap/typescript-configs/library.json"], + "include": ["**/*", ".storybook/*"], + "exclude": ["dist", "node_modules", ".storybook/storybook-static"] +} +``` + +### Monorepo Workspace Root + +```json +// tsconfig.json +{ + "extends": "@workleap/typescript-configs/monorepo-workspace.json", + "exclude": ["packages", "node_modules"] +} +``` + +With Storybook: + +```json +{ + "extends": "@workleap/typescript-configs/monorepo-workspace.json", + "include": ["**/*", ".storybook/*"], + "exclude": ["packages", "node_modules", ".storybook/storybook-static"] +} +``` + +## Customization + +### Override a Compiler Option + +```json +{ + "extends": ["@workleap/typescript-configs/web-application.json"], + "compilerOptions": { + "strict": false + }, + "exclude": ["dist", "node_modules"] +} +``` + +### Non-ESM Projects (Migration) + +To allow imports without file extensions during migration: + +```json +{ + "extends": ["@workleap/typescript-configs/web-application.json"], + "compilerOptions": { + "moduleResolution": "Bundler", + "module": "ESNext" + }, + "exclude": ["dist", "node_modules"] +} +``` + +### Monorepo Path Mappings + +For projects referencing other workspace packages without JIT packages: + +```json +// packages/pkg-1/tsconfig.json +{ + "extends": "@workleap/typescript-configs/web-application.json", + "compilerOptions": { + "paths": { + "@sample/components": ["../components/index.ts"], + "@sample/utils": ["../utils/index.ts"] + } + }, + "exclude": ["dist", "node_modules"] +} +``` + +### Advanced: Custom Composition + +For React projects needing custom setup: + +```json +{ + "extends": "@workleap/typescript-configs/react", + "compilerOptions": { /* custom options */ }, + "exclude": ["dist", "node_modules"] +} +``` + +For non-React projects: + +```json +{ + "extends": "@workleap/typescript-configs/core", + "compilerOptions": { /* custom options */ }, + "exclude": ["dist", "node_modules"] +} +``` + +## CLI Scripts + +### Polyrepo + +```json +{ + "lint:types": "tsc" +} +``` + +### Turborepo Workspace + +```json +{ + "lint": "turbo run lint --continue", + "typecheck": "tsgo" +} +``` + +### Turborepo Project + +```json +{ + "typecheck": "tsgo" +} +``` + +## Turborepo Configuration + +```json +// turbo.json +{ + "$schema": "https://turbo.build/schema.json", + "ui": "tui", + "tasks": { + "lint": { "dependsOn": ["typecheck"] }, + "//#typecheck": { "outputs": ["node_modules/.cache/tsbuildinfo.json"] }, + "typecheck": { "outputs": ["node_modules/.cache/tsbuildinfo.json"] } + } +} +``` diff --git a/.syncpackrc.js b/.syncpackrc.js index f437cb38..7208c290 100644 --- a/.syncpackrc.js +++ b/.syncpackrc.js @@ -49,6 +49,20 @@ export default { "dependencyTypes": ["peer"], "isIgnored": true }, + { + // Sample apps use "workspace:*" (pinned) while packages use "workspace:^" (range). + // Ignore this expected mismatch. + "packages": ["@all-platforms/*"], + "dependencies": ["@workleap/telemetry", "@workleap/common-room"], + "isIgnored": true + }, + { + // Sample apps use "workspace:*" (pinned) while packages use "workspace:^" (range). + // Ignore this expected mismatch. + "packages": ["@honeycomb-api-key/*", "@honeycomb-proxy/*"], + "dependencies": ["@workleap/honeycomb"], + "isIgnored": true + }, { "packages": ["**"], "dependencyTypes": ["prod", "dev", "peer"], diff --git a/packages/common-room/package.json b/packages/common-room/package.json index 4f986033..a464bca2 100644 --- a/packages/common-room/package.json +++ b/packages/common-room/package.json @@ -54,7 +54,7 @@ } }, "dependencies": { - "@workleap-telemetry/core": "workspace:*", + "@workleap-telemetry/core": "workspace:^", "@workleap/logging": "^1.3.6" }, "devDependencies": { diff --git a/packages/honeycomb/package.json b/packages/honeycomb/package.json index 99cbfddc..9b74e252 100644 --- a/packages/honeycomb/package.json +++ b/packages/honeycomb/package.json @@ -63,7 +63,7 @@ "@opentelemetry/instrumentation-user-interaction": "^0.55.0", "@opentelemetry/instrumentation-xml-http-request": "^0.211.0", "@opentelemetry/sdk-trace-web": "^2.5.0", - "@workleap-telemetry/core": "workspace:*", + "@workleap-telemetry/core": "workspace:^", "@workleap/logging": "^1.3.6" }, "devDependencies": { diff --git a/packages/logrocket/package.json b/packages/logrocket/package.json index e3dcd64e..eebcb110 100644 --- a/packages/logrocket/package.json +++ b/packages/logrocket/package.json @@ -55,7 +55,7 @@ } }, "dependencies": { - "@workleap-telemetry/core": "workspace:*", + "@workleap-telemetry/core": "workspace:^", "@workleap/logging": "^1.3.6", "logrocket-fuzzy-search-sanitizer": "0.0.2" }, diff --git a/packages/mixpanel/package.json b/packages/mixpanel/package.json index b3cc5193..e1d86d18 100644 --- a/packages/mixpanel/package.json +++ b/packages/mixpanel/package.json @@ -42,7 +42,7 @@ "test": "vitest --config vitest.config.ts --no-watch" }, "peerDependencies": { - "@workleap/logrocket": "workspace:*", + "@workleap/logrocket": "workspace:^", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, @@ -58,7 +58,7 @@ } }, "dependencies": { - "@workleap-telemetry/core": "workspace:*", + "@workleap-telemetry/core": "workspace:^", "@workleap/logging": "^1.3.6" }, "devDependencies": { diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index 9805d1ee..6bd831fa 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -62,11 +62,11 @@ }, "dependencies": { "@opentelemetry/sdk-trace-web": "^2.5.0", - "@workleap-telemetry/core": "workspace:*", - "@workleap/honeycomb": "workspace:*", + "@workleap-telemetry/core": "workspace:^", + "@workleap/honeycomb": "workspace:^", "@workleap/logging": "^1.3.6", - "@workleap/logrocket": "workspace:*", - "@workleap/mixpanel": "workspace:*" + "@workleap/logrocket": "workspace:^", + "@workleap/mixpanel": "workspace:^" }, "devDependencies": { "@eslint/js": "9.39.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a7c515e..70b641f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,7 +68,7 @@ importers: packages/common-room: dependencies: '@workleap-telemetry/core': - specifier: workspace:* + specifier: workspace:^ version: link:../core '@workleap/logging': specifier: ^1.3.6 @@ -208,7 +208,7 @@ importers: specifier: ^2.5.0 version: 2.5.1(@opentelemetry/api@1.9.0) '@workleap-telemetry/core': - specifier: workspace:* + specifier: workspace:^ version: link:../core '@workleap/logging': specifier: ^1.3.6 @@ -266,7 +266,7 @@ importers: packages/logrocket: dependencies: '@workleap-telemetry/core': - specifier: workspace:* + specifier: workspace:^ version: link:../core '@workleap/logging': specifier: ^1.3.6 @@ -330,13 +330,13 @@ importers: packages/mixpanel: dependencies: '@workleap-telemetry/core': - specifier: workspace:* + specifier: workspace:^ version: link:../core '@workleap/logging': specifier: ^1.3.6 version: 1.3.6 '@workleap/logrocket': - specifier: workspace:* + specifier: workspace:^ version: link:../logrocket react: specifier: ^18.0.0 || ^19.0.0 @@ -397,19 +397,19 @@ importers: specifier: ^2.5.0 version: 2.5.1(@opentelemetry/api@1.9.0) '@workleap-telemetry/core': - specifier: workspace:* + specifier: workspace:^ version: link:../core '@workleap/honeycomb': - specifier: workspace:* + specifier: workspace:^ version: link:../honeycomb '@workleap/logging': specifier: ^1.3.6 version: 1.3.6 '@workleap/logrocket': - specifier: workspace:* + specifier: workspace:^ version: link:../logrocket '@workleap/mixpanel': - specifier: workspace:* + specifier: workspace:^ version: link:../mixpanel logrocket: specifier: ^12.0.0 diff --git a/skills-lock.json b/skills-lock.json index 14e004d8..d31c57fa 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -1,50 +1,55 @@ { - "version": 1, - "skills": { - "accessibility": { - "source": "addyosmani/web-quality-skills", - "sourceType": "github", - "computedHash": "3f390c14d09ca45da332b6c421b3195d12da099ec24ee549f80e666ec48dc89b" - }, - "agent-browser": { - "source": "vercel-labs/agent-browser", - "sourceType": "github", - "computedHash": "4a448025404360fb966eb707116b188b55a52dbb20deecb13d83e7f4db5087f1" - }, - "best-practices": { - "source": "addyosmani/web-quality-skills", - "sourceType": "github", - "computedHash": "f5d58f0adf9cc9646b35daf2241377e1ad7b8ab094e54c9792a826af1e0dca5b" - }, - "git-commit": { - "source": "github/awesome-copilot", - "sourceType": "github", - "computedHash": "2607fc60629b82b257136dd2a7a373f0a4466c0b49df7746d845d59313c99b21" - }, - "pnpm": { - "source": "antfu/skills", - "sourceType": "github", - "computedHash": "d473204df25defd08a7d47b55c6d124b41ebde440b774995952b65223d0d8d6f" - }, - "vitest": { - "source": "antfu/skills", - "sourceType": "github", - "computedHash": "0efd9cf8c56aada060d418d009cfe621a7915d0c73a958fe611985bc0c2fa4b8" - }, - "workleap-logging": { - "source": "workleap/wl-logging", - "sourceType": "github", - "computedHash": "5ba89e02b31ffabcebe72b45237dfaaa7e790b51dfdb7511174436841c85d161" - }, - "workleap-react-best-practices": { - "source": "workleap/wl-web-configs", - "sourceType": "github", - "computedHash": "f3849c705dd508216673f24efeff9ef25add517ed6a16743675ad53c09f3aba4" - }, - "workleap-web-configs": { - "source": "workleap/wl-web-configs", - "sourceType": "github", - "computedHash": "9c66162f92a9e6b809ba08361e540ce545deb884219d4430542b778fa117ea69" - } + "version": 1, + "skills": { + "accessibility": { + "source": "addyosmani/web-quality-skills", + "sourceType": "github", + "computedHash": "3f390c14d09ca45da332b6c421b3195d12da099ec24ee549f80e666ec48dc89b" + }, + "agent-browser": { + "source": "vercel-labs/agent-browser", + "sourceType": "github", + "computedHash": "4a448025404360fb966eb707116b188b55a52dbb20deecb13d83e7f4db5087f1" + }, + "best-practices": { + "source": "addyosmani/web-quality-skills", + "sourceType": "github", + "computedHash": "f5d58f0adf9cc9646b35daf2241377e1ad7b8ab094e54c9792a826af1e0dca5b" + }, + "git-commit": { + "source": "github/awesome-copilot", + "sourceType": "github", + "computedHash": "2607fc60629b82b257136dd2a7a373f0a4466c0b49df7746d845d59313c99b21" + }, + "pnpm": { + "source": "antfu/skills", + "sourceType": "github", + "computedHash": "d473204df25defd08a7d47b55c6d124b41ebde440b774995952b65223d0d8d6f" + }, + "turborepo": { + "source": "vercel/turborepo", + "sourceType": "github", + "computedHash": "5277d4e4fcd73245ae8a5bbd958589defc12c2e5d397bdd48b788c53430edcbc" + }, + "vitest": { + "source": "antfu/skills", + "sourceType": "github", + "computedHash": "0efd9cf8c56aada060d418d009cfe621a7915d0c73a958fe611985bc0c2fa4b8" + }, + "workleap-logging": { + "source": "workleap/wl-logging", + "sourceType": "github", + "computedHash": "5ba89e02b31ffabcebe72b45237dfaaa7e790b51dfdb7511174436841c85d161" + }, + "workleap-react-best-practices": { + "source": "workleap/wl-web-configs", + "sourceType": "github", + "computedHash": "f3849c705dd508216673f24efeff9ef25add517ed6a16743675ad53c09f3aba4" + }, + "workleap-web-configs": { + "source": "workleap/wl-web-configs", + "sourceType": "github", + "computedHash": "a6d35aad36edeb536708ac166d3b27cf93bb7ec781fa2845da25d92550313641" } + } }