@lonik/oh-image 1.2.7 → 1.3.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/dist/plugin.js +121 -66
- package/dist/react.d.ts +31 -2
- package/dist/react.js +129 -12
- package/package.json +10 -6
package/dist/plugin.js
CHANGED
|
@@ -1,11 +1,36 @@
|
|
|
1
1
|
import { basename, dirname, extname, join, parse } from "node:path";
|
|
2
|
+
import queryString from "query-string";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
-
import queryString from "query-string";
|
|
5
|
-
import sharp from "sharp";
|
|
6
5
|
import pLimit from "p-limit";
|
|
6
|
+
import sharp from "sharp";
|
|
7
7
|
|
|
8
8
|
//#region src/plugin/utils.ts
|
|
9
|
+
function queryToOptions(processKey, uri) {
|
|
10
|
+
const [path, query] = uri.split("?");
|
|
11
|
+
if (!query || !path) return {
|
|
12
|
+
shouldProcess: false,
|
|
13
|
+
path: ""
|
|
14
|
+
};
|
|
15
|
+
const parsed = queryString.parse(query, {
|
|
16
|
+
parseBooleans: true,
|
|
17
|
+
parseNumbers: true,
|
|
18
|
+
arrayFormat: "comma",
|
|
19
|
+
types: { bps: "number[]" }
|
|
20
|
+
});
|
|
21
|
+
if (processKey in parsed) return {
|
|
22
|
+
shouldProcess: true,
|
|
23
|
+
options: parsed,
|
|
24
|
+
path
|
|
25
|
+
};
|
|
26
|
+
else return {
|
|
27
|
+
shouldProcess: false,
|
|
28
|
+
path
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/plugin/file-utils.ts
|
|
9
34
|
async function getFileHash(filePath) {
|
|
10
35
|
const content = await readFile(filePath);
|
|
11
36
|
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
@@ -22,33 +47,84 @@ async function saveFileSafe(path, data) {
|
|
|
22
47
|
try {
|
|
23
48
|
await mkdir(dir, { recursive: true });
|
|
24
49
|
await writeFile(path, data);
|
|
25
|
-
console.log(`Successfully saved to ${path}`);
|
|
26
50
|
} catch (err) {
|
|
27
51
|
console.error("Failed to save file:", err);
|
|
28
52
|
}
|
|
29
53
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/plugin/image-identifier.ts
|
|
57
|
+
function createId(name, format, prefix, hash, dirs) {
|
|
58
|
+
const uniqueFileId = `${prefix}-${hash}-${basename(name)}.${format}`;
|
|
59
|
+
if (!dirs.isBuild) return join(dirs.devDir, uniqueFileId);
|
|
60
|
+
return join(dirs.assetsDir, dirs.distDir, uniqueFileId);
|
|
61
|
+
}
|
|
62
|
+
function createImageIdentifier(name, hash, dirs) {
|
|
63
|
+
return {
|
|
64
|
+
main(format) {
|
|
65
|
+
return createId(name, format, "main", hash, dirs);
|
|
66
|
+
},
|
|
67
|
+
placeholder(format) {
|
|
68
|
+
return createId(name, format, "placeholder", hash, dirs);
|
|
69
|
+
},
|
|
70
|
+
srcSet(format, breakpoint) {
|
|
71
|
+
return createId(name, format, `breakpoint-${breakpoint}`, hash, dirs);
|
|
72
|
+
}
|
|
46
73
|
};
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/plugin/image-entries.ts
|
|
78
|
+
const PLACEHOLDER_IMG_SIZE = 8;
|
|
79
|
+
const PLACEHOLDER_BLUR_QUALITY = 70;
|
|
80
|
+
function createImageEntries() {
|
|
81
|
+
const map = /* @__PURE__ */ new Map();
|
|
82
|
+
return {
|
|
83
|
+
get(key) {
|
|
84
|
+
return map.get(key);
|
|
85
|
+
},
|
|
86
|
+
set(key, entry) {
|
|
87
|
+
map.set(key, entry);
|
|
88
|
+
},
|
|
89
|
+
entries() {
|
|
90
|
+
return map.entries();
|
|
91
|
+
},
|
|
92
|
+
createMainEntry(identifier, entry) {
|
|
93
|
+
const mainEntry = {
|
|
94
|
+
width: entry.width,
|
|
95
|
+
height: entry.height,
|
|
96
|
+
format: entry.format,
|
|
97
|
+
origin: entry.origin
|
|
98
|
+
};
|
|
99
|
+
this.set(identifier, mainEntry);
|
|
100
|
+
},
|
|
101
|
+
createPlaceholderEntry(identifier, main) {
|
|
102
|
+
let placeholderHeight = 0;
|
|
103
|
+
let placeholderWidth = 0;
|
|
104
|
+
if (main.width >= main.height) {
|
|
105
|
+
placeholderWidth = PLACEHOLDER_IMG_SIZE;
|
|
106
|
+
placeholderHeight = Math.max(Math.round(main.height / main.width * PLACEHOLDER_IMG_SIZE), 10);
|
|
107
|
+
} else {
|
|
108
|
+
placeholderWidth = Math.max(Math.round(main.width / main.height * PLACEHOLDER_IMG_SIZE), 10);
|
|
109
|
+
placeholderHeight = PLACEHOLDER_IMG_SIZE;
|
|
110
|
+
}
|
|
111
|
+
const placeholderEntry = {
|
|
112
|
+
width: placeholderWidth,
|
|
113
|
+
height: placeholderHeight,
|
|
114
|
+
format: main.format,
|
|
115
|
+
blur: PLACEHOLDER_BLUR_QUALITY,
|
|
116
|
+
origin: main.origin
|
|
117
|
+
};
|
|
118
|
+
this.set(identifier, placeholderEntry);
|
|
119
|
+
},
|
|
120
|
+
createSrcSetEntry(identifier, entry) {
|
|
121
|
+
this.set(identifier, entry);
|
|
122
|
+
}
|
|
50
123
|
};
|
|
51
124
|
}
|
|
125
|
+
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/plugin/image-process.ts
|
|
52
128
|
async function processImage(path, options) {
|
|
53
129
|
let processed = sharp(path);
|
|
54
130
|
if (options.width || options.height) processed = processed.resize({
|
|
@@ -63,8 +139,6 @@ async function processImage(path, options) {
|
|
|
63
139
|
//#endregion
|
|
64
140
|
//#region src/plugin/plugin.ts
|
|
65
141
|
const DEFAULT_IMAGE_FORMAT = "webp";
|
|
66
|
-
const PLACEHOLDER_IMG_SIZE = 8;
|
|
67
|
-
const PLACEHOLDER_BLUR_QUALITY = 70;
|
|
68
142
|
const DEFAULT_CONFIGS = {
|
|
69
143
|
distDir: "oh-images",
|
|
70
144
|
bps: [
|
|
@@ -91,24 +165,11 @@ function ohImage(options) {
|
|
|
91
165
|
let assetsDir;
|
|
92
166
|
let outDir;
|
|
93
167
|
let cacheDir;
|
|
94
|
-
const imageEntries =
|
|
168
|
+
const imageEntries = createImageEntries();
|
|
95
169
|
const config = {
|
|
96
170
|
...DEFAULT_CONFIGS,
|
|
97
171
|
...options
|
|
98
172
|
};
|
|
99
|
-
/**
|
|
100
|
-
* used for dev server to match url to path
|
|
101
|
-
* @param url
|
|
102
|
-
*/
|
|
103
|
-
function urlToPath(url) {
|
|
104
|
-
const fileId = basename(url);
|
|
105
|
-
return join(cacheDir, fileId);
|
|
106
|
-
}
|
|
107
|
-
function genIdentifier(uri, format, prefix, hash) {
|
|
108
|
-
const uniqueFileId = `${prefix}-${hash}-${basename(uri)}.${format}`;
|
|
109
|
-
if (!isBuild) return join(DEV_DIR, uniqueFileId);
|
|
110
|
-
return join(assetsDir, config.distDir, uniqueFileId);
|
|
111
|
-
}
|
|
112
173
|
return {
|
|
113
174
|
name: "oh-image",
|
|
114
175
|
configResolved(viteConfig) {
|
|
@@ -122,14 +183,14 @@ function ohImage(options) {
|
|
|
122
183
|
server.middlewares.use(async (req, res, next) => {
|
|
123
184
|
const url = req.url;
|
|
124
185
|
if (!url?.includes(DEV_DIR) || !SUPPORTED_IMAGE_FORMATS.test(url)) return next();
|
|
125
|
-
const
|
|
186
|
+
const fileId = basename(url);
|
|
187
|
+
const path = join(cacheDir, fileId);
|
|
126
188
|
const ext = extname(url).slice(1);
|
|
127
189
|
const image = await readFileSafe(path);
|
|
128
190
|
const imageEntry = imageEntries.get(url);
|
|
129
191
|
if (!imageEntry) {
|
|
130
192
|
console.warn("Image entry not found with id: " + url);
|
|
131
|
-
next();
|
|
132
|
-
return;
|
|
193
|
+
return next();
|
|
133
194
|
}
|
|
134
195
|
if (image) {
|
|
135
196
|
res.setHeader("Content-Type", `image/${ext}`);
|
|
@@ -156,14 +217,20 @@ function ohImage(options) {
|
|
|
156
217
|
...config,
|
|
157
218
|
...parsed.options
|
|
158
219
|
};
|
|
159
|
-
const
|
|
160
|
-
const
|
|
220
|
+
const format = mergedOptions.format ?? ext.slice(1);
|
|
221
|
+
const identifier = createImageIdentifier(name, hash, {
|
|
222
|
+
isBuild,
|
|
223
|
+
devDir: DEV_DIR,
|
|
224
|
+
assetsDir,
|
|
225
|
+
distDir: config.distDir
|
|
226
|
+
});
|
|
227
|
+
const mainIdentifier = identifier.main(format);
|
|
228
|
+
imageEntries.createMainEntry(mainIdentifier, {
|
|
161
229
|
width: mergedOptions.width,
|
|
162
230
|
height: mergedOptions.height,
|
|
163
231
|
format: mergedOptions.format,
|
|
164
232
|
origin
|
|
165
|
-
};
|
|
166
|
-
imageEntries.set(mainIdentifier, mainEntry);
|
|
233
|
+
});
|
|
167
234
|
const src = {
|
|
168
235
|
width: metadata.width,
|
|
169
236
|
height: metadata.height,
|
|
@@ -171,36 +238,24 @@ function ohImage(options) {
|
|
|
171
238
|
srcSets: ""
|
|
172
239
|
};
|
|
173
240
|
if (parsed.options?.placeholder) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
placeholderHeight = Math.max(Math.round(metadata.height / metadata.width * PLACEHOLDER_IMG_SIZE), 1);
|
|
179
|
-
} else {
|
|
180
|
-
placeholderWidth = Math.max(Math.round(metadata.width / metadata.height * PLACEHOLDER_IMG_SIZE), 1);
|
|
181
|
-
placeholderHeight = PLACEHOLDER_IMG_SIZE;
|
|
182
|
-
}
|
|
183
|
-
const placeholderIdentifier = genIdentifier(name, DEFAULT_IMAGE_FORMAT, "placeholder", hash);
|
|
184
|
-
const placeholderEntry = {
|
|
185
|
-
width: placeholderWidth,
|
|
186
|
-
height: placeholderHeight,
|
|
241
|
+
const placeholderIdentifier = identifier.placeholder(DEFAULT_IMAGE_FORMAT);
|
|
242
|
+
imageEntries.createPlaceholderEntry(placeholderIdentifier, {
|
|
243
|
+
width: metadata.width,
|
|
244
|
+
height: metadata.height,
|
|
187
245
|
format: DEFAULT_IMAGE_FORMAT,
|
|
188
|
-
blur: PLACEHOLDER_BLUR_QUALITY,
|
|
189
246
|
origin
|
|
190
|
-
};
|
|
191
|
-
imageEntries.set(placeholderIdentifier, placeholderEntry);
|
|
247
|
+
});
|
|
192
248
|
src.placeholderUrl = placeholderIdentifier;
|
|
193
249
|
}
|
|
194
250
|
if (mergedOptions.bps) {
|
|
195
251
|
const srcSets = [];
|
|
196
252
|
for (const breakpoint of mergedOptions.bps) {
|
|
197
|
-
const srcSetIdentifier =
|
|
198
|
-
|
|
253
|
+
const srcSetIdentifier = identifier.srcSet(DEFAULT_IMAGE_FORMAT, breakpoint);
|
|
254
|
+
imageEntries.createSrcSetEntry(srcSetIdentifier, {
|
|
199
255
|
width: breakpoint,
|
|
200
256
|
format: DEFAULT_IMAGE_FORMAT,
|
|
201
257
|
origin
|
|
202
|
-
};
|
|
203
|
-
imageEntries.set(srcSetIdentifier, srcSetEntry);
|
|
258
|
+
});
|
|
204
259
|
srcSets.push(`${srcSetIdentifier} ${breakpoint}w`);
|
|
205
260
|
}
|
|
206
261
|
src.srcSets = srcSets.join(", ");
|
|
@@ -214,7 +269,7 @@ function ohImage(options) {
|
|
|
214
269
|
},
|
|
215
270
|
async writeBundle() {
|
|
216
271
|
const limit = pLimit(30);
|
|
217
|
-
const tasks = Array.from(imageEntries, ([key, value]) => limit(async () => {
|
|
272
|
+
const tasks = Array.from(imageEntries.entries(), ([key, value]) => limit(async () => {
|
|
218
273
|
const processed = await processImage(value.origin, value);
|
|
219
274
|
await saveFileSafe(join(outDir, key), processed);
|
|
220
275
|
}));
|
package/dist/react.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
1
|
import { ImgHTMLAttributes } from "react";
|
|
2
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
3
|
|
|
4
4
|
//#region src/react/types.d.ts
|
|
5
5
|
type ImageSrcType = string | ImageSrc;
|
|
@@ -22,4 +22,33 @@ interface ImageProps extends Partial<Pick<ImgHTMLAttributes<HTMLImageElement>, "
|
|
|
22
22
|
//#region src/react/image.d.ts
|
|
23
23
|
declare function Image(props: ImageProps): react_jsx_runtime0.JSX.Element;
|
|
24
24
|
//#endregion
|
|
25
|
-
|
|
25
|
+
//#region src/react/use-img-loaded.d.ts
|
|
26
|
+
/**
|
|
27
|
+
* A hook that tracks whether an image element has finished loading.
|
|
28
|
+
*
|
|
29
|
+
* Handles all edge cases:
|
|
30
|
+
* - Image already cached/complete on mount
|
|
31
|
+
* - Normal load via event listener
|
|
32
|
+
* - Errors (resets to `false`)
|
|
33
|
+
* - `src` changes (resets to `false` until the new source loads)
|
|
34
|
+
* - Element unmount / ref set to `null`
|
|
35
|
+
*
|
|
36
|
+
* @returns `[ref, isLoaded]` — attach the ref callback to an `<img>` element.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* function Avatar({ src }: { src: string }) {
|
|
41
|
+
* const [imgRef, isLoaded] = useImgLoaded(src);
|
|
42
|
+
* return (
|
|
43
|
+
* <img
|
|
44
|
+
* ref={imgRef}
|
|
45
|
+
* src={src}
|
|
46
|
+
* style={{ opacity: isLoaded ? 1 : 0 }}
|
|
47
|
+
* />
|
|
48
|
+
* );
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
declare function useImgLoaded(src: string | undefined): [(img: HTMLImageElement | null) => void, boolean];
|
|
53
|
+
//#endregion
|
|
54
|
+
export { Image, type ImageProps, useImgLoaded };
|
package/dist/react.js
CHANGED
|
@@ -1,10 +1,92 @@
|
|
|
1
1
|
import * as ReactDOM from "react-dom";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
3
|
import { jsx } from "react/jsx-runtime";
|
|
3
4
|
|
|
4
|
-
//#region src/react/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
//#region src/react/use-img-loaded.ts
|
|
6
|
+
/**
|
|
7
|
+
* A hook that tracks whether an image element has finished loading.
|
|
8
|
+
*
|
|
9
|
+
* Handles all edge cases:
|
|
10
|
+
* - Image already cached/complete on mount
|
|
11
|
+
* - Normal load via event listener
|
|
12
|
+
* - Errors (resets to `false`)
|
|
13
|
+
* - `src` changes (resets to `false` until the new source loads)
|
|
14
|
+
* - Element unmount / ref set to `null`
|
|
15
|
+
*
|
|
16
|
+
* @returns `[ref, isLoaded]` — attach the ref callback to an `<img>` element.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* function Avatar({ src }: { src: string }) {
|
|
21
|
+
* const [imgRef, isLoaded] = useImgLoaded(src);
|
|
22
|
+
* return (
|
|
23
|
+
* <img
|
|
24
|
+
* ref={imgRef}
|
|
25
|
+
* src={src}
|
|
26
|
+
* style={{ opacity: isLoaded ? 1 : 0 }}
|
|
27
|
+
* />
|
|
28
|
+
* );
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
function useImgLoaded(src) {
|
|
33
|
+
const [state, setState] = useState({
|
|
34
|
+
src,
|
|
35
|
+
loaded: false
|
|
36
|
+
});
|
|
37
|
+
let isLoaded = state.loaded;
|
|
38
|
+
if (state.src !== src) {
|
|
39
|
+
isLoaded = false;
|
|
40
|
+
setState({
|
|
41
|
+
src,
|
|
42
|
+
loaded: false
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const imgRef = useRef(null);
|
|
46
|
+
const onLoad = useCallback(() => setState((prev) => ({
|
|
47
|
+
...prev,
|
|
48
|
+
loaded: true
|
|
49
|
+
})), []);
|
|
50
|
+
const onError = useCallback(() => setState((prev) => ({
|
|
51
|
+
...prev,
|
|
52
|
+
loaded: false
|
|
53
|
+
})), []);
|
|
54
|
+
const ref = useCallback((img) => {
|
|
55
|
+
const prev = imgRef.current;
|
|
56
|
+
if (prev) {
|
|
57
|
+
prev.removeEventListener("load", onLoad);
|
|
58
|
+
prev.removeEventListener("error", onError);
|
|
59
|
+
}
|
|
60
|
+
imgRef.current = img;
|
|
61
|
+
if (!img) return;
|
|
62
|
+
if (img.complete && img.naturalWidth > 0) setState((prev$1) => ({
|
|
63
|
+
...prev$1,
|
|
64
|
+
loaded: true
|
|
65
|
+
}));
|
|
66
|
+
img.addEventListener("load", onLoad);
|
|
67
|
+
img.addEventListener("error", onError);
|
|
68
|
+
}, [onLoad, onError]);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
return () => {
|
|
71
|
+
const img = imgRef.current;
|
|
72
|
+
if (img) {
|
|
73
|
+
img.removeEventListener("load", onLoad);
|
|
74
|
+
img.removeEventListener("error", onError);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}, [onLoad, onError]);
|
|
78
|
+
return [ref, isLoaded];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/react/prop-resolvers.ts
|
|
83
|
+
/**
|
|
84
|
+
* RegExpr to determine whether a src in a srcset is using width descriptors.
|
|
85
|
+
* Should match something like: "100w, 200w".
|
|
86
|
+
*/
|
|
87
|
+
const VALID_WIDTH_DESCRIPTOR_SRCSET = /^((\s*\d+w\s*(,|$)){1,})$/;
|
|
88
|
+
function resolveOptions(prop) {
|
|
89
|
+
const { src, ...rest } = prop;
|
|
8
90
|
const resolved = { ...rest };
|
|
9
91
|
if (typeof src === "object") {
|
|
10
92
|
resolved.src = src.src;
|
|
@@ -13,18 +95,46 @@ function resolveOptions(props) {
|
|
|
13
95
|
resolved.srcSet ??= src.srcSets;
|
|
14
96
|
resolved.placeholderUrl ??= src.placeholderUrl;
|
|
15
97
|
} else resolved.src = src;
|
|
16
|
-
if (
|
|
98
|
+
if (prop.asap) {
|
|
17
99
|
resolved.decoding = "async";
|
|
18
100
|
resolved.loading = "eager";
|
|
19
101
|
resolved.fetchPriority = "high";
|
|
20
|
-
if (preload) preload(resolved.src, {
|
|
21
|
-
as: "image",
|
|
22
|
-
fetchPriority: "high"
|
|
23
|
-
});
|
|
24
102
|
}
|
|
25
|
-
|
|
103
|
+
resolved.sizes = resolveSizes(prop);
|
|
104
|
+
resolved.loading = resolveLoading(prop);
|
|
26
105
|
return resolved;
|
|
27
106
|
}
|
|
107
|
+
function resolveLoading(prop) {
|
|
108
|
+
if (!prop.asap && prop.loading !== void 0) return prop.loading;
|
|
109
|
+
return prop.asap ? "eager" : "lazy";
|
|
110
|
+
}
|
|
111
|
+
function resolveSizes(prop) {
|
|
112
|
+
const loading = resolveLoading(prop);
|
|
113
|
+
let sizes = prop.sizes;
|
|
114
|
+
if (prop.fill) sizes ||= "100vw";
|
|
115
|
+
if (sizes) {
|
|
116
|
+
if (loading === "lazy") sizes = "auto, " + sizes;
|
|
117
|
+
} else if (prop.srcSet && VALID_WIDTH_DESCRIPTOR_SRCSET.test(prop.srcSet) && loading === "lazy") sizes = "auto, 100vw";
|
|
118
|
+
return sizes;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/react/prop-asserts.ts
|
|
123
|
+
function assertProps(prop) {
|
|
124
|
+
assertLoadingProp(prop);
|
|
125
|
+
}
|
|
126
|
+
function assert(assertion, message) {
|
|
127
|
+
if (import.meta.env.DEV) {
|
|
128
|
+
if (assertion()) throw new Error(message);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function assertLoadingProp(prop) {
|
|
132
|
+
assert(() => prop.loading && prop.asap, `Do not use \`loading\` on a asap image — asap images are always eagerly loaded.`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/react/image.tsx
|
|
137
|
+
const preload = "preload" in ReactDOM && typeof ReactDOM.preload === "function" ? ReactDOM.preload : null;
|
|
28
138
|
function getPlaceholderStyles(props) {
|
|
29
139
|
if (!props.placeholderUrl) return {};
|
|
30
140
|
return {
|
|
@@ -43,15 +153,22 @@ function getFillStyles(props) {
|
|
|
43
153
|
};
|
|
44
154
|
}
|
|
45
155
|
function Image(props) {
|
|
156
|
+
assertProps(props);
|
|
46
157
|
const options = resolveOptions(props);
|
|
47
|
-
const
|
|
158
|
+
const [imgRef, isLoaded] = useImgLoaded(options.src);
|
|
159
|
+
const placeholderStyles = isLoaded ? {} : getPlaceholderStyles(options);
|
|
48
160
|
const fillStyles = getFillStyles(options);
|
|
49
161
|
const styles = {
|
|
50
162
|
...placeholderStyles,
|
|
51
163
|
...fillStyles,
|
|
52
164
|
...props.style
|
|
53
165
|
};
|
|
166
|
+
if (preload && options.asap) preload(options.src, {
|
|
167
|
+
as: "image",
|
|
168
|
+
fetchPriority: "high"
|
|
169
|
+
});
|
|
54
170
|
return /* @__PURE__ */ jsx("img", {
|
|
171
|
+
ref: imgRef,
|
|
55
172
|
className: props.className,
|
|
56
173
|
style: styles,
|
|
57
174
|
src: options.src,
|
|
@@ -67,4 +184,4 @@ function Image(props) {
|
|
|
67
184
|
}
|
|
68
185
|
|
|
69
186
|
//#endregion
|
|
70
|
-
export { Image };
|
|
187
|
+
export { Image, useImgLoaded };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lonik/oh-image",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"description": "A React component library for optimized image handling.",
|
|
6
6
|
"author": "Luka Onikadze <lukonik@gmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -51,29 +51,33 @@
|
|
|
51
51
|
"sharp": ">=0.34.5"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
+
"@commitlint/config-conventional": "^20.4.1",
|
|
54
55
|
"@eslint/js": "^9.39.2",
|
|
56
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
57
|
+
"@testing-library/react": "^16.3.2",
|
|
55
58
|
"@tsconfig/strictest": "^2.0.8",
|
|
56
59
|
"@types/node": "^25.0.3",
|
|
57
60
|
"@types/react": "^18",
|
|
58
61
|
"@types/react-dom": "^18",
|
|
59
62
|
"@vitejs/plugin-react": "^5.1.2",
|
|
60
|
-
"@vitest/browser-playwright": "^4.0.16",
|
|
61
63
|
"bumpp": "^10.3.2",
|
|
62
64
|
"eslint": "^9.39.2",
|
|
63
65
|
"eslint-config-prettier": "^10.1.8",
|
|
64
66
|
"eslint-plugin-react": "^7.37.5",
|
|
65
67
|
"globals": "^17.3.0",
|
|
68
|
+
"happy-dom": "^20.6.0",
|
|
66
69
|
"tsdown": "^0.18.1",
|
|
67
70
|
"typescript": "^5.9.3",
|
|
68
71
|
"typescript-eslint": "^8.54.0",
|
|
69
72
|
"vite": "^7.3.0",
|
|
70
|
-
"vitest": "^4.0.16"
|
|
71
|
-
"vitest-browser-react": "^2.0.2",
|
|
72
|
-
"@commitlint/config-conventional": "^20.4.1"
|
|
73
|
+
"vitest": "^4.0.16"
|
|
73
74
|
},
|
|
74
75
|
"dependencies": {
|
|
76
|
+
"@types/supertest": "^6.0.3",
|
|
77
|
+
"memfs": "^4.56.10",
|
|
75
78
|
"p-limit": "^7.3.0",
|
|
76
|
-
"query-string": "^9.3.1"
|
|
79
|
+
"query-string": "^9.3.1",
|
|
80
|
+
"supertest": "^7.2.2"
|
|
77
81
|
},
|
|
78
82
|
"keywords": [
|
|
79
83
|
"react",
|