diff --git a/__tests__/mdx-lint.spec.ts b/__tests__/mdx-lint.spec.ts new file mode 100644 index 0000000..7374cc9 --- /dev/null +++ b/__tests__/mdx-lint.spec.ts @@ -0,0 +1,129 @@ +import { spawnSync } from "child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs"; +import { tmpdir } from "os"; +import * as path from "path"; +import lintWorker from "../src/utils/lint-worker"; + +const CLI = path.resolve(__dirname, "../lib/src/lint-md.js"); +const RULES = { "space-around-alphabet": 2 }; +const MDX = [ + 'import Widget from "中文English";', + "", + 'export const title = "中文English";', + "", + '', + "", + '{"中文English"}', + "", + "正文English内容。", + "", +].join("\n"); + +describe("MDX lint", () => { + let tmpDir: string; + let configPath: string; + + beforeEach(() => { + tmpDir = mkdtempSync(path.join(tmpdir(), "lint-md-mdx-")); + configPath = path.join(tmpDir, ".lintmdrc"); + writeFileSync(configPath, JSON.stringify({ rules: RULES }), "utf8"); + }); + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); + }); + + test("lints Markdown text", async () => { + const filePath = path.join(tmpDir, "document.md"); + writeFileSync(filePath, "正文English内容。\n", "utf8"); + + const result = await lintWorker({ + filePath, + rules: RULES, + isFixMode: false, + }); + + expect(result.diagnostics).toHaveLength(2); + expect( + result.diagnostics.map(({ ruleId, line }) => [ruleId, line]) + ).toEqual([ + ["space-around-alphabet", 1], + ["space-around-alphabet", 1], + ]); + }); + + test.each([".mdx", ".MDX"])( + "lints %s through the CLI and skips JSX, ESM, and expressions", + (extension) => { + const filePath = path.join(tmpDir, `document${extension}`); + writeFileSync(filePath, MDX, "utf8"); + + const result = spawnSync( + process.execPath, + [CLI, filePath, "--config", configPath], + { encoding: "utf8" } + ); + + expect(result.error).toBeUndefined(); + expect(result.status).toBe(1); + expect(result.stdout).toContain(filePath); + expect(result.stdout).toContain("9:10"); + expect(result.stdout).toContain("9:17"); + expect(result.stdout).toContain("2 problems"); + } + ); + + test("routes Markdown and MDX through the CLI", () => { + const markdownPath = path.join(tmpDir, "document.md"); + const mdxPath = path.join(tmpDir, "document.mdx"); + writeFileSync(markdownPath, "正文English内容。\n", "utf8"); + writeFileSync(mdxPath, MDX, "utf8"); + + const result = spawnSync( + process.execPath, + [CLI, markdownPath, mdxPath, "--config", configPath], + { encoding: "utf8" } + ); + + expect(result.error).toBeUndefined(); + expect(result.status).toBe(1); + expect(result.stdout).toContain(markdownPath); + expect(result.stdout).toContain(mdxPath); + expect(result.stdout).toContain("4 problems"); + }); + + test("rejects a direct worker fix request for MDX", async () => { + const filePath = path.join(tmpDir, "document.mdx"); + writeFileSync(filePath, MDX, "utf8"); + + await expect( + lintWorker({ filePath, rules: RULES, isFixMode: true }) + ).rejects.toThrow("--fix does not support .mdx files"); + expect(readFileSync(filePath, "utf8")).toBe(MDX); + }); + + test.each([".mdx", ".MDX"])( + "rejects %s fix requests before changing any file", + (extension) => { + const markdownPath = path.join(tmpDir, "document.md"); + const mdxPath = path.join(tmpDir, `document${extension}`); + const markdown = "正文English内容。\n"; + writeFileSync(markdownPath, markdown, "utf8"); + writeFileSync(mdxPath, MDX, "utf8"); + + const result = spawnSync( + process.execPath, + [CLI, markdownPath, mdxPath, "--fix", "--config", configPath], + { encoding: "utf8" } + ); + + expect(result.error).toBeUndefined(); + expect(result.status).toBe(1); + expect(result.stderr).toContain( + "--fix does not support .mdx files. No files were changed." + ); + expect(readFileSync(markdownPath, "utf8")).toBe(markdown); + expect(readFileSync(mdxPath, "utf8")).toBe(MDX); + } + ); +}); diff --git a/package.json b/package.json index bf5f697..87b9313 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ ] }, "dependencies": { - "@lint-md/core": "^2.5.2", + "@lint-md/core": "^2.6.0", "chalk": "^4", "commander": "^9.4.1", "glob": "^13.0.6", diff --git a/scripts/test-package.mjs b/scripts/test-package.mjs index b4790ce..7044cef 100644 --- a/scripts/test-package.mjs +++ b/scripts/test-package.mjs @@ -87,14 +87,17 @@ try { const fixtureDirectory = join(temporaryDirectory, "fixtures"); const cleanFile = join(fixtureDirectory, "clean.md"); + const cleanMdxFile = join(fixtureDirectory, "clean.mdx"); const fixableFile = join(fixtureDirectory, "fixable.md"); mkdirSync(fixtureDirectory); cpSync(join(projectRoot, "examples", "space-around.md"), fixableFile, { recursive: false, }); writeFileSync(cleanFile, cleanMarkdown, "utf8"); + writeFileSync(cleanMdxFile, '# Hello\n\n\n', "utf8"); run(lintMd, [cleanFile]); + run(lintMd, [cleanMdxFile]); const originalFixableContent = readFileSync(fixableFile, "utf8"); run(lintMd, [fixableFile, "--fix"]); const fixedContent = readFileSync(fixableFile, "utf8"); diff --git a/src/utils/lint-worker.ts b/src/utils/lint-worker.ts index 28907a7..6ab1205 100644 --- a/src/utils/lint-worker.ts +++ b/src/utils/lint-worker.ts @@ -1,15 +1,29 @@ import { readFile } from "fs/promises"; +import { extname } from "path"; import { fixMarkdown, lintMarkdown } from "@lint-md/core"; +import type { LintMdResult } from "@lint-md/core"; import type { LintWorkerOptions } from "../types"; import { toBatchLintItem } from "./to-batch-lint-item"; const lintWorker = async (options: LintWorkerOptions) => { const { filePath, rules, isFixMode } = options; + const isMdx = extname(filePath).toLowerCase() === ".mdx"; + + if (isMdx && isFixMode) { + throw new Error("--fix does not support .mdx files"); + } const content = await readFile(filePath, "utf8"); - const result = isFixMode - ? fixMarkdown(content, { rules }) - : lintMarkdown(content, { rules: rules ?? {} }); + let result: LintMdResult; + if (isMdx) { + const { lintMdx } = + require("@lint-md/core/mdx") as typeof import("@lint-md/core/mdx"); + result = lintMdx(content, { rules: rules ?? {} }); + } else { + result = isFixMode + ? fixMarkdown(content, { rules }) + : lintMarkdown(content, { rules: rules ?? {} }); + } // Core returns fixedResult even when no rule changed the text. Compare with // the content we already read so the main thread writes only real changes. diff --git a/src/utils/load-md-files.ts b/src/utils/load-md-files.ts index 990bb81..c2a245c 100644 --- a/src/utils/load-md-files.ts +++ b/src/utils/load-md-files.ts @@ -25,7 +25,13 @@ export const loadMdFiles = async ( for (const entry of entries) { if (entry.isSymbolicLink()) continue; const fullPath = entry.fullpath(); - if (extensions.some((ext) => fullPath.endsWith(ext))) files.add(fullPath); + if ( + extensions.some((ext) => + fullPath.toLowerCase().endsWith(ext.toLowerCase()) + ) + ) { + files.add(fullPath); + } } // Sort so lint report order stays stable across OS, filesystem, and