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 CHANGED
@@ -62,8 +62,8 @@ Example:
62
62
  ---
63
63
  import { Image, Picture } from 'astro-lqip/components';
64
64
 
65
- import image from './path/to/image.png';
66
- import otherImage from './path/to/other-image.png';
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/path/to/image.png" alt="Cover Image" width={220} height={220} />
84
- <Picture src="/src/path/to/other-image.png" alt="Other Image" width={220} height={220} />
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
- <Image src="../path/to/image.png" alt="Cover Image" width={220} height={220} />
95
- <Picture src="../path/to/other-image.png" alt="Other Image" width={220} height={220} />
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 './path/to/image.png';
132
- import otherImage from './path/to/other-image.png';
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.6.0",
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.37.0",
41
- "@typescript-eslint/parser": "8.46.1",
42
- "astro": "5.14.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.37.0",
45
- "eslint-plugin-astro": "1.3.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.56.4",
48
+ "eslint-plugin-package-json": "0.71.0",
49
49
  "eslint-plugin-yml": "1.19.0",
50
- "globals": "16.4.0",
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 = {
@@ -7,5 +7,4 @@ export type ComponentsOptions = {
7
7
  styleProps: Record<string, string | number | undefined>
8
8
  forbiddenVars: string[]
9
9
  isDevelopment: boolean | undefined
10
- isPrerendered: boolean | undefined
11
10
  }
@@ -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 buffer = await readFile(imagePath)
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
 
@@ -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
- const lqip = await generateLqip(tempPath, lqipType, lqipSize, isDevelopment)
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 (!isPrerendered && !isDevelopment) {
55
- const filePath = join(process.cwd(), 'dist', 'client', imagePath.src)
56
- return await generateLqip(filePath, lqipType, lqipSize, isDevelopment)
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
- if (!isDevelopment && imagePath.src.startsWith('/_astro/')) {
60
- const buildPath = join(process.cwd(), 'dist', imagePath.src)
61
- return await generateLqip(buildPath, lqipType, lqipSize, isDevelopment)
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, isPrerendered)
26
+ lqipImage = await getLqip(lqipInput, lqip, lqipSize, isDevelopment)
28
27
  }
29
28
 
30
29
  let svgHTML = ''