Skip to content

Commit 5f810b4

Browse files
authored
feat: Centralize configuration in one place. (#312)
* Move all (most) process.env references into config.ts. * Add zod checks for config. * Remove ts-node-dev. Doesn't seem to be needed, creates some weird situations where processes don't exist (e.g. buildProd). * Refine the buildProd script. * fix: Prettier config in bisonapp is different from prettier config in packages/create-bison-app/template. So if you save a file while editing bisonapp and then create a bison app from it, it may have lint errors. Resolved this by copying the prettier config from packages/create-bison-app/template into the root folder.
1 parent 808a48e commit 5f810b4

21 files changed

+181
-76
lines changed

.github/workflows/tests.yml

+5-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
runs-on: ${{ matrix.os }}
1313
strategy:
1414
matrix:
15-
os: ["ubuntu-latest", "windows-latest"]
15+
os: ['ubuntu-latest', 'windows-latest']
1616
node-version: [16.x, 18.x]
1717
outputs:
1818
appName: ${{ steps.create-app.outputs.appName }}
@@ -25,7 +25,7 @@ jobs:
2525
uses: actions/setup-node@v3
2626
with:
2727
node-version: ${{ matrix.node-version }}
28-
cache: "yarn"
28+
cache: 'yarn'
2929

3030
- name: Install packages
3131
run: yarn && yarn lerna bootstrap
@@ -56,7 +56,7 @@ jobs:
5656
image: postgres:15
5757
env:
5858
POSTGRES_PASSWORD: postgres
59-
ports: ["5432:5432"]
59+
ports: ['5432:5432']
6060
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
6161

6262
strategy:
@@ -85,6 +85,8 @@ jobs:
8585

8686
- name: Build production bison app
8787
run: yarn build
88+
env:
89+
DATABASE_URL: not_used_but_needs_to_be_set_to_pass_zod_config_check
8890

8991
- name: Lint bison app
9092
run: yarn lint

packages/create-bison-app/tasks/copyFiles.js

+1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ async function copyFiles({ variables, targetFolder }) {
120120
"prettier.config.js",
121121
"tsconfig.json",
122122
"tsconfig.cjs.json",
123+
"config.ts",
123124
],
124125
targetFolder,
125126
{

packages/create-bison-app/template/_.env.development.ejs

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88

99
# Include NODE_ENV for package script withEnv to work as expected
1010

11-
APP_ENV="development"
11+
NEXT_PUBLIC_APP_ENV="development"
1212
NODE_ENV="development"
1313
PORT=3000
14-
BASE_URL="http://localhost:3000"
14+
SHOULD_MIGRATE=0
1515

1616
DATABASE_URL="postgresql://<%= db.dev.user %><% if (db.dev.password) { %>:<%= db.dev.password %><% } %>@<%= db.dev.host %>:<%= db.dev.port %>/<%= db.dev.name %>?schema=public"
1717

1818
NEXTAUTH_SECRET="bisonDev"
19-
NEXTAUTH_URL="http://localhost:3000"
19+
NEXTAUTH_URL="http://localhost:3000"

packages/create-bison-app/template/_.env.development.local.ejs

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88

99
# Include NODE_ENV for package script withEnv to work as expected
1010

11-
APP_ENV="development"
11+
NEXT_PUBLIC_APP_ENV="development"
1212
NODE_ENV="development"
1313
PORT=3000
14-
BASE_URL="http://localhost:3000"
14+
SHOULD_MIGRATE=0
1515

1616
DATABASE_URL="postgresql://<%= db.dev.user %><% if (db.dev.password) { %>:<%= db.dev.password %><% } %>@<%= db.dev.host %>:<%= db.dev.port %>/<%= db.dev.name %>?schema=public"
1717

1818
NEXTAUTH_SECRET="bisonDev"
19-
NEXTAUTH_URL="http://localhost:3000"
19+
NEXTAUTH_URL="http://localhost:3000"

packages/create-bison-app/template/_.env.ejs

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88

99
# Include NODE_ENV for package script withEnv to work as expected
1010

11-
APP_ENV="production"
11+
NEXT_PUBLIC_APP_ENV="production"
1212
NODE_ENV="development" # production // change if you truly want PROD
1313
PORT=3000
14-
BASE_URL="http://localhost:3000"
14+
SHOULD_MIGRATE=0
1515

1616
DATABASE_URL="postgresql://<%= db.dev.user %><% if (db.dev.password) { %>:<%= db.dev.password %><% } %>@<%= db.dev.host %>:<%= db.dev.port %>/<%= db.dev.name %>?schema=public"
1717

1818
NEXTAUTH_SECRET="bisonProd"
19-
NEXTAUTH_URL="http://localhost:3000"
19+
NEXTAUTH_URL="http://localhost:3000"
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Overwrite envs here to mimic production
22
# Include NODE_ENV for package script withEnv to work as expected
33

4-
APP_ENV="production"
4+
NEXT_PUBLIC_APP_ENV="production"
55
NODE_ENV="development" # production // change if you truly want PROD
66
PORT=3000
7-
BASE_URL="http://localhost:3000"
7+
SHOULD_MIGRATE=0
88

99
DATABASE_URL="postgresql://<%= db.dev.user %><% if (db.dev.password) { %>:<%= db.dev.password %><% } %>@<%= db.dev.host %>:<%= db.dev.port %>/<%= db.dev.name %>?schema=public"
1010

1111
NEXTAUTH_SECRET="bisonProd"
12-
NEXTAUTH_URL="http://localhost:3000"
12+
NEXTAUTH_URL="http://localhost:3000"
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Copy to .env.test.local to override
22
# Include NODE_ENV for package script withEnv to work as expected
33

4-
APP_ENV="test"
4+
NEXT_PUBLIC_APP_ENV="test"
55
NODE_ENV="test"
66
PORT=3001
7-
BASE_URL="http://localhost:3001"
7+
SHOULD_MIGRATE=0
88

99
DATABASE_URL="postgresql://<%= db.dev.user %><% if (db.dev.password) { %>:<%= db.dev.password %><% } %>@<%= db.dev.host %>:<%= db.dev.port %>/<%= db.test.name %>?schema=public"
1010

1111
NEXTAUTH_SECRET="bisonTest"
12-
NEXTAUTH_URL="http://localhost:3001"
12+
NEXTAUTH_URL="http://localhost:3001"
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# ENV vars here override .env.test when running locally
22
# Include NODE_ENV for package script withEnv to work as expected
33

4-
APP_ENV="test"
4+
NEXT_PUBLIC_APP_ENV="test"
55
NODE_ENV="test"
66
PORT=3001
7-
BASE_URL="http://localhost:3001"
7+
SHOULD_MIGRATE=0
88

99
DATABASE_URL="postgresql://<%= db.dev.user %><% if (db.dev.password) { %>:<%= db.dev.password %><% } %>@<%= db.dev.host %>:<%= db.dev.port %>/<%= db.test.name %>?schema=public"
1010

1111
NEXTAUTH_SECRET="bisonTest"
12-
NEXTAUTH_URL="http://localhost:3001"
12+
NEXTAUTH_URL="http://localhost:3001"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { z } from 'zod';
2+
3+
import { notEmpty } from './lib/type-witchcraft';
4+
5+
const stages = ['production', 'development', 'test'] as const;
6+
7+
type Stage = (typeof stages)[number];
8+
9+
function getStage(stages: Stage[]) {
10+
if (!stages.length) return 'development';
11+
12+
for (const stage of stages) {
13+
// if any of the provided stages is not production, assume we aren't in production
14+
if (stage !== 'production') {
15+
return stage;
16+
}
17+
}
18+
19+
return stages[0];
20+
}
21+
22+
function isStage(potentialStage: string): potentialStage is Stage {
23+
return stages.includes(potentialStage as Stage);
24+
}
25+
26+
function envToBoolean(value: string | undefined, defaultValue = false): boolean {
27+
if (value === undefined || value === '') {
28+
return defaultValue;
29+
}
30+
31+
// Explicitly test for true instead of false because we don't want to turn
32+
// something on by accident.
33+
return ['1', 'true'].includes(value.trim().toLowerCase()) ? true : false;
34+
}
35+
36+
export function isProduction() {
37+
return stage === 'production';
38+
}
39+
40+
export function isDevelopment() {
41+
return stage === 'development';
42+
}
43+
44+
export function isTesting() {
45+
return stage === 'test';
46+
}
47+
48+
export function isLocal() {
49+
return isDevelopment() || isTesting();
50+
}
51+
52+
// a bit more versatile form of boolean coercion than zod provides
53+
const coerceBoolean = z
54+
.string()
55+
.optional()
56+
.transform((value) => envToBoolean(value))
57+
.pipe(z.boolean());
58+
59+
const configSchema = z.object({
60+
stage: z.enum(stages),
61+
ci: z.object({
62+
isCi: coerceBoolean,
63+
isPullRequest: coerceBoolean,
64+
}),
65+
database: z.object({
66+
url: z.string(),
67+
shouldMigrate: coerceBoolean,
68+
}),
69+
});
70+
71+
const stage = getStage(
72+
[process.env.NODE_ENV, process.env.NEXT_PUBLIC_APP_ENV].filter(notEmpty).filter(isStage)
73+
);
74+
75+
// NOTE: Remember that only env variables that start with NEXT_PUBLIC or are
76+
// listed in next.config.js will be available on the client.
77+
export const config = configSchema.parse({
78+
stage,
79+
ci: {
80+
isCi: process.env.CI,
81+
isPullRequest: process.env.IS_PULL_REQUEST,
82+
},
83+
database: {
84+
url: process.env.DATABASE_URL,
85+
shouldMigrate: process.env.SHOULD_MIGRATE,
86+
},
87+
git: {
88+
commit: process.env.FC_GIT_COMMIT_SHA || process.env.RENDER_GIT_COMMIT,
89+
},
90+
auth: {
91+
secret: process.env.NEXTAUTH_SECRET,
92+
},
93+
});

packages/create-bison-app/template/constants.ts

-2
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,3 @@
22
export const EMAIL_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
33

44
export const MIN_PASSWORD_LENGTH = 8;
5-
6-
export const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';

packages/create-bison-app/template/lib/prisma.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Profile, User } from '@prisma/client';
22
import { Prisma, PrismaClient } from '@prisma/client';
33

4+
import { isProduction } from '@/config';
5+
46
/**
57
* Instantiate prisma client for Next.js:
68
* https://www.prisma.io/docs/support/help-articles/nextjs-prisma-client-dev-practices#solution
@@ -27,7 +29,7 @@ export const prisma =
2729
log: logOptions,
2830
});
2931

30-
if (process.env.NODE_ENV !== 'production') {
32+
if (!isProduction()) {
3133
global.prisma = prisma;
3234
}
3335

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
2+
return value !== null && value !== undefined;
3+
}

packages/create-bison-app/template/package.json.ejs

+11-11
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@
66
"cacheDirectories": [".next/cache"],
77
<% } -%>
88
"scripts": {
9-
"build": "yarn ts-node ./scripts/buildProd",
9+
"build": "yarn ts-node-wrap ./scripts/buildProd",
1010
"build:prisma": "prisma generate",
1111
"build:next": "next build",
1212
"db:migrate": "prisma migrate dev",
1313
"db:migrate:prod": "prisma migrate deploy",
14-
"db:deploy": "yarn prisma migrate deploy",
15-
"db:reset": "yarn prisma migrate reset",
14+
"db:deploy": "prisma migrate deploy",
15+
"db:reset": "prisma migrate reset",
1616
"db:reset:test": "yarn withEnv:test prisma migrate reset",
17-
"db:seed": "yarn prisma db seed",
18-
"db:seed:prod": "cross-env APP_ENV=production prisma db seed",
17+
"db:seed": "prisma db seed",
18+
"db:seed:prod": "cross-env NODE_ENV=production prisma db seed",
1919
"db:setup": "yarn db:reset",
2020
"dev": "next dev",
2121
"dev:typecheck": "tsc --noEmit",
@@ -29,10 +29,10 @@
2929
"g:test:factory": "hygen test factory --name",
3030
"g:test:request": "hygen test request --name",
3131
"g:test:util": "hygen test util --name",
32-
"g:e2e": "yarn playwright codegen",
33-
"lint": "yarn eslint . --ext .ts,.tsx --ignore-pattern tmp",
32+
"g:e2e": "playwright codegen",
33+
"lint": "eslint . --ext .ts,.tsx --ignore-pattern tmp",
3434
"lint:fix": "yarn lint --fix",
35-
"run:script": "yarn ts-node prisma/scripts/run.ts -f",
35+
"run:script": "yarn ts-node-wrap prisma/scripts/run.ts -f",
3636
"setup": "yarn setup:dev && yarn setup:test",
3737
"setup:dev": "yarn build:prisma && yarn db:deploy && yarn db:seed",
3838
"setup:test": "yarn withEnv:test -- yarn db:deploy",
@@ -43,7 +43,7 @@
4343
"test:db:reset": "yarn withEnv:test prisma migrate reset --force",
4444
"test:e2e": "yarn withEnv:test playwright test --workers 1",
4545
"test:e2e:debug": "PWDEBUG=1 yarn test:e2e",
46-
"ts-node": "ts-node-dev --project tsconfig.cjs.json -r tsconfig-paths/register",
46+
"ts-node-wrap": "ts-node --project tsconfig.cjs.json -r tsconfig-paths/register",
4747
"withEnv:test": "dotenv -c test --",
4848
"watch:ts": "yarn dev:typecheck --watch"
4949
},
@@ -117,7 +117,7 @@
117117
"prisma": "^4.12.0",
118118
"start-server-and-test": "^2.0.0",
119119
"ts-jest": "^29.1.0",
120-
"ts-node-dev": "^2.0.0",
120+
"ts-node": "^10.9.1",
121121
"tsconfig-paths": "^4.2.0",
122122
"typescript": "^5.0.3"
123123
},
@@ -133,6 +133,6 @@
133133
"*.{ts,tsx}": "yarn lint"
134134
},
135135
"prisma": {
136-
"seed": "yarn ts-node prisma/seed.ts"
136+
"seed": "yarn ts-node-wrap prisma/seed.ts"
137137
}
138138
}

packages/create-bison-app/template/pages/api/health.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextApiRequest, NextApiResponse } from 'next';
22

3+
import { config } from '@/config';
34
import { prisma } from '@/lib/prisma';
45

56
export default async function handler(_req: NextApiRequest, res: NextApiResponse) {
@@ -11,6 +12,7 @@ export default async function handler(_req: NextApiRequest, res: NextApiResponse
1112
} catch (err) {}
1213

1314
const data = {
15+
stage: config.stage,
1416
env: {
1517
NODE_ENV: process.env.NODE_ENV,
1618
NEXT_PUBLIC_APP_ENV: process.env.NEXT_PUBLIC_APP_ENV,

packages/create-bison-app/template/playwright.config.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { PlaywrightTestConfig } from '@playwright/test';
22
import { devices } from '@playwright/test';
33

4+
import { config as appConfig } from '@/config';
5+
46
const TEST_SERVER_PORT = process.env.PORT ? Number(process.env.PORT) : 3001;
5-
const IS_CI = process.env.CI === 'true';
7+
const IS_CI = appConfig.ci.isCi;
68

79
/**
810
* Read environment variables from file.
@@ -30,11 +32,11 @@ const config: PlaywrightTestConfig = {
3032
/* Run tests in files in parallel */
3133
fullyParallel: true,
3234
/* Fail the build on CI if you accidentally left test.only in the source code. */
33-
forbidOnly: !!process.env.CI,
35+
forbidOnly: IS_CI,
3436
/* Retry on CI only */
35-
retries: process.env.CI ? 2 : 0,
37+
retries: IS_CI ? 2 : 0,
3638
/* Opt out of parallel tests on CI. */
37-
workers: process.env.CI ? 1 : undefined,
39+
workers: IS_CI ? 1 : undefined,
3840
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
3941
reporter: 'html',
4042
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */

packages/create-bison-app/template/prisma/SeedsAndScripts.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
- `yarn db:seed` (DEV)
88
- `yarn db:seed:prod` (PROD)
99

10-
We use `APP_ENV` in the scripts to determine the dataset returned for seeds.
10+
We use `NODE_ENV` in the scripts to determine the dataset returned for seeds.
1111
This ENV is set in your .env.local file as well, and can be manually set as an ENV in your deploy environment if needed for other scripts.
1212

1313
### File Breakdown (Seeds)
@@ -20,7 +20,7 @@ This ENV is set in your .env.local file as well, and can be manually set as an E
2020
----/index.ts
2121
```
2222

23-
**data.ts** contains the exported function `seedModelData: ModelCreateInput[]` this function leverages APP_ENV to return the dataset expected for Dev vs Prod. In the case of `users` this returns `initialDevUsers: UserCreateInput[]` or `initalProdUsers: UserCreateInput[]`.
23+
**data.ts** contains the exported function `seedModelData: ModelCreateInput[]` this function leverages `NODE_ENV` to return the dataset expected for Dev vs Prod. In the case of `users` this returns `initialDevUsers: UserCreateInput[]` or `initalProdUsers: UserCreateInput[]`.
2424

2525
**prismaRunner.ts** this file contains the Prisma `UPSERT` call for the model. We leverage upsert here so that seeds can potentially be ran more than once as your models and data expand over time.
2626

0 commit comments

Comments
 (0)