astro-lqip 1.6.0 → 1.7.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 +14 -11
- package/package.json +8 -8
- package/src/components/Image.astro +1 -3
- package/src/components/Picture.astro +1 -3
- package/src/types/components-options.type.ts +0 -1
- package/src/utils/generateLqip.ts +29 -1
- package/src/utils/getLqip.ts +107 -13
- package/src/utils/useLqipImage.ts +2 -3
package/README.md
CHANGED
|
@@ -62,8 +62,8 @@ Example:
|
|
|
62
62
|
---
|
|
63
63
|
import { Image, Picture } from 'astro-lqip/components';
|
|
64
64
|
|
|
65
|
-
import image from '
|
|
66
|
-
import otherImage from '
|
|
65
|
+
import image from '/src/assets/images/image.png';
|
|
66
|
+
import otherImage from '/src/assets/images/other-image.png';
|
|
67
67
|
---
|
|
68
68
|
|
|
69
69
|
<Image src={image} alt="Cover Image" width={220} height={220} />
|
|
@@ -80,8 +80,8 @@ Example with absolute path:
|
|
|
80
80
|
import { Image, Picture } from 'astro-lqip/components';
|
|
81
81
|
---
|
|
82
82
|
|
|
83
|
-
<Image src="/src/
|
|
84
|
-
<Picture src="/src/
|
|
83
|
+
<Image src="/src/assets/images/image.png" alt="Cover Image" width={220} height={220} />
|
|
84
|
+
<Picture src="/src/assets/images/other-image.png" alt="Other Image" width={220} height={220} />
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
Example with relative path:
|
|
@@ -91,8 +91,9 @@ Example with relative path:
|
|
|
91
91
|
import { Image, Picture } from 'astro-lqip/components';
|
|
92
92
|
---
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
<
|
|
94
|
+
<!-- assuming you are on the path `/src/pages/index.astro` -->
|
|
95
|
+
<Image src="../assets/images/image.png" alt="Cover Image" width={220} height={220} />
|
|
96
|
+
<Picture src="../assets/images/other-image.png" alt="Other Image" width={220} height={220} />
|
|
96
97
|
```
|
|
97
98
|
|
|
98
99
|
Example with alias:
|
|
@@ -102,11 +103,11 @@ Example with alias:
|
|
|
102
103
|
import { Image, Picture } from 'astro-lqip/components';
|
|
103
104
|
---
|
|
104
105
|
|
|
105
|
-
<Image src="@/assets/image.png" alt="Cover Image" width={220} height={220} />
|
|
106
|
-
<Picture src="@/assets/other-image.png" alt="Other Image" width={220} height={220} />
|
|
106
|
+
<Image src="@/assets/images/image.png" alt="Cover Image" width={220} height={220} />
|
|
107
|
+
<Picture src="@/assets/images/other-image.png" alt="Other Image" width={220} height={220} />
|
|
107
108
|
```
|
|
108
109
|
|
|
109
|
-
Learn how to configure path aliasing in the [Astro documentation](https://docs.astro.build/en/guides/typescript/#import-aliases).
|
|
110
|
+
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.
|
|
110
111
|
|
|
111
112
|
## ⚙️ Props
|
|
112
113
|
|
|
@@ -128,8 +129,8 @@ Example:
|
|
|
128
129
|
---
|
|
129
130
|
import { Image, Picture } from 'astro-lqip/components';
|
|
130
131
|
|
|
131
|
-
import image from '
|
|
132
|
-
import otherImage from '
|
|
132
|
+
import image from '/src/assets/images/image.png';
|
|
133
|
+
import otherImage from '/src/assets/images/other-image.png';
|
|
133
134
|
---
|
|
134
135
|
|
|
135
136
|
<Image src={image} alt="Cover Image" width={220} height={220} lqip="svg" lqipSize={10} />
|
|
@@ -143,6 +144,8 @@ import otherImage from './path/to/other-image.png';
|
|
|
143
144
|
|
|
144
145
|
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.
|
|
145
146
|
|
|
147
|
+
For some simple tips, visit the [Usage Tips](https://astro-lqip.web.app/usage-tips/) page.
|
|
148
|
+
|
|
146
149
|
## 🤝 Contributing
|
|
147
150
|
If you wish to contribute to this project, you can do so by reading the [contribution guide](https://github.com/felixicaza/astro-lqip/blob/main/CONTRIBUTING.md).
|
|
148
151
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-lqip",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "Native extended Astro components for generating low quality image placeholders (LQIP).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -37,17 +37,17 @@
|
|
|
37
37
|
"plaiceholder": "3.0.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@eslint/js": "9.
|
|
41
|
-
"@typescript-eslint/parser": "8.46.
|
|
42
|
-
"astro": "5.
|
|
40
|
+
"@eslint/js": "9.39.1",
|
|
41
|
+
"@typescript-eslint/parser": "8.46.4",
|
|
42
|
+
"astro": "5.15.5",
|
|
43
43
|
"bumpp": "10.3.1",
|
|
44
|
-
"eslint": "9.
|
|
45
|
-
"eslint-plugin-astro": "1.
|
|
44
|
+
"eslint": "9.39.1",
|
|
45
|
+
"eslint-plugin-astro": "1.5.0",
|
|
46
46
|
"eslint-plugin-jsonc": "2.21.0",
|
|
47
47
|
"eslint-plugin-jsx-a11y": "6.10.2",
|
|
48
|
-
"eslint-plugin-package-json": "0.
|
|
48
|
+
"eslint-plugin-package-json": "0.71.0",
|
|
49
49
|
"eslint-plugin-yml": "1.19.0",
|
|
50
|
-
"globals": "16.
|
|
50
|
+
"globals": "16.5.0",
|
|
51
51
|
"jiti": "2.6.1",
|
|
52
52
|
"nano-staged": "0.8.0",
|
|
53
53
|
"neostandard": "0.12.2",
|
|
@@ -16,7 +16,6 @@ type Props = (LocalImageProps | RemoteImageProps) & LqipProps & {
|
|
|
16
16
|
const { class: className, lqip = 'base64', lqipSize = 4, parentAttributes = {}, ...props } = Astro.props as Props
|
|
17
17
|
|
|
18
18
|
const isDevelopment = import.meta.env.MODE === 'development'
|
|
19
|
-
const isPrerendered = Astro.isPrerendered
|
|
20
19
|
|
|
21
20
|
const { combinedStyle, resolvedSrc } = await useLqipImage({
|
|
22
21
|
src: props.src,
|
|
@@ -24,8 +23,7 @@ const { combinedStyle, resolvedSrc } = await useLqipImage({
|
|
|
24
23
|
lqipSize,
|
|
25
24
|
styleProps: (parentAttributes.style ?? {}) as Record<string, string | number | undefined>,
|
|
26
25
|
forbiddenVars: [],
|
|
27
|
-
isDevelopment
|
|
28
|
-
isPrerendered
|
|
26
|
+
isDevelopment
|
|
29
27
|
})
|
|
30
28
|
|
|
31
29
|
const componentProps = {
|
|
@@ -13,7 +13,6 @@ type Props = AstroPictureProps & LqipProps
|
|
|
13
13
|
const { class: className, lqip = 'base64', lqipSize = 4, pictureAttributes = {}, ...props } = Astro.props as Props
|
|
14
14
|
|
|
15
15
|
const isDevelopment = import.meta.env.MODE === 'development'
|
|
16
|
-
const isPrerendered = Astro.isPrerendered
|
|
17
16
|
|
|
18
17
|
const { combinedStyle, resolvedSrc } = await useLqipImage({
|
|
19
18
|
src: props.src,
|
|
@@ -21,8 +20,7 @@ const { combinedStyle, resolvedSrc } = await useLqipImage({
|
|
|
21
20
|
lqipSize,
|
|
22
21
|
styleProps: (pictureAttributes.style ?? {}) as Record<string, string | number | undefined>,
|
|
23
22
|
forbiddenVars: [],
|
|
24
|
-
isDevelopment
|
|
25
|
-
isPrerendered
|
|
23
|
+
isDevelopment
|
|
26
24
|
})
|
|
27
25
|
|
|
28
26
|
const componentProps = {
|
|
@@ -6,6 +6,26 @@ import { PREFIX } from '../constants'
|
|
|
6
6
|
|
|
7
7
|
import { getPlaiceholder } from 'plaiceholder'
|
|
8
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
|
+
|
|
9
29
|
export async function generateLqip(
|
|
10
30
|
imagePath: string,
|
|
11
31
|
lqipType: LqipType,
|
|
@@ -13,7 +33,15 @@ export async function generateLqip(
|
|
|
13
33
|
isDevelopment: boolean | undefined
|
|
14
34
|
) {
|
|
15
35
|
try {
|
|
16
|
-
const
|
|
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
|
+
|
|
17
45
|
const plaiceholderResult = await getPlaiceholder(buffer, { size: lqipSize })
|
|
18
46
|
let lqipValue: string | GetSVGReturn | undefined
|
|
19
47
|
|
package/src/utils/getLqip.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
|
-
import { mkdir, writeFile, unlink } from 'node:fs/promises'
|
|
3
|
-
import { existsSync } from 'node:fs'
|
|
2
|
+
import { mkdir, writeFile, unlink, readdir } from 'node:fs/promises'
|
|
3
|
+
import { existsSync, statSync } from 'node:fs'
|
|
4
4
|
|
|
5
5
|
import type { LqipType } from '../types'
|
|
6
6
|
|
|
7
7
|
import { generateLqip } from './generateLqip'
|
|
8
8
|
|
|
9
|
+
import { PREFIX } from '../constants'
|
|
10
|
+
|
|
9
11
|
function isRemoteUrl(url: string) {
|
|
10
12
|
return /^https?:\/\//.test(url)
|
|
11
13
|
}
|
|
12
14
|
|
|
13
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
|
+
|
|
19
|
+
const searchCache = new Map<string, string | null>()
|
|
14
20
|
|
|
15
21
|
async function ensureCacheDir() {
|
|
16
22
|
if (!existsSync(CACHE_DIR)) {
|
|
@@ -18,12 +24,80 @@ async function ensureCacheDir() {
|
|
|
18
24
|
}
|
|
19
25
|
}
|
|
20
26
|
|
|
27
|
+
function extractOriginalFileName(filename: string) {
|
|
28
|
+
const file = filename.split('/').pop() || ''
|
|
29
|
+
|
|
30
|
+
const match = file.match(/^(.+?)\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9]+$/)
|
|
31
|
+
if (match) return match[1]
|
|
32
|
+
|
|
33
|
+
const parts = file.split('.')
|
|
34
|
+
if (parts.length >= 3) return parts.slice(0, parts.length - 2).join('.')
|
|
35
|
+
|
|
36
|
+
return parts[0]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function recursiveFind(basename: string): Promise<string | undefined> {
|
|
40
|
+
if (!basename) return
|
|
41
|
+
|
|
42
|
+
if (searchCache.has(basename)) {
|
|
43
|
+
const cached = searchCache.get(basename)
|
|
44
|
+
return cached || undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const ignoreDirs = new Set(['node_modules', 'dist', '.astro'])
|
|
48
|
+
|
|
49
|
+
async function walk(dir: string): Promise<string | undefined> {
|
|
50
|
+
let entries: string[]
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
entries = await readdir(dir)
|
|
54
|
+
} catch {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
const full = join(dir, entry)
|
|
60
|
+
let st: ReturnType<typeof statSync>
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
st = statSync(full)
|
|
64
|
+
} catch {
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (st.isDirectory()) {
|
|
69
|
+
if (ignoreDirs.has(entry)) continue
|
|
70
|
+
const found = await walk(full)
|
|
71
|
+
if (found) return found
|
|
72
|
+
} else {
|
|
73
|
+
// match by basename and extension
|
|
74
|
+
if (EXTENSIONS.some((ext) => entry === `${basename}.${ext}`)) {
|
|
75
|
+
return full
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const rootRel of SEARCH_ROOT) {
|
|
82
|
+
const rootAbs = join(process.cwd(), rootRel)
|
|
83
|
+
|
|
84
|
+
if (existsSync(rootAbs)) {
|
|
85
|
+
const found = await walk(rootAbs)
|
|
86
|
+
if (found) {
|
|
87
|
+
searchCache.set(basename, found)
|
|
88
|
+
return found
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
searchCache.set(basename, null)
|
|
94
|
+
}
|
|
95
|
+
|
|
21
96
|
export async function getLqip(
|
|
22
97
|
imagePath: { src: string },
|
|
23
98
|
lqipType: LqipType,
|
|
24
99
|
lqipSize: number,
|
|
25
|
-
isDevelopment: boolean | undefined
|
|
26
|
-
isPrerendered: boolean | undefined
|
|
100
|
+
isDevelopment: boolean | undefined
|
|
27
101
|
) {
|
|
28
102
|
if (!imagePath?.src) return undefined
|
|
29
103
|
|
|
@@ -39,8 +113,7 @@ export async function getLqip(
|
|
|
39
113
|
await writeFile(tempPath, buffer)
|
|
40
114
|
|
|
41
115
|
try {
|
|
42
|
-
|
|
43
|
-
return lqip
|
|
116
|
+
return await generateLqip(tempPath, lqipType, lqipSize, isDevelopment)
|
|
44
117
|
} finally {
|
|
45
118
|
await unlink(tempPath)
|
|
46
119
|
}
|
|
@@ -51,14 +124,35 @@ export async function getLqip(
|
|
|
51
124
|
return await generateLqip(filePath, lqipType, lqipSize, isDevelopment)
|
|
52
125
|
}
|
|
53
126
|
|
|
54
|
-
if (!
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
127
|
+
if (!isDevelopment) {
|
|
128
|
+
const src = imagePath.src
|
|
129
|
+
|
|
130
|
+
const clean = src.replace(/^\//, '')
|
|
131
|
+
|
|
132
|
+
const candidatePaths = [
|
|
133
|
+
join(process.cwd(), 'dist', 'client', clean),
|
|
134
|
+
join(process.cwd(), 'dist', clean),
|
|
135
|
+
join(process.cwd(), 'dist', '_astro', clean.replace(/^_astro\//, ''))
|
|
136
|
+
]
|
|
58
137
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
138
|
+
for (const path of candidatePaths) {
|
|
139
|
+
if (existsSync(path)) {
|
|
140
|
+
return await generateLqip(path, lqipType, lqipSize, isDevelopment)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// if not found, try to recursively find the original source
|
|
145
|
+
if (src.startsWith('/_astro/')) {
|
|
146
|
+
const originalBase = extractOriginalFileName(src)
|
|
147
|
+
const originalSource = await recursiveFind(originalBase)
|
|
148
|
+
|
|
149
|
+
if (originalSource) {
|
|
150
|
+
console.log(`${PREFIX} fallback recursive source found:`, originalSource)
|
|
151
|
+
return await generateLqip(originalSource, lqipType, lqipSize, isDevelopment)
|
|
152
|
+
} else {
|
|
153
|
+
console.warn(`${PREFIX} original source not found recursively for basename:`, originalBase)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
62
156
|
}
|
|
63
157
|
|
|
64
158
|
return undefined
|
|
@@ -13,8 +13,7 @@ export async function useLqipImage({
|
|
|
13
13
|
lqipSize = 4,
|
|
14
14
|
styleProps = {},
|
|
15
15
|
forbiddenVars = ['--lqip-background', '--z-index', '--opacity'],
|
|
16
|
-
isDevelopment
|
|
17
|
-
isPrerendered
|
|
16
|
+
isDevelopment
|
|
18
17
|
}: ComponentsOptions) {
|
|
19
18
|
// resolve any kind of src (string, alias, import result, dynamic import)
|
|
20
19
|
const resolved = await resolveImagePath(src as unknown as ImagePath)
|
|
@@ -24,7 +23,7 @@ export async function useLqipImage({
|
|
|
24
23
|
let lqipImage
|
|
25
24
|
if (resolvedSrc) {
|
|
26
25
|
const lqipInput = typeof resolvedSrc === 'string' ? { src: resolvedSrc } : resolvedSrc
|
|
27
|
-
lqipImage = await getLqip(lqipInput, lqip, lqipSize, isDevelopment
|
|
26
|
+
lqipImage = await getLqip(lqipInput, lqip, lqipSize, isDevelopment)
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
let svgHTML = ''
|