@ecopages/image-processor 0.2.0-alpha.5 → 0.2.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,16 +8,15 @@ All notable changes to `@ecopages/image-processor` are documented here.
8
8
 
9
9
  ### Features
10
10
 
11
- - **`image-plugins.ts`** New file extracting image processing plugin logic for reuse across build adapters (`image-plugins.ts`).
12
- - Bun-specific plugin helpers moved to `bun-plugins.ts`, separating runtime concerns from the image processing core.
11
+ - Added `image-plugins.ts` with shared image plugin creation so the processor can target multiple build adapters.
12
+ - Added `bun-plugins.ts` with Bun-specific build adapter helpers aligned with the shared image plugin layer.
13
13
 
14
- ### Refactoring
14
+ ### Bug Fixes
15
15
 
16
- - `plugin.ts` updated to use the new `image-plugins.ts` abstraction.
17
- - `image-processor.ts` minor updates.
18
- - README updated with usage clarification.
16
+ - Switched public internal imports to explicit relative ESM specifiers so Node thin-host builds can externalize the package without `ERR_MODULE_NOT_FOUND`.
17
+ - Inlined the generated `ecopages:images` declaration source so bundled runtime bootstrap no longer depends on a sibling `types` module at runtime.
18
+ - Re-emitted generated image outputs after static export cleanup so routes that reference `/images/...` keep their optimized files inside `dist`.
19
19
 
20
20
  ### Tests
21
21
 
22
- - `image-renderer.test.ts` expanded (68 lines added).
23
- - `image-processor.test.ts` and `image-utils.test.ts` updated.
22
+ - Added image processor and renderer coverage for the current build pipeline.
package/README.md CHANGED
@@ -1,35 +1,38 @@
1
1
  # @ecopages/image-processor
2
2
 
3
- A powerful and flexible image processing library designed to optimize and manage responsive images in ecopages applications.
3
+ Image processing pipeline for responsive, optimized images in Ecopages.
4
+
5
+ It provides automatic image processing (e.g. converting and compressing to WebP) and virtual module integration, allowing you to import your optimized images safely and directly via `ecopages:images`.
4
6
 
5
7
  ## Features
6
8
 
7
- - **Automatic Image Optimization**: Converts and compresses images to modern formats
8
- - **Responsive Image Generation**: Creates multiple image variants for different screen sizes
9
- - **Virtual Module Integration**: Direct import of optimized images through `ecopages:images`
10
- - **TypeScript Support**: Full type definitions and auto-generated types
11
- - **Multiple Layout Options**: Supports fixed, constrained, and full-width layouts
12
- - **Ecopages Integration**: Seamlessly integrated with the ecopages framework
9
+ - **Automatic Image Optimization**: Converts and compresses images to modern formats at build time.
10
+ - **Responsive Image Generation**: Creates multiple variants for different screen sizes.
11
+ - **Virtual Module Integration**: Type-safe imports through `ecopages:images`.
12
+ - **Ecopages Components**: Ready-to-use HTML (`EcoImage`) and React (`EcoImage`) components.
13
+ - **Multiple Layout Options**: Fixed, constrained, and full-width layouts built-in.
13
14
 
14
15
  ## Installation
15
16
 
16
17
  ```bash
17
- npm install @ecopages/image-processor
18
+ bunx jsr add @ecopages/image-processor
18
19
  ```
19
20
 
20
21
  ## Configuration
21
22
 
23
+ Import and register the processor in your `eco.config.ts`:
24
+
22
25
  ```typescript
23
26
  import path from 'node:path';
24
- import { ConfigBuilder } from '@ecopages/core';
27
+ import { ConfigBuilder } from '@ecopages/core/config-builder';
25
28
  import { ImageProcessorPlugin } from '@ecopages/image-processor';
26
29
 
27
30
  const imageProcessor = new ImageProcessorPlugin({
28
31
  name: 'ecopages-image-processor',
29
32
  type: 'image',
30
33
  options: {
31
- sourceDir: path.resolve(import.meta.dir, 'src/images'),
32
- outputDir: path.resolve(import.meta.dir, '.eco/images'),
34
+ sourceDir: path.resolve(import.meta.dirname, 'src/images'),
35
+ outputDir: path.resolve(import.meta.dirname, 'dist/images'),
33
36
  publicPath: '/images',
34
37
  acceptedFormats: ['jpg', 'jpeg', 'png', 'webp'],
35
38
  quality: 80,
@@ -44,175 +47,62 @@ const imageProcessor = new ImageProcessorPlugin({
44
47
  });
45
48
 
46
49
  export default await new ConfigBuilder()
47
- .setRootDir(import.meta.dir)
50
+ .setRootDir(import.meta.dirname)
48
51
  .setBaseUrl(import.meta.env.ECOPAGES_BASE_URL)
49
52
  .setProcessors([imageProcessor])
50
53
  .build();
51
54
  ```
52
55
 
53
- ### Configuration Options
54
-
55
- #### ImageProcessorConfig
56
-
57
- | Option | Type | Default | Description |
58
- | ----------------- | --------------------------------------- | ----------------------------- | ------------------------------------- |
59
- | `sourceDir` | `string` | `'/src/public/assets/images'` | Source directory for images |
60
- | `outputDir` | `string` | `'/dist/assets/optimized'` | Output directory for processed images |
61
- | `publicPath` | `string` | `'/assets/optimized'` | Public URL path for images |
62
- | `sizes` | `Array<{width: number, label: string}>` | `[]` | Image variants configuration |
63
- | `quality` | `number` | `80` | Output image quality (0-100) |
64
- | `format` | `'webp' \| 'jpeg' \| 'png' \| 'avif'` | `'webp'` | Output image format |
65
- | `acceptedFormats` | `string[]` | `['jpg','jpeg','png','webp']` | Accepted input formats |
66
-
67
56
  ## Usage
68
57
 
69
58
  ### Virtual Module System
70
59
 
71
- The `ecopages:images` virtual module provides a unified, type-safe way to handle images across your project:
60
+ The `ecopages:images` virtual module provides a type-safe way to import processed images:
72
61
 
73
62
  ```typescript
74
- // All images from your source directory are available as named exports
75
- import { heroImage, profilePicture, blogThumbnail } from 'ecopages:images';
76
-
77
- // Names are automatically converted to camelCase
78
- // example:
79
- // src/images/hero-image.jpg -> heroImage
80
- // src/images/profile_picture.png -> profilePicture
63
+ // Imports from your source directory are resolved automatically and camelCased
64
+ import { heroImage, profilePicture } from 'ecopages:images';
81
65
  ```
82
66
 
83
- No manual `dependencies.modules` declaration is required for `ecopages:images`; imports are automatically detected and included in the client bundle.
84
-
85
- #### Benefits:
86
-
87
- - **TypeScript Integration**: Full autocompletion support for image names
88
- - **Automatic Processing**: Images are processed at build time
89
- - **Tree Shaking**: Only imported images and their required metadata are included in the final bundle
90
- - **Type Safety**: Prevents imports of non-existent images
91
- - **Unified API**: Consistent way to handle images across your project
67
+ > [!TIP]
68
+ > **No manual dependencies required.**
69
+ > Ecopages automatically detects these virtual module imports and processes them during the build, enabling effective tree-shaking for only the required images.
92
70
 
93
- ### Importing Images
71
+ ### Components
94
72
 
95
- Images are available through the virtual module `ecopages:images`:
96
-
97
- ```typescript
98
- import { myImage } from 'ecopages:images';
99
-
100
- // myImage contains:
101
- // {
102
- // attributes: {
103
- // src: string,
104
- // width: number, // original image width
105
- // height: number, // original image height
106
- // sizes: string,
107
- // srcset: string
108
- // },
109
- // variants: Array<{ width, height, src, label }>
110
- // }
111
- ```
73
+ The plugin provides ready-to-use components for HTML (`@kitajs/html`) and React:
112
74
 
113
- ### HTML Component
75
+ **HTML Component:**
114
76
 
115
77
  ```typescript
116
78
  import { EcoImage } from '@ecopages/image-processor/component/html';
117
79
 
118
- // Basic usage
119
80
  EcoImage({
120
- ...myImage,
121
- width: 800,
122
- height: 600,
123
- alt: 'My image',
124
- });
125
-
126
- // Advanced usage
127
- EcoImage({
128
- ...myImage,
129
- alt: 'My image',
81
+ ...heroImage,
130
82
  layout: 'constrained',
83
+ alt: 'Hero banner',
131
84
  priority: true,
132
- aspectRatio: '16/9',
133
- staticVariant: 'xl',
134
85
  });
135
86
  ```
136
87
 
137
- ### React Component
88
+ **React Component:**
138
89
 
139
90
  ```jsx
140
- import { EcoImage } from "@ecopages/image-processor/component/react";
141
-
142
- // Basic usage
143
- <EcoImage
144
- {...myImage}
145
- alt="My image"
146
- />
147
-
148
- // Advanced usage
149
- <EcoImage
150
- {...myImage}
151
- alt="My image"
152
- layout="constrained"
153
- priority
154
- aspectRatio="16/9"
155
- staticVariant="xl"
156
- />
157
- ```
158
-
159
- ### Component Props
160
-
161
- The component accepts all standard HTML/React image attributes (`src`, `alt`, `data-*`, `crossOrigin`, etc.) in addition to the following specific props:
162
-
163
- | Prop | Type | Default | Description |
164
- | --------------- | ------------------------------------------ | ------------------- | -------------------------------------------- |
165
- | `width` | `number` | From image metadata | Original width, can be overridden if needed |
166
- | `height` | `number` | From image metadata | Original height, can be overridden if needed |
167
- | `priority` | `boolean` | `false` | Prioritize loading |
168
- | `layout` | `'fixed' \| 'constrained' \| 'full-width'` | `'constrained'` | Layout behavior |
169
- | `staticVariant` | `string` | - | Force specific size variant |
170
- | `aspectRatio` | `string` | From width/height | Override the natural aspect ratio |
171
- | `unstyled` | `boolean` | `false` | Disable default styling |
172
-
173
- Note: Images imported through `ecopages:images` automatically include their width and height metadata, preventing layout shifts by default. These values can be overridden when needed, for example when using a different aspect ratio or specific layout requirements.
91
+ import { EcoImage } from '@ecopages/image-processor/component/react';
174
92
 
175
- ## Layout Modes
176
-
177
- ### Fixed Layout
178
-
179
- ```typescript
180
- EcoImage({
181
- ...myImage,
182
- layout: 'fixed',
183
- width: 400,
184
- height: 300,
185
- alt: 'Fixed image',
186
- });
93
+ <EcoImage {...heroImage} alt="Hero banner" layout="constrained" priority />;
187
94
  ```
188
95
 
189
- ### Constrained Layout
190
-
191
- ```typescript
192
- EcoImage({
193
- ...myImage,
194
- layout: 'constrained',
195
- width: 800,
196
- alt: 'Constrained image',
197
- });
198
- ```
199
-
200
- ### Full-Width Layout
201
-
202
- ```typescript
203
- EcoImage({
204
- ...myImage,
205
- layout: 'full-width',
206
- alt: 'Full-width image',
207
- });
208
- ```
209
-
210
- ## Best Practices
96
+ ### Component Props
211
97
 
212
- 1. Always provide `alt` text for accessibility
213
- 2. Use `priority` for above-the-fold images
214
- 3. Always specify both `width` and `height` to prevent layout shifts
215
- 4. Use `aspectRatio` only when you need to force a different aspect ratio than width/height
216
- 5. Choose appropriate `layout` modes based on your needs
217
- 6. Utilize the virtual module for type-safe image imports
218
- 7. Configure size variants that match your breakpoints
98
+ The components accept standard HTML/React attributes plus these specifics:
99
+
100
+ | Prop | Type | Default |
101
+ | :-------------- | :----------------------------------------- | :-------------- |
102
+ | `layout` | `'fixed' \| 'constrained' \| 'full-width'` | `'constrained'` |
103
+ | `priority` | `boolean` | `false` |
104
+ | `width` | `number` | From metadata |
105
+ | `height` | `number` | From metadata |
106
+ | `aspectRatio` | `string` | Natural ratio |
107
+ | `staticVariant` | `string` | - |
108
+ | `unstyled` | `boolean` | `false` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/image-processor",
3
- "version": "0.2.0-alpha.5",
3
+ "version": "0.2.0-alpha.7",
4
4
  "description": "Image processor, transform and optimize images for web",
5
5
  "keywords": [
6
6
  "image",
@@ -15,10 +15,10 @@
15
15
  "directory": "packages/processors/image-processor"
16
16
  },
17
17
  "peerDependencies": {
18
- "@ecopages/core": "0.2.0-alpha.5"
18
+ "@ecopages/core": "0.2.0-alpha.7"
19
19
  },
20
20
  "dependencies": {
21
- "@ecopages/file-system": "0.2.0-alpha.5",
21
+ "@ecopages/file-system": "0.2.0-alpha.7",
22
22
  "@ecopages/logger": "latest",
23
23
  "react": "^19",
24
24
  "react-dom": "^19",
@@ -3,7 +3,7 @@
3
3
  * @module @ecopages/image-processor/bun-plugins
4
4
  */
5
5
  import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
6
- import type { ImageMap } from './plugin';
6
+ import type { ImageMap } from './plugin.js';
7
7
  /**
8
8
  * This function creates a plugin for bundling the image specifications.
9
9
  * https://bun.sh/docs/runtime/plugins#virtual-modules
@@ -1,4 +1,4 @@
1
- import { anyCaseToCamelCase } from "./utils";
1
+ import { anyCaseToCamelCase } from "./utils.js";
2
2
  function createPluginResult(exports) {
3
3
  return {
4
4
  contents: `${Object.entries(exports).map(([key, value]) => `export const ${anyCaseToCamelCase(key)} = ${JSON.stringify(value)};`).join("\n")}`,
@@ -4,8 +4,8 @@
4
4
  */
5
5
 
6
6
  import type { EcoBuildOnLoadResult, EcoBuildPlugin } from '@ecopages/core/build/build-types';
7
- import type { ImageMap } from './plugin';
8
- import { anyCaseToCamelCase } from './utils';
7
+ import type { ImageMap } from './plugin.ts';
8
+ import { anyCaseToCamelCase } from './utils.ts';
9
9
 
10
10
  /**
11
11
  * This function creates the plugin result for the image specifications.
@@ -2,7 +2,7 @@
2
2
  * Image component that renders the image as a string.
3
3
  * @module @ecopages/image-processor/component/html
4
4
  */
5
- import { type EcoImageProps } from '../image-renderer';
5
+ import { type EcoImageProps } from '../image-renderer.js';
6
6
  /**
7
7
  * EcoImage
8
8
  * This component generates the image element based on the provided props as a string
@@ -1,4 +1,4 @@
1
- import { renderer } from "../image-renderer";
1
+ import { renderer } from "../image-renderer.js";
2
2
  const EcoImage = (props) => {
3
3
  return renderer.renderToString(props);
4
4
  };
@@ -3,7 +3,7 @@
3
3
  * @module @ecopages/image-processor/component/html
4
4
  */
5
5
 
6
- import { type EcoImageProps, renderer } from '../image-renderer';
6
+ import { type EcoImageProps, renderer } from '../image-renderer.ts';
7
7
 
8
8
  /**
9
9
  * EcoImage
@@ -3,7 +3,7 @@
3
3
  * @module @ecopages/image-processor/component/react
4
4
  */
5
5
  import { type JSX } from 'react';
6
- import { type EcoImageProps } from '../image-renderer';
6
+ import { type EcoImageProps } from '../image-renderer.js';
7
7
  /**
8
8
  * EcoImage
9
9
  * This component generates the image element based on the provided props as JSX
@@ -1,5 +1,5 @@
1
1
  import { createElement } from "react";
2
- import { renderer } from "../image-renderer";
2
+ import { renderer } from "../image-renderer.js";
3
3
  const EcoImage = (props) => {
4
4
  return createElement("img", {
5
5
  ...renderer.generateAttributesJsx(props),
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { createElement, type JSX } from 'react';
7
- import { type EcoImageProps, renderer } from '../image-renderer';
7
+ import { type EcoImageProps, renderer } from '../image-renderer.ts';
8
8
 
9
9
  /**
10
10
  * EcoImage
@@ -1,4 +1,4 @@
1
- import type { ImageLayout } from './image-renderer';
1
+ import type { ImageLayout } from './image-renderer.js';
2
2
  /**
3
3
  * Default image layout
4
4
  * @constant "constrained"
package/src/constants.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ImageLayout } from './image-renderer';
1
+ import type { ImageLayout } from './image-renderer.ts';
2
2
 
3
3
  /**
4
4
  * Default image layout
@@ -3,7 +3,7 @@
3
3
  * @module @ecopages/image-processor/image-plugins
4
4
  */
5
5
  import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
6
- import type { ImageMap } from './plugin';
6
+ import type { ImageMap } from './plugin.js';
7
7
  /**
8
8
  * This function creates a plugin for bundling the image specifications.
9
9
  * https://bun.sh/docs/runtime/plugins#virtual-modules
@@ -1,4 +1,4 @@
1
- import { anyCaseToCamelCase } from "./utils";
1
+ import { anyCaseToCamelCase } from "./utils.js";
2
2
  function createPluginResult(exports) {
3
3
  return {
4
4
  contents: `${Object.entries(exports).map(([key, value]) => `export const ${anyCaseToCamelCase(key)} = ${JSON.stringify(value)};`).join("\n")}`,
@@ -4,8 +4,8 @@
4
4
  */
5
5
 
6
6
  import type { EcoBuildOnLoadResult, EcoBuildPlugin } from '@ecopages/core/build/build-types';
7
- import type { ImageMap } from './plugin';
8
- import { anyCaseToCamelCase } from './utils';
7
+ import type { ImageMap } from './plugin.ts';
8
+ import { anyCaseToCamelCase } from './utils.ts';
9
9
 
10
10
  /**
11
11
  * This function creates the plugin result for the image specifications.
@@ -1,5 +1,5 @@
1
- import type { ImageMap, ImageProcessorConfig } from './plugin';
2
- import type { ImageSpecifications } from './types';
1
+ import type { ImageMap, ImageProcessorConfig } from './plugin.js';
2
+ import type { ImageSpecifications } from './types.js';
3
3
  /**
4
4
  * ImageProcessor
5
5
  * This is the core class for processing images.
@@ -3,7 +3,7 @@ import { deepMerge } from "@ecopages/core/utils/deep-merge";
3
3
  import { fileSystem } from "@ecopages/file-system";
4
4
  import { Logger } from "@ecopages/logger";
5
5
  import sharp from "sharp";
6
- import { ImageUtils } from "./image-utils";
6
+ import { ImageUtils } from "./image-utils.js";
7
7
  const appLogger = new Logger("[@ecopages/image-processor]", {
8
8
  debug: process.env.ECOPAGES_LOGGER_DEBUG === "true"
9
9
  });
@@ -3,9 +3,9 @@ import { deepMerge } from '@ecopages/core/utils/deep-merge';
3
3
  import { fileSystem } from '@ecopages/file-system';
4
4
  import { Logger } from '@ecopages/logger';
5
5
  import sharp from 'sharp';
6
- import { ImageUtils } from './image-utils';
7
- import type { ImageMap, ImageProcessorConfig } from './plugin';
8
- import type { ImageAttributes, ImageSpecifications, ImageVariant } from './types';
6
+ import { ImageUtils } from './image-utils.ts';
7
+ import type { ImageMap, ImageProcessorConfig } from './plugin.ts';
8
+ import type { ImageAttributes, ImageSpecifications, ImageVariant } from './types.ts';
9
9
 
10
10
  const appLogger = new Logger('[@ecopages/image-processor]', {
11
11
  debug: process.env.ECOPAGES_LOGGER_DEBUG === 'true',
@@ -2,7 +2,7 @@
2
2
  * ImageRenderer
3
3
  * @module
4
4
  */
5
- import type { ImageSpecifications } from './types';
5
+ import type { ImageSpecifications } from './types.js';
6
6
  /**
7
7
  * Image layout options
8
8
  */
@@ -1,5 +1,5 @@
1
- import { DEFAULT_LAYOUT } from "./constants";
2
- import { ImageUtils } from "./image-utils";
1
+ import { DEFAULT_LAYOUT } from "./constants.js";
2
+ import { ImageUtils } from "./image-utils.js";
3
3
  class LayoutAttributesManager {
4
4
  static shouldIncludeWidthHeight(layout) {
5
5
  return layout === "fixed";
@@ -3,9 +3,9 @@
3
3
  * @module
4
4
  */
5
5
 
6
- import { DEFAULT_LAYOUT } from './constants';
7
- import { ImageUtils } from './image-utils';
8
- import type { ImageSpecifications, ImageVariant } from './types';
6
+ import { DEFAULT_LAYOUT } from './constants.ts';
7
+ import { ImageUtils } from './image-utils.ts';
8
+ import type { ImageSpecifications, ImageVariant } from './types.ts';
9
9
 
10
10
  /**
11
11
  * Image layout options
@@ -1,4 +1,4 @@
1
- import type { EcoImageProps } from './image-renderer';
1
+ import type { EcoImageProps } from './image-renderer.js';
2
2
  /**
3
3
  * ImageUtils
4
4
  * This class contains utility methods for working with images
@@ -7,7 +7,7 @@ import type { EcoImageProps } from './image-renderer';
7
7
  export declare class ImageUtils {
8
8
  private static readonly BREAKPOINTS;
9
9
  private static readonly VIEWPORT_SIZES;
10
- static readonly DEFAULT_LAYOUT: import("./image-renderer").ImageLayout;
10
+ static readonly DEFAULT_LAYOUT: import("./image-renderer.js").ImageLayout;
11
11
  /**
12
12
  * Generates a srcset string from processed image variants using relative paths
13
13
  * @param {ImageVariant[]} variants - Array of processed image variants
@@ -1,4 +1,4 @@
1
- import { DEFAULT_LAYOUT } from "./constants";
1
+ import { DEFAULT_LAYOUT } from "./constants.js";
2
2
  class ImageUtils {
3
3
  static BREAKPOINTS = {
4
4
  desktop: 1024,
@@ -1,5 +1,5 @@
1
- import { DEFAULT_LAYOUT } from './constants';
2
- import type { EcoImageProps } from './image-renderer';
1
+ import { DEFAULT_LAYOUT } from './constants.ts';
2
+ import type { EcoImageProps } from './image-renderer.ts';
3
3
 
4
4
  /**
5
5
  * ImageUtils
package/src/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export * from './image-processor';
2
- export * from './plugin';
3
- export * from './types';
1
+ export * from './image-processor.js';
2
+ export * from './plugin.js';
3
+ export * from './types.js';
package/src/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export * from "./image-processor";
2
- export * from "./plugin";
3
- export * from "./types";
1
+ export * from "./image-processor.js";
2
+ export * from "./plugin.js";
3
+ export * from "./types.js";
package/src/index.ts CHANGED
@@ -1,3 +1,3 @@
1
- export * from './image-processor';
2
- export * from './plugin';
3
- export * from './types';
1
+ export * from './image-processor.ts';
2
+ export * from './plugin.ts';
3
+ export * from './types.ts';
package/src/plugin.d.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  */
5
5
  import { Processor, type ProcessorConfig } from '@ecopages/core/plugins/processor';
6
6
  import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
7
- import { ImageProcessor } from './image-processor';
8
- import type { ImageSize, ImageSpecifications } from './types';
7
+ import { ImageProcessor } from './image-processor.js';
8
+ import type { ImageSize, ImageSpecifications } from './types.js';
9
9
  /**
10
10
  * Configuration for the image processor
11
11
  */
@@ -40,10 +40,29 @@ export type ImageMap = Record<string, ImageSpecifications>;
40
40
  */
41
41
  export declare class ImageProcessorPlugin extends Processor<ImageProcessorConfig> {
42
42
  private processor;
43
+ private buildContributionsPrepared;
44
+ private resolvedConfig?;
43
45
  processedImages: Record<string, ImageSpecifications>;
44
46
  constructor(config: Omit<ProcessorConfig<ImageProcessorConfig>, 'name' | 'description'>);
45
47
  get buildPlugins(): EcoBuildPlugin[];
46
48
  get plugins(): EcoBuildPlugin[];
49
+ /**
50
+ * Replaces image-map contents without swapping the backing object.
51
+ *
52
+ * @remarks
53
+ * The build/runtime virtual-module plugins close over `processedImages`, so
54
+ * mutating the existing object keeps those plugins live after preparation.
55
+ */
56
+ private replaceProcessedImages;
57
+ /**
58
+ * Prepares the image virtual-module state before config build seals the app
59
+ * manifest.
60
+ */
61
+ prepareBuildContributions(): Promise<void>;
62
+ private getRuntimeVirtualModulePath;
63
+ private getGeneratedOutputPath;
64
+ private hasGeneratedOutputs;
65
+ private rehydrateGeneratedOutputs;
47
66
  /**
48
67
  * Generate dependencies for processor.
49
68
  * It is ossible to define which one should be included in the final bundle based on the environment.
@@ -51,7 +70,7 @@ export declare class ImageProcessorPlugin extends Processor<ImageProcessorConfig
51
70
  */
52
71
  private generateDependencies;
53
72
  /**
54
- * Setup the image processor and create the virtual module.
73
+ * Prepares build contributions if not already done and rehydrates previously generated image outputs.
55
74
  */
56
75
  setup(): Promise<void>;
57
76
  /**
package/src/plugin.js CHANGED
@@ -1,13 +1,12 @@
1
1
  import path from "node:path";
2
- import { fileURLToPath } from "node:url";
3
2
  import { deepMerge } from "@ecopages/core/utils/deep-merge";
4
3
  import { GENERATED_BASE_PATHS } from "@ecopages/core/constants";
5
4
  import { fileSystem } from "@ecopages/file-system";
6
5
  import { Processor } from "@ecopages/core/plugins/processor";
7
6
  import { Logger } from "@ecopages/logger";
8
- import { createImagePlugin, createImagePluginBundler } from "./image-plugins";
9
- import { ImageProcessor } from "./image-processor";
10
- import { anyCaseToCamelCase } from "./utils";
7
+ import { createImagePlugin, createImagePluginBundler } from "./image-plugins.js";
8
+ import { ImageProcessor } from "./image-processor.js";
9
+ import { anyCaseToCamelCase } from "./utils.js";
11
10
  function resolveGeneratedPath(type, options) {
12
11
  const { root, module, subPath } = options;
13
12
  const parts = [root, GENERATED_BASE_PATHS[type], module, subPath].filter(Boolean);
@@ -16,8 +15,54 @@ function resolveGeneratedPath(type, options) {
16
15
  const logger = new Logger("[@ecopages/image-processor]", {
17
16
  debug: process.env.ECOPAGES_LOGGER_DEBUG === "true"
18
17
  });
19
- const currentDir = path.dirname(fileURLToPath(import.meta.url));
18
+ const IMAGE_VIRTUAL_MODULE_TYPES = `/**
19
+ * ImageAttributes
20
+ * These are the core attributes for the image element generated by the image processor
21
+ */
22
+ interface ImageAttributes {
23
+ src: string;
24
+ width: number;
25
+ height: number;
26
+ sizes: string;
27
+ srcset?: string;
28
+ }
29
+
30
+ /**
31
+ * This represents a single image variant created using the size configuration
32
+ */
33
+ interface ImageVariant {
34
+ width: number;
35
+ height: number;
36
+ src: string;
37
+ label: string;
38
+ }
39
+
40
+ /**
41
+ * These are the core attributes for the image element and the image variants
42
+ * This is the representation of the image element in the virtual module
43
+ */
44
+ interface ImageSpecifications {
45
+ attributes: ImageAttributes;
46
+ variants: ImageVariant[];
47
+ /**
48
+ * A unique key used to cache the image specifications.
49
+ * This key should uniquely identify the combination of attributes and variants
50
+ * to ensure proper caching behavior.
51
+ */
52
+ cacheKey: string;
53
+ }
54
+
55
+ /**
56
+ * This is the representation of an image breakpoint
57
+ * This is used to generate the srcset attribute
58
+ */
59
+ type ImageSize = {
60
+ width: number;
61
+ label: string;
62
+ };`;
20
63
  class ImageProcessorPlugin extends Processor {
64
+ buildContributionsPrepared = false;
65
+ resolvedConfig;
21
66
  processedImages = {};
22
67
  constructor(config) {
23
68
  const acceptedFormats = config.options?.acceptedFormats ?? ["jpg", "jpeg", "png", "webp"];
@@ -50,20 +95,26 @@ class ImageProcessorPlugin extends Processor {
50
95
  return [createImagePlugin(this.processedImages)];
51
96
  }
52
97
  /**
53
- * Generate dependencies for processor.
54
- * It is ossible to define which one should be included in the final bundle based on the environment.
55
- * @returns
98
+ * Replaces image-map contents without swapping the backing object.
99
+ *
100
+ * @remarks
101
+ * The build/runtime virtual-module plugins close over `processedImages`, so
102
+ * mutating the existing object keeps those plugins live after preparation.
56
103
  */
57
- generateDependencies() {
58
- const deps = [];
59
- if (process.env.NODE_ENV === "development") {
104
+ replaceProcessedImages(images) {
105
+ for (const key of Object.keys(this.processedImages)) {
106
+ delete this.processedImages[key];
60
107
  }
61
- return deps;
108
+ Object.assign(this.processedImages, images);
62
109
  }
63
110
  /**
64
- * Setup the image processor and create the virtual module.
111
+ * Prepares the image virtual-module state before config build seals the app
112
+ * manifest.
65
113
  */
66
- async setup() {
114
+ async prepareBuildContributions() {
115
+ if (this.buildContributionsPrepared) {
116
+ return;
117
+ }
67
118
  if (!this.context) {
68
119
  throw new Error("ImageProcessor requires context to be set");
69
120
  }
@@ -80,16 +131,75 @@ class ImageProcessorPlugin extends Processor {
80
131
  format: "webp"
81
132
  };
82
133
  const config = this.options ? deepMerge(defaultConfig, this.options) : defaultConfig;
134
+ this.resolvedConfig = config;
83
135
  this.processor = new ImageProcessor(config, {
84
136
  readCache: (key) => this.readCache(key),
85
137
  writeCache: (key, data) => this.writeCache(key, data)
86
138
  });
87
- this.processedImages = await this.processor.processDirectory();
139
+ this.replaceProcessedImages(await this.processor.processDirectory());
88
140
  if (this.watchConfig) {
89
141
  this.watchConfig.paths = [config.sourceDir];
90
142
  }
91
143
  this.dependencies = this.generateDependencies();
92
144
  this.generateTypes();
145
+ this.buildContributionsPrepared = true;
146
+ }
147
+ getRuntimeVirtualModulePath() {
148
+ if (!this.context) {
149
+ throw new Error("ImageProcessor requires context to be set");
150
+ }
151
+ return resolveGeneratedPath("cache", {
152
+ root: this.context.distDir,
153
+ module: this.name,
154
+ subPath: "virtual-module.ts"
155
+ });
156
+ }
157
+ getGeneratedOutputPath(src) {
158
+ if (!this.resolvedConfig) {
159
+ throw new Error("ImageProcessor not initialized");
160
+ }
161
+ return path.join(this.resolvedConfig.outputDir, path.basename(src));
162
+ }
163
+ hasGeneratedOutputs() {
164
+ if (!this.resolvedConfig) {
165
+ return false;
166
+ }
167
+ if (!fileSystem.exists(this.resolvedConfig.outputDir) || !fileSystem.exists(this.getRuntimeVirtualModulePath())) {
168
+ return false;
169
+ }
170
+ return Object.values(this.processedImages).every((image) => {
171
+ const outputPaths = [image.attributes.src, ...image.variants.map((variant) => variant.src)];
172
+ return outputPaths.every((src) => fileSystem.exists(this.getGeneratedOutputPath(src)));
173
+ });
174
+ }
175
+ async rehydrateGeneratedOutputs() {
176
+ if (!this.processor) {
177
+ throw new Error("ImageProcessor not initialized");
178
+ }
179
+ if (this.hasGeneratedOutputs()) {
180
+ return;
181
+ }
182
+ this.replaceProcessedImages(await this.processor.processDirectory());
183
+ this.dependencies = this.generateDependencies();
184
+ this.generateTypes();
185
+ }
186
+ /**
187
+ * Generate dependencies for processor.
188
+ * It is ossible to define which one should be included in the final bundle based on the environment.
189
+ * @returns
190
+ */
191
+ generateDependencies() {
192
+ const deps = [];
193
+ if (process.env.NODE_ENV === "development") {
194
+ }
195
+ return deps;
196
+ }
197
+ /**
198
+ * Prepares build contributions if not already done and rehydrates previously generated image outputs.
199
+ */
200
+ async setup() {
201
+ await this.prepareBuildContributions();
202
+ await this.rehydrateGeneratedOutputs();
93
203
  }
94
204
  /**
95
205
  * Process images.
@@ -159,14 +269,13 @@ class ImageProcessorPlugin extends Processor {
159
269
  if (!this.options?.outputDir) {
160
270
  throw new Error("Output directory not set");
161
271
  }
162
- const requiredTypes = fileSystem.readFileSync(path.join(currentDir, "types.ts")).toString().replaceAll("export ", "");
163
272
  const content = `
164
273
  /**
165
274
  * Do not edit manually. This file is auto-generated.
166
275
  * This file contains the type definitions for the virtual module "ecopages:images".
167
276
  */
168
277
 
169
- ${requiredTypes}
278
+ ${IMAGE_VIRTUAL_MODULE_TYPES}
170
279
 
171
280
  declare module "ecopages:images" {
172
281
  ${Object.keys(this.processedImages).map((key) => `export const ${anyCaseToCamelCase(key)}: ImageSpecifications;`).join("\n ")}
package/src/plugin.ts CHANGED
@@ -4,7 +4,6 @@
4
4
  */
5
5
 
6
6
  import path from 'node:path';
7
- import { fileURLToPath } from 'node:url';
8
7
  import { deepMerge } from '@ecopages/core/utils/deep-merge';
9
8
  import { GENERATED_BASE_PATHS } from '@ecopages/core/constants';
10
9
  import { fileSystem } from '@ecopages/file-system';
@@ -12,10 +11,10 @@ import { Processor, type ProcessorConfig, type ProcessorWatchConfig } from '@eco
12
11
  import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
13
12
  import type { AssetDefinition } from '@ecopages/core/services/asset-processing-service';
14
13
  import { Logger } from '@ecopages/logger';
15
- import { createImagePlugin, createImagePluginBundler } from './image-plugins';
16
- import { ImageProcessor } from './image-processor';
17
- import type { ImageSize, ImageSpecifications } from './types';
18
- import { anyCaseToCamelCase } from './utils';
14
+ import { createImagePlugin, createImagePluginBundler } from './image-plugins.ts';
15
+ import { ImageProcessor } from './image-processor.ts';
16
+ import type { ImageSize, ImageSpecifications } from './types.ts';
17
+ import { anyCaseToCamelCase } from './utils.ts';
19
18
 
20
19
  function resolveGeneratedPath(
21
20
  type: keyof typeof GENERATED_BASE_PATHS,
@@ -30,7 +29,51 @@ const logger = new Logger('[@ecopages/image-processor]', {
30
29
  debug: process.env.ECOPAGES_LOGGER_DEBUG === 'true',
31
30
  });
32
31
 
33
- const currentDir = path.dirname(fileURLToPath(import.meta.url));
32
+ const IMAGE_VIRTUAL_MODULE_TYPES = `/**
33
+ * ImageAttributes
34
+ * These are the core attributes for the image element generated by the image processor
35
+ */
36
+ interface ImageAttributes {
37
+ src: string;
38
+ width: number;
39
+ height: number;
40
+ sizes: string;
41
+ srcset?: string;
42
+ }
43
+
44
+ /**
45
+ * This represents a single image variant created using the size configuration
46
+ */
47
+ interface ImageVariant {
48
+ width: number;
49
+ height: number;
50
+ src: string;
51
+ label: string;
52
+ }
53
+
54
+ /**
55
+ * These are the core attributes for the image element and the image variants
56
+ * This is the representation of the image element in the virtual module
57
+ */
58
+ interface ImageSpecifications {
59
+ attributes: ImageAttributes;
60
+ variants: ImageVariant[];
61
+ /**
62
+ * A unique key used to cache the image specifications.
63
+ * This key should uniquely identify the combination of attributes and variants
64
+ * to ensure proper caching behavior.
65
+ */
66
+ cacheKey: string;
67
+ }
68
+
69
+ /**
70
+ * This is the representation of an image breakpoint
71
+ * This is used to generate the srcset attribute
72
+ */
73
+ type ImageSize = {
74
+ width: number;
75
+ label: string;
76
+ };`;
34
77
 
35
78
  /**
36
79
  * Configuration for the image processor
@@ -68,6 +111,8 @@ export type ImageMap = Record<string, ImageSpecifications>;
68
111
  */
69
112
  export class ImageProcessorPlugin extends Processor<ImageProcessorConfig> {
70
113
  declare private processor: ImageProcessor;
114
+ private buildContributionsPrepared = false;
115
+ private resolvedConfig?: ImageProcessorConfig;
71
116
  public processedImages: Record<string, ImageSpecifications> = {};
72
117
 
73
118
  constructor(config: Omit<ProcessorConfig<ImageProcessorConfig>, 'name' | 'description'>) {
@@ -106,35 +151,29 @@ export class ImageProcessorPlugin extends Processor<ImageProcessorConfig> {
106
151
  }
107
152
 
108
153
  /**
109
- * Generate dependencies for processor.
110
- * It is ossible to define which one should be included in the final bundle based on the environment.
111
- * @returns
154
+ * Replaces image-map contents without swapping the backing object.
155
+ *
156
+ * @remarks
157
+ * The build/runtime virtual-module plugins close over `processedImages`, so
158
+ * mutating the existing object keeps those plugins live after preparation.
112
159
  */
113
- private generateDependencies(): AssetDefinition[] {
114
- const deps: AssetDefinition[] = [];
115
-
116
- if (process.env.NODE_ENV === 'development') {
117
- /**
118
- * Here we can define the dependencies for the development environment
119
- * @example
120
- * deps.push(
121
- * AssetFactory.createInlineScriptAsset({
122
- * content: `document.addEventListener("DOMContentLoaded",() => console.log("[@ecopages/image-processor] Processor is loaded"));`,
123
- * attributes: {
124
- * type: 'module',
125
- * },
126
- * }),
127
- * );
128
- */
160
+ private replaceProcessedImages(images: ImageMap): void {
161
+ for (const key of Object.keys(this.processedImages)) {
162
+ delete this.processedImages[key];
129
163
  }
130
164
 
131
- return deps;
165
+ Object.assign(this.processedImages, images);
132
166
  }
133
167
 
134
168
  /**
135
- * Setup the image processor and create the virtual module.
169
+ * Prepares the image virtual-module state before config build seals the app
170
+ * manifest.
136
171
  */
137
- async setup(): Promise<void> {
172
+ override async prepareBuildContributions(): Promise<void> {
173
+ if (this.buildContributionsPrepared) {
174
+ return;
175
+ }
176
+
138
177
  if (!this.context) {
139
178
  throw new Error('ImageProcessor requires context to be set');
140
179
  }
@@ -154,23 +193,110 @@ export class ImageProcessorPlugin extends Processor<ImageProcessorConfig> {
154
193
  };
155
194
 
156
195
  const config = this.options ? deepMerge(defaultConfig, this.options) : defaultConfig;
196
+ this.resolvedConfig = config;
157
197
 
158
198
  this.processor = new ImageProcessor(config, {
159
199
  readCache: (key) => this.readCache(key),
160
200
  writeCache: (key, data) => this.writeCache(key, data),
161
201
  });
162
202
 
163
- this.processedImages = await this.processor.processDirectory();
203
+ this.replaceProcessedImages(await this.processor.processDirectory());
164
204
 
165
205
  if (this.watchConfig) {
166
206
  this.watchConfig.paths = [config.sourceDir];
167
207
  }
168
208
 
169
209
  this.dependencies = this.generateDependencies();
210
+ this.generateTypes();
211
+ this.buildContributionsPrepared = true;
212
+ }
213
+
214
+ private getRuntimeVirtualModulePath(): string {
215
+ if (!this.context) {
216
+ throw new Error('ImageProcessor requires context to be set');
217
+ }
218
+
219
+ return resolveGeneratedPath('cache', {
220
+ root: this.context.distDir,
221
+ module: this.name,
222
+ subPath: 'virtual-module.ts',
223
+ });
224
+ }
170
225
 
226
+ private getGeneratedOutputPath(src: string): string {
227
+ if (!this.resolvedConfig) {
228
+ throw new Error('ImageProcessor not initialized');
229
+ }
230
+
231
+ return path.join(this.resolvedConfig.outputDir, path.basename(src));
232
+ }
233
+
234
+ private hasGeneratedOutputs(): boolean {
235
+ if (!this.resolvedConfig) {
236
+ return false;
237
+ }
238
+
239
+ if (
240
+ !fileSystem.exists(this.resolvedConfig.outputDir) ||
241
+ !fileSystem.exists(this.getRuntimeVirtualModulePath())
242
+ ) {
243
+ return false;
244
+ }
245
+
246
+ return Object.values(this.processedImages).every((image) => {
247
+ const outputPaths = [image.attributes.src, ...image.variants.map((variant) => variant.src)];
248
+ return outputPaths.every((src) => fileSystem.exists(this.getGeneratedOutputPath(src)));
249
+ });
250
+ }
251
+
252
+ private async rehydrateGeneratedOutputs(): Promise<void> {
253
+ if (!this.processor) {
254
+ throw new Error('ImageProcessor not initialized');
255
+ }
256
+
257
+ if (this.hasGeneratedOutputs()) {
258
+ return;
259
+ }
260
+
261
+ this.replaceProcessedImages(await this.processor.processDirectory());
262
+ this.dependencies = this.generateDependencies();
171
263
  this.generateTypes();
172
264
  }
173
265
 
266
+ /**
267
+ * Generate dependencies for processor.
268
+ * It is ossible to define which one should be included in the final bundle based on the environment.
269
+ * @returns
270
+ */
271
+ private generateDependencies(): AssetDefinition[] {
272
+ const deps: AssetDefinition[] = [];
273
+
274
+ if (process.env.NODE_ENV === 'development') {
275
+ /**
276
+ * Here we can define the dependencies for the development environment
277
+ * @example
278
+ * deps.push(
279
+ * AssetFactory.createInlineScriptAsset({
280
+ * content: `document.addEventListener("DOMContentLoaded",() => console.log("[@ecopages/image-processor] Processor is loaded"));`,
281
+ * attributes: {
282
+ * type: 'module',
283
+ * },
284
+ * }),
285
+ * );
286
+ */
287
+ }
288
+
289
+ return deps;
290
+ }
291
+
292
+ /**
293
+ * Prepares build contributions if not already done and rehydrates previously generated image outputs.
294
+ */
295
+ async setup(): Promise<void> {
296
+ await this.prepareBuildContributions();
297
+ await this.rehydrateGeneratedOutputs();
298
+ }
299
+
174
300
  /**
175
301
  * Process images.
176
302
  * @param images
@@ -254,18 +380,13 @@ export class ImageProcessorPlugin extends Processor<ImageProcessorConfig> {
254
380
  throw new Error('Output directory not set');
255
381
  }
256
382
 
257
- const requiredTypes = fileSystem
258
- .readFileSync(path.join(currentDir, 'types.ts'))
259
- .toString()
260
- .replaceAll('export ', '');
261
-
262
383
  const content = `
263
384
  /**
264
385
  * Do not edit manually. This file is auto-generated.
265
386
  * This file contains the type definitions for the virtual module "ecopages:images".
266
387
  */
267
388
 
268
- ${requiredTypes}
389
+ ${IMAGE_VIRTUAL_MODULE_TYPES}
269
390
 
270
391
  declare module "ecopages:images" {
271
392
  ${Object.keys(this.processedImages)