@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/README.md +41 -151
- package/package.json +7 -7
- package/src/bun-plugins.d.ts +2 -2
- package/src/bun-plugins.js +1 -1
- package/src/component/html.d.ts +1 -1
- package/src/component/html.js +1 -1
- package/src/component/react.d.ts +1 -1
- package/src/component/react.js +1 -1
- package/src/constants.d.ts +1 -1
- package/src/image-plugins.d.ts +2 -2
- package/src/image-plugins.js +1 -1
- package/src/image-processor.d.ts +2 -2
- package/src/image-processor.js +3 -3
- package/src/image-renderer.d.ts +1 -1
- package/src/image-renderer.js +2 -2
- package/src/image-utils.d.ts +2 -2
- package/src/image-utils.js +1 -1
- package/src/index.d.ts +3 -3
- package/src/index.js +3 -3
- package/src/plugin.d.ts +23 -5
- package/src/plugin.js +132 -21
- package/CHANGELOG.md +0 -23
- package/src/bun-plugins.ts +0 -59
- package/src/component/html.ts +0 -15
- package/src/component/react.ts +0 -20
- package/src/constants.ts +0 -7
- package/src/image-plugins.ts +0 -59
- package/src/image-processor.ts +0 -201
- package/src/image-renderer.ts +0 -427
- package/src/image-utils.ts +0 -128
- package/src/index.ts +0 -3
- package/src/plugin.ts +0 -336
- package/src/types.ts +0 -45
- package/src/utils.ts +0 -30
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
|
-
}
|