@page-speed/img 0.4.5 → 0.4.7

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/core/Img.cjs CHANGED
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { forwardRef, memo, useCallback, useEffect, useMemo, useRef } from "react";
3
+ import { forwardRef, memo, useCallback, useMemo, useRef } from "react";
4
4
  import { useOptimizedImage } from "@page-speed/hooks/media";
5
+ import { useImgDebugLog } from "./useImgDebugLog.js";
5
6
  import { useMediaSelectionEffect } from "./useMediaSelectionEffect.js";
6
7
  import { useResponsiveReset } from "./useResponsiveReset.js";
7
8
  const TRANSPARENT_PIXEL = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
@@ -20,7 +21,6 @@ const resolveOptixFlowConfig = (config) => {
20
21
  export const setDefaultOptixFlowConfig = (config) => {
21
22
  defaultOptixFlowConfig = config ?? undefined;
22
23
  };
23
- const isUrlString = (value) => typeof value === "string" && value.trim().length > 0;
24
24
  const parseDimension = (value) => {
25
25
  if (value === "" || value === null || typeof value === "undefined")
26
26
  return undefined;
@@ -44,18 +44,19 @@ const composeRefs = (hookRef, forwardedRef, localRef) => useCallback((node) => {
44
44
  forwardedRef.current = node;
45
45
  }
46
46
  }, [hookRef, forwardedRef, localRef]);
47
- const ModernImg = ({ sizes, loading, decoding, alt, title, src: directSrc, eager, intersectionMargin, intersectionThreshold, optixFlowConfig, forwardedRef, ...rest }) => {
47
+ const ModernImg = ({ sizes, loading, decoding, alt, title, src: directSrc, eager, width, height, fetchPriority, intersectionMargin, intersectionThreshold, optixFlowConfig, useDebugMode, forwardedRef, ...restProps }) => {
48
48
  const imgRef = useRef(null);
49
49
  const pictureRef = useRef(null);
50
- const logKeyRef = useRef(null);
51
50
  useResponsiveReset(pictureRef);
52
51
  useMediaSelectionEffect();
53
52
  const normalizedSrc = useMemo(() => (typeof directSrc === "string" ? directSrc.trim() : ""), [directSrc]);
54
- const numericWidth = useMemo(() => parseDimension(rest.width), [rest]);
55
- const numericHeight = useMemo(() => parseDimension(rest.height), [rest]);
53
+ const numericWidth = useMemo(() => parseDimension(width), [width]);
54
+ const numericHeight = useMemo(() => parseDimension(height), [height]);
56
55
  const resolvedOptixConfig = useMemo(() => resolveOptixFlowConfig(optixFlowConfig), [optixFlowConfig]);
57
- const eagerLoad = eager ?? loading === "eager";
58
- const { ref: hookRef, src, srcset, sizes: computedSizes, loading: hookLoading, isInView, size, } = useOptimizedImage({
56
+ const eagerLoad = useMemo(() => {
57
+ return eager ?? loading === "eager";
58
+ }, [eager, loading]);
59
+ const hookOptions = useMemo(() => ({
59
60
  src: normalizedSrc,
60
61
  eager: eagerLoad,
61
62
  width: numericWidth,
@@ -63,57 +64,57 @@ const ModernImg = ({ sizes, loading, decoding, alt, title, src: directSrc, eager
63
64
  rootMargin: intersectionMargin ?? "200px",
64
65
  threshold: intersectionThreshold ?? 0.1,
65
66
  optixFlowConfig: resolvedOptixConfig,
66
- });
67
+ }), [
68
+ normalizedSrc,
69
+ eagerLoad,
70
+ numericWidth,
71
+ numericHeight,
72
+ intersectionMargin,
73
+ intersectionThreshold,
74
+ resolvedOptixConfig,
75
+ ]);
76
+ const { ref: hookRef, src, srcset, sizes: computedSizes, loading: hookLoading, isInView, size, } = useOptimizedImage(hookOptions);
67
77
  const mergedRef = composeRefs(hookRef, forwardedRef, imgRef);
68
- const { width, height, ...restProps } = rest;
69
- const sizesAttr = sizes ?? (computedSizes || undefined);
70
- const loadingAttr = loading ?? hookLoading ?? "lazy";
71
- const decodingAttr = decoding ?? "async";
72
- const hasSrcSet = Boolean(srcset.avif || srcset.webp || srcset.jpeg);
73
- const imgSrc = src || normalizedSrc || TRANSPARENT_PIXEL;
74
- const inlineSrcSet = hasSrcSet && !srcset.avif && !srcset.webp ? srcset.jpeg : "";
75
- const parsedWidth = parseDimension(width);
76
- const parsedHeight = parseDimension(height);
77
- const widthAttr = parsedWidth ?? (size.width || numericWidth || undefined);
78
- const heightAttr = parsedHeight ?? (size.height || numericHeight || undefined);
79
- // Temporary logging to detect repeated transform requests and URL churn.
80
- useEffect(() => {
81
- if (typeof window === "undefined")
82
- return;
83
- if (!eagerLoad && !isInView)
84
- return;
85
- if (!imgSrc || imgSrc === TRANSPARENT_PIXEL)
86
- return;
87
- const logKey = [
88
- imgSrc,
89
- srcset.avif,
90
- srcset.webp,
91
- srcset.jpeg,
92
- sizesAttr ?? "",
93
- ].join("|");
94
- if (logKeyRef.current === logKey)
95
- return;
96
- logKeyRef.current = logKey;
97
- if (typeof console !== "undefined" && console.info) {
98
- console.info("[PageSpeedImg] image request", {
99
- src: imgSrc,
100
- srcset,
101
- sizes: sizesAttr,
102
- });
103
- }
104
- }, [
78
+ const sizesAttr = useMemo(() => {
79
+ return sizes ?? (computedSizes || undefined);
80
+ }, [sizes, computedSizes]);
81
+ const loadingAttr = useMemo(() => {
82
+ return loading ?? hookLoading ?? "lazy";
83
+ }, [loading, hookLoading]);
84
+ const decodingAttr = useMemo(() => {
85
+ return decoding ?? "async";
86
+ }, [decoding]);
87
+ const fetchPriorityAttr = useMemo(() => {
88
+ return fetchPriority ?? (eagerLoad ? "high" : undefined);
89
+ }, [fetchPriority, eagerLoad]);
90
+ const hasSrcSet = useMemo(() => {
91
+ return Boolean(srcset.avif || srcset.webp || srcset.jpeg);
92
+ }, [srcset.avif, srcset.webp, srcset.jpeg]);
93
+ const imgSrc = useMemo(() => {
94
+ return src || normalizedSrc || TRANSPARENT_PIXEL;
95
+ }, [src, normalizedSrc]);
96
+ const inlineSrcSet = useMemo(() => {
97
+ return hasSrcSet && !srcset.avif && !srcset.webp ? srcset.jpeg : "";
98
+ }, [hasSrcSet, srcset.avif, srcset.webp, srcset.jpeg]);
99
+ const widthAttr = useMemo(() => {
100
+ return numericWidth ?? (size.width || undefined);
101
+ }, [numericWidth, size?.width]);
102
+ const heightAttr = useMemo(() => {
103
+ return numericHeight ?? (size.height || undefined);
104
+ }, [numericHeight, size?.height]);
105
+ useImgDebugLog({
106
+ enabled: useDebugMode ?? false,
105
107
  eagerLoad,
106
- imgSrc,
107
108
  isInView,
109
+ imgSrc,
110
+ transparentPixel: TRANSPARENT_PIXEL,
111
+ srcset,
108
112
  sizesAttr,
109
- srcset.avif,
110
- srcset.webp,
111
- srcset.jpeg,
112
- ]);
113
+ });
113
114
  if (!hasSrcSet) {
114
- return (_jsx("img", { ref: mergedRef, src: imgSrc, loading: loadingAttr, decoding: decodingAttr, alt: alt, title: title, width: widthAttr, height: heightAttr, ...restProps }));
115
+ return (_jsx("img", { ref: mergedRef, src: imgSrc, loading: loadingAttr, decoding: decodingAttr, fetchPriority: fetchPriorityAttr, alt: alt, title: title, width: widthAttr, height: heightAttr, ...restProps }));
115
116
  }
116
- return (_jsxs("picture", { ref: pictureRef, children: [srcset.avif ? (_jsx("source", { type: "image/avif", srcSet: srcset.avif, sizes: sizesAttr })) : null, srcset.webp ? (_jsx("source", { type: "image/webp", srcSet: srcset.webp, sizes: sizesAttr })) : null, _jsx("img", { ref: mergedRef, src: imgSrc, srcSet: inlineSrcSet || undefined, sizes: inlineSrcSet ? sizesAttr : undefined, loading: loadingAttr, decoding: decodingAttr, alt: alt, title: title, width: widthAttr, height: heightAttr, ...restProps })] }));
117
+ return (_jsxs("picture", { ref: pictureRef, children: [srcset.avif ? (_jsx("source", { type: "image/avif", srcSet: srcset.avif, sizes: sizesAttr })) : null, srcset.webp ? (_jsx("source", { type: "image/webp", srcSet: srcset.webp, sizes: sizesAttr })) : null, _jsx("img", { ref: mergedRef, src: imgSrc, srcSet: inlineSrcSet || undefined, sizes: inlineSrcSet ? sizesAttr : undefined, loading: loadingAttr, decoding: decodingAttr, fetchPriority: fetchPriorityAttr, alt: alt, title: title, width: widthAttr, height: heightAttr, ...restProps })] }));
117
118
  };
118
119
  const ImgBase = forwardRef(function Img(props, ref) {
119
120
  const hasSrc = typeof props.src === "string" && props.src.trim().length > 0;
@@ -14,6 +14,8 @@ export type ImgProps = NativeImgProps & {
14
14
  intersectionMargin?: string;
15
15
  /** OptixFlow integration options */
16
16
  optixFlowConfig?: UseOptimizedImageOptions["optixFlowConfig"];
17
+ /** Enable debug logging for image requests */
18
+ useDebugMode?: boolean;
17
19
  };
18
20
  export declare const setDefaultOptixFlowConfig: (config?: UseOptimizedImageOptions["optixFlowConfig"] | null) => void;
19
21
  export declare const Img: React.MemoExoticComponent<React.ForwardRefExoticComponent<Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "srcSet" | "sizes"> & {
@@ -29,5 +31,7 @@ export declare const Img: React.MemoExoticComponent<React.ForwardRefExoticCompon
29
31
  intersectionMargin?: string;
30
32
  /** OptixFlow integration options */
31
33
  optixFlowConfig?: UseOptimizedImageOptions["optixFlowConfig"];
34
+ /** Enable debug logging for image requests */
35
+ useDebugMode?: boolean;
32
36
  } & React.RefAttributes<HTMLImageElement>>>;
33
37
  export {};
package/dist/core/Img.js CHANGED
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { forwardRef, memo, useCallback, useEffect, useMemo, useRef } from "react";
3
+ import { forwardRef, memo, useCallback, useMemo, useRef } from "react";
4
4
  import { useOptimizedImage } from "@page-speed/hooks/media";
5
+ import { useImgDebugLog } from "./useImgDebugLog.js";
5
6
  import { useMediaSelectionEffect } from "./useMediaSelectionEffect.js";
6
7
  import { useResponsiveReset } from "./useResponsiveReset.js";
7
8
  const TRANSPARENT_PIXEL = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
@@ -20,7 +21,6 @@ const resolveOptixFlowConfig = (config) => {
20
21
  export const setDefaultOptixFlowConfig = (config) => {
21
22
  defaultOptixFlowConfig = config ?? undefined;
22
23
  };
23
- const isUrlString = (value) => typeof value === "string" && value.trim().length > 0;
24
24
  const parseDimension = (value) => {
25
25
  if (value === "" || value === null || typeof value === "undefined")
26
26
  return undefined;
@@ -44,18 +44,19 @@ const composeRefs = (hookRef, forwardedRef, localRef) => useCallback((node) => {
44
44
  forwardedRef.current = node;
45
45
  }
46
46
  }, [hookRef, forwardedRef, localRef]);
47
- const ModernImg = ({ sizes, loading, decoding, alt, title, src: directSrc, eager, intersectionMargin, intersectionThreshold, optixFlowConfig, forwardedRef, ...rest }) => {
47
+ const ModernImg = ({ sizes, loading, decoding, alt, title, src: directSrc, eager, width, height, fetchPriority, intersectionMargin, intersectionThreshold, optixFlowConfig, useDebugMode, forwardedRef, ...restProps }) => {
48
48
  const imgRef = useRef(null);
49
49
  const pictureRef = useRef(null);
50
- const logKeyRef = useRef(null);
51
50
  useResponsiveReset(pictureRef);
52
51
  useMediaSelectionEffect();
53
52
  const normalizedSrc = useMemo(() => (typeof directSrc === "string" ? directSrc.trim() : ""), [directSrc]);
54
- const numericWidth = useMemo(() => parseDimension(rest.width), [rest]);
55
- const numericHeight = useMemo(() => parseDimension(rest.height), [rest]);
53
+ const numericWidth = useMemo(() => parseDimension(width), [width]);
54
+ const numericHeight = useMemo(() => parseDimension(height), [height]);
56
55
  const resolvedOptixConfig = useMemo(() => resolveOptixFlowConfig(optixFlowConfig), [optixFlowConfig]);
57
- const eagerLoad = eager ?? loading === "eager";
58
- const { ref: hookRef, src, srcset, sizes: computedSizes, loading: hookLoading, isInView, size, } = useOptimizedImage({
56
+ const eagerLoad = useMemo(() => {
57
+ return eager ?? loading === "eager";
58
+ }, [eager, loading]);
59
+ const hookOptions = useMemo(() => ({
59
60
  src: normalizedSrc,
60
61
  eager: eagerLoad,
61
62
  width: numericWidth,
@@ -63,57 +64,57 @@ const ModernImg = ({ sizes, loading, decoding, alt, title, src: directSrc, eager
63
64
  rootMargin: intersectionMargin ?? "200px",
64
65
  threshold: intersectionThreshold ?? 0.1,
65
66
  optixFlowConfig: resolvedOptixConfig,
66
- });
67
+ }), [
68
+ normalizedSrc,
69
+ eagerLoad,
70
+ numericWidth,
71
+ numericHeight,
72
+ intersectionMargin,
73
+ intersectionThreshold,
74
+ resolvedOptixConfig,
75
+ ]);
76
+ const { ref: hookRef, src, srcset, sizes: computedSizes, loading: hookLoading, isInView, size, } = useOptimizedImage(hookOptions);
67
77
  const mergedRef = composeRefs(hookRef, forwardedRef, imgRef);
68
- const { width, height, ...restProps } = rest;
69
- const sizesAttr = sizes ?? (computedSizes || undefined);
70
- const loadingAttr = loading ?? hookLoading ?? "lazy";
71
- const decodingAttr = decoding ?? "async";
72
- const hasSrcSet = Boolean(srcset.avif || srcset.webp || srcset.jpeg);
73
- const imgSrc = src || normalizedSrc || TRANSPARENT_PIXEL;
74
- const inlineSrcSet = hasSrcSet && !srcset.avif && !srcset.webp ? srcset.jpeg : "";
75
- const parsedWidth = parseDimension(width);
76
- const parsedHeight = parseDimension(height);
77
- const widthAttr = parsedWidth ?? (size.width || numericWidth || undefined);
78
- const heightAttr = parsedHeight ?? (size.height || numericHeight || undefined);
79
- // Temporary logging to detect repeated transform requests and URL churn.
80
- useEffect(() => {
81
- if (typeof window === "undefined")
82
- return;
83
- if (!eagerLoad && !isInView)
84
- return;
85
- if (!imgSrc || imgSrc === TRANSPARENT_PIXEL)
86
- return;
87
- const logKey = [
88
- imgSrc,
89
- srcset.avif,
90
- srcset.webp,
91
- srcset.jpeg,
92
- sizesAttr ?? "",
93
- ].join("|");
94
- if (logKeyRef.current === logKey)
95
- return;
96
- logKeyRef.current = logKey;
97
- if (typeof console !== "undefined" && console.info) {
98
- console.info("[PageSpeedImg] image request", {
99
- src: imgSrc,
100
- srcset,
101
- sizes: sizesAttr,
102
- });
103
- }
104
- }, [
78
+ const sizesAttr = useMemo(() => {
79
+ return sizes ?? (computedSizes || undefined);
80
+ }, [sizes, computedSizes]);
81
+ const loadingAttr = useMemo(() => {
82
+ return loading ?? hookLoading ?? "lazy";
83
+ }, [loading, hookLoading]);
84
+ const decodingAttr = useMemo(() => {
85
+ return decoding ?? "async";
86
+ }, [decoding]);
87
+ const fetchPriorityAttr = useMemo(() => {
88
+ return fetchPriority ?? (eagerLoad ? "high" : undefined);
89
+ }, [fetchPriority, eagerLoad]);
90
+ const hasSrcSet = useMemo(() => {
91
+ return Boolean(srcset.avif || srcset.webp || srcset.jpeg);
92
+ }, [srcset.avif, srcset.webp, srcset.jpeg]);
93
+ const imgSrc = useMemo(() => {
94
+ return src || normalizedSrc || TRANSPARENT_PIXEL;
95
+ }, [src, normalizedSrc]);
96
+ const inlineSrcSet = useMemo(() => {
97
+ return hasSrcSet && !srcset.avif && !srcset.webp ? srcset.jpeg : "";
98
+ }, [hasSrcSet, srcset.avif, srcset.webp, srcset.jpeg]);
99
+ const widthAttr = useMemo(() => {
100
+ return numericWidth ?? (size.width || undefined);
101
+ }, [numericWidth, size?.width]);
102
+ const heightAttr = useMemo(() => {
103
+ return numericHeight ?? (size.height || undefined);
104
+ }, [numericHeight, size?.height]);
105
+ useImgDebugLog({
106
+ enabled: useDebugMode ?? false,
105
107
  eagerLoad,
106
- imgSrc,
107
108
  isInView,
109
+ imgSrc,
110
+ transparentPixel: TRANSPARENT_PIXEL,
111
+ srcset,
108
112
  sizesAttr,
109
- srcset.avif,
110
- srcset.webp,
111
- srcset.jpeg,
112
- ]);
113
+ });
113
114
  if (!hasSrcSet) {
114
- return (_jsx("img", { ref: mergedRef, src: imgSrc, loading: loadingAttr, decoding: decodingAttr, alt: alt, title: title, width: widthAttr, height: heightAttr, ...restProps }));
115
+ return (_jsx("img", { ref: mergedRef, src: imgSrc, loading: loadingAttr, decoding: decodingAttr, fetchPriority: fetchPriorityAttr, alt: alt, title: title, width: widthAttr, height: heightAttr, ...restProps }));
115
116
  }
116
- return (_jsxs("picture", { ref: pictureRef, children: [srcset.avif ? (_jsx("source", { type: "image/avif", srcSet: srcset.avif, sizes: sizesAttr })) : null, srcset.webp ? (_jsx("source", { type: "image/webp", srcSet: srcset.webp, sizes: sizesAttr })) : null, _jsx("img", { ref: mergedRef, src: imgSrc, srcSet: inlineSrcSet || undefined, sizes: inlineSrcSet ? sizesAttr : undefined, loading: loadingAttr, decoding: decodingAttr, alt: alt, title: title, width: widthAttr, height: heightAttr, ...restProps })] }));
117
+ return (_jsxs("picture", { ref: pictureRef, children: [srcset.avif ? (_jsx("source", { type: "image/avif", srcSet: srcset.avif, sizes: sizesAttr })) : null, srcset.webp ? (_jsx("source", { type: "image/webp", srcSet: srcset.webp, sizes: sizesAttr })) : null, _jsx("img", { ref: mergedRef, src: imgSrc, srcSet: inlineSrcSet || undefined, sizes: inlineSrcSet ? sizesAttr : undefined, loading: loadingAttr, decoding: decodingAttr, fetchPriority: fetchPriorityAttr, alt: alt, title: title, width: widthAttr, height: heightAttr, ...restProps })] }));
117
118
  };
118
119
  const ImgBase = forwardRef(function Img(props, ref) {
119
120
  const hasSrc = typeof props.src === "string" && props.src.trim().length > 0;
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { setDefaultOptixFlowConfig } from "./Img.js";
5
+ /**
6
+ * A component that sets the default OptixFlow configuration for all Img components
7
+ * in an SSR-safe way. This component should be rendered once at the app root level.
8
+ *
9
+ * The config is applied inside a useEffect, which means it only runs on the client
10
+ * and is never executed during server-side rendering.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * // In your app root
15
+ * <OptixFlowConfig config={{ apiKey: 'your-api-key' }}>
16
+ * <App />
17
+ * </OptixFlowConfig>
18
+ * ```
19
+ *
20
+ * Or without children:
21
+ * ```tsx
22
+ * <OptixFlowConfig config={{ apiKey: 'your-api-key' }} />
23
+ * ```
24
+ */
25
+ export function OptixFlowConfig({ config, children, }) {
26
+ React.useEffect(() => {
27
+ setDefaultOptixFlowConfig(config ?? null);
28
+ }, [config]);
29
+ return children ? _jsx(_Fragment, { children: children }) : null;
30
+ }
@@ -0,0 +1,37 @@
1
+ import * as React from "react";
2
+ import type { UseOptimizedImageOptions } from "@page-speed/hooks/media";
3
+ /**
4
+ * Props for the OptixFlowConfig component
5
+ */
6
+ export interface OptixFlowConfigProps {
7
+ /**
8
+ * OptixFlow configuration to set as default for all images
9
+ * @example { apiKey: 'your-api-key', compressionLevel: 80 }
10
+ */
11
+ config: UseOptimizedImageOptions["optixFlowConfig"];
12
+ /**
13
+ * Optional children (component returns null regardless)
14
+ */
15
+ children?: React.ReactNode;
16
+ }
17
+ /**
18
+ * A component that sets the default OptixFlow configuration for all Img components
19
+ * in an SSR-safe way. This component should be rendered once at the app root level.
20
+ *
21
+ * The config is applied inside a useEffect, which means it only runs on the client
22
+ * and is never executed during server-side rendering.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * // In your app root
27
+ * <OptixFlowConfig config={{ apiKey: 'your-api-key' }}>
28
+ * <App />
29
+ * </OptixFlowConfig>
30
+ * ```
31
+ *
32
+ * Or without children:
33
+ * ```tsx
34
+ * <OptixFlowConfig config={{ apiKey: 'your-api-key' }} />
35
+ * ```
36
+ */
37
+ export declare function OptixFlowConfig({ config, children, }: OptixFlowConfigProps): React.ReactElement | null;
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { setDefaultOptixFlowConfig } from "./Img.js";
5
+ /**
6
+ * A component that sets the default OptixFlow configuration for all Img components
7
+ * in an SSR-safe way. This component should be rendered once at the app root level.
8
+ *
9
+ * The config is applied inside a useEffect, which means it only runs on the client
10
+ * and is never executed during server-side rendering.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * // In your app root
15
+ * <OptixFlowConfig config={{ apiKey: 'your-api-key' }}>
16
+ * <App />
17
+ * </OptixFlowConfig>
18
+ * ```
19
+ *
20
+ * Or without children:
21
+ * ```tsx
22
+ * <OptixFlowConfig config={{ apiKey: 'your-api-key' }} />
23
+ * ```
24
+ */
25
+ export function OptixFlowConfig({ config, children, }) {
26
+ React.useEffect(() => {
27
+ setDefaultOptixFlowConfig(config ?? null);
28
+ }, [config]);
29
+ return children ? _jsx(_Fragment, { children: children }) : null;
30
+ }
@@ -1 +1,2 @@
1
- export { Img, setDefaultOptixFlowConfig } from './Img.js';
1
+ export { Img, setDefaultOptixFlowConfig } from "./Img.js";
2
+ export { OptixFlowConfig } from "./OptixFlowConfig.js";
@@ -1 +1,4 @@
1
- export { Img, setDefaultOptixFlowConfig } from './Img.js';
1
+ export { Img, setDefaultOptixFlowConfig } from "./Img.js";
2
+ export type { ImgProps } from "./Img.js";
3
+ export { OptixFlowConfig } from "./OptixFlowConfig.js";
4
+ export type { OptixFlowConfigProps } from "./OptixFlowConfig.js";
@@ -1 +1,2 @@
1
- export { Img, setDefaultOptixFlowConfig } from './Img.js';
1
+ export { Img, setDefaultOptixFlowConfig } from "./Img.js";
2
+ export { OptixFlowConfig } from "./OptixFlowConfig.js";
@@ -0,0 +1,46 @@
1
+ import { useEffect, useRef } from "react";
2
+ /**
3
+ * Logs image-request details to the console when `enabled` is true.
4
+ * When disabled (the default), the hook short-circuits on the very first
5
+ * line so there is no meaningful runtime cost.
6
+ */
7
+ export function useImgDebugLog({ enabled, eagerLoad, isInView, imgSrc, transparentPixel, srcset, sizesAttr, }) {
8
+ const logKeyRef = useRef(null);
9
+ useEffect(() => {
10
+ if (!enabled)
11
+ return;
12
+ if (typeof window === "undefined")
13
+ return;
14
+ if (!eagerLoad && !isInView)
15
+ return;
16
+ if (!imgSrc || imgSrc === transparentPixel)
17
+ return;
18
+ const logKey = [
19
+ imgSrc,
20
+ srcset.avif,
21
+ srcset.webp,
22
+ srcset.jpeg,
23
+ sizesAttr ?? "",
24
+ ].join("|");
25
+ if (logKeyRef.current === logKey)
26
+ return;
27
+ logKeyRef.current = logKey;
28
+ if (typeof console !== "undefined" && console.info) {
29
+ console.info("[PageSpeedImg] image request", {
30
+ src: imgSrc,
31
+ srcset,
32
+ sizes: sizesAttr,
33
+ });
34
+ }
35
+ }, [
36
+ enabled,
37
+ eagerLoad,
38
+ imgSrc,
39
+ isInView,
40
+ sizesAttr,
41
+ srcset.avif,
42
+ srcset.webp,
43
+ srcset.jpeg,
44
+ transparentPixel,
45
+ ]);
46
+ }
@@ -0,0 +1,20 @@
1
+ interface ImgDebugLogParams {
2
+ enabled: boolean;
3
+ eagerLoad: boolean;
4
+ isInView: boolean;
5
+ imgSrc: string;
6
+ transparentPixel: string;
7
+ srcset: {
8
+ avif: string;
9
+ webp: string;
10
+ jpeg: string;
11
+ };
12
+ sizesAttr: string | undefined;
13
+ }
14
+ /**
15
+ * Logs image-request details to the console when `enabled` is true.
16
+ * When disabled (the default), the hook short-circuits on the very first
17
+ * line so there is no meaningful runtime cost.
18
+ */
19
+ export declare function useImgDebugLog({ enabled, eagerLoad, isInView, imgSrc, transparentPixel, srcset, sizesAttr, }: ImgDebugLogParams): void;
20
+ export {};
@@ -0,0 +1,46 @@
1
+ import { useEffect, useRef } from "react";
2
+ /**
3
+ * Logs image-request details to the console when `enabled` is true.
4
+ * When disabled (the default), the hook short-circuits on the very first
5
+ * line so there is no meaningful runtime cost.
6
+ */
7
+ export function useImgDebugLog({ enabled, eagerLoad, isInView, imgSrc, transparentPixel, srcset, sizesAttr, }) {
8
+ const logKeyRef = useRef(null);
9
+ useEffect(() => {
10
+ if (!enabled)
11
+ return;
12
+ if (typeof window === "undefined")
13
+ return;
14
+ if (!eagerLoad && !isInView)
15
+ return;
16
+ if (!imgSrc || imgSrc === transparentPixel)
17
+ return;
18
+ const logKey = [
19
+ imgSrc,
20
+ srcset.avif,
21
+ srcset.webp,
22
+ srcset.jpeg,
23
+ sizesAttr ?? "",
24
+ ].join("|");
25
+ if (logKeyRef.current === logKey)
26
+ return;
27
+ logKeyRef.current = logKey;
28
+ if (typeof console !== "undefined" && console.info) {
29
+ console.info("[PageSpeedImg] image request", {
30
+ src: imgSrc,
31
+ srcset,
32
+ sizes: sizesAttr,
33
+ });
34
+ }
35
+ }, [
36
+ enabled,
37
+ eagerLoad,
38
+ imgSrc,
39
+ isInView,
40
+ sizesAttr,
41
+ srcset.avif,
42
+ srcset.webp,
43
+ srcset.jpeg,
44
+ transparentPixel,
45
+ ]);
46
+ }
package/dist/index.cjs CHANGED
@@ -13,3 +13,5 @@ if (globalObject) {
13
13
  }
14
14
  }
15
15
  export * from './core/index.js';
16
+ // Re-export specific items for clarity and CDN usage
17
+ export { Img, setDefaultOptixFlowConfig, OptixFlowConfig } from './core/index.js';
package/dist/index.d.ts CHANGED
@@ -2,3 +2,5 @@ import type { UseOptimizedImageOptions } from '@page-speed/hooks/media';
2
2
  export * from './core/index.js';
3
3
  export type { ImageFormat, SrcsetByFormat, UseOptimizedImageOptions, UseOptimizedImageState, } from '@page-speed/hooks/media';
4
4
  export type OptixFlowConfig = UseOptimizedImageOptions['optixFlowConfig'];
5
+ export { Img, setDefaultOptixFlowConfig, OptixFlowConfig } from './core/index.js';
6
+ export type { ImgProps, OptixFlowConfigProps } from './core/index.js';
package/dist/index.js CHANGED
@@ -13,3 +13,5 @@ if (globalObject) {
13
13
  }
14
14
  }
15
15
  export * from './core/index.js';
16
+ // Re-export specific items for clarity and CDN usage
17
+ export { Img, setDefaultOptixFlowConfig, OptixFlowConfig } from './core/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@page-speed/img",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Performance-optimized React Image component. Drop-in image implementation of web.dev best practices with zero configuration.",
5
5
  "keywords": [
6
6
  "react",
@@ -25,7 +25,7 @@
25
25
  "url": "https://github.com/opensite-ai/page-speed-img/issues"
26
26
  },
27
27
  "author": "OpenSite AI (https://opensite.ai)",
28
- "license": "BSD 3",
28
+ "license": "BSD-3-Clause",
29
29
  "private": false,
30
30
  "type": "module",
31
31
  "main": "dist/index.cjs",
@@ -66,7 +66,7 @@
66
66
  "bundle-analysis": "node scripts/analyze-bundle.js || true",
67
67
  "prepare": "husky",
68
68
  "prepack": "pnpm run build",
69
- "prepublishOnly": "pnpm run build"
69
+ "prepublishOnly": "pnpm run build && pnpm run test"
70
70
  },
71
71
  "peerDependencies": {
72
72
  "react": ">=17.0.0",
@@ -75,6 +75,7 @@
75
75
  "devDependencies": {
76
76
  "@commitlint/cli": "^20.1.0",
77
77
  "@commitlint/config-conventional": "^20.0.0",
78
+ "@testing-library/react": "^16.3.2",
78
79
  "@types/node": "^20.17.6",
79
80
  "@types/react": "^18.3.3",
80
81
  "@types/react-dom": "^18.3.0",
@@ -90,7 +91,7 @@
90
91
  "vitest": "^3.2.4"
91
92
  },
92
93
  "dependencies": {
93
- "@opensite/hooks": "2.0.3",
94
+ "@opensite/hooks": "2.1.0",
94
95
  "@page-speed/hooks": "0.4.5"
95
96
  },
96
97
  "packageManager": "pnpm@10.24.0",
@@ -1,2 +0,0 @@
1
- !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).OpensiteImg={},t.React)}(this,function(t,e){"use strict";const n=new Map;function i(t,e){n.set(t,e)}const r="https://cdn.ing";function l(t){return(t??r).replace(/\/$/,"")}function o(t,e){return`${l(e)}/assets/images/${t}`}function s(t,e){return`${l(e)}/i/r/${t}`}function a(t){return!!t&&["AVIF","WEBP","JPEG"].some(e=>{const n=null==t?void 0:t[e];return!!(i=n)&&[i.sm,i.md,i.lg,i.full].some(t=>"string"==typeof t&&t.trim().length>0);var i})}function u(t){return"string"==typeof t&&t.trim().length>0}function d(t){var e;if(!t)return!1;if(a((null==(e=t.variants_data)?void 0:e.variants)??null))return!0;const n=t;return[n.img_url,n.file_data_url,n.file_data_thumbnail_url,n.img_src,n.med_src,n.thumb_src,n.low_res_thumb].some(u)}async function c(t,e){const n=await fetch(t,{signal:e});if(!n.ok){const e=new Error(`Failed to fetch image data (status ${n.status}) from ${t}`);throw e.status=n.status,e}return await n.json()}async function f(t,e={}){if(!Number.isFinite(t))throw new Error("Invalid mediaId provided to fetchImageData");const r=l(e.cdnHost),a=`image:${r}:${t}`;if(!e.bypassCache){const t=(u=a,n.get(u));if(t)return t}var u;const f=[o(t,r),s(t,r)];let m;for(const n of f)try{const t=await c(n,e.signal);return d(t)&&i(a,t),t}catch(v){if("AbortError"===(null==v?void 0:v.name))throw v;m=v}if(m instanceof Error)throw m;throw new Error(`Failed to fetch image data for mediaId ${t}`)}const m="dt:media-selected";function v(t){t&&t.querySelectorAll("source").forEach(t=>{const e=t.getAttribute("srcset");e&&(t.setAttribute("data-srcset",e),t.removeAttribute("srcset"),requestAnimationFrame(()=>{t.setAttribute("srcset",e)}))})}const g={sm:640,md:1024,lg:1536,full:2560},h=t=>"string"==typeof t&&t.trim().length>0;function p(t){const e=null==t?void 0:t.widths;return e?{sm:e.small??e.sm??g.sm,md:e.medium??e.md??g.md,lg:e.large??e.lg??g.lg,full:e.full_size??e.full??g.full}:null}function w(t){if(t)return t.md||t.lg||t.sm||t.full||Object.values(t).find(Boolean)}const _=e.forwardRef(function({mediaId:t,cdnHost:n,sizes:i,onImageData:o,loading:s,decoding:u,alt:d,title:c,src:_,...E},b){const y=e.useRef(null);e.useImperativeHandle(b,()=>y.current);!function(t){e.useEffect(()=>{const e=t.current;e&&(e instanceof HTMLPictureElement?v(e):e.parentElement instanceof HTMLPictureElement&&v(e.parentElement))},[t])}(e.useRef(null)),e.useEffect(()=>{if("undefined"==typeof window)return;const t=()=>{};return window.addEventListener(m,t),()=>window.removeEventListener(m,t)},[]);const[$,I]=e.useState(null),[S,M]=e.useState(0),x=Number.isFinite(t),z=s??"lazy",A=u??"async",[O,N]=e.useState(()=>!x||"lazy"!==z),P=e.useMemo(()=>(n??r).replace(/\/$/,""),[n]);e.useEffect(()=>{if(!x)return I(null),void M(0);I(null),M(0)},[x,t,n]),e.useEffect(()=>{if(!x)return;const e=new AbortController;return f(t,{cdnHost:n,signal:e.signal,bypassCache:S>0}).then(t=>{I(t),null==o||o(t)}).catch(t=>{"AbortError"!==(null==t?void 0:t.name)&&console.warn("Image data fetch failed:",t)}),()=>e.abort()},[x,t,n,o,S]),e.useEffect(()=>{N(!x||"lazy"!==z)},[x,t,z]),e.useEffect(()=>{if(!x||"lazy"!==z||O)return;if("undefined"==typeof window||void 0===window.IntersectionObserver)return void N(!0);const t=y.current;if(!t)return;const e=new IntersectionObserver(t=>{t.some(t=>t.isIntersecting)&&(N(!0),e.disconnect())},{rootMargin:"200px"});return e.observe(t),()=>e.disconnect()},[x,z,O]);const T=e.useMemo(()=>{var t,e,n;if(!$)return null;const i=(null==(t=$.variants_data)?void 0:t.variants)??{},r=i.WEBP,l=i.AVIF,o=i.JPEG,s=p(null==(e=i.WEBP)?void 0:e.metadata)||p(null==(n=i.JPEG)?void 0:n.metadata)||{...g},a=t=>(t=>{if(h(t))return/^https?:\/\//i.test(t)||t.startsWith("data:")?t:t.startsWith("//")?`https:${t}`:t.startsWith("/")?`${P}${t}`:`${P}/${t}`})("string"==typeof t?t:void 0),u=[w(r),w(o),w(l),null==r?void 0:r.sm,null==r?void 0:r.md,null==r?void 0:r.lg,null==r?void 0:r.full,null==o?void 0:o.sm,null==o?void 0:o.md,null==o?void 0:o.lg,null==o?void 0:o.full,null==l?void 0:l.sm,null==l?void 0:l.md,null==l?void 0:l.lg,null==l?void 0:l.full].map(t=>a(t??void 0)).filter(h),d=$,c=[d.img_url,d.file_data_url,d.file_data_thumbnail_url,d.img_src,d.med_src,d.thumb_src,d.low_res_thumb].map(t=>h(t)?a(t):void 0).filter(h),f=d.fallback_url?[a(d.fallback_url)].filter(h):[],m=[...u,...c,...f][0];if(!m)return null;return{webp:r,avif:l,jpeg:o,toSrcSet:t=>{if(!t)return;const e=[],n=(t,n)=>{const i=a(t);i&&n&&e.push(`${i} ${n}w`)};return n(t.sm,s.sm),n(t.md,s.md),n(t.lg,s.lg),n(t.full,s.full),e.length?e.join(", "):void 0},fallback:m,widths:s,hasVariantSource:u.length>0}},[$,P]),j=e.useMemo(()=>{var t;return a((null==(t=null==$?void 0:$.variants_data)?void 0:t.variants)??null)},[$]),F=e.useMemo(()=>{var t;const e=(null==(t=null==$?void 0:$.variants_data)?void 0:t.status)??(null==$?void 0:$.variants_status)??"";return"string"==typeof e?e.toLowerCase():""},[$]),k="failed"===F||"error"===F,H=x&&Boolean($)&&!k&&!j&&S<5;e.useEffect(()=>{if(!H)return;if("undefined"==typeof window)return;const t=window.setTimeout(()=>{M(t=>t+1)},3e3);return()=>window.clearTimeout(t)},[H]);const L=e.useMemo(()=>{var t,e;return"string"==typeof d?d:(null==(e=null==(t=null==$?void 0:$.meta)?void 0:t.content_manifest)?void 0:e.summary)??void 0},[d,$]),V=e.useMemo(()=>{var t,e;return"string"==typeof c?c:(null==(e=null==(t=null==$?void 0:$.meta)?void 0:t.content_manifest)?void 0:e.title)??void 0},[c,$]),W=e.useMemo(()=>{var t,e,n,i;return(null==(e=null==(t=null==$?void 0:$.meta)?void 0:t.sizing)?void 0:e.width)??(null==(i=null==(n=null==$?void 0:$.variants_data)?void 0:n.metadata)?void 0:i.width)??void 0},[$]),B=e.useMemo(()=>{var t,e,n,i;return(null==(e=null==(t=null==$?void 0:$.meta)?void 0:t.sizing)?void 0:e.height)??(null==(i=null==(n=null==$?void 0:$.variants_data)?void 0:n.metadata)?void 0:i.height)??void 0},[$]),C=e.useMemo(()=>{var t,e;const n=null==(e=null==(t=null==$?void 0:$.meta)?void 0:t.content_manifest)?void 0:e.optimized_filename;if(!n)return;const i=null==T?void 0:T.fallback;if(!i)return;const r=i.lastIndexOf(".");return`${n}.${r>-1?i.slice(r+1).toLowerCase():"jpg"}`},[$,T]);if(!x){const t={...E};return e.createElement("img",{ref:y,src:_,loading:z,decoding:A,alt:L,title:V,width:t.width,height:t.height,...t})}const D=function(t,e){return`${l(e)}/assets/low_res_thumb/${t}`}(t,n);if(!$||!T||!O){const t={...E};return e.createElement("img",{ref:y,src:D,loading:z,decoding:A,alt:L,title:V,width:t.width??W,height:t.height??B,...t})}const R=i??"(max-width:640px) 640px, (max-width:1024px) 1024px, 1536px",{webp:q,avif:G,jpeg:J,toSrcSet:K,fallback:Q}=T,U=K(q),X=K(G),Y=K(J);return U||X||Y?e.createElement("picture",null,X?e.createElement("source",{type:"image/avif",srcSet:X,sizes:R}):null,U?e.createElement("source",{type:"image/webp",srcSet:U,sizes:R}):null,e.createElement("img",{ref:y,src:Q,srcSet:!Y||U||X?void 0:Y,sizes:!Y||U||X?void 0:R,loading:z,decoding:A,alt:L,title:V,width:W,height:B,"data-filename":C,...E})):e.createElement("img",{ref:y,src:Q,loading:z,decoding:A,alt:L,title:V,width:W,height:B,"data-filename":C,...E})}),E=e.memo(_);E.displayName="OpenSiteImg";const b="undefined"!=typeof globalThis?globalThis:void 0;if(b)if(b.process){const t=b.process.env??(b.process.env={});void 0===t.NODE_ENV&&(t.NODE_ENV="production")}else b.process={env:{NODE_ENV:"production"}};t.Img=E,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})});
2
- //# sourceMappingURL=opensite-img.umd.js.map