Build and dev/The project generator

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.

Template order
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:

SituationResult
Nothing exists yetThe file is written once, untouched
The base created itThe file is merged, not overwritten
A later template already wrote itThe 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:

FileStrategy
package.jsonDependencies merge deeply; later templates can override individual version ranges but never drop an existing one
pyproject.tomlDependency and tool sections merge per key; the same key from a later template wins
.env.exampleKeys are unioned; an existing key keeps its first value
Everything elseLast 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.

Deep merge for package.json
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:

Generated from selections
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:

create-bionicjs-app --auth better-auth --database sqlite --toolkit drizzle --ai llm/anthropic --jobs celery
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.toml without 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.
  1. Pick capabilities - the interactive prompt or the flags.
  2. Templates resolve onto the base in declaration order.
  3. Conflicts merge by file type (or last writer wins).
  4. bionicjs.config.ts is generated from the same selections.