diff --git a/.env.sample b/.env.sample index 03a8b48..ca0be25 100644 --- a/.env.sample +++ b/.env.sample @@ -1,20 +1,27 @@ +NODE_ENV=development + +# Next.js NEXT_PUBLIC_SITE_URL=http://localhost:3000 -POSTGRES_USER=dev -POSTGRES_PASSWORD=1234 -POSTGRES_DB=dev -POSTGRES_HOST=localhost -POSTGRES_PORT=5432 -POSTGRES_SCHEMA=public -POSTGRES_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}?schema=${POSTGRES_SCHEMA} +# Database +DATABASE_USER=dev +DATABASE_PASSWORD=1234 +DATABASE_DB=dev +DATABASE_HOST=localhost +DATABASE_PORT=5432 +DATABASE_SCHEMA=public +# for prisma migration +DATABASE_URL=postgresql://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_DB}?schema=${DATABASE_SCHEMA} +# Google OAuth # https://console.cloud.google.com/apis/credentials # Set values below # AUTHORIZED JAVASCRIPT ORIGINS: http://localhost:3000 # AUTHORIZED REDIRECT URIS: http://localhost:3000/api/auth/callback/google -GOOGLE_CLIENT_ID= -GOOGLE_CLIENT_SECRET= +GOOGLE_CLIENT_ID=xxxx +GOOGLE_CLIENT_SECRET=xxxx +# NextAuth.js NEXTAUTH_URL=${NEXT_PUBLIC_SITE_URL} # https://next-auth.js.org/configuration/options#secret # you must generate a new secret @@ -22,4 +29,5 @@ NEXTAUTH_URL=${NEXT_PUBLIC_SITE_URL} # $ openssl rand -base64 32 NEXTAUTH_SECRET=TKDdLVjf7cTyTs5oWVpv04senu6fia4RGQbYHRQIR5Q= +# OpenTelemetry TRACE_EXPORTER_URL= diff --git a/.env.test b/.env.test index 0542cb8..1713130 100644 --- a/.env.test +++ b/.env.test @@ -1,20 +1,27 @@ +NODE_ENV=test + +# Next.js NEXT_PUBLIC_SITE_URL=http://localhost:3000 -POSTGRES_USER=test -POSTGRES_PASSWORD=1234 -POSTGRES_DB=test -POSTGRES_HOST=localhost -POSTGRES_PORT=5432 -POSTGRES_SCHEMA=public -POSTGRES_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}?schema=${POSTGRES_SCHEMA} +# Database +DATABASE_USER=test +DATABASE_PASSWORD=1234 +DATABASE_DB=test +DATABASE_HOST=localhost +DATABASE_PORT=5432 +DATABASE_SCHEMA=public +# for prisma migration +DATABASE_URL=postgresql://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_DB}?schema=${DATABASE_SCHEMA} +# Google OAuth # https://console.cloud.google.com/apis/credentials # Set values below # AUTHORIZED JAVASCRIPT ORIGINS: http://localhost:3000 # AUTHORIZED REDIRECT URIS: http://localhost:3000/api/auth/callback/google -GOOGLE_CLIENT_ID= -GOOGLE_CLIENT_SECRET= +GOOGLE_CLIENT_ID=dummy +GOOGLE_CLIENT_SECRET=dummy +# NextAuth.js NEXTAUTH_URL=${NEXT_PUBLIC_SITE_URL} # https://next-auth.js.org/configuration/options#secret # you must generate a new secret diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28c9dbc..cb8ab92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest env: # should store them to github.secrets - POSTGRES_URL: postgresql://dev:1234@172.17.0.1:5432/dev?schema=public + DATABASE_URL: postgresql://dev:1234@172.17.0.1:5432/dev?schema=public NEXTAUTH_SECRET: UfxvOS6HetHOFkL44YTITYgc0DOuOlz5TBp3jkbnZ3w= NEXT_PUBLIC_SITE_URL: http://localhost:3000 steps: @@ -51,7 +51,7 @@ jobs: docker build \ -t app \ -f Dockerfile \ - --build-arg POSTGRES_URL=${{env.POSTGRES_URL}} \ + --build-arg DATABASE_URL=${{env.DATABASE_URL}} \ --build-arg NEXTAUTH_SECRET=${{env.NEXTAUTH_SECRET}} \ --build-arg NEXT_PUBLIC_SITE_URL=${{env.NEXT_PUBLIC_SITE_URL}} \ . diff --git a/.github/workflows/migration.yml b/.github/workflows/migration.yml index f94ada2..51b6f9b 100644 --- a/.github/workflows/migration.yml +++ b/.github/workflows/migration.yml @@ -18,4 +18,4 @@ jobs: - uses: ./.github/actions/setup-db - run: pnpm db:deploy env: - DATABASE_URL: ${{secrets.POSTGRES_URL}} + DATABASE_URL: ${{secrets.DATABASE_URL}} diff --git a/.internal/tests/common.test.mjs.snapshot b/.internal/tests/common.test.mjs.snapshot index bc8cce4..840feee 100644 --- a/.internal/tests/common.test.mjs.snapshot +++ b/.internal/tests/common.test.mjs.snapshot @@ -18,6 +18,7 @@ exports[`common > should put files 1`] = ` "internal-tests-output-common/README.md", "internal-tests-output-common/biome.json", "internal-tests-output-common/compose.yml", + "internal-tests-output-common/env.ts", "internal-tests-output-common/lefthook.yml", "internal-tests-output-common/next.config.ts", "internal-tests-output-common/otel-collector-config.yml", @@ -81,7 +82,7 @@ exports[`common > should update .github/workflows/ci.yml 1`] = ` " runs-on: ubuntu-latest", " env:", " # should store them to github.secrets", - " POSTGRES_URL: postgresql://dev:1234@172.17.0.1:5432/dev?schema=public", + " DATABASE_URL: postgresql://dev:1234@172.17.0.1:5432/dev?schema=public", " NEXTAUTH_SECRET: UfxvOS6HetHOFkL44YTITYgc0DOuOlz5TBp3jkbnZ3w=", " NEXT_PUBLIC_SITE_URL: http://localhost:3000", " steps:", @@ -92,7 +93,7 @@ exports[`common > should update .github/workflows/ci.yml 1`] = ` " docker build \\\\", " -t app \\\\", " -f Dockerfile \\\\", - " --build-arg POSTGRES_URL=\${{env.POSTGRES_URL}} \\\\", + " --build-arg DATABASE_URL=\${{env.DATABASE_URL}} \\\\", " --build-arg NEXTAUTH_SECRET=\${{env.NEXTAUTH_SECRET}} \\\\", " --build-arg NEXT_PUBLIC_SITE_URL=\${{env.NEXT_PUBLIC_SITE_URL}} \\\\", " .", @@ -230,6 +231,7 @@ exports[`common > should update README.md 1`] = ` "## Links", "", "- [Database ER diagram](/prisma/ERD.md)", + "- [Web App Template](https://hiroppy.github.io/web-app-template/)", "" ] `; @@ -243,11 +245,11 @@ exports[`common > should update compose.yml 1`] = ` " db:", " image: postgres:17", " ports:", - " - \${POSTGRES_PORT:-5432}:5432", + " - \${DATABASE_PORT:-5432}:5432", " environment:", - " - POSTGRES_USER=\${POSTGRES_USER}", - " - POSTGRES_PASSWORD=\${POSTGRES_PASSWORD}", - " - POSTGRES_DB=\${POSTGRES_DB}", + " - POSTGRES_USER=\${DATABASE_USER}", + " - POSTGRES_PASSWORD=\${DATABASE_PASSWORD}", + " - POSTGRES_DB=\${DATABASE_DB}", " jaeger:", " image: jaegertracing/all-in-one", " ports:", @@ -328,6 +330,9 @@ exports[`common > should update dependencies 1`] = ` exports[`common > should update next.config.ts 1`] = ` [ "import type { NextConfig } from \\"next\\";", + "import { config } from \\"./env\\";", + "", + "config();", "", "const nextConfig: NextConfig = {", " images: {", diff --git a/.internal/tests/no-docker.test.mjs.snapshot b/.internal/tests/no-docker.test.mjs.snapshot index 875d949..4e17f8b 100644 --- a/.internal/tests/no-docker.test.mjs.snapshot +++ b/.internal/tests/no-docker.test.mjs.snapshot @@ -17,6 +17,7 @@ exports[`no-docker > should put files 1`] = ` "internal-tests-output-no-docker/README.md", "internal-tests-output-no-docker/biome.json", "internal-tests-output-no-docker/compose.yml", + "internal-tests-output-no-docker/env.ts", "internal-tests-output-no-docker/lefthook.yml", "internal-tests-output-no-docker/next.config.ts", "internal-tests-output-no-docker/otel-collector-config.yml", diff --git a/.internal/tests/no-e2e.test.mjs.snapshot b/.internal/tests/no-e2e.test.mjs.snapshot index 3d8a4f3..bb75183 100644 --- a/.internal/tests/no-e2e.test.mjs.snapshot +++ b/.internal/tests/no-e2e.test.mjs.snapshot @@ -18,6 +18,7 @@ exports[`no-e2e > should put files 1`] = ` "internal-tests-output-no-e2e/README.md", "internal-tests-output-no-e2e/biome.json", "internal-tests-output-no-e2e/compose.yml", + "internal-tests-output-no-e2e/env.ts", "internal-tests-output-no-e2e/lefthook.yml", "internal-tests-output-no-e2e/next.config.ts", "internal-tests-output-no-e2e/otel-collector-config.yml", @@ -80,7 +81,7 @@ exports[`no-e2e > should update .github/workflows/ci.yml 1`] = ` " runs-on: ubuntu-latest", " env:", " # should store them to github.secrets", - " POSTGRES_URL: postgresql://dev:1234@172.17.0.1:5432/dev?schema=public", + " DATABASE_URL: postgresql://dev:1234@172.17.0.1:5432/dev?schema=public", " NEXTAUTH_SECRET: UfxvOS6HetHOFkL44YTITYgc0DOuOlz5TBp3jkbnZ3w=", " NEXT_PUBLIC_SITE_URL: http://localhost:3000", " steps:", @@ -91,7 +92,7 @@ exports[`no-e2e > should update .github/workflows/ci.yml 1`] = ` " docker build \\\\", " -t app \\\\", " -f Dockerfile \\\\", - " --build-arg POSTGRES_URL=\${{env.POSTGRES_URL}} \\\\", + " --build-arg DATABASE_URL=\${{env.DATABASE_URL}} \\\\", " --build-arg NEXTAUTH_SECRET=\${{env.NEXTAUTH_SECRET}} \\\\", " --build-arg NEXT_PUBLIC_SITE_URL=\${{env.NEXT_PUBLIC_SITE_URL}} \\\\", " .", @@ -198,6 +199,7 @@ exports[`no-e2e > should update README.md 1`] = ` "## Links", "", "- [Database ER diagram](/prisma/ERD.md)", + "- [Web App Template](https://hiroppy.github.io/web-app-template/)", "" ] `; diff --git a/.internal/tests/no-otel.test.mjs.snapshot b/.internal/tests/no-otel.test.mjs.snapshot index 1519b7b..b7bcf36 100644 --- a/.internal/tests/no-otel.test.mjs.snapshot +++ b/.internal/tests/no-otel.test.mjs.snapshot @@ -18,6 +18,7 @@ exports[`no-otel > should put files 1`] = ` "internal-tests-output-no-otel/README.md", "internal-tests-output-no-otel/biome.json", "internal-tests-output-no-otel/compose.yml", + "internal-tests-output-no-otel/env.ts", "internal-tests-output-no-otel/lefthook.yml", "internal-tests-output-no-otel/next.config.ts", "internal-tests-output-no-otel/package.json", @@ -102,6 +103,7 @@ exports[`no-otel > should update README.md 1`] = ` "## Links", "", "- [Database ER diagram](/prisma/ERD.md)", + "- [Web App Template](https://hiroppy.github.io/web-app-template/)", "" ] `; @@ -115,11 +117,11 @@ exports[`no-otel > should update compose.yml 1`] = ` " db:", " image: postgres:17", " ports:", - " - \${POSTGRES_PORT:-5432}:5432", + " - \${DATABASE_PORT:-5432}:5432", " environment:", - " - POSTGRES_USER=\${POSTGRES_USER}", - " - POSTGRES_PASSWORD=\${POSTGRES_PASSWORD}", - " - POSTGRES_DB=\${POSTGRES_DB}", + " - POSTGRES_USER=\${DATABASE_USER}", + " - POSTGRES_PASSWORD=\${DATABASE_PASSWORD}", + " - POSTGRES_DB=\${DATABASE_DB}", "" ] `; @@ -166,6 +168,9 @@ exports[`no-otel > should update dependencies 1`] = ` exports[`no-otel > should update next.config.ts 1`] = ` [ "import type { NextConfig } from \\"next\\";", + "import { config } from \\"./env\\";", + "", + "config();", "", "const nextConfig: NextConfig = {", " images: {", diff --git a/.vscode/settings.json b/.vscode/settings.json index eb0df30..a8857f8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,7 +22,7 @@ }, "prettier.requireConfig": false, "cSpell.enabled": true, - "cSpell.words": ["biomejs", "otel", "otlp"], + "cSpell.words": ["dotenv", "biomejs", "otel", "otlp"], "cSpell.allowCompoundWords": true, "cSpell.ignorePaths": ["**/node_modules/**", "**/vscode-extension/**"] } diff --git a/Dockerfile b/Dockerfile index a81610d..e3a362a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,14 +2,14 @@ FROM node:22.12.0-slim AS base WORKDIR /app -ARG POSTGRES_URL='' +ARG DATABASE_URL='' ARG NEXTAUTH_SECRET='' ARG NEXT_PUBLIC_SITE_URL='' ARG TRACE_EXPORTER_URL='' ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" -ENV POSTGRES_URL=$POSTGRES_URL +ENV DATABASE_URL=$DATABASE_URL ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET ENV NEXTAUTH_URL=$NEXT_PUBLIC_SITE_URL ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL diff --git a/compose.yml b/compose.yml index 9ee94a4..ff7b087 100644 --- a/compose.yml +++ b/compose.yml @@ -5,11 +5,11 @@ services: db: image: postgres:17 ports: - - ${POSTGRES_PORT:-5432}:5432 + - ${DATABASE_PORT:-5432}:5432 environment: - - POSTGRES_USER=${POSTGRES_USER} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - - POSTGRES_DB=${POSTGRES_DB} + - POSTGRES_USER=${DATABASE_USER} + - POSTGRES_PASSWORD=${DATABASE_PASSWORD} + - POSTGRES_DB=${DATABASE_DB} ####### otel ####### jaeger: image: jaegertracing/all-in-one diff --git a/e2e/globalSetup.ts b/e2e/globalSetup.ts index e53d734..ad9201d 100644 --- a/e2e/globalSetup.ts +++ b/e2e/globalSetup.ts @@ -3,7 +3,7 @@ import { setupDB } from "../tests/db.setup"; export default async function globalSetup(config: FullConfig) { const { down } = await setupDB({ - port: Number(process.env.POSTGRES_PORT), + port: Number(process.env.DATABASE_PORT), }); global.down = down; diff --git a/env.ts b/env.ts new file mode 100644 index 0000000..2252f04 --- /dev/null +++ b/env.ts @@ -0,0 +1,48 @@ +import dotenv from "dotenv"; +import { z } from "zod"; + +export type Schema = z.infer; + +export const schema = z.object({ + NODE_ENV: z + .union([ + z.literal("development"), + z.literal("test"), + z.literal("production"), + ]) + .default("development"), + + // for client and server + NEXT_PUBLIC_SITE_URL: z.string().url(), + + // for server + DATABASE_USER: z.string().min(1), + DATABASE_PASSWORD: z.string().min(1), + DATABASE_DB: z.string().min(1), + DATABASE_HOST: z.string().min(1), + DATABASE_PORT: z.coerce.number().min(1), + DATABASE_SCHEMA: z.string().min(1), + DATABASE_URL: z.string().min(1), + + GOOGLE_CLIENT_ID: z.string().min(1), + GOOGLE_CLIENT_SECRET: z.string().min(1), + + NEXTAUTH_URL: z.string().min(1), + NEXTAUTH_SECRET: z.string().min(1), + + TRACE_EXPORTER_URL: z.string().url().optional().or(z.literal("")), +}); + +export function config(file?: ".env" | ".env.test") { + if (file) { + dotenv.config({ path: file }); + } + + const res = schema.safeParse(process.env); + + if (res.error) { + console.error("\x1b[31m%s\x1b[0m", "[Errors] environment variables"); + console.error(JSON.stringify(res.error.errors, null, 2)); + process.exit(1); + } +} diff --git a/next.config.ts b/next.config.ts index f79ff85..0adcbad 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,7 @@ import type { NextConfig } from "next"; +import { config } from "./env"; + +config(); const nextConfig: NextConfig = { images: { diff --git a/playwright.config.ts b/playwright.config.ts index d8d9ec7..2a9ee2d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,7 +1,7 @@ import { defineConfig, devices } from "@playwright/test"; -import { config } from "dotenv"; +import { config } from "./env"; -config({ path: ".env.test" }); +config(".env.test"); export default defineConfig({ testDir: "./e2e", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b79827d..b28b7df 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -2,7 +2,7 @@ datasource db { provider = "postgresql" - url = env("POSTGRES_URL") + url = env("DATABASE_URL") } generator client { diff --git a/src/app/_utils/db.ts b/src/app/_utils/db.ts index a3b5154..5ddd69c 100644 --- a/src/app/_utils/db.ts +++ b/src/app/_utils/db.ts @@ -1,10 +1,10 @@ export function createDBUrl({ - user = process.env.POSTGRES_USER, - password = process.env.POSTGRES_PASSWORD, - host = process.env.POSTGRES_HOST, - port = Number(process.env.POSTGRES_PORT), - db = process.env.POSTGRES_DB, - schema = process.env.POSTGRES_SCHEMA, + user = process.env.DATABASE_USER, + password = process.env.DATABASE_PASSWORD, + host = process.env.DATABASE_HOST, + port = Number(process.env.DATABASE_PORT), + db = process.env.DATABASE_DB, + schema = process.env.DATABASE_SCHEMA, }: { user?: string; password?: string; diff --git a/src/app/globals.d.ts b/src/app/globals.d.ts index 7452613..655c1df 100644 --- a/src/app/globals.d.ts +++ b/src/app/globals.d.ts @@ -1,26 +1,8 @@ -export type {}; +import type { Schema } from "../../env"; declare global { namespace NodeJS { - interface ProcessEnv { - // public - NEXT_PUBLIC_SITE_URL: string; - - // private - POSTGRES_USER: string; - POSTGRES_PASSWORD: string; - POSTGRES_HOST: string; - POSTGRES_PORT: string; - POSTGRES_DB: string; - POSTGRES_SCHEMA: string; - - GOOGLE_CLIENT_ID: string; - GOOGLE_CLIENT_SECRET: string; - - TRACE_EXPORTER_URL?: string; - - NEXTAUTH_TEST_MODE?: string; - } + interface ProcessEnv extends Schema {} } type PartialWithNullable = { diff --git a/tests/db.setup.ts b/tests/db.setup.ts index 57c2687..ab1cdb7 100644 --- a/tests/db.setup.ts +++ b/tests/db.setup.ts @@ -9,9 +9,9 @@ const execAsync = promisify(exec); export async function setupDB({ port }: { port: "random" | number }) { const container = await new DockerComposeEnvironment(".", "compose.yml") .withEnvironmentFile(".env.test") - // overwrite config + // overwrite environment variables .withEnvironment({ - POSTGRES_PORT: port === "random" ? "0" : `${port}`, + DATABASE_PORT: port === "random" ? "0" : `${port}`, }) .withWaitStrategy("db", Wait.forListeningPorts()) .up(["db"]); @@ -22,7 +22,7 @@ export async function setupDB({ port }: { port: "random" | number }) { port: mappedPort, }); - await execAsync(`POSTGRES_URL=${url} npx prisma db push`); + await execAsync(`DATABASE_URL=${url} npx prisma db push`); const prisma = new PrismaClient({ datasources: { diff --git a/tsconfig.json b/tsconfig.json index 1457330..cb6ab6e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,12 @@ } ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "env.ts" + ], "exclude": ["node_modules"] } diff --git a/vitest.config.ts b/vitest.config.ts index ed111b0..7521722 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,8 +1,8 @@ import react from "@vitejs/plugin-react"; -import { config } from "dotenv"; import { defineConfig } from "vitest/config"; +import { config } from "./env"; -config({ path: ".env.test" }); +config(".env.test"); export default defineConfig({ plugins: [react()],