@lonik/oh-image 1.2.8 → 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 +290 -25
- 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
|
|
@@ -79,30 +79,168 @@ function useImgLoaded(src) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
//#endregion
|
|
82
|
-
//#region src/react/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
if (props.fill) resolved.sizes ||= "100vw";
|
|
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, 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);
|
|
104
102
|
return resolved;
|
|
105
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
|
+
}
|
|
126
|
+
function resolveLoading(prop) {
|
|
127
|
+
if (!prop.asap && prop.loading !== void 0) return prop.loading;
|
|
128
|
+
return prop.asap ? "eager" : "lazy";
|
|
129
|
+
}
|
|
130
|
+
function resolveSizes(prop, resolvedSrcSet, resolvedLoading) {
|
|
131
|
+
const loading = resolvedLoading ?? resolveLoading(prop);
|
|
132
|
+
const srcSet = resolvedSrcSet ?? prop.srcSet;
|
|
133
|
+
let sizes = prop.sizes;
|
|
134
|
+
if (prop.fill) sizes ||= "100vw";
|
|
135
|
+
if (sizes) {
|
|
136
|
+
if (loading === "lazy") sizes = "auto, " + sizes;
|
|
137
|
+
} else if (srcSet && VALID_WIDTH_DESCRIPTOR_SRCSET.test(srcSet) && loading === "lazy") sizes = "auto, 100vw";
|
|
138
|
+
return sizes;
|
|
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
|
+
}
|
|
167
|
+
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/react/prop-asserts.ts
|
|
170
|
+
function assertProps(prop) {
|
|
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
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function assert(assertion, message) {
|
|
184
|
+
if (import.meta.env.DEV) {
|
|
185
|
+
if (assertion()) throw new Error(message || void 0);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function assertLoadingProp(prop) {
|
|
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
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/react/image.tsx
|
|
243
|
+
const preload = "preload" in ReactDOM && typeof ReactDOM.preload === "function" ? ReactDOM.preload : null;
|
|
106
244
|
function getPlaceholderStyles(props) {
|
|
107
245
|
if (!props.placeholderUrl) return {};
|
|
108
246
|
return {
|
|
@@ -121,7 +259,8 @@ function getFillStyles(props) {
|
|
|
121
259
|
};
|
|
122
260
|
}
|
|
123
261
|
function Image(props) {
|
|
124
|
-
|
|
262
|
+
assertProps(props);
|
|
263
|
+
const options = resolveOptions(props, useImageContext());
|
|
125
264
|
const [imgRef, isLoaded] = useImgLoaded(options.src);
|
|
126
265
|
const placeholderStyles = isLoaded ? {} : getPlaceholderStyles(options);
|
|
127
266
|
const fillStyles = getFillStyles(options);
|
|
@@ -130,6 +269,10 @@ function Image(props) {
|
|
|
130
269
|
...fillStyles,
|
|
131
270
|
...props.style
|
|
132
271
|
};
|
|
272
|
+
if (preload && options.asap) preload(options.src, {
|
|
273
|
+
as: "image",
|
|
274
|
+
fetchPriority: "high"
|
|
275
|
+
});
|
|
133
276
|
return /* @__PURE__ */ jsx("img", {
|
|
134
277
|
ref: imgRef,
|
|
135
278
|
className: props.className,
|
|
@@ -147,4 +290,126 @@ function Image(props) {
|
|
|
147
290
|
}
|
|
148
291
|
|
|
149
292
|
//#endregion
|
|
150
|
-
|
|
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",
|