@ecopages/image-processor 0.2.0-beta.31 → 0.2.0-beta.32
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/package.json +3 -3
- package/src/bun-plugins.js +48 -29
- package/src/component/html.js +12 -6
- package/src/component/react.js +17 -10
- package/src/constants.js +5 -4
- package/src/image-plugins.js +48 -29
- package/src/image-processor.js +147 -139
- package/src/image-renderer.js +222 -220
- package/src/image-runtime-state.js +51 -50
- package/src/image-utils.js +109 -98
- package/src/index.js +3 -3
- package/src/plugin.js +292 -283
- package/src/types.js +1 -0
- package/src/utils.js +24 -25
package/src/image-renderer.js
CHANGED
|
@@ -1,222 +1,224 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
layout,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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();
|
|
@@ -1,58 +1,59 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import { fileSystem } from
|
|
3
|
-
import { anyCaseToCamelCase } from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
}
|
|
13
18
|
}
|
|
14
|
-
|
|
15
|
-
return exports;
|
|
19
|
+
return exports;
|
|
16
20
|
}
|
|
17
|
-
async function getSourceImagePaths(resolvedConfig) {
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
export async function getSourceImagePaths(resolvedConfig) {
|
|
22
|
+
const acceptedFormats = resolvedConfig.acceptedFormats ?? ['jpg', 'jpeg', 'png', 'webp'];
|
|
23
|
+
return fileSystem.glob([`${resolvedConfig.sourceDir}/**/*.{${acceptedFormats.join(',')}}`]);
|
|
20
24
|
}
|
|
21
25
|
function getGeneratedOutputPath(outputDir, src) {
|
|
22
|
-
|
|
26
|
+
return path.join(outputDir, path.basename(src));
|
|
23
27
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const exportsByName = parseVirtualModuleExports(fileSystem.readFileSync(options.runtimeVirtualModulePath));
|
|
33
|
-
if (!exportsByName) {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
const imageMap = {};
|
|
37
|
-
for (const file of sourceImages) {
|
|
38
|
-
const basename = path.basename(file);
|
|
39
|
-
const exportName = anyCaseToCamelCase(basename);
|
|
40
|
-
const spec = exportsByName[exportName];
|
|
41
|
-
if (!spec) {
|
|
42
|
-
return null;
|
|
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 {};
|
|
43
36
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
(src) => fileSystem.exists(getGeneratedOutputPath(options.resolvedConfig.outputDir, src))
|
|
47
|
-
)) {
|
|
48
|
-
return null;
|
|
37
|
+
if (!fileSystem.exists(options.runtimeVirtualModulePath)) {
|
|
38
|
+
return null;
|
|
49
39
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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;
|
|
53
59
|
}
|
|
54
|
-
export {
|
|
55
|
-
getSourceImagePaths,
|
|
56
|
-
loadProcessedImagesFromDisk,
|
|
57
|
-
parseVirtualModuleExports
|
|
58
|
-
};
|