Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[@astrojs/image] adds a logger to the the image integration #4342

Merged
merged 10 commits into from
Aug 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this @tony-sull! Nice QOL improvement.

I like allowing this to be configured at the integration level to allow debugging only images but do you think this should use a default set by the existing logging options?

Running DEBUG=* astro build or DEBUG=vite:* astro build is something we support via Vite, so maybe we should be tapping into that same system?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I had considered that but ended up just mimicking core's logger for now. Ideally we could expose Astro's internal logger to all integrations, there's a bit of work to really get that logger ready for outside use first though

...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: 'http://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: 'http://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: 'http://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.