import { isPromise } from "#Source/basic/index.ts" import type { StringAutoCompletable } from "#Source/type/index.ts" import type { Standard } from "#Source/validation/index.ts" import { useRuntimes } from "../runtime.ts" /** * @description Define a key-value map for environment variables. */ export type AnyVariables = Record export type AnyVariablesContent = string /** * @description Options for parsing dotenv-style variables content. */ export interface ParseVariablesOptions { variablesContent: AnyVariablesContent } /** * @description Parses an environment variables string into an object. */ export const parseVariables = ( _options: ParseVariablesOptions, ): Variables => { throw new Error("Parsing variables from string content is not implemented yet.") } export interface SerializeVariablesOptions { variables: AnyVariables } /** * @description Serializes an object of environment variables into a dotenv-style string. */ export const serializeVariables = (options: SerializeVariablesOptions): AnyVariablesContent => { const { variables } = options const lines = Object.entries(variables).map(([key, value]) => `${key}=${value}`) return lines.join("\n") } export interface VariablesPredefined { /** * @description Can be used to change the default timezone at runtime * * @see {@link NodeJS.ProcessEnv}, {@link Bun.Env} */ TZ?: string | undefined /** * @see {@link Bun.Env} * @see {@link https://vite.dev/guide/env-and-mode} * @see {@link https://vitest.dev/config/} */ NODE_ENV?: | StringAutoCompletable | "test" | "benchmark" | "personal" | "development" | "production" | undefined MODE?: | StringAutoCompletable | "test" | "benchmark" | "personal" | "development" | "production" | undefined VITEST?: StringAutoCompletable | "true" | "false" | undefined TEST?: StringAutoCompletable | "true" | "false" | undefined DEV?: StringAutoCompletable | "true" | "false" | undefined PROD?: StringAutoCompletable | "true" | "false" | undefined SSR?: StringAutoCompletable | "true" | "false" | undefined BASE_URL?: string | undefined } export type VariablesMode = NonNullable /** * @description 通过此函数获取的 Mode 可以用来加载指定的环境变量文件。 */ export const getVariablesMode = (): VariablesMode => { const variablesPredefined: VariablesPredefined = useRuntimes({ browser: (context) => { const variables = context.importMeta.env return variables }, nodejs: (context) => { const variables = {} Object.assign(variables, context.global.process.env) Object.assign(variables, context.global.process.env) return variables }, bun: (context) => { const variables = {} Object.assign(variables, context.global.process.env) Object.assign(variables, context.global.process.env) return variables }, }) as Record /** * @description 首先检查 MODE,如果 MODE 存在则直接使用 MODE 作为当前环境的标识。 */ if (variablesPredefined.MODE !== undefined) { return variablesPredefined.MODE as VariablesMode } /** * @description Vitest (CLI 和 VSCode Extension)在运行的时候会将 process.env.VITEST 和 * import.meta.env.VITEST 都设置为 "true",因此可以通过检查这些变量来判断当前 * 是否在 Vitest 环境中运行。 * * 附:使用 Vitest VSCode Extension 运行测试时,各个环境变量的值如下: * [ * process.env.NODE_ENV, // "test" * import.meta.env.NODE_ENV, // "test" * process.env.MODE, // "test" * import.meta.env.MODE, // "test" * process.env.VITEST, // "true" * import.meta.env.VITEST, // "true" * process.env.TEST, // "true" * import.meta.env.TEST, // "true" * process.env.DEV, // "1" * import.meta.env.DEV, // true * process.env.PROD, // "" * import.meta.env.PROD, // false * process.env.SSR, // "1" * import.meta.env.SSR, // true * ] */ if (variablesPredefined.VITEST === "true") { return "test" } if (variablesPredefined.TEST === "true") { return "test" } if (variablesPredefined.NODE_ENV !== undefined) { return variablesPredefined.NODE_ENV as VariablesMode } if (String(variablesPredefined.DEV) === "true") { return "development" } if (String(variablesPredefined.PROD) === "true") { return "production" } throw new Error("Unable to determine the current mode from environment variables.") } /** * @description Describe a writable host for resolved environment variables. */ export interface VariablesHost extends VariablesPredefined { /** * @see {@link ImportMetaEnv} */ [key: string]: string | undefined } /** * @description Get the variables host for the current runtime. */ export const getVariablesHost = (): VariablesHost => { const variablesHost = useRuntimes({ browser: (context) => { return context.importMeta.env }, nodejs: (context) => { return context.global.process.env }, bun: (context) => { return context.global.process.env }, }) return variablesHost } /** * @description Options for loading parsed variables into a host. */ export interface LoadVariablesOptions { variables: AnyVariables variablesPrefixs: string[] variablesHost: VariablesHost } /** * @description Result of loading variables into host storage. */ export interface LoadVariablesResult { variablesToLoad: AnyVariables variablesLoaded: AnyVariables variablesHost: VariablesHost } /** * @description Load environment variables from the provided `variables` object to the `variablesHost`. * Variables without prefix will be ignored. * * @see {@link https://github.com/vitejs/vite/blob/main/packages/vite/src/node/env.ts#L27-L95} */ export const loadVariables = (options: LoadVariablesOptions): LoadVariablesResult => { const { variables, variablesPrefixs, variablesHost } = options const variablesLoaded: AnyVariables = {} // only keys that start with prefix are exposed to client for (const [key, value] of Object.entries(variables)) { if (variablesPrefixs.some((prefix) => key.startsWith(prefix))) { variablesHost[key] = value variablesLoaded[key] = value } } // check if there are actual env variables starting with variablesPrefix // these are typically provided inline and should be prioritized for (const key in variablesHost) { if (variablesPrefixs.some((prefix) => key.startsWith(prefix))) { variablesLoaded[key] = String(variablesHost[key]) } } return { variablesToLoad: variables, variablesLoaded, variablesHost, } } /** * @description Define options for validating variables with a standard schema. */ export interface ValidateVariablesOptions { variables: AnyVariables variablesSchema: VariablesSchema } export interface ValidateVariablesResult { variablesToValidate: AnyVariables variablesSchema: VariablesSchema variablesValidated: Standard.Schema.InferOutput } /** * @description Validates the provided variables against the given schema. * * @see {@link https://github.com/t3-oss/t3-env} */ export const validateVariables = ( options: ValidateVariablesOptions, ): ValidateVariablesResult => { const { variables, variablesSchema } = options const validateResult = variablesSchema["~standard"].validate(variables) if (isPromise(validateResult)) { throw new Error("Validation result is a promise, expected synchronous validation.") } if (validateResult.issues !== undefined) { throw new Error( `Variable validation failed, number of issues: ${validateResult.issues.length}, first issue: ${JSON.stringify(validateResult.issues[0])}`, ) } const variableValidated = validateResult.value return { variablesToValidate: variables, variablesSchema, variablesValidated: variableValidated, } } /** * @description Gets the hosted variables. */ export const getVariablesHosted = (): AnyVariables => { const variableHost = getVariablesHost() return variableHost as AnyVariables } /** * @description Define options for parsing, loading, and validating variables. */ export interface GetVariablesOptions { variablesPrefixs?: LoadVariablesOptions["variablesPrefixs"] | undefined variablesHost?: LoadVariablesOptions["variablesHost"] | undefined variablesSchema: ValidateVariablesOptions["variablesSchema"] } export interface GetVariablesResult { variablesParsed: AnyVariables variablesLoaded: AnyVariables variablesHost: VariablesHost variablesSchema: VariablesSchema variablesValidated: Standard.Schema.InferOutput } /** * @description Gets the variables based on the provided options. */ export const getVariables = ( options: GetVariablesOptions, ): GetVariablesResult => { const loadResult = loadVariables({ variables: {}, variablesPrefixs: options.variablesPrefixs ?? [""], variablesHost: options.variablesHost ?? getVariablesHost(), }) const variableToValidate = loadResult.variablesHost as AnyVariables const validateResult = validateVariables({ variables: variableToValidate, variablesSchema: options.variablesSchema, }) return { variablesParsed: {}, variablesLoaded: loadResult.variablesLoaded, variablesHost: loadResult.variablesHost, variablesSchema: validateResult.variablesSchema, variablesValidated: validateResult.variablesValidated, } } /** * @description Define options used to construct a variables manager. */ export interface VariablesManagerOptions { variablesPrefixs?: LoadVariablesOptions["variablesPrefixs"] | undefined variablesHost?: LoadVariablesOptions["variablesHost"] | undefined variablesSchema: VariablesSchema } /** * @description Manage typed environment variables with cached access. */ export class VariablesManager { protected options: VariablesManagerOptions protected getVariablesResult: GetVariablesResult constructor(options: VariablesManagerOptions) { this.options = options this.getVariablesResult = this.getFreshGetVariablesResult() } /** * @description Refresh variables from current options and schema. */ getFreshGetVariablesResult(): GetVariablesResult { const getVariablesResult = getVariables({ variablesPrefixs: this.options.variablesPrefixs, variablesHost: this.options.variablesHost, variablesSchema: this.options.variablesSchema, }) this.getVariablesResult = getVariablesResult return getVariablesResult } getFreshValidatedVariables(): Standard.Schema.InferOutput { const getVariablesResult = this.getFreshGetVariablesResult() return getVariablesResult.variablesValidated } /** * @description Return the schema used by this manager. */ getVariablesSchema(): VariablesSchema { return this.options.variablesSchema } /** * @description Return the cached validated variables snapshot. */ getValidatedVariables(): Standard.Schema.InferOutput { return this.getVariablesResult.variablesValidated } /** * @description Get a specific validated variable by name from the cached variables result. */ getValidatedVariable>( variableName: Key, ): Standard.Schema.InferOutput[Key] { const validatedVariables = this.getValidatedVariables() const validatedVariable = validatedVariables[variableName] return validatedVariable } }