2021-08-02 15:39:40 +02:00
|
|
|
// 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';
|
2021-08-09 01:30:31 -07:00
|
|
|
import { challengeTypes } from '../../../../utils/challenge-types';
|
2021-08-02 15:39:40 +02:00
|
|
|
import { cssToHtml, jsToHtml, concatHtml } from '../rechallenge/builders.js';
|
|
|
|
import { getTransformers } from '../rechallenge/transformers';
|
2019-02-13 15:47:00 +03:00
|
|
|
import {
|
|
|
|
createTestFramer,
|
|
|
|
runTestInTestFrame,
|
2021-11-29 19:30:28 +01:00
|
|
|
createMainPreviewFramer,
|
|
|
|
createProjectPreviewFramer
|
2019-02-13 15:47:00 +03:00
|
|
|
} from './frame';
|
2021-08-02 15:39:40 +02:00
|
|
|
import createWorker from './worker-executor';
|
2021-03-26 00:43:43 +05:30
|
|
|
|
|
|
|
const { filename: runner } = frameRunnerData;
|
|
|
|
const { filename: testEvaluator } = testEvaluatorData;
|
2019-11-14 21:13:44 +01:00
|
|
|
|
2018-12-29 11:34:03 +03:00
|
|
|
const frameRunner = [
|
|
|
|
{
|
2019-11-14 21:13:44 +01:00
|
|
|
src: `/js/${runner}.js`
|
2018-12-29 11:34:03 +03:00
|
|
|
}
|
|
|
|
];
|
2018-09-30 11:37:19 +01: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 =>
|
2021-03-11 00:31:46 +05:30
|
|
|
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;
|
2019-01-15 17:18:56 +03:00
|
|
|
} 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));
|
2018-09-30 11:37:19 +01:00
|
|
|
|
2021-08-12 19:48:28 +01:00
|
|
|
function buildSourceMap(challengeFiles) {
|
2020-05-28 17:21:26 +02:00
|
|
|
// 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
|
2020-07-15 11:28:20 +02:00
|
|
|
// setting the name to 'index'. Use 'contents' instead?
|
|
|
|
// TODO: is file.source ever defined?
|
2021-08-12 19:48:28 +01:00
|
|
|
const source = challengeFiles.reduce(
|
|
|
|
(sources, challengeFile) => {
|
|
|
|
sources[challengeFile.name] +=
|
|
|
|
challengeFile.source || challengeFile.contents;
|
|
|
|
sources.editableContents += challengeFile.editableContents || '';
|
2020-05-28 17:21:26 +02:00
|
|
|
return sources;
|
|
|
|
},
|
2020-07-15 11:28:20 +02:00
|
|
|
{ index: '', editableContents: '' }
|
2020-05-28 17:21:26 +02:00
|
|
|
);
|
2021-08-12 19:48:28 +01:00
|
|
|
return source;
|
2018-12-29 11:34:03 +03:00
|
|
|
}
|
|
|
|
|
2021-08-12 19:48:28 +01: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;
|
|
|
|
}
|
2021-08-12 19:48:28 +01:00
|
|
|
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,
|
2019-06-11 18:46:36 +03:00
|
|
|
[challengeTypes.backend]: buildBackendChallenge,
|
2020-02-25 00:10:32 +05:30
|
|
|
[challengeTypes.backEndProject]: buildBackendChallenge,
|
|
|
|
[challengeTypes.pythonProject]: buildBackendChallenge
|
2019-02-08 17:33:05 +03:00
|
|
|
};
|
|
|
|
|
2019-11-19 12:46:48 +01:00
|
|
|
export function canBuildChallenge(challengeData) {
|
|
|
|
const { challengeType } = challengeData;
|
|
|
|
return buildFunctions.hasOwnProperty(challengeType);
|
|
|
|
}
|
|
|
|
|
2020-02-04 06:03:56 +01:00
|
|
|
export async function buildChallenge(challengeData, options) {
|
2019-02-08 17:33:05 +03:00
|
|
|
const { challengeType } = challengeData;
|
|
|
|
let build = buildFunctions[challengeType];
|
|
|
|
if (build) {
|
2020-02-04 06:03:56 +01:00
|
|
|
return build(challengeData, options);
|
2019-02-08 17:33:05 +03:00
|
|
|
}
|
2019-02-13 15:47:00 +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,
|
2020-02-25 00:10:32 +05:30
|
|
|
[challengeTypes.backend]: getDOMTestRunner,
|
|
|
|
[challengeTypes.pythonProject]: getDOMTestRunner
|
2019-02-08 17:33:05 +03:00
|
|
|
};
|
2021-04-30 21:30:06 +02:00
|
|
|
export function getTestRunner(buildData, runnerConfig, document) {
|
2019-02-13 15:47:00 +03:00
|
|
|
const { challengeType } = buildData;
|
|
|
|
const testRunner = testRunners[challengeType];
|
|
|
|
if (testRunner) {
|
2021-04-30 21:30:06 +02:00
|
|
|
return testRunner(buildData, runnerConfig, document);
|
2019-02-13 15:47:00 +03:00
|
|
|
}
|
|
|
|
throw new Error(`Cannot get test runner for challenge type ${challengeType}`);
|
2019-02-08 17:33:05 +03:00
|
|
|
}
|
|
|
|
|
2021-04-30 21:30:06 +02:00
|
|
|
function getJSTestRunner({ build, sources }, { proxyLogger, removeComments }) {
|
2020-07-15 11:28:20 +02:00
|
|
|
const code = {
|
|
|
|
contents: sources.index,
|
|
|
|
editableContents: sources.editableContents
|
|
|
|
};
|
2019-02-08 17:33:05 +03:00
|
|
|
|
2019-11-14 21:13:44 +01:00
|
|
|
const testWorker = createWorker(testEvaluator, { terminateWorker: true });
|
2019-02-08 17:33:05 +03:00
|
|
|
|
2019-11-04 12:42:19 +01:00
|
|
|
return (testString, testTimeout, firstTest = true) => {
|
2019-03-14 12:08:15 +03:00
|
|
|
return testWorker
|
2021-04-30 21:30:06 +02:00
|
|
|
.execute(
|
|
|
|
{ build, testString, code, sources, firstTest, removeComments },
|
|
|
|
testTimeout
|
|
|
|
)
|
2019-03-14 12:08:15 +03:00
|
|
|
.on('LOG', proxyLogger).done;
|
2019-02-08 17:33:05 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-04-30 21:30:06 +02:00
|
|
|
async function getDOMTestRunner(buildData, { proxyLogger }, document) {
|
2019-02-08 17:33:05 +03:00
|
|
|
await new Promise(resolve =>
|
2021-11-29 19:30:28 +01:00
|
|
|
createTestFramer(document, proxyLogger, resolve)(buildData)
|
2019-02-08 17:33:05 +03:00
|
|
|
);
|
|
|
|
return (testString, testTimeout) =>
|
|
|
|
runTestInTestFrame(document, testString, testTimeout);
|
|
|
|
}
|
|
|
|
|
2021-08-12 19:48:28 +01:00
|
|
|
export function buildDOMChallenge({
|
|
|
|
challengeFiles,
|
|
|
|
required = [],
|
|
|
|
template = ''
|
|
|
|
}) {
|
2018-12-29 11:34:03 +03:00
|
|
|
const finalRequires = [...globalRequires, ...required, ...frameRunner];
|
2021-08-12 19:48:28 +01:00
|
|
|
const loadEnzyme = challengeFiles.some(
|
|
|
|
challengeFile => challengeFile.ext === 'jsx'
|
|
|
|
);
|
2018-12-29 16:42:09 +03:00
|
|
|
const toHtml = [jsToHtml, cssToHtml];
|
2020-02-04 06:03:56 +01:00
|
|
|
const pipeLine = composeFunctions(...getTransformers(), ...toHtml);
|
2021-08-12 19:48:28 +01:00
|
|
|
const finalFiles = challengeFiles.map(pipeLine);
|
2019-01-09 03:23:17 +03:00
|
|
|
return Promise.all(finalFiles)
|
|
|
|
.then(checkFilesErrors)
|
2021-08-12 19:48:28 +01:00
|
|
|
.then(challengeFiles => ({
|
2019-02-08 17:33:05 +03:00
|
|
|
challengeType: challengeTypes.html,
|
2021-08-12 19:48:28 +01:00
|
|
|
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
|
|
|
}));
|
2018-09-30 11:37:19 +01:00
|
|
|
}
|
|
|
|
|
2021-08-12 19:48:28 +01:00
|
|
|
export function buildJSChallenge({ challengeFiles }, options) {
|
2020-02-04 06:03:56 +01:00
|
|
|
const pipeLine = composeFunctions(...getTransformers(options));
|
|
|
|
|
2021-08-12 19:48:28 +01:00
|
|
|
const finalFiles = challengeFiles.map(pipeLine);
|
2019-01-09 03:23:17 +03:00
|
|
|
return Promise.all(finalFiles)
|
|
|
|
.then(checkFilesErrors)
|
2021-08-12 19:48:28 +01:00
|
|
|
.then(challengeFiles => ({
|
2021-04-30 21:30:06 +02:00
|
|
|
challengeType: challengeTypes.js,
|
2021-08-12 19:48:28 +01:00
|
|
|
build: challengeFiles
|
2019-01-09 03:23:17 +03:00
|
|
|
.reduce(
|
2021-08-12 19:48:28 +01:00
|
|
|
(body, challengeFile) => [
|
|
|
|
...body,
|
|
|
|
challengeFile.head,
|
|
|
|
challengeFile.contents,
|
|
|
|
challengeFile.tail
|
|
|
|
],
|
2019-01-09 03:23:17 +03:00
|
|
|
[]
|
|
|
|
)
|
2021-04-30 21:30:06 +02:00
|
|
|
.join('\n'),
|
2021-08-12 19:48:28 +01:00
|
|
|
sources: buildSourceMap(challengeFiles)
|
2021-04-30 21:30:06 +02:00
|
|
|
}));
|
2018-11-26 02:17:38 +03:00
|
|
|
}
|
|
|
|
|
2019-02-08 17:33:05 +03:00
|
|
|
export function buildBackendChallenge({ url }) {
|
2018-12-10 10:00:26 +03:00
|
|
|
return {
|
2019-02-08 17:33:05 +03:00
|
|
|
challengeType: challengeTypes.backend,
|
2019-01-09 03:23:17 +03:00
|
|
|
build: concatHtml({ required: frameRunner }),
|
2018-12-10 10:00:26 +03:00
|
|
|
sources: { url }
|
|
|
|
};
|
2018-09-30 11:37:19 +01:00
|
|
|
}
|
2019-02-13 15:47:00 +03:00
|
|
|
|
2021-11-29 19:30:28 +01:00
|
|
|
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}`
|
2019-11-01 13:02:23 +01:00
|
|
|
);
|
2021-11-29 19:30:28 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function updateProjectPreview(buildData, document) {
|
|
|
|
if (buildData.challengeType === challengeTypes.html) {
|
|
|
|
createProjectPreviewFramer(document)(buildData);
|
2019-02-13 15:47:00 +03:00
|
|
|
} else {
|
2021-11-29 19:30:28 +01:00
|
|
|
throw new Error(
|
|
|
|
`Cannot show preview for challenge type ${buildData.challengeType}`
|
|
|
|
);
|
2019-02-13 15:47:00 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function challengeHasPreview({ challengeType }) {
|
|
|
|
return (
|
|
|
|
challengeType === challengeTypes.html ||
|
|
|
|
challengeType === challengeTypes.modern
|
|
|
|
);
|
|
|
|
}
|
2019-11-02 12:03:47 +01:00
|
|
|
|
|
|
|
export function isJavaScriptChallenge({ challengeType }) {
|
2019-11-04 10:43:32 +01:00
|
|
|
return (
|
|
|
|
challengeType === challengeTypes.js ||
|
|
|
|
challengeType === challengeTypes.bonfire
|
|
|
|
);
|
2019-11-02 12:03:47 +01:00
|
|
|
}
|
2020-02-04 06:03:56 +01:00
|
|
|
|
|
|
|
export function isLoopProtected(challengeMeta) {
|
|
|
|
return challengeMeta.superBlock !== 'Coding Interview Prep';
|
|
|
|
}
|