Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions boilerplates/auth0/bati.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,29 @@ export default defineConfig({
if(meta) {
return meta.BATI.has("auth0");
},
env: () => [
{
key: "AUTH0_CLIENT_ID",
scope: "secret",
comment: "Auth0 Client ID",
devValueFrom: "TEST_AUTH0_CLIENT_ID",
group: "auth0",
},
{
key: "AUTH0_CLIENT_SECRET",
scope: "secret",
comment: "Auth0 Client Secret",
devValueFrom: "TEST_AUTH0_CLIENT_SECRET",
group: "auth0",
},
{
key: "AUTH0_ISSUER_BASE_URL",
scope: "secret",
comment: "Auth0 base URL",
devValueFrom: "TEST_AUTH0_ISSUER_BASE_URL",
group: "auth0",
},
],
nextSteps(_meta, _packageManager, { bold }) {
return [
{
Expand Down
17 changes: 0 additions & 17 deletions boilerplates/auth0/files/$.env.ts

This file was deleted.

20 changes: 0 additions & 20 deletions boilerplates/auth0/files/$wrangler.jsonc.ts

This file was deleted.

45 changes: 45 additions & 0 deletions boilerplates/cloudflare/env.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { EnvRegistry } from "@batijs/core";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { wranglerEnv } from "./env";

const registry: EnvRegistry = [
{ key: "AUTH0_CLIENT_ID", scope: "secret", devValueFrom: "TEST_AUTH0_CLIENT_ID", group: "auth0" },
{ key: "SENTRY_DSN", scope: "secret", devValueFrom: "TEST_SENTRY_DSN", group: "sentry" },
{ key: "API_BASE", scope: "server-default", default: "https://api.example.com" },
{ key: "PUBLIC_ENV__SENTRY_DSN", scope: "public", default: "" },
];

// May be set in the ambient environment (e.g. .env.test); isolate each test so
// the "empty secret" assertions are deterministic.
const TEST_VARS = ["TEST_AUTH0_CLIENT_ID", "TEST_SENTRY_DSN"] as const;
let savedEnv: Record<string, string | undefined>;

beforeEach(() => {
savedEnv = {};
for (const k of TEST_VARS) {
savedEnv[k] = process.env[k];
delete process.env[k];
}
});

afterEach(() => {
for (const k of TEST_VARS) {
if (savedEnv[k] === undefined) delete process.env[k];
else process.env[k] = savedEnv[k];
}
});

describe("wranglerEnv", () => {
test("emits server-runtime vars with empty secrets, skips public", () => {
expect(wranglerEnv(registry)).toEqual({
AUTH0_CLIENT_ID: "",
SENTRY_DSN: "",
API_BASE: "https://api.example.com",
});
});

test("injects a secret's dev/test value when its source env var is set", () => {
process.env.TEST_SENTRY_DSN = "https://dsn";
expect(wranglerEnv(registry).SENTRY_DSN).toBe("https://dsn");
});
});
9 changes: 9 additions & 0 deletions boilerplates/cloudflare/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { committedValue, type EnvRecord, type EnvRegistry, isServerVar, secretDevValue } from "@batijs/core";

export function wranglerEnv(registry: EnvRegistry): EnvRecord {
const vars: EnvRecord = {};
for (const def of registry.filter(isServerVar)) {
vars[def.key] = def.scope === "secret" ? secretDevValue(def) : committedValue(def, "wrangler");
}
return vars;
}
11 changes: 11 additions & 0 deletions boilerplates/cloudflare/files/$wrangler.jsonc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { loadAsJson, type TransformerProps } from "@batijs/core";
import { wranglerEnv } from "../env";

export default async function getWrangler(props: TransformerProps): Promise<unknown> {
const wrangler = await loadAsJson(props);

// Inject environment variables
const vars = wranglerEnv(props.env);
if (Object.keys(vars).length > 0) wrangler.vars = { ...wrangler.vars, ...vars };
return wrangler;
}
2 changes: 2 additions & 0 deletions boilerplates/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"type": "module",
"scripts": {
"check-types": "tsc --noEmit",
"test": "vitest run",
"build": "bati-compile-boilerplate"
},
"keywords": [],
Expand All @@ -16,6 +17,7 @@
"@batijs/core": "workspace:",
"@cloudflare/vite-plugin": "^1.38.0",
"@types/node": "^20.19.37",
"vitest": "^4.1.7",
"wrangler": "^4.94.0"
},
"files": [
Expand Down
5 changes: 4 additions & 1 deletion boilerplates/cloudflare/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"extends": ["../tsconfig.base.json"]
"extends": ["../tsconfig.base.json"],
"compilerOptions": {
"types": ["node"]
}
}
37 changes: 37 additions & 0 deletions boilerplates/docker-compose/env.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/** biome-ignore-all lint/suspicious/noTemplateCurlyInString: valid */
import type { EnvRegistry } from "@batijs/core";
import { describe, expect, test } from "vitest";
import { composeEnvEntries, serverEnvDefaults } from "./env";

const registry: EnvRegistry = [
{
key: "DATABASE_URL",
scope: "server-default",
default: "sqlite.db",
perSink: { compose: "/app/data/db.sqlite", dockerfile: "/app/database/sqlite.db" },
group: "non-D1 database",
},
{ key: "AUTH0_CLIENT_ID", scope: "secret", group: "auth0" },
{ key: "SENTRY_DSN", scope: "secret", group: "sentry" },
{ key: "PUBLIC_ENV__SENTRY_DSN", scope: "public", default: "" },
];

describe("composeEnvEntries", () => {
test("secrets pull from host, defaulted vars are host-overridable, public is omitted", () => {
expect(composeEnvEntries(registry)).toEqual([
"DATABASE_URL=${DATABASE_URL:-/app/data/db.sqlite}",
"AUTH0_CLIENT_ID=${AUTH0_CLIENT_ID}",
"SENTRY_DSN=${SENTRY_DSN}",
]);
});
});

describe("serverEnvDefaults", () => {
test("groups by feature, defaults secrets empty, skips public", () => {
expect(serverEnvDefaults(registry)).toEqual([
{ comment: "non-D1 database", vars: { DATABASE_URL: "/app/database/sqlite.db" } },
{ comment: "auth0", vars: { AUTH0_CLIENT_ID: "" } },
{ comment: "sentry", vars: { SENTRY_DSN: "" } },
]);
});
});
41 changes: 41 additions & 0 deletions boilerplates/docker-compose/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { committedValue, type EnvRecord, type EnvRegistry, isServerVar } from "@batijs/core";

/**
* `KEY=<expr>` lines for the docker-compose `services.<app>.environment` list:
* secrets are pulled from the host (`${KEY}`), defaulted vars are host-overridable
* (`${KEY:-<default>}`).
*/
export function composeEnvEntries(registry: EnvRegistry): string[] {
return registry
.filter(isServerVar)
.map((def) =>
def.scope === "secret"
? `${def.key}=\${${def.key}}`
: `${def.key}=\${${def.key}:-${committedValue(def, "compose")}}`,
);
}

/** A group of Dockerfile `ENV` defaults sharing a comment. */
export interface DockerfileEnvGroup {
comment?: string;
vars: EnvRecord;
}

/**
* Dockerfile `ENV` defaults grouped by `group` (first-seen order), one
* `.env(vars, { comment })` per group. Secrets default to empty — compose
* overrides them at runtime.
*/
export function serverEnvDefaults(registry: EnvRegistry): DockerfileEnvGroup[] {
const groups = new Map<string, DockerfileEnvGroup>();
for (const def of registry.filter(isServerVar)) {
const groupKey = def.group ?? "";
let group = groups.get(groupKey);
if (!group) {
group = { comment: def.group, vars: {} };
groups.set(groupKey, group);
}
group.vars[def.key] = def.scope === "secret" ? "" : committedValue(def, "dockerfile");
}
return [...groups.values()];
}
16 changes: 7 additions & 9 deletions boilerplates/docker-compose/files/$Dockerfile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { dockerfile, dockerPackageManager, packageManager, type TransformerProps } from "@batijs/core";
import { serverEnvDefaults } from "../env";

export default async function getDockerfile(props: TransformerProps): Promise<string> {
const { meta } = props;
Expand Down Expand Up @@ -77,15 +78,12 @@ export default async function getDockerfile(props: TransformerProps): Promise<st
.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" }))
// Add environment variables from the env registry
.pipe((b) => {
for (const group of serverEnvDefaults(props.env)) {
b.env(group.vars, { comment: group.comment });
}
})
.when(config.corepack, (b) => b.run("corepack enable"))
.copy(installSources, "./")
.copy(["/app/node_modules"], "./node_modules", { from: "deps-prod" })
Expand Down
10 changes: 10 additions & 0 deletions boilerplates/docker-compose/files/$docker-compose.yml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { setComposeEnvironment, type TransformerProps } from "@batijs/core";
import { composeEnvEntries } from "../env";

export default async function getDockerCompose(props: TransformerProps): Promise<string> {
// biome-ignore lint/style/noNonNullAssertion: docker-compose.yml is always copied first
const code = await props.readfile!();

// Inject env vars
return setComposeEnvironment(code, composeEnvEntries(props.env));
}
10 changes: 0 additions & 10 deletions boilerplates/docker-compose/files/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,6 @@ services:
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
Expand Down
4 changes: 3 additions & 1 deletion boilerplates/docker-compose/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"type": "module",
"scripts": {
"check-types": "tsc --noEmit",
"test": "vitest run",
"build": "bati-compile-boilerplate"
},
"keywords": [],
Expand All @@ -14,7 +15,8 @@
"devDependencies": {
"@batijs/compile": "workspace:*",
"@batijs/core": "workspace:",
"@types/node": "^20.19.37"
"@types/node": "^20.19.37",
"vitest": "^4.1.7"
},
"nx": {
"tags": [
Expand Down
8 changes: 0 additions & 8 deletions boilerplates/drizzle/files/$.env.ts

This file was deleted.

10 changes: 10 additions & 0 deletions boilerplates/google-analytics/bati.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,14 @@ export default defineConfig({
if(meta) {
return meta.BATI.has("google-analytics");
},
env: () => [
{
key: "PUBLIC_ENV__GOOGLE_ANALYTICS",
scope: "public",
default: "G-XXXXXXXXXX",
comment: `Google Analytics

See the documentation https://support.google.com/analytics/answer/9304153?hl=en#zippy=%2Cweb`,
},
],
});
14 changes: 0 additions & 14 deletions boilerplates/google-analytics/files/$.env.ts

This file was deleted.

8 changes: 0 additions & 8 deletions boilerplates/kysely/files/$.env.ts

This file was deleted.

14 changes: 14 additions & 0 deletions boilerplates/prisma/bati.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ export default defineConfig({
if(meta) {
return meta.BATI.has("prisma");
},
env: () => [
{
key: "DATABASE_URL",
scope: "server-default",
default: "postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public",
comment: `Prisma

Environment variables declared in this file are automatically made available to Prisma.
See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema

Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
See the documentation for all the connection string options: https://pris.ly/d/connection-strings`,
},
],
nextSteps(_meta, packageManager) {
return [
{
Expand Down
Loading
Loading