summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/forty-horses-act.md5
-rw-r--r--packages/astro/src/@types/astro.ts17
-rw-r--r--packages/astro/src/assets/internal.ts59
-rw-r--r--packages/astro/src/core/build/generate.ts11
-rw-r--r--packages/astro/src/core/config/schema.ts10
-rw-r--r--packages/astro/test/core-image.test.js38
6 files changed, 127 insertions, 13 deletions
diff --git a/.changeset/forty-horses-act.md b/.changeset/forty-horses-act.md
new file mode 100644
index 000000000..77eec497c
--- /dev/null
+++ b/.changeset/forty-horses-act.md
@@ -0,0 +1,5 @@
+---
+'astro': minor
+---
+
+Generated optimized images are now cached inside the `node_modules/.astro/assets` folder. The cached images will be used to avoid doing extra work and speed up subsequent builds.
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 872f2bb56..5c9f46c2e 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -432,6 +432,23 @@ export interface AstroUserConfig {
/**
* @docs
+ * @name cacheDir
+ * @type {string}
+ * @default `"./node_modules/.astro"`
+ * @description Set the directory for caching build artifacts. Files in this directory will be used in subsequent builds to speed up the build time.
+ *
+ * The value can be either an absolute file system path or a path relative to the project root.
+ *
+ * ```js
+ * {
+ * cacheDir: './my-custom-cache-directory'
+ * }
+ * ```
+ */
+ cacheDir?: string;
+
+ /**
+ * @docs
* @name site
* @type {string}
* @description
diff --git a/packages/astro/src/assets/internal.ts b/packages/astro/src/assets/internal.ts
index c4b8ca751..365bbcf62 100644
--- a/packages/astro/src/assets/internal.ts
+++ b/packages/astro/src/assets/internal.ts
@@ -73,13 +73,20 @@ export function getStaticImageList(): Iterable<
return globalThis.astroAsset.staticImages?.entries();
}
-interface GenerationData {
+interface GenerationDataUncached {
+ cached: false;
weight: {
before: number;
after: number;
};
}
+interface GenerationDataCached {
+ cached: true;
+}
+
+type GenerationData = GenerationDataUncached | GenerationDataCached;
+
export async function generateImage(
buildOpts: StaticBuildOptions,
options: ImageTransform,
@@ -89,7 +96,19 @@ export async function generateImage(
return undefined;
}
- const imageService = (await getConfiguredImageService()) as LocalImageService;
+ let useCache = true;
+ const assetsCacheDir = new URL('assets/', buildOpts.settings.config.cacheDir);
+
+ // Ensure that the cache directory exists
+ try {
+ await fs.promises.mkdir(assetsCacheDir, { recursive: true });
+ } catch (err) {
+ console.error(
+ 'An error was encountered while creating the cache directory. Proceeding without caching. Error: ',
+ err
+ );
+ useCache = false;
+ }
let serverRoot: URL, clientRoot: URL;
if (buildOpts.settings.config.output === 'server') {
@@ -100,6 +119,20 @@ export async function generateImage(
clientRoot = buildOpts.settings.config.outDir;
}
+ const finalFileURL = new URL('.' + filepath, clientRoot);
+ const finalFolderURL = new URL('./', finalFileURL);
+ const cachedFileURL = new URL(basename(filepath), assetsCacheDir);
+
+ try {
+ await fs.promises.copyFile(cachedFileURL, finalFileURL);
+
+ return {
+ cached: true,
+ };
+ } catch (e) {
+ // no-op
+ }
+
// The original file's path (the `src` attribute of the ESM imported image passed by the user)
const originalImagePath = options.src.src;
@@ -112,19 +145,33 @@ export async function generateImage(
serverRoot
)
);
+
+ const imageService = (await getConfiguredImageService()) as LocalImageService;
const resultData = await imageService.transform(
fileData,
{ ...options, src: originalImagePath },
buildOpts.settings.config.image.service.config
);
- const finalFileURL = new URL('.' + filepath, clientRoot);
- const finalFolderURL = new URL('./', finalFileURL);
-
await fs.promises.mkdir(finalFolderURL, { recursive: true });
- await fs.promises.writeFile(finalFileURL, resultData.data);
+
+ if (useCache) {
+ try {
+ await fs.promises.writeFile(cachedFileURL, resultData.data);
+ await fs.promises.copyFile(cachedFileURL, finalFileURL);
+ } catch (e) {
+ console.error(
+ `There was an error creating the cache entry for ${filepath}. Attempting to write directly to output directory. Error: `,
+ e
+ );
+ await fs.promises.writeFile(finalFileURL, resultData.data);
+ }
+ } else {
+ await fs.promises.writeFile(finalFileURL, resultData.data);
+ }
return {
+ cached: false,
weight: {
before: Math.trunc(fileData.byteLength / 1024),
after: Math.trunc(resultData.data.byteLength / 1024),
diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts
index 7d1f85211..8d195bab4 100644
--- a/packages/astro/src/core/build/generate.ts
+++ b/packages/astro/src/core/build/generate.ts
@@ -146,13 +146,10 @@ async function generateImage(opts: StaticBuildOptions, transform: ImageTransform
const timeEnd = performance.now();
const timeChange = getTimeStat(timeStart, timeEnd);
const timeIncrease = `(+${timeChange})`;
- info(
- opts.logging,
- null,
- ` ${green('▶')} ${path} ${dim(
- `(before: ${generationData.weight.before}kb, after: ${generationData.weight.after}kb)`
- )} ${dim(timeIncrease)}`
- );
+ const statsText = generationData.cached
+ ? `(reused cache entry)`
+ : `(before: ${generationData.weight.before}kb, after: ${generationData.weight.after}kb)`;
+ info(opts.logging, null, ` ${green('▶')} ${path} ${dim(statsText)} ${dim(timeIncrease)}`);
}
async function generatePage(
diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts
index 033e55222..fd8d88c4d 100644
--- a/packages/astro/src/core/config/schema.ts
+++ b/packages/astro/src/core/config/schema.ts
@@ -13,6 +13,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
srcDir: './src',
publicDir: './public',
outDir: './dist',
+ cacheDir: './node_modules/.astro',
base: '/',
trailingSlash: 'ignore',
build: {
@@ -63,6 +64,11 @@ export const AstroConfigSchema = z.object({
.optional()
.default(ASTRO_CONFIG_DEFAULTS.outDir)
.transform((val) => new URL(val)),
+ cacheDir: z
+ .string()
+ .optional()
+ .default(ASTRO_CONFIG_DEFAULTS.cacheDir)
+ .transform((val) => new URL(val)),
site: z.string().url().optional(),
base: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.base),
trailingSlash: z
@@ -220,6 +226,10 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) {
.string()
.default(ASTRO_CONFIG_DEFAULTS.outDir)
.transform((val) => new URL(appendForwardSlash(val), fileProtocolRoot)),
+ cacheDir: z
+ .string()
+ .default(ASTRO_CONFIG_DEFAULTS.cacheDir)
+ .transform((val) => new URL(appendForwardSlash(val), fileProtocolRoot)),
build: z
.object({
format: z
diff --git a/packages/astro/test/core-image.test.js b/packages/astro/test/core-image.test.js
index 57720f0c4..7417b4895 100644
--- a/packages/astro/test/core-image.test.js
+++ b/packages/astro/test/core-image.test.js
@@ -1,7 +1,9 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
+import { basename } from 'node:path';
import { Writable } from 'node:stream';
import { fileURLToPath } from 'node:url';
+import { removeDir } from '../dist/core/fs/index.js';
import testAdapter from './test-adapter.js';
import { loadFixture } from './test-utils.js';
@@ -455,6 +457,9 @@ describe('astro:image', () => {
assets: true,
},
});
+ // Remove cache directory
+ removeDir(new URL('./fixtures/core-image-ssg/node_modules/.astro', import.meta.url));
+
await fixture.build();
});
@@ -569,6 +574,39 @@ describe('astro:image', () => {
const $ = cheerio.load(html);
expect($('#no-format img').attr('src')).to.not.equal($('#format-avif img').attr('src'));
});
+
+ it('has cache entries', async () => {
+ const generatedImages = (await fixture.glob('_astro/**/*.webp')).map((path) =>
+ basename(path)
+ );
+ const cachedImages = (await fixture.glob('../node_modules/.astro/assets/**/*.webp')).map(
+ (path) => basename(path)
+ );
+
+ expect(generatedImages).to.deep.equal(cachedImages);
+ });
+
+ it('uses cache entries', async () => {
+ const logs = [];
+ const logging = {
+ dest: {
+ write(chunk) {
+ logs.push(chunk);
+ },
+ },
+ };
+
+ await fixture.build({ logging });
+ const generatingImageIndex = logs.findIndex((logLine) =>
+ logLine.message.includes('generating optimized images')
+ );
+ const relevantLogs = logs.slice(generatingImageIndex + 1, -1);
+ const isReusingCache = relevantLogs.every((logLine) =>
+ logLine.message.includes('(reused cache entry)')
+ );
+
+ expect(isReusingCache).to.be.true;
+ });
});
describe('prod ssr', () => {