From c4af8723bd232d78d24dbd58feaef87dbaec07c7 Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Mon, 22 Aug 2022 19:13:19 +0000 Subject: [PATCH] [@astrojs/image] adds a logger to the the image integration (#4342) * WIP: adding a console logger that respect vite.logLevel * adds an optional prefix for messages * remove temporary debug log * typo fix * cleaning up log syntax * fixing logger whitespace * adding README docs * test: disable integration logging in tests * chore: add changeset --- .changeset/itchy-crews-care.md | 5 ++ packages/integrations/image/README.md | 20 ++++- packages/integrations/image/package.json | 4 +- packages/integrations/image/src/build/ssg.ts | 50 ++++++++++--- packages/integrations/image/src/index.ts | 7 +- .../integrations/image/src/utils/logger.ts | 74 +++++++++++++++++++ .../fixtures/basic-image/astro.config.mjs | 2 +- .../fixtures/basic-picture/astro.config.mjs | 2 +- .../test/fixtures/rotation/astro.config.mjs | 2 +- pnpm-lock.yaml | 2 + 10 files changed, 149 insertions(+), 19 deletions(-) create mode 100644 .changeset/itchy-crews-care.md create mode 100644 packages/integrations/image/src/utils/logger.ts diff --git a/.changeset/itchy-crews-care.md b/.changeset/itchy-crews-care.md new file mode 100644 index 000000000..01b4621dc --- /dev/null +++ b/.changeset/itchy-crews-care.md @@ -0,0 +1,5 @@ +--- +'@astrojs/image': patch +--- + +The integration now includes a logger to better track progress in SSG builds. Use the new `logLevel: "debug"` integration option to see detailed logs of every image transformation built in your project. diff --git a/packages/integrations/image/README.md b/packages/integrations/image/README.md index d2440125e..1e1275e96 100644 --- a/packages/integrations/image/README.md +++ b/packages/integrations/image/README.md @@ -7,6 +7,7 @@ This **[Astro integration][astro-integration]** makes it easy to optimize images - [Why `@astrojs/image`?](#why-astrojsimage) - [Installation](#installation) - [Usage](#usage) +- [Debugging](#debugging) - [Configuration](#configuration) - [Examples](#examples) - [Troubleshooting](#troubleshooting) @@ -272,8 +273,6 @@ The integration can be configured to run with a different image service, either > During development, local images may not have been published yet and would not be available to hosted image services. Local images will always use the built-in `sharp` service when using `astro dev`. -There are currently no other configuration options for the `@astrojs/image` integration. Please [open an issue](https://github.com/withastro/astro/issues/new/choose) if you have a compelling use case to share. - ### config.serviceEntryPoint @@ -291,6 +290,23 @@ export default { } ``` +### config.logLevel + +The `logLevel` controls can be used to control how much detail is logged by the integration during builds. This may be useful to track down a specific image or transformation that is taking a long time to build. + +```js +// astro.config.mjs +import image from '@astrojs/image'; + +export default { + integrations: [image({ + // supported levels: 'debug' | 'info' | 'warn' | 'error' | 'silent' + // default: 'info' + logLevel: 'debug' + })], +} +``` + ## Examples ### Local images diff --git a/packages/integrations/image/package.json b/packages/integrations/image/package.json index 27d69fa50..be0f1c2c3 100644 --- a/packages/integrations/image/package.json +++ b/packages/integrations/image/package.json @@ -52,6 +52,8 @@ "@types/etag": "^1.8.1", "@types/sharp": "^0.30.4", "astro": "workspace:*", - "astro-scripts": "workspace:*" + "astro-scripts": "workspace:*", + "kleur": "^4.1.4", + "tiny-glob": "^0.2.9" } } diff --git a/packages/integrations/image/src/build/ssg.ts b/packages/integrations/image/src/build/ssg.ts index 496905f92..c4f873f33 100644 --- a/packages/integrations/image/src/build/ssg.ts +++ b/packages/integrations/image/src/build/ssg.ts @@ -1,3 +1,4 @@ +import { bgGreen, black, cyan, dim, green, bold } from 'kleur/colors'; import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -5,19 +6,30 @@ import { OUTPUT_DIR } from '../constants.js'; import type { SSRImageService, TransformOptions } from '../loaders/index.js'; import { isRemoteImage, loadLocalImage, loadRemoteImage } from '../utils/images.js'; import { ensureDir } from '../utils/paths.js'; +import { debug, info, warn, LoggerLevel } from '../utils/logger.js'; + +function getTimeStat(timeStart: number, timeEnd: number) { + const buildTime = timeEnd - timeStart; + return buildTime < 750 ? `${Math.round(buildTime)}ms` : `${(buildTime / 1000).toFixed(2)}s`; +} export interface SSGBuildParams { loader: SSRImageService; staticImages: Map>; srcDir: URL; outDir: URL; + logLevel: LoggerLevel; } -export async function ssgBuild({ loader, staticImages, srcDir, outDir }: SSGBuildParams) { +export async function ssgBuild({ loader, staticImages, srcDir, outDir, logLevel }: SSGBuildParams) { + const timer = performance.now(); + + info({ level: logLevel, prefix: false, message: `${bgGreen(black(` optimizing ${staticImages.size} image${staticImages.size > 1 ? 's' : ''} `))}` }); + const inputFiles = new Set(); // process transforms one original image file at a time - for await (const [src, transformsMap] of staticImages) { + for (const [src, transformsMap] of staticImages) { let inputFile: string | undefined = undefined; let inputBuffer: Buffer | undefined = undefined; @@ -35,14 +47,30 @@ export async function ssgBuild({ loader, staticImages, srcDir, outDir }: SSGBuil if (!inputBuffer) { // eslint-disable-next-line no-console - console.warn(`"${src}" image could not be fetched`); + warn({ level: logLevel, message : `"${src}" image could not be fetched` }); continue; } const transforms = Array.from(transformsMap.entries()); + debug({ level: logLevel, prefix: false, message: `${green('▶')} ${src}` }); + let timeStart = performance.now(); + + if (inputFile) { + const to = inputFile.replace(fileURLToPath(srcDir), fileURLToPath(outDir)); + await ensureDir(path.dirname(to)); + await fs.copyFile(inputFile, to); + + const timeEnd = performance.now(); + const timeChange = getTimeStat(timeStart, timeEnd); + const timeIncrease = `(+${timeChange})`; + const pathRelative = inputFile.replace(fileURLToPath(srcDir), ''); + debug({ level: logLevel, prefix: false, message: ` ${cyan('└─')} ${dim(`(original) ${pathRelative}`)} ${dim(timeIncrease)}` }); + } + // process each transformed versiono of the - for await (const [filename, transform] of transforms) { + for (const [filename, transform] of transforms) { + timeStart = performance.now(); let outputFile: string; if (isRemoteImage(src)) { @@ -58,14 +86,14 @@ export async function ssgBuild({ loader, staticImages, srcDir, outDir }: SSGBuil ensureDir(path.dirname(outputFile)); await fs.writeFile(outputFile, data); + + const timeEnd = performance.now(); + const timeChange = getTimeStat(timeStart, timeEnd); + const timeIncrease = `(+${timeChange})`; + const pathRelative = outputFile.replace(fileURLToPath(outDir), ''); + debug({ level: logLevel, prefix: false, message: ` ${cyan('└─')} ${dim(pathRelative)} ${dim(timeIncrease)}` }); } } - // copy all original local images to dist - for await (const original of inputFiles) { - const to = original.replace(fileURLToPath(srcDir), fileURLToPath(outDir)); - - await ensureDir(path.dirname(to)); - await fs.copyFile(original, to); - } + info({ level: logLevel, prefix: false, message: (dim(`Completed in ${getTimeStat(timer, performance.now())}.\n`)) }); } diff --git a/packages/integrations/image/src/index.ts b/packages/integrations/image/src/index.ts index 78f3d3f4e..d3e154d62 100644 --- a/packages/integrations/image/src/index.ts +++ b/packages/integrations/image/src/index.ts @@ -3,6 +3,7 @@ import { ssgBuild } from './build/ssg.js'; import { ssrBuild } from './build/ssr.js'; import { PKG_NAME, ROUTE_PATTERN } from './constants.js'; import { ImageService, TransformOptions } from './loaders/index.js'; +import type { LoggerLevel } from './utils/logger.js'; import { filenameFormat, propsToFilename } from './utils/paths.js'; import { createPlugin } from './vite-plugin-astro-image.js'; @@ -27,11 +28,13 @@ export interface IntegrationOptions { * Entry point for the @type {HostedImageService} or @type {LocalImageService} to be used. */ serviceEntryPoint?: string; + logLevel?: LoggerLevel; } export default function integration(options: IntegrationOptions = {}): AstroIntegration { const resolvedOptions = { serviceEntryPoint: '@astrojs/image/sharp', + logLevel: 'info' as LoggerLevel, ...options, }; @@ -72,7 +75,7 @@ export default function integration(options: IntegrationOptions = {}): AstroInte }); } }, - 'astro:server:setup': async () => { + 'astro:server:setup': async ({ server }) => { globalThis.astroImage = {}; }, 'astro:build:setup': () => { @@ -107,7 +110,7 @@ export default function integration(options: IntegrationOptions = {}): AstroInte const loader = globalThis?.astroImage?.loader; if (loader && 'transform' in loader && staticImages.size > 0) { - await ssgBuild({ loader, staticImages, srcDir: _config.srcDir, outDir: dir }); + await ssgBuild({ loader, staticImages, srcDir: _config.srcDir, outDir: dir, logLevel: resolvedOptions.logLevel }); } } }, diff --git a/packages/integrations/image/src/utils/logger.ts b/packages/integrations/image/src/utils/logger.ts new file mode 100644 index 000000000..3c11dacdc --- /dev/null +++ b/packages/integrations/image/src/utils/logger.ts @@ -0,0 +1,74 @@ +// eslint-disable no-console +import { bold, cyan, dim, green, red, yellow } from 'kleur/colors'; + +const PREFIX = '@astrojs/image'; + +// Hey, locales are pretty complicated! Be careful modifying this logic... +// If we throw at the top-level, international users can't use Astro. +// +// Using `[]` sets the default locale properly from the system! +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#parameters +// +// Here be the dragons we've slain: +// https://github.com/withastro/astro/issues/2625 +// https://github.com/withastro/astro/issues/3309 +const dateTimeFormat = new Intl.DateTimeFormat([], { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', +}); + +export type LoggerLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; // same as Pino + +export interface LogMessage { + level: LoggerLevel; + message: string; + prefix?: boolean; + timestamp?: boolean; +} + +export const levels: Record = { + debug: 20, + info: 30, + warn: 40, + error: 50, + silent: 90, +}; + +function getPrefix(level: LoggerLevel, timestamp: boolean) { + let prefix = ''; + + if (timestamp) { + prefix += dim(dateTimeFormat.format(new Date()) + ' '); + } + + switch (level) { + case 'debug': + prefix += bold(green(`[${PREFIX}] `)); + break; + case 'info': + prefix += bold(cyan(`[${PREFIX}] `)); + break; + case 'warn': + prefix += bold(yellow(`[${PREFIX}] `)); + break; + case 'error': + prefix += bold(red(`[${PREFIX}] `)); + break; + } + + return prefix; +} + +const log = (_level: LoggerLevel, dest: (message: string) => void) => + ({ message, level, prefix = true, timestamp = true }: LogMessage) => { + if (levels[_level] >= levels[level]) { + dest(`${prefix ? getPrefix(level, timestamp) : ''}${message}`); + } + } + +export const info = log('info', console.info); +export const debug = log('debug', console.debug); +export const warn = log('warn', console.warn); +export const error = log('error', console.error); + diff --git a/packages/integrations/image/test/fixtures/basic-image/astro.config.mjs b/packages/integrations/image/test/fixtures/basic-image/astro.config.mjs index 45a11dc9d..7dafac3b6 100644 --- a/packages/integrations/image/test/fixtures/basic-image/astro.config.mjs +++ b/packages/integrations/image/test/fixtures/basic-image/astro.config.mjs @@ -4,5 +4,5 @@ import image from '@astrojs/image'; // https://astro.build/config export default defineConfig({ site: 'http://localhost:3000', - integrations: [image()] + integrations: [image({ logLevel: 'silent' })] }); diff --git a/packages/integrations/image/test/fixtures/basic-picture/astro.config.mjs b/packages/integrations/image/test/fixtures/basic-picture/astro.config.mjs index 45a11dc9d..7dafac3b6 100644 --- a/packages/integrations/image/test/fixtures/basic-picture/astro.config.mjs +++ b/packages/integrations/image/test/fixtures/basic-picture/astro.config.mjs @@ -4,5 +4,5 @@ import image from '@astrojs/image'; // https://astro.build/config export default defineConfig({ site: 'http://localhost:3000', - integrations: [image()] + integrations: [image({ logLevel: 'silent' })] }); diff --git a/packages/integrations/image/test/fixtures/rotation/astro.config.mjs b/packages/integrations/image/test/fixtures/rotation/astro.config.mjs index 45a11dc9d..7dafac3b6 100644 --- a/packages/integrations/image/test/fixtures/rotation/astro.config.mjs +++ b/packages/integrations/image/test/fixtures/rotation/astro.config.mjs @@ -4,5 +4,5 @@ import image from '@astrojs/image'; // https://astro.build/config export default defineConfig({ site: 'http://localhost:3000', - integrations: [image()] + integrations: [image({ logLevel: 'silent' })] }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 479f96f99..1a28c6da2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2192,6 +2192,7 @@ importers: astro-scripts: workspace:* etag: ^1.8.1 image-size: ^1.0.1 + kleur: ^4.1.4 mrmime: ^1.0.0 sharp: ^0.30.6 slash: ^4.0.0 @@ -2208,6 +2209,7 @@ importers: '@types/sharp': 0.30.5 astro: link:../../astro astro-scripts: link:../../../scripts + kleur: 4.1.5 packages/integrations/image/test/fixtures/basic-image: specifiers: