astro-lqip 1.1.0 → 1.2.2

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  <a href="https://github.com/felixicaza/astro-lqip/">
2
- <img src="./assets/cover.png" alt="Astro LQIP Cover" width="896" height="280" style="object-fit: cover" />
2
+ <img src="./assets/logo.png" alt="Astro LQIP Logo" width="200" height="200" />
3
3
  </a>
4
4
 
5
5
  # 🖼️ astro-lqip
@@ -7,9 +7,7 @@
7
7
  [![GitHub Release](https://img.shields.io/github/v/release/felixicaza/astro-lqip?logo=npm)](https://www.npmjs.com/package/astro-lqip)
8
8
  [![GitHub License](https://img.shields.io/github/license/felixicaza/astro-lqip)](https://github.com/felixicaza/astro-lqip/blob/main/LICENSE)
9
9
 
10
- A integration built over the native Astro component that generates low quality image placeholders (LQIP) for your images.
11
-
12
- [See Demo.](https://astro-lqip.web.app/)
10
+ Native extended Astro component for generating low quality image placeholders (LQIP).
13
11
 
14
12
  ## ⬇️ Installation
15
13
 
@@ -33,29 +31,57 @@ yarn add astro-lqip
33
31
 
34
32
  ## 🚀 Usage
35
33
 
36
- In your current Astro project, just replace the native Astro `<Picture>` component import with the one provided by this package.
34
+ In your current Astro project, just replace the import of the native Astro `<Picture />` component with the one provided by [astro-lqip](https://www.npmjs.com/package/astro-lqip).
35
+
36
+ ```diff
37
+ - import { Picture } from 'astro:assets';
38
+ + import { Picture } from 'astro-lqip/components';
39
+ ```
40
+
41
+ Example:
37
42
 
38
43
  ```astro
39
44
  ---
40
- // import { Picture } from 'astro:assets';
41
- import { Picture } from 'astro-lqip/components';
45
+ import { Picture } from 'astro-lqip/components'
46
+ import image from './path/to/image.png'
47
+ ---
48
+
49
+ <Picture src={image} alt="Cover Image" width={220} height={220} />
50
+ ```
42
51
 
43
- import image from './path/to/image.jpg';
52
+ ## ⚙️ Props
53
+
54
+ This `<Picture />` component supports all the props of the [native Astro component](https://docs.astro.build/en/reference/modules/astro-assets/#picture-properties), but adds a couple of props for LQIP management:
55
+
56
+ - `lqip`: The LQIP type to use. It can be one of the following:
57
+ - `base64`: Generates a Base64-encoded LQIP image. (default option)
58
+ - `color`: Generates a solid color placeholder. Not compatible with `lqipSize`.
59
+ - `css`: Generates a CSS-based LQIP image.
60
+ - `svg`: Generates an SVG-based LQIP image.
61
+ - `lqipSize`: The size of the LQIP image, which can be any number from `4` to `64`. (default is 4)
62
+
63
+ > [!WARNING]
64
+ > A major size in the lqipSize prop can significantly impact the performance of your application.
65
+
66
+ Example:
67
+
68
+ ```astro
69
+ ---
70
+ import { Picture } from 'astro-lqip/components'
71
+ import image from './path/to/image.png'
44
72
  ---
45
73
 
46
- <Picture
47
- src={image}
48
- alt="Description of the image"
49
- />
74
+ <Picture src={image} alt="Cover Image" width={220} height={220} lqip="css" lqipSize={7} />
50
75
  ```
51
76
 
52
77
  ## 📝 ToDo
53
78
 
54
79
  - [ ] Add support for Image component.
55
- - [ ] Add support for more lqip techniques.
80
+ - [x] Add support for more lqip techniques.
56
81
  - [ ] Test for remote images.
57
82
  - [ ] Optimize current CSS usage.
58
- - [ ] Improve demo page.
83
+ - [x] Improve docs page.
84
+ - [ ] Test support for SSR mode.
59
85
 
60
86
  ## 💡 Knowledge
61
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-lqip",
3
- "version": "1.1.0",
3
+ "version": "1.2.2",
4
4
  "description": "Native extended Astro component for generating low quality image placeholders (LQIP).",
5
5
  "keywords": [
6
6
  "astro",
@@ -36,20 +36,20 @@
36
36
  "plaiceholder": "3.0.0"
37
37
  },
38
38
  "devDependencies": {
39
- "@eslint/js": "9.30.0",
40
- "@typescript-eslint/parser": "8.35.0",
41
- "astro": "5.10.1",
39
+ "@eslint/js": "9.30.1",
40
+ "@typescript-eslint/parser": "8.36.0",
41
+ "astro": "5.11.0",
42
42
  "bumpp": "10.2.0",
43
- "eslint": "9.30.0",
43
+ "eslint": "9.30.1",
44
44
  "eslint-plugin-astro": "1.3.1",
45
45
  "eslint-plugin-jsonc": "2.20.1",
46
46
  "eslint-plugin-jsx-a11y": "6.10.2",
47
- "eslint-plugin-package-json": "0.42.0",
47
+ "eslint-plugin-package-json": "0.44.1",
48
48
  "eslint-plugin-yml": "1.18.0",
49
- "globals": "16.2.0",
49
+ "globals": "16.3.0",
50
50
  "jiti": "2.4.2",
51
51
  "nano-staged": "0.8.0",
52
- "neostandard": "0.12.1",
52
+ "neostandard": "0.12.2",
53
53
  "simple-git-hooks": "2.13.0"
54
54
  },
55
55
  "peerDependencies": {
@@ -1,59 +1,49 @@
1
1
  ---
2
- import { join } from 'node:path'
3
- import { readFile } from 'node:fs/promises'
2
+ import type { Props } from '../types'
4
3
 
5
- import { getPlaiceholder } from 'plaiceholder'
4
+ import { PREFIX } from '../constants'
6
5
 
7
- import { Picture as PictureComponent } from 'astro:assets'
8
- import type { Props as PictureProps } from 'astro/components/Picture.astro'
6
+ import { resolveImageMetadata } from '../utils/resolveImageMetadata'
7
+ import { renderSVGNode } from '../utils/renderSVGNode'
8
+ import { getLqipStyle } from '../utils/getLqipStyle'
9
+ import { getLqip } from '../utils/getLqip'
9
10
 
10
- type Props = PictureProps
11
+ import { Picture as PictureComponent } from 'astro:assets'
11
12
 
12
- const { class: className, ...props } = Astro.props as Props
13
+ const { class: className, lqip = 'base64', lqipSize = 4, pictureAttributes = {}, ...props } = Astro.props as Props
13
14
 
14
- const PREFIX = '[astro-lqip]'
15
+ const isDevelopment = import.meta.env.MODE === 'development'
15
16
 
16
17
  const imageMetadata = await resolveImageMetadata(props.src)
17
- const lqip = await getLqip(imageMetadata)
18
+ const lqipImage = await getLqip(imageMetadata, isDevelopment, lqip, lqipSize)
18
19
 
19
- async function resolveImageMetadata(src: any) {
20
- if (typeof src === 'string') return null
21
- if ('then' in src && typeof src.then === 'function') return (await src).default
22
- if ('src' in src) return src
23
- return null
24
- }
20
+ let svgHTML = ''
25
21
 
26
- async function tryGenerateLqip(filePath: string, errorPrefix: string, isDevelopment: boolean) {
27
- try {
28
- const buffer = await readFile(filePath)
29
- const { base64 } = await getPlaiceholder(buffer)
30
-
31
- if (isDevelopment) {
32
- console.log(`${PREFIX} LQIP successfully generated!`)
33
- } else {
34
- console.log(`${PREFIX} LQIP successfully generated for:`, imageMetadata.src)
35
- }
36
- return base64
37
- } catch (err) {
38
- console.error(`${errorPrefix} Error generating LQIP in:`, filePath, '\n', err)
39
- return undefined
40
- }
22
+ if (lqip === 'svg' && Array.isArray(lqipImage)) {
23
+ svgHTML = renderSVGNode(lqipImage as [string, Record<string, any>, any[]])
41
24
  }
42
25
 
43
- async function getLqip(imageMetadata: any) {
44
- if (!imageMetadata?.src) return undefined
26
+ const lqipStyle = getLqipStyle(lqip, lqipImage, svgHTML)
45
27
 
46
- const isDevelopment = import.meta.env.MODE === 'development'
28
+ const forbiddenVars = ['--lqip-background', '--z-index', '--opacity']
29
+ const styleProps = pictureAttributes.style ?? {}
47
30
 
48
- if (isDevelopment && imageMetadata.src.startsWith('/@fs/')) {
49
- const filePath = imageMetadata.src.replace(/^\/@fs/, '').split('?')[0]
50
- return await tryGenerateLqip(filePath, PREFIX, isDevelopment)
31
+ for (const key of Object.keys(styleProps)) {
32
+ if (forbiddenVars.includes(key)) {
33
+ console.warn(
34
+ `${PREFIX} The CSS variable “${key}” should not be passed in \`pictureAttributes.style\` because it can override the functionality of LQIP.`
35
+ )
51
36
  }
37
+ }
52
38
 
53
- if (!isDevelopment && imageMetadata.src.startsWith('/_astro/')) {
54
- const buildPath = join(process.cwd(), 'dist', imageMetadata.src)
55
- return await tryGenerateLqip(buildPath, PREFIX, isDevelopment)
56
- }
39
+ const combinedStyle = {
40
+ ...styleProps,
41
+ ...lqipStyle
42
+ }
43
+
44
+ const combinedPictureAttributes = {
45
+ ...pictureAttributes,
46
+ style: combinedStyle
57
47
  }
58
48
  ---
59
49
 
@@ -91,6 +81,6 @@ async function getLqip(imageMetadata: any) {
91
81
  <PictureComponent
92
82
  {...props}
93
83
  class={className}
94
- pictureAttributes={{ style: { '--lqip-background': `url('${lqip}')` } }}
84
+ pictureAttributes={{ ...combinedPictureAttributes }}
95
85
  onload="parentElement.style.setProperty('--z-index', 1), parentElement.style.setProperty('--opacity', 0)"
96
86
  />
@@ -0,0 +1 @@
1
+ export const PREFIX = '[astro-lqip]'
@@ -0,0 +1,3 @@
1
+ export * from './plaiceholder.type'
2
+ export * from './lqip.type'
3
+ export * from './props.type'
@@ -0,0 +1 @@
1
+ export type LqipType = 'color' | 'css' | 'base64' | 'svg'
@@ -0,0 +1,3 @@
1
+ import type { GetPlaiceholderReturn } from 'plaiceholder'
2
+
3
+ export type GetSVGReturn = GetPlaiceholderReturn['svg']
@@ -0,0 +1,18 @@
1
+ import type { Props as PictureProps } from 'astro/components/Picture.astro'
2
+
3
+ import type { LqipType } from './lqip.type'
4
+
5
+ export type Props = PictureProps & {
6
+ /**
7
+ * LQIP type.
8
+ * This can be 'color', 'css', 'svg' or 'base64'.
9
+ * The default value is 'base64'.
10
+ */
11
+ lqip?: LqipType
12
+ /**
13
+ * Size of the LQIP image in pixels.
14
+ * This value should be between 4 and 64.
15
+ * The default value is 4.
16
+ */
17
+ lqipSize?: number
18
+ }
@@ -0,0 +1,44 @@
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
+ export async function generateLqip(filePath: string, isDevelopment: boolean, lqipType: LqipType, lqipSize: number) {
10
+ try {
11
+ const buffer = await readFile(filePath)
12
+ const plaiceholderResult = await getPlaiceholder(buffer, { size: lqipSize })
13
+ let lqipValue: string | GetSVGReturn | undefined
14
+
15
+ switch (lqipType) {
16
+ case 'color':
17
+ lqipValue = plaiceholderResult.color?.hex
18
+ break
19
+ case 'css':
20
+ lqipValue = typeof plaiceholderResult.css === 'object' && plaiceholderResult.css.backgroundImage
21
+ ? plaiceholderResult.css.backgroundImage
22
+ : String(plaiceholderResult.css)
23
+ break
24
+ case 'svg':
25
+ lqipValue = plaiceholderResult.svg
26
+ break
27
+ case 'base64':
28
+ default:
29
+ lqipValue = plaiceholderResult.base64
30
+ break
31
+ }
32
+
33
+ if (isDevelopment) {
34
+ console.log(`${PREFIX} LQIP (${lqipType}) successfully generated!`)
35
+ } else {
36
+ console.log(`${PREFIX} LQIP (${lqipType}) successfully generated for:`, filePath)
37
+ }
38
+
39
+ return lqipValue
40
+ } catch (err) {
41
+ console.error(`${PREFIX} Error generating LQIP (${lqipType}) in:`, filePath, '\n', err)
42
+ return undefined
43
+ }
44
+ }
@@ -0,0 +1,20 @@
1
+ import { join } from 'node:path'
2
+
3
+ import type { ImageMetadata } from 'astro'
4
+ import type { LqipType } from '../types'
5
+
6
+ import { generateLqip } from './generateLqip'
7
+
8
+ export async function getLqip(imageMetadata: ImageMetadata, envMode: boolean, lqipType: LqipType, lqipSize: number) {
9
+ if (!imageMetadata?.src) return undefined
10
+
11
+ if (envMode && imageMetadata.src.startsWith('/@fs/')) {
12
+ const filePath = imageMetadata.src.replace(/^\/@fs/, '').split('?')[0]
13
+ return await generateLqip(filePath, envMode, lqipType, lqipSize)
14
+ }
15
+
16
+ if (!envMode && imageMetadata.src.startsWith('/_astro/')) {
17
+ const buildPath = join(process.cwd(), 'dist', imageMetadata.src)
18
+ return await generateLqip(buildPath, envMode, lqipType, lqipSize)
19
+ }
20
+ }
@@ -0,0 +1,17 @@
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
+ }
@@ -0,0 +1,22 @@
1
+ function styleToString(style: Record<string, string>) {
2
+ return Object.entries(style)
3
+ .map(([key, val]) => `${key.replace(/([A-Z])/g, '-$1').toLowerCase()}:${val}`)
4
+ .join(';')
5
+ }
6
+
7
+ export function renderSVGNode([tag, attrs, children]: [string, Record<string, any>, any[]]): string {
8
+ let attrString = ''
9
+ for (const [k, v] of Object.entries(attrs || {})) {
10
+ if (k === 'style') {
11
+ attrString += ` style="${styleToString(v)}"`
12
+ } else {
13
+ attrString += ` ${k}="${v}"`
14
+ }
15
+ }
16
+
17
+ if (children && children.length > 0) {
18
+ return `<${tag}${attrString}>${children.map(renderSVGNode).join('')}</${tag}>`
19
+ } else {
20
+ return `<${tag}${attrString} />`
21
+ }
22
+ }
@@ -0,0 +1,6 @@
1
+ export async function resolveImageMetadata(src: any) {
2
+ if (typeof src === 'string') return null
3
+ if ('then' in src && typeof src.then === 'function') return (await src).default
4
+ if ('src' in src) return src
5
+ return null
6
+ }