lake-cimg 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cimg.js +43 -0
- package/lib/compress.js +7 -28
- package/lib/pictureStack.js +14 -30
- package/lib/scanCodeReferences.js +110 -10
- package/lib/sharpHelpers.js +67 -0
- package/package.json +1 -1
- package/skills/cimg-audit/SKILL.md +4 -4
- package/skills/cimg-audit/reference.md +18 -18
package/bin/cimg.js
CHANGED
|
@@ -217,6 +217,20 @@ program
|
|
|
217
217
|
}
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
+
function collectString(val, memo) {
|
|
221
|
+
const arr = memo ?? [];
|
|
222
|
+
arr.push(val);
|
|
223
|
+
return arr;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseAliasPair(val) {
|
|
227
|
+
const i = val.indexOf("=");
|
|
228
|
+
if (i <= 0) {
|
|
229
|
+
throw new Error("格式须为 key=相对路径,例如 @=src 或 @assets=src/assets");
|
|
230
|
+
}
|
|
231
|
+
return [val.slice(0, i).trim(), val.slice(i + 1).trim()];
|
|
232
|
+
}
|
|
233
|
+
|
|
220
234
|
program
|
|
221
235
|
.command("scan-code [path]")
|
|
222
236
|
.description(
|
|
@@ -231,17 +245,46 @@ program
|
|
|
231
245
|
500
|
|
232
246
|
)
|
|
233
247
|
.option("--issues-only", "仅输出含 issues 的条目")
|
|
248
|
+
.option(
|
|
249
|
+
"--project-root <dir>",
|
|
250
|
+
"解析 /images/...、@/... 时的工程根目录(默认当前工作目录)"
|
|
251
|
+
)
|
|
252
|
+
.option(
|
|
253
|
+
"--public-dir <dir>",
|
|
254
|
+
"根路径下静态目录,可重复;用于解析以 / 开头的 URL 路径(默认 public、static)",
|
|
255
|
+
collectString
|
|
256
|
+
)
|
|
257
|
+
.option(
|
|
258
|
+
"--alias <pair>",
|
|
259
|
+
"路径别名 key=相对工程根的路径,可重复;默认 @=src。例: --alias @aaa=packages/aaa",
|
|
260
|
+
(v, prev) => {
|
|
261
|
+
const pair = parseAliasPair(v);
|
|
262
|
+
return [...(prev ?? []), pair];
|
|
263
|
+
},
|
|
264
|
+
[]
|
|
265
|
+
)
|
|
234
266
|
.action(async (pathArg, opts) => {
|
|
235
267
|
const raw =
|
|
236
268
|
pathArg != null && typeof pathArg === "string" ? pathArg.trim() : "";
|
|
237
269
|
const root = resolveUserPath(raw !== "" ? raw : ".");
|
|
238
270
|
const limit = Number.isNaN(opts.limit) ? 500 : opts.limit;
|
|
271
|
+
const aliasList = opts.alias ?? [];
|
|
272
|
+
const aliases =
|
|
273
|
+
aliasList.length > 0 ? Object.fromEntries(aliasList) : undefined;
|
|
274
|
+
const publicDirs = opts.publicDir;
|
|
239
275
|
try {
|
|
240
276
|
const result = await scanCodeReferences(root, {
|
|
241
277
|
recursive: opts.recursive !== false,
|
|
242
278
|
cwd: process.cwd(),
|
|
243
279
|
limit,
|
|
244
280
|
issuesOnly: !!opts.issuesOnly,
|
|
281
|
+
...(opts.projectRoot != null && String(opts.projectRoot).trim() !== ""
|
|
282
|
+
? { projectRoot: String(opts.projectRoot).trim() }
|
|
283
|
+
: {}),
|
|
284
|
+
...(aliases != null ? { aliases } : {}),
|
|
285
|
+
...(publicDirs != null && publicDirs.length > 0
|
|
286
|
+
? { publicDirs }
|
|
287
|
+
: {}),
|
|
245
288
|
});
|
|
246
289
|
console.log(JSON.stringify(result, null, 2));
|
|
247
290
|
process.exit(0);
|
package/lib/compress.js
CHANGED
|
@@ -9,9 +9,13 @@ import sharp from "sharp";
|
|
|
9
9
|
import {
|
|
10
10
|
SUPPORTED_EXT,
|
|
11
11
|
DEFAULT_QUALITY,
|
|
12
|
-
DEFAULT_EFFORT,
|
|
13
12
|
supportedFormatsLabel,
|
|
14
13
|
} from "./constants.js";
|
|
14
|
+
import {
|
|
15
|
+
applyResizeInside,
|
|
16
|
+
encodeWebpBuffer,
|
|
17
|
+
encodeJpegBuffer,
|
|
18
|
+
} from "./sharpHelpers.js";
|
|
15
19
|
|
|
16
20
|
/** Max concurrent tasks for processing images */
|
|
17
21
|
const MAX_CONCURRENCY = Math.max(1, cpus().length);
|
|
@@ -59,23 +63,6 @@ function isSupported(filePath) {
|
|
|
59
63
|
return SUPPORTED_EXT.includes(extname(filePath).toLowerCase());
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
/**
|
|
63
|
-
* Encode pipeline to WebP buffer with shared sharp options.
|
|
64
|
-
* @param {import("sharp").Sharp} pipeline
|
|
65
|
-
* @param {number} quality
|
|
66
|
-
* @param {{ lossless?: boolean }} [options]
|
|
67
|
-
*/
|
|
68
|
-
async function encodeWebpBuffer(pipeline, quality, { lossless = false } = {}) {
|
|
69
|
-
return pipeline
|
|
70
|
-
.webp({
|
|
71
|
-
quality,
|
|
72
|
-
effort: DEFAULT_EFFORT,
|
|
73
|
-
smartSubsample: true,
|
|
74
|
-
lossless,
|
|
75
|
-
})
|
|
76
|
-
.toBuffer();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
66
|
/**
|
|
80
67
|
* Recursively collect image paths under dir (or return [dir] if dir is a file).
|
|
81
68
|
* @param {string} inputPath - Absolute path to file or directory
|
|
@@ -135,15 +122,7 @@ export async function processOne(inputPath, options = {}) {
|
|
|
135
122
|
let pipeline = sharp(inputBuffer, { animated: true });
|
|
136
123
|
const inputMeta = await pipeline.metadata();
|
|
137
124
|
|
|
138
|
-
|
|
139
|
-
pipeline = pipeline.resize({
|
|
140
|
-
width: size,
|
|
141
|
-
height: size,
|
|
142
|
-
fit: "inside",
|
|
143
|
-
withoutEnlargement: true,
|
|
144
|
-
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
145
|
-
});
|
|
146
|
-
}
|
|
125
|
+
pipeline = applyResizeInside(pipeline, size);
|
|
147
126
|
|
|
148
127
|
const outputBuffer = toWebp
|
|
149
128
|
? await encodeWebpBuffer(pipeline, quality, { lossless: false })
|
|
@@ -172,7 +151,7 @@ async function toFormatBuffer(pipeline, ext, quality) {
|
|
|
172
151
|
switch (ext) {
|
|
173
152
|
case ".jpg":
|
|
174
153
|
case ".jpeg":
|
|
175
|
-
return pipeline
|
|
154
|
+
return encodeJpegBuffer(pipeline, quality);
|
|
176
155
|
case ".png":
|
|
177
156
|
return pipeline.png({ compressionLevel: 9 }).toBuffer();
|
|
178
157
|
case ".webp":
|
package/lib/pictureStack.js
CHANGED
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
import { readFile, writeFile, mkdir, stat } from "fs/promises";
|
|
6
6
|
import { dirname, join, resolve, basename, extname } from "path";
|
|
7
7
|
import sharp from "sharp";
|
|
8
|
-
import { DEFAULT_QUALITY
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
import { DEFAULT_QUALITY } from "./constants.js";
|
|
9
|
+
import {
|
|
10
|
+
applyResizeInside,
|
|
11
|
+
encodeAvifBuffer,
|
|
12
|
+
encodeWebpBuffer,
|
|
13
|
+
encodeJpegBuffer,
|
|
14
|
+
} from "./sharpHelpers.js";
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* @param {import("sharp").Sharp} pipeline - Configured pipeline (e.g. after resize)
|
|
@@ -16,22 +19,9 @@ const DEFAULT_AVIF_EFFORT = 4;
|
|
|
16
19
|
*/
|
|
17
20
|
async function encodeTriple(pipeline, q) {
|
|
18
21
|
const [avifBuf, webpBuf, jpegBuf] = await Promise.all([
|
|
19
|
-
pipeline
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.toBuffer(),
|
|
23
|
-
pipeline
|
|
24
|
-
.clone()
|
|
25
|
-
.webp({
|
|
26
|
-
quality: q.quality,
|
|
27
|
-
effort: DEFAULT_EFFORT,
|
|
28
|
-
smartSubsample: true,
|
|
29
|
-
})
|
|
30
|
-
.toBuffer(),
|
|
31
|
-
pipeline
|
|
32
|
-
.clone()
|
|
33
|
-
.jpeg({ quality: q.jpegQuality, mozjpeg: true })
|
|
34
|
-
.toBuffer(),
|
|
22
|
+
encodeAvifBuffer(pipeline.clone(), q.avifQuality),
|
|
23
|
+
encodeWebpBuffer(pipeline.clone(), q.quality),
|
|
24
|
+
encodeJpegBuffer(pipeline.clone(), q.jpegQuality),
|
|
35
25
|
]);
|
|
36
26
|
return { avifBuf, webpBuf, jpegBuf };
|
|
37
27
|
}
|
|
@@ -85,16 +75,10 @@ export async function processPictureStack(inputPath, options) {
|
|
|
85
75
|
const inputBuffer = await readFile(absoluteIn);
|
|
86
76
|
const sizeBefore = inputBuffer.length;
|
|
87
77
|
|
|
88
|
-
let pipeline =
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
height: size,
|
|
93
|
-
fit: "inside",
|
|
94
|
-
withoutEnlargement: true,
|
|
95
|
-
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
96
|
-
});
|
|
97
|
-
}
|
|
78
|
+
let pipeline = applyResizeInside(
|
|
79
|
+
sharp(inputBuffer, { animated: false }),
|
|
80
|
+
size
|
|
81
|
+
);
|
|
98
82
|
|
|
99
83
|
const { avifBuf, webpBuf, jpegBuf } = await encodeTriple(pipeline, {
|
|
100
84
|
quality,
|
|
@@ -40,6 +40,40 @@ const DEFAULT_EXCLUDE_DIRS = new Set([
|
|
|
40
40
|
/** Relative ratio difference threshold (1% = 0.01) */
|
|
41
41
|
const DEFAULT_RATIO_TOLERANCE = 0.01;
|
|
42
42
|
|
|
43
|
+
/** Try these folders under projectRoot for URL paths like `/images/a.png` (public 静态资源) */
|
|
44
|
+
const DEFAULT_PUBLIC_DIRS = ["public", "static"];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} ref
|
|
48
|
+
* @param {Record<string, string>} aliases
|
|
49
|
+
* @returns {object | null}
|
|
50
|
+
*/
|
|
51
|
+
function matchAliasRef(ref, aliases) {
|
|
52
|
+
const keys = Object.keys(aliases)
|
|
53
|
+
.filter((k) => k !== "~")
|
|
54
|
+
.sort((a, b) => b.length - a.length);
|
|
55
|
+
for (const key of keys) {
|
|
56
|
+
const prefix = `${key}/`;
|
|
57
|
+
if (ref.startsWith(prefix)) {
|
|
58
|
+
return { key, rest: ref.slice(prefix.length) };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} abs
|
|
66
|
+
* @returns {Promise<boolean>}
|
|
67
|
+
*/
|
|
68
|
+
async function isExistingFile(abs) {
|
|
69
|
+
try {
|
|
70
|
+
const s = await stat(abs);
|
|
71
|
+
return s.isFile();
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
43
77
|
/**
|
|
44
78
|
* @param {string} text
|
|
45
79
|
* @param {number} index
|
|
@@ -313,11 +347,17 @@ function extractPugImgRefs(content) {
|
|
|
313
347
|
|
|
314
348
|
/**
|
|
315
349
|
* @param {string} ref
|
|
316
|
-
* @param {string} hostDir
|
|
317
|
-
* @
|
|
350
|
+
* @param {string} hostDir - 源码文件所在目录(相对路径 ./ ../ 以此为基准)
|
|
351
|
+
* @param {{
|
|
352
|
+
* projectRoot: string,
|
|
353
|
+
* aliases: Record<string, string>,
|
|
354
|
+
* publicDirs: string[],
|
|
355
|
+
* }} opts
|
|
356
|
+
* @returns {Promise<{ status: 'ok', path: string } | { status: 'skip', reason: string }>}
|
|
318
357
|
*/
|
|
319
|
-
function resolveLocalImageRef(ref, hostDir) {
|
|
320
|
-
const
|
|
358
|
+
async function resolveLocalImageRef(ref, hostDir, opts) {
|
|
359
|
+
const { projectRoot, aliases, publicDirs } = opts;
|
|
360
|
+
let trimmed = ref.trim();
|
|
321
361
|
if (!trimmed) return { status: "skip", reason: "empty" };
|
|
322
362
|
if (/^https?:\/\//i.test(trimmed)) {
|
|
323
363
|
return { status: "skip", reason: "remote_url" };
|
|
@@ -325,14 +365,57 @@ function resolveLocalImageRef(ref, hostDir) {
|
|
|
325
365
|
if (trimmed.startsWith("data:")) {
|
|
326
366
|
return { status: "skip", reason: "data_uri" };
|
|
327
367
|
}
|
|
328
|
-
|
|
329
|
-
|
|
368
|
+
|
|
369
|
+
if (trimmed.startsWith("~@/")) {
|
|
370
|
+
trimmed = `@/${trimmed.slice(3)}`;
|
|
330
371
|
}
|
|
372
|
+
|
|
373
|
+
if (trimmed.startsWith("/") && !trimmed.startsWith("//")) {
|
|
374
|
+
const relFromWebRoot = trimmed.replace(/^\/+/, "");
|
|
375
|
+
for (const pd of publicDirs) {
|
|
376
|
+
const candidate = resolve(projectRoot, pd, relFromWebRoot);
|
|
377
|
+
if (await isExistingFile(candidate)) {
|
|
378
|
+
return { status: "ok", path: candidate };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return { status: "skip", reason: "public_path_not_found" };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (trimmed.startsWith("~/")) {
|
|
385
|
+
const tildeBase = aliases["~"];
|
|
386
|
+
if (tildeBase == null || tildeBase === "") {
|
|
387
|
+
return { status: "skip", reason: "alias_path" };
|
|
388
|
+
}
|
|
389
|
+
const rest = trimmed.slice(2);
|
|
390
|
+
const candidate = resolve(projectRoot, tildeBase, rest);
|
|
391
|
+
if (await isExistingFile(candidate)) {
|
|
392
|
+
return { status: "ok", path: candidate };
|
|
393
|
+
}
|
|
394
|
+
return { status: "skip", reason: "alias_target_not_found" };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const aliasHit = matchAliasRef(trimmed, aliases);
|
|
398
|
+
if (aliasHit) {
|
|
399
|
+
const baseRel = aliases[aliasHit.key];
|
|
400
|
+
if (baseRel == null || baseRel === "") {
|
|
401
|
+
return { status: "skip", reason: "unknown_alias" };
|
|
402
|
+
}
|
|
403
|
+
const candidate = resolve(projectRoot, baseRel, aliasHit.rest);
|
|
404
|
+
if (await isExistingFile(candidate)) {
|
|
405
|
+
return { status: "ok", path: candidate };
|
|
406
|
+
}
|
|
407
|
+
return { status: "skip", reason: "alias_target_not_found" };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (trimmed.startsWith("@") && trimmed.includes("/")) {
|
|
411
|
+
return { status: "skip", reason: "unknown_alias" };
|
|
412
|
+
}
|
|
413
|
+
|
|
331
414
|
if (!IMG_EXT_RE.test(trimmed)) {
|
|
332
415
|
return { status: "skip", reason: "not_image_extension" };
|
|
333
416
|
}
|
|
334
|
-
const
|
|
335
|
-
return { status: "ok", path };
|
|
417
|
+
const pathAbs = resolve(hostDir, trimmed);
|
|
418
|
+
return { status: "ok", path: pathAbs };
|
|
336
419
|
}
|
|
337
420
|
|
|
338
421
|
/**
|
|
@@ -515,6 +598,9 @@ async function runPool(tasks, concurrency) {
|
|
|
515
598
|
* issuesOnly?: boolean,
|
|
516
599
|
* ratioTolerance?: number,
|
|
517
600
|
* metadataConcurrency?: number,
|
|
601
|
+
* projectRoot?: string,
|
|
602
|
+
* aliases?: Record<string, string>,
|
|
603
|
+
* publicDirs?: string[],
|
|
518
604
|
* }} [options]
|
|
519
605
|
*/
|
|
520
606
|
export async function scanCodeReferences(rootDir, options = {}) {
|
|
@@ -525,8 +611,22 @@ export async function scanCodeReferences(rootDir, options = {}) {
|
|
|
525
611
|
issuesOnly = false,
|
|
526
612
|
ratioTolerance = DEFAULT_RATIO_TOLERANCE,
|
|
527
613
|
metadataConcurrency = 8,
|
|
614
|
+
projectRoot: projectRootOpt,
|
|
615
|
+
aliases: aliasesOpt,
|
|
616
|
+
publicDirs: publicDirsOpt,
|
|
528
617
|
} = options;
|
|
529
618
|
|
|
619
|
+
const projectRoot = resolve(cwd, projectRootOpt != null ? projectRootOpt : ".");
|
|
620
|
+
const aliases = {
|
|
621
|
+
"@": "src",
|
|
622
|
+
...(aliasesOpt && typeof aliasesOpt === "object" ? aliasesOpt : {}),
|
|
623
|
+
};
|
|
624
|
+
const publicDirs =
|
|
625
|
+
Array.isArray(publicDirsOpt) && publicDirsOpt.length > 0
|
|
626
|
+
? publicDirsOpt
|
|
627
|
+
: DEFAULT_PUBLIC_DIRS;
|
|
628
|
+
const resolveOpts = { projectRoot, aliases, publicDirs };
|
|
629
|
+
|
|
530
630
|
const sourceExts =
|
|
531
631
|
options.sourceExts != null
|
|
532
632
|
? new Set(
|
|
@@ -602,7 +702,7 @@ export async function scanCodeReferences(rootDir, options = {}) {
|
|
|
602
702
|
for (const { absHost, ref } of staged) {
|
|
603
703
|
if (ref.parse?.dynamic || ref.parse?.missingSrc) continue;
|
|
604
704
|
if (!ref.rawRef) continue;
|
|
605
|
-
const res = resolveLocalImageRef(ref.rawRef, absHost);
|
|
705
|
+
const res = await resolveLocalImageRef(ref.rawRef, absHost, resolveOpts);
|
|
606
706
|
if (res.status === "ok") uniquePaths.add(res.path);
|
|
607
707
|
}
|
|
608
708
|
|
|
@@ -691,7 +791,7 @@ export async function scanCodeReferences(rootDir, options = {}) {
|
|
|
691
791
|
continue;
|
|
692
792
|
}
|
|
693
793
|
|
|
694
|
-
const resolved = resolveLocalImageRef(r.rawRef, absHost);
|
|
794
|
+
const resolved = await resolveLocalImageRef(r.rawRef, absHost, resolveOpts);
|
|
695
795
|
if (resolved.status === "skip") {
|
|
696
796
|
const row = {
|
|
697
797
|
file,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Sharp pipeline helpers (resize + encode) for compress and picture stack.
|
|
3
|
+
*/
|
|
4
|
+
import { DEFAULT_EFFORT } from "./constants.js";
|
|
5
|
+
|
|
6
|
+
const RESIZE_BACKGROUND = { r: 0, g: 0, b: 0, alpha: 0 };
|
|
7
|
+
|
|
8
|
+
/** AVIF effort 0–9 (higher = slower, often smaller) */
|
|
9
|
+
export const DEFAULT_AVIF_EFFORT = 4;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Apply "fit inside" square resize when size > 0; otherwise return pipeline unchanged.
|
|
13
|
+
* @param {import("sharp").Sharp} pipeline
|
|
14
|
+
* @param {number | null | undefined} size
|
|
15
|
+
* @returns {import("sharp").Sharp}
|
|
16
|
+
*/
|
|
17
|
+
export function applyResizeInside(pipeline, size) {
|
|
18
|
+
if (size == null || size <= 0) return pipeline;
|
|
19
|
+
return pipeline.resize({
|
|
20
|
+
width: size,
|
|
21
|
+
height: size,
|
|
22
|
+
fit: "inside",
|
|
23
|
+
withoutEnlargement: true,
|
|
24
|
+
background: RESIZE_BACKGROUND,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {import("sharp").Sharp} pipeline
|
|
30
|
+
* @param {number} quality
|
|
31
|
+
* @param {{ lossless?: boolean }} [options]
|
|
32
|
+
*/
|
|
33
|
+
export async function encodeWebpBuffer(
|
|
34
|
+
pipeline,
|
|
35
|
+
quality,
|
|
36
|
+
{ lossless = false } = {}
|
|
37
|
+
) {
|
|
38
|
+
return pipeline
|
|
39
|
+
.webp({
|
|
40
|
+
quality,
|
|
41
|
+
effort: DEFAULT_EFFORT,
|
|
42
|
+
smartSubsample: true,
|
|
43
|
+
lossless,
|
|
44
|
+
})
|
|
45
|
+
.toBuffer();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {import("sharp").Sharp} pipeline
|
|
50
|
+
* @param {number} quality
|
|
51
|
+
*/
|
|
52
|
+
export async function encodeJpegBuffer(pipeline, quality) {
|
|
53
|
+
return pipeline.jpeg({ quality, mozjpeg: true }).toBuffer();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {import("sharp").Sharp} pipeline
|
|
58
|
+
* @param {number} quality
|
|
59
|
+
* @param {number} [effort]
|
|
60
|
+
*/
|
|
61
|
+
export async function encodeAvifBuffer(
|
|
62
|
+
pipeline,
|
|
63
|
+
quality,
|
|
64
|
+
effort = DEFAULT_AVIF_EFFORT
|
|
65
|
+
) {
|
|
66
|
+
return pipeline.avif({ quality, effort }).toBuffer();
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -38,14 +38,14 @@ More flags: `npx lake-cimg@latest scan-code --help` (e.g. `--limit`, `--issues-o
|
|
|
38
38
|
## Workflow
|
|
39
39
|
|
|
40
40
|
- [ ] **1. Scan (read-only):** `npx lake-cimg@latest scan-code [path]` — see **Invoke `scan-code`** above.
|
|
41
|
-
- [ ] **2. Triage:** Prefer **`hints`** for what to change; use **`issues`** codes to group or filter. Common codes: `missing_dimensions`, `aspect_ratio_mismatch`, `suggest_modern_format` (
|
|
42
|
-
- [ ] **3. Fix then optimize:** Apply markup
|
|
41
|
+
- [ ] **2. Triage:** Prefer **`hints`** for what to change; use **`issues`** codes to group or filter. Common codes: `missing_dimensions`, `aspect_ratio_mismatch`, `suggest_modern_format` (default: **single WebP** — point `src` / `srcset` at `.webp` after exporting; use **`<picture>`** with AVIF/WebP + legacy fallback **only if the user explicitly asks** for multi-format markup or old-browser JPEG/PNG fallback), `needs_manual_review`, `missing_src`, `cannot_resolve`, `cannot_read_metadata`.
|
|
42
|
+
- [ ] **3. Fix then optimize:** Apply markup using [reference.md](reference.md) (`<img>` first; `<picture>` only when the user requires it). For **all** raster refs that need format or responsive delivery, not only one hero row. Compress / emit WebP: `npx lake-cimg@latest <path> [options]`. **Optional** full stack for `<picture>` when requested: `npx lake-cimg@latest picture <input> -O <outDir>` (details: package [README.md](../../README.md)).
|
|
43
43
|
|
|
44
44
|
## Rules of thumb
|
|
45
45
|
|
|
46
46
|
- **Aspect ratio:** Match display ratio to intrinsic (w÷h), or use **`object-fit`** + explicit box / `aspect-ratio` for crop/letterbox.
|
|
47
47
|
- **CLS:** Add `width` and `height` to `<img>`, or use CSS `aspect-ratio`. For `missing_dimensions`, fill in the exact `intrinsicWidth` / `intrinsicHeight`.
|
|
48
|
-
- **Responsive:**
|
|
48
|
+
- **Responsive:** **`srcset` + `sizes`** on `<img>` (works with a single WebP). **`<picture>`** only when the user wants multiple formats in HTML; each `<source type="…">` must match the real file type. Width variants: separate files or `picture … -s <px>` when building a stack.
|
|
49
49
|
- **LCP:** At most one hero per view: **`fetchPriority="high"`**, **`loading="eager"`**; lazy-load the rest.
|
|
50
50
|
- **Alt:** Describe content and purpose; no keyword stuffing; **`alt=""`** only for decorative images.
|
|
51
51
|
|
|
@@ -60,4 +60,4 @@ If **`hints`** mention alias skip: use **`rawRef`** and map the alias via Vite/w
|
|
|
60
60
|
| Command | Role |
|
|
61
61
|
| --- | --- |
|
|
62
62
|
| `npx lake-cimg@latest <path>` | Compress / WebP — see `npx lake-cimg@latest --help` (`-o`, `-s`, `-q`, `-r`). |
|
|
63
|
-
| `npx lake-cimg@latest picture <input> -O <outDir>` | One raster → AVIF + WebP + JPEG
|
|
63
|
+
| `npx lake-cimg@latest picture <input> -O <outDir>` | One raster → AVIF + WebP + JPEG **when the user wants `<picture>` / multi-format**; not the default if single WebP is enough. |
|
|
@@ -8,12 +8,15 @@ Supplement for [SKILL.md](SKILL.md). Copy-paste starting points; adjust paths, d
|
|
|
8
8
|
|
|
9
9
|
```html
|
|
10
10
|
<img
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
srcset="
|
|
12
|
+
maine-coon-nap-320w.webp 320w,
|
|
13
|
+
maine-coon-nap-480w.webp 480w,
|
|
14
|
+
maine-coon-nap-800w.webp 800w
|
|
15
|
+
"
|
|
16
|
+
sizes="(max-width: 320px) 280px, (max-width: 480px) 440px, 800px"
|
|
17
|
+
src="maine-coon-nap-800w.webp"
|
|
18
|
+
alt="A watercolor illustration of a maine coon napping leisurely in front of a fireplace"
|
|
19
|
+
/>
|
|
17
20
|
```
|
|
18
21
|
|
|
19
22
|
## `<picture>` + `srcset` + `sizes`
|
|
@@ -24,26 +27,23 @@ Supplement for [SKILL.md](SKILL.md). Copy-paste starting points; adjust paths, d
|
|
|
24
27
|
<picture>
|
|
25
28
|
<source
|
|
26
29
|
type="image/avif"
|
|
27
|
-
srcset="hero-400.avif 400w,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
sizes="(max-width: 600px) 100vw, 50vw">
|
|
30
|
+
srcset="hero-400.avif 400w, hero-800.avif 800w, hero-1200.avif 1200w"
|
|
31
|
+
sizes="(max-width: 600px) 100vw, 50vw"
|
|
32
|
+
/>
|
|
31
33
|
<source
|
|
32
34
|
type="image/webp"
|
|
33
|
-
srcset="hero-400.webp 400w,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
sizes="(max-width: 600px) 100vw, 50vw">
|
|
35
|
+
srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w"
|
|
36
|
+
sizes="(max-width: 600px) 100vw, 50vw"
|
|
37
|
+
/>
|
|
37
38
|
<img
|
|
38
39
|
src="hero-800.jpg"
|
|
39
|
-
srcset="hero-400.jpg 400w,
|
|
40
|
-
hero-800.jpg 800w,
|
|
41
|
-
hero-1200.jpg 1200w"
|
|
40
|
+
srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w"
|
|
42
41
|
sizes="(max-width: 600px) 100vw, 50vw"
|
|
43
42
|
width="1200"
|
|
44
43
|
height="600"
|
|
45
44
|
alt="Describe subject and purpose on this page"
|
|
46
45
|
loading="lazy"
|
|
47
|
-
decoding="async"
|
|
46
|
+
decoding="async"
|
|
47
|
+
/>
|
|
48
48
|
</picture>
|
|
49
49
|
```
|