@page-speed/img 0.4.3 → 0.4.6
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/README.md +9 -11
- package/dist/browser/page-speed-img.umd.cjs +1 -1
- package/dist/browser/page-speed-img.umd.js +1 -1
- package/dist/browser/page-speed-img.umd.js.map +1 -1
- package/dist/core/Img.cjs +73 -294
- package/dist/core/Img.d.ts +10 -18
- package/dist/core/Img.js +73 -294
- package/dist/core/useImgDebugLog.cjs +46 -0
- package/dist/core/useImgDebugLog.d.ts +20 -0
- package/dist/core/useImgDebugLog.js +46 -0
- package/dist/core/useMediaSelectionEffect.cjs +16 -4
- package/dist/core/useMediaSelectionEffect.js +16 -4
- package/dist/index.cjs +0 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +0 -1
- package/package.json +7 -6
- package/dist/browser/opensite-img.umd.cjs +0 -2
- package/dist/browser/opensite-img.umd.js +0 -2
- package/dist/browser/opensite-img.umd.js.map +0 -1
- package/dist/types.cjs +0 -1
- package/dist/types.d.ts +0 -55
- package/dist/types.js +0 -1
- package/dist/utils/api.cjs +0 -94
- package/dist/utils/api.d.ts +0 -11
- package/dist/utils/api.js +0 -94
- package/dist/utils/cache.cjs +0 -12
- package/dist/utils/cache.d.ts +0 -3
- package/dist/utils/cache.js +0 -12
package/dist/core/Img.js
CHANGED
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { forwardRef, memo, useCallback,
|
|
4
|
-
import { useOptimizedImage } from
|
|
5
|
-
import {
|
|
6
|
-
import { useMediaSelectionEffect } from
|
|
7
|
-
import { useResponsiveReset } from
|
|
8
|
-
const
|
|
9
|
-
sm: 640,
|
|
10
|
-
md: 1024,
|
|
11
|
-
lg: 1536,
|
|
12
|
-
full: 2560,
|
|
13
|
-
};
|
|
14
|
-
const MAX_VARIANT_REFRESH_ATTEMPTS = 5;
|
|
15
|
-
const VARIANT_REFRESH_DELAY_MS = 3000;
|
|
16
|
-
const TRANSPARENT_PIXEL = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==';
|
|
3
|
+
import { forwardRef, memo, useCallback, useMemo, useRef } from "react";
|
|
4
|
+
import { useOptimizedImage } from "@page-speed/hooks/media";
|
|
5
|
+
import { useImgDebugLog } from "./useImgDebugLog.js";
|
|
6
|
+
import { useMediaSelectionEffect } from "./useMediaSelectionEffect.js";
|
|
7
|
+
import { useResponsiveReset } from "./useResponsiveReset.js";
|
|
8
|
+
const TRANSPARENT_PIXEL = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
|
|
17
9
|
let defaultOptixFlowConfig;
|
|
18
|
-
const deprecatedMediaWarnings = new Set();
|
|
19
10
|
const readGlobalOptixFlowConfig = () => {
|
|
20
|
-
if (typeof globalThis ===
|
|
11
|
+
if (typeof globalThis === "undefined")
|
|
21
12
|
return undefined;
|
|
22
13
|
const globalAny = globalThis;
|
|
23
14
|
return (globalAny.PageSpeedImgDefaults?.optixFlowConfig ||
|
|
@@ -30,24 +21,12 @@ const resolveOptixFlowConfig = (config) => {
|
|
|
30
21
|
export const setDefaultOptixFlowConfig = (config) => {
|
|
31
22
|
defaultOptixFlowConfig = config ?? undefined;
|
|
32
23
|
};
|
|
33
|
-
const warnDeprecatedMediaId = (mediaId) => {
|
|
34
|
-
if (!Number.isFinite(mediaId) || mediaId == null)
|
|
35
|
-
return;
|
|
36
|
-
const id = mediaId;
|
|
37
|
-
if (deprecatedMediaWarnings.has(id))
|
|
38
|
-
return;
|
|
39
|
-
deprecatedMediaWarnings.add(id);
|
|
40
|
-
if (typeof console !== 'undefined' && console.warn) {
|
|
41
|
-
console.warn('[DEPRECATED] <Img mediaId> is deprecated. Provide src + optixFlowConfig instead.', { mediaId: id });
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
const isUrlString = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
45
24
|
const parseDimension = (value) => {
|
|
46
|
-
if (value ===
|
|
25
|
+
if (value === "" || value === null || typeof value === "undefined")
|
|
47
26
|
return undefined;
|
|
48
|
-
if (typeof value ===
|
|
27
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
49
28
|
return value;
|
|
50
|
-
if (typeof value ===
|
|
29
|
+
if (typeof value === "string") {
|
|
51
30
|
const numeric = Number(value);
|
|
52
31
|
if (Number.isFinite(numeric))
|
|
53
32
|
return numeric;
|
|
@@ -58,294 +37,94 @@ const composeRefs = (hookRef, forwardedRef, localRef) => useCallback((node) => {
|
|
|
58
37
|
hookRef(node);
|
|
59
38
|
// eslint-disable-next-line no-param-reassign
|
|
60
39
|
localRef.current = node;
|
|
61
|
-
if (typeof forwardedRef ===
|
|
40
|
+
if (typeof forwardedRef === "function") {
|
|
62
41
|
forwardedRef(node);
|
|
63
42
|
}
|
|
64
|
-
else if (forwardedRef && typeof forwardedRef ===
|
|
43
|
+
else if (forwardedRef && typeof forwardedRef === "object") {
|
|
65
44
|
forwardedRef.current = node;
|
|
66
45
|
}
|
|
67
46
|
}, [hookRef, forwardedRef, localRef]);
|
|
68
|
-
|
|
69
|
-
const w = v?.widths;
|
|
70
|
-
if (!w)
|
|
71
|
-
return null;
|
|
72
|
-
return {
|
|
73
|
-
sm: w.small ?? w.sm ?? DEFAULT_WIDTHS.sm,
|
|
74
|
-
md: w.medium ?? w.md ?? DEFAULT_WIDTHS.md,
|
|
75
|
-
lg: w.large ?? w.lg ?? DEFAULT_WIDTHS.lg,
|
|
76
|
-
full: w.full_size ?? w.full ?? DEFAULT_WIDTHS.full,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
function pickBest(sizes) {
|
|
80
|
-
if (!sizes)
|
|
81
|
-
return undefined;
|
|
82
|
-
return sizes.md || sizes.lg || sizes.sm || sizes.full || Object.values(sizes).find(Boolean);
|
|
83
|
-
}
|
|
84
|
-
const DEFAULT_SIZES = '(max-width:640px) 640px, (max-width:1024px) 1024px, 1536px';
|
|
85
|
-
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 }) => {
|
|
86
48
|
const imgRef = useRef(null);
|
|
87
49
|
const pictureRef = useRef(null);
|
|
88
50
|
useResponsiveReset(pictureRef);
|
|
89
51
|
useMediaSelectionEffect();
|
|
90
|
-
const normalizedSrc = useMemo(() => (typeof directSrc ===
|
|
91
|
-
const numericWidth = useMemo(() => parseDimension(
|
|
92
|
-
const numericHeight = useMemo(() => parseDimension(
|
|
52
|
+
const normalizedSrc = useMemo(() => (typeof directSrc === "string" ? directSrc.trim() : ""), [directSrc]);
|
|
53
|
+
const numericWidth = useMemo(() => parseDimension(width), [width]);
|
|
54
|
+
const numericHeight = useMemo(() => parseDimension(height), [height]);
|
|
93
55
|
const resolvedOptixConfig = useMemo(() => resolveOptixFlowConfig(optixFlowConfig), [optixFlowConfig]);
|
|
94
|
-
const eagerLoad =
|
|
95
|
-
|
|
56
|
+
const eagerLoad = useMemo(() => {
|
|
57
|
+
return eager ?? loading === "eager";
|
|
58
|
+
}, [eager, loading]);
|
|
59
|
+
const hookOptions = useMemo(() => ({
|
|
96
60
|
src: normalizedSrc,
|
|
97
61
|
eager: eagerLoad,
|
|
98
62
|
width: numericWidth,
|
|
99
63
|
height: numericHeight,
|
|
100
|
-
rootMargin: intersectionMargin ??
|
|
64
|
+
rootMargin: intersectionMargin ?? "200px",
|
|
101
65
|
threshold: intersectionThreshold ?? 0.1,
|
|
102
66
|
optixFlowConfig: resolvedOptixConfig,
|
|
103
|
-
})
|
|
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);
|
|
104
77
|
const mergedRef = composeRefs(hookRef, forwardedRef, imgRef);
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
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,
|
|
107
|
+
eagerLoad,
|
|
108
|
+
isInView,
|
|
109
|
+
imgSrc,
|
|
110
|
+
transparentPixel: TRANSPARENT_PIXEL,
|
|
111
|
+
srcset,
|
|
112
|
+
sizesAttr,
|
|
113
|
+
});
|
|
116
114
|
if (!hasSrcSet) {
|
|
117
|
-
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 }));
|
|
118
116
|
}
|
|
119
|
-
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 })] }));
|
|
120
|
-
};
|
|
121
|
-
const LegacyImg = ({ mediaId, cdnHost, sizes, onImageData, loading, decoding, alt, title, src: directSrc, forwardedRef, ...rest }) => {
|
|
122
|
-
const imgRef = useRef(null);
|
|
123
|
-
const pictureRef = useRef(null);
|
|
124
|
-
useImperativeHandle(forwardedRef, () => imgRef.current);
|
|
125
|
-
useResponsiveReset(pictureRef);
|
|
126
|
-
useMediaSelectionEffect();
|
|
127
|
-
const [data, setData] = useState(null);
|
|
128
|
-
const [retryCount, setRetryCount] = useState(0);
|
|
129
|
-
const hasMediaId = Number.isFinite(mediaId);
|
|
130
|
-
const loadingAttr = loading ?? 'lazy';
|
|
131
|
-
const decodingAttr = decoding ?? 'async';
|
|
132
|
-
const [isInView, setIsInView] = useState(() => !hasMediaId || loadingAttr !== 'lazy');
|
|
133
|
-
const cdnOrigin = useMemo(() => (cdnHost ?? DEFAULT_CDN_HOST).replace(/\/$/, ''), [cdnHost]);
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
if (!hasMediaId) {
|
|
136
|
-
setData(null);
|
|
137
|
-
setRetryCount(0);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
setData(null);
|
|
141
|
-
setRetryCount(0);
|
|
142
|
-
}, [hasMediaId, mediaId, cdnHost]);
|
|
143
|
-
useEffect(() => {
|
|
144
|
-
if (!hasMediaId) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
const controller = new AbortController();
|
|
148
|
-
fetchImageData(mediaId, {
|
|
149
|
-
cdnHost,
|
|
150
|
-
signal: controller.signal,
|
|
151
|
-
bypassCache: retryCount > 0,
|
|
152
|
-
})
|
|
153
|
-
.then((d) => {
|
|
154
|
-
setData(d);
|
|
155
|
-
onImageData?.(d);
|
|
156
|
-
})
|
|
157
|
-
.catch((err) => {
|
|
158
|
-
if (err?.name !== 'AbortError') {
|
|
159
|
-
// eslint-disable-next-line no-console
|
|
160
|
-
console.warn('Image data fetch failed:', err);
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
return () => controller.abort();
|
|
164
|
-
}, [hasMediaId, mediaId, cdnHost, onImageData, retryCount]);
|
|
165
|
-
useEffect(() => {
|
|
166
|
-
if (!hasMediaId || loadingAttr !== 'lazy') {
|
|
167
|
-
setIsInView(true);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
setIsInView(false);
|
|
171
|
-
}, [hasMediaId, mediaId, loadingAttr]);
|
|
172
|
-
useEffect(() => {
|
|
173
|
-
if (!hasMediaId || loadingAttr !== 'lazy' || isInView) {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
if (typeof window === 'undefined' || typeof window.IntersectionObserver === 'undefined') {
|
|
177
|
-
setIsInView(true);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
const node = imgRef.current;
|
|
181
|
-
if (!node) {
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
const observer = new IntersectionObserver((entries) => {
|
|
185
|
-
if (entries.some((entry) => entry.isIntersecting)) {
|
|
186
|
-
setIsInView(true);
|
|
187
|
-
observer.disconnect();
|
|
188
|
-
}
|
|
189
|
-
}, { rootMargin: '200px' });
|
|
190
|
-
observer.observe(node);
|
|
191
|
-
return () => observer.disconnect();
|
|
192
|
-
}, [hasMediaId, loadingAttr, isInView]);
|
|
193
|
-
// Build picture/source/srcset from variants
|
|
194
|
-
const picture = useMemo(() => {
|
|
195
|
-
if (!data)
|
|
196
|
-
return null;
|
|
197
|
-
const v = data.variants_data?.variants ?? {};
|
|
198
|
-
const webp = v.WEBP;
|
|
199
|
-
const avif = v.AVIF;
|
|
200
|
-
const jpeg = v.JPEG;
|
|
201
|
-
const widths = widthMapFromMetadata(v.WEBP?.metadata) ||
|
|
202
|
-
widthMapFromMetadata(v.JPEG?.metadata) ||
|
|
203
|
-
{ ...DEFAULT_WIDTHS };
|
|
204
|
-
const ensureAbsolute = (url) => {
|
|
205
|
-
if (!isUrlString(url))
|
|
206
|
-
return undefined;
|
|
207
|
-
if (/^https?:\/\//i.test(url) || url.startsWith('data:'))
|
|
208
|
-
return url;
|
|
209
|
-
if (url.startsWith('//'))
|
|
210
|
-
return `https:${url}`;
|
|
211
|
-
if (url.startsWith('/'))
|
|
212
|
-
return `${cdnOrigin}${url}`;
|
|
213
|
-
return `${cdnOrigin}/${url}`;
|
|
214
|
-
};
|
|
215
|
-
const normalizeCandidate = (candidate) => ensureAbsolute(typeof candidate === 'string' ? candidate : undefined);
|
|
216
|
-
const variantCandidates = [
|
|
217
|
-
pickBest(webp),
|
|
218
|
-
pickBest(jpeg),
|
|
219
|
-
pickBest(avif),
|
|
220
|
-
webp?.sm,
|
|
221
|
-
webp?.md,
|
|
222
|
-
webp?.lg,
|
|
223
|
-
webp?.full,
|
|
224
|
-
jpeg?.sm,
|
|
225
|
-
jpeg?.md,
|
|
226
|
-
jpeg?.lg,
|
|
227
|
-
jpeg?.full,
|
|
228
|
-
avif?.sm,
|
|
229
|
-
avif?.md,
|
|
230
|
-
avif?.lg,
|
|
231
|
-
avif?.full,
|
|
232
|
-
]
|
|
233
|
-
.map((candidate) => normalizeCandidate(candidate ?? undefined))
|
|
234
|
-
.filter(isUrlString);
|
|
235
|
-
const raw = data;
|
|
236
|
-
const directCandidates = [
|
|
237
|
-
raw.img_url,
|
|
238
|
-
raw.file_data_url,
|
|
239
|
-
raw.file_data_thumbnail_url,
|
|
240
|
-
raw.img_src,
|
|
241
|
-
raw.med_src,
|
|
242
|
-
raw.thumb_src,
|
|
243
|
-
raw.low_res_thumb,
|
|
244
|
-
]
|
|
245
|
-
.map((candidate) => (isUrlString(candidate) ? normalizeCandidate(candidate) : undefined))
|
|
246
|
-
.filter(isUrlString);
|
|
247
|
-
// Add fallback_url as the final option if no variants or direct candidates
|
|
248
|
-
const fallbackCandidates = raw.fallback_url ? [normalizeCandidate(raw.fallback_url)].filter(isUrlString) : [];
|
|
249
|
-
const fallback = [...variantCandidates, ...directCandidates, ...fallbackCandidates][0];
|
|
250
|
-
if (!fallback) {
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
const toSrcSet = (sizes) => {
|
|
254
|
-
if (!sizes)
|
|
255
|
-
return undefined;
|
|
256
|
-
const entries = [];
|
|
257
|
-
const push = (url, width) => {
|
|
258
|
-
const absolute = normalizeCandidate(url);
|
|
259
|
-
if (absolute && width)
|
|
260
|
-
entries.push(`${absolute} ${width}w`);
|
|
261
|
-
};
|
|
262
|
-
push(sizes.sm, widths.sm);
|
|
263
|
-
push(sizes.md, widths.md);
|
|
264
|
-
push(sizes.lg, widths.lg);
|
|
265
|
-
push(sizes.full, widths.full);
|
|
266
|
-
return entries.length ? entries.join(', ') : undefined;
|
|
267
|
-
};
|
|
268
|
-
return { webp, avif, jpeg, toSrcSet, fallback, widths, hasVariantSource: variantCandidates.length > 0 };
|
|
269
|
-
}, [data, cdnOrigin]);
|
|
270
|
-
const hasVariantEntries = useMemo(() => imageVariantsHaveRenderableSource(data?.variants_data?.variants ?? null), [data]);
|
|
271
|
-
const variantsStatus = useMemo(() => {
|
|
272
|
-
const status = (data?.variants_data?.status ?? data?.variants_status) ?? '';
|
|
273
|
-
return typeof status === 'string' ? status.toLowerCase() : '';
|
|
274
|
-
}, [data]);
|
|
275
|
-
const variantsFailed = variantsStatus === 'failed' || variantsStatus === 'error';
|
|
276
|
-
const shouldPollForVariants = hasMediaId && Boolean(data) && !variantsFailed && !hasVariantEntries && retryCount < MAX_VARIANT_REFRESH_ATTEMPTS;
|
|
277
|
-
useEffect(() => {
|
|
278
|
-
if (!shouldPollForVariants) {
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
if (typeof window === 'undefined') {
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
const timeoutId = window.setTimeout(() => {
|
|
285
|
-
setRetryCount((count) => count + 1);
|
|
286
|
-
}, VARIANT_REFRESH_DELAY_MS);
|
|
287
|
-
return () => window.clearTimeout(timeoutId);
|
|
288
|
-
}, [shouldPollForVariants]);
|
|
289
|
-
// Map HTML attributes from content manifest and sizing
|
|
290
|
-
const altAttr = useMemo(() => {
|
|
291
|
-
if (typeof alt === 'string')
|
|
292
|
-
return alt;
|
|
293
|
-
return data?.meta?.content_manifest?.summary ?? undefined;
|
|
294
|
-
}, [alt, data]);
|
|
295
|
-
const titleAttr = useMemo(() => {
|
|
296
|
-
if (typeof title === 'string')
|
|
297
|
-
return title;
|
|
298
|
-
return data?.meta?.content_manifest?.title ?? undefined;
|
|
299
|
-
}, [title, data]);
|
|
300
|
-
const widthAttr = useMemo(() => data?.meta?.sizing?.width ?? data?.variants_data?.metadata?.width ?? undefined, [data]);
|
|
301
|
-
const heightAttr = useMemo(() => data?.meta?.sizing?.height ?? data?.variants_data?.metadata?.height ?? undefined, [data]);
|
|
302
|
-
// Compute data-filename for consumers that need semantic filenames
|
|
303
|
-
const dataFilename = useMemo(() => {
|
|
304
|
-
const base = data?.meta?.content_manifest?.optimized_filename;
|
|
305
|
-
if (!base)
|
|
306
|
-
return undefined;
|
|
307
|
-
// ext derived from chosen fallback url
|
|
308
|
-
const href = picture?.fallback;
|
|
309
|
-
if (!href)
|
|
310
|
-
return undefined;
|
|
311
|
-
const dot = href.lastIndexOf('.');
|
|
312
|
-
const ext = dot > -1 ? href.slice(dot + 1).toLowerCase() : 'jpg';
|
|
313
|
-
return `${base}.${ext}`;
|
|
314
|
-
}, [data, picture]);
|
|
315
|
-
// If mediaId not provided but src is, render plain img
|
|
316
|
-
if (!hasMediaId) {
|
|
317
|
-
const r = { ...rest };
|
|
318
|
-
return (_jsx("img", { ref: imgRef, src: directSrc, loading: loadingAttr, decoding: decodingAttr, alt: altAttr, title: titleAttr, width: r.width, height: r.height, ...r }));
|
|
319
|
-
}
|
|
320
|
-
const placeholderSrc = buildPlaceholderImageUrl(mediaId, cdnHost);
|
|
321
|
-
if (!data || !picture || !isInView) {
|
|
322
|
-
const r = { ...rest };
|
|
323
|
-
return (_jsx("img", { ref: imgRef, src: placeholderSrc, loading: loadingAttr, decoding: decodingAttr, alt: altAttr, title: titleAttr, width: r.width ?? widthAttr, height: r.height ?? heightAttr, ...r }));
|
|
324
|
-
}
|
|
325
|
-
const sizesAttr = sizes ?? DEFAULT_SIZES;
|
|
326
|
-
const { webp, avif, jpeg, toSrcSet, fallback } = picture;
|
|
327
|
-
const webpSet = toSrcSet(webp);
|
|
328
|
-
const avifSet = toSrcSet(avif);
|
|
329
|
-
const jpegSet = toSrcSet(jpeg);
|
|
330
|
-
if (webpSet || avifSet || jpegSet) {
|
|
331
|
-
return (_jsxs("picture", { children: [avifSet ? _jsx("source", { type: "image/avif", srcSet: avifSet, sizes: sizesAttr }) : null, webpSet ? _jsx("source", { type: "image/webp", srcSet: webpSet, sizes: sizesAttr }) : null, _jsx("img", { ref: imgRef, src: fallback, srcSet: jpegSet && !webpSet && !avifSet ? jpegSet : undefined, sizes: jpegSet && !webpSet && !avifSet ? sizesAttr : undefined, loading: loadingAttr, decoding: decodingAttr, alt: altAttr, title: titleAttr, width: widthAttr, height: heightAttr, "data-filename": dataFilename, ...rest })] }));
|
|
332
|
-
}
|
|
333
|
-
return (_jsx("img", { ref: imgRef, src: fallback, loading: loadingAttr, decoding: decodingAttr, alt: altAttr, title: titleAttr, width: widthAttr, height: heightAttr, "data-filename": dataFilename, ...rest }));
|
|
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 })] }));
|
|
334
118
|
};
|
|
335
119
|
const ImgBase = forwardRef(function Img(props, ref) {
|
|
336
|
-
const
|
|
337
|
-
if (hasMediaId) {
|
|
338
|
-
warnDeprecatedMediaId(props.mediaId);
|
|
339
|
-
return _jsx(LegacyImg, { ...props, forwardedRef: ref });
|
|
340
|
-
}
|
|
341
|
-
const hasSrc = typeof props.src === 'string' && props.src.trim().length > 0;
|
|
120
|
+
const hasSrc = typeof props.src === "string" && props.src.trim().length > 0;
|
|
342
121
|
if (!hasSrc) {
|
|
343
|
-
if (typeof console !==
|
|
344
|
-
console.warn(
|
|
122
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
123
|
+
console.warn("<Img /> requires src. No src provided, rendering null.");
|
|
345
124
|
}
|
|
346
125
|
return null;
|
|
347
126
|
}
|
|
348
127
|
return _jsx(ModernImg, { ...props, forwardedRef: ref });
|
|
349
128
|
});
|
|
350
129
|
export const Img = memo(ImgBase);
|
|
351
|
-
Img.displayName =
|
|
130
|
+
Img.displayName = "PageSpeedImg";
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
2
|
const MEDIA_SELECTED_EVENT = 'dt:media-selected';
|
|
3
|
+
const mediaSelectionHandler = () => {
|
|
4
|
+
// no-op: the real handler is attached in the builder via addEventListener
|
|
5
|
+
};
|
|
6
|
+
let mediaSelectionListenerCount = 0;
|
|
7
|
+
let isMediaSelectionListenerAttached = false;
|
|
3
8
|
export function sendMediaSelection(blockId, payload) {
|
|
4
9
|
if (typeof window === 'undefined')
|
|
5
10
|
return;
|
|
@@ -11,10 +16,17 @@ export function useMediaSelectionEffect() {
|
|
|
11
16
|
useEffect(() => {
|
|
12
17
|
if (typeof window === 'undefined')
|
|
13
18
|
return;
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
if (!isMediaSelectionListenerAttached) {
|
|
20
|
+
window.addEventListener(MEDIA_SELECTED_EVENT, mediaSelectionHandler);
|
|
21
|
+
isMediaSelectionListenerAttached = true;
|
|
22
|
+
}
|
|
23
|
+
mediaSelectionListenerCount += 1;
|
|
24
|
+
return () => {
|
|
25
|
+
mediaSelectionListenerCount -= 1;
|
|
26
|
+
if (mediaSelectionListenerCount <= 0 && isMediaSelectionListenerAttached) {
|
|
27
|
+
window.removeEventListener(MEDIA_SELECTED_EVENT, mediaSelectionHandler);
|
|
28
|
+
isMediaSelectionListenerAttached = false;
|
|
29
|
+
}
|
|
16
30
|
};
|
|
17
|
-
window.addEventListener(MEDIA_SELECTED_EVENT, handler);
|
|
18
|
-
return () => window.removeEventListener(MEDIA_SELECTED_EVENT, handler);
|
|
19
31
|
}, []);
|
|
20
32
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
2
|
const MEDIA_SELECTED_EVENT = 'dt:media-selected';
|
|
3
|
+
const mediaSelectionHandler = () => {
|
|
4
|
+
// no-op: the real handler is attached in the builder via addEventListener
|
|
5
|
+
};
|
|
6
|
+
let mediaSelectionListenerCount = 0;
|
|
7
|
+
let isMediaSelectionListenerAttached = false;
|
|
3
8
|
export function sendMediaSelection(blockId, payload) {
|
|
4
9
|
if (typeof window === 'undefined')
|
|
5
10
|
return;
|
|
@@ -11,10 +16,17 @@ export function useMediaSelectionEffect() {
|
|
|
11
16
|
useEffect(() => {
|
|
12
17
|
if (typeof window === 'undefined')
|
|
13
18
|
return;
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
if (!isMediaSelectionListenerAttached) {
|
|
20
|
+
window.addEventListener(MEDIA_SELECTED_EVENT, mediaSelectionHandler);
|
|
21
|
+
isMediaSelectionListenerAttached = true;
|
|
22
|
+
}
|
|
23
|
+
mediaSelectionListenerCount += 1;
|
|
24
|
+
return () => {
|
|
25
|
+
mediaSelectionListenerCount -= 1;
|
|
26
|
+
if (mediaSelectionListenerCount <= 0 && isMediaSelectionListenerAttached) {
|
|
27
|
+
window.removeEventListener(MEDIA_SELECTED_EVENT, mediaSelectionHandler);
|
|
28
|
+
isMediaSelectionListenerAttached = false;
|
|
29
|
+
}
|
|
16
30
|
};
|
|
17
|
-
window.addEventListener(MEDIA_SELECTED_EVENT, handler);
|
|
18
|
-
return () => window.removeEventListener(MEDIA_SELECTED_EVENT, handler);
|
|
19
31
|
}, []);
|
|
20
32
|
}
|
package/dist/index.cjs
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
import type { UseOptimizedImageOptions } from '@page-speed/hooks/media';
|
|
1
2
|
export * from './core/index.js';
|
|
2
|
-
export
|
|
3
|
+
export type { ImageFormat, SrcsetByFormat, UseOptimizedImageOptions, UseOptimizedImageState, } from '@page-speed/hooks/media';
|
|
4
|
+
export type OptixFlowConfig = UseOptimizedImageOptions['optixFlowConfig'];
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@page-speed/img",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
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",
|
|
@@ -75,22 +75,23 @@
|
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@commitlint/cli": "^20.1.0",
|
|
77
77
|
"@commitlint/config-conventional": "^20.0.0",
|
|
78
|
+
"@types/node": "^20.17.6",
|
|
78
79
|
"@types/react": "^18.3.3",
|
|
79
80
|
"@types/react-dom": "^18.3.0",
|
|
80
81
|
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
|
81
82
|
"@typescript-eslint/parser": "^8.46.0",
|
|
83
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
82
84
|
"eslint": "^9.37.0",
|
|
83
85
|
"happy-dom": "^15.11.7",
|
|
84
86
|
"husky": "^9.1.7",
|
|
87
|
+
"terser": "^5.44.0",
|
|
85
88
|
"typescript": "^5.6.2",
|
|
86
89
|
"vite": "^5.4.20",
|
|
87
|
-
"
|
|
88
|
-
"terser": "^5.44.0",
|
|
89
|
-
"vitest": "^3.2.4",
|
|
90
|
-
"@types/node": "^20.17.6"
|
|
90
|
+
"vitest": "^3.2.4"
|
|
91
91
|
},
|
|
92
92
|
"dependencies": {
|
|
93
|
-
"@
|
|
93
|
+
"@opensite/hooks": "2.0.8",
|
|
94
|
+
"@page-speed/hooks": "0.4.5"
|
|
94
95
|
},
|
|
95
96
|
"packageManager": "pnpm@10.24.0",
|
|
96
97
|
"engines": {
|