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`);
|
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}`)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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 ? '' : '.');
|
|
||||||
}
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
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 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue