Format config errors for humans (#1298)
* format config errors * fix bad root
This commit is contained in:
parent
ac2c00e99b
commit
3b4bbdc98d
7 changed files with 92 additions and 14 deletions
5
.changeset/cuddly-feet-hug.md
Normal file
5
.changeset/cuddly-feet-hug.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Add human readable config verification errors
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')}`;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
42
packages/astro/test/config-validate.test.js
Normal file
42
packages/astro/test/config-validate.test.js
Normal 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();
|
|
@ -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/',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue