diff --git a/README.en-US.md b/README.en-US.md index 6ef738f..5a84e73 100644 --- a/README.en-US.md +++ b/README.en-US.md @@ -77,7 +77,7 @@ The image runs as a non-root user by default. If the mounted directory has stric - `-c, --config `: use a configuration file, defaults to `./.lintmdrc` - `-f, --fix`: automatically fix fixable issues -- `-t, --threads [thread-count]`: set the worker count; the default `auto` mode uses conservative file-size limits +- `-t, --threads `: set a positive worker count or `auto`; omitting this option uses `auto` - `-s, --suppress-warnings`: ignore warnings when deciding the exit code, which helps with gradual CI adoption - `-i, --stdin`: read Markdown content from standard input - `--max-file-size `: skip Markdown files larger than `` (e.g. `5mb`, `500kb`, `1gb`), with a warning to stderr @@ -89,6 +89,10 @@ The image runs as a non-root user by default. If the mounted directory has stric `--fix` supports `.mdx` files. The CLI calls Core `fixMdx()` for `.mdx` and `fixMarkdown()` for Markdown. The CLI uses the MDX Parser to separate Markdown body text from MDX code regions. Built-in rules preserve ESM, JSX attributes, and JavaScript expressions in supported cases. The fix safety of third-party rules depends on the rule implementation. To exclude MDX, set `"extensions": [".md", ".markdown"]`. Use `--threads 1` or `--threads 2` for complex Markdown or memory-limited CI environments. +For example, `lint-md a.md` uses `auto`. Use `lint-md a.md --threads=4` to set four workers. +Use `lint-md a.md --threads=auto` to select `auto` explicitly. +The short option also accepts `lint-md -t 4 a.md` and `lint-md -t auto a.md`. +Do not put a file path immediately after `-t`. Higher explicit concurrency can increase peak memory. ## Configuration Example (`.lintmdrc`) diff --git a/README.md b/README.md index 1f0ad5c..36af32b 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ docker run --rm \ - `-c, --config `:指定配置文件(默认 `./.lintmdrc`) - `-f, --fix`:自动修复可修复问题 -- `-t, --threads [thread-count]`:设置并发线程数;默认 `auto` 按文件大小使用保守上限,传数字可显式覆盖自动限制 +- `-t, --threads `:设置并发线程数;传入正整数或 `auto`。省略此选项时默认使用 `auto` - `-s, --suppress-warnings`:忽略 warning 对退出码的影响(便于 CI 渐进接入) - `-i, --stdin`:从标准输入读取 Markdown 内容 - `--max-file-size `:跳过超过指定大小的 Markdown 文件(如 `5mb`、`500kb`、`1gb`),并向 stderr 输出警告 @@ -90,6 +90,9 @@ docker run --rm \ 未指定 `--threads` 时,程序等同于使用 `--threads auto`。`auto` 根据过滤后最大文件的大小设置 worker 上限: +例如:`lint-md a.md` 使用默认 `auto`。`lint-md a.md --threads=4` 使用 4 个线程。`lint-md a.md --threads=auto` 显式使用 `auto`。 +短选项也接受 `lint-md -t 4 a.md` 和 `lint-md -t auto a.md`。不要将文件路径紧跟在 `-t` 后面。 + | 过滤后最大文件大小 | worker 上限 | | --- | ---: | | 小于 `1 MiB` | 4 | diff --git a/__tests__/cli.spec.ts b/__tests__/cli.spec.ts index 3a2cb7b..48bb9c3 100644 --- a/__tests__/cli.spec.ts +++ b/__tests__/cli.spec.ts @@ -101,6 +101,81 @@ describe("cli tests", () => { expect(process.exitCode).toBe(1); }); + test.each([ + [["a.md"], "auto"], + [["-t", "4", "a.md"], 4], + [["-t", "auto", "a.md"], "auto"], + [["a.md", "--threads=4"], 4], + [["a.md", "--threads=auto"], "auto"], + ] as const)("passes %p with %p threads", async (args, threadCount) => { + const runFileLint = jest.fn().mockResolvedValue({ exitCode: 0 }); + jest.doMock("../src/cli/run-lint", () => ({ + runFileLint, + runStdinLint: jest.fn(), + })); + const { main } = require("../src/lint-md"); + + await main(["node", "lint-md", ...args]); + + expect(runFileLint).toHaveBeenCalledWith( + expect.objectContaining({ files: ["a.md"], threadCount }) + ); + }); + + test.each(["a.md", "README"])( + "rejects a file path consumed by -t: %s", + async (filePath) => { + const runFileLint = jest.fn(); + jest.doMock("../src/cli/run-lint", () => ({ + runFileLint, + runStdinLint: jest.fn(), + })); + const { main } = require("../src/lint-md"); + + await expect( + main(["node", "lint-md", "-t", filePath]) + ).rejects.toMatchObject({ + code: "INVALID_THREADS", + message: expect.stringContaining(`Received "${filePath}"`), + }); + expect(runFileLint).not.toHaveBeenCalled(); + } + ); + + test("rejects the first file when -t precedes two file paths", async () => { + const runFileLint = jest.fn(); + jest.doMock("../src/cli/run-lint", () => ({ + runFileLint, + runStdinLint: jest.fn(), + })); + const { main } = require("../src/lint-md"); + + await expect( + main(["node", "lint-md", "-t", "a.md", "b.md"]) + ).rejects.toMatchObject({ + code: "INVALID_THREADS", + message: expect.stringContaining('Received "a.md"'), + }); + expect(runFileLint).not.toHaveBeenCalled(); + }); + + test("rejects zero threads before file lint", async () => { + const runFileLint = jest.fn(); + jest.doMock("../src/cli/run-lint", () => ({ + runFileLint, + runStdinLint: jest.fn(), + })); + const { main } = require("../src/lint-md"); + + await expect( + main(["node", "lint-md", "-t", "0", "a.md"]) + ).rejects.toMatchObject({ + code: "INVALID_THREADS", + message: "[lint-md] --threads must be a positive integer.", + }); + expect(runFileLint).not.toHaveBeenCalled(); + }); + test("runCli handles rejected actions", async () => { const error = new Error("load failed"); const loadMdFiles = jest.fn(() => Promise.reject(error)); diff --git a/src/lint-md.ts b/src/lint-md.ts index 3c612be..7cdfd0d 100644 --- a/src/lint-md.ts +++ b/src/lint-md.ts @@ -16,13 +16,14 @@ import { getThreadCount, } from "./utils/configure"; import type { ThreadCount } from "./types"; +import { sanitizeTerminalText } from "./utils/sanitize-terminal"; interface CLIOptions { fix?: boolean; dev?: boolean; config?: string; suppressWarnings: boolean; - threads?: string | boolean; + threads?: string; stdin?: boolean; maxFileSize?: string; } @@ -45,7 +46,7 @@ export const createProgram = (): Command => { .option("-f, --fix", "fix the errors automatically(开启修复模式)") .option("-d, --dev", "open dev mode(开启开发者模式)") .option( - "-t, --threads [thread-count]", + "-t, --threads ", 'Worker threads, or "auto" for conservative file-size limits. Default: auto.(执行 Lint / Fix 的线程数。默认 auto;传数字可显式覆盖自动限制)' ) .option( @@ -76,13 +77,6 @@ export const createProgram = (): Command => { const isFixMode = Boolean(fix); const isDev = Boolean(dev); - // No input at all: show help before touching config, threads, or - // stdin. A broken .lintmdrc must not turn bare "lint-md" into a - // config error. - if (!files.length && !stdin) { - program.help(); - } - // Fail before touching stdin or the file list so neither input mode // starts on a command that mixes both. if (stdin && files.length > 0) { @@ -117,14 +111,39 @@ export const createProgram = (): Command => { ); } + // Bare invocation shows help before reading config or thread settings. + if (!files.length && !stdin && threads === undefined) { + program.help(); + } + + // Commander can consume a file path as the value of -t. + // Report nonnumeric values before the no-input help path. + if ( + typeof threads === "string" && + threads !== "auto" && + !Number.isFinite(Number(threads)) + ) { + throw new CliError( + "INVALID_THREADS", + `[lint-md] --threads must be a positive integer or auto. Received ${JSON.stringify( + sanitizeTerminalText(threads) + )}. If this is a file path, omit -t. Use --threads=auto or --threads=N.` + ); + } + + const threadCount: ThreadCount = getThreadCount(threads); + + // A valid thread setting without a file still needs input. + if (!files.length && !stdin) { + program.help(); + } + if (isDev) { console.log(`dev -- version: ${version}, ${new Date().toString()}`); } const { rules, excludeFiles, extensions } = getLintConfig(config); - const threadCount: ThreadCount = getThreadCount(threads); - const maxFileSizeBytes = getMaxFileSizeOption(maxFileSize); if (stdin) {