lake-cimg 0.0.2 → 1.0.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
@@ -103,24 +103,24 @@ npx lake-cimg@latest picture <input> -O <outDir> [选项]
103
103
 
104
104
  ### 子命令:`scan-code`(源码中的图片引用)
105
105
 
106
- 在 **html / htm / vue / js / mjs / cjs / ts / tsx / jsx** 中查找图片引用(`<img>`、`import … from '…png'`、`new URL('…', import.meta.url)`、`url(…)` 等),将**可解析的本地路径**与 **sharp 读出的真实宽高**对比,只读输出 JSON(默认),用于:
106
+ 在 **html / htm / vue / pug / js / mjs / cjs / ts / tsx / jsx** 中查找图片引用(`<img>`、Pug `img(src=…)`、`import … from '…png'`、`new URL('…', import.meta.url)`、`url(…)` 等),将**可解析的本地路径**与 **sharp 读出的真实宽高**对比,只读输出 JSON(默认),用于:
107
107
 
108
108
  - **CLS**:缺少 `width`/`height` 且无 `aspect-ratio` 等占位
109
109
  - **比例**:声明的宽高比与素材像素比不一致(未使用 `object-fit: cover|contain|fill` 等时标为问题)
110
110
  - **格式**:对 JPEG/PNG 等提示可考虑 WebP/AVIF 或 `picture` 子命令
111
111
 
112
112
  ```text
113
- npx lake-cimg@latest scan-code <dir> [--no-recursive] [--limit <n>] [--issues-only] [--plain]
113
+ npx lake-cimg@latest scan-code [path] [--no-recursive] [--limit <n>] [--issues-only]
114
114
  ```
115
115
 
116
- | 选项 | 说明 |
116
+ | 参数 / 选项 | 说明 |
117
117
  | --- | --- |
118
- | `-r, --recursive` | 递归子目录(默认开启) |
118
+ | `path` | 可选。省略时等价于 `.`(当前工作目录,一般在项目根执行)。可为**目录**(递归扫描其下源码)或**单个源码文件**(如某 `.pug` / `.vue`,只扫该文件) |
119
+ | `-r, --recursive` | 递归子目录(默认开启;仅对**目录**扫描有效) |
119
120
  | `--limit <n>` | 最多输出多少条引用点(默认 `500`) |
120
- | `--issues-only` | 仅输出 `issues` 非空的条目 |
121
- | `--plain` | 简要文本而非 JSON(默认 stdout 为 JSON) |
121
+ | `--issues-only` | 仅输出 `issues` 非空的条目(全绿引用不出现在 `items` 里,JSON 更小、省 token) |
122
122
 
123
- 默认会跳过 `node_modules`、`dist`、`.git` 等目录。动态 `src`、远程 URL、别名路径会进入报告但通常无法解析到磁盘文件。
123
+ 目录扫描时默认会跳过 `node_modules`、`dist`、`.git` 等子目录。动态 `src`、远程 URL、别名路径会进入报告但通常无法解析到磁盘文件。
124
124
 
125
125
  ### 前端最佳实践(React / `<picture>`)
126
126
 
@@ -130,8 +130,6 @@ npx lake-cimg@latest scan-code <dir> [--no-recursive] [--limit <n>] [--issues-on
130
130
  4. **响应式**:若同一图有多套宽度,应为每条 `source`/`img` 提供 **`sizes`** 与多宽度 `srcSet`(需配合构建或 `npx lake-cimg@latest picture` 多次导出不同 `size`);当前 CLI 一次导出的是单套 URL。
131
131
  5. **路径**:把生成文件放到 **`public/`**(如 Next.js)或 CDN,片段里的路径与部署前缀一致。
132
132
 
133
- `--snippet` 输出的是可直接改的模板;生产环境请按设计稿补上准确的 `width`/`height`/`alt`/`sizes`。
134
-
135
133
  ## 支持格式
136
134
 
137
135
  输入:**jpg / jpeg、png、webp、gif**(gif 含动图,处理时由 sharp 按能力读写)。
@@ -153,7 +151,7 @@ npx lake-cimg@latest scan-code <dir> [--no-recursive] [--limit <n>] [--issues-on
153
151
  - `processOne(inputPath, options)` — 单文件
154
152
  - `collectFiles(inputPath, recursive)` — 枚举待处理路径
155
153
  - `scanAssets(dir, options)` — `lib/scanAssets.js`,按文件体积筛选偏大图片并生成建议(供脚本或自建工具使用)
156
- - `scanCodeReferences(rootDir, options)` — `lib/scanCodeReferences.js`,扫描源码引用并结合像素尺寸给出 CLS / 比例 / 格式类建议(供 CLI 或脚本使用)
154
+ - `scanCodeReferences(path, options)` — `lib/scanCodeReferences.js`,`path` 为目录或单个支持的源码文件(或 `.`),扫描源码引用并结合像素尺寸给出 CLS / 比例 / 格式类建议(供 CLI 或脚本使用)
157
155
  - `processPictureStack(inputPath, options)` — `lib/pictureStack.js`,一次写出 AVIF / WebP / JPEG
158
156
 
159
157
  适合在构建脚本或 Node 服务中复用同一套逻辑。
package/bin/cimg.js CHANGED
@@ -218,9 +218,9 @@ program
218
218
  });
219
219
 
220
220
  program
221
- .command("scan-code <dir>")
221
+ .command("scan-code [path]")
222
222
  .description(
223
- "扫描源码中的图片引用(html/vue/js/ts/tsx/jsx),结合像素尺寸给出 CLS / 比例 / 格式建议(只读)"
223
+ "扫描源码图片引用并输出 JSON 建议(CLS/比例/格式)。path 可为目录或源码文件,省略时为当前目录"
224
224
  )
225
225
  .option("-r, --recursive", "递归扫描子目录", true)
226
226
  .option("--no-recursive", "不递归子目录")
@@ -231,13 +231,10 @@ program
231
231
  500
232
232
  )
233
233
  .option("--issues-only", "仅输出含 issues 的条目")
234
- .option("--plain", "纯文本输出(默认 stdout 为 JSON)")
235
- .action(async (dir, opts) => {
236
- if (!dir || dir.trim() === "") {
237
- program.outputHelp();
238
- process.exit(1);
239
- }
240
- const root = resolveUserPath(dir);
234
+ .action(async (pathArg, opts) => {
235
+ const raw =
236
+ pathArg != null && typeof pathArg === "string" ? pathArg.trim() : "";
237
+ const root = resolveUserPath(raw !== "" ? raw : ".");
241
238
  const limit = Number.isNaN(opts.limit) ? 500 : opts.limit;
242
239
  try {
243
240
  const result = await scanCodeReferences(root, {
@@ -246,16 +243,7 @@ program
246
243
  limit,
247
244
  issuesOnly: !!opts.issuesOnly,
248
245
  });
249
- if (!opts.plain) {
250
- console.log(JSON.stringify(result, null, 2));
251
- } else {
252
- console.log(result.summary);
253
- for (const it of result.items) {
254
- console.log(
255
- `${it.file}:${it.line} ${it.issues?.join(",") || ""} ${it.rawRef || ""} -> ${it.resolvedPath || "-"}`
256
- );
257
- }
258
- }
246
+ console.log(JSON.stringify(result, null, 2));
259
247
  process.exit(0);
260
248
  } catch (err) {
261
249
  console.error("错误:", err.message);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Scan JS/HTML/Vue/TS/TSX/JSX for local image references, join sharp metadata,
2
+ * Scan JS/HTML/Vue/Pug/TS/TSX/JSX for local image references, join sharp metadata,
3
3
  * and flag CLS / aspect ratio / modern-format hints (read-only).
4
4
  */
5
5
  import { readdir, readFile, stat } from "fs/promises";
@@ -20,6 +20,7 @@ const DEFAULT_SOURCE_EXTS = new Set([
20
20
  ".ts",
21
21
  ".tsx",
22
22
  ".jsx",
23
+ ".pug",
23
24
  ]);
24
25
 
25
26
  const DEFAULT_EXCLUDE_DIRS = new Set([
@@ -51,6 +52,43 @@ function lineColAt(text, index) {
51
52
  return { line, column };
52
53
  }
53
54
 
55
+ /**
56
+ * Ranges [start, end) of `<picture>...</picture>` blocks that contain at least one
57
+ * AVIF/WebP `<source>` (MIME type or srcset extension). Non-nested `<picture>` typical case.
58
+ * @param {string} content
59
+ * @returns {Array<{ start: number, end: number }>}
60
+ */
61
+ function collectPictureModernIntervals(content) {
62
+ const re = /<picture\b[^>]*>([\s\S]*?)<\/picture>/gi;
63
+ const intervals = [];
64
+ let m;
65
+ while ((m = re.exec(content)) !== null) {
66
+ const full = m[0];
67
+ const start = m.index;
68
+ const end = start + full.length;
69
+ const hasModern =
70
+ /type\s*=\s*["']image\/(?:avif|webp)["']/i.test(full) ||
71
+ /\bsrcset\s*=\s*["'][^"']*\.(?:avif|webp)\b/i.test(full);
72
+ if (hasModern) {
73
+ intervals.push({ start, end });
74
+ }
75
+ }
76
+ return intervals;
77
+ }
78
+
79
+ /**
80
+ * @param {number} imgIndex
81
+ * @param {number} imgTagLen
82
+ * @param {Array<{ start: number, end: number }>} intervals
83
+ */
84
+ function imgInsidePictureModern(imgIndex, imgTagLen, intervals) {
85
+ const imgEnd = imgIndex + imgTagLen;
86
+ for (const { start, end } of intervals) {
87
+ if (imgIndex >= start && imgEnd <= end) return true;
88
+ }
89
+ return false;
90
+ }
91
+
54
92
  /**
55
93
  * @param {string} dir
56
94
  * @param {Set<string>} excludeDirs
@@ -146,6 +184,133 @@ function parseImgTag(tag) {
146
184
  return { srcRaw, srcKind, width, height, style };
147
185
  }
148
186
 
187
+ /**
188
+ * Parse Pug `img(src=… width=… height=…)` (one call block).
189
+ * @param {string} block - e.g. `img(src='a.png' width=300 height=100)`
190
+ */
191
+ function parsePugImgBlock(block) {
192
+ let srcRaw = null;
193
+ let srcKind = null;
194
+ const srcQuoted = block.match(/\bsrc\s*=\s*["']([^"']*)["']/i);
195
+ const srcUnquoted = block.match(
196
+ /\bsrc\s*=\s*([\w./$-]+\.(?:jpg|jpeg|png|webp|gif))\b/i
197
+ );
198
+ if (srcQuoted) {
199
+ const val = srcQuoted[1].trim();
200
+ if (
201
+ IMG_EXT_RE.test(val) ||
202
+ val.startsWith("./") ||
203
+ val.startsWith("../") ||
204
+ val.startsWith("/")
205
+ ) {
206
+ srcRaw = val.split("?")[0];
207
+ srcKind = "static";
208
+ } else {
209
+ srcKind = "dynamic";
210
+ }
211
+ } else if (srcUnquoted) {
212
+ srcRaw = srcUnquoted[1].split("?")[0];
213
+ srcKind = "static";
214
+ } else if (/\bsrc\s*=/i.test(block)) {
215
+ srcKind = "dynamic";
216
+ }
217
+
218
+ const w1 = block.match(/\bwidth\s*=\s*["']?(\d+)["']?/i);
219
+ const h1 = block.match(/\bheight\s*=\s*["']?(\d+)["']?/i);
220
+ const width = w1 ? parseInt(w1[1], 10) : null;
221
+ const height = h1 ? parseInt(h1[1], 10) : null;
222
+ const styleMatch = block.match(/\bstyle\s*=\s*["']([^"']*)["']/i);
223
+ const style = styleMatch ? styleMatch[1] : null;
224
+
225
+ return { srcRaw, srcKind, width, height, style };
226
+ }
227
+
228
+ /**
229
+ * @param {string} content
230
+ * @returns {Array<{ index: number, line: number, column: number, rawRef: string, kind: string, context: string, parse: object }>}
231
+ */
232
+ function extractPugImgRefs(content) {
233
+ /** @type {Array<{ index: number, line: number, column: number, rawRef: string, kind: string, context: string, parse: object }>} */
234
+ const refs = [];
235
+ const re = /\bimg\s*\(/g;
236
+ let m;
237
+ while ((m = re.exec(content)) !== null) {
238
+ const startParen = m.index + m[0].length - 1;
239
+ let depth = 1;
240
+ let i = startParen + 1;
241
+ while (i < content.length && depth > 0) {
242
+ const c = content[i];
243
+ if (c === "(") {
244
+ depth++;
245
+ i++;
246
+ continue;
247
+ }
248
+ if (c === ")") {
249
+ depth--;
250
+ i++;
251
+ continue;
252
+ }
253
+ if (c === '"' || c === "'" || c === "`") {
254
+ const q = c;
255
+ i++;
256
+ while (i < content.length) {
257
+ if (content[i] === "\\") {
258
+ i += 2;
259
+ continue;
260
+ }
261
+ if (content[i] === q) {
262
+ i++;
263
+ break;
264
+ }
265
+ i++;
266
+ }
267
+ continue;
268
+ }
269
+ i++;
270
+ }
271
+ const block = content.slice(m.index, i);
272
+ const index = m.index;
273
+ const { line, column } = lineColAt(content, index);
274
+ const parsed = parsePugImgBlock(block);
275
+
276
+ if (parsed.srcKind === "dynamic") {
277
+ refs.push({
278
+ index,
279
+ line,
280
+ column,
281
+ rawRef: "",
282
+ kind: "img",
283
+ context: block.slice(0, 200),
284
+ parse: { ...parsed, dynamic: true },
285
+ });
286
+ continue;
287
+ }
288
+ if (!parsed.srcRaw) {
289
+ refs.push({
290
+ index,
291
+ line,
292
+ column,
293
+ rawRef: "",
294
+ kind: "img",
295
+ context: block.slice(0, 200),
296
+ parse: { ...parsed, missingSrc: true },
297
+ });
298
+ continue;
299
+ }
300
+
301
+ refs.push({
302
+ index,
303
+ line,
304
+ column,
305
+ rawRef: parsed.srcRaw,
306
+ kind: "img",
307
+ context: block.slice(0, 200),
308
+ parse: parsed,
309
+ });
310
+ }
311
+ return refs;
312
+ }
313
+
149
314
  /**
150
315
  * @param {string} ref
151
316
  * @param {string} hostDir
@@ -177,12 +342,22 @@ function resolveLocalImageRef(ref, hostDir) {
177
342
  /**
178
343
  * @param {string} content
179
344
  * @param {string} hostDir
345
+ * @param {string} [sourceExt] - lowercase extension e.g. `.pug` to run Pug `img()` extraction
180
346
  * @returns {RawRef[]}
181
347
  */
182
- function extractFromContent(content, hostDir) {
348
+ function extractFromContent(content, hostDir, sourceExt = "") {
183
349
  /** @type {RawRef[]} */
184
350
  const refs = [];
185
351
 
352
+ if (sourceExt === ".pug") {
353
+ const pugRefs = extractPugImgRefs(content);
354
+ for (const r of pugRefs) {
355
+ refs.push(r);
356
+ }
357
+ }
358
+
359
+ const pictureModernIntervals = collectPictureModernIntervals(content);
360
+
186
361
  const imgRe = /<img\b[^>]*>/gis;
187
362
  let m;
188
363
  while ((m = imgRe.exec(content)) !== null) {
@@ -190,6 +365,11 @@ function extractFromContent(content, hostDir) {
190
365
  const index = m.index;
191
366
  const { line, column } = lineColAt(content, index);
192
367
  const parsed = parseImgTag(tag);
368
+ const pictureHasModernSources = imgInsidePictureModern(
369
+ index,
370
+ tag.length,
371
+ pictureModernIntervals
372
+ );
193
373
 
194
374
  if (parsed.srcKind === "dynamic") {
195
375
  refs.push({
@@ -199,7 +379,11 @@ function extractFromContent(content, hostDir) {
199
379
  rawRef: "",
200
380
  kind: "img",
201
381
  context: tag.slice(0, 200),
202
- parse: { ...parsed, dynamic: true },
382
+ parse: {
383
+ ...parsed,
384
+ dynamic: true,
385
+ ...(pictureHasModernSources ? { pictureHasModernSources: true } : {}),
386
+ },
203
387
  });
204
388
  continue;
205
389
  }
@@ -211,7 +395,11 @@ function extractFromContent(content, hostDir) {
211
395
  rawRef: "",
212
396
  kind: "img",
213
397
  context: tag.slice(0, 200),
214
- parse: { ...parsed, missingSrc: true },
398
+ parse: {
399
+ ...parsed,
400
+ missingSrc: true,
401
+ ...(pictureHasModernSources ? { pictureHasModernSources: true } : {}),
402
+ },
215
403
  });
216
404
  continue;
217
405
  }
@@ -223,7 +411,9 @@ function extractFromContent(content, hostDir) {
223
411
  rawRef: parsed.srcRaw,
224
412
  kind: "img",
225
413
  context: tag.slice(0, 200),
226
- parse: parsed,
414
+ parse: pictureHasModernSources
415
+ ? { ...parsed, pictureHasModernSources: true }
416
+ : parsed,
227
417
  });
228
418
  }
229
419
 
@@ -315,7 +505,7 @@ async function runPool(tasks, concurrency) {
315
505
  }
316
506
 
317
507
  /**
318
- * @param {string} rootDir
508
+ * @param {string} rootDir 扫描根路径:可为**目录**(递归枚举源码)或**单个源码文件**(仅扫描该文件)。传 `.` 表示当前工作目录。
319
509
  * @param {{
320
510
  * recursive?: boolean,
321
511
  * cwd?: string,
@@ -364,17 +554,26 @@ export async function scanCodeReferences(rootDir, options = {}) {
364
554
  } catch {
365
555
  throw new Error(`路径不存在: ${absoluteRoot}`);
366
556
  }
367
- if (!st.isDirectory()) {
368
- throw new Error(`不是目录: ${absoluteRoot}`);
557
+ /** @type {string[]} */
558
+ let sourcePaths;
559
+ if (st.isFile()) {
560
+ const ext = extname(absoluteRoot).toLowerCase();
561
+ if (!sourceExts.has(ext)) {
562
+ const supported = [...sourceExts].sort().join(", ");
563
+ throw new Error(`不支持的源码扩展名: ${ext || "(无)"}(支持: ${supported})`);
564
+ }
565
+ sourcePaths = [absoluteRoot];
566
+ } else if (st.isDirectory()) {
567
+ sourcePaths = await collectSourceFiles(
568
+ absoluteRoot,
569
+ excludeDirs,
570
+ sourceExts,
571
+ recursive
572
+ );
573
+ } else {
574
+ throw new Error(`不是文件或目录: ${absoluteRoot}`);
369
575
  }
370
576
 
371
- const sourcePaths = await collectSourceFiles(
372
- absoluteRoot,
373
- excludeDirs,
374
- sourceExts,
375
- recursive
376
- );
377
-
378
577
  /** @type {Array<{ file: string, absHost: string, ref: RawRef }>} */
379
578
  const staged = [];
380
579
 
@@ -389,7 +588,8 @@ export async function scanCodeReferences(rootDir, options = {}) {
389
588
  const relFile = relative(cwd, absPath);
390
589
  const displayFile =
391
590
  relFile && !relFile.startsWith("..") ? relFile : absPath;
392
- const rawRefs = extractFromContent(content, hostDir);
591
+ const sourceExt = extname(absPath).toLowerCase();
592
+ const rawRefs = extractFromContent(content, hostDir, sourceExt);
393
593
  for (const ref of rawRefs) {
394
594
  staged.push({ file: displayFile, absHost: hostDir, ref });
395
595
  }
@@ -451,7 +651,7 @@ export async function scanCodeReferences(rootDir, options = {}) {
451
651
 
452
652
  if (r.parse?.dynamic) {
453
653
  issues.push("needs_manual_review");
454
- hints.push("Vue/React 动态 :src 或绑定,需人工确认资源与尺寸");
654
+ hints.push("Vue/React/Pug 动态 src 或绑定,需人工确认资源与尺寸");
455
655
  items.push({
456
656
  file,
457
657
  line: r.line,
@@ -505,7 +705,7 @@ export async function scanCodeReferences(rootDir, options = {}) {
505
705
  intrinsicHeight: null,
506
706
  intrinsicFormat: null,
507
707
  issues: ["cannot_resolve"],
508
- hints: [`跳过: ${resolved.reason}`],
708
+ hints: [`Skipped: ${resolved.reason}`],
509
709
  snippet: r.context,
510
710
  };
511
711
  if (!issuesOnly || row.issues.length) items.push(row);
@@ -564,7 +764,7 @@ export async function scanCodeReferences(rootDir, options = {}) {
564
764
  if (!reservesSpace && iw > 0 && ih > 0) {
565
765
  issues.push("missing_dimensions");
566
766
  hints.push(
567
- "补充 widthheight,或 style 中的 aspect-ratio,以减少 CLS"
767
+ `missing_dimensions: set width/height or CSS aspect-ratio for CLS; intrinsicWidth=${iw} intrinsicHeight=${ih}`
568
768
  );
569
769
  }
570
770
 
@@ -587,6 +787,7 @@ export async function scanCodeReferences(rootDir, options = {}) {
587
787
  }
588
788
 
589
789
  if (
790
+ !(r.kind === "img" && r.parse?.pictureHasModernSources) &&
590
791
  (ext === ".jpg" ||
591
792
  ext === ".jpeg" ||
592
793
  ext === ".png" ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lake-cimg",
3
- "version": "0.0.2",
3
+ "version": "1.0.0",
4
4
  "description": "Batch image compression to WebP — CLI plus Agent Skills for frontend image audit (scan-code, picture stack)",
5
5
  "keywords": [
6
6
  "image",
@@ -34,7 +34,8 @@
34
34
  },
35
35
  "scripts": {
36
36
  "compress": "node bin/cimg.js",
37
- "start": "node bin/cimg.js"
37
+ "start": "node bin/cimg.js",
38
+ "test": "node --test test/*.test.mjs"
38
39
  },
39
40
  "dependencies": {
40
41
  "commander": "^12.0.0",
package/skills/README.md CHANGED
@@ -4,7 +4,7 @@ This directory follows the layout expected by **[Skills CLI](https://www.npmjs.c
4
4
 
5
5
  | Skill | Summary |
6
6
  | --- | --- |
7
- | [cimg-audit](./cimg-audit/SKILL.md) | Audit `<img>` / imports / `url()` for CLS, aspect ratio, and format hints via `npx lake-cimg@latest scan-code` |
7
+ | [cimg-audit](./cimg-audit/SKILL.md) | Audit `<img>` / Pug `img()` / imports / `url()` for CLS, aspect ratio, and format hints via `npx lake-cimg@latest scan-code` |
8
8
 
9
9
  ### When to use
10
10
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: cimg-audit
3
3
  description: >-
4
- Audits HTML, Vue, JS, TS, TSX, and JSX image references against intrinsic
4
+ Audits HTML, Vue, Pug, JS, TS, TSX, and JSX image references against intrinsic
5
5
  dimensions for CLS risk, aspect-ratio mismatches, and modern-format hints.
6
6
  Use when optimizing images, fixing layout shift, LCP heroes, picture/srcset,
7
7
  or running lake-cimg scan-code via npx lake-cimg@latest.
@@ -9,49 +9,55 @@ description: >-
9
9
 
10
10
  # cimg image audit
11
11
 
12
- Use **`npx lake-cimg@latest`** from the project root (no global install).
12
+ Run **`npx lake-cimg@latest`** from the **project root** (no global install).
13
13
 
14
- ## Workflow
14
+ <a id="invoke-scan-code"></a>
15
15
 
16
- Copy into a task list and tick as you go:
16
+ ## Invoke `scan-code`
17
17
 
18
- - [ ] **1. Scan (read-only):** `npx lake-cimg@latest scan-code <dir>` — **stdout is JSON** (default); **`--plain`** for a short text summary. Prefer an **absolute** path for `<dir>`.
19
- - [ ] **2. Triage `issues`:** Each JSON row has `issues` (string codes) and **`hints`** (human-readable, often Chinese)—use **`hints` first** when choosing a fix; codes are for filtering and grouping. Possible codes: `missing_dimensions`, `aspect_ratio_mismatch`, `suggest_modern_format`, `needs_manual_review`, `missing_src`, `cannot_resolve`, `cannot_read_metadata`.
20
- - [ ] **3. Fix markup:** add `width`/`height`, CSS `aspect-ratio`, or intentional `object-fit: cover|contain` when the box ratio must differ from the asset.
21
- - [ ] **4. Optimize assets last:** `npx lake-cimg@latest <path> [options]`; for AVIF+WebP+JPEG stacks: `npx lake-cimg@latest picture <input> -O <outDir>` (see package README).
18
+ Use **one** command starting with `npx` — **not** `cd && npx …` (PowerShell 5.1 on Windows does not support `&&`).
22
19
 
23
- ## Rules of thumb
20
+ | `path` | Behavior |
21
+ | --- | --- |
22
+ | *(omitted)* | Scans **`.`** (current working directory). Run from repo root to cover the tree. |
23
+ | **Directory** | Recursively scans supported sources under that folder (honors `--no-recursive`). |
24
+ | **Single file** | Only that file. Extension must be `.html`, `.htm`, `.vue`, `.pug`, `.js`, `.mjs`, `.cjs`, `.ts`, `.tsx`, or `.jsx`. |
24
25
 
25
- - **Aspect ratio:** For undistorted display, match **display ratio** to **intrinsic** (width ÷ height). For crop/letterbox, use **`object-fit`** with explicit size/aspect-ratio.
26
- - **CLS:** Prefer **`width` and `height`** on `<img>` (or **`aspect-ratio`** when fluid) so space is reserved before paint.
27
- - **Responsive:** `<picture>` / **`srcset` + `sizes`**; align `type` with AVIF/WebP/JPEG; React uses **`srcSet`**. Use `picture` (and `-s`) to emit multiple width variants if needed.
28
- - **LCP:** At most one hero: **`fetchPriority="high"`**, **`loading="eager"`**; lazy-load the rest.
29
- - **Alt:** Describe **what’s in the image** and **how it supports the page** (subject + role in context). **Do not** stack keywords for SEO; empty **`alt=""`** only when the image is decorative. More: [reference.md](reference.md#alt-text).
26
+ **Output:** stdout is **only** pretty-printed JSON: `items[]` with `issues`, `hints`, `snippet`, `intrinsicWidth` / `intrinsicHeight` when metadata was read, plus top-level `summary` and scan metadata. **`--issues-only`** drops rows with empty `issues`. For **`missing_dimensions`**, use **`intrinsicWidth` / `intrinsicHeight`** on the same item and/or the English hint that repeats them.
30
27
 
31
- ## Limits
28
+ **Examples:**
32
29
 
33
- Dynamic **`src`** (e.g. `:src` without a static path) → **`needs_manual_review`**. Remote URLs, **`data:`**, and aliases (**`@/`**) → **`cannot_resolve`** until mapped to real paths. The scanner does not fetch network images.
30
+ ```bash
31
+ npx lake-cimg@latest scan-code
32
+ npx lake-cimg@latest scan-code /absolute/path/to/src
33
+ npx lake-cimg@latest scan-code /absolute/path/to/about.pug
34
+ ```
34
35
 
35
- ## CLI
36
+ More flags: `npx lake-cimg@latest scan-code --help` (e.g. `--limit`, `--issues-only`, `--no-recursive`). Prefer an **absolute** `path` if the shell cwd may not be the repo root.
36
37
 
37
- | Command | Role |
38
- | --- | --- |
39
- | `npx lake-cimg@latest scan-code <dir>` | Reference audit (JSON default; `--plain` for text) |
40
- | `npx lake-cimg@latest <path>` | Compress / WebP (`--help` for `-o`, `-s`, `-q`, `-r`) |
41
- | `npx lake-cimg@latest picture <input> -O <outDir>` | One source → AVIF + WebP + JPEG for `<picture>` |
38
+ ## Workflow
42
39
 
43
- `npx lake-cimg@latest --help` global options. `npx lake-cimg@latest scan-code --help` — `--limit`, `--issues-only`, `--no-recursive`, `--plain`.
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` (consider AVIF/WebP + `<picture>` — match product/browser support), `needs_manual_review`, `missing_src`, `cannot_resolve`, `cannot_read_metadata`.
42
+ - [ ] **3. Fix then optimize:** Apply markup fixes using [reference.md](reference.md) templates (`<img>`, `<picture>`). For **all** raster refs that need format or responsive delivery, not only one hero row. Then run compression / stacks: `npx lake-cimg@latest <path> [options]`; for AVIF + WebP + JPEG from one source: `npx lake-cimg@latest picture <input> -O <outDir>` (details: package [README.md](../../README.md)).
44
43
 
45
- ## Examples
44
+ ## Rules of thumb
46
45
 
47
- **Read-only audit (stdout = JSON for parsing):**
46
+ - **Aspect ratio:** Match display ratio to intrinsic (w÷h), or use **`object-fit`** + explicit box / `aspect-ratio` for crop/letterbox.
47
+ - **CLS:** Add `width` and `height` to `<img>`, or use CSS `aspect-ratio`. For `missing_dimensions`, fill in the exact `intrinsicWidth` / `intrinsicHeight`.
48
+ - **Responsive:** `<picture>` and/or **`srcset` + `sizes`**; each `<source type="…">` must match the real file type; Generate width variants with `picture … -s <px>` or separate exports.
49
+ - **LCP:** At most one hero per view: **`fetchPriority="high"`**, **`loading="eager"`**; lazy-load the rest.
50
+ - **Alt:** Describe content and purpose; no keyword stuffing; **`alt=""`** only for decorative images.
48
51
 
49
- ```bash
50
- npx lake-cimg@latest scan-code /absolute/path/to/project
51
- ```
52
+ ## What the scanner cannot resolve
52
53
 
53
- Use **`--plain`** when you want a short **text** summary in the terminal instead of JSON.
54
+ Dynamic **`src`** without a static path **`needs_manual_review`**. **`http(s):`**, **`data:`**, and path aliases (**`@/`**, **`~/`**, etc.) → **`cannot_resolve`** (no alias map reads; no network fetch).
54
55
 
55
- ## Additional resources
56
+ If **`hints`** mention alias skip: use **`rawRef`** and map the alias via Vite/webpack/tsconfig/Nuxt config. With a **real filesystem path**, run `npx lake-cimg@latest scan <resolved-path>` on assets or re-run **`scan-code`** on markup that uses resolvable relative paths. If unresolved, triage without intrinsic dimensions.
56
57
 
57
- - Format choice, responsive `<picture>` patterns, and LCP markup examples: [reference.md](reference.md)
58
+ ## Other CLI (after scan-code)
59
+
60
+ | Command | Role |
61
+ | --- | --- |
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 for `<picture>`. |
@@ -1,48 +1,39 @@
1
- # Image optimization reference
1
+ # Image markup templates
2
2
 
3
- Supplement for [SKILL.md](SKILL.md). Browser share figures are **approximate** (global usage trends; check [Can I use](https://caniuse.com/) for current data).
3
+ Supplement for [SKILL.md](SKILL.md). Copy-paste starting points; adjust paths, dimensions, and `sizes` to match your assets and layout.
4
4
 
5
- ## Format selection
5
+ ## Basic `<img>`
6
6
 
7
- | Format | Use case | Browser support (approx.) |
8
- | --- | --- | --- |
9
- | **AVIF** | Photos, best compression | 92%+ |
10
- | **WebP** | Photos, good fallback | 97%+ |
11
- | **PNG** | Graphics with transparency | Universal |
12
- | **SVG** | Icons, logos, illustrations | Universal |
7
+ **When:** Single raster URL; reserve space for CLS. **Check:** `width` / `height` match intrinsic pixels (or use `aspect-ratio` for fluid); meaningful `alt` or `alt=""` if decorative.
13
8
 
14
- Pair with **`lake-cimg`** `picture` subcommand for AVIF + WebP + JPEG from one source when you need stacks; use PNG/SVG where vector or lossless transparency matters.
15
-
16
- ## Alt text
17
-
18
- Write **`alt`** as: **图里是什么 / 与页面主题的关系** — what the image shows and why it belongs on this screen (one concise phrase or sentence).
19
-
20
- - **Do:** name the subject, setting, or action that matters; match **language** and **tone** of the page.
21
- - **Don’t:** repeat the page title, brand slogans, or comma‑separated “SEO” keywords; don’t start with “image of …” unless the medium itself matters (e.g. screenshot vs photo).
9
+ ```html
10
+ <img
11
+ src="photo.jpg"
12
+ width="1200"
13
+ height="800"
14
+ alt="Describe subject and purpose on this page"
15
+ loading="lazy"
16
+ decoding="async">
17
+ ```
22
18
 
23
- Decorative visuals: **`alt=""`** and ensure they’re not the only way to convey essential information.
19
+ ## `<picture>` + `srcset` + `sizes`
24
20
 
25
- ## Responsive images (`<picture>` + `srcset` + `sizes`)
21
+ **When:** AVIF/WebP stack with JPEG (or similar) fallback; responsive widths. **Check:** Each `<source type="…">` matches the real file type; `sizes` matches breakpoints; `<img>` is the final fallback with matching `srcset` / `sizes`.
26
22
 
27
23
  ```html
28
24
  <picture>
29
- <!-- AVIF for modern browsers -->
30
25
  <source
31
26
  type="image/avif"
32
27
  srcset="hero-400.avif 400w,
33
28
  hero-800.avif 800w,
34
29
  hero-1200.avif 1200w"
35
30
  sizes="(max-width: 600px) 100vw, 50vw">
36
-
37
- <!-- WebP fallback -->
38
31
  <source
39
32
  type="image/webp"
40
33
  srcset="hero-400.webp 400w,
41
34
  hero-800.webp 800w,
42
35
  hero-1200.webp 1200w"
43
36
  sizes="(max-width: 600px) 100vw, 50vw">
44
-
45
- <!-- JPEG fallback -->
46
37
  <img
47
38
  src="hero-800.jpg"
48
39
  srcset="hero-400.jpg 400w,
@@ -51,45 +42,8 @@ Decorative visuals: **`alt=""`** and ensure they’re not the only way to convey
51
42
  sizes="(max-width: 600px) 100vw, 50vw"
52
43
  width="1200"
53
44
  height="600"
54
- alt="Short: what’s shown + why it’s on this page (no keyword stuffing)"
45
+ alt="Describe subject and purpose on this page"
55
46
  loading="lazy"
56
47
  decoding="async">
57
48
  </picture>
58
49
  ```
59
-
60
- - Match **`sizes`** to real layout breakpoints; generate multiple widths with **`npx lake-cimg@latest picture … -s <px>`** runs or separate exports per width.
61
- - In **React**, use **`srcSet`** and **`fetchPriority`** (camelCase) on `<img>`.
62
-
63
- ## LCP image priority
64
-
65
- **Above-the-fold LCP candidate** — eager load and high fetch priority:
66
-
67
- ```html
68
- <img
69
- src="hero.webp"
70
- fetchpriority="high"
71
- loading="eager"
72
- decoding="sync"
73
- alt="Hero: subject + role on this page (not SEO keywords)">
74
- ```
75
-
76
- **Below-the-fold** — defer work off the critical path:
77
-
78
- ```html
79
- <img
80
- src="product.webp"
81
- loading="lazy"
82
- decoding="async"
83
- alt="Product: what’s visible + relevance (no keyword list)">
84
- ```
85
-
86
- Use **at most one** `fetchpriority="high"` (or `fetchPriority="high"` in React) per route/view for the true LCP image; lazy-load the rest.
87
-
88
- ## CLI tie-in
89
-
90
- | Need | Command |
91
- | --- | --- |
92
- | Audit references + intrinsic size vs markup | `npx lake-cimg@latest scan-code <dir>` |
93
- | One file → AVIF + WebP + JPEG | `npx lake-cimg@latest picture <input> -O <outDir>` |
94
-
95
- Full options: repository **README.md** at package root.