Bati is a next-generation scaffolding CLI tool for the Vike (Vite-based) ecosystem. It generates fully-functional starter apps by combining boilerplates for different features (React/Vue/Solid, servers, databases, auth, etc.).
Repository Structure:
- TypeScript monorepo managed with pnpm workspaces and Turborepo
- Node.js ≥20 required, pnpm 10.24.0 (as specified in package.json
packageManager) - Workspaces across
/packages/(~11 packages) and/boilerplates/(~42 feature templates)
ALWAYS run these commands from the monorepo root:
# 1. Install dependencies (required before any build)
pnpm install
# 2. Build all packages (~55 seconds)
pnpm run build
# 3. Run unit tests (~15 seconds)
pnpm run test
# 4. Run type checking (~60 seconds)
pnpm run check-types
# 5. Run linting (Biome)
pnpm run lintImportant Notes:
pnpm installtriggers@batijs/compileprepublish build automaticallypnpm run buildmust complete before running tests or CLI- Build uses Turborepo caching; use
pnpm run build:forceto rebuild without cache - The
formatstep runs automatically after build via Biome
# Unit tests (fast, ~15s)
pnpm run test
# E2E tests (extensive, run on CI - not recommended locally due to time)
pnpm run test:e2e
# Filter E2E tests
pnpm run test:e2e --filter solid,authjsWhen adding a new feature, add E2E tests to verify it works correctly.
Tests are in packages/tests/tests/ with naming convention: FRAMEWORK+<feature>.spec.ts
Existing test files and their purposes:
FRAMEWORK+ANALYTICS.spec.ts- Analytics (plausible.io, google-analytics)FRAMEWORK+CSS.spec.ts- CSS frameworks (tailwindcss, daisyui)FRAMEWORK+SERVER+AUTH.spec.ts- Server + auth combinations (authjs, auth0)FRAMEWORK+SERVER+DATA.spec.ts- Server + data fetching (trpc, telefunc, ts-rest, drizzle, sqlite)FRAMEWORK+sentry.spec.ts- Sentry error trackingFRAMEWORK+prisma.spec.ts- Prisma ORMFRAMEWORK+cloudflare.spec.ts- Cloudflare deploymentFRAMEWORK+vercel.spec.ts- Vercel deploymentFRAMEWORK+aws.spec.ts- AWS Lambda deploymentFRAMEWORK+prettier.spec.ts- Prettier formatterreact+UI.spec.ts- React-specific UI libs (compiled-css, mantine)remove-linter-comments.spec.ts- Linter comment cleanup verification
Each test file exports a matrix array and optionally an exclude array:
import { describeBati } from "@batijs/tests-utils";
// Matrix defines feature combinations to test
// Arrays create permutations, single values are always included
export const matrix = [
["solid", "react", "vue"], // One of these UI frameworks
["feature1", "feature2", undefined], // Optional features (undefined = without)
"eslint", "biome", "oxlint" // Always included
];
// Exclude specific combinations to reduce test count
export const exclude = [
["react", "feature1"], // Don't test react + feature1
["vue", "feature2"], // Don't test vue + feature2
];
await describeBati(({ test, expect, fetch, testMatch }) => {
test("home", async () => {
const res = await fetch("/");
expect(res.status).toBe(200);
});
// Conditional tests based on matrix
testMatch<typeof matrix>("feature-specific test", {
feature1: async () => { /* test for feature1 */ },
feature2: async () => { /* test for feature2 */ },
_: async () => { /* default/fallback test */ },
});
});- Avoid duplicate combinations: Check
.github/workflows/tests-entry.ymlto ensure your test combinations don't overlap with existing ones - Use
excludearray: Reduce test permutations by excluding unnecessary combinations - Add to existing matrix when possible: If your feature fits an existing test category
- Create new spec file only if needed: For truly unique features
- Regenerate workflow matrix: Run
pnpm run test:e2e workflow-writeto auto-generate entries intests-entry.yml(do NOT edit manually)
Tests can run in different modes via describeBati options:
mode: "dev"(default) - Development servermode: "prod"- Production build + previewmode: "build"- Build only, no servermode: "none"- No build, no server (file checks only)
After adding a new test file or modifying matrices, regenerate workflow entries:
pnpm run test:e2e workflow-writeThis auto-generates the matrix entries in .github/workflows/tests-entry.yml. Never edit this file manually.
/
├── packages/
│ ├── cli/ # Main Bati CLI (@batijs/cli)
│ ├── core/ # Core utilities for boilerplate processing
│ ├── compile/ # Boilerplate compilation tools
│ ├── build/ # Build orchestration
│ ├── features/ # Feature definitions and rules
│ ├── tests/ # E2E test infrastructure
│ └── tests-utils/ # Test utilities
├── boilerplates/ # Feature boilerplates (~40 folders)
│ ├── shared/ # Base shared boilerplate (processed first via `enforce: "pre"`)
│ ├── react/ # React UI framework
│ ├── vue/ # Vue UI framework
│ ├── solid/ # SolidJS UI framework
│ ├── hono/ # Hono server
│ └── ... # Other features (auth, db, hosting, etc.)
├── website/ # batijs.dev website
├── turbo.json # Turborepo configuration
├── biome.json # Biome linter/formatter config
├── pnpm-workspace.yaml
└── tsconfig.json # Root TypeScript config
| File | Purpose |
|---|---|
turbo.json |
Turborepo task definitions and caching |
biome.json |
Linting and formatting rules (extends @vikejs/biome-config) |
pnpm-workspace.yaml |
Workspace package locations and hoisting config |
packages/cli/turbo.json |
CLI-specific build dependencies |
packages/features/src/features.ts |
Feature flag definitions |
packages/features/src/rules/rules.ts |
Feature compatibility rules |
MAINTAINABILITY is the top priority. Strive for clean code and good separation of concerns.
Use pnpm run new-boilerplate <name> when:
- The feature is UI-framework independent (e.g.,
sentry,tailwindcss,auth0)
Create UI-specific boilerplates when the feature requires framework-specific code:
- Example:
sentryfeature has:sentry/(shared),react-sentry/,vue-sentry/,solid-sentry/ - The UI-specific configs use combined conditions:
meta.BATI.has("react") && meta.BATI.has("sentry")
Prefer editing existing boilerplates when creating a new one would add too much complexity or duplication:
- Example:
tailwindcssdoesn't duplicate all components; it uses BATI compiler syntax to conditionally add classes inreact/,vue/,solid/boilerplates:<div //# BATI.has("tailwindcss") className={"flex max-w-5xl m-auto"} //# !BATI.has("tailwindcss") && !BATI.has("compiled-css") style={{ display: "flex", maxWidth: 900, margin: "auto" }} >
- Create:
pnpm run new-boilerplate <name>thenpnpm install - Configure
boilerplates/<name>/bati.config.ts:export default defineConfig({ if(meta) { return meta.BATI.has("feature-name"); }, });
- Add files to
boilerplates/<name>/files/ - Use
$*.tsprefix for dynamic files (e.g.,$package.json.ts)
The packages/features/src/helpers.ts file provides BatiSet with useful helpers:
BATI.has("feature")- Check if feature is enabledBATI.hasServer- Check if any server feature is enabledBATI.hasDatabase- Check if database features (sqlite/drizzle) are enabledBATI.hasD1- Check if Cloudflare D1 is usedBATI.hasPhoton- Check if Photon-compatible hosting is used
Update helpers when adding features that need cross-cutting detection logic.
For detailed syntax documentation, see boilerplates/README.md.
Key Concept: BATI is a global Set available during the templating phase, containing all chosen features.
| Pattern | Description | Priority (low to high) |
|---|---|---|
filename |
Standard file | 1 (lowest) |
!filename |
Higher priority override file | 2 |
$filename.ts |
Dynamic file processed through callback (e.g., $README.md.ts) |
3 |
!$filename.ts |
Highest priority dynamic file | 4 (highest) |
If/else statements:
if (BATI.has("feature")) {
console.log("A");
} else {
console.log("B");
}
// Also works with else-ifTernary expressions:
const myvar = BATI.has("feature") ? "A" : "B";Comment-based conditional (next line only):
// BATI.has("feature")
import "./mycss";
//# BATI.has("feature") // Alternative with # (commonly used for JSX attributes)
import "./other";Include file only if imported:
/*# BATI include-if-imported #*/
const a = 1;Type casting helper:
// Equivalent to `as any` but dropped entirely when compiled
const a = 'react' as BATI.Any;Conditional types with BATI.If:
interface Context {
ui: BATI.If<{
'BATI.has("react")': "react";
'BATI.has("vue")': "vue";
'BATI.has("solid")': "solid";
_: "other"; // fallback
}>;
}Conditional attributes (use //# comment before attribute):
<div
//# BATI.has("feature")
class="p-5"
//# !BATI.has("feature")
style={{ padding: "20px" }}
>
{props.children}
</div>Conditional elements (next sibling only):
<!-- BATI.has("feature") -->
<div>
<span>my text</span>
</div>
<span>my other text</span>Uses SquirellyJS with custom /*{ ... }*/ tags:
/*{ @if (it.BATI.has("feature")) }*/
@import "./feature.css";
/*{ /if }*/- Unsupported JSX pattern:
{BATI.has("feature") && <div>show me</div>}is NOT supported - Unused imports are automatically removed after compilation
- Code is automatically formatted with prettier after compilation
- Empty files are not deployed; if an empty file overrides another file, the original is deleted
On Pull Requests:
- Checks workflow (
checks.yml): Runs on Node 20 & 22pnpm install→pnpm run build→pnpm run check-types→pnpm run lint
- Tests workflow (
tests-entry.yml): Matrix of E2E tests across OS/features
To replicate CI locally:
pnpm install && pnpm run build && pnpm run check-types && pnpm run lint && pnpm run test| Issue | Solution |
|---|---|
| Build fails with missing deps | Run pnpm install first |
| Type errors after changes | Run pnpm run build to regenerate dist files |
| Lint errors | Run pnpm run check to auto-fix (includes format) |
| Stale turbo cache | Use pnpm run build:force |
| Full reset needed | Run pnpm run reset (cleans, reinstalls, rebuilds) |
- Formatter/Linter: Biome (not ESLint/Prettier for this repo)
- Module System: ES Modules (
"type": "module") - TypeScript: Strict mode, NodeNext resolution
- Line Endings: LF only (Unix-style)
# Build and run CLI to generate test app
pnpm run cli # Creates /tmp/bati-app with default options
# Or manually after build:
node packages/cli/dist/index.js --react --hono /tmp/my-appThese instructions have been validated. Only search the codebase if information appears incorrect or incomplete.