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
4 changes: 4 additions & 0 deletions README.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ The image runs as a non-root user by default. If the mounted directory has stric
- `-d, --dev`: enable development debug mode
- `-v, --version`: print the current version

`--stdin` always parses input as Markdown. Do not use `--stdin --fix` on MDX content. MDX autofix currently supports file-path mode only.

`--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.
Higher explicit concurrency can increase peak memory.

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ docker run --rm \
- `-d, --dev`:开发调试模式
- `-v, --version`:查看版本

`--fix` 不支持 `.mdx` 文件。只要待修复文件中包含 `.mdx`,命令就以退出码 `1` 结束,且不修改任何文件。如需排除 MDX,请在配置中设置 `"extensions": [".md", ".markdown"]`。
`--stdin` 始终按 Markdown 解析。请勿通过 `--stdin --fix` 修复 MDX 内容;MDX 自动修复目前仅支持文件路径模式。

`--fix` 支持 `.mdx` 文件。CLI 对 `.mdx` 调用 Core 的 `fixMdx()`,对普通 Markdown 调用 `fixMarkdown()`。CLI 使用 MDX Parser 区分 Markdown 正文与 MDX 代码区域。内置规则在已支持的场景下保留 ESM、JSX 属性和 JavaScript 表达式;第三方规则的修复安全性取决于规则自身实现。如需排除 MDX,请在配置中设置 `"extensions": [".md", ".markdown"]`。

未指定 `--threads` 时,程序等同于使用 `--threads auto`。`auto` 根据过滤后最大文件的大小设置 worker 上限:

Expand Down
22 changes: 13 additions & 9 deletions __tests__/fix-exit-code.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,30 +97,34 @@ describe.each(["file", "stdin"] as const)("--fix exit code with %s", (mode) => {
});
});

test("--fix rejects a mixed Markdown and MDX batch without changing files", () => {
test("--fix repairs a mixed Markdown and MDX batch", () => {
const tmpDir = mkdtempSync(path.join(tmpdir(), "lint-md-mixed-mdx-"));
const mdPath = path.join(tmpDir, "test.md");
const mdxPath = path.join(tmpDir, "test.mdx");
const mdContent = readFileSync(FIXABLE, "utf8");
const mdxContent = "export const 用户ID = 42\n";
const mdContent = "正文English内容。\n";
const mdxContent =
'export const title = "中文English";\n\n<Card>正文English内容。</Card>\n';

try {
writeFileSync(mdPath, mdContent);
writeFileSync(mdxPath, mdxContent);
writeFileSync(path.join(tmpDir, ".lintmdrc"), "{}");
writeFileSync(
path.join(tmpDir, ".lintmdrc"),
JSON.stringify({ rules: { "space-around-alphabet": 2 } })
);

const result = spawnSync(
process.execPath,
[TSX, CLI, "--fix", path.join(tmpDir, "*"), "--threads", "1"],
{ cwd: tmpDir, encoding: "utf8" }
);

expect(result.status).toBe(1);
expect(result.stderr).toContain(
"[lint-md] --fix does not support .mdx files. No files were changed."
expect(result.status).toBe(0);
expect(result.stderr).not.toContain("does not support .mdx");
expect(readFileSync(mdPath, "utf8")).toBe("正文 English 内容。\n");
expect(readFileSync(mdxPath, "utf8")).toBe(
'export const title = "中文English";\n\n<Card>正文 English 内容。</Card>\n'
);
expect(readFileSync(mdPath, "utf8")).toBe(mdContent);
expect(readFileSync(mdxPath, "utf8")).toBe(mdxContent);
} finally {
rmSync(tmpDir, { recursive: true, force: true });
}
Expand Down
99 changes: 99 additions & 0 deletions __tests__/lint-worker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ jest.mock("@lint-md/core", () => ({
fixMarkdown: jest.fn(),
lintMarkdown: jest.fn(),
}));
jest.mock("@lint-md/core/mdx", () => ({
fixMdx: jest.fn(),
lintMdx: jest.fn(),
}));

import { fixMarkdown, lintMarkdown } from "@lint-md/core";
import { fixMdx, lintMdx } from "@lint-md/core/mdx";
import lintWorker from "../src/utils/lint-worker";
import { writeFile, mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
Expand All @@ -17,6 +22,8 @@ const mockedLintMarkdown = lintMarkdown as jest.MockedFunction<
const mockedFixMarkdown = fixMarkdown as jest.MockedFunction<
typeof fixMarkdown
>;
const mockedLintMdx = lintMdx as jest.MockedFunction<typeof lintMdx>;
const mockedFixMdx = fixMdx as jest.MockedFunction<typeof fixMdx>;

describe("lintWorker executionErrors passthrough", () => {
let tmpDir: string;
Expand All @@ -25,6 +32,8 @@ describe("lintWorker executionErrors passthrough", () => {
tmpDir = await mkdtemp(path.join(tmpdir(), "lint-worker-exec-"));
mockedFixMarkdown.mockReset();
mockedLintMarkdown.mockReset();
mockedFixMdx.mockReset();
mockedLintMdx.mockReset();
});

afterEach(async () => {
Expand Down Expand Up @@ -183,6 +192,96 @@ describe("lintWorker executionErrors passthrough", () => {
expect(result.changed).toBe(false);
});

test("uses fixMdx for MDX fix mode", async () => {
const file = path.join(tmpDir, "fix.mdx");
const rules = { "space-around-alphabet": 2 };
const source = "<Card>正文English内容。</Card>\n";
await writeFile(file, source, "utf8");

mockedFixMdx.mockReturnValue({
lintResult: [],
diagnostics: [],
summary: {
errorCount: 0,
warningCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0,
},
remainingSummary: {
errorCount: 0,
warningCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0,
},
fixedResult: { result: "<Card>正文 English 内容。</Card>\n" },
executionErrors: [],
} as any);

const result = await lintWorker({ filePath: file, rules, isFixMode: true });

expect(mockedFixMdx).toHaveBeenCalledWith(source, { rules });
expect(mockedFixMarkdown).not.toHaveBeenCalled();
expect(result.changed).toBe(true);
});

test("does not mark an unchanged MDX fix as changed", async () => {
const file = path.join(tmpDir, "unchanged.mdx");
const source = '<Card label="中文English" />\n';
await writeFile(file, source, "utf8");

mockedFixMdx.mockReturnValue({
lintResult: [],
diagnostics: [],
summary: {
errorCount: 0,
warningCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0,
},
remainingSummary: {
errorCount: 0,
warningCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0,
},
fixedResult: { result: source },
executionErrors: [],
} as any);

const result = await lintWorker({
filePath: file,
rules: {},
isFixMode: true,
});

expect(result.changed).toBe(false);
expect(mockedFixMarkdown).not.toHaveBeenCalled();
});

test("uses lintMdx for MDX lint mode", async () => {
const file = path.join(tmpDir, "lint.mdx");
const source = "<Card>正文English内容。</Card>\n";
await writeFile(file, source, "utf8");

mockedLintMdx.mockReturnValue({
lintResult: [],
diagnostics: [],
summary: {
errorCount: 0,
warningCount: 0,
fixableErrorCount: 0,
fixableWarningCount: 0,
},
fixedResult: null,
} as any);

await lintWorker({ filePath: file, rules: {}, isFixMode: false });

expect(mockedLintMdx).toHaveBeenCalledWith(source, { rules: {} });
expect(mockedFixMdx).not.toHaveBeenCalled();
expect(mockedLintMarkdown).not.toHaveBeenCalled();
});

test("does not mark changed outside fix mode", async () => {
const file = path.join(tmpDir, "lint-only.md");
await writeFile(file, "# Lint\n", "utf8");
Expand Down
Loading
Loading