Files
freeCodeCamp/client/src/templates/Challenges/utils/build.js

238 lines
7.0 KiB
JavaScript
Raw Normal View History

// the config files are created during the build, but not before linting
// eslint-disable-next-line import/no-unresolved
import frameRunnerData from '../../../../../config/client/frame-runner.json';
// eslint-disable-next-line import/no-unresolved
import testEvaluatorData from '../../../../../config/client/test-evaluator.json';
import { challengeTypes } from '../../../../utils/challenge-types';
import { cssToHtml, jsToHtml, concatHtml } from '../rechallenge/builders.js';
import { getTransformers } from '../rechallenge/transformers';
import {
createTestFramer,
runTestInTestFrame,
createMainPreviewFramer,
createProjectPreviewFramer
} from './frame';
import createWorker from './worker-executor';
const { filename: runner } = frameRunnerData;
const { filename: testEvaluator } = testEvaluatorData;
2018-12-29 11:34:03 +03:00
const frameRunner = [
{
src: `/js/${runner}.js`
2018-12-29 11:34:03 +03:00
}
];
const globalRequires = [
{
link:
'https://cdnjs.cloudflare.com/' +
'ajax/libs/normalize/4.2.0/normalize.min.css'
}
];
2018-12-29 16:42:09 +03:00
const applyFunction = fn =>
async function (file) {
2018-12-29 16:42:09 +03:00
try {
if (file.error) {
return file;
}
const newFile = await fn.call(this, file);
if (typeof newFile !== 'undefined') {
return newFile;
2018-10-06 02:36:38 +03:00
}
2018-12-29 16:42:09 +03:00
return file;
} catch (error) {
return { ...file, error };
2018-10-06 02:36:38 +03:00
}
2019-01-09 03:23:17 +03:00
};
2018-10-06 02:36:38 +03:00
2018-12-29 16:42:09 +03:00
const composeFunctions = (...fns) =>
2019-01-09 03:23:17 +03:00
fns.map(applyFunction).reduce((f, g) => x => f(x).then(g));
function buildSourceMap(challengeFiles) {
// TODO: concatenating the source/contents is a quick hack for multi-file
// editing. It is used because all the files (js, html and css) end up with
// the same name 'index'. This made the last file the only file to appear in
// sources.
// A better solution is to store and handle them separately. Perhaps never
// setting the name to 'index'. Use 'contents' instead?
// TODO: is file.source ever defined?
const source = challengeFiles.reduce(
(sources, challengeFile) => {
sources[challengeFile.name] +=
challengeFile.source || challengeFile.contents;
sources.editableContents += challengeFile.editableContents || '';
return sources;
},
{ index: '', editableContents: '' }
);
return source;
2018-12-29 11:34:03 +03:00
}
function checkFilesErrors(challengeFiles) {
const errors = challengeFiles
.filter(({ error }) => error)
.map(({ error }) => error);
2019-01-09 03:23:17 +03:00
if (errors.length) {
throw errors;
}
return challengeFiles;
2019-01-09 03:23:17 +03:00
}
2019-02-08 17:33:05 +03:00
const buildFunctions = {
[challengeTypes.js]: buildJSChallenge,
[challengeTypes.bonfire]: buildJSChallenge,
[challengeTypes.html]: buildDOMChallenge,
[challengeTypes.modern]: buildDOMChallenge,
[challengeTypes.backend]: buildBackendChallenge,
[challengeTypes.backEndProject]: buildBackendChallenge,
[challengeTypes.pythonProject]: buildBackendChallenge
2019-02-08 17:33:05 +03:00
};
export function canBuildChallenge(challengeData) {
const { challengeType } = challengeData;
return buildFunctions.hasOwnProperty(challengeType);
}
export async function buildChallenge(challengeData, options) {
2019-02-08 17:33:05 +03:00
const { challengeType } = challengeData;
let build = buildFunctions[challengeType];
if (build) {
return build(challengeData, options);
2019-02-08 17:33:05 +03:00
}
throw new Error(`Cannot build challenge of type ${challengeType}`);
2019-02-08 17:33:05 +03:00
}
const testRunners = {
[challengeTypes.js]: getJSTestRunner,
[challengeTypes.html]: getDOMTestRunner,
[challengeTypes.backend]: getDOMTestRunner,
[challengeTypes.pythonProject]: getDOMTestRunner
2019-02-08 17:33:05 +03:00
};
export function getTestRunner(buildData, runnerConfig, document) {
const { challengeType } = buildData;
const testRunner = testRunners[challengeType];
if (testRunner) {
return testRunner(buildData, runnerConfig, document);
}
throw new Error(`Cannot get test runner for challenge type ${challengeType}`);
2019-02-08 17:33:05 +03:00
}
function getJSTestRunner({ build, sources }, { proxyLogger, removeComments }) {
const code = {
contents: sources.index,
editableContents: sources.editableContents
};
2019-02-08 17:33:05 +03:00
const testWorker = createWorker(testEvaluator, { terminateWorker: true });
2019-02-08 17:33:05 +03:00
return (testString, testTimeout, firstTest = true) => {
return testWorker
.execute(
{ build, testString, code, sources, firstTest, removeComments },
testTimeout
)
.on('LOG', proxyLogger).done;
2019-02-08 17:33:05 +03:00
};
}
async function getDOMTestRunner(buildData, { proxyLogger }, document) {
2019-02-08 17:33:05 +03:00
await new Promise(resolve =>
createTestFramer(document, proxyLogger, resolve)(buildData)
2019-02-08 17:33:05 +03:00
);
return (testString, testTimeout) =>
runTestInTestFrame(document, testString, testTimeout);
}
export function buildDOMChallenge({
challengeFiles,
required = [],
template = ''
}) {
2018-12-29 11:34:03 +03:00
const finalRequires = [...globalRequires, ...required, ...frameRunner];
const loadEnzyme = challengeFiles.some(
challengeFile => challengeFile.ext === 'jsx'
);
2018-12-29 16:42:09 +03:00
const toHtml = [jsToHtml, cssToHtml];
const pipeLine = composeFunctions(...getTransformers(), ...toHtml);
const finalFiles = challengeFiles.map(pipeLine);
2019-01-09 03:23:17 +03:00
return Promise.all(finalFiles)
.then(checkFilesErrors)
.then(challengeFiles => ({
2019-02-08 17:33:05 +03:00
challengeType: challengeTypes.html,
build: concatHtml({ required: finalRequires, template, challengeFiles }),
sources: buildSourceMap(challengeFiles),
2019-02-08 17:33:05 +03:00
loadEnzyme
2019-01-09 03:23:17 +03:00
}));
}
export function buildJSChallenge({ challengeFiles }, options) {
const pipeLine = composeFunctions(...getTransformers(options));
const finalFiles = challengeFiles.map(pipeLine);
2019-01-09 03:23:17 +03:00
return Promise.all(finalFiles)
.then(checkFilesErrors)
.then(challengeFiles => ({
challengeType: challengeTypes.js,
build: challengeFiles
2019-01-09 03:23:17 +03:00
.reduce(
(body, challengeFile) => [
...body,
challengeFile.head,
challengeFile.contents,
challengeFile.tail
],
2019-01-09 03:23:17 +03:00
[]
)
.join('\n'),
sources: buildSourceMap(challengeFiles)
}));
2018-11-26 02:17:38 +03:00
}
2019-02-08 17:33:05 +03:00
export function buildBackendChallenge({ url }) {
return {
2019-02-08 17:33:05 +03:00
challengeType: challengeTypes.backend,
2019-01-09 03:23:17 +03:00
build: concatHtml({ required: frameRunner }),
sources: { url }
};
}
export function updatePreview(buildData, document, proxyLogger) {
if (buildData.challengeType === challengeTypes.html) {
createMainPreviewFramer(document, proxyLogger)(buildData);
} else {
throw new Error(
`Cannot show preview for challenge type ${buildData.challengeType}`
);
}
}
export function updateProjectPreview(buildData, document) {
if (buildData.challengeType === challengeTypes.html) {
createProjectPreviewFramer(document)(buildData);
} else {
throw new Error(
`Cannot show preview for challenge type ${buildData.challengeType}`
);
}
}
export function challengeHasPreview({ challengeType }) {
return (
challengeType === challengeTypes.html ||
challengeType === challengeTypes.modern
);
}
export function isJavaScriptChallenge({ challengeType }) {
return (
challengeType === challengeTypes.js ||
challengeType === challengeTypes.bonfire
);
}
export function isLoopProtected(challengeMeta) {
return challengeMeta.superBlock !== 'Coding Interview Prep';
}