diff --git a/client/src/templates/Challenges/redux/execute-challenge-saga.js b/client/src/templates/Challenges/redux/execute-challenge-saga.js index 33bc039d3de..c259b50aa8e 100644 --- a/client/src/templates/Challenges/redux/execute-challenge-saga.js +++ b/client/src/templates/Challenges/redux/execute-challenge-saga.js @@ -15,7 +15,10 @@ import { } from 'redux-saga/effects'; import { challengeTypes } from '@freecodecamp/shared/config/challenge-types'; -import { buildChallenge } from '@freecodecamp/challenge-builder/build'; +import { + buildChallenge, + canBuildChallenge +} from '@freecodecamp/challenge-builder/build'; import { createFlashMessage } from '../../../components/Flash/redux'; import { FlashMessages } from '../../../components/Flash/redux/flash-messages'; @@ -27,7 +30,6 @@ import { } from '../../../utils/challenge-request-helpers'; import { playTone } from '../../../utils/tone'; import { - canBuildChallenge, challengeHasPreview, getTestRunner, isJavaScriptChallenge, diff --git a/client/src/templates/Challenges/utils/build.ts b/client/src/templates/Challenges/utils/build.ts index f8f3c0df4af..88999d004db 100644 --- a/client/src/templates/Challenges/utils/build.ts +++ b/client/src/templates/Challenges/utils/build.ts @@ -1,13 +1,6 @@ import { challengeTypes } from '@freecodecamp/shared/config/challenge-types'; import type { ChallengeFile } from '@freecodecamp/shared/utils/polyvinyl'; -import { - getTransformers, - embedFilesInHtml, - getPythonTransformers, - getMultifileJSXTransformers -} from '@freecodecamp/challenge-builder/transformers'; -import { concatHtml } from '@freecodecamp/challenge-builder/builders'; import { runnerTypes } from '@freecodecamp/challenge-builder/build'; import { @@ -16,7 +9,6 @@ import { createProjectPreviewFramer, ProxyLogger, Context, - Source, prepTestRunner } from './frame'; @@ -28,77 +20,6 @@ interface BuildChallengeData extends Context { url: string; } -interface BuildOptions { - preview: boolean; - disableLoopProtectTests: boolean; - disableLoopProtectPreview: boolean; - usesTestRunner?: boolean; -} - -type ApplyFunctionProps = ( - file: ChallengeFile -) => Promise | ChallengeFile; - -const applyFunction = - (fn: ApplyFunctionProps) => async (file: ChallengeFile) => { - try { - if (file.error) { - return file; - } - const newFile = await fn.call(this, file); - if (typeof newFile !== 'undefined') { - return newFile; - } - return file; - } catch (error) { - return { ...file, error }; - } - }; - -const composeFunctions = (...fns: ApplyFunctionProps[]) => - fns.map(applyFunction).reduce((f, g) => x => f(x).then(g)); - -function buildSourceMap(challengeFiles: ChallengeFile[]): Source | undefined { - // TODO: rename sources.index to sources.contents. - const source: Source | undefined = challengeFiles?.reduce( - (sources, challengeFile) => { - sources.index += challengeFile.source || ''; - sources.contents = sources.index; - sources.editableContents += challengeFile.editableContents || ''; - return sources; - }, - { - index: '', - editableContents: '' - } as Source - ); - return source; -} - -export const buildFunctions = { - [challengeTypes.js]: buildJSChallenge, - [challengeTypes.jsProject]: buildJSChallenge, - [challengeTypes.html]: buildDOMChallenge, - [challengeTypes.modern]: buildDOMChallenge, - [challengeTypes.backend]: buildBackendChallenge, - [challengeTypes.backEndProject]: buildBackendChallenge, - [challengeTypes.pythonProject]: buildBackendChallenge, - [challengeTypes.multifileCertProject]: buildDOMChallenge, - [challengeTypes.colab]: buildBackendChallenge, - [challengeTypes.python]: buildPythonChallenge, - [challengeTypes.multifilePythonCertProject]: buildPythonChallenge, - [challengeTypes.lab]: buildDOMChallenge, - [challengeTypes.jsLab]: buildJSChallenge, - [challengeTypes.pyLab]: buildPythonChallenge, - [challengeTypes.dailyChallengeJs]: buildJSChallenge, - [challengeTypes.dailyChallengePy]: buildPythonChallenge -}; - -export function canBuildChallenge(challengeData: BuildChallengeData): boolean { - const { challengeType } = challengeData; - return Object.prototype.hasOwnProperty.call(buildFunctions, challengeType); -} - export async function getTestRunner(buildData: BuildChallengeData) { const { challengeType } = buildData; // TODO: Fully type BuildChallengeData @@ -115,130 +36,6 @@ export async function getTestRunner(buildData: BuildChallengeData) { runTestsInTestFrame(testStrings, testTimeout, type); } -type BuildResult = { - challengeType: number; - build?: string; - sources: Source | undefined; - loadEnzyme?: boolean; - error?: unknown; -}; - -// TODO: All the buildXChallenge files have a similar structure, so make that -// abstraction (function, class, whatever) and then create the various functions -// out of it. -export async function buildDOMChallenge( - { - challengeFiles, - required = [], - template = '', - challengeType - }: BuildChallengeData, - options?: BuildOptions -): Promise { - // TODO: make this required in the schema. - if (!challengeFiles) throw Error('No challenge files provided'); - const hasJsx = challengeFiles.some( - challengeFile => challengeFile.ext === 'jsx' || challengeFile.ext === 'tsx' - ); - const isMultifile = challengeFiles.length > 1; - - const requiresReact16 = required.some(({ src }) => - src?.includes('https://cdnjs.cloudflare.com/ajax/libs/react/16.') - ); - - // I'm reasonably sure this is fine, but we need to migrate transformers to - // TypeScript to be sure. - const transformers: ApplyFunctionProps[] = (isMultifile && hasJsx - ? getMultifileJSXTransformers(options) - : getTransformers(options)) as unknown as ApplyFunctionProps[]; - - const pipeLine = composeFunctions(...transformers); - const finalFiles = await Promise.all(challengeFiles.map(pipeLine)); - const error = finalFiles.find(({ error }) => error)?.error; - const contents = (await embedFilesInHtml(finalFiles)) as string; - - // if there is an error, we just build the test runner so that it can be - // used to run tests against the code without actually running the code. - const toBuild = error - ? {} - : { - required, - template, - contents - }; - - return { - challengeType, - build: concatHtml(toBuild), - sources: buildSourceMap(finalFiles), - loadEnzyme: requiresReact16, - error - }; -} - -export async function buildJSChallenge( - { - challengeFiles, - challengeType - }: { challengeFiles?: ChallengeFile[]; challengeType: number }, - options: BuildOptions -): Promise { - if (!challengeFiles) throw Error('No challenge files provided'); - const pipeLine = composeFunctions( - ...(getTransformers(options) as unknown as ApplyFunctionProps[]) - ); - - const finalFiles = await Promise.all(challengeFiles?.map(pipeLine)); - const error = finalFiles.find(({ error }) => error)?.error; - - const toBuild = error ? [] : finalFiles; - - return { - challengeType, - build: toBuild - .reduce( - (body, challengeFile) => [ - ...body, - challengeFile.head, - challengeFile.contents, - challengeFile.tail - ], - [] as string[] - ) - .join('\n'), - sources: buildSourceMap(finalFiles), - error - }; -} - -function buildBackendChallenge({ url, challengeType }: BuildChallengeData) { - return { - challengeType, - build: '', - sources: { contents: url } - }; -} - -export async function buildPythonChallenge({ - challengeFiles, - challengeType -}: BuildChallengeData): Promise { - if (!challengeFiles) throw new Error('No challenge files provided'); - const pipeLine = composeFunctions( - ...(getPythonTransformers() as unknown as ApplyFunctionProps[]) - ); - const finalFiles = await Promise.all(challengeFiles.map(pipeLine)); - const error = finalFiles.find(({ error }) => error)?.error; - const sources = buildSourceMap(finalFiles); - - return { - challengeType, - sources, - build: sources?.contents, - error - }; -} - export function updatePreview( buildData: BuildChallengeData, document: Document, diff --git a/client/src/templates/Challenges/utils/frame.ts b/client/src/templates/Challenges/utils/frame.ts index da7a051a5a1..adee26e092f 100644 --- a/client/src/templates/Challenges/utils/frame.ts +++ b/client/src/templates/Challenges/utils/frame.ts @@ -1,6 +1,8 @@ import { flow } from 'lodash-es'; import i18next, { type i18n } from 'i18next'; +import { prefixDoctype } from '@freecodecamp/challenge-builder/build'; + import { version as _helperVersion, type FCCTestRunner @@ -24,7 +26,7 @@ declare global { const utilsFormat: (x: T) => string = format; -export interface Source { +interface Source { index: string; contents?: string; editableContents: string; @@ -410,19 +412,6 @@ const waitForFrame = (frameContext: Context) => { }); }; -export const prefixDoctype = ({ - build, - sources -}: { - build: string; - sources: Source; -}) => { - // DOCTYPE should be the first thing written to the frame, so if the user code - // includes a DOCTYPE declaration, we need to find it and write it first. - const doctype = sources.contents?.match(/^/i)?.[0] || ''; - return doctype + build; -}; - const createContent = ( id: string, { build, sources }: { build: string; sources: Source; hooks?: Hooks } diff --git a/curriculum/src/test/test-challenges.js b/curriculum/src/test/test-challenges.js index 17eb63147b3..aee153c7bc9 100644 --- a/curriculum/src/test/test-challenges.js +++ b/curriculum/src/test/test-challenges.js @@ -8,7 +8,7 @@ import { hasNoSolution } from '@freecodecamp/shared/config/challenge-types'; import { getLines } from '@freecodecamp/shared/utils/get-lines'; -import { prefixDoctype } from '../../../client/src/templates/Challenges/utils/frame'; +import { prefixDoctype } from '@freecodecamp/challenge-builder/build'; import { getChallengesForLang } from '../get-challenges.js'; import { challengeSchemaValidator } from '../../schema/challenge-schema.js'; diff --git a/packages/challenge-builder/src/build.ts b/packages/challenge-builder/src/build.ts index 8c94925ec04..90560260941 100644 --- a/packages/challenge-builder/src/build.ts +++ b/packages/challenge-builder/src/build.ts @@ -70,7 +70,7 @@ function buildSourceMap(challengeFiles: ChallengeFile[]): Source | undefined { return source; } -export const buildFunctions = { +const buildFunctions = { [challengeTypes.js]: buildJSChallenge, [challengeTypes.jsProject]: buildJSChallenge, [challengeTypes.html]: buildDOMChallenge, @@ -106,6 +106,19 @@ export async function buildChallenge( throw new Error(`Cannot build challenge of type ${challengeType}`); } +export const prefixDoctype = ({ + build, + sources +}: { + build: string; + sources: Source; +}) => { + // DOCTYPE should be the first thing written to the frame, so if the user code + // includes a DOCTYPE declaration, we need to find it and write it first. + const doctype = sources.contents?.match(/^/i)?.[0] || ''; + return doctype + build; +}; + export const runnerTypes: Record< (typeof challengeTypes)[keyof typeof challengeTypes], 'javascript' | 'dom' | 'python' @@ -155,7 +168,7 @@ type BuildResult = { // TODO: All the buildXChallenge files have a similar structure, so make that // abstraction (function, class, whatever) and then create the various functions // out of it. -export async function buildDOMChallenge( +async function buildDOMChallenge( { challengeFiles, required = [], @@ -205,7 +218,7 @@ export async function buildDOMChallenge( }; } -export async function buildJSChallenge( +async function buildJSChallenge( { challengeFiles, challengeType @@ -248,7 +261,7 @@ function buildBackendChallenge({ url, challengeType }: BuildChallengeData) { }; } -export async function buildPythonChallenge({ +async function buildPythonChallenge({ challengeFiles, challengeType }: BuildChallengeData): Promise {