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

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/src/plugin.ts DELETED
@@ -1,336 +0,0 @@
1
- /**
2
- * ImageProcessorPlugin
3
- * @module @ecopages/image-processor
4
- */
5
-
6
- import path from 'node:path';
7
- import { fileURLToPath } from 'node:url';
8
- import { deepMerge } from '@ecopages/core/utils/deep-merge';
9
- import { GENERATED_BASE_PATHS } from '@ecopages/core/constants';
10
- import { fileSystem } from '@ecopages/file-system';
11
- import { Processor, type ProcessorConfig, type ProcessorWatchConfig } from '@ecopages/core/plugins/processor';
12
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
13
- import type { AssetDefinition } from '@ecopages/core/services/asset-processing-service';
14
- 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';
19
-
20
- function resolveGeneratedPath(
21
- type: keyof typeof GENERATED_BASE_PATHS,
22
- options: { root: string; module: string; subPath?: string },
23
- ): string {
24
- const { root, module, subPath } = options;
25
- const parts = [root, GENERATED_BASE_PATHS[type], module, subPath].filter(Boolean);
26
- return path.join(...(parts as string[]));
27
- }
28
-
29
- const logger = new Logger('[@ecopages/image-processor]', {
30
- debug: process.env.ECOPAGES_LOGGER_DEBUG === 'true',
31
- });
32
-
33
- const currentDir = path.dirname(fileURLToPath(import.meta.url));
34
-
35
- /**
36
- * Configuration for the image processor
37
- */
38
- export interface ImageProcessorConfig {
39
- sourceDir: string;
40
- outputDir: string;
41
- publicPath: string;
42
- /**
43
- * @default []
44
- */
45
- sizes: ImageSize[];
46
- quality: number;
47
- format: 'webp' | 'jpeg' | 'png' | 'avif';
48
- /**
49
- * Optional list of accepted image formats
50
- * @default ["jpg", "jpeg", "png", "webp"]
51
- */
52
- acceptedFormats?: string[];
53
- /**
54
- * @default true
55
- */
56
- cacheEnabled?: boolean;
57
- }
58
-
59
- /**
60
- * ImageMap
61
- * This is the representation of the image map in the virtual module
62
- */
63
- export type ImageMap = Record<string, ImageSpecifications>;
64
-
65
- /**
66
- * ImageProcessorPlugin
67
- * A Processor for optimizing images.
68
- */
69
- export class ImageProcessorPlugin extends Processor<ImageProcessorConfig> {
70
- declare private processor: ImageProcessor;
71
- public processedImages: Record<string, ImageSpecifications> = {};
72
-
73
- constructor(config: Omit<ProcessorConfig<ImageProcessorConfig>, 'name' | 'description'>) {
74
- const acceptedFormats = config.options?.acceptedFormats ?? ['jpg', 'jpeg', 'png', 'webp'];
75
- const extensionPatterns = acceptedFormats.map((format) => `*.${format.replace(/^\./, '')}`);
76
-
77
- const defaultWatchConfig: ProcessorWatchConfig = {
78
- paths: [],
79
- extensions: acceptedFormats,
80
- onCreate: async (ctx) => this.process([ctx.path]),
81
- onChange: async (ctx) => this.process([ctx.path]),
82
- onDelete: async (ctx) => this.deleteProcessedImagesbyPath(ctx.path),
83
- onError: (error) => logger.error('Watcher error', { error }),
84
- };
85
-
86
- super({
87
- ...config,
88
- name: 'ecopages-image-processor',
89
- description: 'A Processor for optimizing images.',
90
- capabilities: [
91
- {
92
- kind: 'image',
93
- extensions: extensionPatterns,
94
- },
95
- ],
96
- watch: config.watch ? deepMerge(defaultWatchConfig, config.watch) : defaultWatchConfig,
97
- });
98
- }
99
-
100
- get buildPlugins(): EcoBuildPlugin[] {
101
- return [createImagePluginBundler(this.processedImages)];
102
- }
103
-
104
- get plugins(): EcoBuildPlugin[] {
105
- return [createImagePlugin(this.processedImages)];
106
- }
107
-
108
- /**
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
112
- */
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
- */
129
- }
130
-
131
- return deps;
132
- }
133
-
134
- /**
135
- * Setup the image processor and create the virtual module.
136
- */
137
- async setup(): Promise<void> {
138
- if (!this.context) {
139
- throw new Error('ImageProcessor requires context to be set');
140
- }
141
-
142
- logger.debug('Setting up image processor', {
143
- srcDir: this.context.srcDir,
144
- distDir: this.context.distDir,
145
- });
146
-
147
- const defaultConfig = {
148
- sourceDir: `${this.context.srcDir}/public/assets/images`,
149
- outputDir: `${this.context.distDir}/assets/optimized`,
150
- publicPath: '/assets/optimized',
151
- sizes: [],
152
- quality: 80,
153
- format: 'webp' as const,
154
- };
155
-
156
- const config = this.options ? deepMerge(defaultConfig, this.options) : defaultConfig;
157
-
158
- this.processor = new ImageProcessor(config, {
159
- readCache: (key) => this.readCache(key),
160
- writeCache: (key, data) => this.writeCache(key, data),
161
- });
162
-
163
- this.processedImages = await this.processor.processDirectory();
164
-
165
- if (this.watchConfig) {
166
- this.watchConfig.paths = [config.sourceDir];
167
- }
168
-
169
- this.dependencies = this.generateDependencies();
170
-
171
- this.generateTypes();
172
- }
173
-
174
- /**
175
- * Process images.
176
- * @param images
177
- */
178
- async process(images: string[]): Promise<void> {
179
- if (!this.processor) {
180
- throw new Error('ImageProcessor not initialized');
181
- }
182
-
183
- logger.debug('Processing images', { count: images.length });
184
-
185
- for (const image of images) {
186
- try {
187
- const result = await this.processor.processImage(image);
188
- if (result) {
189
- this.processedImages[path.basename(image)] = result;
190
- }
191
- } catch (error) {
192
- logger.error('Failed to process image', { image, error });
193
- }
194
- }
195
-
196
- this.generateTypes();
197
- }
198
-
199
- /**
200
- * Delete processed images using the original image path.
201
- * @param imagePath
202
- */
203
- async deleteProcessedImagesbyPath(imagePath: string): Promise<void> {
204
- if (!this.processor) {
205
- throw new Error('ImageProcessor not initialized');
206
- }
207
-
208
- logger.debug('Deleting processed images', { path: imagePath });
209
-
210
- try {
211
- const baseNameWithoutExt = path.basename(imagePath, path.extname(imagePath));
212
-
213
- if (!this.options) {
214
- throw new Error('Options not set');
215
- }
216
-
217
- const outputDir = this.options.outputDir;
218
-
219
- const files = await fileSystem.glob([`${outputDir}/${baseNameWithoutExt}-*`]);
220
-
221
- await Promise.all(
222
- files.map(async (file) => {
223
- try {
224
- await fileSystem.removeAsync(file);
225
- logger.debug('Deleted processed image', { file });
226
- } catch (error) {
227
- logger.error('Failed to delete processed image', { file, error });
228
- }
229
- }),
230
- );
231
-
232
- try {
233
- const cacheKey = this.processedImages[path.basename(imagePath)].cacheKey;
234
- const cachePath = this.getCachePath(cacheKey);
235
- await fileSystem.removeAsync(cachePath);
236
- logger.debug('Deleted cache file for image', { cachePath });
237
- } catch (error) {
238
- logger.error('Failed to delete cache file for image', { imagePath, error });
239
- }
240
-
241
- delete this.processedImages[path.basename(imagePath)];
242
-
243
- this.generateTypes();
244
- } catch (error) {
245
- logger.error('Failed to delete processed images', { path: imagePath, error });
246
- }
247
- }
248
-
249
- /**
250
- * Generate types for the virtual module.
251
- */
252
- private generateTypes(): void {
253
- if (!this.options?.outputDir) {
254
- throw new Error('Output directory not set');
255
- }
256
-
257
- const requiredTypes = fileSystem
258
- .readFileSync(path.join(currentDir, 'types.ts'))
259
- .toString()
260
- .replaceAll('export ', '');
261
-
262
- const content = `
263
- /**
264
- * Do not edit manually. This file is auto-generated.
265
- * This file contains the type definitions for the virtual module "ecopages:images".
266
- */
267
-
268
- ${requiredTypes}
269
-
270
- declare module "ecopages:images" {
271
- ${Object.keys(this.processedImages)
272
- .map((key) => `export const ${anyCaseToCamelCase(key)}: ImageSpecifications;`)
273
- .join('\n ')}
274
- }`;
275
-
276
- if (!this.context) throw new Error('Processor is not configured correctly');
277
-
278
- const typesDir = resolveGeneratedPath('types', {
279
- root: this.context.rootDir,
280
- module: this.name,
281
- subPath: 'virtual-module.d.ts',
282
- });
283
-
284
- fileSystem.ensureDir(path.dirname(typesDir));
285
- fileSystem.write(typesDir, content);
286
- logger.debug('Generated types for virtual module', { typesDir });
287
-
288
- const indexTypesDir = resolveGeneratedPath('types', {
289
- root: this.context.rootDir,
290
- module: this.name,
291
- subPath: 'index.d.ts',
292
- });
293
-
294
- const indexContent = 'import "./virtual-module.d.ts";';
295
-
296
- fileSystem.write(indexTypesDir, indexContent);
297
- logger.debug('Generated index types for virtual module', { indexTypesDir });
298
-
299
- const runtimeVirtualModulePath = resolveGeneratedPath('cache', {
300
- root: this.context.distDir,
301
- module: this.name,
302
- subPath: 'virtual-module.ts',
303
- });
304
-
305
- const runtimeModuleContent = Object.entries(this.processedImages)
306
- .map(([key, value]) => {
307
- return `export const ${anyCaseToCamelCase(key)} = ${JSON.stringify(value, null, 2)} as const;`;
308
- })
309
- .join('\n\n');
310
-
311
- fileSystem.ensureDir(path.dirname(runtimeVirtualModulePath));
312
- fileSystem.write(runtimeVirtualModulePath, runtimeModuleContent);
313
- logger.debug('Generated runtime virtual module for images', { runtimeVirtualModulePath });
314
- }
315
-
316
- /**
317
- * Teardown the image processor.
318
- */
319
- async teardown(): Promise<void> {
320
- logger.debug('Tearing down image processor');
321
- }
322
-
323
- /**
324
- * Get the image processor instance.
325
- * @returns The image processor instance.
326
- */
327
- getProcessor(): ImageProcessor | undefined {
328
- return this.processor;
329
- }
330
- }
331
-
332
- export const imageProcessorPlugin = (
333
- config: Omit<ProcessorConfig<ImageProcessorConfig>, 'name' | 'description'>,
334
- ): ImageProcessorPlugin => {
335
- return new ImageProcessorPlugin(config);
336
- };
package/src/types.ts DELETED
@@ -1,45 +0,0 @@
1
- /**
2
- * ImageAttributes
3
- * These are the core attributes for the image element generated by the image processor
4
- */
5
- export interface ImageAttributes {
6
- src: string;
7
- width: number;
8
- height: number;
9
- sizes: string;
10
- srcset?: string;
11
- }
12
-
13
- /**
14
- * This represents a single image variant created using the size configuration
15
- */
16
- export interface ImageVariant {
17
- width: number;
18
- height: number;
19
- src: string;
20
- label: string;
21
- }
22
-
23
- /**
24
- * These are the core attributes for the image element and the image variants
25
- * This is the representation of the image element in the virtual module
26
- */
27
- export interface ImageSpecifications {
28
- attributes: ImageAttributes;
29
- variants: ImageVariant[];
30
- /**
31
- * A unique key used to cache the image specifications.
32
- * This key should uniquely identify the combination of attributes and variants
33
- * to ensure proper caching behavior.
34
- */
35
- cacheKey: string;
36
- }
37
-
38
- /**
39
- * This is the representation of an image breakpoint
40
- * This is used to generate the srcset attribute
41
- */
42
- export type ImageSize = {
43
- width: number;
44
- label: string;
45
- };
package/src/utils.ts DELETED
@@ -1,30 +0,0 @@
1
- export function anyCaseToCamelCase(str: string): string {
2
- if (!str) return '';
3
-
4
- let result = '';
5
- let capitalize = false;
6
-
7
- for (let i = 0; i < str.length; i++) {
8
- const char = str[i];
9
-
10
- if (/[0-9]/.test(char)) {
11
- result += char;
12
- capitalize = true;
13
- continue;
14
- }
15
-
16
- if (/[a-zA-Z]/.test(char)) {
17
- if (capitalize) {
18
- result += char.toUpperCase();
19
- capitalize = false;
20
- } else {
21
- result += char.toLowerCase();
22
- }
23
- continue;
24
- }
25
-
26
- capitalize = true;
27
- }
28
-
29
- return result;
30
- }