@webstudio-is/image 0.95.0 → 0.97.0
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/lib/index.js +178 -4
- package/package.json +3 -3
- package/lib/image-dev.stories.js +0 -71
- package/lib/image-loaders.js +0 -17
- package/lib/image-optimize.js +0 -95
- package/lib/image-optimize.test.js +0 -157
- package/lib/image.js +0 -59
package/lib/index.js
CHANGED
|
@@ -1,4 +1,178 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// src/image.tsx
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
|
|
4
|
+
// src/image-optimize.ts
|
|
5
|
+
var imageSizes = [16, 32, 48, 64, 96, 128, 256, 384];
|
|
6
|
+
var deviceSizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
|
7
|
+
var allSizes = [...imageSizes, ...deviceSizes];
|
|
8
|
+
var getWidths = (width, sizes) => {
|
|
9
|
+
if (sizes) {
|
|
10
|
+
const viewportWidthRe = /(^|\s)(1?\d?\d)vw/g;
|
|
11
|
+
const percentSizes = [];
|
|
12
|
+
for (let match; match = viewportWidthRe.exec(sizes); match) {
|
|
13
|
+
percentSizes.push(Number.parseInt(match[2], 10));
|
|
14
|
+
}
|
|
15
|
+
if (percentSizes.length) {
|
|
16
|
+
const smallestRatio = Math.min(...percentSizes) * 0.01;
|
|
17
|
+
return {
|
|
18
|
+
widths: allSizes.filter((s) => s >= deviceSizes[0] * smallestRatio),
|
|
19
|
+
kind: "w"
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return { widths: allSizes, kind: "w" };
|
|
23
|
+
}
|
|
24
|
+
if (width == null) {
|
|
25
|
+
return { widths: deviceSizes, kind: "w" };
|
|
26
|
+
}
|
|
27
|
+
const widths = [
|
|
28
|
+
...new Set(
|
|
29
|
+
[width, width * 2].map(
|
|
30
|
+
(w) => allSizes.find((p) => p >= w) || allSizes[allSizes.length - 1]
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
];
|
|
34
|
+
return { widths, kind: "x" };
|
|
35
|
+
};
|
|
36
|
+
var generateImgAttrs = ({
|
|
37
|
+
src,
|
|
38
|
+
width,
|
|
39
|
+
quality,
|
|
40
|
+
sizes,
|
|
41
|
+
loader
|
|
42
|
+
}) => {
|
|
43
|
+
const { widths, kind } = getWidths(width, sizes);
|
|
44
|
+
return {
|
|
45
|
+
sizes: !sizes && kind === "w" ? "100vw" : sizes,
|
|
46
|
+
srcSet: widths.map(
|
|
47
|
+
(w, i) => `${loader({ src, quality, width: w })} ${kind === "w" ? w : i + 1}${kind}`
|
|
48
|
+
).join(", "),
|
|
49
|
+
// Must be last, to prevent Safari to load images twice
|
|
50
|
+
src: loader({
|
|
51
|
+
src,
|
|
52
|
+
quality,
|
|
53
|
+
width: widths[widths.length - 1]
|
|
54
|
+
})
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
var getInt = (value) => {
|
|
58
|
+
if (typeof value === "number") {
|
|
59
|
+
return Math.round(value);
|
|
60
|
+
}
|
|
61
|
+
if (typeof value === "string") {
|
|
62
|
+
const vNum = Number.parseFloat(value);
|
|
63
|
+
if (!Number.isNaN(vNum)) {
|
|
64
|
+
return Math.round(vNum);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return void 0;
|
|
68
|
+
};
|
|
69
|
+
var DEFAULT_SIZES = "(min-width: 1280px) 50vw, 100vw";
|
|
70
|
+
var DEFAULT_QUALITY = 80;
|
|
71
|
+
var getImageAttributes = (props) => {
|
|
72
|
+
const width = getInt(props.width);
|
|
73
|
+
const quality = Math.max(
|
|
74
|
+
Math.min(getInt(props.quality) ?? DEFAULT_QUALITY, 100),
|
|
75
|
+
0
|
|
76
|
+
);
|
|
77
|
+
if (props.src != null && props.src !== "") {
|
|
78
|
+
if (props.srcSet == null && props.optimize) {
|
|
79
|
+
const sizes = props.sizes ?? (props.width == null ? DEFAULT_SIZES : void 0);
|
|
80
|
+
return generateImgAttrs({
|
|
81
|
+
src: props.src,
|
|
82
|
+
width,
|
|
83
|
+
quality,
|
|
84
|
+
sizes,
|
|
85
|
+
loader: props.loader
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const resAttrs = { src: props.src };
|
|
89
|
+
if (props.srcSet != null) {
|
|
90
|
+
resAttrs.srcSet = props.srcSet;
|
|
91
|
+
}
|
|
92
|
+
if (props.sizes != null) {
|
|
93
|
+
resAttrs.sizes = props.sizes;
|
|
94
|
+
}
|
|
95
|
+
return resAttrs;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// src/image.tsx
|
|
101
|
+
import { jsx } from "react/jsx-runtime";
|
|
102
|
+
var Image = forwardRef(
|
|
103
|
+
({
|
|
104
|
+
quality,
|
|
105
|
+
loader,
|
|
106
|
+
optimize = true,
|
|
107
|
+
loading = "lazy",
|
|
108
|
+
decoding = "async",
|
|
109
|
+
...imageProps
|
|
110
|
+
}, ref) => {
|
|
111
|
+
const imageAttributes = getImageAttributes({
|
|
112
|
+
src: imageProps.src,
|
|
113
|
+
srcSet: imageProps.srcSet,
|
|
114
|
+
sizes: imageProps.sizes,
|
|
115
|
+
width: imageProps.width,
|
|
116
|
+
quality,
|
|
117
|
+
loader,
|
|
118
|
+
optimize
|
|
119
|
+
}) ?? { src: imagePlaceholderSvg };
|
|
120
|
+
return /* @__PURE__ */ jsx(
|
|
121
|
+
"img",
|
|
122
|
+
{
|
|
123
|
+
...imageProps,
|
|
124
|
+
...imageAttributes,
|
|
125
|
+
decoding,
|
|
126
|
+
loading,
|
|
127
|
+
ref
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
Image.displayName = "Image";
|
|
133
|
+
var imagePlaceholderSvg = `data:image/svg+xml;base64,${btoa(`<svg
|
|
134
|
+
width="140"
|
|
135
|
+
height="140"
|
|
136
|
+
viewBox="0 0 600 600"
|
|
137
|
+
fill="none"
|
|
138
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
139
|
+
>
|
|
140
|
+
<rect width="600" height="600" fill="#CCCCCC" />
|
|
141
|
+
<path
|
|
142
|
+
fill-rule="evenodd"
|
|
143
|
+
clip-rule="evenodd"
|
|
144
|
+
d="M450 170H150C141.716 170 135 176.716 135 185V415C135 423.284 141.716 430 150 430H450C458.284 430 465 423.284 465 415V185C465 176.716 458.284 170 450 170ZM150 145C127.909 145 110 162.909 110 185V415C110 437.091 127.909 455 150 455H450C472.091 455 490 437.091 490 415V185C490 162.909 472.091 145 450 145H150Z"
|
|
145
|
+
fill="#A2A2A2"
|
|
146
|
+
/>
|
|
147
|
+
<path
|
|
148
|
+
d="M237.135 235.012C237.135 255.723 220.345 272.512 199.635 272.512C178.924 272.512 162.135 255.723 162.135 235.012C162.135 214.301 178.924 197.512 199.635 197.512C220.345 197.512 237.135 214.301 237.135 235.012Z"
|
|
149
|
+
fill="#A2A2A2"
|
|
150
|
+
/>
|
|
151
|
+
<path
|
|
152
|
+
d="M160 405V367.205L221.609 306.364L256.552 338.628L358.161 234L440 316.043V405H160Z"
|
|
153
|
+
fill="#A2A2A2"
|
|
154
|
+
/>
|
|
155
|
+
</svg>`)}`;
|
|
156
|
+
|
|
157
|
+
// src/image-loaders.ts
|
|
158
|
+
import warnOnce from "warn-once";
|
|
159
|
+
var createImageLoader = (loaderOptions) => ({ width, src, quality }) => {
|
|
160
|
+
if (true) {
|
|
161
|
+
warnOnce(
|
|
162
|
+
allSizes.includes(width) === false,
|
|
163
|
+
"Width must be only from allowed values"
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const { imageBaseUrl } = loaderOptions;
|
|
167
|
+
const searchParams = new URLSearchParams();
|
|
168
|
+
searchParams.set("width", width.toString());
|
|
169
|
+
searchParams.set("quality", quality.toString());
|
|
170
|
+
searchParams.set("format", "auto");
|
|
171
|
+
return `${imageBaseUrl}${src}?${searchParams.toString()}`;
|
|
172
|
+
};
|
|
173
|
+
export {
|
|
174
|
+
Image,
|
|
175
|
+
allSizes,
|
|
176
|
+
createImageLoader,
|
|
177
|
+
getImageAttributes
|
|
178
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webstudio-is/image",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.97.0",
|
|
4
4
|
"description": "Image optimization",
|
|
5
5
|
"author": "Webstudio <github@webstudio.is>",
|
|
6
6
|
"homepage": "https://webstudio.is",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"typecheck": "tsc",
|
|
43
43
|
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
44
44
|
"checks": "pnpm typecheck && pnpm test",
|
|
45
|
-
"dev": "
|
|
46
|
-
"build": "rm -rf lib && esbuild
|
|
45
|
+
"dev": "rm -rf lib && esbuild 'src/**/*.ts' 'src/**/*.tsx' --outdir=lib --watch",
|
|
46
|
+
"build": "rm -rf lib && esbuild src/index.ts --outdir=lib --bundle --format=esm --packages=external",
|
|
47
47
|
"dts": "tsc --project tsconfig.dts.json",
|
|
48
48
|
"storybook:dev": "storybook dev -p 6006",
|
|
49
49
|
"storybook:build": "storybook build"
|
package/lib/image-dev.stories.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import { Image as ImagePrimitive, createImageLoader } from "./";
|
|
4
|
-
import localLogoImage from "../storybook-assets/logo.webp";
|
|
5
|
-
export default {
|
|
6
|
-
title: "Components/ImageDev"
|
|
7
|
-
};
|
|
8
|
-
const USE_CLOUDFLARE_IMAGE_TRANSFORM = false;
|
|
9
|
-
const REMOTE_SELF_DOMAIN_IMAGE = "https://webstudio.is/logo.webp";
|
|
10
|
-
const imageSrc = USE_CLOUDFLARE_IMAGE_TRANSFORM ? REMOTE_SELF_DOMAIN_IMAGE : localLogoImage;
|
|
11
|
-
const imageLoader = createImageLoader({
|
|
12
|
-
imageBaseUrl: USE_CLOUDFLARE_IMAGE_TRANSFORM ? "https://webstudio.is/cdn-cgi/image/" : ""
|
|
13
|
-
});
|
|
14
|
-
const ImageBase = (args) => {
|
|
15
|
-
const style = {
|
|
16
|
-
maxWidth: "100%",
|
|
17
|
-
display: "block",
|
|
18
|
-
...args.style
|
|
19
|
-
};
|
|
20
|
-
return /* @__PURE__ */ jsx(
|
|
21
|
-
ImagePrimitive,
|
|
22
|
-
{
|
|
23
|
-
...args,
|
|
24
|
-
optimize: true,
|
|
25
|
-
loader: imageLoader,
|
|
26
|
-
style
|
|
27
|
-
}
|
|
28
|
-
);
|
|
29
|
-
};
|
|
30
|
-
export const FixedWidthImage = () => /* @__PURE__ */ jsx(ImageBase, { src: imageSrc, width: "300", height: "400" });
|
|
31
|
-
export const FixedWidthImageCover = () => /* @__PURE__ */ jsx(
|
|
32
|
-
ImageBase,
|
|
33
|
-
{
|
|
34
|
-
src: imageSrc,
|
|
35
|
-
width: "300",
|
|
36
|
-
height: "400",
|
|
37
|
-
style: { objectFit: "cover" }
|
|
38
|
-
}
|
|
39
|
-
);
|
|
40
|
-
export const UnknownWidthImage = () => /* @__PURE__ */ jsx(ImageBase, { src: imageSrc });
|
|
41
|
-
export const AspectRatioImage = () => /* @__PURE__ */ jsx("div", { style: { width: "50%" }, children: /* @__PURE__ */ jsx(
|
|
42
|
-
ImageBase,
|
|
43
|
-
{
|
|
44
|
-
src: imageSrc,
|
|
45
|
-
style: { aspectRatio: "2/1", objectFit: "cover", width: "100%" }
|
|
46
|
-
}
|
|
47
|
-
) });
|
|
48
|
-
export const FillParentImage = () => /* @__PURE__ */ jsx("div", { style: { width: "50%", aspectRatio: "2/1", position: "relative" }, children: /* @__PURE__ */ jsx(
|
|
49
|
-
ImageBase,
|
|
50
|
-
{
|
|
51
|
-
src: imageSrc,
|
|
52
|
-
style: {
|
|
53
|
-
objectFit: "cover",
|
|
54
|
-
position: "absolute",
|
|
55
|
-
width: "100%",
|
|
56
|
-
height: "100%"
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
) });
|
|
60
|
-
export const HeroImage = () => /* @__PURE__ */ jsx(
|
|
61
|
-
ImageBase,
|
|
62
|
-
{
|
|
63
|
-
src: imageSrc,
|
|
64
|
-
sizes: "100vw",
|
|
65
|
-
style: {
|
|
66
|
-
aspectRatio: "3/1",
|
|
67
|
-
objectFit: "cover",
|
|
68
|
-
width: "100%"
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
);
|
package/lib/image-loaders.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
import warnOnce from "warn-once";
|
|
3
|
-
import { allSizes } from "./image-optimize";
|
|
4
|
-
export const createImageLoader = (loaderOptions) => ({ width, src, quality }) => {
|
|
5
|
-
if (true) {
|
|
6
|
-
warnOnce(
|
|
7
|
-
allSizes.includes(width) === false,
|
|
8
|
-
"Width must be only from allowed values"
|
|
9
|
-
);
|
|
10
|
-
}
|
|
11
|
-
const { imageBaseUrl } = loaderOptions;
|
|
12
|
-
const searchParams = new URLSearchParams();
|
|
13
|
-
searchParams.set("width", width.toString());
|
|
14
|
-
searchParams.set("quality", quality.toString());
|
|
15
|
-
searchParams.set("format", "auto");
|
|
16
|
-
return `${imageBaseUrl}${src}?${searchParams.toString()}`;
|
|
17
|
-
};
|
package/lib/image-optimize.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
const imageSizes = [16, 32, 48, 64, 96, 128, 256, 384];
|
|
3
|
-
const deviceSizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
|
4
|
-
export const allSizes = [...imageSizes, ...deviceSizes];
|
|
5
|
-
const getWidths = (width, sizes) => {
|
|
6
|
-
if (sizes) {
|
|
7
|
-
const viewportWidthRe = /(^|\s)(1?\d?\d)vw/g;
|
|
8
|
-
const percentSizes = [];
|
|
9
|
-
for (let match; match = viewportWidthRe.exec(sizes); match) {
|
|
10
|
-
percentSizes.push(Number.parseInt(match[2], 10));
|
|
11
|
-
}
|
|
12
|
-
if (percentSizes.length) {
|
|
13
|
-
const smallestRatio = Math.min(...percentSizes) * 0.01;
|
|
14
|
-
return {
|
|
15
|
-
widths: allSizes.filter((s) => s >= deviceSizes[0] * smallestRatio),
|
|
16
|
-
kind: "w"
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
return { widths: allSizes, kind: "w" };
|
|
20
|
-
}
|
|
21
|
-
if (width == null) {
|
|
22
|
-
return { widths: deviceSizes, kind: "w" };
|
|
23
|
-
}
|
|
24
|
-
const widths = [
|
|
25
|
-
...new Set(
|
|
26
|
-
[width, width * 2].map(
|
|
27
|
-
(w) => allSizes.find((p) => p >= w) || allSizes[allSizes.length - 1]
|
|
28
|
-
)
|
|
29
|
-
)
|
|
30
|
-
];
|
|
31
|
-
return { widths, kind: "x" };
|
|
32
|
-
};
|
|
33
|
-
const generateImgAttrs = ({
|
|
34
|
-
src,
|
|
35
|
-
width,
|
|
36
|
-
quality,
|
|
37
|
-
sizes,
|
|
38
|
-
loader
|
|
39
|
-
}) => {
|
|
40
|
-
const { widths, kind } = getWidths(width, sizes);
|
|
41
|
-
return {
|
|
42
|
-
sizes: !sizes && kind === "w" ? "100vw" : sizes,
|
|
43
|
-
srcSet: widths.map(
|
|
44
|
-
(w, i) => `${loader({ src, quality, width: w })} ${kind === "w" ? w : i + 1}${kind}`
|
|
45
|
-
).join(", "),
|
|
46
|
-
// Must be last, to prevent Safari to load images twice
|
|
47
|
-
src: loader({
|
|
48
|
-
src,
|
|
49
|
-
quality,
|
|
50
|
-
width: widths[widths.length - 1]
|
|
51
|
-
})
|
|
52
|
-
};
|
|
53
|
-
};
|
|
54
|
-
const getInt = (value) => {
|
|
55
|
-
if (typeof value === "number") {
|
|
56
|
-
return Math.round(value);
|
|
57
|
-
}
|
|
58
|
-
if (typeof value === "string") {
|
|
59
|
-
const vNum = Number.parseFloat(value);
|
|
60
|
-
if (!Number.isNaN(vNum)) {
|
|
61
|
-
return Math.round(vNum);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return void 0;
|
|
65
|
-
};
|
|
66
|
-
const DEFAULT_SIZES = "(min-width: 1280px) 50vw, 100vw";
|
|
67
|
-
const DEFAULT_QUALITY = 80;
|
|
68
|
-
export const getImageAttributes = (props) => {
|
|
69
|
-
const width = getInt(props.width);
|
|
70
|
-
const quality = Math.max(
|
|
71
|
-
Math.min(getInt(props.quality) ?? DEFAULT_QUALITY, 100),
|
|
72
|
-
0
|
|
73
|
-
);
|
|
74
|
-
if (props.src != null && props.src !== "") {
|
|
75
|
-
if (props.srcSet == null && props.optimize) {
|
|
76
|
-
const sizes = props.sizes ?? (props.width == null ? DEFAULT_SIZES : void 0);
|
|
77
|
-
return generateImgAttrs({
|
|
78
|
-
src: props.src,
|
|
79
|
-
width,
|
|
80
|
-
quality,
|
|
81
|
-
sizes,
|
|
82
|
-
loader: props.loader
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
const resAttrs = { src: props.src };
|
|
86
|
-
if (props.srcSet != null) {
|
|
87
|
-
resAttrs.srcSet = props.srcSet;
|
|
88
|
-
}
|
|
89
|
-
if (props.sizes != null) {
|
|
90
|
-
resAttrs.sizes = props.sizes;
|
|
91
|
-
}
|
|
92
|
-
return resAttrs;
|
|
93
|
-
}
|
|
94
|
-
return null;
|
|
95
|
-
};
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
import { describe, test, expect } from "@jest/globals";
|
|
3
|
-
import { getImageAttributes } from "./image-optimize";
|
|
4
|
-
import { createImageLoader } from "./image-loaders";
|
|
5
|
-
describe("Image optimizations applied", () => {
|
|
6
|
-
test("width is number, create pixel density descriptor 'x'", () => {
|
|
7
|
-
const imgAttr = getImageAttributes({
|
|
8
|
-
optimize: true,
|
|
9
|
-
width: 100,
|
|
10
|
-
src: "logo.webp",
|
|
11
|
-
srcSet: void 0,
|
|
12
|
-
sizes: void 0,
|
|
13
|
-
quality: 100,
|
|
14
|
-
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
15
|
-
});
|
|
16
|
-
expect(imgAttr).toMatchInlineSnapshot(`
|
|
17
|
-
{
|
|
18
|
-
"sizes": undefined,
|
|
19
|
-
"src": "/asset/image/logo.webp?width=256&quality=100&format=auto",
|
|
20
|
-
"srcSet": "/asset/image/logo.webp?width=128&quality=100&format=auto 1x, /asset/image/logo.webp?width=256&quality=100&format=auto 2x",
|
|
21
|
-
}
|
|
22
|
-
`);
|
|
23
|
-
});
|
|
24
|
-
test("width is undefined, create 'w' descriptor and sizes prop", () => {
|
|
25
|
-
const imgAttr = getImageAttributes({
|
|
26
|
-
optimize: true,
|
|
27
|
-
width: void 0,
|
|
28
|
-
src: "logo.webp",
|
|
29
|
-
srcSet: void 0,
|
|
30
|
-
sizes: void 0,
|
|
31
|
-
quality: 90,
|
|
32
|
-
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
33
|
-
});
|
|
34
|
-
expect(imgAttr).toMatchInlineSnapshot(`
|
|
35
|
-
{
|
|
36
|
-
"sizes": "(min-width: 1280px) 50vw, 100vw",
|
|
37
|
-
"src": "/asset/image/logo.webp?width=3840&quality=90&format=auto",
|
|
38
|
-
"srcSet": "/asset/image/logo.webp?width=384&quality=90&format=auto 384w, /asset/image/logo.webp?width=640&quality=90&format=auto 640w, /asset/image/logo.webp?width=750&quality=90&format=auto 750w, /asset/image/logo.webp?width=828&quality=90&format=auto 828w, /asset/image/logo.webp?width=1080&quality=90&format=auto 1080w, /asset/image/logo.webp?width=1200&quality=90&format=auto 1200w, /asset/image/logo.webp?width=1920&quality=90&format=auto 1920w, /asset/image/logo.webp?width=2048&quality=90&format=auto 2048w, /asset/image/logo.webp?width=3840&quality=90&format=auto 3840w",
|
|
39
|
-
}
|
|
40
|
-
`);
|
|
41
|
-
});
|
|
42
|
-
test("width is undefined and size defined, creates 'w' descriptor and use input sizes props", () => {
|
|
43
|
-
const imgAttr = getImageAttributes({
|
|
44
|
-
optimize: true,
|
|
45
|
-
width: void 0,
|
|
46
|
-
src: "logo.webp",
|
|
47
|
-
srcSet: void 0,
|
|
48
|
-
sizes: "100vw",
|
|
49
|
-
quality: 70,
|
|
50
|
-
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
51
|
-
});
|
|
52
|
-
expect(imgAttr).toMatchInlineSnapshot(`
|
|
53
|
-
{
|
|
54
|
-
"sizes": "100vw",
|
|
55
|
-
"src": "/asset/image/logo.webp?width=3840&quality=70&format=auto",
|
|
56
|
-
"srcSet": "/asset/image/logo.webp?width=640&quality=70&format=auto 640w, /asset/image/logo.webp?width=750&quality=70&format=auto 750w, /asset/image/logo.webp?width=828&quality=70&format=auto 828w, /asset/image/logo.webp?width=1080&quality=70&format=auto 1080w, /asset/image/logo.webp?width=1200&quality=70&format=auto 1200w, /asset/image/logo.webp?width=1920&quality=70&format=auto 1920w, /asset/image/logo.webp?width=2048&quality=70&format=auto 2048w, /asset/image/logo.webp?width=3840&quality=70&format=auto 3840w",
|
|
57
|
-
}
|
|
58
|
-
`);
|
|
59
|
-
});
|
|
60
|
-
test("width is undefined and size defined, creates 'w' descriptor and use input sizes props, resizeOrigin defined", () => {
|
|
61
|
-
const imgAttr = getImageAttributes({
|
|
62
|
-
optimize: true,
|
|
63
|
-
width: void 0,
|
|
64
|
-
src: "logo.webp",
|
|
65
|
-
srcSet: void 0,
|
|
66
|
-
sizes: "100vw",
|
|
67
|
-
quality: 70,
|
|
68
|
-
loader: createImageLoader({
|
|
69
|
-
imageBaseUrl: "https://resize-origin.is/asset/image/"
|
|
70
|
-
})
|
|
71
|
-
});
|
|
72
|
-
expect(imgAttr).toMatchInlineSnapshot(`
|
|
73
|
-
{
|
|
74
|
-
"sizes": "100vw",
|
|
75
|
-
"src": "https://resize-origin.is/asset/image/logo.webp?width=3840&quality=70&format=auto",
|
|
76
|
-
"srcSet": "https://resize-origin.is/asset/image/logo.webp?width=640&quality=70&format=auto 640w, https://resize-origin.is/asset/image/logo.webp?width=750&quality=70&format=auto 750w, https://resize-origin.is/asset/image/logo.webp?width=828&quality=70&format=auto 828w, https://resize-origin.is/asset/image/logo.webp?width=1080&quality=70&format=auto 1080w, https://resize-origin.is/asset/image/logo.webp?width=1200&quality=70&format=auto 1200w, https://resize-origin.is/asset/image/logo.webp?width=1920&quality=70&format=auto 1920w, https://resize-origin.is/asset/image/logo.webp?width=2048&quality=70&format=auto 2048w, https://resize-origin.is/asset/image/logo.webp?width=3840&quality=70&format=auto 3840w",
|
|
77
|
-
}
|
|
78
|
-
`);
|
|
79
|
-
});
|
|
80
|
-
test("custom loader", () => {
|
|
81
|
-
const imgAttr = getImageAttributes({
|
|
82
|
-
optimize: true,
|
|
83
|
-
width: void 0,
|
|
84
|
-
src: "https://webstudio.is/logo.webp",
|
|
85
|
-
srcSet: void 0,
|
|
86
|
-
sizes: "100vw",
|
|
87
|
-
quality: 70,
|
|
88
|
-
loader: ({ width, src, quality }) => `${new URL(src).pathname}?w=${width}&q=${quality}`
|
|
89
|
-
});
|
|
90
|
-
expect(imgAttr).toMatchInlineSnapshot(`
|
|
91
|
-
{
|
|
92
|
-
"sizes": "100vw",
|
|
93
|
-
"src": "/logo.webp?w=3840&q=70",
|
|
94
|
-
"srcSet": "/logo.webp?w=640&q=70 640w, /logo.webp?w=750&q=70 750w, /logo.webp?w=828&q=70 828w, /logo.webp?w=1080&q=70 1080w, /logo.webp?w=1200&q=70 1200w, /logo.webp?w=1920&q=70 1920w, /logo.webp?w=2048&q=70 2048w, /logo.webp?w=3840&q=70 3840w",
|
|
95
|
-
}
|
|
96
|
-
`);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
describe("Image optimizations not applied", () => {
|
|
100
|
-
test("optimize is false", () => {
|
|
101
|
-
const imgAttr = getImageAttributes({
|
|
102
|
-
optimize: false,
|
|
103
|
-
width: 100,
|
|
104
|
-
src: "https://webstudio.is/logo.webp",
|
|
105
|
-
srcSet: void 0,
|
|
106
|
-
sizes: void 0,
|
|
107
|
-
quality: 100,
|
|
108
|
-
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
109
|
-
});
|
|
110
|
-
expect(imgAttr).toMatchInlineSnapshot(`
|
|
111
|
-
{
|
|
112
|
-
"src": "https://webstudio.is/logo.webp",
|
|
113
|
-
}
|
|
114
|
-
`);
|
|
115
|
-
});
|
|
116
|
-
test("srcSet is defined", () => {
|
|
117
|
-
const imgAttr = getImageAttributes({
|
|
118
|
-
optimize: true,
|
|
119
|
-
width: 100,
|
|
120
|
-
src: "https://webstudio.is/logo.webp",
|
|
121
|
-
srcSet: "user-defined-srcset",
|
|
122
|
-
sizes: void 0,
|
|
123
|
-
quality: 100,
|
|
124
|
-
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
125
|
-
});
|
|
126
|
-
expect(imgAttr).toMatchInlineSnapshot(`
|
|
127
|
-
{
|
|
128
|
-
"src": "https://webstudio.is/logo.webp",
|
|
129
|
-
"srcSet": "user-defined-srcset",
|
|
130
|
-
}
|
|
131
|
-
`);
|
|
132
|
-
});
|
|
133
|
-
test("src is empty", () => {
|
|
134
|
-
const imgAttr = getImageAttributes({
|
|
135
|
-
optimize: true,
|
|
136
|
-
width: 100,
|
|
137
|
-
src: "",
|
|
138
|
-
srcSet: void 0,
|
|
139
|
-
sizes: void 0,
|
|
140
|
-
quality: 100,
|
|
141
|
-
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
142
|
-
});
|
|
143
|
-
expect(imgAttr).toMatchInlineSnapshot(`null`);
|
|
144
|
-
});
|
|
145
|
-
test("src is undefined", () => {
|
|
146
|
-
const imgAttr = getImageAttributes({
|
|
147
|
-
optimize: true,
|
|
148
|
-
width: 100,
|
|
149
|
-
src: void 0,
|
|
150
|
-
srcSet: void 0,
|
|
151
|
-
sizes: void 0,
|
|
152
|
-
quality: 100,
|
|
153
|
-
loader: createImageLoader({ imageBaseUrl: "/asset/image/" })
|
|
154
|
-
});
|
|
155
|
-
expect(imgAttr).toMatchInlineSnapshot(`null`);
|
|
156
|
-
});
|
|
157
|
-
});
|
package/lib/image.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import { forwardRef } from "react";
|
|
4
|
-
import { getImageAttributes } from "./image-optimize";
|
|
5
|
-
const defaultTag = "img";
|
|
6
|
-
export const Image = forwardRef(
|
|
7
|
-
({
|
|
8
|
-
quality,
|
|
9
|
-
loader,
|
|
10
|
-
optimize = true,
|
|
11
|
-
loading = "lazy",
|
|
12
|
-
decoding = "async",
|
|
13
|
-
...imageProps
|
|
14
|
-
}, ref) => {
|
|
15
|
-
const imageAttributes = getImageAttributes({
|
|
16
|
-
src: imageProps.src,
|
|
17
|
-
srcSet: imageProps.srcSet,
|
|
18
|
-
sizes: imageProps.sizes,
|
|
19
|
-
width: imageProps.width,
|
|
20
|
-
quality,
|
|
21
|
-
loader,
|
|
22
|
-
optimize
|
|
23
|
-
}) ?? { src: imagePlaceholderSvg };
|
|
24
|
-
return /* @__PURE__ */ jsx(
|
|
25
|
-
"img",
|
|
26
|
-
{
|
|
27
|
-
...imageProps,
|
|
28
|
-
...imageAttributes,
|
|
29
|
-
decoding,
|
|
30
|
-
loading,
|
|
31
|
-
ref
|
|
32
|
-
}
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
);
|
|
36
|
-
Image.displayName = "Image";
|
|
37
|
-
const imagePlaceholderSvg = `data:image/svg+xml;base64,${btoa(`<svg
|
|
38
|
-
width="140"
|
|
39
|
-
height="140"
|
|
40
|
-
viewBox="0 0 600 600"
|
|
41
|
-
fill="none"
|
|
42
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
43
|
-
>
|
|
44
|
-
<rect width="600" height="600" fill="#CCCCCC" />
|
|
45
|
-
<path
|
|
46
|
-
fill-rule="evenodd"
|
|
47
|
-
clip-rule="evenodd"
|
|
48
|
-
d="M450 170H150C141.716 170 135 176.716 135 185V415C135 423.284 141.716 430 150 430H450C458.284 430 465 423.284 465 415V185C465 176.716 458.284 170 450 170ZM150 145C127.909 145 110 162.909 110 185V415C110 437.091 127.909 455 150 455H450C472.091 455 490 437.091 490 415V185C490 162.909 472.091 145 450 145H150Z"
|
|
49
|
-
fill="#A2A2A2"
|
|
50
|
-
/>
|
|
51
|
-
<path
|
|
52
|
-
d="M237.135 235.012C237.135 255.723 220.345 272.512 199.635 272.512C178.924 272.512 162.135 255.723 162.135 235.012C162.135 214.301 178.924 197.512 199.635 197.512C220.345 197.512 237.135 214.301 237.135 235.012Z"
|
|
53
|
-
fill="#A2A2A2"
|
|
54
|
-
/>
|
|
55
|
-
<path
|
|
56
|
-
d="M160 405V367.205L221.609 306.364L256.552 338.628L358.161 234L440 316.043V405H160Z"
|
|
57
|
-
fill="#A2A2A2"
|
|
58
|
-
/>
|
|
59
|
-
</svg>`)}`;
|