@son426/vite-image 0.1.4 → 0.1.5

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/LICENSE CHANGED
@@ -20,3 +20,4 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
22
 
23
+
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  - **Bring the power of Next.js's automatic image optimization to your Vite projects.**
6
6
  - **Dedicated to the Vite + React ecosystem.**
7
7
 
8
- BSimply add the plugin to your config, and start using the `<Image />` component immediately. No complex setups, just performant images.
8
+ Simply add the plugin to your config, and start using the `<Image />` component immediately. No complex setups, just performant images.
9
9
 
10
10
  ## ✨ Why use this?
11
11
 
@@ -54,8 +54,9 @@ yarn add @son426/vite-image
54
54
 
55
55
  Just a standard Vite + React project.
56
56
 
57
- vite (>= 4.0.0)
58
- react (>= 18.0.0)
57
+ - vite (>= 4.0.0)
58
+ - react (>= 18.0.0)
59
+ - react-dom (>= 18.0.0)
59
60
 
60
61
  ## Usage
61
62
 
@@ -86,14 +87,7 @@ import Image from "@son426/vite-image/react";
86
87
  import bgImage from "@/assets/image.webp?vite-image";
87
88
 
88
89
  function MyComponent() {
89
- return (
90
- <Image
91
- src={bgImage}
92
- fill={false}
93
- sizes="(max-width: 640px) 100vw, (max-width: 1024px) 100vw, 1920px"
94
- alt="Description"
95
- />
96
- );
90
+ return <Image src={bgImage} fill={false} alt="Description" />;
97
91
  }
98
92
  ```
99
93
 
@@ -103,16 +97,82 @@ The `?vite-image` query automatically generates:
103
97
 
104
98
  - `src`: Optimized image URL
105
99
  - `srcSet`: Responsive srcSet string
106
- - `lqipSrc`: Low Quality Image Placeholder (base64 inline)
100
+ - `blurDataURL`: Low Quality Image Placeholder (base64 inline)
107
101
  - `width` and `height`: Image dimensions
108
102
 
109
- ### Fill Mode
103
+ #### Usage Examples
110
104
 
111
- For images that fill their container (similar to Next.js Image):
105
+ **Basic usage:**
112
106
 
113
- ```typescript
107
+ ```tsx
108
+ import Image from "@son426/vite-image/react";
109
+ import heroImage from "@/assets/hero.jpg?vite-image";
110
+
111
+ <Image src={heroImage} alt="Hero" />;
112
+ ```
113
+
114
+ **Fill mode (container-filling images):**
115
+
116
+ ```tsx
114
117
  <div style={{ position: "relative", width: "100%", height: "400px" }}>
115
- <Image src={bgImage} fill={true} sizes="100vw" alt="Description" />
118
+ <Image src={bgImage} fill alt="Background" />
119
+ </div>
120
+ ```
121
+
122
+ **With priority (LCP images):**
123
+
124
+ ```tsx
125
+ <Image src={heroImage} alt="Hero" priority />
126
+ ```
127
+
128
+ **With blur placeholder:**
129
+
130
+ ```tsx
131
+ <Image src={heroImage} alt="Hero" placeholder="blur" />
132
+ ```
133
+
134
+ **Without placeholder:**
135
+
136
+ ```tsx
137
+ <Image src={heroImage} alt="Hero" placeholder="empty" />
138
+ ```
139
+
140
+ **Custom data URL placeholder:**
141
+
142
+ ```tsx
143
+ <Image src={heroImage} alt="Hero" placeholder="data:image/jpeg;base64,..." />
144
+ ```
145
+
146
+ **Custom sizes:**
147
+
148
+ ```tsx
149
+ <Image src={heroImage} alt="Hero" sizes="(max-width: 768px) 100vw, 50vw" />
150
+ ```
151
+
152
+ **With onLoad and onError callbacks:**
153
+
154
+ ```tsx
155
+ <Image
156
+ src={heroImage}
157
+ alt="Hero"
158
+ onLoad={(e) => console.log("Image loaded", e)}
159
+ onError={(e) => console.error("Image failed to load", e)}
160
+ />
161
+ ```
162
+
163
+ **Combined usage:**
164
+
165
+ ```tsx
166
+ <div style={{ position: "relative", width: "100%", height: "600px" }}>
167
+ <Image
168
+ src={heroImage}
169
+ alt="Hero"
170
+ fill
171
+ priority
172
+ placeholder="blur"
173
+ className="rounded-lg"
174
+ onLoad={(e) => console.log("Loaded")}
175
+ />
116
176
  </div>
117
177
  ```
118
178
 
@@ -120,16 +180,25 @@ For images that fill their container (similar to Next.js Image):
120
180
 
121
181
  ### Image Props
122
182
 
123
- | Prop | Type | Required | Description |
124
- | ----------- | --------------------- | -------- | ------------------------------------------ |
125
- | `src` | `ResponsiveImageData` | Yes | Image data object from `?vite-image` query |
126
- | `fill` | `boolean` | No | Fill container mode (default: `false`) |
127
- | `sizes` | `string` | No | Sizes attribute (default: `"100vw"`) |
128
- | `className` | `string` | No | Additional CSS classes |
129
- | `style` | `CSSProperties` | No | Additional inline styles |
130
- | `...props` | `ImgHTMLAttributes` | No | All standard img element attributes |
131
-
132
- **Note**: The `src` prop must be an object imported from `?vite-image` query. String URLs are not supported. The `width` and `height` are automatically extracted from the `src` object.
183
+ | Prop | Type | Required | Default | Description |
184
+ | ------------- | --------------------------------------------------------- | -------- | --------- | ----------------------------------------------------------------------- |
185
+ | `src` | `ResponsiveImageData` | Yes | - | Image data object from `?vite-image` query |
186
+ | `fill` | `boolean` | No | `false` | Fill container mode (requires parent with `position: relative`) |
187
+ | `sizes` | `string` | No | auto | Sizes attribute (auto-calculated from srcSet if not provided) |
188
+ | `priority` | `boolean` | No | `false` | High priority loading (preload + eager + fetchPriority high) |
189
+ | `placeholder` | `'empty' \| 'blur' \| string` | No | `'empty'` | Placeholder type: `'empty'` (none), `'blur'` (blurDataURL), or data URL |
190
+ | `onLoad` | `(event: React.SyntheticEvent<HTMLImageElement>) => void` | No | - | Callback fired when image loads successfully |
191
+ | `onError` | `(event: React.SyntheticEvent<HTMLImageElement>) => void` | No | - | Callback fired when image fails to load |
192
+ | `className` | `string` | No | - | Additional CSS classes |
193
+ | `style` | `CSSProperties` | No | - | Additional inline styles |
194
+ | `...props` | `ImgHTMLAttributes` | No | - | All standard img element attributes |
195
+
196
+ **Notes**:
197
+
198
+ - The `src` prop must be an object imported from `?vite-image` query. String URLs are not supported.
199
+ - The `width` and `height` are automatically extracted from the `src` object.
200
+ - When `priority={true}`, the image is preloaded using `react-dom`'s `preload` API and loaded with `loading="eager"` and `fetchPriority="high"`.
201
+ - When `sizes` is not provided, it's automatically calculated from `srcSet` breakpoints.
133
202
 
134
203
  ### ResponsiveImageData
135
204
 
@@ -141,7 +210,8 @@ interface ResponsiveImageData {
141
210
  width: number;
142
211
  height: number;
143
212
  srcSet?: string;
144
- lqipSrc?: string;
213
+ lqipSrc?: string; // Deprecated: use blurDataURL instead
214
+ blurDataURL?: string; // Base64 encoded blur placeholder (Next.js Image compatible)
145
215
  }
146
216
  ```
147
217
 
@@ -160,10 +230,12 @@ import type { ImageProps, ResponsiveImageData } from "@son426/vite-image/react";
160
230
 
161
231
  - Responsive srcSet (640px, 1024px, 1920px widths)
162
232
  - Image metadata (1920px width)
163
- - LQIP (20px width, blurred, low quality, inline base64)
233
+ - Blur placeholder (20px width, blurred, low quality, inline base64 as `blurDataURL`)
164
234
 
165
235
  2. **Image Component**: The `<Image />` component handles:
166
- - LQIP display while the main image loads
236
+ - Automatic `sizes` calculation from `srcSet` breakpoints
237
+ - Placeholder display (`blur`, `empty`, or custom data URL)
238
+ - Priority loading with `react-dom`'s `preload` API when `priority={true}`
167
239
  - Responsive image loading with srcSet
168
240
  - Proper aspect ratio maintenance
169
241
  - Fill mode for container-filling images
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import 'react';
2
+ import 'react-dom';
2
3
  import 'react/jsx-runtime';
3
4
  import { imagetools } from 'vite-imagetools';
4
5
 
@@ -24,7 +25,8 @@ function viteImage(options) {
24
25
  width: meta.width,
25
26
  height: meta.height,
26
27
  srcSet: srcSet,
27
- lqipSrc: lqipSrc
28
+ lqipSrc: lqipSrc, // Deprecated: \uD558\uC704 \uD638\uD658\uC131\uC744 \uC704\uD574 \uC720\uC9C0
29
+ blurDataURL: lqipSrc // Next.js Image \uD638\uD658\uC131\uC744 \uC704\uD55C blurDataURL
28
30
  };
29
31
  `;
30
32
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/plugin/index.ts"],"names":[],"mappings":";;;;;AAyBO,SAAS,UAAU,OAAA,EAAkD;AAE1E,EAAA,MAAM,cAAA,GAA+B;AAAA,IACnC,IAAA,EAAM,8BAAA;AAAA,IACN,OAAA,EAAS,KAAA;AAAA,IACT,MAAM,KAAK,EAAA,EAAY;AACrB,MAAA,MAAM,CAAC,QAAA,EAAU,MAAM,CAAA,GAAI,EAAA,CAAG,MAAM,GAAG,CAAA;AACvC,MAAA,MAAM,MAAA,GAAS,IAAI,eAAA,CAAgB,MAAM,CAAA;AAEzC,MAAA,IAAI,MAAA,CAAO,GAAA,CAAI,YAAY,CAAA,EAAG;AAC5B,QAAA,MAAM,YAAA,GAAe,uCAAA;AACrB,QAAA,MAAM,UAAA,GAAa,4BAAA;AACnB,QAAA,MAAM,UAAA,GAAa,2CAAA;AAEnB,QAAA,OAAO;AAAA,8BAAA,EACiB,QAAQ,IAAI,YAAY,CAAA;AAAA,4BAAA,EAC1B,QAAQ,IAAI,UAAU,CAAA;AAAA,+BAAA,EACnB,QAAQ,IAAI,UAAU,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAAA,CAAA;AAAA,MAUjD;AAEA,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,GACF;AAEA,EAAA,OAAO,CAAC,cAAA,EAAgB,UAAA,CAAW,OAAO,CAAC,CAAA;AAC7C","file":"index.js","sourcesContent":["import type { PluginOption } from \"vite\";\nimport { imagetools } from \"vite-imagetools\";\n\nexport type ViteImagePluginOptions = Parameters<typeof imagetools>[0];\n\n/**\n * Vite plugin for image optimization using vite-imagetools\n * This plugin handles ?vite-image queries and uses imagetools for image processing\n *\n * @param options - Options to pass to vite-imagetools\n * @returns Array of Vite plugins (vite-image macro and imagetools)\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { defineConfig } from 'vite';\n * import { viteImage } from '@son426/vite-image/plugin';\n *\n * export default defineConfig({\n * plugins: [\n * ...viteImage(),\n * ],\n * });\n * ```\n */\nexport function viteImage(options?: ViteImagePluginOptions): PluginOption[] {\n // 커스텀 플러그인: ?vite-image 쿼리를 처리\n const viteImageMacro: PluginOption = {\n name: \"vite-plugin-vite-image-macro\",\n enforce: \"pre\" as const,\n async load(id: string) {\n const [basePath, search] = id.split(\"?\");\n const params = new URLSearchParams(search);\n\n if (params.has(\"vite-image\")) {\n const srcSetParams = \"w=640;1024;1920&format=webp&as=srcset\";\n const metaParams = \"w=1920&format=webp&as=meta\";\n const lqipParams = \"w=20&blur=2&quality=20&format=webp&inline\";\n\n return `\n import srcSet from \"${basePath}?${srcSetParams}\";\n import meta from \"${basePath}?${metaParams}\";\n import lqipSrc from \"${basePath}?${lqipParams}\";\n \n export default {\n src: meta.src,\n width: meta.width,\n height: meta.height,\n srcSet: srcSet,\n lqipSrc: lqipSrc\n };\n `;\n }\n\n return null;\n },\n };\n\n return [viteImageMacro, imagetools(options)];\n}\n"]}
1
+ {"version":3,"sources":["../src/plugin/index.ts"],"names":[],"mappings":";;;;;;AAyBO,SAAS,UAAU,OAAA,EAAkD;AAE1E,EAAA,MAAM,cAAA,GAA+B;AAAA,IACnC,IAAA,EAAM,8BAAA;AAAA,IACN,OAAA,EAAS,KAAA;AAAA,IACT,MAAM,KAAK,EAAA,EAAY;AACrB,MAAA,MAAM,CAAC,QAAA,EAAU,MAAM,CAAA,GAAI,EAAA,CAAG,MAAM,GAAG,CAAA;AACvC,MAAA,MAAM,MAAA,GAAS,IAAI,eAAA,CAAgB,MAAM,CAAA;AAEzC,MAAA,IAAI,MAAA,CAAO,GAAA,CAAI,YAAY,CAAA,EAAG;AAC5B,QAAA,MAAM,YAAA,GAAe,uCAAA;AACrB,QAAA,MAAM,UAAA,GAAa,4BAAA;AACnB,QAAA,MAAM,UAAA,GAAa,2CAAA;AAEnB,QAAA,OAAO;AAAA,8BAAA,EACiB,QAAQ,IAAI,YAAY,CAAA;AAAA,4BAAA,EAC1B,QAAQ,IAAI,UAAU,CAAA;AAAA,+BAAA,EACnB,QAAQ,IAAI,UAAU,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAAA,CAAA;AAAA,MAWjD;AAEA,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,GACF;AAEA,EAAA,OAAO,CAAC,cAAA,EAAgB,UAAA,CAAW,OAAO,CAAC,CAAA;AAC7C","file":"index.js","sourcesContent":["import type { PluginOption } from \"vite\";\nimport { imagetools } from \"vite-imagetools\";\n\nexport type ViteImagePluginOptions = Parameters<typeof imagetools>[0];\n\n/**\n * Vite plugin for image optimization using vite-imagetools\n * This plugin handles ?vite-image queries and uses imagetools for image processing\n *\n * @param options - Options to pass to vite-imagetools\n * @returns Array of Vite plugins (vite-image macro and imagetools)\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { defineConfig } from 'vite';\n * import { viteImage } from '@son426/vite-image/plugin';\n *\n * export default defineConfig({\n * plugins: [\n * ...viteImage(),\n * ],\n * });\n * ```\n */\nexport function viteImage(options?: ViteImagePluginOptions): PluginOption[] {\n // 커스텀 플러그인: ?vite-image 쿼리를 처리\n const viteImageMacro: PluginOption = {\n name: \"vite-plugin-vite-image-macro\",\n enforce: \"pre\" as const,\n async load(id: string) {\n const [basePath, search] = id.split(\"?\");\n const params = new URLSearchParams(search);\n\n if (params.has(\"vite-image\")) {\n const srcSetParams = \"w=640;1024;1920&format=webp&as=srcset\";\n const metaParams = \"w=1920&format=webp&as=meta\";\n const lqipParams = \"w=20&blur=2&quality=20&format=webp&inline\";\n\n return `\n import srcSet from \"${basePath}?${srcSetParams}\";\n import meta from \"${basePath}?${metaParams}\";\n import lqipSrc from \"${basePath}?${lqipParams}\";\n \n export default {\n src: meta.src,\n width: meta.width,\n height: meta.height,\n srcSet: srcSet,\n lqipSrc: lqipSrc, // Deprecated: 하위 호환성을 위해 유지\n blurDataURL: lqipSrc // Next.js Image 호환성을 위한 blurDataURL\n };\n `;\n }\n\n return null;\n },\n };\n\n return [viteImageMacro, imagetools(options)];\n}\n"]}
@@ -22,7 +22,8 @@ function viteImage(options) {
22
22
  width: meta.width,
23
23
  height: meta.height,
24
24
  srcSet: srcSet,
25
- lqipSrc: lqipSrc
25
+ lqipSrc: lqipSrc, // Deprecated: \uD558\uC704 \uD638\uD658\uC131\uC744 \uC704\uD574 \uC720\uC9C0
26
+ blurDataURL: lqipSrc // Next.js Image \uD638\uD658\uC131\uC744 \uC704\uD55C blurDataURL
26
27
  };
27
28
  `;
28
29
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/plugin/index.ts"],"names":[],"mappings":";;;AAyBO,SAAS,UAAU,OAAA,EAAkD;AAE1E,EAAA,MAAM,cAAA,GAA+B;AAAA,IACnC,IAAA,EAAM,8BAAA;AAAA,IACN,OAAA,EAAS,KAAA;AAAA,IACT,MAAM,KAAK,EAAA,EAAY;AACrB,MAAA,MAAM,CAAC,QAAA,EAAU,MAAM,CAAA,GAAI,EAAA,CAAG,MAAM,GAAG,CAAA;AACvC,MAAA,MAAM,MAAA,GAAS,IAAI,eAAA,CAAgB,MAAM,CAAA;AAEzC,MAAA,IAAI,MAAA,CAAO,GAAA,CAAI,YAAY,CAAA,EAAG;AAC5B,QAAA,MAAM,YAAA,GAAe,uCAAA;AACrB,QAAA,MAAM,UAAA,GAAa,4BAAA;AACnB,QAAA,MAAM,UAAA,GAAa,2CAAA;AAEnB,QAAA,OAAO;AAAA,8BAAA,EACiB,QAAQ,IAAI,YAAY,CAAA;AAAA,4BAAA,EAC1B,QAAQ,IAAI,UAAU,CAAA;AAAA,+BAAA,EACnB,QAAQ,IAAI,UAAU,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAAA,CAAA;AAAA,MAUjD;AAEA,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,GACF;AAEA,EAAA,OAAO,CAAC,cAAA,EAAgB,UAAA,CAAW,OAAO,CAAC,CAAA;AAC7C","file":"index.js","sourcesContent":["import type { PluginOption } from \"vite\";\nimport { imagetools } from \"vite-imagetools\";\n\nexport type ViteImagePluginOptions = Parameters<typeof imagetools>[0];\n\n/**\n * Vite plugin for image optimization using vite-imagetools\n * This plugin handles ?vite-image queries and uses imagetools for image processing\n *\n * @param options - Options to pass to vite-imagetools\n * @returns Array of Vite plugins (vite-image macro and imagetools)\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { defineConfig } from 'vite';\n * import { viteImage } from '@son426/vite-image/plugin';\n *\n * export default defineConfig({\n * plugins: [\n * ...viteImage(),\n * ],\n * });\n * ```\n */\nexport function viteImage(options?: ViteImagePluginOptions): PluginOption[] {\n // 커스텀 플러그인: ?vite-image 쿼리를 처리\n const viteImageMacro: PluginOption = {\n name: \"vite-plugin-vite-image-macro\",\n enforce: \"pre\" as const,\n async load(id: string) {\n const [basePath, search] = id.split(\"?\");\n const params = new URLSearchParams(search);\n\n if (params.has(\"vite-image\")) {\n const srcSetParams = \"w=640;1024;1920&format=webp&as=srcset\";\n const metaParams = \"w=1920&format=webp&as=meta\";\n const lqipParams = \"w=20&blur=2&quality=20&format=webp&inline\";\n\n return `\n import srcSet from \"${basePath}?${srcSetParams}\";\n import meta from \"${basePath}?${metaParams}\";\n import lqipSrc from \"${basePath}?${lqipParams}\";\n \n export default {\n src: meta.src,\n width: meta.width,\n height: meta.height,\n srcSet: srcSet,\n lqipSrc: lqipSrc\n };\n `;\n }\n\n return null;\n },\n };\n\n return [viteImageMacro, imagetools(options)];\n}\n"]}
1
+ {"version":3,"sources":["../../src/plugin/index.ts"],"names":[],"mappings":";;;AAyBO,SAAS,UAAU,OAAA,EAAkD;AAE1E,EAAA,MAAM,cAAA,GAA+B;AAAA,IACnC,IAAA,EAAM,8BAAA;AAAA,IACN,OAAA,EAAS,KAAA;AAAA,IACT,MAAM,KAAK,EAAA,EAAY;AACrB,MAAA,MAAM,CAAC,QAAA,EAAU,MAAM,CAAA,GAAI,EAAA,CAAG,MAAM,GAAG,CAAA;AACvC,MAAA,MAAM,MAAA,GAAS,IAAI,eAAA,CAAgB,MAAM,CAAA;AAEzC,MAAA,IAAI,MAAA,CAAO,GAAA,CAAI,YAAY,CAAA,EAAG;AAC5B,QAAA,MAAM,YAAA,GAAe,uCAAA;AACrB,QAAA,MAAM,UAAA,GAAa,4BAAA;AACnB,QAAA,MAAM,UAAA,GAAa,2CAAA;AAEnB,QAAA,OAAO;AAAA,8BAAA,EACiB,QAAQ,IAAI,YAAY,CAAA;AAAA,4BAAA,EAC1B,QAAQ,IAAI,UAAU,CAAA;AAAA,+BAAA,EACnB,QAAQ,IAAI,UAAU,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAAA,CAAA;AAAA,MAWjD;AAEA,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,GACF;AAEA,EAAA,OAAO,CAAC,cAAA,EAAgB,UAAA,CAAW,OAAO,CAAC,CAAA;AAC7C","file":"index.js","sourcesContent":["import type { PluginOption } from \"vite\";\nimport { imagetools } from \"vite-imagetools\";\n\nexport type ViteImagePluginOptions = Parameters<typeof imagetools>[0];\n\n/**\n * Vite plugin for image optimization using vite-imagetools\n * This plugin handles ?vite-image queries and uses imagetools for image processing\n *\n * @param options - Options to pass to vite-imagetools\n * @returns Array of Vite plugins (vite-image macro and imagetools)\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { defineConfig } from 'vite';\n * import { viteImage } from '@son426/vite-image/plugin';\n *\n * export default defineConfig({\n * plugins: [\n * ...viteImage(),\n * ],\n * });\n * ```\n */\nexport function viteImage(options?: ViteImagePluginOptions): PluginOption[] {\n // 커스텀 플러그인: ?vite-image 쿼리를 처리\n const viteImageMacro: PluginOption = {\n name: \"vite-plugin-vite-image-macro\",\n enforce: \"pre\" as const,\n async load(id: string) {\n const [basePath, search] = id.split(\"?\");\n const params = new URLSearchParams(search);\n\n if (params.has(\"vite-image\")) {\n const srcSetParams = \"w=640;1024;1920&format=webp&as=srcset\";\n const metaParams = \"w=1920&format=webp&as=meta\";\n const lqipParams = \"w=20&blur=2&quality=20&format=webp&inline\";\n\n return `\n import srcSet from \"${basePath}?${srcSetParams}\";\n import meta from \"${basePath}?${metaParams}\";\n import lqipSrc from \"${basePath}?${lqipParams}\";\n \n export default {\n src: meta.src,\n width: meta.width,\n height: meta.height,\n srcSet: srcSet,\n lqipSrc: lqipSrc, // Deprecated: 하위 호환성을 위해 유지\n blurDataURL: lqipSrc // Next.js Image 호환성을 위한 blurDataURL\n };\n `;\n }\n\n return null;\n },\n };\n\n return [viteImageMacro, imagetools(options)];\n}\n"]}
@@ -10,11 +10,17 @@ interface ResponsiveImageData {
10
10
  height: number;
11
11
  srcSet?: string;
12
12
  lqipSrc?: string;
13
+ blurDataURL?: string;
13
14
  }
14
15
 
16
+ type PlaceholderValue = "empty" | "blur" | string;
15
17
  interface BaseImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "srcSet" | "width" | "height"> {
16
18
  src: ResponsiveImageData;
17
19
  sizes?: string;
20
+ placeholder?: PlaceholderValue;
21
+ priority?: boolean;
22
+ onLoad?: (event: React.SyntheticEvent<HTMLImageElement, Event>) => void;
23
+ onError?: (event: React.SyntheticEvent<HTMLImageElement, Event>) => void;
18
24
  }
19
25
  interface FillImageProps extends BaseImageProps {
20
26
  fill: true;
@@ -24,6 +30,8 @@ interface StandardImageProps extends BaseImageProps {
24
30
  }
25
31
  type ImageProps = FillImageProps | StandardImageProps;
26
32
  declare function Image({ src, // 이제 이 src는 객체입니다.
27
- fill, sizes, className, style, ...props }: ImageProps): react_jsx_runtime.JSX.Element;
33
+ fill, sizes, placeholder, // 기본값: empty (Next.js Image 호환)
34
+ priority, // 기본값: false (Next.js Image 호환)
35
+ className, style, onLoad, onError, ...props }: ImageProps): react_jsx_runtime.JSX.Element;
28
36
 
29
37
  export { type ImageProps, type ResponsiveImageData, Image as default };
@@ -1,14 +1,44 @@
1
1
  import { useState } from 'react';
2
+ import { preload } from 'react-dom';
2
3
  import { jsxs, jsx } from 'react/jsx-runtime';
3
4
 
4
5
  // src/react/Image.tsx
6
+ function generateSizesFromSrcSet(srcSet) {
7
+ if (!srcSet) {
8
+ return "100vw";
9
+ }
10
+ const widthMatches = srcSet.match(/(\d+)w/g);
11
+ if (!widthMatches || widthMatches.length === 0) {
12
+ return "100vw";
13
+ }
14
+ const breakpoints = widthMatches.map((match) => parseInt(match.replace("w", ""), 10)).sort((a, b) => a - b);
15
+ if (breakpoints.length === 0) {
16
+ return "100vw";
17
+ }
18
+ const sizeParts = [];
19
+ for (let i = 0; i < breakpoints.length; i++) {
20
+ const breakpoint = breakpoints[i];
21
+ if (i === breakpoints.length - 1) {
22
+ sizeParts.push(`${breakpoint}px`);
23
+ } else {
24
+ sizeParts.push(`(max-width: ${breakpoint}px) 100vw`);
25
+ }
26
+ }
27
+ return sizeParts.join(", ");
28
+ }
5
29
  function Image({
6
30
  src,
7
31
  // 이제 이 src는 객체입니다.
8
32
  fill = false,
9
- sizes = "100vw",
33
+ sizes,
34
+ placeholder = "empty",
35
+ // 기본값: empty (Next.js Image 호환)
36
+ priority = false,
37
+ // 기본값: false (Next.js Image 호환)
10
38
  className = "",
11
39
  style,
40
+ onLoad,
41
+ onError,
12
42
  ...props
13
43
  }) {
14
44
  const [isImageLoaded, setIsImageLoaded] = useState(false);
@@ -16,9 +46,33 @@ function Image({
16
46
  src: currentSrc,
17
47
  srcSet: currentSrcSet,
18
48
  lqipSrc: currentLqip,
49
+ blurDataURL,
19
50
  width: currentWidth,
20
51
  height: currentHeight
21
52
  } = src;
53
+ const computedSizes = sizes ?? (fill ? "100vw" : generateSizesFromSrcSet(currentSrcSet));
54
+ if (priority && currentSrc) {
55
+ preload(currentSrc, {
56
+ as: "image",
57
+ fetchPriority: "high",
58
+ ...currentSrcSet ? { imageSrcSet: currentSrcSet } : {},
59
+ ...computedSizes ? { imageSizes: computedSizes } : {}
60
+ });
61
+ }
62
+ const getPlaceholderSrc = () => {
63
+ if (placeholder === "empty") {
64
+ return void 0;
65
+ }
66
+ if (placeholder === "blur") {
67
+ return blurDataURL ?? currentLqip;
68
+ }
69
+ if (placeholder.startsWith("data:image/")) {
70
+ return placeholder;
71
+ }
72
+ return void 0;
73
+ };
74
+ const placeholderSrc = getPlaceholderSrc();
75
+ const hasShowPlaceholder = !!placeholderSrc;
22
76
  const containerStyle = fill ? {
23
77
  position: "absolute",
24
78
  top: 0,
@@ -46,13 +100,17 @@ function Image({
46
100
  height: "100%",
47
101
  objectFit: "cover"
48
102
  };
49
- const lqipStyle = {
103
+ const placeholderStyle = {
50
104
  ...imgStyle,
51
- filter: "blur(20px)",
52
- transform: "scale(1.1)",
53
105
  transition: "opacity 500ms ease-out",
54
106
  opacity: isImageLoaded ? 0 : 1,
55
- zIndex: 1
107
+ zIndex: 1,
108
+ pointerEvents: "none",
109
+ // blur placeholder일 때만 blur 효과 적용
110
+ ...placeholder === "blur" ? {
111
+ filter: "blur(20px)",
112
+ transform: "scale(1.1)"
113
+ } : {}
56
114
  };
57
115
  return /* @__PURE__ */ jsxs("div", { className, style: mergedContainerStyle, children: [
58
116
  /* @__PURE__ */ jsx(
@@ -61,14 +119,28 @@ function Image({
61
119
  ...props,
62
120
  src: currentSrc,
63
121
  srcSet: currentSrcSet,
64
- sizes,
122
+ sizes: computedSizes,
65
123
  width: fill ? void 0 : currentWidth,
66
124
  height: fill ? void 0 : currentHeight,
67
- onLoad: () => setIsImageLoaded(true),
125
+ loading: priority ? "eager" : "lazy",
126
+ fetchPriority: priority ? "high" : void 0,
127
+ onLoad: (e) => {
128
+ setIsImageLoaded(true);
129
+ onLoad?.(e);
130
+ },
131
+ onError,
68
132
  style: { ...imgStyle, zIndex: 0 }
69
133
  }
70
134
  ),
71
- currentLqip && /* @__PURE__ */ jsx("img", { src: currentLqip, alt: "", "aria-hidden": "true", style: lqipStyle })
135
+ hasShowPlaceholder && /* @__PURE__ */ jsx(
136
+ "img",
137
+ {
138
+ src: placeholderSrc,
139
+ alt: "",
140
+ "aria-hidden": "true",
141
+ style: placeholderStyle
142
+ }
143
+ )
72
144
  ] });
73
145
  }
74
146
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/Image.tsx"],"names":[],"mappings":";;;;AAwBe,SAAR,KAAA,CAAuB;AAAA,EAC5B,GAAA;AAAA;AAAA,EACA,IAAA,GAAO,KAAA;AAAA,EACP,KAAA,GAAQ,OAAA;AAAA,EACR,SAAA,GAAY,EAAA;AAAA,EACZ,KAAA;AAAA,EACA,GAAG;AACL,CAAA,EAAe;AACb,EAAA,MAAM,CAAC,aAAA,EAAe,gBAAgB,CAAA,GAAI,SAAS,KAAK,CAAA;AAGxD,EAAA,MAAM;AAAA,IACJ,GAAA,EAAK,UAAA;AAAA,IACL,MAAA,EAAQ,aAAA;AAAA,IACR,OAAA,EAAS,WAAA;AAAA,IACT,KAAA,EAAO,YAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACV,GAAI,GAAA;AAGJ,EAAA,MAAM,iBAAgC,IAAA,GAClC;AAAA,IACE,QAAA,EAAU,UAAA;AAAA,IACV,GAAA,EAAK,CAAA;AAAA,IACL,IAAA,EAAM,CAAA;AAAA,IACN,KAAA,EAAO,CAAA;AAAA,IACP,MAAA,EAAQ,CAAA;AAAA,IACR,KAAA,EAAO,MAAA;AAAA,IACP,MAAA,EAAQ,MAAA;AAAA,IACR,QAAA,EAAU;AAAA,GACZ,GACA;AAAA,IACE,QAAA,EAAU,UAAA;AAAA,IACV,KAAA,EAAO,MAAA;AAAA,IACP,QAAA,EAAU,QAAA;AAAA;AAAA,IAEV,GAAI,YAAA,IAAgB,aAAA,GAChB,EAAE,WAAA,EAAa,CAAA,EAAG,YAAY,CAAA,GAAA,EAAM,aAAa,CAAA,CAAA,EAAG,GACpD;AAAC,GACP;AAEJ,EAAA,MAAM,oBAAA,GAAuB,EAAE,GAAG,cAAA,EAAgB,GAAG,KAAA,EAAM;AAG3D,EAAA,MAAM,QAAA,GAA0B;AAAA,IAC9B,QAAA,EAAU,UAAA;AAAA,IACV,GAAA,EAAK,CAAA;AAAA,IACL,IAAA,EAAM,CAAA;AAAA,IACN,KAAA,EAAO,CAAA;AAAA,IACP,MAAA,EAAQ,CAAA;AAAA,IACR,KAAA,EAAO,MAAA;AAAA,IACP,MAAA,EAAQ,MAAA;AAAA,IACR,SAAA,EAAW;AAAA,GACb;AAGA,EAAA,MAAM,SAAA,GAA2B;AAAA,IAC/B,GAAG,QAAA;AAAA,IACH,MAAA,EAAQ,YAAA;AAAA,IACR,SAAA,EAAW,YAAA;AAAA,IACX,UAAA,EAAY,wBAAA;AAAA,IACZ,OAAA,EAAS,gBAAgB,CAAA,GAAI,CAAA;AAAA,IAC7B,MAAA,EAAQ;AAAA,GACV;AAEA,EAAA,uBACE,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAsB,KAAA,EAAO,oBAAA,EAEhC,QAAA,EAAA;AAAA,oBAAA,GAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACE,GAAG,KAAA;AAAA,QACJ,GAAA,EAAK,UAAA;AAAA,QACL,MAAA,EAAQ,aAAA;AAAA,QACR,KAAA;AAAA,QACA,KAAA,EAAO,OAAO,MAAA,GAAY,YAAA;AAAA,QAC1B,MAAA,EAAQ,OAAO,MAAA,GAAY,aAAA;AAAA,QAC3B,MAAA,EAAQ,MAAM,gBAAA,CAAiB,IAAI,CAAA;AAAA,QACnC,KAAA,EAAO,EAAE,GAAG,QAAA,EAAU,QAAQ,CAAA;AAAE;AAAA,KAClC;AAAA,IAGC,WAAA,oBACC,GAAA,CAAC,KAAA,EAAA,EAAI,GAAA,EAAK,WAAA,EAAa,KAAI,EAAA,EAAG,aAAA,EAAY,MAAA,EAAO,KAAA,EAAO,SAAA,EAAW;AAAA,GAAA,EAEvE,CAAA;AAEJ","file":"index.js","sourcesContent":["import { useState, type ImgHTMLAttributes, type CSSProperties } from \"react\";\nimport type { ResponsiveImageData } from \"../types\";\n\n// ?vite-image import 결과 타입\ninterface BaseImageProps\n extends Omit<\n ImgHTMLAttributes<HTMLImageElement>,\n \"src\" | \"srcSet\" | \"width\" | \"height\"\n > {\n // 핵심 변경: src는 무조건 최적화된 이미지 객체만 받음\n src: ResponsiveImageData;\n sizes?: string;\n}\n\ninterface FillImageProps extends BaseImageProps {\n fill: true;\n}\n\ninterface StandardImageProps extends BaseImageProps {\n fill?: false | undefined;\n}\n\nexport type ImageProps = FillImageProps | StandardImageProps;\n\nexport default function Image({\n src, // 이제 이 src는 객체입니다.\n fill = false,\n sizes = \"100vw\",\n className = \"\",\n style,\n ...props\n}: ImageProps) {\n const [isImageLoaded, setIsImageLoaded] = useState(false);\n\n // 1. 데이터 추출: src 객체에서 바로 꺼내씀 (병합 로직 제거)\n const {\n src: currentSrc,\n srcSet: currentSrcSet,\n lqipSrc: currentLqip,\n width: currentWidth,\n height: currentHeight,\n } = src;\n\n // 2. 컨테이너 스타일 계산 (기존 로직 유지)\n const containerStyle: CSSProperties = fill\n ? {\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n width: \"100%\",\n height: \"100%\",\n overflow: \"hidden\",\n }\n : {\n position: \"relative\",\n width: \"100%\",\n overflow: \"hidden\",\n // Standard 모드일 때 aspect-ratio 처리\n ...(currentWidth && currentHeight\n ? { aspectRatio: `${currentWidth} / ${currentHeight}` }\n : {}),\n };\n\n const mergedContainerStyle = { ...containerStyle, ...style };\n\n // 3. 실제 이미지 스타일 (기존 로직 유지)\n const imgStyle: CSSProperties = {\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n };\n\n // 4. LQIP 스타일 (기존 로직 유지)\n const lqipStyle: CSSProperties = {\n ...imgStyle,\n filter: \"blur(20px)\",\n transform: \"scale(1.1)\",\n transition: \"opacity 500ms ease-out\",\n opacity: isImageLoaded ? 0 : 1,\n zIndex: 1,\n };\n\n return (\n <div className={className} style={mergedContainerStyle}>\n {/* 실제 이미지 */}\n <img\n {...props}\n src={currentSrc}\n srcSet={currentSrcSet}\n sizes={sizes}\n width={fill ? undefined : currentWidth}\n height={fill ? undefined : currentHeight}\n onLoad={() => setIsImageLoaded(true)}\n style={{ ...imgStyle, zIndex: 0 }}\n />\n\n {/* LQIP 레이어 */}\n {currentLqip && (\n <img src={currentLqip} alt=\"\" aria-hidden=\"true\" style={lqipStyle} />\n )}\n </div>\n );\n}\n"]}
1
+ {"version":3,"sources":["../../src/react/Image.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { useState, type ImgHTMLAttributes, type CSSProperties } from \"react\";\nimport { preload } from \"react-dom\";\nimport type { ResponsiveImageData } from \"../types\";\n\n// placeholder 타입 정의 (Next.js Image 호환)\ntype PlaceholderValue = \"empty\" | \"blur\" | string; // string은 data:image/... 형식\n\n// ?vite-image import 결과 타입\ninterface BaseImageProps\n extends Omit<\n ImgHTMLAttributes<HTMLImageElement>,\n \"src\" | \"srcSet\" | \"width\" | \"height\"\n > {\n // 핵심 변경: src는 무조건 최적화된 이미지 객체만 받음\n src: ResponsiveImageData;\n sizes?: string; // Optional: 제공되지 않으면 자동 계산\n placeholder?: PlaceholderValue; // Next.js Image 호환: 'empty' | 'blur' | 'data:image/...'\n priority?: boolean; // Next.js Image 호환: true일 경우 높은 우선순위로 preload\n onLoad?: (event: React.SyntheticEvent<HTMLImageElement, Event>) => void;\n onError?: (event: React.SyntheticEvent<HTMLImageElement, Event>) => void;\n}\n\ninterface FillImageProps extends BaseImageProps {\n fill: true;\n}\n\ninterface StandardImageProps extends BaseImageProps {\n fill?: false | undefined;\n}\n\nexport type ImageProps = FillImageProps | StandardImageProps;\n\n/**\n * srcSet에서 breakpoints를 추출하여 sizes 문자열을 자동 생성\n * @param srcSet - \"url1 640w, url2 1024w, url3 1920w\" 형식의 문자열\n * @returns sizes 속성 문자열\n */\nfunction generateSizesFromSrcSet(srcSet?: string): string {\n if (!srcSet) {\n return \"100vw\";\n }\n\n // srcSet에서 width 값들 추출 (예: \"640w\", \"1024w\", \"1920w\")\n const widthMatches = srcSet.match(/(\\d+)w/g);\n if (!widthMatches || widthMatches.length === 0) {\n return \"100vw\";\n }\n\n // width 값들을 숫자로 변환하고 정렬\n const breakpoints = widthMatches\n .map((match) => parseInt(match.replace(\"w\", \"\"), 10))\n .sort((a, b) => a - b);\n\n if (breakpoints.length === 0) {\n return \"100vw\";\n }\n\n // breakpoints를 기반으로 sizes 문자열 생성\n // 예: \"(max-width: 640px) 100vw, (max-width: 1024px) 100vw, 1920px\"\n const sizeParts: string[] = [];\n\n for (let i = 0; i < breakpoints.length; i++) {\n const breakpoint = breakpoints[i];\n if (i === breakpoints.length - 1) {\n // 마지막 breakpoint는 최대 크기로 설정\n sizeParts.push(`${breakpoint}px`);\n } else {\n // 중간 breakpoint들은 미디어 쿼리로 설정\n sizeParts.push(`(max-width: ${breakpoint}px) 100vw`);\n }\n }\n\n return sizeParts.join(\", \");\n}\n\nexport default function Image({\n src, // 이제 이 src는 객체입니다.\n fill = false,\n sizes,\n placeholder = \"empty\", // 기본값: empty (Next.js Image 호환)\n priority = false, // 기본값: false (Next.js Image 호환)\n className = \"\",\n style,\n onLoad,\n onError,\n ...props\n}: ImageProps) {\n const [isImageLoaded, setIsImageLoaded] = useState(false);\n\n // 1. 데이터 추출: src 객체에서 바로 꺼내씀 (병합 로직 제거)\n const {\n src: currentSrc,\n srcSet: currentSrcSet,\n lqipSrc: currentLqip,\n blurDataURL,\n width: currentWidth,\n height: currentHeight,\n } = src;\n\n // 2. sizes 자동 계산: 제공되지 않으면 srcSet 기반으로 자동 생성\n const computedSizes =\n sizes ?? (fill ? \"100vw\" : generateSizesFromSrcSet(currentSrcSet));\n\n if (priority && currentSrc) {\n preload(currentSrc, {\n as: \"image\",\n fetchPriority: \"high\",\n ...(currentSrcSet ? { imageSrcSet: currentSrcSet } : {}),\n ...(computedSizes ? { imageSizes: computedSizes } : {}),\n });\n }\n\n // 3. placeholder 처리 (Next.js Image 호환)\n const getPlaceholderSrc = (): string | undefined => {\n if (placeholder === \"empty\") {\n return undefined;\n }\n if (placeholder === \"blur\") {\n // blurDataURL 우선, 없으면 lqipSrc 사용 (하위 호환성)\n return blurDataURL ?? currentLqip;\n }\n // data:image/... 형식의 직접 제공된 placeholder\n if (placeholder.startsWith(\"data:image/\")) {\n return placeholder;\n }\n // 기본값: empty\n return undefined;\n };\n\n const placeholderSrc = getPlaceholderSrc();\n const hasShowPlaceholder = !!placeholderSrc;\n\n // 5. 컨테이너 스타일 계산 (기존 로직 유지)\n const containerStyle: CSSProperties = fill\n ? {\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n width: \"100%\",\n height: \"100%\",\n overflow: \"hidden\",\n }\n : {\n position: \"relative\",\n width: \"100%\",\n overflow: \"hidden\",\n // Standard 모드일 때 aspect-ratio 처리\n ...(currentWidth && currentHeight\n ? { aspectRatio: `${currentWidth} / ${currentHeight}` }\n : {}),\n };\n\n const mergedContainerStyle = { ...containerStyle, ...style };\n\n // 6. 실제 이미지 스타일 (기존 로직 유지)\n const imgStyle: CSSProperties = {\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n };\n\n // 7. Placeholder 스타일 (blur 모드일 때만 blur 효과 적용)\n const placeholderStyle: CSSProperties = {\n ...imgStyle,\n transition: \"opacity 500ms ease-out\",\n opacity: isImageLoaded ? 0 : 1,\n zIndex: 1,\n pointerEvents: \"none\",\n\n // blur placeholder일 때만 blur 효과 적용\n ...(placeholder === \"blur\"\n ? {\n filter: \"blur(20px)\",\n transform: \"scale(1.1)\",\n }\n : {}),\n };\n\n return (\n <div className={className} style={mergedContainerStyle}>\n {/* 실제 이미지 */}\n <img\n {...props}\n src={currentSrc}\n srcSet={currentSrcSet}\n sizes={computedSizes}\n width={fill ? undefined : currentWidth}\n height={fill ? undefined : currentHeight}\n loading={priority ? \"eager\" : \"lazy\"}\n fetchPriority={priority ? \"high\" : undefined}\n onLoad={(e) => {\n setIsImageLoaded(true);\n onLoad?.(e);\n }}\n onError={onError}\n style={{ ...imgStyle, zIndex: 0 }}\n />\n\n {/* Placeholder 레이어 (placeholder prop에 따라 표시) */}\n {hasShowPlaceholder && (\n <img\n src={placeholderSrc}\n alt=\"\"\n aria-hidden=\"true\"\n style={placeholderStyle}\n />\n )}\n </div>\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@son426/vite-image",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "A Vite plugin and React component for optimized images with LQIP support using vite-imagetools",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -52,12 +52,15 @@
52
52
  },
53
53
  "peerDependencies": {
54
54
  "react": "^18.0.0 || ^19.0.0",
55
+ "react-dom": "^18.0.0 || ^19.0.0",
55
56
  "vite": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@types/node": "^20.0.0",
59
60
  "@types/react": "^18.0.0 || ^19.0.0",
61
+ "@types/react-dom": "^19.2.3",
60
62
  "react": "^19.1.0",
63
+ "react-dom": "^19.1.0",
61
64
  "tsup": "^8.0.0",
62
65
  "typescript": "^5.0.0",
63
66
  "vite": "^7.0.4"