@ecopages/image-processor 0.2.1 → 0.3.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.
@@ -1,103 +1,114 @@
1
- import { DEFAULT_LAYOUT } from "./constants.js";
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]);
1
+ import { DEFAULT_LAYOUT } from './constants.js';
2
+ /**
3
+ * ImageUtils
4
+ * This class contains utility methods for working with images
5
+ * It contains methods for generating srcset, sizes and styles attributes for the image element
6
+ */
7
+ export class ImageUtils {
8
+ static BREAKPOINTS = {
9
+ desktop: 1024,
10
+ tablet: 768,
11
+ };
12
+ static VIEWPORT_SIZES = {
13
+ desktop: '70vw',
14
+ tablet: '80vw',
15
+ mobile: '100vw',
16
+ };
17
+ static DEFAULT_LAYOUT = DEFAULT_LAYOUT;
18
+ /**
19
+ * Generates a srcset string from processed image variants using relative paths
20
+ * @param {ImageVariant[]} variants - Array of processed image variants
21
+ * @returns {string} srcset attribute string
22
+ * @private
23
+ */
24
+ static generateSrcset(variants) {
25
+ return variants
26
+ .sort((a, b) => b.width - a.width)
27
+ .map(({ src, width }) => `${src} ${width}w`)
28
+ .join(', ');
66
29
  }
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}`]);
30
+ /**
31
+ * Generates sizes attribute based on image variants.
32
+ * Sizes are generated based on the variant widths and breakpoints.
33
+ * Here we use a smart approach to generate sizes based on the variant widths.
34
+ * We start with the largest variant and set a min-width condition for its width.
35
+ * Then we add conditions for each variant based on the viewport width.
36
+ * Finally, we add a catch-all for the smallest screens.
37
+ * This approach ensures that the browser will select the correct image variant based on the viewport width.
38
+ * @see https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images#resolution_switching_different_sizes
39
+ * @param {ImageVariant[]} variants - Array of processed image variants
40
+ * @returns {string} sizes attribute string
41
+ * @private
42
+ */
43
+ static generateSizes(variants) {
44
+ if (!variants?.length)
45
+ return '';
46
+ const sortedVariants = [...variants].sort((a, b) => b.width - a.width);
47
+ const [largest, ...rest] = sortedVariants;
48
+ const conditions = [
49
+ `(min-width: ${largest.width}px) ${largest.width}px`,
50
+ ...rest
51
+ .map((variant) => {
52
+ if (variant.width >= ImageUtils.BREAKPOINTS.desktop) {
53
+ return `(min-width: ${variant.width}px) ${ImageUtils.VIEWPORT_SIZES.desktop}`;
54
+ }
55
+ if (variant.width >= ImageUtils.BREAKPOINTS.tablet) {
56
+ return `(min-width: ${variant.width}px) ${ImageUtils.VIEWPORT_SIZES.tablet}`;
57
+ }
58
+ return null;
59
+ })
60
+ .filter(Boolean),
61
+ ImageUtils.VIEWPORT_SIZES.mobile,
62
+ ];
63
+ return conditions.join(', ');
64
+ }
65
+ /**
66
+ * Generates a styles string based on image layout and config
67
+ * @param {ImageLayout} layout - Image layout type
68
+ * @param {LayoutAttributes} config - Image layout configuration
69
+ * @returns {string} styles string
70
+ * @private
71
+ */
72
+ static generateLayoutStyles(config) {
73
+ const layout = config.layout || ImageUtils.DEFAULT_LAYOUT;
74
+ const styles = [['object-fit', 'cover']];
75
+ if (config.aspectRatio) {
76
+ styles.push(['aspect-ratio', config.aspectRatio]);
89
77
  }
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`]);
78
+ switch (layout) {
79
+ case 'fixed':
80
+ if (config.width) {
81
+ styles.push(['width', `${config.width}px`], ['min-width', `${config.width}px`]);
82
+ }
83
+ if (config.height) {
84
+ styles.push(['height', `${config.height}px`], ['min-height', `${config.height}px`]);
85
+ }
86
+ break;
87
+ case 'constrained':
88
+ styles.push(['width', '100%']);
89
+ if (config.width && config.height) {
90
+ styles.push(['max-width', `${config.width}px`], ['max-height', `${config.height}px`]);
91
+ if (!config.aspectRatio) {
92
+ styles.push(['aspect-ratio', `${config.width}/${config.height}`]);
93
+ }
94
+ }
95
+ else if (config.height) {
96
+ styles.push(['height', `${config.height}px`], ['min-height', `${config.height}px`]);
97
+ }
98
+ else if (config.width) {
99
+ styles.push(['max-width', `${config.width}px`]);
100
+ }
101
+ else if (!config.aspectRatio) {
102
+ styles.push(['aspect-ratio', `${config.attributes.width}/${config.attributes.height}`]);
103
+ }
104
+ break;
105
+ case 'full-width':
106
+ styles.push(['width', '100%']);
107
+ if (config.height) {
108
+ styles.push(['height', `${config.height}px`], ['min-height', `${config.height}px`]);
109
+ }
110
+ break;
95
111
  }
96
- break;
112
+ return styles;
97
113
  }
98
- return styles;
99
- }
100
114
  }
101
- export {
102
- ImageUtils
103
- };
package/src/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export * from "./image-processor.js";
2
- export * from "./plugin.js";
3
- export * from "./types.js";
1
+ export * from './image-processor.js';
2
+ export * from './plugin.js';
3
+ export * from './types.js';
package/src/plugin.d.ts CHANGED
@@ -2,9 +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';
7
- import { ImageProcessor } from './image-processor.js';
5
+ import { Processor, type EcoBuildPlugin, type ProcessorConfig } from '@ecopages/core/processor';
8
6
  import type { ImageSize, ImageSpecifications } from './types.js';
9
7
  /**
10
8
  * Configuration for the image processor
@@ -33,19 +31,19 @@ export interface ImageProcessorConfig {
33
31
  * ImageMap
34
32
  * This is the representation of the image map in the virtual module
35
33
  */
36
- export type ImageMap = Record<string, ImageSpecifications>;
34
+ export type { ImageMap } from './image-runtime-state.js';
37
35
  /**
38
36
  * ImageProcessorPlugin
39
37
  * A Processor for optimizing images.
40
38
  */
41
39
  export declare class ImageProcessorPlugin extends Processor<ImageProcessorConfig> {
42
40
  private processor;
43
- private buildContributionsPrepared;
44
41
  private resolvedConfig?;
45
42
  processedImages: Record<string, ImageSpecifications>;
46
- constructor(config: Omit<ProcessorConfig<ImageProcessorConfig>, 'name' | 'description'>);
47
- get buildPlugins(): EcoBuildPlugin[];
48
- get plugins(): EcoBuildPlugin[];
43
+ private preparation?;
44
+ constructor(config: Omit<ProcessorConfig<ImageProcessorConfig>, 'name'>);
45
+ private handleImageChanges;
46
+ plugins(): EcoBuildPlugin[];
49
47
  /**
50
48
  * Replaces image-map contents without swapping the backing object.
51
49
  *
@@ -55,46 +53,59 @@ export declare class ImageProcessorPlugin extends Processor<ImageProcessorConfig
55
53
  */
56
54
  private replaceProcessedImages;
57
55
  /**
58
- * Prepares the image virtual-module state before config build seals the app
59
- * manifest.
56
+ * Resolves processor config and creates the sharp-backed worker instance.
60
57
  */
61
- prepareBuildContributions(): Promise<void>;
58
+ private initializeProcessor;
62
59
  private getRuntimeVirtualModulePath;
63
- private getGeneratedOutputPath;
64
- private hasGeneratedOutputs;
65
- private rehydrateGeneratedOutputs;
66
60
  /**
67
- * Generate dependencies for processor.
68
- * It is ossible to define which one should be included in the final bundle based on the environment.
69
- * @returns
61
+ * Restores in-memory image state from previously generated dist artifacts.
62
+ *
63
+ * @returns `true` when state was restored without invoking sharp.
70
64
  */
71
- private generateDependencies;
65
+ private syncProcessedImagesFromDisk;
66
+ private ensureGeneratedOutputs;
67
+ private writeGeneratedFile;
72
68
  /**
73
- * Prepares build contributions if not already done and rehydrates previously generated image outputs.
69
+ * Ensures optimized image outputs and virtual-module state exist on disk.
74
70
  */
75
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;
76
81
  /**
77
82
  * Process images.
78
83
  * @param images
79
84
  */
80
85
  process(images: string[]): Promise<void>;
81
86
  /**
82
- * Delete processed images using the original image path.
83
- * @param imagePath
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.
84
92
  */
85
93
  deleteProcessedImagesbyPath(imagePath: string): Promise<void>;
86
94
  /**
87
- * Generate types for the virtual module.
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.
88
100
  */
89
- private generateTypes;
101
+ private removeSupersededOutputs;
90
102
  /**
91
- * Teardown the image processor.
103
+ * Generate types for the virtual module.
92
104
  */
93
- teardown(): Promise<void>;
105
+ private generateTypes;
94
106
  /**
95
- * Get the image processor instance.
96
- * @returns The image processor instance.
107
+ * Writes the ambient declaration package for `ecopages:images`.
97
108
  */
98
- getProcessor(): ImageProcessor | undefined;
109
+ private writeVirtualModuleTypes;
99
110
  }
100
- export declare const imageProcessorPlugin: (config: Omit<ProcessorConfig<ImageProcessorConfig>, "name" | "description">) => ImageProcessorPlugin;
111
+ export declare const imageProcessorPlugin: (config: Omit<ProcessorConfig<ImageProcessorConfig>, "name">) => ImageProcessorPlugin;