Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 🙏
Expand Down
57 changes: 32 additions & 25 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<string, unknown>,
),
}
return variables
}

async function validateAndLog(ui: UI, env: Record<string, string>, options: PluginOptions) {
Expand Down Expand Up @@ -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 = <T extends PluginOptions>(config: T): T => config

export { schema as Schema } from '@poppinss/validator-lite'
Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -23,6 +24,9 @@ export type StandardSchema = RecordViteKeys<StandardSchemaV1>

export type Schema = PoppinsSchema | StandardSchema

export type ConfigOptions = Pick<UserConfig, 'envDir' | 'envPrefix' | 'root'> &
Pick<ConfigEnv, 'mode'>

/**
* Infer the schema type from the plugin options
*/
Expand Down
32 changes: 31 additions & 1 deletion tests/common.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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
})
})