# Contributing Guide This guide will help you add new image CDN providers to the library. It covers implementation details, utility functions, and best practices. ## Overview Each provider consists of: 1. A TypeScript file containing the implementation (`src/providers/[provider].ts`) and types for provider-specific operations and options. 2. A test file (`src/providers/[provider].test.ts`) 3. Example URLs in `src/demo/examples.json` 4. Detection domains or paths in `data` if appropriate 5. Adding to the types and exports in: - `src/providers/types.ts` - `src/types.ts` - `src/extract.ts` - `src/transform.ts` - `src/async.ts` - `deno.jsonc` ## Core Concepts and Utilities ### URL Manipulation The library provides several utilities for URL handling: ```typescript // Convert strings or URLs to URL objects (handles relative URLs) const url = toUrl("https://example.com/image.jpg"); // Convert back to string, preserving relativeness const urlString = toCanonicalUrlString(url); // Path manipulation const cleanPath = stripLeadingSlash("/path/to/image.jpg"); const formattedPath = addTrailingSlash("path/to/image"); ``` ### Operations Handlers The most important utility is `createOperationsHandlers`, which creates standardized parser and generator functions: ```typescript const { operationsGenerator, operationsParser } = createOperationsHandlers< ExampleCdnOperations >({ // Map standard operation names to provider-specific names keyMap: { width: "w", height: "h", quality: "q", format: "fmt", }, // Set default values defaults: { quality: 80, format: "auto", }, // Normalize format names formatMap: { jpg: "jpeg", }, // Define parameter formatting kvSeparator: "=", // key=value paramSeparator: "&", // param1¶m2 }); ``` ## Step-by-Step Implementation Let's create a complete example provider "example-cdn": ### 1. Define Operations Interface ```typescript // example-cdn.ts import type { Operations, URLExtractor, URLGenerator, URLTransformer, } from "../types.ts"; import { createExtractAndGenerate, createOperationsHandlers, toCanonicalUrlString, toUrl, } from "../utils.ts"; // Only add NEW operations specific to your provider export interface ExampleCdnOperations extends Operations { // Provider-specific operations specialCrop?: "smart" | "center"; blur?: number; // DON'T include these - they're in base Operations // width?: number; ❌ // height?: number; ❌ // quality?: number; ❌ // format?: string; ❌ } // Optional provider-specific options export interface ExampleCdnOptions { baseUrl?: string; } ``` ### 2. Configure Operations Handlers ```typescript // Different parameter formatting styles: // Query parameters: ?width=100&height=200 const queryStyle = createOperationsHandlers({ keyMap: { width: "w", height: "h", quality: "q", format: "fmt", }, defaults: { quality: 80, }, kvSeparator: "=", paramSeparator: "&", }); // Path segments: /w_100/h_200/q_80 const pathStyle = createOperationsHandlers({ keyMap: { width: "w", height: "h", quality: "q", format: "fmt", }, defaults: { quality: 80, }, kvSeparator: "_", paramSeparator: "/", }); // You can also disable parameters: const noHeightStyle = createOperationsHandlers({ keyMap: { width: "w", height: false, // Height parameter will be removed quality: "q", }, }); ``` ### 3. Implement Core Functions ```typescript // Extract operations from existing URL export const extract: URLExtractor<"example-cdn"> = (url) => { const parsedUrl = toUrl(url); const operations = operationsParser(parsedUrl); parsedUrl.search = ""; return { src: toCanonicalUrlString(parsedUrl), operations, options: { baseUrl: parsedUrl.origin, }, }; }; // Generate new URL with operations export const generate: URLGenerator<"example-cdn"> = ( src, operations, options = {}, ) => { const url = toUrl(src, options.baseUrl); url.search = operationsGenerator(operations); return toCanonicalUrlString(url); }; // Transform existing URL with new operations export const transform: URLTransformer<"example-cdn"> = createExtractAndGenerate(extract, generate); ``` ### 4. Add Comprehensive Tests ```typescript // example-cdn.test.ts import { assertEquals } from "jsr:@std/assert"; import { extract, generate, transform } from "./example-cdn.ts"; import { assertEqualIgnoringQueryOrder } from "../test-utils.ts"; const img = "https://example-cdn.com/image.jpg"; Deno.test("Example CDN", async (t) => { // Test extraction await t.step("should extract operations from URL", () => { const url = `${img}?w=300&h=200&q=80&fmt=webp&specialCrop=smart`; const result = extract(url); assertEquals(result, { src: img, operations: { width: 300, height: 200, quality: 80, format: "webp", specialCrop: "smart", }, options: { baseUrl: "https://example-cdn.com", }, }); }); // Test URL generation await t.step("should generate URL with operations", () => { const result = generate(img, { width: 400, height: 300, quality: 90, specialCrop: "center", }); assertEqualIgnoringQueryOrder( result, `${img}?w=400&h=300&q=90&specialCrop=center`, ); }); // Test transformation await t.step("should transform existing URL", () => { const url = `${img}?w=300&h=200`; const result = transform(url, { width: 500, blur: 5, }); assertEqualIgnoringQueryOrder( result, `${img}?w=500&h=200&blur=5`, ); }); // Test error cases await t.step("should handle invalid URLs", () => { const result = extract("invalid-url"); assertEquals(result, null); }); // Test relative URLs await t.step("should handle relative URLs", () => { const result = generate("/image.jpg", { width: 300 }); assertEqualIgnoringQueryOrder( result, "/image.jpg?w=300", ); }); }); ``` ### 5. Update Types and Add Examples ```typescript // types.ts export interface ProviderOperations { "example-cdn": ExampleCdnOperations; // ... } export interface ProviderOptions { "example-cdn": ExampleCdnOptions; // ... } ``` ```json // examples.json { "example-cdn": [ "Example CDN", "https://example-cdn.com/demo-image.jpg" ] } ``` ## Parameter Handling Patterns ### Query Parameters vs Path Segments Providers use different URL patterns for operations: ```typescript // Standard query parameters // https://example.com/image.jpg?width=100&height=200 { kvSeparator: "=", paramSeparator: "&" } // Path segments // https://example.com/image/w_100/h_200/image.jpg { kvSeparator: "_", paramSeparator: "/" } // Custom separators // https://example.com/image:w=100,h=200/image.jpg { kvSeparator: "=", paramSeparator: "," } ``` ## Best Practices ### When to Use Utilities Use the provided utilities when: - You need standard parameter mapping - Your provider follows common URL patterns - You want automatic parameter normalization ### When to Create Custom Solutions Create custom handlers when: - Your provider has unique URL structures - Parameters have complex interdependencies - You need special encoding or encryption - The provider requires custom protocols ### Error Handling Always handle: - Invalid URLs - Missing parameters - Malformed parameters - Unsupported formats - Edge cases ### Type Safety - Define clear interfaces extending `Operations` - Only add provider-specific operations - Use proper TypeScript generics - Document supported operations ## Testing Requirements 1. Basic Operations - Width/height resizing - Format conversion - Quality settings - Provider-specific features 2. URL Handling - Absolute URLs - Relative URLs - URL with existing parameters - Invalid URLs 3. Parameter Edge Cases - Missing parameters - Invalid values - Parameter combinations - Default values 4. Common Scenarios - Standard transformations - Format conversion - Quality adjustment - Size constraints ## Final Checklist Before submitting: - [ ] Implementation complete with proper types - [ ] Comprehensive tests covering all features - [ ] Types updated in all files listed above - [ ] Example added to examples.json - [ ] Detection domains or paths added if needed - [ ] All tests passing, including unit tests and E2E tests ## Development Environment ### Deno Setup This project uses Deno for development. If you haven't already, install Deno from https://deno.com. Basic commands: ```bash # Run tests deno test # Run tests with watch mode deno test --watch # Type checking deno check # Format code deno fmt ``` ### Running Tests Tests are written using Deno's built-in test framework. Run them from the project root: ```bash # Run all tests deno test # Run tests for a specific provider deno test src/providers/example-cdn.test.ts # Run E2E tests. These need network access. deno test --allow-net e2e.test.ts ``` ## Image Defaults When implementing a provider, follow these default behaviors for consistency across CDNs. If the provider does not support a feature then it can be omitted, but these are the defaults to aim for so that users have a consistent experience. ### Format Handling - Enable auto format detection/content negotiation when supported - When supported, priority order for formats should be. For services that generate images locally, it is ok to prefer WebP over AVIF for performance reasons. 1. AVIF 2. WebP 3. Original format ### Image Fitting Default to `fit=cover` behavior (equivalent to CSS `object-fit: cover`). This means: - Image should fill requested dimensions - Maintain aspect ratio - Crop if necessary - Avoid distortion ### Size Handling - _Never_ upscale images beyond their original dimensions - Return largest available size when requested size is too large - Maintain requested aspect ratio even when size is constrained ### Local Development Server The project includes a playground application in the `demo` directory for testing providers visually: 1. Start the development server: ```bash cd demo pnpm install pnpm dev ``` 2. Open http://localhost:1234 The playground is crucial for testing as it: - Provides real-world testing with actual CDN endpoints - Allows visual verification of image operations - Tests responsive image behavior - Verifies URL generation patterns When adding a new provider: 1. Add an example URL to `demo/src/examples.json` - Ideally use a public sample image from the CDN's documentation - If unavailable, use any publicly-accessible image on that CDN - **Do not skip this** - no provider can be added without an example URL, because otherwise it cannot be tested 2. Test comprehensively: - Verify resizing behavior - Check that defaults are properly applied - Test format conversion - Verify responsive behavior - Ensure upscaling limits work - Check aspect ratio handling ### End-to-End Testing The E2E tests in `e2e.test.ts` verify that providers work with real CDN endpoints. They use the images from `examples.json` to test real operations: ```bash deno test --allow-net e2e.test.ts ``` ## Getting Help If you need help: 1. Review existing provider implementations 2. Check test files for patterns 3. Open an issue for discussion 4. Ask questions in pull requests Remember that clear, well-tested code is more important than clever solutions. Take time to write comprehensive tests and documentation.