@page-speed/img 0.4.2 → 0.4.5
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 +6 -6
- 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 +55 -277
- package/dist/core/Img.d.ts +6 -18
- package/dist/core/Img.js +55 -277
- 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/dist/types.d.ts +1 -50
- package/package.json +7 -6
package/dist/core/Img.js
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { forwardRef, memo, useCallback, useEffect,
|
|
4
|
-
import { useOptimizedImage } from
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
const DEFAULT_WIDTHS = {
|
|
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, useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import { useOptimizedImage } from "@page-speed/hooks/media";
|
|
5
|
+
import { useMediaSelectionEffect } from "./useMediaSelectionEffect.js";
|
|
6
|
+
import { useResponsiveReset } from "./useResponsiveReset.js";
|
|
7
|
+
const TRANSPARENT_PIXEL = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
|
|
17
8
|
let defaultOptixFlowConfig;
|
|
18
|
-
const deprecatedMediaWarnings = new Set();
|
|
19
9
|
const readGlobalOptixFlowConfig = () => {
|
|
20
|
-
if (typeof globalThis ===
|
|
10
|
+
if (typeof globalThis === "undefined")
|
|
21
11
|
return undefined;
|
|
22
12
|
const globalAny = globalThis;
|
|
23
13
|
return (globalAny.PageSpeedImgDefaults?.optixFlowConfig ||
|
|
@@ -30,24 +20,13 @@ const resolveOptixFlowConfig = (config) => {
|
|
|
30
20
|
export const setDefaultOptixFlowConfig = (config) => {
|
|
31
21
|
defaultOptixFlowConfig = config ?? undefined;
|
|
32
22
|
};
|
|
33
|
-
const
|
|
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;
|
|
23
|
+
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,93 @@ 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
|
-
function widthMapFromMetadata(v) {
|
|
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
47
|
const ModernImg = ({ sizes, loading, decoding, alt, title, src: directSrc, eager, intersectionMargin, intersectionThreshold, optixFlowConfig, forwardedRef, ...rest }) => {
|
|
86
48
|
const imgRef = useRef(null);
|
|
87
49
|
const pictureRef = useRef(null);
|
|
50
|
+
const logKeyRef = useRef(null);
|
|
88
51
|
useResponsiveReset(pictureRef);
|
|
89
52
|
useMediaSelectionEffect();
|
|
90
|
-
const normalizedSrc = useMemo(() => (typeof directSrc ===
|
|
53
|
+
const normalizedSrc = useMemo(() => (typeof directSrc === "string" ? directSrc.trim() : ""), [directSrc]);
|
|
91
54
|
const numericWidth = useMemo(() => parseDimension(rest.width), [rest]);
|
|
92
55
|
const numericHeight = useMemo(() => parseDimension(rest.height), [rest]);
|
|
93
56
|
const resolvedOptixConfig = useMemo(() => resolveOptixFlowConfig(optixFlowConfig), [optixFlowConfig]);
|
|
94
|
-
const eagerLoad = eager ?? loading ===
|
|
95
|
-
const { ref: hookRef, src, srcset, sizes: computedSizes, loading: hookLoading, size } = useOptimizedImage({
|
|
57
|
+
const eagerLoad = eager ?? loading === "eager";
|
|
58
|
+
const { ref: hookRef, src, srcset, sizes: computedSizes, loading: hookLoading, isInView, size, } = useOptimizedImage({
|
|
96
59
|
src: normalizedSrc,
|
|
97
60
|
eager: eagerLoad,
|
|
98
61
|
width: numericWidth,
|
|
99
62
|
height: numericHeight,
|
|
100
|
-
rootMargin: intersectionMargin ??
|
|
63
|
+
rootMargin: intersectionMargin ?? "200px",
|
|
101
64
|
threshold: intersectionThreshold ?? 0.1,
|
|
102
65
|
optixFlowConfig: resolvedOptixConfig,
|
|
103
66
|
});
|
|
104
67
|
const mergedRef = composeRefs(hookRef, forwardedRef, imgRef);
|
|
105
68
|
const { width, height, ...restProps } = rest;
|
|
106
69
|
const sizesAttr = sizes ?? (computedSizes || undefined);
|
|
107
|
-
const loadingAttr = loading ?? hookLoading ??
|
|
108
|
-
const decodingAttr = decoding ??
|
|
70
|
+
const loadingAttr = loading ?? hookLoading ?? "lazy";
|
|
71
|
+
const decodingAttr = decoding ?? "async";
|
|
109
72
|
const hasSrcSet = Boolean(srcset.avif || srcset.webp || srcset.jpeg);
|
|
110
73
|
const imgSrc = src || normalizedSrc || TRANSPARENT_PIXEL;
|
|
111
|
-
const inlineSrcSet = hasSrcSet && !srcset.avif && !srcset.webp ? srcset.jpeg :
|
|
74
|
+
const inlineSrcSet = hasSrcSet && !srcset.avif && !srcset.webp ? srcset.jpeg : "";
|
|
112
75
|
const parsedWidth = parseDimension(width);
|
|
113
76
|
const parsedHeight = parseDimension(height);
|
|
114
77
|
const widthAttr = parsedWidth ?? (size.width || numericWidth || undefined);
|
|
115
78
|
const heightAttr = parsedHeight ?? (size.height || numericHeight || undefined);
|
|
116
|
-
|
|
117
|
-
return (_jsx("img", { ref: mergedRef, src: imgSrc, loading: loadingAttr, decoding: decodingAttr, alt: alt, title: title, width: widthAttr, height: heightAttr, ...restProps }));
|
|
118
|
-
}
|
|
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]);
|
|
79
|
+
// Temporary logging to detect repeated transform requests and URL churn.
|
|
165
80
|
useEffect(() => {
|
|
166
|
-
if (
|
|
167
|
-
setIsInView(true);
|
|
81
|
+
if (typeof window === "undefined")
|
|
168
82
|
return;
|
|
169
|
-
|
|
170
|
-
setIsInView(false);
|
|
171
|
-
}, [hasMediaId, mediaId, loadingAttr]);
|
|
172
|
-
useEffect(() => {
|
|
173
|
-
if (!hasMediaId || loadingAttr !== 'lazy' || isInView) {
|
|
83
|
+
if (!eagerLoad && !isInView)
|
|
174
84
|
return;
|
|
175
|
-
|
|
176
|
-
if (typeof window === 'undefined' || typeof window.IntersectionObserver === 'undefined') {
|
|
177
|
-
setIsInView(true);
|
|
85
|
+
if (!imgSrc || imgSrc === TRANSPARENT_PIXEL)
|
|
178
86
|
return;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
87
|
+
const logKey = [
|
|
88
|
+
imgSrc,
|
|
89
|
+
srcset.avif,
|
|
90
|
+
srcset.webp,
|
|
91
|
+
srcset.jpeg,
|
|
92
|
+
sizesAttr ?? "",
|
|
93
|
+
].join("|");
|
|
94
|
+
if (logKeyRef.current === logKey)
|
|
182
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
|
+
});
|
|
183
103
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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 })] }));
|
|
104
|
+
}, [
|
|
105
|
+
eagerLoad,
|
|
106
|
+
imgSrc,
|
|
107
|
+
isInView,
|
|
108
|
+
sizesAttr,
|
|
109
|
+
srcset.avif,
|
|
110
|
+
srcset.webp,
|
|
111
|
+
srcset.jpeg,
|
|
112
|
+
]);
|
|
113
|
+
if (!hasSrcSet) {
|
|
114
|
+
return (_jsx("img", { ref: mergedRef, src: imgSrc, loading: loadingAttr, decoding: decodingAttr, alt: alt, title: title, width: widthAttr, height: heightAttr, ...restProps }));
|
|
332
115
|
}
|
|
333
|
-
return (_jsx("img", { ref:
|
|
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 })] }));
|
|
334
117
|
};
|
|
335
118
|
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;
|
|
119
|
+
const hasSrc = typeof props.src === "string" && props.src.trim().length > 0;
|
|
342
120
|
if (!hasSrc) {
|
|
343
|
-
if (typeof console !==
|
|
344
|
-
console.warn(
|
|
121
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
122
|
+
console.warn("<Img /> requires src. No src provided, rendering null.");
|
|
345
123
|
}
|
|
346
124
|
return null;
|
|
347
125
|
}
|
|
348
126
|
return _jsx(ModernImg, { ...props, forwardedRef: ref });
|
|
349
127
|
});
|
|
350
128
|
export const Img = memo(ImgBase);
|
|
351
|
-
Img.displayName =
|
|
129
|
+
Img.displayName = "PageSpeedImg";
|
|
@@ -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/dist/types.d.ts
CHANGED
|
@@ -1,55 +1,6 @@
|
|
|
1
|
-
export type ImageFormat =
|
|
1
|
+
export type ImageFormat = "avif" | "webp" | "jpeg" | "png";
|
|
2
2
|
export type OptixFlowConfig = {
|
|
3
3
|
apiKey: string;
|
|
4
4
|
compressionLevel?: number;
|
|
5
5
|
renderedFileType?: ImageFormat;
|
|
6
6
|
};
|
|
7
|
-
export type ProgressiveSizes = Partial<Record<'sm' | 'md' | 'lg' | 'full', string>>;
|
|
8
|
-
export type ImageVariantsMap = Partial<Record<'AVIF' | 'WEBP' | 'JPEG', ProgressiveSizes & {
|
|
9
|
-
metadata?: {
|
|
10
|
-
widths?: {
|
|
11
|
-
small?: number;
|
|
12
|
-
medium?: number;
|
|
13
|
-
large?: number;
|
|
14
|
-
full_size?: number;
|
|
15
|
-
};
|
|
16
|
-
};
|
|
17
|
-
}>>;
|
|
18
|
-
export interface ImageVariantsData {
|
|
19
|
-
variants?: ImageVariantsMap | null;
|
|
20
|
-
metadata?: {
|
|
21
|
-
width?: number;
|
|
22
|
-
height?: number;
|
|
23
|
-
};
|
|
24
|
-
status?: string;
|
|
25
|
-
}
|
|
26
|
-
export interface ImageData {
|
|
27
|
-
id: number;
|
|
28
|
-
name?: string;
|
|
29
|
-
media_type?: string;
|
|
30
|
-
img_url?: string | null;
|
|
31
|
-
img_src?: string | null;
|
|
32
|
-
thumb_src?: string | null;
|
|
33
|
-
med_src?: string | null;
|
|
34
|
-
file_data_url?: string | null;
|
|
35
|
-
file_data_thumbnail_url?: string | null;
|
|
36
|
-
low_res_thumb?: string | null;
|
|
37
|
-
fallback_url?: string;
|
|
38
|
-
variants_status?: string | null;
|
|
39
|
-
meta?: {
|
|
40
|
-
sizing?: {
|
|
41
|
-
height?: number;
|
|
42
|
-
width?: number;
|
|
43
|
-
size_in_mb?: number;
|
|
44
|
-
aspect_ratio?: number;
|
|
45
|
-
};
|
|
46
|
-
semantic_filename?: string;
|
|
47
|
-
content_manifest?: {
|
|
48
|
-
title?: string;
|
|
49
|
-
summary?: string;
|
|
50
|
-
description?: string;
|
|
51
|
-
optimized_filename?: string;
|
|
52
|
-
};
|
|
53
|
-
};
|
|
54
|
-
variants_data?: ImageVariantsData | null;
|
|
55
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@page-speed/img",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
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.3",
|
|
94
|
+
"@page-speed/hooks": "0.4.5"
|
|
94
95
|
},
|
|
95
96
|
"packageManager": "pnpm@10.24.0",
|
|
96
97
|
"engines": {
|