[@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
This commit is contained in:
Tony Sullivan 2022-08-22 19:13:19 +00:00 committed by GitHub
parent 4ac2718883
commit c4af8723bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 149 additions and 19 deletions

View file

@ -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.

View file

@ -7,6 +7,7 @@ This **[Astro integration][astro-integration]** makes it easy to optimize images
- <strong>[Why `@astrojs/image`?](#why-astrojsimage)</strong>
- <strong>[Installation](#installation)</strong>
- <strong>[Usage](#usage)</strong>
- <strong>[Debugging](#debugging)</strong>
- <strong>[Configuration](#configuration)</strong>
- <strong>[Examples](#examples)</strong>
- <strong>[Troubleshooting](#troubleshooting)</strong>
@ -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

View file

@ -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"
}
}

View file

@ -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<string, Map<string, TransformOptions>>;
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<string>();
// 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`)) });
}

View file

@ -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 });
}
}
},

View file

@ -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<LoggerLevel, number> = {
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);

View file

@ -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' })]
});

View file

@ -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' })]
});

View file

@ -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' })]
});

View file

@ -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: