-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Build time validation for client env #2392
base: main
Are you sure you want to change the base?
Changes from all commits
ddd10d1
90baa94
b5c3c63
7afc4f0
b4e2362
465856c
5c329a4
e04aac3
1928b6f
e231dda
f431153
c8b2120
08304fe
a509762
9d618ea
7333b25
80baf9c
fb899a0
7593ac2
42d121c
f877543
025d185
1da4542
67de093
ee7c6de
7b0fedd
598eeab
4732a27
fcdc0d6
68e99ba
c740e4e
88d7ea9
107fa51
42e9c78
3aa07d2
35bb045
e068427
9dc30c1
4766a94
ba46aa8
cdd5e03
e43a01e
900ce7f
920cc90
835e17a
d275338
73e5e11
41e24be
ae51b4c
a7caff0
a007937
c6c3a11
218d552
2395640
109d55a
6d1e22f
afd6980
9c59b0d
c2e304b
3db8cb0
3d7e4bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { type Plugin, loadEnv } from 'vite' | ||
|
||
import { | ||
getValidatedEnvOrError, | ||
type EnvValidationResult, | ||
} from 'wasp/env/validation' | ||
import { clientEnvSchema } from 'wasp/client/env/schema' | ||
|
||
const redColor = '\x1b[31m' | ||
|
||
export function validateEnv(): Plugin { | ||
let validationResult: EnvValidationResult<unknown> | null = null | ||
return { | ||
name: 'wasp-validate-env', | ||
configResolved: (config) => { | ||
const env = loadEnv(config.mode, process.cwd(), config.envPrefix) | ||
|
||
validationResult = getValidatedEnvOrError(env, clientEnvSchema) | ||
|
||
if (validationResult.type !== 'error') { | ||
return | ||
} | ||
|
||
if (config.command !== 'build') { | ||
return | ||
} | ||
|
||
console.error(`${redColor}${validationResult.message}`) | ||
// Exit early if we are in build mode, because we can't show the error in the browser. | ||
process.exit(1) | ||
}, | ||
configureServer: (server) => { | ||
if (validationResult === null || validationResult.type !== 'error') { | ||
return | ||
} | ||
|
||
// Send the error to the browser. | ||
const message = validationResult.message | ||
server.ws.on('connection', () => { | ||
server.ws.send({ | ||
type: 'error', | ||
err: { | ||
message, | ||
stack: '', | ||
}, | ||
}) | ||
}) | ||
}, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,5 @@ | ||
{{={= =}=}} | ||
import * as z from 'zod' | ||
|
||
import { clientEnvSchema } from './env/schema.js' | ||
import { ensureEnvSchema } from '../env/validation.js' | ||
|
||
{=# envValidationSchema.isDefined =} | ||
{=& envValidationSchema.importStatement =} | ||
const userClientEnvSchema = {= envValidationSchema.importIdentifier =} | ||
{=/ envValidationSchema.isDefined =} | ||
{=^ envValidationSchema.isDefined =} | ||
const userClientEnvSchema = z.object({}) | ||
{=/ envValidationSchema.isDefined =} | ||
|
||
const waspClientEnvSchema = z.object({ | ||
REACT_APP_API_URL: z | ||
.string({ | ||
required_error: 'REACT_APP_API_URL is required', | ||
}) | ||
.default('{= defaultServerUrl =}') | ||
}) | ||
|
||
const clientEnvSchema = userClientEnvSchema.merge(waspClientEnvSchema) | ||
|
||
// PUBLIC API | ||
export const env = ensureEnvSchema(import.meta.env, clientEnvSchema) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{{={= =}=}} | ||
import * as z from 'zod' | ||
|
||
{=# envValidationSchema.isDefined =} | ||
{=& envValidationSchema.importStatement =} | ||
const userClientEnvSchema = {= envValidationSchema.importIdentifier =} | ||
{=/ envValidationSchema.isDefined =} | ||
{=^ envValidationSchema.isDefined =} | ||
const userClientEnvSchema = z.object({}) | ||
{=/ envValidationSchema.isDefined =} | ||
|
||
const waspClientEnvSchema = z.object({ | ||
REACT_APP_API_URL: z | ||
.string() | ||
.url({ | ||
message: 'REACT_APP_API_URL must be a valid URL', | ||
}) | ||
.default('{= defaultServerUrl =}'), | ||
}) | ||
|
||
// PRIVATE API (sdk, Vite config) | ||
export const clientEnvSchema = userClientEnvSchema.merge(waspClientEnvSchema) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,12 +2,43 @@ import * as z from 'zod' | |
|
||
const redColor = '\x1b[31m' | ||
|
||
// PRIVATE API (SDK, Vite config) | ||
export function ensureEnvSchema<Schema extends z.ZodTypeAny>( | ||
data: unknown, | ||
schema: Schema | ||
): z.infer<Schema> { | ||
const result = getValidatedEnvOrError(data, schema) | ||
switch (result.type) { | ||
case 'error': | ||
console.error(`${redColor}${result.message}`) | ||
throw new Error('Error parsing environment variables') | ||
case 'success': | ||
return result.env | ||
default: | ||
result satisfies never; | ||
} | ||
} | ||
|
||
// PRIVATE API (Vite config) | ||
export type EnvValidationResult<Env> = { | ||
type: 'error', | ||
message: string, | ||
} | { | ||
type: 'success', | ||
env: Env, | ||
} | ||
Comment on lines
+23
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a nice candidate for You can optionally make the message type generic as well (like in |
||
|
||
// PRIVATE API (SDK, Vite config) | ||
export function getValidatedEnvOrError<Schema extends z.ZodTypeAny>( | ||
env: unknown, | ||
schema: Schema | ||
): EnvValidationResult<z.infer<Schema>> { | ||
try { | ||
return schema.parse(data) | ||
const validatedEnv = schema.parse(env) | ||
return { | ||
type: 'success', | ||
env: validatedEnv, | ||
} | ||
} catch (e) { | ||
if (e instanceof z.ZodError) { | ||
const errorOutput = ['', '══ Env vars validation failed ══', ''] | ||
|
@@ -16,10 +47,15 @@ export function ensureEnvSchema<Schema extends z.ZodTypeAny>( | |
} | ||
errorOutput.push('') | ||
errorOutput.push('════════════════════════════════') | ||
console.error(redColor, errorOutput.join('\n')) | ||
throw new Error('Error parsing environment variables') | ||
return { | ||
type: 'error', | ||
message: errorOutput.join('\n'), | ||
} | ||
} else { | ||
throw e | ||
return { | ||
type: 'error', | ||
message: e.message, | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -110,6 +110,10 @@ | |
"./client": "./dist/client/index.js", | ||
"./dev": "./dist/dev/index.js", | ||
"./env": "./dist/env/index.js", | ||
{=! Private: [client] =} | ||
"./env/validation": "./dist/env/validation.js", | ||
{=! Private: [client, sdk] =} | ||
"./client/env/schema": "./dist/client/env/schema.js", | ||
Comment on lines
+113
to
+116
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please update the google sheet with these guys if you haven't :) |
||
|
||
{=! todo(filip): Fixes below are for type errors in 0.13.1, remove ASAP =} | ||
{=! Used by our code (SDK for full-stack type safety), uncodumented (but accessible) for users. =} | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
redColor
is defined in two places.We also have
yellowColor
defined inproviders/dummy.ts
.Maybe it's time we created an
ansiColors
file that exposes a function (or more) for coloring text into ANSI colors? I know there's a dependency for this but I don't think it's worth it. We can create a small module supporting only the colors we currently need, and build from there.Also, when dealing with ASNI escape sequencees, the protocol is to reset the color after outputting the colored text. This is done by appending an additional escape sequence after the text, see this post for details).
Our code never does this. That might be OK since our colors are always used with
console.log
(which I'm guessing resets all colors when flushing the output). But, if we end up building the coloring module, it shouldn't assume the caller is usingconsole.log
(perhaps they want to output different text in different colors in a single line).