vite-smart-img 1.1.7
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/dist/index.d.ts +40 -0
- package/dist/index.js +384 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.cjs +310 -0
- package/dist/plugin.cjs.map +1 -0
- package/dist/plugin.d.cts +37 -0
- package/dist/plugin.d.ts +37 -0
- package/dist/plugin.js +270 -0
- package/dist/plugin.js.map +1 -0
- package/dist/vite-asset-types.d.ts +39 -0
- package/package.json +52 -0
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// src/plugin/imageThumbnails.ts
|
|
2
|
+
import sharp from "sharp";
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { extname } from "path";
|
|
6
|
+
|
|
7
|
+
// src/plugin/assetsPath.ts
|
|
8
|
+
import { normalize } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
function cleanLoadId(id) {
|
|
11
|
+
let raw = id.split("?")[0] ?? id;
|
|
12
|
+
if (raw.startsWith("\0")) return raw;
|
|
13
|
+
if (raw.startsWith("@fs/")) raw = raw.slice("@fs/".length);
|
|
14
|
+
else if (raw.startsWith("@fs\\")) raw = raw.slice("@fs\\".length);
|
|
15
|
+
if (raw.startsWith("file:")) {
|
|
16
|
+
try {
|
|
17
|
+
return fileURLToPath(raw);
|
|
18
|
+
} catch {
|
|
19
|
+
return raw;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return raw;
|
|
23
|
+
}
|
|
24
|
+
function isViteUrlImportRequest(id) {
|
|
25
|
+
const q = id.indexOf("?");
|
|
26
|
+
if (q === -1) return false;
|
|
27
|
+
const search = id.slice(q + 1);
|
|
28
|
+
if (!search) return false;
|
|
29
|
+
try {
|
|
30
|
+
return new URLSearchParams(search).has("url");
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function isPathInsideAssetsFolder(id) {
|
|
36
|
+
const fsPath = cleanLoadId(id);
|
|
37
|
+
if (fsPath.startsWith("\0")) return false;
|
|
38
|
+
const unified = normalize(fsPath).replace(/\\/g, "/");
|
|
39
|
+
return /(^|\/)assets\//.test(unified);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/plugin/imageThumbnails.ts
|
|
43
|
+
var SIZES = [
|
|
44
|
+
400,
|
|
45
|
+
600,
|
|
46
|
+
800,
|
|
47
|
+
1e3,
|
|
48
|
+
1200,
|
|
49
|
+
1400,
|
|
50
|
+
1600,
|
|
51
|
+
1800,
|
|
52
|
+
2e3,
|
|
53
|
+
2500,
|
|
54
|
+
3e3,
|
|
55
|
+
3500,
|
|
56
|
+
4e3,
|
|
57
|
+
5e3,
|
|
58
|
+
6e3,
|
|
59
|
+
7e3,
|
|
60
|
+
8e3
|
|
61
|
+
];
|
|
62
|
+
var SOURCE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png"]);
|
|
63
|
+
var JPEG_QUALITY = 85;
|
|
64
|
+
var WEBP_QUALITY = 82;
|
|
65
|
+
var ORIGINAL_QUALITY_CANDIDATES = [85, 80, 75, 70, 65];
|
|
66
|
+
function contentHash(buf) {
|
|
67
|
+
return createHash("sha256").update(buf).digest("hex").slice(0, 8);
|
|
68
|
+
}
|
|
69
|
+
function normalizeExt(ext) {
|
|
70
|
+
const e = ext.replace(/^\./, "").toLowerCase();
|
|
71
|
+
return e === "jpeg" ? "jpg" : e;
|
|
72
|
+
}
|
|
73
|
+
function smaller(a, b) {
|
|
74
|
+
return a.length <= b.length ? a : b;
|
|
75
|
+
}
|
|
76
|
+
function imageThumbnails() {
|
|
77
|
+
return {
|
|
78
|
+
name: "vite-smart-img-thumbnails",
|
|
79
|
+
apply: "build",
|
|
80
|
+
enforce: "pre",
|
|
81
|
+
async load(id) {
|
|
82
|
+
if (!isPathInsideAssetsFolder(id)) return null;
|
|
83
|
+
const pathOnly = cleanLoadId(id);
|
|
84
|
+
const ext = extname(pathOnly).toLowerCase();
|
|
85
|
+
if (!SOURCE_EXTS.has(ext)) return null;
|
|
86
|
+
const rawContent = readFileSync(pathOnly);
|
|
87
|
+
const cleanExt = normalizeExt(ext);
|
|
88
|
+
const isPng = cleanExt === "png";
|
|
89
|
+
let cleanedOriginal;
|
|
90
|
+
if (isPng) {
|
|
91
|
+
cleanedOriginal = await sharp(rawContent).rotate().png({ compressionLevel: 9 }).toBuffer();
|
|
92
|
+
} else {
|
|
93
|
+
cleanedOriginal = rawContent;
|
|
94
|
+
let found = false;
|
|
95
|
+
for (const q of ORIGINAL_QUALITY_CANDIDATES) {
|
|
96
|
+
const buf = await sharp(rawContent).rotate().jpeg({ quality: q, progressive: true }).toBuffer();
|
|
97
|
+
if (buf.length <= rawContent.length) {
|
|
98
|
+
cleanedOriginal = buf;
|
|
99
|
+
found = true;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!found) {
|
|
104
|
+
cleanedOriginal = await sharp(rawContent).rotate().jpeg({ quality: 65, progressive: true }).toBuffer();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const hash = contentHash(cleanedOriginal);
|
|
108
|
+
const origFileName = `assets/img/${hash}.${cleanExt}`;
|
|
109
|
+
const origRef = this.emitFile({
|
|
110
|
+
type: "asset",
|
|
111
|
+
fileName: origFileName,
|
|
112
|
+
source: cleanedOriginal
|
|
113
|
+
});
|
|
114
|
+
const meta = await sharp(cleanedOriginal).metadata();
|
|
115
|
+
const originalW = meta.width && meta.width > 0 ? meta.width : 1;
|
|
116
|
+
const originalH = meta.height && meta.height > 0 ? meta.height : 1;
|
|
117
|
+
const applicableSizes = SIZES.filter((s) => s <= originalW);
|
|
118
|
+
const ctx = this;
|
|
119
|
+
await Promise.all(
|
|
120
|
+
applicableSizes.flatMap((size) => {
|
|
121
|
+
const base = sharp(cleanedOriginal).rotate().resize({
|
|
122
|
+
width: size,
|
|
123
|
+
withoutEnlargement: true,
|
|
124
|
+
fit: "inside"
|
|
125
|
+
});
|
|
126
|
+
return [
|
|
127
|
+
base.clone().webp({ quality: WEBP_QUALITY }).toBuffer().then((buf) => {
|
|
128
|
+
ctx.emitFile({
|
|
129
|
+
type: "asset",
|
|
130
|
+
fileName: `assets/img/${hash}-${size}.webp`,
|
|
131
|
+
source: smaller(buf, cleanedOriginal)
|
|
132
|
+
});
|
|
133
|
+
}),
|
|
134
|
+
base.clone()[isPng ? "png" : "jpeg"]({
|
|
135
|
+
...isPng ? { compressionLevel: 9 } : { quality: JPEG_QUALITY, progressive: true }
|
|
136
|
+
}).toBuffer().then((buf) => {
|
|
137
|
+
ctx.emitFile({
|
|
138
|
+
type: "asset",
|
|
139
|
+
fileName: `assets/img/${hash}-${size}.${cleanExt}`,
|
|
140
|
+
source: smaller(buf, cleanedOriginal)
|
|
141
|
+
});
|
|
142
|
+
})
|
|
143
|
+
];
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
return `const src = import.meta.ROLLUP_FILE_URL_${origRef}
|
|
147
|
+
export default { src, originalW: ${originalW}, originalH: ${originalH} }
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/plugin/importedAssetsAlwaysFiles.ts
|
|
154
|
+
function importedAssetsAlwaysFiles() {
|
|
155
|
+
return {
|
|
156
|
+
name: "vite-smart-img-imported-assets-always-files",
|
|
157
|
+
enforce: "pre",
|
|
158
|
+
config() {
|
|
159
|
+
return {
|
|
160
|
+
build: {
|
|
161
|
+
assetsInlineLimit: 0
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/plugin/smartImgLocalAssetImport.ts
|
|
169
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
170
|
+
import { extname as extname2 } from "path";
|
|
171
|
+
import sharp2 from "sharp";
|
|
172
|
+
|
|
173
|
+
// src/plugin/parseSvgDimensions.ts
|
|
174
|
+
function parseSvgDimensions(svgSource) {
|
|
175
|
+
const widthAttr = svgSource.match(/\bwidth\s*=\s*["']([^"']+)["']/i);
|
|
176
|
+
const heightAttr = svgSource.match(/\bheight\s*=\s*["']([^"']+)["']/i);
|
|
177
|
+
if (widthAttr && heightAttr) {
|
|
178
|
+
const w = parseFloat(String(widthAttr[1]).replace(/px|pt|em|rem|%$/i, ""));
|
|
179
|
+
const h = parseFloat(String(heightAttr[1]).replace(/px|pt|em|rem|%$/i, ""));
|
|
180
|
+
if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {
|
|
181
|
+
return { originalW: Math.round(w), originalH: Math.round(h) };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const vb = svgSource.match(/viewBox\s*=\s*["']\s*([^"']+)\s*["']/i);
|
|
185
|
+
if (vb) {
|
|
186
|
+
const parts = vb[1].trim().split(/[\s,]+/).map((s) => Number(s));
|
|
187
|
+
if (parts.length >= 4) {
|
|
188
|
+
const w = parts[2];
|
|
189
|
+
const h = parts[3];
|
|
190
|
+
if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {
|
|
191
|
+
return { originalW: Math.round(w), originalH: Math.round(h) };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/plugin/smartImgLocalAssetImport.ts
|
|
199
|
+
var RASTER_EXT = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png"]);
|
|
200
|
+
function smartImgLocalAssetImport() {
|
|
201
|
+
return {
|
|
202
|
+
name: "vite-smart-img-local-asset-import",
|
|
203
|
+
enforce: "pre",
|
|
204
|
+
apply: "serve",
|
|
205
|
+
async load(id) {
|
|
206
|
+
if (isViteUrlImportRequest(id)) return null;
|
|
207
|
+
if (!isPathInsideAssetsFolder(id)) return null;
|
|
208
|
+
const pathOnly = cleanLoadId(id);
|
|
209
|
+
const ext = extname2(pathOnly).toLowerCase();
|
|
210
|
+
if (RASTER_EXT.has(ext)) {
|
|
211
|
+
const buf = readFileSync2(pathOnly);
|
|
212
|
+
const meta = await sharp2(buf).rotate().metadata();
|
|
213
|
+
const originalW = meta.width && meta.width > 0 ? meta.width : 1;
|
|
214
|
+
const originalH = meta.height && meta.height > 0 ? meta.height : 1;
|
|
215
|
+
const urlParam = JSON.stringify(`${pathOnly}?url`);
|
|
216
|
+
return `import _smiResolvedUrl from ${urlParam}
|
|
217
|
+
export default { src: _smiResolvedUrl, originalW: ${originalW}, originalH: ${originalH} }
|
|
218
|
+
`;
|
|
219
|
+
}
|
|
220
|
+
if (ext === ".svg") {
|
|
221
|
+
const source = readFileSync2(pathOnly, "utf8");
|
|
222
|
+
const d = parseSvgDimensions(source) ?? { originalW: 1, originalH: 1 };
|
|
223
|
+
const urlParam = JSON.stringify(`${pathOnly}?url`);
|
|
224
|
+
return `import _smiResolvedUrl from ${urlParam}
|
|
225
|
+
export default { src: _smiResolvedUrl, originalW: ${d.originalW}, originalH: ${d.originalH} }
|
|
226
|
+
`;
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/plugin/smartImgSvgAssetBuild.ts
|
|
234
|
+
import { createHash as createHash2 } from "crypto";
|
|
235
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
236
|
+
import { extname as extname3 } from "path";
|
|
237
|
+
function contentHash2(text) {
|
|
238
|
+
return createHash2("sha256").update(text, "utf8").digest("hex").slice(0, 8);
|
|
239
|
+
}
|
|
240
|
+
function smartImgSvgAssetBuild() {
|
|
241
|
+
return {
|
|
242
|
+
name: "vite-smart-img-svg-asset-build",
|
|
243
|
+
enforce: "pre",
|
|
244
|
+
apply: "build",
|
|
245
|
+
load(id) {
|
|
246
|
+
if (!isPathInsideAssetsFolder(id)) return null;
|
|
247
|
+
const pathOnly = cleanLoadId(id);
|
|
248
|
+
if (extname3(pathOnly).toLowerCase() !== ".svg") return null;
|
|
249
|
+
const source = readFileSync3(pathOnly, "utf8");
|
|
250
|
+
const dims = parseSvgDimensions(source) ?? { originalW: 1, originalH: 1 };
|
|
251
|
+
const hash = contentHash2(source);
|
|
252
|
+
const buf = Buffer.from(source, "utf8");
|
|
253
|
+
const origRef = this.emitFile({
|
|
254
|
+
type: "asset",
|
|
255
|
+
fileName: `assets/img/${hash}.svg`,
|
|
256
|
+
source: buf
|
|
257
|
+
});
|
|
258
|
+
return `const src = import.meta.ROLLUP_FILE_URL_${origRef}
|
|
259
|
+
export default { src, originalW: ${dims.originalW}, originalH: ${dims.originalH} }
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
export {
|
|
265
|
+
imageThumbnails,
|
|
266
|
+
importedAssetsAlwaysFiles,
|
|
267
|
+
smartImgLocalAssetImport,
|
|
268
|
+
smartImgSvgAssetBuild
|
|
269
|
+
};
|
|
270
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/plugin/imageThumbnails.ts","../src/plugin/assetsPath.ts","../src/plugin/importedAssetsAlwaysFiles.ts","../src/plugin/smartImgLocalAssetImport.ts","../src/plugin/parseSvgDimensions.ts","../src/plugin/smartImgSvgAssetBuild.ts"],"sourcesContent":["/**\n * Vite-плагин продакшн-обработки изображений из папок assets/.\n *\n * Работает только в режиме `build` (apply: 'build').\n * В dev-режиме IS_DEV в smartImgManager грузит оригинал напрямую.\n *\n * Что делает при билде:\n * 1. Перехватывает импорты картинок из любой папки assets/ в проекте.\n * 2. Хеширует содержимое файла (SHA-256, 8 символов) → имя без коллизий.\n * 3. Прогоняет оригинал через sharp — снимает EXIF/метаданные, корректирует\n * ориентацию по EXIF (.rotate()) перед его удалением.\n * 4. Нарезает миниатюры для размеров из SIZES, не превышающих ширину оригинала.\n * 5. Для каждого размера создаёт .jpg/.png и .webp.\n * Если миниатюра тяжелее очищенного оригинала — записывает оригинал вместо неё.\n * 6. Эмитирует все файлы в dist/assets/ с точными именами (без Rollup-хеша).\n * 7. Модуль по умолчанию: `{ src, originalW, originalH }` — `src` как раньше (URL оригинала).\n */\n\nimport sharp from 'sharp'\nimport { createHash } from 'crypto'\nimport { readFileSync } from 'fs'\nimport { extname } from 'path'\nimport type { Plugin } from 'vite'\n\nimport { cleanLoadId, isPathInsideAssetsFolder } from './assetsPath'\n\n// ─── Конфигурация ─────────────────────────────────────────────────────────────\n\nconst SIZES = [\n 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000,\n 2500, 3000, 3500, 4000, 5000, 6000, 7000, 8000,\n] as const\n\nconst SOURCE_EXTS = new Set(['.jpg', '.jpeg', '.png'])\n\n/** Качество JPEG при нарезке миниатюр */\nconst JPEG_QUALITY = 85\n\n/** Качество WebP при нарезке миниатюр */\nconst WEBP_QUALITY = 82\n\n/**\n * Набор качеств JPEG для перебора при очистке оригинала.\n * Берём первое, при котором очищенный файл ≤ сырого исходника.\n */\nconst ORIGINAL_QUALITY_CANDIDATES = [85, 80, 75, 70, 65] as const\n\n// ─── Утилиты ──────────────────────────────────────────────────────────────────\n\nfunction contentHash(buf: Buffer): string {\n return createHash('sha256').update(buf).digest('hex').slice(0, 8)\n}\n\nfunction normalizeExt(ext: string): string {\n const e = ext.replace(/^\\./, '').toLowerCase()\n return e === 'jpeg' ? 'jpg' : e\n}\n\n/** Меньший из двух буферов */\nfunction smaller(a: Buffer, b: Buffer): Buffer {\n return a.length <= b.length ? a : b\n}\n\n// ─── Плагин ───────────────────────────────────────────────────────────────────\n\nexport function imageThumbnails(): Plugin {\n return {\n name: 'vite-smart-img-thumbnails',\n apply: 'build',\n enforce: 'pre',\n\n async load(id) {\n if (!isPathInsideAssetsFolder(id)) return null\n const pathOnly = cleanLoadId(id)\n const ext = extname(pathOnly).toLowerCase()\n if (!SOURCE_EXTS.has(ext)) return null\n\n const rawContent = readFileSync(pathOnly)\n const cleanExt = normalizeExt(ext)\n const isPng = cleanExt === 'png'\n\n // ── Шаг 1: очистить оригинал от метаданных ──────────────────────────────\n let cleanedOriginal: Buffer\n\n if (isPng) {\n cleanedOriginal = await sharp(rawContent)\n .rotate()\n .png({ compressionLevel: 9 })\n .toBuffer()\n } else {\n cleanedOriginal = rawContent\n let found = false\n for (const q of ORIGINAL_QUALITY_CANDIDATES) {\n const buf = await sharp(rawContent)\n .rotate()\n .jpeg({ quality: q, progressive: true })\n .toBuffer()\n if (buf.length <= rawContent.length) {\n cleanedOriginal = buf\n found = true\n break\n }\n }\n if (!found) {\n cleanedOriginal = await sharp(rawContent)\n .rotate()\n .jpeg({ quality: 65, progressive: true })\n .toBuffer()\n }\n }\n\n const hash = contentHash(cleanedOriginal)\n const origFileName = `assets/img/${hash}.${cleanExt}`\n\n const origRef = this.emitFile({\n type: 'asset',\n fileName: origFileName,\n source: cleanedOriginal,\n })\n\n // ── Шаг 2: нарезать миниатюры ──────────────────────────────────────────\n const meta = await sharp(cleanedOriginal).metadata()\n const originalW = meta.width && meta.width > 0 ? meta.width : 1\n const originalH = meta.height && meta.height > 0 ? meta.height : 1\n const applicableSizes = SIZES.filter((s) => s <= originalW)\n\n const ctx = this\n await Promise.all(\n applicableSizes.flatMap((size) => {\n const base = sharp(cleanedOriginal).rotate().resize({\n width: size,\n withoutEnlargement: true,\n fit: 'inside',\n })\n\n return [\n base\n .clone()\n .webp({ quality: WEBP_QUALITY })\n .toBuffer()\n .then((buf) => {\n ctx.emitFile({\n type: 'asset',\n fileName: `assets/img/${hash}-${size}.webp`,\n source: smaller(buf, cleanedOriginal),\n })\n }),\n\n base\n .clone()\n [isPng ? 'png' : 'jpeg']({\n ...(isPng\n ? { compressionLevel: 9 }\n : { quality: JPEG_QUALITY, progressive: true }),\n })\n .toBuffer()\n .then((buf) => {\n ctx.emitFile({\n type: 'asset',\n fileName: `assets/img/${hash}-${size}.${cleanExt}`,\n source: smaller(buf, cleanedOriginal),\n })\n }),\n ]\n }),\n )\n\n return (\n `const src = import.meta.ROLLUP_FILE_URL_${origRef}\\n` +\n `export default { src, originalW: ${originalW}, originalH: ${originalH} }\\n`\n )\n },\n }\n}\n","/**\r\n * Vite `load(id)` может отдавать POSIX-путь, `file://` URL или префикс `@fs/`.\r\n * Сегмент папки `assets` проверяем в унифицированном виде (`/assets/`), чтобы\r\n * работало на Windows и не зависело от `path.sep`.\r\n */\r\nimport { normalize } from 'path'\r\nimport { fileURLToPath } from 'url'\r\n\r\nexport function cleanLoadId(id: string): string {\r\n let raw = id.split('?')[0] ?? id\r\n if (raw.startsWith('\\0')) return raw\r\n\r\n if (raw.startsWith('@fs/')) raw = raw.slice('@fs/'.length)\r\n else if (raw.startsWith('@fs\\\\')) raw = raw.slice('@fs\\\\'.length)\r\n\r\n if (raw.startsWith('file:')) {\r\n try {\r\n return fileURLToPath(raw)\r\n } catch {\r\n return raw\r\n }\r\n }\r\n return raw\r\n}\r\n\r\n/**\r\n * Явный запрос URL ассета (`?url`, `?import&url`, …) — его обрабатывает встроенный пайплайн Vite.\r\n * Наш `load` для обёртки не должен перехватывать повторно (цикл / TDZ по имени импорта).\r\n */\r\nexport function isViteUrlImportRequest(id: string): boolean {\r\n const q = id.indexOf('?')\r\n if (q === -1) return false\r\n const search = id.slice(q + 1)\r\n if (!search) return false\r\n try {\r\n return new URLSearchParams(search).has('url')\r\n } catch {\r\n return false\r\n }\r\n}\r\n\r\n/** Файл лежит в каталоге `.../assets/...` (сегмент `assets` в пути). */\r\nexport function isPathInsideAssetsFolder(id: string): boolean {\r\n const fsPath = cleanLoadId(id)\r\n if (fsPath.startsWith('\\0')) return false\r\n const unified = normalize(fsPath).replace(/\\\\/g, '/')\r\n return /(^|\\/)assets\\//.test(unified)\r\n}\r\n","import type { Plugin } from 'vite'\r\n\r\n/**\r\n * При `vite build` не встраивает импортируемые статические ассеты внутрь JS (base64 / data URL).\r\n * Вместо этого они остаются отдельными файлами в `dist`, в коде — только строковые URL.\r\n *\r\n * Задаёт `build.assetsInlineLimit: 0` (в Vite один лимит на весь пайплайн статических импортов:\r\n * отключается inlining не только у изображений, но и у других мелких ассетов, которые Vite\r\n * мог бы положить в бандл).\r\n */\r\nexport function importedAssetsAlwaysFiles(): Plugin {\r\n return {\r\n name: 'vite-smart-img-imported-assets-always-files',\r\n enforce: 'pre',\r\n config() {\r\n return {\r\n build: {\r\n assetsInlineLimit: 0,\r\n },\r\n }\r\n },\r\n }\r\n}\r\n","/**\r\n * Только dev (`apply: 'serve'`): импорт `*.jpg|jpeg|png|svg` из `.../assets/...`\r\n * → `export default { src, originalW, originalH }` (как после production-плагинов).\r\n * Для растра — sharp.rotate().metadata(); для SVG — parseSvgDimensions.\r\n */\r\nimport { readFileSync } from 'fs'\r\nimport { extname } from 'path'\r\nimport type { Plugin } from 'vite'\r\nimport sharp from 'sharp'\r\nimport { cleanLoadId, isPathInsideAssetsFolder, isViteUrlImportRequest } from './assetsPath'\r\nimport { parseSvgDimensions } from './parseSvgDimensions'\r\n\r\nconst RASTER_EXT = new Set(['.jpg', '.jpeg', '.png'])\r\n\r\nexport function smartImgLocalAssetImport(): Plugin {\r\n return {\r\n name: 'vite-smart-img-local-asset-import',\r\n enforce: 'pre',\r\n apply: 'serve',\r\n\r\n async load(id) {\r\n // Обёртка импортирует `path?url`. Vite часто отдаёт `?import&url`, а не голый `?url`.\r\n if (isViteUrlImportRequest(id)) return null\r\n\r\n if (!isPathInsideAssetsFolder(id)) return null\r\n const pathOnly = cleanLoadId(id)\r\n const ext = extname(pathOnly).toLowerCase()\r\n\r\n if (RASTER_EXT.has(ext)) {\r\n const buf = readFileSync(pathOnly)\r\n const meta = await sharp(buf).rotate().metadata()\r\n const originalW = meta.width && meta.width > 0 ? meta.width : 1\r\n const originalH = meta.height && meta.height > 0 ? meta.height : 1\r\n const urlParam = JSON.stringify(`${pathOnly}?url`)\r\n return `import _smiResolvedUrl from ${urlParam}\\nexport default { src: _smiResolvedUrl, originalW: ${originalW}, originalH: ${originalH} }\\n`\r\n }\r\n\r\n if (ext === '.svg') {\r\n const source = readFileSync(pathOnly, 'utf8')\r\n const d = parseSvgDimensions(source) ?? { originalW: 1, originalH: 1 }\r\n const urlParam = JSON.stringify(`${pathOnly}?url`)\r\n return `import _smiResolvedUrl from ${urlParam}\\nexport default { src: _smiResolvedUrl, originalW: ${d.originalW}, originalH: ${d.originalH} }\\n`\r\n }\r\n\r\n return null\r\n },\r\n }\r\n}\r\n","/**\r\n * Базовый разбор width/height/viewBox у корневого <svg> (достаточно для типичных экспортов).\r\n * При неудаче возвращает null — вызывающий может подставить fallback.\r\n */\r\nexport function parseSvgDimensions(svgSource: string): { originalW: number; originalH: number } | null {\r\n const widthAttr = svgSource.match(/\\bwidth\\s*=\\s*[\"']([^\"']+)[\"']/i)\r\n const heightAttr = svgSource.match(/\\bheight\\s*=\\s*[\"']([^\"']+)[\"']/i)\r\n if (widthAttr && heightAttr) {\r\n const w = parseFloat(String(widthAttr[1]).replace(/px|pt|em|rem|%$/i, ''))\r\n const h = parseFloat(String(heightAttr[1]).replace(/px|pt|em|rem|%$/i, ''))\r\n if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {\r\n return { originalW: Math.round(w), originalH: Math.round(h) }\r\n }\r\n }\r\n const vb = svgSource.match(/viewBox\\s*=\\s*[\"']\\s*([^\"']+)\\s*[\"']/i)\r\n if (vb) {\r\n const parts = vb[1].trim().split(/[\\s,]+/).map((s) => Number(s))\r\n if (parts.length >= 4) {\r\n const w = parts[2]\r\n const h = parts[3]\r\n if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {\r\n return { originalW: Math.round(w), originalH: Math.round(h) }\r\n }\r\n }\r\n }\r\n return null\r\n}\r\n","/**\r\n * Только build: SVG из `.../assets/...` → тот же контракт `{ src, originalW, originalH }`.\r\n * Растр по-прежнему в `imageThumbnails`.\r\n */\r\nimport { createHash } from 'crypto'\r\nimport { readFileSync } from 'fs'\r\nimport { extname } from 'path'\r\nimport type { Plugin } from 'vite'\r\nimport { cleanLoadId, isPathInsideAssetsFolder } from './assetsPath'\r\nimport { parseSvgDimensions } from './parseSvgDimensions'\r\n\r\nfunction contentHash(text: string): string {\r\n return createHash('sha256').update(text, 'utf8').digest('hex').slice(0, 8)\r\n}\r\n\r\nexport function smartImgSvgAssetBuild(): Plugin {\r\n return {\r\n name: 'vite-smart-img-svg-asset-build',\r\n enforce: 'pre',\r\n apply: 'build',\r\n\r\n load(id) {\r\n if (!isPathInsideAssetsFolder(id)) return null\r\n const pathOnly = cleanLoadId(id)\r\n if (extname(pathOnly).toLowerCase() !== '.svg') return null\r\n\r\n const source = readFileSync(pathOnly, 'utf8')\r\n const dims = parseSvgDimensions(source) ?? { originalW: 1, originalH: 1 }\r\n const hash = contentHash(source)\r\n const buf = Buffer.from(source, 'utf8')\r\n const origRef = this.emitFile({\r\n type: 'asset',\r\n fileName: `assets/img/${hash}.svg`,\r\n source: buf,\r\n })\r\n\r\n return (\r\n `const src = import.meta.ROLLUP_FILE_URL_${origRef}\\n` +\r\n `export default { src, originalW: ${dims.originalW}, originalH: ${dims.originalH} }\\n`\r\n )\r\n },\r\n }\r\n}\r\n"],"mappings":"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","names":["readFileSync","extname","sharp","extname","readFileSync","sharp","createHash","readFileSync","extname","contentHash","createHash","extname","readFileSync"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* В `src/vite-env.d.ts` подключай **раньше**, чем `vite/client`, иначе `*.jpg` останутся `string`:
|
|
3
|
+
*
|
|
4
|
+
* /// <reference types="vite-smart-img/vite-asset-types" />
|
|
5
|
+
* /// <reference types="vite/client" />
|
|
6
|
+
*
|
|
7
|
+
* Форма совпадает с `SmartImgImportedAsset` (`smartImgAsset.ts`) — менять синхронно.
|
|
8
|
+
*/
|
|
9
|
+
type SmartImgImportedAssetVite = {
|
|
10
|
+
src: string
|
|
11
|
+
originalW: number
|
|
12
|
+
originalH: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare module '*.jpg' {
|
|
16
|
+
const _smartImgAsset: SmartImgImportedAssetVite
|
|
17
|
+
export default _smartImgAsset
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
declare module '*.jpeg' {
|
|
21
|
+
const _smartImgAsset: SmartImgImportedAssetVite
|
|
22
|
+
export default _smartImgAsset
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
declare module '*.png' {
|
|
26
|
+
const _smartImgAsset: SmartImgImportedAssetVite
|
|
27
|
+
export default _smartImgAsset
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
declare module '*.svg' {
|
|
31
|
+
const _smartImgAsset: SmartImgImportedAssetVite
|
|
32
|
+
export default _smartImgAsset
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Растр вне пайплайна SmartImg — как у `vite/client`, строка URL. */
|
|
36
|
+
declare module '*.webp' {
|
|
37
|
+
const src: string
|
|
38
|
+
export default src
|
|
39
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vite-smart-img",
|
|
3
|
+
"version": "1.1.7",
|
|
4
|
+
"description": "Smart lazy-loading image component for React + Vite with automatic thumbnail generation",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"./plugin": {
|
|
13
|
+
"types": "./dist/plugin.d.ts",
|
|
14
|
+
"import": "./dist/plugin.js",
|
|
15
|
+
"require": "./dist/plugin.cjs"
|
|
16
|
+
},
|
|
17
|
+
"./vite-asset-types": {
|
|
18
|
+
"types": "./dist/vite-asset-types.d.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"main": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"files": ["dist"],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup && node scripts/copy-vite-asset-types.cjs",
|
|
26
|
+
"dev": "tsup --watch",
|
|
27
|
+
"typecheck": "tsc --noEmit"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"react": ">=18",
|
|
31
|
+
"react-dom": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"vite": {
|
|
35
|
+
"optional": true
|
|
36
|
+
},
|
|
37
|
+
"sharp": {
|
|
38
|
+
"optional": true
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.0.0",
|
|
43
|
+
"@types/react": "^19.1.0",
|
|
44
|
+
"@types/react-dom": "^19.1.0",
|
|
45
|
+
"react": "^19.2.4",
|
|
46
|
+
"react-dom": "^19.2.4",
|
|
47
|
+
"sharp": "^0.34.0",
|
|
48
|
+
"tsup": "^8.0.0",
|
|
49
|
+
"typescript": "^6.0.2",
|
|
50
|
+
"vite": "^8.0.2"
|
|
51
|
+
}
|
|
52
|
+
}
|