mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
refactor: move challenge build outside client (#65513)
This commit is contained in:
committed by
GitHub
parent
c8b21dfc4a
commit
5ff971687c
+3
-4
@@ -47,14 +47,13 @@
|
|||||||
"@babel/preset-env": "7.23.7",
|
"@babel/preset-env": "7.23.7",
|
||||||
"@babel/preset-react": "7.23.3",
|
"@babel/preset-react": "7.23.3",
|
||||||
"@babel/preset-typescript": "7.23.3",
|
"@babel/preset-typescript": "7.23.3",
|
||||||
"@babel/standalone": "7.23.7",
|
|
||||||
"@codesandbox/sandpack-react": "2.6.9",
|
"@codesandbox/sandpack-react": "2.6.9",
|
||||||
"@codesandbox/sandpack-themes": "2.0.21",
|
"@codesandbox/sandpack-themes": "2.0.21",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.7.1",
|
"@fortawesome/fontawesome-svg-core": "6.7.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "6.7.1",
|
"@fortawesome/free-brands-svg-icons": "6.7.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.7.1",
|
"@fortawesome/free-solid-svg-icons": "6.7.1",
|
||||||
"@fortawesome/react-fontawesome": "0.2.2",
|
"@fortawesome/react-fontawesome": "0.2.2",
|
||||||
"@freecodecamp/loop-protect": "3.0.0",
|
"@freecodecamp/challenge-builder": "workspace:*",
|
||||||
"@freecodecamp/ui": "5.0.1",
|
"@freecodecamp/ui": "5.0.1",
|
||||||
"@gatsbyjs/reach-router": "1.3.9",
|
"@gatsbyjs/reach-router": "1.3.9",
|
||||||
"@growthbook/growthbook-react": "1.6.0",
|
"@growthbook/growthbook-react": "1.6.0",
|
||||||
@@ -141,10 +140,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||||
|
"@freecodecamp/browser-scripts": "workspace:*",
|
||||||
|
"@freecodecamp/curriculum": "workspace:*",
|
||||||
"@freecodecamp/eslint-config": "workspace:*",
|
"@freecodecamp/eslint-config": "workspace:*",
|
||||||
"@freecodecamp/shared": "workspace:*",
|
"@freecodecamp/shared": "workspace:*",
|
||||||
"@freecodecamp/curriculum": "workspace:*",
|
|
||||||
"@freecodecamp/browser-scripts": "workspace:*",
|
|
||||||
"@testing-library/jest-dom": "^6.8.0",
|
"@testing-library/jest-dom": "^6.8.0",
|
||||||
"@testing-library/react": "12.1.5",
|
"@testing-library/react": "12.1.5",
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { runSaga } from 'redux-saga';
|
import { runSaga } from 'redux-saga';
|
||||||
import { describe, test, it, expect, beforeEach, vi, Mock } from 'vitest';
|
import { describe, test, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||||
|
import { buildChallenge } from '@freecodecamp/challenge-builder/build';
|
||||||
|
|
||||||
import { render } from '../../../../utils/test-utils';
|
import { render } from '../../../../utils/test-utils';
|
||||||
|
|
||||||
import { getCompletedPercentage } from '../../../utils/get-completion-percentage';
|
import { getCompletedPercentage } from '../../../utils/get-completion-percentage';
|
||||||
@@ -14,7 +16,7 @@ import {
|
|||||||
isBuildEnabledSelector,
|
isBuildEnabledSelector,
|
||||||
isBlockNewlyCompletedSelector
|
isBlockNewlyCompletedSelector
|
||||||
} from '../redux/selectors';
|
} from '../redux/selectors';
|
||||||
import { buildChallenge, getTestRunner } from '../utils/build';
|
import { getTestRunner } from '../utils/build';
|
||||||
import CompletionModal, { combineFileData } from './completion-modal';
|
import CompletionModal, { combineFileData } from './completion-modal';
|
||||||
vi.mock('../../../analytics');
|
vi.mock('../../../analytics');
|
||||||
vi.mock('../../../utils/fire-confetti');
|
vi.mock('../../../utils/fire-confetti');
|
||||||
@@ -22,6 +24,7 @@ vi.mock('../../../components/Progress');
|
|||||||
vi.mock('../redux/selectors');
|
vi.mock('../redux/selectors');
|
||||||
vi.mock('../utils/build');
|
vi.mock('../utils/build');
|
||||||
vi.mock('../../../utils/get-words');
|
vi.mock('../../../utils/get-words');
|
||||||
|
vi.mock('@freecodecamp/challenge-builder/build');
|
||||||
const mockFireConfetti = fireConfetti as Mock;
|
const mockFireConfetti = fireConfetti as Mock;
|
||||||
const mockTestRunner = vi.fn().mockReturnValue({ pass: true });
|
const mockTestRunner = vi.fn().mockReturnValue({ pass: true });
|
||||||
const mockBuildEnabledSelector = isBuildEnabledSelector as Mock;
|
const mockBuildEnabledSelector = isBuildEnabledSelector as Mock;
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
} from 'redux-saga/effects';
|
} from 'redux-saga/effects';
|
||||||
|
|
||||||
import { challengeTypes } from '@freecodecamp/shared/config/challenge-types';
|
import { challengeTypes } from '@freecodecamp/shared/config/challenge-types';
|
||||||
|
import { buildChallenge } from '@freecodecamp/challenge-builder/build';
|
||||||
|
|
||||||
import { createFlashMessage } from '../../../components/Flash/redux';
|
import { createFlashMessage } from '../../../components/Flash/redux';
|
||||||
import { FlashMessages } from '../../../components/Flash/redux/flash-messages';
|
import { FlashMessages } from '../../../components/Flash/redux/flash-messages';
|
||||||
import {
|
import {
|
||||||
@@ -25,7 +27,6 @@ import {
|
|||||||
} from '../../../utils/challenge-request-helpers';
|
} from '../../../utils/challenge-request-helpers';
|
||||||
import { playTone } from '../../../utils/tone';
|
import { playTone } from '../../../utils/tone';
|
||||||
import {
|
import {
|
||||||
buildChallenge,
|
|
||||||
canBuildChallenge,
|
canBuildChallenge,
|
||||||
challengeHasPreview,
|
challengeHasPreview,
|
||||||
getTestRunner,
|
getTestRunner,
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { challengeTypes } from '@freecodecamp/shared/config/challenge-types';
|
import { challengeTypes } from '@freecodecamp/shared/config/challenge-types';
|
||||||
|
import type { ChallengeFile } from '@freecodecamp/shared/utils/polyvinyl';
|
||||||
|
|
||||||
import type { ChallengeFile } from '../../../redux/prop-types';
|
|
||||||
import { concatHtml } from '../rechallenge/builders';
|
|
||||||
import {
|
import {
|
||||||
getTransformers,
|
getTransformers,
|
||||||
embedFilesInHtml,
|
embedFilesInHtml,
|
||||||
getPythonTransformers,
|
getPythonTransformers,
|
||||||
getMultifileJSXTransformers
|
getMultifileJSXTransformers
|
||||||
} from '../rechallenge/transformers';
|
} from '@freecodecamp/challenge-builder/transformers';
|
||||||
|
import { concatHtml } from '@freecodecamp/challenge-builder/builders';
|
||||||
|
import { runnerTypes } from '@freecodecamp/challenge-builder/build';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
runTestsInTestFrame,
|
runTestsInTestFrame,
|
||||||
createMainPreviewFramer,
|
createMainPreviewFramer,
|
||||||
@@ -97,56 +99,6 @@ export function canBuildChallenge(challengeData: BuildChallengeData): boolean {
|
|||||||
return Object.prototype.hasOwnProperty.call(buildFunctions, challengeType);
|
return Object.prototype.hasOwnProperty.call(buildFunctions, challengeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildChallenge(
|
|
||||||
challengeData: BuildChallengeData,
|
|
||||||
options: BuildOptions
|
|
||||||
) {
|
|
||||||
const { challengeType } = challengeData;
|
|
||||||
const build = buildFunctions[challengeType];
|
|
||||||
if (build) {
|
|
||||||
return build(challengeData, options);
|
|
||||||
}
|
|
||||||
throw new Error(`Cannot build challenge of type ${challengeType}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const runnerTypes: Record<
|
|
||||||
(typeof challengeTypes)[keyof typeof challengeTypes],
|
|
||||||
'javascript' | 'dom' | 'python'
|
|
||||||
> = {
|
|
||||||
[challengeTypes.html]: 'dom',
|
|
||||||
[challengeTypes.js]: 'javascript',
|
|
||||||
[challengeTypes.backend]: 'dom',
|
|
||||||
[challengeTypes.zipline]: 'dom',
|
|
||||||
[challengeTypes.frontEndProject]: 'dom',
|
|
||||||
[challengeTypes.backEndProject]: 'dom',
|
|
||||||
[challengeTypes.pythonProject]: 'python',
|
|
||||||
[challengeTypes.jsProject]: 'javascript',
|
|
||||||
[challengeTypes.modern]: 'dom',
|
|
||||||
[challengeTypes.step]: 'dom',
|
|
||||||
[challengeTypes.quiz]: 'dom',
|
|
||||||
[challengeTypes.invalid]: 'dom',
|
|
||||||
[challengeTypes.video]: 'dom',
|
|
||||||
[challengeTypes.codeAllyPractice]: 'dom',
|
|
||||||
[challengeTypes.codeAllyCert]: 'dom',
|
|
||||||
[challengeTypes.multifileCertProject]: 'dom',
|
|
||||||
[challengeTypes.theOdinProject]: 'dom',
|
|
||||||
[challengeTypes.colab]: 'dom',
|
|
||||||
[challengeTypes.exam]: 'dom',
|
|
||||||
[challengeTypes.msTrophy]: 'dom',
|
|
||||||
[challengeTypes.multipleChoice]: 'dom',
|
|
||||||
[challengeTypes.python]: 'python',
|
|
||||||
[challengeTypes.dialogue]: 'dom',
|
|
||||||
[challengeTypes.fillInTheBlank]: 'dom',
|
|
||||||
[challengeTypes.multifilePythonCertProject]: 'python',
|
|
||||||
[challengeTypes.generic]: 'dom',
|
|
||||||
[challengeTypes.lab]: 'dom',
|
|
||||||
[challengeTypes.jsLab]: 'javascript',
|
|
||||||
[challengeTypes.pyLab]: 'python',
|
|
||||||
[challengeTypes.dailyChallengeJs]: 'javascript',
|
|
||||||
[challengeTypes.dailyChallengePy]: 'python',
|
|
||||||
[challengeTypes.review]: 'dom'
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getTestRunner(buildData: BuildChallengeData) {
|
export async function getTestRunner(buildData: BuildChallengeData) {
|
||||||
const { challengeType } = buildData;
|
const { challengeType } = buildData;
|
||||||
// TODO: Fully type BuildChallengeData
|
// TODO: Fully type BuildChallengeData
|
||||||
|
|||||||
@@ -48,9 +48,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.23.7",
|
"@babel/core": "7.23.7",
|
||||||
"@babel/register": "7.23.7",
|
"@babel/register": "7.23.7",
|
||||||
|
"@freecodecamp/browser-scripts": "workspace:*",
|
||||||
|
"@freecodecamp/challenge-builder": "workspace:*",
|
||||||
"@freecodecamp/eslint-config": "workspace:*",
|
"@freecodecamp/eslint-config": "workspace:*",
|
||||||
"@freecodecamp/shared": "workspace:*",
|
"@freecodecamp/shared": "workspace:*",
|
||||||
"@freecodecamp/browser-scripts": "workspace:*",
|
|
||||||
"@total-typescript/ts-reset": "^0.6.1",
|
"@total-typescript/ts-reset": "^0.6.1",
|
||||||
"@types/debug": "^4.1.12",
|
"@types/debug": "^4.1.12",
|
||||||
"@types/js-yaml": "4.0.5",
|
"@types/js-yaml": "4.0.5",
|
||||||
|
|||||||
@@ -26,16 +26,16 @@ import { sortChallenges } from './utils/sort-challenges.js';
|
|||||||
const { flatten, isEmpty, cloneDeep } = lodash;
|
const { flatten, isEmpty, cloneDeep } = lodash;
|
||||||
|
|
||||||
vi.mock(
|
vi.mock(
|
||||||
'../../../client/src/templates/Challenges/utils/typescript-worker-handler',
|
'@freecodecamp/challenge-builder/typescript-worker-handler',
|
||||||
async importOriginal => {
|
async importOriginal => {
|
||||||
const actual = await importOriginal();
|
const actual = await importOriginal();
|
||||||
|
|
||||||
// ts and tsvfs must match the versions used in the typescript-worker.
|
// ts and tsvfs must match the versions used in the typescript-worker.
|
||||||
const tsvfs = await import('@typescript/vfs-1.6.1');
|
const tsvfs = await import('@typescript/vfs-1.6.1');
|
||||||
const ts = await import('typescript-5.9.2');
|
const ts = await import('typescript-5.9.2');
|
||||||
// use the same TS compiler as the client
|
// use the same TS compiler as the challenge-builder
|
||||||
const tsCompiler = await import(
|
const tsCompiler = await import(
|
||||||
'../../../tools/client-plugins/browser-scripts/modules/typescript-compiler'
|
'@freecodecamp/browser-scripts/ts-compiler'
|
||||||
);
|
);
|
||||||
const compiler = new tsCompiler.Compiler(ts, tsvfs);
|
const compiler = new tsCompiler.Compiler(ts, tsvfs);
|
||||||
await compiler.setup({ useNodeModules: true });
|
await compiler.setup({ useNodeModules: true });
|
||||||
@@ -151,7 +151,7 @@ async function populateTestsForLang({ lang, challenges, meta }) {
|
|||||||
// Presumably this is because we import from_this file in the generated block
|
// Presumably this is because we import from_this file in the generated block
|
||||||
// test files and that happens before the mock is applied.
|
// test files and that happens before the mock is applied.
|
||||||
const { buildChallenge } = await import(
|
const { buildChallenge } = await import(
|
||||||
'../../../client/src/templates/Challenges/utils/build'
|
'@freecodecamp/challenge-builder/build'
|
||||||
);
|
);
|
||||||
const validateChallenge = challengeSchemaValidator();
|
const validateChallenge = challengeSchemaValidator();
|
||||||
|
|
||||||
@@ -370,9 +370,7 @@ async function createTestRunner(
|
|||||||
buildChallenge,
|
buildChallenge,
|
||||||
solutionFromNext
|
solutionFromNext
|
||||||
) {
|
) {
|
||||||
const { runnerTypes } = await import(
|
const { runnerTypes } = await import('@freecodecamp/challenge-builder/build');
|
||||||
'../../../client/src/templates/Challenges/utils/build'
|
|
||||||
);
|
|
||||||
|
|
||||||
const challengeFiles = replaceChallengeFilesContentsWithSolutions(
|
const challengeFiles = replaceChallengeFilesContentsWithSolutions(
|
||||||
challenge.challengeFiles,
|
challenge.challengeFiles,
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
dist
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/* eslint-disable filenames-simple/naming-convention */
|
||||||
|
import { createLintStagedConfig } from '@freecodecamp/eslint-config/lintstaged';
|
||||||
|
|
||||||
|
export default createLintStagedConfig(import.meta.dirname);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { configTypeChecked } from '@freecodecamp/eslint-config/base';
|
||||||
|
import globals from 'globals';
|
||||||
|
|
||||||
|
import { defineConfig } from 'eslint/config';
|
||||||
|
|
||||||
|
const baseLanguageOptions = {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node // TODO: necessary?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
extends: [configTypeChecked],
|
||||||
|
languageOptions: {
|
||||||
|
...baseLanguageOptions
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "@freecodecamp/challenge-builder",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"author": "freeCodeCamp <team@freecodecamp.org>",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"description": "Builds challenges for testing and rendering",
|
||||||
|
"private": false,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24",
|
||||||
|
"pnpm": ">=10"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"./build": "./dist/build.js",
|
||||||
|
"./transformers": "./dist/transformers.js",
|
||||||
|
"./typescript-worker-handler": "./dist/typescript-worker-handler.js",
|
||||||
|
"./builders": "./dist/builders.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"test:ui": "vitest --ui",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"compile": "tsc",
|
||||||
|
"lint": "eslint --max-warnings 0"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/freeCodeCamp/freeCodeCamp.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/freeCodeCamp/freeCodeCamp/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"@freecodecamp/eslint-config": "workspace:*",
|
||||||
|
"@types/lodash-es": "4.17.12",
|
||||||
|
"@vitest/ui": "^3.2.4",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/preset-env": "7.23.7",
|
||||||
|
"@babel/preset-react": "7.28.5",
|
||||||
|
"@babel/standalone": "7.23.7",
|
||||||
|
"@freecodecamp/browser-scripts": "workspace:*",
|
||||||
|
"@freecodecamp/loop-protect": "3.0.0",
|
||||||
|
"@freecodecamp/shared": "workspace:*",
|
||||||
|
"lodash-es": "4.17.23"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
import { challengeTypes } from '@freecodecamp/shared/config/challenge-types';
|
||||||
|
import type { ChallengeFile } from '@freecodecamp/shared/utils/polyvinyl';
|
||||||
|
|
||||||
|
import { concatHtml } from './builders.js';
|
||||||
|
import {
|
||||||
|
getTransformers,
|
||||||
|
embedFilesInHtml,
|
||||||
|
getPythonTransformers,
|
||||||
|
getMultifileJSXTransformers
|
||||||
|
} from './transformers.js';
|
||||||
|
|
||||||
|
interface Source {
|
||||||
|
index: string;
|
||||||
|
contents?: string;
|
||||||
|
editableContents: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BuildChallengeData {
|
||||||
|
challengeType: number;
|
||||||
|
challengeFiles?: ChallengeFile[];
|
||||||
|
required: { src?: string }[];
|
||||||
|
template: string;
|
||||||
|
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 buildChallenge(
|
||||||
|
challengeData: BuildChallengeData,
|
||||||
|
options: BuildOptions
|
||||||
|
) {
|
||||||
|
const { challengeType } = challengeData;
|
||||||
|
const build = buildFunctions[challengeType];
|
||||||
|
if (build) {
|
||||||
|
return build(challengeData, options);
|
||||||
|
}
|
||||||
|
throw new Error(`Cannot build challenge of type ${challengeType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const runnerTypes: Record<
|
||||||
|
(typeof challengeTypes)[keyof typeof challengeTypes],
|
||||||
|
'javascript' | 'dom' | 'python'
|
||||||
|
> = {
|
||||||
|
[challengeTypes.html]: 'dom',
|
||||||
|
[challengeTypes.js]: 'javascript',
|
||||||
|
[challengeTypes.backend]: 'dom',
|
||||||
|
[challengeTypes.zipline]: 'dom',
|
||||||
|
[challengeTypes.frontEndProject]: 'dom',
|
||||||
|
[challengeTypes.backEndProject]: 'dom',
|
||||||
|
[challengeTypes.pythonProject]: 'python',
|
||||||
|
[challengeTypes.jsProject]: 'javascript',
|
||||||
|
[challengeTypes.modern]: 'dom',
|
||||||
|
[challengeTypes.step]: 'dom',
|
||||||
|
[challengeTypes.quiz]: 'dom',
|
||||||
|
[challengeTypes.invalid]: 'dom',
|
||||||
|
[challengeTypes.video]: 'dom',
|
||||||
|
[challengeTypes.codeAllyPractice]: 'dom',
|
||||||
|
[challengeTypes.codeAllyCert]: 'dom',
|
||||||
|
[challengeTypes.multifileCertProject]: 'dom',
|
||||||
|
[challengeTypes.theOdinProject]: 'dom',
|
||||||
|
[challengeTypes.colab]: 'dom',
|
||||||
|
[challengeTypes.exam]: 'dom',
|
||||||
|
[challengeTypes.msTrophy]: 'dom',
|
||||||
|
[challengeTypes.multipleChoice]: 'dom',
|
||||||
|
[challengeTypes.python]: 'python',
|
||||||
|
[challengeTypes.dialogue]: 'dom',
|
||||||
|
[challengeTypes.fillInTheBlank]: 'dom',
|
||||||
|
[challengeTypes.multifilePythonCertProject]: 'python',
|
||||||
|
[challengeTypes.generic]: 'dom',
|
||||||
|
[challengeTypes.lab]: 'dom',
|
||||||
|
[challengeTypes.jsLab]: 'javascript',
|
||||||
|
[challengeTypes.pyLab]: 'python',
|
||||||
|
[challengeTypes.dailyChallengeJs]: 'javascript',
|
||||||
|
[challengeTypes.dailyChallengePy]: 'python',
|
||||||
|
[challengeTypes.review]: 'dom'
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
+2
-2
@@ -17,11 +17,11 @@ import {
|
|||||||
} from '@freecodecamp/shared/utils/polyvinyl';
|
} from '@freecodecamp/shared/utils/polyvinyl';
|
||||||
import { version } from '@freecodecamp/browser-scripts/package.json';
|
import { version } from '@freecodecamp/browser-scripts/package.json';
|
||||||
|
|
||||||
import { WorkerExecutor } from '../utils/worker-executor';
|
import { WorkerExecutor } from './worker-executor';
|
||||||
import {
|
import {
|
||||||
compileTypeScriptCode,
|
compileTypeScriptCode,
|
||||||
checkTSServiceIsReady
|
checkTSServiceIsReady
|
||||||
} from '../utils/typescript-worker-handler';
|
} from './typescript-worker-handler';
|
||||||
|
|
||||||
const protectTimeout = 100;
|
const protectTimeout = 100;
|
||||||
const testProtectTimeout = 1500;
|
const testProtectTimeout = 1500;
|
||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
import { version } from '@freecodecamp/browser-scripts/package.json';
|
import browserScripts from '@freecodecamp/browser-scripts/package.json';
|
||||||
|
|
||||||
import { awaitResponse } from './awaitable-messenger';
|
import { awaitResponse } from './awaitable-messenger.js';
|
||||||
|
|
||||||
const typeScriptWorkerSrc = `/js/workers/${version}/typescript-worker.js`;
|
const typeScriptWorkerSrc = `/js/workers/${browserScripts.version}/typescript-worker.js`;
|
||||||
|
|
||||||
let worker: Worker | null = null;
|
let worker: Worker | null = null;
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"include": ["src"],
|
||||||
|
"extends": "../../tsconfig-base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"module": "es2020",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"outDir": "dist",
|
||||||
|
"noEmit": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+383
-257
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,8 @@
|
|||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
|
".": "./index.d.ts",
|
||||||
|
"./ts-compiler": "./modules/typescript-compiler.ts",
|
||||||
"./test-runner": "./test-runner.ts",
|
"./test-runner": "./test-runner.ts",
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user