@lonik/oh-image 1.3.0 → 2.0.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.d.ts +22 -2
- package/dist/plugin.js +79 -10
- package/dist/react.d.ts +67 -2
- package/dist/react.js +254 -26
- package/package.json +5 -1
package/dist/plugin.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { FormatEnum } from "sharp";
|
|
|
2
2
|
import { Plugin } from "vite";
|
|
3
3
|
|
|
4
4
|
//#region src/plugin/types.d.ts
|
|
5
|
-
interface PluginConfig extends Required<Pick<ImageOptions, "placeholder" | "
|
|
5
|
+
interface PluginConfig extends Required<Pick<ImageOptions, "placeholder" | "breakpoints" | "format">> {
|
|
6
6
|
/** Directory name where processed images will be output during build */
|
|
7
7
|
distDir: string;
|
|
8
8
|
}
|
|
@@ -16,7 +16,27 @@ interface ImageOptions {
|
|
|
16
16
|
/** Whether to generate a placeholder image for lazy loading */
|
|
17
17
|
placeholder?: boolean;
|
|
18
18
|
/** Breakpoints array - widths in pixels for responsive srcSet generation */
|
|
19
|
-
|
|
19
|
+
breakpoints?: number[];
|
|
20
|
+
/** Blur the image */
|
|
21
|
+
blur?: number | null;
|
|
22
|
+
/** Flip the image vertically */
|
|
23
|
+
flip?: boolean | null;
|
|
24
|
+
/** Flop the image horizontally */
|
|
25
|
+
flop?: boolean | null;
|
|
26
|
+
/** Rotate the image */
|
|
27
|
+
rotate?: number | null;
|
|
28
|
+
/** Sharpen the image */
|
|
29
|
+
sharpen?: number | null;
|
|
30
|
+
/** Apply median filter */
|
|
31
|
+
median?: number | null;
|
|
32
|
+
/** Apply gamma correction */
|
|
33
|
+
gamma?: number | null;
|
|
34
|
+
/** Negate the image */
|
|
35
|
+
negate?: boolean | null;
|
|
36
|
+
/** Normalize the image */
|
|
37
|
+
normalize?: boolean | null;
|
|
38
|
+
/** Apply threshold */
|
|
39
|
+
threshold?: number | null;
|
|
20
40
|
}
|
|
21
41
|
//#endregion
|
|
22
42
|
//#region src/plugin/plugin.d.ts
|
package/dist/plugin.js
CHANGED
|
@@ -16,7 +16,19 @@ function queryToOptions(processKey, uri) {
|
|
|
16
16
|
parseBooleans: true,
|
|
17
17
|
parseNumbers: true,
|
|
18
18
|
arrayFormat: "comma",
|
|
19
|
-
types: {
|
|
19
|
+
types: {
|
|
20
|
+
breakpoints: "number[]",
|
|
21
|
+
blur: "number",
|
|
22
|
+
flip: "boolean",
|
|
23
|
+
flop: "boolean",
|
|
24
|
+
rotate: "number",
|
|
25
|
+
sharpen: "number",
|
|
26
|
+
median: "number",
|
|
27
|
+
gamma: "number",
|
|
28
|
+
negate: "boolean",
|
|
29
|
+
normalize: "boolean",
|
|
30
|
+
threshold: "number"
|
|
31
|
+
}
|
|
20
32
|
});
|
|
21
33
|
if (processKey in parsed) return {
|
|
22
34
|
shouldProcess: true,
|
|
@@ -94,7 +106,17 @@ function createImageEntries() {
|
|
|
94
106
|
width: entry.width,
|
|
95
107
|
height: entry.height,
|
|
96
108
|
format: entry.format,
|
|
97
|
-
origin: entry.origin
|
|
109
|
+
origin: entry.origin,
|
|
110
|
+
blur: entry.blur,
|
|
111
|
+
flip: entry.flip,
|
|
112
|
+
flop: entry.flop,
|
|
113
|
+
rotate: entry.rotate,
|
|
114
|
+
sharpen: entry.sharpen,
|
|
115
|
+
median: entry.median,
|
|
116
|
+
gamma: entry.gamma,
|
|
117
|
+
negate: entry.negate,
|
|
118
|
+
normalize: entry.normalize,
|
|
119
|
+
threshold: entry.threshold
|
|
98
120
|
};
|
|
99
121
|
this.set(identifier, mainEntry);
|
|
100
122
|
},
|
|
@@ -113,7 +135,16 @@ function createImageEntries() {
|
|
|
113
135
|
height: placeholderHeight,
|
|
114
136
|
format: main.format,
|
|
115
137
|
blur: PLACEHOLDER_BLUR_QUALITY,
|
|
116
|
-
origin: main.origin
|
|
138
|
+
origin: main.origin,
|
|
139
|
+
flip: main.flip,
|
|
140
|
+
flop: main.flop,
|
|
141
|
+
rotate: main.rotate,
|
|
142
|
+
sharpen: main.sharpen,
|
|
143
|
+
median: main.median,
|
|
144
|
+
gamma: main.gamma,
|
|
145
|
+
negate: main.negate,
|
|
146
|
+
normalize: main.normalize,
|
|
147
|
+
threshold: main.threshold
|
|
117
148
|
};
|
|
118
149
|
this.set(identifier, placeholderEntry);
|
|
119
150
|
},
|
|
@@ -133,6 +164,15 @@ async function processImage(path, options) {
|
|
|
133
164
|
});
|
|
134
165
|
if (options.format) processed = processed.toFormat(options.format);
|
|
135
166
|
if (options.blur) processed = processed.blur(options.blur);
|
|
167
|
+
if (options.flip) processed = processed.flip();
|
|
168
|
+
if (options.flop) processed = processed.flop();
|
|
169
|
+
if (options.rotate) processed = processed.rotate(options.rotate);
|
|
170
|
+
if (options.sharpen) processed = processed.sharpen(options.sharpen);
|
|
171
|
+
if (options.median) processed = processed.median(options.median);
|
|
172
|
+
if (options.gamma) processed = processed.gamma(options.gamma);
|
|
173
|
+
if (options.negate) processed = processed.negate();
|
|
174
|
+
if (options.normalize) processed = processed.normalize();
|
|
175
|
+
if (options.threshold) processed = processed.threshold(options.threshold);
|
|
136
176
|
return await processed.toBuffer();
|
|
137
177
|
}
|
|
138
178
|
|
|
@@ -141,7 +181,7 @@ async function processImage(path, options) {
|
|
|
141
181
|
const DEFAULT_IMAGE_FORMAT = "webp";
|
|
142
182
|
const DEFAULT_CONFIGS = {
|
|
143
183
|
distDir: "oh-images",
|
|
144
|
-
|
|
184
|
+
breakpoints: [
|
|
145
185
|
16,
|
|
146
186
|
48,
|
|
147
187
|
96,
|
|
@@ -229,7 +269,17 @@ function ohImage(options) {
|
|
|
229
269
|
width: mergedOptions.width,
|
|
230
270
|
height: mergedOptions.height,
|
|
231
271
|
format: mergedOptions.format,
|
|
232
|
-
origin
|
|
272
|
+
origin,
|
|
273
|
+
blur: mergedOptions.blur,
|
|
274
|
+
flip: mergedOptions.flip,
|
|
275
|
+
flop: mergedOptions.flop,
|
|
276
|
+
rotate: mergedOptions.rotate,
|
|
277
|
+
sharpen: mergedOptions.sharpen,
|
|
278
|
+
median: mergedOptions.median,
|
|
279
|
+
gamma: mergedOptions.gamma,
|
|
280
|
+
negate: mergedOptions.negate,
|
|
281
|
+
normalize: mergedOptions.normalize,
|
|
282
|
+
threshold: mergedOptions.threshold
|
|
233
283
|
});
|
|
234
284
|
const src = {
|
|
235
285
|
width: metadata.width,
|
|
@@ -237,24 +287,43 @@ function ohImage(options) {
|
|
|
237
287
|
src: mainIdentifier,
|
|
238
288
|
srcSets: ""
|
|
239
289
|
};
|
|
240
|
-
if (
|
|
290
|
+
if (mergedOptions.placeholder) {
|
|
241
291
|
const placeholderIdentifier = identifier.placeholder(DEFAULT_IMAGE_FORMAT);
|
|
242
292
|
imageEntries.createPlaceholderEntry(placeholderIdentifier, {
|
|
243
293
|
width: metadata.width,
|
|
244
294
|
height: metadata.height,
|
|
245
295
|
format: DEFAULT_IMAGE_FORMAT,
|
|
246
|
-
origin
|
|
296
|
+
origin,
|
|
297
|
+
flip: mergedOptions.flip,
|
|
298
|
+
flop: mergedOptions.flop,
|
|
299
|
+
rotate: mergedOptions.rotate,
|
|
300
|
+
sharpen: mergedOptions.sharpen,
|
|
301
|
+
median: mergedOptions.median,
|
|
302
|
+
gamma: mergedOptions.gamma,
|
|
303
|
+
negate: mergedOptions.negate,
|
|
304
|
+
normalize: mergedOptions.normalize,
|
|
305
|
+
threshold: mergedOptions.threshold
|
|
247
306
|
});
|
|
248
307
|
src.placeholderUrl = placeholderIdentifier;
|
|
249
308
|
}
|
|
250
|
-
if (mergedOptions.
|
|
309
|
+
if (mergedOptions.breakpoints) {
|
|
251
310
|
const srcSets = [];
|
|
252
|
-
for (const breakpoint of mergedOptions.
|
|
311
|
+
for (const breakpoint of mergedOptions.breakpoints) {
|
|
253
312
|
const srcSetIdentifier = identifier.srcSet(DEFAULT_IMAGE_FORMAT, breakpoint);
|
|
254
313
|
imageEntries.createSrcSetEntry(srcSetIdentifier, {
|
|
255
314
|
width: breakpoint,
|
|
256
315
|
format: DEFAULT_IMAGE_FORMAT,
|
|
257
|
-
origin
|
|
316
|
+
origin,
|
|
317
|
+
blur: mergedOptions.blur,
|
|
318
|
+
flip: mergedOptions.flip,
|
|
319
|
+
flop: mergedOptions.flop,
|
|
320
|
+
rotate: mergedOptions.rotate,
|
|
321
|
+
sharpen: mergedOptions.sharpen,
|
|
322
|
+
median: mergedOptions.median,
|
|
323
|
+
gamma: mergedOptions.gamma,
|
|
324
|
+
negate: mergedOptions.negate,
|
|
325
|
+
normalize: mergedOptions.normalize,
|
|
326
|
+
threshold: mergedOptions.threshold
|
|
258
327
|
});
|
|
259
328
|
srcSets.push(`${srcSetIdentifier} ${breakpoint}w`);
|
|
260
329
|
}
|
package/dist/react.d.ts
CHANGED
|
@@ -2,8 +2,15 @@ import { ImgHTMLAttributes } from "react";
|
|
|
2
2
|
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
3
|
|
|
4
4
|
//#region src/react/types.d.ts
|
|
5
|
+
interface ImageLoaderOptions {
|
|
6
|
+
src: string;
|
|
7
|
+
width?: number | null | undefined;
|
|
8
|
+
height?: number | null | undefined;
|
|
9
|
+
isPlaceholder?: boolean;
|
|
10
|
+
}
|
|
11
|
+
type ImageLoader = (options: ImageLoaderOptions) => string;
|
|
5
12
|
type ImageSrcType = string | ImageSrc;
|
|
6
|
-
interface ImageProps extends Partial<Pick<ImgHTMLAttributes<HTMLImageElement>, "fetchPriority" | "decoding" | "loading" | "
|
|
13
|
+
interface ImageProps extends Partial<Pick<ImgHTMLAttributes<HTMLImageElement>, "fetchPriority" | "decoding" | "loading" | "srcSet" | "className" | "sizes" | "style">> {
|
|
7
14
|
/** Alternative text for the image, required for accessibility. Use an empty string for decorative images. */
|
|
8
15
|
alt: string;
|
|
9
16
|
/** Configures the Image component to load the image immediately. */
|
|
@@ -17,6 +24,10 @@ interface ImageProps extends Partial<Pick<ImgHTMLAttributes<HTMLImageElement>, "
|
|
|
17
24
|
* styles such that the image fills its containing element.
|
|
18
25
|
*/
|
|
19
26
|
fill?: boolean;
|
|
27
|
+
loader?: ImageLoader | null;
|
|
28
|
+
width?: number | undefined;
|
|
29
|
+
height?: number | undefined;
|
|
30
|
+
breakpoints?: number[];
|
|
20
31
|
}
|
|
21
32
|
//#endregion
|
|
22
33
|
//#region src/react/image.d.ts
|
|
@@ -51,4 +62,58 @@ declare function Image(props: ImageProps): react_jsx_runtime0.JSX.Element;
|
|
|
51
62
|
*/
|
|
52
63
|
declare function useImgLoaded(src: string | undefined): [(img: HTMLImageElement | null) => void, boolean];
|
|
53
64
|
//#endregion
|
|
54
|
-
|
|
65
|
+
//#region src/react/loaders/cloudflare-context.d.ts
|
|
66
|
+
interface CloudflareLoaderOptions {
|
|
67
|
+
path: string;
|
|
68
|
+
placeholder: boolean;
|
|
69
|
+
format: string;
|
|
70
|
+
params?: Record<string, string>;
|
|
71
|
+
placeholderParams?: Record<string, string>;
|
|
72
|
+
breakpoints?: number[];
|
|
73
|
+
}
|
|
74
|
+
declare function useCloudflareContext(): CloudflareLoaderOptions;
|
|
75
|
+
declare function CloudflareLoaderProvider({
|
|
76
|
+
children,
|
|
77
|
+
...props
|
|
78
|
+
}: {
|
|
79
|
+
children: React.ReactNode;
|
|
80
|
+
} & Partial<CloudflareLoaderOptions>): react_jsx_runtime0.JSX.Element;
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/react/loaders/cloudflare-loader.d.ts
|
|
83
|
+
declare function useCloudflareLoader(options?: Partial<CloudflareLoaderOptions>): ImageLoader;
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/react/loaders/imgproxy-context.d.ts
|
|
86
|
+
interface ImgproxyLoaderOptions {
|
|
87
|
+
path: string;
|
|
88
|
+
placeholder: boolean;
|
|
89
|
+
format: string;
|
|
90
|
+
params?: Record<string, string>;
|
|
91
|
+
placeholderParams?: Record<string, string>;
|
|
92
|
+
breakpoints?: number[];
|
|
93
|
+
paramsSeparator?: string;
|
|
94
|
+
}
|
|
95
|
+
declare function useImgproxyContext(): ImgproxyLoaderOptions;
|
|
96
|
+
declare function ImgproxyLoaderProvider({
|
|
97
|
+
children,
|
|
98
|
+
...props
|
|
99
|
+
}: {
|
|
100
|
+
children: React.ReactNode;
|
|
101
|
+
} & Partial<ImgproxyLoaderOptions>): react_jsx_runtime0.JSX.Element;
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/react/loaders/imgproxy-loader.d.ts
|
|
104
|
+
declare function useImgproxyLoader(options?: Partial<ImgproxyLoaderOptions>): ImageLoader;
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/react/image-context.d.ts
|
|
107
|
+
interface ImageContextValue extends Pick<ImageProps, "loading"> {
|
|
108
|
+
breakpoints: number[];
|
|
109
|
+
loader: ImageLoader | null;
|
|
110
|
+
}
|
|
111
|
+
declare function useImageContext(): ImageContextValue;
|
|
112
|
+
declare function ImageProvider({
|
|
113
|
+
children,
|
|
114
|
+
...props
|
|
115
|
+
}: {
|
|
116
|
+
children: React.ReactNode;
|
|
117
|
+
} & Partial<ImageContextValue>): react_jsx_runtime0.JSX.Element;
|
|
118
|
+
//#endregion
|
|
119
|
+
export { type CloudflareLoaderOptions, CloudflareLoaderProvider, Image, type ImageLoader, type ImageLoaderOptions, type ImageProps, ImageProvider, type ImageSrcType, type ImgproxyLoaderOptions, ImgproxyLoaderProvider, useCloudflareContext, useCloudflareLoader, useImageContext, useImgLoaded, useImgproxyContext, useImgproxyLoader };
|
package/dist/react.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ReactDOM from "react-dom";
|
|
2
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
|
|
5
5
|
//#region src/react/use-img-loaded.ts
|
|
@@ -85,51 +85,157 @@ function useImgLoaded(src) {
|
|
|
85
85
|
* Should match something like: "100w, 200w".
|
|
86
86
|
*/
|
|
87
87
|
const VALID_WIDTH_DESCRIPTOR_SRCSET = /^((\s*\d+w\s*(,|$)){1,})$/;
|
|
88
|
-
function resolveOptions(prop) {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
resolved.sizes = resolveSizes(prop);
|
|
104
|
-
resolved.loading = resolveLoading(prop);
|
|
88
|
+
function resolveOptions(prop, defaultOptions) {
|
|
89
|
+
const resolved = {
|
|
90
|
+
...defaultOptions,
|
|
91
|
+
...prop
|
|
92
|
+
};
|
|
93
|
+
resolved.decoding = resolveDecoding(resolved);
|
|
94
|
+
resolved.fetchPriority = resolveFetchPriority(resolved);
|
|
95
|
+
resolved.loading = resolveLoading(resolved);
|
|
96
|
+
resolved.srcSet = resolveSrcSet(resolved);
|
|
97
|
+
resolved.sizes = resolveSizes(resolved, resolved.srcSet, resolved.loading);
|
|
98
|
+
resolved.placeholderUrl = resolvePlaceholderURL(resolved);
|
|
99
|
+
resolved.height = resolveHeight(resolved);
|
|
100
|
+
resolved.width = resolveWidth(resolved);
|
|
101
|
+
resolved.src = resolveSrc(resolved);
|
|
105
102
|
return resolved;
|
|
106
103
|
}
|
|
104
|
+
function resolveDecoding(prop) {
|
|
105
|
+
return prop.asap ? "async" : prop.decoding;
|
|
106
|
+
}
|
|
107
|
+
function resolveFetchPriority(prop) {
|
|
108
|
+
if (prop.asap) return "high";
|
|
109
|
+
return prop.fetchPriority ?? "auto";
|
|
110
|
+
}
|
|
111
|
+
function resolveSrcSet(prop) {
|
|
112
|
+
if (prop.srcSet) return prop.srcSet;
|
|
113
|
+
if (typeof prop.src === "object") return prop.src.srcSets;
|
|
114
|
+
if (!prop.breakpoints) return;
|
|
115
|
+
const baseSrc = prop.src;
|
|
116
|
+
const entries = [];
|
|
117
|
+
for (const breakpoint of prop.breakpoints) if (prop.loader) entries.push(`${prop.loader({
|
|
118
|
+
src: baseSrc,
|
|
119
|
+
width: breakpoint,
|
|
120
|
+
height: prop.height,
|
|
121
|
+
isPlaceholder: false
|
|
122
|
+
})} ${breakpoint}w`);
|
|
123
|
+
if (entries.length === 0) return;
|
|
124
|
+
return entries.join(", ");
|
|
125
|
+
}
|
|
107
126
|
function resolveLoading(prop) {
|
|
108
127
|
if (!prop.asap && prop.loading !== void 0) return prop.loading;
|
|
109
128
|
return prop.asap ? "eager" : "lazy";
|
|
110
129
|
}
|
|
111
|
-
function resolveSizes(prop) {
|
|
112
|
-
const loading = resolveLoading(prop);
|
|
130
|
+
function resolveSizes(prop, resolvedSrcSet, resolvedLoading) {
|
|
131
|
+
const loading = resolvedLoading ?? resolveLoading(prop);
|
|
132
|
+
const srcSet = resolvedSrcSet ?? prop.srcSet;
|
|
113
133
|
let sizes = prop.sizes;
|
|
114
134
|
if (prop.fill) sizes ||= "100vw";
|
|
115
135
|
if (sizes) {
|
|
116
136
|
if (loading === "lazy") sizes = "auto, " + sizes;
|
|
117
|
-
} else if (
|
|
137
|
+
} else if (srcSet && VALID_WIDTH_DESCRIPTOR_SRCSET.test(srcSet) && loading === "lazy") sizes = "auto, 100vw";
|
|
118
138
|
return sizes;
|
|
119
139
|
}
|
|
140
|
+
function resolveSrc(prop) {
|
|
141
|
+
if (typeof prop.src === "object") return prop.src.src;
|
|
142
|
+
if (prop.loader) return prop.loader({
|
|
143
|
+
src: prop.src,
|
|
144
|
+
width: prop.width,
|
|
145
|
+
height: prop.height
|
|
146
|
+
});
|
|
147
|
+
return prop.src;
|
|
148
|
+
}
|
|
149
|
+
function resolveWidth(prop) {
|
|
150
|
+
if (prop.width) return prop.width;
|
|
151
|
+
if (typeof prop.src === "object") return prop.src.width;
|
|
152
|
+
}
|
|
153
|
+
function resolveHeight(prop) {
|
|
154
|
+
if (prop.height) return prop.height;
|
|
155
|
+
if (typeof prop.src === "object") return prop.src.height;
|
|
156
|
+
}
|
|
157
|
+
function resolvePlaceholderURL(prop) {
|
|
158
|
+
if (prop.placeholderUrl) return prop.placeholderUrl;
|
|
159
|
+
if (typeof prop.src === "object") return prop.src.placeholderUrl;
|
|
160
|
+
if (prop.loader) return prop.loader({
|
|
161
|
+
isPlaceholder: true,
|
|
162
|
+
src: prop.src,
|
|
163
|
+
width: prop.width,
|
|
164
|
+
height: prop.height
|
|
165
|
+
});
|
|
166
|
+
}
|
|
120
167
|
|
|
121
168
|
//#endregion
|
|
122
169
|
//#region src/react/prop-asserts.ts
|
|
123
170
|
function assertProps(prop) {
|
|
124
|
-
|
|
171
|
+
try {
|
|
172
|
+
assertLoadingProp(prop);
|
|
173
|
+
assertDecodingProp(prop);
|
|
174
|
+
assertFetchPriorityProp(prop);
|
|
175
|
+
assertBreakpointsProp(prop);
|
|
176
|
+
assertFillProp(prop);
|
|
177
|
+
assertDimensionsProp(prop);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
const message = err instanceof Error ? err.message : err;
|
|
180
|
+
console.warn(message);
|
|
181
|
+
}
|
|
125
182
|
}
|
|
126
183
|
function assert(assertion, message) {
|
|
127
184
|
if (import.meta.env.DEV) {
|
|
128
|
-
if (assertion()) throw new Error(message);
|
|
185
|
+
if (assertion()) throw new Error(message || void 0);
|
|
129
186
|
}
|
|
130
187
|
}
|
|
131
188
|
function assertLoadingProp(prop) {
|
|
132
|
-
assert(() => prop.loading && prop.asap, `Do not use \`loading\` on a asap image — asap images are always eagerly loaded.`);
|
|
189
|
+
assert(() => prop.loading && prop.asap, import.meta.env.DEV && `Do not use \`loading\` on a asap image — asap images are always eagerly loaded.`);
|
|
190
|
+
}
|
|
191
|
+
function assertDecodingProp(prop) {
|
|
192
|
+
assert(() => prop.decoding && prop.asap, import.meta.env.DEV && `Do not use \`decoding\` on a asap image — asap images always use async decoding.`);
|
|
193
|
+
}
|
|
194
|
+
function assertFetchPriorityProp(prop) {
|
|
195
|
+
assert(() => prop.fetchPriority && prop.asap, import.meta.env.DEV && `Do not use \`fetchPriority\` on a asap image — asap images always use high fetch priority.`);
|
|
196
|
+
}
|
|
197
|
+
function assertBreakpointsProp(prop) {
|
|
198
|
+
assert(() => prop.breakpoints && typeof prop.src === "object", import.meta.env.DEV && `Do not use \`breakpoints\` when \`src\` is an imported image — the image's built-in srcSets are used instead.`);
|
|
199
|
+
assert(() => prop.breakpoints && typeof prop.src === "string" && !prop.loader, import.meta.env.DEV && `Do not use \`breakpoints\` without a \`loader\` — breakpoints require a loader to generate srcSet entries.`);
|
|
200
|
+
}
|
|
201
|
+
function assertFillProp(prop) {
|
|
202
|
+
assert(() => prop.fill && (prop.width !== void 0 || prop.height !== void 0), import.meta.env.DEV && `Do not use \`width\` or \`height\` with \`fill\` — fill mode makes the image fill its container.`);
|
|
203
|
+
}
|
|
204
|
+
function assertDimensionsProp(prop) {
|
|
205
|
+
assert(() => typeof prop.src === "string" && !prop.fill && prop.width === void 0 && prop.height === void 0, import.meta.env.DEV && `Image is missing \`width\` and \`height\` props. Either provide dimensions, use \`fill\`, or use an imported image source.`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/react/image-context.tsx
|
|
210
|
+
const ImageContext = createContext({
|
|
211
|
+
breakpoints: [
|
|
212
|
+
16,
|
|
213
|
+
48,
|
|
214
|
+
96,
|
|
215
|
+
128,
|
|
216
|
+
384,
|
|
217
|
+
640,
|
|
218
|
+
750,
|
|
219
|
+
828,
|
|
220
|
+
1080,
|
|
221
|
+
1200,
|
|
222
|
+
1920
|
|
223
|
+
],
|
|
224
|
+
loading: "lazy",
|
|
225
|
+
loader: null
|
|
226
|
+
});
|
|
227
|
+
function useImageContext() {
|
|
228
|
+
return useContext(ImageContext);
|
|
229
|
+
}
|
|
230
|
+
function ImageProvider({ children, ...props }) {
|
|
231
|
+
const ctx = useImageContext();
|
|
232
|
+
return /* @__PURE__ */ jsx(ImageContext.Provider, {
|
|
233
|
+
value: {
|
|
234
|
+
...ctx,
|
|
235
|
+
...props
|
|
236
|
+
},
|
|
237
|
+
children
|
|
238
|
+
});
|
|
133
239
|
}
|
|
134
240
|
|
|
135
241
|
//#endregion
|
|
@@ -154,7 +260,7 @@ function getFillStyles(props) {
|
|
|
154
260
|
}
|
|
155
261
|
function Image(props) {
|
|
156
262
|
assertProps(props);
|
|
157
|
-
const options = resolveOptions(props);
|
|
263
|
+
const options = resolveOptions(props, useImageContext());
|
|
158
264
|
const [imgRef, isLoaded] = useImgLoaded(options.src);
|
|
159
265
|
const placeholderStyles = isLoaded ? {} : getPlaceholderStyles(options);
|
|
160
266
|
const fillStyles = getFillStyles(options);
|
|
@@ -184,4 +290,126 @@ function Image(props) {
|
|
|
184
290
|
}
|
|
185
291
|
|
|
186
292
|
//#endregion
|
|
187
|
-
|
|
293
|
+
//#region src/react/loaders/image-loader-utils.ts
|
|
294
|
+
function normalizeLoaderParams(params, separator) {
|
|
295
|
+
return Object.entries(params).map(([key, value]) => `${key}${separator}${value}`);
|
|
296
|
+
}
|
|
297
|
+
function isAbsoluteUrl(src) {
|
|
298
|
+
return /^https?:\/\//.test(src);
|
|
299
|
+
}
|
|
300
|
+
function assertPath(path) {
|
|
301
|
+
assert(() => !path?.trim(), import.meta.env.DEV && `Path is required`);
|
|
302
|
+
assert(() => {
|
|
303
|
+
try {
|
|
304
|
+
new URL(path);
|
|
305
|
+
return !isAbsoluteUrl(path);
|
|
306
|
+
} catch {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
}, import.meta.env.DEV && `Path is invalid url: ${path}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/react/loaders/cloudflare-context.tsx
|
|
314
|
+
const CloudflareContext = createContext({
|
|
315
|
+
path: "",
|
|
316
|
+
placeholder: true,
|
|
317
|
+
format: "auto",
|
|
318
|
+
placeholderParams: { quality: "low" }
|
|
319
|
+
});
|
|
320
|
+
function useCloudflareContext() {
|
|
321
|
+
return useContext(CloudflareContext);
|
|
322
|
+
}
|
|
323
|
+
function CloudflareLoaderProvider({ children, ...props }) {
|
|
324
|
+
const ctx = useCloudflareContext();
|
|
325
|
+
return /* @__PURE__ */ jsx(CloudflareContext.Provider, {
|
|
326
|
+
value: {
|
|
327
|
+
...ctx,
|
|
328
|
+
...props
|
|
329
|
+
},
|
|
330
|
+
children
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region src/react/loaders/cloudflare-loader.ts
|
|
336
|
+
function useCloudflareLoader(options) {
|
|
337
|
+
const resolvedOptions = {
|
|
338
|
+
...useCloudflareContext(),
|
|
339
|
+
...options
|
|
340
|
+
};
|
|
341
|
+
assertPath(resolvedOptions.path);
|
|
342
|
+
return (imageOptions) => {
|
|
343
|
+
const parts = [];
|
|
344
|
+
const format = resolvedOptions.format;
|
|
345
|
+
if (format) parts.push(`format=${format}`);
|
|
346
|
+
if (imageOptions.width) parts.push(`width=${imageOptions.width}`);
|
|
347
|
+
if (imageOptions.height) parts.push(`height=${imageOptions.height}`);
|
|
348
|
+
if (resolvedOptions.params) parts.push(...normalizeLoaderParams(resolvedOptions.params, "="));
|
|
349
|
+
if (imageOptions.isPlaceholder) {
|
|
350
|
+
if (resolvedOptions.placeholderParams) {
|
|
351
|
+
const placeholderParams = normalizeLoaderParams(resolvedOptions.placeholderParams, "=");
|
|
352
|
+
parts.push(...placeholderParams);
|
|
353
|
+
}
|
|
354
|
+
} else if (resolvedOptions.params) {
|
|
355
|
+
const params = normalizeLoaderParams(resolvedOptions.params, "=");
|
|
356
|
+
parts.push(...params);
|
|
357
|
+
}
|
|
358
|
+
const processingOptions = parts.join(",");
|
|
359
|
+
return `${resolvedOptions.path}/cdn-cgi/image/${processingOptions}/${imageOptions.src}`;
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region src/react/loaders/imgproxy-context.tsx
|
|
365
|
+
const ImgproxyContext = createContext({
|
|
366
|
+
path: "",
|
|
367
|
+
placeholder: true,
|
|
368
|
+
format: "webp",
|
|
369
|
+
placeholderParams: { quality: "1" }
|
|
370
|
+
});
|
|
371
|
+
function useImgproxyContext() {
|
|
372
|
+
return useContext(ImgproxyContext);
|
|
373
|
+
}
|
|
374
|
+
function ImgproxyLoaderProvider({ children, ...props }) {
|
|
375
|
+
const ctx = useImgproxyContext();
|
|
376
|
+
return /* @__PURE__ */ jsx(ImgproxyContext.Provider, {
|
|
377
|
+
value: {
|
|
378
|
+
...ctx,
|
|
379
|
+
...props
|
|
380
|
+
},
|
|
381
|
+
children
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region src/react/loaders/imgproxy-loader.ts
|
|
387
|
+
function useImgproxyLoader(options) {
|
|
388
|
+
const resolvedOptions = {
|
|
389
|
+
...useImgproxyContext(),
|
|
390
|
+
...options
|
|
391
|
+
};
|
|
392
|
+
assertPath(resolvedOptions.path);
|
|
393
|
+
return (imageOptions) => {
|
|
394
|
+
const parts = [];
|
|
395
|
+
const format = resolvedOptions.format;
|
|
396
|
+
const paramsSeparator = resolvedOptions.paramsSeparator ?? "/";
|
|
397
|
+
if (format) parts.push(`format:${format}`);
|
|
398
|
+
if (imageOptions.width) parts.push(`width:${imageOptions.width}`);
|
|
399
|
+
if (imageOptions.height) parts.push(`height:${imageOptions.height}`);
|
|
400
|
+
if (imageOptions.isPlaceholder) {
|
|
401
|
+
if (resolvedOptions.placeholderParams) {
|
|
402
|
+
const placeholderParams = normalizeLoaderParams(resolvedOptions.placeholderParams, ":");
|
|
403
|
+
parts.push(...placeholderParams);
|
|
404
|
+
}
|
|
405
|
+
} else if (resolvedOptions.params) {
|
|
406
|
+
const params = normalizeLoaderParams(resolvedOptions.params, ":");
|
|
407
|
+
parts.push(...params);
|
|
408
|
+
}
|
|
409
|
+
const processingOptions = parts.join(paramsSeparator);
|
|
410
|
+
return `${resolvedOptions.path}/${processingOptions}/plain/${imageOptions.src}`;
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
//#endregion
|
|
415
|
+
export { CloudflareLoaderProvider, Image, ImageProvider, ImgproxyLoaderProvider, useCloudflareContext, useCloudflareLoader, useImageContext, useImgLoaded, useImgproxyContext, useImgproxyLoader };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lonik/oh-image",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.0",
|
|
5
5
|
"description": "A React component library for optimized image handling.",
|
|
6
6
|
"author": "Luka Onikadze <lukonik@gmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"play:build": "vite build",
|
|
42
42
|
"play:preview": "vite preview",
|
|
43
43
|
"test": "vitest",
|
|
44
|
+
"coverage": "vitest --coverage",
|
|
44
45
|
"typecheck": "tsc --noEmit",
|
|
45
46
|
"release": "bumpp && pnpm publish",
|
|
46
47
|
"prepublishOnly": "pnpm run build"
|
|
@@ -60,10 +61,13 @@
|
|
|
60
61
|
"@types/react": "^18",
|
|
61
62
|
"@types/react-dom": "^18",
|
|
62
63
|
"@vitejs/plugin-react": "^5.1.2",
|
|
64
|
+
"@vitest/coverage-v8": "4.0.18",
|
|
63
65
|
"bumpp": "^10.3.2",
|
|
64
66
|
"eslint": "^9.39.2",
|
|
65
67
|
"eslint-config-prettier": "^10.1.8",
|
|
66
68
|
"eslint-plugin-react": "^7.37.5",
|
|
69
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
70
|
+
"eslint-plugin-react-refresh": "^0.5.0",
|
|
67
71
|
"globals": "^17.3.0",
|
|
68
72
|
"happy-dom": "^20.6.0",
|
|
69
73
|
"tsdown": "^0.18.1",
|