astro-lqip 1.7.2 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -8
- package/dist/astro-lqip.css +1 -0
- package/dist/index.d.mts +140 -0
- package/dist/index.mjs +18 -0
- package/package.json +49 -19
- package/src/components/Image.astro +0 -45
- package/src/components/Picture.astro +0 -41
- package/src/constants/index.ts +0 -1
- package/src/index.ts +0 -2
- package/src/styles/lqip.css +0 -37
- package/src/types/components-options.type.ts +0 -10
- package/src/types/image-path.type.ts +0 -4
- package/src/types/index.ts +0 -6
- package/src/types/lqip.type.ts +0 -1
- package/src/types/plaiceholder.type.ts +0 -3
- package/src/types/props.type.ts +0 -16
- package/src/types/svg-node.type.ts +0 -7
- package/src/utils/generateLqip.ts +0 -77
- package/src/utils/getLqip.ts +0 -189
- package/src/utils/getLqipStyle.ts +0 -17
- package/src/utils/renderSVGNode.ts +0 -25
- package/src/utils/resolveImagePath.ts +0 -128
- package/src/utils/useLqipImage.ts +0 -50
package/README.md
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
# 🖼️ astro-lqip
|
|
6
6
|
|
|
7
|
-
[](https://
|
|
7
|
+
[](https://npmx.dev/package/astro-lqip)
|
|
8
8
|
[](https://github.com/felixicaza/astro-lqip/blob/main/LICENSE)
|
|
9
9
|
|
|
10
|
-
Native extended Astro components for generating low
|
|
10
|
+
Native extended Astro components for generating low-quality image placeholders (LQIP), compatible with `<img>`, `<picture>` and CSS background images.
|
|
11
11
|
|
|
12
12
|
## ✨ Features
|
|
13
|
-
- 🖼️ Supports both `<Image>` and `<Picture>` components.
|
|
13
|
+
- 🖼️ Supports both `<Image>` and `<Picture>` components and CSS background images.
|
|
14
14
|
- 🎨 Multiple LQIP techniques: base64, solid color, CSS via gradients and SVG.
|
|
15
15
|
- 🚀 Easy to use, just replace the native Astro components with the ones from [astro-lqip](https://astro-lqip.web.app/).
|
|
16
16
|
- ⚡️ Support images as static imports or using string paths.
|
|
@@ -40,7 +40,7 @@ yarn add astro-lqip
|
|
|
40
40
|
|
|
41
41
|
## 🚀 Usage
|
|
42
42
|
|
|
43
|
-
In your current Astro project, just replace the import of the native Astro `<Image>` or `<Picture>` components with the ones provided by [astro-lqip](https://astro-lqip.web.app/).
|
|
43
|
+
In your current Astro project, just replace the import of the native Astro `<Image>` or `<Picture>` components with the ones provided by [astro-lqip](https://astro-lqip.web.app/) or import the `<Background>` component for CSS background images.
|
|
44
44
|
|
|
45
45
|
### Image
|
|
46
46
|
|
|
@@ -56,18 +56,38 @@ In your current Astro project, just replace the import of the native Astro `<Ima
|
|
|
56
56
|
+ import { Picture } from 'astro-lqip/components';
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
### Background
|
|
60
|
+
|
|
61
|
+
```diff
|
|
62
|
+
+ import { Background } from 'astro-lqip/components';
|
|
63
|
+
```
|
|
64
|
+
|
|
59
65
|
Example:
|
|
60
66
|
|
|
61
67
|
```astro
|
|
62
68
|
---
|
|
63
|
-
import { Image, Picture } from 'astro-lqip/components';
|
|
69
|
+
import { Image, Picture, Background } from 'astro-lqip/components';
|
|
64
70
|
|
|
65
71
|
import image from '/src/assets/images/image.png';
|
|
66
72
|
import otherImage from '/src/assets/images/other-image.png';
|
|
73
|
+
import backgroundImage from '/src/assets/images/background-image.png';
|
|
67
74
|
---
|
|
68
75
|
|
|
69
76
|
<Image src={image} alt="Cover Image" width={220} height={220} />
|
|
70
77
|
<Picture src={otherImage} alt="Other Image" width={220} height={220} />
|
|
78
|
+
<Background src={backgroundImage}>
|
|
79
|
+
<section>
|
|
80
|
+
<p>Optimized background</p>
|
|
81
|
+
</section>
|
|
82
|
+
</Background>
|
|
83
|
+
|
|
84
|
+
<style>
|
|
85
|
+
section {
|
|
86
|
+
background-image: var(--background);
|
|
87
|
+
background-size: cover;
|
|
88
|
+
background-position: center;
|
|
89
|
+
}
|
|
90
|
+
</style>
|
|
71
91
|
```
|
|
72
92
|
|
|
73
93
|
> [!TIP]
|
|
@@ -77,40 +97,81 @@ Example with absolute path:
|
|
|
77
97
|
|
|
78
98
|
```astro
|
|
79
99
|
---
|
|
80
|
-
import { Image, Picture } from 'astro-lqip/components';
|
|
100
|
+
import { Image, Picture, Background } from 'astro-lqip/components';
|
|
81
101
|
---
|
|
82
102
|
|
|
83
103
|
<Image src="/src/assets/images/image.png" alt="Cover Image" width={220} height={220} />
|
|
84
104
|
<Picture src="/src/assets/images/other-image.png" alt="Other Image" width={220} height={220} />
|
|
105
|
+
<Background src="/src/assets/images/background-image.png">
|
|
106
|
+
<section>
|
|
107
|
+
<p>Optimized background</p>
|
|
108
|
+
</section>
|
|
109
|
+
</Background>
|
|
110
|
+
|
|
111
|
+
<style>
|
|
112
|
+
section {
|
|
113
|
+
background-image: var(--background);
|
|
114
|
+
background-size: cover;
|
|
115
|
+
background-position: center;
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
85
118
|
```
|
|
86
119
|
|
|
87
120
|
Example with relative path:
|
|
88
121
|
|
|
89
122
|
```astro
|
|
90
123
|
---
|
|
91
|
-
import { Image, Picture } from 'astro-lqip/components';
|
|
124
|
+
import { Image, Picture, Background } from 'astro-lqip/components';
|
|
92
125
|
---
|
|
93
126
|
|
|
94
127
|
<!-- assuming you are on the path `/src/pages/index.astro` -->
|
|
95
128
|
<Image src="../assets/images/image.png" alt="Cover Image" width={220} height={220} />
|
|
96
129
|
<Picture src="../assets/images/other-image.png" alt="Other Image" width={220} height={220} />
|
|
130
|
+
<Background src="../assets/images/background-image.png">
|
|
131
|
+
<section>
|
|
132
|
+
<p>Optimized background</p>
|
|
133
|
+
</section>
|
|
134
|
+
</Background>
|
|
135
|
+
|
|
136
|
+
<style>
|
|
137
|
+
section {
|
|
138
|
+
background-image: var(--background);
|
|
139
|
+
background-size: cover;
|
|
140
|
+
background-position: center;
|
|
141
|
+
}
|
|
142
|
+
</style>
|
|
97
143
|
```
|
|
98
144
|
|
|
99
145
|
Example with alias:
|
|
100
146
|
|
|
101
147
|
```astro
|
|
102
148
|
---
|
|
103
|
-
import { Image, Picture } from 'astro-lqip/components';
|
|
149
|
+
import { Image, Picture, Background } from 'astro-lqip/components';
|
|
104
150
|
---
|
|
105
151
|
|
|
106
152
|
<Image src="@/assets/images/image.png" alt="Cover Image" width={220} height={220} />
|
|
107
153
|
<Picture src="@/assets/images/other-image.png" alt="Other Image" width={220} height={220} />
|
|
154
|
+
<Background src="@/assets/images/background-image.png">
|
|
155
|
+
<section>
|
|
156
|
+
<p>Optimized background</p>
|
|
157
|
+
</section>
|
|
158
|
+
</Background>
|
|
159
|
+
|
|
160
|
+
<style>
|
|
161
|
+
section {
|
|
162
|
+
background-image: var(--background);
|
|
163
|
+
background-size: cover;
|
|
164
|
+
background-position: center;
|
|
165
|
+
}
|
|
166
|
+
</style>
|
|
108
167
|
```
|
|
109
168
|
|
|
110
169
|
Learn how to configure path aliasing in the [Astro documentation](https://docs.astro.build/en/guides/typescript/#import-aliases). If you want more examples of uses you can see the [Usage Tips](https://astro-lqip.web.app/usage-tips/) page.
|
|
111
170
|
|
|
112
171
|
## ⚙️ Props
|
|
113
172
|
|
|
173
|
+
### Image and Picture
|
|
174
|
+
|
|
114
175
|
Both `<Image>` and `<Picture>` components support all the props of the [native Astro components](https://docs.astro.build/en/reference/modules/astro-assets/), but adds a couple of props for LQIP management:
|
|
115
176
|
|
|
116
177
|
- `lqip`: The LQIP type to use. It can be one of the following:
|
|
@@ -140,6 +201,102 @@ import otherImage from '/src/assets/images/other-image.png';
|
|
|
140
201
|
> [!TIP]
|
|
141
202
|
> For the `<Image>` component, a `parentAttributes` prop similar to `pictureAttributes` has been added.
|
|
142
203
|
|
|
204
|
+
### Background
|
|
205
|
+
|
|
206
|
+
The `<Background>` component supports the following props:
|
|
207
|
+
|
|
208
|
+
- `src` (required): The source of the background image located in `src` folder. It can be a static import, absolute path, relative path, alias path or remote URL.
|
|
209
|
+
- `lqip`: The LQIP type to use. It can be one of the following:
|
|
210
|
+
- `base64`: Generates a Base64-encoded LQIP image. (default option)
|
|
211
|
+
- `color`: Generates a solid color placeholder.
|
|
212
|
+
- `cssVariable`: A string that represents the name of the CSS variable to store the background data.
|
|
213
|
+
- By default, the background data is stored in a CSS variable named `--background`.
|
|
214
|
+
- For responsive backgrounds, the CSS variable names are generated based on the provided widths, following the pattern `--background-small` (lower than 768px), `--background-medium` (768px to 1199px), `--background-large` (1200px to 1919px) and `--background-xlarge` (1920px and above). `--background` is also generated for the largest image for backward compatibility.
|
|
215
|
+
- If the `cssVariable` prop is provided, the generated CSS variable names will follow the pattern `--{cssVariable}-small`, `--{cssVariable}-medium`, `--{cssVariable}-large` and `--{cssVariable}-xlarge`.
|
|
216
|
+
- `format`: The image format to use for the background. It can be one of the following in string or an array of strings. If an array is provided, this generates multiple background images with the native [`image-set()`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/image/image-set) CSS function, which allows the browser to choose the best format to use based on its support:
|
|
217
|
+
- `avif`
|
|
218
|
+
- `webp` (default option)
|
|
219
|
+
- `jpeg`
|
|
220
|
+
- `png`
|
|
221
|
+
- `widths`: An array of numbers that represents the widths to use for responsive background images.
|
|
222
|
+
- `width`: The width to use for the background image. This prop is ignored if the `widths` prop is provided.
|
|
223
|
+
- `height`: The height to use for the background image. This prop is ignored if the `widths` prop is provided.
|
|
224
|
+
- `quality`: The quality to use for the background image. It can be a number from `1` to `100`.
|
|
225
|
+
- `fit`: The fit to use for the background image. It can be one of the following:
|
|
226
|
+
- `cover`
|
|
227
|
+
- `contain`
|
|
228
|
+
- `fill`
|
|
229
|
+
- `inside`
|
|
230
|
+
- `outside`
|
|
231
|
+
|
|
232
|
+
Example:
|
|
233
|
+
|
|
234
|
+
```astro
|
|
235
|
+
---
|
|
236
|
+
import { Background } from 'astro-lqip/components';
|
|
237
|
+
import backgroundImage from '/src/assets/images/background-image.png';
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
<Background src={backgroundImage} lqip="color" cssVariable="--bg-lqip" format={["avif", "webp"]} width={500} height={300} quality={80} fit="cover">
|
|
241
|
+
<section>
|
|
242
|
+
<p>Optimized background</p>
|
|
243
|
+
</section>
|
|
244
|
+
</Background>
|
|
245
|
+
|
|
246
|
+
<style>
|
|
247
|
+
section {
|
|
248
|
+
background-image: var(--bg-lqip);
|
|
249
|
+
background-size: cover;
|
|
250
|
+
background-position: center;
|
|
251
|
+
}
|
|
252
|
+
</style>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Example with responsive background:
|
|
256
|
+
|
|
257
|
+
```astro
|
|
258
|
+
---
|
|
259
|
+
import { Background } from 'astro-lqip/components';
|
|
260
|
+
import backgroundImage from '/src/assets/images/background-image.png';
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
<Background src={backgroundImage} format="avif" widths={[475, 1000, 1536, 2100]}>
|
|
264
|
+
<section>
|
|
265
|
+
<p>Optimized background</p>
|
|
266
|
+
</section>
|
|
267
|
+
</Background>
|
|
268
|
+
|
|
269
|
+
<style>
|
|
270
|
+
section {
|
|
271
|
+
background-image: var(--background-small); /* 475px */
|
|
272
|
+
background-size: cover;
|
|
273
|
+
background-position: center;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
@media (width >= 768px) {
|
|
277
|
+
section {
|
|
278
|
+
background-image: var(--background-medium); /* 1000px */
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@media (width >= 1200px) {
|
|
283
|
+
section {
|
|
284
|
+
background-image: var(--background-large); /* 1536px */
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@media (width >= 1920px) {
|
|
289
|
+
section {
|
|
290
|
+
/* or var(--background), since it's the default variable for the largest image */
|
|
291
|
+
background-image: var(--background-xlarge); /* 2100px */
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
</style>
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
> [!NOTE]
|
|
298
|
+
> The `lqipSize` prop is not compatible with this component, to avoid large CSS outputs.
|
|
299
|
+
|
|
143
300
|
## 💡 Knowledge
|
|
144
301
|
|
|
145
302
|
Since this integration is built on top of Astro native `<Image>` and `<Picture>` components, you can refer to the [Astro documentation](https://docs.astro.build/en/guides/images/) for more information on how to use it.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer astro-lqip{[data-astro-lqip]{--opacity:1;--z-index:0;isolation:isolate;width:fit-content;height:fit-content;line-height:0;display:inline-block;position:relative;overflow:clip;&:after{content:"";pointer-events:none;opacity:var(--opacity);z-index:var(--z-index);background:var(--lqip-background);background-position:50%;background-size:cover;transition:opacity 1s;position:absolute;inset:0}& img{z-index:1;position:relative}@media (scripting:none){--opacity:0;--z-index:1}}[data-astro-lqip-bg]{display:contents}}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { LocalImageProps, Picture as Picture$1, RemoteImageProps } from "astro:assets";
|
|
2
|
+
import { GetPlaiceholderReturn } from "plaiceholder";
|
|
3
|
+
import { ImageMetadata } from "astro";
|
|
4
|
+
import { ComponentProps, HTMLAttributes } from "astro/types";
|
|
5
|
+
|
|
6
|
+
//#region src/types/lqip.type.d.ts
|
|
7
|
+
type LqipType = 'color' | 'css' | 'base64' | 'svg';
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/types/style.type.d.ts
|
|
10
|
+
type StylePrimitive = string | number | undefined;
|
|
11
|
+
type StyleMap<TExtra = never> = Record<string, StylePrimitive | TExtra>;
|
|
12
|
+
type StyleInput<TExtra = never> = StyleMap<TExtra> | string;
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/types/components-options.type.d.ts
|
|
15
|
+
type ComponentsOptions = {
|
|
16
|
+
src: string | object;
|
|
17
|
+
lqip: LqipType;
|
|
18
|
+
lqipSize: number;
|
|
19
|
+
styleProps: StyleMap;
|
|
20
|
+
forbiddenVars: string[];
|
|
21
|
+
isDevelopment: boolean | undefined;
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/types/image-path.type.d.ts
|
|
25
|
+
type ImagePath = string | {
|
|
26
|
+
src: string;
|
|
27
|
+
} | Promise<{
|
|
28
|
+
default: {
|
|
29
|
+
src: string;
|
|
30
|
+
};
|
|
31
|
+
}>;
|
|
32
|
+
type ResolvedImage = {
|
|
33
|
+
src: string;
|
|
34
|
+
width?: number;
|
|
35
|
+
height?: number;
|
|
36
|
+
[k: string]: unknown;
|
|
37
|
+
};
|
|
38
|
+
type ImportModule = Record<string, unknown> & {
|
|
39
|
+
default?: unknown;
|
|
40
|
+
};
|
|
41
|
+
type GlobMap = Record<string, () => Promise<ImportModule>>;
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/types/image-transform.type.d.ts
|
|
44
|
+
type ValidOutputFormats = ['avif', 'png', 'webp', 'jpeg', 'jpg', 'svg'];
|
|
45
|
+
type ImageQualityPreset = 'low' | 'mid' | 'high' | 'max' | (string & {});
|
|
46
|
+
type ImageQuality = ImageQualityPreset | number;
|
|
47
|
+
type ImageOutputFormat = ValidOutputFormats[number] | (string & {});
|
|
48
|
+
type ImageFit = 'fill' | 'contain' | 'cover' | 'none' | 'scale-down' | (string & {});
|
|
49
|
+
interface ImageTransform {
|
|
50
|
+
/**
|
|
51
|
+
* Path of the image that will be used as the background.
|
|
52
|
+
* It can be a relative path, an absolute path, an alias or ImageMetadata.
|
|
53
|
+
*/
|
|
54
|
+
src: string | ImageMetadata;
|
|
55
|
+
/**
|
|
56
|
+
* CSS custom property that will receive the generated background.
|
|
57
|
+
* (defaults to --background)
|
|
58
|
+
*/
|
|
59
|
+
cssVariable?: string;
|
|
60
|
+
/**
|
|
61
|
+
* LQIP placeholder strategy for Background (defaults to 'base64').
|
|
62
|
+
* Only 'base64' and 'color' are supported to keep CSS-friendly values.
|
|
63
|
+
*/
|
|
64
|
+
lqip?: 'base64' | 'color';
|
|
65
|
+
/**
|
|
66
|
+
* Specifies one or more output formats (first entry is preferred fallback).
|
|
67
|
+
* Accepts a single `ImageOutputFormat` or an ordered array.
|
|
68
|
+
* (defaults to webp)
|
|
69
|
+
*/
|
|
70
|
+
format?: ImageOutputFormat | ImageOutputFormat[] | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Explicit widths used for responsive variants.
|
|
73
|
+
*/
|
|
74
|
+
widths?: number[] | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Single width fallback when `widths` is omitted.
|
|
77
|
+
*/
|
|
78
|
+
width?: number | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Target height for generated assets.
|
|
81
|
+
*/
|
|
82
|
+
height?: number | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Compression quality preset or numeric percentage.
|
|
85
|
+
*/
|
|
86
|
+
quality?: ImageQuality | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Object-fit behavior applied during resizing.
|
|
89
|
+
*/
|
|
90
|
+
fit?: ImageFit | undefined;
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/types/plaiceholder.type.d.ts
|
|
94
|
+
type GetSVGReturn = GetPlaiceholderReturn['svg'];
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/types/props.type.d.ts
|
|
97
|
+
type Props = {
|
|
98
|
+
/**
|
|
99
|
+
* LQIP type.
|
|
100
|
+
* This can be 'color', 'css', 'svg' or 'base64'.
|
|
101
|
+
* The default value is 'base64'.
|
|
102
|
+
*/
|
|
103
|
+
lqip?: LqipType;
|
|
104
|
+
/**
|
|
105
|
+
* Size of the LQIP image in pixels.
|
|
106
|
+
* This value should be between 4 and 64.
|
|
107
|
+
* The default value is 4.
|
|
108
|
+
*/
|
|
109
|
+
lqipSize?: number;
|
|
110
|
+
};
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/types/svg-node.type.d.ts
|
|
113
|
+
type StyleAttrs = StyleInput<GetSVGReturn>;
|
|
114
|
+
type SVGNodeAttrs = {
|
|
115
|
+
style?: StyleAttrs;
|
|
116
|
+
} & Record<string, string | number>;
|
|
117
|
+
type SVGNode = [string, SVGNodeAttrs, SVGNode[]];
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/components/Picture.d.ts
|
|
120
|
+
type AstroPictureProps = ComponentProps<typeof Picture$1>;
|
|
121
|
+
type Props$3 = AstroPictureProps & Props;
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/components/Image.d.ts
|
|
124
|
+
type Props$2 = (LocalImageProps | RemoteImageProps) & Props & {
|
|
125
|
+
parentAttributes?: HTMLAttributes<'div'>;
|
|
126
|
+
};
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/components/Background.d.ts
|
|
129
|
+
type Props$1 = ImageTransform;
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/index.d.ts
|
|
132
|
+
type AstroTypedComponent<Props> = ((props: Props) => unknown) & {
|
|
133
|
+
isAstroComponentFactory?: boolean;
|
|
134
|
+
moduleId?: string;
|
|
135
|
+
};
|
|
136
|
+
declare const Image: AstroTypedComponent<Props$2>;
|
|
137
|
+
declare const Picture: AstroTypedComponent<Props$3>;
|
|
138
|
+
declare const Background: AstroTypedComponent<Props$1>;
|
|
139
|
+
//#endregion
|
|
140
|
+
export { Background, ComponentsOptions, GetSVGReturn, GlobMap, Image, ImagePath, ImageTransform, ImportModule, LqipType, Picture, Props, ResolvedImage, SVGNode, StyleAttrs, StyleInput, StyleMap };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import './astro-lqip.css';
|
|
2
|
+
import{createComponent as e,renderComponent as t,renderSlotToString as n,renderTemplate as r,spreadAttributes as i}from"astro/runtime/server/index.js";import{Image as a,Picture as o,getImage as s,inferRemoteSize as c}from"astro:assets";import{existsSync as l,statSync as u}from"node:fs";import{mkdir as d,readFile as f,readdir as p,unlink as m,writeFile as h}from"node:fs/promises";import{basename as g,dirname as _,extname as v,join as y,relative as b,resolve as x,sep as S}from"node:path";import{fileURLToPath as C}from"node:url";import{getPlaiceholder as w}from"plaiceholder";const T=`[astro-lqip]`,E=process.cwd(),D=y(E,`src`),O=y(E,`public`),k=y(E,`dist`),ee=import.meta.env?.MODE===`development`,te=/(file:\/\/[^\s)]+|\/[^\s)]+|[A-Za-z]:[^\s)]+):\d+:\d+/,ne=[`${S}node_modules${S}`,`${S}dist${S}`,`${S}.astro${S}`,`${S}.prerender${S}`],re=new Set([`node_modules`,`dist`,`.astro`]),ie=new Set([`.astro`,`.vite`]),A=Object.assign({}),j=new Map,M=new Map;function N(e){if(!e)return;let t=e.toLowerCase();(t.includes(`/public/`)||e.startsWith(O))&&console.warn(`${T} Warning: image resolved from /public. Images should not be placed in /public - move them to /src so Astro can process them correctly.`),(t.endsWith(`.webp`)||t.endsWith(`.avif`))&&console.warn(`${T} Warning: image is in ${t.endsWith(`.webp`)?`webp`:`avif`} format. These formats are usually already optimized; using this component to re-process them may degrade quality.`)}function P(e){return typeof e==`object`&&!!e}function ae(e){return P(e)&&typeof e.then==`function`}function F(e){return P(e)&&typeof e.src==`string`}function oe(e){return/^https?:\/\//.test(e)}function se(e){let t=e.indexOf(`?`),n=e.indexOf(`#`),r=e.length;return t!==-1&&(r=Math.min(r,t)),n!==-1&&(r=Math.min(r,n)),e.slice(0,r)}function I(e){let t=se(e.trim()).replace(/\\/g,`/`);return t.startsWith(`@/`)?`src/${t.slice(2)}`:t.startsWith(`~@/`)?`src/${t.slice(3)}`:t}function L(e){return e.startsWith(`./`)||e.startsWith(`../`)}function R(e){let t=e;for(;t.startsWith(`./`)||t.startsWith(`../`);)t=t.startsWith(`./`)?t.slice(2):t.slice(3);return t}function ce(e){let t=e.match(te);if(!t?.[1])return null;let n=t[1];if(!n.startsWith(`file://`))return n;try{return C(n)}catch{return n.replace(/^file:\/\//,``)}}function le(e){return!e||!e.startsWith(`/`)&&!/^[A-Za-z]:/.test(e)||!e.startsWith(E)?!1:!ne.some(t=>e.includes(t))}function ue(){let e=Error().stack;if(!e)return null;for(let t of e.split(`
|
|
3
|
+
`).slice(2)){let e=ce(t);if(e&&le(e))return _(e)}return null}function z(e){let t=b(E,e);return!e||t.startsWith(`..`)||t.includes(`..${S}`)||t.includes(`node_modules`)?null:e}function de(e){let t=b(E,e).replace(/\\/g,`/`);return!t||t.startsWith(`../`)?null:`/${t}`}function fe(e,t){let n=I(e),r=new Set;if(n.startsWith(`/src/`)?r.add(n):n.startsWith(`src/`)&&r.add(`/${n}`),t&&L(n)){let e=de(x(t,n));e?.startsWith(`/src/`)&&r.add(e)}if(L(n)){let e=R(n);e&&r.add(e.startsWith(`src/`)?`/${e}`:`/src/${e}`);let t=`/${e}`;for(let e of Object.keys(A))e.endsWith(t)&&r.add(e)}return!n.startsWith(`/`)&&!n.startsWith(`src/`)&&!L(n)&&r.add(`/src/${n}`),Array.from(r)}function pe(e,t){let n=I(e),r=new Set,i=n.startsWith(`/`)?n.slice(1):n;if(t&&L(n)){let e=z(x(t,n));e&&r.add(e)}if(L(n)){let e=R(n);if(e){let t=z(y(D,e));t&&r.add(t);let n=z(y(O,e));n&&r.add(n)}}if(n.startsWith(`/src/`)?r.add(y(E,n.slice(1))):n.startsWith(`src/`)&&r.add(y(E,n)),n.startsWith(`/public/`)?r.add(y(E,n.slice(1))):n.startsWith(`public/`)&&r.add(y(E,n)),!n.startsWith(`/`)){let e=z(y(D,n));e&&r.add(e);let t=z(y(E,n));t&&r.add(t)}else if(i){let e=z(y(E,i));e&&r.add(e)}return Array.from(r)}async function B(e){if(!e)return null;let t=`${D}::${e}`;if(j.has(t))return j.get(t)??null;async function n(t){let r;try{r=await p(t,{withFileTypes:!0})}catch{return}for(let i of r){let r=y(t,i.name);if(i.isDirectory()){if(re.has(i.name))continue;let e=await n(r);if(e)return e}else if(i.name===e)return r}}let r=await n(D);return j.set(t,r??null),r??null}function me(e,t){if(e!=null)return String(e).toLowerCase();let n=v(t).replace(`.`,``);return n?n.toLowerCase():void 0}function he(e,t,n,r){return`/@fs${e.replace(/\\/g,`/`)}?origWidth=${t}&origHeight=${n}&origFormat=${r}`}function ge(e,t){let n=g(e);if(t===n)return!0;let r=v(n),i=r?n.slice(0,-r.length):n;return t.startsWith(`${i}.`)&&t.endsWith(r)}function _e(e){let t=e.replace(/\\/g,`/`),n=t.lastIndexOf(`/_astro/`);if(n!==-1)return t.slice(n);let r=b(k,e).replace(/\\/g,`/`),i=r.indexOf(`.prerender/`);return i===-1?`/${r}`:`/${r.slice(i+11)}`}async function ve(e){if(M.has(e))return M.get(e)??null;if(!l(k))return M.set(e,null),null;async function t(n){let r;try{r=await p(n,{withFileTypes:!0})}catch{return}for(let i of r){let r=y(n,i.name);if(i.isDirectory()){if(ie.has(i.name))continue;let e=await t(r);if(e)return e}else if(ge(e,i.name))return r}}let n=await t(k);if(!n)return M.set(e,null),null;let r=_e(n);return M.set(e,r),r}async function V(e){try{let{metadata:t}=await w(await f(e),{size:4}),n=t?.width??0,r=t?.height??0,i=me(t?.format,e);if(!n||!r||!i)return console.warn(`${T} Missing metadata for "${e}".`),null;let a=ee?he(e,n,r,i):await ve(e);if(!a)return null;let o={src:a,width:n,height:r,format:i};return Object.defineProperty(o,`fsPath`,{value:e,enumerable:!1,configurable:!1,writable:!1}),o}catch(t){return console.warn(`${T} Failed to derive metadata for "${e}".`,t),null}}async function ye(e,t){let n=fe(e,t);for(let e of n){let t=A[e];if(!t)continue;let n=await t(),r=n.default??n;if(F(r))return N(r.src),r}return null}async function be(e,t){let n=pe(e,t);for(let e of n){if(!e||!l(e))continue;let t=await V(e);if(t)return N(e),t}let r=I(e).split(`/`).pop();if(!r)return null;let i=await B(r);if(!i||!l(i))return null;let a=await V(i);return a?(N(i),a):null}async function H(e){if(e==null)return null;if(ae(e)){let t=await e,n=t.default??t;return F(n)?(N(n.src),n):typeof n==`string`?(N(n),n):null}if(P(e)){let t=e;return N(typeof t.src==`string`?t.src:void 0),F(t)?t:null}if(typeof e==`string`){if(oe(e))return e;let t=I(e),n=L(t)?ue():null;return await ye(t,n)||await be(t,n)||null}return null}const xe=/[A-Z]/g;function Se(e){return e.replace(xe,e=>`-${e.toLowerCase()}`)}function U(e){if(e==null)return;if(typeof e==`string`)return e;let t=[];for(let[n,r]of Object.entries(e))r!=null&&t.push(`${Se(n)}:${r}`);return t.length>0?t.join(`;`):void 0}function W([e,t,n=[]]){let r=``;for(let[e,n]of Object.entries(t||{}))e===`style`&&n&&typeof n==`object`?r+=` style="${U(n)}"`:n!==void 0&&(r+=` ${e}="${n}"`);return n.length>0?`<${e}${r}>${n.map(W).join(``)}</${e}>`:`<${e}${r} />`}function Ce(e,t,n=``){if(!t)return{};switch(e){case`css`:return{"--lqip-background":t};case`svg`:return{"--lqip-background":`url('data:image/svg+xml;utf8,${encodeURIComponent(n)}')`};case`color`:return{"--lqip-background":t};default:return{"--lqip-background":`url('${t}')`}}}function we(e){return process.platform===`win32`&&/^\/[A-Za-z]:\//.test(e)?e.slice(1):e}function Te(){return typeof process<`u`&&!!process.versions?.node}async function Ee(e){if(Te())try{return await f(e)}catch{return}}function De(e,t=4){let n=Number(e);return Number.isFinite(n)?Math.min(64,Math.max(4,Math.round(n))):t}async function G(e,t,n,r){try{let i=we(e),a=De(n),o=await Ee(i);if(!o){console.warn(`${T} image not found for:`,e);return}let s=await w(o,{size:a}),c;switch(t){case`color`:c=s.color?.hex;break;case`css`:c=typeof s.css==`object`&&s.css.backgroundImage?s.css.backgroundImage:String(s.css);break;case`svg`:c=s.svg;break;default:c=s.base64;break}return r?console.log(`${T} LQIP (${t}) successfully generated!`):console.log(`${T} LQIP (${t}) successfully generated for:`,e),c}catch(n){console.error(`${T} Error generating LQIP (${t}) in:`,e,`
|
|
4
|
+
`,n);return}}function Oe(e){return/^https?:\/\//.test(e)}const K=y(process.cwd(),`node_modules`,`.cache`,`astro-lqip`),ke=[`jpg`,`jpeg`,`png`,`webp`,`avif`],Ae=[`src`],je=/\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9]+$/,Me=/^(.+?)\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9]+$/,q=(()=>{try{let e=import.meta.env?.BASE_URL??`/`;return!e||e===`/`?`/`:e.endsWith(`/`)?e.slice(0,-1):e}catch{return`/`}})(),J=new Map;function Ne(e){if(typeof e!=`string`)return e;let t=e.indexOf(`?`),n=t>=0?e.slice(0,t):e;return q!==`/`&&n.startsWith(q)&&(n=n.slice(q.length)||`/`),n.startsWith(`/`)||(n=`/${n}`),n}async function Pe(){l(K)||await d(K,{recursive:!0})}function Fe(e){let t=e.split(`/`).pop()||``,n=t.match(Me);if(n)return n[1];let r=t.split(`.`);return r.length>=3?r.slice(0,r.length-2).join(`.`):r[0]}async function Ie(e){if(!e)return;if(J.has(e))return J.get(e)||void 0;let t=new Set([`node_modules`,`dist`,`.astro`]);async function n(r){let i;try{i=await p(r)}catch{return}for(let a of i){let i=y(r,a),o;try{o=u(i)}catch{continue}if(o.isDirectory()){if(t.has(a))continue;let e=await n(i);if(e)return e}else if(ke.some(t=>a===`${e}.${t}`))return i}}for(let t of Ae){let r=y(process.cwd(),t);if(l(r)){let t=await n(r);if(t)return J.set(e,t),t}}J.set(e,null)}async function Y(e,t,n,r){if(e?.src){if(Oe(e.src)){await Pe();let i=await fetch(e.src);if(!i.ok)return;let a=await i.arrayBuffer(),o=Buffer.from(a),s=y(K,`astro-lqip-${Math.random().toString(36).slice(2)}.jpg`);await h(s,o);try{return await G(s,t,n,r)}finally{await m(s)}}if(r&&e.src.startsWith(`/@fs/`)){let i=e.src.replace(/^\/@fs/,``).split(`?`)[0];return await G(i,t,n,r)}if(!r){let i=e.src,a=Ne(i),o=a.replace(/^\//,``);if(o){let e=[y(process.cwd(),`dist`,`client`,o),y(process.cwd(),`dist`,o)];for(let i of e)if(l(i))return await G(i,t,n,r)}let s=a.split(`/`).pop()??``;if(je.test(s)){let e=Fe(a),i=await Ie(e);if(i)return console.log(`${T} fallback recursive source found:`,i),await G(i,t,n,r);console.warn(`${T} original source not found recursively for basename:`,e)}}}}async function X({src:e,lqip:t=`base64`,lqipSize:n=4,styleProps:r={},forbiddenVars:i=[`--lqip-background`,`--z-index`,`--opacity`],isDevelopment:a}){let o=await H(e)??null,s;o&&(s=await Y(typeof o==`string`?{src:o}:o,t,n,a));let c=``;t===`svg`&&Array.isArray(s)&&(c=W(s));let l=Ce(t,s,c);for(let e of Object.keys(r))i.includes(e)&&console.warn(`${T} The CSS variable “${e}” should not be passed in style because it can override the functionality of LQIP.`);let u={...r,...l};return{lqipImage:s,svgHTML:c,lqipStyle:l,combinedStyle:u,resolvedSrc:o}}const Le=e({factory:async(e,n)=>{let{class:r,lqip:i=`base64`,lqipSize:a=4,pictureAttributes:s={},...c}=n,{combinedStyle:l,resolvedSrc:u}=await X({src:c.src,lqip:i,lqipSize:a,styleProps:s.style??{},forbiddenVars:[],isDevelopment:import.meta.env.MODE===`development`});return await t(e,`Picture`,o,{...c,class:r,src:u??c.src,pictureAttributes:{"data-astro-lqip":``,...s,style:l},onload:`
|
|
5
|
+
parentElement.style.setProperty("--z-index", 1);
|
|
6
|
+
parentElement.style.setProperty("--opacity", 0);
|
|
7
|
+
`})}}),Re=e({factory:async(e,n)=>{let{class:o,lqip:s=`base64`,lqipSize:c=4,parentAttributes:l={},...u}=n,{combinedStyle:d,resolvedSrc:f}=await X({src:u.src,lqip:s,lqipSize:c,styleProps:l.style??{},forbiddenVars:[],isDevelopment:import.meta.env.MODE===`development`});return r`
|
|
8
|
+
<div ${i({...l,class:[l.class,o].filter(Boolean).join(` `)||void 0,"data-astro-lqip":``,style:U(d)})}>
|
|
9
|
+
${t(e,`Image`,a,{...u,class:o,src:f??u.src,onload:`
|
|
10
|
+
parentElement.style.setProperty("--z-index", 1);
|
|
11
|
+
parentElement.style.setProperty("--opacity", 0);
|
|
12
|
+
`})}
|
|
13
|
+
</div>
|
|
14
|
+
`}});async function ze({src:e,cssVariable:t=`--background`,format:n=`webp`,widths:r,width:i,height:a,quality:o,fit:l,lqip:u=`base64`,isDevelopment:d}){let f=i,p=a,m=await H(e);if(!m)throw Error(`${T} Could not resolve background image in "${e}"`);let h=typeof m==`string`&&Ue(m)?m:null;if(h&&(!f||!p)){try{let{width:e,height:t}=await c(h);f??=e,p??=t}catch(e){console.warn(`${T} Failed to infer remote background size for "${h}".`,e)}if(!f||!p)throw Error(`${T} Remote background images require width and height. Provide both props or ensure the URL is reachable in your Astro config 'image.domains'.`)}let g=m,_,v=typeof m==`string`?{src:m}:m;if(v){let e=await Y(v,u,12,d);_=qe(u,typeof e==`string`?e:void 0)}let y=He((Array.isArray(n)?n:[n]).filter(e=>typeof e==`string`&&e.trim().length>0)),b=y.length?y:[`webp`],x=Array.isArray(n),S={src:g,widths:r,width:f,height:p,quality:o,fit:l},C=await Promise.all(b.map(e=>s({...S,format:e}))),w=C.map((e,t)=>({format:b[t],mimeType:Z(b[t]),fallbackSrc:e.src,sources:We(e.srcSet?.attribute)})),E=Be(t),D=w[0]?.sources??[],O=Array.isArray(r)&&r.length>0,k=e=>Ke({formatSources:w,optimizedImages:C,isFormatArray:x,selector:e,layer:_});return{style:O?Ge({referenceSources:D,baseVariable:E,createValue:k}):`${E}: ${k()}`,resolvedSrc:m}}function Be(e){let t=e.trim();return t?t.startsWith(`--`)?t:`--${t}`:`--background`}function Ve(e){return e.trim().toLowerCase()}function He(e){let t=[`avif`,`webp`,`png`,`jpeg`,`jpg`,`svg`],n=new Set;return e.map(e=>Ve(e)).filter(e=>!e||n.has(e)?!1:(n.add(e),!0)).sort((e,n)=>{let r=t.indexOf(e),i=t.indexOf(n),a=r===-1?2**53-1:r,o=i===-1?2**53-1:i;return a===o?e.localeCompare(n):a-o})}function Ue(e){return typeof e==`string`&&/^https?:\/\//.test(e)}function Z(e){switch(e){case`avif`:return`image/avif`;case`webp`:return`image/webp`;case`png`:return`image/png`;case`jpeg`:case`jpg`:return`image/jpeg`;case`svg`:return`image/svg+xml`;default:return`image/${e}`}}function We(e){if(!e)return[];let t=e.split(`,`).map(e=>e.trim()).filter(Boolean).map(e=>{let t=e.lastIndexOf(` `);if(t===-1)return{url:e};let n=e.slice(0,t),r=e.slice(t+1).match(/(?<value>\d+(?:\.\d+)?)(?<unit>[wx])/i);return{url:n,width:r?.groups?.unit?.toLowerCase()===`w`?Number.parseFloat(r.groups.value):void 0}}),n=new Map(t.map(e=>[e.url,e]));return Array.from(n.values()).sort((e,t)=>(e.width??0)-(t.width??0))}function Ge({referenceSources:e,baseVariable:t,createValue:n}){if(!e.length)return`${t}: ${n()}`;let r=[`${t}: ${n(e=>e.sources[e.sources.length-1])}`],i=[{suffix:`-small`,match:e=>typeof e==`number`&&e<768},{suffix:`-medium`,match:e=>typeof e==`number`&&e>=768&&e<=1200},{suffix:`-large`,match:e=>typeof e==`number`&&e>1200&&e<=1920},{suffix:`-xlarge`,match:e=>typeof e==`number`&&e>1920}];for(let a of i){let i=[...e].reverse().find(e=>a.match(e.width));if(!i||typeof i.width!=`number`)continue;let o=n(e=>e.sources.find(e=>e.width===i.width));r.push(`${t}${a.suffix}: ${o}`)}return r.join(`; `)}function Q(e,t){return t?.(e)??e.sources[e.sources.length-1]??{url:e.fallbackSrc}}function $(e,t){return e&&t?`${e}, ${t}`:e||t||``}function Ke({formatSources:e,optimizedImages:t,isFormatArray:n,selector:r,layer:i}){if(!e.length){let e=t[0]?.src??``;return $(e?`url('${e}')`:``,i)}if(!n){let t=e[0];return $(`url('${Q(t,r).url??t.fallbackSrc}')`,i)}return $(`image-set(${e.map(e=>`url('${Q(e,r).url??e.fallbackSrc}') type('${e.mimeType}')`).join(`, `)})`,i)}function qe(e,t){if(!(!e||!t))return e===`color`?`linear-gradient(${t}, ${t})`:`url("${t}")`}const Je=e({factory:async(e,t,a)=>{let o=import.meta.env.MODE===`development`,{style:s}=await ze({...t,isDevelopment:o}),c=await n(e,a.default);return r`
|
|
15
|
+
<div data-astro-lqip-bg ${i({style:s})}>
|
|
16
|
+
${c}
|
|
17
|
+
</div>
|
|
18
|
+
`}}),Ye=Re,Xe=Le,Ze=Je;export{Ze as Background,Ye as Image,Xe as Picture};
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-lqip",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Native extended Astro components for generating low
|
|
3
|
+
"version": "1.8.1",
|
|
4
|
+
"description": "🖼️ Native extended Astro components for generating low-quality image placeholders (LQIP), compatible with <img>, <picture> and CSS background images.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
7
7
|
"astro-component",
|
|
8
8
|
"astro-integration",
|
|
9
9
|
"image",
|
|
10
10
|
"picture",
|
|
11
|
+
"css",
|
|
12
|
+
"background",
|
|
11
13
|
"lqip"
|
|
12
14
|
],
|
|
13
15
|
"homepage": "https://astro-lqip.web.app",
|
|
@@ -21,12 +23,21 @@
|
|
|
21
23
|
"license": "MIT",
|
|
22
24
|
"author": "Felix Icaza",
|
|
23
25
|
"type": "module",
|
|
26
|
+
"types": "./dist/index.d.mts",
|
|
24
27
|
"exports": {
|
|
25
|
-
"
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.mts",
|
|
30
|
+
"import": "./dist/index.mjs"
|
|
31
|
+
},
|
|
32
|
+
"./components": {
|
|
33
|
+
"types": "./dist/index.d.mts",
|
|
34
|
+
"import": "./dist/index.mjs"
|
|
35
|
+
}
|
|
26
36
|
},
|
|
27
37
|
"files": [
|
|
28
|
-
"
|
|
38
|
+
"dist"
|
|
29
39
|
],
|
|
40
|
+
"sideEffects": false,
|
|
30
41
|
"simple-git-hooks": {
|
|
31
42
|
"pre-commit": "pnpm nano-staged"
|
|
32
43
|
},
|
|
@@ -37,34 +48,53 @@
|
|
|
37
48
|
"plaiceholder": "3.0.0"
|
|
38
49
|
},
|
|
39
50
|
"devDependencies": {
|
|
40
|
-
"@eslint/js": "9.39.
|
|
41
|
-
"@
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
51
|
+
"@eslint/js": "9.39.4",
|
|
52
|
+
"@tsdown/css": "0.21.7",
|
|
53
|
+
"@types/node": "25.5.0",
|
|
54
|
+
"@typescript-eslint/parser": "8.57.0",
|
|
55
|
+
"astro": "6.1.2",
|
|
56
|
+
"bumpp": "11.0.1",
|
|
57
|
+
"concurrently": "9.2.1",
|
|
58
|
+
"eslint": "9.39.4",
|
|
59
|
+
"eslint-plugin-astro": "1.6.0",
|
|
60
|
+
"eslint-plugin-jsonc": "3.1.2",
|
|
47
61
|
"eslint-plugin-jsx-a11y": "6.10.2",
|
|
48
|
-
"eslint-plugin-package-json": "0.
|
|
49
|
-
"eslint-plugin-yml": "
|
|
50
|
-
"globals": "
|
|
62
|
+
"eslint-plugin-package-json": "0.90.1",
|
|
63
|
+
"eslint-plugin-yml": "3.3.1",
|
|
64
|
+
"globals": "17.4.0",
|
|
51
65
|
"jiti": "2.6.1",
|
|
66
|
+
"lightningcss": "1.32.0",
|
|
52
67
|
"nano-staged": "0.9.0",
|
|
53
|
-
"neostandard": "0.
|
|
54
|
-
"simple-git-hooks": "2.13.1"
|
|
68
|
+
"neostandard": "0.13.0",
|
|
69
|
+
"simple-git-hooks": "2.13.1",
|
|
70
|
+
"tsdown": "0.21.7"
|
|
55
71
|
},
|
|
56
72
|
"peerDependencies": {
|
|
57
73
|
"astro": ">=3.3.0"
|
|
58
74
|
},
|
|
59
75
|
"scripts": {
|
|
76
|
+
"build": "tsdown",
|
|
60
77
|
"build:docs": "pnpm --filter ./docs -r build",
|
|
61
|
-
"
|
|
78
|
+
"build:watch": "tsdown --watch",
|
|
79
|
+
"dev": "concurrently \"pnpm dev:lib\" \"pnpm dev:fixtures\"",
|
|
62
80
|
"dev:docs": "pnpm --filter ./docs -r dev",
|
|
81
|
+
"dev:fixtures": "pnpm --filter \"./tests/fixtures/**\" -r --parallel dev",
|
|
82
|
+
"dev:lib": "tsdown --watch",
|
|
63
83
|
"lint": "eslint --cache .",
|
|
64
84
|
"lint:fix": "eslint --cache --fix .",
|
|
65
|
-
"preview": "pnpm -r preview",
|
|
66
85
|
"release": "bumpp",
|
|
86
|
+
"sync:types": " pnpm --filter \"./tests/fixtures/**\" astro sync",
|
|
87
|
+
"test": "pnpm test:fixtures",
|
|
67
88
|
"test:fixtures": "pnpm --filter \"./tests/fixtures/**\" -r build",
|
|
68
|
-
"test:fixtures:
|
|
89
|
+
"test:fixtures:custom-base": "pnpm --filter \"./tests/fixtures/custom-base\" build",
|
|
90
|
+
"test:fixtures:default": "pnpm --filter \"./tests/fixtures/default\" build",
|
|
91
|
+
"test:fixtures:preview": "pnpm --filter \"./tests/fixtures/**\" -r preview",
|
|
92
|
+
"test:fixtures:preview:custom-base": "pnpm --filter \"./tests/fixtures/custom-base\" preview",
|
|
93
|
+
"test:fixtures:preview:default": "pnpm --filter \"./tests/fixtures/default\" preview",
|
|
94
|
+
"test:fixtures:preview:ssr": "pnpm --filter \"./tests/fixtures/ssr-node\" preview",
|
|
95
|
+
"test:fixtures:preview:ssr:custom-base": "pnpm --filter \"./tests/fixtures/ssr-node-custom-base\" preview",
|
|
96
|
+
"test:fixtures:ssr": "pnpm --filter \"./tests/fixtures/ssr-node\" build",
|
|
97
|
+
"test:fixtures:ssr:custom-base": "pnpm --filter \"./tests/fixtures/ssr-node-custom-base\" build",
|
|
98
|
+
"typecheck": "tsc --noEmit"
|
|
69
99
|
}
|
|
70
100
|
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import type { LocalImageProps, RemoteImageProps } from 'astro:assets'
|
|
3
|
-
import type { HTMLAttributes } from 'astro/types'
|
|
4
|
-
import type { Props as LqipProps } from '../types'
|
|
5
|
-
|
|
6
|
-
import { useLqipImage } from '../utils/useLqipImage'
|
|
7
|
-
|
|
8
|
-
import { Image as ImageComponent } from 'astro:assets'
|
|
9
|
-
|
|
10
|
-
import '../styles/lqip.css'
|
|
11
|
-
|
|
12
|
-
type Props = (LocalImageProps | RemoteImageProps) & LqipProps & {
|
|
13
|
-
parentAttributes?: HTMLAttributes<'div'>
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const { class: className, lqip = 'base64', lqipSize = 4, parentAttributes = {}, ...props } = Astro.props as Props
|
|
17
|
-
|
|
18
|
-
const isDevelopment = import.meta.env.MODE === 'development'
|
|
19
|
-
|
|
20
|
-
const { combinedStyle, resolvedSrc } = await useLqipImage({
|
|
21
|
-
src: props.src,
|
|
22
|
-
lqip,
|
|
23
|
-
lqipSize,
|
|
24
|
-
styleProps: (parentAttributes.style ?? {}) as Record<string, string | number | undefined>,
|
|
25
|
-
forbiddenVars: [],
|
|
26
|
-
isDevelopment
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
const componentProps = {
|
|
30
|
-
...props,
|
|
31
|
-
src: resolvedSrc ?? props.src
|
|
32
|
-
}
|
|
33
|
-
const combinedParentAttributes = {
|
|
34
|
-
...parentAttributes,
|
|
35
|
-
style: combinedStyle
|
|
36
|
-
}
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
<div class={className} data-astro-lqip {...combinedParentAttributes}>
|
|
40
|
-
<ImageComponent
|
|
41
|
-
{...componentProps as LocalImageProps | RemoteImageProps}
|
|
42
|
-
class={className}
|
|
43
|
-
onload="parentElement.style.setProperty('--z-index', 1), parentElement.style.setProperty('--opacity', 0)"
|
|
44
|
-
/>
|
|
45
|
-
</div>
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import type { Props as AstroPictureProps } from 'astro/components/Picture.astro'
|
|
3
|
-
import type { Props as LqipProps } from '../types'
|
|
4
|
-
|
|
5
|
-
import { useLqipImage } from '../utils/useLqipImage'
|
|
6
|
-
|
|
7
|
-
import { Picture as PictureComponent } from 'astro:assets'
|
|
8
|
-
|
|
9
|
-
import '../styles/lqip.css'
|
|
10
|
-
|
|
11
|
-
type Props = AstroPictureProps & LqipProps
|
|
12
|
-
|
|
13
|
-
const { class: className, lqip = 'base64', lqipSize = 4, pictureAttributes = {}, ...props } = Astro.props as Props
|
|
14
|
-
|
|
15
|
-
const isDevelopment = import.meta.env.MODE === 'development'
|
|
16
|
-
|
|
17
|
-
const { combinedStyle, resolvedSrc } = await useLqipImage({
|
|
18
|
-
src: props.src,
|
|
19
|
-
lqip,
|
|
20
|
-
lqipSize,
|
|
21
|
-
styleProps: (pictureAttributes.style ?? {}) as Record<string, string | number | undefined>,
|
|
22
|
-
forbiddenVars: [],
|
|
23
|
-
isDevelopment
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
const componentProps = {
|
|
27
|
-
...props,
|
|
28
|
-
src: resolvedSrc ?? props.src
|
|
29
|
-
}
|
|
30
|
-
const combinedPictureAttributes = {
|
|
31
|
-
...pictureAttributes,
|
|
32
|
-
style: combinedStyle
|
|
33
|
-
}
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
<PictureComponent
|
|
37
|
-
{...(componentProps as AstroPictureProps)}
|
|
38
|
-
class={className}
|
|
39
|
-
pictureAttributes={{ 'data-astro-lqip': '', ...combinedPictureAttributes }}
|
|
40
|
-
onload="parentElement.style.setProperty('--z-index', 1), parentElement.style.setProperty('--opacity', 0)"
|
|
41
|
-
/>
|
package/src/constants/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const PREFIX = '[astro-lqip]'
|
package/src/index.ts
DELETED
package/src/styles/lqip.css
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
[data-astro-lqip] {
|
|
2
|
-
--opacity: 1;
|
|
3
|
-
--z-index: 0;
|
|
4
|
-
|
|
5
|
-
position: relative;
|
|
6
|
-
display: inline-block;
|
|
7
|
-
width: fit-content;
|
|
8
|
-
height: fit-content;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
[data-astro-lqip]::after {
|
|
12
|
-
content: "";
|
|
13
|
-
inset: 0;
|
|
14
|
-
width: 100%;
|
|
15
|
-
height: 100%;
|
|
16
|
-
position: absolute;
|
|
17
|
-
pointer-events: none;
|
|
18
|
-
transition: opacity 1s;
|
|
19
|
-
opacity: var(--opacity);
|
|
20
|
-
z-index: var(--z-index);
|
|
21
|
-
background: var(--lqip-background);
|
|
22
|
-
background-size: cover;
|
|
23
|
-
background-position: 50% 50%;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
[data-astro-lqip] img {
|
|
27
|
-
z-index: 1;
|
|
28
|
-
position: relative;
|
|
29
|
-
overflow: hidden;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
@media (scripting: none) {
|
|
33
|
-
[data-astro-lqip] {
|
|
34
|
-
--opacity: 0;
|
|
35
|
-
--z-index: 1;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { LqipType } from './lqip.type'
|
|
2
|
-
|
|
3
|
-
export type ComponentsOptions = {
|
|
4
|
-
src: string | object
|
|
5
|
-
lqip: LqipType
|
|
6
|
-
lqipSize: number
|
|
7
|
-
styleProps: Record<string, string | number | undefined>
|
|
8
|
-
forbiddenVars: string[]
|
|
9
|
-
isDevelopment: boolean | undefined
|
|
10
|
-
}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export type ImagePath = string | { src: string } | Promise<{ default: { src: string } }>
|
|
2
|
-
export type ResolvedImage = { src: string, width?: number, height?: number, [k: string]: unknown }
|
|
3
|
-
export type ImportModule = Record<string, unknown> & { default?: unknown }
|
|
4
|
-
export type GlobMap = Record<string, () => Promise<ImportModule>>
|
package/src/types/index.ts
DELETED
package/src/types/lqip.type.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export type LqipType = 'color' | 'css' | 'base64' | 'svg'
|
package/src/types/props.type.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { LqipType } from './lqip.type'
|
|
2
|
-
|
|
3
|
-
export type Props = {
|
|
4
|
-
/**
|
|
5
|
-
* LQIP type.
|
|
6
|
-
* This can be 'color', 'css', 'svg' or 'base64'.
|
|
7
|
-
* The default value is 'base64'.
|
|
8
|
-
*/
|
|
9
|
-
lqip?: LqipType
|
|
10
|
-
/**
|
|
11
|
-
* Size of the LQIP image in pixels.
|
|
12
|
-
* This value should be between 4 and 64.
|
|
13
|
-
* The default value is 4.
|
|
14
|
-
*/
|
|
15
|
-
lqipSize?: number
|
|
16
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
2
|
-
|
|
3
|
-
import type { GetSVGReturn, LqipType } from '../types'
|
|
4
|
-
|
|
5
|
-
import { PREFIX } from '../constants'
|
|
6
|
-
|
|
7
|
-
import { getPlaiceholder } from 'plaiceholder'
|
|
8
|
-
|
|
9
|
-
function normalizeFsPath(path: string) {
|
|
10
|
-
if (process.platform === 'win32' && /^\/[A-Za-z]:\//.test(path)) {
|
|
11
|
-
return path.slice(1)
|
|
12
|
-
}
|
|
13
|
-
return path
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function isNode() {
|
|
17
|
-
return typeof process !== 'undefined' && !!process.versions?.node
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function readIfExists(path: string): Promise<Buffer | undefined> {
|
|
21
|
-
if (!isNode()) return undefined
|
|
22
|
-
try {
|
|
23
|
-
return await readFile(path)
|
|
24
|
-
} catch {
|
|
25
|
-
return undefined
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function generateLqip(
|
|
30
|
-
imagePath: string,
|
|
31
|
-
lqipType: LqipType,
|
|
32
|
-
lqipSize: number,
|
|
33
|
-
isDevelopment: boolean | undefined
|
|
34
|
-
) {
|
|
35
|
-
try {
|
|
36
|
-
const normalizedPath = normalizeFsPath(imagePath)
|
|
37
|
-
|
|
38
|
-
const buffer = await readIfExists(normalizedPath)
|
|
39
|
-
|
|
40
|
-
if (!buffer) {
|
|
41
|
-
console.warn(`${PREFIX} image not found for:`, imagePath)
|
|
42
|
-
return undefined
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const plaiceholderResult = await getPlaiceholder(buffer, { size: lqipSize })
|
|
46
|
-
let lqipValue: string | GetSVGReturn | undefined
|
|
47
|
-
|
|
48
|
-
switch (lqipType) {
|
|
49
|
-
case 'color':
|
|
50
|
-
lqipValue = plaiceholderResult.color?.hex
|
|
51
|
-
break
|
|
52
|
-
case 'css':
|
|
53
|
-
lqipValue = typeof plaiceholderResult.css === 'object' && plaiceholderResult.css.backgroundImage
|
|
54
|
-
? plaiceholderResult.css.backgroundImage
|
|
55
|
-
: String(plaiceholderResult.css)
|
|
56
|
-
break
|
|
57
|
-
case 'svg':
|
|
58
|
-
lqipValue = plaiceholderResult.svg
|
|
59
|
-
break
|
|
60
|
-
case 'base64':
|
|
61
|
-
default:
|
|
62
|
-
lqipValue = plaiceholderResult.base64
|
|
63
|
-
break
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (isDevelopment) {
|
|
67
|
-
console.log(`${PREFIX} LQIP (${lqipType}) successfully generated!`)
|
|
68
|
-
} else {
|
|
69
|
-
console.log(`${PREFIX} LQIP (${lqipType}) successfully generated for:`, imagePath)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return lqipValue
|
|
73
|
-
} catch (err) {
|
|
74
|
-
console.error(`${PREFIX} Error generating LQIP (${lqipType}) in:`, imagePath, '\n', err)
|
|
75
|
-
return undefined
|
|
76
|
-
}
|
|
77
|
-
}
|
package/src/utils/getLqip.ts
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path'
|
|
2
|
-
import { mkdir, writeFile, unlink, readdir } from 'node:fs/promises'
|
|
3
|
-
import { existsSync, statSync } from 'node:fs'
|
|
4
|
-
|
|
5
|
-
import type { LqipType } from '../types'
|
|
6
|
-
|
|
7
|
-
import { generateLqip } from './generateLqip'
|
|
8
|
-
|
|
9
|
-
import { PREFIX } from '../constants'
|
|
10
|
-
|
|
11
|
-
function isRemoteUrl(url: string) {
|
|
12
|
-
return /^https?:\/\//.test(url)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const CACHE_DIR = join(process.cwd(), 'node_modules', '.cache', 'astro-lqip')
|
|
16
|
-
const EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'avif']
|
|
17
|
-
const SEARCH_ROOT = ['src']
|
|
18
|
-
const HASHED_FILENAME_REGEX = /\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9]+$/
|
|
19
|
-
const HASHED_FILENAME_CAPTURE_REGEX = /^(.+?)\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9]+$/
|
|
20
|
-
|
|
21
|
-
const BASE_URL = (() => {
|
|
22
|
-
try {
|
|
23
|
-
const base = (import.meta.env?.BASE_URL ?? '/') as string
|
|
24
|
-
if (!base || base === '/') return '/'
|
|
25
|
-
return base.endsWith('/') ? base.slice(0, -1) : base
|
|
26
|
-
} catch {
|
|
27
|
-
return '/'
|
|
28
|
-
}
|
|
29
|
-
})()
|
|
30
|
-
|
|
31
|
-
const searchCache = new Map<string, string | null>()
|
|
32
|
-
|
|
33
|
-
function stripBasePath(src: string) {
|
|
34
|
-
if (typeof src !== 'string') return src
|
|
35
|
-
|
|
36
|
-
const queryIndex = src.indexOf('?')
|
|
37
|
-
let pathOnly = queryIndex >= 0 ? src.slice(0, queryIndex) : src
|
|
38
|
-
|
|
39
|
-
if (BASE_URL !== '/' && pathOnly.startsWith(BASE_URL)) {
|
|
40
|
-
pathOnly = pathOnly.slice(BASE_URL.length) || '/'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (!pathOnly.startsWith('/')) {
|
|
44
|
-
pathOnly = `/${pathOnly}`
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return pathOnly
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function ensureCacheDir() {
|
|
51
|
-
if (!existsSync(CACHE_DIR)) {
|
|
52
|
-
await mkdir(CACHE_DIR, { recursive: true })
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function extractOriginalFileName(filename: string) {
|
|
57
|
-
const file = filename.split('/').pop() || ''
|
|
58
|
-
|
|
59
|
-
const match = file.match(HASHED_FILENAME_CAPTURE_REGEX)
|
|
60
|
-
if (match) return match[1]
|
|
61
|
-
|
|
62
|
-
const parts = file.split('.')
|
|
63
|
-
if (parts.length >= 3) return parts.slice(0, parts.length - 2).join('.')
|
|
64
|
-
|
|
65
|
-
return parts[0]
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function recursiveFind(basename: string): Promise<string | undefined> {
|
|
69
|
-
if (!basename) return
|
|
70
|
-
|
|
71
|
-
if (searchCache.has(basename)) {
|
|
72
|
-
const cached = searchCache.get(basename)
|
|
73
|
-
return cached || undefined
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const ignoreDirs = new Set(['node_modules', 'dist', '.astro'])
|
|
77
|
-
|
|
78
|
-
async function walk(dir: string): Promise<string | undefined> {
|
|
79
|
-
let entries: string[]
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
entries = await readdir(dir)
|
|
83
|
-
} catch {
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
for (const entry of entries) {
|
|
88
|
-
const full = join(dir, entry)
|
|
89
|
-
let st: ReturnType<typeof statSync>
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
st = statSync(full)
|
|
93
|
-
} catch {
|
|
94
|
-
continue
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (st.isDirectory()) {
|
|
98
|
-
if (ignoreDirs.has(entry)) continue
|
|
99
|
-
const found = await walk(full)
|
|
100
|
-
if (found) return found
|
|
101
|
-
} else {
|
|
102
|
-
// match by basename and extension
|
|
103
|
-
if (EXTENSIONS.some((ext) => entry === `${basename}.${ext}`)) {
|
|
104
|
-
return full
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
for (const rootRel of SEARCH_ROOT) {
|
|
111
|
-
const rootAbs = join(process.cwd(), rootRel)
|
|
112
|
-
|
|
113
|
-
if (existsSync(rootAbs)) {
|
|
114
|
-
const found = await walk(rootAbs)
|
|
115
|
-
if (found) {
|
|
116
|
-
searchCache.set(basename, found)
|
|
117
|
-
return found
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
searchCache.set(basename, null)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export async function getLqip(
|
|
126
|
-
imagePath: { src: string },
|
|
127
|
-
lqipType: LqipType,
|
|
128
|
-
lqipSize: number,
|
|
129
|
-
isDevelopment: boolean | undefined
|
|
130
|
-
) {
|
|
131
|
-
if (!imagePath?.src) return undefined
|
|
132
|
-
|
|
133
|
-
if (isRemoteUrl(imagePath.src)) {
|
|
134
|
-
await ensureCacheDir()
|
|
135
|
-
|
|
136
|
-
const response = await fetch(imagePath.src)
|
|
137
|
-
if (!response.ok) return undefined
|
|
138
|
-
|
|
139
|
-
const arrayBuffer = await response.arrayBuffer()
|
|
140
|
-
const buffer = Buffer.from(arrayBuffer)
|
|
141
|
-
const tempPath = join(CACHE_DIR, `astro-lqip-${Math.random().toString(36).slice(2)}.jpg`)
|
|
142
|
-
await writeFile(tempPath, buffer)
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
return await generateLqip(tempPath, lqipType, lqipSize, isDevelopment)
|
|
146
|
-
} finally {
|
|
147
|
-
await unlink(tempPath)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (isDevelopment && imagePath.src.startsWith('/@fs/')) {
|
|
152
|
-
const filePath = imagePath.src.replace(/^\/@fs/, '').split('?')[0]
|
|
153
|
-
return await generateLqip(filePath, lqipType, lqipSize, isDevelopment)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (!isDevelopment) {
|
|
157
|
-
const src = imagePath.src
|
|
158
|
-
const normalizedSrc = stripBasePath(src)
|
|
159
|
-
const clean = normalizedSrc.replace(/^\//, '')
|
|
160
|
-
|
|
161
|
-
if (clean) {
|
|
162
|
-
const candidatePaths = [
|
|
163
|
-
join(process.cwd(), 'dist', 'client', clean),
|
|
164
|
-
join(process.cwd(), 'dist', clean)
|
|
165
|
-
]
|
|
166
|
-
|
|
167
|
-
for (const path of candidatePaths) {
|
|
168
|
-
if (existsSync(path)) {
|
|
169
|
-
return await generateLqip(path, lqipType, lqipSize, isDevelopment)
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const fileName = normalizedSrc.split('/').pop() ?? ''
|
|
175
|
-
if (HASHED_FILENAME_REGEX.test(fileName)) {
|
|
176
|
-
const originalBase = extractOriginalFileName(normalizedSrc)
|
|
177
|
-
const originalSource = await recursiveFind(originalBase)
|
|
178
|
-
|
|
179
|
-
if (originalSource) {
|
|
180
|
-
console.log(`${PREFIX} fallback recursive source found:`, originalSource)
|
|
181
|
-
return await generateLqip(originalSource, lqipType, lqipSize, isDevelopment)
|
|
182
|
-
} else {
|
|
183
|
-
console.warn(`${PREFIX} original source not found recursively for basename:`, originalBase)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return undefined
|
|
189
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { GetSVGReturn, LqipType } from '../types'
|
|
2
|
-
|
|
3
|
-
export function getLqipStyle(lqipType: LqipType, lqipImage: string | GetSVGReturn | undefined, svgHTML: string = '') {
|
|
4
|
-
if (!lqipImage) return {}
|
|
5
|
-
|
|
6
|
-
switch (lqipType) {
|
|
7
|
-
case 'css':
|
|
8
|
-
return { '--lqip-background': lqipImage }
|
|
9
|
-
case 'svg':
|
|
10
|
-
return { '--lqip-background': `url('data:image/svg+xml;utf8,${encodeURIComponent(svgHTML)}')` }
|
|
11
|
-
case 'color':
|
|
12
|
-
return { '--lqip-background': lqipImage }
|
|
13
|
-
case 'base64':
|
|
14
|
-
default:
|
|
15
|
-
return { '--lqip-background': `url('${lqipImage}')` }
|
|
16
|
-
}
|
|
17
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { StyleAttrs, SVGNode } from '../types'
|
|
2
|
-
|
|
3
|
-
function styleToString(style: StyleAttrs) {
|
|
4
|
-
return Object.entries(style)
|
|
5
|
-
.map(([key, val]) => `${key.replace(/([A-Z])/g, '-$1').toLowerCase()}:${val}`)
|
|
6
|
-
.join(';')
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function renderSVGNode([tag, attrs, children = []]: SVGNode): string {
|
|
10
|
-
let attrString = ''
|
|
11
|
-
|
|
12
|
-
for (const [k, v] of Object.entries(attrs || {})) {
|
|
13
|
-
if (k === 'style' && v && typeof v === 'object') {
|
|
14
|
-
attrString += ` style="${styleToString(v)}"`
|
|
15
|
-
} else if (v !== undefined) {
|
|
16
|
-
attrString += ` ${k}="${v}"`
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (children.length > 0) {
|
|
21
|
-
return `<${tag}${attrString}>${children.map(renderSVGNode).join('')}</${tag}>`
|
|
22
|
-
} else {
|
|
23
|
-
return `<${tag}${attrString} />`
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
|
|
4
|
-
import type { GlobMap, ImagePath, ImportModule, ResolvedImage } from '../types'
|
|
5
|
-
|
|
6
|
-
import { PREFIX } from '../constants'
|
|
7
|
-
|
|
8
|
-
const PUBLIC_DIR = join(process.cwd(), 'public')
|
|
9
|
-
|
|
10
|
-
const globFilesInSrc: GlobMap = ({ ...import.meta.glob('/src/**/*.{png,jpg,jpeg,svg}') } as unknown) as GlobMap
|
|
11
|
-
|
|
12
|
-
function warnFiles(filePath: string | undefined) {
|
|
13
|
-
if (!filePath) return
|
|
14
|
-
|
|
15
|
-
const lowerPath = filePath.toLowerCase()
|
|
16
|
-
|
|
17
|
-
if (lowerPath.includes(`${join('/', 'public')}`) || lowerPath.includes('/public/') || filePath.startsWith(PUBLIC_DIR)) {
|
|
18
|
-
console.warn(
|
|
19
|
-
`${PREFIX} Warning: image resolved from /public. Images should not be placed in /public — move them to /src so Astro can process them correctly.`
|
|
20
|
-
)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (lowerPath.endsWith('.webp') || lowerPath.endsWith('.avif')) {
|
|
24
|
-
const extension = lowerPath.endsWith('.webp') ? 'webp' : 'avif'
|
|
25
|
-
console.warn(
|
|
26
|
-
`${PREFIX} Warning: image is in ${extension} format. These formats are usually already optimized; using this component to re-process them may degrade quality.`
|
|
27
|
-
)
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function isObject(v: unknown): v is Record<string, unknown> {
|
|
32
|
-
return typeof v === 'object' && v !== null
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isPromise(v: unknown): v is Promise<unknown> {
|
|
36
|
-
if (!isObject(v)) return false
|
|
37
|
-
const promise = v as { then?: unknown }
|
|
38
|
-
return typeof promise.then === 'function'
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function hasSrc(v: unknown): v is ResolvedImage {
|
|
42
|
-
return isObject(v) && typeof (v as Record<string, unknown>)['src'] === 'string'
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function isRemoteUrl(v: string) {
|
|
46
|
-
return /^https?:\/\//.test(v)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function findGlobMatch(keys: string[], path: string) {
|
|
50
|
-
const candidates = [path.replace(/^\//, ''), `/${path.replace(/^\//, '')}`]
|
|
51
|
-
const match = keys.find((k) => candidates.includes(k) || k.endsWith(path) || k.endsWith(path.replace(/^\//, '')))
|
|
52
|
-
if (match) return match
|
|
53
|
-
|
|
54
|
-
const fileName = path.split('/').pop()
|
|
55
|
-
if (!fileName) return null
|
|
56
|
-
|
|
57
|
-
return keys.find((k) => k.endsWith(`/${fileName}`) || k.endsWith(fileName)) ?? null
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export async function resolveImagePath(path: ImagePath) {
|
|
61
|
-
if (path == null) return null
|
|
62
|
-
|
|
63
|
-
// validate dynamic import (Promise-like)
|
|
64
|
-
if (isPromise(path)) {
|
|
65
|
-
const mod = (await (path as Promise<ImportModule>)) as ImportModule
|
|
66
|
-
const resolved = (mod.default ?? mod) as unknown
|
|
67
|
-
if (hasSrc(resolved)) {
|
|
68
|
-
warnFiles(resolved.src)
|
|
69
|
-
return resolved
|
|
70
|
-
}
|
|
71
|
-
if (typeof resolved === 'string') {
|
|
72
|
-
warnFiles(resolved)
|
|
73
|
-
return resolved
|
|
74
|
-
}
|
|
75
|
-
return null
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// validate already-resolved object (import result or { src: ... })
|
|
79
|
-
if (isObject(path)) {
|
|
80
|
-
const obj = path as Record<string, unknown>
|
|
81
|
-
const objSrc = typeof obj['src'] === 'string' ? (obj['src'] as string) : undefined
|
|
82
|
-
warnFiles(objSrc)
|
|
83
|
-
return hasSrc(obj) ? (obj as ResolvedImage) : null
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// validate string path
|
|
87
|
-
if (typeof path === 'string') {
|
|
88
|
-
if (isRemoteUrl(path)) return path
|
|
89
|
-
|
|
90
|
-
const keys = Object.keys(globFilesInSrc)
|
|
91
|
-
const matchKey = findGlobMatch(keys, path)
|
|
92
|
-
|
|
93
|
-
if (matchKey) {
|
|
94
|
-
try {
|
|
95
|
-
const mod = await globFilesInSrc[matchKey]()
|
|
96
|
-
const resolved = (mod.default ?? mod) as unknown
|
|
97
|
-
|
|
98
|
-
if (hasSrc(resolved)) {
|
|
99
|
-
warnFiles((resolved as ResolvedImage).src)
|
|
100
|
-
return resolved as ResolvedImage
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (typeof resolved === 'string') {
|
|
104
|
-
warnFiles(resolved)
|
|
105
|
-
return resolved
|
|
106
|
-
}
|
|
107
|
-
} catch (err) {
|
|
108
|
-
console.log(`${PREFIX} resolveImagePath: failed to import glob match "${matchKey}" — falling back to filesystem.`, err)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// If module doesn't expose a usable value, fall through to filesystem check
|
|
113
|
-
try {
|
|
114
|
-
const absCandidate = path.startsWith('/') ? join(process.cwd(), path) : join(process.cwd(), path)
|
|
115
|
-
|
|
116
|
-
if (existsSync(absCandidate)) {
|
|
117
|
-
warnFiles(absCandidate)
|
|
118
|
-
return { src: `/@fs${absCandidate}` }
|
|
119
|
-
}
|
|
120
|
-
} catch (err) {
|
|
121
|
-
console.debug(`${PREFIX} resolveImagePath: filesystem check failed for "${path}".`, err)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return null
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return null
|
|
128
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import type { ComponentsOptions, ImagePath, SVGNode } from '../types'
|
|
2
|
-
|
|
3
|
-
import { PREFIX } from '../constants'
|
|
4
|
-
|
|
5
|
-
import { resolveImagePath } from './resolveImagePath'
|
|
6
|
-
import { renderSVGNode } from './renderSVGNode'
|
|
7
|
-
import { getLqipStyle } from './getLqipStyle'
|
|
8
|
-
import { getLqip } from './getLqip'
|
|
9
|
-
|
|
10
|
-
export async function useLqipImage({
|
|
11
|
-
src,
|
|
12
|
-
lqip = 'base64',
|
|
13
|
-
lqipSize = 4,
|
|
14
|
-
styleProps = {},
|
|
15
|
-
forbiddenVars = ['--lqip-background', '--z-index', '--opacity'],
|
|
16
|
-
isDevelopment
|
|
17
|
-
}: ComponentsOptions) {
|
|
18
|
-
// resolve any kind of src (string, alias, import result, dynamic import)
|
|
19
|
-
const resolved = await resolveImagePath(src as unknown as ImagePath)
|
|
20
|
-
// resolved may be an object (module-like), { src: '...' } or null
|
|
21
|
-
const resolvedSrc = resolved ?? null
|
|
22
|
-
|
|
23
|
-
let lqipImage
|
|
24
|
-
if (resolvedSrc) {
|
|
25
|
-
const lqipInput = typeof resolvedSrc === 'string' ? { src: resolvedSrc } : resolvedSrc
|
|
26
|
-
lqipImage = await getLqip(lqipInput, lqip, lqipSize, isDevelopment)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
let svgHTML = ''
|
|
30
|
-
if (lqip === 'svg' && Array.isArray(lqipImage)) {
|
|
31
|
-
svgHTML = renderSVGNode(lqipImage as unknown as SVGNode)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const lqipStyle = getLqipStyle(lqip, lqipImage, svgHTML)
|
|
35
|
-
|
|
36
|
-
for (const key of Object.keys(styleProps)) {
|
|
37
|
-
if (forbiddenVars.includes(key)) {
|
|
38
|
-
console.warn(
|
|
39
|
-
`${PREFIX} The CSS variable “${key}” should not be passed in style because it can override the functionality of LQIP.`
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const combinedStyle = {
|
|
45
|
-
...styleProps,
|
|
46
|
-
...lqipStyle
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return { lqipImage, svgHTML, lqipStyle, combinedStyle, resolvedSrc }
|
|
50
|
-
}
|