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
+88
View File
@@ -0,0 +1,88 @@
import { ChallengeFile } from '../../src/redux/prop-types';
export const challengeFiles: ChallengeFile[] = [
{
contents: 'some ts',
error: null,
ext: 'ts',
history: ['index.ts'],
fileKey: 'indexts',
name: 'index',
seed: 'some ts',
editableRegionBoundaries: [],
usesMultifileEditor: true,
path: 'index.ts'
},
{
contents: 'some css',
error: null,
ext: 'css',
history: ['styles.css'],
fileKey: 'stylescss',
name: 'styles',
seed: 'some css',
editableRegionBoundaries: [],
usesMultifileEditor: true,
path: 'styles.css'
},
{
contents: 'some html',
error: null,
ext: 'html',
history: ['index.html'],
fileKey: 'indexhtml',
name: 'index',
seed: 'some html',
editableRegionBoundaries: [],
usesMultifileEditor: true,
path: 'index.html'
},
{
contents: 'some js',
error: null,
ext: 'js',
history: ['script.js'],
fileKey: 'scriptjs',
name: 'script',
seed: 'some js',
editableRegionBoundaries: [],
usesMultifileEditor: true,
path: 'script.js'
},
{
contents: 'some jsx',
error: null,
ext: 'jsx',
history: ['index.jsx'],
fileKey: 'indexjsx',
name: 'index',
seed: 'some jsx',
editableRegionBoundaries: [],
usesMultifileEditor: true,
path: 'index.jsx'
},
{
contents: 'some tsx',
error: null,
ext: 'tsx',
history: ['index.tsx'],
fileKey: 'indextsx',
name: 'index',
seed: 'some tsx',
editableRegionBoundaries: [],
usesMultifileEditor: true,
path: 'index.tsx'
},
{
contents: '{\n "compilerOptions": {}\n}',
error: null,
ext: 'json',
history: ['tsconfig.json'],
fileKey: 'tsconfigjson',
name: 'tsconfig',
seed: '{\n "compilerOptions": {}\n}',
editableRegionBoundaries: [],
usesMultifileEditor: true,
path: 'tsconfig.json'
}
];
+91
View File
@@ -0,0 +1,91 @@
const path = require('path');
const _ = require('lodash');
const {
getBlockCreator,
getSuperblocks,
superBlockToFilename
} = require('@freecodecamp/curriculum/build-curriculum');
const {
getContentDir,
getBlockStructure,
getSuperblockStructure,
CURRICULUM_DIR
} = require('@freecodecamp/curriculum/file-handler');
const {
transformSuperBlock
} = require('@freecodecamp/curriculum/build-superblock');
const { getSuperOrder } = require('@freecodecamp/curriculum/super-order');
const { readFile } = require('fs/promises');
const curriculumLocale = process.env.CURRICULUM_LOCALE || 'english';
exports.localeChallengesRootDir = getContentDir(curriculumLocale);
const blockCreator = getBlockCreator(curriculumLocale);
function getBlockMetadata(block, superBlock) {
// Compute metadata for the given block in the specified superblock
const sbFilename = superBlockToFilename[superBlock];
const sbData = getSuperblockStructure(sbFilename);
const blocks = transformSuperBlock(sbData, {
showComingSoon: process.env.SHOW_UPCOMING_CHANGES === 'true'
});
const order = blocks.findIndex(b => b.dashedName === block);
const superOrder = getSuperOrder(superBlock);
if (order === -1) {
throw new Error(`Block ${block} not found in superblock ${superBlock}`);
}
return { order, superOrder };
}
exports.replaceChallengeNodes = () => {
return async function replaceChallengeNodes(filePath) {
const parentDir = path.dirname(filePath);
const block = path.basename(parentDir);
const filename = path.basename(filePath);
const meta = getBlockStructure(block);
const superblocks = getSuperblocks(block);
// Create a challenge for each superblock containing this block
const challenges = await Promise.all(
superblocks.map(async superBlock => {
const { order, superOrder } = getBlockMetadata(block, superBlock);
return blockCreator.createChallenge({
filename,
block,
meta: { ...meta, superBlock, order, superOrder },
isAudited: true
});
})
);
return challenges;
};
};
exports.buildChallenges = async function buildChallenges() {
const curriculum = JSON.parse(
await readFile(
path.resolve(CURRICULUM_DIR, 'generated', 'curriculum.json'),
'utf-8'
)
);
const superBlocks = Object.keys(curriculum);
const blocks = superBlocks
.map(superBlock => curriculum[superBlock].blocks)
.reduce((blocks, superBlock) => {
const currentBlocks = Object.keys(superBlock).map(key => superBlock[key]);
return blocks.concat(_.flatten(currentBlocks));
}, []);
const builtChallenges = blocks
.map(({ challenges }) => challenges)
.reduce((accu, current) => accu.concat(current), []);
return builtChallenges;
};
+12
View File
@@ -0,0 +1,12 @@
declare global {
interface Window {
// This is a feature Gatsby adds to the `window` object.
// https://github.com/gatsbyjs/gatsby/blob/deb41cdfefbefe0c170b5dd7c10a19ba2b338f6e/packages/gatsby/cache-dir/production-app.js#L28
___loader: {
enqueue: () => void;
hovering: (path: string | null) => void;
};
}
}
export {};
@@ -0,0 +1,176 @@
const path = require('path');
const { viewTypes } = require('@freecodecamp/shared/config/challenge-types');
const backend = path.resolve(
__dirname,
'../../src/templates/Challenges/projects/backend/show.tsx'
);
const classic = path.resolve(
__dirname,
'../../src/templates/Challenges/classic/show.tsx'
);
const frontend = path.resolve(
__dirname,
'../../src/templates/Challenges/projects/frontend/show.tsx'
);
const codeAlly = path.resolve(
__dirname,
'../../src/templates/Challenges/codeally/show.tsx'
);
const superBlockIntro = path.resolve(
__dirname,
'../../src/templates/Introduction/super-block-intro.tsx'
);
const quiz = path.resolve(
__dirname,
'../../src/templates/Challenges/quiz/show.tsx'
);
const exam = path.resolve(
__dirname,
'../../src/templates/Challenges/exam/show.tsx'
);
const msTrophy = path.resolve(
__dirname,
'../../src/templates/Challenges/ms-trophy/show.tsx'
);
const fillInTheBlank = path.resolve(
__dirname,
'../../src/templates/Challenges/fill-in-the-blank/show.tsx'
);
const generic = path.resolve(
__dirname,
'../../src/templates/Challenges/generic/show.tsx'
);
const examDownload = path.resolve(
__dirname,
'../../src/templates/Challenges/exam-download/show.tsx'
);
const views = {
backend,
classic,
modern: classic,
frontend,
quiz,
codeAlly,
exam,
msTrophy,
fillInTheBlank,
generic,
examDownload
};
function getIsFirstStepInBlock(id, nodes) {
const current = nodes[id];
const previous = nodes[id - 1];
if (!previous) return true;
return previous.challenge.block !== current.challenge.block;
}
function getTemplateComponent(challengeType) {
return views[viewTypes[challengeType]];
}
exports.getTemplateComponent = getTemplateComponent;
exports.createChallengePages = function (
createPage,
{ idToNextPathCurrentCurriculum, idToPrevPathCurrentCurriculum }
) {
return function (node, index, allChallengeNodes) {
const {
dashedName,
disableLoopProtectTests,
disableLoopProtectPreview,
certification,
superBlock,
chapter,
module,
block,
fields: { slug, blockHashSlug },
required = [],
template,
challengeType,
id,
isLastChallengeInBlock,
saveSubmissionToDB
} = node.challenge;
createPage({
path: slug,
component: getTemplateComponent(challengeType),
context: {
challengeMeta: {
blockHashSlug,
dashedName,
certification,
disableLoopProtectTests,
disableLoopProtectPreview,
superBlock,
chapter,
module,
block,
isFirstStep: getIsFirstStepInBlock(index, allChallengeNodes),
template,
required,
isLastChallengeInBlock: isLastChallengeInBlock,
nextChallengePath: idToNextPathCurrentCurriculum[node.id],
prevChallengePath: idToPrevPathCurrentCurriculum[node.id],
id,
saveSubmissionToDB
},
projectPreview: getProjectPreviewConfig(
node.challenge,
allChallengeNodes
),
id: node.id
}
});
};
};
// TODO: figure out a cleaner way to get the last challenge in a block.
function getProjectPreviewConfig(challenge, allChallengeNodes) {
const { block } = challenge;
const challengesInBlock = allChallengeNodes
.filter(({ challenge }) => challenge.block === block)
.map(({ challenge }) => challenge);
const lastChallenge = challengesInBlock[challengesInBlock.length - 1];
const solutionFiles = lastChallenge.solutions[0] ?? [];
const lastChallengeFiles = lastChallenge.challengeFiles ?? [];
const findFileByKey = (key, files) =>
files.find(file => file.fileKey === key);
const projectPreviewChallengeFiles = lastChallengeFiles.map(file => ({
...file,
contents:
findFileByKey(file.fileKey, solutionFiles)?.contents ?? file.contents
}));
return {
challengeData: {
challengeType: lastChallenge.challengeType,
challengeFiles: projectPreviewChallengeFiles
}
};
}
exports.createSuperBlockIntroPages = function (createPage) {
return function ({ superBlock }) {
createPage({
path: `/learn/${superBlock}/`,
component: superBlockIntro,
context: {
superBlock
}
});
};
};
+1
View File
@@ -0,0 +1 @@
module.exports = require('./challenge-page-creator');
@@ -0,0 +1,142 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import React from 'react';
import { Provider } from 'react-redux';
import ShallowRenderer from 'react-test-renderer/shallow';
import { describe, test, expect, vi } from 'vitest';
import FourOhFourPage from '../../src/pages/404';
import Certification from '../../src/pages/certification';
import Learn from '../../src/pages/learn';
import { createStore } from '../../src/redux/create-store';
import layoutSelector from './layout-selector';
vi.mock('../../src/analytics');
vi.mock('../../src/utils/get-words');
const store = createStore();
// TODO: rather than testing which props passed from layoutSelector to the
// component it renders, test that the rendered component has the expected
// features (i.e. has a footer or not, etc.). That should be possible in
// react-testing-library.
interface NameAndProps {
props: Record<string, unknown>;
name: string;
}
function getComponentNameAndProps(
elementType: React.JSXElementConstructor<never>,
pathname: string,
pageContext?: { challengeMeta?: { block?: string; superBlock?: string } }
): NameAndProps {
const utils = ShallowRenderer.createRenderer();
const LayoutReactComponent = layoutSelector({
element: { type: elementType, props: {}, key: '' },
props: {
data: {},
location: {
pathname
},
pageContext,
params: { '*': '' },
path: ''
}
});
utils.render(<Provider store={store}>{LayoutReactComponent}</Provider>);
const view = utils.getRenderOutput();
return {
props: view.props.children.props as Record<string, unknown>,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
name: view.props.children.type.WrappedComponent.displayName
// TODO: Revisit this when react-test-renderer is replaced with
// react-testing-library
};
}
const challengePageContext = {
challengeMeta: {
block: 'Basic HTML and HTML5',
superBlock: 'responsive-web-design'
}
};
describe('Layout selector', () => {
test('Challenges should have DefaultLayout and no footer', () => {
const challengePath =
'/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements';
const componentObj = getComponentNameAndProps(
Learn,
challengePath,
challengePageContext
);
expect(componentObj.name).toEqual('DefaultLayout');
expect(componentObj.props.showFooter).toEqual(false);
});
test('SuperBlock path should have DefaultLayout and footer', () => {
const superBlockPath = '/learn/responsive-web-design/';
const componentObj = getComponentNameAndProps(Learn, superBlockPath);
expect(componentObj.name).toEqual('DefaultLayout');
expect(componentObj.props.showFooter).toEqual(true);
});
test('i18n challenge path should have DefaultLayout and no footer', () => {
const challengePath =
'espanol/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements/';
const componentObj = getComponentNameAndProps(
Learn,
challengePath,
challengePageContext
);
expect(componentObj.name).toEqual('DefaultLayout');
expect(componentObj.props.showFooter).toEqual(false);
});
test('i18n superBlock path should have DefaultLayout and footer', () => {
const superBlockPath = '/learn/responsive-web-design/';
const componentObj = getComponentNameAndProps(Learn, superBlockPath);
expect(componentObj.name).toEqual('DefaultLayout');
expect(componentObj.props.showFooter).toEqual(true);
});
test('404 page should have DefaultLayout and footer', () => {
const challengePath =
'/espanol/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements/';
const componentObj = getComponentNameAndProps(
FourOhFourPage,
challengePath
);
expect(componentObj.name).toEqual('DefaultLayout');
expect(componentObj.props.showFooter).toEqual(true);
});
test('Certification path should have CertificationLayout', () => {
const challengePath =
'/certification/mot01/javascript-algorithms-and-data-structures/';
const componentObj = getComponentNameAndProps(Certification, challengePath);
expect(componentObj.name).toEqual('CertificationLayout');
});
test('Status paths should return raw element without layout', () => {
const TestComponent = () => <div>Test</div>;
const statusPath = '/status/version';
const result = layoutSelector({
element: { type: TestComponent, props: {}, key: '' },
props: {
data: {},
location: {
pathname: statusPath
},
params: { '*': '' },
path: ''
}
});
// The result should be the element directly, not wrapped in a layout
expect(result.type).toBe(TestComponent);
});
});
+69
View File
@@ -0,0 +1,69 @@
import React from 'react';
import CertificationLayout from '../../src/components/layouts/certification';
import DefaultLayout from '../../src/components/layouts/default';
import FourOhFourPage from '../../src/pages/404';
interface LayoutSelectorProps {
element: JSX.Element;
props: {
data: { challengeNode?: { challenge?: { usesMultifileEditor?: boolean } } };
location: { pathname: string };
pageContext?: { challengeMeta?: { block?: string; superBlock?: string } };
params: { '*'?: string };
path: string;
};
}
export default function layoutSelector({
element,
props
}: LayoutSelectorProps): JSX.Element {
const {
location: { pathname }
} = props;
const isDailyChallenge = props.path === '/learn/daily-coding-challenge/*';
const dailyChallengeParam = props.params['*'];
const isChallenge = !!props.pageContext?.challengeMeta || isDailyChallenge;
// Return raw element for status endpoints without any layout
if (/^\/status\//.test(pathname)) {
return element;
}
if (element.type === FourOhFourPage) {
return (
<DefaultLayout pathname={pathname} showFooter={true}>
{element}
</DefaultLayout>
);
} else if (/\/certification\//.test(pathname)) {
return (
<CertificationLayout pathname={pathname}>{element}</CertificationLayout>
);
} else if (isChallenge) {
return (
<DefaultLayout
pathname={pathname}
showFooter={false}
isChallenge={true}
isDailyChallenge={isDailyChallenge}
dailyChallengeParam={dailyChallengeParam}
usesMultifileEditor={
props.data?.challengeNode?.challenge?.usesMultifileEditor
}
block={props.pageContext?.challengeMeta?.block}
superBlock={props.pageContext?.challengeMeta?.superBlock}
>
{element}
</DefaultLayout>
);
} else {
return (
<DefaultLayout pathname={pathname} showFooter={true}>
{element}
</DefaultLayout>
);
}
}
+10
View File
@@ -0,0 +1,10 @@
/**
* The function is useful in cases where we want to preload a page
* but the link of the page isn't rendered on the screen.
* For more details, see https://github.com/freeCodeCamp/freeCodeCamp/pull/55472.
*/
export const preloadPage = (path?: string) => {
if (!window.___loader || !path) return;
window.___loader.hovering(path);
};
+5
View File
@@ -0,0 +1,5 @@
const envData = require('../../config/env.json');
const { clientLocale } = envData;
exports.pathPrefix = clientLocale === 'english' ? '' : '/' + clientLocale;
+37
View File
@@ -0,0 +1,37 @@
import { Certification } from '@freecodecamp/shared/config/certification-settings';
const idToPath = new Map(
Object.entries({
'561add10cb82ac38a17523bc': Certification.BackEndDevApis,
'5a553ca864b52e1d8bceea14': Certification.DataVis,
'561acd10cb82ac38a17513bc': Certification.FrontEndDevLibs,
'5e611829481575a52dc59c0e': Certification.QualityAssurance,
'5e6021435ac9d0ecd8b94b00': Certification.InfoSec,
'561abd10cb81ac38a17513bc': Certification.JsAlgoDataStruct,
'561add10cb82ac38a17513bc': Certification.RespWebDesign,
'660add10cb82ac38a17513be': Certification.LegacyBackEnd,
'561add10cb82ac39a17513bc': Certification.LegacyDataVis,
'561add10cb82ac38a17513be': Certification.LegacyFrontEnd,
'561add10cb82ac38a17213bc': Certification.LegacyInfoSecQa,
'561add10cb82ac38a17213bd': Certification.LegacyFullStack,
'5e44431b903586ffb414c951': Certification.SciCompPy,
'5e46fc95ac417301a38fb934': Certification.DataAnalysisPy,
'5e46fc95ac417301a38fb935': Certification.MachineLearningPy,
'68db314d3c11a8bff07c7535': Certification.RespWebDesignV9,
'68c4069c1ef859270e17c495': Certification.JsV9,
'68e008aa5f80c6099d47b3a2': Certification.FrontEndDevLibsV9,
'68e6bd5020effa1586e79855': Certification.PythonV9,
'68e6bd5120effa1586e79856': Certification.RelationalDbV9,
'68e6bd5120effa1586e79857': Certification.BackEndDevApisV9,
'64514fda6c245de4d11eb7bb': Certification.FullStackDeveloperV9,
'651dd7e01d697d0aab7833b7': Certification.A2English
})
);
export const getCertIds = (): IterableIterator<string> => idToPath.keys();
export const getPathFromID = (id: string): string | undefined =>
idToPath.get(id);
export function isBrowser(): boolean {
return typeof window !== 'undefined';
}
+33
View File
@@ -0,0 +1,33 @@
import { describe, it, expect } from 'vitest';
import { challengeFiles } from './__fixtures__/challenges';
import { sortChallengeFiles } from './sort-challengefiles';
describe('sort-files', () => {
describe('sortChallengeFiles', () => {
it('should return an array', () => {
const sorted = sortChallengeFiles(challengeFiles);
expect(Array.isArray(sorted)).toBe(true);
});
it('should not modify the challenges', () => {
const sorted = sortChallengeFiles(challengeFiles);
const expected = challengeFiles;
expect(sorted).toEqual(expect.arrayContaining(expected));
expect(sorted.length).toEqual(expected.length);
});
it('should sort the objects into jsx, tsx, html, css, js, ts, tsconfig order', () => {
const sorted = sortChallengeFiles(challengeFiles);
const sortedKeys = sorted.map(({ fileKey }) => fileKey);
const expected = [
'indexjsx',
'indextsx',
'indexhtml',
'stylescss',
'scriptjs',
'indexts',
'tsconfigjson'
];
expect(sortedKeys).toStrictEqual(expected);
});
});
});
+21
View File
@@ -0,0 +1,21 @@
export function sortChallengeFiles<File extends { fileKey: string }>(
challengeFiles: File[]
): File[] {
return challengeFiles.toSorted((a, b) => {
if (a.fileKey === 'indexjsx') return -1;
if (b.fileKey === 'indexjsx') return 1;
if (a.fileKey === 'indextsx') return -1;
if (b.fileKey === 'indextsx') return 1;
if (a.fileKey === 'indexhtml') return -1;
if (b.fileKey === 'indexhtml') return 1;
if (a.fileKey === 'stylescss') return -1;
if (b.fileKey === 'stylescss') return 1;
if (a.fileKey === 'scriptjs') return -1;
if (b.fileKey === 'scriptjs') return 1;
if (a.fileKey === 'indexts') return -1;
if (b.fileKey === 'indexts') return 1;
if (a.fileKey === 'tsconfigjson') return -1;
if (b.fileKey === 'tsconfigjson') return 1;
return 0;
});
}
+97
View File
@@ -0,0 +1,97 @@
import i18next from 'i18next';
import React from 'react';
import { isMathJaxAllowed, mathJaxSrc } from '../src/utils/math-jax';
export function getheadTagComponents(): JSX.Element[] {
const socialImage =
'https://cdn.freecodecamp.org/platform/universal/fcc_meta_1920X1080-indigo.png';
const headTags = [
<meta content='freeCodeCamp.org' key='og:title' name='og:title' />,
<meta
content={i18next.t('metaTags:social-description')}
key='og:description'
name='og:description'
/>,
<meta content={socialImage} key='og:image' property='og:image' />,
<meta
content='summary_large_image'
key='twitter:card'
name='twitter:card'
/>,
<meta
content={socialImage}
key='twitter:image:src'
name='twitter:image:src'
/>,
<meta
content='freeCodeCamp.org'
key='twitter:title'
name='twitter:title'
/>,
<meta
content={i18next.t('metaTags:social-description')}
key='twitter:description'
name='twitter:description'
/>,
<meta
content='$ilp.uphold.com/LJmbPn7WD4JB'
key='monetization'
name='monetization'
/>
];
return headTags;
}
export function getPostBodyComponents(superblock: string): JSX.Element[] {
const scripts = [];
const mathJaxScriptElement = (
<script
async={false}
id='mathjax'
key='mathjax'
src={mathJaxSrc}
type='text/javascript'
/>
);
if (isMathJaxAllowed(superblock)) {
scripts.push(mathJaxScriptElement);
}
return scripts.filter(Boolean);
}
export function getPreBodyThemeScript(): JSX.Element[] {
const script = (
<script
key='prebody-theme-init'
dangerouslySetInnerHTML={{
__html: `
(function(){
let theme = 'light';
const localTheme = localStorage.getItem('theme');
if (localTheme !== null) {
theme = localTheme === 'dark' ? 'dark' : 'light';
} else if (
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
) {
theme = 'dark';
}
const bodyEl = document && document.body;
if (bodyEl && bodyEl.classList) {
bodyEl.classList.remove('light-palette');
bodyEl.classList.remove('dark-palette');
bodyEl.classList.add(theme + '-palette');
}
})();`
}}
/>
);
return [script];
}
+18
View File
@@ -0,0 +1,18 @@
import React, { type ReactElement, type ReactNode } from 'react';
import { render as rtlRender } from '@testing-library/react';
import { Provider } from 'react-redux';
import type { Store } from 'redux';
function render(ui: ReactElement, store: Store) {
function Wrapper({ children }: { children: ReactNode }) {
return <Provider store={store}>{children}</Provider>;
}
return rtlRender(ui, { wrapper: Wrapper });
}
// re-export everything
// eslint-disable-next-line import/export
export * from '@testing-library/react';
// override render method
// eslint-disable-next-line import/export
export { render };