astro-lqip 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +8 -8
- package/src/components/Image.astro +8 -32
- package/src/components/Picture.astro +8 -32
- package/src/types/components-options.type.ts +10 -0
- package/src/types/index.ts +3 -1
- package/src/types/svg-node.type.ts +7 -0
- package/src/utils/generateLqip.ts +4 -4
- package/src/utils/getLqip.ts +43 -9
- package/src/utils/renderSVGNode.ts +8 -5
- package/src/utils/resolveImagePath.ts +6 -0
- package/src/utils/useLqipImage.ts +55 -0
- package/src/utils/resolveImageMetadata.ts +0 -6
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
|
-
- [
|
|
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
|
+
"version": "1.4.0",
|
|
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.
|
|
41
|
-
"@typescript-eslint/parser": "8.
|
|
42
|
-
"astro": "5.13.
|
|
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.
|
|
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.
|
|
48
|
+
"eslint-plugin-package-json": "0.56.3",
|
|
49
49
|
"eslint-plugin-yml": "1.18.0",
|
|
50
|
-
"globals": "16.
|
|
51
|
-
"jiti": "2.
|
|
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 {
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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 {
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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,
|
package/src/types/index.ts
CHANGED
|
@@ -6,9 +6,9 @@ import { PREFIX } from '../constants'
|
|
|
6
6
|
|
|
7
7
|
import { getPlaiceholder } from 'plaiceholder'
|
|
8
8
|
|
|
9
|
-
export async function generateLqip(
|
|
9
|
+
export async function generateLqip(imagePath: string, isDevelopment: boolean, lqipType: LqipType, lqipSize: number) {
|
|
10
10
|
try {
|
|
11
|
-
const buffer = await readFile(
|
|
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:`,
|
|
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:`,
|
|
41
|
+
console.error(`${PREFIX} Error generating LQIP (${lqipType}) in:`, imagePath, '\n', err)
|
|
42
42
|
return undefined
|
|
43
43
|
}
|
|
44
44
|
}
|
package/src/utils/getLqip.ts
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
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 (
|
|
12
|
-
|
|
13
|
-
|
|
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 (
|
|
17
|
-
const
|
|
18
|
-
return await generateLqip(
|
|
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
|
-
|
|
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
|
|
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
|
|
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,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
|
+
}
|