@ecopages/image-processor 0.2.0-alpha.1
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/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/package.json +63 -0
- package/src/bun-plugins.d.ts +22 -0
- package/src/bun-plugins.js +33 -0
- package/src/bun-plugins.ts +59 -0
- package/src/component/html.d.ts +11 -0
- package/src/component/html.js +7 -0
- package/src/component/html.ts +15 -0
- package/src/component/react.d.ts +13 -0
- package/src/component/react.js +11 -0
- package/src/component/react.ts +20 -0
- package/src/constants.d.ts +6 -0
- package/src/constants.js +4 -0
- package/src/constants.ts +7 -0
- package/src/image-plugins.d.ts +22 -0
- package/src/image-plugins.js +33 -0
- package/src/image-plugins.ts +59 -0
- package/src/image-processor.d.ts +19 -0
- package/src/image-processor.js +141 -0
- package/src/image-processor.ts +201 -0
- package/src/image-renderer.d.ts +152 -0
- package/src/image-renderer.js +222 -0
- package/src/image-renderer.ts +427 -0
- package/src/image-utils.d.ts +45 -0
- package/src/image-utils.js +103 -0
- package/src/image-utils.ts +128 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +3 -0
- package/src/index.ts +3 -0
- package/src/plugin.d.ts +81 -0
- package/src/plugin.js +223 -0
- package/src/plugin.ts +336 -0
- package/src/types.d.ts +42 -0
- package/src/types.js +0 -0
- package/src/types.ts +45 -0
- package/src/utils.d.ts +1 -0
- package/src/utils.js +27 -0
- package/src/utils.ts +30 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { DEFAULT_LAYOUT } from "./constants";
|
|
2
|
+
class ImageUtils {
|
|
3
|
+
static BREAKPOINTS = {
|
|
4
|
+
desktop: 1024,
|
|
5
|
+
tablet: 768
|
|
6
|
+
};
|
|
7
|
+
static VIEWPORT_SIZES = {
|
|
8
|
+
desktop: "70vw",
|
|
9
|
+
tablet: "80vw",
|
|
10
|
+
mobile: "100vw"
|
|
11
|
+
};
|
|
12
|
+
static DEFAULT_LAYOUT = DEFAULT_LAYOUT;
|
|
13
|
+
/**
|
|
14
|
+
* Generates a srcset string from processed image variants using relative paths
|
|
15
|
+
* @param {ImageVariant[]} variants - Array of processed image variants
|
|
16
|
+
* @returns {string} srcset attribute string
|
|
17
|
+
* @private
|
|
18
|
+
*/
|
|
19
|
+
static generateSrcset(variants) {
|
|
20
|
+
return variants.sort((a, b) => b.width - a.width).map(({ src, width }) => `${src} ${width}w`).join(", ");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Generates sizes attribute based on image variants.
|
|
24
|
+
* Sizes are generated based on the variant widths and breakpoints.
|
|
25
|
+
* Here we use a smart approach to generate sizes based on the variant widths.
|
|
26
|
+
* We start with the largest variant and set a min-width condition for its width.
|
|
27
|
+
* Then we add conditions for each variant based on the viewport width.
|
|
28
|
+
* Finally, we add a catch-all for the smallest screens.
|
|
29
|
+
* This approach ensures that the browser will select the correct image variant based on the viewport width.
|
|
30
|
+
* @see https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images#resolution_switching_different_sizes
|
|
31
|
+
* @param {ImageVariant[]} variants - Array of processed image variants
|
|
32
|
+
* @returns {string} sizes attribute string
|
|
33
|
+
* @private
|
|
34
|
+
*/
|
|
35
|
+
static generateSizes(variants) {
|
|
36
|
+
if (!variants?.length) return "";
|
|
37
|
+
const sortedVariants = [...variants].sort((a, b) => b.width - a.width);
|
|
38
|
+
const [largest, ...rest] = sortedVariants;
|
|
39
|
+
const conditions = [
|
|
40
|
+
`(min-width: ${largest.width}px) ${largest.width}px`,
|
|
41
|
+
...rest.map((variant) => {
|
|
42
|
+
if (variant.width >= ImageUtils.BREAKPOINTS.desktop) {
|
|
43
|
+
return `(min-width: ${variant.width}px) ${ImageUtils.VIEWPORT_SIZES.desktop}`;
|
|
44
|
+
}
|
|
45
|
+
if (variant.width >= ImageUtils.BREAKPOINTS.tablet) {
|
|
46
|
+
return `(min-width: ${variant.width}px) ${ImageUtils.VIEWPORT_SIZES.tablet}`;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}).filter(Boolean),
|
|
50
|
+
ImageUtils.VIEWPORT_SIZES.mobile
|
|
51
|
+
];
|
|
52
|
+
return conditions.join(", ");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Generates a styles string based on image layout and config
|
|
56
|
+
* @param {ImageLayout} layout - Image layout type
|
|
57
|
+
* @param {LayoutAttributes} config - Image layout configuration
|
|
58
|
+
* @returns {string} styles string
|
|
59
|
+
* @private
|
|
60
|
+
*/
|
|
61
|
+
static generateLayoutStyles(config) {
|
|
62
|
+
const layout = config.layout || ImageUtils.DEFAULT_LAYOUT;
|
|
63
|
+
const styles = [["object-fit", "cover"]];
|
|
64
|
+
if (config.aspectRatio) {
|
|
65
|
+
styles.push(["aspect-ratio", config.aspectRatio]);
|
|
66
|
+
}
|
|
67
|
+
switch (layout) {
|
|
68
|
+
case "fixed":
|
|
69
|
+
if (config.width) {
|
|
70
|
+
styles.push(["width", `${config.width}px`], ["min-width", `${config.width}px`]);
|
|
71
|
+
}
|
|
72
|
+
if (config.height) {
|
|
73
|
+
styles.push(["height", `${config.height}px`], ["min-height", `${config.height}px`]);
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
case "constrained":
|
|
77
|
+
styles.push(["width", "100%"]);
|
|
78
|
+
if (config.width && config.height) {
|
|
79
|
+
styles.push(["max-width", `${config.width}px`], ["max-height", `${config.height}px`]);
|
|
80
|
+
if (!config.aspectRatio) {
|
|
81
|
+
styles.push(["aspect-ratio", `${config.width}/${config.height}`]);
|
|
82
|
+
}
|
|
83
|
+
} else if (config.height) {
|
|
84
|
+
styles.push(["height", `${config.height}px`], ["min-height", `${config.height}px`]);
|
|
85
|
+
} else if (config.width) {
|
|
86
|
+
styles.push(["max-width", `${config.width}px`]);
|
|
87
|
+
} else if (!config.aspectRatio) {
|
|
88
|
+
styles.push(["aspect-ratio", `${config.attributes.width}/${config.attributes.height}`]);
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
case "full-width":
|
|
92
|
+
styles.push(["width", "100%"]);
|
|
93
|
+
if (config.height) {
|
|
94
|
+
styles.push(["height", `${config.height}px`], ["min-height", `${config.height}px`]);
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
return styles;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
ImageUtils
|
|
103
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { DEFAULT_LAYOUT } from './constants';
|
|
2
|
+
import type { EcoImageProps } from './image-renderer';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ImageUtils
|
|
6
|
+
* This class contains utility methods for working with images
|
|
7
|
+
* It contains methods for generating srcset, sizes and styles attributes for the image element
|
|
8
|
+
*/
|
|
9
|
+
export class ImageUtils {
|
|
10
|
+
private static readonly BREAKPOINTS = {
|
|
11
|
+
desktop: 1024,
|
|
12
|
+
tablet: 768,
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
private static readonly VIEWPORT_SIZES = {
|
|
16
|
+
desktop: '70vw',
|
|
17
|
+
tablet: '80vw',
|
|
18
|
+
mobile: '100vw',
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
static readonly DEFAULT_LAYOUT = DEFAULT_LAYOUT;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generates a srcset string from processed image variants using relative paths
|
|
25
|
+
* @param {ImageVariant[]} variants - Array of processed image variants
|
|
26
|
+
* @returns {string} srcset attribute string
|
|
27
|
+
* @private
|
|
28
|
+
*/
|
|
29
|
+
static generateSrcset(variants: Array<{ width: number; src: string }>): string {
|
|
30
|
+
return variants
|
|
31
|
+
.sort((a, b) => b.width - a.width)
|
|
32
|
+
.map(({ src, width }) => `${src} ${width}w`)
|
|
33
|
+
.join(', ');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generates sizes attribute based on image variants.
|
|
38
|
+
* Sizes are generated based on the variant widths and breakpoints.
|
|
39
|
+
* Here we use a smart approach to generate sizes based on the variant widths.
|
|
40
|
+
* We start with the largest variant and set a min-width condition for its width.
|
|
41
|
+
* Then we add conditions for each variant based on the viewport width.
|
|
42
|
+
* Finally, we add a catch-all for the smallest screens.
|
|
43
|
+
* This approach ensures that the browser will select the correct image variant based on the viewport width.
|
|
44
|
+
* @see https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images#resolution_switching_different_sizes
|
|
45
|
+
* @param {ImageVariant[]} variants - Array of processed image variants
|
|
46
|
+
* @returns {string} sizes attribute string
|
|
47
|
+
* @private
|
|
48
|
+
*/
|
|
49
|
+
static generateSizes(variants: Array<{ width: number }>): string {
|
|
50
|
+
if (!variants?.length) return '';
|
|
51
|
+
|
|
52
|
+
const sortedVariants = [...variants].sort((a, b) => b.width - a.width);
|
|
53
|
+
const [largest, ...rest] = sortedVariants;
|
|
54
|
+
|
|
55
|
+
const conditions = [
|
|
56
|
+
`(min-width: ${largest.width}px) ${largest.width}px`,
|
|
57
|
+
...rest
|
|
58
|
+
.map((variant) => {
|
|
59
|
+
if (variant.width >= ImageUtils.BREAKPOINTS.desktop) {
|
|
60
|
+
return `(min-width: ${variant.width}px) ${ImageUtils.VIEWPORT_SIZES.desktop}`;
|
|
61
|
+
}
|
|
62
|
+
if (variant.width >= ImageUtils.BREAKPOINTS.tablet) {
|
|
63
|
+
return `(min-width: ${variant.width}px) ${ImageUtils.VIEWPORT_SIZES.tablet}`;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
})
|
|
67
|
+
.filter(Boolean),
|
|
68
|
+
ImageUtils.VIEWPORT_SIZES.mobile,
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
return conditions.join(', ');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Generates a styles string based on image layout and config
|
|
76
|
+
* @param {ImageLayout} layout - Image layout type
|
|
77
|
+
* @param {LayoutAttributes} config - Image layout configuration
|
|
78
|
+
* @returns {string} styles string
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
static generateLayoutStyles(
|
|
82
|
+
config: Pick<EcoImageProps, 'layout' | 'width' | 'height' | 'aspectRatio' | 'attributes'>,
|
|
83
|
+
): [string, string][] {
|
|
84
|
+
const layout = config.layout || ImageUtils.DEFAULT_LAYOUT;
|
|
85
|
+
const styles: [string, string][] = [['object-fit', 'cover']];
|
|
86
|
+
|
|
87
|
+
if (config.aspectRatio) {
|
|
88
|
+
styles.push(['aspect-ratio', config.aspectRatio]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
switch (layout) {
|
|
92
|
+
case 'fixed':
|
|
93
|
+
if (config.width) {
|
|
94
|
+
styles.push(['width', `${config.width}px`], ['min-width', `${config.width}px`]);
|
|
95
|
+
}
|
|
96
|
+
if (config.height) {
|
|
97
|
+
styles.push(['height', `${config.height}px`], ['min-height', `${config.height}px`]);
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
|
|
101
|
+
case 'constrained':
|
|
102
|
+
styles.push(['width', '100%']);
|
|
103
|
+
|
|
104
|
+
if (config.width && config.height) {
|
|
105
|
+
styles.push(['max-width', `${config.width}px`], ['max-height', `${config.height}px`]);
|
|
106
|
+
if (!config.aspectRatio) {
|
|
107
|
+
styles.push(['aspect-ratio', `${config.width}/${config.height}`]);
|
|
108
|
+
}
|
|
109
|
+
} else if (config.height) {
|
|
110
|
+
styles.push(['height', `${config.height}px`], ['min-height', `${config.height}px`]);
|
|
111
|
+
} else if (config.width) {
|
|
112
|
+
styles.push(['max-width', `${config.width}px`]);
|
|
113
|
+
} else if (!config.aspectRatio) {
|
|
114
|
+
styles.push(['aspect-ratio', `${config.attributes.width}/${config.attributes.height}`]);
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
|
|
118
|
+
case 'full-width':
|
|
119
|
+
styles.push(['width', '100%']);
|
|
120
|
+
if (config.height) {
|
|
121
|
+
styles.push(['height', `${config.height}px`], ['min-height', `${config.height}px`]);
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return styles;
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/index.d.ts
ADDED
package/src/index.js
ADDED
package/src/index.ts
ADDED
package/src/plugin.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageProcessorPlugin
|
|
3
|
+
* @module @ecopages/image-processor
|
|
4
|
+
*/
|
|
5
|
+
import { Processor, type ProcessorConfig } from '@ecopages/core/plugins/processor';
|
|
6
|
+
import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
|
|
7
|
+
import { ImageProcessor } from './image-processor';
|
|
8
|
+
import type { ImageSize, ImageSpecifications } from './types';
|
|
9
|
+
/**
|
|
10
|
+
* Configuration for the image processor
|
|
11
|
+
*/
|
|
12
|
+
export interface ImageProcessorConfig {
|
|
13
|
+
sourceDir: string;
|
|
14
|
+
outputDir: string;
|
|
15
|
+
publicPath: string;
|
|
16
|
+
/**
|
|
17
|
+
* @default []
|
|
18
|
+
*/
|
|
19
|
+
sizes: ImageSize[];
|
|
20
|
+
quality: number;
|
|
21
|
+
format: 'webp' | 'jpeg' | 'png' | 'avif';
|
|
22
|
+
/**
|
|
23
|
+
* Optional list of accepted image formats
|
|
24
|
+
* @default ["jpg", "jpeg", "png", "webp"]
|
|
25
|
+
*/
|
|
26
|
+
acceptedFormats?: string[];
|
|
27
|
+
/**
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
cacheEnabled?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* ImageMap
|
|
34
|
+
* This is the representation of the image map in the virtual module
|
|
35
|
+
*/
|
|
36
|
+
export type ImageMap = Record<string, ImageSpecifications>;
|
|
37
|
+
/**
|
|
38
|
+
* ImageProcessorPlugin
|
|
39
|
+
* A Processor for optimizing images.
|
|
40
|
+
*/
|
|
41
|
+
export declare class ImageProcessorPlugin extends Processor<ImageProcessorConfig> {
|
|
42
|
+
private processor;
|
|
43
|
+
processedImages: Record<string, ImageSpecifications>;
|
|
44
|
+
constructor(config: Omit<ProcessorConfig<ImageProcessorConfig>, 'name' | 'description'>);
|
|
45
|
+
get buildPlugins(): EcoBuildPlugin[];
|
|
46
|
+
get plugins(): EcoBuildPlugin[];
|
|
47
|
+
/**
|
|
48
|
+
* Generate dependencies for processor.
|
|
49
|
+
* It is ossible to define which one should be included in the final bundle based on the environment.
|
|
50
|
+
* @returns
|
|
51
|
+
*/
|
|
52
|
+
private generateDependencies;
|
|
53
|
+
/**
|
|
54
|
+
* Setup the image processor and create the virtual module.
|
|
55
|
+
*/
|
|
56
|
+
setup(): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Process images.
|
|
59
|
+
* @param images
|
|
60
|
+
*/
|
|
61
|
+
process(images: string[]): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Delete processed images using the original image path.
|
|
64
|
+
* @param imagePath
|
|
65
|
+
*/
|
|
66
|
+
deleteProcessedImagesbyPath(imagePath: string): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Generate types for the virtual module.
|
|
69
|
+
*/
|
|
70
|
+
private generateTypes;
|
|
71
|
+
/**
|
|
72
|
+
* Teardown the image processor.
|
|
73
|
+
*/
|
|
74
|
+
teardown(): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Get the image processor instance.
|
|
77
|
+
* @returns The image processor instance.
|
|
78
|
+
*/
|
|
79
|
+
getProcessor(): ImageProcessor | undefined;
|
|
80
|
+
}
|
|
81
|
+
export declare const imageProcessorPlugin: (config: Omit<ProcessorConfig<ImageProcessorConfig>, "name" | "description">) => ImageProcessorPlugin;
|
package/src/plugin.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { deepMerge } from "@ecopages/core/utils/deep-merge";
|
|
4
|
+
import { GENERATED_BASE_PATHS } from "@ecopages/core/constants";
|
|
5
|
+
import { fileSystem } from "@ecopages/file-system";
|
|
6
|
+
import { Processor } from "@ecopages/core/plugins/processor";
|
|
7
|
+
import { Logger } from "@ecopages/logger";
|
|
8
|
+
import { createImagePlugin, createImagePluginBundler } from "./image-plugins";
|
|
9
|
+
import { ImageProcessor } from "./image-processor";
|
|
10
|
+
import { anyCaseToCamelCase } from "./utils";
|
|
11
|
+
function resolveGeneratedPath(type, options) {
|
|
12
|
+
const { root, module, subPath } = options;
|
|
13
|
+
const parts = [root, GENERATED_BASE_PATHS[type], module, subPath].filter(Boolean);
|
|
14
|
+
return path.join(...parts);
|
|
15
|
+
}
|
|
16
|
+
const logger = new Logger("[@ecopages/image-processor]", {
|
|
17
|
+
debug: process.env.ECOPAGES_LOGGER_DEBUG === "true"
|
|
18
|
+
});
|
|
19
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
class ImageProcessorPlugin extends Processor {
|
|
21
|
+
processedImages = {};
|
|
22
|
+
constructor(config) {
|
|
23
|
+
const acceptedFormats = config.options?.acceptedFormats ?? ["jpg", "jpeg", "png", "webp"];
|
|
24
|
+
const extensionPatterns = acceptedFormats.map((format) => `*.${format.replace(/^\./, "")}`);
|
|
25
|
+
const defaultWatchConfig = {
|
|
26
|
+
paths: [],
|
|
27
|
+
extensions: acceptedFormats,
|
|
28
|
+
onCreate: async (ctx) => this.process([ctx.path]),
|
|
29
|
+
onChange: async (ctx) => this.process([ctx.path]),
|
|
30
|
+
onDelete: async (ctx) => this.deleteProcessedImagesbyPath(ctx.path),
|
|
31
|
+
onError: (error) => logger.error("Watcher error", { error })
|
|
32
|
+
};
|
|
33
|
+
super({
|
|
34
|
+
...config,
|
|
35
|
+
name: "ecopages-image-processor",
|
|
36
|
+
description: "A Processor for optimizing images.",
|
|
37
|
+
capabilities: [
|
|
38
|
+
{
|
|
39
|
+
kind: "image",
|
|
40
|
+
extensions: extensionPatterns
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
watch: config.watch ? deepMerge(defaultWatchConfig, config.watch) : defaultWatchConfig
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
get buildPlugins() {
|
|
47
|
+
return [createImagePluginBundler(this.processedImages)];
|
|
48
|
+
}
|
|
49
|
+
get plugins() {
|
|
50
|
+
return [createImagePlugin(this.processedImages)];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Generate dependencies for processor.
|
|
54
|
+
* It is ossible to define which one should be included in the final bundle based on the environment.
|
|
55
|
+
* @returns
|
|
56
|
+
*/
|
|
57
|
+
generateDependencies() {
|
|
58
|
+
const deps = [];
|
|
59
|
+
if (process.env.NODE_ENV === "development") {
|
|
60
|
+
}
|
|
61
|
+
return deps;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Setup the image processor and create the virtual module.
|
|
65
|
+
*/
|
|
66
|
+
async setup() {
|
|
67
|
+
if (!this.context) {
|
|
68
|
+
throw new Error("ImageProcessor requires context to be set");
|
|
69
|
+
}
|
|
70
|
+
logger.debug("Setting up image processor", {
|
|
71
|
+
srcDir: this.context.srcDir,
|
|
72
|
+
distDir: this.context.distDir
|
|
73
|
+
});
|
|
74
|
+
const defaultConfig = {
|
|
75
|
+
sourceDir: `${this.context.srcDir}/public/assets/images`,
|
|
76
|
+
outputDir: `${this.context.distDir}/assets/optimized`,
|
|
77
|
+
publicPath: "/assets/optimized",
|
|
78
|
+
sizes: [],
|
|
79
|
+
quality: 80,
|
|
80
|
+
format: "webp"
|
|
81
|
+
};
|
|
82
|
+
const config = this.options ? deepMerge(defaultConfig, this.options) : defaultConfig;
|
|
83
|
+
this.processor = new ImageProcessor(config, {
|
|
84
|
+
readCache: (key) => this.readCache(key),
|
|
85
|
+
writeCache: (key, data) => this.writeCache(key, data)
|
|
86
|
+
});
|
|
87
|
+
this.processedImages = await this.processor.processDirectory();
|
|
88
|
+
if (this.watchConfig) {
|
|
89
|
+
this.watchConfig.paths = [config.sourceDir];
|
|
90
|
+
}
|
|
91
|
+
this.dependencies = this.generateDependencies();
|
|
92
|
+
this.generateTypes();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Process images.
|
|
96
|
+
* @param images
|
|
97
|
+
*/
|
|
98
|
+
async process(images) {
|
|
99
|
+
if (!this.processor) {
|
|
100
|
+
throw new Error("ImageProcessor not initialized");
|
|
101
|
+
}
|
|
102
|
+
logger.debug("Processing images", { count: images.length });
|
|
103
|
+
for (const image of images) {
|
|
104
|
+
try {
|
|
105
|
+
const result = await this.processor.processImage(image);
|
|
106
|
+
if (result) {
|
|
107
|
+
this.processedImages[path.basename(image)] = result;
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
logger.error("Failed to process image", { image, error });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
this.generateTypes();
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Delete processed images using the original image path.
|
|
117
|
+
* @param imagePath
|
|
118
|
+
*/
|
|
119
|
+
async deleteProcessedImagesbyPath(imagePath) {
|
|
120
|
+
if (!this.processor) {
|
|
121
|
+
throw new Error("ImageProcessor not initialized");
|
|
122
|
+
}
|
|
123
|
+
logger.debug("Deleting processed images", { path: imagePath });
|
|
124
|
+
try {
|
|
125
|
+
const baseNameWithoutExt = path.basename(imagePath, path.extname(imagePath));
|
|
126
|
+
if (!this.options) {
|
|
127
|
+
throw new Error("Options not set");
|
|
128
|
+
}
|
|
129
|
+
const outputDir = this.options.outputDir;
|
|
130
|
+
const files = await fileSystem.glob([`${outputDir}/${baseNameWithoutExt}-*`]);
|
|
131
|
+
await Promise.all(
|
|
132
|
+
files.map(async (file) => {
|
|
133
|
+
try {
|
|
134
|
+
await fileSystem.removeAsync(file);
|
|
135
|
+
logger.debug("Deleted processed image", { file });
|
|
136
|
+
} catch (error) {
|
|
137
|
+
logger.error("Failed to delete processed image", { file, error });
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
try {
|
|
142
|
+
const cacheKey = this.processedImages[path.basename(imagePath)].cacheKey;
|
|
143
|
+
const cachePath = this.getCachePath(cacheKey);
|
|
144
|
+
await fileSystem.removeAsync(cachePath);
|
|
145
|
+
logger.debug("Deleted cache file for image", { cachePath });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
logger.error("Failed to delete cache file for image", { imagePath, error });
|
|
148
|
+
}
|
|
149
|
+
delete this.processedImages[path.basename(imagePath)];
|
|
150
|
+
this.generateTypes();
|
|
151
|
+
} catch (error) {
|
|
152
|
+
logger.error("Failed to delete processed images", { path: imagePath, error });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Generate types for the virtual module.
|
|
157
|
+
*/
|
|
158
|
+
generateTypes() {
|
|
159
|
+
if (!this.options?.outputDir) {
|
|
160
|
+
throw new Error("Output directory not set");
|
|
161
|
+
}
|
|
162
|
+
const requiredTypes = fileSystem.readFileSync(path.join(currentDir, "types.ts")).toString().replaceAll("export ", "");
|
|
163
|
+
const content = `
|
|
164
|
+
/**
|
|
165
|
+
* Do not edit manually. This file is auto-generated.
|
|
166
|
+
* This file contains the type definitions for the virtual module "ecopages:images".
|
|
167
|
+
*/
|
|
168
|
+
|
|
169
|
+
${requiredTypes}
|
|
170
|
+
|
|
171
|
+
declare module "ecopages:images" {
|
|
172
|
+
${Object.keys(this.processedImages).map((key) => `export const ${anyCaseToCamelCase(key)}: ImageSpecifications;`).join("\n ")}
|
|
173
|
+
}`;
|
|
174
|
+
if (!this.context) throw new Error("Processor is not configured correctly");
|
|
175
|
+
const typesDir = resolveGeneratedPath("types", {
|
|
176
|
+
root: this.context.rootDir,
|
|
177
|
+
module: this.name,
|
|
178
|
+
subPath: "virtual-module.d.ts"
|
|
179
|
+
});
|
|
180
|
+
fileSystem.ensureDir(path.dirname(typesDir));
|
|
181
|
+
fileSystem.write(typesDir, content);
|
|
182
|
+
logger.debug("Generated types for virtual module", { typesDir });
|
|
183
|
+
const indexTypesDir = resolveGeneratedPath("types", {
|
|
184
|
+
root: this.context.rootDir,
|
|
185
|
+
module: this.name,
|
|
186
|
+
subPath: "index.d.ts"
|
|
187
|
+
});
|
|
188
|
+
const indexContent = 'import "./virtual-module.d.js";';
|
|
189
|
+
fileSystem.write(indexTypesDir, indexContent);
|
|
190
|
+
logger.debug("Generated index types for virtual module", { indexTypesDir });
|
|
191
|
+
const runtimeVirtualModulePath = resolveGeneratedPath("cache", {
|
|
192
|
+
root: this.context.distDir,
|
|
193
|
+
module: this.name,
|
|
194
|
+
subPath: "virtual-module.ts"
|
|
195
|
+
});
|
|
196
|
+
const runtimeModuleContent = Object.entries(this.processedImages).map(([key, value]) => {
|
|
197
|
+
return `export const ${anyCaseToCamelCase(key)} = ${JSON.stringify(value, null, 2)} as const;`;
|
|
198
|
+
}).join("\n\n");
|
|
199
|
+
fileSystem.ensureDir(path.dirname(runtimeVirtualModulePath));
|
|
200
|
+
fileSystem.write(runtimeVirtualModulePath, runtimeModuleContent);
|
|
201
|
+
logger.debug("Generated runtime virtual module for images", { runtimeVirtualModulePath });
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Teardown the image processor.
|
|
205
|
+
*/
|
|
206
|
+
async teardown() {
|
|
207
|
+
logger.debug("Tearing down image processor");
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Get the image processor instance.
|
|
211
|
+
* @returns The image processor instance.
|
|
212
|
+
*/
|
|
213
|
+
getProcessor() {
|
|
214
|
+
return this.processor;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const imageProcessorPlugin = (config) => {
|
|
218
|
+
return new ImageProcessorPlugin(config);
|
|
219
|
+
};
|
|
220
|
+
export {
|
|
221
|
+
ImageProcessorPlugin,
|
|
222
|
+
imageProcessorPlugin
|
|
223
|
+
};
|