astro-lqip 1.3.2 → 1.4.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 CHANGED
@@ -96,7 +96,7 @@ import otherImage from './path/to/other-image.png';
96
96
 
97
97
  - [x] Add support for Image component.
98
98
  - [x] Add support for more lqip techniques.
99
- - [ ] Test for remote images.
99
+ - [x] Test for remote images.
100
100
  - [x] Optimize current CSS usage.
101
101
  - [x] Improve docs page.
102
102
  - [ ] Test support for SSR mode.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-lqip",
3
- "version": "1.3.2",
3
+ "version": "1.4.1",
4
4
  "description": "Native extended Astro components for generating low quality image placeholders (LQIP).",
5
5
  "keywords": [
6
6
  "astro",
@@ -37,18 +37,18 @@
37
37
  "plaiceholder": "3.0.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@eslint/js": "9.33.0",
41
- "@typescript-eslint/parser": "8.40.0",
42
- "astro": "5.13.2",
40
+ "@eslint/js": "9.36.0",
41
+ "@typescript-eslint/parser": "8.44.1",
42
+ "astro": "5.13.11",
43
43
  "bumpp": "10.2.3",
44
- "eslint": "9.33.0",
44
+ "eslint": "9.36.0",
45
45
  "eslint-plugin-astro": "1.3.1",
46
46
  "eslint-plugin-jsonc": "2.20.1",
47
47
  "eslint-plugin-jsx-a11y": "6.10.2",
48
- "eslint-plugin-package-json": "0.54.0",
48
+ "eslint-plugin-package-json": "0.56.3",
49
49
  "eslint-plugin-yml": "1.18.0",
50
- "globals": "16.3.0",
51
- "jiti": "2.5.1",
50
+ "globals": "16.4.0",
51
+ "jiti": "2.6.0",
52
52
  "nano-staged": "0.8.0",
53
53
  "neostandard": "0.12.2",
54
54
  "simple-git-hooks": "2.13.1"
@@ -3,12 +3,7 @@ import type { LocalImageProps, RemoteImageProps } from 'astro:assets'
3
3
  import type { HTMLAttributes } from 'astro/types'
4
4
  import type { Props as LqipProps } from '../types'
5
5
 
6
- import { PREFIX } from '../constants'
7
-
8
- import { resolveImageMetadata } from '../utils/resolveImageMetadata'
9
- import { renderSVGNode } from '../utils/renderSVGNode'
10
- import { getLqipStyle } from '../utils/getLqipStyle'
11
- import { getLqip } from '../utils/getLqip'
6
+ import { useLqipImage } from '../utils/useLqipImage'
12
7
 
13
8
  import { Image as ImageComponent } from 'astro:assets'
14
9
 
@@ -22,32 +17,13 @@ const { class: className, lqip = 'base64', lqipSize = 4, parentAttributes = {},
22
17
 
23
18
  const isDevelopment = import.meta.env.MODE === 'development'
24
19
 
25
- const imageMetadata = await resolveImageMetadata(props.src)
26
- const lqipImage = await getLqip(imageMetadata, isDevelopment, lqip, lqipSize)
27
-
28
- let svgHTML = ''
29
-
30
- if (lqip === 'svg' && Array.isArray(lqipImage)) {
31
- svgHTML = renderSVGNode(lqipImage as [string, Record<string, any>, any[]])
32
- }
33
-
34
- const lqipStyle = getLqipStyle(lqip, lqipImage, svgHTML)
35
-
36
- const forbiddenVars = ['--lqip-background', '--z-index', '--opacity']
37
- const styleProps = parentAttributes.style ?? {}
38
-
39
- for (const key of Object.keys(styleProps)) {
40
- if (forbiddenVars.includes(key)) {
41
- console.warn(
42
- `${PREFIX} The CSS variable “${key}” should not be passed in \`parentAttributes.style\` because it can override the functionality of LQIP.`
43
- )
44
- }
45
- }
46
-
47
- const combinedStyle = {
48
- ...styleProps,
49
- ...lqipStyle
50
- }
20
+ const { combinedStyle } = await useLqipImage({
21
+ src: props.src,
22
+ lqip,
23
+ lqipSize,
24
+ styleProps: parentAttributes.style ?? {},
25
+ isDevelopment
26
+ })
51
27
 
52
28
  const combinedParentAttributes = {
53
29
  ...parentAttributes,
@@ -2,12 +2,7 @@
2
2
  import type { Props as AstroPictureProps } from 'astro/components/Picture.astro'
3
3
  import type { Props as LqipProps } from '../types'
4
4
 
5
- import { PREFIX } from '../constants'
6
-
7
- import { resolveImageMetadata } from '../utils/resolveImageMetadata'
8
- import { renderSVGNode } from '../utils/renderSVGNode'
9
- import { getLqipStyle } from '../utils/getLqipStyle'
10
- import { getLqip } from '../utils/getLqip'
5
+ import { useLqipImage } from '../utils/useLqipImage'
11
6
 
12
7
  import { Picture as PictureComponent } from 'astro:assets'
13
8
 
@@ -19,32 +14,13 @@ const { class: className, lqip = 'base64', lqipSize = 4, pictureAttributes = {},
19
14
 
20
15
  const isDevelopment = import.meta.env.MODE === 'development'
21
16
 
22
- const imageMetadata = await resolveImageMetadata(props.src)
23
- const lqipImage = await getLqip(imageMetadata, isDevelopment, lqip, lqipSize)
24
-
25
- let svgHTML = ''
26
-
27
- if (lqip === 'svg' && Array.isArray(lqipImage)) {
28
- svgHTML = renderSVGNode(lqipImage as [string, Record<string, any>, any[]])
29
- }
30
-
31
- const lqipStyle = getLqipStyle(lqip, lqipImage, svgHTML)
32
-
33
- const forbiddenVars = ['--lqip-background', '--z-index', '--opacity']
34
- const styleProps = pictureAttributes.style ?? {}
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 \`pictureAttributes.style\` because it can override the functionality of LQIP.`
40
- )
41
- }
42
- }
43
-
44
- const combinedStyle = {
45
- ...styleProps,
46
- ...lqipStyle
47
- }
17
+ const { combinedStyle } = await useLqipImage({
18
+ src: props.src,
19
+ lqip,
20
+ lqipSize,
21
+ styleProps: pictureAttributes.style ?? {},
22
+ isDevelopment
23
+ })
48
24
 
49
25
  const combinedPictureAttributes = {
50
26
  ...pictureAttributes,
@@ -0,0 +1,10 @@
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, any>
8
+ forbiddenVars?: string[]
9
+ isDevelopment?: boolean
10
+ }
@@ -0,0 +1 @@
1
+ export type ImagePath = string | { src: string } | Promise<{ default: { src: string } }>
@@ -1,3 +1,6 @@
1
- export * from './plaiceholder.type'
1
+ export * from './components-options.type'
2
+ export * from './image-path.type'
2
3
  export * from './lqip.type'
4
+ export * from './plaiceholder.type'
3
5
  export * from './props.type'
6
+ export * from './svg-node.type'
@@ -0,0 +1,7 @@
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[]]
@@ -6,9 +6,9 @@ import { PREFIX } from '../constants'
6
6
 
7
7
  import { getPlaiceholder } from 'plaiceholder'
8
8
 
9
- export async function generateLqip(filePath: string, isDevelopment: boolean, lqipType: LqipType, lqipSize: number) {
9
+ export async function generateLqip(imagePath: string, isDevelopment: boolean, lqipType: LqipType, lqipSize: number) {
10
10
  try {
11
- const buffer = await readFile(filePath)
11
+ const buffer = await readFile(imagePath)
12
12
  const plaiceholderResult = await getPlaiceholder(buffer, { size: lqipSize })
13
13
  let lqipValue: string | GetSVGReturn | undefined
14
14
 
@@ -33,12 +33,12 @@ export async function generateLqip(filePath: string, isDevelopment: boolean, lqi
33
33
  if (isDevelopment) {
34
34
  console.log(`${PREFIX} LQIP (${lqipType}) successfully generated!`)
35
35
  } else {
36
- console.log(`${PREFIX} LQIP (${lqipType}) successfully generated for:`, filePath)
36
+ console.log(`${PREFIX} LQIP (${lqipType}) successfully generated for:`, imagePath)
37
37
  }
38
38
 
39
39
  return lqipValue
40
40
  } catch (err) {
41
- console.error(`${PREFIX} Error generating LQIP (${lqipType}) in:`, filePath, '\n', err)
41
+ console.error(`${PREFIX} Error generating LQIP (${lqipType}) in:`, imagePath, '\n', err)
42
42
  return undefined
43
43
  }
44
44
  }
@@ -1,20 +1,54 @@
1
1
  import { join } from 'node:path'
2
+ import { mkdir, writeFile, unlink } from 'node:fs/promises'
3
+ import { existsSync } from 'node:fs'
2
4
 
3
- import type { ImageMetadata } from 'astro'
4
5
  import type { LqipType } from '../types'
5
6
 
6
7
  import { generateLqip } from './generateLqip'
7
8
 
8
- export async function getLqip(imageMetadata: ImageMetadata, envMode: boolean, lqipType: LqipType, lqipSize: number) {
9
- if (!imageMetadata?.src) return undefined
9
+ function isRemoteUrl(url: string) {
10
+ return /^https?:\/\//.test(url)
11
+ }
12
+
13
+ const CACHE_DIR = join(process.cwd(), 'node_modules', '.cache', 'astro-lqip')
14
+
15
+ async function ensureCacheDir() {
16
+ if (!existsSync(CACHE_DIR)) {
17
+ await mkdir(CACHE_DIR, { recursive: true })
18
+ }
19
+ }
20
+
21
+ export async function getLqip(imagePath: { src: string }, isDevelopment: boolean, lqipType: LqipType, lqipSize: number) {
22
+ if (!imagePath?.src) return undefined
10
23
 
11
- if (envMode && imageMetadata.src.startsWith('/@fs/')) {
12
- const filePath = imageMetadata.src.replace(/^\/@fs/, '').split('?')[0]
13
- return await generateLqip(filePath, envMode, lqipType, lqipSize)
24
+ if (isRemoteUrl(imagePath.src)) {
25
+ await ensureCacheDir()
26
+
27
+ const response = await fetch(imagePath.src)
28
+ if (!response.ok) return undefined
29
+
30
+ const arrayBuffer = await response.arrayBuffer()
31
+ const buffer = Buffer.from(arrayBuffer)
32
+ const tempPath = join(CACHE_DIR, `astro-lqip-${Math.random().toString(36).slice(2)}.jpg`)
33
+ await writeFile(tempPath, buffer)
34
+
35
+ try {
36
+ const lqip = await generateLqip(tempPath, isDevelopment, lqipType, lqipSize)
37
+ return lqip
38
+ } finally {
39
+ await unlink(tempPath)
40
+ }
14
41
  }
15
42
 
16
- if (!envMode && imageMetadata.src.startsWith('/_astro/')) {
17
- const buildPath = join(process.cwd(), 'dist', imageMetadata.src)
18
- return await generateLqip(buildPath, envMode, lqipType, lqipSize)
43
+ if (isDevelopment && imagePath.src.startsWith('/@fs/')) {
44
+ const filePath = imagePath.src.replace(/^\/@fs/, '').split('?')[0]
45
+ return await generateLqip(filePath, isDevelopment, lqipType, lqipSize)
19
46
  }
47
+
48
+ if (!isDevelopment && imagePath.src.startsWith('/_astro/')) {
49
+ const buildPath = join(process.cwd(), 'dist', imagePath.src)
50
+ return await generateLqip(buildPath, isDevelopment, lqipType, lqipSize)
51
+ }
52
+
53
+ return undefined
20
54
  }
@@ -1,20 +1,23 @@
1
- function styleToString(style: Record<string, string>) {
1
+ import type { StyleAttrs, SVGNode } from '../types'
2
+
3
+ function styleToString(style: StyleAttrs) {
2
4
  return Object.entries(style)
3
5
  .map(([key, val]) => `${key.replace(/([A-Z])/g, '-$1').toLowerCase()}:${val}`)
4
6
  .join(';')
5
7
  }
6
8
 
7
- export function renderSVGNode([tag, attrs, children]: [string, Record<string, any>, any[]]): string {
9
+ export function renderSVGNode([tag, attrs, children = []]: SVGNode): string {
8
10
  let attrString = ''
11
+
9
12
  for (const [k, v] of Object.entries(attrs || {})) {
10
- if (k === 'style') {
13
+ if (k === 'style' && v && typeof v === 'object') {
11
14
  attrString += ` style="${styleToString(v)}"`
12
- } else {
15
+ } else if (v !== undefined) {
13
16
  attrString += ` ${k}="${v}"`
14
17
  }
15
18
  }
16
19
 
17
- if (children && children.length > 0) {
20
+ if (children.length > 0) {
18
21
  return `<${tag}${attrString}>${children.map(renderSVGNode).join('')}</${tag}>`
19
22
  } else {
20
23
  return `<${tag}${attrString} />`
@@ -0,0 +1,10 @@
1
+ import type { ImagePath } from '../types'
2
+
3
+ export async function resolveImagePath(path: ImagePath) {
4
+ // If it's a string, we can't resolve it here. ex: Remote images URLs
5
+ if (typeof path === 'string') return null
6
+ // Handle dynamic imports
7
+ if ('then' in path && typeof path.then === 'function') return (await path).default
8
+ if ('src' in path) return path
9
+ return null
10
+ }
@@ -0,0 +1,55 @@
1
+ import type { ComponentsOptions, 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 = false
17
+ }: ComponentsOptions) {
18
+ let getImagePath: string | { src: string } | null
19
+
20
+ if (typeof src === 'string') {
21
+ getImagePath = src
22
+ } else if (typeof src === 'object' && src !== null) {
23
+ getImagePath = await resolveImagePath(src as unknown as string)
24
+ } else {
25
+ getImagePath = null
26
+ }
27
+
28
+ let lqipImage
29
+ if (getImagePath) {
30
+ const lqipInput = typeof getImagePath === 'string' ? { src: getImagePath } : getImagePath
31
+ lqipImage = await getLqip(lqipInput, isDevelopment, lqip, lqipSize)
32
+ }
33
+
34
+ let svgHTML = ''
35
+ if (lqip === 'svg' && Array.isArray(lqipImage)) {
36
+ svgHTML = renderSVGNode(lqipImage as unknown as SVGNode)
37
+ }
38
+
39
+ const lqipStyle = getLqipStyle(lqip, lqipImage, svgHTML)
40
+
41
+ for (const key of Object.keys(styleProps)) {
42
+ if (forbiddenVars.includes(key)) {
43
+ console.warn(
44
+ `${PREFIX} The CSS variable “${key}” should not be passed in style because it can override the functionality of LQIP.`
45
+ )
46
+ }
47
+ }
48
+
49
+ const combinedStyle = {
50
+ ...styleProps,
51
+ ...lqipStyle
52
+ }
53
+
54
+ return { lqipImage, svgHTML, lqipStyle, combinedStyle }
55
+ }
@@ -1,6 +0,0 @@
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
- }