diff --git a/.changeset/odd-books-live.md b/.changeset/odd-books-live.md new file mode 100644 index 000000000000..40f0d7c1762a --- /dev/null +++ b/.changeset/odd-books-live.md @@ -0,0 +1,23 @@ +--- +'astro': minor +--- + +Integrations can now log messages using Astro’s built-in logger. + +The logger is available to all hooks as an additional parameter: + +```ts +import {AstroIntegration} from "./astro"; + +// integration.js +export function myIntegration(): AstroIntegration { + return { + name: "my-integration", + hooks: { + "astro:config:done": ({ logger }) => { + logger.info("Configure integration..."); + } + } + } +} +``` diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 5e4a72d87a9b..14b0a74893d0 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -22,6 +22,7 @@ import type { AstroCookies } from '../core/cookies'; 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'; +import { AstroIntegrationLogger } from '../core/logger/core'; export type { MarkdownHeading, MarkdownMetadata, @@ -1874,6 +1875,7 @@ export interface AstroIntegration { injectScript: (stage: InjectedScriptStage, content: string) => void; injectRoute: (injectRoute: InjectedRoute) => void; addClientDirective: (directive: ClientDirectiveConfig) => void; + logger: AstroIntegrationLogger; // TODO: Add support for `injectElement()` for full HTML element injection, not just scripts. // This may require some refactoring of `scripts`, `styles`, and `links` into something // more generalized. Consider the SSR use-case as well. @@ -1882,10 +1884,17 @@ export interface AstroIntegration { 'astro:config:done'?: (options: { config: AstroConfig; setAdapter: (adapter: AstroAdapter) => void; + logger: AstroIntegrationLogger; }) => void | Promise; - 'astro:server:setup'?: (options: { server: vite.ViteDevServer }) => void | Promise; - 'astro:server:start'?: (options: { address: AddressInfo }) => void | Promise; - 'astro:server:done'?: () => void | Promise; + 'astro:server:setup'?: (options: { + server: vite.ViteDevServer; + logger: AstroIntegrationLogger; + }) => void | Promise; + 'astro:server:start'?: (options: { + address: AddressInfo; + logger: AstroIntegrationLogger; + }) => void | Promise; + 'astro:server:done'?: (options: { logger: AstroIntegrationLogger }) => void | Promise; 'astro:build:ssr'?: (options: { manifest: SerializedSSRManifest; /** @@ -1897,19 +1906,25 @@ export interface AstroIntegration { * File path of the emitted middleware */ middlewareEntryPoint: URL | undefined; + logger: AstroIntegrationLogger; }) => void | Promise; - 'astro:build:start'?: () => void | Promise; + 'astro:build:start'?: (options: { logger: AstroIntegrationLogger }) => void | Promise; 'astro:build:setup'?: (options: { vite: vite.InlineConfig; pages: Map; target: 'client' | 'server'; updateConfig: (newConfig: vite.InlineConfig) => void; + logger: AstroIntegrationLogger; + }) => void | Promise; + 'astro:build:generated'?: (options: { + dir: URL; + logger: AstroIntegrationLogger; }) => void | Promise; - 'astro:build:generated'?: (options: { dir: URL }) => void | Promise; 'astro:build:done'?: (options: { pages: { pathname: string }[]; dir: URL; routes: RouteData[]; + logger: AstroIntegrationLogger; }) => void | Promise; }; } diff --git a/packages/astro/src/core/logger/console.ts b/packages/astro/src/core/logger/console.ts index dfe732bd771a..f39f6b74d233 100644 --- a/packages/astro/src/core/logger/console.ts +++ b/packages/astro/src/core/logger/console.ts @@ -15,7 +15,7 @@ export const consoleLogDestination = { function getPrefix() { let prefix = ''; - let type = event.type; + let type = event.label; if (type) { // hide timestamp when type is undefined prefix += dim(dateTimeFormat.format(new Date()) + ' '); diff --git a/packages/astro/src/core/logger/core.ts b/packages/astro/src/core/logger/core.ts index 4f0c281e0d8c..e5d0aee1f0c5 100644 --- a/packages/astro/src/core/logger/core.ts +++ b/packages/astro/src/core/logger/core.ts @@ -6,7 +6,6 @@ interface LogWritable { } export type LoggerLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; // same as Pino -export type LoggerEvent = 'info' | 'warn' | 'error'; export interface LogOptions { dest: LogWritable; @@ -29,7 +28,7 @@ export const dateTimeFormat = new Intl.DateTimeFormat([], { }); export interface LogMessage { - type: string | null; + label: string | null; level: LoggerLevel; message: string; } @@ -43,11 +42,11 @@ export const levels: Record = { }; /** Full logging API */ -export function log(opts: LogOptions, level: LoggerLevel, type: string | null, message: string) { +export function log(opts: LogOptions, level: LoggerLevel, label: string | null, message: string) { const logLevel = opts.level; const dest = opts.dest; const event: LogMessage = { - type, + label, level, message, }; @@ -61,18 +60,18 @@ export function log(opts: LogOptions, level: LoggerLevel, type: string | null, m } /** Emit a user-facing message. Useful for UI and other console messages. */ -export function info(opts: LogOptions, type: string | null, message: string) { - return log(opts, 'info', type, message); +export function info(opts: LogOptions, label: string | null, message: string) { + return log(opts, 'info', label, message); } /** Emit a warning message. Useful for high-priority messages that aren't necessarily errors. */ -export function warn(opts: LogOptions, type: string | null, message: string) { - return log(opts, 'warn', type, message); +export function warn(opts: LogOptions, label: string | null, message: string) { + return log(opts, 'warn', label, message); } /** Emit a error message, Useful when Astro can't recover from some error. */ -export function error(opts: LogOptions, type: string | null, message: string) { - return log(opts, 'error', type, message); +export function error(opts: LogOptions, label: string | null, message: string) { + return log(opts, 'error', label, message); } type LogFn = typeof info | typeof warn | typeof error; @@ -127,3 +126,53 @@ export function timerMessage(message: string, startTime: number = Date.now()) { timeDiff < 750 ? `${Math.round(timeDiff)}ms` : `${(timeDiff / 1000).toFixed(1)}s`; return `${message} ${dim(timeDisplay)}`; } + +export class Logger { + options: LogOptions; + constructor(options: LogOptions) { + this.options = options; + } + + info(label: string, message: string) { + info(this.options, label, message); + } + warn(label: string, message: string) { + warn(this.options, label, message); + } + error(label: string, message: string) { + error(this.options, label, message); + } + debug(label: string, message: string) { + debug(this.options, label, message); + } +} + +export class AstroIntegrationLogger { + options: LogOptions; + label: string; + + constructor(logging: LogOptions, label: string) { + this.options = logging; + this.label = label; + } + + /** + * Creates a new logger instance with a new label, but the same log options. + */ + fork(label: string): AstroIntegrationLogger { + return new AstroIntegrationLogger(this.options, label); + } + + info(message: string) { + info(this.options, this.label, message); + } + warn(message: string) { + warn(this.options, this.label, message); + } + error(message: string) { + error(this.options, this.label, message); + } + debug(message: string) { + debug(this.options, this.label, message); + } +} diff --git a/packages/astro/src/core/logger/node.ts b/packages/astro/src/core/logger/node.ts index 513ba257ef57..aeef4bd84219 100644 --- a/packages/astro/src/core/logger/node.ts +++ b/packages/astro/src/core/logger/node.ts @@ -21,19 +21,19 @@ export const nodeLogDestination = new Writable({ function getPrefix() { let prefix = ''; - let type = event.type; - if (type) { + let label = event.label; + if (label) { // hide timestamp when type is undefined prefix += dim(dateTimeFormat.format(new Date()) + ' '); if (event.level === 'info') { - type = bold(cyan(`[${type}]`)); + label = bold(cyan(`[${label}]`)); } else if (event.level === 'warn') { - type = bold(yellow(`[${type}]`)); + label = bold(yellow(`[${label}]`)); } else if (event.level === 'error') { - type = bold(red(`[${type}]`)); + label = bold(red(`[${label}]`)); } - prefix += `${type} `; + prefix += `${label} `; } return reset(prefix); } @@ -87,7 +87,7 @@ export const nodeLogOptions: Required = { }; export interface LogMessage { - type: string | null; + label: string | null; level: LoggerLevel; message: string; } diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts index cf50df0e1854..d54ac38037c8 100644 --- a/packages/astro/src/integrations/index.ts +++ b/packages/astro/src/integrations/index.ts @@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'; import type { InlineConfig, ViteDevServer } from 'vite'; import type { AstroConfig, + AstroIntegration, AstroRenderer, AstroSettings, ContentEntryType, @@ -16,7 +17,7 @@ import type { SerializedSSRManifest } from '../core/app/types'; import type { PageBuildData } from '../core/build/types'; import { buildClientDirectiveEntrypoint } from '../core/client-directive/index.js'; import { mergeConfig } from '../core/config/index.js'; -import { info, type LogOptions } from '../core/logger/core.js'; +import { info, type LogOptions, AstroIntegrationLogger } from '../core/logger/core.js'; import { isServerLikeOutput } from '../prerender/utils.js'; async function withTakingALongTimeMsg({ @@ -38,6 +39,19 @@ async function withTakingALongTimeMsg({ return result; } +// Used internally to store instances of loggers. +const Loggers = new WeakMap(); + +function getLogger(integration: AstroIntegration, logging: LogOptions) { + if (Loggers.has(integration)) { + // SAFETY: we check the existence in the if block + return Loggers.get(integration)!; + } + const logger = new AstroIntegrationLogger(logging, integration.name); + Loggers.set(integration, logger); + return logger; +} + export async function runHookConfigSetup({ settings, command, @@ -72,6 +86,8 @@ export async function runHookConfigSetup({ * ``` */ if (integration.hooks?.['astro:config:setup']) { + const logger = getLogger(integration, logging); + const hooks: HookParameters<'astro:config:setup'> = { config: updatedConfig, command, @@ -107,6 +123,7 @@ export async function runHookConfigSetup({ } addedClientDirectives.set(name, buildClientDirectiveEntrypoint(name, entrypoint)); }, + logger, }; // --- @@ -167,6 +184,7 @@ export async function runHookConfigDone({ logging: LogOptions; }) { for (const integration of settings.config.integrations) { + const logger = getLogger(integration, logging); if (integration?.hooks?.['astro:config:done']) { await withTakingALongTimeMsg({ name: integration.name, @@ -180,6 +198,7 @@ export async function runHookConfigDone({ } settings.adapter = adapter; }, + logger, }), logging, }); @@ -198,9 +217,10 @@ export async function runHookServerSetup({ }) { for (const integration of config.integrations) { if (integration?.hooks?.['astro:server:setup']) { + const logger = getLogger(integration, logging); await withTakingALongTimeMsg({ name: integration.name, - hookResult: integration.hooks['astro:server:setup']({ server }), + hookResult: integration.hooks['astro:server:setup']({ server, logger }), logging, }); } @@ -217,10 +237,12 @@ export async function runHookServerStart({ logging: LogOptions; }) { for (const integration of config.integrations) { + const logger = getLogger(integration, logging); + if (integration?.hooks?.['astro:server:start']) { await withTakingALongTimeMsg({ name: integration.name, - hookResult: integration.hooks['astro:server:start']({ address }), + hookResult: integration.hooks['astro:server:start']({ address, logger }), logging, }); } @@ -235,10 +257,12 @@ export async function runHookServerDone({ logging: LogOptions; }) { for (const integration of config.integrations) { + const logger = getLogger(integration, logging); + if (integration?.hooks?.['astro:server:done']) { await withTakingALongTimeMsg({ name: integration.name, - hookResult: integration.hooks['astro:server:done'](), + hookResult: integration.hooks['astro:server:done']({ logger }), logging, }); } @@ -254,9 +278,11 @@ export async function runHookBuildStart({ }) { for (const integration of config.integrations) { if (integration?.hooks?.['astro:build:start']) { + const logger = getLogger(integration, logging); + await withTakingALongTimeMsg({ name: integration.name, - hookResult: integration.hooks['astro:build:start'](), + hookResult: integration.hooks['astro:build:start']({ logger }), logging, }); } @@ -280,6 +306,8 @@ export async function runHookBuildSetup({ for (const integration of config.integrations) { if (integration?.hooks?.['astro:build:setup']) { + const logger = getLogger(integration, logging); + await withTakingALongTimeMsg({ name: integration.name, hookResult: integration.hooks['astro:build:setup']({ @@ -289,6 +317,7 @@ export async function runHookBuildSetup({ updateConfig: (newConfig) => { updatedConfig = mergeConfig(updatedConfig, newConfig); }, + logger, }), logging, }); @@ -315,12 +344,15 @@ export async function runHookBuildSsr({ }: RunHookBuildSsr) { for (const integration of config.integrations) { if (integration?.hooks?.['astro:build:ssr']) { + const logger = getLogger(integration, logging); + await withTakingALongTimeMsg({ name: integration.name, hookResult: integration.hooks['astro:build:ssr']({ manifest, entryPoints, middlewareEntryPoint, + logger, }), logging, }); @@ -338,10 +370,12 @@ export async function runHookBuildGenerated({ const dir = isServerLikeOutput(config) ? config.build.client : config.outDir; for (const integration of config.integrations) { + const logger = getLogger(integration, logging); + if (integration?.hooks?.['astro:build:generated']) { await withTakingALongTimeMsg({ name: integration.name, - hookResult: integration.hooks['astro:build:generated']({ dir }), + hookResult: integration.hooks['astro:build:generated']({ dir, logger }), logging, }); } @@ -361,12 +395,15 @@ export async function runHookBuildDone({ config, pages, routes, logging }: RunHo for (const integration of config.integrations) { if (integration?.hooks?.['astro:build:done']) { + const logger = getLogger(integration, logging); + await withTakingALongTimeMsg({ name: integration.name, hookResult: integration.hooks['astro:build:done']({ pages: pages.map((p) => ({ pathname: p })), dir, routes, + logger, }), logging, }); diff --git a/packages/astro/test/static-build.test.js b/packages/astro/test/static-build.test.js index d4a687a5d85f..0552c353f068 100644 --- a/packages/astro/test/static-build.test.js +++ b/packages/astro/test/static-build.test.js @@ -175,7 +175,7 @@ describe('Static build', () => { let found = false; for (const log of logs) { if ( - log.type === 'ssg' && + log.label === 'ssg' && /[hH]eaders are not exposed in static \(SSG\) output mode/.test(log.message) ) { found = true;