polymech-astro/packages/imagetools_3/docs/classes.md
2025-08-27 18:44:10 +02:00

9.4 KiB

Imagetools Architecture Overview

This document outlines the architecture of the imagetools package, detailing the relationships between its different modules and the data flow during image processing.

Core Concepts

The system is divided into several layers:

  1. Astro Components: User-facing components for displaying images.
  2. API Layer: JavaScript APIs that components call to get image data and attributes.
  3. Vite Plugin: The core image processing and transformation engine that hooks into Vite's build process.
  4. Astro Integration: Glues the Vite plugin into the Astro lifecycle, handling configuration and final asset generation.

Data Flow Diagram

The following diagram illustrates how a request for an image flows through the system during a production build.

graph TD
    subgraph " "
        direction LR
        subgraph "User Facing"
            A["Astro Component<br/>(e.g., Img.astro)"]
        end

        subgraph "API Layer"
            B["renderImg.js"]
            C["getImage.js"]
        end
    end
    
    subgraph "Build Process"
        direction LR
        subgraph "Astro Integration"
            I["integration/index.js"]
            J["astro:config:setup hook"]
            K["astro:build:done hook"]
            L["saveAndCopyAsset.js"]
        end

        subgraph "Vite Plugin"
            D["plugin/index.js"]
            E["load hook<br/>(load.js)"]
            F["Image Transformation<br/>(imagetools.js/codecs.js)"]
            G["Persistent Cache<br/>(@polymech/cache)"]
            H["In-memory Store"]
        end
    end

    A -- "1. Renders" --> B;
    B -- "2. Calls" --> C;
    C -- "3. Triggers image import" --> D;
    
    I -- "Registers" --> D;
    J -- "Writes config for" --> D;

    D -- "Uses" --> E;
    E -- "4. Processes import" --> F;
    E -- "Uses" --> G;
    E -- "5. Populates" --> H;

    K -- "6. Reads from" --> H;
    K -- "7. Saves assets via" --> L;

Explanation of Components

  • Astro Component (Img.astro): The entry point. A developer uses this component in their .astro or .mdx files. It receives props like src, width, etc.

  • renderImg.js: An API function called by the Astro component. It sanitizes props and prepares them for image processing.

  • getImage.js: This function is responsible for orchestrating the generation of image sources. It triggers the actual image import that will be handled by Vite.

  • Vite Plugin (plugin/index.js): The core of the image processing pipeline. It's a Vite plugin that intercepts image imports.

  • load hook (load.js): The main hook in the Vite plugin. When an image is imported (e.g., import img from './my.jpg?w=300'), this hook:

    1. Parses the query parameters.
    2. Checks a persistent cache (@polymech/cache) for the transformed image.
    3. If not cached, it uses Sharp or other codecs for transformation (imagetools.js/codecs.js).
    4. Stores the processed image buffer in an in-memory store.
    5. Returns the path for the final asset.
  • Astro Integration (integration/index.js): Manages the lifecycle of the tool within Astro.

    • astro:config:setup: Injects the Vite plugin into the Astro configuration.
    • astro:build:done: This hook runs after the build is complete. It iterates over the in-memory store populated by the load hook and writes the processed images to the final output directory using saveAndCopyAsset.js.

Recommendations for Refactoring

The current implementation is functional but has room for simplification and improved robustness. Here are the key areas to focus on in the next refactoring slice:

  1. Unify Caching Strategy:

    • Problem: Caching logic is spread across multiple files (getImage.js, load.js, saveAndCopyAsset.js) with different strategies (in-memory map, persistent file-system cache). This makes it hard to reason about and debug.
    • Recommendation: Consolidate all caching into a single, cohesive module. This module should expose clear functions for getting, setting, and clearing cached items, and manage both in-memory and persistent layers. This will centralize logic and reduce redundancy.
  2. Refactor load.js Hook:

    • Problem: The load.js file is long and handles multiple responsibilities: config parsing, caching, image transformation, and path generation. This violates the Single Responsibility Principle and makes the code hard to maintain and test.
    • Recommendation: Break down load.js into smaller, pure functions. For example:
      • parseImageQuery(searchParams)
      • transformImage(buffer, options)
      • getAssetPath(source, options) This will improve readability, testability, and maintainability.
  3. Simplify Plugin-Integration Communication:

    • Problem: The communication between the Vite plugin's load hook and the Astro astro:build:done hook relies on a shared, mutable store object. This pattern can be fragile, hard to debug, and an indirect way of handling assets.
    • Recommendation: The shared store should be replaced with a more robust, standard mechanism for handling assets during the build process. Below are several options, with the first being the most idiomatic for the Vite ecosystem.

    This is the standard way to generate assets in Rollup/Vite plugins.

    • How it works: In the load hook, instead of adding the image buffer to a shared store, you call this.emitFile({ type: 'asset', source: imageBuffer, name: '...' }). Vite takes over, saves the file to the output directory with a hashed filename, and returns a reference. This eliminates the need for the store and the manual saveAndCopyAsset logic in astro:build:done.
    • Pros:
      • Idiomatic and follows Vite best practices.
      • Leverages Vite's content hashing, bundling, and asset-handling pipeline.
      • Removes the need for a shared global store and manual file I/O in the Astro hook.
      • More robust and less prone to race conditions or state management bugs.
    • Cons:
      • Requires a significant refactoring of the load hook and removal of logic from astro:build:done.

    Option 2: Use an Asset Manifest File

    Decouple the plugin from the integration hook using the file system.

    • How it works:
      1. The Vite plugin's load hook processes an image and prepares to save it.
      2. In the generateBundle or closeBundle hook of the Vite plugin, write a manifest file (e.g., imagetools-manifest.json) to the build output directory. This file would contain a mapping of the intended asset path to the source of the processed image buffer (e.g., a path in a temporary cache directory).
      3. The Astro astro:build:done hook reads this manifest file and performs the necessary file copying operations.
    • Pros:
      • Completely decouples the plugin from the Astro integration; they no longer need to share memory.
      • More resilient if the build process were to involve separate processes (though not currently the case).
    • Cons:
      • Adds file I/O overhead.
      • Introduces the complexity of creating, managing, and reading a manifest file.
      • Still requires manual file copying logic in the Astro hook, bypassing Vite's asset handling.

    Option 3: Encapsulate the Store

    If removing the store is not feasible immediately, its risks can be mitigated.

    • How it works: Instead of using a raw Map as the store, create a dedicated AssetStore class. This class would encapsulate the Map and expose clear, intentional methods like addAsset({ path, buffer }) and getAssets(). The global instance of this class would still be shared, but its direct manipulation would be prevented.
    • Pros:
      • Requires the least amount of refactoring.
      • Makes the existing pattern safer and the data flow more explicit.
      • Acts as a good intermediate step toward a larger refactor.
    • Cons:
      • Does not fix the underlying problem of using shared mutable state.
      • Still relies on a non-standard, in-memory communication channel.
  4. Streamline Configuration:

    • Problem: Configuration is passed between the Astro integration and the Vite plugin by writing a temporary astroViteConfigs.js file. This is a workaround that can be brittle.
    • Recommendation: Leverage the Vite plugin's configResolved hook. This hook receives the final, resolved Vite configuration and is the proper place to access build-related information without needing to write temporary files. The Astro integration can pass options directly to the Vite plugin during setup.

By tackling these areas, the codebase will become simpler, more robust, and easier to extend in the future.

Integration with Full-Page Caching Plugins

When using imagetools with other build-caching tools that operate at a page level (like @domain-expansion/astro), conflicts can arise that lead to missing images in the final build.

For a detailed explanation of this problem and the recommended solution using a persistent asset manifest, please see the dedicated documentation:

**Solving Caching Conflicts with a Persistent Asset Manifest**