diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts index 60a8f3c32..a9399aae5 100644 --- a/packages/astro/src/cli/index.ts +++ b/packages/astro/src/cli/index.ts @@ -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}`) - ); } /** diff --git a/packages/astro/src/core/config/config.ts b/packages/astro/src/core/config/config.ts index 4395dceae..dc7de2bc7 100644 --- a/packages/astro/src/core/config/config.ts +++ b/packages/astro/src/core/config/config.ts @@ -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 { 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; - filePath?: string; -} - -async function tryLoadConfig( +async function loadConfig( configOptions: LoadConfigOptions, root: string -): Promise { +): Promise> { const fsMod = configOptions.fsMod ?? fs; - let finallyCleanup = async () => {}; - try { - let 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 configPath = await resolveConfigPath({ + cwd: configOptions.cwd, + flags: configOptions.flags, + fs: fsMod, + }); + if (!configPath) return {}; - 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 - const config = await loadConfigWithVite({ - configPath, - fs: fsMod, - root, - }); - return config as TryLoadConfigResult; - } finally { - await finallyCleanup(); - } + // Create a vite server to load the config + return await loadConfigWithVite({ + configPath, + fs: fsMod, + root, + }); } /** 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, - overrides: Record, - rootPath: string -) { - const merged: Record = { ...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, - overrides: Record, - isRoot = true -): Record { - return mergeConfigRecursively(defaults, overrides, isRoot ? '' : '.'); -} diff --git a/packages/astro/src/core/config/index.ts b/packages/astro/src/core/config/index.ts index bbc64f512..b7b616951 100644 --- a/packages/astro/src/core/config/index.ts +++ b/packages/astro/src/core/config/index.ts @@ -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'; diff --git a/packages/astro/src/core/config/merge.ts b/packages/astro/src/core/config/merge.ts new file mode 100644 index 000000000..3a498332b --- /dev/null +++ b/packages/astro/src/core/config/merge.ts @@ -0,0 +1,53 @@ +import { mergeConfig as mergeViteConfig } from 'vite'; +import { arraify, isObject, isURL } from '../util.js'; + +function mergeConfigRecursively( + defaults: Record, + overrides: Record, + rootPath: string +) { + const merged: Record = { ...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, + overrides: Record, + isRoot = true +): Record { + return mergeConfigRecursively(defaults, overrides, isRoot ? '' : '.'); +} diff --git a/packages/astro/src/core/config/vite-load.ts b/packages/astro/src/core/config/vite-load.ts index a4fc5e9a6..86e3cf80e 100644 --- a/packages/astro/src/core/config/vite-load.ts +++ b/packages/astro/src/core/config/vite-load.ts @@ -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 { - const viteServer = await vite.createServer({ +async function createViteServer(root: string, fs: typeof fsType): Promise { + 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; - 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> { 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(); } } } diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts index b243ba979..ba8bcc1ef 100644 --- a/packages/astro/src/integrations/index.ts +++ b/packages/astro/src/integrations/index.ts @@ -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';