Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Centralize configuration in one place. #312

Merged
merged 15 commits into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from 9 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
8 changes: 5 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: ["ubuntu-latest", "windows-latest"]
os: ['ubuntu-latest', 'windows-latest']
node-version: [16.x, 18.x]
outputs:
appName: ${{ steps.create-app.outputs.appName }}
Expand All @@ -25,7 +25,7 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"
cache: 'yarn'

- name: Install packages
run: yarn && yarn lerna bootstrap
Expand Down Expand Up @@ -56,7 +56,7 @@ jobs:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
ports: ["5432:5432"]
ports: ['5432:5432']
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5

strategy:
Expand Down Expand Up @@ -85,6 +85,8 @@ jobs:

- name: Build production bison app
run: yarn build
env:
DATABASE_URL: not_used_but_needs_to_be_set_to_pass_zod_config_check

- name: Lint bison app
run: yarn lint
Expand Down
1 change: 1 addition & 0 deletions packages/create-bison-app/tasks/copyFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ async function copyFiles({ variables, targetFolder }) {
"prettier.config.js",
"tsconfig.json",
"tsconfig.cjs.json",
"config.ts",
],
targetFolder,
{
Expand Down
93 changes: 93 additions & 0 deletions packages/create-bison-app/template/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { z } from 'zod';

import { notEmpty } from './lib/type-witchcraft';

const stages = ['production', 'development', 'test'] as const;

type Stage = (typeof stages)[number];

function getStage(stages: Stage[]) {
if (!stages.length) return 'development';

for (const stage of stages) {
// if any of the provided stages is not production, assume we aren't in production
if (stage !== 'production') {
return stage;
}
}

return stages[0];
}

function isStage(potentialStage: string): potentialStage is Stage {
return stages.includes(potentialStage as Stage);
}

function envToBoolean(value: string | undefined, defaultValue = false): boolean {
if (value === undefined || value === '') {
return defaultValue;
}

// Explicitly test for true instead of false because we don't want to turn
// something on by accident.
return ['1', 'true'].includes(value.trim().toLowerCase()) ? true : false;
}

export function isProduction() {
return stage === 'production';
}

export function isDevelopment() {
return stage === 'development';
}

export function isTesting() {
return stage === 'test';
}

export function isLocal() {
return isDevelopment() || isTesting();
}

// a bit more versatile form of boolean coercion than zod provides
const coerceBoolean = z
.string()
.optional()
.transform((value) => envToBoolean(value))
.pipe(z.boolean());

const configSchema = z.object({
stage: z.enum(stages),
ci: z.object({
isCi: coerceBoolean,
isPullRequest: coerceBoolean,
}),
database: z.object({
url: z.string(),
shouldMigrate: coerceBoolean,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to default shouldMigrate to true or process.env.NODE_ENV === 'production'? I think this might trip people up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's set explicitly by the SHOULD_MIGRATE environment variable. Which, I realized I forgot to add to the .env files.

}),
});

const stage = getStage(
[process.env.NODE_ENV, process.env.NEXT_PUBLIC_APP_ENV].filter(notEmpty).filter(isStage)
);

// NOTE: Remember that only env variables that start with NEXT_PUBLIC or are
// listed in next.config.js will be available on the client.
export const config = configSchema.parse({
stage,
ci: {
isCi: process.env.CI,
isPullRequest: process.env.IS_PULL_REQUEST,
},
database: {
url: process.env.DATABASE_URL,
shouldMigrate: process.env.SHOULD_MIGRATE,
cullylarson marked this conversation as resolved.
Show resolved Hide resolved
},
git: {
commit: process.env.FC_GIT_COMMIT_SHA || process.env.RENDER_GIT_COMMIT,
},
auth: {
secret: process.env.NEXTAUTH_SECRET,
},
});
2 changes: 0 additions & 2 deletions packages/create-bison-app/template/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,3 @@
export const EMAIL_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;

export const MIN_PASSWORD_LENGTH = 8;

export const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
4 changes: 3 additions & 1 deletion packages/create-bison-app/template/lib/prisma.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Profile, User } from '@prisma/client';
import { Prisma, PrismaClient } from '@prisma/client';

import { isProduction } from '@/config';

/**
* Instantiate prisma client for Next.js:
* https://www.prisma.io/docs/support/help-articles/nextjs-prisma-client-dev-practices#solution
Expand All @@ -27,7 +29,7 @@ export const prisma =
log: logOptions,
});

if (process.env.NODE_ENV !== 'production') {
if (!isProduction()) {
global.prisma = prisma;
}

Expand Down
3 changes: 3 additions & 0 deletions packages/create-bison-app/template/lib/type-witchcraft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
return value !== null && value !== undefined;
}
20 changes: 10 additions & 10 deletions packages/create-bison-app/template/package.json.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
"cacheDirectories": [".next/cache"],
<% } -%>
"scripts": {
"build": "yarn ts-node ./scripts/buildProd",
"build": "yarn ts-node-wrap ./scripts/buildProd",
"build:prisma": "prisma generate",
"build:next": "next build",
"db:migrate": "prisma migrate dev",
"db:migrate:prod": "prisma migrate deploy",
"db:deploy": "yarn prisma migrate deploy",
"db:reset": "yarn prisma migrate reset",
"db:deploy": "prisma migrate deploy",
"db:reset": "prisma migrate reset",
"db:reset:test": "yarn withEnv:test prisma migrate reset",
"db:seed": "yarn prisma db seed",
"db:seed": "prisma db seed",
"db:seed:prod": "cross-env APP_ENV=production prisma db seed",
"db:setup": "yarn db:reset",
"dev": "next dev",
Expand All @@ -27,10 +27,10 @@
"g:test:factory": "hygen test factory --name",
"g:test:request": "hygen test request --name",
"g:test:util": "hygen test util --name",
"g:e2e": "yarn playwright codegen",
"lint": "yarn eslint . --ext .ts,.tsx --ignore-pattern tmp",
"g:e2e": "playwright codegen",
"lint": "eslint . --ext .ts,.tsx --ignore-pattern tmp",
"lint:fix": "yarn lint --fix",
"run:script": "yarn ts-node prisma/scripts/run.ts -f",
"run:script": "yarn ts-node-wrap prisma/scripts/run.ts -f",
"setup": "yarn setup:dev && yarn setup:test",
"setup:dev": "yarn build:prisma && yarn db:deploy && yarn db:seed",
"setup:test": "yarn withEnv:test -- yarn db:deploy",
Expand All @@ -41,7 +41,7 @@
"test:db:reset": "yarn withEnv:test prisma migrate reset --force",
"test:e2e": "yarn withEnv:test playwright test --workers 1",
"test:e2e:debug": "PWDEBUG=1 yarn test:e2e",
"ts-node": "ts-node-dev --project tsconfig.cjs.json -r tsconfig-paths/register",
"ts-node-wrap": "ts-node --project tsconfig.cjs.json -r tsconfig-paths/register",
"withEnv:test": "dotenv -c test --",
"watch:ts": "yarn dev:typecheck --watch"
},
Expand Down Expand Up @@ -115,7 +115,7 @@
"prisma": "^4.12.0",
"start-server-and-test": "^2.0.0",
"ts-jest": "^29.1.0",
"ts-node-dev": "^2.0.0",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.0.3"
},
Expand All @@ -131,6 +131,6 @@
"*.{ts,tsx}": "yarn lint"
},
"prisma": {
"seed": "yarn ts-node prisma/seed.ts"
"seed": "yarn ts-node-wrap prisma/seed.ts"
}
}
10 changes: 6 additions & 4 deletions packages/create-bison-app/template/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { PlaywrightTestConfig } from '@playwright/test';
import { devices } from '@playwright/test';

import { config as appConfig } from '@/config';

const TEST_SERVER_PORT = process.env.PORT ? Number(process.env.PORT) : 3001;
const IS_CI = process.env.CI === 'true';
const IS_CI = appConfig.ci.isCi;

/**
* Read environment variables from file.
Expand Down Expand Up @@ -30,11 +32,11 @@ const config: PlaywrightTestConfig = {
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
forbidOnly: IS_CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
retries: IS_CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
workers: IS_CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
Expand Down
9 changes: 5 additions & 4 deletions packages/create-bison-app/template/prisma/seeds/users/data.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Prisma, Role } from '@prisma/client';

import { hashPassword } from '@/services/auth';
import { isLocal } from '@/config';

// *********************************************
// ** DEVELOPMENT DATA SET
// *********************************************
Expand Down Expand Up @@ -45,7 +47,6 @@ const initialProdUsers: Prisma.UserCreateInput[] = [
// ** MAIN DATA EXPORT
// *********************************************

const appEnv = process.env.APP_ENV || 'development';

export const userSeedData: Prisma.UserCreateInput[] =
appEnv === 'production' ? initialProdUsers : initialDevUsers;
export const userSeedData: Prisma.UserCreateInput[] = isLocal()
? initialDevUsers
: initialProdUsers;
39 changes: 12 additions & 27 deletions packages/create-bison-app/template/scripts/buildProd.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,17 @@
export {};
const spawn = require('child_process').spawn;
import { spawn } from 'child_process';
import { config } from '@/config';

const DEFAULT_BUILD_COMMAND = `yarn build:prisma && yarn build:next`;

/**
* This builds the production app.
* if the current branch should be migrated, it runs migrations first.
*/
function buildProd() {
let buildCommand = DEFAULT_BUILD_COMMAND;
const shouldMigrate = process.env.NODE_ENV === 'production';
const buildCommand = config.database.shouldMigrate
? `yarn db:deploy && ${DEFAULT_BUILD_COMMAND}`
: DEFAULT_BUILD_COMMAND;

if (shouldMigrate) {
buildCommand = `yarn db:deploy && ${buildCommand}`;
}
const child = spawn(buildCommand, {
shell: true,
stdio: 'inherit',
});

const child = spawn(buildCommand, {
shell: true,
stdio: 'inherit',
});

child.on('exit', function (code: number) {
process.exit(code);
});
}

if (require.main === module) {
buildProd();
}

module.exports = { buildProd };
child.on('exit', function (code: number) {
process.exit(code);
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { t } from '@/server/trpc';
import { isTesting } from '@/config';

const timingMiddleware = t.middleware(async ({ path, type, next }) => {
// Don't log timing in tests.
if (process.env.NODE_ENV === 'test') return await next();
if (isTesting()) {
return await next();
}

const start = Date.now();
const result = await next();
Expand Down
10 changes: 7 additions & 3 deletions packages/create-bison-app/template/tests/helpers/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,25 @@ import util from 'util';

import { Client } from 'pg';

import { config, isProduction } from '@/config';

const exec = util.promisify(childProcess.exec);

/**
* Resets a database to a blank state.
* Truncates all tables except for _Migrations
*/
export const resetDB = async (): Promise<boolean> => {
if (process.env.NODE_ENV === 'production') return Promise.resolve(false);
if (isProduction()) {
return Promise.resolve(false);
}

const match = process.env.DATABASE_URL?.match(/schema=(.*)(&.*)*$/);
const match = config.database.url.match(/schema=(.*)(&.*)*$/);
const schema = match ? match[1] : 'public';

// NOTE: the prisma client does not handle this query well, use pg instead
const client = new Client({
connectionString: process.env.DATABASE_URL,
connectionString: config.database.url,
});

await client.connect();
Expand Down
9 changes: 9 additions & 0 deletions prettier.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// https://prettier.io/docs/en/options.html
module.exports = {
trailingComma: 'es5',
bracketSpacing: true,
printWidth: 100,
tabWidth: 2,
singleQuote: true,
arrowParens: 'always',
};