mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-06-05 21:04:28 +08:00
refactor: add prefixDoctype to challenge-builder + DRY build functions (#65523)
This commit is contained in:
parent
528c2afe14
commit
e2ebf5e09d
@ -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,
|
||||
|
||||
@ -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> | 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<BuildResult> {
|
||||
// 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<BuildResult> {
|
||||
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<BuildResult> {
|
||||
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,
|
||||
|
||||
@ -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: <T>(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(/^<!DOCTYPE html>/i)?.[0] || '';
|
||||
return doctype + build;
|
||||
};
|
||||
|
||||
const createContent = (
|
||||
id: string,
|
||||
{ build, sources }: { build: string; sources: Source; hooks?: Hooks }
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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(/^<!DOCTYPE html>/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<BuildResult> {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user