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
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
97 changes: 97 additions & 0 deletions packages/create-bison-app/template/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { z } from 'zod';

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

const stages = ['production', 'staging', 'development', 'test'] as const;
cullylarson marked this conversation as resolved.
Show resolved Hide resolved

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 isStaging() {
return stage === 'staging';
}

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;
}
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;
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