Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ The image runs as a non-root user by default. If the mounted directory has stric

- `-c, --config <configure-file>`: 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 <thread-count>`: 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 <size>`: skip Markdown files larger than `<size>` (e.g. `5mb`, `500kb`, `1gb`), with a warning to stderr
Expand All @@ -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`)
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ docker run --rm \

- `-c, --config <configure-file>`:指定配置文件(默认 `./.lintmdrc`)
- `-f, --fix`:自动修复可修复问题
- `-t, --threads [thread-count]`:设置并发线程数;默认 `auto` 按文件大小使用保守上限,传数字可显式覆盖自动限制
- `-t, --threads <thread-count>`:设置并发线程数;传入正整数或 `auto`。省略此选项时默认使用 `auto`
- `-s, --suppress-warnings`:忽略 warning 对退出码的影响(便于 CI 渐进接入)
- `-i, --stdin`:从标准输入读取 Markdown 内容
- `--max-file-size <size>`:跳过超过指定大小的 Markdown 文件(如 `5mb`、`500kb`、`1gb`),并向 stderr 输出警告
Expand All @@ -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 |
Expand Down
75 changes: 75 additions & 0 deletions __tests__/cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
41 changes: 30 additions & 11 deletions src/lint-md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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 <thread-count>",
'Worker threads, or "auto" for conservative file-size limits. Default: auto.(执行 Lint / Fix 的线程数。默认 auto;传数字可显式覆盖自动限制)'
)
.option(
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
Loading