@ecopages/image-processor 0.2.0-beta.8 → 0.2.0-rc.0
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 +11 -3
- package/package.json +19 -5
- package/src/bun-plugins.js +48 -29
- package/src/component/html.js +12 -6
- package/src/component/jsx.d.ts +12 -0
- package/src/component/jsx.js +18 -0
- 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 -135
- package/src/image-renderer.js +222 -220
- package/src/image-runtime-state.d.ts +20 -0
- package/src/image-runtime-state.js +59 -0
- package/src/image-utils.js +109 -98
- package/src/index.js +3 -3
- package/src/plugin.d.ts +17 -11
- package/src/plugin.js +292 -274
- 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();
|
|
@@ -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
|
+
}
|