summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/wise-cameras-trade.md5
-rw-r--r--packages/astro/src/core/errors/errors-data.ts4
-rw-r--r--packages/astro/src/env/validators.ts151
-rw-r--r--packages/astro/src/env/vite-plugin-env.ts24
-rw-r--r--packages/astro/test/env-secret.test.js2
-rw-r--r--packages/astro/test/units/env/env-validators.test.js65
6 files changed, 151 insertions, 100 deletions
diff --git a/.changeset/wise-cameras-trade.md b/.changeset/wise-cameras-trade.md
new file mode 100644
index 000000000..f2c000630
--- /dev/null
+++ b/.changeset/wise-cameras-trade.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Improves `astro:env` invalid variables errors
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),
});
}
diff --git a/packages/astro/test/env-secret.test.js b/packages/astro/test/env-secret.test.js
index 4505254a6..7a569e35a 100644
--- a/packages/astro/test/env-secret.test.js
+++ b/packages/astro/test/env-secret.test.js
@@ -84,7 +84,7 @@ describe('astro:env secret variables', () => {
} catch (error) {
assert.equal(error instanceof Error, true);
assert.equal(error.title, 'Invalid Environment Variables');
- assert.equal(error.message.includes('Variable KNOWN_SECRET is not of type: number.'), true);
+ assert.equal(error.message.includes('KNOWN_SECRET is missing'), true);
}
});
});
diff --git a/packages/astro/test/units/env/env-validators.test.js b/packages/astro/test/units/env/env-validators.test.js
index 468c86d8e..8d446c045 100644
--- a/packages/astro/test/units/env/env-validators.test.js
+++ b/packages/astro/test/units/env/env-validators.test.js
@@ -29,9 +29,17 @@ const createFixture = () => {
assert.equal(result.value, value);
input = undefined;
},
- thenResultShouldBeInvalid() {
+ /**
+ * @param {string | Array<string>} providedErrors
+ */
+ thenResultShouldBeInvalid(providedErrors) {
const result = validateEnvVariable(input.value, input.options);
assert.equal(result.ok, false);
+ const errors = typeof providedErrors === 'string' ? [providedErrors] : providedErrors;
+ assert.equal(
+ result.errors.every((element) => errors.includes(element)),
+ true
+ );
input = undefined;
},
};
@@ -158,7 +166,7 @@ describe('astro:env validators', () => {
fixture.givenInput(undefined, {
type: 'string',
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('missing');
});
it('Should not fail is the variable type is incorrect', () => {
@@ -179,7 +187,7 @@ describe('astro:env validators', () => {
type: 'string',
max: 3,
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('max');
fixture.givenInput('abc', {
type: 'string',
@@ -191,7 +199,7 @@ describe('astro:env validators', () => {
type: 'string',
min: 5,
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('min');
fixture.givenInput('abc', {
type: 'string',
@@ -203,13 +211,13 @@ describe('astro:env validators', () => {
type: 'string',
length: 10,
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('length');
fixture.givenInput('abc', {
type: 'string',
url: true,
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('url');
fixture.givenInput('https://example.com', {
type: 'string',
@@ -221,7 +229,7 @@ describe('astro:env validators', () => {
type: 'string',
includes: 'cd',
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('includes');
fixture.givenInput('abc', {
type: 'string',
@@ -233,7 +241,7 @@ describe('astro:env validators', () => {
type: 'string',
startsWith: 'za',
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('startsWith');
fixture.givenInput('abc', {
type: 'string',
@@ -245,7 +253,7 @@ describe('astro:env validators', () => {
type: 'string',
endsWith: 'za',
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('endsWith');
fixture.givenInput('abc', {
type: 'string',
@@ -264,7 +272,14 @@ describe('astro:env validators', () => {
type: 'string',
min: 5,
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('missing');
+
+ fixture.givenInput('ab', {
+ type: 'string',
+ startsWith: 'x',
+ min: 5
+ })
+ fixture.thenResultShouldBeInvalid(['startsWith', 'min']);
});
it('Should not fail if the optional variable is missing', () => {
@@ -297,14 +312,14 @@ describe('astro:env validators', () => {
fixture.givenInput(undefined, {
type: 'number',
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('missing');
});
it('Should fail is the variable type is incorrect', () => {
fixture.givenInput('abc', {
type: 'number',
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('type');
});
it('Should fail if conditions are not met', () => {
@@ -312,13 +327,13 @@ describe('astro:env validators', () => {
type: 'number',
gt: 15,
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('gt');
fixture.givenInput('10', {
type: 'number',
gt: 10,
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('gt');
fixture.givenInput('10', {
type: 'number',
@@ -330,7 +345,7 @@ describe('astro:env validators', () => {
type: 'number',
min: 25,
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('min');
fixture.givenInput('20', {
type: 'number',
@@ -348,13 +363,13 @@ describe('astro:env validators', () => {
type: 'number',
lt: 10,
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('lt');
fixture.givenInput('10', {
type: 'number',
lt: 10,
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('lt');
fixture.givenInput('5', {
type: 'number',
@@ -366,7 +381,7 @@ describe('astro:env validators', () => {
type: 'number',
max: 20,
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('max');
fixture.givenInput('25', {
type: 'number',
@@ -384,7 +399,7 @@ describe('astro:env validators', () => {
type: 'number',
int: true,
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('int');
fixture.givenInput('25', {
type: 'number',
@@ -396,7 +411,7 @@ describe('astro:env validators', () => {
type: 'number',
int: false,
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('int');
fixture.givenInput('4.5', {
type: 'number',
@@ -408,7 +423,7 @@ describe('astro:env validators', () => {
type: 'number',
gt: 10,
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('missing');
});
it('Should accept integers', () => {
@@ -455,14 +470,14 @@ describe('astro:env validators', () => {
fixture.givenInput(undefined, {
type: 'boolean',
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('missing');
});
it('Should fail is the variable type is incorrect', () => {
fixture.givenInput('abc', {
type: 'boolean',
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('type');
});
it('Should not fail if the optional variable is missing', () => {
@@ -496,7 +511,7 @@ describe('astro:env validators', () => {
type: 'enum',
values: ['a', 'b'],
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('missing');
});
it('Should fail is the variable type is incorrect', () => {
@@ -504,7 +519,7 @@ describe('astro:env validators', () => {
type: 'enum',
values: ['a', 'b'],
});
- fixture.thenResultShouldBeInvalid();
+ fixture.thenResultShouldBeInvalid('type');
});
it('Should not fail if the optional variable is missing', () => {