@lonik/oh-image 1.1.0 → 1.2.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/client.d.ts CHANGED
@@ -1,4 +1,17 @@
1
+ export interface ImageSrc {
2
+ /** Original width of the source image in pixels */
3
+ width: number;
4
+ /** Original height of the source image in pixels */
5
+ height: number;
6
+ /** URL to the placeholder image (if placeholder was enabled) */
7
+ placeholderUrl?: string;
8
+ /** Array of responsive image sources at different breakpoints */
9
+ srcSets: string;
10
+ /** URL to the main processed image */
11
+ src: string;
12
+ }
13
+
1
14
  declare module "*?oh" {
2
- const imageSrc: any;
15
+ const imageSrc: ImageSrc;
3
16
  export default imageSrc;
4
17
  }
package/dist/plugin.d.ts CHANGED
@@ -3,18 +3,10 @@ import * as vite0 from "vite";
3
3
  import * as rollup0 from "rollup";
4
4
 
5
5
  //#region src/plugin/types.d.ts
6
- /**
7
- * Configuration options for the oh-image Vite plugin.
8
- * Extends ImageOptions with all properties required, plus plugin-specific settings.
9
- */
10
- interface PluginConfig extends Required<ImageOptions> {
6
+ interface PluginConfig extends Required<Pick<ImageOptions, "placeholder" | "bps" | "format">> {
11
7
  /** Directory name where processed images will be output during build */
12
8
  distDir: string;
13
9
  }
14
- /**
15
- * Options for image processing and transformation.
16
- * Can be passed via query parameters or plugin configuration.
17
- */
18
10
  interface ImageOptions {
19
11
  /** Target width for the processed image in pixels */
20
12
  width?: number | null;
@@ -26,44 +18,8 @@ interface ImageOptions {
26
18
  blur?: number | boolean;
27
19
  /** Whether to generate a placeholder image for lazy loading */
28
20
  placeholder?: boolean;
29
- /** Width of the placeholder image in pixels */
30
- placeholderW?: number;
31
- /** Height of the placeholder image in pixels */
32
- placeholderH?: number;
33
- /** Output format for the placeholder image */
34
- placeholderF?: keyof FormatEnum;
35
- /** Blur setting for the placeholder (true for default, or sigma value) */
36
- placeholderB: boolean | number;
37
21
  /** Breakpoints array - widths in pixels for responsive srcSet generation */
38
22
  bps?: number[];
39
- /** Output format for srcSet images */
40
- srcSetsF: keyof FormatEnum;
41
- }
42
- /**
43
- * Represents a single entry in the srcSet array.
44
- * Used for responsive image loading at different viewport widths.
45
- */
46
- interface ImageSrcSet {
47
- /** Width descriptor (e.g., '640w', '1080w') */
48
- width: string;
49
- /** URL or path to the image at this breakpoint */
50
- src: string;
51
- }
52
- /**
53
- * The processed image source object returned by the plugin.
54
- * Contains all URLs and metadata needed for responsive image rendering.
55
- */
56
- interface ImageSrc {
57
- /** Original width of the source image in pixels */
58
- width: number;
59
- /** Original height of the source image in pixels */
60
- height: number;
61
- /** URL to the placeholder image (if placeholder was enabled) */
62
- placeholderUrl?: string;
63
- /** Array of responsive image sources at different breakpoints */
64
- srcSets: ImageSrcSet[];
65
- /** URL to the main processed image */
66
- src: string;
67
23
  }
68
24
  //#endregion
69
25
  //#region src/plugin/plugin.d.ts
@@ -81,4 +37,4 @@ declare function ohImage(options?: Partial<PluginConfig>): {
81
37
  writeBundle(this: rollup0.PluginContext): Promise<void>;
82
38
  };
83
39
  //#endregion
84
- export { type ImageOptions, type ImageSrc, type ImageSrcSet, type PluginConfig, ohImage };
40
+ export { type ImageOptions, type PluginConfig, ohImage };
package/dist/plugin.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { basename, dirname, extname, join, parse } from "node:path";
2
- import { randomBytes } from "node:crypto";
2
+ import { createHash } from "node:crypto";
3
3
  import { mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import queryString from "query-string";
5
5
  import sharp from "sharp";
6
6
  import pLimit from "p-limit";
7
7
 
8
8
  //#region src/plugin/utils.ts
9
- function getRandomString(length = 32) {
10
- return randomBytes(Math.ceil(length * 3 / 4)).toString("base64").slice(0, length).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
9
+ async function getFileHash(filePath) {
10
+ const content = await readFile(filePath);
11
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
11
12
  }
12
13
  async function readFileSafe(path) {
13
14
  try {
@@ -60,6 +61,9 @@ async function processImage(path, options) {
60
61
 
61
62
  //#endregion
62
63
  //#region src/plugin/plugin.ts
64
+ const DEFAULT_IMAGE_FORMAT = "webp";
65
+ const PLACEHOLDER_IMG_SIZE = 8;
66
+ const PLACEHOLDER_BLUR_QUALITY = 70;
63
67
  const DEFAULT_CONFIGS = {
64
68
  distDir: "oh-images",
65
69
  bps: [
@@ -76,18 +80,10 @@ const DEFAULT_CONFIGS = {
76
80
  1920
77
81
  ],
78
82
  format: "webp",
79
- blur: false,
80
- width: null,
81
- height: null,
82
- placeholder: false,
83
- placeholderH: 100,
84
- placeholderW: 100,
85
- placeholderB: true,
86
- placeholderF: "webp",
87
- srcSetsF: "webp"
83
+ placeholder: false
88
84
  };
89
85
  const PROCESS_KEY = "oh";
90
- const SUPPORTED_IMAGE_FORMATS = /\.(jpe?g|png|webp|avif|gif|tiff?|svg)(\?.*)?$/i;
86
+ const SUPPORTED_IMAGE_FORMATS = /\.(jpe?g|png|webp|avif|gif|svg)(\?.*)?$/i;
91
87
  const DEV_DIR = "/@oh-images/";
92
88
  function ohImage(options) {
93
89
  let isBuild = false;
@@ -107,9 +103,8 @@ function ohImage(options) {
107
103
  const fileId = basename(url);
108
104
  return join(cacheDir, fileId);
109
105
  }
110
- function genIdentifier(uri, format, prefix) {
111
- const fileId = basename(uri);
112
- const uniqueFileId = `${prefix}-${getRandomString()}-${fileId}.${format}`;
106
+ function genIdentifier(uri, format, prefix, hash) {
107
+ const uniqueFileId = `${prefix}-${hash}-${basename(uri)}.${format}`;
113
108
  if (!isBuild) return join(DEV_DIR, uniqueFileId);
114
109
  return join(assetsDir, config.distDir, uniqueFileId);
115
110
  }
@@ -155,15 +150,15 @@ function ohImage(options) {
155
150
  const origin = parsed.path;
156
151
  const { name, ext } = parse(parsed.path);
157
152
  const metadata = await sharp(parsed.path).metadata();
153
+ const hash = await getFileHash(origin);
158
154
  const mergedOptions = {
159
155
  ...config,
160
156
  ...parsed.options
161
157
  };
162
- const mainIdentifier = genIdentifier(name, mergedOptions.format ?? ext.slice(1), "main");
158
+ const mainIdentifier = genIdentifier(name, mergedOptions.format ?? ext.slice(1), "main", hash);
163
159
  const mainEntry = {
164
160
  width: mergedOptions.width,
165
161
  height: mergedOptions.height,
166
- blur: mergedOptions.blur,
167
162
  format: mergedOptions.format,
168
163
  origin
169
164
  };
@@ -172,32 +167,42 @@ function ohImage(options) {
172
167
  width: metadata.width,
173
168
  height: metadata.height,
174
169
  src: mainIdentifier,
175
- srcSets: []
170
+ srcSets: ""
176
171
  };
177
172
  if (parsed.options?.placeholder) {
178
- const placeholderIdentifier = genIdentifier(name, mergedOptions.placeholderF, "placeholder");
173
+ let placeholderHeight = 0;
174
+ let placeholderWidth = 0;
175
+ if (metadata.width >= metadata.height) {
176
+ placeholderWidth = PLACEHOLDER_IMG_SIZE;
177
+ placeholderHeight = Math.max(Math.round(metadata.height / metadata.width * PLACEHOLDER_IMG_SIZE), 1);
178
+ } else {
179
+ placeholderWidth = Math.max(Math.round(metadata.width / metadata.height * PLACEHOLDER_IMG_SIZE), 1);
180
+ placeholderHeight = PLACEHOLDER_IMG_SIZE;
181
+ }
182
+ const placeholderIdentifier = genIdentifier(name, DEFAULT_IMAGE_FORMAT, "placeholder", hash);
179
183
  const placeholderEntry = {
180
- width: mergedOptions.placeholderW,
181
- height: mergedOptions.placeholderH,
182
- format: mergedOptions.placeholderF,
183
- blur: mergedOptions.placeholderB,
184
+ width: placeholderWidth,
185
+ height: placeholderHeight,
186
+ format: DEFAULT_IMAGE_FORMAT,
187
+ blur: PLACEHOLDER_BLUR_QUALITY,
184
188
  origin
185
189
  };
186
190
  imageEntries.set(placeholderIdentifier, placeholderEntry);
187
191
  src.placeholderUrl = placeholderIdentifier;
188
192
  }
189
- if (mergedOptions.bps) for (const breakpoint of mergedOptions.bps) {
190
- const srcSetIdentifier = genIdentifier(name, mergedOptions.srcSetsF, `breakpoint-${breakpoint}`);
191
- const srcSetEntry = {
192
- width: breakpoint,
193
- format: mergedOptions.srcSetsF,
194
- origin
195
- };
196
- imageEntries.set(srcSetIdentifier, srcSetEntry);
197
- src.srcSets.push({
198
- src: srcSetIdentifier,
199
- width: `${breakpoint}w`
200
- });
193
+ if (mergedOptions.bps) {
194
+ const srcSets = [];
195
+ for (const breakpoint of mergedOptions.bps) {
196
+ const srcSetIdentifier = genIdentifier(name, DEFAULT_IMAGE_FORMAT, `breakpoint-${breakpoint}`, hash);
197
+ const srcSetEntry = {
198
+ width: breakpoint,
199
+ format: DEFAULT_IMAGE_FORMAT,
200
+ origin
201
+ };
202
+ imageEntries.set(srcSetIdentifier, srcSetEntry);
203
+ srcSets.push(`${srcSetIdentifier} ${breakpoint}w`);
204
+ }
205
+ src.srcSets = srcSets.join(", ");
201
206
  }
202
207
  return `export default ${JSON.stringify(src)};`;
203
208
  } catch (err) {
package/dist/react.d.ts CHANGED
@@ -1,55 +1,41 @@
1
1
  import * as react_jsx_runtime0 from "react/jsx-runtime";
2
- import { CSSProperties } from "react";
2
+ import { ImgHTMLAttributes } from "react";
3
3
 
4
- //#region src/react/types.d.ts
5
- /**
6
- * Image source type - can be either a simple URL string or a full ImageSrc object
7
- */
8
- type ImageSrcType = string | ImageSrc;
9
- /**
10
- * Optimized image source with multiple responsive variants
11
- */
4
+ //#region src/client.d.ts
12
5
  interface ImageSrc {
13
- /** Original image width in pixels */
6
+ /** Original width of the source image in pixels */
14
7
  width: number;
15
- /** Original image height in pixels */
8
+ /** Original height of the source image in pixels */
16
9
  height: number;
17
- /** Optional low-quality placeholder image URL for blur-up effect */
10
+ /** URL to the placeholder image (if placeholder was enabled) */
18
11
  placeholderUrl?: string;
19
- /** Array of responsive image variants for different screen sizes */
20
- srcSets: ImageSrcSet[];
21
- /** Primary image source URL */
12
+ /** Array of responsive image sources at different breakpoints */
13
+ srcSets: string;
14
+ /** URL to the main processed image */
22
15
  src: string;
23
16
  }
24
- /**
25
- * Single responsive image variant in a srcset
26
- */
27
- interface ImageSrcSet {
28
- /** Width descriptor (e.g., "1920w") */
29
- width: string;
30
- /** Image URL for this variant */
31
- src: string;
17
+ declare module "*?oh" {
18
+ const imageSrc: ImageSrc;
19
+ export default imageSrc;
32
20
  }
33
- /**
34
- * Props for the optimized Image component
35
- * Extends standard HTML image attributes with optimization features
36
- */
37
- interface ImageProps extends Partial<Pick<HTMLImageElement, "alt" | "fetchPriority" | "decoding" | "loading" | "height" | "width" | "srcset" | "className" | "sizes">> {
38
- /** Load the image immediately, bypassing lazy loading */
21
+ //#endregion
22
+ //#region src/react/types.d.ts
23
+ type ImageSrcType = string | ImageSrc;
24
+ interface ImageProps extends Partial<Pick<ImgHTMLAttributes<HTMLImageElement>, "alt" | "fetchPriority" | "decoding" | "loading" | "height" | "width" | "srcSet" | "className" | "sizes" | "style">> {
25
+ /** Configures the Image component to load the image immediately. */
39
26
  asap?: boolean;
40
- /** Image source - either a URL string or ImageSrc object with responsive variants */
27
+ /** */
41
28
  src: ImageSrcType;
42
- /** Override placeholder URL (takes precedence over ImageSrc.placeholderUrl) */
29
+ /** The URL of the placeholder image to display while loading. */
43
30
  placeholderUrl?: string | undefined;
44
- /** Enable blur-up placeholder effect during image loading */
45
- placeholder?: boolean;
46
- /** Inline CSS styles */
47
- style?: CSSProperties;
48
- /** Make image fill its container (position: absolute) */
31
+ /**
32
+ * Sets the image to "fill mode", which eliminates the height/width requirement and adds
33
+ * styles such that the image fills its containing element.
34
+ */
49
35
  fill?: boolean;
50
36
  }
51
37
  //#endregion
52
38
  //#region src/react/image.d.ts
53
39
  declare function Image(props: ImageProps): react_jsx_runtime0.JSX.Element;
54
40
  //#endregion
55
- export { Image, type ImageProps, type ImageSrc, type ImageSrcSet };
41
+ export { Image, type ImageProps };
package/dist/react.js CHANGED
@@ -10,7 +10,7 @@ function resolveOptions(props) {
10
10
  resolved.src = src.src;
11
11
  resolved.width ??= src.width;
12
12
  resolved.height ??= src.height;
13
- resolved.srcset ??= src.srcSets.map((set) => `${set.src} ${set.width}`).join(", ");
13
+ resolved.srcSet ??= src.srcSets;
14
14
  resolved.placeholderUrl ??= src.placeholderUrl;
15
15
  } else resolved.src = src;
16
16
  if (props.asap) {
@@ -26,11 +26,7 @@ function resolveOptions(props) {
26
26
  return resolved;
27
27
  }
28
28
  function getPlaceholderStyles(props) {
29
- if (!props.placeholder) return {};
30
- if (!props.placeholderUrl) {
31
- console.warn("Blur URL is required for placeholder");
32
- return {};
33
- }
29
+ if (!props.placeholderUrl) return {};
34
30
  return {
35
31
  backgroundPosition: "50% 50%",
36
32
  backgroundRepeat: "no-repeat",
@@ -61,7 +57,7 @@ function Image(props) {
61
57
  src: options.src,
62
58
  width: options.width,
63
59
  height: options.height,
64
- srcSet: options.srcset,
60
+ srcSet: options.srcSet,
65
61
  alt: options.alt,
66
62
  loading: options.loading,
67
63
  decoding: options.decoding,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lonik/oh-image",
3
3
  "type": "module",
4
- "version": "1.1.0",
4
+ "version": "1.2.1",
5
5
  "description": "A React component library for optimized image handling.",
6
6
  "author": "Luka Onikadze <lukonik@gmail.com>",
7
7
  "license": "MIT",