Skip to content

Commit

Permalink
fix: show zod default values
Browse files Browse the repository at this point in the history
If a zod value is defaulted we should show the default, not `undefined`.

This also fixes the tests so that the logs do not bleed into other tests.
  • Loading branch information
SimeonC authored and Julien-R44 committed Nov 28, 2023
1 parent ec546b3 commit 4a1e93b
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 36 deletions.
61 changes: 39 additions & 22 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { cwd } from 'node:process'
import { createConfigLoader as createLoader } from 'unconfig'
import { type ConfigEnv, type Plugin, type UserConfig, loadEnv, normalizePath } from 'vite'

import { ui } from './utils/cliui.js'
import { initUi } from './utils/cliui.js'
import type { UI } from './utils/cliui.js'
import { zodValidation } from './validators/zod/index.js'
import { builtinValidation } from './validators/builtin/index.js'
import type { FullPluginOptions, PluginOptions, Schema } from './contracts/index.js'
Expand Down Expand Up @@ -47,11 +48,11 @@ function getNormalizedOptions(options: PluginOptions) {
/**
* Log environment variables
*/
function logVariables(schema: Schema, env: Record<string, string>) {
function logVariables(ui: UI, variables: { key: string; value: any }[]) {
ui.logger.log(`${ui.colors.cyan('[vite-plugin-validate-env]')} debug process.env content`)

for (const [key] of Object.entries(schema)) {
ui.logger.log(`${ui.icons.pointer} ${ui.colors.cyan(key)}: ${env[key]}`)
for (const { key, value } of variables) {
ui.logger.log(`${ui.icons.pointer} ${ui.colors.cyan(key)}: ${value}`)
}
}

Expand All @@ -62,7 +63,12 @@ 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(userConfig: UserConfig, envConfig: ConfigEnv, options?: PluginOptions) {
async function validateEnv(
ui: UI,
userConfig: UserConfig,
envConfig: ConfigEnv,
options?: PluginOptions,
) {
const rootDir = userConfig.root || cwd()

const resolvedRoot = normalizePath(
Expand All @@ -84,19 +90,7 @@ async function validateEnv(userConfig: UserConfig, envConfig: ConfigEnv, options
throw new Error('Missing configuration for vite-plugin-validate-env')
}

const { schema, validator } = getNormalizedOptions(options)
const validatorFn = {
builtin: builtinValidation,
zod: zodValidation,
}[validator]

if (!validatorFn) {
throw new Error(`Invalid validator "${validator}"`)
}

if (shouldLogVariables(options)) logVariables(schema, env)

const variables = await validatorFn(env, schema as any)
const variables = await validateAndLog(ui, env, options)

return {
define: variables.reduce(
Expand All @@ -109,13 +103,36 @@ async function validateEnv(userConfig: UserConfig, envConfig: ConfigEnv, options
}
}

async function validateAndLog(ui: UI, env: ReturnType<typeof loadEnv>, options: PluginOptions) {
const { schema, validator } = getNormalizedOptions(options)
const showDebug = shouldLogVariables(options)
const validate = { zod: zodValidation, builtin: builtinValidation }[validator]
try {
const variables = await validate(ui, env, schema as any)
if (showDebug) logVariables(ui, variables)
return variables
} catch (error) {
if (showDebug)
logVariables(
ui,
Object.entries(schema).map(([key]) => ({ key, value: env[key] })),
)
throw error
}
}

/**
* Validate environment variables against a schema
*/
export const ValidateEnv = (options?: PluginOptions): Plugin => ({
name: 'vite-plugin-validate-env',
config: (config, env) => validateEnv(config, env, options),
})
export const ValidateEnv = (options?: PluginOptions): Plugin => {
const ui = initUi()
return {
// @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),
}
}

export const defineConfig = <T extends PluginOptions>(config: T): T => config

Expand Down
6 changes: 5 additions & 1 deletion src/utils/cliui.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { cliui } from '@poppinss/cliui'

export const ui = cliui({ mode: process.env.NODE_ENV === 'testing' ? 'raw' : 'normal' })
export type UI = ReturnType<typeof initUi>

export function initUi() {
return cliui({ mode: process.env.NODE_ENV === 'testing' ? 'raw' : 'normal' })
}
8 changes: 4 additions & 4 deletions src/validators/builtin/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ui } from '../../utils/cliui.js'
import type { UI } from '../../utils/cliui.js'
import type { PoppinsSchema } from '../../contracts/index.js'

export function errorReporter(errors: any[]) {
export function errorReporter(ui: UI, errors: any[]) {
let finalMessage = ui.colors.red('Failed to validate environment variables : \n')

for (const error of errors) {
Expand All @@ -18,7 +18,7 @@ export function errorReporter(errors: any[]) {
/**
* Validate the env values with builtin validator
*/
export function builtinValidation(env: Record<string, string>, schema: PoppinsSchema) {
export function builtinValidation(ui: UI, env: Record<string, string>, schema: PoppinsSchema) {
const errors = []
const variables = []

Expand All @@ -36,7 +36,7 @@ export function builtinValidation(env: Record<string, string>, schema: PoppinsSc
}

if (errors.length) {
throw new Error(errorReporter(errors))
throw new Error(errorReporter(ui, errors))
}

return variables
Expand Down
8 changes: 4 additions & 4 deletions src/validators/zod/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { ZodSchema } from 'zod'

import { ui } from '../../utils/cliui.js'
import type { UI } from '../../utils/cliui.js'

export function errorReporter(errors: any[]) {
export function errorReporter(ui: UI, errors: any[]) {
let finalMessage = ui.colors.red('Failed to validate environment variables : \n')

for (const error of errors) {
Expand All @@ -19,7 +19,7 @@ export function errorReporter(errors: any[]) {
/**
* Validate the env values with Zod validator
*/
export async function zodValidation(env: Record<string, string>, schema: ZodSchema) {
export async function zodValidation(ui: UI, env: Record<string, string>, schema: ZodSchema) {
const errors = []
const variables = []

Expand All @@ -38,7 +38,7 @@ export async function zodValidation(env: Record<string, string>, schema: ZodSche
}

if (errors.length) {
throw new Error(errorReporter(errors))
throw new Error(errorReporter(ui, errors))
}

return variables
Expand Down
12 changes: 8 additions & 4 deletions tests/common.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { test } from '@japa/runner'

import { ui } from '../src/utils/cliui.js'
import { Schema, ValidateEnv } from '../src/index.js'
import type { UI } from '../src/utils/cliui.js'
import { Schema, ValidateEnv as CoreTypedValidateEnv } from '../src/index.js'

const viteEnvConfig = { mode: 'development', command: 'serve' } as const

const ValidateEnv = CoreTypedValidateEnv as (
...args: Parameters<typeof CoreTypedValidateEnv>
) => ReturnType<typeof CoreTypedValidateEnv> & { ui: UI }

test.group('vite-plugin-validate-env', () => {
test('Basic validation', async ({ assert, fs }) => {
assert.plan(1)
Expand Down Expand Up @@ -246,7 +250,7 @@ test.group('vite-plugin-validate-env', () => {
// @ts-ignore
await plugin.config({ root: fs.basePath }, viteEnvConfig)

const logs = ui.logger.getLogs()
const logs = plugin.ui.logger.getLogs()
assert.deepEqual(logs[0].message, 'cyan([vite-plugin-validate-env]) debug process.env content')
assert.deepInclude(logs[1].message, 'cyan(VITE_BOOLEAN): true')
})
Expand All @@ -267,7 +271,7 @@ test.group('vite-plugin-validate-env', () => {
assert.include(error.message, 'Value for environment variable "VITE_TESTX" must be a boolean')
}

const logs = ui.logger.getLogs()
const logs = plugin.ui.logger.getLogs()
const messages = logs.map((log) => log.message)
assert.isDefined(
messages.find(
Expand Down
73 changes: 72 additions & 1 deletion tests/zod.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { z } from 'zod'
import { test } from '@japa/runner'

import { ValidateEnv } from '../src/index.js'
import type { UI } from '../src/utils/cliui.js'
import { ValidateEnv as CoreTypedValidateEnv } from '../src/index.js'

const viteEnvConfig = { mode: 'development', command: 'serve' } as const

const ENV_FILENAME = '.env.development'

const ValidateEnv = CoreTypedValidateEnv as (
...args: Parameters<typeof CoreTypedValidateEnv>
) => ReturnType<typeof CoreTypedValidateEnv> & { ui: UI }

test.group('Zod validation adaptater', () => {
test('Basic', async ({ assert, fs }) => {
assert.plan(1)
Expand Down Expand Up @@ -189,4 +194,70 @@ test.group('Zod validation adaptater', () => {
const { define: define2 } = await plugin.config({ root: fs.basePath }, viteEnvConfig)
assert.equal(define2['import.meta.env.VITE_BOOLEAN'], 'true')
})

test('log variables when debug is enabled', async ({ assert, fs }) => {
const plugin = ValidateEnv({
validator: 'zod',
schema: {
VITE_BOOLEAN: z.preprocess((value) => value === 'true' || value === '1', z.boolean()),
},
debug: true,
})

await fs.create('.env.development', 'VITE_BOOLEAN=true')

// @ts-ignore
await plugin.config({ root: fs.basePath }, viteEnvConfig)

const logs = plugin.ui.logger.getLogs()
assert.deepEqual(logs[0].message, 'cyan([vite-plugin-validate-env]) debug process.env content')
assert.deepInclude(logs[1].message, 'cyan(VITE_BOOLEAN): true')
})

test('Optional Variables with Default', async ({ assert, fs }) => {
const plugin = ValidateEnv({
validator: 'zod',
schema: { VITE_OPTIONAL_ZOD: z.string().max(2).optional().default('d') },
debug: true,
})

// Test without variable
await fs.create(ENV_FILENAME, '')
// @ts-ignore
const { define } = await plugin.config({ root: fs.basePath }, viteEnvConfig)
assert.equal(define['import.meta.env.VITE_OPTIONAL_ZOD'], '"d"')
const logs = plugin.ui.logger.getLogs()
assert.deepEqual(logs[0].message, 'cyan([vite-plugin-validate-env]) debug process.env content')
assert.deepInclude(logs[1].message, 'cyan(VITE_OPTIONAL_ZOD): d')
})

test('log variables even if validation is failing', async ({ assert, fs }) => {
const plugin = ValidateEnv({
validator: 'zod',
schema: { VITE_TESTX: z.boolean() },
debug: true,
})

await fs.create('.env.development', 'VITE_TESTX=not boolean')

try {
// @ts-ignore
await plugin.config({ root: fs.basePath }, viteEnvConfig)
} catch (error: any) {
assert.include(
error.message,
'Invalid value for "VITE_TESTX" : Expected boolean, received string',
)
}

const logs = plugin.ui.logger.getLogs()
const messages = logs.map((log) => log.message)
assert.isDefined(
messages.find(
(message) => message === 'cyan([vite-plugin-validate-env]) debug process.env content',
),
)

assert.isDefined(messages.find((message) => message.includes('cyan(VITE_TESTX): not boolean')))
})
})

0 comments on commit 4a1e93b

Please sign in to comment.