@ecopages/image-processor 0.2.0-alpha.9 → 0.2.0-beta.0

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
@@ -15,7 +15,7 @@ It provides automatic image processing (e.g. converting and compressing to WebP)
15
15
  ## Installation
16
16
 
17
17
  ```bash
18
- bunx jsr add @ecopages/image-processor
18
+ bun add @ecopages/image-processor
19
19
  ```
20
20
 
21
21
  ## Configuration
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/image-processor",
3
- "version": "0.2.0-alpha.9",
3
+ "version": "0.2.0-beta.0",
4
4
  "description": "Image processor, transform and optimize images for web",
5
5
  "keywords": [
6
6
  "image",
@@ -15,14 +15,14 @@
15
15
  "directory": "packages/processors/image-processor"
16
16
  },
17
17
  "peerDependencies": {
18
- "@ecopages/core": "0.2.0-alpha.9"
18
+ "@ecopages/core": "0.2.0-beta.0"
19
19
  },
20
20
  "dependencies": {
21
- "@ecopages/file-system": "0.2.0-alpha.9",
22
- "@ecopages/logger": "latest",
23
- "react": "^19",
24
- "react-dom": "^19",
25
- "sharp": "^0.33.5"
21
+ "@ecopages/file-system": "0.2.0-beta.0",
22
+ "@ecopages/logger": "^0.2.3",
23
+ "react": "^19.2.6",
24
+ "react-dom": "^19.2.6",
25
+ "sharp": "^0.34.5"
26
26
  },
27
27
  "exports": {
28
28
  ".": {
@@ -2,7 +2,7 @@
2
2
  * This file contains the plugins for bundling the image specifications.
3
3
  * @module @ecopages/image-processor/bun-plugins
4
4
  */
5
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
5
+ import type { EcoBuildPlugin } from '@ecopages/core/plugins/processor';
6
6
  import type { ImageMap } from './plugin.js';
7
7
  /**
8
8
  * This function creates a plugin for bundling the image specifications.
@@ -2,7 +2,7 @@
2
2
  * This file contains the plugins for bundling the image specifications.
3
3
  * @module @ecopages/image-processor/image-plugins
4
4
  */
5
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
5
+ import type { EcoBuildPlugin } from '@ecopages/core/plugins/processor';
6
6
  import type { ImageMap } from './plugin.js';
7
7
  /**
8
8
  * This function creates a plugin for bundling the image specifications.
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { deepMerge } from "@ecopages/core/utils/deep-merge";
2
+ import { mergeProcessorOptions } from "@ecopages/core/plugins/processor";
3
3
  import { fileSystem } from "@ecopages/file-system";
4
4
  import { Logger } from "@ecopages/logger";
5
5
  import sharp from "sharp";
@@ -11,7 +11,7 @@ class ImageProcessor {
11
11
  config;
12
12
  cacheManager;
13
13
  constructor(config, cacheManager) {
14
- this.config = deepMerge({ cacheEnabled: true }, config);
14
+ this.config = mergeProcessorOptions({ cacheEnabled: true }, config);
15
15
  this.cacheManager = cacheManager;
16
16
  fileSystem.ensureDir(this.config.outputDir);
17
17
  }
package/src/plugin.d.ts CHANGED
@@ -2,8 +2,7 @@
2
2
  * ImageProcessorPlugin
3
3
  * @module @ecopages/image-processor
4
4
  */
5
- import { Processor, type ProcessorConfig } from '@ecopages/core/plugins/processor';
6
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
5
+ import { Processor, type EcoBuildPlugin, type ProcessorConfig } from '@ecopages/core/plugins/processor';
7
6
  import { ImageProcessor } from './image-processor.js';
8
7
  import type { ImageSize, ImageSpecifications } from './types.js';
9
8
  /**
package/src/plugin.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import path from "node:path";
2
- import { deepMerge } from "@ecopages/core/utils/deep-merge";
3
2
  import { GENERATED_BASE_PATHS } from "@ecopages/core/constants";
4
3
  import { fileSystem } from "@ecopages/file-system";
5
- import { Processor } from "@ecopages/core/plugins/processor";
4
+ import {
5
+ mergeProcessorOptions,
6
+ Processor
7
+ } from "@ecopages/core/plugins/processor";
6
8
  import { Logger } from "@ecopages/logger";
7
9
  import { createImagePlugin, createImagePluginBundler } from "./image-plugins.js";
8
10
  import { ImageProcessor } from "./image-processor.js";
@@ -85,7 +87,7 @@ class ImageProcessorPlugin extends Processor {
85
87
  extensions: extensionPatterns
86
88
  }
87
89
  ],
88
- watch: config.watch ? deepMerge(defaultWatchConfig, config.watch) : defaultWatchConfig
90
+ watch: config.watch ? mergeProcessorOptions(defaultWatchConfig, config.watch) : defaultWatchConfig
89
91
  });
90
92
  }
91
93
  get buildPlugins() {
@@ -130,7 +132,7 @@ class ImageProcessorPlugin extends Processor {
130
132
  quality: 80,
131
133
  format: "webp"
132
134
  };
133
- const config = this.options ? deepMerge(defaultConfig, this.options) : defaultConfig;
135
+ const config = this.options ? mergeProcessorOptions(defaultConfig, this.options) : defaultConfig;
134
136
  this.resolvedConfig = config;
135
137
  this.processor = new ImageProcessor(config, {
136
138
  readCache: (key) => this.readCache(key),
package/CHANGELOG.md DELETED
@@ -1,22 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to `@ecopages/image-processor` are documented here.
4
-
5
- > **Note:** Changelog tracking begins at version `0.2.0`. Changes prior to this release are not recorded here but are available in the git history.
6
-
7
- ## [UNRELEASED] — TBD
8
-
9
- ### Features
10
-
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
-
14
- ### Bug Fixes
15
-
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
-
20
- ### Tests
21
-
22
- - Added image processor and renderer coverage for the current build pipeline.
@@ -1,59 +0,0 @@
1
- /**
2
- * This file contains the plugins for bundling the image specifications.
3
- * @module @ecopages/image-processor/bun-plugins
4
- */
5
-
6
- import type { EcoBuildOnLoadResult, EcoBuildPlugin } from '@ecopages/core/build/build-types';
7
- import type { ImageMap } from './plugin.ts';
8
- import { anyCaseToCamelCase } from './utils.ts';
9
-
10
- /**
11
- * This function creates the plugin result for the image specifications.
12
- */
13
- function createPluginResult(exports: ImageMap): EcoBuildOnLoadResult {
14
- return {
15
- contents: `${Object.entries(exports)
16
- .map(([key, value]) => `export const ${anyCaseToCamelCase(key)} = ${JSON.stringify(value)};`)
17
- .join('\n')}`,
18
- loader: 'ts',
19
- };
20
- }
21
-
22
- /**
23
- * This function creates a plugin for bundling the image specifications.
24
- * https://bun.sh/docs/runtime/plugins#virtual-modules
25
- * @param exports
26
- * @returns
27
- */
28
- export function createImagePlugin(exports: ImageMap): EcoBuildPlugin {
29
- return {
30
- name: 'ecopages:images',
31
- setup(build) {
32
- build.module('ecopages:images', () => createPluginResult(exports));
33
- },
34
- };
35
- }
36
-
37
- /**
38
- * This function creates a plugin for bundling the image specifications.
39
- * Due to some limitations in the bundler, we need to use a different approach.
40
- * https://bun.sh/docs/runtime/plugins#virtual-modules > bun-v1.2.5
41
- * (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.)
42
- * @param exports
43
- * @returns
44
- */
45
- export function createImagePluginBundler(exports: ImageMap): EcoBuildPlugin {
46
- return {
47
- name: 'ecopages:images',
48
- setup(build) {
49
- build.onResolve({ filter: /^ecopages:images$/ }, () => {
50
- return {
51
- namespace: 'ecopages-images',
52
- path: 'ecopages:images',
53
- };
54
- });
55
-
56
- build.onLoad({ filter: /.*/, namespace: 'ecopages-images' }, () => createPluginResult(exports));
57
- },
58
- };
59
- }
@@ -1,15 +0,0 @@
1
- /**
2
- * Image component that renders the image as a string.
3
- * @module @ecopages/image-processor/component/html
4
- */
5
-
6
- import { type EcoImageProps, renderer } from '../image-renderer.ts';
7
-
8
- /**
9
- * EcoImage
10
- * This component generates the image element based on the provided props as a string
11
- * @param props {@link EcoImageProps}
12
- */
13
- export const EcoImage = (props: EcoImageProps): string => {
14
- return renderer.renderToString(props);
15
- };
@@ -1,20 +0,0 @@
1
- /**
2
- * Image component that renders the image as a string.
3
- * @module @ecopages/image-processor/component/react
4
- */
5
-
6
- import { createElement, type JSX } from 'react';
7
- import { type EcoImageProps, renderer } from '../image-renderer.ts';
8
-
9
- /**
10
- * EcoImage
11
- * This component generates the image element based on the provided props as JSX
12
- * @param props {@link EcoImageProps}
13
- * @returns
14
- */
15
- export const EcoImage = (props: EcoImageProps): JSX.Element => {
16
- return createElement('img', {
17
- ...renderer.generateAttributesJsx(props),
18
- suppressHydrationWarning: true,
19
- });
20
- };
package/src/constants.ts DELETED
@@ -1,7 +0,0 @@
1
- import type { ImageLayout } from './image-renderer.ts';
2
-
3
- /**
4
- * Default image layout
5
- * @constant "constrained"
6
- */
7
- export const DEFAULT_LAYOUT: ImageLayout = 'constrained';
@@ -1,59 +0,0 @@
1
- /**
2
- * This file contains the plugins for bundling the image specifications.
3
- * @module @ecopages/image-processor/image-plugins
4
- */
5
-
6
- import type { EcoBuildOnLoadResult, EcoBuildPlugin } from '@ecopages/core/build/build-types';
7
- import type { ImageMap } from './plugin.ts';
8
- import { anyCaseToCamelCase } from './utils.ts';
9
-
10
- /**
11
- * This function creates the plugin result for the image specifications.
12
- */
13
- function createPluginResult(exports: ImageMap): EcoBuildOnLoadResult {
14
- return {
15
- contents: `${Object.entries(exports)
16
- .map(([key, value]) => `export const ${anyCaseToCamelCase(key)} = ${JSON.stringify(value)};`)
17
- .join('\n')}`,
18
- loader: 'ts',
19
- };
20
- }
21
-
22
- /**
23
- * This function creates a plugin for bundling the image specifications.
24
- * https://bun.sh/docs/runtime/plugins#virtual-modules
25
- * @param exports
26
- * @returns
27
- */
28
- export function createImagePlugin(exports: ImageMap): EcoBuildPlugin {
29
- return {
30
- name: 'ecopages:images',
31
- setup(build) {
32
- build.module('ecopages:images', () => createPluginResult(exports));
33
- },
34
- };
35
- }
36
-
37
- /**
38
- * This function creates a plugin for bundling the image specifications.
39
- * Due to some limitations in the bundler, we need to use a different approach.
40
- * https://bun.sh/docs/runtime/plugins#virtual-modules > bun-v1.2.5
41
- * (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.)
42
- * @param exports
43
- * @returns
44
- */
45
- export function createImagePluginBundler(exports: ImageMap): EcoBuildPlugin {
46
- return {
47
- name: 'ecopages:images',
48
- setup(build) {
49
- build.onResolve({ filter: /^ecopages:images$/ }, () => {
50
- return {
51
- namespace: 'ecopages-images',
52
- path: 'ecopages:images',
53
- };
54
- });
55
-
56
- build.onLoad({ filter: /.*/, namespace: 'ecopages-images' }, () => createPluginResult(exports));
57
- },
58
- };
59
- }
@@ -1,201 +0,0 @@
1
- import path from 'node:path';
2
- import { deepMerge } from '@ecopages/core/utils/deep-merge';
3
- import { fileSystem } from '@ecopages/file-system';
4
- import { Logger } from '@ecopages/logger';
5
- import sharp from 'sharp';
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
-
10
- const appLogger = new Logger('[@ecopages/image-processor]', {
11
- debug: process.env.ECOPAGES_LOGGER_DEBUG === 'true',
12
- });
13
-
14
- /**
15
- * ImageProcessor
16
- * This is the core class for processing images.
17
- * It uses the sharp library to resize and optimize images.
18
- */
19
- export class ImageProcessor {
20
- private readonly config: ImageProcessorConfig;
21
- private readonly cacheManager: {
22
- readCache: <T>(key: string) => Promise<T | null>;
23
- writeCache: <T>(key: string, data: T) => Promise<void>;
24
- };
25
-
26
- constructor(
27
- config: ImageProcessorConfig,
28
- cacheManager: {
29
- readCache: <T>(key: string) => Promise<T | null>;
30
- writeCache: <T>(key: string, data: T) => Promise<void>;
31
- },
32
- ) {
33
- this.config = deepMerge({ cacheEnabled: true }, config);
34
- this.cacheManager = cacheManager;
35
- fileSystem.ensureDir(this.config.outputDir);
36
- }
37
-
38
- private async calculateDimensions(metadata: sharp.Metadata, targetWidth: number) {
39
- const originalWidth = metadata.width || 0;
40
- const originalHeight = metadata.height || 0;
41
- const aspectRatio = originalHeight / originalWidth;
42
- const width = Math.min(targetWidth, originalWidth);
43
- const height = Math.round(width * aspectRatio);
44
- return { width, height };
45
- }
46
-
47
- private getOutputPath(imagePath: string, width: number) {
48
- const hash = fileSystem.hash(imagePath);
49
- const ext = path.extname(imagePath);
50
- const base = path.basename(imagePath, ext);
51
- const filename = `${base}-${hash}-${width}.${this.config.format}`;
52
- return path.join(this.config.outputDir, filename);
53
- }
54
-
55
- async processImage(imagePath: string): Promise<ImageSpecifications | null> {
56
- try {
57
- const fileHash = fileSystem.hash(imagePath);
58
- const cacheKey = `${path.basename(imagePath)}:${fileHash}`;
59
-
60
- if (this.config.cacheEnabled) {
61
- const cached = await this.cacheManager.readCache<ImageSpecifications>(cacheKey);
62
- if (cached) {
63
- /**
64
- * Verify that the files actually exist
65
- * We construct the absolute path relative to the process current working directory
66
- * since the src in attributes is relative from the root
67
- */
68
- const mainFilePath = path.join(process.cwd(), cached.attributes.src);
69
- const mainFileExists = fileSystem.exists(mainFilePath);
70
- const variantsExist = cached.variants.every((v) =>
71
- fileSystem.exists(path.join(process.cwd(), v.src)),
72
- );
73
-
74
- if (mainFileExists && variantsExist) {
75
- appLogger.debug(`Cache hit for ${imagePath}`);
76
- return cached;
77
- }
78
-
79
- appLogger.debug(`Cache invalid for ${imagePath}, reprocessing`);
80
- }
81
- }
82
-
83
- fileSystem.ensureDir(this.config.outputDir);
84
-
85
- const metadata = await sharp(imagePath).metadata();
86
- const originalWidth = metadata.width || 0;
87
- const originalHeight = metadata.height || 0;
88
-
89
- if (this.config.sizes.length === 0) {
90
- const outputPath = this.getOutputPath(imagePath, originalWidth);
91
-
92
- if (fileSystem.exists(outputPath)) {
93
- appLogger.debug(`Using existing file for ${imagePath}`);
94
- } else {
95
- await sharp(imagePath)
96
- .toFormat(this.config.format, { quality: this.config.quality })
97
- .toFile(outputPath);
98
- }
99
-
100
- const src = path.join(this.config.publicPath, path.basename(outputPath));
101
-
102
- const imageSpecifications: ImageSpecifications = {
103
- attributes: {
104
- src,
105
- width: originalWidth,
106
- height: originalHeight,
107
- sizes: '',
108
- },
109
- variants: [],
110
- cacheKey,
111
- };
112
-
113
- if (this.config.cacheEnabled) {
114
- await this.cacheManager.writeCache(cacheKey, imageSpecifications);
115
- }
116
-
117
- return imageSpecifications;
118
- }
119
-
120
- let applicableSizes = this.config.sizes
121
- .filter((size) => size.width <= originalWidth)
122
- .sort((a, b) => b.width - a.width);
123
-
124
- if (applicableSizes.length === 0) {
125
- applicableSizes = this.config.sizes.sort((a, b) => b.width - a.width).slice(0, 1);
126
- }
127
-
128
- const variants: ImageVariant[] = await Promise.all(
129
- applicableSizes.map(async ({ width: targetWidth, label }) => {
130
- const { width, height } = await this.calculateDimensions(metadata, targetWidth);
131
- const outputPath = this.getOutputPath(imagePath, width);
132
-
133
- if (fileSystem.exists(outputPath)) {
134
- appLogger.debug(`Variant ${width}px already exists for ${imagePath}`);
135
- } else {
136
- await sharp(imagePath)
137
- .resize(width, height)
138
- .toFormat(this.config.format, { quality: this.config.quality })
139
- .toFile(outputPath);
140
- }
141
-
142
- const src = path.join(this.config.publicPath, path.basename(outputPath));
143
-
144
- return {
145
- width,
146
- height,
147
- src,
148
- label,
149
- };
150
- }),
151
- );
152
-
153
- const mainVariant = variants[0];
154
- const attributes: ImageAttributes = {
155
- src: mainVariant.src,
156
- width: mainVariant.width,
157
- height: mainVariant.height,
158
- sizes: ImageUtils.generateSizes(variants),
159
- srcset: ImageUtils.generateSrcset(variants),
160
- };
161
-
162
- const imageSpecifications: ImageSpecifications = {
163
- attributes,
164
- variants,
165
- cacheKey,
166
- };
167
-
168
- if (this.config.cacheEnabled) {
169
- await this.cacheManager.writeCache(cacheKey, imageSpecifications);
170
- }
171
-
172
- return imageSpecifications;
173
- } catch (error) {
174
- appLogger.error(`Failed to process image ${imagePath}:`, error as Error);
175
- return null;
176
- }
177
- }
178
-
179
- async processDirectory(): Promise<ImageMap> {
180
- const acceptedFormats = this.config.acceptedFormats || ['jpg', 'jpeg', 'png', 'webp'];
181
-
182
- const images = await fileSystem.glob([`${this.config.sourceDir}/**/*.{${acceptedFormats.join(',')}}`]);
183
-
184
- appLogger.debugTime('Processing images');
185
-
186
- const results = (
187
- await Promise.all(
188
- images.map(async (file) => {
189
- const processed = await this.processImage(file);
190
- if (!processed) return null;
191
- return [path.basename(file), processed] as [string, ImageSpecifications];
192
- }),
193
- )
194
- ).filter(Boolean) as [string, ImageSpecifications][];
195
-
196
- appLogger.debugTimeEnd('Processing images');
197
- appLogger.info(`Processed ${results.length} images`);
198
-
199
- return Object.fromEntries(results);
200
- }
201
- }