diff --git a/packages/loader/src/cache.ts b/packages/loader/src/cache.ts new file mode 100644 index 0000000..423b302 --- /dev/null +++ b/packages/loader/src/cache.ts @@ -0,0 +1,22 @@ +import { join } from 'node:path' +import { SrcSetCacheStorage } from '@srcset/core' + +const storages = new Map() + +/** + * Get the disk cache storage shared between loader runs. + * Every run of a compilation gets the same storage, so a variant + * generated for one module is a hit for the next one. + * @param context - Root context directory of the compiler. + * @returns Cache storage. + */ +export function getSharedCache(context: string) { + let storage = storages.get(context) + + if (!storage) { + storage = new SrcSetCacheStorage(join(context, 'node_modules', '.cache', 'srcset')) + storages.set(context, storage) + } + + return storage +} diff --git a/packages/loader/src/loader.spec.ts b/packages/loader/src/loader.spec.ts index 0ba3086..11f5793 100644 --- a/packages/loader/src/loader.spec.ts +++ b/packages/loader/src/loader.spec.ts @@ -1,7 +1,8 @@ import { describe, it, - expect + expect, + vi } from 'vitest' import sharp from 'sharp' import webpack from 'webpack' @@ -155,6 +156,48 @@ describe('loader', () => { expect(assets).toEqual(['image.jpg.webp']) }) + it('should reuse the disk cache across builds when enabled', async () => { + const dir = await createFixtureProject(defaultEntry) + const optimize = vi.fn((contents: Buffer) => contents) + const options = { + cache: true, + optimization: { + jpg: optimize + } + } + + await compile(createCompiler, dir, options) + + const generatedCalls = optimize.mock.calls.length + + expect(generatedCalls).toBeGreaterThan(0) + + const { assets } = await compile(createCompiler, dir, options) + + expect(optimize.mock.calls.length).toBe(generatedCalls) + expect(assets.length).toBeGreaterThan(0) + }) + + it('should regenerate without the cache option', async () => { + const dir = await createFixtureProject(defaultEntry) + const optimize = vi.fn((contents: Buffer) => contents) + const options = { + optimization: { + jpg: optimize + } + } + + await compile(createCompiler, dir, options) + + const generatedCalls = optimize.mock.calls.length + + expect(generatedCalls).toBeGreaterThan(0) + + await compile(createCompiler, dir, options) + + expect(optimize.mock.calls.length).toBe(generatedCalls * 2) + }) + it('should export placeholder data-url when enabled', async () => { const dir = await createFixtureProject(defaultEntry) const { exports } = await compile(createCompiler, dir, { diff --git a/packages/loader/src/loader.ts b/packages/loader/src/loader.ts index 3bac385..9a284eb 100644 --- a/packages/loader/src/loader.ts +++ b/packages/loader/src/loader.ts @@ -14,17 +14,19 @@ import { resolvePublicPath } from './paths.ts' import { getSharedLimit } from './limit.ts' +import { getSharedCache } from './cache.ts' async function generateModule(ctx: SrcSetLoaderContext, contents: Buffer) { - const options = ctx.getOptions() const { context = ctx.rootContext, emitFile = true, name = getDefaultName(ctx.mode), + cache = false, concurrency, outputPath, - publicPath - } = options + publicPath, + ...moduleOptions + } = ctx.getOptions() const source = { path: ctx.resourcePath, contents @@ -56,7 +58,10 @@ async function generateModule(ctx: SrcSetLoaderContext, contents: Buffer) { return generateSrcSetModule( source, query, - options, + { + ...moduleOptions, + cache: cache ? getSharedCache(ctx.rootContext) : undefined + }, emitImage, limit ) diff --git a/packages/loader/src/types.ts b/packages/loader/src/types.ts index ad5ac11..a2c7a5c 100644 --- a/packages/loader/src/types.ts +++ b/packages/loader/src/types.ts @@ -49,7 +49,13 @@ export interface SrcSetLoaderContext { async(): (error?: Error | null, content?: string | Buffer) => void } -export interface SrcSetLoaderOptions extends SrcSetModuleOptions { +export interface SrcSetLoaderOptions extends Omit { + /** + * Cache generated variants on disk, in `node_modules/.cache/srcset`: + * repeated builds skip the generation. Disabled by default - the + * persistent cache of the bundler covers it, when it is enabled. + */ + cache?: boolean /** * Output file name template. * Supports `[name]`, `[postfix]`, `[ext]`, `[path]`, `[sourceext]` and