@ecopages/image-processor 0.2.0-beta.9 → 0.2.0-rc.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,222 +1,224 @@
1
- import { DEFAULT_LAYOUT } from "./constants.js";
2
- import { ImageUtils } from "./image-utils.js";
3
- class LayoutAttributesManager {
4
- static shouldIncludeWidthHeight(layout) {
5
- return layout === "fixed";
6
- }
7
- static filterDimensionAttributes(props) {
8
- const layout = props.layout || "constrained";
9
- if (!LayoutAttributesManager.shouldIncludeWidthHeight(layout)) {
10
- return {};
11
- }
12
- return {
13
- ...props.width && { width: props.width },
14
- ...props.height && { height: props.height }
15
- };
16
- }
17
- static getEffectiveDimensions(props, variants) {
18
- const mainVariant = variants?.[0];
19
- const layout = props.layout || DEFAULT_LAYOUT;
20
- if (layout === "constrained" && props.width && !props.height) {
21
- return { width: props.width };
22
- }
23
- const effectiveWidth = props.width || mainVariant?.width;
24
- let effectiveHeight = props.height || mainVariant?.height;
25
- if (props.aspectRatio && effectiveWidth) {
26
- const [aspectWidth, aspectHeight] = props.aspectRatio.split("/").map(Number);
27
- effectiveHeight = Math.round(effectiveWidth * aspectHeight / aspectWidth);
28
- }
29
- return { width: effectiveWidth, height: effectiveHeight };
30
- }
1
+ /**
2
+ * ImageRenderer
3
+ * @module
4
+ */
5
+ import { DEFAULT_LAYOUT } from './constants.js';
6
+ import { ImageUtils } from './image-utils.js';
7
+ export class LayoutAttributesManager {
8
+ static shouldIncludeWidthHeight(layout) {
9
+ return layout === 'fixed';
10
+ }
11
+ static filterDimensionAttributes(props) {
12
+ const layout = props.layout || 'constrained';
13
+ if (!LayoutAttributesManager.shouldIncludeWidthHeight(layout)) {
14
+ return {};
15
+ }
16
+ return {
17
+ ...(props.width && { width: props.width }),
18
+ ...(props.height && { height: props.height }),
19
+ };
20
+ }
21
+ static getEffectiveDimensions(props, variants) {
22
+ const mainVariant = variants?.[0];
23
+ const layout = props.layout || DEFAULT_LAYOUT;
24
+ if (layout === 'constrained' && props.width && !props.height) {
25
+ return { width: props.width };
26
+ }
27
+ const effectiveWidth = props.width || mainVariant?.width;
28
+ let effectiveHeight = props.height || mainVariant?.height;
29
+ if (props.aspectRatio && effectiveWidth) {
30
+ const [aspectWidth, aspectHeight] = props.aspectRatio.split('/').map(Number);
31
+ effectiveHeight = Math.round((effectiveWidth * aspectHeight) / aspectWidth);
32
+ }
33
+ return { width: effectiveWidth, height: effectiveHeight };
34
+ }
31
35
  }
32
- class ImageRenderer {
33
- originalProps;
34
- internalProps = [
35
- "attributes",
36
- "variants",
37
- "layout",
38
- "staticVariant",
39
- "aspectRatio",
40
- "unstyled",
41
- "priority",
42
- "width",
43
- "height",
44
- "cacheKey"
45
- ];
46
- generateAttributes(props) {
47
- this.originalProps = props;
48
- const collected = this.collectAttributes(props);
49
- if (!collected) return null;
50
- const { styles, width, height, ...rest } = collected;
51
- const dimensions = LayoutAttributesManager.filterDimensionAttributes({
52
- width,
53
- height,
54
- layout: props.layout
55
- });
56
- const htmlAttributes = this.getHtmlAttributes(props);
57
- const generatedStyles = styles ? this.generateStyleString(styles) : "";
58
- const userStyles = props.style || "";
59
- return {
60
- ...htmlAttributes,
61
- ...rest,
62
- ...dimensions,
63
- style: userStyles ? `${generatedStyles};${userStyles}` : generatedStyles
64
- };
65
- }
66
- getHtmlAttributes(props) {
67
- return Object.fromEntries(
68
- Object.entries(props).filter(
69
- ([key]) => !this.internalProps.includes(key) && key !== "attributes" && key !== "variants"
70
- )
71
- );
72
- }
73
- generateAttributesJsx(props) {
74
- this.originalProps = props;
75
- const collected = this.collectAttributes(props);
76
- if (!collected) return null;
77
- const { styles, fetchpriority, loading, decoding, src, srcset, sizes, width, height } = collected;
78
- const dimensions = LayoutAttributesManager.filterDimensionAttributes({
79
- width,
80
- height,
81
- layout: props.layout
82
- });
83
- const validHtmlAttributes = Object.fromEntries(
84
- Object.entries(props).filter(([key]) => !this.internalProps.includes(key))
85
- );
86
- const generatedStyles = styles ? this.createCamelCaseKeysOnStyle(styles) : {};
87
- const userStyles = typeof props.style === "object" ? props.style : {};
88
- return {
89
- fetchPriority: fetchpriority,
90
- loading,
91
- decoding,
92
- src,
93
- srcSet: srcset,
94
- sizes,
95
- ...dimensions,
96
- ...validHtmlAttributes,
97
- style: { ...generatedStyles, ...userStyles }
98
- };
99
- }
100
- renderToString({ attributes, variants, ...rest }) {
101
- this.originalProps = { attributes, variants, ...rest };
102
- const derivedAttributes = this.generateAttributes(this.originalProps);
103
- if (!derivedAttributes) return "";
104
- const stringifiedAttributes = this.stringifyAttributes(derivedAttributes);
105
- return `<img ${stringifiedAttributes} />`;
106
- }
107
- collectAttributes(props) {
108
- const { variants, priority, layout = DEFAULT_LAYOUT, unstyled, staticVariant } = props;
109
- const priorityAttributes = this.getPriorityAttributes(priority);
110
- if (!variants || variants.length === 0) {
111
- return this.handleNoVariants(props, priorityAttributes);
112
- }
113
- const mainVariant = this.getMainVariant(variants, staticVariant);
114
- if (!mainVariant) return null;
115
- const dimensions = this.calculateEffectiveDimensions(props, mainVariant);
116
- const styles = this.calculateStyles({
117
- dimensions,
118
- layout,
119
- attributes: props.attributes,
120
- unstyled
121
- });
122
- const imageAttributes = this.buildImageAttributes(mainVariant, dimensions, props, priorityAttributes, styles);
123
- return imageAttributes;
124
- }
125
- getPriorityAttributes(priority) {
126
- return {
127
- loading: priority ? "eager" : "lazy",
128
- fetchpriority: priority ? "high" : "auto",
129
- decoding: priority ? "auto" : "async"
130
- };
131
- }
132
- getMainVariant(variants, staticVariant) {
133
- if (staticVariant) {
134
- return variants.find((v) => v.label === staticVariant) || null;
135
- }
136
- return variants.sort((a, b) => b.width - a.width)[0] || null;
137
- }
138
- calculateEffectiveDimensions(props, variant) {
139
- return LayoutAttributesManager.getEffectiveDimensions(props, [variant]);
140
- }
141
- calculateStyles({
142
- dimensions,
143
- layout,
144
- unstyled,
145
- attributes
146
- }) {
147
- if (unstyled) return [];
148
- return ImageUtils.generateLayoutStyles({
149
- ...dimensions,
150
- layout,
151
- aspectRatio: this.originalProps?.aspectRatio,
152
- attributes
153
- });
154
- }
155
- buildImageAttributes(variant, dimensions, props, priorityAttributes, styles) {
156
- const { staticVariant, attributes } = props;
157
- const useResponsiveImage = !staticVariant;
158
- return {
159
- ...dimensions,
160
- ...priorityAttributes,
161
- src: variant.src,
162
- ...useResponsiveImage ? {
163
- srcset: attributes.srcset,
164
- sizes: attributes.sizes
165
- } : {},
166
- styles
167
- };
168
- }
169
- handleNoVariants(props, priorityAttributes) {
170
- const { attributes, width, height, layout, unstyled } = props;
171
- const dimensions = {
172
- width,
173
- height
174
- };
175
- const styles = this.calculateStyles({
176
- dimensions,
177
- layout: layout || DEFAULT_LAYOUT,
178
- attributes,
179
- unstyled
180
- });
181
- return {
182
- ...priorityAttributes,
183
- ...dimensions,
184
- src: attributes.src,
185
- styles
186
- };
187
- }
188
- stringifyAttributes(attributes) {
189
- const attributePairs = [];
190
- for (const [key, value] of Object.entries(attributes)) {
191
- if (value == null || value === "") continue;
192
- if (typeof value === "boolean") {
193
- if (value) attributePairs.push(key);
194
- } else {
195
- attributePairs.push(`${key}="${value}"`);
196
- }
197
- }
198
- return attributePairs.join(" ");
199
- }
200
- generateStyleString(styles) {
201
- const styleMap = /* @__PURE__ */ new Map();
202
- for (const [key, value] of styles) {
203
- const camelKey = key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
204
- styleMap.set(camelKey, value);
205
- }
206
- return Array.from(styleMap.entries()).map(([key, value]) => {
207
- const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
208
- return `${cssKey}:${value}`;
209
- }).join(";");
210
- }
211
- createCamelCaseKeysOnStyle(styles) {
212
- return Object.fromEntries(
213
- styles.map(([key, value]) => [key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()), value])
214
- );
215
- }
36
+ /**
37
+ * ImageRenderer
38
+ * This class is responsible for generating the attributes for the image element
39
+ */
40
+ export class ImageRenderer {
41
+ originalProps;
42
+ internalProps = [
43
+ 'attributes',
44
+ 'variants',
45
+ 'layout',
46
+ 'staticVariant',
47
+ 'aspectRatio',
48
+ 'unstyled',
49
+ 'priority',
50
+ 'width',
51
+ 'height',
52
+ 'cacheKey',
53
+ ];
54
+ generateAttributes(props) {
55
+ this.originalProps = props;
56
+ const collected = this.collectAttributes(props);
57
+ if (!collected)
58
+ return null;
59
+ const { styles, width, height, ...rest } = collected;
60
+ const dimensions = LayoutAttributesManager.filterDimensionAttributes({
61
+ width,
62
+ height,
63
+ layout: props.layout,
64
+ });
65
+ const htmlAttributes = this.getHtmlAttributes(props);
66
+ const generatedStyles = styles ? this.generateStyleString(styles) : '';
67
+ const userStyles = props.style || '';
68
+ return {
69
+ ...htmlAttributes,
70
+ ...rest,
71
+ ...dimensions,
72
+ style: userStyles ? `${generatedStyles};${userStyles}` : generatedStyles,
73
+ };
74
+ }
75
+ getHtmlAttributes(props) {
76
+ return Object.fromEntries(Object.entries(props).filter(([key]) => !this.internalProps.includes(key) && key !== 'attributes' && key !== 'variants'));
77
+ }
78
+ generateAttributesJsx(props) {
79
+ this.originalProps = props;
80
+ const collected = this.collectAttributes(props);
81
+ if (!collected)
82
+ return null;
83
+ const { styles, fetchpriority, loading, decoding, src, srcset, sizes, width, height } = collected;
84
+ const dimensions = LayoutAttributesManager.filterDimensionAttributes({
85
+ width,
86
+ height,
87
+ layout: props.layout,
88
+ });
89
+ const validHtmlAttributes = Object.fromEntries(Object.entries(props).filter(([key]) => !this.internalProps.includes(key)));
90
+ const generatedStyles = styles ? this.createCamelCaseKeysOnStyle(styles) : {};
91
+ const userStyles = typeof props.style === 'object' ? props.style : {};
92
+ return {
93
+ fetchPriority: fetchpriority,
94
+ loading,
95
+ decoding,
96
+ src,
97
+ srcSet: srcset,
98
+ sizes,
99
+ ...dimensions,
100
+ ...validHtmlAttributes,
101
+ style: { ...generatedStyles, ...userStyles },
102
+ };
103
+ }
104
+ renderToString({ attributes, variants, ...rest }) {
105
+ this.originalProps = { attributes, variants, ...rest };
106
+ const derivedAttributes = this.generateAttributes(this.originalProps);
107
+ if (!derivedAttributes)
108
+ return '';
109
+ const stringifiedAttributes = this.stringifyAttributes(derivedAttributes);
110
+ return `<img ${stringifiedAttributes} />`;
111
+ }
112
+ collectAttributes(props) {
113
+ const { variants, priority, layout = DEFAULT_LAYOUT, unstyled, staticVariant } = props;
114
+ const priorityAttributes = this.getPriorityAttributes(priority);
115
+ if (!variants || variants.length === 0) {
116
+ return this.handleNoVariants(props, priorityAttributes);
117
+ }
118
+ const mainVariant = this.getMainVariant(variants, staticVariant);
119
+ if (!mainVariant)
120
+ return null;
121
+ const dimensions = this.calculateEffectiveDimensions(props, mainVariant);
122
+ const styles = this.calculateStyles({
123
+ dimensions,
124
+ layout,
125
+ attributes: props.attributes,
126
+ unstyled,
127
+ });
128
+ const imageAttributes = this.buildImageAttributes(mainVariant, dimensions, props, priorityAttributes, styles);
129
+ return imageAttributes;
130
+ }
131
+ getPriorityAttributes(priority) {
132
+ return {
133
+ loading: priority ? 'eager' : 'lazy',
134
+ fetchpriority: priority ? 'high' : 'auto',
135
+ decoding: priority ? 'auto' : 'async',
136
+ };
137
+ }
138
+ getMainVariant(variants, staticVariant) {
139
+ if (staticVariant) {
140
+ return variants.find((v) => v.label === staticVariant) || null;
141
+ }
142
+ return variants.sort((a, b) => b.width - a.width)[0] || null;
143
+ }
144
+ calculateEffectiveDimensions(props, variant) {
145
+ return LayoutAttributesManager.getEffectiveDimensions(props, [variant]);
146
+ }
147
+ calculateStyles({ dimensions, layout, unstyled, attributes, }) {
148
+ if (unstyled)
149
+ return [];
150
+ return ImageUtils.generateLayoutStyles({
151
+ ...dimensions,
152
+ layout,
153
+ aspectRatio: this.originalProps?.aspectRatio,
154
+ attributes,
155
+ });
156
+ }
157
+ buildImageAttributes(variant, dimensions, props, priorityAttributes, styles) {
158
+ const { staticVariant, attributes } = props;
159
+ const useResponsiveImage = !staticVariant;
160
+ return {
161
+ ...dimensions,
162
+ ...priorityAttributes,
163
+ src: variant.src,
164
+ ...(useResponsiveImage
165
+ ? {
166
+ srcset: attributes.srcset,
167
+ sizes: attributes.sizes,
168
+ }
169
+ : {}),
170
+ styles,
171
+ };
172
+ }
173
+ handleNoVariants(props, priorityAttributes) {
174
+ const { attributes, width, height, layout, unstyled } = props;
175
+ const dimensions = {
176
+ width,
177
+ height,
178
+ };
179
+ const styles = this.calculateStyles({
180
+ dimensions,
181
+ layout: layout || DEFAULT_LAYOUT,
182
+ attributes,
183
+ unstyled,
184
+ });
185
+ return {
186
+ ...priorityAttributes,
187
+ ...dimensions,
188
+ src: attributes.src,
189
+ styles,
190
+ };
191
+ }
192
+ stringifyAttributes(attributes) {
193
+ const attributePairs = [];
194
+ for (const [key, value] of Object.entries(attributes)) {
195
+ if (value == null || value === '')
196
+ continue;
197
+ if (typeof value === 'boolean') {
198
+ if (value)
199
+ attributePairs.push(key);
200
+ }
201
+ else {
202
+ attributePairs.push(`${key}="${value}"`);
203
+ }
204
+ }
205
+ return attributePairs.join(' ');
206
+ }
207
+ generateStyleString(styles) {
208
+ const styleMap = new Map();
209
+ for (const [key, value] of styles) {
210
+ const camelKey = key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
211
+ styleMap.set(camelKey, value);
212
+ }
213
+ return Array.from(styleMap.entries())
214
+ .map(([key, value]) => {
215
+ const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
216
+ return `${cssKey}:${value}`;
217
+ })
218
+ .join(';');
219
+ }
220
+ createCamelCaseKeysOnStyle(styles) {
221
+ return Object.fromEntries(styles.map(([key, value]) => [key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()), value]));
222
+ }
216
223
  }
217
- const renderer = new ImageRenderer();
218
- export {
219
- ImageRenderer,
220
- LayoutAttributesManager,
221
- renderer
222
- };
224
+ export const renderer = new ImageRenderer();
@@ -0,0 +1,20 @@
1
+ import type { ImageSpecifications } from './types.js';
2
+ export type ImageMap = Record<string, ImageSpecifications>;
3
+ export type ImageProcessorDiskConfig = {
4
+ sourceDir: string;
5
+ outputDir: string;
6
+ acceptedFormats?: string[];
7
+ };
8
+ /**
9
+ * Parses export bindings from the generated image virtual-module source file.
10
+ */
11
+ export declare function parseVirtualModuleExports(content: string): Record<string, ImageSpecifications> | null;
12
+ export declare function getSourceImagePaths(resolvedConfig: ImageProcessorDiskConfig): Promise<string[]>;
13
+ /**
14
+ * Restores processed image state from generated dist artifacts when every source
15
+ * image has a matching virtual-module export and output files on disk.
16
+ */
17
+ export declare function loadProcessedImagesFromDisk(options: {
18
+ resolvedConfig: ImageProcessorDiskConfig;
19
+ runtimeVirtualModulePath: string;
20
+ }): Promise<ImageMap | null>;
@@ -0,0 +1,59 @@
1
+ import path from 'node:path';
2
+ import { fileSystem } from '@ecopages/file-system';
3
+ import { anyCaseToCamelCase } from './utils.js';
4
+ /**
5
+ * Parses export bindings from the generated image virtual-module source file.
6
+ */
7
+ export function parseVirtualModuleExports(content) {
8
+ const exports = {};
9
+ const pattern = /export const (\w+) = ([\s\S]*?) as const;/g;
10
+ let match;
11
+ while ((match = pattern.exec(content)) !== null) {
12
+ try {
13
+ exports[match[1]] = JSON.parse(match[2]);
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ return exports;
20
+ }
21
+ export async function getSourceImagePaths(resolvedConfig) {
22
+ const acceptedFormats = resolvedConfig.acceptedFormats ?? ['jpg', 'jpeg', 'png', 'webp'];
23
+ return fileSystem.glob([`${resolvedConfig.sourceDir}/**/*.{${acceptedFormats.join(',')}}`]);
24
+ }
25
+ function getGeneratedOutputPath(outputDir, src) {
26
+ return path.join(outputDir, path.basename(src));
27
+ }
28
+ /**
29
+ * Restores processed image state from generated dist artifacts when every source
30
+ * image has a matching virtual-module export and output files on disk.
31
+ */
32
+ export async function loadProcessedImagesFromDisk(options) {
33
+ const sourceImages = await getSourceImagePaths(options.resolvedConfig);
34
+ if (sourceImages.length === 0) {
35
+ return {};
36
+ }
37
+ if (!fileSystem.exists(options.runtimeVirtualModulePath)) {
38
+ return null;
39
+ }
40
+ const exportsByName = parseVirtualModuleExports(fileSystem.readFileSync(options.runtimeVirtualModulePath));
41
+ if (!exportsByName) {
42
+ return null;
43
+ }
44
+ const imageMap = {};
45
+ for (const file of sourceImages) {
46
+ const basename = path.basename(file);
47
+ const exportName = anyCaseToCamelCase(basename);
48
+ const spec = exportsByName[exportName];
49
+ if (!spec) {
50
+ return null;
51
+ }
52
+ const outputPaths = [spec.attributes.src, ...spec.variants.map((variant) => variant.src)];
53
+ if (!outputPaths.every((src) => fileSystem.exists(getGeneratedOutputPath(options.resolvedConfig.outputDir, src)))) {
54
+ return null;
55
+ }
56
+ imageMap[basename] = spec;
57
+ }
58
+ return imageMap;
59
+ }
@@ -0,0 +1,3 @@
1
+ export type { ImageAttributes, ImageSize, ImageSpecifications, ImageVariant } from './types.js';
2
+
3
+ declare module 'ecopages:images' {}