155 lines
9.4 KiB
Markdown
155 lines
9.4 KiB
Markdown
# 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.
|
|
|
|
```mermaid
|
|
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.
|
|
|
|
#### Option 1: Use `this.emitFile` (Recommended)
|
|
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](./caching.md)**
|