Format config errors for humans (#1298)

* format config errors

* fix bad root
This commit is contained in:
Fred K. Schott 2021-09-03 10:47:12 -07:00 committed by GitHub
parent ac2c00e99b
commit 3b4bbdc98d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 92 additions and 14 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Add human readable config verification errors

View file

@ -113,7 +113,8 @@
"@types/sass": "^1.16.0",
"@types/yargs-parser": "^20.2.0",
"astro-scripts": "0.0.1",
"is-windows": "^1.0.2"
"is-windows": "^1.0.2",
"strip-ansi": "^7.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0",

View file

@ -1,12 +1,11 @@
/* eslint-disable no-console */
import type { AstroConfig } from './@types/astro';
import * as colors from 'kleur/colors';
import { promises as fsPromises } from 'fs';
import * as colors from 'kleur/colors';
import yargs from 'yargs-parser';
import { loadConfig } from './config.js';
import { z } from 'zod';
import type { AstroConfig } from './@types/astro';
import { build } from './build.js';
import { formatConfigError, loadConfig } from './config.js';
import devServer from './dev.js';
import { preview } from './preview.js';
import { reload } from './reload.js';
@ -113,7 +112,11 @@ async function runCommand(rawRoot: string, cmd: (a: AstroConfig, opts: any) => P
return cmd(astroConfig, options);
} catch (err) {
console.error(colors.red(err.toString() || err));
if (err instanceof z.ZodError) {
console.log(formatConfigError(err));
} else {
console.error(colors.red(err.toString() || err));
}
process.exit(1);
}
}

View file

@ -1,6 +1,8 @@
import { existsSync } from 'fs';
import getPort from 'get-port';
import * as colors from 'kleur/colors';
import path from 'path';
import { pathToFileURL } from 'url';
import { z } from 'zod';
import { AstroConfig, AstroUserConfig } from './@types/astro';
import { addTrailingSlash } from './util.js';
@ -70,8 +72,8 @@ export const AstroConfigSchema = z.object({
});
/** Turn raw config values into normalized values */
async function validateConfig(userConfig: any, root: string): Promise<AstroConfig> {
const fileProtocolRoot = `file://${root}/`;
export async function validateConfig(userConfig: any, root: string): Promise<AstroConfig> {
const fileProtocolRoot = pathToFileURL(root + path.sep);
// We need to extend the global schema to add transforms that are relative to root.
// This is type checked against the global schema to make sure we still match.
const AstroConfigRelativeSchema = AstroConfigSchema.extend({
@ -109,6 +111,10 @@ export async function loadConfig(rawRoot: string | undefined, configFileName = '
userConfig = (await import(astroConfigPath.href)).default;
}
// normalize, validate, and return
const config = await validateConfig(userConfig, root);
return config;
return validateConfig(userConfig, root);
}
export function formatConfigError(err: z.ZodError) {
const errorList = err.issues.map((issue) => ` ! ${colors.bold(issue.path.join('.'))} ${colors.red(issue.message + '.')}`);
return `${colors.red('[config]')} Astro found issue(s) with your configuration:\n${errorList.join('\n')}`;
}

View file

@ -14,9 +14,11 @@ import {
startServer as startSnowpackServer,
} from 'snowpack';
import { fileURLToPath } from 'url';
import type { AstroConfig, RSSFunctionArgs, GetStaticPathsArgs, GetStaticPathsResult, ManifestData, Params, RuntimeMode } from './@types/astro';
import { z } from 'zod';
import type { AstroConfig, GetStaticPathsArgs, GetStaticPathsResult, ManifestData, Params, RSSFunctionArgs, RuntimeMode } from './@types/astro';
import { generatePaginateFunction } from './build/paginate.js';
import { canonicalURL, getSrcPath, stopTimer } from './build/util.js';
import { formatConfigError } from './config.js';
import { ConfigManager } from './config_manager.js';
import snowpackExternals from './external.js';
import { debug, info, LogOptions } from './logger.js';
@ -48,6 +50,7 @@ type LoadResultSuccess = {
type LoadResultNotFound = { statusCode: 404; error: Error };
type LoadResultError = { statusCode: 500 } & (
| { type: 'parse-error'; error: ICompileError }
| { type: 'config-error'; error: z.ZodError }
| { type: 'ssr'; error: Error }
| { type: 'not-found'; error: ICompileError }
| { type: 'unknown'; error: Error }
@ -173,6 +176,15 @@ async function load(config: AstroRuntimeConfig, rawPathname: string | undefined)
rss: undefined, // TODO: Add back rss support
};
} catch (err) {
if (err instanceof z.ZodError) {
console.log(formatConfigError(err));
return {
statusCode: 500,
type: 'config-error',
error: err,
};
}
if (err.code === 'parse-error' || err instanceof SyntaxError) {
return {
statusCode: 500,

View file

@ -0,0 +1,42 @@
import { z } from 'zod';
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import stripAnsi from 'strip-ansi';
import { formatConfigError, validateConfig } from '#astro/config';
const ConfigValidate = suite('Config Validation');
ConfigValidate('empty user config is valid', async (context) => {
const configError = await validateConfig({}, process.cwd()).catch(err => err);
assert.ok(!(configError instanceof Error));
});
ConfigValidate('Zod errors are returned when invalid config is used', async (context) => {
const configError = await validateConfig({buildOptions: {sitemap: 42}}, process.cwd()).catch(err => err);
assert.ok(configError instanceof z.ZodError);
});
ConfigValidate('A validation error can be formatted correctly', async (context) => {
const configError = await validateConfig({buildOptions: {sitemap: 42}}, process.cwd()).catch(err => err);
assert.ok(configError instanceof z.ZodError);
const formattedError = stripAnsi(formatConfigError(configError));
assert.equal(formattedError, `[config] Astro found issue(s) with your configuration:
! buildOptions.sitemap Expected boolean, received number.`);
});
ConfigValidate('Multiple validation errors can be formatted correctly', async (context) => {
const veryBadConfig = {
renderers: [42],
buildOptions: {pageUrlFormat: 'invalid'},
pages: {},
};
const configError = await validateConfig(veryBadConfig, process.cwd()).catch(err => err);
assert.ok(configError instanceof z.ZodError);
const formattedError = stripAnsi(formatConfigError(configError));
assert.equal(formattedError, `[config] Astro found issue(s) with your configuration:
! pages Expected string, received object.
! renderers.0 Expected string, received number.
! buildOptions.pageUrlFormat Invalid input.`);
});
ConfigValidate.run();

View file

@ -1,6 +1,15 @@
export default {
// Full Astro Configuration API Documentation:
// https://docs.astro.build/reference/configuration-reference
// @type-check enabled!
// VSCode and other TypeScript-enabled text editors will provide auto-completion,
// helpful tooltips, and warnings if your exported object is invalid.
// You can disable this by removing "@ts-check" and `@type` comments below.
// @ts-check
export default /** @type {import('astro').AstroUserConfig} */ ({
buildOptions: {
sitemap: true,
site: 'https://astro.build/',
},
};
});