@ecopages/image-processor 0.2.0-beta.4 → 0.2.0-beta.41

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/README.md CHANGED
@@ -9,7 +9,7 @@ It provides automatic image processing (e.g. converting and compressing to WebP)
9
9
  - **Automatic Image Optimization**: Converts and compresses images to modern formats at build time.
10
10
  - **Responsive Image Generation**: Creates multiple variants for different screen sizes.
11
11
  - **Virtual Module Integration**: Type-safe imports through `ecopages:images`.
12
- - **Ecopages Components**: Ready-to-use HTML (`EcoImage`) and React (`EcoImage`) components.
12
+ - **Ecopages Components**: Ready-to-use HTML, Ecopages JSX, and React (`EcoImage`) components.
13
13
  - **Multiple Layout Options**: Fixed, constrained, and full-width layouts built-in.
14
14
 
15
15
  ## Installation
@@ -70,9 +70,9 @@ import { heroImage, profilePicture } from 'ecopages:images';
70
70
 
71
71
  ### Components
72
72
 
73
- The plugin provides ready-to-use components for HTML (`@kitajs/html`) and React:
73
+ The plugin provides ready-to-use components for string HTML (KitaJS), Ecopages JSX, and React.
74
74
 
75
- **HTML Component:**
75
+ **HTML Component** (returns a markup string; use as a function, or in string-based JSX runtimes like KitaJS):
76
76
 
77
77
  ```typescript
78
78
  import { EcoImage } from '@ecopages/image-processor/component/html';
@@ -85,6 +85,14 @@ EcoImage({
85
85
  });
86
86
  ```
87
87
 
88
+ **Ecopages JSX Component:**
89
+
90
+ ```jsx
91
+ import { EcoImage } from '@ecopages/image-processor/component/jsx';
92
+
93
+ <EcoImage {...heroImage} alt="Hero banner" layout="constrained" priority />;
94
+ ```
95
+
88
96
  **React Component:**
89
97
 
90
98
  ```jsx
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/image-processor",
3
- "version": "0.2.0-beta.4",
3
+ "version": "0.2.0-beta.41",
4
4
  "description": "Image processor, transform and optimize images for web",
5
5
  "keywords": [
6
6
  "image",
@@ -11,18 +11,24 @@
11
11
  "type": "module",
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "https://github.com/ecopages/ecopages.git",
14
+ "url": "git+https://github.com/ecopages/ecopages.git",
15
15
  "directory": "packages/processors/image-processor"
16
16
  },
17
17
  "peerDependencies": {
18
- "@ecopages/core": "0.2.0-beta.4"
18
+ "@ecopages/core": "0.2.0-beta.41",
19
+ "@ecopages/jsx": ">=0.3.0-beta.6"
20
+ },
21
+ "peerDependenciesMeta": {
22
+ "@ecopages/jsx": {
23
+ "optional": true
24
+ }
19
25
  },
20
26
  "dependencies": {
21
- "@ecopages/file-system": "0.2.0-beta.4",
27
+ "@ecopages/file-system": "0.2.0-beta.41",
22
28
  "@ecopages/logger": "^0.2.3",
23
29
  "react": "^19.2.6",
24
30
  "react-dom": "^19.2.6",
25
- "sharp": "^0.34.5"
31
+ "sharp": "^0.35.3"
26
32
  },
27
33
  "exports": {
28
34
  ".": {
@@ -36,6 +42,10 @@
36
42
  "default": "./src/component/html.js",
37
43
  "types": "./src/component/html.d.ts"
38
44
  },
45
+ "./component/jsx": {
46
+ "default": "./src/component/jsx.js",
47
+ "types": "./src/component/jsx.d.ts"
48
+ },
39
49
  "./component/react": {
40
50
  "default": "./src/component/react.js",
41
51
  "types": "./src/component/react.d.ts"
@@ -47,6 +57,10 @@
47
57
  "default": "./src/component/html.js",
48
58
  "types": "./src/component/html.d.ts"
49
59
  },
60
+ "./component/jsx.ts": {
61
+ "default": "./src/component/jsx.js",
62
+ "types": "./src/component/jsx.d.ts"
63
+ },
50
64
  "./component/react.ts": {
51
65
  "default": "./src/component/react.js",
52
66
  "types": "./src/component/react.d.ts"
@@ -1,33 +1,52 @@
1
- import { anyCaseToCamelCase } from "./utils.js";
1
+ /**
2
+ * This file contains the plugins for bundling the image specifications.
3
+ * @module @ecopages/image-processor/bun-plugins
4
+ */
5
+ import { anyCaseToCamelCase } from './utils.js';
6
+ /**
7
+ * This function creates the plugin result for the image specifications.
8
+ */
2
9
  function createPluginResult(exports) {
3
- return {
4
- contents: `${Object.entries(exports).map(([key, value]) => `export const ${anyCaseToCamelCase(key)} = ${JSON.stringify(value)};`).join("\n")}`,
5
- loader: "ts"
6
- };
10
+ return {
11
+ contents: `${Object.entries(exports)
12
+ .map(([key, value]) => `export const ${anyCaseToCamelCase(key)} = ${JSON.stringify(value)};`)
13
+ .join('\n')}`,
14
+ loader: 'ts',
15
+ };
7
16
  }
8
- function createImagePlugin(exports) {
9
- return {
10
- name: "ecopages:images",
11
- setup(build) {
12
- build.module("ecopages:images", () => createPluginResult(exports));
13
- }
14
- };
17
+ /**
18
+ * This function creates a plugin for bundling the image specifications.
19
+ * https://bun.sh/docs/runtime/plugins#virtual-modules
20
+ * @param exports
21
+ * @returns
22
+ */
23
+ export function createImagePlugin(exports) {
24
+ return {
25
+ name: 'ecopages:images',
26
+ setup(build) {
27
+ build.module('ecopages:images', () => createPluginResult(exports));
28
+ },
29
+ };
15
30
  }
16
- function createImagePluginBundler(exports) {
17
- return {
18
- name: "ecopages:images",
19
- setup(build) {
20
- build.onResolve({ filter: /^ecopages:images$/ }, () => {
21
- return {
22
- namespace: "ecopages-images",
23
- path: "ecopages:images"
24
- };
25
- });
26
- build.onLoad({ filter: /.*/, namespace: "ecopages-images" }, () => createPluginResult(exports));
27
- }
28
- };
31
+ /**
32
+ * This function creates a plugin for bundling the image specifications.
33
+ * Due to some limitations in the bundler, we need to use a different approach.
34
+ * https://bun.sh/docs/runtime/plugins#virtual-modules > bun-v1.2.5
35
+ * (This feature is currently only available at runtime with Bun.plugin and not yet supported in the bundler, but you can mimic the behavior using onResolve and onLoad.)
36
+ * @param exports
37
+ * @returns
38
+ */
39
+ export function createImagePluginBundler(exports) {
40
+ return {
41
+ name: 'ecopages:images',
42
+ setup(build) {
43
+ build.onResolve({ filter: /^ecopages:images$/ }, () => {
44
+ return {
45
+ namespace: 'ecopages-images',
46
+ path: 'ecopages:images',
47
+ };
48
+ });
49
+ build.onLoad({ filter: /.*/, namespace: 'ecopages-images' }, () => createPluginResult(exports));
50
+ },
51
+ };
29
52
  }
30
- export {
31
- createImagePlugin,
32
- createImagePluginBundler
33
- };
@@ -1,7 +1,13 @@
1
- import { renderer } from "../image-renderer.js";
2
- const EcoImage = (props) => {
3
- return renderer.renderToString(props);
4
- };
5
- export {
6
- EcoImage
1
+ /**
2
+ * Image component that renders the image as a string.
3
+ * @module @ecopages/image-processor/component/html
4
+ */
5
+ import { renderer } from '../image-renderer.js';
6
+ /**
7
+ * EcoImage
8
+ * This component generates the image element based on the provided props as a string
9
+ * @param props {@link EcoImageProps}
10
+ */
11
+ export const EcoImage = (props) => {
12
+ return renderer.renderToString(props);
7
13
  };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Image component that renders as an Ecopages JSX element.
3
+ * @module @ecopages/image-processor/component/jsx
4
+ */
5
+ import { type JsxRenderable } from '@ecopages/jsx';
6
+ import { type EcoImageProps } from '../image-renderer.js';
7
+ /**
8
+ * EcoImage
9
+ * This component generates the image element based on the provided props as JSX
10
+ * @param props {@link EcoImageProps}
11
+ */
12
+ export declare const EcoImage: (props: EcoImageProps) => JsxRenderable;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Image component that renders as an Ecopages JSX element.
3
+ * @module @ecopages/image-processor/component/jsx
4
+ */
5
+ import { jsx } from '@ecopages/jsx';
6
+ import { renderer } from '../image-renderer.js';
7
+ /**
8
+ * EcoImage
9
+ * This component generates the image element based on the provided props as JSX
10
+ * @param props {@link EcoImageProps}
11
+ */
12
+ export const EcoImage = (props) => {
13
+ const attributes = renderer.generateAttributes(props);
14
+ if (!attributes) {
15
+ return null;
16
+ }
17
+ return jsx('img', attributes);
18
+ };
@@ -1,11 +1,18 @@
1
- import { createElement } from "react";
2
- import { renderer } from "../image-renderer.js";
3
- const EcoImage = (props) => {
4
- return createElement("img", {
5
- ...renderer.generateAttributesJsx(props),
6
- suppressHydrationWarning: true
7
- });
8
- };
9
- export {
10
- EcoImage
1
+ /**
2
+ * Image component that renders the image as a string.
3
+ * @module @ecopages/image-processor/component/react
4
+ */
5
+ import { createElement } from 'react';
6
+ import { renderer } from '../image-renderer.js';
7
+ /**
8
+ * EcoImage
9
+ * This component generates the image element based on the provided props as JSX
10
+ * @param props {@link EcoImageProps}
11
+ * @returns
12
+ */
13
+ export const EcoImage = (props) => {
14
+ return createElement('img', {
15
+ ...renderer.generateAttributesJsx(props),
16
+ suppressHydrationWarning: true,
17
+ });
11
18
  };
package/src/constants.js CHANGED
@@ -1,4 +1,5 @@
1
- const DEFAULT_LAYOUT = "constrained";
2
- export {
3
- DEFAULT_LAYOUT
4
- };
1
+ /**
2
+ * Default image layout
3
+ * @constant "constrained"
4
+ */
5
+ export const DEFAULT_LAYOUT = 'constrained';
@@ -1,33 +1,52 @@
1
- import { anyCaseToCamelCase } from "./utils.js";
1
+ /**
2
+ * This file contains the plugins for bundling the image specifications.
3
+ * @module @ecopages/image-processor/image-plugins
4
+ */
5
+ import { anyCaseToCamelCase } from './utils.js';
6
+ /**
7
+ * This function creates the plugin result for the image specifications.
8
+ */
2
9
  function createPluginResult(exports) {
3
- return {
4
- contents: `${Object.entries(exports).map(([key, value]) => `export const ${anyCaseToCamelCase(key)} = ${JSON.stringify(value)};`).join("\n")}`,
5
- loader: "ts"
6
- };
10
+ return {
11
+ contents: `${Object.entries(exports)
12
+ .map(([key, value]) => `export const ${anyCaseToCamelCase(key)} = ${JSON.stringify(value)};`)
13
+ .join('\n')}`,
14
+ loader: 'ts',
15
+ };
7
16
  }
8
- function createImagePlugin(exports) {
9
- return {
10
- name: "ecopages:images",
11
- setup(build) {
12
- build.module("ecopages:images", () => createPluginResult(exports));
13
- }
14
- };
17
+ /**
18
+ * This function creates a plugin for bundling the image specifications.
19
+ * https://bun.sh/docs/runtime/plugins#virtual-modules
20
+ * @param exports
21
+ * @returns
22
+ */
23
+ export function createImagePlugin(exports) {
24
+ return {
25
+ name: 'ecopages:images',
26
+ setup(build) {
27
+ build.module('ecopages:images', () => createPluginResult(exports));
28
+ },
29
+ };
15
30
  }
16
- function createImagePluginBundler(exports) {
17
- return {
18
- name: "ecopages:images",
19
- setup(build) {
20
- build.onResolve({ filter: /^ecopages:images$/ }, () => {
21
- return {
22
- namespace: "ecopages-images",
23
- path: "ecopages:images"
24
- };
25
- });
26
- build.onLoad({ filter: /.*/, namespace: "ecopages-images" }, () => createPluginResult(exports));
27
- }
28
- };
31
+ /**
32
+ * This function creates a plugin for bundling the image specifications.
33
+ * Due to some limitations in the bundler, we need to use a different approach.
34
+ * https://bun.sh/docs/runtime/plugins#virtual-modules > bun-v1.2.5
35
+ * (This feature is currently only available at runtime with Bun.plugin and not yet supported in the bundler, but you can mimic the behavior using onResolve and onLoad.)
36
+ * @param exports
37
+ * @returns
38
+ */
39
+ export function createImagePluginBundler(exports) {
40
+ return {
41
+ name: 'ecopages:images',
42
+ setup(build) {
43
+ build.onResolve({ filter: /^ecopages:images$/ }, () => {
44
+ return {
45
+ namespace: 'ecopages-images',
46
+ path: 'ecopages:images',
47
+ };
48
+ });
49
+ build.onLoad({ filter: /.*/, namespace: 'ecopages-images' }, () => createPluginResult(exports));
50
+ },
51
+ };
29
52
  }
30
- export {
31
- createImagePlugin,
32
- createImagePluginBundler
33
- };
@@ -1,141 +1,153 @@
1
- import path from "node:path";
2
- import { mergeProcessorOptions } from "@ecopages/core/plugins/processor";
3
- import { fileSystem } from "@ecopages/file-system";
4
- import { Logger } from "@ecopages/logger";
5
- import sharp from "sharp";
6
- import { ImageUtils } from "./image-utils.js";
7
- const appLogger = new Logger("[@ecopages/image-processor]", {
8
- debug: process.env.ECOPAGES_LOGGER_DEBUG === "true"
1
+ import path from 'node:path';
2
+ import { mergeProcessorOptions } from '@ecopages/core/plugins/processor';
3
+ import { fileSystem } from '@ecopages/file-system';
4
+ import { Logger } from '@ecopages/logger';
5
+ import sharp, {} from 'sharp';
6
+ import { ImageUtils } from './image-utils.js';
7
+ const appLogger = new Logger('[@ecopages/image-processor]', {
8
+ debug: process.env.ECOPAGES_LOGGER_DEBUG === 'true',
9
9
  });
10
- class ImageProcessor {
11
- config;
12
- cacheManager;
13
- constructor(config, cacheManager) {
14
- this.config = mergeProcessorOptions({ cacheEnabled: true }, config);
15
- this.cacheManager = cacheManager;
16
- fileSystem.ensureDir(this.config.outputDir);
17
- }
18
- async calculateDimensions(metadata, targetWidth) {
19
- const originalWidth = metadata.width || 0;
20
- const originalHeight = metadata.height || 0;
21
- const aspectRatio = originalHeight / originalWidth;
22
- const width = Math.min(targetWidth, originalWidth);
23
- const height = Math.round(width * aspectRatio);
24
- return { width, height };
25
- }
26
- getOutputPath(imagePath, width) {
27
- const hash = fileSystem.hash(imagePath);
28
- const ext = path.extname(imagePath);
29
- const base = path.basename(imagePath, ext);
30
- const filename = `${base}-${hash}-${width}.${this.config.format}`;
31
- return path.join(this.config.outputDir, filename);
32
- }
33
- async processImage(imagePath) {
34
- try {
35
- const fileHash = fileSystem.hash(imagePath);
36
- const cacheKey = `${path.basename(imagePath)}:${fileHash}`;
37
- if (this.config.cacheEnabled) {
38
- const cached = await this.cacheManager.readCache(cacheKey);
39
- if (cached) {
40
- const mainFilePath = path.join(process.cwd(), cached.attributes.src);
41
- const mainFileExists = fileSystem.exists(mainFilePath);
42
- const variantsExist = cached.variants.every(
43
- (v) => fileSystem.exists(path.join(process.cwd(), v.src))
44
- );
45
- if (mainFileExists && variantsExist) {
46
- appLogger.debug(`Cache hit for ${imagePath}`);
47
- return cached;
48
- }
49
- appLogger.debug(`Cache invalid for ${imagePath}, reprocessing`);
10
+ /**
11
+ * ImageProcessor
12
+ * This is the core class for processing images.
13
+ * It uses the sharp library to resize and optimize images.
14
+ */
15
+ export class ImageProcessor {
16
+ config;
17
+ cacheManager;
18
+ constructor(config, cacheManager) {
19
+ this.config = mergeProcessorOptions({ cacheEnabled: true }, config);
20
+ this.cacheManager = cacheManager;
21
+ fileSystem.ensureDir(this.config.outputDir);
22
+ }
23
+ async calculateDimensions(metadata, targetWidth) {
24
+ const originalWidth = metadata.width || 0;
25
+ const originalHeight = metadata.height || 0;
26
+ const aspectRatio = originalHeight / originalWidth;
27
+ const width = Math.min(targetWidth, originalWidth);
28
+ const height = Math.round(width * aspectRatio);
29
+ return { width, height };
30
+ }
31
+ getOutputPath(imagePath, width) {
32
+ const hash = fileSystem.hash(imagePath);
33
+ const ext = path.extname(imagePath);
34
+ const base = path.basename(imagePath, ext);
35
+ const filename = `${base}-${hash}-${width}.${this.config.format}`;
36
+ return path.join(this.config.outputDir, filename);
37
+ }
38
+ async processImage(imagePath) {
39
+ try {
40
+ const fileHash = fileSystem.hash(imagePath);
41
+ const cacheKey = `${path.basename(imagePath)}:${fileHash}`;
42
+ if (this.config.cacheEnabled) {
43
+ const cached = await this.cacheManager.readCache(cacheKey);
44
+ if (cached) {
45
+ const mainFilePath = path.join(this.config.outputDir, path.basename(cached.attributes.src));
46
+ const mainFileExists = fileSystem.exists(mainFilePath);
47
+ const variantsExist = cached.variants.every((variant) => fileSystem.exists(path.join(this.config.outputDir, path.basename(variant.src))));
48
+ if (mainFileExists && variantsExist) {
49
+ appLogger.debug(`Cache hit for ${imagePath}`);
50
+ return cached;
51
+ }
52
+ appLogger.debug(`Cache invalid for ${imagePath}, reprocessing`);
53
+ }
54
+ }
55
+ fileSystem.ensureDir(this.config.outputDir);
56
+ const metadata = await sharp(imagePath).metadata();
57
+ const originalWidth = metadata.width || 0;
58
+ const originalHeight = metadata.height || 0;
59
+ if (this.config.sizes.length === 0) {
60
+ const outputPath = this.getOutputPath(imagePath, originalWidth);
61
+ if (fileSystem.exists(outputPath)) {
62
+ appLogger.debug(`Using existing file for ${imagePath}`);
63
+ }
64
+ else {
65
+ await sharp(imagePath)
66
+ .toFormat(this.config.format, { quality: this.config.quality })
67
+ .toFile(outputPath);
68
+ }
69
+ const src = path.join(this.config.publicPath, path.basename(outputPath));
70
+ const imageSpecifications = {
71
+ attributes: {
72
+ src,
73
+ width: originalWidth,
74
+ height: originalHeight,
75
+ sizes: '',
76
+ },
77
+ variants: [],
78
+ cacheKey,
79
+ };
80
+ if (this.config.cacheEnabled) {
81
+ await this.cacheManager.writeCache(cacheKey, imageSpecifications);
82
+ }
83
+ return imageSpecifications;
84
+ }
85
+ let applicableSizes = this.config.sizes
86
+ .filter((size) => size.width <= originalWidth)
87
+ .sort((a, b) => b.width - a.width);
88
+ if (applicableSizes.length === 0) {
89
+ applicableSizes = this.config.sizes.sort((a, b) => b.width - a.width).slice(0, 1);
90
+ }
91
+ const variants = await Promise.all(applicableSizes.map(async ({ width: targetWidth, label }) => {
92
+ const { width, height } = await this.calculateDimensions(metadata, targetWidth);
93
+ const outputPath = this.getOutputPath(imagePath, width);
94
+ if (fileSystem.exists(outputPath)) {
95
+ appLogger.debug(`Variant ${width}px already exists for ${imagePath}`);
96
+ }
97
+ else {
98
+ await sharp(imagePath)
99
+ .resize(width, height)
100
+ .toFormat(this.config.format, { quality: this.config.quality })
101
+ .toFile(outputPath);
102
+ }
103
+ const src = path.join(this.config.publicPath, path.basename(outputPath));
104
+ return {
105
+ width,
106
+ height,
107
+ src,
108
+ label,
109
+ };
110
+ }));
111
+ const mainVariant = variants[0];
112
+ const attributes = {
113
+ src: mainVariant.src,
114
+ width: mainVariant.width,
115
+ height: mainVariant.height,
116
+ sizes: ImageUtils.generateSizes(variants),
117
+ srcset: ImageUtils.generateSrcset(variants),
118
+ };
119
+ const imageSpecifications = {
120
+ attributes,
121
+ variants,
122
+ cacheKey,
123
+ };
124
+ if (this.config.cacheEnabled) {
125
+ await this.cacheManager.writeCache(cacheKey, imageSpecifications);
126
+ }
127
+ return imageSpecifications;
50
128
  }
51
- }
52
- fileSystem.ensureDir(this.config.outputDir);
53
- const metadata = await sharp(imagePath).metadata();
54
- const originalWidth = metadata.width || 0;
55
- const originalHeight = metadata.height || 0;
56
- if (this.config.sizes.length === 0) {
57
- const outputPath = this.getOutputPath(imagePath, originalWidth);
58
- if (fileSystem.exists(outputPath)) {
59
- appLogger.debug(`Using existing file for ${imagePath}`);
60
- } else {
61
- await sharp(imagePath).toFormat(this.config.format, { quality: this.config.quality }).toFile(outputPath);
129
+ catch (error) {
130
+ appLogger.error(`Failed to process image ${imagePath}:`, error);
131
+ return null;
132
+ }
133
+ }
134
+ async processDirectory() {
135
+ const acceptedFormats = this.config.acceptedFormats || ['jpg', 'jpeg', 'png', 'webp'];
136
+ const images = await fileSystem.glob([`${this.config.sourceDir}/**/*.{${acceptedFormats.join(',')}}`]);
137
+ appLogger.debugTime('Processing images');
138
+ const results = (await Promise.all(images.map(async (file) => {
139
+ const processed = await this.processImage(file);
140
+ if (!processed)
141
+ return null;
142
+ return [path.basename(file), processed];
143
+ }))).filter(Boolean);
144
+ appLogger.debugTimeEnd('Processing images');
145
+ if (process.env.ECOPAGES_BENCH === '1' && process.env.ECOPAGES_BENCH_VERBOSE !== '1') {
146
+ appLogger.debug(`Processed ${results.length} images`);
62
147
  }
63
- const src = path.join(this.config.publicPath, path.basename(outputPath));
64
- const imageSpecifications2 = {
65
- attributes: {
66
- src,
67
- width: originalWidth,
68
- height: originalHeight,
69
- sizes: ""
70
- },
71
- variants: [],
72
- cacheKey
73
- };
74
- if (this.config.cacheEnabled) {
75
- await this.cacheManager.writeCache(cacheKey, imageSpecifications2);
148
+ else {
149
+ appLogger.info(`Processed ${results.length} images`);
76
150
  }
77
- return imageSpecifications2;
78
- }
79
- let applicableSizes = this.config.sizes.filter((size) => size.width <= originalWidth).sort((a, b) => b.width - a.width);
80
- if (applicableSizes.length === 0) {
81
- applicableSizes = this.config.sizes.sort((a, b) => b.width - a.width).slice(0, 1);
82
- }
83
- const variants = await Promise.all(
84
- applicableSizes.map(async ({ width: targetWidth, label }) => {
85
- const { width, height } = await this.calculateDimensions(metadata, targetWidth);
86
- const outputPath = this.getOutputPath(imagePath, width);
87
- if (fileSystem.exists(outputPath)) {
88
- appLogger.debug(`Variant ${width}px already exists for ${imagePath}`);
89
- } else {
90
- await sharp(imagePath).resize(width, height).toFormat(this.config.format, { quality: this.config.quality }).toFile(outputPath);
91
- }
92
- const src = path.join(this.config.publicPath, path.basename(outputPath));
93
- return {
94
- width,
95
- height,
96
- src,
97
- label
98
- };
99
- })
100
- );
101
- const mainVariant = variants[0];
102
- const attributes = {
103
- src: mainVariant.src,
104
- width: mainVariant.width,
105
- height: mainVariant.height,
106
- sizes: ImageUtils.generateSizes(variants),
107
- srcset: ImageUtils.generateSrcset(variants)
108
- };
109
- const imageSpecifications = {
110
- attributes,
111
- variants,
112
- cacheKey
113
- };
114
- if (this.config.cacheEnabled) {
115
- await this.cacheManager.writeCache(cacheKey, imageSpecifications);
116
- }
117
- return imageSpecifications;
118
- } catch (error) {
119
- appLogger.error(`Failed to process image ${imagePath}:`, error);
120
- return null;
151
+ return Object.fromEntries(results);
121
152
  }
122
- }
123
- async processDirectory() {
124
- const acceptedFormats = this.config.acceptedFormats || ["jpg", "jpeg", "png", "webp"];
125
- const images = await fileSystem.glob([`${this.config.sourceDir}/**/*.{${acceptedFormats.join(",")}}`]);
126
- appLogger.debugTime("Processing images");
127
- const results = (await Promise.all(
128
- images.map(async (file) => {
129
- const processed = await this.processImage(file);
130
- if (!processed) return null;
131
- return [path.basename(file), processed];
132
- })
133
- )).filter(Boolean);
134
- appLogger.debugTimeEnd("Processing images");
135
- appLogger.info(`Processed ${results.length} images`);
136
- return Object.fromEntries(results);
137
- }
138
153
  }
139
- export {
140
- ImageProcessor
141
- };