Refactor config loading (#7622)
This commit is contained in:
parent
b07d61a895
commit
0952a815b7
6 changed files with 89 additions and 150 deletions
|
@ -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}`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 ? '' : '.');
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
53
packages/astro/src/core/config/merge.ts
Normal file
53
packages/astro/src/core/config/merge.ts
Normal 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 ? '' : '.');
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
Loading…
Reference in a new issue