@ecopages/image-processor 0.3.0 → 0.3.2
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 +1 -1
- package/package.json +3 -4
- package/src/image-plugins.d.ts +5 -2
- package/src/image-plugins.js +10 -4
- package/src/image-runtime-state.d.ts +4 -0
- package/src/image-runtime-state.js +7 -1
- package/src/plugin.d.ts +23 -9
- package/src/plugin.js +74 -42
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ It provides automatic image processing (e.g. converting and compressing to WebP)
|
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
Build contributions use one `plugins(app)` method with server and browser environment declarations. Image processing, type generation and the cache restore run in `setup()`; `ecopages types` runs it. The virtual image module resolves through the implementation for the requested environment.
|
|
9
|
+
Build contributions use one `plugins(app)` method with server and browser environment declarations. Image processing, type generation and the cache restore run in `setup()`; `ecopages types` runs it. The virtual image module resolves through the implementation for the requested environment. In a process where `setup()` has not run, such as the Lit static render worker, the first load of `ecopages:images` restores the image map from the outputs already on disk.
|
|
10
10
|
|
|
11
11
|
- **Automatic Image Optimization**: Converts and compresses images to modern formats at build time. Each variant is named from a hash of its encoded bytes, so a quality or format change gets a new URL.
|
|
12
12
|
- **Responsive Image Generation**: Creates multiple variants for different screen sizes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecopages/image-processor",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Image processor, transform and optimize images for web",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"image",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"directory": "packages/processors/image-processor"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"@ecopages/core": "0.3.
|
|
21
|
+
"@ecopages/core": "0.3.2",
|
|
22
22
|
"@ecopages/jsx": "^0.4.0"
|
|
23
23
|
},
|
|
24
24
|
"peerDependenciesMeta": {
|
|
@@ -27,10 +27,9 @@
|
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@ecopages/file-system": "0.3.
|
|
30
|
+
"@ecopages/file-system": "0.3.2",
|
|
31
31
|
"@ecopages/logger": "^0.2.3",
|
|
32
32
|
"react": "^19.2.6",
|
|
33
|
-
"react-dom": "^19.2.6",
|
|
34
33
|
"sharp": "^0.35.3"
|
|
35
34
|
},
|
|
36
35
|
"exports": {
|
package/src/image-plugins.d.ts
CHANGED
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { EcoBuildPlugin } from '@ecopages/core/processor';
|
|
6
6
|
import type { ImageMap } from './plugin.js';
|
|
7
|
+
/** Fills the image map before the module is loaded. */
|
|
8
|
+
type EnsureImagesReady = () => Promise<void>;
|
|
7
9
|
/**
|
|
8
10
|
* Declares `ecopages:images` as a virtual module for server builds.
|
|
9
11
|
*/
|
|
10
|
-
export declare function createImagePlugin(exports: ImageMap): EcoBuildPlugin;
|
|
12
|
+
export declare function createImagePlugin(exports: ImageMap, ensureReady: EnsureImagesReady): EcoBuildPlugin;
|
|
11
13
|
/**
|
|
12
14
|
* Resolves `ecopages:images` with `onResolve` and `onLoad` for browser builds.
|
|
13
15
|
*/
|
|
14
|
-
export declare function createImagePluginBundler(exports: ImageMap): EcoBuildPlugin;
|
|
16
|
+
export declare function createImagePluginBundler(exports: ImageMap, ensureReady: EnsureImagesReady): EcoBuildPlugin;
|
|
17
|
+
export {};
|
package/src/image-plugins.js
CHANGED
|
@@ -17,18 +17,21 @@ function createPluginResult(exports) {
|
|
|
17
17
|
/**
|
|
18
18
|
* Declares `ecopages:images` as a virtual module for server builds.
|
|
19
19
|
*/
|
|
20
|
-
export function createImagePlugin(exports) {
|
|
20
|
+
export function createImagePlugin(exports, ensureReady) {
|
|
21
21
|
return {
|
|
22
22
|
name: 'ecopages:images',
|
|
23
23
|
setup(build) {
|
|
24
|
-
build.module('ecopages:images', () =>
|
|
24
|
+
build.module('ecopages:images', async () => {
|
|
25
|
+
await ensureReady();
|
|
26
|
+
return createPluginResult(exports);
|
|
27
|
+
});
|
|
25
28
|
},
|
|
26
29
|
};
|
|
27
30
|
}
|
|
28
31
|
/**
|
|
29
32
|
* Resolves `ecopages:images` with `onResolve` and `onLoad` for browser builds.
|
|
30
33
|
*/
|
|
31
|
-
export function createImagePluginBundler(exports) {
|
|
34
|
+
export function createImagePluginBundler(exports, ensureReady) {
|
|
32
35
|
return {
|
|
33
36
|
name: 'ecopages:images',
|
|
34
37
|
setup(build) {
|
|
@@ -38,7 +41,10 @@ export function createImagePluginBundler(exports) {
|
|
|
38
41
|
path: 'ecopages:images',
|
|
39
42
|
};
|
|
40
43
|
});
|
|
41
|
-
build.onLoad({ filter: /.*/, namespace: 'ecopages-images' }, () =>
|
|
44
|
+
build.onLoad({ filter: /.*/, namespace: 'ecopages-images' }, async () => {
|
|
45
|
+
await ensureReady();
|
|
46
|
+
return createPluginResult(exports);
|
|
47
|
+
});
|
|
42
48
|
},
|
|
43
49
|
};
|
|
44
50
|
}
|
|
@@ -10,6 +10,10 @@ export type ImageProcessorDiskConfig = {
|
|
|
10
10
|
*/
|
|
11
11
|
export declare function parseVirtualModuleExports(content: string): Record<string, ImageSpecifications> | null;
|
|
12
12
|
export declare function getSourceImagePaths(resolvedConfig: ImageProcessorDiskConfig): Promise<string[]>;
|
|
13
|
+
/**
|
|
14
|
+
* Lists the public paths of every file a specification points at.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getOutputSources(spec: ImageSpecifications): string[];
|
|
13
17
|
/**
|
|
14
18
|
* Restores processed image state from generated dist artifacts when every source
|
|
15
19
|
* image has a matching virtual-module export and output files on disk.
|
|
@@ -22,6 +22,12 @@ export async function getSourceImagePaths(resolvedConfig) {
|
|
|
22
22
|
const acceptedFormats = resolvedConfig.acceptedFormats ?? ['jpg', 'jpeg', 'png', 'webp'];
|
|
23
23
|
return fileSystem.glob([`${resolvedConfig.sourceDir}/**/*.{${acceptedFormats.join(',')}}`]);
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Lists the public paths of every file a specification points at.
|
|
27
|
+
*/
|
|
28
|
+
export function getOutputSources(spec) {
|
|
29
|
+
return [spec.attributes.src, ...spec.variants.map((variant) => variant.src)];
|
|
30
|
+
}
|
|
25
31
|
function getGeneratedOutputPath(outputDir, src) {
|
|
26
32
|
return path.join(outputDir, path.basename(src));
|
|
27
33
|
}
|
|
@@ -49,7 +55,7 @@ export async function loadProcessedImagesFromDisk(options) {
|
|
|
49
55
|
if (!spec) {
|
|
50
56
|
return null;
|
|
51
57
|
}
|
|
52
|
-
const outputPaths =
|
|
58
|
+
const outputPaths = getOutputSources(spec);
|
|
53
59
|
if (!outputPaths.every((src) => fileSystem.exists(getGeneratedOutputPath(options.resolvedConfig.outputDir, src)))) {
|
|
54
60
|
return null;
|
|
55
61
|
}
|
package/src/plugin.d.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* @module @ecopages/image-processor
|
|
4
4
|
*/
|
|
5
5
|
import { Processor, type EcoBuildPlugin, type ProcessorConfig } from '@ecopages/core/processor';
|
|
6
|
-
import { ImageProcessor } from './image-processor.js';
|
|
7
6
|
import type { ImageSize, ImageSpecifications } from './types.js';
|
|
8
7
|
/**
|
|
9
8
|
* Configuration for the image processor
|
|
@@ -41,6 +40,7 @@ export declare class ImageProcessorPlugin extends Processor<ImageProcessorConfig
|
|
|
41
40
|
private processor;
|
|
42
41
|
private resolvedConfig?;
|
|
43
42
|
processedImages: Record<string, ImageSpecifications>;
|
|
43
|
+
private preparation?;
|
|
44
44
|
constructor(config: Omit<ProcessorConfig<ImageProcessorConfig>, 'name'>);
|
|
45
45
|
private handleImageChanges;
|
|
46
46
|
plugins(): EcoBuildPlugin[];
|
|
@@ -57,7 +57,6 @@ export declare class ImageProcessorPlugin extends Processor<ImageProcessorConfig
|
|
|
57
57
|
*/
|
|
58
58
|
private initializeProcessor;
|
|
59
59
|
private getRuntimeVirtualModulePath;
|
|
60
|
-
private getGeneratedOutputPath;
|
|
61
60
|
/**
|
|
62
61
|
* Restores in-memory image state from previously generated dist artifacts.
|
|
63
62
|
*
|
|
@@ -70,16 +69,36 @@ export declare class ImageProcessorPlugin extends Processor<ImageProcessorConfig
|
|
|
70
69
|
* Ensures optimized image outputs and virtual-module state exist on disk.
|
|
71
70
|
*/
|
|
72
71
|
setup(): Promise<void>;
|
|
72
|
+
private prepare;
|
|
73
|
+
/**
|
|
74
|
+
* Prepares the image map on first load of `ecopages:images` when `setup()` has not run in this process.
|
|
75
|
+
*
|
|
76
|
+
* @remarks
|
|
77
|
+
* The Lit static render worker skips Processor setup because the main thread already wrote the outputs, so
|
|
78
|
+
* the worker's map starts empty and is restored from those outputs here.
|
|
79
|
+
*/
|
|
80
|
+
private ensurePrepared;
|
|
73
81
|
/**
|
|
74
82
|
* Process images.
|
|
75
83
|
* @param images
|
|
76
84
|
*/
|
|
77
85
|
process(images: string[]): Promise<void>;
|
|
78
86
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
87
|
+
* Deletes the optimized files and cache entry recorded for a removed source image.
|
|
88
|
+
*
|
|
89
|
+
* @remarks
|
|
90
|
+
* Variant files are named after a hash of their bytes, so the files to delete are
|
|
91
|
+
* the ones the recorded specification points at, not a pattern on the source name.
|
|
81
92
|
*/
|
|
82
93
|
deleteProcessedImagesbyPath(imagePath: string): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Removes the cache entry and the output files of `previous` that `next` no longer uses.
|
|
96
|
+
*
|
|
97
|
+
* @remarks
|
|
98
|
+
* Identical encoded bytes share one hashed file across source images, so a file
|
|
99
|
+
* is only removed once no remaining specification references it.
|
|
100
|
+
*/
|
|
101
|
+
private removeSupersededOutputs;
|
|
83
102
|
/**
|
|
84
103
|
* Generate types for the virtual module.
|
|
85
104
|
*/
|
|
@@ -88,10 +107,5 @@ export declare class ImageProcessorPlugin extends Processor<ImageProcessorConfig
|
|
|
88
107
|
* Writes the ambient declaration package for `ecopages:images`.
|
|
89
108
|
*/
|
|
90
109
|
private writeVirtualModuleTypes;
|
|
91
|
-
/**
|
|
92
|
-
* Get the image processor instance.
|
|
93
|
-
* @returns The image processor instance.
|
|
94
|
-
*/
|
|
95
|
-
getProcessor(): ImageProcessor | undefined;
|
|
96
110
|
}
|
|
97
111
|
export declare const imageProcessorPlugin: (config: Omit<ProcessorConfig<ImageProcessorConfig>, "name">) => ImageProcessorPlugin;
|
package/src/plugin.js
CHANGED
|
@@ -9,7 +9,7 @@ import { Logger } from '@ecopages/logger';
|
|
|
9
9
|
import { isEcopagesEnvEnabled } from '@ecopages/core/processor';
|
|
10
10
|
import { createImagePlugin, createImagePluginBundler } from './image-plugins.js';
|
|
11
11
|
import { ImageProcessor } from './image-processor.js';
|
|
12
|
-
import { getSourceImagePaths, loadProcessedImagesFromDisk } from './image-runtime-state.js';
|
|
12
|
+
import { getOutputSources, getSourceImagePaths, loadProcessedImagesFromDisk, } from './image-runtime-state.js';
|
|
13
13
|
import { anyCaseToCamelCase } from './utils.js';
|
|
14
14
|
const logger = new Logger('[@ecopages/image-processor]', {
|
|
15
15
|
debug: isEcopagesEnvEnabled('ECOPAGES_LOGGER_DEBUG'),
|
|
@@ -66,6 +66,7 @@ type ImageSize = {
|
|
|
66
66
|
export class ImageProcessorPlugin extends Processor {
|
|
67
67
|
resolvedConfig;
|
|
68
68
|
processedImages = {};
|
|
69
|
+
preparation;
|
|
69
70
|
constructor(config) {
|
|
70
71
|
const acceptedFormats = config.options?.acceptedFormats ?? ['jpg', 'jpeg', 'png', 'webp'];
|
|
71
72
|
const extensionPatterns = acceptedFormats.map((format) => `*.${format.replace(/^\./, '')}`);
|
|
@@ -98,8 +99,11 @@ export class ImageProcessorPlugin extends Processor {
|
|
|
98
99
|
}
|
|
99
100
|
plugins() {
|
|
100
101
|
return [
|
|
101
|
-
{ ...createImagePlugin(this.processedImages), environments: ['server'] },
|
|
102
|
-
{
|
|
102
|
+
{ ...createImagePlugin(this.processedImages, () => this.ensurePrepared()), environments: ['server'] },
|
|
103
|
+
{
|
|
104
|
+
...createImagePluginBundler(this.processedImages, () => this.ensurePrepared()),
|
|
105
|
+
environments: ['browser'],
|
|
106
|
+
},
|
|
103
107
|
];
|
|
104
108
|
}
|
|
105
109
|
/**
|
|
@@ -157,12 +161,6 @@ export class ImageProcessorPlugin extends Processor {
|
|
|
157
161
|
subPath: 'virtual-module.ts',
|
|
158
162
|
});
|
|
159
163
|
}
|
|
160
|
-
getGeneratedOutputPath(src) {
|
|
161
|
-
if (!this.resolvedConfig) {
|
|
162
|
-
throw new Error('ImageProcessor not initialized');
|
|
163
|
-
}
|
|
164
|
-
return path.join(this.resolvedConfig.outputDir, path.basename(src));
|
|
165
|
-
}
|
|
166
164
|
/**
|
|
167
165
|
* Restores in-memory image state from previously generated dist artifacts.
|
|
168
166
|
*
|
|
@@ -210,9 +208,24 @@ export class ImageProcessorPlugin extends Processor {
|
|
|
210
208
|
* Ensures optimized image outputs and virtual-module state exist on disk.
|
|
211
209
|
*/
|
|
212
210
|
async setup() {
|
|
211
|
+
this.preparation = this.prepare();
|
|
212
|
+
await this.preparation;
|
|
213
|
+
}
|
|
214
|
+
async prepare() {
|
|
213
215
|
this.initializeProcessor();
|
|
214
216
|
await this.ensureGeneratedOutputs();
|
|
215
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Prepares the image map on first load of `ecopages:images` when `setup()` has not run in this process.
|
|
220
|
+
*
|
|
221
|
+
* @remarks
|
|
222
|
+
* The Lit static render worker skips Processor setup because the main thread already wrote the outputs, so
|
|
223
|
+
* the worker's map starts empty and is restored from those outputs here.
|
|
224
|
+
*/
|
|
225
|
+
ensurePrepared() {
|
|
226
|
+
this.preparation ??= this.prepare();
|
|
227
|
+
return this.preparation;
|
|
228
|
+
}
|
|
216
229
|
/**
|
|
217
230
|
* Process images.
|
|
218
231
|
* @param images
|
|
@@ -226,7 +239,12 @@ export class ImageProcessorPlugin extends Processor {
|
|
|
226
239
|
try {
|
|
227
240
|
const result = await this.processor.processImage(image);
|
|
228
241
|
if (result) {
|
|
229
|
-
|
|
242
|
+
const key = path.basename(image);
|
|
243
|
+
const previous = this.processedImages[key];
|
|
244
|
+
this.processedImages[key] = result;
|
|
245
|
+
if (previous) {
|
|
246
|
+
await this.removeSupersededOutputs(previous, result);
|
|
247
|
+
}
|
|
230
248
|
}
|
|
231
249
|
}
|
|
232
250
|
catch (error) {
|
|
@@ -236,44 +254,65 @@ export class ImageProcessorPlugin extends Processor {
|
|
|
236
254
|
this.generateTypes();
|
|
237
255
|
}
|
|
238
256
|
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
257
|
+
* Deletes the optimized files and cache entry recorded for a removed source image.
|
|
258
|
+
*
|
|
259
|
+
* @remarks
|
|
260
|
+
* Variant files are named after a hash of their bytes, so the files to delete are
|
|
261
|
+
* the ones the recorded specification points at, not a pattern on the source name.
|
|
241
262
|
*/
|
|
242
263
|
async deleteProcessedImagesbyPath(imagePath) {
|
|
243
264
|
if (!this.processor) {
|
|
244
265
|
throw new Error('ImageProcessor not initialized');
|
|
245
266
|
}
|
|
246
267
|
logger.debug('Deleting processed images', { path: imagePath });
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
268
|
+
const key = path.basename(imagePath);
|
|
269
|
+
const previous = this.processedImages[key];
|
|
270
|
+
if (!previous) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
delete this.processedImages[key];
|
|
274
|
+
await this.removeSupersededOutputs(previous);
|
|
275
|
+
this.generateTypes();
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Removes the cache entry and the output files of `previous` that `next` no longer uses.
|
|
279
|
+
*
|
|
280
|
+
* @remarks
|
|
281
|
+
* Identical encoded bytes share one hashed file across source images, so a file
|
|
282
|
+
* is only removed once no remaining specification references it.
|
|
283
|
+
*/
|
|
284
|
+
async removeSupersededOutputs(previous, next) {
|
|
285
|
+
if (!this.resolvedConfig) {
|
|
286
|
+
throw new Error('ImageProcessor not initialized');
|
|
287
|
+
}
|
|
288
|
+
const referenced = new Set();
|
|
289
|
+
for (const spec of [...Object.values(this.processedImages), ...(next ? [next] : [])]) {
|
|
290
|
+
for (const src of getOutputSources(spec)) {
|
|
291
|
+
referenced.add(path.basename(src));
|
|
251
292
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
293
|
+
}
|
|
294
|
+
const files = getOutputSources(previous)
|
|
295
|
+
.map((src) => path.basename(src))
|
|
296
|
+
.filter((name) => !referenced.has(name));
|
|
297
|
+
for (const file of new Set(files)) {
|
|
298
|
+
const filePath = path.join(this.resolvedConfig.outputDir, file);
|
|
299
|
+
try {
|
|
300
|
+
await fileSystem.removeAsync(filePath);
|
|
301
|
+
logger.debug('Deleted processed image', { file: filePath });
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
logger.error('Failed to delete processed image', { file: filePath, error });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (previous.cacheKey !== next?.cacheKey) {
|
|
308
|
+
const cachePath = this.getCachePath(previous.cacheKey);
|
|
263
309
|
try {
|
|
264
|
-
const cacheKey = this.processedImages[path.basename(imagePath)].cacheKey;
|
|
265
|
-
const cachePath = this.getCachePath(cacheKey);
|
|
266
310
|
await fileSystem.removeAsync(cachePath);
|
|
267
311
|
logger.debug('Deleted cache file for image', { cachePath });
|
|
268
312
|
}
|
|
269
313
|
catch (error) {
|
|
270
|
-
logger.error('Failed to delete cache file for image', {
|
|
314
|
+
logger.error('Failed to delete cache file for image', { cachePath, error });
|
|
271
315
|
}
|
|
272
|
-
delete this.processedImages[path.basename(imagePath)];
|
|
273
|
-
this.generateTypes();
|
|
274
|
-
}
|
|
275
|
-
catch (error) {
|
|
276
|
-
logger.error('Failed to delete processed images', { path: imagePath, error });
|
|
277
316
|
}
|
|
278
317
|
}
|
|
279
318
|
/**
|
|
@@ -335,13 +374,6 @@ declare module "ecopages:images" {
|
|
|
335
374
|
writeFile: (filePath, content) => this.writeGeneratedFile(filePath, content),
|
|
336
375
|
});
|
|
337
376
|
}
|
|
338
|
-
/**
|
|
339
|
-
* Get the image processor instance.
|
|
340
|
-
* @returns The image processor instance.
|
|
341
|
-
*/
|
|
342
|
-
getProcessor() {
|
|
343
|
-
return this.processor;
|
|
344
|
-
}
|
|
345
377
|
}
|
|
346
378
|
export const imageProcessorPlugin = (config) => {
|
|
347
379
|
return new ImageProcessorPlugin(config);
|