Files
freeCodeCamp/tools/challenge-auditor/index.ts
2025-10-23 10:54:57 +05:30

119 lines
3.7 KiB
TypeScript

import { readdir } from 'fs/promises';
import { join, resolve } from 'path';
import { flatten } from 'lodash/fp';
import { config } from 'dotenv';
const envPath = resolve(__dirname, '../../.env');
config({ path: envPath });
import { availableLangs } from '../../shared/config/i18n';
import { getChallengesForLang } from '../../curriculum/src/get-challenges';
import {
SuperBlocks,
getAuditedSuperBlocks
} from '../../shared/config/curriculum';
// TODO: re-organise the types to a common 'types' folder that can be shared
// between the workspaces so we don't have to declare ChallengeNode here and in
// the client.
// This cannot be imported from the client, without causing tsc to attempt to
// compile the client (something it cannot do)
type ChallengeNode = {
block: string;
dashedName: string;
superBlock: SuperBlocks;
id: string;
challengeType: number;
};
// Adding types for getChallengesForLang is possible, but not worth the effort
// at this time.
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
const getChallenges = async (lang: string) => {
const curriculum = await getChallengesForLang(lang);
return (
Object.keys(curriculum)
// @ts-expect-error - curriculum comes from a JS file.
.map(key => curriculum[key].blocks)
.reduce((challengeArray, superBlock) => {
const challengesForBlock = Object.keys(superBlock).map(
key => superBlock[key].challenges
);
return [...challengeArray, ...flatten(challengesForBlock)];
}, []) as unknown as ChallengeNode[]
);
};
/* eslint-enable @typescript-eslint/no-unsafe-return */
/* eslint-enable @typescript-eslint/no-unsafe-argument */
/* eslint-enable @typescript-eslint/no-unsafe-assignment */
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
void (async () => {
let actionShouldFail = false;
const englishCurriculumDirectory = join(
process.cwd(),
'curriculum',
'challenges',
'english'
);
const englishFilePaths: string[] = [];
const englishBlocks = await readdir(englishCurriculumDirectory);
for (const englishBlock of englishBlocks) {
if (englishBlock.endsWith('.txt')) {
continue;
}
const englishChallenges = await readdir(
join(englishCurriculumDirectory, englishBlock)
);
for (const englishChallenge of englishChallenges) {
englishFilePaths.push(join(englishBlock, englishChallenge));
}
}
const langsToCheck = availableLangs.curriculum.filter(
lang => String(lang) !== 'english'
);
for (const language of langsToCheck) {
console.log(`\n=== ${language} ===`);
const certs = getAuditedSuperBlocks({ language });
const noDuplicateSlugs = await auditSlugs(language, certs);
if (noDuplicateSlugs) {
console.log(`All challenges pass.`);
} else {
actionShouldFail = true;
}
}
process.exit(actionShouldFail ? 1 : 0);
})();
async function auditSlugs(lang: string, certs: SuperBlocks[]) {
let auditPassed = true;
const slugs = new Map<string, string>();
const challenges = await getChallenges(lang);
for (const challenge of challenges) {
const { block, dashedName, superBlock } = challenge;
const slug = `/learn/${superBlock}/${block}/${dashedName}`;
// Skipping certifications
const isCertification = challenge.challengeType === 7;
if (certs.includes(superBlock) && !isCertification && slugs.has(slug)) {
console.log(
`${slug} appears more than once: ${slugs.get(slug) ?? ''} and ${
challenge.id
}`
);
auditPassed = false;
}
slugs.set(slug, challenge.id);
}
return auditPassed;
}