From ebf7ebbf7ae767625d736fad327954cfb853837e Mon Sep 17 00:00:00 2001 From: Bjorn Lu Date: Tue, 1 Aug 2023 17:11:26 +0800 Subject: [PATCH] Refactor and improve Astro config loading flow (#7879) --- .changeset/twenty-oranges-poke.md | 5 + packages/astro/e2e/errors.test.js | 7 +- packages/astro/e2e/test-utils.js | 3 +- packages/astro/src/@types/astro.ts | 12 +- packages/astro/src/cli/add/index.ts | 16 +- packages/astro/src/cli/build/index.ts | 30 ++- packages/astro/src/cli/check/index.ts | 46 +++-- packages/astro/src/cli/dev/index.ts | 46 ++--- packages/astro/src/cli/flags.ts | 49 +++++ packages/astro/src/cli/index.ts | 23 +-- packages/astro/src/cli/info/index.ts | 10 +- packages/astro/src/cli/load-settings.ts | 47 ----- packages/astro/src/cli/preview/index.ts | 30 ++- packages/astro/src/cli/sync/index.ts | 27 ++- packages/astro/src/cli/throw-and-exit.ts | 4 + packages/astro/src/config/index.ts | 4 +- packages/astro/src/core/build/index.ts | 60 +++--- packages/astro/src/core/config/config.ts | 176 +++++++++--------- packages/astro/src/core/config/index.ts | 12 +- packages/astro/src/core/config/logging.ts | 13 ++ packages/astro/src/core/config/settings.ts | 14 +- packages/astro/src/core/dev/container.ts | 60 ++---- packages/astro/src/core/dev/dev.ts | 69 ++----- packages/astro/src/core/dev/index.ts | 2 +- packages/astro/src/core/dev/restart.ts | 116 +++++------- packages/astro/src/core/errors/errors.ts | 18 ++ packages/astro/src/core/preview/index.ts | 44 ++--- packages/astro/src/core/sync/index.ts | 67 ++++--- .../astro/test/astro-markdown-url.test.js | 12 +- packages/astro/test/astro-sync.test.js | 6 +- packages/astro/test/client-address.test.js | 5 +- packages/astro/test/dev-routing.test.js | 6 +- .../test/dynamic-endpoint-collision.test.js | 6 +- packages/astro/test/error-bad-js.test.js | 6 +- packages/astro/test/error-non-error.test.js | 6 +- packages/astro/test/preview-routing.test.js | 24 +-- packages/astro/test/react-component.test.js | 6 +- packages/astro/test/test-utils.js | 118 ++++-------- .../test/units/config/config-server.test.js | 27 +-- .../test/units/config/config-validate.test.js | 2 +- .../astro/test/units/config/format.test.js | 17 +- .../content-collections/frontmatter.test.js | 5 +- packages/astro/test/units/dev/base.test.js | 21 +-- .../collections-mixed-content-errors.test.js | 10 +- .../units/dev/collections-renderentry.test.js | 26 ++- packages/astro/test/units/dev/dev.test.js | 28 +-- .../test/units/dev/head-injection.test.js | 13 +- .../astro/test/units/dev/hydration.test.js | 15 +- packages/astro/test/units/dev/restart.test.js | 57 ++---- .../test/units/render/components.test.js | 11 +- .../astro/test/units/routing/manifest.test.js | 50 +++-- .../test/units/routing/route-matching.test.js | 23 ++- packages/astro/test/units/shiki/shiki.test.js | 5 +- packages/astro/test/units/test-utils.js | 42 +++++ .../vite-plugin-astro-server/request.test.js | 4 +- .../mdx/test/mdx-get-static-paths.test.js | 2 +- .../integrations/mdx/test/mdx-plugins.test.js | 2 +- 57 files changed, 740 insertions(+), 825 deletions(-) create mode 100644 .changeset/twenty-oranges-poke.md create mode 100644 packages/astro/src/cli/flags.ts delete mode 100644 packages/astro/src/cli/load-settings.ts create mode 100644 packages/astro/src/core/config/logging.ts diff --git a/.changeset/twenty-oranges-poke.md b/.changeset/twenty-oranges-poke.md new file mode 100644 index 000000000..a36c175dc --- /dev/null +++ b/.changeset/twenty-oranges-poke.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Refactor and improve Astro config loading flow diff --git a/packages/astro/e2e/errors.test.js b/packages/astro/e2e/errors.test.js index 9276370f5..e23b22c65 100644 --- a/packages/astro/e2e/errors.test.js +++ b/packages/astro/e2e/errors.test.js @@ -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 }) => { diff --git a/packages/astro/e2e/test-utils.js b/packages/astro/e2e/test-utils.js index 92ae9dc67..0768bff81 100644 --- a/packages/astro/e2e/test-utils.js +++ b/packages/astro/e2e/test-utils.js @@ -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)), }, diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index e30ee3b9e..59e295eba 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -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 { // 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; diff --git a/packages/astro/src/cli/add/index.ts b/packages/astro/src/cli/add/index.ts index 821b2dee2..6ef96561d 100644 --- a/packages/astro/src/cli/add/index.ts +++ b/packages/astro/src/cli/add/index.ts @@ -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 { } } -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) { diff --git a/packages/astro/src/cli/build/index.ts b/packages/astro/src/cli/build/index.ts index ab3765731..9e26108a2 100644 --- a/packages/astro/src/cli/build/index.ts +++ b/packages/astro/src/cli/build/index.ts @@ -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, }); } diff --git a/packages/astro/src/cli/check/index.ts b/packages/astro/src/cli/check/index.ts index 09d45ee56..96bee308d 100644 --- a/packages/astro/src/cli/check/index.ts +++ b/packages/astro/src/cli/check/index.ts @@ -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 { +export async function check({ flags }: CheckPayload): Promise { if (flags.help || flags.h) { printHelp({ commandName: 'astro check', @@ -95,8 +94,12 @@ export async function check({ logging, flags }: CheckPayload): Promise Promise; + syncInternal: typeof syncInternal; settings: Readonly; @@ -148,7 +151,7 @@ type CheckerConstructor = { export class AstroChecker { readonly #diagnosticsChecker: AstroCheck; readonly #shouldWatch: boolean; - readonly #syncCli: (settings: AstroSettings, opts: SyncOptions) => Promise; + 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 { - 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, }); diff --git a/packages/astro/src/cli/dev/index.ts b/packages/astro/src/cli/dev/index.ts index d3230a05b..e55496c4a 100644 --- a/packages/astro/src/cli/dev/index.ts +++ b/packages/astro/src/cli/dev/index.ts @@ -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 ', `Expose on a network IP address at `], + ['--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); } diff --git a/packages/astro/src/cli/flags.ts b/packages/astro/src/cli/flags.ts new file mode 100644 index 000000000..703422d50 --- /dev/null +++ b/packages/astro/src/cli/flags.ts @@ -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; +} diff --git a/packages/astro/src/cli/index.ts b/packages/astro/src/cli/index.ts index 7c26b2d01..d16ea91e2 100644 --- a/packages/astro/src/cli/index.ts +++ b/packages/astro/src/cli/index.ts @@ -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); } } diff --git a/packages/astro/src/cli/info/index.ts b/packages/astro/src/cli/info/index.ts index 3d27d7f3f..4944432e7 100644 --- a/packages/astro/src/cli/info/index.ts +++ b/packages/astro/src/cli/info/index.ts @@ -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; } diff --git a/packages/astro/src/cli/load-settings.ts b/packages/astro/src/cli/load-settings.ts deleted file mode 100644 index 9377825c4..000000000 --- a/packages/astro/src/cli/load-settings.ts +++ /dev/null @@ -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'); - } -} diff --git a/packages/astro/src/cli/preview/index.ts b/packages/astro/src/cli/preview/index.ts index 96146cebc..39bb2de0e 100644 --- a/packages/astro/src/cli/preview/index.ts +++ b/packages/astro/src/cli/preview/index.ts @@ -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); } diff --git a/packages/astro/src/cli/sync/index.ts b/packages/astro/src/cli/sync/index.ts index f96b8fd0b..66a277e46 100644 --- a/packages/astro/src/cli/sync/index.ts +++ b/packages/astro/src/cli/sync/index.ts @@ -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; } diff --git a/packages/astro/src/cli/throw-and-exit.ts b/packages/astro/src/cli/throw-and-exit.ts index cea76ee98..3196092d2 100644 --- a/packages/astro/src/cli/throw-and-exit.ts +++ b/packages/astro/src/cli/throw-and-exit.ts @@ -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; let errorMessage: string; function exitWithErrorMessage() { diff --git a/packages/astro/src/config/index.ts b/packages/astro/src/config/index.ts index a9e32186d..d32af35b8 100644 --- a/packages/astro/src/config/index.ts +++ b/packages/astro/src/config/index.ts @@ -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( diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts index 5ac5d2b0f..5b1ecf404 100644 --- a/packages/astro/src/core/build/index.ts +++ b/packages/astro/src/core/build/index.ts @@ -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 { +export default async function build( + inlineConfig: AstroInlineConfig, + options: BuildOptions +): Promise { 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; 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); } diff --git a/packages/astro/src/core/config/config.ts b/packages/astro/src/core/config/config.ts index 1be371523..0872145db 100644 --- a/packages/astro/src/core/config/config.ts +++ b/packages/astro/src/core/config/config.ts @@ -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): 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 { - 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 { - 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> { - 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 { - const mergedConfig = mergeCLIFlags(userConfig, flags); - const validatedConfig = await validateConfig(mergedConfig, root, cmd); + inlineConfig: AstroInlineConfig, + command: string, + fsMod = fs +): Promise { + 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 }; } diff --git a/packages/astro/src/core/config/index.ts b/packages/astro/src/core/config/index.ts index b7b616951..23db73382 100644 --- a/packages/astro/src/core/config/index.ts +++ b/packages/astro/src/core/config/index.ts @@ -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'; diff --git a/packages/astro/src/core/config/logging.ts b/packages/astro/src/core/config/logging.ts new file mode 100644 index 000000000..ea0b29b88 --- /dev/null +++ b/packages/astro/src/core/config/logging.ts @@ -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', + }; +} diff --git a/packages/astro/src/core/config/settings.ts b/packages/astro/src/core/config/settings.ts index 35d7d252b..b15961e50 100644 --- a/packages/astro/src/core/config/settings.ts +++ b/packages/astro/src/core/config/settings.ts @@ -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 { - if (root && typeof root !== 'string') { - root = fileURLToPath(root); - } - const config = await createDefaultDevConfig(userConfig, root); - return createBaseSettings(config); -} diff --git a/packages/astro/src/core/dev/container.ts b/packages/astro/src/core/dev/container.ts index d7ab1ae9f..4dd3f15b9 100644 --- a/packages/astro/src/core/dev/container.ts +++ b/packages/astro/src/core/dev/container.ts @@ -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; } 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 { - 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 { // 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 -) { - const container = await createContainer(params); - try { - await callback(container); - } finally { - await container.close(); - } -} diff --git a/packages/astro/src/core/dev/dev.ts b/packages/astro/src/core/dev/dev.ts index f6fd35685..b14656d26 100644 --- a/packages/astro/src/core/dev/dev.ts +++ b/packages/astro/src/core/dev/dev.ts @@ -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) => void; @@ -30,68 +19,34 @@ export interface DevServer { } /** `astro dev` */ -export default async function dev( - settings: AstroSettings, - options: DevOptions -): Promise { - 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 ', `Expose on a network IP address at `], - ['--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 { 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); diff --git a/packages/astro/src/core/dev/index.ts b/packages/astro/src/core/dev/index.ts index a677ad928..473f7198a 100644 --- a/packages/astro/src/core/dev/index.ts +++ b/packages/astro/src/core/dev/index.ts @@ -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'; diff --git a/packages/astro/src/core/dev/restart.ts b/packages/astro/src/core/dev/restart.ts index d96cc0b50..1acda433e 100644 --- a/packages/astro/src/core/dev/restart.ts +++ b/packages/astro/src/core/dev/restart.ts @@ -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 { - 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; - 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; - 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 { - 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((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(); diff --git a/packages/astro/src/core/errors/errors.ts b/packages/astro/src/core/errors/errors.ts index a73728124..ca4392891 100644 --- a/packages/astro/src/core/errors/errors.ts +++ b/packages/astro/src/core/errors/errors.ts @@ -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(); + +/** + * 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 diff --git a/packages/astro/src/core/preview/index.ts b/packages/astro/src/core/preview/index.ts index d47e54499..fdd5d6fe7 100644 --- a/packages/astro/src/core/preview/index.ts +++ b/packages/astro/src/core/preview/index.ts @@ -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 { - 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, diff --git a/packages/astro/src/core/sync/index.ts b/packages/astro/src/core/sync/index.ts index 95377eeb8..be51f0039 100644 --- a/packages/astro/src/core/sync/index.ts +++ b/packages/astro/src/core/sync/index.ts @@ -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 { - 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 { + 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} */ -export async function sync( +export async function syncInternal( settings: AstroSettings, - { logging, fs }: SyncOptions + { logging, fs }: SyncInternalOptions ): Promise { 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; } diff --git a/packages/astro/test/astro-markdown-url.test.js b/packages/astro/test/astro-markdown-url.test.js index 01d777b9f..a96ce60d4 100644 --- a/packages/astro/test/astro-markdown-url.test.js +++ b/packages/astro/test/astro-markdown-url.test.js @@ -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(); diff --git a/packages/astro/test/astro-sync.test.js b/packages/astro/test/astro-sync.test.js index 5f1fb2d14..ba436fe21 100644 --- a/packages/astro/test/astro-sync.test.js +++ b/packages/astro/test/astro-sync.test.js @@ -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(`/// `); @@ -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(`/// `); diff --git a/packages/astro/test/client-address.test.js b/packages/astro/test/client-address.test.js index e351b44cd..6e78832ce 100644 --- a/packages/astro/test/client-address.test.js +++ b/packages/astro/test/client-address.test.js @@ -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 () => { diff --git a/packages/astro/test/dev-routing.test.js b/packages/astro/test/dev-routing.test.js index c42c25dc0..186355b43 100644 --- a/packages/astro/test/dev-routing.test.js +++ b/packages/astro/test/dev-routing.test.js @@ -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 () => { diff --git a/packages/astro/test/dynamic-endpoint-collision.test.js b/packages/astro/test/dynamic-endpoint-collision.test.js index c0a11b5f8..329e3603b 100644 --- a/packages/astro/test/dynamic-endpoint-collision.test.js +++ b/packages/astro/test/dynamic-endpoint-collision.test.js @@ -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 () => { diff --git a/packages/astro/test/error-bad-js.test.js b/packages/astro/test/error-bad-js.test.js index ba02c62ff..dc04016f5 100644 --- a/packages/astro/test/error-bad-js.test.js +++ b/packages/astro/test/error-bad-js.test.js @@ -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 () => { diff --git a/packages/astro/test/error-non-error.test.js b/packages/astro/test/error-non-error.test.js index 023807be8..facf99633 100644 --- a/packages/astro/test/error-non-error.test.js +++ b/packages/astro/test/error-non-error.test.js @@ -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 () => { diff --git a/packages/astro/test/preview-routing.test.js b/packages/astro/test/preview-routing.test.js index 591618126..5c365b07c 100644 --- a/packages/astro/test/preview-routing.test.js +++ b/packages/astro/test/preview-routing.test.js @@ -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', }, diff --git a/packages/astro/test/react-component.test.js b/packages/astro/test/react-component.test.js index 7c0de3686..a6bb8cfae 100644 --- a/packages/astro/test/react-component.test.js +++ b/packages/astro/test/react-component.test.js @@ -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 () => { diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index 94912687f..27f5d83f7 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -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} writeFile * @property {(path: string) => Promise} readdir * @property {(pattern: string) => Promise} glob - * @property {() => Promise} startDevServer - * @property {() => Promise} preview + * @property {typeof dev} startDevServer + * @property {typeof preview} preview * @property {() => Promise} clean * @property {() => Promise} loadTestAdapterApp * @property {() => Promise} onNextChange - * @property {(opts: CheckPayload) => Promise} 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} 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. */ diff --git a/packages/astro/test/units/config/config-server.test.js b/packages/astro/test/units/config/config-server.test.js index 3486374e8..8a60c53bf 100644 --- a/packages/astro/test/units/config/config-server.test.js +++ b/packages/astro/test/units/config/config-server.test.js @@ -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, }); diff --git a/packages/astro/test/units/config/config-validate.test.js b/packages/astro/test/units/config/config-validate.test.js index fa7418c56..49fd6b418 100644 --- a/packages/astro/test/units/config/config-validate.test.js +++ b/packages/astro/test/units/config/config-validate.test.js @@ -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 () => { diff --git a/packages/astro/test/units/config/format.test.js b/packages/astro/test/units/config/format.test.js index b230ea6b4..6cd332101 100644 --- a/packages/astro/test/units/config/format.test.js +++ b/packages/astro/test/units/config/format.test.js @@ -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.' diff --git a/packages/astro/test/units/content-collections/frontmatter.test.js b/packages/astro/test/units/content-collections/frontmatter.test.js index d35a6df33..f06b33710 100644 --- a/packages/astro/test/units/content-collections/frontmatter.test.js +++ b/packages/astro/test/units/content-collections/frontmatter.test.js @@ -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( diff --git a/packages/astro/test/units/dev/base.test.js b/packages/astro/test/units/dev/base.test.js index d3dd94341..041d6bcb5 100644 --- a/packages/astro/test/units/dev/base.test.js +++ b/packages/astro/test/units/dev/base.test.js @@ -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', }, diff --git a/packages/astro/test/units/dev/collections-mixed-content-errors.test.js b/packages/astro/test/units/dev/collections-mixed-content-errors.test.js index 4ebf2b510..344615346 100644 --- a/packages/astro/test/units/dev/collections-mixed-content-errors.test.js +++ b/packages/astro/test/units/dev/collections-mixed-content-errors.test.js @@ -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', () => { diff --git a/packages/astro/test/units/dev/collections-renderentry.test.js b/packages/astro/test/units/dev/collections-renderentry.test.js index d3f784925..873bb9164 100644 --- a/packages/astro/test/units/dev/collections-renderentry.test.js +++ b/packages/astro/test/units/dev/collections-renderentry.test.js @@ -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 } }, }, }, diff --git a/packages/astro/test/units/dev/dev.test.js b/packages/astro/test/units/dev/dev.test.js index 7c361b9de..9762be7eb 100644 --- a/packages/astro/test/units/dev/dev.test.js +++ b/packages/astro/test/units/dev/dev.test.js @@ -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', diff --git a/packages/astro/test/units/dev/head-injection.test.js b/packages/astro/test/units/dev/head-injection.test.js index ed3e085d3..566e7ab48 100644 --- a/packages/astro/test/units/dev/head-injection.test.js +++ b/packages/astro/test/units/dev/head-injection.test.js @@ -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 } }, }, }, diff --git a/packages/astro/test/units/dev/hydration.test.js b/packages/astro/test/units/dev/hydration.test.js index 7285f5e71..ae4cb2d99 100644 --- a/packages/astro/test/units/dev/hydration.test.js +++ b/packages/astro/test/units/dev/hydration.test.js @@ -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) => { diff --git a/packages/astro/test/units/dev/restart.test.js b/packages/astro/test/units/dev/restart.test.js index 5dd03fe46..c1a7a3c21 100644 --- a/packages/astro/test/units/dev/restart.test.js +++ b/packages/astro/test/units/dev/restart.test.js @@ -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); diff --git a/packages/astro/test/units/render/components.test.js b/packages/astro/test/units/render/components.test.js index 38f9e1062..0b7352453 100644 --- a/packages/astro/test/units/render/components.test.js +++ b/packages/astro/test/units/render/components.test.js @@ -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()], }, }, diff --git a/packages/astro/test/units/routing/manifest.test.js b/packages/astro/test/units/routing/manifest.test.js index 7b8ee3e26..cf3fb0bf1 100644 --- a/packages/astro/test/units/routing/manifest.test.js +++ b/packages/astro/test/units/routing/manifest.test.js @@ -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), diff --git a/packages/astro/test/units/routing/route-matching.test.js b/packages/astro/test/units/routing/route-matching.test.js index 50b8d58c9..e1c4df5c5 100644 --- a/packages/astro/test/units/routing/route-matching.test.js +++ b/packages/astro/test/units/routing/route-matching.test.js @@ -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); diff --git a/packages/astro/test/units/shiki/shiki.test.js b/packages/astro/test/units/shiki/shiki.test.js index 0d67dda35..e5b78963f 100644 --- a/packages/astro/test/units/shiki/shiki.test.js +++ b/packages/astro/test/units/shiki/shiki.test.js @@ -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('', () => { 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'); }); diff --git a/packages/astro/test/units/test-utils.js b/packages/astro/test/units/test-utils.js index 46910416d..80dff5ddb 100644 --- a/packages/astro/test/units/test-utils.js +++ b/packages/astro/test/units/test-utils.js @@ -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} + */ +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} 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(); + } +} diff --git a/packages/astro/test/units/vite-plugin-astro-server/request.test.js b/packages/astro/test/units/vite-plugin-astro-server/request.test.js index fba0a49ab..48d449ccd 100644 --- a/packages/astro/test/units/vite-plugin-astro-server/request.test.js +++ b/packages/astro/test/units/vite-plugin-astro-server/request.test.js @@ -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); diff --git a/packages/integrations/mdx/test/mdx-get-static-paths.test.js b/packages/integrations/mdx/test/mdx-get-static-paths.test.js index b4dc179d0..c5a34f7de 100644 --- a/packages/integrations/mdx/test/mdx-get-static-paths.test.js +++ b/packages/integrations/mdx/test/mdx-get-static-paths.test.js @@ -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' diff --git a/packages/integrations/mdx/test/mdx-plugins.test.js b/packages/integrations/mdx/test/mdx-plugins.test.js index 139d2042f..324e00c9c 100644 --- a/packages/integrations/mdx/test/mdx-plugins.test.js +++ b/packages/integrations/mdx/test/mdx-plugins.test.js @@ -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()], });