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: