refactor: lint challenges as part of curriculum, not root (#65665)

Co-authored-by: Mrugesh Mohapatra <1884376+raisedadead@users.noreply.github.com>
This commit is contained in:
Oliver Eyton-Williams
2026-02-04 11:42:21 +01:00
committed by GitHub
parent 5fa67063f5
commit 20e48dd846
30 changed files with 287 additions and 171 deletions
-4
View File
@@ -1,4 +0,0 @@
/* eslint-disable filenames-simple/naming-convention */
import { createLintStagedConfig } from '@freecodecamp/eslint-config/lintstaged';
export default createLintStagedConfig(import.meta.dirname);
-13
View File
@@ -1,13 +0,0 @@
import { configTypeChecked } from '@freecodecamp/eslint-config/base';
import globals from 'globals';
export default [
...configTypeChecked,
{
languageOptions: {
globals: {
...globals.node // TODO: migrate to ESM and remove globals
}
}
}
];
-40
View File
@@ -1,40 +0,0 @@
---
id: ''
title: ''
challengeType: 0
videoUrl: ''
---
## Description
<section id='description'>
</section>
## Instructions
<section id='instructions'>
</section>
## Tests
<section id='tests'>
text
```yml
tests:
- text: text
testString: testString
```
moretext
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='html-seed'>
</div>
</section>
## Solution
<section id='solution'>
</section>
-40
View File
@@ -1,40 +0,0 @@
---
id: ''
title: ''
challengeType: 0
videoUrl: ''
---
## Description
<section id='description'>
</section>
## Instructions
<section id='instructions'>
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: text
testString: testString
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='html-seed'>
</div>
</section>
## Solution
<section id='solution'>
</section>
-40
View File
@@ -1,40 +0,0 @@
---
id: ''
title: ''
challengeType: 0
videoUrl: ''
---
## Description
<section id='description'>
</section>
## Instructions
<section id='instructions'>
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: text
testString: testString
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='html-seed'>
</div>
</section>
## Solution
<section id='solution'>
</section>
-20
View File
@@ -1,20 +0,0 @@
const fs = require('fs');
const path = require('path');
const YAML = require('js-yaml');
const argv = require('yargs').argv;
const linter = require('./linter');
const CONFIG_PATH = path.resolve(
__dirname,
'../../../curriculum/challenges/.markdownlint.yaml'
);
const isMDRE = /.*\.md$/;
const lintRules = fs.readFileSync(CONFIG_PATH, 'utf8');
const lint = linter(YAML.load(lintRules));
const files = argv._.filter(arg => isMDRE.test(arg));
files.forEach(path => lint({ path: path }));
module.exports = lint;
-44
View File
@@ -1,44 +0,0 @@
import path from 'path';
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
import lint from '.';
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') };
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);
});
it('should fail invalid YML blocks', async () => {
await new Promise(resolve => lint(badYML, resolve));
expect(process.exitCode).toBe(1);
});
it('should fail when code fences are not surrounded by newlines', async () => {
await new Promise(resolve => lint(badFencing, resolve));
expect(process.exitCode).toBe(1);
});
it('should write to the console describing the problem', async () => {
await new Promise(resolve => lint(badYML, resolve));
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)
);
});
});
@@ -1,17 +0,0 @@
module.exports = {
names: ['closed-code-blocks'],
description: 'Code blocks must have closing triple backticks',
tags: ['code'],
function: function rule(params, onError) {
params.parsers.micromark.tokens
.filter(token => token.type === 'codeFenced')
.forEach(token => {
if (token.text.trim().slice(-3) !== '```') {
onError({
lineNumber: token.endLine,
detail: `Code blocks must have closing triple backticks.`
});
}
});
}
};
-26
View File
@@ -1,26 +0,0 @@
const markdownlint = require('markdownlint');
const lintPrism = require('./markdown-prism');
const lintYAML = require('./markdown-yaml');
const fencedCodeBlock = require('./fenced-code-block');
function linter(rules) {
const lint = (file, next) => {
const options = {
files: [file.path],
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 (next) next(err, file);
});
};
return lint;
}
module.exports = linter;
@@ -1,44 +0,0 @@
const components = require(`prismjs/components`);
module.exports = {
names: ['prism-langs'],
description: 'Code block languages should be supported by PrismJS',
tags: ['prism'],
function: function rule(params, onError) {
params.tokens
.filter(param => param.type === 'fence')
.forEach(codeBlock => {
// whitespace around the language is ignored by the parser, as is case:
const baseLang = codeBlock.info.trim().toLowerCase();
const lang = getBaseLanguageName(baseLang);
// Rule MD040 checks if the block has a language, so this rule only
// comes into play if a language has been specified.
if (baseLang && !lang) {
onError({
lineNumber: codeBlock.lineNumber,
detail: `'${baseLang}' is not recognised.`
});
}
});
}
};
/*
* This is the method used by https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-remark-prismjs/src/load-prism-language.js
*/
// Get the real name of a language given it or an alias
const getBaseLanguageName = nameOrAlias => {
if (components.languages[nameOrAlias]) {
return nameOrAlias;
}
return Object.keys(components.languages).find(language => {
const { alias } = components.languages[language];
if (!alias) return false;
if (Array.isArray(alias)) {
return alias.includes(nameOrAlias);
} else {
return alias === nameOrAlias;
}
});
};
@@ -1,24 +0,0 @@
const yaml = require('js-yaml');
module.exports = {
names: ['yaml-linter'],
description: 'YAML code blocks should be valid',
tags: ['yaml'],
function: function rule(params, onError) {
params.tokens
.filter(param => param.type === 'fence')
.filter(param => param.info === 'yml' || param.info === 'yaml')
// TODO since the parser only looks for yml, should we reject yaml blocks?
.forEach(codeBlock => {
try {
yaml.safeLoad(codeBlock.content);
} catch (e) {
onError({
lineNumber: codeBlock.lineNumber,
detail: e.message,
context: codeBlock.line
});
}
});
}
};
-29
View File
@@ -1,29 +0,0 @@
{
"name": "@freecodecamp/scripts-lint",
"version": "0.0.1",
"description": "The freeCodeCamp.org open-source codebase and curriculum",
"license": "BSD-3-Clause",
"private": true,
"main": "none",
"repository": {
"type": "git",
"url": "git+https://github.com/freeCodeCamp/freeCodeCamp.git"
},
"bugs": {
"url": "https://github.com/freeCodeCamp/freeCodeCamp/issues"
},
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
"author": "freeCodeCamp <team@freecodecamp.org>",
"scripts": {
"lint": "eslint --max-warnings 0",
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui"
},
"devDependencies": {
"@freecodecamp/eslint-config": "workspace:*",
"@vitest/ui": "^3.2.4",
"eslint": "^9.39.1",
"vitest": "^3.2.4"
}
}
-3
View File
@@ -1,3 +0,0 @@
{
"extends": "../../../tsconfig-base.json"
}