@pracht/image 0.1.1 → 0.2.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 CHANGED
@@ -16,10 +16,14 @@ next/image's loader pattern.
16
16
  [sharp](https://sharp.pixelplumbing.com) (optional peer dependency) with a
17
17
  trusted-local-origin/remote-allowlist security model and revalidated cache
18
18
  headers.
19
+ - Build-time `?pracht` image imports (`@pracht/image/vite`): Vite-managed asset
20
+ URL (hashed source assets or stable `publicDir` URLs), intrinsic dimensions
21
+ (EXIF-orientation aware), and a tiny inline `blurDataURL` for CSS-only
22
+ `placeholder="blur"`.
19
23
 
20
24
  ```bash
21
25
  pnpm add @pracht/image
22
- pnpm add sharp # only needed for the built-in endpoint
26
+ pnpm add sharp # only needed for the built-in endpoint and ?pracht imports
23
27
  ```
24
28
 
25
29
  ```tsx
@@ -28,6 +32,13 @@ import { Image } from "@pracht/image";
28
32
  <Image src="/banner.jpg" alt="Banner" width={1200} height={280} priority />;
29
33
  ```
30
34
 
35
+ ```tsx
36
+ // vite.config.ts: plugins: [pracht({ … }), prachtImage()] (from "@pracht/image/vite")
37
+ import hero from "./hero.jpg?pracht"; // { src, width, height, blurDataURL }
38
+
39
+ <Image src={hero} alt="Hero" placeholder="blur" />;
40
+ ```
41
+
31
42
  ```ts
32
43
  // src/api/_pracht/image.ts — mounts the optimization endpoint
33
44
  import { createImageHandler } from "@pracht/image/node";
package/client.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ // Type declarations for build-time `?pracht` image imports (enabled by the
2
+ // `prachtImage()` Vite plugin from "@pracht/image/vite").
3
+ //
4
+ // Reference this file once in your app, either with a triple-slash directive
5
+ // in any .d.ts file:
6
+ //
7
+ // /// <reference types="@pracht/image/client" />
8
+ //
9
+ // or via tsconfig: `"types": ["@pracht/image/client"]`.
10
+
11
+ declare module "*?pracht" {
12
+ const metadata: import("@pracht/image").PrachtImageMetadata;
13
+ export const src: string;
14
+ export const width: number;
15
+ export const height: number;
16
+ /** Undefined for SVG sources (vectors scale cleanly without a blur). */
17
+ export const blurDataURL: string | undefined;
18
+ export default metadata;
19
+ }
package/dist/index.d.mts CHANGED
@@ -1,3 +1,4 @@
1
+ import { t as PrachtImageMetadata } from "./metadata-CA97TjOR.mjs";
1
2
  import { JSX, VNode } from "preact";
2
3
 
3
4
  //#region src/loaders.d.ts
@@ -48,9 +49,13 @@ declare const vercelLoader: ImageLoader;
48
49
  declare const passthroughLoader: ImageLoader;
49
50
  //#endregion
50
51
  //#region src/image.d.ts
51
- interface ImageProps extends Omit<JSX.HTMLAttributes<HTMLImageElement>, "src" | "srcset" | "srcSet" | "width" | "height" | "sizes" | "loading" | "alt" | "style"> {
52
- /** Source path (`/hero.jpg`) or absolute URL. Passed to the loader. */
53
- src: string;
52
+ interface ImageProps extends Omit<JSX.HTMLAttributes<HTMLImageElement>, "src" | "srcset" | "srcSet" | "width" | "height" | "sizes" | "loading" | "alt" | "style" | "placeholder"> {
53
+ /**
54
+ * Source path (`/hero.jpg`), absolute URL, or the metadata object from a
55
+ * build-time `?pracht` import (which supplies `width`, `height`, and
56
+ * `blurDataURL` automatically).
57
+ */
58
+ src: string | PrachtImageMetadata;
54
59
  /** Required for accessibility. Use `alt=""` for decorative images. */
55
60
  alt: string;
56
61
  /** Intrinsic width in pixels. Required unless `fill` is set. */
@@ -74,6 +79,22 @@ interface ImageProps extends Omit<JSX.HTMLAttributes<HTMLImageElement>, "src" |
74
79
  loading?: "lazy" | "eager";
75
80
  /** Per-component loader override; falls back to the configured loader. */
76
81
  loader?: ImageLoader;
82
+ /**
83
+ * `"blur"` paints a tiny inline preview behind the image while it loads.
84
+ * Requires a `blurDataURL` — supplied automatically when `src` is a
85
+ * `?pracht` import, or pass it by hand. The placeholder is pure CSS
86
+ * (a `background-image` on the `<img>` itself), so it needs no hydration
87
+ * and works with `hydration: "none"`; the real image simply covers it once
88
+ * it paints. Note: images with transparency show the placeholder through
89
+ * transparent regions — prefer `placeholder="empty"` for those.
90
+ */
91
+ placeholder?: "blur" | "empty";
92
+ /**
93
+ * `data:image/…` URI painted behind the image when `placeholder="blur"`.
94
+ * Values that are not well-formed image data URIs are ignored (they could
95
+ * otherwise inject CSS via the style attribute).
96
+ */
97
+ blurDataURL?: string;
77
98
  style?: string | JSX.CSSProperties;
78
99
  }
79
100
  /**
@@ -114,4 +135,4 @@ declare function getImageConfig(): ImageConfig;
114
135
  /** Restore the default configuration. Primarily useful in tests. */
115
136
  declare function resetImageConfig(): void;
116
137
  //#endregion
117
- export { DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_ENDPOINT, DEFAULT_IMAGE_SIZES, DEFAULT_QUALITY, Image, type ImageConfig, type ImageLoader, type ImageLoaderArgs, type ImageProps, cloudflareLoader, configureImage, createDefaultLoader, defaultLoader, getImageConfig, passthroughLoader, resetImageConfig, vercelLoader };
138
+ export { DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_ENDPOINT, DEFAULT_IMAGE_SIZES, DEFAULT_QUALITY, Image, type ImageConfig, type ImageLoader, type ImageLoaderArgs, type ImageProps, type PrachtImageMetadata, cloudflareLoader, configureImage, createDefaultLoader, defaultLoader, getImageConfig, passthroughLoader, resetImageConfig, vercelLoader };
package/dist/index.mjs CHANGED
@@ -11,6 +11,19 @@ const FILL_STYLE = {
11
11
  bottom: 0
12
12
  };
13
13
  const FILL_STYLE_STRING = "position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;";
14
+ const BLUR_DATA_URL_PATTERN = /^data:image\/[a-z0-9.+-]+(?:;[a-z0-9=+-]+)*,[a-z0-9+/=._%-]*$/i;
15
+ function blurBackground(blurDataURL) {
16
+ const image = `url("${blurDataURL}")`;
17
+ return {
18
+ styleString: `background-image:${image};background-size:cover;background-position:50% 50%;background-repeat:no-repeat;`,
19
+ styleObject: {
20
+ backgroundImage: image,
21
+ backgroundSize: "cover",
22
+ backgroundPosition: "50% 50%",
23
+ backgroundRepeat: "no-repeat"
24
+ }
25
+ };
26
+ }
14
27
  const warned = /* @__PURE__ */ new Set();
15
28
  function getNodeEnv() {
16
29
  return globalThis.process?.env?.NODE_ENV;
@@ -84,31 +97,42 @@ function planSrcSet(deviceSizes, imageSizes, width, sizes) {
84
97
  * loader (see `configureImage()` and the `loader` prop).
85
98
  */
86
99
  function Image(props) {
87
- const { src, alt, width, height, fill = false, sizes, quality, priority = false, loading, loader, style, ...rest } = props;
100
+ const { src, alt, width, height, fill = false, sizes, quality, priority = false, loading, loader, placeholder = "empty", blurDataURL, style, ...rest } = props;
101
+ const metadata = typeof src === "string" ? void 0 : src;
102
+ const srcString = typeof src === "string" ? src : src.src;
88
103
  const config = getImageConfig();
89
104
  const resolvedLoader = loader ?? config.loader;
90
105
  const resolvedQuality = quality ?? config.quality;
91
- const numericWidth = toDimension(width);
92
- const numericHeight = toDimension(height);
106
+ const numericWidth = toDimension(width) ?? (fill ? void 0 : metadata?.width);
107
+ const numericHeight = toDimension(height) ?? (fill ? void 0 : metadata?.height);
108
+ const resolvedBlurDataURL = placeholder === "blur" ? blurDataURL ?? metadata?.blurDataURL : void 0;
109
+ const safeBlurDataURL = resolvedBlurDataURL != null && BLUR_DATA_URL_PATTERN.test(resolvedBlurDataURL) ? resolvedBlurDataURL : void 0;
93
110
  if (isDevWarningsEnabled()) {
94
- if (!fill && (numericWidth == null || numericHeight == null)) warnOnce(`dimensions:${src}`, `[pracht/image] <Image src="${src}"> is missing required "width" and "height" props. Provide the intrinsic dimensions (or use the "fill" prop) so the browser can reserve space and avoid layout shift.`);
95
- if (fill && (width != null || height != null)) warnOnce(`fill-dimensions:${src}`, `[pracht/image] <Image src="${src}"> uses "fill" together with "width"/"height". "fill" images size themselves to their positioned parent; remove the explicit dimensions.`);
111
+ if (!fill && (numericWidth == null || numericHeight == null)) warnOnce(`dimensions:${srcString}`, `[pracht/image] <Image src="${srcString}"> is missing required "width" and "height" props. Provide the intrinsic dimensions (or use the "fill" prop) so the browser can reserve space and avoid layout shift.`);
112
+ if (fill && (width != null || height != null)) warnOnce(`fill-dimensions:${srcString}`, `[pracht/image] <Image src="${srcString}"> uses "fill" together with "width"/"height". "fill" images size themselves to their positioned parent; remove the explicit dimensions.`);
113
+ if (placeholder === "blur" && resolvedBlurDataURL == null) warnOnce(`blur-missing:${srcString}`, `[pracht/image] <Image src="${srcString}"> uses placeholder="blur" without a blurDataURL. Import the image with the "?pracht" query (via prachtImage() from "@pracht/image/vite") or pass a blurDataURL prop. Rendering without a placeholder.`);
114
+ if (resolvedBlurDataURL != null && safeBlurDataURL == null) warnOnce(`blur-invalid:${srcString}`, `[pracht/image] <Image src="${srcString}"> received a blurDataURL that is not a well-formed "data:image/…" URI. It was ignored because interpolating arbitrary strings into the style attribute could inject CSS.`);
96
115
  }
97
116
  const effectiveSizes = sizes ?? (fill ? "100vw" : void 0);
98
117
  const plan = planSrcSet(config.deviceSizes, config.imageSizes, numericWidth, effectiveSizes);
99
118
  const candidates = plan.widths.map((candidateWidth) => resolvedLoader({
100
- src,
119
+ src: srcString,
101
120
  width: candidateWidth,
102
121
  quality: resolvedQuality
103
122
  }));
104
123
  const largestSrc = candidates[candidates.length - 1];
105
124
  const optimized = new Set(candidates).size > 1;
106
125
  const srcset = optimized ? candidates.map((url, index) => plan.descriptor === "w" ? `${url} ${plan.widths[index]}w` : `${url} ${index + 1}x`).join(", ") : void 0;
126
+ const blur = safeBlurDataURL != null ? blurBackground(safeBlurDataURL) : void 0;
107
127
  let mergedStyle = style;
108
- if (fill) mergedStyle = typeof style === "string" ? `${FILL_STYLE_STRING}${style}` : {
109
- ...FILL_STYLE,
110
- ...style
111
- };
128
+ if (blur || fill) {
129
+ const baseString = `${blur?.styleString ?? ""}${fill ? FILL_STYLE_STRING : ""}`;
130
+ mergedStyle = typeof style === "string" ? `${baseString}${style}` : {
131
+ ...blur?.styleObject,
132
+ ...fill ? FILL_STYLE : void 0,
133
+ ...style
134
+ };
135
+ }
112
136
  const imgProps = {
113
137
  ...rest,
114
138
  src: largestSrc,
@@ -0,0 +1,29 @@
1
+ //#region src/metadata.d.ts
2
+ /**
3
+ * Metadata produced by a build-time `?pracht` image import (see
4
+ * `prachtImage()` from `@pracht/image/vite`).
5
+ *
6
+ * ```ts
7
+ * import hero from "./hero.jpg?pracht";
8
+ * // hero: { src, width, height, blurDataURL }
9
+ * ```
10
+ *
11
+ * Pass the whole object to `<Image src={hero} …>` to get intrinsic sizing and
12
+ * `placeholder="blur"` support without repeating dimensions by hand.
13
+ */
14
+ interface PrachtImageMetadata {
15
+ /** Final asset URL (hashed in production builds, dev-served in dev). */
16
+ src: string;
17
+ /** Intrinsic width in pixels, after applying EXIF orientation. */
18
+ width: number;
19
+ /** Intrinsic height in pixels, after applying EXIF orientation. */
20
+ height: number;
21
+ /**
22
+ * Tiny inline preview (`data:image/webp;base64,…`, ~8px wide) for
23
+ * `placeholder="blur"`. Undefined for SVG sources: vectors scale cleanly,
24
+ * so a raster blur adds bytes without adding information.
25
+ */
26
+ blurDataURL?: string;
27
+ }
28
+ //#endregion
29
+ export { PrachtImageMetadata as t };
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Vite plugin enabling build-time image imports:
3
+ *
4
+ * ```ts
5
+ * // vite.config.ts — add it next to pracht(); it is not included by default.
6
+ * import { prachtImage } from "@pracht/image/vite";
7
+ * export default { plugins: [pracht({ … }), prachtImage()] };
8
+ * ```
9
+ *
10
+ * ```tsx
11
+ * import hero from "./hero.jpg?pracht";
12
+ * <Image src={hero} alt="…" placeholder="blur" />;
13
+ * ```
14
+ *
15
+ * The import yields `{ src, width, height, blurDataURL }`: `src` goes through
16
+ * Vite's normal asset pipeline (hashed source assets, stable publicDir URLs,
17
+ * `base`, dev server), dimensions come from sharp metadata with EXIF
18
+ * orientation applied, and `blurDataURL` is a tiny inline WebP generated at
19
+ * build time. Add
20
+ * `/// <reference types="@pracht/image/client" />` (or `"types":
21
+ * ["@pracht/image/client"]` in tsconfig) so TypeScript understands the query.
22
+ */import { t as PrachtImageMetadata } from "./metadata-CA97TjOR.mjs";
23
+ import { Plugin } from "vite";
24
+
25
+ //#region src/vite.d.ts
26
+ /**
27
+ * Options for the `?pracht` image import plugin.
28
+ */
29
+ interface PrachtImageOptions {
30
+ /**
31
+ * Width in pixels of the generated blur placeholder. Kept tiny on purpose:
32
+ * the placeholder is inlined as a base64 data URI in HTML and JS.
33
+ * Defaults to 8.
34
+ */
35
+ blurWidth?: number;
36
+ /** WebP quality (1-100) of the blur placeholder. Defaults to 70. */
37
+ blurQuality?: number;
38
+ /** Override how sharp is imported (useful for tests). */
39
+ loadSharp?: () => Promise<unknown>;
40
+ }
41
+ /** Minimal structural typing for the parts of sharp used at build time. */
42
+ interface SharpMetadata {
43
+ format?: string;
44
+ width?: number;
45
+ height?: number;
46
+ orientation?: number;
47
+ pages?: number;
48
+ }
49
+ interface SharpPipeline {
50
+ metadata(): Promise<SharpMetadata>;
51
+ rotate(): SharpPipeline;
52
+ resize(options: {
53
+ width: number;
54
+ withoutEnlargement: boolean;
55
+ }): SharpPipeline;
56
+ webp(options: {
57
+ quality: number;
58
+ }): SharpPipeline;
59
+ toBuffer(): Promise<Uint8Array>;
60
+ }
61
+ type SharpFactory = (input: Uint8Array) => SharpPipeline;
62
+ /** `/path/to/hero.jpg?pracht` → true; `?pracht` may combine with other params. */
63
+ declare function isPrachtImageId(id: string): boolean;
64
+ /** Strip the entire query, leaving the file path. */
65
+ declare function stripImageQuery(id: string): string;
66
+ /**
67
+ * Read intrinsic dimensions (respecting EXIF orientation) and generate the
68
+ * blur placeholder for one image buffer. Exported for tests.
69
+ */
70
+ declare function analyzeImage(sharp: SharpFactory, source: Uint8Array, options: {
71
+ blurWidth: number;
72
+ blurQuality: number;
73
+ }): Promise<Omit<PrachtImageMetadata, "src">>;
74
+ /**
75
+ * Generate the virtual module for a `?pracht` import. The `?url` import
76
+ * delegates the actual file to Vite's asset pipeline, so hashing, `base`,
77
+ * and dev serving all behave exactly like a plain asset import. `no-inline`
78
+ * opts out of `assetsInlineLimit`: without it, images under the limit
79
+ * (default 4 KB) turn `src` into a `data:` URI, which breaks
80
+ * optimization-endpoint loaders (`/api/_pracht/image?url=data%3A…` is not a
81
+ * fetchable same-origin path) and double-ships the bytes next to
82
+ * `blurDataURL`. The metadata contract promises a real asset URL (hashed for
83
+ * source files, stable for publicDir files). Exported for tests.
84
+ */
85
+ declare function createImageModuleCode(assetId: string, analyzed: Omit<PrachtImageMetadata, "src">): string;
86
+ declare function prachtImage(options?: PrachtImageOptions): Plugin;
87
+ //#endregion
88
+ export { PrachtImageOptions, analyzeImage, createImageModuleCode, isPrachtImageId, prachtImage, stripImageQuery };
package/dist/vite.mjs ADDED
@@ -0,0 +1,184 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { isAbsolute, relative, resolve, sep } from "node:path";
3
+ //#region src/vite.ts
4
+ const PRACHT_IMAGE_QUERY = "pracht";
5
+ const DEFAULT_BLUR_WIDTH = 8;
6
+ const DEFAULT_BLUR_QUALITY = 70;
7
+ const SHARP_INSTALL_HINT = "[pracht/image] \"?pracht\" image imports require the optional \"sharp\" dependency at build time. Install it in your app with \"pnpm add -D sharp\" (or npm install -D sharp / yarn add -D sharp).";
8
+ function createSharpImporter(load) {
9
+ let cached;
10
+ return () => {
11
+ cached ??= load().then((mod) => mod.default ?? mod, () => {
12
+ cached = void 0;
13
+ throw new Error(SHARP_INSTALL_HINT);
14
+ });
15
+ return cached;
16
+ };
17
+ }
18
+ /** `/path/to/hero.jpg?pracht` → true; `?pracht` may combine with other params. */
19
+ function isPrachtImageId(id) {
20
+ const queryStart = id.indexOf("?");
21
+ if (queryStart === -1) return false;
22
+ return id.slice(queryStart + 1).split("&").some((part) => part === PRACHT_IMAGE_QUERY);
23
+ }
24
+ /** Strip the entire query, leaving the file path. */
25
+ function stripImageQuery(id) {
26
+ const queryStart = id.indexOf("?");
27
+ return queryStart === -1 ? id : id.slice(0, queryStart);
28
+ }
29
+ /**
30
+ * Read intrinsic dimensions (respecting EXIF orientation) and generate the
31
+ * blur placeholder for one image buffer. Exported for tests.
32
+ */
33
+ async function analyzeImage(sharp, source, options) {
34
+ const metadata = await sharp(source).metadata();
35
+ let width = metadata.width;
36
+ let height = metadata.height;
37
+ const orientation = metadata.orientation ?? 1;
38
+ if (orientation >= 5 && orientation <= 8) {
39
+ width = metadata.height;
40
+ height = metadata.width;
41
+ }
42
+ if (width == null || height == null || width <= 0 || height <= 0) throw new Error(`could not determine intrinsic dimensions (format: ${metadata.format ?? "unknown"}). For SVG sources, add width/height or a viewBox attribute.`);
43
+ if (metadata.format === "svg") return {
44
+ width,
45
+ height
46
+ };
47
+ const blur = await sharp(source).rotate().resize({
48
+ width: options.blurWidth,
49
+ withoutEnlargement: true
50
+ }).webp({ quality: options.blurQuality }).toBuffer();
51
+ return {
52
+ width,
53
+ height,
54
+ blurDataURL: `data:image/webp;base64,${Buffer.from(blur).toString("base64")}`
55
+ };
56
+ }
57
+ /**
58
+ * Generate the virtual module for a `?pracht` import. The `?url` import
59
+ * delegates the actual file to Vite's asset pipeline, so hashing, `base`,
60
+ * and dev serving all behave exactly like a plain asset import. `no-inline`
61
+ * opts out of `assetsInlineLimit`: without it, images under the limit
62
+ * (default 4 KB) turn `src` into a `data:` URI, which breaks
63
+ * optimization-endpoint loaders (`/api/_pracht/image?url=data%3A…` is not a
64
+ * fetchable same-origin path) and double-ships the bytes next to
65
+ * `blurDataURL`. The metadata contract promises a real asset URL (hashed for
66
+ * source files, stable for publicDir files). Exported for tests.
67
+ */
68
+ function createImageModuleCode(assetId, analyzed) {
69
+ const assetImport = `${assetId.replace(/\\/g, "/")}?url&no-inline`;
70
+ return [
71
+ `import src from ${JSON.stringify(assetImport)};`,
72
+ `export const width = ${JSON.stringify(analyzed.width)};`,
73
+ `export const height = ${JSON.stringify(analyzed.height)};`,
74
+ `export const blurDataURL = ${JSON.stringify(analyzed.blurDataURL)};`,
75
+ "export { src };",
76
+ "export default { src, width, height, blurDataURL };"
77
+ ].join("\n");
78
+ }
79
+ async function resolvePublicFile(publicDir, source) {
80
+ if (!publicDir || !source.startsWith("/")) return void 0;
81
+ const candidate = resolve(publicDir, `.${source}`);
82
+ const relativePath = relative(publicDir, candidate);
83
+ if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) return;
84
+ return (await stat(candidate).catch(() => void 0))?.isFile() ? candidate : void 0;
85
+ }
86
+ /**
87
+ * Vite plugin enabling build-time image imports:
88
+ *
89
+ * ```ts
90
+ * // vite.config.ts — add it next to pracht(); it is not included by default.
91
+ * import { prachtImage } from "@pracht/image/vite";
92
+ * export default { plugins: [pracht({ … }), prachtImage()] };
93
+ * ```
94
+ *
95
+ * ```tsx
96
+ * import hero from "./hero.jpg?pracht";
97
+ * <Image src={hero} alt="…" placeholder="blur" />;
98
+ * ```
99
+ *
100
+ * The import yields `{ src, width, height, blurDataURL }`: `src` goes through
101
+ * Vite's normal asset pipeline (hashed source assets, stable publicDir URLs,
102
+ * `base`, dev server), dimensions come from sharp metadata with EXIF
103
+ * orientation applied, and `blurDataURL` is a tiny inline WebP generated at
104
+ * build time. Add
105
+ * `/// <reference types="@pracht/image/client" />` (or `"types":
106
+ * ["@pracht/image/client"]` in tsconfig) so TypeScript understands the query.
107
+ */
108
+ function prachtImage(options = {}) {
109
+ const blurWidth = options.blurWidth ?? DEFAULT_BLUR_WIDTH;
110
+ const blurQuality = options.blurQuality ?? DEFAULT_BLUR_QUALITY;
111
+ if (!Number.isInteger(blurWidth) || blurWidth < 1 || blurWidth > 64) throw new Error("prachtImage({ blurWidth }) expects an integer between 1 and 64.");
112
+ if (!Number.isInteger(blurQuality) || blurQuality < 1 || blurQuality > 100) throw new Error("prachtImage({ blurQuality }) expects an integer between 1 and 100.");
113
+ const importSharp = createSharpImporter(options.loadSharp ?? (() => import("sharp")));
114
+ const cache = /* @__PURE__ */ new Map();
115
+ const resolvedImages = /* @__PURE__ */ new Map();
116
+ let publicDir = "";
117
+ async function transform(filePath, assetId) {
118
+ const stats = await stat(filePath).catch(() => {
119
+ throw new Error(`[pracht/image] Could not read "${filePath}" for a "?pracht" import.`);
120
+ });
121
+ const cacheKey = `${filePath}\0${assetId}`;
122
+ const cached = cache.get(cacheKey);
123
+ if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) return cached.code;
124
+ const code = (async () => {
125
+ const [sharp, source] = await Promise.all([importSharp(), readFile(filePath)]);
126
+ let analyzed;
127
+ try {
128
+ analyzed = await analyzeImage(sharp, source, {
129
+ blurWidth,
130
+ blurQuality
131
+ });
132
+ } catch (error) {
133
+ throw new Error(`[pracht/image] Failed to process "?pracht" import of "${filePath}": ${error instanceof Error ? error.message : String(error)}`);
134
+ }
135
+ return createImageModuleCode(assetId, analyzed);
136
+ })();
137
+ code.catch(() => cache.delete(cacheKey));
138
+ cache.set(cacheKey, {
139
+ mtimeMs: stats.mtimeMs,
140
+ size: stats.size,
141
+ code
142
+ });
143
+ return code;
144
+ }
145
+ return {
146
+ name: "pracht:image-imports",
147
+ enforce: "pre",
148
+ configResolved(config) {
149
+ publicDir = config.publicDir;
150
+ },
151
+ async resolveId(source, importer) {
152
+ if (!isPrachtImageId(source)) return null;
153
+ const sourcePath = stripImageQuery(source);
154
+ const resolved = await this.resolve(sourcePath, importer, { skipSelf: true });
155
+ if (!resolved) return null;
156
+ const publicFile = resolved.id === sourcePath ? await resolvePublicFile(publicDir, sourcePath) : void 0;
157
+ const moduleId = `${resolved.id}${resolved.id.includes("?") ? "&" : "?"}${PRACHT_IMAGE_QUERY}`;
158
+ resolvedImages.set(moduleId, {
159
+ filePath: publicFile ?? resolved.id,
160
+ assetId: publicFile ? sourcePath : resolved.id
161
+ });
162
+ return moduleId;
163
+ },
164
+ async load(id) {
165
+ if (!isPrachtImageId(id)) return null;
166
+ const resolved = resolvedImages.get(id) ?? {
167
+ filePath: stripImageQuery(id),
168
+ assetId: stripImageQuery(id)
169
+ };
170
+ this.addWatchFile(resolved.filePath);
171
+ return transform(resolved.filePath, resolved.assetId);
172
+ },
173
+ watchChange(filePath) {
174
+ if (this.environment.mode !== "dev") return;
175
+ for (const [moduleId, resolved] of resolvedImages) {
176
+ if (resolved.filePath.replace(/\\/g, "/") !== filePath.replace(/\\/g, "/")) continue;
177
+ const module = this.environment.moduleGraph.getModuleById(moduleId);
178
+ if (module) this.environment.moduleGraph.invalidateModule(module);
179
+ }
180
+ }
181
+ };
182
+ }
183
+ //#endregion
184
+ export { analyzeImage, createImageModuleCode, isPrachtImageId, prachtImage, stripImageQuery };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pracht/image",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Responsive, CLS-safe <Image> component for Pracht with pluggable optimization loaders and a Node image endpoint.",
5
5
  "keywords": [
6
6
  "pracht",
@@ -23,7 +23,8 @@
23
23
  "directory": "packages/image"
24
24
  },
25
25
  "files": [
26
- "dist"
26
+ "dist",
27
+ "client.d.ts"
27
28
  ],
28
29
  "type": "module",
29
30
  "exports": {
@@ -34,22 +35,34 @@
34
35
  "./node": {
35
36
  "types": "./dist/node.d.mts",
36
37
  "default": "./dist/node.mjs"
38
+ },
39
+ "./vite": {
40
+ "types": "./dist/vite.d.mts",
41
+ "default": "./dist/vite.mjs"
42
+ },
43
+ "./client": {
44
+ "types": "./client.d.ts"
37
45
  }
38
46
  },
39
47
  "publishConfig": {
40
48
  "provenance": true
41
49
  },
42
50
  "peerDependencies": {
43
- "preact": "^10.0.0",
44
- "sharp": "^0.33.0 || ^0.34.0"
51
+ "preact": "^10.0.0 || ^11.0.0-0",
52
+ "sharp": "^0.33.0 || ^0.34.0",
53
+ "vite": "^8.0.0"
45
54
  },
46
55
  "peerDependenciesMeta": {
47
56
  "sharp": {
48
57
  "optional": true
58
+ },
59
+ "vite": {
60
+ "optional": true
49
61
  }
50
62
  },
51
63
  "devDependencies": {
52
- "sharp": "^0.34.0"
64
+ "sharp": "^0.34.0",
65
+ "vite": "^8.0.0"
53
66
  },
54
67
  "scripts": {
55
68
  "build": "tsdown"