diff --git a/README.md b/README.md index f351504..c52b44d 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Highly customizable tools for generating [responsive images](https://developer.m - ⚑️ Powered by [sharp](https://sharp.pixelplumbing.com/) - 🧩 [Webpack](https://webpack.js.org/), [Rspack](https://rspack.rs/) and [Vite](https://vite.dev/) integrations - πŸ–Ό Π‘omponents for React, Preact and Svelte: blur-up placeholders, priority loading, tree-shakable image modules -- 🌐 Image proxy backends - [imgproxy](https://imgproxy.net/) and [Cloudflare](https://developers.cloudflare.com/images/) - build variant urls on top of emitted originals and CMS content images +- 🌐 Image proxy adapters - [imgproxy](https://imgproxy.net/) and [Cloudflare](https://developers.cloudflare.com/images/) - runtime srcset urls for content images, e.g. from an API or a CMS For more details, guides and API references, check out the [documentation website](https://srcset.js.org). @@ -29,8 +29,8 @@ For more details, guides and API references, check out the [documentation websit | [`@srcset/runtime`](packages/runtime#readme) | Browser runtime helpers: srcset strings and attributes of image variants. | [![NPM version][runtime-npm]][runtime-npm-url] | | [`@srcset/loader`](packages/loader#readme) | Webpack and Rspack loader for generating responsive images. | [![NPM version][loader-npm]][loader-npm-url] | | [`@srcset/vite-plugin`](packages/vite-plugin#readme) | Vite plugin for generating responsive images. | [![NPM version][vite-plugin-npm]][vite-plugin-npm-url] | -| [`@srcset/imgproxy`](packages/imgproxy#readme) | Adapter for imgproxy: srcset variant urls for content images and static assets. | [![NPM version][imgproxy-npm]][imgproxy-npm-url] | -| [`@srcset/cloudflare`](packages/cloudflare#readme) | Adapter for Cloudflare image transformations: srcset variant urls for content images and static assets. | [![NPM version][cloudflare-npm]][cloudflare-npm-url] | +| [`@srcset/imgproxy`](packages/imgproxy#readme) | Adapter for imgproxy: srcset variant urls for content images, e.g. from an API or a CMS. | [![NPM version][imgproxy-npm]][imgproxy-npm-url] | +| [`@srcset/cloudflare`](packages/cloudflare#readme) | Adapter for Cloudflare image transformations: srcset variant urls for content images, e.g. from an API or a CMS. | [![NPM version][cloudflare-npm]][cloudflare-npm-url] | | [`@srcset/react`](packages/react#readme) | React components for responsive images: Picture and Image. | [![NPM version][react-npm]][react-npm-url] | | [`@srcset/preact`](packages/preact#readme) | Preact components for responsive images: Picture and Image. | [![NPM version][preact-npm]][preact-npm-url] | | [`@srcset/svelte`](packages/svelte#readme) | Svelte components for responsive images: Picture and Image. | [![NPM version][svelte-npm]][svelte-npm-url] | diff --git a/packages/bundler-utils/README.md b/packages/bundler-utils/README.md index 1cddbb1..e558f8b 100644 --- a/packages/bundler-utils/README.md +++ b/packages/bundler-utils/README.md @@ -29,10 +29,9 @@ [coverage]: https://img.shields.io/codecov/c/github/TrigenSoftware/srcset.svg [coverage-url]: https://app.codecov.io/gh/TrigenSoftware/srcset -Shared machinery of the [srcset](https://github.com/TrigenSoftware/srcset#readme) bundler integrations: import query parsing, image module codegen and the backend contract. +Shared machinery of the [srcset](https://github.com/TrigenSoftware/srcset#readme) bundler integrations: import query parsing and image module codegen. - 🧩 Build custom bundler integrations on top of `generateSrcSetModule` -- 🌐 Implement custom image backends - encode variants or build image proxy urls ## Install @@ -55,8 +54,8 @@ const module = await generateSrcSetModule(source, query, options, emitImage, lim // ES module code: default url, src, srcSet, srcMap and placeholder exports ``` -You probably need [@srcset/loader](https://npmjs.com/package/@srcset/loader) or [@srcset/vite-plugin](https://npmjs.com/package/@srcset/vite-plugin) instead, unless you are building an integration or a backend. +You probably need [@srcset/loader](https://npmjs.com/package/@srcset/loader) or [@srcset/vite-plugin](https://npmjs.com/package/@srcset/vite-plugin) instead, unless you are building an integration. ## Documentation -For more details, guides and API references, check out the [documentation website](https://srcset.js.org/api/backends/). +For more details, guides and API references, check out the [documentation website](https://srcset.js.org/api/bundler-utils/). diff --git a/packages/bundler-utils/package.json b/packages/bundler-utils/package.json index 951193b..451f310 100644 --- a/packages/bundler-utils/package.json +++ b/packages/bundler-utils/package.json @@ -9,7 +9,7 @@ "url": "https://github.com/dangreen" }, "license": "MIT", - "homepage": "https://srcset.js.org/api/backends/", + "homepage": "https://srcset.js.org/api/bundler-utils/", "funding": "https://ko-fi.com/dangreen", "repository": { "type": "git", diff --git a/packages/bundler-utils/src/generate.spec.ts b/packages/bundler-utils/src/generate.spec.ts index 08a4a00..a75f445 100644 --- a/packages/bundler-utils/src/generate.spec.ts +++ b/packages/bundler-utils/src/generate.spec.ts @@ -1,16 +1,10 @@ import { describe, it, - expect, - vi + expect } from 'vitest' import sharp from 'sharp' -import pLimit from 'p-limit' -import type { SrcSetBackend } from './generate.types.ts' -import { - generateSrcSetModule, - srcsetBackend -} from './generate.ts' +import { generateSrcSetModule } from './generate.ts' async function createImage(width = 640, height = 480) { const contents = await sharp({ @@ -139,27 +133,6 @@ describe('bundler-utils', () => { expect(module).toMatch(/export const placeholder = "data:image\/webp;base64,[^"]+";/) }) - it('should pass options, emitImage and limit to the backend', async () => { - const limit = pLimit(1) - let received: unknown[] = [] - const backend: typeof srcsetBackend = (backendOptions, backendEmitImage, backendLimit) => { - received = [backendOptions, backendEmitImage, backendLimit] - - return srcsetBackend(backendOptions, backendEmitImage, backendLimit) - } - const image = await createImage() - const options = { - skipOptimization: true, - backend - } - - await generateSrcSetModule(image, {}, options, emitToPath, limit) - - expect(received[0]).toBe(options) - expect(received[1]).toBe(emitToPath) - expect(received[2]).toBe(limit) - }) - it('should build public path expression without a plain public path', async () => { const image = await createImage() const module = await generateSrcSetModule(image, {}, { @@ -184,35 +157,6 @@ describe('bundler-utils', () => { expect(module).toContain('const url = "/images/image.jpg";') }) - - it('should use url variants without emitting', async () => { - const image = await createImage() - const emitImage = vi.fn(emitToPath) - const urlBackend: SrcSetBackend = { - * generate(_source, metadata, rule) { - const [width = 1] = Array.isArray(rule.width) ? rule.width : [rule.width] - - yield { - format: metadata.format, - width: width <= 1 ? metadata.width : width, - height: metadata.height, - originMultiplier: width <= 1 ? width : null, - url: `https://proxy.test/w:${String(width)}` - } - } - } - const module = await generateSrcSetModule(image, {}, { - backend: () => urlBackend, - rules: [ - { - width: [320] - } - ] - }, emitImage) - - expect(emitImage).not.toHaveBeenCalled() - expect(module).toContain('const url = "https://proxy.test/w:320";') - }) }) }) }) diff --git a/packages/bundler-utils/src/generate.ts b/packages/bundler-utils/src/generate.ts index 8259c55..6c82f71 100644 --- a/packages/bundler-utils/src/generate.ts +++ b/packages/bundler-utils/src/generate.ts @@ -9,7 +9,6 @@ import { import type { QueryOptions } from './query.ts' import type { SrcSetModuleOptions, - SrcSetBackendFactory, EmitImage } from './generate.types.ts' import { @@ -21,43 +20,15 @@ import { createPlaceholder } from './placeholder.ts' export type * from './generate.types.ts' -/** - * The default backend factory: generates images with the sharp - * generator and emits them on the bundler side. - * @param options - Generator options. - * @param emitImage - Emits an image on the bundler side. - * @param limit - Concurrency limit of the integration. - * @returns Backend. - */ -export const srcsetBackend: SrcSetBackendFactory = (options, emitImage, limit) => { - const generator = new SrcSetGenerator({ - ...options, - limit - }) - - return { - async* generate(source, _metadata, rule) { - for await (const image of generator.generate(source, rule)) { - yield { - format: image.format, - width: image.width, - height: image.height, - originMultiplier: image.originMultiplier, - url: emitImage(image) - } - } - } - } -} - /** * Generate ES module code for the image import: match the rules, - * collect the images from the backend and make the module code. + * generate the image variants with the sharp generator, emit them + * on the bundler side and make the module code. * @param source - Image file. * @param query - Parsed import query options. * @param options - Bundler integration options. - * @param emitImage - Emits an image on the bundler side, passed to the backend. - * @param limit - Concurrency limit of the integration, passed to the backend. + * @param emitImage - Emits an image on the bundler side. + * @param limit - Concurrency limit of the integration. * @returns Module code. */ export async function generateSrcSetModule( @@ -67,12 +38,12 @@ export async function generateSrcSetModule( emitImage: EmitImage, limit?: LimitFunction ) { - const { - backend: backendFactory = srcsetBackend, - resourceId = defaultResourceId - } = options + const { resourceId = defaultResourceId } = options const rules = query.rules ?? options.rules ?? [{}] - const backend = backendFactory(options, emitImage, limit) + const generator = new SrcSetGenerator({ + ...options, + limit + }) const metadata = await getImageMetadata(source) const placeholder = await createPlaceholder( source, @@ -94,11 +65,15 @@ export async function generateSrcSetModule( continue } - for await (const image of backend.generate(source, metadata, rule)) { + for await (const image of generator.generate(source, rule)) { srcSet.push({ - ...image, id: resourceId(image.width, image.originMultiplier ?? image.width, image.format), - type: mimeTypes[image.format] + format: image.format, + type: mimeTypes[image.format], + width: image.width, + height: image.height, + originMultiplier: image.originMultiplier, + url: emitImage(image) }) } diff --git a/packages/bundler-utils/src/generate.types.ts b/packages/bundler-utils/src/generate.types.ts index 0a2bd23..876b79a 100644 --- a/packages/bundler-utils/src/generate.types.ts +++ b/packages/bundler-utils/src/generate.types.ts @@ -1,14 +1,10 @@ -import type { LimitFunction } from 'p-limit' import type { - ImageMetadata, - ImageSource, SrcSetImage, SrcSetGeneratorOptions } from '@srcset/core' import type { SrcSetRule, - SrcSetImagePaths, - SrcSetBackendImage + SrcSetImagePaths } from './types.ts' import type { SrcSetEntrySelect, @@ -20,11 +16,6 @@ import type { PlaceholderOptions } from './placeholder.ts' * Common options of a srcset bundler integration. */ export interface SrcSetModuleOptions extends Omit { - /** - * Backend factory to produce image variants: the backend emits images - * with `emitImage` and yields their urls. Defaults to the sharp generator backend. - */ - backend?: SrcSetBackendFactory /** * Rules to generate image variants. An import query rule takes precedence. */ @@ -44,38 +35,6 @@ export interface SrcSetModuleOptions extends Omit | Iterable -} - -/** - * Factory of a backend: creates the backend instance from the integration - * options, the image emitter and the shared concurrency limit. - * @param options - Integration options. - * @param emitImage - Emits an image on the bundler side. - * @param limit - Concurrency limit of the integration. - * @returns Backend. - */ -export type SrcSetBackendFactory = ( - options: Omit, - emitImage: EmitImage, - limit?: LimitFunction -) => SrcSetBackend - /** * Emits an image on the bundler side. * @param image - Image variant. diff --git a/packages/bundler-utils/src/module.ts b/packages/bundler-utils/src/module.ts index 62cbd2d..47f5cf1 100644 --- a/packages/bundler-utils/src/module.ts +++ b/packages/bundler-utils/src/module.ts @@ -19,17 +19,13 @@ export const defaultResourceId: ResourceIdFormatter = (width, _requestedWidth, f const emptyUrlExpression = "''" /** - * Make a JS expression for the image variant url: plain urls are encoded, - * emitted images without a plain public url are prefixed with the - * public path expression of the integration, like `__webpack_public_path__`. - * @param url - Plain url string, or the emitted image paths. + * Make a JS expression for the image variant url: emitted images + * without a plain public url are prefixed with the public path + * expression of the integration, like `__webpack_public_path__`. + * @param url - Emitted image paths. * @returns JS expression string. */ -function toUrlExpression(url: string | SrcSetImagePaths) { - if (typeof url === 'string') { - return JSON.stringify(url) - } - +function toUrlExpression(url: SrcSetImagePaths) { if (url.publicPath !== null) { return JSON.stringify(url.publicPath) } diff --git a/packages/bundler-utils/src/module.types.ts b/packages/bundler-utils/src/module.types.ts index f6c0327..e966996 100644 --- a/packages/bundler-utils/src/module.types.ts +++ b/packages/bundler-utils/src/module.types.ts @@ -1,5 +1,5 @@ import type { ImageFormat } from '@srcset/core' -import type { SrcSetBackendImage } from './types.ts' +import type { SrcSetImagePaths } from './types.ts' /** * Resource id formatter function. @@ -11,19 +11,39 @@ import type { SrcSetBackendImage } from './types.ts' export type ResourceIdFormatter = (width: number, requestedWidth: number, format: ImageFormat) => string /** - * Generated image variant entry of the module: the backend image plus - * the resource id and the mime type. Serialized to the runtime + * Generated image variant entry of the module: the emitted image paths + * plus the variant properties. Serialized to the runtime * `SrcSetEntry` shape in the module code. */ -export interface SrcSetModuleEntry extends SrcSetBackendImage { +export interface SrcSetModuleEntry { /** * Resource id of the variant. */ id: string + /** + * Image format. + */ + format: ImageFormat /** * Image mime type. */ type: string + /** + * Image width in pixels. + */ + width: number + /** + * Image height in pixels. + */ + height: number + /** + * Width multiplier relative to the original image, if the variant was requested with one. + */ + originMultiplier: number | null + /** + * Paths of the image variant emitted with `EmitImage`. + */ + url: SrcSetImagePaths } /** diff --git a/packages/bundler-utils/src/types.ts b/packages/bundler-utils/src/types.ts index deb9e49..2c49981 100644 --- a/packages/bundler-utils/src/types.ts +++ b/packages/bundler-utils/src/types.ts @@ -1,5 +1,4 @@ import type { - ImageFormat, Matcher, GenerateOptions } from '@srcset/core' @@ -24,33 +23,6 @@ export interface SrcSetImagePaths { publicPathExpression?: string } -/** - * Image produced by a backend: the variant url and its properties. - */ -export interface SrcSetBackendImage { - /** - * Image format. - */ - format: ImageFormat - /** - * Image width in pixels. - */ - width: number - /** - * Image height in pixels. - */ - height: number - /** - * Width multiplier relative to the original image, if the variant was requested with one. - */ - originMultiplier: number | null - /** - * Image variant url: a plain url string, or the paths - * of the image emitted with `EmitImage`. - */ - url: string | SrcSetImagePaths -} - /** * Rule to generate image variants: match options plus generate options. */ diff --git a/packages/imgproxy/package.json b/packages/imgproxy/package.json index 968f8c4..f05641c 100644 --- a/packages/imgproxy/package.json +++ b/packages/imgproxy/package.json @@ -2,7 +2,7 @@ "name": "@srcset/imgproxy", "type": "module", "version": "1.0.0", - "description": "Adapter for imgproxy: srcset variant urls for content images and static assets.", + "description": "Adapter for imgproxy: srcset variant urls for content images, e.g. from an API or a CMS.", "author": { "name": "Dan Onoshko", "email": "danon0404@gmail.com", diff --git a/packages/loader/README.md b/packages/loader/README.md index 6359e21..865f849 100644 --- a/packages/loader/README.md +++ b/packages/loader/README.md @@ -34,7 +34,7 @@ - 🧩 Image imports are processed by default - no query markers required - 🌳 Tree-shakable image modules: unused exports are dropped from the bundle - 🌫 Blur-up placeholders inlined as data-urls -- 🌐 Pluggable backends: encode with [sharp](https://sharp.pixelplumbing.com/) or build [imgproxy](https://npmjs.com/package/@srcset/imgproxy) / [Cloudflare](https://npmjs.com/package/@srcset/cloudflare) urls +- πŸ–Ό Variants encoded with [sharp](https://sharp.pixelplumbing.com/): widths, modern formats and optimization ## Install diff --git a/packages/loader/src/index.ts b/packages/loader/src/index.ts index 9741116..5974c06 100644 --- a/packages/loader/src/index.ts +++ b/packages/loader/src/index.ts @@ -7,7 +7,6 @@ export type * from './types.ts' export type { SrcSetRule, SrcSetEntrySelect, - SrcSetModuleEntry, ResourceIdFormatter, PlaceholderOptions } from '@srcset/bundler-utils' diff --git a/packages/loader/src/loader.spec.ts b/packages/loader/src/loader.spec.ts index bd32eea..d27a1f6 100644 --- a/packages/loader/src/loader.spec.ts +++ b/packages/loader/src/loader.spec.ts @@ -195,82 +195,6 @@ describe('loader', () => { expect(exports.default).toMatch(/^\/assets\/image\./) }) - it('should use url variants from custom backend without emitting', async () => { - const dir = await createFixtureProject(defaultEntry) - const { - assets, - exports - } = await compile(createCompiler, dir, { - backend: () => ({ - * generate(_source: unknown, metadata: { - format: string - width: number - height: number - }) { - yield { - format: metadata.format, - width: metadata.width, - height: metadata.height, - originMultiplier: 1, - url: `https://proxy.test/w:${metadata.width}/f:${metadata.format}` - } - } - }) - }) - - expect(assets).toEqual([]) - expect(exports.default).toBe(`https://proxy.test/w:${imageWidth}/f:jpg`) - expect(exports.srcMap[`jpg${imageWidth}`]).toBe(exports.default) - }) - - it('should emit the original for a backend enforcing public path', async () => { - const dir = await createFixtureProject(defaultEntry) - const urlBackend = ( - _options: unknown, - emitImage: (image: unknown) => { - outputPath: string - publicPath: string | null - } - ) => ({ - * generate(source: { - path: string - contents: Buffer - }, metadata: { - format: string - width: number - height: number - }) { - const { publicPath: publicUrl } = emitImage({ - path: source.path, - contents: source.contents, - format: metadata.format, - width: metadata.width, - height: metadata.height, - postfix: '', - originMultiplier: 1 - }) - - yield { - format: metadata.format, - width: metadata.width, - height: metadata.height, - originMultiplier: 1, - url: `https://proxy.test/plain/${publicUrl ?? ''}` - } - } - }) - const { - assets, - exports - } = await compile(createCompiler, dir, { - publicPath: 'https://static.example.com/assets/', - backend: urlBackend - }) - - expect(assets).toEqual([expect.stringMatching(/^image\.[0-9a-f]{8}\.jpg$/) as string]) - expect(exports.default).toBe(`https://proxy.test/plain/https://static.example.com/assets/${assets[0]}`) - }) - describe('tree shaking', () => { it('should keep only src when only src is used', async () => { const dir = await createFixtureProject(usageEntry('src')) diff --git a/packages/vite-plugin/README.md b/packages/vite-plugin/README.md index c5b7133..3541cc7 100644 --- a/packages/vite-plugin/README.md +++ b/packages/vite-plugin/README.md @@ -34,7 +34,7 @@ Vite plugin for generating responsive images. - 🧩 Image imports are processed by default; native Vite queries like `?url` stay in the asset pipeline - 🌳 Tree-shakable image modules: unused exports are dropped from the bundle - 🌫 Blur-up placeholders inlined as data-urls -- 🌐 Pluggable backends: encode with [sharp](https://sharp.pixelplumbing.com/) or build [imgproxy](https://npmjs.com/package/@srcset/imgproxy) / [Cloudflare](https://npmjs.com/package/@srcset/cloudflare) urls +- πŸ–Ό Variants encoded with [sharp](https://sharp.pixelplumbing.com/): widths, modern formats and optimization ## Install diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index 9ac5a7f..ae4d646 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -3,7 +3,6 @@ export type * from './types.ts' export type { SrcSetRule, SrcSetEntrySelect, - SrcSetModuleEntry, ResourceIdFormatter, PlaceholderOptions } from '@srcset/bundler-utils' diff --git a/packages/vite-plugin/src/plugin.spec.ts b/packages/vite-plugin/src/plugin.spec.ts index a85751a..7cd3197 100644 --- a/packages/vite-plugin/src/plugin.spec.ts +++ b/packages/vite-plugin/src/plugin.spec.ts @@ -115,55 +115,6 @@ describe('vite-plugin', () => { expect(assets.length).toBe(4) }) - it('should emit the original for a backend enforcing public path', async () => { - const dir = await createFixtureProject(defaultEntry) - const urlBackend = ( - _options: unknown, - emitImage: (image: unknown) => { - outputPath: string - publicPath: string | null - } - ) => ({ - * generate(source: { - path: string - contents: Buffer - }, metadata: { - format: string - width: number - height: number - }) { - const { publicPath: publicUrl } = emitImage({ - path: source.path, - contents: source.contents, - format: metadata.format, - width: metadata.width, - height: metadata.height, - postfix: '', - originMultiplier: 1 - }) - - yield { - format: metadata.format, - width: metadata.width, - height: metadata.height, - originMultiplier: 1, - url: `https://proxy.test/plain${publicUrl ?? ''}` - } - } - }) - const { - assets, - exports - } = await buildFixture(dir, { - backend: urlBackend - }) - const [original] = assets - - expect(assets.length).toBe(1) - // The `__VITE_ASSET__` placeholder is replaced inside the built url string. - expect(exports.default).toBe(`https://proxy.test/plain/assets/${original.fileName}`) - }) - it('should leave public directory imports to Vite', async () => { const dir = await createFixtureProject(`import logo from '/logo.png' export default logo