From 939ab985c4d1fcce47e8193b588724a40b55efb0 Mon Sep 17 00:00:00 2001 From: Roger Qiu Date: Sun, 22 Feb 2026 05:27:08 +0000 Subject: [PATCH 1/2] feat(cli): add markdown target flag for scoped markdown linting add markdown target option and selection parity with eslint and shell pass markdown patterns through lint context to markdown detection add tests and docs for markdown target workflows --- README.md | 15 +++++++++++++++ package.json | 4 ++-- src/bin/lint.ts | 4 ++++ src/domains/engine.ts | 1 + src/domains/index.ts | 12 +++++++++++- src/domains/markdown.ts | 7 +++++-- src/types.ts | 1 + tests/bin/lint.test.ts | 25 +++++++++++++++++++++++++ tests/domains/index.test.ts | 28 ++++++++++++++++++++++++++++ 9 files changed, 92 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 10a4bb4..39bc4e6 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ matrixai-lint --fix | `--user-config` | Uses detected `eslint.config.[js,mjs,cjs,ts]` from the project root if found | | `--eslint-config ` | Explicitly use a custom ESLint config file | | `--eslint ` | ESLint targets (files, roots, or globs); implies ESLint domain selection | +| `--markdown ` | Markdown targets (files, roots, or globs); implies markdown domain selection | | `--shell ` | Shell targets (files, roots, or globs); implies shell domain selection | | `--domain ` | Run only selected domains (`eslint`, `shell`, `markdown`) | | `--skip-domain ` | Skip selected domains (`eslint`, `shell`, `markdown`) | @@ -58,6 +59,9 @@ Domain selection behavior: - `--eslint ...` runs ESLint only. - `--shell ...` runs shell only. - Passing both runs both. +- Passing `--markdown` implies markdown domain selection. + - `--markdown ...` runs markdown only. + - Combined with other target flags, only those targeted domains run. - `shellcheck` is optional only for default auto-run shell execution. - If shell is explicitly requested (`--shell ...` or `--domain shell`), missing `shellcheck` is a failure. @@ -65,6 +69,10 @@ Domain selection behavior: - Directories are used as roots. - File paths and glob patterns are reduced to search roots, then `*.sh` files are discovered under those roots. +- `--markdown` accepts target paths and glob patterns. + - Directories are used as roots. + - File paths and glob patterns are reduced to search roots, then `*.md` / + `*.mdx` files are discovered under those roots. #### Targeted workflows @@ -86,6 +94,12 @@ Domain selection behavior: matrixai-lint --domain markdown ``` +- Markdown only under selected roots: + + ```sh + matrixai-lint --markdown standards templates README.md + ``` + - Mixed scoped run (ESLint + shell only): ```sh @@ -99,6 +113,7 @@ matrixai-lint --fix matrixai-lint --user-config matrixai-lint --eslint-config ./eslint.config.js --fix matrixai-lint --eslint "src/**/*.{ts,tsx}" --shell scripts +matrixai-lint --markdown standards templates README.md matrixai-lint --domain eslint markdown matrixai-lint --skip-domain markdown matrixai-lint --list-domains diff --git a/package.json b/package.json index 492d85d..1fbf23d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "https://github.com/MatrixAI/js-eslint.git" + "url": "git+https://github.com/MatrixAI/js-eslint.git" }, "type": "module", "exports": { @@ -29,7 +29,7 @@ "#*": "./dist/*" }, "bin": { - "matrixai-lint": "./dist/bin/lint.js" + "matrixai-lint": "dist/bin/lint.js" }, "scripts": { "prepare": "tsc -p ./tsconfig.build.json", diff --git a/src/bin/lint.ts b/src/bin/lint.ts index 8f9b9a4..43c81e1 100644 --- a/src/bin/lint.ts +++ b/src/bin/lint.ts @@ -44,6 +44,7 @@ program ) .option('--eslint-config ', 'Path to explicit ESLint config file') .option('--eslint ', 'ESLint targets (files, roots, or globs)') + .option('--markdown ', 'Markdown targets (files, roots, or globs)') .option( '--shell ', 'Shell targets (files, roots, or globs) used to derive shellcheck search roots', @@ -139,6 +140,7 @@ async function main(argv = process.argv) { const explain = Boolean(options.explain); const eslintPatterns: string[] | undefined = options.eslint; + const markdownPatterns: string[] | undefined = options.markdown; const shellPatterns: string[] | undefined = options.shell; const { selectedDomains, explicitlyRequestedDomains, selectionSources } = resolveDomainSelection(options); @@ -197,6 +199,7 @@ async function main(argv = process.argv) { chosenConfig, isConfigValid, eslintPatterns, + markdownPatterns, shellPatterns, }, }); @@ -214,6 +217,7 @@ async function main(argv = process.argv) { chosenConfig, isConfigValid, eslintPatterns, + markdownPatterns, shellPatterns, }, }); diff --git a/src/domains/engine.ts b/src/domains/engine.ts index 76ab4a3..fd6d5de 100644 --- a/src/domains/engine.ts +++ b/src/domains/engine.ts @@ -8,6 +8,7 @@ type LintDomainEngineContext = { isConfigValid: boolean; eslintPatterns?: string[]; shellPatterns?: string[]; + markdownPatterns?: string[]; }; type LintDomainAvailabilityKind = 'required' | 'optional'; diff --git a/src/domains/index.ts b/src/domains/index.ts index 3c85369..90b806a 100644 --- a/src/domains/index.ts +++ b/src/domains/index.ts @@ -29,6 +29,7 @@ function resolveDomainSelection(options: CLIOptions): { const hasDomainSelectors = domainFlags.length > 0 || skipDomains.size > 0; const hasExplicitESLintTargets = (options.eslint?.length ?? 0) > 0; const hasExplicitShellTargets = (options.shell?.length ?? 0) > 0; + const hasExplicitMarkdownTargets = (options.markdown?.length ?? 0) > 0; const explicitlyRequestedDomains = new Set(domainFlags); const selectionSources = new Map(); @@ -38,6 +39,9 @@ function resolveDomainSelection(options: CLIOptions): { if (hasExplicitShellTargets) { explicitlyRequestedDomains.add('shell'); } + if (hasExplicitMarkdownTargets) { + explicitlyRequestedDomains.add('markdown'); + } let selectedDomains: Set; @@ -48,7 +52,9 @@ function resolveDomainSelection(options: CLIOptions): { } } else if ( !hasDomainSelectors && - (hasExplicitESLintTargets || hasExplicitShellTargets) + (hasExplicitESLintTargets || + hasExplicitShellTargets || + hasExplicitMarkdownTargets) ) { selectedDomains = new Set(); if (hasExplicitESLintTargets) { @@ -59,6 +65,10 @@ function resolveDomainSelection(options: CLIOptions): { selectedDomains.add('shell'); selectionSources.set('shell', 'target-flag'); } + if (hasExplicitMarkdownTargets) { + selectedDomains.add('markdown'); + selectionSources.set('markdown', 'target-flag'); + } } else { selectedDomains = new Set(LINT_DOMAINS); for (const domain of LINT_DOMAINS) { diff --git a/src/domains/markdown.ts b/src/domains/markdown.ts index 52f5cd0..731f517 100644 --- a/src/domains/markdown.ts +++ b/src/domains/markdown.ts @@ -46,8 +46,11 @@ function createMarkdownDomainPlugin({ return { domain: 'markdown', description: 'Format and check Markdown/MDX files with Prettier.', - detect: () => { - const searchPatterns = DEFAULT_MARKDOWN_SEARCH_ROOTS; + detect: ({ markdownPatterns }) => { + const searchPatterns = + markdownPatterns != null && markdownPatterns.length > 0 + ? markdownPatterns + : DEFAULT_MARKDOWN_SEARCH_ROOTS; const searchRoots = resolveSearchRootsFromPatterns(searchPatterns); const matchedFiles = collectMarkdownFilesFromScope(searchRoots); diff --git a/src/types.ts b/src/types.ts index 07a7207..619f3d1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -35,6 +35,7 @@ type CLIOptions = { eslintConfig?: string; eslint?: string[]; shell?: string[]; + markdown?: string[]; domain?: LintDomain[]; skipDomain?: LintDomain[]; listDomains?: boolean; diff --git a/tests/bin/lint.test.ts b/tests/bin/lint.test.ts index e5b4526..551a616 100644 --- a/tests/bin/lint.test.ts +++ b/tests/bin/lint.test.ts @@ -96,6 +96,31 @@ describe('matrixai-lint CLI domain semantics', () => { expect(prettierCalls).toHaveLength(0); }); + test('--markdown no longer triggers eslint/shell domains', async () => { + await fs.promises.mkdir(path.join(dataDir, 'standards'), { + recursive: true, + }); + await fs.promises.writeFile( + path.join(dataDir, 'standards', 'guide.md'), + '# guide\n', + 'utf8', + ); + + await expect( + main(['node', 'matrixai-lint', '--markdown', 'standards']), + ).resolves.toBeUndefined(); + + const shellCalls = capturedExecCalls.filter((c) => c.file === 'shellcheck'); + expect(shellCalls).toHaveLength(0); + + const prettierCalls = capturedExecCalls.filter( + (c) => + c.file === 'prettier' || + c.args.some((arg) => /prettier\.cjs$/.test(arg)), + ); + expect(prettierCalls.length).toBeGreaterThan(0); + }); + test('explicit shell request + missing shellcheck fails', async () => { jest .spyOn(childProcess, 'spawnSync') diff --git a/tests/domains/index.test.ts b/tests/domains/index.test.ts index a0e5a21..7582163 100644 --- a/tests/domains/index.test.ts +++ b/tests/domains/index.test.ts @@ -457,6 +457,34 @@ describe('domain selection', () => { expect(selectionSources.get('eslint')).toBe('target-flag'); }); + test('markdown target flag implies explicit markdown domain request', () => { + const { selectedDomains, explicitlyRequestedDomains, selectionSources } = + resolveDomainSelection({ + fix: false, + userConfig: false, + markdown: ['standards', 'templates', 'README.md'], + }); + + expect([...selectedDomains]).toStrictEqual(['markdown']); + expect([...explicitlyRequestedDomains]).toStrictEqual(['markdown']); + expect(selectionSources.get('markdown')).toBe('target-flag'); + }); + + test('domain flag remains authoritative over markdown target flag', () => { + const { selectedDomains, explicitlyRequestedDomains, selectionSources } = + resolveDomainSelection({ + fix: false, + userConfig: false, + domain: ['eslint'], + markdown: ['standards'], + }); + + expect([...selectedDomains]).toStrictEqual(['eslint']); + expect([...explicitlyRequestedDomains]).toStrictEqual(['eslint']); + expect(selectionSources.get('eslint')).toBe('domain-flag'); + expect(selectionSources.has('markdown')).toBe(false); + }); + test('--domain keeps explicit domains and --skip-domain removes them', () => { const { selectedDomains, explicitlyRequestedDomains, selectionSources } = resolveDomainSelection({ From fddd9749f28e379a962c0b13cf594ed0f006c12d Mon Sep 17 00:00:00 2001 From: Roger Qiu Date: Sun, 22 Feb 2026 05:27:39 +0000 Subject: [PATCH 2/2] 0.4.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4cf2ac2..cdd741e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@matrixai/lint", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@matrixai/lint", - "version": "0.3.0", + "version": "0.4.0", "license": "Apache-2.0", "dependencies": { "@eslint/compat": "^1.2.5", diff --git a/package.json b/package.json index 1fbf23d..9a0c8d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@matrixai/lint", - "version": "0.3.0", + "version": "0.4.0", "author": "Roger Qiu", "description": "Org wide custom eslint rules", "license": "Apache-2.0",