diff --git a/README.md b/README.md index 6bcc465..7412b59 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..8540eb2 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,32 @@ 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: 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 } + }, } } +/** + * 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 = async (config: ConfigOptions, options?: PluginOptions) => { + const ui = initUi() + 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 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 */ 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 + }) +})