Skip to content

Commit

Permalink
[@astrojs/image] adds a logger to the the image integration (#4342)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Tony Sullivan committed Aug 22, 2022
1 parent 4ac2718 commit c4af872
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/itchy-crews-care.md
Original file line number Diff line number Diff line change
@@ -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.
20 changes: 18 additions & 2 deletions packages/integrations/image/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion packages/integrations/image/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
50 changes: 39 additions & 11 deletions packages/integrations/image/src/build/ssg.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
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';
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;

Expand All @@ -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)) {
Expand All @@ -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`)) });
}
7 changes: 5 additions & 2 deletions packages/integrations/image/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

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

Expand Down Expand Up @@ -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': () => {
Expand Down Expand Up @@ -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 });
}
}
},
Expand Down
74 changes: 74 additions & 0 deletions packages/integrations/image/src/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -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);

Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import image from '@astrojs/image';
// https://astro.build/config
export default defineConfig({
site: 'https://localhost:3000',
integrations: [image()]
integrations: [image({ logLevel: 'silent' })]
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import image from '@astrojs/image';
// https://astro.build/config
export default defineConfig({
site: 'https://localhost:3000',
integrations: [image()]
integrations: [image({ logLevel: 'silent' })]
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import image from '@astrojs/image';
// https://astro.build/config
export default defineConfig({
site: 'https://localhost:3000',
integrations: [image()]
integrations: [image({ logLevel: 'silent' })]
});
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c4af872

Please sign in to comment.