Refactor and improve Astro config loading flow (#7879)

This commit is contained in:
Bjorn Lu 2023-08-01 17:11:26 +08:00 committed by GitHub
parent 9e22038472
commit ebf7ebbf7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 740 additions and 825 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Refactor and improve Astro config loading flow

View file

@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
import { getErrorOverlayContent, silentLogging, testFactory } from './test-utils.js';
import { getErrorOverlayContent, testFactory } from './test-utils.js';
const test = testFactory({
root: './fixtures/errors/',
@ -12,10 +12,7 @@ const test = testFactory({
let devServer;
test.beforeAll(async ({ astro }) => {
devServer = await astro.startDevServer({
// Only test the error overlay, don't print to console
logging: silentLogging,
});
devServer = await astro.startDevServer();
});
test.afterAll(async ({ astro }) => {

View file

@ -1,6 +1,7 @@
import { expect, test as testBase } from '@playwright/test';
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadFixture as baseLoadFixture } from '../test/test-utils.js';
export const isWindows = process.platform === 'win32';
@ -24,7 +25,7 @@ export function loadFixture(inlineConfig) {
// without this, the main `loadFixture` helper will resolve relative to `packages/astro/test`
return baseLoadFixture({
...inlineConfig,
root: new URL(inlineConfig.root, import.meta.url).toString(),
root: fileURLToPath(new URL(inlineConfig.root, import.meta.url)),
server: {
port: testFileToPort.get(path.basename(inlineConfig.root)),
},

View file

@ -19,7 +19,7 @@ import type { PageBuildData } from '../core/build/types';
import type { AstroConfigSchema } from '../core/config';
import type { AstroTimer } from '../core/config/timer';
import type { AstroCookies } from '../core/cookies';
import type { LogOptions } from '../core/logger/core';
import type { LogOptions, LoggerLevel } from '../core/logger/core';
import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server';
import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
export type {
@ -1331,6 +1331,16 @@ export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
// TypeScript still confirms zod validation matches this type.
integrations: AstroIntegration[];
}
export interface AstroInlineConfig extends AstroUserConfig, AstroInlineOnlyConfig {}
export interface AstroInlineOnlyConfig {
configFile?: string | false;
mode?: RuntimeMode;
logLevel?: LoggerLevel;
/**
* @internal for testing only
*/
logging?: LogOptions;
}
export type ContentEntryModule = {
id: string;

View file

@ -9,7 +9,7 @@ import ora from 'ora';
import preferredPM from 'preferred-pm';
import prompts from 'prompts';
import type yargs from 'yargs-parser';
import { loadTSConfig, resolveConfigPath } from '../../core/config/index.js';
import { loadTSConfig, resolveConfigPath, resolveRoot } from '../../core/config/index.js';
import {
defaultTSConfig,
presets,
@ -23,12 +23,12 @@ import { appendForwardSlash } from '../../core/path.js';
import { apply as applyPolyfill } from '../../core/polyfill.js';
import { parseNpmName } from '../../core/util.js';
import { eventCliSession, telemetry } from '../../events/index.js';
import { createLoggingFromFlags } from '../flags.js';
import { generate, parse, t, visit } from './babel.js';
import { ensureImport } from './imports.js';
import { wrapDefaultExport } from './wrapper.js';
interface AddOptions {
logging: LogOptions;
flags: yargs.Arguments;
}
@ -86,7 +86,7 @@ async function getRegistry(): Promise<string> {
}
}
export async function add(names: string[], { flags, logging }: AddOptions) {
export async function add(names: string[], { flags }: AddOptions) {
telemetry.record(eventCliSession('add'));
applyPolyfill();
if (flags.help || names.length === 0) {
@ -130,10 +130,12 @@ export async function add(names: string[], { flags, logging }: AddOptions) {
// Some packages might have a common alias! We normalize those here.
const cwd = flags.root;
const logging = createLoggingFromFlags(flags);
const integrationNames = names.map((name) => (ALIASES.has(name) ? ALIASES.get(name)! : name));
const integrations = await validateIntegrations(integrationNames);
let installResult = await tryToInstallIntegrations({ integrations, cwd, flags, logging });
const root = pathToFileURL(cwd ? path.resolve(cwd) : process.cwd());
const rootPath = resolveRoot(cwd);
const root = pathToFileURL(rootPath);
// Append forward slash to compute relative paths
root.href = appendForwardSlash(root.href);
@ -199,7 +201,11 @@ export async function add(names: string[], { flags, logging }: AddOptions) {
}
}
const rawConfigPath = await resolveConfigPath({ cwd, flags, fs: fsMod });
const rawConfigPath = await resolveConfigPath({
root: rootPath,
configFile: flags.config,
fs: fsMod,
});
let configURL = rawConfigPath ? pathToFileURL(rawConfigPath) : undefined;
if (configURL) {

View file

@ -1,21 +1,31 @@
import type yargs from 'yargs-parser';
import _build from '../../core/build/index.js';
import type { LogOptions } from '../../core/logger/core.js';
import { loadSettings } from '../load-settings.js';
import { printHelp } from '../../core/messages.js';
import { flagsToAstroInlineConfig } from '../flags.js';
interface BuildOptions {
flags: yargs.Arguments;
logging: LogOptions;
}
export async function build({ flags, logging }: BuildOptions) {
const settings = await loadSettings({ cmd: 'build', flags, logging });
if (!settings) return;
export async function build({ flags }: BuildOptions) {
if (flags?.help || flags?.h) {
printHelp({
commandName: 'astro build',
usage: '[...flags]',
tables: {
Flags: [
['--drafts', `Include Markdown draft pages in the build.`],
['--help (-h)', 'See all available flags.'],
],
},
description: `Builds your site for deployment.`,
});
return;
}
await _build(settings, {
flags,
logging,
const inlineConfig = flagsToAstroInlineConfig(flags);
await _build(inlineConfig, {
teardownCompiler: true,
mode: flags.mode,
});
}

View file

@ -13,11 +13,16 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
import ora from 'ora';
import type { Arguments as Flags } from 'yargs-parser';
import type { AstroSettings } from '../../@types/astro';
import { resolveConfig } from '../../core/config/config.js';
import { createNodeLogging } from '../../core/config/logging.js';
import { createSettings } from '../../core/config/settings.js';
import type { LogOptions } from '../../core/logger/core.js';
import { debug, info } from '../../core/logger/core.js';
import { printHelp } from '../../core/messages.js';
import type { ProcessExit, SyncOptions } from '../../core/sync';
import { loadSettings } from '../load-settings.js';
import type { syncInternal } from '../../core/sync';
import { eventCliSession, telemetry } from '../../events/index.js';
import { runHookConfigSetup } from '../../integrations/index.js';
import { flagsToAstroInlineConfig } from '../flags.js';
import { printDiagnostic } from './print.js';
type DiagnosticResult = {
@ -31,11 +36,6 @@ export type CheckPayload = {
* Flags passed via CLI
*/
flags: Flags;
/**
* Logging options
*/
logging: LogOptions;
};
type CheckFlags = {
@ -77,9 +77,8 @@ const ASTRO_GLOB_PATTERN = '**/*.astro';
*
* @param {CheckPayload} options Options passed {@link AstroChecker}
* @param {Flags} options.flags Flags coming from the CLI
* @param {LogOptions} options.logging Logging options
*/
export async function check({ logging, flags }: CheckPayload): Promise<AstroChecker | undefined> {
export async function check({ flags }: CheckPayload): Promise<AstroChecker | undefined> {
if (flags.help || flags.h) {
printHelp({
commandName: 'astro check',
@ -95,8 +94,12 @@ export async function check({ logging, flags }: CheckPayload): Promise<AstroChec
return;
}
const settings = await loadSettings({ cmd: 'check', flags, logging });
if (!settings) return;
// Load settings
const inlineConfig = flagsToAstroInlineConfig(flags);
const logging = createNodeLogging(inlineConfig);
const { userConfig, astroConfig } = await resolveConfig(inlineConfig, 'check');
telemetry.record(eventCliSession('check', userConfig, flags));
const settings = createSettings(astroConfig, fileURLToPath(astroConfig.root));
const checkFlags = parseFlags(flags);
if (checkFlags.watch) {
@ -105,7 +108,7 @@ export async function check({ logging, flags }: CheckPayload): Promise<AstroChec
info(logging, 'check', 'Checking files');
}
const { syncCli } = await import('../../core/sync/index.js');
const { syncInternal } = await import('../../core/sync/index.js');
const root = settings.config.root;
const require = createRequire(import.meta.url);
const diagnosticChecker = new AstroCheck(
@ -116,7 +119,7 @@ export async function check({ logging, flags }: CheckPayload): Promise<AstroChec
);
return new AstroChecker({
syncCli,
syncInternal,
settings,
fileSystem: fs,
logging,
@ -130,7 +133,7 @@ type CheckerConstructor = {
isWatchMode: boolean;
syncCli: (settings: AstroSettings, options: SyncOptions) => Promise<ProcessExit>;
syncInternal: typeof syncInternal;
settings: Readonly<AstroSettings>;
@ -148,7 +151,7 @@ type CheckerConstructor = {
export class AstroChecker {
readonly #diagnosticsChecker: AstroCheck;
readonly #shouldWatch: boolean;
readonly #syncCli: (settings: AstroSettings, opts: SyncOptions) => Promise<ProcessExit>;
readonly #syncInternal: CheckerConstructor['syncInternal'];
readonly #settings: AstroSettings;
@ -162,14 +165,14 @@ export class AstroChecker {
constructor({
diagnosticChecker,
isWatchMode,
syncCli,
syncInternal,
settings,
fileSystem,
logging,
}: CheckerConstructor) {
this.#diagnosticsChecker = diagnosticChecker;
this.#shouldWatch = isWatchMode;
this.#syncCli = syncCli;
this.#syncInternal = syncInternal;
this.#logging = logging;
this.#settings = settings;
this.#fs = fileSystem;
@ -223,7 +226,14 @@ export class AstroChecker {
* @param openDocuments Whether the operation should open all `.astro` files
*/
async #checkAllFiles(openDocuments: boolean): Promise<CheckResult> {
const processExit = await this.#syncCli(this.#settings, {
// Run `astro:config:setup` before syncing to initialize integrations.
// We do this manually as we're calling `syncInternal` directly.
const syncSettings = await runHookConfigSetup({
settings: this.#settings,
logging: this.#logging,
command: 'build',
});
const processExit = await this.#syncInternal(syncSettings, {
logging: this.#logging,
fs: this.#fs,
});

View file

@ -1,31 +1,35 @@
import fs from 'node:fs';
import { cyan } from 'kleur/colors';
import type yargs from 'yargs-parser';
import { resolveConfigPath, resolveFlags } from '../../core/config/index.js';
import devServer from '../../core/dev/index.js';
import { info, type LogOptions } from '../../core/logger/core.js';
import { handleConfigError, loadSettings } from '../load-settings.js';
import { printHelp } from '../../core/messages.js';
import { flagsToAstroInlineConfig } from '../flags.js';
interface DevOptions {
flags: yargs.Arguments;
logging: LogOptions;
}
export async function dev({ flags, logging }: DevOptions) {
const settings = await loadSettings({ cmd: 'dev', flags, logging });
if (!settings) return;
export async function dev({ flags }: DevOptions) {
if (flags.help || flags.h) {
printHelp({
commandName: 'astro dev',
usage: '[...flags]',
tables: {
Flags: [
['--port', `Specify which port to run on. Defaults to 3000.`],
['--host', `Listen on all addresses, including LAN and public addresses.`],
['--host <custom-address>', `Expose on a network IP address at <custom-address>`],
['--open', 'Automatically open the app in the browser on server start'],
['--help (-h)', 'See all available flags.'],
],
},
description: `Check ${cyan(
'https://docs.astro.build/en/reference/cli-reference/#astro-dev'
)} for more information.`,
});
return;
}
const root = flags.root;
const configFlag = resolveFlags(flags).config;
const configFlagPath = configFlag ? await resolveConfigPath({ cwd: root, flags, fs }) : undefined;
const inlineConfig = flagsToAstroInlineConfig(flags);
return await devServer(settings, {
configFlag,
configFlagPath,
flags,
logging,
handleConfigError(e) {
handleConfigError(e, { cmd: 'dev', cwd: root, flags, logging });
info(logging, 'astro', 'Continuing with previous valid configuration\n');
},
});
return await devServer(inlineConfig);
}

View file

@ -0,0 +1,49 @@
import type { Arguments as Flags } from 'yargs-parser';
import type { AstroInlineConfig } from '../@types/astro.js';
import type { LogOptions } from '../core/logger/core.js';
import { nodeLogDestination } from '../core/logger/node.js';
export function flagsToAstroInlineConfig(flags: Flags): AstroInlineConfig {
return {
// Inline-only configs
configFile: typeof flags.config === 'string' ? flags.config : undefined,
mode: typeof flags.mode === 'string' ? (flags.mode as AstroInlineConfig['mode']) : undefined,
logLevel: flags.verbose ? 'debug' : flags.silent ? 'silent' : undefined,
// Astro user configs
root: typeof flags.root === 'string' ? flags.root : undefined,
site: typeof flags.site === 'string' ? flags.site : undefined,
base: typeof flags.base === 'string' ? flags.base : undefined,
markdown: {
drafts: typeof flags.drafts === 'boolean' ? flags.drafts : undefined,
},
server: {
port: typeof flags.port === 'number' ? flags.port : undefined,
host:
typeof flags.host === 'string' || typeof flags.host === 'boolean' ? flags.host : undefined,
open: typeof flags.open === 'boolean' ? flags.open : undefined,
},
experimental: {
assets: typeof flags.experimentalAssets === 'boolean' ? flags.experimentalAssets : undefined,
},
};
}
/**
* The `logging` is usually created from an `AstroInlineConfig`, but some flows like `add`
* doesn't read the AstroConfig directly, so we create a `logging` object from the CLI flags instead.
*/
export function createLoggingFromFlags(flags: Flags): LogOptions {
const logging: LogOptions = {
dest: nodeLogDestination,
level: 'info',
};
if (flags.verbose) {
logging.level = 'debug';
} else if (flags.silent) {
logging.level = 'silent';
}
return logging;
}

View file

@ -2,7 +2,6 @@
import * as colors from 'kleur/colors';
import yargs from 'yargs-parser';
import { ASTRO_VERSION } from '../core/constants.js';
import type { LogOptions } from '../core/logger/core.js';
type CLICommand =
| 'help'
@ -112,16 +111,10 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {
}
}
const { enableVerboseLogging, nodeLogDestination } = await import('../core/logger/node.js');
const logging: LogOptions = {
dest: nodeLogDestination,
level: 'info',
};
// In verbose/debug mode, we log the debug logs asap before any potential errors could appear
if (flags.verbose) {
logging.level = 'debug';
const { enableVerboseLogging } = await import('../core/logger/node.js');
enableVerboseLogging();
} else if (flags.silent) {
logging.level = 'silent';
}
// Start with a default NODE_ENV so Vite doesn't set an incorrect default when loading the Astro config
@ -135,12 +128,12 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {
case 'add': {
const { add } = await import('./add/index.js');
const packages = flags._.slice(3) as string[];
await add(packages, { flags, logging });
await add(packages, { flags });
return;
}
case 'dev': {
const { dev } = await import('./dev/index.js');
const server = await dev({ flags, logging });
const server = await dev({ flags });
if (server) {
return await new Promise(() => {}); // lives forever
}
@ -148,12 +141,12 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {
}
case 'build': {
const { build } = await import('./build/index.js');
await build({ flags, logging });
await build({ flags });
return;
}
case 'preview': {
const { preview } = await import('./preview/index.js');
const server = await preview({ flags, logging });
const server = await preview({ flags });
if (server) {
return await server.closed(); // keep alive until the server is closed
}
@ -162,7 +155,7 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {
case 'check': {
const { check } = await import('./check/index.js');
// We create a server to start doing our operations
const checkServer = await check({ flags, logging });
const checkServer = await check({ flags });
if (checkServer) {
if (checkServer.isWatchMode) {
await checkServer.watch();
@ -176,7 +169,7 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {
}
case 'sync': {
const { sync } = await import('./sync/index.js');
const exitCode = await sync({ flags, logging });
const exitCode = await sync({ flags });
return process.exit(exitCode);
}
}

View file

@ -3,14 +3,16 @@ import * as colors from 'kleur/colors';
import { arch, platform } from 'node:os';
import whichPm from 'which-pm';
import type yargs from 'yargs-parser';
import { openConfig } from '../../core/config/index.js';
import { resolveConfig } from '../../core/config/index.js';
import { ASTRO_VERSION } from '../../core/constants.js';
import { flagsToAstroInlineConfig } from '../flags.js';
interface InfoOptions {
flags: yargs.Arguments;
}
export async function printInfo({ flags }: InfoOptions) {
const inlineConfig = flagsToAstroInlineConfig(flags);
const packageManager = await whichPm(process.cwd());
let adapter = "Couldn't determine.";
let integrations = [];
@ -22,11 +24,7 @@ export async function printInfo({ flags }: InfoOptions) {
}
try {
const { userConfig } = await openConfig({
cwd: flags.root,
flags,
cmd: 'info',
});
const { userConfig } = await resolveConfig(inlineConfig, 'info');
if (userConfig.adapter?.name) {
adapter = userConfig.adapter.name;
}

View file

@ -1,47 +0,0 @@
/* eslint-disable no-console */
import * as colors from 'kleur/colors';
import fs from 'node:fs';
import type { Arguments as Flags } from 'yargs-parser';
import { ZodError } from 'zod';
import { createSettings, openConfig, resolveConfigPath } from '../core/config/index.js';
import { collectErrorMetadata } from '../core/errors/dev/index.js';
import { error, type LogOptions } from '../core/logger/core.js';
import { formatConfigErrorMessage, formatErrorMessage } from '../core/messages.js';
import * as event from '../events/index.js';
import { eventConfigError, telemetry } from '../events/index.js';
interface LoadSettingsOptions {
cmd: string;
flags: Flags;
logging: LogOptions;
}
export async function loadSettings({ cmd, flags, logging }: LoadSettingsOptions) {
const root = flags.root;
const { astroConfig: initialAstroConfig, userConfig: initialUserConfig } = await openConfig({
cwd: root,
flags,
cmd,
}).catch(async (e) => {
await handleConfigError(e, { cmd, cwd: root, flags, logging });
return {} as any;
});
if (!initialAstroConfig) return;
telemetry.record(event.eventCliSession(cmd, initialUserConfig, flags));
return createSettings(initialAstroConfig, root);
}
export async function handleConfigError(
e: any,
{ cmd, cwd, flags, logging }: { cmd: string; cwd?: string; flags?: Flags; logging: LogOptions }
) {
const path = await resolveConfigPath({ cwd, flags, fs });
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');
}
}

View file

@ -1,16 +1,32 @@
import { cyan } from 'kleur/colors';
import type yargs from 'yargs-parser';
import type { LogOptions } from '../../core/logger/core.js';
import { printHelp } from '../../core/messages.js';
import previewServer from '../../core/preview/index.js';
import { loadSettings } from '../load-settings.js';
import { flagsToAstroInlineConfig } from '../flags.js';
interface PreviewOptions {
flags: yargs.Arguments;
logging: LogOptions;
}
export async function preview({ flags, logging }: PreviewOptions) {
const settings = await loadSettings({ cmd: 'preview', flags, logging });
if (!settings) return;
export async function preview({ flags }: PreviewOptions) {
if (flags?.help || flags?.h) {
printHelp({
commandName: 'astro preview',
usage: '[...flags]',
tables: {
Flags: [
['--open', 'Automatically open the app in the browser on server start'],
['--help (-h)', 'See all available flags.'],
],
},
description: `Starts a local server to serve your static dist/ directory. Check ${cyan(
'https://docs.astro.build/en/reference/cli-reference/#astro-preview'
)} for more information.`,
});
return;
}
return await previewServer(settings, { flags, logging });
const inlineConfig = flagsToAstroInlineConfig(flags);
return await previewServer(inlineConfig);
}

View file

@ -1,18 +1,27 @@
import fs from 'node:fs';
import type yargs from 'yargs-parser';
import type { LogOptions } from '../../core/logger/core.js';
import { syncCli } from '../../core/sync/index.js';
import { loadSettings } from '../load-settings.js';
import { printHelp } from '../../core/messages.js';
import { sync as _sync } from '../../core/sync/index.js';
import { flagsToAstroInlineConfig } from '../flags.js';
interface SyncOptions {
flags: yargs.Arguments;
logging: LogOptions;
}
export async function sync({ flags, logging }: SyncOptions) {
const settings = await loadSettings({ cmd: 'sync', flags, logging });
if (!settings) return;
export async function sync({ flags }: SyncOptions) {
if (flags?.help || flags?.h) {
printHelp({
commandName: 'astro sync',
usage: '[...flags]',
tables: {
Flags: [['--help (-h)', 'See all available flags.']],
},
description: `Generates TypeScript types for all Astro modules.`,
});
return 0;
}
const exitCode = await syncCli(settings, { logging, fs, flags });
const inlineConfig = flagsToAstroInlineConfig(flags);
const exitCode = await _sync(inlineConfig);
return exitCode;
}

View file

@ -1,5 +1,6 @@
/* eslint-disable no-console */
import { collectErrorMetadata } from '../core/errors/dev/index.js';
import { isAstroConfigZodError } from '../core/errors/errors.js';
import { createSafeError } from '../core/errors/index.js';
import { debug } from '../core/logger/core.js';
import { formatErrorMessage } from '../core/messages.js';
@ -7,6 +8,9 @@ import { eventError, telemetry } from '../events/index.js';
/** Display error and exit */
export async function throwAndExit(cmd: string, err: unknown) {
// Suppress ZodErrors from AstroConfig as the pre-logged error is sufficient
if (isAstroConfigZodError(err)) return;
let telemetryPromise: Promise<any>;
let errorMessage: string;
function exitWithErrorMessage() {

View file

@ -17,7 +17,7 @@ export function getViteConfig(inlineConfig: UserConfig) {
fs,
{ mergeConfig },
{ nodeLogDestination },
{ openConfig, createSettings },
{ resolveConfig, createSettings },
{ createVite },
{ runHookConfigSetup, runHookConfigDone },
{ astroContentListenPlugin },
@ -34,7 +34,7 @@ export function getViteConfig(inlineConfig: UserConfig) {
dest: nodeLogDestination,
level: 'info',
};
const { astroConfig: config } = await openConfig({ cmd });
const { astroConfig: config } = await resolveConfig({}, cmd);
const settings = createSettings(config, inlineConfig.root);
await runHookConfigSetup({ settings, command: cmd, logging });
const viteConfig = await createVite(

View file

@ -1,10 +1,18 @@
import * as colors from 'kleur/colors';
import fs from 'node:fs';
import { performance } from 'node:perf_hooks';
import { fileURLToPath } from 'node:url';
import type * as vite from 'vite';
import type yargs from 'yargs-parser';
import type { AstroConfig, AstroSettings, ManifestData, RuntimeMode } from '../../@types/astro';
import type {
AstroConfig,
AstroInlineConfig,
AstroSettings,
ManifestData,
RuntimeMode,
} from '../../@types/astro';
import { injectImageEndpoint } from '../../assets/internal.js';
import { telemetry } from '../../events/index.js';
import { eventCliSession } from '../../events/session.js';
import {
runHookBuildDone,
runHookBuildStart,
@ -12,9 +20,11 @@ import {
runHookConfigSetup,
} from '../../integrations/index.js';
import { isServerLikeOutput } from '../../prerender/utils.js';
import { resolveConfig } from '../config/config.js';
import { createNodeLogging } from '../config/logging.js';
import { createSettings } from '../config/settings.js';
import { createVite } from '../create-vite.js';
import { debug, info, levels, timerMessage, warn, type LogOptions } from '../logger/core.js';
import { printHelp } from '../messages.js';
import { apply as applyPolyfill } from '../polyfill.js';
import { RouteCache } from '../render/route-cache.js';
import { createRouteManifest } from '../routing/index.js';
@ -24,38 +34,38 @@ import type { StaticBuildOptions } from './types.js';
import { getTimeStat } from './util.js';
export interface BuildOptions {
mode?: RuntimeMode;
logging: LogOptions;
/**
* Teardown the compiler WASM instance after build. This can improve performance when
* building once, but may cause a performance hit if building multiple times in a row.
*/
teardownCompiler?: boolean;
flags?: yargs.Arguments;
}
/** `astro build` */
export default async function build(settings: AstroSettings, options: BuildOptions): Promise<void> {
export default async function build(
inlineConfig: AstroInlineConfig,
options: BuildOptions
): Promise<void> {
applyPolyfill();
if (options.flags?.help || options.flags?.h) {
printHelp({
commandName: 'astro build',
usage: '[...flags]',
tables: {
Flags: [
['--drafts', `Include Markdown draft pages in the build.`],
['--help (-h)', 'See all available flags.'],
],
},
description: `Builds your site for deployment.`,
});
return;
}
const logging = createNodeLogging(inlineConfig);
const { userConfig, astroConfig } = await resolveConfig(inlineConfig, 'build');
telemetry.record(eventCliSession('build', userConfig));
const builder = new AstroBuilder(settings, options);
const settings = createSettings(astroConfig, fileURLToPath(astroConfig.root));
const builder = new AstroBuilder(settings, {
...options,
logging,
mode: inlineConfig.mode,
});
await builder.run();
}
interface AstroBuilderOptions extends BuildOptions {
logging: LogOptions;
mode?: RuntimeMode;
}
class AstroBuilder {
private settings: AstroSettings;
private logging: LogOptions;
@ -66,7 +76,7 @@ class AstroBuilder {
private timer: Record<string, number>;
private teardownCompiler: boolean;
constructor(settings: AstroSettings, options: BuildOptions) {
constructor(settings: AstroSettings, options: AstroBuilderOptions) {
if (options.mode) {
this.mode = options.mode;
}
@ -112,8 +122,8 @@ class AstroBuilder {
);
await runHookConfigDone({ settings: this.settings, logging });
const { sync } = await import('../sync/index.js');
const syncRet = await sync(this.settings, { logging, fs });
const { syncInternal } = await import('../sync/index.js');
const syncRet = await syncInternal(this.settings, { logging, fs });
if (syncRet !== 0) {
return process.exit(syncRet);
}

View file

@ -1,16 +1,26 @@
import type { Arguments as Flags } from 'yargs-parser';
import type { AstroConfig, AstroUserConfig, CLIFlags } from '../../@types/astro';
import type {
AstroConfig,
AstroInlineConfig,
AstroInlineOnlyConfig,
AstroUserConfig,
CLIFlags,
} from '../../@types/astro';
import * as colors from 'kleur/colors';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { ZodError } from 'zod';
import { eventConfigError, telemetry } from '../../events/index.js';
import { trackAstroConfigZodError } from '../errors/errors.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import { formatConfigErrorMessage } from '../messages.js';
import { mergeConfig } from './merge.js';
import { createRelativeSchema } from './schema.js';
import { loadConfigWithVite } from './vite-load.js';
export const LEGACY_ASTRO_CONFIG_KEYS = new Set([
const LEGACY_ASTRO_CONFIG_KEYS = new Set([
'projectRoot',
'src',
'pages',
@ -80,13 +90,29 @@ export async function validateConfig(
const AstroConfigRelativeSchema = createRelativeSchema(cmd, root);
// First-Pass Validation
const result = await AstroConfigRelativeSchema.parseAsync(userConfig);
let result: AstroConfig;
try {
result = await AstroConfigRelativeSchema.parseAsync(userConfig);
} catch (e) {
// Improve config zod error messages
if (e instanceof ZodError) {
// Mark this error so the callee can decide to suppress Zod's error if needed.
// We still want to throw the error to signal an error in validation.
trackAstroConfigZodError(e);
// eslint-disable-next-line no-console
console.error(formatConfigErrorMessage(e) + '\n');
telemetry.record(eventConfigError({ cmd, err: e, isFatal: true }));
}
throw e;
}
// If successful, return the result as a verified AstroConfig object.
return result;
}
/** Convert the generic "yargs" flag object into our own, custom TypeScript object. */
// NOTE: This function will be removed in a later PR. Use `flagsToAstroInlineConfig` instead.
// All CLI related flow should be located in the `packages/astro/src/cli` directory.
export function resolveFlags(flags: Partial<Flags>): CLIFlags {
return {
root: typeof flags.root === 'string' ? flags.root : undefined,
@ -110,22 +136,6 @@ export function resolveRoot(cwd?: string | URL): string {
return cwd ? path.resolve(cwd) : process.cwd();
}
/** Merge CLI flags & user config object (CLI flags take priority) */
function mergeCLIFlags(astroConfig: AstroUserConfig, flags: CLIFlags) {
return mergeConfig(astroConfig, {
site: flags.site,
base: flags.base,
markdown: {
drafts: flags.drafts,
},
server: {
port: flags.port,
host: flags.host,
open: flags.open,
},
});
}
async function search(fsMod: typeof fs, root: string) {
const paths = [
'astro.config.mjs',
@ -143,19 +153,9 @@ async function search(fsMod: typeof fs, root: string) {
}
}
interface LoadConfigOptions {
cwd?: string;
flags?: Flags;
cmd: string;
validate?: boolean;
/** Invalidate when reloading a previously loaded config */
isRestart?: boolean;
fsMod?: typeof fs;
}
interface ResolveConfigPathOptions {
cwd?: string;
flags?: Flags;
root: string;
configFile?: string;
fs: typeof fs;
}
@ -163,87 +163,85 @@ interface ResolveConfigPathOptions {
* Resolve the file URL of the user's `astro.config.js|cjs|mjs|ts` file
*/
export async function resolveConfigPath(
configOptions: ResolveConfigPathOptions
options: ResolveConfigPathOptions
): Promise<string | undefined> {
const root = resolveRoot(configOptions.cwd);
const flags = resolveFlags(configOptions.flags || {});
let userConfigPath: string | undefined;
if (flags?.config) {
userConfigPath = /^\.*\//.test(flags.config) ? flags.config : `./${flags.config}`;
userConfigPath = fileURLToPath(new URL(userConfigPath, `file://${root}/`));
if (!configOptions.fs.existsSync(userConfigPath)) {
if (options.configFile) {
userConfigPath = path.join(options.root, options.configFile);
if (!options.fs.existsSync(userConfigPath)) {
throw new AstroError({
...AstroErrorData.ConfigNotFound,
message: AstroErrorData.ConfigNotFound.message(flags.config),
message: AstroErrorData.ConfigNotFound.message(options.configFile),
});
}
} else {
userConfigPath = await search(configOptions.fs, root);
userConfigPath = await search(options.fs, options.root);
}
return userConfigPath;
}
interface OpenConfigResult {
userConfig: AstroUserConfig;
astroConfig: AstroConfig;
flags: CLIFlags;
root: string;
}
/** Load a configuration file, returning both the userConfig and astroConfig */
export async function openConfig(configOptions: LoadConfigOptions): Promise<OpenConfigResult> {
const root = resolveRoot(configOptions.cwd);
const flags = resolveFlags(configOptions.flags || {});
const userConfig = await loadConfig(configOptions, root);
const astroConfig = await resolveConfig(userConfig, root, flags, configOptions.cmd);
return {
astroConfig,
userConfig,
flags,
root,
};
}
async function loadConfig(
configOptions: LoadConfigOptions,
root: string
root: string,
configFile?: string | false,
fsMod = fs
): Promise<Record<string, any>> {
const fsMod = configOptions.fsMod ?? fs;
if (configFile === false) return {};
const configPath = await resolveConfigPath({
cwd: configOptions.cwd,
flags: configOptions.flags,
root,
configFile,
fs: fsMod,
});
if (!configPath) return {};
// Create a vite server to load the config
return await loadConfigWithVite({
configPath,
fs: fsMod,
root,
});
try {
return await loadConfigWithVite({
root,
configPath,
fs: fsMod,
});
} catch (e) {
const configPathText = configFile ? colors.bold(configFile) : 'your Astro config';
// Config errors should bypass log level as it breaks startup
// eslint-disable-next-line no-console
console.error(`${colors.bold(colors.red('[astro]'))} Unable to load ${configPathText}\n`);
throw e;
}
}
function splitInlineConfig(inlineConfig: AstroInlineConfig): {
inlineUserConfig: AstroUserConfig;
inlineOnlyConfig: AstroInlineOnlyConfig;
} {
const { configFile, mode, logLevel, ...inlineUserConfig } = inlineConfig;
return {
inlineUserConfig,
inlineOnlyConfig: {
configFile,
mode,
logLevel,
},
};
}
interface ResolveConfigResult {
userConfig: AstroUserConfig;
astroConfig: AstroConfig;
}
/** Attempt to resolve an Astro configuration object. Normalize, validate, and return. */
export async function resolveConfig(
userConfig: AstroUserConfig,
root: string,
flags: CLIFlags = {},
cmd: string
): Promise<AstroConfig> {
const mergedConfig = mergeCLIFlags(userConfig, flags);
const validatedConfig = await validateConfig(mergedConfig, root, cmd);
inlineConfig: AstroInlineConfig,
command: string,
fsMod = fs
): Promise<ResolveConfigResult> {
const root = resolveRoot(inlineConfig.root);
const { inlineUserConfig, inlineOnlyConfig } = splitInlineConfig(inlineConfig);
return validatedConfig;
}
const userConfig = await loadConfig(root, inlineOnlyConfig.configFile, fsMod);
const mergedConfig = mergeConfig(userConfig, inlineUserConfig);
const astroConfig = await validateConfig(mergedConfig, root, command);
export function createDefaultDevConfig(
userConfig: AstroUserConfig = {},
root: string = process.cwd()
) {
return resolveConfig(userConfig, root, undefined, 'dev');
return { userConfig, astroConfig };
}

View file

@ -1,12 +1,6 @@
export {
createDefaultDevConfig,
openConfig,
resolveConfigPath,
resolveFlags,
resolveRoot,
validateConfig,
} from './config.js';
export { resolveConfig, resolveConfigPath, resolveFlags, resolveRoot } from './config.js';
export { createNodeLogging } from './logging.js';
export { mergeConfig } from './merge.js';
export type { AstroConfigSchema } from './schema';
export { createDefaultDevSettings, createSettings } from './settings.js';
export { createSettings } from './settings.js';
export { loadTSConfig, updateTSConfigForFramework } from './tsconfig.js';

View file

@ -0,0 +1,13 @@
import type { AstroInlineConfig } from '../../@types/astro.js';
import type { LogOptions } from '../logger/core.js';
import { nodeLogDestination } from '../logger/node.js';
export function createNodeLogging(inlineConfig: AstroInlineConfig): LogOptions {
// For internal testing, the inline config can pass the raw `logging` object directly
if (inlineConfig.logging) return inlineConfig.logging;
return {
dest: nodeLogDestination,
level: inlineConfig.logLevel ?? 'info',
};
}

View file

@ -1,7 +1,7 @@
import yaml from 'js-yaml';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { AstroConfig, AstroSettings, AstroUserConfig } from '../../@types/astro';
import type { AstroConfig, AstroSettings } from '../../@types/astro';
import { getContentPaths } from '../../content/index.js';
import jsxRenderer from '../../jsx/renderer.js';
import { markdownContentEntryType } from '../../vite-plugin-markdown/content-entry-type.js';
@ -9,7 +9,6 @@ import { getDefaultClientDirectives } from '../client-directive/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import { formatYAMLException, isYAMLException } from '../errors/utils.js';
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../constants.js';
import { createDefaultDevConfig } from './config.js';
import { AstroTimer } from './timer.js';
import { loadTSConfig } from './tsconfig.js';
@ -119,14 +118,3 @@ export function createSettings(config: AstroConfig, cwd?: string): AstroSettings
settings.watchFiles = watchFiles;
return settings;
}
export async function createDefaultDevSettings(
userConfig: AstroUserConfig = {},
root?: string | URL
): Promise<AstroSettings> {
if (root && typeof root !== 'string') {
root = fileURLToPath(root);
}
const config = await createDefaultDevConfig(userConfig, root);
return createBaseSettings(config);
}

View file

@ -1,6 +1,6 @@
import type * as http from 'node:http';
import type { AddressInfo } from 'node:net';
import type { AstroSettings, AstroUserConfig } from '../../@types/astro';
import type { AstroInlineConfig, AstroSettings } from '../../@types/astro';
import nodeFs from 'node:fs';
import * as vite from 'vite';
@ -11,52 +11,36 @@ import {
runHookServerDone,
runHookServerStart,
} from '../../integrations/index.js';
import { createDefaultDevSettings, resolveRoot } from '../config/index.js';
import { createVite } from '../create-vite.js';
import type { LogOptions } from '../logger/core.js';
import { nodeLogDestination } from '../logger/node.js';
import { appendForwardSlash } from '../path.js';
import { apply as applyPolyfill } from '../polyfill.js';
const defaultLogging: LogOptions = {
dest: nodeLogDestination,
level: 'error',
};
export interface Container {
fs: typeof nodeFs;
logging: LogOptions;
settings: AstroSettings;
viteConfig: vite.InlineConfig;
viteServer: vite.ViteDevServer;
resolvedRoot: string;
configFlag: string | undefined;
configFlagPath: string | undefined;
inlineConfig: AstroInlineConfig;
restartInFlight: boolean; // gross
handle: (req: http.IncomingMessage, res: http.ServerResponse) => void;
close: () => Promise<void>;
}
export interface CreateContainerParams {
logging: LogOptions;
settings: AstroSettings;
inlineConfig?: AstroInlineConfig;
isRestart?: boolean;
logging?: LogOptions;
userConfig?: AstroUserConfig;
settings?: AstroSettings;
fs?: typeof nodeFs;
root?: string | URL;
// The string passed to --config and the resolved path
configFlag?: string;
configFlagPath?: string;
}
export async function createContainer(params: CreateContainerParams = {}): Promise<Container> {
let {
isRestart = false,
logging = defaultLogging,
settings = await createDefaultDevSettings(params.userConfig, params.root),
fs = nodeFs,
} = params;
export async function createContainer({
isRestart = false,
logging,
inlineConfig,
settings,
fs = nodeFs,
}: CreateContainerParams): Promise<Container> {
// Initialize
applyPolyfill();
settings = await runHookConfigSetup({
@ -94,14 +78,11 @@ export async function createContainer(params: CreateContainerParams = {}): Promi
const viteServer = await vite.createServer(viteConfig);
const container: Container = {
configFlag: params.configFlag,
configFlagPath: params.configFlagPath,
inlineConfig: inlineConfig ?? {},
fs,
logging,
resolvedRoot: appendForwardSlash(resolveRoot(params.root)),
restartInFlight: false,
settings,
viteConfig,
viteServer,
handle(req, res) {
viteServer.middlewares.handle(req, res, Function.prototype);
@ -143,18 +124,3 @@ export async function startContainer({
export function isStarted(container: Container): boolean {
return !!container.viteServer.httpServer?.listening;
}
/**
* Only used in tests
*/
export async function runInContainer(
params: CreateContainerParams,
callback: (container: Container) => Promise<void> | void
) {
const container = await createContainer(params);
try {
await callback(container);
} finally {
await container.close();
}
}

View file

@ -1,27 +1,16 @@
import { cyan } from 'kleur/colors';
import fs from 'node:fs';
import type http from 'node:http';
import type { AddressInfo } from 'node:net';
import { performance } from 'perf_hooks';
import type * as vite from 'vite';
import type yargs from 'yargs-parser';
import type { AstroSettings } from '../../@types/astro';
import type { AstroInlineConfig } from '../../@types/astro';
import { attachContentServerListeners } from '../../content/index.js';
import { telemetry } from '../../events/index.js';
import { info, warn, type LogOptions } from '../logger/core.js';
import { info, warn } from '../logger/core.js';
import * as msg from '../messages.js';
import { printHelp } from '../messages.js';
import { startContainer } from './container.js';
import { createContainerWithAutomaticRestart } from './restart.js';
export interface DevOptions {
configFlag: string | undefined;
configFlagPath: string | undefined;
flags?: yargs.Arguments;
logging: LogOptions;
handleConfigError: (error: Error) => void;
isRestart?: boolean;
}
export interface DevServer {
address: AddressInfo;
handle: (req: http.IncomingMessage, res: http.ServerResponse<http.IncomingMessage>) => void;
@ -30,68 +19,34 @@ export interface DevServer {
}
/** `astro dev` */
export default async function dev(
settings: AstroSettings,
options: DevOptions
): Promise<DevServer | undefined> {
if (options.flags?.help || options.flags?.h) {
printHelp({
commandName: 'astro dev',
usage: '[...flags]',
tables: {
Flags: [
['--port', `Specify which port to run on. Defaults to 3000.`],
['--host', `Listen on all addresses, including LAN and public addresses.`],
['--host <custom-address>', `Expose on a network IP address at <custom-address>`],
['--open', 'Automatically open the app in the browser on server start'],
['--help (-h)', 'See all available flags.'],
],
},
description: `Check ${cyan(
'https://docs.astro.build/en/reference/cli-reference/#astro-dev'
)} for more information.`,
});
return;
}
export default async function dev(inlineConfig: AstroInlineConfig): Promise<DevServer> {
const devStart = performance.now();
await telemetry.record([]);
// Create a container which sets up the Vite server.
const restart = await createContainerWithAutomaticRestart({
flags: options.flags ?? {},
handleConfigError: options.handleConfigError,
// eslint-disable-next-line no-console
beforeRestart: () => console.clear(),
params: {
settings,
root: options.flags?.root,
logging: options.logging,
isRestart: options.isRestart,
},
});
const restart = await createContainerWithAutomaticRestart({ inlineConfig, fs });
const logging = restart.container.logging;
// Start listening to the port
const devServerAddressInfo = await startContainer(restart.container);
info(
options.logging,
logging,
null,
msg.serverStart({
startupTime: performance.now() - devStart,
resolvedUrls: restart.container.viteServer.resolvedUrls || { local: [], network: [] },
host: settings.config.server.host,
base: settings.config.base,
isRestart: options.isRestart,
host: restart.container.settings.config.server.host,
base: restart.container.settings.config.base,
})
);
const currentVersion = process.env.PACKAGE_VERSION ?? '0.0.0';
if (currentVersion.includes('-')) {
warn(options.logging, null, msg.prerelease({ currentVersion }));
warn(logging, null, msg.prerelease({ currentVersion }));
}
if (restart.container.viteConfig.server?.fs?.strict === false) {
warn(options.logging, null, msg.fsStrictWarning());
if (restart.container.viteServer.config.server?.fs?.strict === false) {
warn(logging, null, msg.fsStrictWarning());
}
await attachContentServerListeners(restart.container);

View file

@ -1,3 +1,3 @@
export { createContainer, isStarted, runInContainer, startContainer } from './container.js';
export { createContainer, isStarted, startContainer } from './container.js';
export { default } from './dev.js';
export { createContainerWithAutomaticRestart } from './restart.js';

View file

@ -1,9 +1,15 @@
import nodeFs from 'node:fs';
import { fileURLToPath } from 'node:url';
import * as vite from 'vite';
import type { AstroSettings } from '../../@types/astro';
import { createSettings, openConfig } from '../config/index.js';
import type { AstroInlineConfig, AstroSettings } from '../../@types/astro';
import { eventCliSession, telemetry } from '../../events/index.js';
import { createNodeLogging, createSettings, resolveConfig } from '../config/index.js';
import { collectErrorMetadata } from '../errors/dev/utils.js';
import { isAstroConfigZodError } from '../errors/errors.js';
import { createSafeError } from '../errors/index.js';
import { info } from '../logger/core.js';
import type { Container, CreateContainerParams } from './container';
import { info, error as logError } from '../logger/core.js';
import { formatErrorMessage } from '../messages.js';
import type { Container } from './container';
import { createContainer, isStarted, startContainer } from './container.js';
async function createRestartedContainer(
@ -11,15 +17,13 @@ async function createRestartedContainer(
settings: AstroSettings,
needsStart: boolean
): Promise<Container> {
const { logging, fs, resolvedRoot, configFlag, configFlagPath } = container;
const { logging, fs, inlineConfig } = container;
const newContainer = await createContainer({
isRestart: true,
logging,
settings,
inlineConfig,
fs,
root: resolvedRoot,
configFlag,
configFlagPath,
});
if (needsStart) {
@ -30,7 +34,7 @@ async function createRestartedContainer(
}
export function shouldRestartContainer(
{ settings, configFlag, configFlagPath, restartInFlight }: Container,
{ settings, inlineConfig, restartInFlight }: Container,
changedFile: string
): boolean {
if (restartInFlight) return false;
@ -38,10 +42,8 @@ export function shouldRestartContainer(
let shouldRestart = false;
// If the config file changed, reload the config and restart the server.
if (configFlag) {
if (!!configFlagPath) {
shouldRestart = vite.normalizePath(configFlagPath) === vite.normalizePath(changedFile);
}
if (inlineConfig.configFile) {
shouldRestart = vite.normalizePath(inlineConfig.configFile) === vite.normalizePath(changedFile);
}
// Otherwise, watch for any astro.config.* file changes in project root
else {
@ -60,39 +62,16 @@ export function shouldRestartContainer(
return shouldRestart;
}
interface RestartContainerParams {
container: Container;
flags: any;
logMsg: string;
handleConfigError: (err: Error) => Promise<void> | void;
beforeRestart?: () => void;
}
export async function restartContainer({
container,
flags,
logMsg,
handleConfigError,
beforeRestart,
}: RestartContainerParams): Promise<{ container: Container; error: Error | null }> {
const { logging, close, resolvedRoot, settings: existingSettings } = container;
export async function restartContainer(
container: Container
): Promise<{ container: Container; error: Error | null }> {
const { logging, close, settings: existingSettings } = container;
container.restartInFlight = true;
if (beforeRestart) {
beforeRestart();
}
const needsStart = isStarted(container);
try {
const newConfig = await openConfig({
cwd: resolvedRoot,
flags,
cmd: 'dev',
isRestart: true,
fsMod: container.fs,
});
info(logging, 'astro', logMsg + '\n');
let astroConfig = newConfig.astroConfig;
const settings = createSettings(astroConfig, resolvedRoot);
const { astroConfig } = await resolveConfig(container.inlineConfig, 'dev', container.fs);
const settings = createSettings(astroConfig, fileURLToPath(existingSettings.config.root));
await close();
return {
container: await createRestartedContainer(container, settings, needsStart),
@ -100,7 +79,18 @@ export async function restartContainer({
};
} catch (_err) {
const error = createSafeError(_err);
await handleConfigError(error);
// Print all error messages except ZodErrors from AstroConfig as the pre-logged error is sufficient
if (!isAstroConfigZodError(_err)) {
logError(logging, 'config', formatErrorMessage(collectErrorMetadata(error)) + '\n');
}
// Inform connected clients of the config error
container.viteServer.ws.send({
type: 'error',
err: {
message: error.message,
stack: error.stack || '',
},
});
await close();
info(logging, 'astro', 'Continuing with previous valid configuration\n');
return {
@ -111,10 +101,8 @@ export async function restartContainer({
}
export interface CreateContainerWithAutomaticRestart {
flags: any;
params: CreateContainerParams;
handleConfigError?: (error: Error) => void | Promise<void>;
beforeRestart?: () => void;
inlineConfig?: AstroInlineConfig;
fs: typeof nodeFs;
}
interface Restart {
@ -123,12 +111,17 @@ interface Restart {
}
export async function createContainerWithAutomaticRestart({
flags,
handleConfigError = () => {},
beforeRestart,
params,
inlineConfig,
fs,
}: CreateContainerWithAutomaticRestart): Promise<Restart> {
const initialContainer = await createContainer(params);
const logging = createNodeLogging(inlineConfig ?? {});
const { userConfig, astroConfig } = await resolveConfig(inlineConfig ?? {}, 'dev', fs);
telemetry.record(eventCliSession('dev', userConfig));
const settings = createSettings(astroConfig, fileURLToPath(astroConfig.root));
const initialContainer = await createContainer({ settings, logging, inlineConfig, fs });
let resolveRestart: (value: Error | null) => void;
let restartComplete = new Promise<Error | null>((resolve) => {
resolveRestart = resolve;
@ -142,24 +135,9 @@ export async function createContainerWithAutomaticRestart({
};
async function handleServerRestart(logMsg: string) {
info(logging, 'astro', logMsg + '\n');
const container = restart.container;
const { container: newContainer, error } = await restartContainer({
beforeRestart,
container,
flags,
logMsg,
async handleConfigError(err) {
// Send an error message to the client if one is connected.
await handleConfigError(err);
container.viteServer.ws.send({
type: 'error',
err: {
message: err.message,
stack: err.stack || '',
},
});
},
});
const { container: newContainer, error } = await restartContainer(container);
restart.container = newContainer;
// Add new watches because this is a new container with a new Vite server
addWatches();

View file

@ -1,3 +1,4 @@
import type { ZodError } from 'zod';
import { codeFrame } from './printer.js';
import { getErrorDataByTitle } from './utils.js';
@ -141,6 +142,23 @@ export class AggregateError extends AstroError {
}
}
const astroConfigZodErrors = new WeakSet<ZodError>();
/**
* Check if an error is a ZodError from an AstroConfig validation.
* Used to suppress formatting a ZodError if needed.
*/
export function isAstroConfigZodError(error: unknown): error is ZodError {
return astroConfigZodErrors.has(error as ZodError);
}
/**
* Track that a ZodError comes from an AstroConfig validation.
*/
export function trackAstroConfigZodError(error: ZodError): void {
astroConfigZodErrors.add(error);
}
/**
* Generic object representing an error with all possible data
* Compatible with both Astro's and Vite's errors

View file

@ -1,40 +1,24 @@
import { cyan } from 'kleur/colors';
import { createRequire } from 'module';
import { pathToFileURL } from 'node:url';
import type { Arguments } from 'yargs-parser';
import type { AstroSettings, PreviewModule, PreviewServer } from '../../@types/astro';
import { createRequire } from 'node:module';
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { AstroInlineConfig, PreviewModule, PreviewServer } from '../../@types/astro';
import { telemetry } from '../../events/index.js';
import { eventCliSession } from '../../events/session.js';
import { runHookConfigDone, runHookConfigSetup } from '../../integrations/index.js';
import type { LogOptions } from '../logger/core';
import { printHelp } from '../messages.js';
import { resolveConfig } from '../config/config.js';
import { createNodeLogging } from '../config/logging.js';
import { createSettings } from '../config/settings.js';
import createStaticPreviewServer from './static-preview-server.js';
import { getResolvedHostForHttpServer } from './util.js';
interface PreviewOptions {
logging: LogOptions;
flags?: Arguments;
}
/** The primary dev action */
export default async function preview(
_settings: AstroSettings,
{ logging, flags }: PreviewOptions
inlineConfig: AstroInlineConfig
): Promise<PreviewServer | undefined> {
if (flags?.help || flags?.h) {
printHelp({
commandName: 'astro preview',
usage: '[...flags]',
tables: {
Flags: [
['--open', 'Automatically open the app in the browser on server start'],
['--help (-h)', 'See all available flags.'],
],
},
description: `Starts a local server to serve your static dist/ directory. Check ${cyan(
'https://docs.astro.build/en/reference/cli-reference/#astro-preview'
)} for more information.`,
});
return;
}
const logging = createNodeLogging(inlineConfig);
const { userConfig, astroConfig } = await resolveConfig(inlineConfig ?? {}, 'preview');
telemetry.record(eventCliSession('preview', userConfig));
const _settings = createSettings(astroConfig, fileURLToPath(astroConfig.root));
const settings = await runHookConfigSetup({
settings: _settings,

View file

@ -1,48 +1,54 @@
import { dim } from 'kleur/colors';
import type fsMod from 'node:fs';
import fsMod from 'node:fs';
import { performance } from 'node:perf_hooks';
import { fileURLToPath } from 'node:url';
import { createServer, type HMRPayload } from 'vite';
import type { Arguments } from 'yargs-parser';
import type { AstroSettings } from '../../@types/astro';
import type { AstroInlineConfig, AstroSettings } from '../../@types/astro';
import { createContentTypesGenerator } from '../../content/index.js';
import { globalContentConfigObserver } from '../../content/utils.js';
import { telemetry } from '../../events/index.js';
import { eventCliSession } from '../../events/session.js';
import { runHookConfigSetup } from '../../integrations/index.js';
import { setUpEnvTs } from '../../vite-plugin-inject-env-ts/index.js';
import { getTimeStat } from '../build/util.js';
import { resolveConfig } from '../config/config.js';
import { createNodeLogging } from '../config/logging.js';
import { createSettings } from '../config/settings.js';
import { createVite } from '../create-vite.js';
import { AstroError, AstroErrorData, createSafeError, isAstroError } from '../errors/index.js';
import { info, type LogOptions } from '../logger/core.js';
import { printHelp } from '../messages.js';
export type ProcessExit = 0 | 1;
export type SyncOptions = {
logging: LogOptions;
fs: typeof fsMod;
/**
* Only used for testing
* @internal
*/
fs?: typeof fsMod;
};
export async function syncCli(
settings: AstroSettings,
{ logging, fs, flags }: { logging: LogOptions; fs: typeof fsMod; flags?: Arguments }
): Promise<ProcessExit> {
if (flags?.help || flags?.h) {
printHelp({
commandName: 'astro sync',
usage: '[...flags]',
tables: {
Flags: [['--help (-h)', 'See all available flags.']],
},
description: `Generates TypeScript types for all Astro modules.`,
});
return 0;
}
export type SyncInternalOptions = SyncOptions & {
logging: LogOptions;
};
const resolvedSettings = await runHookConfigSetup({
settings,
logging,
export async function sync(
inlineConfig: AstroInlineConfig,
options?: SyncOptions
): Promise<ProcessExit> {
const logging = createNodeLogging(inlineConfig);
const { userConfig, astroConfig } = await resolveConfig(inlineConfig ?? {}, 'sync');
telemetry.record(eventCliSession('sync', userConfig));
const _settings = createSettings(astroConfig, fileURLToPath(astroConfig.root));
const settings = await runHookConfigSetup({
settings: _settings,
logging: logging,
command: 'build',
});
return sync(resolvedSettings, { logging, fs });
return await syncInternal(settings, { logging, fs: options?.fs });
}
/**
@ -50,15 +56,18 @@ export async function syncCli(
*
* A non-zero process signal is emitted in case there's an error while generating content collection types.
*
* This should only be used when the callee already has an `AstroSetting`, otherwise use `sync()` instead.
* @internal
*
* @param {SyncOptions} options
* @param {AstroSettings} settings Astro settings
* @param {typeof fsMod} options.fs The file system
* @param {LogOptions} options.logging Logging options
* @return {Promise<ProcessExit>}
*/
export async function sync(
export async function syncInternal(
settings: AstroSettings,
{ logging, fs }: SyncOptions
{ logging, fs }: SyncInternalOptions
): Promise<ProcessExit> {
const timerStart = performance.now();
// Needed to load content config
@ -88,7 +97,7 @@ export async function sync(
const contentTypesGenerator = await createContentTypesGenerator({
contentConfigObserver: globalContentConfigObserver,
logging,
fs,
fs: fs ?? fsMod,
settings,
viteServer: tempViteServer,
});
@ -124,7 +133,7 @@ export async function sync(
}
info(logging, 'content', `Types generated ${dim(getTimeStat(timerStart, performance.now()))}`);
await setUpEnvTs({ settings, logging, fs });
await setUpEnvTs({ settings, logging, fs: fs ?? fsMod });
return 0;
}

View file

@ -9,7 +9,7 @@ describe('Astro Markdown URL', () => {
it('trailingSlash: always', async () => {
let fixture = await loadFixture({
root: './fixtures/astro-markdown-url/',
outDir: new URL('./fixtures/astro-markdown-url/with-subpath-always/', import.meta.url),
outDir: './with-subpath-always',
base: '/my-cool-base',
trailingSlash: 'always',
});
@ -24,7 +24,7 @@ describe('Astro Markdown URL', () => {
it('trailingSlash: never', async () => {
let fixture = await loadFixture({
root: './fixtures/astro-markdown-url/',
outDir: new URL('./fixtures/astro-markdown-url/with-subpath-never/', import.meta.url),
outDir: './with-subpath-never',
base: '/my-cool-base',
trailingSlash: 'never',
});
@ -39,7 +39,7 @@ describe('Astro Markdown URL', () => {
it('trailingSlash: ignore', async () => {
let fixture = await loadFixture({
root: './fixtures/astro-markdown-url/',
outDir: new URL('./fixtures/astro-markdown-url/with-subpath-ignore/', import.meta.url),
outDir: './with-subpath-ignore',
base: '/my-cool-base',
trailingSlash: 'ignore',
});
@ -58,7 +58,7 @@ describe('Astro Markdown URL', () => {
it('trailingSlash: always', async () => {
let fixture = await loadFixture({
root: './fixtures/astro-markdown-url/',
outDir: new URL('./fixtures/astro-markdown-url/without-subpath-always/', import.meta.url),
outDir: './without-subpath-always',
trailingSlash: 'always',
});
await fixture.build();
@ -72,7 +72,7 @@ describe('Astro Markdown URL', () => {
it('trailingSlash: never', async () => {
let fixture = await loadFixture({
root: './fixtures/astro-markdown-url/',
outDir: new URL('./fixtures/astro-markdown-url/without-subpath-never/', import.meta.url),
outDir: './without-subpath-never',
trailingSlash: 'never',
});
await fixture.build();
@ -86,7 +86,7 @@ describe('Astro Markdown URL', () => {
it('trailingSlash: ignore', async () => {
let fixture = await loadFixture({
root: './fixtures/astro-markdown-url/',
outDir: new URL('./fixtures/astro-markdown-url/without-subpath-ignore/', import.meta.url),
outDir: './without-subpath-ignore',
trailingSlash: 'ignore',
});
await fixture.build();

View file

@ -19,7 +19,7 @@ describe('astro sync', () => {
},
},
};
await fixture.sync({ fs: fsMock });
await fixture.sync({}, { fs: fsMock });
const expectedTypesFile = new URL('.astro/types.d.ts', fixture.config.root).href;
expect(writtenFiles).to.haveOwnProperty(expectedTypesFile);
@ -55,7 +55,7 @@ describe('astro sync', () => {
},
},
};
await fixture.sync({ fs: fsMock });
await fixture.sync({}, { fs: fsMock });
expect(writtenFiles, 'Did not try to update env.d.ts file.').to.haveOwnProperty(typesEnvPath);
expect(writtenFiles[typesEnvPath]).to.include(`/// <reference path="../.astro/types.d.ts" />`);
@ -79,7 +79,7 @@ describe('astro sync', () => {
},
},
};
await fixture.sync({ fs: fsMock });
await fixture.sync({}, { fs: fsMock });
expect(writtenFiles, 'Did not try to write env.d.ts file.').to.haveOwnProperty(typesEnvPath);
expect(writtenFiles[typesEnvPath]).to.include(`/// <reference types="astro/client" />`);

View file

@ -1,5 +1,5 @@
import { expect } from 'chai';
import { loadFixture, silentLogging } from './test-utils.js';
import { loadFixture } from './test-utils.js';
import testAdapter from './test-adapter.js';
import * as cheerio from 'cheerio';
@ -108,8 +108,7 @@ describe('Astro.clientAddress', () => {
let devServer;
before(async () => {
// We expect an error, so silence the output
devServer = await fixture.startDevServer({ logging: silentLogging });
devServer = await fixture.startDevServer();
});
after(async () => {

View file

@ -1,5 +1,5 @@
import { expect } from 'chai';
import { loadFixture, silentLogging } from './test-utils.js';
import { loadFixture } from './test-utils.js';
describe('Development Routing', () => {
describe('No site config', () => {
@ -10,9 +10,7 @@ describe('Development Routing', () => {
before(async () => {
fixture = await loadFixture({ root: './fixtures/without-site-config/' });
devServer = await fixture.startDevServer({
logging: silentLogging,
});
devServer = await fixture.startDevServer();
});
after(async () => {

View file

@ -1,6 +1,6 @@
import { expect } from 'chai';
import { load as cheerioLoad } from 'cheerio';
import { loadFixture, silentLogging } from './test-utils.js';
import { loadFixture } from './test-utils.js';
describe('Dynamic endpoint collision', () => {
describe('build', () => {
@ -31,9 +31,7 @@ describe('Dynamic endpoint collision', () => {
root: './fixtures/dynamic-endpoint-collision/',
});
devServer = await fixture.startDevServer({
logging: silentLogging,
});
devServer = await fixture.startDevServer();
});
after(async () => {

View file

@ -1,5 +1,5 @@
import { expect } from 'chai';
import { loadFixture, silentLogging } from './test-utils.js';
import { loadFixture } from './test-utils.js';
describe('Errors in JavaScript', () => {
/** @type {import('./test-utils').Fixture} */
@ -15,9 +15,7 @@ describe('Errors in JavaScript', () => {
logLevel: 'silent',
},
});
devServer = await fixture.startDevServer({
logging: silentLogging,
});
devServer = await fixture.startDevServer();
});
after(async () => {

View file

@ -1,5 +1,5 @@
import { expect } from 'chai';
import { loadFixture, silentLogging } from './test-utils.js';
import { loadFixture } from './test-utils.js';
describe('Can handle errors that are not instanceof Error', () => {
/** @type {import('./test-utils').Fixture} */
@ -12,9 +12,7 @@ describe('Can handle errors that are not instanceof Error', () => {
fixture = await loadFixture({
root: './fixtures/error-non-error',
});
devServer = await fixture.startDevServer({
logging: silentLogging,
});
devServer = await fixture.startDevServer();
});
after(async () => {

View file

@ -13,7 +13,7 @@ describe('Preview Routing', () => {
fixture = await loadFixture({
root: './fixtures/with-subpath-no-trailing-slash/',
base: '/blog',
outDir: new URL('./fixtures/with-subpath-no-trailing-slash/dist-4000/', import.meta.url),
outDir: './dist-4000',
build: {
format: 'directory',
},
@ -41,9 +41,10 @@ describe('Preview Routing', () => {
expect(response.redirected).to.equal(false);
});
it('404 when loading subpath root without trailing slash', async () => {
it('200 when loading subpath root without trailing slash', async () => {
const response = await fixture.fetch('/blog');
expect(response.status).to.equal(404);
expect(response.status).to.equal(200);
expect(response.redirected).to.equal(false);
});
it('404 when loading another page with subpath used', async () => {
@ -72,7 +73,7 @@ describe('Preview Routing', () => {
fixture = await loadFixture({
root: './fixtures/with-subpath-no-trailing-slash/',
base: '/blog',
outDir: new URL('./fixtures/with-subpath-no-trailing-slash/dist-4001/', import.meta.url),
outDir: './dist-4001',
trailingSlash: 'always',
server: {
port: 4001,
@ -132,7 +133,7 @@ describe('Preview Routing', () => {
fixture = await loadFixture({
root: './fixtures/with-subpath-no-trailing-slash/',
base: '/blog',
outDir: new URL('./fixtures/with-subpath-no-trailing-slash/dist-4002/', import.meta.url),
outDir: './dist-4002',
trailingSlash: 'ignore',
server: {
port: 4002,
@ -194,7 +195,7 @@ describe('Preview Routing', () => {
fixture = await loadFixture({
root: './fixtures/with-subpath-no-trailing-slash/',
base: '/blog',
outDir: new URL('./fixtures/with-subpath-no-trailing-slash/dist-4003/', import.meta.url),
outDir: './dist-4003',
build: {
format: 'file',
},
@ -222,9 +223,10 @@ describe('Preview Routing', () => {
expect(response.redirected).to.equal(false);
});
it('404 when loading subpath root without trailing slash', async () => {
it('200 when loading subpath root without trailing slash', async () => {
const response = await fixture.fetch('/blog');
expect(response.status).to.equal(404);
expect(response.status).to.equal(200);
expect(response.redirected).to.equal(false);
});
it('404 when loading another page with subpath used', async () => {
@ -253,7 +255,7 @@ describe('Preview Routing', () => {
fixture = await loadFixture({
root: './fixtures/with-subpath-no-trailing-slash/',
base: '/blog',
outDir: new URL('./fixtures/with-subpath-no-trailing-slash/dist-4004/', import.meta.url),
outDir: './dist-4004',
build: {
format: 'file',
},
@ -316,7 +318,7 @@ describe('Preview Routing', () => {
fixture = await loadFixture({
root: './fixtures/with-subpath-no-trailing-slash/',
base: '/blog',
outDir: new URL('./fixtures/with-subpath-no-trailing-slash/dist-4005/', import.meta.url),
outDir: './dist-4005',
build: {
format: 'file',
},
@ -379,7 +381,7 @@ describe('Preview Routing', () => {
fixture = await loadFixture({
root: './fixtures/with-subpath-no-trailing-slash/',
base: '/blog',
outDir: new URL('./fixtures/with-subpath-no-trailing-slash/dist-4006/', import.meta.url),
outDir: './dist-4006',
build: {
format: 'file',
},

View file

@ -1,6 +1,6 @@
import { expect } from 'chai';
import { load as cheerioLoad } from 'cheerio';
import { isWindows, loadFixture, silentLogging } from './test-utils.js';
import { isWindows, loadFixture } from './test-utils.js';
let fixture;
@ -108,9 +108,7 @@ describe('React Components', () => {
let devServer;
before(async () => {
devServer = await fixture.startDevServer({
logging: silentLogging,
});
devServer = await fixture.startDevServer();
});
after(async () => {

View file

@ -3,6 +3,7 @@ import { execa } from 'execa';
import fastGlob from 'fast-glob';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import stripAnsi from 'strip-ansi';
import { check } from '../dist/cli/check/index.js';
@ -10,8 +11,7 @@ import build from '../dist/core/build/index.js';
import { RESOLVED_SPLIT_MODULE_ID } from '../dist/core/build/plugins/plugin-ssr.js';
import { getVirtualModulePageNameFromPath } from '../dist/core/build/plugins/util.js';
import { makeSplitEntryPointFileName } from '../dist/core/build/static-build.js';
import { openConfig } from '../dist/core/config/config.js';
import { createSettings } from '../dist/core/config/index.js';
import { mergeConfig, resolveConfig } from '../dist/core/config/index.js';
import dev from '../dist/core/dev/index.js';
import { nodeLogDestination } from '../dist/core/logger/node.js';
import preview from '../dist/core/preview/index.js';
@ -28,7 +28,7 @@ process.env.ASTRO_TELEMETRY_DISABLED = true;
/**
* @typedef {import('undici').Response} Response
* @typedef {import('../src/core/dev/dev').DedvServer} DevServer
* @typedef {import('../src/@types/astro').AstroConfig} AstroConfig
* @typedef {import('../src/@types/astro').AstroInlineConfig & { root?: string | URL }} AstroInlineConfig
* @typedef {import('../src/core/preview/index').PreviewServer} PreviewServer
* @typedef {import('../src/core/app/index').App} App
* @typedef {import('../src/cli/check/index').AstroChecker} AstroChecker
@ -43,12 +43,13 @@ process.env.ASTRO_TELEMETRY_DISABLED = true;
* @property {(path: string, updater: (content: string) => string) => Promise<void>} writeFile
* @property {(path: string) => Promise<string[]>} readdir
* @property {(pattern: string) => Promise<string[]>} glob
* @property {() => Promise<DevServer>} startDevServer
* @property {() => Promise<PreviewServer>} preview
* @property {typeof dev} startDevServer
* @property {typeof preview} preview
* @property {() => Promise<void>} clean
* @property {() => Promise<App>} loadTestAdapterApp
* @property {() => Promise<void>} onNextChange
* @property {(opts: CheckPayload) => Promise<AstroChecker>} check
* @property {typeof check} check
* @property {typeof sync} sync
*
* This function returns an instance of the Check
*
@ -82,7 +83,7 @@ export const silentLogging = {
/**
* Load Astro fixture
* @param {AstroConfig} inlineConfig Astro config partial (note: must specify `root`)
* @param {AstroInlineConfig} inlineConfig Astro config partial (note: must specify `root`)
* @returns {Promise<Fixture>} The fixture. Has the following properties:
* .config - Returns the final config. Will be automatically passed to the methods below:
*
@ -103,50 +104,25 @@ export const silentLogging = {
export async function loadFixture(inlineConfig) {
if (!inlineConfig?.root) throw new Error("Must provide { root: './fixtures/...' }");
// load config
let cwd = inlineConfig.root;
delete inlineConfig.root;
if (typeof cwd === 'string') {
try {
cwd = new URL(cwd.replace(/\/?$/, '/'));
} catch (err1) {
cwd = new URL(cwd.replace(/\/?$/, '/'), import.meta.url);
}
// Silent by default during tests to not pollute the console output
inlineConfig.logLevel = 'silent';
let root = inlineConfig.root;
// Handle URL, should already be absolute so just convert to path
if (typeof root !== 'string') {
root = fileURLToPath(root);
}
/** @type {import('../src/core/logger/core').LogOptions} */
const logging = defaultLogging;
// Handle "file:///C:/Users/fred", convert to "C:/Users/fred"
else if (root.startsWith('file://')) {
root = fileURLToPath(new URL(root));
}
// Handle "./fixtures/...", convert to absolute path
else if (!path.isAbsolute(root)) {
root = fileURLToPath(new URL(root, import.meta.url));
}
inlineConfig = { ...inlineConfig, root };
// Load the config.
let { astroConfig: config } = await openConfig({
cwd: fileURLToPath(cwd),
logging,
cmd: 'dev',
});
config = merge(config, { ...inlineConfig, root: cwd });
// HACK: the inline config doesn't run through config validation where these normalizations usually occur
if (typeof inlineConfig.site === 'string') {
config.site = new URL(inlineConfig.site);
}
if (inlineConfig.base && !inlineConfig.base.endsWith('/')) {
config.base = inlineConfig.base + '/';
}
/**
* The dev/build/sync/check commands run integrations' `astro:config:setup` hook that could mutate
* the `AstroSettings`. This function helps to create a fresh settings object that is used by the
* command functions below to prevent tests from polluting each other.
*/
const getSettings = async () => {
let settings = createSettings(config, fileURLToPath(cwd));
if (config.integrations.find((integration) => integration.name === '@astrojs/mdx')) {
// Enable default JSX integration. It needs to come first, so unshift rather than push!
const { default: jsxRenderer } = await import('astro/jsx/renderer.js');
settings.renderers.unshift(jsxRenderer);
}
return settings;
};
const { astroConfig: config } = await resolveConfig(inlineConfig, 'dev');
const resolveUrl = (url) =>
`http://${config.server.host || 'localhost'}:${config.server.port}${url.replace(/^\/?/, '/')}`;
@ -177,17 +153,19 @@ export async function loadFixture(inlineConfig) {
let devServer;
return {
build: async (opts = {}) => {
build: async (extraInlineConfig = {}) => {
process.env.NODE_ENV = 'production';
return build(await getSettings('build'), { logging, ...opts });
return build(mergeConfig(inlineConfig, extraInlineConfig));
},
sync: async (extraInlineConfig = {}, opts) => {
return sync(mergeConfig(inlineConfig, extraInlineConfig), opts);
},
sync: async (opts) => sync(await getSettings('build'), { logging, fs, ...opts }),
check: async (opts) => {
return await check(await getSettings('build'), { logging, ...opts });
return await check(opts);
},
startDevServer: async (opts = {}) => {
startDevServer: async (extraInlineConfig = {}) => {
process.env.NODE_ENV = 'development';
devServer = await dev(await getSettings('dev'), { logging, ...opts });
devServer = await dev(mergeConfig(inlineConfig, extraInlineConfig));
config.server.host = parseAddressToHost(devServer.address.address); // update host
config.server.port = devServer.address.port; // update port
return devServer;
@ -207,9 +185,9 @@ export async function loadFixture(inlineConfig) {
throw err;
}
},
preview: async (opts = {}) => {
preview: async (extraInlineConfig = {}) => {
process.env.NODE_ENV = 'production';
const previewServer = await preview(await getSettings('build'), { logging, ...opts });
const previewServer = await preview(mergeConfig(inlineConfig, extraInlineConfig));
config.server.host = parseAddressToHost(previewServer.host); // update host
config.server.port = previewServer.port; // update port
return previewServer;
@ -282,32 +260,6 @@ function parseAddressToHost(address) {
return address;
}
/**
* Basic object merge utility. Returns new copy of merged Object.
* @param {Object} a
* @param {Object} b
* @returns {Object}
*/
function merge(a, b) {
const allKeys = new Set([...Object.keys(a), ...Object.keys(b)]);
const c = {};
for (const k of allKeys) {
const needsObjectMerge =
typeof a[k] === 'object' &&
typeof b[k] === 'object' &&
(Object.keys(a[k]).length || Object.keys(b[k]).length) &&
!Array.isArray(a[k]) &&
!Array.isArray(b[k]);
if (needsObjectMerge) {
c[k] = merge(a[k] || {}, b[k] || {});
continue;
}
c[k] = a[k];
if (b[k] !== undefined) c[k] = b[k];
}
return c;
}
const cliPath = fileURLToPath(new URL('../astro.js', import.meta.url));
/** Returns a process running the Astro CLI. */

View file

@ -1,24 +1,25 @@
import { expect } from 'chai';
import { fileURLToPath } from 'node:url';
import { defaultLogging } from '../test-utils.js';
import { openConfig } from '../../../dist/core/config/index.js';
import { flagsToAstroInlineConfig } from '../../../dist/cli/flags.js';
import { resolveConfig } from '../../../dist/core/config/index.js';
const cwd = fileURLToPath(new URL('../../fixtures/config-host/', import.meta.url));
describe('config.server', () => {
function openConfigWithFlags(flags) {
return openConfig({
cwd: flags.root || cwd,
flags,
cmd: 'dev',
logging: defaultLogging,
});
function resolveConfigWithFlags(flags) {
return resolveConfig(
flagsToAstroInlineConfig({
root: cwd,
...flags,
}),
'dev'
);
}
describe('host', () => {
it('can be specified via --host flag', async () => {
const projectRootURL = new URL('../../fixtures/astro-basic/', import.meta.url);
const { astroConfig } = await openConfigWithFlags({
const { astroConfig } = await resolveConfigWithFlags({
root: fileURLToPath(projectRootURL),
host: true,
});
@ -32,7 +33,7 @@ describe('config.server', () => {
it('can be passed via relative --config', async () => {
const projectRootURL = new URL('../../fixtures/astro-basic/', import.meta.url);
const configFileURL = 'my-config.mjs';
const { astroConfig } = await openConfigWithFlags({
const { astroConfig } = await resolveConfigWithFlags({
root: fileURLToPath(projectRootURL),
config: configFileURL,
});
@ -44,7 +45,7 @@ describe('config.server', () => {
it('can be passed via relative --config', async () => {
const projectRootURL = new URL('../../fixtures/astro-basic/', import.meta.url);
const configFileURL = './my-config.mjs';
const { astroConfig } = await openConfigWithFlags({
const { astroConfig } = await resolveConfigWithFlags({
root: fileURLToPath(projectRootURL),
config: configFileURL,
});
@ -57,7 +58,7 @@ describe('config.server', () => {
const projectRootURL = new URL('../../fixtures/astro-basic/', import.meta.url);
const configFileURL = './does-not-exist.mjs';
try {
await openConfigWithFlags({
await resolveConfigWithFlags({
root: fileURLToPath(projectRootURL),
config: configFileURL,
});

View file

@ -2,7 +2,7 @@ import { expect } from 'chai';
import { z } from 'zod';
import stripAnsi from 'strip-ansi';
import { formatConfigErrorMessage } from '../../../dist/core/messages.js';
import { validateConfig } from '../../../dist/core/config/index.js';
import { validateConfig } from '../../../dist/core/config/config.js';
describe('Config Validation', () => {
it('empty user config is valid', async () => {

View file

@ -1,8 +1,6 @@
import { fileURLToPath } from 'node:url';
import { expect } from 'chai';
import { createSettings, openConfig } from '../../../dist/core/config/index.js';
import { runInContainer } from '../../../dist/core/dev/index.js';
import { createFs, defaultLogging } from '../test-utils.js';
import { createFs, runInContainer } from '../test-utils.js';
const root = new URL('../../fixtures/tailwindcss-ts/', import.meta.url);
@ -20,16 +18,7 @@ describe('Astro config formats', () => {
root
);
const { astroConfig } = await openConfig({
cwd: root,
flags: {},
cmd: 'dev',
logging: defaultLogging,
fsMod: fs,
});
const settings = createSettings(astroConfig);
await runInContainer({ fs, root, settings }, () => {
await runInContainer({ fs, inlineConfig: { root: fileURLToPath(root) } }, () => {
expect(true).to.equal(
true,
'We were able to get into the container which means the config loaded.'

View file

@ -2,9 +2,8 @@ import { fileURLToPath } from 'node:url';
import nodeFS from 'node:fs';
import path from 'node:path';
import { runInContainer } from '../../../dist/core/dev/index.js';
import { attachContentServerListeners } from '../../../dist/content/index.js';
import { createFs, triggerFSEvent } from '../test-utils.js';
import { createFs, runInContainer, triggerFSEvent } from '../test-utils.js';
const root = new URL('../../fixtures/alias/', import.meta.url);
@ -53,7 +52,7 @@ describe('frontmatter', () => {
root
);
await runInContainer({ fs, root }, async (container) => {
await runInContainer({ fs, inlineConfig: { root: fileURLToPath(root) } }, async (container) => {
await attachContentServerListeners(container);
fs.writeFileFromRootSync(

View file

@ -1,7 +1,6 @@
import { expect } from 'chai';
import { runInContainer } from '../../../dist/core/dev/index.js';
import { createFs, createRequestAndResponse } from '../test-utils.js';
import { fileURLToPath } from 'node:url';
import { createFs, createRequestAndResponse, runInContainer } from '../test-utils.js';
const root = new URL('../../fixtures/alias/', import.meta.url);
@ -19,8 +18,8 @@ describe('base configuration', () => {
await runInContainer(
{
fs,
root,
userConfig: {
inlineConfig: {
root: fileURLToPath(root),
base: '/docs',
trailingSlash: 'never',
},
@ -48,8 +47,8 @@ describe('base configuration', () => {
await runInContainer(
{
fs,
root,
userConfig: {
inlineConfig: {
root: fileURLToPath(root),
base: '/docs',
trailingSlash: 'never',
},
@ -79,8 +78,8 @@ describe('base configuration', () => {
await runInContainer(
{
fs,
root,
userConfig: {
inlineConfig: {
root: fileURLToPath(root),
base: '/docs',
trailingSlash: 'never',
},
@ -108,8 +107,8 @@ describe('base configuration', () => {
await runInContainer(
{
fs,
root,
userConfig: {
inlineConfig: {
root: fileURLToPath(root),
base: '/docs',
trailingSlash: 'never',
},

View file

@ -1,18 +1,12 @@
import { expect } from 'chai';
import { fileURLToPath } from 'node:url';
import { validateConfig } from '../../../dist/core/config/config.js';
import { createSettings } from '../../../dist/core/config/index.js';
import { sync as _sync } from '../../../dist/core/sync/index.js';
import { createFsWithFallback, defaultLogging } from '../test-utils.js';
import { createFsWithFallback } from '../test-utils.js';
const root = new URL('../../fixtures/content-mixed-errors/', import.meta.url);
const logging = defaultLogging;
async function sync({ fs, config = {} }) {
const astroConfig = await validateConfig(config, fileURLToPath(root), 'prod');
const settings = createSettings(astroConfig, fileURLToPath(root));
return _sync(settings, { logging, fs });
return _sync({ ...config, root: fileURLToPath(root), logLevel: 'silent' }, { fs });
}
describe('Content Collections - mixed content errors', () => {

View file

@ -1,16 +1,16 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
import mdx from '../../../../integrations/mdx/dist/index.js';
import { attachContentServerListeners } from '../../../dist/content/server-listeners.js';
import { runInContainer } from '../../../dist/core/dev/index.js';
import { createFsWithFallback, createRequestAndResponse } from '../test-utils.js';
import { createFsWithFallback, createRequestAndResponse, runInContainer } from '../test-utils.js';
const root = new URL('../../fixtures/content/', import.meta.url);
const describe = os.platform() === 'win32' ? global.describe.skip : global.describe;
/** @type {typeof runInContainer} */
async function runInContainerWithContentListeners(params, callback) {
return await runInContainer(params, async (container) => {
await attachContentServerListeners(container);
@ -56,9 +56,8 @@ describe('Content Collections - render()', () => {
await runInContainerWithContentListeners(
{
fs,
root,
userConfig: {
integrations: [mdx()],
inlineConfig: {
root: fileURLToPath(root),
vite: { server: { middlewareMode: true } },
},
},
@ -129,9 +128,8 @@ description: Astro is launching this week!
await runInContainerWithContentListeners(
{
fs,
root,
userConfig: {
integrations: [mdx()],
inlineConfig: {
root: fileURLToPath(root),
vite: { server: { middlewareMode: true } },
},
},
@ -200,9 +198,8 @@ description: Astro is launching this week!
await runInContainerWithContentListeners(
{
fs,
root,
userConfig: {
integrations: [mdx()],
inlineConfig: {
root: fileURLToPath(root),
vite: { server: { middlewareMode: true } },
},
},
@ -270,9 +267,8 @@ description: Astro is launching this week!
await runInContainerWithContentListeners(
{
fs,
root,
userConfig: {
integrations: [mdx()],
inlineConfig: {
root: fileURLToPath(root),
vite: { server: { middlewareMode: true } },
},
},

View file

@ -1,8 +1,12 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { runInContainer } from '../../../dist/core/dev/index.js';
import { createFs, createRequestAndResponse, triggerFSEvent } from '../test-utils.js';
import { fileURLToPath } from 'node:url';
import {
createFs,
createRequestAndResponse,
triggerFSEvent,
runInContainer,
} from '../test-utils.js';
const root = new URL('../../fixtures/alias/', import.meta.url);
@ -25,7 +29,7 @@ describe('dev container', () => {
root
);
await runInContainer({ fs, root }, async (container) => {
await runInContainer({ fs, inlineConfig: { root: fileURLToPath(root) } }, async (container) => {
const { req, res, text } = createRequestAndResponse({
method: 'GET',
url: '/',
@ -60,7 +64,7 @@ describe('dev container', () => {
root
);
await runInContainer({ fs, root }, async (container) => {
await runInContainer({ fs, inlineConfig: { root: fileURLToPath(root) } }, async (container) => {
let r = createRequestAndResponse({
method: 'GET',
url: '/',
@ -119,8 +123,8 @@ describe('dev container', () => {
await runInContainer(
{
fs,
root,
userConfig: {
inlineConfig: {
root: fileURLToPath(root),
output: 'server',
integrations: [
{
@ -170,8 +174,8 @@ describe('dev container', () => {
await runInContainer(
{
fs,
root,
userConfig: {
inlineConfig: {
root: fileURLToPath(root),
output: 'server',
integrations: [
{
@ -223,8 +227,8 @@ describe('dev container', () => {
it('items in public/ are not available from root when using a base', async () => {
await runInContainer(
{
root,
userConfig: {
inlineConfig: {
root: fileURLToPath(root),
base: '/sub/',
},
},
@ -256,7 +260,7 @@ describe('dev container', () => {
});
it('items in public/ are available from root when not using a base', async () => {
await runInContainer({ root }, async (container) => {
await runInContainer({ inlineConfig: { root: fileURLToPath(root) } }, async (container) => {
// Try the root path
let r = createRequestAndResponse({
method: 'GET',

View file

@ -1,8 +1,7 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { runInContainer } from '../../../dist/core/dev/index.js';
import { createFs, createRequestAndResponse } from '../test-utils.js';
import { fileURLToPath } from 'node:url';
import { createFs, createRequestAndResponse, runInContainer } from '../test-utils.js';
const root = new URL('../../fixtures/alias/', import.meta.url);
@ -65,8 +64,8 @@ describe('head injection', () => {
await runInContainer(
{
fs,
root,
userConfig: {
inlineConfig: {
root: fileURLToPath(root),
vite: { server: { middlewareMode: true } },
},
},
@ -154,8 +153,8 @@ describe('head injection', () => {
await runInContainer(
{
fs,
root,
userConfig: {
inlineConfig: {
root: fileURLToPath(root),
vite: { server: { middlewareMode: true } },
},
},

View file

@ -1,12 +1,10 @@
import { expect } from 'chai';
import { runInContainer } from '../../../dist/core/dev/index.js';
import { createFs, createRequestAndResponse, silentLogging } from '../test-utils.js';
import svelte from '../../../../integrations/svelte/dist/index.js';
import { fileURLToPath } from 'node:url';
import { createFs, createRequestAndResponse, runInContainer } from '../test-utils.js';
const root = new URL('../../fixtures/alias/', import.meta.url);
describe('dev container', () => {
describe('hydration', () => {
it('should not crash when reassigning a hydrated component', async () => {
const fs = createFs(
{
@ -31,10 +29,9 @@ describe('dev container', () => {
await runInContainer(
{
fs,
root,
logging: silentLogging,
userConfig: {
integrations: [svelte()],
inlineConfig: {
root: fileURLToPath(root),
logLevel: 'silent',
},
},
async (container) => {

View file

@ -2,18 +2,12 @@ import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { fileURLToPath } from 'node:url';
import { createSettings, openConfig } from '../../../dist/core/config/index.js';
import {
createContainerWithAutomaticRestart,
isStarted,
startContainer,
} from '../../../dist/core/dev/index.js';
import {
createFs,
createRequestAndResponse,
defaultLogging,
triggerFSEvent,
} from '../test-utils.js';
import { createFs, createRequestAndResponse, triggerFSEvent } from '../test-utils.js';
const root = new URL('../../fixtures/alias/', import.meta.url);
@ -36,8 +30,9 @@ describe('dev container restarts', () => {
root
);
let restart = await createContainerWithAutomaticRestart({
params: { fs, root },
const restart = await createContainerWithAutomaticRestart({
fs,
inlineConfig: { root: fileURLToPath(root), logLevel: 'silent' },
});
try {
@ -99,8 +94,9 @@ describe('dev container restarts', () => {
root
);
let restart = await createContainerWithAutomaticRestart({
params: { fs, root },
const restart = await createContainerWithAutomaticRestart({
fs,
inlineConfig: { root: fileURLToPath(root), logLevel: 'silent' },
});
await startContainer(restart.container);
expect(isStarted(restart.container)).to.equal(true);
@ -127,16 +123,9 @@ describe('dev container restarts', () => {
troot
);
const { astroConfig } = await openConfig({
cwd: troot,
flags: {},
cmd: 'dev',
logging: defaultLogging,
});
const settings = createSettings(astroConfig);
let restart = await createContainerWithAutomaticRestart({
params: { fs, root, settings },
const restart = await createContainerWithAutomaticRestart({
fs,
inlineConfig: { root: fileURLToPath(root), logLevel: 'silent' },
});
await startContainer(restart.container);
expect(isStarted(restart.container)).to.equal(true);
@ -161,16 +150,9 @@ describe('dev container restarts', () => {
root
);
const { astroConfig } = await openConfig({
cwd: root,
flags: {},
cmd: 'dev',
logging: defaultLogging,
});
const settings = createSettings(astroConfig, fileURLToPath(root));
let restart = await createContainerWithAutomaticRestart({
params: { fs, root, settings },
const restart = await createContainerWithAutomaticRestart({
fs,
inlineConfig: { root: fileURLToPath(root), logLevel: 'silent' },
});
await startContainer(restart.container);
expect(isStarted(restart.container)).to.equal(true);
@ -193,16 +175,9 @@ describe('dev container restarts', () => {
root
);
const { astroConfig } = await openConfig({
cwd: root,
flags: {},
cmd: 'dev',
logging: defaultLogging,
});
const settings = createSettings(astroConfig, fileURLToPath(root));
let restart = await createContainerWithAutomaticRestart({
params: { fs, root, settings },
const restart = await createContainerWithAutomaticRestart({
fs,
inlineConfig: { root: fileURLToPath(root), logLevel: 'silent' },
});
await startContainer(restart.container);
expect(isStarted(restart.container)).to.equal(true);

View file

@ -1,9 +1,8 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { runInContainer } from '../../../dist/core/dev/index.js';
import { createFs, createRequestAndResponse, silentLogging } from '../test-utils.js';
import { fileURLToPath } from 'node:url';
import svelte from '../../../../integrations/svelte/dist/index.js';
import { createFs, createRequestAndResponse, runInContainer } from '../test-utils.js';
const root = new URL('../../fixtures/alias/', import.meta.url);
@ -31,9 +30,9 @@ describe('core/render components', () => {
await runInContainer(
{
fs,
root,
logging: silentLogging,
userConfig: {
inlineConfig: {
root: fileURLToPath(root),
logLevel: 'silent',
integrations: [svelte()],
},
},

View file

@ -1,10 +1,8 @@
import { expect } from 'chai';
import { createFs } from '../test-utils.js';
import { createRouteManifest } from '../../../dist/core/routing/manifest/create.js';
import { createDefaultDevSettings } from '../../../dist/core/config/index.js';
import { fileURLToPath } from 'node:url';
import { defaultLogging } from '../test-utils.js';
import { createRouteManifest } from '../../../dist/core/routing/manifest/create.js';
import { createBasicSettings, createFs, defaultLogging } from '../test-utils.js';
const root = new URL('../../fixtures/alias/', import.meta.url);
@ -16,13 +14,11 @@ describe('routing - createRouteManifest', () => {
},
root
);
const settings = await createDefaultDevSettings(
{
base: '/search',
trailingSlash: 'never',
},
root
);
const settings = await createBasicSettings({
root: fileURLToPath(root),
base: '/search',
trailingSlash: 'never',
});
const manifest = createRouteManifest({
cwd: fileURLToPath(root),
settings,
@ -41,17 +37,15 @@ describe('routing - createRouteManifest', () => {
},
root
);
const settings = await createDefaultDevSettings(
{
base: '/search',
trailingSlash: 'never',
redirects: {
'/blog/[...slug]': '/',
'/blog/contributing': '/another',
},
const settings = await createBasicSettings({
root: fileURLToPath(root),
base: '/search',
trailingSlash: 'never',
redirects: {
'/blog/[...slug]': '/',
'/blog/contributing': '/another',
},
root
);
});
const manifest = createRouteManifest({
cwd: fileURLToPath(root),
settings,
@ -70,15 +64,13 @@ describe('routing - createRouteManifest', () => {
},
root
);
const settings = await createDefaultDevSettings(
{
trailingSlash: 'never',
redirects: {
'/foo': '/bar',
},
const settings = await createBasicSettings({
root: fileURLToPath(root),
trailingSlash: 'never',
redirects: {
'/foo': '/bar',
},
root
);
});
const manifest = createRouteManifest(
{
cwd: fileURLToPath(root),

View file

@ -1,5 +1,9 @@
// @ts-check
import { createFs, createRequestAndResponse, defaultLogging } from '../test-utils.js';
import {
createBasicSettings,
createFs,
createRequestAndResponse,
defaultLogging,
} from '../test-utils.js';
import { createRouteManifest, matchAllRoutes } from '../../../dist/core/routing/index.js';
import { fileURLToPath } from 'node:url';
import { createViteLoader } from '../../../dist/core/module-loader/vite.js';
@ -127,16 +131,17 @@ describe('Route matching', () => {
before(async () => {
const fs = createFs(fileSystem, root);
settings = await createBasicSettings({
root: fileURLToPath(root),
trailingSlash: 'never',
output: 'hybrid',
adapter: testAdapter(),
});
container = await createContainer({
fs,
root,
userConfig: {
trailingSlash: 'never',
output: 'hybrid',
adapter: testAdapter(),
},
settings,
logging: defaultLogging,
});
settings = container.settings;
const loader = createViteLoader(container.viteServer);
const manifest = createDevelopmentManifest(container.settings);

View file

@ -1,6 +1,8 @@
import { expect } from 'chai';
import { fileURLToPath } from 'node:url';
import { createContainer } from '../../../dist/core/dev/index.js';
import { createViteLoader } from '../../../dist/core/module-loader/index.js';
import { createBasicSettings, defaultLogging } from '../test-utils.js';
const root = new URL('../../fixtures/alias/', import.meta.url);
@ -9,7 +11,8 @@ describe('<Code />', () => {
let container;
let mod;
before(async () => {
container = await createContainer({ root });
const settings = await createBasicSettings({ root: fileURLToPath(root) });
container = await createContainer({ settings, logging: defaultLogging });
const loader = createViteLoader(container.viteServer);
mod = await loader.import('astro/components/Shiki.js');
});

View file

@ -8,6 +8,9 @@ import { getDefaultClientDirectives } from '../../dist/core/client-directive/ind
import { nodeLogDestination } from '../../dist/core/logger/node.js';
import { createEnvironment } from '../../dist/core/render/index.js';
import { RouteCache } from '../../dist/core/render/route-cache.js';
import { resolveConfig } from '../../dist/core/config/index.js';
import { createBaseSettings } from '../../dist/core/config/settings.js';
import { createContainer } from '../../dist/core/dev/container.js';
import { unixify } from './correct-path.js';
/** @type {import('../../src/core/logger/core').LogOptions} */
@ -189,3 +192,42 @@ export function createBasicEnvironment(options = {}) {
streaming: options.streaming ?? true,
});
}
/**
* @param {import('../../src/@types/astro.js').AstroInlineConfig} inlineConfig
* @returns {Promise<import('../../src/@types/astro.js').AstroSettings>}
*/
export async function createBasicSettings(inlineConfig = {}) {
if (!inlineConfig.root) {
inlineConfig.root = fileURLToPath(new URL('.', import.meta.url));
}
const { astroConfig } = await resolveConfig(inlineConfig, 'dev');
return createBaseSettings(astroConfig);
}
/**
* @typedef {{
* fs?: typeof realFS,
* inlineConfig?: import('../../src/@types/astro.js').AstroInlineConfig,
* logging?: import('../../src/core/logger/core').LogOptions,
* }} RunInContainerOptions
*/
/**
* @param {RunInContainerOptions} options
* @param {(container: import('../../src/core/dev/container.js').Container) => Promise<void> | void} callback
*/
export async function runInContainer(options = {}, callback) {
const settings = await createBasicSettings(options.inlineConfig ?? {});
const container = await createContainer({
fs: options?.fs ?? realFS,
settings,
inlineConfig: options.inlineConfig ?? {},
logging: defaultLogging,
});
try {
await callback(container);
} finally {
await container.close();
}
}

View file

@ -1,6 +1,5 @@
import { expect } from 'chai';
import { createDefaultDevSettings } from '../../../dist/core/config/index.js';
import { createLoader } from '../../../dist/core/module-loader/index.js';
import { createRouteManifest } from '../../../dist/core/routing/index.js';
import { createComponent, render } from '../../../dist/runtime/server/index.js';
@ -8,6 +7,7 @@ import { createController, handleRequest } from '../../../dist/vite-plugin-astro
import {
createAstroModule,
createBasicEnvironment,
createBasicSettings,
createFs,
createRequestAndResponse,
defaultLogging,
@ -15,7 +15,7 @@ import {
async function createDevEnvironment(overrides = {}) {
const env = createBasicEnvironment();
env.settings = await createDefaultDevSettings({}, '/');
env.settings = await createBasicSettings({ root: '/' });
env.settings.renderers = [];
env.loader = createLoader();
Object.assign(env, overrides);

View file

@ -23,7 +23,7 @@ describe('getStaticPaths', () => {
const $ = cheerio.load(html);
expect($('p').text()).to.equal('First mdx file');
expect($('#one').text()).to.equal('hello', 'Frontmatter included');
expect($('#url').text()).to.equal('/src/content/1.mdx', 'url is included');
expect($('#url').text()).to.equal('src/content/1.mdx', 'url is included');
expect($('#file').text()).to.contain(
'fixtures/mdx-get-static-paths/src/content/1.mdx',
'file is included'

View file

@ -96,7 +96,7 @@ describe('MDX plugins', () => {
it('ignores string-based plugins in markdown config', async () => {
const fixture = await buildFixture({
markdown: {
remarkPlugins: [['remark-toc']],
remarkPlugins: [['remark-toc', {}]],
},
integrations: [mdx()],
});