astro-lqip 1.5.0 → 1.7.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 +48 -6
- package/package.json +8 -8
- package/src/components/Image.astro +9 -6
- package/src/components/Picture.astro +9 -6
- package/src/types/components-options.type.ts +1 -2
- package/src/types/image-path.type.ts +3 -0
- package/src/utils/generateLqip.ts +16 -1
- package/src/utils/getLqip.ts +107 -13
- package/src/utils/resolveImagePath.ts +124 -6
- package/src/utils/useLqipImage.ts +10 -16
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Native extended Astro components for generating low quality image placeholders (
|
|
|
13
13
|
- 🖼️ Supports both `<Image>` and `<Picture>` components.
|
|
14
14
|
- 🎨 Multiple LQIP techniques: base64, solid color, CSS via gradients and SVG.
|
|
15
15
|
- 🚀 Easy to use, just replace the native Astro components with the ones from [astro-lqip](https://astro-lqip.web.app/).
|
|
16
|
+
- ⚡️ Support images as static imports or using string paths.
|
|
16
17
|
- 🔧 Fully compatible with [Astro's image optimization features](https://docs.astro.build/en/guides/images/).
|
|
17
18
|
- 🌍 Supports both local and remote images.
|
|
18
19
|
- ⚙️ Supports SSR mode with [Node Adapter](https://docs.astro.build/en/guides/integrations-guide/node/).
|
|
@@ -61,14 +62,53 @@ Example:
|
|
|
61
62
|
---
|
|
62
63
|
import { Image, Picture } from 'astro-lqip/components';
|
|
63
64
|
|
|
64
|
-
import image from '
|
|
65
|
-
import otherImage from '
|
|
65
|
+
import image from '/src/assets/images/image.png';
|
|
66
|
+
import otherImage from '/src/assets/images/other-image.png';
|
|
66
67
|
---
|
|
67
68
|
|
|
68
69
|
<Image src={image} alt="Cover Image" width={220} height={220} />
|
|
69
|
-
<Picture src={otherImage} alt="Other
|
|
70
|
+
<Picture src={otherImage} alt="Other Image" width={220} height={220} />
|
|
70
71
|
```
|
|
71
72
|
|
|
73
|
+
> [!TIP]
|
|
74
|
+
> Since version `1.6.0`, you can also put the image path as string directly in the `src` prop. Support absolute paths in `src`, relative paths and alias.
|
|
75
|
+
|
|
76
|
+
Example with absolute path:
|
|
77
|
+
|
|
78
|
+
```astro
|
|
79
|
+
---
|
|
80
|
+
import { Image, Picture } from 'astro-lqip/components';
|
|
81
|
+
---
|
|
82
|
+
|
|
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
|
+
```
|
|
86
|
+
|
|
87
|
+
Example with relative path:
|
|
88
|
+
|
|
89
|
+
```astro
|
|
90
|
+
---
|
|
91
|
+
import { Image, Picture } from 'astro-lqip/components';
|
|
92
|
+
---
|
|
93
|
+
|
|
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} />
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Example with alias:
|
|
100
|
+
|
|
101
|
+
```astro
|
|
102
|
+
---
|
|
103
|
+
import { Image, Picture } from 'astro-lqip/components';
|
|
104
|
+
---
|
|
105
|
+
|
|
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} />
|
|
108
|
+
```
|
|
109
|
+
|
|
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.
|
|
111
|
+
|
|
72
112
|
## ⚙️ Props
|
|
73
113
|
|
|
74
114
|
Both `<Image>` and `<Picture>` components support all the props of the [native Astro components](https://docs.astro.build/en/reference/modules/astro-assets/), but adds a couple of props for LQIP management:
|
|
@@ -89,12 +129,12 @@ Example:
|
|
|
89
129
|
---
|
|
90
130
|
import { Image, Picture } from 'astro-lqip/components';
|
|
91
131
|
|
|
92
|
-
import image from '
|
|
93
|
-
import otherImage from '
|
|
132
|
+
import image from '/src/assets/images/image.png';
|
|
133
|
+
import otherImage from '/src/assets/images/other-image.png';
|
|
94
134
|
---
|
|
95
135
|
|
|
96
136
|
<Image src={image} alt="Cover Image" width={220} height={220} lqip="svg" lqipSize={10} />
|
|
97
|
-
<Picture src={otherImage} alt="Other
|
|
137
|
+
<Picture src={otherImage} alt="Other Image" width={220} height={220} lqip="css" lqipSize={7} />
|
|
98
138
|
```
|
|
99
139
|
|
|
100
140
|
> [!TIP]
|
|
@@ -104,6 +144,8 @@ import otherImage from './path/to/other-image.png';
|
|
|
104
144
|
|
|
105
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.
|
|
106
146
|
|
|
147
|
+
For some simple tips, visit the [Usage Tips](https://astro-lqip.web.app/usage-tips/) page.
|
|
148
|
+
|
|
107
149
|
## 🤝 Contributing
|
|
108
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).
|
|
109
151
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-lqip",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
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,17 +16,20 @@ 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
|
-
const { combinedStyle } = await useLqipImage({
|
|
20
|
+
const { combinedStyle, resolvedSrc } = await useLqipImage({
|
|
22
21
|
src: props.src,
|
|
23
22
|
lqip,
|
|
24
23
|
lqipSize,
|
|
25
|
-
styleProps: parentAttributes.style ?? {},
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
styleProps: (parentAttributes.style ?? {}) as Record<string, string | number | undefined>,
|
|
25
|
+
forbiddenVars: [],
|
|
26
|
+
isDevelopment
|
|
28
27
|
})
|
|
29
28
|
|
|
29
|
+
const componentProps = {
|
|
30
|
+
...props,
|
|
31
|
+
src: resolvedSrc ?? props.src
|
|
32
|
+
}
|
|
30
33
|
const combinedParentAttributes = {
|
|
31
34
|
...parentAttributes,
|
|
32
35
|
style: combinedStyle
|
|
@@ -35,7 +38,7 @@ const combinedParentAttributes = {
|
|
|
35
38
|
|
|
36
39
|
<div class={className} data-astro-lqip {...combinedParentAttributes}>
|
|
37
40
|
<ImageComponent
|
|
38
|
-
{...
|
|
41
|
+
{...componentProps as LocalImageProps | RemoteImageProps}
|
|
39
42
|
class={className}
|
|
40
43
|
onload="parentElement.style.setProperty('--z-index', 1), parentElement.style.setProperty('--opacity', 0)"
|
|
41
44
|
/>
|
|
@@ -13,17 +13,20 @@ 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
|
-
const { combinedStyle } = await useLqipImage({
|
|
17
|
+
const { combinedStyle, resolvedSrc } = await useLqipImage({
|
|
19
18
|
src: props.src,
|
|
20
19
|
lqip,
|
|
21
20
|
lqipSize,
|
|
22
|
-
styleProps: pictureAttributes.style ?? {},
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
styleProps: (pictureAttributes.style ?? {}) as Record<string, string | number | undefined>,
|
|
22
|
+
forbiddenVars: [],
|
|
23
|
+
isDevelopment
|
|
25
24
|
})
|
|
26
25
|
|
|
26
|
+
const componentProps = {
|
|
27
|
+
...props,
|
|
28
|
+
src: resolvedSrc ?? props.src
|
|
29
|
+
}
|
|
27
30
|
const combinedPictureAttributes = {
|
|
28
31
|
...pictureAttributes,
|
|
29
32
|
style: combinedStyle
|
|
@@ -31,7 +34,7 @@ const combinedPictureAttributes = {
|
|
|
31
34
|
---
|
|
32
35
|
|
|
33
36
|
<PictureComponent
|
|
34
|
-
{...
|
|
37
|
+
{...(componentProps as AstroPictureProps)}
|
|
35
38
|
class={className}
|
|
36
39
|
pictureAttributes={{ 'data-astro-lqip': '', ...combinedPictureAttributes }}
|
|
37
40
|
onload="parentElement.style.setProperty('--z-index', 1), parentElement.style.setProperty('--opacity', 0)"
|
|
@@ -4,8 +4,7 @@ export type ComponentsOptions = {
|
|
|
4
4
|
src: string | object
|
|
5
5
|
lqip: LqipType
|
|
6
6
|
lqipSize: number
|
|
7
|
-
styleProps: Record<string,
|
|
7
|
+
styleProps: Record<string, string | number | undefined>
|
|
8
8
|
forbiddenVars: string[]
|
|
9
9
|
isDevelopment: boolean | undefined
|
|
10
|
-
isPrerendered: boolean | undefined
|
|
11
10
|
}
|
|
@@ -1 +1,4 @@
|
|
|
1
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>>
|
|
@@ -6,6 +6,13 @@ 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
|
+
|
|
9
16
|
export async function generateLqip(
|
|
10
17
|
imagePath: string,
|
|
11
18
|
lqipType: LqipType,
|
|
@@ -13,7 +20,15 @@ export async function generateLqip(
|
|
|
13
20
|
isDevelopment: boolean | undefined
|
|
14
21
|
) {
|
|
15
22
|
try {
|
|
16
|
-
const
|
|
23
|
+
const normalizedPath = normalizeFsPath(imagePath)
|
|
24
|
+
|
|
25
|
+
const buffer = await readFile(normalizedPath)
|
|
26
|
+
|
|
27
|
+
if (!buffer) {
|
|
28
|
+
console.warn(`${PREFIX} image not found for:`, imagePath)
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
const plaiceholderResult = await getPlaiceholder(buffer, { size: lqipSize })
|
|
18
33
|
let lqipValue: string | GetSVGReturn | undefined
|
|
19
34
|
|
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
|
|
@@ -1,10 +1,128 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { GlobMap, ImagePath, ImportModule, ResolvedImage } from '../types'
|
|
5
|
+
|
|
6
|
+
import { PREFIX } from '../constants'
|
|
7
|
+
|
|
8
|
+
const PUBLIC_DIR = join(process.cwd(), 'public')
|
|
9
|
+
|
|
10
|
+
const globFilesInSrc: GlobMap = ({ ...import.meta.glob('/src/**/*.{png,jpg,jpeg,svg}') } as unknown) as GlobMap
|
|
11
|
+
|
|
12
|
+
function warnFiles(filePath: string | undefined) {
|
|
13
|
+
if (!filePath) return
|
|
14
|
+
|
|
15
|
+
const lowerPath = filePath.toLowerCase()
|
|
16
|
+
|
|
17
|
+
if (lowerPath.includes(`${join('/', 'public')}`) || lowerPath.includes('/public/') || filePath.startsWith(PUBLIC_DIR)) {
|
|
18
|
+
console.warn(
|
|
19
|
+
`${PREFIX} Warning: image resolved from /public. Images should not be placed in /public — move them to /src so Astro can process them correctly.`
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (lowerPath.endsWith('.webp') || lowerPath.endsWith('.avif')) {
|
|
24
|
+
const extension = lowerPath.endsWith('.webp') ? 'webp' : 'avif'
|
|
25
|
+
console.warn(
|
|
26
|
+
`${PREFIX} Warning: image is in ${extension} format. These formats are usually already optimized; using this component to re-process them may degrade quality.`
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isObject(v: unknown): v is Record<string, unknown> {
|
|
32
|
+
return typeof v === 'object' && v !== null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isPromise(v: unknown): v is Promise<unknown> {
|
|
36
|
+
if (!isObject(v)) return false
|
|
37
|
+
const promise = v as { then?: unknown }
|
|
38
|
+
return typeof promise.then === 'function'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasSrc(v: unknown): v is ResolvedImage {
|
|
42
|
+
return isObject(v) && typeof (v as Record<string, unknown>)['src'] === 'string'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isRemoteUrl(v: string) {
|
|
46
|
+
return /^https?:\/\//.test(v)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function findGlobMatch(keys: string[], path: string) {
|
|
50
|
+
const candidates = [path.replace(/^\//, ''), `/${path.replace(/^\//, '')}`]
|
|
51
|
+
const match = keys.find((k) => candidates.includes(k) || k.endsWith(path) || k.endsWith(path.replace(/^\//, '')))
|
|
52
|
+
if (match) return match
|
|
53
|
+
|
|
54
|
+
const fileName = path.split('/').pop()
|
|
55
|
+
if (!fileName) return null
|
|
56
|
+
|
|
57
|
+
return keys.find((k) => k.endsWith(`/${fileName}`) || k.endsWith(fileName)) ?? null
|
|
58
|
+
}
|
|
2
59
|
|
|
3
60
|
export async function resolveImagePath(path: ImagePath) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
if (
|
|
8
|
-
|
|
61
|
+
if (path == null) return null
|
|
62
|
+
|
|
63
|
+
// validate dynamic import (Promise-like)
|
|
64
|
+
if (isPromise(path)) {
|
|
65
|
+
const mod = (await (path as Promise<ImportModule>)) as ImportModule
|
|
66
|
+
const resolved = (mod.default ?? mod) as unknown
|
|
67
|
+
if (hasSrc(resolved)) {
|
|
68
|
+
warnFiles(resolved.src)
|
|
69
|
+
return resolved
|
|
70
|
+
}
|
|
71
|
+
if (typeof resolved === 'string') {
|
|
72
|
+
warnFiles(resolved)
|
|
73
|
+
return resolved
|
|
74
|
+
}
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// validate already-resolved object (import result or { src: ... })
|
|
79
|
+
if (isObject(path)) {
|
|
80
|
+
const obj = path as Record<string, unknown>
|
|
81
|
+
const objSrc = typeof obj['src'] === 'string' ? (obj['src'] as string) : undefined
|
|
82
|
+
warnFiles(objSrc)
|
|
83
|
+
return hasSrc(obj) ? (obj as ResolvedImage) : null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// validate string path
|
|
87
|
+
if (typeof path === 'string') {
|
|
88
|
+
if (isRemoteUrl(path)) return path
|
|
89
|
+
|
|
90
|
+
const keys = Object.keys(globFilesInSrc)
|
|
91
|
+
const matchKey = findGlobMatch(keys, path)
|
|
92
|
+
|
|
93
|
+
if (matchKey) {
|
|
94
|
+
try {
|
|
95
|
+
const mod = await globFilesInSrc[matchKey]()
|
|
96
|
+
const resolved = (mod.default ?? mod) as unknown
|
|
97
|
+
|
|
98
|
+
if (hasSrc(resolved)) {
|
|
99
|
+
warnFiles((resolved as ResolvedImage).src)
|
|
100
|
+
return resolved as ResolvedImage
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (typeof resolved === 'string') {
|
|
104
|
+
warnFiles(resolved)
|
|
105
|
+
return resolved
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.log(`${PREFIX} resolveImagePath: failed to import glob match "${matchKey}" — falling back to filesystem.`, err)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// If module doesn't expose a usable value, fall through to filesystem check
|
|
113
|
+
try {
|
|
114
|
+
const absCandidate = path.startsWith('/') ? join(process.cwd(), path) : join(process.cwd(), path)
|
|
115
|
+
|
|
116
|
+
if (existsSync(absCandidate)) {
|
|
117
|
+
warnFiles(absCandidate)
|
|
118
|
+
return { src: `/@fs${absCandidate}` }
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.debug(`${PREFIX} resolveImagePath: filesystem check failed for "${path}".`, err)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
|
|
9
127
|
return null
|
|
10
128
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentsOptions, SVGNode } from '../types'
|
|
1
|
+
import type { ComponentsOptions, ImagePath, SVGNode } from '../types'
|
|
2
2
|
|
|
3
3
|
import { PREFIX } from '../constants'
|
|
4
4
|
|
|
@@ -13,23 +13,17 @@ 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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} else if (typeof src === 'object' && src !== null) {
|
|
24
|
-
getImagePath = await resolveImagePath(src as unknown as string)
|
|
25
|
-
} else {
|
|
26
|
-
getImagePath = null
|
|
27
|
-
}
|
|
18
|
+
// resolve any kind of src (string, alias, import result, dynamic import)
|
|
19
|
+
const resolved = await resolveImagePath(src as unknown as ImagePath)
|
|
20
|
+
// resolved may be an object (module-like), { src: '...' } or null
|
|
21
|
+
const resolvedSrc = resolved ?? null
|
|
28
22
|
|
|
29
23
|
let lqipImage
|
|
30
|
-
if (
|
|
31
|
-
const lqipInput = typeof
|
|
32
|
-
lqipImage = await getLqip(lqipInput, lqip, lqipSize, isDevelopment
|
|
24
|
+
if (resolvedSrc) {
|
|
25
|
+
const lqipInput = typeof resolvedSrc === 'string' ? { src: resolvedSrc } : resolvedSrc
|
|
26
|
+
lqipImage = await getLqip(lqipInput, lqip, lqipSize, isDevelopment)
|
|
33
27
|
}
|
|
34
28
|
|
|
35
29
|
let svgHTML = ''
|
|
@@ -52,5 +46,5 @@ export async function useLqipImage({
|
|
|
52
46
|
...lqipStyle
|
|
53
47
|
}
|
|
54
48
|
|
|
55
|
-
return { lqipImage, svgHTML, lqipStyle, combinedStyle }
|
|
49
|
+
return { lqipImage, svgHTML, lqipStyle, combinedStyle, resolvedSrc }
|
|
56
50
|
}
|