chore: import upstream snapshot with attribution
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 11:55:53 +08:00
commit dde272c4b8
19405 changed files with 2730632 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
/* eslint-disable filenames-simple/naming-convention */
import { createLintStagedConfig } from '@freecodecamp/eslint-config/lintstaged';
export default createLintStagedConfig(import.meta.dirname);
+9
View File
@@ -0,0 +1,9 @@
Script to seed the daily challenges. Used to seed challenges for local or production databases.
To run:
Copy the `sample.env` to `.env`,
Make sure dependencies are installed,
Run the main client with upcoming changes shown - this is so the script can get the challenges from GraphQL,
`cd tools/daily-challenges` to go into the `daily-challenges` folder,
Run `pnpm seed-daily-challenges` to seed the challenges from the "Dev Playground" superblock to a `DailyCodingChallenges` collection in the `freecodecamp` database.
+3
View File
@@ -0,0 +1,3 @@
import { configTypeChecked } from '@freecodecamp/eslint-config/base';
export default configTypeChecked;
+148
View File
@@ -0,0 +1,148 @@
import { MongoClient, ObjectId } from 'mongodb';
import { Challenge, QueryResult } from './types';
const GRAPHQL_ENDPOINT = 'http://localhost:8000/___graphql';
// Query graphQL - note that the main client needs to be running to query the challenges
export async function queryGraphQL(query: string) {
const response = await fetch(GRAPHQL_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ query })
});
const json = (await response.json()) as QueryResult;
if (!json?.data?.allChallengeNode?.edges?.length) {
throw new Error(
'Failed to find any challenges with GraphQL query. The client needs to be running'
);
}
return json;
}
export async function fetchChallenges(language: 'javascript' | 'python') {
const query = `
query {
allChallengeNode(
filter: {challenge: {superBlock: {eq: "dev-playground"}, block: {eq: "daily-coding-challenges-${language}"}}}
sort: {challenge: {challengeOrder: ASC}}
) {
edges {
node {
challenge {
id
title
description
tests {
testString
text
}
challengeFiles {
contents
fileKey
}
}
}
}
}
}
`;
const queryRes = await queryGraphQL(query);
const challenges = queryRes.data.allChallengeNode.edges.map(
({ node }) => node.challenge
);
return challenges;
}
export function combineChallenges({
jsChallenge,
pyChallenge,
challengeNumber,
date
}: {
jsChallenge: Challenge;
pyChallenge: Challenge;
challengeNumber: number;
date: Date;
}) {
const {
id: jsId,
title: jsTitle,
description: jsDescription,
tests: jsTests,
challengeFiles: jsChallengeFiles
} = jsChallenge;
const {
title: pyTitle,
description: pyDescription,
tests: pyTests,
challengeFiles: pyChallengeFiles
} = pyChallenge;
if (jsTitle !== pyTitle) {
throw new Error(
`JavaScript and Python titles do not match for challenge ${challengeNumber}: "${jsTitle}" vs "${pyTitle}"`
);
}
if (jsDescription !== pyDescription) {
throw new Error(
`JavaScript and Python descriptions do not match for challenge ${challengeNumber}`
);
}
if (jsTests.length !== pyTests.length) {
throw new Error(
`JavaScript and Python do not have the same number of tests for challenge ${challengeNumber}: ${jsTests.length} JavaScript vs ${pyTests.length} Python tests`
);
}
// Use the JS challenge info for the new challenge meta - e.g. id, title, description, etc
const challengeData = {
// **DO NOT CHANGE THE ID** it's used as the challenge ID - and what gets added to completedDailyCodingChallenges[]
_id: new ObjectId(`${jsId}`),
challengeNumber,
title: jsTitle.replace(`Challenge ${challengeNumber}: `, ''),
date,
description: removeSection(jsDescription),
javascript: {
tests: jsTests,
challengeFiles: jsChallengeFiles
},
python: {
tests: pyTests,
challengeFiles: pyChallengeFiles
}
};
return challengeData;
}
export async function handleError(err: unknown, client: MongoClient) {
if (err) {
console.error('Oh noes!! Error seeding Daily Challenges.');
console.error(err);
try {
await client.close();
} catch {
// no-op
} finally {
process.exit(1);
}
}
}
// Remove the <section id="description"> that our parser adds.
export function removeSection(str: string) {
return str
.replace(/^<section id="description">\n?/, '')
.replace(/\n?<\/section>$/, '');
}
+22
View File
@@ -0,0 +1,22 @@
{
"name": "daily-challenges",
"version": "1.0.0",
"main": "seed-daily-challenges.js",
"scripts": {
"seed-daily-challenges": "tsx seed-daily-challenges.ts",
"lint": "eslint --max-warnings 0",
"type-check": "tsc --noEmit"
},
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"@freecodecamp/eslint-config": "workspace:*",
"dotenv": "16.6.1",
"eslint": "^9.39.1",
"mongodb": "6.21.0",
"tsx": "4.21.0",
"typescript": "5.9.3"
},
"type": "module"
}
+1
View File
@@ -0,0 +1 @@
MONGOHQ_URL=mongodb://127.0.0.1:27017/freecodecamp?directConnection=true
@@ -0,0 +1,115 @@
/*
Script to seed daily challenges to freeCodeCamp database, DailyCodingChallenges collection.
It gets the daily challenge data from the dev-playground superblock using GraphQL.
The main client needs to be running with upcoming changes shown to get the info from GraphQL.
Run the curriculum tests on the dev-playground superblock before seeding to make sure they pass.
*/
import 'dotenv/config';
import { MongoClient } from 'mongodb';
import { combineChallenges, fetchChallenges, handleError } from './helpers';
const { MONGOHQ_URL } = process.env;
// Number challenges in the dev-playground blocks
// Update this if the number of challenges changes
const EXPECTED_CHALLENGE_COUNT = 365;
// Date to set for the first challenge, second challenge will be one day later, etc...
// **DO NOT CHANGE THIS AFTER RELEASE (if seeding production - okay for local dev)**
const year = 2025;
const monthIndex = 7; // 0-indexed -> 5 = June
const day = 11;
const START_DATE = new Date(Date.UTC(year, monthIndex, day));
const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
// Sanity check to make sure the start date hasn't unintentionally changed
// **IT SHOULD NOT CHANGE AFTER RELEASE**
const startDateString = '2025-08-11T00:00:00.000Z';
if (START_DATE.toISOString() !== startDateString) {
throw new Error(
`It appears the start date has changed from "${startDateString}".
Are you sure you want to change that? If you are seeding production,
you should not change the start date after the daily challenges have been release.
`
);
}
const client = new MongoClient(
MONGOHQ_URL || 'mongodb://127.0.0.1:27017/freecodecamp?directConnection=true'
);
const seed = async () => {
await client.db('admin').command({ ping: 1 });
console.log('Successfully connected to mongo');
const db = client.db('freecodecamp');
const dailyCodingChallenges = db.collection('DailyCodingChallenges');
console.log('Fetching challenges...');
const jsChallenges = await fetchChallenges('javascript');
const pyChallenges = await fetchChallenges('python');
if (jsChallenges.length !== pyChallenges.length) {
throw new Error(
`Number of challenges do not match: ${jsChallenges.length} JavaScript vs ${pyChallenges.length} Python challenges found`
);
}
if (jsChallenges.length !== EXPECTED_CHALLENGE_COUNT) {
throw new Error(
`Expected ${EXPECTED_CHALLENGE_COUNT} challenges, but found ${jsChallenges.length} challenges`
);
}
console.log(`${jsChallenges.length} challenges found for each language`);
console.log('Creating new challenges...');
const newChallenges = [];
for (let i = 0; i < jsChallenges.length; i++) {
const jsChallenge = jsChallenges[i];
const pyChallenge = pyChallenges[i];
const newChallenge = combineChallenges({
jsChallenge,
pyChallenge,
challengeNumber: i + 1,
date: new Date(START_DATE.getTime() + i * ONE_DAY_IN_MS)
});
newChallenges.push(newChallenge);
}
console.log('Finished creating new challenges');
console.log(`Writing ${newChallenges.length} challenges to database...`);
// Replace if the object exists, create new one if it doesn't
const bulkOps = newChallenges.map(challenge => ({
replaceOne: {
filter: { _id: challenge._id },
replacement: challenge,
upsert: true
}
}));
await dailyCodingChallenges.bulkWrite(bulkOps);
console.log(`Finished writing challenges to database`);
const count = await dailyCodingChallenges.countDocuments();
if (count !== EXPECTED_CHALLENGE_COUNT) {
console.warn(
'\n********** WARNING *********\n' +
'*\n' +
`* Expected ${EXPECTED_CHALLENGE_COUNT} challenges in the database,\n` +
`* but found ${count} documents\n` +
'*\n' +
'********** WARNING *********\n'
);
}
};
seed()
.then(() => client.close())
.catch(err => handleError(err, client));
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig-base.json"
}
+26
View File
@@ -0,0 +1,26 @@
export type QueryResult = {
data: {
allChallengeNode: {
edges: {
node: {
challenge: Challenge;
};
}[];
};
};
};
export type Challenge = {
id: string;
title: string;
date: Date;
description: string;
tests: {
testString: string;
text: string;
}[];
challengeFiles: {
contents: string;
filekey: string;
}[];
};