diff options
Diffstat (limited to 'packages/astro/src')
-rw-r--r-- | packages/astro/src/core/errors/errors-data.ts | 4 | ||||
-rw-r--r-- | packages/astro/src/env/validators.ts | 151 | ||||
-rw-r--r-- | packages/astro/src/env/vite-plugin-env.ts | 24 |
3 files changed, 105 insertions, 74 deletions
diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index cae077053..236dd4dfd 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -1239,8 +1239,8 @@ export const RouteNotFound = { export const EnvInvalidVariables = { name: 'EnvInvalidVariables', title: 'Invalid Environment Variables', - message: (variables: string) => - `The following environment variables do not match the data type and/or properties defined in \`experimental.env.schema\`:\n\n${variables}\n`, + message: (errors: Array<string>) => + `The following environment variables defined in \`experimental.env.schema\` are invalid:\n\n${errors.map(err => `- ${err}`).join('\n')}\n`, } satisfies ErrorData; /** diff --git a/packages/astro/src/env/validators.ts b/packages/astro/src/env/validators.ts index a09eeda8f..4e5d34287 100644 --- a/packages/astro/src/env/validators.ts +++ b/packages/astro/src/env/validators.ts @@ -1,16 +1,16 @@ import type { EnumSchema, EnvFieldType, NumberSchema, StringSchema } from './schema.js'; export type ValidationResultValue = EnvFieldType['default']; +export type ValidationResultErrors = ['missing'] | ['type'] | Array<string>; type ValidationResult = | { ok: true; - type: string; value: ValidationResultValue; } | { ok: false; - type: string; + errors: ValidationResultErrors; }; export function getEnvFieldType(options: EnvFieldType) { @@ -26,45 +26,50 @@ export function getEnvFieldType(options: EnvFieldType) { return `${type}${optional ? ' | undefined' : ''}`; } -type ValueValidator = (input: string | undefined) => { - valid: boolean; - parsed: ValidationResultValue; -}; +type ValueValidator = (input: string | undefined) => ValidationResult; const stringValidator = ({ max, min, length, url, includes, startsWith, endsWith }: StringSchema): ValueValidator => (input) => { - let valid = typeof input === 'string'; + if (typeof input !== 'string') { + return { + ok: false, + errors: ['type'], + }; + } + const errors: Array<string> = []; - if (valid && max !== undefined) { - valid = input!.length <= max; + if (max !== undefined && !(input.length <= max)) { + errors.push('max'); } - if (valid && min !== undefined) { - valid = input!.length >= min; + if (min !== undefined && !(input.length >= min)) { + errors.push('min'); } - if (valid && length !== undefined) { - valid = input!.length === length; + if (length !== undefined && !(input.length === length)) { + errors.push('length'); } - if (valid && url !== undefined) { - try { - new URL(input!); - } catch (_) { - valid = false; - } + if (url !== undefined && !URL.canParse(input)) { + errors.push('url'); } - if (valid && includes !== undefined) { - valid = input!.includes(includes); + if (includes !== undefined && !input.includes(includes)) { + errors.push('includes'); } - if (valid && startsWith !== undefined) { - valid = input!.startsWith(startsWith); + if (startsWith !== undefined && !input.startsWith(startsWith)) { + errors.push('startsWith'); } - if (valid && endsWith !== undefined) { - valid = input!.endsWith(endsWith); + if (endsWith !== undefined && !input.endsWith(endsWith)) { + errors.push('endsWith'); } + if (errors.length > 0) { + return { + ok: false, + errors, + }; + } return { - valid, - parsed: input, + ok: true, + value: input, }; }; @@ -72,45 +77,71 @@ const numberValidator = ({ gt, min, lt, max, int }: NumberSchema): ValueValidator => (input) => { const num = parseFloat(input ?? ''); - let valid = !isNaN(num); + if (isNaN(num)) { + return { + ok: false, + errors: ['type'], + }; + } + const errors: Array<string> = []; - if (valid && gt !== undefined) { - valid = num > gt; + if (gt !== undefined && !(num > gt)) { + errors.push('gt'); } - if (valid && min !== undefined) { - valid = num >= min; + if (min !== undefined && !(num >= min)) { + errors.push('min'); } - if (valid && lt !== undefined) { - valid = num < lt; + if (lt !== undefined && !(num < lt)) { + errors.push('lt'); } - if (valid && max !== undefined) { - valid = num <= max; + if (max !== undefined && !(num <= max)) { + errors.push('max'); } - if (valid && int !== undefined) { + if (int !== undefined) { const isInt = Number.isInteger(num); - valid = int ? isInt : !isInt; + if (!(int ? isInt : !isInt)) { + errors.push('int'); + } } + if (errors.length > 0) { + return { + ok: false, + errors, + }; + } return { - valid, - parsed: num, + ok: true, + value: num, }; }; const booleanValidator: ValueValidator = (input) => { const bool = input === 'true' ? true : input === 'false' ? false : undefined; + if (typeof bool !== 'boolean') { + return { + ok: false, + errors: ['type'], + }; + } return { - valid: typeof bool === 'boolean', - parsed: bool, + ok: true, + value: bool, }; }; const enumValidator = ({ values }: EnumSchema): ValueValidator => (input) => { + if (!(typeof input === 'string' ? values.includes(input) : false)) { + return { + ok: false, + errors: ['type'], + }; + } return { - valid: typeof input === 'string' ? values.includes(input) : false, - parsed: input, + ok: true, + value: input, }; }; @@ -131,29 +162,19 @@ export function validateEnvVariable( value: string | undefined, options: EnvFieldType ): ValidationResult { - const validator = selectValidator(options); - - const type = getEnvFieldType(options); - - if (options.optional || options.default !== undefined) { - if (value === undefined) { - return { - ok: true, - value: options.default, - type, - }; - } - } - const { valid, parsed } = validator(value); - if (valid) { + const isOptional = options.optional || options.default !== undefined; + if (isOptional && value === undefined) { return { ok: true, - value: parsed, - type, + value: options.default, }; } - return { - ok: false, - type, - }; + if (!isOptional && value === undefined) { + return { + ok: false, + errors: ['missing'], + }; + } + + return selectValidator(options)(value); } diff --git a/packages/astro/src/env/vite-plugin-env.ts b/packages/astro/src/env/vite-plugin-env.ts index 1bcb021e0..9eeb7dcd9 100644 --- a/packages/astro/src/env/vite-plugin-env.ts +++ b/packages/astro/src/env/vite-plugin-env.ts @@ -9,7 +9,7 @@ import { VIRTUAL_MODULES_IDS_VALUES, } from './constants.js'; import type { EnvSchema } from './schema.js'; -import { validateEnvVariable } from './validators.js'; +import { getEnvFieldType, validateEnvVariable, type ValidationResultErrors } from './validators.js'; // TODO: reminders for when astro:env comes out of experimental // Types should always be generated (like in types/content.d.ts). That means the client module will be empty @@ -105,7 +105,7 @@ function validatePublicVariables({ validateSecrets: boolean; }) { const valid: Array<{ key: string; value: any; type: string; context: 'server' | 'client' }> = []; - const invalid: Array<{ key: string; type: string }> = []; + const invalid: Array<{ key: string; type: string; errors: ValidationResultErrors }> = []; for (const [key, options] of Object.entries(schema)) { const variable = loadedEnv[key] === '' ? undefined : loadedEnv[key]; @@ -115,20 +115,30 @@ function validatePublicVariables({ } const result = validateEnvVariable(variable, options); + const type = getEnvFieldType(options); if (!result.ok) { - invalid.push({ key, type: result.type }); + invalid.push({ key, type, errors: result.errors }); // We don't do anything with validated secrets so we don't store them } else if (options.access === 'public') { - valid.push({ key, value: result.value, type: result.type, context: options.context }); + valid.push({ key, value: result.value, type, context: options.context }); } } if (invalid.length > 0) { + const _errors: Array<string> = []; + for (const { key, type, errors } of invalid) { + if (errors[0] === 'missing') { + _errors.push(`${key} is missing`); + } else if (errors[0] === 'type') { + _errors.push(`${key}'s type is invalid, expected: ${type}`); + } else { + // constraints + _errors.push(`The following constraints for ${key} are not met: ${errors.join(', ')}`); + } + } throw new AstroError({ ...AstroErrorData.EnvInvalidVariables, - message: AstroErrorData.EnvInvalidVariables.message( - invalid.map(({ key, type }) => `Variable ${key} is not of type: ${type}.`).join('\n') - ), + message: AstroErrorData.EnvInvalidVariables.message(_errors), }); } |