Skip to content
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

Improve config error handling #7852

Merged
merged 9 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/astro/src/cli/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type yargs from 'yargs-parser';
import _build from '../../core/build/index.js';
import type { LogOptions } from '../../core/logger/core.js';
import { printHelp } from '../../core/messages.js';
import { flagsToAstroInlineConfig } from '../load-settings.js';
import { flagsToAstroInlineConfig } from '../flags.js';

interface BuildOptions {
flags: yargs.Arguments;
Expand Down
12 changes: 9 additions & 3 deletions packages/astro/src/cli/check/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
import ora from 'ora';
import type { Arguments as Flags } from 'yargs-parser';
import type { AstroSettings } from '../../@types/astro';
import { resolveConfig } from '../../core/config/config.js';
import { createSettings } from '../../core/config/settings.js';
import type { LogOptions } from '../../core/logger/core.js';
import { debug, info } from '../../core/logger/core.js';
import { printHelp } from '../../core/messages.js';
import type { ProcessExit, SyncOptions } from '../../core/sync';
import { eventCliSession, telemetry } from '../../events/index.js';
import { runHookConfigSetup } from '../../integrations/index.js';
import { loadSettings } from '../load-settings.js';
import { flagsToAstroInlineConfig } from '../flags.js';
import { printDiagnostic } from './print.js';

type DiagnosticResult = {
Expand Down Expand Up @@ -96,8 +99,11 @@ export async function check({ logging, flags }: CheckPayload): Promise<AstroChec
return;
}

const settings = await loadSettings({ cmd: 'check', flags, logging });
if (!settings) return;
// Load settings
const inlineConfig = flagsToAstroInlineConfig(flags);
const { userConfig, astroConfig } = await resolveConfig(inlineConfig, 'check');
telemetry.record(eventCliSession('check', userConfig, flags));
const settings = createSettings(astroConfig, fileURLToPath(astroConfig.root));

const checkFlags = parseFlags(flags);
if (checkFlags.watch) {
Expand Down
14 changes: 3 additions & 11 deletions packages/astro/src/cli/dev/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { cyan } from 'kleur/colors';
import type yargs from 'yargs-parser';
import { resolveRoot } from '../../core/config/index.js';
import devServer from '../../core/dev/index.js';
import { info, type LogOptions } from '../../core/logger/core.js';
import type { LogOptions } from '../../core/logger/core.js';
import { printHelp } from '../../core/messages.js';
import { flagsToAstroInlineConfig, handleConfigError } from '../load-settings.js';
import { flagsToAstroInlineConfig } from '../flags.js';

interface DevOptions {
flags: yargs.Arguments;
Expand Down Expand Up @@ -34,12 +33,5 @@ export async function dev({ flags, logging }: DevOptions) {

const inlineConfig = flagsToAstroInlineConfig(flags);

return await devServer(inlineConfig, {
logging,
handleConfigError(e) {
const root = resolveRoot(flags.root);
handleConfigError(e, { cmd: 'dev', cwd: root, flags, logging });
info(logging, 'astro', 'Continuing with previous valid configuration\n');
},
});
return await devServer(inlineConfig, { logging });
}
23 changes: 23 additions & 0 deletions packages/astro/src/cli/flags.ts
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is a rename from load-settings.ts but Git doesn't see it that way.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Arguments as Flags } from 'yargs-parser';
import type { AstroInlineConfig } from '../@types/astro.js';

export function flagsToAstroInlineConfig(flags: Flags): AstroInlineConfig {
return {
configFile: typeof flags.config === 'string' ? flags.config : undefined,
root: typeof flags.root === 'string' ? flags.root : undefined,
site: typeof flags.site === 'string' ? flags.site : undefined,
base: typeof flags.base === 'string' ? flags.base : undefined,
markdown: {
drafts: typeof flags.drafts === 'boolean' ? flags.drafts : undefined,
},
server: {
port: typeof flags.port === 'number' ? flags.port : undefined,
host:
typeof flags.host === 'string' || typeof flags.host === 'boolean' ? flags.host : undefined,
open: typeof flags.open === 'boolean' ? flags.open : undefined,
},
experimental: {
assets: typeof flags.experimentalAssets === 'boolean' ? flags.experimentalAssets : undefined,
},
};
Comment on lines +19 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have a --experimentalViewTransitions flag?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't; I believe we forgot about it. We should probably add it.

}
2 changes: 1 addition & 1 deletion packages/astro/src/cli/info/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import whichPm from 'which-pm';
import type yargs from 'yargs-parser';
import { resolveConfig } from '../../core/config/index.js';
import { ASTRO_VERSION } from '../../core/constants.js';
import { flagsToAstroInlineConfig } from '../load-settings.js';
import { flagsToAstroInlineConfig } from '../flags.js';

interface InfoOptions {
flags: yargs.Arguments;
Expand Down
74 changes: 0 additions & 74 deletions packages/astro/src/cli/load-settings.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/astro/src/cli/preview/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type yargs from 'yargs-parser';
import type { LogOptions } from '../../core/logger/core.js';
import { printHelp } from '../../core/messages.js';
import previewServer from '../../core/preview/index.js';
import { flagsToAstroInlineConfig } from '../load-settings.js';
import { flagsToAstroInlineConfig } from '../flags.js';

interface PreviewOptions {
flags: yargs.Arguments;
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/cli/sync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type yargs from 'yargs-parser';
import type { LogOptions } from '../../core/logger/core.js';
import { printHelp } from '../../core/messages.js';
import { sync as _sync } from '../../core/sync/index.js';
import { flagsToAstroInlineConfig } from '../load-settings.js';
import { flagsToAstroInlineConfig } from '../flags.js';

interface SyncOptions {
flags: yargs.Arguments;
Expand Down
4 changes: 4 additions & 0 deletions packages/astro/src/cli/throw-and-exit.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
/* eslint-disable no-console */
import { collectErrorMetadata } from '../core/errors/dev/index.js';
import { isAstroConfigZodError } from '../core/errors/errors.js';
import { createSafeError } from '../core/errors/index.js';
import { debug } from '../core/logger/core.js';
import { formatErrorMessage } from '../core/messages.js';
import { eventError, telemetry } from '../events/index.js';

/** Display error and exit */
export async function throwAndExit(cmd: string, err: unknown) {
// Suppress ZodErrors from AstroConfig as the pre-logged error is sufficient
if (isAstroConfigZodError(err)) return;

let telemetryPromise: Promise<any>;
let errorMessage: string;
function exitWithErrorMessage() {
Expand Down
38 changes: 32 additions & 6 deletions packages/astro/src/core/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import * as colors from 'kleur/colors';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { ZodError } from 'zod';
import { eventConfigError, telemetry } from '../../events/index.js';
import { trackAstroConfigZodError } from '../errors/errors.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import { formatConfigErrorMessage } from '../messages.js';
import { mergeConfig } from './merge.js';
import { createRelativeSchema } from './schema.js';
import { loadConfigWithVite } from './vite-load.js';
Expand Down Expand Up @@ -80,7 +84,21 @@ export async function validateConfig(
const AstroConfigRelativeSchema = createRelativeSchema(cmd, root);

// First-Pass Validation
const result = await AstroConfigRelativeSchema.parseAsync(userConfig);
let result: AstroConfig;
try {
result = await AstroConfigRelativeSchema.parseAsync(userConfig);
} catch (e) {
// Improve config zod error messages
if (e instanceof ZodError) {
Comment on lines +88 to +92
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errors are now logged more directly.

// Mark this error so the callee can decide to suppress Zod's error if needed.
// We still want to throw the error to signal an error in validation.
trackAstroConfigZodError(e);
// eslint-disable-next-line no-console
console.error(formatConfigErrorMessage(e) + '\n');
telemetry.record(eventConfigError({ cmd, err: e, isFatal: true }));
}
throw e;
}

// If successful, return the result as a verified AstroConfig object.
return result;
Expand Down Expand Up @@ -172,11 +190,19 @@ async function loadConfig(
if (!configPath) return {};

// Create a vite server to load the config
return await loadConfigWithVite({
root,
configPath,
fs: fsMod,
});
try {
return await loadConfigWithVite({
root,
configPath,
fs: fsMod,
});
} catch (e) {
const configPathText = configFile ? colors.bold(configFile) : 'your Astro config';
// Config errors should bypass log level as it breaks startup
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the comment: Is it because --silent can potentially not log these errors?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah --silent would otherwise not log that the config failed to load. Try to pass the logging down here is a bit tricky, especially the fact that an Astro config logLevel can also contribute to how logging works.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually there's no logLevel config in the Astro config, got it mixed up. I think it's still probably fine always logging it here as the other code in this file also does that, but I'll revisit this again if it's not ideal.

// eslint-disable-next-line no-console
console.error(`${colors.bold(colors.red('[astro]'))} Unable to load ${configPathText}\n`);
throw e;
}
}

interface ResolveConfigResult {
Expand Down
4 changes: 0 additions & 4 deletions packages/astro/src/core/dev/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { createContainerWithAutomaticRestart } from './restart.js';

export interface DevOptions {
logging: LogOptions;
handleConfigError: (error: Error) => void;
}

export interface DevServer {
Expand All @@ -36,9 +35,6 @@ export default async function dev(
inlineConfig,
logging: options.logging,
fs,
handleConfigError: options.handleConfigError,
// eslint-disable-next-line no-console
beforeRestart: () => console.clear(),
Comment on lines -40 to -41
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The beforeRestart was only created because on console.clear(), as we don't want to call console.clear in tests (test results gets removed).

Personally I find clearing the console moves and cuts out the terminal a lot that it's better to not do it instead, so I removed it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally agree, especially if there are scripts/users that rely on stdout/stderr

});

// Start listening to the port
Expand Down
60 changes: 21 additions & 39 deletions packages/astro/src/core/dev/restart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import * as vite from 'vite';
import type { AstroInlineConfig, AstroSettings } from '../../@types/astro';
import { eventCliSession, telemetry } from '../../events/index.js';
import { createSettings, resolveConfig } from '../config/index.js';
import { collectErrorMetadata } from '../errors/dev/utils.js';
import { isAstroConfigZodError } from '../errors/errors.js';
import { createSafeError } from '../errors/index.js';
import { info, type LogOptions } from '../logger/core.js';
import { error as logError, info, type LogOptions } from '../logger/core.js';
import { formatErrorMessage } from '../messages.js';
import type { Container } from './container';
import { createContainer, isStarted, startContainer } from './container.js';

Expand Down Expand Up @@ -59,29 +62,15 @@ export function shouldRestartContainer(
return shouldRestart;
}

interface RestartContainerParams {
container: Container;
logMsg: string;
handleConfigError: (err: Error) => Promise<void> | void;
beforeRestart?: () => void;
}

export async function restartContainer({
container,
logMsg,
handleConfigError,
beforeRestart,
}: RestartContainerParams): Promise<{ container: Container; error: Error | null }> {
export async function restartContainer(
container: Container
): Promise<{ container: Container; error: Error | null }> {
const { logging, close, settings: existingSettings } = container;
container.restartInFlight = true;

if (beforeRestart) {
beforeRestart();
}
const needsStart = isStarted(container);
try {
const { astroConfig } = await resolveConfig(container.inlineConfig, 'dev', container.fs);
info(logging, 'astro', logMsg + '\n');
const settings = createSettings(astroConfig, fileURLToPath(existingSettings.config.root));
await close();
return {
Expand All @@ -90,7 +79,18 @@ export async function restartContainer({
};
} catch (_err) {
const error = createSafeError(_err);
await handleConfigError(error);
// Print all error messages except ZodErrors from AstroConfig as the pre-logged error is sufficient
if (!isAstroConfigZodError(_err)) {
logError(logging, 'config', formatErrorMessage(collectErrorMetadata(error)) + '\n');
}
// Inform connected clients of the config error
container.viteServer.ws.send({
type: 'error',
err: {
message: error.message,
stack: error.stack || '',
},
});
await close();
info(logging, 'astro', 'Continuing with previous valid configuration\n');
return {
Expand All @@ -104,8 +104,6 @@ export interface CreateContainerWithAutomaticRestart {
inlineConfig?: AstroInlineConfig;
logging: LogOptions;
fs: typeof nodeFs;
handleConfigError?: (error: Error) => void | Promise<void>;
beforeRestart?: () => void;
}

interface Restart {
Expand All @@ -117,8 +115,6 @@ export async function createContainerWithAutomaticRestart({
inlineConfig,
logging,
fs,
handleConfigError = () => {},
beforeRestart,
}: CreateContainerWithAutomaticRestart): Promise<Restart> {
const { userConfig, astroConfig } = await resolveConfig(inlineConfig ?? {}, 'dev', fs);
telemetry.record(eventCliSession('dev', userConfig));
Expand All @@ -140,23 +136,9 @@ export async function createContainerWithAutomaticRestart({
};

async function handleServerRestart(logMsg: string) {
info(logging, 'astro', logMsg + '\n');
const container = restart.container;
const { container: newContainer, error } = await restartContainer({
container,
logMsg,
beforeRestart,
async handleConfigError(err) {
// Send an error message to the client if one is connected.
await handleConfigError(err);
container.viteServer.ws.send({
type: 'error',
err: {
message: err.message,
stack: err.stack || '',
},
});
},
});
const { container: newContainer, error } = await restartContainer(container);
restart.container = newContainer;
// Add new watches because this is a new container with a new Vite server
addWatches();
Expand Down
Loading