From 2ee4f3197d6df799d18cdb7f33100d124de4abdd Mon Sep 17 00:00:00 2001 From: EGOIST Date: Fri, 6 May 2022 21:18:25 +0800 Subject: [PATCH] optimize the way we consume env variables --- .env.example | 4 +- app/components/main/MainLayout.tsx | 5 +-- app/components/site/SiteLayout.tsx | 3 +- app/lib/auth.server.ts | 4 +- app/lib/config.server.ts | 17 -------- app/lib/{config.shared.ts => constants.ts} | 5 +-- app/lib/db.server.ts | 6 +-- app/lib/env.ts | 40 +++++++++++++++++ app/lib/helpers.ts | 3 +- app/lib/mailgun.server.ts | 10 ++++- app/lib/s3.server.ts | 2 +- app/lib/tenant.server.ts | 2 +- app/lib/user-contents.ts | 5 ++- app/root.tsx | 6 +-- app/routes/api/login-complete.ts | 3 +- app/routes/api/login.ts | 3 +- app/routes/api/request-login-link.ts | 5 ++- .../dashboard/$subdomain/settings/domains.tsx | 2 +- app/routes/dashboard/new-site.tsx | 2 +- app/routes/dev-s3-proxy.ts | 6 +-- app/routes/index.tsx | 17 +++----- server/fly.ts | 15 +++---- types.d.ts | 43 +++++++++++-------- 23 files changed, 120 insertions(+), 88 deletions(-) delete mode 100644 app/lib/config.server.ts rename app/lib/{config.shared.ts => constants.ts} (50%) create mode 100644 app/lib/env.ts diff --git a/.env.example b/.env.example index 44971291..b09d157b 100644 --- a/.env.example +++ b/.env.example @@ -9,9 +9,6 @@ GOOGLE_CLIENT_SECRET= AUTH_COOKIE_NAME=app.session-token -# openssl rand -base64 32 -AUTH_SECRET=xxx - OUR_DOMAIN=localhost:3000 S3_BUCKET_NAME=xxx @@ -19,6 +16,7 @@ S3_REGION=xxx S3_ACCESS_KEY_ID=xxx S3_SECRET_ACCESS_KEY=xxx S3_ENDPOINT=s3.us-west-004.backblazeb2.com +S3_CDN_PREFIX=xxx MAILGUN_APIKEY=xxx MAILGUN_DOMAIN=xxx diff --git a/app/components/main/MainLayout.tsx b/app/components/main/MainLayout.tsx index 8c49355c..6c653f96 100644 --- a/app/components/main/MainLayout.tsx +++ b/app/components/main/MainLayout.tsx @@ -1,14 +1,13 @@ import { Link } from "@remix-run/react" import { logout } from "~/lib/auth.client" +import { APP_NAME } from "~/lib/env" import { useStore } from "~/lib/store" export function MainLayout({ isLoggedIn, - appName, children, }: { isLoggedIn: boolean - appName: string children: React.ReactNode }) { const setLoginModalOpened = useStore((store) => store.setLoginModalOpened) @@ -42,7 +41,7 @@ export function MainLayout({
- {appName} + {APP_NAME}
diff --git a/app/components/site/SiteLayout.tsx b/app/components/site/SiteLayout.tsx index 93e3762d..493d5dda 100644 --- a/app/components/site/SiteLayout.tsx +++ b/app/components/site/SiteLayout.tsx @@ -9,7 +9,8 @@ import { SubscribeModal } from "../common/SubscribeModal" import { Avatar } from "../ui/Avatar" import { Button } from "../ui/Button" import { UniLink } from "../ui/UniLink" -import { IS_PROD, OUR_DOMAIN } from "~/lib/config.shared" +import { IS_PROD } from "~/lib/constants" +import { OUR_DOMAIN } from "~/lib/env" type MenuLink = { text: string diff --git a/app/lib/auth.server.ts b/app/lib/auth.server.ts index f7234458..12ebd3dd 100644 --- a/app/lib/auth.server.ts +++ b/app/lib/auth.server.ts @@ -1,9 +1,9 @@ import type { User, Membership } from "@prisma/client" -import { AUTH_COOKIE_NAME } from "./config.server" import { prismaRead } from "./db.server" import dayjs from "dayjs" import { type CookieSerializeOptions, createCookie } from "@remix-run/node" -import { IS_PROD, OUR_DOMAIN } from "./config.shared" +import { AUTH_COOKIE_NAME, OUR_DOMAIN } from "./env" +import { IS_PROD } from "./constants" export type AuthUser = User & { memberships: Membership[] diff --git a/app/lib/config.server.ts b/app/lib/config.server.ts deleted file mode 100644 index bb0a93fd..00000000 --- a/app/lib/config.server.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const AUTH_COOKIE_NAME = process.env.AUTH_COOKIE_NAME -export const S3_REGION = process.env.S3_REGION -export const S3_ACCESS_KEY_ID = process.env.S3_ACCESS_KEY_ID -export const S3_SECRET_ACCESS_KEY = process.env.S3_SECRET_ACCESS_KEY - -export const S3_BUCKET_NAME = process.env.S3_BUCKET_NAME -export const S3_ENDPOINT = process.env.S3_ENDPOINT -export const MAILGUN_APIKEY = process.env.MAILGUN_APIKEY -export const MAILGUN_DOMAIN = process.env.MAILGUN_DOMAIN -export const MAILGUN_EU = process.env.MAILGUN_EU -// Primary Fly region -export const PRIMARY_REGION = process.env.PRIMARY_REGION -// Curreny Fly region -export const FLY_REGION = process.env.FLY_REGION -export const IS_PRIMARY_REGION = Boolean( - FLY_REGION && PRIMARY_REGION === FLY_REGION -) diff --git a/app/lib/config.shared.ts b/app/lib/constants.ts similarity index 50% rename from app/lib/config.shared.ts rename to app/lib/constants.ts index 2fd15595..b4e20c70 100644 --- a/app/lib/config.shared.ts +++ b/app/lib/constants.ts @@ -1,12 +1,9 @@ import { description } from "../../package.json" -export const IS_BROWSER = typeof window !== "undefined" +export const IS_BROWSER = typeof document !== "undefined" export const IS_PROD = IS_BROWSER ? !location.hostname.endsWith("localhost") : process.env.NODE_ENV === "production" -export const APP_NAME = IS_BROWSER ? ENV.APP_NAME : process.env.APP_NAME -export const OUR_DOMAIN = IS_BROWSER ? ENV.OUR_DOMAIN : process.env.OUR_DOMAIN - export const APP_DESCRIPTION = description diff --git a/app/lib/db.server.ts b/app/lib/db.server.ts index 4b9ecc5e..b16fefff 100644 --- a/app/lib/db.server.ts +++ b/app/lib/db.server.ts @@ -1,6 +1,6 @@ import { PrismaClient } from "@prisma/client" -import { IS_PRIMARY_REGION } from "./config.server" -import { IS_PROD } from "./config.shared" +import { IS_PROD } from "./constants" +import { isPrimaryRegion } from "./env" import { singleton } from "./singleton.server" export const prismaWrite = /* @__PURE__ */ singleton( @@ -12,7 +12,7 @@ export const prismaWrite = /* @__PURE__ */ singleton( export const prismaRead = /* @__PURE__ */ singleton("prisma-read", () => { // 5433 is the read-replica port let url = process.env.DATABASE_URL - if (!IS_PRIMARY_REGION && IS_PROD) { + if (!isPrimaryRegion() && IS_PROD) { url = url.replace(":5432", ":5433") } console.log("read replica url", url) diff --git a/app/lib/env.ts b/app/lib/env.ts new file mode 100644 index 00000000..bdadfe8f --- /dev/null +++ b/app/lib/env.ts @@ -0,0 +1,40 @@ +import { IS_BROWSER } from "./constants" + +/** + * Can be called in browser and server + * only exposing env variables that're available in browser + */ +export const getCommonEnv = ( + key: T +): BrowserEnv[T] => { + return IS_BROWSER ? ENV[key] : process.env[key] +} + +export const getServerEnv = ( + key: T +): ServerEnv[T] => { + if (IS_BROWSER) throw new Error(`getServerEnv() called in browser`) + return process.env[key] +} + +export const isPrimaryRegion = () => { + const FLY_REGION = getServerEnv("FLY_REGION") + const PRIMARY_REGION = getServerEnv("PRIMARY_REGION") + return Boolean(FLY_REGION && PRIMARY_REGION === FLY_REGION) +} + +// Use /* @__PURE__ */ annotation to make tree-shaking work +export const AUTH_COOKIE_NAME = /* @__PURE__ */ getServerEnv("AUTH_COOKIE_NAME") +export const APP_NAME = /* @__PURE__ */ getCommonEnv("APP_NAME") +export const OUR_DOMAIN = /* @__PURE__ */ getCommonEnv("OUR_DOMAIN") +export const S3_CDN_PREFIX = /* @__PURE__ */ getCommonEnv("S3_CDN_PREFIX") +export const S3_REGION = /* @__PURE__ */ getServerEnv("S3_REGION") +export const S3_ACCESS_KEY_ID = /* @__PURE__ */ getServerEnv("S3_ACCESS_KEY_ID") +export const S3_SECRET_ACCESS_KEY = /* @__PURE__ */ getServerEnv( + "S3_SECRET_ACCESS_KEY" +) +export const S3_BUCKET_NAME = /* @__PURE__ */ getServerEnv("S3_BUCKET_NAME") +export const S3_ENDPOINT = /* @__PURE__ */ getServerEnv("S3_ENDPOINT") +export const MAILGUN_APIKEY = /* @__PURE__ */ getServerEnv("MAILGUN_APIKEY") +export const MAILGUN_DOMAIN = /* @__PURE__ */ getServerEnv("MAILGUN_DOMAIN") +export const MAILGUN_EU = /* @__PURE__ */ getServerEnv("MAILGUN_EU") diff --git a/app/lib/helpers.ts b/app/lib/helpers.ts index cd0fb15d..e94b111a 100644 --- a/app/lib/helpers.ts +++ b/app/lib/helpers.ts @@ -1,4 +1,5 @@ -import { IS_PROD, OUR_DOMAIN } from "./config.shared" +import { IS_PROD } from "./constants" +import { OUR_DOMAIN } from "./env" export const getSiteLink = ({ subdomain }: { subdomain: string }) => { return `${IS_PROD ? "https" : "http"}://${subdomain}.${OUR_DOMAIN}` diff --git a/app/lib/mailgun.server.ts b/app/lib/mailgun.server.ts index a571b2b8..13deafa0 100644 --- a/app/lib/mailgun.server.ts +++ b/app/lib/mailgun.server.ts @@ -1,9 +1,15 @@ import Mailgun from "mailgun.js" import FormData from "form-data" import { singleton } from "./singleton.server" -import { MAILGUN_APIKEY, MAILGUN_DOMAIN, MAILGUN_EU } from "~/lib/config.server" +import { + APP_NAME, + MAILGUN_APIKEY, + MAILGUN_DOMAIN, + MAILGUN_EU, + OUR_DOMAIN, +} from "~/lib/env" import type { MailgunMessageData } from "mailgun.js/interfaces/Messages" -import { APP_NAME, IS_PROD, OUR_DOMAIN } from "./config.shared" +import { IS_PROD } from "./constants" const getClient = () => singleton("mailgun", () => { diff --git a/app/lib/s3.server.ts b/app/lib/s3.server.ts index b9d135ea..6a33cb7a 100644 --- a/app/lib/s3.server.ts +++ b/app/lib/s3.server.ts @@ -4,7 +4,7 @@ import { S3_SECRET_ACCESS_KEY, S3_BUCKET_NAME, S3_ENDPOINT, -} from "~/lib/config.server" +} from "~/lib/env" import { S3Client, PutObjectCommand, diff --git a/app/lib/tenant.server.ts b/app/lib/tenant.server.ts index fcae2272..07935ad7 100644 --- a/app/lib/tenant.server.ts +++ b/app/lib/tenant.server.ts @@ -1,4 +1,4 @@ -import { OUR_DOMAIN } from "./config.shared" +import { OUR_DOMAIN } from "./env" export const getTenant = (request: Request) => { const host = request.headers.get("host") diff --git a/app/lib/user-contents.ts b/app/lib/user-contents.ts index 2ba430ea..80196587 100644 --- a/app/lib/user-contents.ts +++ b/app/lib/user-contents.ts @@ -1,4 +1,5 @@ -import { IS_PROD, OUR_DOMAIN } from "./config.shared" +import { IS_PROD } from "./constants" +import { S3_CDN_PREFIX } from "./env" export function getUserContentsUrl(filename: string): string export function getUserContentsUrl(filename: undefined | null): undefined @@ -8,7 +9,7 @@ export function getUserContentsUrl( export function getUserContentsUrl(filename: string | undefined | null) { if (!filename) return undefined if (IS_PROD) { - return `https://usercontents.${OUR_DOMAIN}/${filename}` + return `${S3_CDN_PREFIX}/${filename}` } return `/dev-s3-proxy?${new URLSearchParams({ filename: filename, diff --git a/app/root.tsx b/app/root.tsx index 0dede57b..8556d41f 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -6,7 +6,6 @@ import { } from "@remix-run/node" import { Links, - LiveReload, Meta, Outlet, Scripts, @@ -17,7 +16,7 @@ import LoginModal from "~/components/common/LoginModal" import { createStore, StoreProvider } from "./lib/store" import { Toaster } from "react-hot-toast" import css from "./generated.css" -import { APP_NAME } from "./lib/config.shared" +import { APP_NAME } from "./lib/env" export const meta: MetaFunction = () => { return { @@ -31,13 +30,14 @@ export const links: LinksFunction = () => { return [{ href: css, rel: "stylesheet", type: "text/css" }] } -type LoaderData = { ENV: Record } +type LoaderData = { ENV: BrowserEnv } export const loader: LoaderFunction = async () => { return json({ ENV: { APP_NAME: process.env.APP_NAME, OUR_DOMAIN: process.env.OUR_DOMAIN, + S3_CDN_PREFIX: process.env.S3_CDN_PREFIX, }, }) } diff --git a/app/routes/api/login-complete.ts b/app/routes/api/login-complete.ts index 47de4bf9..2ba1b5f5 100644 --- a/app/routes/api/login-complete.ts +++ b/app/routes/api/login-complete.ts @@ -1,8 +1,9 @@ import { type LoaderFunction, redirect } from "@remix-run/node" import { z } from "zod" import { generateCookie } from "~/lib/auth.server" -import { IS_PROD, OUR_DOMAIN } from "~/lib/config.shared" +import { IS_PROD } from "~/lib/constants" import { prismaRead, prismaWrite } from "~/lib/db.server" +import { OUR_DOMAIN } from "~/lib/env" export const loader: LoaderFunction = async ({ request }) => { const url = new URL(request.url) diff --git a/app/routes/api/login.ts b/app/routes/api/login.ts index 0a9f962e..05ee24e1 100644 --- a/app/routes/api/login.ts +++ b/app/routes/api/login.ts @@ -4,8 +4,9 @@ import { prismaRead, prismaWrite } from "~/lib/db.server" import { nanoid } from "nanoid" import UAParser from "ua-parser-js" import dayjs from "dayjs" -import { IS_PROD, OUR_DOMAIN } from "~/lib/config.shared" import { generateCookie } from "~/lib/auth.server" +import { OUR_DOMAIN } from "~/lib/env" +import { IS_PROD } from "~/lib/constants" export const loader: LoaderFunction = async ({ request }) => { const url = new URL(request.url) diff --git a/app/routes/api/request-login-link.ts b/app/routes/api/request-login-link.ts index 8cd44d29..77492328 100644 --- a/app/routes/api/request-login-link.ts +++ b/app/routes/api/request-login-link.ts @@ -1,8 +1,9 @@ -import { ActionFunction, redirect } from "@remix-run/node" +import { ActionFunction } from "@remix-run/node" import dayjs from "dayjs" import { z } from "zod" -import { IS_PROD, OUR_DOMAIN } from "~/lib/config.shared" +import { IS_PROD } from "~/lib/constants" import { prismaWrite } from "~/lib/db.server" +import { OUR_DOMAIN } from "~/lib/env" import { sendLoginEmail } from "~/lib/mailgun.server" export const action: ActionFunction = async ({ request }) => { diff --git a/app/routes/dashboard/$subdomain/settings/domains.tsx b/app/routes/dashboard/$subdomain/settings/domains.tsx index 3b916234..f2a59670 100644 --- a/app/routes/dashboard/$subdomain/settings/domains.tsx +++ b/app/routes/dashboard/$subdomain/settings/domains.tsx @@ -6,7 +6,7 @@ import { Button } from "~/components/ui/Button" import { Input } from "~/components/ui/Input" import { siteController } from "~/controllers/site.controller" import { getAuthUser } from "~/lib/auth.server" -import { OUR_DOMAIN } from "~/lib/config.shared" +import { OUR_DOMAIN } from "~/lib/env" export const loader: LoaderFunction = async ({ request, params }) => { await getAuthUser(request, true) diff --git a/app/routes/dashboard/new-site.tsx b/app/routes/dashboard/new-site.tsx index 26ea1a8c..b4206f1a 100644 --- a/app/routes/dashboard/new-site.tsx +++ b/app/routes/dashboard/new-site.tsx @@ -10,7 +10,7 @@ import { Button } from "~/components/ui/Button" import { Input } from "~/components/ui/Input" import { siteController } from "~/controllers/site.controller" import { getAuthUser } from "~/lib/auth.server" -import { OUR_DOMAIN } from "~/lib/config.shared" +import { OUR_DOMAIN } from "~/lib/env" export const loader: LoaderFunction = async ({ request }) => { const user = await getAuthUser(request, true) diff --git a/app/routes/dev-s3-proxy.ts b/app/routes/dev-s3-proxy.ts index 2265ff78..d89f2429 100644 --- a/app/routes/dev-s3-proxy.ts +++ b/app/routes/dev-s3-proxy.ts @@ -1,6 +1,6 @@ -import { LoaderFunction } from "@remix-run/node" -import { S3_BUCKET_NAME, S3_ENDPOINT } from "~/lib/config.server" -import { IS_PROD } from "~/lib/config.shared" +import { type LoaderFunction } from "@remix-run/node" +import { IS_PROD } from "~/lib/constants" +import { S3_BUCKET_NAME, S3_ENDPOINT } from "~/lib/env" export const loader: LoaderFunction = ({ request }) => { if (IS_PROD) { diff --git a/app/routes/index.tsx b/app/routes/index.tsx index cc6b90fd..7c83b396 100644 --- a/app/routes/index.tsx +++ b/app/routes/index.tsx @@ -5,7 +5,8 @@ import { SiteHome } from "~/components/site/SiteHome" import { SiteLayout, type SiteLayoutProps } from "~/components/site/SiteLayout" import { siteController } from "~/controllers/site.controller" import { getAuthUser } from "~/lib/auth.server" -import { APP_DESCRIPTION, APP_NAME } from "~/lib/config.shared" +import { APP_DESCRIPTION } from "~/lib/constants" +import { APP_NAME } from "~/lib/env" import { getTenant } from "~/lib/tenant.server" import { PageVisibilityEnum, type PostOnSiteHome } from "~/lib/types" import { getSubscription } from "~/models/site.model" @@ -13,13 +14,11 @@ import { getSubscription } from "~/models/site.model" type LoaderData = | { type: "main" - appName: string isLoggedIn: boolean } | { type: "tenant" isLoggedIn: boolean - appName: string tenant?: string posts: PostOnSiteHome[] site: SiteLayoutProps["site"] @@ -62,7 +61,6 @@ export const loader: LoaderFunction = async (ctx) => { return json({ type: "tenant", isLoggedIn, - appName: APP_NAME, tenant, posts: pages, site: { @@ -75,7 +73,10 @@ export const loader: LoaderFunction = async (ctx) => { }) } - return json({ type: "main", isLoggedIn, appName: APP_NAME }) + return json({ + type: "main", + isLoggedIn, + }) } export default function Home() { @@ -93,9 +94,5 @@ export default function Home() { ) } - return ( - - {""} - - ) + return {""} } diff --git a/server/fly.ts b/server/fly.ts index f0b47481..976babb7 100644 --- a/server/fly.ts +++ b/server/fly.ts @@ -1,13 +1,12 @@ import { type RequestHandler } from "express" -import { - FLY_REGION, - IS_PRIMARY_REGION, - PRIMARY_REGION, -} from "~/lib/config.server" -import { IS_PROD } from "~/lib/config.shared" +import { IS_PROD } from "~/lib/constants" +import { getServerEnv, isPrimaryRegion } from "~/lib/env" + +const PRIMARY_REGION = getServerEnv("PRIMARY_REGION") +const FLY_REGION = getServerEnv("FLY_REGION") export const setFlyRegionHeader: RequestHandler = (req, res, next) => { - res.setHeader("x-fly-region", FLY_REGION || "unknown") + res.setHeader("x-fly-region", PRIMARY_REGION || "unknown") next() } @@ -17,7 +16,7 @@ export const getReplayResponse: RequestHandler = (req, res, next) => { if ( !IS_PROD || ["GET", "OPTIONS", "HEAD"].includes(method) || - IS_PRIMARY_REGION + isPrimaryRegion() ) { return next() } diff --git a/types.d.ts b/types.d.ts index 9329edbe..cee843e1 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,23 +1,30 @@ -declare namespace NodeJS { - interface ProcessEnv { - // Additional environment variables - APP_NAME: string - DATABASE_URL: string - AUTH_COOKIE_NAME: string - AUTH_SECRET: string - OUR_DOMAIN: string - S3_REGION: string - S3_ACCESS_KEY_ID: string - S3_SECRET_ACCESS_KEY: string - S3_BUCKET_NAME: string - S3_ENDPOINT?: string - MAILGUN_APIKEY: string - MAILGUN_DOMAIN: string - MAILGUN_EU?: string - } +declare interface ServerEnv { + // Additional environment variables + APP_NAME: string + DATABASE_URL: string + AUTH_COOKIE_NAME: string + OUR_DOMAIN: string + S3_REGION: string + S3_ACCESS_KEY_ID: string + S3_SECRET_ACCESS_KEY: string + S3_BUCKET_NAME: string + S3_ENDPOINT?: string + MAILGUN_APIKEY: string + MAILGUN_DOMAIN: string + MAILGUN_EU?: string + S3_CDN_PREFIX: string + FLY_REGION?: string + PRIMARY_REGION?: string } -declare const ENV: { +declare interface BrowserEnv { APP_NAME: string OUR_DOMAIN: string + S3_CDN_PREFIX: string } + +declare namespace NodeJS { + interface ProcessEnv extends ServerEnv {} +} + +declare const ENV: BrowserEnv