Skip to content
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand All @@ -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] |
Expand Down
7 changes: 3 additions & 4 deletions packages/bundler-utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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/).
2 changes: 1 addition & 1 deletion packages/bundler-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
60 changes: 2 additions & 58 deletions packages/bundler-utils/src/generate.spec.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand Down Expand Up @@ -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, {}, {
Expand All @@ -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";')
})
})
})
})
57 changes: 16 additions & 41 deletions packages/bundler-utils/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
import type { QueryOptions } from './query.ts'
import type {
SrcSetModuleOptions,
SrcSetBackendFactory,
EmitImage
} from './generate.types.ts'
import {
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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)
})
}

Expand Down
43 changes: 1 addition & 42 deletions packages/bundler-utils/src/generate.types.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -20,11 +16,6 @@ import type { PlaceholderOptions } from './placeholder.ts'
* Common options of a srcset bundler integration.
*/
export interface SrcSetModuleOptions extends Omit<SrcSetGeneratorOptions, 'limit'> {
/**
* 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.
*/
Expand All @@ -44,38 +35,6 @@ export interface SrcSetModuleOptions extends Omit<SrcSetGeneratorOptions, 'limit
select?: SrcSetEntrySelect
}

/**
* Backend of a srcset bundler integration: produces image variants.
*/
export interface SrcSetBackend {
/**
* Produce images for the rule.
* @param source - Image file.
* @param metadata - Image metadata.
* @param rule - Rule to generate images.
* @returns Images.
*/
generate(
source: ImageSource,
metadata: ImageMetadata,
rule: SrcSetRule
): AsyncIterable<SrcSetBackendImage> | Iterable<SrcSetBackendImage>
}

/**
* 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<SrcSetGeneratorOptions, 'limit'>,
emitImage: EmitImage,
limit?: LimitFunction
) => SrcSetBackend

/**
* Emits an image on the bundler side.
* @param image - Image variant.
Expand Down
14 changes: 5 additions & 9 deletions packages/bundler-utils/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
28 changes: 24 additions & 4 deletions packages/bundler-utils/src/module.types.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
}

/**
Expand Down
Loading