mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
refactor(tooling): allow markdownlint to handle multiple files (#66771)
This commit is contained in:
committed by
GitHub
parent
448d320d21
commit
d69f24b31b
@@ -31,16 +31,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@freecodecamp/eslint-config": "workspace:*",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/js-yaml": "4.0.5",
|
||||
"@types/yargs": "^17.0.35",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"eslint": "^9.39.1",
|
||||
"glob": "^8.1.0",
|
||||
"@types/yargs": "17.0.35",
|
||||
"@vitest/ui": "3.2.4",
|
||||
"eslint": "9.39.1",
|
||||
"markdownlint": "0.33.0",
|
||||
"prismjs": "1.29.0",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "^3.2.4",
|
||||
"yargs": "^17.7.2"
|
||||
"vitest": "3.2.4",
|
||||
"yargs": "17.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
|
||||
import { configure } from './index.js';
|
||||
import { configure, processLintErrors } from './index.js';
|
||||
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.options({ config: { type: 'string' } })
|
||||
.parseSync();
|
||||
|
||||
const configPath = argv.config;
|
||||
const files = argv._;
|
||||
const files = argv._ as string[];
|
||||
|
||||
if (!configPath) {
|
||||
console.error(
|
||||
@@ -25,6 +25,17 @@ if (files.length === 0) {
|
||||
|
||||
const { lint } = configure(configPath);
|
||||
|
||||
files.forEach(filePath => {
|
||||
lint({ path: filePath });
|
||||
});
|
||||
const runLint = async () => {
|
||||
const results = await lint(files);
|
||||
const errors = processLintErrors(results);
|
||||
|
||||
if (errors.length > 0) {
|
||||
errors.forEach(({ file, errors: fileErrors }) => {
|
||||
console.log('Errors in file', file);
|
||||
console.log(fileErrors);
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
void runLint();
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import YAML from 'js-yaml';
|
||||
import glob from 'glob';
|
||||
|
||||
import { linter } from './linter/index.js';
|
||||
|
||||
export const configure = (configPath: string) => {
|
||||
interface LintResults {
|
||||
[key: string]: unknown[];
|
||||
}
|
||||
|
||||
const configure = (configPath: string) => {
|
||||
const lintRules = readFileSync(configPath, 'utf8');
|
||||
const lint = linter(YAML.load(lintRules));
|
||||
const lintAll = (pattern: string) => {
|
||||
glob(pattern, (err, files) => {
|
||||
if (!files.length) throw Error('No files found');
|
||||
files.forEach(file => lint({ path: file }));
|
||||
});
|
||||
};
|
||||
|
||||
return { lint, lintAll };
|
||||
return { lint };
|
||||
};
|
||||
|
||||
const processLintErrors = (results: LintResults) => {
|
||||
return Object.entries(results)
|
||||
.map(([file, errors]) => ({ file, errors }))
|
||||
.filter(({ errors }) => errors.length > 0);
|
||||
};
|
||||
|
||||
export { configure, processLintErrors };
|
||||
|
||||
@@ -1,60 +1,56 @@
|
||||
import path from 'path';
|
||||
|
||||
import {
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { configure } from './index.js';
|
||||
import { configure, processLintErrors } from './index.js';
|
||||
|
||||
const badYMLError = {
|
||||
errorContext: '```yml',
|
||||
errorDetail: `bad indentation of a mapping entry at line 3, column 17:
|
||||
testString: testString
|
||||
^`,
|
||||
errorRange: null,
|
||||
fixInfo: null,
|
||||
lineNumber: 19,
|
||||
ruleDescription: 'YAML code blocks should be valid',
|
||||
ruleInformation: null,
|
||||
ruleNames: ['yaml-linter']
|
||||
};
|
||||
|
||||
describe('markdown linter', () => {
|
||||
const good = { path: path.join(__dirname, './fixtures/good.md') };
|
||||
const badYML = { path: path.join(__dirname, './fixtures/badYML.md') };
|
||||
const badFencing = { path: path.join(__dirname, './fixtures/badFencing.md') };
|
||||
const good = path.join(__dirname, './fixtures/good.md');
|
||||
const badYML = path.join(__dirname, './fixtures/badYML.md');
|
||||
const badFencing = path.join(__dirname, './fixtures/badFencing.md');
|
||||
const configPath = path.join(__dirname, './fixtures/rules.yaml');
|
||||
let lint;
|
||||
|
||||
beforeAll(() => {
|
||||
lint = configure(configPath).lint;
|
||||
({ lint } = configure(configPath));
|
||||
});
|
||||
beforeEach(() => {
|
||||
console.log = vi.fn();
|
||||
// the linter signals that a file failed by setting
|
||||
// exitCode to 1, so it needs (re)setting to 0
|
||||
process.exitCode = 0;
|
||||
});
|
||||
afterEach(() => {
|
||||
process.exitCode = 0;
|
||||
});
|
||||
|
||||
it('should pass `good` markdown', async () => {
|
||||
await new Promise(resolve => lint(good, resolve));
|
||||
expect(process.exitCode).toBe(0);
|
||||
const result = await lint([good]);
|
||||
expect(result[good]).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should fail invalid YML blocks', async () => {
|
||||
await new Promise(resolve => lint(badYML, resolve));
|
||||
expect(process.exitCode).toBe(1);
|
||||
const result = await lint([badYML]);
|
||||
expect(result[badYML]).not.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should fail when code fences are not surrounded by newlines', async () => {
|
||||
await new Promise(resolve => lint(badFencing, resolve));
|
||||
expect(process.exitCode).toBe(1);
|
||||
const result = await lint([badFencing]);
|
||||
expect(result[badFencing]).not.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should write to the console describing the problem', async () => {
|
||||
await new Promise(resolve => lint(badYML, resolve));
|
||||
const results = await lint([badYML]);
|
||||
const errors = processLintErrors(results);
|
||||
|
||||
const expected =
|
||||
'badYML.md: 19: yaml-linter YAML code blocks should be valid [bad indentation of a mapping entry at line 3, column 17:\n testString: testString\n ^] [Context: "```yml"]';
|
||||
expect(console.log.mock.calls.length).toBe(1);
|
||||
expect(console.log.mock.calls[0][0]).toEqual(
|
||||
expect.stringContaining(expected)
|
||||
);
|
||||
expect(errors[0].file).toContain('badYML.md');
|
||||
expect(errors[0].errors).toContainEqual(badYMLError);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,25 +5,14 @@ import * as lintYAML from './markdown-yaml.js';
|
||||
import * as fencedCodeBlock from './fenced-code-block.js';
|
||||
|
||||
export function linter(rules) {
|
||||
const lint = (file, next) => {
|
||||
const lint = async files => {
|
||||
const options = {
|
||||
files: [file.path],
|
||||
files,
|
||||
config: rules,
|
||||
customRules: [lintYAML, lintPrism, fencedCodeBlock]
|
||||
};
|
||||
|
||||
markdownlint(options, function callback(err, result) {
|
||||
const resultString = (result || '').toString();
|
||||
if (resultString) {
|
||||
process.exitCode = 1;
|
||||
console.log(resultString);
|
||||
}
|
||||
if (err) {
|
||||
process.exitCode = 1;
|
||||
console.error(err);
|
||||
}
|
||||
if (next) next(err, file);
|
||||
});
|
||||
return await markdownlint.promises.markdownlint(options);
|
||||
};
|
||||
return lint;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user