From 25cdfaeda4d33eb601a4e955734ba7beccb216b2 Mon Sep 17 00:00:00 2001 From: Wee Bit Date: Thu, 22 May 2025 04:58:13 +0200 Subject: [PATCH 1/2] feat: expose function for validation without running Vite --- README.md | 27 +++++++++++++++++++++++ src/index.ts | 62 +++++++++++++++++++++++++++++++--------------------- src/types.ts | 4 ++++ 3 files changed, 68 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 6bcc465..8d0d20e 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,33 @@ interface ImportMetaEnv extends ImportMetaEnvAugmented { } ``` +## Validation without Vite + +In some cases, you might want to validate environment variables outside of Vite and reuse the same schema. You can do so by using the `loadAndValidateEnv` function directly. This function will validate and also load the environment variables inside the `process.env` object. + +> ![WARNING] +> `process.env` only accept string values, so don't be surprised if a `number` or `boolean` variable comes back as a string when accessing it after validation. + +```ts +import { loadAndValidateEnv } from '@julr/vite-plugin-validate-env'; + +const env = await loadAndValidateEnv( + { + mode: 'development', // required + root: import.meta.dirname, // optional + }, + { + // Plugin options. Also optional if you + // are using a dedicated `env.ts` file + validator: 'builtin', + schema: { VITE_MY_VAR: Schema.string() }, + }, +); + +console.log(env.VITE_MY_VAR); +console.log(process.env.VITE_MY_VAR) +``` + ## 💖 Sponsors If you find this useful, consider [sponsoring me](https://github.com/sponsors/Julien-R44)! It helps support and maintain the project 🙏 diff --git a/src/index.ts b/src/index.ts index ce710b5..dd2d676 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,12 @@ import path from 'node:path' import { cwd } from 'node:process' +import { type Plugin } from 'vite' import { createConfigLoader as createLoader } from 'unconfig' -import { type ConfigEnv, type Plugin, type UserConfig } from 'vite' import { initUi, type UI } from './ui.js' import { builtinValidation } from './validators/builtin/index.js' import { standardValidation } from './validators/standard/index.js' -import type { FullPluginOptions, PluginOptions, Schema } from './types.js' +import type { ConfigOptions, FullPluginOptions, PluginOptions, Schema } from './types.js' /** * Load schema defined in `env.ts` file using unconfig @@ -71,42 +71,27 @@ function shouldLogVariables(options: PluginOptions) { /** * Main function. Will call each validator defined in the schema and throw an error if any of them fails. */ -async function validateEnv( - ui: UI, - userConfig: UserConfig, - envConfig: ConfigEnv, - inlineOptions?: PluginOptions, -) { +async function validateEnv(ui: UI, config: ConfigOptions, inlineOptions?: PluginOptions) { /** * Dynamic import of Vite helpers to using the ESM build of Vite and * avoiding CJS since it will be deprecated * See : https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated */ const { normalizePath, loadEnv } = await import('vite') - const rootDir = userConfig.root || cwd() + const rootDir = config.root || cwd() - const resolvedRoot = normalizePath( - userConfig.root ? path.resolve(userConfig.root) : process.cwd(), - ) + const resolvedRoot = normalizePath(config.root ? path.resolve(config.root) : process.cwd()) - const envDir = userConfig.envDir - ? normalizePath(path.resolve(resolvedRoot, userConfig.envDir)) + const envDir = config.envDir + ? normalizePath(path.resolve(resolvedRoot, config.envDir)) : resolvedRoot - const env = loadEnv(envConfig.mode, envDir, userConfig.envPrefix) + const env = loadEnv(config.mode, envDir, config.envPrefix) const options = await loadOptions(rootDir, inlineOptions) const variables = await validateAndLog(ui, env, options) - return { - define: variables.reduce( - (acc, { key, value }) => { - acc[`import.meta.env.${key}`] = JSON.stringify(value) - return acc - }, - {} as Record, - ), - } + return variables } async function validateAndLog(ui: UI, env: Record, options: PluginOptions) { @@ -139,10 +124,37 @@ export const ValidateEnv = (options?: PluginOptions): Plugin => { // @ts-expect-error - only used for testing as we need to keep each instance of the plugin unique to a test ui: process.env.NODE_ENV === 'testing' ? ui : undefined, name: 'vite-plugin-validate-env', - config: (config, env) => validateEnv(ui, config, env, options), + config: ({ envDir, envPrefix, root }, { mode }) => + validateEnv(ui, { envDir, envPrefix, root, mode }, options).then((variables) => ({ + define: variables.reduce( + (acc, { key, value }) => { + acc[`import.meta.env.${key}`] = JSON.stringify(value) + return acc + }, + {} as Record, + ), + })), } } +/** + * Load environment variables using the provided Vite configuration + * and validate them against a schema the same way `ValidateEnv` does it + * @returns An object mapping environment variable names to validation results + */ +export const loadAndValidateEnv = (config: ConfigOptions, options?: PluginOptions) => { + const ui = initUi() + return validateEnv(ui, config, options).then((variables) => + variables.reduce( + (acc, { key, value }) => { + acc[key] = value + return acc + }, + {} as Record, + ), + ) +} + export const defineConfig = (config: T): T => config export { schema as Schema } from '@poppinss/validator-lite' diff --git a/src/types.ts b/src/types.ts index 43c9d15..55a39c6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,5 @@ import type { z } from 'zod' +import type { ConfigEnv, UserConfig } from 'vite' import type { StandardSchemaV1 } from '@standard-schema/spec' import type { ValidateFn } from '@poppinss/validator-lite/types' @@ -23,6 +24,9 @@ export type StandardSchema = RecordViteKeys export type Schema = PoppinsSchema | StandardSchema +export type ConfigOptions = Pick & + Pick + /** * Infer the schema type from the plugin options */ From fafb3b283c5e10ab2c063838dfb2e60320882640 Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Mon, 26 May 2025 00:05:48 +0200 Subject: [PATCH 2/2] refactor: some minor refactor --- README.md | 2 +- src/index.ts | 41 ++++++++++++++++++----------------------- tests/common.spec.ts | 32 +++++++++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 8d0d20e..7412b59 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ interface ImportMetaEnv extends ImportMetaEnvAugmented { In some cases, you might want to validate environment variables outside of Vite and reuse the same schema. You can do so by using the `loadAndValidateEnv` function directly. This function will validate and also load the environment variables inside the `process.env` object. -> ![WARNING] +> [!WARNING] > `process.env` only accept string values, so don't be surprised if a `number` or `boolean` variable comes back as a string when accessing it after validation. ```ts diff --git a/src/index.ts b/src/index.ts index dd2d676..8540eb2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -124,35 +124,30 @@ export const ValidateEnv = (options?: PluginOptions): Plugin => { // @ts-expect-error - only used for testing as we need to keep each instance of the plugin unique to a test ui: process.env.NODE_ENV === 'testing' ? ui : undefined, name: 'vite-plugin-validate-env', - config: ({ envDir, envPrefix, root }, { mode }) => - validateEnv(ui, { envDir, envPrefix, root, mode }, options).then((variables) => ({ - define: variables.reduce( - (acc, { key, value }) => { - acc[`import.meta.env.${key}`] = JSON.stringify(value) - return acc - }, - {} as Record, - ), - })), + config: async ({ envDir, envPrefix, root }, { mode }) => { + const env = await validateEnv(ui, { envDir, envPrefix, root, mode }, options) + const define = Object.fromEntries( + env.map(({ key, value }) => [`import.meta.env.${key}`, JSON.stringify(value)]), + ) + + return { define } + }, } } /** - * Load environment variables using the provided Vite configuration - * and validate them against a schema the same way `ValidateEnv` does it - * @returns An object mapping environment variable names to validation results + * Validate environment variables and load them inside `process.env` + * Can be useful when you want to validate outside of Vite's build process. */ -export const loadAndValidateEnv = (config: ConfigOptions, options?: PluginOptions) => { +export const loadAndValidateEnv = async (config: ConfigOptions, options?: PluginOptions) => { const ui = initUi() - return validateEnv(ui, config, options).then((variables) => - variables.reduce( - (acc, { key, value }) => { - acc[key] = value - return acc - }, - {} as Record, - ), - ) + const variables = await validateEnv(ui, config, options) + + for (const { key, value } of variables) { + process.env[key] = value + } + + return Object.fromEntries(variables.map(({ key, value }) => [key, value])) } export const defineConfig = (config: T): T => config diff --git a/tests/common.spec.ts b/tests/common.spec.ts index e182786..d4fbc62 100644 --- a/tests/common.spec.ts +++ b/tests/common.spec.ts @@ -1,6 +1,6 @@ import { test } from '@japa/runner' -import { Schema } from '../src/index.js' +import { loadAndValidateEnv, Schema } from '../src/index.js' import { createEnvFile, executeValidateEnv, ValidateEnv } from './helpers.js' test.group('vite-plugin-validate-env', () => { @@ -183,3 +183,33 @@ test.group('vite-plugin-validate-env', () => { assert.isDefined(messages.find((message) => message.includes('cyan(VITE_TESTX): not boolean'))) }) }) + +test.group('vite-plugin-validate-env - node usage', () => { + test('Basic validation', async ({ assert, fs }) => { + await createEnvFile({ VITE_TEST: 'not boolean' }) + const validate = async () => + await loadAndValidateEnv( + { mode: 'development', root: fs.basePath }, + { VITE_TEST: Schema.boolean() }, + ) + + await assert.rejects(async () => { + await validate() + }, /"VITE_TEST" env variable must be a boolean \(Current value: "not boolean"\)/) + + delete process.env.VITE_TEST + }) + + test('assign to process.env', async ({ assert, fs }) => { + await createEnvFile({ VITE_TEST: 'true' }) + const result = await loadAndValidateEnv( + { mode: 'development', root: fs.basePath }, + { VITE_TEST: Schema.boolean() }, + ) + + assert.deepEqual(result, { VITE_TEST: true }) + assert.equal(process.env.VITE_TEST, 'true') + + delete process.env.VITE_TEST + }) +})