chore: zod validate env schema (#127)

* chore: zod validate

Signed-off-by: Innei <i@innei.in>

* feat: replace VITE_BUILD_TYPE with channel in app version

* fix: bypass in prod

Signed-off-by: Innei <i@innei.in>

* fix: update

Signed-off-by: Innei <i@innei.in>

---------

Signed-off-by: Innei <i@innei.in>
Co-authored-by: DIYgod <i@diygod.me>
This commit is contained in:
Innei 2024-07-12 17:44:44 +08:00 committed by GitHub
parent 5e846b9015
commit 42b68a402b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 98 additions and 33 deletions

View File

@ -10,7 +10,6 @@ env:
VITE_IMGPROXY_URL: ${{ vars.VITE_IMGPROXY_URL }}
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ vars.VITE_POSTHOG_KEY }}
VITE_BUILD_TYPE: internal
jobs:
release:

View File

@ -12,16 +12,24 @@ export default defineConfig({
resolve: {
alias: {
"@shared": resolve("src/shared/src"),
"@env": resolve("./src/env.ts"),
},
},
},
preload: {
resolve: {
alias: {
"@env": resolve("./src/env.ts"),
},
},
},
preload: {},
renderer: {
resolve: {
alias: {
"@renderer": resolve("src/renderer/src"),
"@shared": resolve("src/shared/src"),
"@pkg": resolve("./package.json"),
"@env": resolve("./src/env.ts"),
},
},
plugins: [

View File

@ -57,6 +57,7 @@
"@sentry/react": "8.9.2",
"@sentry/vite-plugin": "2.20.1",
"@shikijs/transformers": "1.10.0",
"@t3-oss/env-core": "^0.10.1",
"@tanstack/query-sync-storage-persister": "5.49.0",
"@tanstack/react-query": "5.49.0",
"@tanstack/react-query-persist-client": "5.49.0",

View File

@ -103,6 +103,9 @@ importers:
'@shikijs/transformers':
specifier: 1.10.0
version: 1.10.0
'@t3-oss/env-core':
specifier: ^0.10.1
version: 0.10.1(typescript@5.5.2)(zod@3.23.8)
'@tanstack/query-sync-storage-persister':
specifier: 5.49.0
version: 5.49.0
@ -2368,6 +2371,15 @@ packages:
resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
engines: {node: '>=10'}
'@t3-oss/env-core@0.10.1':
resolution: {integrity: sha512-GcKZiCfWks5CTxhezn9k5zWX3sMDIYf6Kaxy2Gx9YEQftFcz8hDRN56hcbylyAO3t4jQnQ5ifLawINsNgCDpOg==}
peerDependencies:
typescript: '>=5.0.0'
zod: ^3.0.0
peerDependenciesMeta:
typescript:
optional: true
'@tailwindcss/container-queries@0.1.1':
resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==}
peerDependencies:
@ -9163,6 +9175,12 @@ snapshots:
dependencies:
defer-to-connect: 2.0.1
'@t3-oss/env-core@0.10.1(typescript@5.5.2)(zod@3.23.8)':
dependencies:
zod: 3.23.8
optionalDependencies:
typescript: 5.5.2
'@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.4)':
dependencies:
tailwindcss: 3.4.4

18
src/env.ts Normal file
View File

@ -0,0 +1,18 @@
import { createEnv } from "@t3-oss/env-core"
import { z } from "zod"
const isDev = "process" in globalThis ? process.env.NODE_ENV === "development" : import.meta.env.DEV
export const env = createEnv({
clientPrefix: "VITE_",
client: {
VITE_WEB_URL: z.string().url(),
VITE_API_URL: z.string().url(),
VITE_IMGPROXY_URL: z.string().url(),
VITE_SENTRY_DSN: z.string().optional(),
VITE_POSTHOG_KEY: z.string().optional(),
},
emptyStringAsUndefined: true,
runtimeEnv: "process" in globalThis ? process.env : import.meta.env,
skipValidation: !isDev,
})

View File

@ -1,16 +0,0 @@
import { z } from "zod"
export const ReleaseTypeSchema = z.enum([
"stable",
"beta",
"internal",
])
export const envBuildType = (process.env.VITE_BUILD_TYPE || "internal")
.trim()
.toLowerCase()
export const buildType = ReleaseTypeSchema.parse(envBuildType)
export const mode = process.env.NODE_ENV
export const isDev = mode === "development"

7
src/main/env.ts Normal file
View File

@ -0,0 +1,7 @@
import { app } from "electron"
import * as semver from "semver"
export const mode = process.env.NODE_ENV
export const isDev = mode === "development"
export const channel = isDev ? "development" : ((semver.prerelease(app.getVersion())?.[0] as string) || "stable")

View File

@ -7,10 +7,13 @@ import { APP_PROTOCOL, DEEPLINK_SCHEME } from "@shared/constants"
import { extractElectronWindowOptions } from "@shared/electron"
import { app, BrowserWindow, session } from "electron"
import { env } from "../env"
import { isDev } from "./env"
import { initializationApp } from "./init"
import { registerUpdater } from "./updater"
import { createMainWindow, createWindow } from "./window"
if (isDev) console.info("[main] env loaded:", env)
initializationApp()
let mainWindow: BrowserWindow

View File

@ -1,7 +1,7 @@
import { getRendererHandlers } from "@egoist/tipc/main"
import { autoUpdater } from "electron-updater"
import { buildType, isDev } from "../config"
import { channel, isDev } from "../env"
import { logger } from "../logger"
import type { RendererHandlers } from "../renderer-handlers"
import { getMainWindow } from "../window"
@ -63,17 +63,17 @@ export const registerUpdater = async () => {
const allowAutoUpdate = true
autoUpdater.autoDownload = false
autoUpdater.allowPrerelease = buildType !== "stable"
autoUpdater.allowPrerelease = channel !== "stable"
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.autoRunAppAfterInstall = true
const feedUrl: Exclude<Parameters<typeof autoUpdater.setFeedURL>[0], string> = {
channel: buildType,
channel,
// hack for custom provider
provider: "custom" as "github",
repo: "follow",
owner: "RSSNext",
releaseType: buildType === "stable" ? "release" : "prerelease",
releaseType: channel === "stable" ? "release" : "prerelease",
// @ts-expect-error hack for custom provider
updateProvider: CustomGitHubProvider,
}

View File

@ -2,6 +2,7 @@ import { queryClient } from "@renderer/lib/query-client"
import { useEffect, useLayoutEffect } from "react"
import { Outlet } from "react-router-dom"
import { env } from "../../env.js"
import { useAppIsReady } from "./atoms/app"
import { useUISettingKey } from "./atoms/settings/ui"
import { Logo } from "./components/icons/logo"
@ -11,6 +12,9 @@ import { getOS } from "./lib/utils"
import { RootProviders } from "./providers/root-providers"
import { handlers } from "./tipc"
if (import.meta.env.DEV) {
console.info("[renderer] env loaded:", env)
}
function App() {
useEffect(() => {
const cleanup = handlers?.invalidateQuery.listen((queryKey) => {

View File

@ -1,3 +1,4 @@
import { env } from "@env"
import type { BrowserOptions } from "@sentry/react"
import { FetchError } from "ofetch"
@ -5,7 +6,7 @@ export const SentryConfig: BrowserOptions = {
// Performance Monitoring
tracesSampleRate: 1, // Capture 100% of the transactions
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
tracePropagationTargets: ["localhost", import.meta.env.VITE_API_URL],
tracePropagationTargets: ["localhost", env.VITE_API_URL],
// Session Replay
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1,

View File

@ -1,3 +1,4 @@
import { env } from "@env"
import { authConfigManager } from "@hono/auth-js/react"
import { repository } from "@pkg"
import { browserDB } from "@renderer/database"
@ -65,7 +66,7 @@ export const initializeApp = async () => {
// Initialize the auth config
authConfigManager.setConfig({
baseUrl: import.meta.env.VITE_API_URL,
baseUrl: env.VITE_API_URL,
basePath: "/auth",
credentials: "include",
})

View File

@ -1,3 +1,4 @@
import { env } from "@env"
import { getUser } from "@renderer/atoms/user"
declare global {
@ -9,7 +10,9 @@ declare global {
export const initPostHog = async () => {
if (import.meta.env.DEV) return
const { default: posthog } = await import("posthog-js")
posthog.init(import.meta.env.VITE_POSTHOG_KEY, {
if (env.VITE_POSTHOG_KEY === undefined) return
posthog.init(env.VITE_POSTHOG_KEY, {
person_profiles: "identified_only",
})

View File

@ -1,4 +1,6 @@
import { env } from "@env"
import { version } from "@pkg"
import { channel } from "@renderer/lib/constants"
import { useEffect } from "react"
import { createRoutesFromChildren, matchRoutes, useLocation, useNavigationType } from "react-router-dom"
@ -11,8 +13,8 @@ export const initSentry = async () => {
import("posthog-js").then((module) => module.default),
])
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.VITE_BUILD_TYPE ?? "internal",
dsn: env.VITE_SENTRY_DSN,
environment: channel,
integrations: [
Sentry.moduleMetadataIntegration(),
Sentry.reactRouterV6BrowserTracingIntegration({

View File

@ -1,3 +1,5 @@
import * as semver from "semver"
import { getStorageNS } from "./ns"
export const levels = {
@ -83,3 +85,5 @@ export const QUERY_PERSIST_KEY = getStorageNS("REACT_QUERY_OFFLINE_CACHE")
export const ROUTE_FEED_PENDING = "all"
export const ROUTE_ENTRY_PENDING = "pending"
export const ROUTE_FEED_IN_FOLDER = "folder-"
export const channel = import.meta.env.DEV ? "development" : ((semver.prerelease(APP_VERSION)?.[0] as string) || "stable")

View File

@ -1,3 +1,5 @@
import { env } from "@env"
export const getProxyUrl = ({
url,
width,
@ -6,4 +8,4 @@ export const getProxyUrl = ({
url: string
width: number
height: number
}) => `${import.meta.env.VITE_IMGPROXY_URL}/unsafe/${width}x${height}/${encodeURIComponent(url)}`
}) => `${env.VITE_IMGPROXY_URL}/unsafe/${width}x${height}/${encodeURIComponent(url)}`

View File

@ -1,5 +1,7 @@
import { env } from "@env"
export const APP_PROTOCOL = import.meta.env.DEV ? "follow-dev" : "follow"
export const DEEPLINK_SCHEME = `${APP_PROTOCOL}://`
// export const WEB_URL = import.meta.env.VITE_VERCEL_URL ?? import.meta.env.VITE_WEB_URL
export const WEB_URL = import.meta.env.VITE_WEB_URL
export const WEB_URL = env.VITE_WEB_URL

View File

@ -5,17 +5,22 @@
"src/main/**/*",
"src/preload/**/*",
"types/**/*.d.ts",
"src/shared/src/**/*"
"src/shared/src/**/*",
"src/env.ts"
],
"compilerOptions": {
"composite": true,
"types": [
"electron-vite/node"
],
"moduleResolution": "Bundler",
"baseUrl": ".",
"paths": {
"@shared/*": [
"src/shared/src/*"
],
"@env": [
"./src/env.ts"
]
}
}

View File

@ -6,11 +6,13 @@
"src/renderer/src/**/*.tsx",
"src/preload/*.d.ts",
"src/shared/src/**/*",
"types/**/*.d.ts"
"types/**/*.d.ts",
"src/env.ts"
],
"compilerOptions": {
"composite": true,
"jsx": "react-jsx",
"moduleResolution": "Bundler",
"target": "ES2022",
"noUnusedLocals": false,
"strict": true,
@ -25,7 +27,8 @@
"@shared/*": [
"src/shared/src/*"
],
"@pkg": ["./package.json"]
"@pkg": ["./package.json"],
"@env": ["./src/env.ts"]
}
}
}

1
types/vite.d.ts vendored
View File

@ -6,7 +6,6 @@ interface ImportMetaEnv {
VITE_API_URL: string
VITE_IMGPROXY_URL: string
VITE_SENTRY_DSN: string
VITE_BUILD_TYPE: string
VITE_POSTHOG_KEY: string
}

View File

@ -20,6 +20,7 @@ export default defineConfig({
"@renderer": resolve("src/renderer/src"),
"@shared": resolve("src/shared/src"),
"@pkg": resolve("./package.json"),
"@env": resolve("./src/env.ts"),
},
},
base: "/",