Refactor config loading (#7622)

This commit is contained in:
Bjorn Lu 2023-07-12 19:54:14 +08:00 committed by GitHub
parent b07d61a895
commit 0952a815b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 89 additions and 150 deletions

View file

@ -102,13 +102,10 @@ async function handleConfigError(
error(logging, 'astro', `Unable to load ${path ? colors.bold(path) : 'your Astro config'}\n`); error(logging, 'astro', `Unable to load ${path ? colors.bold(path) : 'your Astro config'}\n`);
if (e instanceof ZodError) { if (e instanceof ZodError) {
console.error(formatConfigErrorMessage(e) + '\n'); console.error(formatConfigErrorMessage(e) + '\n');
telemetry.record(eventConfigError({ cmd, err: e, isFatal: true }));
} else if (e instanceof Error) { } else if (e instanceof Error) {
console.error(formatErrorMessage(collectErrorMetadata(e)) + '\n'); console.error(formatErrorMessage(collectErrorMetadata(e)) + '\n');
} }
const telemetryPromise = telemetry.record(eventConfigError({ cmd, err: e, isFatal: true }));
await telemetryPromise.catch((err2: Error) =>
debug('telemetry', `record() error: ${err2.message}`)
);
} }
/** /**

View file

@ -5,9 +5,7 @@ import fs from 'fs';
import * as colors from 'kleur/colors'; import * as colors from 'kleur/colors';
import path from 'path'; import path from 'path';
import { fileURLToPath, pathToFileURL } from 'url'; import { fileURLToPath, pathToFileURL } from 'url';
import { mergeConfig as mergeViteConfig } from 'vite';
import { AstroError, AstroErrorData } from '../errors/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js';
import { arraify, isObject, isURL } from '../util.js';
import { createRelativeSchema } from './schema.js'; import { createRelativeSchema } from './schema.js';
import { loadConfigWithVite } from './vite-load.js'; import { loadConfigWithVite } from './vite-load.js';
@ -210,12 +208,8 @@ interface OpenConfigResult {
export async function openConfig(configOptions: LoadConfigOptions): Promise<OpenConfigResult> { export async function openConfig(configOptions: LoadConfigOptions): Promise<OpenConfigResult> {
const root = resolveRoot(configOptions.cwd); const root = resolveRoot(configOptions.cwd);
const flags = resolveFlags(configOptions.flags || {}); const flags = resolveFlags(configOptions.flags || {});
let userConfig: AstroUserConfig = {};
const config = await tryLoadConfig(configOptions, root); const userConfig = await loadConfig(configOptions, root);
if (config) {
userConfig = config.value;
}
const astroConfig = await resolveConfig(userConfig, root, flags, configOptions.cmd); const astroConfig = await resolveConfig(userConfig, root, flags, configOptions.cmd);
return { return {
@ -226,54 +220,24 @@ export async function openConfig(configOptions: LoadConfigOptions): Promise<Open
}; };
} }
interface TryLoadConfigResult { async function loadConfig(
value: Record<string, any>;
filePath?: string;
}
async function tryLoadConfig(
configOptions: LoadConfigOptions, configOptions: LoadConfigOptions,
root: string root: string
): Promise<TryLoadConfigResult | undefined> { ): Promise<Record<string, any>> {
const fsMod = configOptions.fsMod ?? fs; const fsMod = configOptions.fsMod ?? fs;
let finallyCleanup = async () => {}; const configPath = await resolveConfigPath({
try {
let configPath = await resolveConfigPath({
cwd: configOptions.cwd, cwd: configOptions.cwd,
flags: configOptions.flags, flags: configOptions.flags,
fs: fsMod, fs: fsMod,
}); });
if (!configPath) return undefined; if (!configPath) return {};
if (configOptions.isRestart) {
// Hack: Write config to temporary file at project root
// This invalidates and reloads file contents when using ESM imports or "resolve"
const tempConfigPath = path.join(
root,
`.temp.${Date.now()}.config${path.extname(configPath)}`
);
const currentConfigContent = await fsMod.promises.readFile(configPath, 'utf-8');
await fs.promises.writeFile(tempConfigPath, currentConfigContent);
finallyCleanup = async () => {
try {
await fs.promises.unlink(tempConfigPath);
} catch {
/** file already removed */
}
};
configPath = tempConfigPath;
}
// Create a vite server to load the config // Create a vite server to load the config
const config = await loadConfigWithVite({ return await loadConfigWithVite({
configPath, configPath,
fs: fsMod, fs: fsMod,
root, root,
}); });
return config as TryLoadConfigResult;
} finally {
await finallyCleanup();
}
} }
/** Attempt to resolve an Astro configuration object. Normalize, validate, and return. */ /** Attempt to resolve an Astro configuration object. Normalize, validate, and return. */
@ -295,54 +259,3 @@ export function createDefaultDevConfig(
) { ) {
return resolveConfig(userConfig, root, undefined, 'dev'); return resolveConfig(userConfig, root, undefined, 'dev');
} }
function mergeConfigRecursively(
defaults: Record<string, any>,
overrides: Record<string, any>,
rootPath: string
) {
const merged: Record<string, any> = { ...defaults };
for (const key in overrides) {
const value = overrides[key];
if (value == null) {
continue;
}
const existing = merged[key];
if (existing == null) {
merged[key] = value;
continue;
}
// fields that require special handling:
if (key === 'vite' && rootPath === '') {
merged[key] = mergeViteConfig(existing, value);
continue;
}
if (Array.isArray(existing) || Array.isArray(value)) {
merged[key] = [...arraify(existing ?? []), ...arraify(value ?? [])];
continue;
}
if (isURL(existing) && isURL(value)) {
merged[key] = value;
continue;
}
if (isObject(existing) && isObject(value)) {
merged[key] = mergeConfigRecursively(existing, value, rootPath ? `${rootPath}.${key}` : key);
continue;
}
merged[key] = value;
}
return merged;
}
export function mergeConfig(
defaults: Record<string, any>,
overrides: Record<string, any>,
isRoot = true
): Record<string, any> {
return mergeConfigRecursively(defaults, overrides, isRoot ? '' : '.');
}

View file

@ -6,6 +6,7 @@ export {
resolveRoot, resolveRoot,
validateConfig, validateConfig,
} from './config.js'; } from './config.js';
export { mergeConfig } from './merge.js';
export type { AstroConfigSchema } from './schema'; export type { AstroConfigSchema } from './schema';
export { createDefaultDevSettings, createSettings } from './settings.js'; export { createDefaultDevSettings, createSettings } from './settings.js';
export { loadTSConfig, updateTSConfigForFramework } from './tsconfig.js'; export { loadTSConfig, updateTSConfigForFramework } from './tsconfig.js';

View file

@ -0,0 +1,53 @@
import { mergeConfig as mergeViteConfig } from 'vite';
import { arraify, isObject, isURL } from '../util.js';
function mergeConfigRecursively(
defaults: Record<string, any>,
overrides: Record<string, any>,
rootPath: string
) {
const merged: Record<string, any> = { ...defaults };
for (const key in overrides) {
const value = overrides[key];
if (value == null) {
continue;
}
const existing = merged[key];
if (existing == null) {
merged[key] = value;
continue;
}
// fields that require special handling:
if (key === 'vite' && rootPath === '') {
merged[key] = mergeViteConfig(existing, value);
continue;
}
if (Array.isArray(existing) || Array.isArray(value)) {
merged[key] = [...arraify(existing ?? []), ...arraify(value ?? [])];
continue;
}
if (isURL(existing) && isURL(value)) {
merged[key] = value;
continue;
}
if (isObject(existing) && isObject(value)) {
merged[key] = mergeConfigRecursively(existing, value, rootPath ? `${rootPath}.${key}` : key);
continue;
}
merged[key] = value;
}
return merged;
}
export function mergeConfig(
defaults: Record<string, any>,
overrides: Record<string, any>,
isRoot = true
): Record<string, any> {
return mergeConfigRecursively(defaults, overrides, isRoot ? '' : '.');
}

View file

@ -1,15 +1,11 @@
import type fsType from 'fs'; import type fsType from 'fs';
import { pathToFileURL } from 'url'; import { pathToFileURL } from 'url';
import * as vite from 'vite'; import { createServer, type ViteDevServer } from 'vite';
import loadFallbackPlugin from '../../vite-plugin-load-fallback/index.js'; import loadFallbackPlugin from '../../vite-plugin-load-fallback/index.js';
import { debug } from '../logger/core.js';
export interface ViteLoader { async function createViteServer(root: string, fs: typeof fsType): Promise<ViteDevServer> {
root: string; const viteServer = await createServer({
viteServer: vite.ViteDevServer;
}
async function createViteLoader(root: string, fs: typeof fsType): Promise<ViteLoader> {
const viteServer = await vite.createServer({
server: { middlewareMode: true, hmr: false, watch: { ignored: ['**'] } }, server: { middlewareMode: true, hmr: false, watch: { ignored: ['**'] } },
optimizeDeps: { disabled: true }, optimizeDeps: { disabled: true },
clearScreen: false, clearScreen: false,
@ -30,15 +26,12 @@ async function createViteLoader(root: string, fs: typeof fsType): Promise<ViteLo
plugins: [loadFallbackPlugin({ fs, root: pathToFileURL(root) })], plugins: [loadFallbackPlugin({ fs, root: pathToFileURL(root) })],
}); });
return { return viteServer;
root,
viteServer,
};
} }
interface LoadConfigWithViteOptions { interface LoadConfigWithViteOptions {
root: string; root: string;
configPath: string | undefined; configPath: string;
fs: typeof fsType; fs: typeof fsType;
} }
@ -46,44 +39,26 @@ export async function loadConfigWithVite({
configPath, configPath,
fs, fs,
root, root,
}: LoadConfigWithViteOptions): Promise<{ }: LoadConfigWithViteOptions): Promise<Record<string, any>> {
value: Record<string, any>;
filePath?: string;
}> {
// No config file found, return an empty config that will be populated with defaults
if (!configPath) {
return {
value: {},
filePath: undefined,
};
}
// Try loading with Node import()
if (/\.[cm]?js$/.test(configPath)) { if (/\.[cm]?js$/.test(configPath)) {
try { try {
const config = await import(pathToFileURL(configPath).toString()); const config = await import(pathToFileURL(configPath).toString() + '?t=' + Date.now());
return { return config.default ?? {};
value: config.default ?? {}, } catch (e) {
filePath: configPath, // We do not need to throw the error here as we have a Vite fallback below
}; debug('Failed to load config with Node', e);
} catch {
// We do not need to keep the error here because with fallback the error will be rethrown
// when/if it fails in Vite.
} }
} }
// Try Loading with Vite // Try Loading with Vite
let loader: ViteLoader | undefined; let server: ViteDevServer | undefined;
try { try {
loader = await createViteLoader(root, fs); server = await createViteServer(root, fs);
const mod = await loader.viteServer.ssrLoadModule(configPath); const mod = await server.ssrLoadModule(configPath, { fixStacktrace: true });
return { return mod.default ?? {};
value: mod.default ?? {},
filePath: configPath,
};
} finally { } finally {
if (loader) { if (server) {
await loader.viteServer.close(); await server.close();
} }
} }
} }

View file

@ -15,7 +15,7 @@ import type {
import type { SerializedSSRManifest } from '../core/app/types'; import type { SerializedSSRManifest } from '../core/app/types';
import type { PageBuildData } from '../core/build/types'; import type { PageBuildData } from '../core/build/types';
import { buildClientDirectiveEntrypoint } from '../core/client-directive/index.js'; import { buildClientDirectiveEntrypoint } from '../core/client-directive/index.js';
import { mergeConfig } from '../core/config/config.js'; import { mergeConfig } from '../core/config/index.js';
import { info, type LogOptions } from '../core/logger/core.js'; import { info, type LogOptions } from '../core/logger/core.js';
import { isServerLikeOutput } from '../prerender/utils.js'; import { isServerLikeOutput } from '../prerender/utils.js';