astro-lqip 1.8.0 → 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.
@@ -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}}
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-lqip",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
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",
@@ -23,11 +23,19 @@
23
23
  "license": "MIT",
24
24
  "author": "Felix Icaza",
25
25
  "type": "module",
26
+ "types": "./dist/index.d.mts",
26
27
  "exports": {
27
- "./components": "./src/index.ts"
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
+ }
28
36
  },
29
37
  "files": [
30
- "src"
38
+ "dist"
31
39
  ],
32
40
  "sideEffects": false,
33
41
  "simple-git-hooks": {
@@ -41,9 +49,12 @@
41
49
  },
42
50
  "devDependencies": {
43
51
  "@eslint/js": "9.39.4",
52
+ "@tsdown/css": "0.21.7",
53
+ "@types/node": "25.5.0",
44
54
  "@typescript-eslint/parser": "8.57.0",
45
- "astro": "6.0.4",
46
- "bumpp": "10.4.1",
55
+ "astro": "6.1.2",
56
+ "bumpp": "11.0.1",
57
+ "concurrently": "9.2.1",
47
58
  "eslint": "9.39.4",
48
59
  "eslint-plugin-astro": "1.6.0",
49
60
  "eslint-plugin-jsonc": "3.1.2",
@@ -52,22 +63,38 @@
52
63
  "eslint-plugin-yml": "3.3.1",
53
64
  "globals": "17.4.0",
54
65
  "jiti": "2.6.1",
66
+ "lightningcss": "1.32.0",
55
67
  "nano-staged": "0.9.0",
56
68
  "neostandard": "0.13.0",
57
- "simple-git-hooks": "2.13.1"
69
+ "simple-git-hooks": "2.13.1",
70
+ "tsdown": "0.21.7"
58
71
  },
59
72
  "peerDependencies": {
60
73
  "astro": ">=3.3.0"
61
74
  },
62
75
  "scripts": {
76
+ "build": "tsdown",
63
77
  "build:docs": "pnpm --filter ./docs -r build",
64
- "dev": "pnpm --filter=!./docs -r dev",
78
+ "build:watch": "tsdown --watch",
79
+ "dev": "concurrently \"pnpm dev:lib\" \"pnpm dev:fixtures\"",
65
80
  "dev:docs": "pnpm --filter ./docs -r dev",
81
+ "dev:fixtures": "pnpm --filter \"./tests/fixtures/**\" -r --parallel dev",
82
+ "dev:lib": "tsdown --watch",
66
83
  "lint": "eslint --cache .",
67
84
  "lint:fix": "eslint --cache --fix .",
68
- "preview": "pnpm -r preview",
69
85
  "release": "bumpp",
86
+ "sync:types": " pnpm --filter \"./tests/fixtures/**\" astro sync",
87
+ "test": "pnpm test:fixtures",
70
88
  "test:fixtures": "pnpm --filter \"./tests/fixtures/**\" -r build",
71
- "test:fixtures:preview": "pnpm --filter \"./tests/fixtures/**\" -r preview"
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"
72
99
  }
73
100
  }
@@ -1,21 +0,0 @@
1
- ---
2
- import type { ImageTransform } from '../types'
3
-
4
- import { useLqipBackground } from '../utils/useLqipBackground'
5
-
6
- import '../styles/background.css'
7
-
8
- type Props = ImageTransform
9
-
10
- const props = Astro.props as Props
11
- const isDevelopment = import.meta.env.MODE === 'development'
12
-
13
- const { style: backgroundStyle } = await useLqipBackground({
14
- ...props,
15
- isDevelopment
16
- })
17
- ---
18
-
19
- <div data-astro-lqip-bg style={backgroundStyle}>
20
- <slot />
21
- </div>
@@ -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
- />
@@ -1 +0,0 @@
1
- export const PREFIX = '[astro-lqip]'
package/src/index.ts DELETED
@@ -1,3 +0,0 @@
1
- export { default as Picture } from './components/Picture.astro'
2
- export { default as Image } from './components/Image.astro'
3
- export { default as Background } from './components/Background.astro'
@@ -1,3 +0,0 @@
1
- [data-astro-lqip-bg] {
2
- display: contents;
3
- }
@@ -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>>
@@ -1,54 +0,0 @@
1
- /**
2
- * This file defines the types for the ImageTransform, which is used to optimize images for the Background component.
3
- * Extracted from Astro's Types, reference: https://github.com/withastro/astro/blob/2dcd8d54c6fb00183228d757bf684e67c79029d8/packages/astro/src/assets/types.ts#L82
4
- */
5
-
6
- type ValidOutputFormats = ['avif', 'png', 'webp', 'jpeg', 'jpg', 'svg']
7
- type ImageQualityPreset = 'low' | 'mid' | 'high' | 'max' | (string & {})
8
- type ImageQuality = ImageQualityPreset | number
9
- type ImageOutputFormat = ValidOutputFormats[number] | (string & {})
10
- type ImageFit = 'fill' | 'contain' | 'cover' | 'none' | 'scale-down' | (string & {})
11
-
12
- export interface ImageTransform {
13
- /**
14
- * Path of the image that will be used as the background.
15
- * It can be a relative path, an absolute path, an alias or ImageMetadata.
16
- */
17
- src: string | ImageMetadata
18
- /**
19
- * CSS custom property that will receive the generated background.
20
- * (defaults to --background)
21
- */
22
- cssVariable?: string
23
- /**
24
- * LQIP placeholder strategy for Background (defaults to 'base64').
25
- * Only 'base64' and 'color' are supported to keep CSS-friendly values.
26
- */
27
- lqip?: 'base64' | 'color'
28
- /**
29
- * Specifies one or more output formats (first entry is preferred fallback).
30
- * Accepts a single `ImageOutputFormat` or an ordered array.
31
- * (defaults to webp)
32
- */
33
- format?: ImageOutputFormat | ImageOutputFormat[] | undefined
34
- /**
35
- * Explicit widths used for responsive variants.
36
- */
37
- widths?: number[] | undefined
38
- /**
39
- * Single width fallback when `widths` is omitted.
40
- */
41
- width?: number | undefined
42
- /**
43
- * Target height for generated assets.
44
- */
45
- height?: number | undefined
46
- /**
47
- * Compression quality preset or numeric percentage.
48
- */
49
- quality?: ImageQuality | undefined
50
- /**
51
- * Object-fit behavior applied during resizing.
52
- */
53
- fit?: ImageFit | undefined
54
- }
@@ -1,7 +0,0 @@
1
- export * from './components-options.type'
2
- export * from './image-path.type'
3
- export * from './image-transform.type'
4
- export * from './lqip.type'
5
- export * from './plaiceholder.type'
6
- export * from './props.type'
7
- export * from './svg-node.type'
@@ -1 +0,0 @@
1
- export type LqipType = 'color' | 'css' | 'base64' | 'svg'
@@ -1,3 +0,0 @@
1
- import type { GetPlaiceholderReturn } from 'plaiceholder'
2
-
3
- export type GetSVGReturn = GetPlaiceholderReturn['svg']
@@ -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,7 +0,0 @@
1
- export type StyleAttrs = Record<string, string | number>
2
-
3
- type SVGNodeAttrs = {
4
- style?: StyleAttrs
5
- } & Record<string, string | number>
6
-
7
- export type SVGNode = [string, SVGNodeAttrs, SVGNode[]]
@@ -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
- }