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`);
if (e instanceof ZodError) {
console.error(formatConfigErrorMessage(e) + '\n');
telemetry.record(eventConfigError({ cmd, err: e, isFatal: true }));
} else if (e instanceof Error) {
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 path from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
import { mergeConfig as mergeViteConfig } from 'vite';
import { AstroError, AstroErrorData } from '../errors/index.js';
import { arraify, isObject, isURL } from '../util.js';
import { createRelativeSchema } from './schema.js';
import { loadConfigWithVite } from './vite-load.js';
@ -210,12 +208,8 @@ interface OpenConfigResult {
export async function openConfig(configOptions: LoadConfigOptions): Promise<OpenConfigResult> {
const root = resolveRoot(configOptions.cwd);
const flags = resolveFlags(configOptions.flags || {});
let userConfig: AstroUserConfig = {};
const config = await tryLoadConfig(configOptions, root);
if (config) {
userConfig = config.value;
}
const userConfig = await loadConfig(configOptions, root);
const astroConfig = await resolveConfig(userConfig, root, flags, configOptions.cmd);
return {
@ -226,54 +220,24 @@ export async function openConfig(configOptions: LoadConfigOptions): Promise<Open
};
}
interface TryLoadConfigResult {
value: Record<string, any>;
filePath?: string;
}
async function tryLoadConfig(
async function loadConfig(
configOptions: LoadConfigOptions,
root: string
): Promise<TryLoadConfigResult | undefined> {
): Promise<Record<string, any>> {
const fsMod = configOptions.fsMod ?? fs;
let finallyCleanup = async () => {};
try {
let configPath = await resolveConfigPath({
const configPath = await resolveConfigPath({
cwd: configOptions.cwd,
flags: configOptions.flags,
fs: fsMod,
});
if (!configPath) return undefined;
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;
}
if (!configPath) return {};
// Create a vite server to load the config
const config = await loadConfigWithVite({
return await loadConfigWithVite({
configPath,
fs: fsMod,
root,
});
return config as TryLoadConfigResult;
} finally {
await finallyCleanup();
}
}
/** Attempt to resolve an Astro configuration object. Normalize, validate, and return. */
@ -295,54 +259,3 @@ export function createDefaultDevConfig(
) {
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,
validateConfig,
} from './config.js';
export { mergeConfig } from './merge.js';
export type { AstroConfigSchema } from './schema';
export { createDefaultDevSettings, createSettings } from './settings.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 { pathToFileURL } from 'url';
import * as vite from 'vite';
import { createServer, type ViteDevServer } from 'vite';
import loadFallbackPlugin from '../../vite-plugin-load-fallback/index.js';
import { debug } from '../logger/core.js';
export interface ViteLoader {
root: string;
viteServer: vite.ViteDevServer;
}
async function createViteLoader(root: string, fs: typeof fsType): Promise<ViteLoader> {
const viteServer = await vite.createServer({
async function createViteServer(root: string, fs: typeof fsType): Promise<ViteDevServer> {
const viteServer = await createServer({
server: { middlewareMode: true, hmr: false, watch: { ignored: ['**'] } },
optimizeDeps: { disabled: true },
clearScreen: false,
@ -30,15 +26,12 @@ async function createViteLoader(root: string, fs: typeof fsType): Promise<ViteLo
plugins: [loadFallbackPlugin({ fs, root: pathToFileURL(root) })],
});
return {
root,
viteServer,
};
return viteServer;
}
interface LoadConfigWithViteOptions {
root: string;
configPath: string | undefined;
configPath: string;
fs: typeof fsType;
}
@ -46,44 +39,26 @@ export async function loadConfigWithVite({
configPath,
fs,
root,
}: LoadConfigWithViteOptions): Promise<{
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()
}: LoadConfigWithViteOptions): Promise<Record<string, any>> {
if (/\.[cm]?js$/.test(configPath)) {
try {
const config = await import(pathToFileURL(configPath).toString());
return {
value: config.default ?? {},
filePath: configPath,
};
} catch {
// We do not need to keep the error here because with fallback the error will be rethrown
// when/if it fails in Vite.
const config = await import(pathToFileURL(configPath).toString() + '?t=' + Date.now());
return config.default ?? {};
} catch (e) {
// We do not need to throw the error here as we have a Vite fallback below
debug('Failed to load config with Node', e);
}
}
// Try Loading with Vite
let loader: ViteLoader | undefined;
let server: ViteDevServer | undefined;
try {
loader = await createViteLoader(root, fs);
const mod = await loader.viteServer.ssrLoadModule(configPath);
return {
value: mod.default ?? {},
filePath: configPath,
};
server = await createViteServer(root, fs);
const mod = await server.ssrLoadModule(configPath, { fixStacktrace: true });
return mod.default ?? {};
} finally {
if (loader) {
await loader.viteServer.close();
if (server) {
await server.close();
}
}
}

View file

@ -15,7 +15,7 @@ import type {
import type { SerializedSSRManifest } from '../core/app/types';
import type { PageBuildData } from '../core/build/types';
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 { isServerLikeOutput } from '../prerender/utils.js';