diff --git a/.env/.env.development b/.env/.env.development new file mode 100644 index 00000000..4ad6150f --- /dev/null +++ b/.env/.env.development @@ -0,0 +1 @@ +PUBLIC_WEBSITE_URL="http://localhost:3000" diff --git a/.env/.env.example b/.env/.env.example new file mode 100644 index 00000000..b4accf64 --- /dev/null +++ b/.env/.env.example @@ -0,0 +1,11 @@ +# Website (Required) +PUBLIC_WEBSITE_URL= + +# Analytics (Optional) +PUBLIC_POSTHOG_KEY= + +# Discord Bot (Optional) +DISCORD_CLIENT_ID= +DISCORD_PUBLIC_KEY= +DISCORD_CLIENT_SECRET= +DISCORD_TOKEN= diff --git a/apps/browser-extension/.env.development b/apps/browser-extension/.env.development deleted file mode 100644 index 8a03b54c..00000000 --- a/apps/browser-extension/.env.development +++ /dev/null @@ -1,2 +0,0 @@ -# Website -PLASMO_PUBLIC_WEBSITE_URL="http://localhost:3000" diff --git a/apps/browser-extension/.env.example b/apps/browser-extension/.env.example deleted file mode 100644 index c40b99a5..00000000 --- a/apps/browser-extension/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# Website -PLASMO_PUBLIC_WEBSITE_URL= - -# PostHog -PLASMO_PUBLIC_POSTHOG_KEY= diff --git a/apps/browser-extension/package.json b/apps/browser-extension/package.json index 9e9c28f9..1e7b00dc 100644 --- a/apps/browser-extension/package.json +++ b/apps/browser-extension/package.json @@ -13,20 +13,19 @@ }, "scripts": { "check": "tsc --noEmit", - "dev": "plasmo dev", + "dev": "use-env -p PLASMO -- plasmo dev", "build:styles": "cp ../../packages/react/style.css ./src/style.css", "build": "pnpm build:chrome", - "build:chrome": "plasmo build --target=chrome-mv3 --zip", - "package": "plasmo package" + "build:chrome": "use-env -p PLASMO -P -- plasmo build --target=chrome-mv3 --zip" }, "dependencies": { "@evaluate/components": "workspace:^", "@evaluate/engine": "workspace:^", - "@evaluate/env": "workspace:^", "@evaluate/helpers": "workspace:^", "@evaluate/hooks": "workspace:^", - "@evaluate/style": "workspace:^", "@evaluate/shapes": "workspace:^", + "@evaluate/style": "workspace:^", + "@t3-oss/env-core": "^0.11.1", "framer-motion": "^11.12.0", "lucide-react": "^0.338.0", "posthog-js": "1.161.3", diff --git a/apps/browser-extension/src/background/index.ts b/apps/browser-extension/src/background/index.ts index a3b7bddd..c2c6744f 100644 --- a/apps/browser-extension/src/background/index.ts +++ b/apps/browser-extension/src/background/index.ts @@ -1,7 +1,7 @@ import { executeCode } from '@evaluate/engine/dist/execute'; import { searchRuntimes } from '@evaluate/engine/dist/runtimes'; import type { PartialRuntime } from '@evaluate/shapes'; -import { env } from '~env'; +import env from '~env'; import analytics from '~services/analytics'; chrome.action.setTitle({ title: 'Evaluate' }); @@ -33,7 +33,7 @@ chrome.action.onClicked.addListener(async () => { }); chrome.tabs.create({ - url: env.PLASMO_PUBLIC_WEBSITE_URL, + url: `${env.PLASMO_PUBLIC_WEBSITE_URL}`, }); }); diff --git a/apps/browser-extension/src/contents/_components/results-dialog.tsx b/apps/browser-extension/src/contents/_components/results-dialog.tsx index fac1e07f..ac602f14 100644 --- a/apps/browser-extension/src/contents/_components/results-dialog.tsx +++ b/apps/browser-extension/src/contents/_components/results-dialog.tsx @@ -14,7 +14,7 @@ import { cn } from '@evaluate/helpers/dist/class'; import type { ExecuteResult, PartialRuntime } from '@evaluate/shapes'; import { ExternalLinkIcon, XIcon } from 'lucide-react'; import { useMemo } from 'react'; -import { env } from '~env'; +import env from '~env'; import { wrapCapture } from '~services/analytics'; export function ResultsCard(p: { @@ -34,7 +34,7 @@ export function ResultsCard(p: { className="mr-auto inline-flex items-center gap-2" target="_blank" rel="noreferrer noopener" - href={env.PLASMO_PUBLIC_WEBSITE_URL} + href={`${env.PLASMO_PUBLIC_WEBSITE_URL}`} > !v.endsWith('/'), 'should not end with a slash'), + .transform((v) => new URL(v).freeze()), PLASMO_PUBLIC_POSTHOG_KEY: z.string().min(1).optional(), }, - variablesStrict: { + runtimeEnv: { + ...process.env, PLASMO_PUBLIC_WEBSITE_URL: process.env.PLASMO_PUBLIC_WEBSITE_URL, PLASMO_PUBLIC_POSTHOG_KEY: process.env.PLASMO_PUBLIC_POSTHOG_KEY, }, - - onValid(env) { - if (!env.PLASMO_PUBLIC_POSTHOG_KEY) - console.warn( - 'Missing Posthog environment variable, analytics will be disabled.', - ); - }, }); diff --git a/apps/browser-extension/src/services/analytics.ts b/apps/browser-extension/src/services/analytics.ts index e6567bfb..1e10b0d2 100644 --- a/apps/browser-extension/src/services/analytics.ts +++ b/apps/browser-extension/src/services/analytics.ts @@ -1,7 +1,9 @@ import posthog from 'posthog-js'; -import { env } from '~env'; +import env from '~env'; -const enabled = env.PLASMO_PUBLIC_POSTHOG_KEY && env.PLASMO_PUBLIC_WEBSITE_URL; +const enabled = Boolean( + env.PLASMO_PUBLIC_POSTHOG_KEY && env.PLASMO_PUBLIC_WEBSITE_URL, +); export default enabled ? posthog : null; if (enabled) { diff --git a/apps/discord-bot/package.json b/apps/discord-bot/package.json index 4595bde9..1a62b3e4 100644 --- a/apps/discord-bot/package.json +++ b/apps/discord-bot/package.json @@ -6,6 +6,10 @@ ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" + }, + "./env": { + "import": "./dist/env.js", + "types": "./dist/env.d.ts" } }, "scripts": { @@ -16,8 +20,9 @@ "dependencies": { "@buape/carbon": "0.0.0-beta-20250120130953", "@evaluate/engine": "workspace:^", - "@evaluate/env": "workspace:^", + "@evaluate/helpers": "workspace:^", "@evaluate/shapes": "workspace:^", + "@t3-oss/env-core": "^0.11.1", "@vercel/functions": "^1.5.1", "posthog-node": "^4.3.1", "zod": "3.22.4" diff --git a/apps/discord-bot/src/components/open-evaluation-button.ts b/apps/discord-bot/src/components/open-evaluation-button.ts index 1abb908c..2d239f17 100644 --- a/apps/discord-bot/src/components/open-evaluation-button.ts +++ b/apps/discord-bot/src/components/open-evaluation-button.ts @@ -1,10 +1,10 @@ import { LinkButton } from '@buape/carbon'; -import { env } from '~/env'; +import env from '~/env'; import { resolveEmoji } from '~/utilities/resolve-emoji'; export class OpenEvaluationButton extends LinkButton { label = 'Open Evaluation'; - url = env.WEBSITE_URL; + url = `${env.WEBSITE_URL}`; emoji = resolveEmoji('globe', true); public constructor(url: string) { diff --git a/apps/discord-bot/src/env.ts b/apps/discord-bot/src/env.ts index 86cd9a23..dc496c06 100644 --- a/apps/discord-bot/src/env.ts +++ b/apps/discord-bot/src/env.ts @@ -1,10 +1,13 @@ -import { validateEnv } from '@evaluate/env/validator'; +import { URL } from '@evaluate/helpers/url'; +import { createEnv } from '@t3-oss/env-core'; import { z } from 'zod'; -export const env = validateEnv({ +export default createEnv({ server: { - ENV: z.enum(['development', 'production']), - WEBSITE_URL: z.string().url(), + WEBSITE_URL: z + .string() + .url() + .transform((v) => new URL(v).freeze()), POSTHOG_KEY: z.string().optional(), DISCORD_TOKEN: z.string().min(1).optional(), DISCORD_PUBLIC_KEY: z.string().min(1).optional(), @@ -12,29 +15,8 @@ export const env = validateEnv({ DISCORD_CLIENT_SECRET: z.string().min(1).optional(), }, - variablesStrict: { - ENV: process.env.NODE_ENV, - WEBSITE_URL: process.env.WEBSITE_URL || `https://${process.env.VERCEL_URL}`, - POSTHOG_KEY: process.env.POSTHOG_KEY || process.env.NEXT_PUBLIC_POSTHOG_KEY, - DISCORD_TOKEN: process.env.DISCORD_TOKEN, - DISCORD_PUBLIC_KEY: process.env.DISCORD_PUBLIC_KEY, - DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID, - DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET, - }, - - onValid(env) { - if ( - !env.DISCORD_TOKEN || - !env.DISCORD_PUBLIC_KEY || - !env.DISCORD_CLIENT_ID || - !env.DISCORD_CLIENT_SECRET - ) - console.warn( - 'Missing Discord bot environment variables, it will be disabled.', - ); - if (!env.POSTHOG_KEY) - console.warn( - 'Missing Posthog environment variable, analytics will be disabled.', - ); + runtimeEnv: { + WEBSITE_URL: `https://${process.env.VERCEL_URL}`, + ...process.env, }, }); diff --git a/apps/discord-bot/src/handlers/evaluate.ts b/apps/discord-bot/src/handlers/evaluate.ts index c36b66d9..abe13a90 100644 --- a/apps/discord-bot/src/handlers/evaluate.ts +++ b/apps/discord-bot/src/handlers/evaluate.ts @@ -14,7 +14,7 @@ import { import type { ExecuteResult, PartialRuntime } from '@evaluate/shapes'; import { EditEvaluationButton } from '~/components/edit-evaluation-button'; import { OpenEvaluationButton } from '~/components/open-evaluation-button'; -import { env } from '~/env'; +import env from '~/env'; import analytics from '~/services/analytics'; import { codeBlock } from '~/utilities/discord-formatting'; diff --git a/apps/discord-bot/src/index.ts b/apps/discord-bot/src/index.ts index baa14b17..aec6651a 100644 --- a/apps/discord-bot/src/index.ts +++ b/apps/discord-bot/src/index.ts @@ -1,23 +1,7 @@ -import { Client, InteractionType } from '@buape/carbon'; -import { EvaluateCommand } from './commands/evaluate'; -import { env } from './env'; +import { InteractionType } from '@buape/carbon'; +import env from './env'; import analytics from './services/analytics'; - -const client = - env.DISCORD_TOKEN && - env.DISCORD_CLIENT_ID && - env.DISCORD_PUBLIC_KEY && - new Client( - { - baseUrl: 'unused', - clientId: env.DISCORD_CLIENT_ID, - publicKey: env.DISCORD_PUBLIC_KEY, - token: env.DISCORD_TOKEN, - deploySecret: 'unused', - requestOptions: { queueRequests: false }, - }, - [new EvaluateCommand()], - ); +import client from './services/client'; export default async function handler(request: Request) { if (!client) return new Response('X|', { status: 503 }); diff --git a/apps/discord-bot/src/services/analytics.ts b/apps/discord-bot/src/services/analytics.ts index 6f928f0f..08501bb6 100644 --- a/apps/discord-bot/src/services/analytics.ts +++ b/apps/discord-bot/src/services/analytics.ts @@ -1,8 +1,14 @@ import { PostHog } from 'posthog-node'; -import { env } from '~/env'; +import env from '~/env'; -export default env.POSTHOG_KEY - ? new PostHog(env.POSTHOG_KEY, { +const enabled = Boolean(env.POSTHOG_KEY); +if (!enabled) + console.warn( + 'Missing Posthog environment variable, analytics will be disabled.', + ); + +export default enabled + ? new PostHog(env.POSTHOG_KEY!, { host: 'https://app.posthog.com/', flushAt: 1, flushInterval: 0, diff --git a/apps/discord-bot/src/services/client.ts b/apps/discord-bot/src/services/client.ts new file mode 100644 index 00000000..056c5d42 --- /dev/null +++ b/apps/discord-bot/src/services/client.ts @@ -0,0 +1,25 @@ +import { Client } from '@buape/carbon'; +import { EvaluateCommand } from '~/commands/evaluate'; +import env from '~/env'; + +const enabled = Boolean( + env.DISCORD_CLIENT_ID && env.DISCORD_PUBLIC_KEY && env.DISCORD_TOKEN, +); +if (!enabled) + console.warn( + 'Missing Discord bot environment variables, bot will be disabled.', + ); + +export default enabled + ? new Client( + { + baseUrl: 'unused', + clientId: env.DISCORD_CLIENT_ID!, + publicKey: env.DISCORD_PUBLIC_KEY!, + token: env.DISCORD_TOKEN!, + deploySecret: 'unused', + requestOptions: { queueRequests: false }, + }, + [new EvaluateCommand()], + ) + : null; diff --git a/apps/discord-bot/tsup.config.ts b/apps/discord-bot/tsup.config.ts index 253c619a..f2b32be9 100644 --- a/apps/discord-bot/tsup.config.ts +++ b/apps/discord-bot/tsup.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'tsup'; export default defineConfig({ - entry: ['src/index.ts'], + entry: ['src/index.ts', 'src/env.ts'], format: 'esm', dts: true, }); diff --git a/apps/website/.env.development b/apps/website/.env.development deleted file mode 100644 index 2598d2bc..00000000 --- a/apps/website/.env.development +++ /dev/null @@ -1,2 +0,0 @@ -# Website -WEBSITE_URL="http://localhost:3000" diff --git a/apps/website/.env.example b/apps/website/.env.example deleted file mode 100644 index 6a956dc2..00000000 --- a/apps/website/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -# Website -WEBSITE_URL= - -# PostHog -POSTHOG_KEY= -NEXT_PUBLIC_POSTHOG_KEY=$POSTHOG_KEY - -# Discord Bot -DISCORD_CLIENT_ID= -DISCORD_PUBLIC_KEY= -DISCORD_CLIENT_SECRET= -DISCORD_TOKEN= diff --git a/apps/website/package.json b/apps/website/package.json index 4d022e81..243b5cba 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -4,21 +4,21 @@ "type": "module", "scripts": { "check": "tsc --noEmit", - "build": "next build --no-lint", - "start": "next start", - "dev": "next dev --turbo" + "build": "use-env -p NEXT -P -- next build --no-lint", + "start": "use-env -p NEXT -P -- next start", + "dev": "use-env -p NEXT -- next dev --turbo" }, "dependencies": { "@codemirror/commands": "^6.7.1", "@codemirror/view": "^6.35.0", "@evaluate/components": "workspace:^", "@evaluate/engine": "workspace:^", - "@evaluate/env": "workspace:^", "@evaluate/helpers": "workspace:^", "@evaluate/hooks": "workspace:^", - "@evaluate/style": "workspace:^", "@evaluate/shapes": "workspace:^", + "@evaluate/style": "workspace:^", "@hookform/resolvers": "^3.9.1", + "@t3-oss/env-nextjs": "^0.11.1", "@tanstack/react-query": "^5.62.0", "@tanstack/react-query-devtools": "^5.62.0", "@uiw/codemirror-extensions-langs": "^4.23.6", diff --git a/apps/website/src/app/(editor)/playgrounds/[playground]/content.tsx b/apps/website/src/app/(editor)/playgrounds/[playground]/content.tsx index 7780b5f6..cdf2b8c8 100644 --- a/apps/website/src/app/(editor)/playgrounds/[playground]/content.tsx +++ b/apps/website/src/app/(editor)/playgrounds/[playground]/content.tsx @@ -22,7 +22,7 @@ export default function EditorContent(p: { runtime: Runtime }) { return (
diff --git a/apps/website/src/app/metadata.ts b/apps/website/src/app/metadata.ts index 30c544c8..f160714e 100644 --- a/apps/website/src/app/metadata.ts +++ b/apps/website/src/app/metadata.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import type { Metadata } from 'next/types'; -import { env } from '~/env'; +import env from '~/env'; export function generateBaseMetadata( pathname: string, diff --git a/apps/website/src/app/sitemap.ts b/apps/website/src/app/sitemap.ts index cb689b4c..3fd8cfaf 100644 --- a/apps/website/src/app/sitemap.ts +++ b/apps/website/src/app/sitemap.ts @@ -2,14 +2,14 @@ import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { fetchRuntimes } from '@evaluate/engine/runtimes'; import type { MetadataRoute } from 'next/types'; -import { env } from '~/env'; +import env from '~/env'; interface RoutesManifest { staticRoutes: { page: string }[]; dynamicRoutes: { page: string }[]; } -async function loadStaticPaths(url: string): Promise { +async function loadStaticPaths(url: URL): Promise { const manifestPath = join(process.cwd(), '.next', 'routes-manifest.json'); const manifest = await readFile(manifestPath, 'utf8') .then((c) => JSON.parse(c) as RoutesManifest) @@ -23,7 +23,7 @@ async function loadStaticPaths(url: string): Promise { })); } -async function loadDynamicPaths(url: string): Promise { +async function loadDynamicPaths(url: URL): Promise { const runtimes = await fetchRuntimes(); return runtimes.map((r) => ({ url: `${url}/playgrounds/${r.id}`, diff --git a/apps/website/src/env.ts b/apps/website/src/env.ts index 201185c9..bc435ef6 100644 --- a/apps/website/src/env.ts +++ b/apps/website/src/env.ts @@ -1,27 +1,25 @@ -import { validateEnv } from '@evaluate/env/validator'; +import { URL } from '@evaluate/helpers/url'; +import { createEnv } from '@t3-oss/env-nextjs'; +import { vercel } from '@t3-oss/env-nextjs/presets'; +import discordEnv from 'discord-bot/env'; import { z } from 'zod'; -export const env = validateEnv({ +export default createEnv({ + extends: [discordEnv, vercel()], + server: { WEBSITE_URL: z .string() .url() - .refine((v) => !v.endsWith('/'), 'should not end with a slash'), + .transform((v) => new URL(v).freeze()), }, - prefix: 'NEXT_PUBLIC_', client: { NEXT_PUBLIC_POSTHOG_KEY: z.string().min(1).optional(), }, - variablesStrict: { - WEBSITE_URL: process.env.WEBSITE_URL || `https://${process.env.VERCEL_URL}`, + runtimeEnv: { + WEBSITE_URL: `https://${process.env.VERCEL_URL}`, + ...process.env, NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, }, - - onValid(env) { - if (!env.NEXT_PUBLIC_POSTHOG_KEY) - console.warn( - 'Missing Posthog environment variable, analytics will be disabled.', - ); - }, }); diff --git a/apps/website/src/services/analytics.ts b/apps/website/src/services/analytics.ts index a7799984..ce66ef2f 100644 --- a/apps/website/src/services/analytics.ts +++ b/apps/website/src/services/analytics.ts @@ -1,10 +1,11 @@ import posthog from 'posthog-js'; -import { env } from '~/env'; +import env from '~/env'; -const enabled = +const enabled = Boolean( typeof window !== 'undefined' && - !window.location.origin.endsWith('.vercel.app') && - env.NEXT_PUBLIC_POSTHOG_KEY; + !window.location.origin.endsWith('.vercel.app') && + env.NEXT_PUBLIC_POSTHOG_KEY, +); export default enabled ? posthog : null; if (enabled) { diff --git a/package.json b/package.json index ff240101..bad726fe 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,11 @@ }, "devDependencies": { "@biomejs/biome": "1.9.4", + "@evaluate/scripts": "workspace:^", "@types/node": "^22.10.7", + "husky": "^9.1.7", "resolve-tspaths": "^0.8.23", "tsup": "^8.3.5", - "husky": "^9.1.7", "tsx": "^4.19.2", "turbo": "^2.3.3", "typescript": "^5.7.3" diff --git a/packages/env/package.json b/packages/env/package.json deleted file mode 100644 index d9b64fc9..00000000 --- a/packages/env/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@evaluate/env", - "version": "1.0.0", - "type": "module", - "main": "./dist/validator.js", - "module": "./dist/validator.js", - "exports": { - ".": { - "import": "./dist/validator.js", - "types": "./dist/validator.d.ts" - }, - "./loader": { - "import": "./dist/loader.js", - "types": "./dist/loader.d.ts" - }, - "./validator": { - "import": "./dist/validator.js", - "types": "./dist/validator.d.ts" - } - }, - "scripts": { - "check": "tsc --noEmit", - "build": "tsup" - }, - "dependencies": { - "dotenv": "^16.4.5", - "dotenv-expand": "^11.0.7", - "zod": "3.22.4" - } -} diff --git a/packages/env/src/loader.ts b/packages/env/src/loader.ts deleted file mode 100644 index f18f161a..00000000 --- a/packages/env/src/loader.ts +++ /dev/null @@ -1,65 +0,0 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import * as dotenv from 'dotenv'; -import { expand as dotenvExpand } from 'dotenv-expand'; - -/** - * Load the environment variables from the environment files into the process. - * @returns process.{@link process.env env} - */ -export function loadEnv() { - insertVariables(readEnv()); - return process.env; -} - -/** - * Read the environment variables from the environment files. - * @returns the environment variables - */ -export function readEnv() { - const variables: Record = {}; - const root = process.env.ORIGINAL_DIR ?? '.'; - - for (const file of getListOfEnvFiles().reverse()) { - const location = path.join(root, file); - if (!fs.existsSync(location)) continue; - - const contents = fs.readFileSync(location, 'utf8'); - const parsed = extractVariablesFromContents(contents); - for (const [key, value] of Object.entries(parsed ?? {})) - if (value !== undefined) variables[key] = value; - } - - return expandVariables(variables); -} - -/** - * Insert the variables into the process environment. - * @param variables the variables to insert - */ -export function insertVariables(variables: Record) { - for (const [key, value] of Object.entries(variables)) - process.env[key] ??= value; -} - -/** - * Get a list of the environment files to look for, in order of priority. - * @returns the list of environment files - */ -function getListOfEnvFiles() { - const mode = process.env.NODE_ENV || 'development'; - return [`.env.${mode}.local`, '.env.local', `.env.${mode}`, '.env']; -} - -/** - * Extract the variables from a string. - * @param content the content to extract the variables from - * @returns the extracted variables - */ -function extractVariablesFromContents(content: string) { - return dotenv.parse(content); -} - -function expandVariables(parsed: Record) { - return dotenvExpand({ parsed, processEnv: parsed }).parsed ?? {}; -} diff --git a/packages/env/src/validator.ts b/packages/env/src/validator.ts deleted file mode 100644 index c814f3bd..00000000 --- a/packages/env/src/validator.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { z } from 'zod'; - -type EmptyRecord = Record; - -type Simplify = { - [P in keyof T]: T[P]; -} & {}; - -// - -interface ListenerOptions> { - onValid?: (env: TVariables) => void; - onInvalid?: (error: Zod.ZodError) => void; - onDisallowed?: (name: keyof TVariables) => void; -} - -// - -interface ServerOptions< - TPrefix extends string | undefined, - TShape extends Record, -> { - server?: { - [TKey in keyof TShape]: TPrefix extends undefined - ? TShape[TKey] - : TPrefix extends '' - ? TShape[TKey] - : TKey extends `${TPrefix}${string}` - ? never - : TShape[TKey]; - }; -} - -interface ClientOptions< - TPrefix extends string | undefined, - TShape extends Record, -> { - prefix?: TPrefix; - client?: { - [TKey in keyof TShape]: TKey extends `${TPrefix}${string}` - ? TShape[TKey] - : never; - }; -} - -// - -interface LooseVariableOptions { - variables: Record; -} - -interface StrictVariableOptions { - variablesStrict: Record; -} - -// - -type Options< - TPrefix extends string | undefined, - TServerShape extends Record = EmptyRecord, - TClientShape extends Record = EmptyRecord, - TVariables extends Simplify< - z.infer> & z.infer> - > = Simplify< - z.infer> & z.infer> - >, -> = ListenerOptions & - ServerOptions & - ClientOptions & - (LooseVariableOptions | StrictVariableOptions); - -// - -/** - * Validate the environment variables and return a proxy object. - * @param options the options to use when validating the environment variables - * @returns a proxy object that can be used to access the environment variables - */ -export function validateEnv< - TPrefix extends string | undefined, - TServerShape extends Record = EmptyRecord, - TClientShape extends Record = EmptyRecord, - TVariables extends Simplify< - z.infer> & z.infer> - > = Simplify< - z.infer> & z.infer> - >, ->( - options: Options, -): TVariables { - const variables = - 'variablesStrict' in options ? options.variablesStrict : options.variables; - for (const [key, value] of Object.entries(variables)) - if (value === '') delete variables[key]; - - const clientSchema = z.object(options.client ?? {}); - const serverSchema = z.object(options.server ?? {}).merge(clientSchema); - - const isServer = typeof window === 'undefined'; - const parseResult = isServer - ? serverSchema.safeParse(variables) - : clientSchema.safeParse(variables); - - function onValid(env: TVariables) { - return options.onValid?.(env); - } - - function onInvalid(error: z.ZodError) { - if (options.onInvalid) return options.onInvalid(error); - console.error( - '❌ Invalid environment variables:', - error.flatten().fieldErrors, - ); - throw new Error('Invalid environment variables'); - } - - function isClientVariable(name: keyof TVariables) { - return ( - String(name).startsWith(options.prefix ?? '') && - name in (options.client ?? {}) - ); - } - - if (parseResult.success) { - onValid(parseResult.data as TVariables); - return buildEnvProxy( - parseResult.data as TVariables, // - { isClientVariable, ...options }, - ); - } else { - onInvalid(parseResult.error); - } - - return {} as never; -} - -/** - * Takes a record of environment variables and returns a proxy object. - * @param variables the environment variables to use - * @param options the options to use when creating the proxy object - * @returns a proxy object that can be used to access the environment variables - */ -function buildEnvProxy>( - variables: T, - options: { - isClientVariable: (name: keyof T) => boolean; - onDisallowed?: (name: keyof T) => void; - }, -): T { - function isDisallowed(name: keyof T) { - return typeof window !== 'undefined' && !options.isClientVariable(name); - } - - function onDisallowed(name: keyof T) { - if (options.onDisallowed) return options.onDisallowed(name); - throw new Error( - '❌ Attempted to access a server-side environment variable from the client', - ); - } - - return new Proxy(variables, { - get(target, name: string) { - if (typeof name !== 'string') return Reflect.get(target, name); - if (isDisallowed(name)) return onDisallowed(name); - return Reflect.get(target, name); - }, - }); -} diff --git a/packages/env/tsconfig.json b/packages/env/tsconfig.json deleted file mode 100644 index f4318e0e..00000000 --- a/packages/env/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { "baseUrl": ".", "paths": { "~/*": ["src/*"] } } -} diff --git a/packages/env/tsup.config.ts b/packages/env/tsup.config.ts deleted file mode 100644 index 66de8558..00000000 --- a/packages/env/tsup.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'tsup'; - -export default defineConfig({ - entry: ['src/*.ts'], - format: 'esm', - dts: true, -}); diff --git a/packages/scripts/bin/_logger.js b/packages/scripts/bin/_logger.js new file mode 100644 index 00000000..7ee30b73 --- /dev/null +++ b/packages/scripts/bin/_logger.js @@ -0,0 +1,16 @@ +const colours = { + green: (_) => `\x1b[32m${_}\x1b[0m`, + yellow: (_) => `\x1b[33m${_}\x1b[0m`, + red: (_) => `\x1b[31m${_}\x1b[0m`, + bold: (_) => `\x1b[1m${_}\x1b[0m`, + italic: (_) => `\x1b[3m${_}\x1b[0m`, + dim: (_) => `\x1b[2m${_}\x1b[0m`, +}; + +const logger = { + info: (_) => console.info(`${colours.bold(colours.green(' >_'))} ${_}`), + warn: (_) => console.warn(colours.yellow(`${colours.bold(' >_')} ${_}`)), + error: (_) => console.error(colours.red(`${colours.bold(' >_')} ${_}`)), +}; + +export { logger, colours }; diff --git a/packages/scripts/bin/run-in b/packages/scripts/bin/run-in new file mode 100644 index 00000000..0abe6169 --- /dev/null +++ b/packages/scripts/bin/run-in @@ -0,0 +1,28 @@ +#!/usr/bin/env node + +import { exec, spawn } from 'node:child_process'; +import { sep } from 'node:path'; +import { promisify } from 'node:util'; +import { createCommand } from 'commander'; +const execAsync = promisify(exec); + +const program = createCommand('run-in') + .arguments(' ') + .parse(process.argv); + +const packagePaths = await execAsync('pnpm m ls --depth -1 --porcelain') // + .then(({ stdout }) => stdout.split('\n')); +const packages = packagePaths // + .map((path) => ({ path, name: path.split(sep).pop() ?? '' })); + +const targetPackage = packages.find((p) => p.name === program.args[0]); +const commandToRun = program.args + .slice(1) + .map((a) => `"${a}"`) + .join(' '); + +spawn(commandToRun, { + stdio: 'inherit', + shell: process.env.SHELL, + cwd: targetPackage.path, +}); diff --git a/packages/scripts/bin/use-env b/packages/scripts/bin/use-env new file mode 100644 index 00000000..659d3b35 --- /dev/null +++ b/packages/scripts/bin/use-env @@ -0,0 +1,63 @@ +#!/usr/bin/env node + +import nextEnv from '@next/env'; +import { spawn } from 'cross-spawn'; +const { loadEnvConfig } = nextEnv; +import { join as joinPath, dirname as pathDirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createCommand } from 'commander'; +import { colours, logger } from './_logger.js'; + +// Reorder arguments if there are two `--` separators +const separatorCount = process.argv.filter((a) => a === '--').length; +if (separatorCount === 2) { + const firstIndex = process.argv.indexOf('--'); + const secondIndex = process.argv.indexOf('--', firstIndex + 1); + const secondGroup = process.argv.splice(secondIndex + 1); + process.argv.splice(firstIndex, 0, ...secondGroup); +} + +const program = createCommand('use-env') + .option( + '-p, --public-prefix [prefix]', + 'Append a prefix to each public variable', + ) + .option( + '-P, --prod', + 'Load production environment', + process.env.NODE_ENV === 'production', + ) + .argument('', 'The command to run') + .parse(process.argv); + +const publicPrefix = program.opts().publicPrefix ?? ''; +const isProduction = program.opts().prod; +const commandToRun = program.args.map((a) => `"${a}"`).join(' '); +const thisDirname = pathDirname(fileURLToPath(import.meta.url)); +const targetDirectory = joinPath(thisDirname, '../../../.env'); + +// Load the environment variables +const { loadedEnvFiles } = loadEnvConfig(targetDirectory, !isProduction); + +// Handle public variables with optional prefix +for (const key in process.env) { + if (key.startsWith('PUBLIC_')) { + process.env[key.slice(7)] = process.env[key]; + if (publicPrefix) { + process.env[`${publicPrefix}_${key}`] = process.env[key]; + delete process.env[key]; + } + } +} + +const loadedFrom = loadedEnvFiles.map((f) => f.path).join(' '); +if (loadedFrom) logger.info(`Environment: ${colours.italic(loadedFrom)}`); +else logger.info('No Environment'); +process.env['>_'] = true; + +const child = spawn(commandToRun, { + stdio: 'inherit', + shell: process.env.SHELL || '/bin/bash', + env: process.env, +}); +child.on('exit', process.exit); diff --git a/packages/scripts/package.json b/packages/scripts/package.json new file mode 100644 index 00000000..be75c895 --- /dev/null +++ b/packages/scripts/package.json @@ -0,0 +1,14 @@ +{ + "name": "@evaluate/scripts", + "version": "1.0.0", + "type": "module", + "bin": { + "run-in": "./bin/run-in", + "use-env": "./bin/use-env" + }, + "dependencies": { + "@next/env": "^15.1.4", + "commander": "^13.0.0", + "cross-spawn": "^7.0.6" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82abf80e..49c07eb5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,9 @@ importers: '@biomejs/biome': specifier: 1.9.4 version: 1.9.4 + '@evaluate/scripts': + specifier: workspace:^ + version: link:packages/scripts '@types/node': specifier: ^22.10.7 version: 22.10.7 @@ -49,9 +52,6 @@ importers: '@evaluate/engine': specifier: workspace:^ version: link:../../packages/engine - '@evaluate/env': - specifier: workspace:^ - version: link:../../packages/env '@evaluate/helpers': specifier: workspace:^ version: link:../../packages/helpers @@ -64,6 +64,9 @@ importers: '@evaluate/style': specifier: workspace:^ version: link:../../packages/style + '@t3-oss/env-core': + specifier: ^0.11.1 + version: 0.11.1(typescript@5.2.2)(zod@3.22.4(patch_hash=j5rlizeyx7cjhu35u4l53omnrq)) framer-motion: specifier: ^11.12.0 version: 11.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -110,12 +113,15 @@ importers: '@evaluate/engine': specifier: workspace:^ version: link:../../packages/engine - '@evaluate/env': + '@evaluate/helpers': specifier: workspace:^ - version: link:../../packages/env + version: link:../../packages/helpers '@evaluate/shapes': specifier: workspace:^ version: link:../../packages/shapes + '@t3-oss/env-core': + specifier: ^0.11.1 + version: 0.11.1(typescript@5.7.3)(zod@3.22.4(patch_hash=j5rlizeyx7cjhu35u4l53omnrq)) '@vercel/functions': specifier: ^1.5.1 version: 1.5.1 @@ -140,9 +146,6 @@ importers: '@evaluate/engine': specifier: workspace:^ version: link:../../packages/engine - '@evaluate/env': - specifier: workspace:^ - version: link:../../packages/env '@evaluate/helpers': specifier: workspace:^ version: link:../../packages/helpers @@ -158,6 +161,9 @@ importers: '@hookform/resolvers': specifier: ^3.9.1 version: 3.9.1(react-hook-form@7.53.2(react@18.3.1)) + '@t3-oss/env-nextjs': + specifier: ^0.11.1 + version: 0.11.1(typescript@5.7.3)(zod@3.22.4(patch_hash=j5rlizeyx7cjhu35u4l53omnrq)) '@tanstack/react-query': specifier: ^5.62.0 version: 5.62.0(react@18.3.1) @@ -375,18 +381,6 @@ importers: specifier: ^2.0.3 version: 2.0.3 - packages/env: - dependencies: - dotenv: - specifier: ^16.4.5 - version: 16.4.5 - dotenv-expand: - specifier: ^11.0.7 - version: 11.0.7 - zod: - specifier: 3.22.4 - version: 3.22.4(patch_hash=j5rlizeyx7cjhu35u4l53omnrq) - packages/helpers: dependencies: class-variance-authority: @@ -419,6 +413,18 @@ importers: specifier: ^19.0.4 version: 19.0.7 + packages/scripts: + dependencies: + '@next/env': + specifier: ^15.1.4 + version: 15.1.5 + commander: + specifier: ^13.0.0 + version: 13.1.0 + cross-spawn: + specifier: ^7.0.6 + version: 7.0.6 + packages/shapes: dependencies: zod: @@ -1431,6 +1437,9 @@ packages: '@next/env@14.2.18': resolution: {integrity: sha512-2vWLOUwIPgoqMJKG6dt35fVXVhgM09tw4tK3/Q34GFXDrfiHlG7iS33VA4ggnjWxjiz9KV5xzfsQzJX6vGAekA==} + '@next/env@15.1.5': + resolution: {integrity: sha512-jg8ygVq99W3/XXb9Y6UQsritwhjc+qeiO7QrGZRYOfviyr/HcdnhdBQu4gbp2rBIh2ZyBYTBMWbPw3JSCb0GHw==} + '@next/swc-darwin-arm64@14.2.18': resolution: {integrity: sha512-tOBlDHCjGdyLf0ube/rDUs6VtwNOajaWV+5FV/ajPgrvHeisllEdymY/oDgv2cx561+gJksfMUtqf8crug7sbA==} engines: {node: '>= 10'} @@ -3082,6 +3091,24 @@ packages: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} + '@t3-oss/env-core@0.11.1': + resolution: {integrity: sha512-MaxOwEoG1ntCFoKJsS7nqwgcxLW1SJw238AJwfJeaz3P/8GtkxXZsPPolsz1AdYvUTbe3XvqZ/VCdfjt+3zmKw==} + peerDependencies: + typescript: '>=5.0.0' + zod: ^3.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@t3-oss/env-nextjs@0.11.1': + resolution: {integrity: sha512-rx2XL9+v6wtOqLNJbD5eD8OezKlQD1BtC0WvvtHwBgK66jnF5+wGqtgkKK4Ygie1LVmoDClths2T4tdFmRvGrQ==} + peerDependencies: + typescript: '>=5.0.0' + zod: ^3.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@tanstack/query-core@5.62.0': resolution: {integrity: sha512-sx38bGrqF9bop92AXOvzDr0L9fWDas5zXdPglxa9cuqeVSWS7lY6OnVyl/oodfXjgOGRk79IfCpgVmxrbHuFHg==} @@ -3575,6 +3602,10 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -3755,10 +3786,6 @@ packages: resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} engines: {node: '>=12'} - dotenv-expand@11.0.7: - resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} - engines: {node: '>=12'} - dotenv-expand@5.1.0: resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} @@ -3766,10 +3793,6 @@ packages: resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} engines: {node: '>=12'} - dotenv@16.4.5: - resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} - engines: {node: '>=12'} - dotenv@7.0.0: resolution: {integrity: sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==} engines: {node: '>=6'} @@ -6651,6 +6674,8 @@ snapshots: '@next/env@14.2.18': {} + '@next/env@15.1.5': {} + '@next/swc-darwin-arm64@14.2.18': optional: true @@ -8612,6 +8637,25 @@ snapshots: dependencies: defer-to-connect: 2.0.1 + '@t3-oss/env-core@0.11.1(typescript@5.2.2)(zod@3.22.4(patch_hash=j5rlizeyx7cjhu35u4l53omnrq))': + dependencies: + zod: 3.22.4(patch_hash=j5rlizeyx7cjhu35u4l53omnrq) + optionalDependencies: + typescript: 5.2.2 + + '@t3-oss/env-core@0.11.1(typescript@5.7.3)(zod@3.22.4(patch_hash=j5rlizeyx7cjhu35u4l53omnrq))': + dependencies: + zod: 3.22.4(patch_hash=j5rlizeyx7cjhu35u4l53omnrq) + optionalDependencies: + typescript: 5.7.3 + + '@t3-oss/env-nextjs@0.11.1(typescript@5.7.3)(zod@3.22.4(patch_hash=j5rlizeyx7cjhu35u4l53omnrq))': + dependencies: + '@t3-oss/env-core': 0.11.1(typescript@5.7.3)(zod@3.22.4(patch_hash=j5rlizeyx7cjhu35u4l53omnrq)) + zod: 3.22.4(patch_hash=j5rlizeyx7cjhu35u4l53omnrq) + optionalDependencies: + typescript: 5.7.3 + '@tanstack/query-core@5.62.0': {} '@tanstack/query-devtools@5.61.4': {} @@ -9161,6 +9205,8 @@ snapshots: commander@12.1.0: {} + commander@13.1.0: {} + commander@2.20.3: optional: true @@ -9327,16 +9373,10 @@ snapshots: dotenv-expand@10.0.0: {} - dotenv-expand@11.0.7: - dependencies: - dotenv: 16.4.5 - dotenv-expand@5.1.0: {} dotenv@16.3.1: {} - dotenv@16.4.5: {} - dotenv@7.0.0: {} duplexer@0.1.2: {}