Skip to content

Commit d1bd806

Browse files
committed
feat: display all validation errors at once
Closes #5
1 parent c779b10 commit d1bd806

File tree

8 files changed

+214
-91
lines changed

8 files changed

+214
-91
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"prepublishOnly": "nr build",
5656
"release": "bumpp --commit --push --tag && pnpm publish",
5757
"start": "node --loader=ts-node/esm src/index.ts",
58-
"test": "ts-node bin/test.ts",
58+
"test": "cross-env NODE_ENV=testing ts-node bin/test.ts",
5959
"test:watch": "nodemon --ignore ./tests/fixtures bin/test.ts",
6060
"typecheck": "tsc --noEmit"
6161
},
@@ -69,6 +69,7 @@
6969
}
7070
},
7171
"dependencies": {
72+
"@poppinss/colors": "^3.0.2",
7273
"@poppinss/validator-lite": "^1.0.1",
7374
"unconfig": "^0.3.6",
7475
"validator": "^13.7.0"
@@ -83,6 +84,7 @@
8384
"@types/node": "^18.7.15",
8485
"@types/validator": "^13.7.6",
8586
"bumpp": "^8.2.1",
87+
"cross-env": "^7.0.3",
8688
"eslint": "^8.23.0",
8789
"nodemon": "^2.0.19",
8890
"pnpm": "^7.11.0",

pnpm-lock.yaml

Lines changed: 13 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.ts

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import { cwd } from 'process'
22
import { type Plugin, loadEnv } from 'vite'
33
import { createConfigLoader as createLoader } from 'unconfig'
4-
import { Exception } from './exception'
5-
import type {
6-
FullPluginOptions,
7-
PluginOptions,
8-
PoppinsSchema,
9-
Schema,
10-
ZodSchema,
11-
} from './contracts'
4+
import { builtinValidation } from './validators/builtin'
5+
import { zodValidation } from './validators/zod'
6+
import type { FullPluginOptions, PluginOptions, Schema } from './contracts'
127
import type { ConfigEnv, UserConfig } from 'vite'
138

149
/**
@@ -29,33 +24,6 @@ async function loadConfig(rootDir: string) {
2924
return result.config
3025
}
3126

32-
/**
33-
* Validate the env values with builtin validator
34-
*/
35-
async function builtinValidation(env: Record<string, string>, schema: PoppinsSchema) {
36-
for (const [key, validator] of Object.entries(schema!)) {
37-
const res = validator(key, env[key])
38-
process.env[key] = res
39-
}
40-
}
41-
42-
/**
43-
* Validate the env values with Zod validator
44-
*/
45-
async function zodValidation(env: Record<string, string>, schema: ZodSchema) {
46-
for (const [key, validator] of Object.entries(schema!)) {
47-
const result = validator.safeParse(env[key])
48-
49-
if (!result.success) {
50-
throw new Exception(
51-
`E_INVALID_ENV_VALUE: Invalid value for "${key}" : ${result.error.issues[0].message}`,
52-
'E_INVALID_ENV_VALUE'
53-
)
54-
}
55-
process.env[key] = result.data
56-
}
57-
}
58-
5927
/**
6028
* Returns the schema and the validator
6129
*/

src/utils/colors.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Colors, FakeColors } from '@poppinss/colors'
2+
3+
export const colors = process.env.NODE_ENV === 'testing' ? new FakeColors() : new Colors()

src/validators/builtin/index.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { colors } from '../../utils/colors'
2+
import type { PoppinsSchema } from '../../contracts'
3+
4+
export function errorReporter(errors: any[]) {
5+
let finalMessage = colors.red('Failed to validate environment variables : \n')
6+
7+
for (const error of errors) {
8+
const errorKey = `[${colors.magenta(error.key)}]`
9+
finalMessage += `\n${errorKey}: \n`
10+
11+
const message = error.err.message.replace(`${error.err.code}: `, '')
12+
finalMessage += ` ${colors.yellow(message)} \n`
13+
}
14+
15+
return finalMessage as string
16+
}
17+
18+
/**
19+
* Validate the env values with builtin validator
20+
*/
21+
export function builtinValidation(env: Record<string, string>, schema: PoppinsSchema) {
22+
const errors = []
23+
24+
for (const [key, validator] of Object.entries(schema!)) {
25+
try {
26+
const res = validator(key, env[key])
27+
process.env[key] = res
28+
} catch (err) {
29+
errors.push({ key, err })
30+
}
31+
}
32+
33+
if (errors.length) {
34+
throw new Error(errorReporter(errors))
35+
}
36+
}

src/validators/zod/index.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { colors } from '../../utils/colors'
2+
import type { ZodSchema } from 'zod'
3+
4+
export function errorReporter(errors: any[]) {
5+
let finalMessage = colors.red('Failed to validate environment variables : \n')
6+
7+
for (const error of errors) {
8+
const errorKey = `[${colors.magenta(error.key)}]`
9+
finalMessage += `\n${errorKey}: \n`
10+
11+
const message = `Invalid value for "${error.key}" : ${error.err.issues[0].message}`
12+
finalMessage += ` ${colors.yellow(message)} \n`
13+
}
14+
15+
return finalMessage as string
16+
}
17+
18+
/**
19+
* Validate the env values with Zod validator
20+
*/
21+
export async function zodValidation(env: Record<string, string>, schema: ZodSchema) {
22+
const errors = []
23+
24+
for (const [key, validator] of Object.entries(schema!)) {
25+
const result = validator.safeParse(env[key])
26+
27+
if (!result.success) {
28+
errors.push({ key, err: result.error })
29+
continue
30+
}
31+
32+
process.env[key] = result.data
33+
}
34+
35+
if (errors.length) {
36+
throw new Error(errorReporter(errors))
37+
}
38+
}

tests/common.spec.ts

Lines changed: 68 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/* eslint-disable @typescript-eslint/prefer-ts-expect-error */
2+
/* eslint-disable @typescript-eslint/ban-ts-comment */
3+
14
import { join } from 'path'
25
import { test } from '@japa/runner'
36
import { Filesystem } from '@poppinss/dev-utils'
@@ -13,46 +16,51 @@ test.group('vite-plugin-validate-env', (group) => {
1316
})
1417

1518
test('Basic validation', async ({ assert }) => {
16-
const plugin = ValidateEnv({
17-
VITE_TEST: Schema.boolean(),
18-
})
19+
assert.plan(1)
1920

21+
const plugin = ValidateEnv({ VITE_TEST: Schema.boolean() })
2022
await fs.add(`.env.development`, `VITE_TEST=not boolean`)
2123

22-
// @ts-expect-error - `config` is the handler
23-
const fn = plugin.config!.bind(plugin, viteConfig, viteEnvConfig)
24-
await assert.rejects(
25-
fn,
26-
'E_INVALID_ENV_VALUE: Value for environment variable "VITE_TEST" must be a boolean, instead received "not boolean"'
27-
)
24+
try {
25+
// @ts-ignore
26+
await plugin.config(viteConfig, viteEnvConfig)
27+
} catch (error: any) {
28+
assert.include(error.message, '"VITE_TEST" must be a boolean')
29+
}
2830
})
2931

3032
test('Custom error message', async ({ assert }) => {
31-
const plugin = ValidateEnv({
32-
VITE_TEST: Schema.boolean({ message: 'Heyhey' }),
33-
})
33+
assert.plan(2)
3434

35+
const plugin = ValidateEnv({ VITE_TEST: Schema.boolean({ message: 'Heyhey' }) })
3536
await fs.add(`.env.development`, `VITE_TEST=not boolean`)
3637

37-
// @ts-expect-error - `config` is the handler
38-
const fn = plugin.config!.bind(plugin, viteConfig, viteEnvConfig)
39-
await assert.rejects(fn, 'E_INVALID_ENV_VALUE: Heyhey')
38+
try {
39+
// @ts-ignore
40+
await plugin.config(viteConfig, viteEnvConfig)
41+
} catch (error: any) {
42+
assert.include(error.message, 'VITE_TEST')
43+
assert.include(error.message, 'Heyhey')
44+
}
4045
})
4146

4247
test('Custom validator method', async ({ assert }) => {
48+
assert.plan(1)
49+
4350
const plugin = ValidateEnv({
4451
VITE_TEST: (_key, value) => {
45-
if (value !== 'valid') {
46-
throw new Error('Value must be "valid"')
47-
}
52+
if (value !== 'valid') throw new Error('Value must be "valid"')
4853
},
4954
})
5055

5156
await fs.add(`.env.development`, `VITE_TEST=not valid`)
5257

53-
// @ts-expect-error - `config` is the handler
54-
const fn = plugin.config!.bind(plugin, viteConfig, viteEnvConfig)
55-
await assert.rejects(fn, 'Value must be "valid"')
58+
try {
59+
// @ts-ignore
60+
await plugin.config(viteConfig, viteEnvConfig)
61+
} catch (error: any) {
62+
assert.include(error.message, 'Value must be "valid"')
63+
}
5664
})
5765

5866
test('Parsing result', async ({ assert }) => {
@@ -72,12 +80,14 @@ test.group('vite-plugin-validate-env', (group) => {
7280

7381
await fs.add(`.env.development`, `VITE_URL_TRAILING=test.com`)
7482

75-
// @ts-expect-error - `config` is the handler
83+
// @ts-ignore
7684
await plugin.config!(viteConfig, viteEnvConfig)
7785
assert.equal(process.env.VITE_URL_TRAILING, 'test.com/')
7886
})
7987

8088
test('Dedicated config file', async ({ assert }) => {
89+
assert.plan(1)
90+
8191
const plugin = ValidateEnv()
8292

8393
await fs.add(`.env.development`, `VITE_MY_VAR=true`)
@@ -90,9 +100,12 @@ test.group('vite-plugin-validate-env', (group) => {
90100
}`
91101
)
92102

93-
// @ts-expect-error - `config` is the handler
94-
const fn = plugin.config!.bind(plugin, viteConfig, viteEnvConfig)
95-
await assert.rejects(fn, 'Error validating')
103+
try {
104+
// @ts-ignore
105+
await plugin.config(viteConfig, viteEnvConfig)
106+
} catch (error: any) {
107+
assert.include(error.message, 'Error validating')
108+
}
96109
})
97110

98111
test('Should fail if no schema is found', async ({ assert }) => {
@@ -106,17 +119,39 @@ test.group('vite-plugin-validate-env', (group) => {
106119
})
107120

108121
test('Should pick up var with custom prefix', async ({ assert }) => {
122+
assert.plan(1)
123+
124+
const plugin = ValidateEnv({ CUSTOM_TEST: Schema.boolean() })
125+
126+
await fs.add(`.env.development`, `CUSTOM_TEST=not boolean`)
127+
128+
try {
129+
// @ts-ignore
130+
await plugin.config({ ...viteConfig, envPrefix: 'CUSTOM_' }, viteEnvConfig)
131+
} catch (error: any) {
132+
assert.include(
133+
error.message,
134+
'Value for environment variable "CUSTOM_TEST" must be a boolean, instead received "not boolean"'
135+
)
136+
}
137+
})
138+
139+
test('Display multiple errors', async ({ assert }) => {
140+
assert.plan(2)
141+
109142
const plugin = ValidateEnv({
110-
CUSTOM_TEST: Schema.boolean(),
143+
VITE_TEST: Schema.boolean(),
144+
VITE_TEST2: Schema.boolean(),
111145
})
112146

113-
await fs.add(`.env.development`, `CUSTOM_TEST=not boolean`)
147+
await fs.add(`.env.development`, '')
114148

115-
// @ts-expect-error - `config` is the handler
116-
const fn = plugin.config!.bind(plugin, { ...viteConfig, envPrefix: 'CUSTOM_' }, viteEnvConfig)
117-
await assert.rejects(
118-
fn,
119-
'E_INVALID_ENV_VALUE: Value for environment variable "CUSTOM_TEST" must be a boolean, instead received "not boolean"'
120-
)
149+
try {
150+
// @ts-ignore
151+
await plugin.config(viteConfig, viteEnvConfig)
152+
} catch (error: any) {
153+
assert.include(error.message, 'Missing environment variable "VITE_TEST"')
154+
assert.include(error.message, 'Missing environment variable "VITE_TEST2"')
155+
}
121156
})
122157
})

0 commit comments

Comments
 (0)