The project generator
How templates compose into a project, and how their files merge.
The scaffolder
create-bionicjs-app does not write a fixed project. It composes one from templates: a base template plus one template per capability you selected. The output is a directory tree that only contains the folders you asked for.
1const templates = [BASE_TEMPLATE, ...options.templates];
2
3for (const template of templates) {
4 const templateDir = path.join(TEMPLATES_DIR, template);
5 await applyTemplate(templateDir, destDir, appName, report);
6}
The base template always runs first. It owns the app skeleton - app/, bionicjs.config.ts, the style entry - and every additional template layers on top of it.
File resolution
For every path a template wants to write, the generator checks what already exists on disk before deciding what to do:
| Situation | Result |
|---|---|
| Nothing exists yet | The file is written once, untouched |
| The base created it | The file is merged, not overwritten |
| A later template already wrote it | The later template wins |
Files that no earlier template claimed are only ever written once, by whichever template claims them first. Because templates are applied in declaration order, the earlier one wins - so the scaffolder is deterministic: the same selections always produce the same project.
--force is the only escape hatch. Without it, the generator refuses to write into a non-empty directory rather than silently mixing a new project into an old one.The merge rule
When a template writes a path the base already created, the generator merges the two sources instead of overwriting. What “merge” means depends on the file type:
| File | Strategy |
|---|---|
package.json | Dependencies merge deeply; later templates can override individual version ranges but never drop an existing one |
pyproject.toml | Dependency and tool sections merge per key; the same key from a later template wins |
.env.example | Keys are unioned; an existing key keeps its first value |
| Everything else | Last writer wins |
Everything outside those three files is replaced by whichever template writes it last. That covers app routes, components, and configuration files where two versions cannot be meaningfully combined.
1function mergePackageJson(target, source) {
2 const result = { ...target };
3 for (const [key, value] of Object.entries(source)) {
4 result[key] =
5 value !== null && typeof value === "object" && !Array.isArray(value)
6 ? mergePackageJson(result[key] ?? {}, value)
7 : value;
8 }
9 return result;
10}
Config generation
The scaffolder does not just copy files - afterwards it synthesizes bionicjs.config.ts from your selections. Only the plugins you chose are imported, and each one contributes its own block:
1import { defineConfig } from "@bionicjs/core";
2import { betterAuth } from "@bionicjs/better-auth";
3import { drizzle } from "@bionicjs/drizzle";
4
5export default defineConfig({
6 auth: betterAuth({ emailAndPassword: { enabled: true } }),
7
8 database: drizzle({
9 provider: "sqlite",
10 url: process.env.DATABASE_URL!,
11 }),
12});
AI capabilities work the same way, with one distinction: the config records the provider (and high-level settings such as RAG or agents), while the implementation folders - server/ai/agents, server/ai/rag, and so on - are scaffolded as real code stubs you edit.
The report
Generation is strictly ordered, so a full selection always composes the same way - and the scaffolder reports exactly what it did:
1templates applied, in order:
2 base
3 + auth/better-auth
4 + database/drizzle/sqlite
5 + ai/llm/anthropic
6 + jobs/celery
7 + jobs/brokers/redis
8
9Created my-app/ with 6 template(s).
1042 files written, 3 files merged.
Why this design
- The base template stays definitive for structure, while feature templates layer onto it.
- A job template can update
pyproject.tomlwithout clobbering a database plugin's dependency. - A route conflict resolves by explicit order, not by accident.
- Every capability is a plugin composed in one file, so the project reads as a list of decisions rather than a pile of merged diffs.
- Pick capabilities - the interactive prompt or the flags.
- Templates resolve onto the base in declaration order.
- Conflicts merge by file type (or last writer wins).
bionicjs.config.tsis generated from the same selections.
08 / 25