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.
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:
- Defaults declared by the provider setup.
- Defaults passed to the provider factory.
- 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: truewhen the provider fetches remote sources. Applications must then allow each source throughdomainsorremotePatterns. - Set
acceptsOpaqueSource: truethrough the fourthconfigureProvider()argument only when values such asasset_123are valid service identifiers. - Leave
supportsAliasunset unless the provider needs to interpret alias prefixes itself. The runtime otherwise resolves aliases before callinggetImage(). - 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.