Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
4429d29
feat: docker-compose support
magne4000 May 19, 2026
5c96ce2
Initial plan: add YAML comment-based compilation support
Copilot May 19, 2026
7125bbd
feat: add YAML comment-based BATI compilation support
Copilot May 19, 2026
d896912
chore
magne4000 May 19, 2026
f01cf61
fix
magne4000 May 19, 2026
5a4e8b6
chore
magne4000 May 19, 2026
3ba0103
chore
magne4000 May 19, 2026
1027011
chore: use ni package
magne4000 May 19, 2026
9659a85
chore
magne4000 May 19, 2026
a5e92bc
feat: convert Dockerfile to dynamic $Dockerfile.ts with packageManage…
Copilot May 19, 2026
c643766
fix: use bun.lockb instead of bun.lock
Copilot May 19, 2026
c1e8184
chore: dokploy does not require a server
magne4000 May 19, 2026
2db4836
chore
magne4000 May 19, 2026
16faa33
chore
magne4000 May 19, 2026
0e1edaa
chore
magne4000 May 19, 2026
19ed7f8
fix: sqlite
magne4000 May 19, 2026
d94a1ee
chore
magne4000 May 19, 2026
7e7ba8d
feat: restructure e2e tests - merge dokploy into SERVER+AUTH/DATA, ad…
Copilot May 19, 2026
a2513df
chore
magne4000 May 19, 2026
2187c4a
refactor
magne4000 May 19, 2026
2edd9c0
chore: try bun --bun in CI
magne4000 May 19, 2026
c754451
chore
magne4000 May 19, 2026
ad2ea35
chore
magne4000 May 19, 2026
a39b579
chore: revert bun --bun
magne4000 May 19, 2026
e2e585e
fix: bun detection
magne4000 May 19, 2026
df58320
chore
magne4000 May 19, 2026
cb6dff5
chore
magne4000 May 19, 2026
9378f7d
feat: generate and copy batijs-tests-utils tgz for docker e2e tests, …
Copilot May 19, 2026
c4a561d
feat: dockerfile builder
magne4000 May 20, 2026
f8a0c76
dockerfile builder
magne4000 May 20, 2026
2f3e45d
refactor
magne4000 May 20, 2026
23f20d1
refactor
magne4000 May 20, 2026
1e4a507
refactor
magne4000 May 20, 2026
ef6fc21
fix
magne4000 May 20, 2026
344563a
fix
magne4000 May 20, 2026
48b5481
fix
magne4000 May 20, 2026
4675d37
lint
magne4000 May 20, 2026
d90a6a5
chore
magne4000 May 20, 2026
92a3801
chore
magne4000 May 20, 2026
b78e48a
knip
magne4000 May 20, 2026
c6a2737
chore
magne4000 May 20, 2026
a34c234
chore
magne4000 May 20, 2026
32439c3
fix: add sqlite3 compilation fix with bun
magne4000 May 21, 2026
f6972ff
fix: remove drizzle specifics in docker
magne4000 May 21, 2026
ae84bb9
fix: drizzle db migration at runtime
magne4000 May 21, 2026
6e9a527
fix: drizzle and kysely migration scripts
magne4000 May 21, 2026
e3d70fb
fix: bun + sqlite
magne4000 May 21, 2026
6cfd64e
fixes
magne4000 May 26, 2026
8765efe
lint
magne4000 May 26, 2026
a5ed478
Merge branch 'main' into magne4000/dev
magne4000 May 26, 2026
2047511
refactor: e2e tests
magne4000 May 26, 2026
21a01db
chore: regen
magne4000 May 26, 2026
bde3dd5
chore: remove comments
magne4000 May 26, 2026
760f9ba
refactor
magne4000 May 26, 2026
bd8d066
fix some devDeps
magne4000 May 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 31 additions & 10 deletions .github/workflows/reusable.run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,35 +86,53 @@ jobs:
# does not interpret backslashes in Windows paths as escape sequences.
run: bun "$GITHUB_WORKSPACE/bati-cli/dist/index.js" --knip ${{ inputs.flags }} "$RUNNER_TEMP/${{ inputs.destination }}"

# Every matrix keeps the generated app pristine and hosts all e2e
# artefacts in a sibling `<destination>.e2e/` workspace. TEST_DIR is
# where Link tests-utils, Link tests files, Prepare Bati tests, lint
# and test all run.
- name: Resolve test working dir
shell: bash
run: |
mkdir -p "$RUNNER_TEMP/${{ inputs.destination }}.e2e"
echo "TEST_DIR=$RUNNER_TEMP/${{ inputs.destination }}.e2e" >> "$GITHUB_ENV"

- name: Link tests-utils
shell: bash
run: cp -s "$GITHUB_WORKSPACE/bati-tests-utils/"* .
working-directory: ${{ runner.temp }}/${{ inputs.destination }}
working-directory: ${{ env.TEST_DIR }}

- name: Link tests files
shell: bash
run: cp -s "$GITHUB_WORKSPACE/bati-tests-files/"* .
working-directory: ${{ runner.temp }}/${{ inputs.destination }}
working-directory: ${{ env.TEST_DIR }}

- name: Prepare Bati tests
shell: bash
run:
bun "$GITHUB_WORKSPACE/bati-tests/prepare.js" --test-files='${{ inputs.test-files }}' ${{
inputs.flags }}
working-directory: ${{ runner.temp }}/${{ inputs.destination }}
working-directory: ${{ env.TEST_DIR }}

- name: Install dependencies
- name: Install dependencies (app)
shell: bash
run: bun install
working-directory: ${{ runner.temp }}/${{ inputs.destination }}

- name: Install dependencies (e2e workspace)
shell: bash
run: bun install
working-directory: ${{ env.TEST_DIR }}

- name: Generate types
if: contains(inputs.flags, '--cloudflare')
shell: bash
run: bun run generate-types
working-directory: ${{ runner.temp }}/${{ inputs.destination }}

# For dokploy the build happens inside `docker compose up --build` during
# the test step; a separate local build would duplicate the work.
- name: Run build
if: "!contains(inputs.flags, '--dokploy')"
shell: bash
run: bun run build
working-directory: ${{ runner.temp }}/${{ inputs.destination }}
Expand All @@ -123,32 +141,35 @@ jobs:
shell: bash
# Bun hangs on Windows
run: ${{ runner.os == 'Windows' && 'npm run test' || 'bun run test' }}
working-directory: ${{ runner.temp }}/${{ inputs.destination }}
working-directory: ${{ env.TEST_DIR }}

# Lint / typecheck / knip scripts live on the .e2e/ workspace and proxy
# to the sibling app via `cd ../<app>`. They run for every matrix that
# selected the corresponding flag — including --dokploy.
- name: Run lint:eslint
if: contains(inputs.flags, '--eslint')
shell: bash
run: bun run lint:eslint
working-directory: ${{ runner.temp }}/${{ inputs.destination }}
working-directory: ${{ env.TEST_DIR }}

- name: Run lint:biome
if: contains(inputs.flags, '--biome')
shell: bash
run: bun run lint:biome
working-directory: ${{ runner.temp }}/${{ inputs.destination }}
working-directory: ${{ env.TEST_DIR }}

- name: Run lint:oxlint
if: contains(inputs.flags, '--oxlint')
shell: bash
run: bun run lint:oxlint
working-directory: ${{ runner.temp }}/${{ inputs.destination }}
working-directory: ${{ env.TEST_DIR }}

- name: Run typecheck
shell: bash
run: bun run typecheck
working-directory: ${{ runner.temp }}/${{ inputs.destination }}
working-directory: ${{ env.TEST_DIR }}

- name: Run knip
shell: bash
run: bun run knip
working-directory: ${{ runner.temp }}/${{ inputs.destination }}
working-directory: ${{ env.TEST_DIR }}
891 changes: 439 additions & 452 deletions .github/workflows/tests-entry.yml

Large diffs are not rendered by default.

54 changes: 29 additions & 25 deletions boilerplates/aws/files/$package.json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,33 @@ import { loadPackageJson, type TransformerProps } from "@batijs/core";
export default async function getPackageJson(props: TransformerProps): Promise<unknown> {
const packageJson = await loadPackageJson(props, await import("../package.json").then((x) => x.default));

return packageJson
.setScript("test", {
value: "vitest",
precedence: 0,
})
.setScript("deploy:cdk-deploy-all", {
value: "cdk deploy --all",
precedence: 0,
})
.setScript("deploy:aws", {
value: "run-s build deploy:cdk-deploy-all",
precedence: 0,
})
.setScript("cdk:app", {
value: "tsx cdk/bin/infrastructure.ts",
precedence: 0,
})
.setScript("cdk", {
value: "cdk",
precedence: 0,
})
.addDependencies(["aws-cdk-lib", "constructs", "source-map-support"])
.addDevDependencies(["cdk", "aws-cdk", "@types/node", "@types/which", "typescript", "esbuild", "vitest", "which"])
.addDevDependencies(["npm-run-all2"], ["deploy:aws"])
.addDevDependencies(["tsx"], ["cdk:app"]);
return (
packageJson
.setScript("test", {
value: "vitest",
precedence: 0,
})
.setScript("deploy:cdk-deploy-all", {
value: "cdk deploy --all",
precedence: 0,
})
.setScript("deploy:aws", {
value: "run-s build deploy:cdk-deploy-all",
precedence: 0,
})
.setScript("cdk:app", {
value: "tsx cdk/bin/infrastructure.ts",
precedence: 0,
})
.setScript("cdk", {
value: "cdk",
precedence: 0,
})
// CDK (infra-as-code) and source-map-support are only loaded by the `cdk/` deploy
// scripts — never by the app at runtime, and CDK esbuild-bundles the lambda anyway.
.addDevDependencies(["aws-cdk-lib", "constructs", "source-map-support"])
.addDevDependencies(["cdk", "aws-cdk", "@types/node", "@types/which", "typescript", "esbuild", "vitest", "which"])
.addDevDependencies(["npm-run-all2"], ["deploy:aws"])
.addDevDependencies(["tsx"], ["cdk:app"])
);
}
8 changes: 8 additions & 0 deletions boilerplates/docker-compose/bati.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from "@batijs/core/config";

export default defineConfig({
if(meta) {
return meta.BATI.has("dokploy");
},
enforce: "post",
});
20 changes: 20 additions & 0 deletions boilerplates/docker-compose/files/$.dockerignore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { TransformerProps } from "@batijs/core";

export default async function getDockerignore(props: TransformerProps): Promise<string> {
const lines = ["node_modules", ".git", "dist", ".env", ".env.*", "!.env.example", "*.log", ".DS_Store"];

// The e2e plumbing lives in the sibling `<app>.e2e/` workspace and must never reach
// the image — but only the test suite ever scaffolds those files. A normal user has
// none of them, so excluding them outside of tests would just be dead, confusing noise.
if (props.meta.BATI_TEST) {
lines.push(
"# e2e plumbing (lives in the sibling `<app>.e2e/` workspace, never the image)",
"batijs-tests-utils-*.tgz",
"bati.config.json",
"vitest.config.ts",
"*.spec.ts",
);
}

return `${lines.join("\n")}\n`;
}
100 changes: 100 additions & 0 deletions boilerplates/docker-compose/files/$Dockerfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { dockerfile, dockerPackageManager, packageManager, type TransformerProps } from "@batijs/core";

export default async function getDockerfile(props: TransformerProps): Promise<string> {
const { meta } = props;
let pm = packageManager();

// better-sqlite3 has no Bun prebuild yet (oven-sh/bun#4290) — build it with node.
const deps = Object.keys({ ...props.packageJson.dependencies, ...props.packageJson.devDependencies });
if (pm.name === "bun" && deps.includes("better-sqlite3")) {
pm = { name: "npm", run: "npm run", exec: "npx" };
}

const nodeCli = pm.name === "bun" ? "bun" : "node";
const run = pm.run;
// e2e apps carry no per-app lockfile (only a workspace one Docker can't see), so
// they install loosely; real users install against their committed lockfile.
const config = dockerPackageManager(pm.name, { frozenLockfile: !meta.BATI_TEST });

// Commands run at container startup, before the server, when a database needs
// migrating — plus the source files each migration script must find in the runner.
const startupMigrations: string[] = [];
const migrationCopies: { sources: string[]; dest: string; from: string }[] = [];
if (meta.BATI.has("sqlite")) {
startupMigrations.push(`${run} sqlite:migrate`);
migrationCopies.push({ sources: ["/app/database/sqlite"], dest: "./database/sqlite", from: "builder" });
}
if (meta.BATI.has("drizzle")) {
startupMigrations.push(`${run} drizzle:migrate`);
migrationCopies.push({ sources: ["/app/database/migrations"], dest: "./database/migrations", from: "deps-dev" });
migrationCopies.push({ sources: ["/app/drizzle.config.ts"], dest: "./drizzle.config.ts", from: "builder" });
}
if (meta.BATI.has("kysely")) {
startupMigrations.push(`${nodeCli} ./dist/server/migrate.mjs`);
migrationCopies.push({
sources: ["/app/database/kysely/migrations"],
dest: "./dist/server/migrations",
from: "builder",
});
}

// Run migrations before the server when present; otherwise launch it directly.
const startCmd =
startupMigrations.length > 0
? ["sh", "-c", [...startupMigrations, `${nodeCli} ./dist/server/index.mjs`].join(" && ")]
: [nodeCli, "./dist/server/index.mjs"];

// Files that participate in dependency installation.
const installSources = ["package.json", ...config.lockfiles];

const df = dockerfile()
// ── deps-dev: all dependencies (devDeps + deps) for build & migrations ───
.from(config.image, { as: "deps-dev", comment: "install all dependencies (devDeps + deps) for build & migrations" })
.workdir("/app")
.when(config.corepack, (b) => b.run("corepack enable"))
.copy(["."], ".")
.run(config.install)
.when(meta.BATI.has("drizzle"), (b) =>
b.env({ DATABASE_URL: "/app/database/sqlite.db" }).run(`${run} drizzle:generate`),
)

// ── deps-prod: production dependencies only ──────────────────────────────
.from(config.image, { as: "deps-prod", comment: "install production-only dependencies for the runtime image" })
.workdir("/app")
.when(config.corepack, (b) => b.run("corepack enable"))
.copy(installSources, "./")
.run(config.installProd)

// ── builder: build the application using deps-dev ────────────────────────
.from(config.image, { as: "builder", comment: "build the application" })
.workdir("/app")
.when(config.corepack, (b) => b.run("corepack enable"))
.copy(["/app/node_modules"], "./node_modules", { from: "deps-dev" })
.copy(["."], ".")
.run(`${run} build`)

// ── runner: production runtime image ─────────────────────────────────────
.from(config.image, { as: "runner", comment: "production runtime image" })
.workdir("/app")
.env({ NODE_ENV: "production", PORT: "3000" })
// Runtime env mirrors docker-compose.yml: every var compose injects gets a default
// here so the image runs on its own. Secrets stay empty — compose overrides them.
.when(!meta.BATI.hasD1 && meta.BATI.hasDatabase, (b) =>
b.env({ DATABASE_URL: "/app/database/sqlite.db" }, { comment: "non-D1 database" }),
)
.when(meta.BATI.has("auth0"), (b) =>
b.env({ AUTH0_CLIENT_ID: "", AUTH0_CLIENT_SECRET: "", AUTH0_ISSUER_BASE_URL: "" }, { comment: "auth0" }),
)
.when(meta.BATI.has("sentry"), (b) => b.env({ SENTRY_DSN: "" }, { comment: "sentry" }))
.when(config.corepack, (b) => b.run("corepack enable"))
.copy(installSources, "./")
.copy(["/app/node_modules"], "./node_modules", { from: "deps-prod" })
.copy(["/app/dist"], "./dist", { from: "builder" })
.pipe((b) => {
for (const { sources, dest, from } of migrationCopies) b.copy(sources, dest, { from });
})
.expose(3000)
.cmd(startCmd);

return `${df.build()}\n`;
}
28 changes: 28 additions & 0 deletions boilerplates/docker-compose/files/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "${PORT:-3000}:3000"
environment:
- NODE_ENV=production
- PORT=3000
# !BATI.hasD1 && BATI.hasDatabase
- DATABASE_URL=${DATABASE_URL:-/app/data/db.sqlite}
# BATI.has("auth0")
- AUTH0_CLIENT_ID=${AUTH0_CLIENT_ID}
# BATI.has("auth0")
- AUTH0_CLIENT_SECRET=${AUTH0_CLIENT_SECRET}
# BATI.has("auth0")
- AUTH0_ISSUER_BASE_URL=${AUTH0_ISSUER_BASE_URL}
# BATI.has("sentry")
- SENTRY_DSN=${SENTRY_DSN}
# BATI.hasDatabase
volumes:
- sqlite_data:/app/data
restart: unless-stopped

# BATI.hasDatabase
volumes:
sqlite_data:
27 changes: 27 additions & 0 deletions boilerplates/docker-compose/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@batijs/docker-compose",
"private": true,
"version": "0.0.1",
"description": "",
"type": "module",
"scripts": {
"check-types": "tsc --noEmit",
"build": "bati-compile-boilerplate"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@batijs/compile": "workspace:*",
"@batijs/core": "workspace:",
"@types/node": "^20.19.37"
},
"nx": {
"tags": [
"type:boilerplate"
]
},
"files": [
"dist/"
]
}
3 changes: 3 additions & 0 deletions boilerplates/docker-compose/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["../tsconfig.base.json"]
}
16 changes: 16 additions & 0 deletions boilerplates/dokploy/bati.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineConfig } from "@batijs/core/config";

export default defineConfig({
if(meta) {
return meta.BATI.has("dokploy");
},
nextSteps(_meta, _packageManager, { bold }) {
return [
{
type: "text",
step: `${bold("dokploy")}: Check ${bold("TODO.md")} for remaining steps.`,
},
];
},
enforce: "post",
});
29 changes: 29 additions & 0 deletions boilerplates/dokploy/files/$TODO.md.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { loadMarkdown, type TransformerProps } from "@batijs/core";

export default async function getTodo(props: TransformerProps): Promise<unknown> {
const content = await loadMarkdown(props);

//language=Markdown
const todo = `
## Dokploy

1. **Push your repository** to a Git provider (GitHub, GitLab, Bitbucket, etc.)

2. **Install Dokploy** on your VPS if you haven't already:
\`\`\`sh
curl -sSL https://dokploy.com/install.sh | sudo sh
\`\`\`

3. **Open Dokploy** at \`http://<your-server-ip>:3000\` and complete the initial setup.

4. **Create a new application** in the Dokploy dashboard and connect it to your repository.

5. **Deploy** — Dokploy will use the \`docker-compose.yml\` at the root of your repository to build and start your application.

See also: https://docs.dokploy.com
`;

content.addMarkdownFeature(todo, "dokploy");

return content;
}
Loading
Loading