@lonik/oh-image 1.3.0 → 2.0.1

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 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" | "bps" | "format">> {
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
- bps?: number[];
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
@@ -10,30 +10,47 @@ function queryToOptions(processKey, uri) {
10
10
  const [path, query] = uri.split("?");
11
11
  if (!query || !path) return {
12
12
  shouldProcess: false,
13
- path: ""
13
+ path: "",
14
+ queryString: ""
14
15
  };
15
16
  const parsed = queryString.parse(query, {
16
17
  parseBooleans: true,
17
18
  parseNumbers: true,
18
19
  arrayFormat: "comma",
19
- types: { bps: "number[]" }
20
+ types: {
21
+ breakpoints: "number[]",
22
+ blur: "number",
23
+ flip: "boolean",
24
+ flop: "boolean",
25
+ rotate: "number",
26
+ sharpen: "number",
27
+ median: "number",
28
+ gamma: "number",
29
+ negate: "boolean",
30
+ normalize: "boolean",
31
+ threshold: "number"
32
+ }
20
33
  });
21
34
  if (processKey in parsed) return {
22
35
  shouldProcess: true,
23
36
  options: parsed,
24
- path
37
+ path,
38
+ queryString: query
25
39
  };
26
40
  else return {
27
41
  shouldProcess: false,
28
- path
42
+ path,
43
+ queryString: query
29
44
  };
30
45
  }
31
46
 
32
47
  //#endregion
33
48
  //#region src/plugin/file-utils.ts
34
- async function getFileHash(filePath) {
35
- const content = await readFile(filePath);
36
- return createHash("sha256").update(content).digest("hex").slice(0, 16);
49
+ async function getHash(value) {
50
+ return createHash("sha256").update(value).digest("hex").slice(0, 16);
51
+ }
52
+ async function getFileHash(filePath, queryString$1) {
53
+ return `${await getHash(await readFile(filePath))}-${await getHash(queryString$1)}`;
37
54
  }
38
55
  async function readFileSafe(path) {
39
56
  try {
@@ -94,7 +111,17 @@ function createImageEntries() {
94
111
  width: entry.width,
95
112
  height: entry.height,
96
113
  format: entry.format,
97
- origin: entry.origin
114
+ origin: entry.origin,
115
+ blur: entry.blur,
116
+ flip: entry.flip,
117
+ flop: entry.flop,
118
+ rotate: entry.rotate,
119
+ sharpen: entry.sharpen,
120
+ median: entry.median,
121
+ gamma: entry.gamma,
122
+ negate: entry.negate,
123
+ normalize: entry.normalize,
124
+ threshold: entry.threshold
98
125
  };
99
126
  this.set(identifier, mainEntry);
100
127
  },
@@ -113,7 +140,16 @@ function createImageEntries() {
113
140
  height: placeholderHeight,
114
141
  format: main.format,
115
142
  blur: PLACEHOLDER_BLUR_QUALITY,
116
- origin: main.origin
143
+ origin: main.origin,
144
+ flip: main.flip,
145
+ flop: main.flop,
146
+ rotate: main.rotate,
147
+ sharpen: main.sharpen,
148
+ median: main.median,
149
+ gamma: main.gamma,
150
+ negate: main.negate,
151
+ normalize: main.normalize,
152
+ threshold: main.threshold
117
153
  };
118
154
  this.set(identifier, placeholderEntry);
119
155
  },
@@ -133,6 +169,15 @@ async function processImage(path, options) {
133
169
  });
134
170
  if (options.format) processed = processed.toFormat(options.format);
135
171
  if (options.blur) processed = processed.blur(options.blur);
172
+ if (options.flip) processed = processed.flip();
173
+ if (options.flop) processed = processed.flop();
174
+ if (options.rotate) processed = processed.rotate(options.rotate);
175
+ if (options.sharpen) processed = processed.sharpen(options.sharpen);
176
+ if (options.median) processed = processed.median(options.median);
177
+ if (options.gamma) processed = processed.gamma(options.gamma);
178
+ if (options.negate) processed = processed.negate();
179
+ if (options.normalize) processed = processed.normalize();
180
+ if (options.threshold) processed = processed.threshold(options.threshold);
136
181
  return await processed.toBuffer();
137
182
  }
138
183
 
@@ -141,7 +186,7 @@ async function processImage(path, options) {
141
186
  const DEFAULT_IMAGE_FORMAT = "webp";
142
187
  const DEFAULT_CONFIGS = {
143
188
  distDir: "oh-images",
144
- bps: [
189
+ breakpoints: [
145
190
  16,
146
191
  48,
147
192
  96,
@@ -186,12 +231,12 @@ function ohImage(options) {
186
231
  const fileId = basename(url);
187
232
  const path = join(cacheDir, fileId);
188
233
  const ext = extname(url).slice(1);
189
- const image = await readFileSafe(path);
190
234
  const imageEntry = imageEntries.get(url);
191
235
  if (!imageEntry) {
192
236
  console.warn("Image entry not found with id: " + url);
193
237
  return next();
194
238
  }
239
+ const image = await readFileSafe(path);
195
240
  if (image) {
196
241
  res.setHeader("Content-Type", `image/${ext}`);
197
242
  res.end(image);
@@ -212,7 +257,7 @@ function ohImage(options) {
212
257
  const origin = parsed.path;
213
258
  const { name, ext } = parse(parsed.path);
214
259
  const metadata = await sharp(parsed.path).metadata();
215
- const hash = await getFileHash(origin);
260
+ const hash = await getFileHash(origin, parsed.queryString);
216
261
  const mergedOptions = {
217
262
  ...config,
218
263
  ...parsed.options
@@ -229,7 +274,17 @@ function ohImage(options) {
229
274
  width: mergedOptions.width,
230
275
  height: mergedOptions.height,
231
276
  format: mergedOptions.format,
232
- origin
277
+ origin,
278
+ blur: mergedOptions.blur,
279
+ flip: mergedOptions.flip,
280
+ flop: mergedOptions.flop,
281
+ rotate: mergedOptions.rotate,
282
+ sharpen: mergedOptions.sharpen,
283
+ median: mergedOptions.median,
284
+ gamma: mergedOptions.gamma,
285
+ negate: mergedOptions.negate,
286
+ normalize: mergedOptions.normalize,
287
+ threshold: mergedOptions.threshold
233
288
  });
234
289
  const src = {
235
290
  width: metadata.width,
@@ -237,24 +292,43 @@ function ohImage(options) {
237
292
  src: mainIdentifier,
238
293
  srcSets: ""
239
294
  };
240
- if (parsed.options?.placeholder) {
295
+ if (mergedOptions.placeholder) {
241
296
  const placeholderIdentifier = identifier.placeholder(DEFAULT_IMAGE_FORMAT);
242
297
  imageEntries.createPlaceholderEntry(placeholderIdentifier, {
243
298
  width: metadata.width,
244
299
  height: metadata.height,
245
300
  format: DEFAULT_IMAGE_FORMAT,
246
- origin
301
+ origin,
302
+ flip: mergedOptions.flip,
303
+ flop: mergedOptions.flop,
304
+ rotate: mergedOptions.rotate,
305
+ sharpen: mergedOptions.sharpen,
306
+ median: mergedOptions.median,
307
+ gamma: mergedOptions.gamma,
308
+ negate: mergedOptions.negate,
309
+ normalize: mergedOptions.normalize,
310
+ threshold: mergedOptions.threshold
247
311
  });
248
312
  src.placeholderUrl = placeholderIdentifier;
249
313
  }
250
- if (mergedOptions.bps) {
314
+ if (mergedOptions.breakpoints) {
251
315
  const srcSets = [];
252
- for (const breakpoint of mergedOptions.bps) {
316
+ for (const breakpoint of mergedOptions.breakpoints) {
253
317
  const srcSetIdentifier = identifier.srcSet(DEFAULT_IMAGE_FORMAT, breakpoint);
254
318
  imageEntries.createSrcSetEntry(srcSetIdentifier, {
255
319
  width: breakpoint,
256
320
  format: DEFAULT_IMAGE_FORMAT,
257
- origin
321
+ origin,
322
+ blur: mergedOptions.blur,
323
+ flip: mergedOptions.flip,
324
+ flop: mergedOptions.flop,
325
+ rotate: mergedOptions.rotate,
326
+ sharpen: mergedOptions.sharpen,
327
+ median: mergedOptions.median,
328
+ gamma: mergedOptions.gamma,
329
+ negate: mergedOptions.negate,
330
+ normalize: mergedOptions.normalize,
331
+ threshold: mergedOptions.threshold
258
332
  });
259
333
  srcSets.push(`${srcSetIdentifier} ${breakpoint}w`);
260
334
  }
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" | "height" | "width" | "srcSet" | "className" | "sizes" | "style">> {
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
- export { Image, type ImageProps, useImgLoaded };
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 { src, ...rest } = prop;
90
- const resolved = { ...rest };
91
- if (typeof src === "object") {
92
- resolved.src = src.src;
93
- resolved.width ??= src.width;
94
- resolved.height ??= src.height;
95
- resolved.srcSet ??= src.srcSets;
96
- resolved.placeholderUrl ??= src.placeholderUrl;
97
- } else resolved.src = src;
98
- if (prop.asap) {
99
- resolved.decoding = "async";
100
- resolved.loading = "eager";
101
- resolved.fetchPriority = "high";
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 (prop.srcSet && VALID_WIDTH_DESCRIPTOR_SRCSET.test(prop.srcSet) && loading === "lazy") sizes = "auto, 100vw";
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
- assertLoadingProp(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
+ }
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
- export { Image, useImgLoaded };
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": "1.3.0",
4
+ "version": "2.0.1",
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",