Provider authoring

Custom providers

A provider translates a normalized image source and modifiers into a URL. Define the mapping once, register it under a name, then use it through the core API or any framework package.

Provider contract

Register an object or a setup function with a getImage() method. The runtime resolves aliases, validates the source, merges options, and calls the provider for each requested image candidate.

src
The normalized local path, remote URL, or allowed opaque identifier.
options
Provider defaults, config-level overrides, and merged image modifiers.
context.options
The resolved image configuration, including presets, aliases, screens, and source rules.
context.$img
A memoized image helper for delegation to another provider or preset.
Return value
Return url. Add format, getMeta, or isOptimized when the provider knows them.

Implement the provider

This example maps shared width, height, quality, format, and fit values to a query-string API. It also accepts one service-specific modifier, sharpen.

import {
  appendQuery,
  configureProvider,
  defineProvider,
  type ImageModifiers,
  type ProviderOptionsOf
} from '@desource/image';

interface AcmeModifiers extends ImageModifiers {
  sharpen?: number;
}

interface AcmeOptions {
  baseURL?: string;
  modifiers?: Partial<AcmeModifiers>;
}

const providerSetup = defineProvider<AcmeOptions>({
  validateDomains: true,
  getImage(src, { baseURL = 'https://images.acme.test/transform', modifiers }) {
    return {
      url: appendQuery(baseURL, {
        src,
        w: modifiers.width,
        h: modifiers.height,
        q: modifiers.quality,
        fm: modifiers.format === 'jpeg' ? 'jpg' : modifiers.format,
        fit: modifiers.fit,
        sharpen: modifiers.sharpen
      }),
      isOptimized: true
    };
  }
});

export type AcmeProviderOptions = Partial<ProviderOptionsOf<typeof providerSetup>>;

export function acmeProvider(options: AcmeProviderOptions = {}) {
  return configureProvider(providerSetup, options, 'acme');
}

defineProvider() memoizes setup. configureProvider() creates a named provider with factory defaults, so each application can point the same implementation at a different endpoint.

Register it

Save the implementation as acme-provider.ts, then register its factory in shared image configuration:

import type { ImageConfig } from '@desource/image';
import { acmeProvider } from './acme-provider';

export const imageConfig = {
  provider: 'acme',
  domains: ['assets.example.com'],
  providers: {
    acme: acmeProvider({
      baseURL: 'https://images.acme.test/transform',
      modifiers: { quality: 75 }
    })
  }
} satisfies ImageConfig;

Top-level provider options are merged in this order, with later values taking priority:

  1. Defaults declared by the provider setup.
  2. Defaults passed to the provider factory.
  3. Overrides from providerOptions.acme.

The runtime merges modifiers separately: provider defaults, config-level provider modifiers, resolved preset and per-image modifiers, then per-image width, height, quality, and format props.

Use it

npm install @desource/image

Use the same imageConfig with the Core API. Standard props and custom modifiers reach getImage() through its merged modifiers object.

import { createImage } from '@desource/image';
import { imageConfig } from './image.config';

const image = createImage(imageConfig);

export const heroURL = image('https://assets.example.com/photo.jpg', {
  width: 960,
  height: 640,
  format: 'webp',
  sharpen: 2
});

Choose source rules deliberately

  • Set validateDomains: true when the provider fetches remote sources. Applications must then allow each source through domains or remotePatterns.
  • Set acceptsOpaqueSource: true through the fourth configureProvider() argument only when values such as asset_123 are valid service identifiers.
  • Leave supportsAlias unset unless the provider needs to interpret alias prefixes itself. The runtime otherwise resolves aliases before calling getImage().
  • When delegating through context.$img, select another provider or a preset backed by another provider. Calling the active provider again would recurse.

Keep signing secrets on the server

Framework components can execute provider code in the browser. Generate signed URLs in server-only code, pass the finished URL to the client, and render it with provider="none".

Test the URL contract

A black-box test through createImage() covers provider registration, option merging, source validation, and URL generation together.

import { describe, expect, it } from 'vitest';
import { createImage } from '@desource/image';
import { acmeProvider } from './acme-provider';

describe('acmeProvider', () => {
  it('maps standard and custom modifiers', () => {
    const image = createImage({
      provider: 'acme',
      domains: ['assets.example.com'],
      providers: {
        acme: acmeProvider({ baseURL: 'https://images.acme.test/transform' })
      }
    });

    expect(
      image('https://assets.example.com/photo.jpg', {
        width: 640,
        format: 'webp',
        sharpen: 2
      })
    ).toBe(
      'https://images.acme.test/transform' +
        '?src=https%3A%2F%2Fassets.example.com%2Fphoto.jpg&w=640&fm=webp&sharpen=2'
    );
  });
});

Also cover requests with no modifiers, local and remote sources, mapped enum values, and invalid inputs.

Authoring utilities

Query parameters

appendQuery, mappedModifiers, and createMappedQueryProvider.

Path operations

createOperationsGenerator, pathOperations, and joinURLParts.

Source handling

providerBaseURL, sourceWithBase, and sourcePath.

← Back to all providers