vitepress-plugin-responsive-images 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shisheng Kai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # vitepress-plugin-responsive-images
2
+
3
+ Automatically convert local Markdown images in VitePress docs into responsive `<picture>` elements with WebP/AVIF sources and JPG/PNG fallbacks.
4
+
5
+ Write normal Markdown:
6
+
7
+ ```md
8
+ ![Dashboard screenshot](./images/dashboard.png)
9
+ ```
10
+
11
+ Build responsive images automatically:
12
+
13
+ ```html
14
+ <picture>
15
+ <source type="image/webp" srcset="... 480w, ... 720w, ... 1440w">
16
+ <img src="...png" srcset="... 480w, ... 720w, ... 1440w" sizes="(max-width: 768px) 100vw, 720px" alt="Dashboard screenshot">
17
+ </picture>
18
+ ```
19
+
20
+ ## Why
21
+
22
+ VitePress lets authors write Markdown quickly, but it does not automatically generate responsive image variants for local Markdown images. This plugin keeps the authoring experience simple while improving image delivery for documentation sites.
23
+
24
+ It is designed for documentation that must remain usable on older enterprise devices: modern browsers can load WebP or AVIF, while older browsers can still use JPG/PNG fallbacks.
25
+
26
+ ## Compatibility
27
+
28
+ | VitePress version | Status |
29
+ | --- | --- |
30
+ | `1.6.x` | Stable supported |
31
+ | `2.0.x` | Supported and continuously tested; alpha caveats apply while VitePress 2 is not stable |
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ npm install -D vitepress-plugin-responsive-images
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```ts
42
+ // docs/.vitepress/config.ts
43
+ import { defineConfig } from 'vitepress'
44
+ import { withResponsiveImages } from 'vitepress-plugin-responsive-images'
45
+
46
+ export default withResponsiveImages(
47
+ defineConfig({
48
+ title: 'My Docs'
49
+ })
50
+ )
51
+ ```
52
+
53
+ With options:
54
+
55
+ ```ts
56
+ export default withResponsiveImages(
57
+ defineConfig({
58
+ title: 'My Docs'
59
+ }),
60
+ {
61
+ widths: [480, 720, 960, 1440],
62
+ formats: ['webp'],
63
+ sizes: '(max-width: 768px) 100vw, 720px'
64
+ }
65
+ )
66
+ ```
67
+
68
+ ## Defaults
69
+
70
+ - Processes local Markdown images written as `![]()`.
71
+ - Processes `jpg`, `jpeg`, and `png` by default.
72
+ - Skips remote URLs, data URLs, SVG, GIF, Vue components, theme images, CSS backgrounds, and handwritten HTML images.
73
+ - Generates WebP plus original-format JPG/PNG fallback by default.
74
+ - Avoids upscaling images.
75
+ - Adds `loading="lazy"` and `decoding="async"` by default.
76
+
77
+ ## Configuration
78
+
79
+ ```ts
80
+ interface ResponsiveImagesOptions {
81
+ widths?: number[]
82
+ formats?: Array<'webp' | 'avif'>
83
+ sizes?: string
84
+ outputDir?: string
85
+ include?: string[]
86
+ exclude?: string[]
87
+ quality?: {
88
+ webp?: number
89
+ avif?: number
90
+ jpeg?: number
91
+ png?: number
92
+ }
93
+ loading?: 'lazy' | 'eager' | false
94
+ decoding?: 'async' | 'sync' | 'auto' | false
95
+ failOnError?: boolean
96
+ debug?: boolean
97
+ }
98
+ ```
99
+
100
+ ## Page opt-out
101
+
102
+ Disable the plugin for a single Markdown page with frontmatter:
103
+
104
+ ```yaml
105
+ ---
106
+ responsiveImages: false
107
+ ---
108
+ ```
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,86 @@
1
+ import MarkdownIt from 'markdown-it';
2
+ import { Plugin } from 'vite';
3
+
4
+ type OutputFormat = 'webp' | 'avif';
5
+ type FallbackFormat = 'original';
6
+ type LoadingValue = 'lazy' | 'eager' | false;
7
+ type DecodingValue = 'async' | 'sync' | 'auto' | false;
8
+ interface FormatQuality {
9
+ webp: number;
10
+ avif: number;
11
+ jpeg: number;
12
+ png?: number;
13
+ }
14
+ interface ResponsiveImagesOptions {
15
+ widths?: number[];
16
+ formats?: OutputFormat[];
17
+ fallbackFormat?: FallbackFormat;
18
+ sizes?: string;
19
+ outputDir?: string;
20
+ include?: string[];
21
+ exclude?: string[];
22
+ quality?: Partial<FormatQuality>;
23
+ loading?: LoadingValue;
24
+ decoding?: DecodingValue;
25
+ skipFormats?: string[];
26
+ failOnError?: boolean;
27
+ debug?: boolean;
28
+ defaultWidth?: number;
29
+ }
30
+ interface NormalizedOptions {
31
+ widths: number[];
32
+ formats: OutputFormat[];
33
+ fallbackFormat: FallbackFormat;
34
+ sizes: string;
35
+ outputDir: string;
36
+ include: string[];
37
+ exclude: string[];
38
+ quality: FormatQuality;
39
+ loading: LoadingValue;
40
+ decoding: DecodingValue;
41
+ skipFormats: string[];
42
+ failOnError: boolean;
43
+ debug: boolean;
44
+ defaultWidth: number;
45
+ }
46
+ interface ImageCandidate {
47
+ width: number;
48
+ path: string;
49
+ url: string;
50
+ }
51
+ interface ManifestEntry {
52
+ key: string;
53
+ markdownPath: string;
54
+ source: string;
55
+ sourcePath: string;
56
+ alt?: string;
57
+ sourceWidth: number;
58
+ sourceHeight: number;
59
+ displayWidth: number;
60
+ displayHeight: number;
61
+ sources: Partial<Record<OutputFormat, ImageCandidate[]>>;
62
+ fallback: {
63
+ format: 'jpg' | 'jpeg' | 'png';
64
+ candidates: ImageCandidate[];
65
+ src: string;
66
+ };
67
+ }
68
+ interface RuntimeState {
69
+ options: NormalizedOptions;
70
+ root: string;
71
+ cacheDir: string;
72
+ outDir: string;
73
+ base: string;
74
+ manifest: Map<string, ManifestEntry>;
75
+ built: boolean;
76
+ }
77
+
78
+ declare function responsiveImages(options?: ResponsiveImagesOptions): (md: MarkdownIt) => void;
79
+ declare function createResponsiveImagesPlugins(options?: ResponsiveImagesOptions): {
80
+ runtime: RuntimeState;
81
+ markdownPlugin: (md: MarkdownIt) => void;
82
+ vitePlugin: Plugin;
83
+ };
84
+ declare function withResponsiveImages<T>(config: T, options?: ResponsiveImagesOptions): T;
85
+
86
+ export { type ResponsiveImagesOptions, createResponsiveImagesPlugins, responsiveImages, withResponsiveImages };
package/dist/index.js ADDED
@@ -0,0 +1,454 @@
1
+ // src/runtime.ts
2
+ import path from "path";
3
+
4
+ // src/config.ts
5
+ var defaultOptions = {
6
+ widths: [480, 720, 960, 1440],
7
+ formats: ["webp"],
8
+ fallbackFormat: "original",
9
+ sizes: "(max-width: 768px) 100vw, 720px",
10
+ outputDir: "_responsive-images",
11
+ include: ["**/*.md"],
12
+ exclude: [],
13
+ quality: {
14
+ webp: 80,
15
+ avif: 50,
16
+ jpeg: 82
17
+ },
18
+ loading: "lazy",
19
+ decoding: "async",
20
+ skipFormats: ["svg", "gif"],
21
+ failOnError: false,
22
+ debug: false,
23
+ defaultWidth: 720
24
+ };
25
+ function normalizeOptions(options = {}) {
26
+ const widths = Array.from(new Set(options.widths ?? defaultOptions.widths)).filter((width) => Number.isInteger(width) && width > 0).sort((a, b) => a - b);
27
+ if (widths.length === 0) {
28
+ throw new Error("vitepress-plugin-responsive-images: at least one positive width is required.");
29
+ }
30
+ const formats = Array.from(new Set(options.formats ?? defaultOptions.formats));
31
+ for (const format of formats) {
32
+ if (format !== "webp" && format !== "avif") {
33
+ throw new Error(`vitepress-plugin-responsive-images: unsupported output format "${format}".`);
34
+ }
35
+ }
36
+ return {
37
+ ...defaultOptions,
38
+ ...options,
39
+ widths,
40
+ formats,
41
+ quality: {
42
+ ...defaultOptions.quality,
43
+ ...options.quality
44
+ },
45
+ include: options.include ?? defaultOptions.include,
46
+ exclude: options.exclude ?? defaultOptions.exclude,
47
+ skipFormats: (options.skipFormats ?? defaultOptions.skipFormats).map((format) => format.toLowerCase()),
48
+ outputDir: stripSlashes(options.outputDir ?? defaultOptions.outputDir)
49
+ };
50
+ }
51
+ function stripSlashes(value) {
52
+ return value.replace(/^\/+|\/+$/g, "");
53
+ }
54
+
55
+ // src/runtime.ts
56
+ function createRuntime(options = {}) {
57
+ const root = process.cwd();
58
+ return {
59
+ options: normalizeOptions(options),
60
+ root,
61
+ cacheDir: path.join(root, ".vitepress", "cache", "responsive-images"),
62
+ outDir: path.join(root, ".vitepress", "dist"),
63
+ base: "/",
64
+ manifest: /* @__PURE__ */ new Map(),
65
+ built: false
66
+ };
67
+ }
68
+
69
+ // src/path.ts
70
+ import path2 from "path";
71
+ import { pathToFileURL } from "url";
72
+ var externalPattern = /^(?:[a-z][a-z\d+.-]*:)?\/\//i;
73
+ function parseImageSource(source) {
74
+ const match = source.match(/^([^?#]*)([?#].*)?$/);
75
+ return {
76
+ cleanSource: match?.[1] ?? source,
77
+ suffix: match?.[2] ?? ""
78
+ };
79
+ }
80
+ function shouldSkipSource(source, skipFormats) {
81
+ if (!source || source.startsWith("#")) return true;
82
+ if (source.startsWith("data:") || source.startsWith("blob:")) return true;
83
+ if (externalPattern.test(source)) return true;
84
+ const { cleanSource } = parseImageSource(source);
85
+ const extension = path2.extname(cleanSource).slice(1).toLowerCase();
86
+ return !extension || skipFormats.includes(extension);
87
+ }
88
+ function resolveImageSource(params) {
89
+ const { root, source, skipFormats } = params;
90
+ if (shouldSkipSource(source, skipFormats)) return void 0;
91
+ const { cleanSource } = parseImageSource(source);
92
+ const extension = path2.extname(cleanSource).slice(1).toLowerCase();
93
+ let sourcePath;
94
+ let markdownPath = params.markdownPath ? normalizePath(params.markdownPath) : "";
95
+ if (cleanSource.startsWith("/")) {
96
+ sourcePath = path2.join(root, "public", cleanSource);
97
+ markdownPath ||= "__public__";
98
+ } else if (cleanSource.startsWith("@/")) {
99
+ sourcePath = path2.join(root, cleanSource.slice(2));
100
+ markdownPath ||= "__src__";
101
+ } else {
102
+ if (!markdownPath) return void 0;
103
+ sourcePath = path2.resolve(path2.dirname(markdownPath), cleanSource);
104
+ }
105
+ return {
106
+ key: createImageKey(markdownPath, source),
107
+ markdownPath,
108
+ source,
109
+ sourcePath,
110
+ extension
111
+ };
112
+ }
113
+ function createImageKey(markdownPath, source) {
114
+ return `${normalizePath(markdownPath)}::${source}`;
115
+ }
116
+ function normalizePath(value) {
117
+ return value.split(path2.sep).join("/");
118
+ }
119
+ function getMarkdownPathFromEnv(root, env) {
120
+ if (!env || typeof env !== "object") return void 0;
121
+ const record = env;
122
+ const candidates = [record.path, record.filePath, record.filename, record.id, record.relativePath];
123
+ for (const candidate of candidates) {
124
+ if (typeof candidate !== "string" || candidate.length === 0) continue;
125
+ return path2.isAbsolute(candidate) ? normalizePath(candidate) : normalizePath(path2.join(root, candidate));
126
+ }
127
+ return void 0;
128
+ }
129
+ function joinUrl(...parts) {
130
+ const normalized = parts.filter(Boolean).map((part, index) => {
131
+ if (index === 0) return part.replace(/\/+$/g, "");
132
+ return part.replace(/^\/+|\/+$/g, "");
133
+ }).filter(Boolean).join("/");
134
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
135
+ }
136
+
137
+ // src/render.ts
138
+ function renderPicture(entry, options, attrs) {
139
+ const sources = options.formats.map((format) => {
140
+ const candidates = entry.sources[format];
141
+ if (!candidates?.length) return "";
142
+ return `<source type="image/${format}" srcset="${escapeAttribute(renderSrcset(candidates))}">`;
143
+ }).filter(Boolean).join("");
144
+ const title = attrs.title ? ` title="${escapeAttribute(attrs.title)}"` : "";
145
+ const loading = options.loading ? ` loading="${escapeAttribute(options.loading)}"` : "";
146
+ const decoding = options.decoding ? ` decoding="${escapeAttribute(options.decoding)}"` : "";
147
+ return `<picture>${sources}<img src="${escapeAttribute(entry.fallback.src)}" srcset="${escapeAttribute(renderSrcset(entry.fallback.candidates))}" sizes="${escapeAttribute(options.sizes)}" width="${entry.displayWidth}" height="${entry.displayHeight}" alt="${escapeAttribute(attrs.alt)}"${title}${loading}${decoding}></picture>`;
148
+ }
149
+ function renderSrcset(candidates) {
150
+ return candidates.map((candidate) => `${candidate.url} ${candidate.width}w`).join(", ");
151
+ }
152
+ function escapeAttribute(value) {
153
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
154
+ }
155
+
156
+ // src/markdownPlugin.ts
157
+ function responsiveImagesMarkdownPlugin(runtime) {
158
+ return (md) => {
159
+ const defaultRender = md.renderer.rules.image ?? ((tokens, idx, options, _env, self) => self.renderToken(tokens, idx, options));
160
+ md.renderer.rules.image = (tokens, idx, options, env, self) => {
161
+ const token = tokens[idx];
162
+ const source = token.attrGet("src");
163
+ if (!source) return defaultRender(tokens, idx, options, env, self);
164
+ const markdownPath = getMarkdownPathFromEnv(runtime.root, env);
165
+ const resolved = resolveImageSource({
166
+ root: runtime.root,
167
+ markdownPath,
168
+ source,
169
+ skipFormats: runtime.options.skipFormats
170
+ });
171
+ const entry = resolved ? runtime.manifest.get(resolved.key) : void 0;
172
+ if (!entry) return defaultRender(tokens, idx, options, env, self);
173
+ return renderPicture(entry, runtime.options, {
174
+ alt: renderAlt(token, options, env, self),
175
+ title: token.attrGet("title") ?? void 0
176
+ });
177
+ };
178
+ };
179
+ }
180
+ function renderAlt(token, options, env, self) {
181
+ return self.renderInlineAsText(token.children ?? [], options, env);
182
+ }
183
+
184
+ // src/vitePlugin.ts
185
+ import fs3 from "fs/promises";
186
+ import path4 from "path";
187
+
188
+ // src/scan.ts
189
+ import fs2 from "fs/promises";
190
+ import { glob } from "tinyglobby";
191
+
192
+ // src/generate.ts
193
+ import crypto from "crypto";
194
+ import fs from "fs/promises";
195
+ import path3 from "path";
196
+ import sharp from "sharp";
197
+ async function generateImage(runtime, image) {
198
+ const sourceBuffer = await fs.readFile(image.sourcePath);
199
+ const hash = crypto.createHash("sha1").update(sourceBuffer).digest("hex").slice(0, 10);
200
+ const metadata = await sharp(sourceBuffer).metadata();
201
+ if (!metadata.width || !metadata.height) {
202
+ throw new Error("Missing image dimensions.");
203
+ }
204
+ await fs.mkdir(runtime.cacheDir, { recursive: true });
205
+ const fallbackFormat = normalizeFallbackFormat(image.extension);
206
+ const widths = selectWidths(runtime.options.widths, metadata.width);
207
+ const baseName = sanitizeBaseName(path3.basename(image.sourcePath, path3.extname(image.sourcePath)));
208
+ const sources = {};
209
+ for (const format of runtime.options.formats) {
210
+ sources[format] = await Promise.all(
211
+ widths.map((width) => writeVariant(runtime, sourceBuffer, baseName, hash, width, format))
212
+ );
213
+ }
214
+ const fallbackCandidates = await Promise.all(
215
+ widths.map((width) => writeVariant(runtime, sourceBuffer, baseName, hash, width, fallbackFormat))
216
+ );
217
+ const displayWidth = Math.min(metadata.width, runtime.options.defaultWidth);
218
+ const displayHeight = Math.round(displayWidth / metadata.width * metadata.height);
219
+ const fallbackSrc = chooseDefaultCandidate(fallbackCandidates, displayWidth).url;
220
+ return {
221
+ key: image.key,
222
+ markdownPath: image.markdownPath,
223
+ source: image.source,
224
+ sourcePath: normalizePath(image.sourcePath),
225
+ sourceWidth: metadata.width,
226
+ sourceHeight: metadata.height,
227
+ displayWidth,
228
+ displayHeight,
229
+ sources,
230
+ fallback: {
231
+ format: fallbackFormat,
232
+ candidates: fallbackCandidates,
233
+ src: fallbackSrc
234
+ }
235
+ };
236
+ }
237
+ async function writeVariant(runtime, sourceBuffer, baseName, hash, width, format) {
238
+ const extension = format === "jpeg" ? "jpg" : format;
239
+ const fileName = `${baseName}-${width}.${hash}.${extension}`;
240
+ const outputPath = path3.join(runtime.cacheDir, fileName);
241
+ try {
242
+ await fs.access(outputPath);
243
+ } catch {
244
+ let pipeline = sharp(sourceBuffer).resize({ width, withoutEnlargement: true });
245
+ if (format === "webp") pipeline = pipeline.webp({ quality: runtime.options.quality.webp });
246
+ if (format === "avif") pipeline = pipeline.avif({ quality: runtime.options.quality.avif });
247
+ if (format === "jpg" || format === "jpeg") pipeline = pipeline.jpeg({ quality: runtime.options.quality.jpeg });
248
+ if (format === "png") pipeline = pipeline.png(runtime.options.quality.png ? { quality: runtime.options.quality.png } : void 0);
249
+ await pipeline.toFile(outputPath);
250
+ }
251
+ return {
252
+ width,
253
+ path: normalizePath(outputPath),
254
+ url: joinUrl(runtime.base, runtime.options.outputDir, fileName)
255
+ };
256
+ }
257
+ function selectWidths(widths, sourceWidth) {
258
+ const selected = widths.filter((width) => width <= sourceWidth);
259
+ return selected.length > 0 ? selected : [sourceWidth];
260
+ }
261
+ function chooseDefaultCandidate(candidates, displayWidth) {
262
+ return candidates.find((candidate) => candidate.width >= displayWidth) ?? candidates[candidates.length - 1];
263
+ }
264
+ function normalizeFallbackFormat(extension) {
265
+ if (extension === "jpg" || extension === "jpeg") return "jpg";
266
+ if (extension === "png") return "png";
267
+ throw new Error(`Unsupported fallback format "${extension}".`);
268
+ }
269
+ function sanitizeBaseName(value) {
270
+ return value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "image";
271
+ }
272
+
273
+ // src/scan.ts
274
+ var markdownImagePattern = /!\[[^\]]*\]\(([^\s)]+)(?:\s+["'][^"']*["'])?\)/g;
275
+ async function buildManifest(runtime) {
276
+ runtime.manifest.clear();
277
+ const files = await glob(runtime.options.include, {
278
+ cwd: runtime.root,
279
+ ignore: runtime.options.exclude,
280
+ absolute: true,
281
+ onlyFiles: true
282
+ });
283
+ for (const file of files) {
284
+ await scanMarkdownFile(runtime, file);
285
+ }
286
+ runtime.built = true;
287
+ }
288
+ async function scanMarkdownFile(runtime, markdownPath) {
289
+ const content = await fs2.readFile(markdownPath, "utf8");
290
+ if (hasResponsiveImagesDisabled(content)) return;
291
+ for (const source of extractMarkdownImageSources(content)) {
292
+ const resolved = resolveImageSource({
293
+ root: runtime.root,
294
+ markdownPath,
295
+ source,
296
+ skipFormats: runtime.options.skipFormats
297
+ });
298
+ if (!resolved) continue;
299
+ try {
300
+ const entry = await generateImage(runtime, resolved);
301
+ runtime.manifest.set(entry.key, entry);
302
+ } catch (error) {
303
+ const message = error instanceof Error ? error.message : String(error);
304
+ if (runtime.options.failOnError) {
305
+ throw new Error(`vitepress-plugin-responsive-images: failed to process ${normalizePath(resolved.sourcePath)}. ${message}`);
306
+ }
307
+ if (runtime.options.debug) {
308
+ console.warn(`vitepress-plugin-responsive-images: skipped ${normalizePath(resolved.sourcePath)}. ${message}`);
309
+ }
310
+ }
311
+ }
312
+ }
313
+ function extractMarkdownImageSources(content) {
314
+ const sources = [];
315
+ let match;
316
+ markdownImagePattern.lastIndex = 0;
317
+ while (match = markdownImagePattern.exec(content)) {
318
+ const source = match[1]?.trim();
319
+ if (source) sources.push(source);
320
+ }
321
+ return sources;
322
+ }
323
+ function hasResponsiveImagesDisabled(content) {
324
+ if (!content.startsWith("---")) return false;
325
+ const end = content.indexOf("\n---", 3);
326
+ if (end === -1) return false;
327
+ const frontmatter = content.slice(3, end);
328
+ return /^responsiveImages:\s*false\s*$/m.test(frontmatter);
329
+ }
330
+
331
+ // src/vitePlugin.ts
332
+ function responsiveImagesVitePlugin(runtime) {
333
+ let resolvedConfig;
334
+ const virtualPrefix = "\0vitepress-responsive-image:";
335
+ return {
336
+ name: "vitepress-plugin-responsive-images",
337
+ async configResolved(config) {
338
+ resolvedConfig = config;
339
+ runtime.root = config.root;
340
+ runtime.base = config.base || "/";
341
+ runtime.cacheDir = path4.join(config.cacheDir, "responsive-images");
342
+ runtime.outDir = config.build.outDir;
343
+ },
344
+ async buildStart() {
345
+ await buildManifest(runtime);
346
+ },
347
+ resolveId(source) {
348
+ const fileName = getGeneratedFileName(source, runtime);
349
+ return fileName ? `${virtualPrefix}${fileName}` : void 0;
350
+ },
351
+ load(id) {
352
+ if (!id.startsWith(virtualPrefix)) return void 0;
353
+ const fileName = id.slice(virtualPrefix.length);
354
+ return `export default ${JSON.stringify(joinUrl(runtime.base, runtime.options.outputDir, fileName))}`;
355
+ },
356
+ async configureServer(server) {
357
+ await buildManifest(runtime);
358
+ installDevMiddleware(server, runtime);
359
+ },
360
+ async closeBundle() {
361
+ if (!resolvedConfig) return;
362
+ await copyGeneratedImages(runtime);
363
+ }
364
+ };
365
+ }
366
+ function getGeneratedFileName(source, runtime) {
367
+ const normalizedBase = runtime.base === "/" ? "" : runtime.base.replace(/\/+$/g, "");
368
+ const prefixes = [`/${runtime.options.outputDir}/`];
369
+ if (normalizedBase) {
370
+ prefixes.push(`${normalizedBase}/${runtime.options.outputDir}/`);
371
+ }
372
+ for (const prefix of prefixes) {
373
+ if (source.startsWith(prefix)) {
374
+ return source.slice(prefix.length);
375
+ }
376
+ }
377
+ return void 0;
378
+ }
379
+ function installDevMiddleware(server, runtime) {
380
+ const mountPath = `/${runtime.options.outputDir}/`;
381
+ server.middlewares.use(mountPath, async (request, response, next) => {
382
+ const requestUrl = request.url?.split("?")[0] ?? "";
383
+ const fileName = decodeURIComponent(requestUrl.replace(/^\/+/, ""));
384
+ const filePath = path4.join(runtime.cacheDir, fileName);
385
+ try {
386
+ const file = await fs3.readFile(filePath);
387
+ response.statusCode = 200;
388
+ response.end(file);
389
+ } catch {
390
+ next();
391
+ }
392
+ });
393
+ }
394
+ async function copyGeneratedImages(runtime) {
395
+ const targetDir = path4.join(runtime.outDir, runtime.options.outputDir);
396
+ await fs3.mkdir(targetDir, { recursive: true });
397
+ try {
398
+ const files = await fs3.readdir(runtime.cacheDir);
399
+ await Promise.all(
400
+ files.map((file) => fs3.copyFile(path4.join(runtime.cacheDir, file), path4.join(targetDir, file)))
401
+ );
402
+ } catch (error) {
403
+ const code = typeof error === "object" && error && "code" in error ? error.code : void 0;
404
+ if (code !== "ENOENT") throw error;
405
+ }
406
+ }
407
+
408
+ // src/index.ts
409
+ function responsiveImages(options = {}) {
410
+ return responsiveImagesMarkdownPlugin(createRuntime(options));
411
+ }
412
+ function createResponsiveImagesPlugins(options = {}) {
413
+ const runtime = createRuntime(options);
414
+ return {
415
+ runtime,
416
+ markdownPlugin: responsiveImagesMarkdownPlugin(runtime),
417
+ vitePlugin: responsiveImagesVitePlugin(runtime)
418
+ };
419
+ }
420
+ function withResponsiveImages(config, options = {}) {
421
+ if (typeof config === "function") {
422
+ return (async (...args) => {
423
+ const resolved = await config(...args);
424
+ return withResponsiveImages(resolved, options);
425
+ });
426
+ }
427
+ const plugins = createResponsiveImagesPlugins(options);
428
+ const userConfig = config ?? {};
429
+ const vite = { ...userConfig.vite ?? {} };
430
+ const markdown = { ...userConfig.markdown ?? {} };
431
+ const previousMarkdownConfig = markdown.config;
432
+ vite.plugins = [...toArray(vite.plugins), plugins.vitePlugin];
433
+ markdown.config = (...args) => {
434
+ if (typeof previousMarkdownConfig === "function") {
435
+ previousMarkdownConfig(...args);
436
+ }
437
+ const md = args[0];
438
+ md.use(plugins.markdownPlugin);
439
+ };
440
+ return {
441
+ ...userConfig,
442
+ vite,
443
+ markdown
444
+ };
445
+ }
446
+ function toArray(value) {
447
+ if (!value) return [];
448
+ return Array.isArray(value) ? value : [value];
449
+ }
450
+ export {
451
+ createResponsiveImagesPlugins,
452
+ responsiveImages,
453
+ withResponsiveImages
454
+ };
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "vitepress-plugin-responsive-images",
3
+ "version": "0.1.0",
4
+ "description": "Automatically convert local Markdown images in VitePress docs into responsive picture elements.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Shisheng Kai",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/shishengkai/vitepress-plugin-responsive-images.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/shishengkai/vitepress-plugin-responsive-images/issues"
14
+ },
15
+ "homepage": "https://github.com/shishengkai/vitepress-plugin-responsive-images#readme",
16
+ "keywords": [
17
+ "vitepress",
18
+ "vitepress-plugin",
19
+ "responsive-images",
20
+ "srcset",
21
+ "picture",
22
+ "webp",
23
+ "avif",
24
+ "sharp",
25
+ "markdown-it"
26
+ ],
27
+ "sideEffects": false,
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js"
37
+ }
38
+ },
39
+ "main": "./dist/index.js",
40
+ "types": "./dist/index.d.ts",
41
+ "scripts": {
42
+ "build": "tsup src/index.ts --format esm --dts --clean",
43
+ "typecheck": "tsc --noEmit",
44
+ "test": "vitest run",
45
+ "test:watch": "vitest",
46
+ "test:integration": "vitest run tests/integration.test.ts",
47
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build"
48
+ },
49
+ "peerDependencies": {
50
+ "vitepress": "^1.6.0 || ^2.0.0-alpha || ^2.0.0",
51
+ "vue": "^3.5.0"
52
+ },
53
+ "dependencies": {
54
+ "sharp": "^0.35.1",
55
+ "tinyglobby": "^0.2.17"
56
+ },
57
+ "devDependencies": {
58
+ "@types/markdown-it": "^14.1.2",
59
+ "@types/node": "^25.9.3",
60
+ "markdown-it": "^14.2.0",
61
+ "tsup": "^8.5.1",
62
+ "typescript": "^6.0.3",
63
+ "vitepress": "^1.6.4",
64
+ "vitest": "^4.1.9",
65
+ "vue": "^3.5.38"
66
+ }
67
+ }