Skip to content

Commit 47026f1

Browse files
aweebitJulien-R44
andauthored
feat: expose function for validation without running Vite (#36)
Co-authored-by: Julien Ripouteau <[email protected]>
1 parent 15d4300 commit 47026f1

File tree

4 files changed

+94
-26
lines changed

4 files changed

+94
-26
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,33 @@ interface ImportMetaEnv extends ImportMetaEnvAugmented {
173173
}
174174
```
175175

176+
## Validation without Vite
177+
178+
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.
179+
180+
> [!WARNING]
181+
> `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.
182+
183+
```ts
184+
import { loadAndValidateEnv } from '@julr/vite-plugin-validate-env';
185+
186+
const env = await loadAndValidateEnv(
187+
{
188+
mode: 'development', // required
189+
root: import.meta.dirname, // optional
190+
},
191+
{
192+
// Plugin options. Also optional if you
193+
// are using a dedicated `env.ts` file
194+
validator: 'builtin',
195+
schema: { VITE_MY_VAR: Schema.string() },
196+
},
197+
);
198+
199+
console.log(env.VITE_MY_VAR);
200+
console.log(process.env.VITE_MY_VAR)
201+
```
202+
176203
## 💖 Sponsors
177204

178205
If you find this useful, consider [sponsoring me](https://github.com/sponsors/Julien-R44)! It helps support and maintain the project 🙏

src/index.ts

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import path from 'node:path'
22
import { cwd } from 'node:process'
3+
import { type Plugin } from 'vite'
34
import { createConfigLoader as createLoader } from 'unconfig'
4-
import { type ConfigEnv, type Plugin, type UserConfig } from 'vite'
55

66
import { initUi, type UI } from './ui.js'
77
import { builtinValidation } from './validators/builtin/index.js'
88
import { standardValidation } from './validators/standard/index.js'
9-
import type { FullPluginOptions, PluginOptions, Schema } from './types.js'
9+
import type { ConfigOptions, FullPluginOptions, PluginOptions, Schema } from './types.js'
1010

1111
/**
1212
* Load schema defined in `env.ts` file using unconfig
@@ -71,42 +71,27 @@ function shouldLogVariables(options: PluginOptions) {
7171
/**
7272
* Main function. Will call each validator defined in the schema and throw an error if any of them fails.
7373
*/
74-
async function validateEnv(
75-
ui: UI,
76-
userConfig: UserConfig,
77-
envConfig: ConfigEnv,
78-
inlineOptions?: PluginOptions,
79-
) {
74+
async function validateEnv(ui: UI, config: ConfigOptions, inlineOptions?: PluginOptions) {
8075
/**
8176
* Dynamic import of Vite helpers to using the ESM build of Vite and
8277
* avoiding CJS since it will be deprecated
8378
* See : https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated
8479
*/
8580
const { normalizePath, loadEnv } = await import('vite')
86-
const rootDir = userConfig.root || cwd()
81+
const rootDir = config.root || cwd()
8782

88-
const resolvedRoot = normalizePath(
89-
userConfig.root ? path.resolve(userConfig.root) : process.cwd(),
90-
)
83+
const resolvedRoot = normalizePath(config.root ? path.resolve(config.root) : process.cwd())
9184

92-
const envDir = userConfig.envDir
93-
? normalizePath(path.resolve(resolvedRoot, userConfig.envDir))
85+
const envDir = config.envDir
86+
? normalizePath(path.resolve(resolvedRoot, config.envDir))
9487
: resolvedRoot
9588

96-
const env = loadEnv(envConfig.mode, envDir, userConfig.envPrefix)
89+
const env = loadEnv(config.mode, envDir, config.envPrefix)
9790

9891
const options = await loadOptions(rootDir, inlineOptions)
9992
const variables = await validateAndLog(ui, env, options)
10093

101-
return {
102-
define: variables.reduce(
103-
(acc, { key, value }) => {
104-
acc[`import.meta.env.${key}`] = JSON.stringify(value)
105-
return acc
106-
},
107-
{} as Record<string, unknown>,
108-
),
109-
}
94+
return variables
11095
}
11196

11297
async function validateAndLog(ui: UI, env: Record<string, string>, options: PluginOptions) {
@@ -139,10 +124,32 @@ export const ValidateEnv = (options?: PluginOptions): Plugin => {
139124
// @ts-expect-error - only used for testing as we need to keep each instance of the plugin unique to a test
140125
ui: process.env.NODE_ENV === 'testing' ? ui : undefined,
141126
name: 'vite-plugin-validate-env',
142-
config: (config, env) => validateEnv(ui, config, env, options),
127+
config: async ({ envDir, envPrefix, root }, { mode }) => {
128+
const env = await validateEnv(ui, { envDir, envPrefix, root, mode }, options)
129+
const define = Object.fromEntries(
130+
env.map(({ key, value }) => [`import.meta.env.${key}`, JSON.stringify(value)]),
131+
)
132+
133+
return { define }
134+
},
143135
}
144136
}
145137

138+
/**
139+
* Validate environment variables and load them inside `process.env`
140+
* Can be useful when you want to validate outside of Vite's build process.
141+
*/
142+
export const loadAndValidateEnv = async (config: ConfigOptions, options?: PluginOptions) => {
143+
const ui = initUi()
144+
const variables = await validateEnv(ui, config, options)
145+
146+
for (const { key, value } of variables) {
147+
process.env[key] = value
148+
}
149+
150+
return Object.fromEntries(variables.map(({ key, value }) => [key, value]))
151+
}
152+
146153
export const defineConfig = <T extends PluginOptions>(config: T): T => config
147154

148155
export { schema as Schema } from '@poppinss/validator-lite'

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { z } from 'zod'
2+
import type { ConfigEnv, UserConfig } from 'vite'
23
import type { StandardSchemaV1 } from '@standard-schema/spec'
34
import type { ValidateFn } from '@poppinss/validator-lite/types'
45

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

2425
export type Schema = PoppinsSchema | StandardSchema
2526

27+
export type ConfigOptions = Pick<UserConfig, 'envDir' | 'envPrefix' | 'root'> &
28+
Pick<ConfigEnv, 'mode'>
29+
2630
/**
2731
* Infer the schema type from the plugin options
2832
*/

tests/common.spec.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test } from '@japa/runner'
22

3-
import { Schema } from '../src/index.js'
3+
import { loadAndValidateEnv, Schema } from '../src/index.js'
44
import { createEnvFile, executeValidateEnv, ValidateEnv } from './helpers.js'
55

66
test.group('vite-plugin-validate-env', () => {
@@ -183,3 +183,33 @@ test.group('vite-plugin-validate-env', () => {
183183
assert.isDefined(messages.find((message) => message.includes('cyan(VITE_TESTX): not boolean')))
184184
})
185185
})
186+
187+
test.group('vite-plugin-validate-env - node usage', () => {
188+
test('Basic validation', async ({ assert, fs }) => {
189+
await createEnvFile({ VITE_TEST: 'not boolean' })
190+
const validate = async () =>
191+
await loadAndValidateEnv(
192+
{ mode: 'development', root: fs.basePath },
193+
{ VITE_TEST: Schema.boolean() },
194+
)
195+
196+
await assert.rejects(async () => {
197+
await validate()
198+
}, /"VITE_TEST" env variable must be a boolean \(Current value: "not boolean"\)/)
199+
200+
delete process.env.VITE_TEST
201+
})
202+
203+
test('assign to process.env', async ({ assert, fs }) => {
204+
await createEnvFile({ VITE_TEST: 'true' })
205+
const result = await loadAndValidateEnv(
206+
{ mode: 'development', root: fs.basePath },
207+
{ VITE_TEST: Schema.boolean() },
208+
)
209+
210+
assert.deepEqual(result, { VITE_TEST: true })
211+
assert.equal(process.env.VITE_TEST, 'true')
212+
213+
delete process.env.VITE_TEST
214+
})
215+
})

0 commit comments

Comments
 (0)