Files
freeCodeCamp/client/src/templates/Challenges/utils/build.js
Bruce B 7d817cb237 fix(a11y): add title attribute to iframes (#45014)
* fix:add title attribute to iframes
2022-02-08 11:22:54 +01:00

243 lines
7.2 KiB
JavaScript

// 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;
const frameRunner = [
{
src: `/js/${runner}.js`
}
];
const applyFunction = fn =>
async function (file) {
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) =>
fns.map(applyFunction).reduce((f, g) => x => f(x).then(g));
function buildSourceMap(challengeFiles) {
// TODO: rename sources.index to sources.contents.
const source = challengeFiles.reduce(
(sources, challengeFile) => {
sources.index += challengeFile.source || challengeFile.contents;
sources.editableContents += challengeFile.editableContents || '';
return sources;
},
{ index: '', editableContents: '' }
);
return source;
}
function checkFilesErrors(challengeFiles) {
const errors = challengeFiles
.filter(({ error }) => error)
.map(({ error }) => error);
if (errors.length) {
throw errors;
}
return challengeFiles;
}
const buildFunctions = {
[challengeTypes.js]: buildJSChallenge,
[challengeTypes.bonfire]: buildJSChallenge,
[challengeTypes.html]: buildDOMChallenge,
[challengeTypes.modern]: buildDOMChallenge,
[challengeTypes.backend]: buildBackendChallenge,
[challengeTypes.backEndProject]: buildBackendChallenge,
[challengeTypes.pythonProject]: buildBackendChallenge,
[challengeTypes.multiFileCertProject]: buildDOMChallenge
};
export function canBuildChallenge(challengeData) {
const { challengeType } = challengeData;
return Object.prototype.hasOwnProperty.call(buildFunctions, challengeType);
}
export async function buildChallenge(challengeData, options) {
const { challengeType } = challengeData;
let build = buildFunctions[challengeType];
if (build) {
return build(challengeData, options);
}
throw new Error(`Cannot build challenge of type ${challengeType}`);
}
const testRunners = {
[challengeTypes.js]: getJSTestRunner,
[challengeTypes.html]: getDOMTestRunner,
[challengeTypes.backend]: getDOMTestRunner,
[challengeTypes.pythonProject]: getDOMTestRunner,
[challengeTypes.multiFileCertProject]: getDOMTestRunner
};
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}`);
}
function getJSTestRunner({ build, sources }, { proxyLogger, removeComments }) {
const code = {
contents: sources.index,
editableContents: sources.editableContents
};
const testWorker = createWorker(testEvaluator, { terminateWorker: true });
return (testString, testTimeout, firstTest = true) => {
return testWorker
.execute(
{ build, testString, code, sources, firstTest, removeComments },
testTimeout
)
.on('LOG', proxyLogger).done;
};
}
async function getDOMTestRunner(buildData, { proxyLogger }, document) {
await new Promise(resolve =>
createTestFramer(document, proxyLogger, resolve)(buildData)
);
return (testString, testTimeout) =>
runTestInTestFrame(document, testString, testTimeout);
}
export function buildDOMChallenge(
{ challengeFiles, required = [], template = '' },
{ usesTestRunner } = { usesTestRunner: false }
) {
const finalRequires = [...required];
if (usesTestRunner) finalRequires.push(...frameRunner);
const loadEnzyme = challengeFiles.some(
challengeFile => challengeFile.ext === 'jsx'
);
const toHtml = [jsToHtml, cssToHtml];
const pipeLine = composeFunctions(...getTransformers(), ...toHtml);
const finalFiles = challengeFiles.map(pipeLine);
return Promise.all(finalFiles)
.then(checkFilesErrors)
.then(challengeFiles => ({
challengeType: challengeTypes.html,
build: concatHtml({
required: finalRequires,
template,
challengeFiles
}),
sources: buildSourceMap(challengeFiles),
loadEnzyme
}));
}
export function buildJSChallenge({ challengeFiles }, options) {
const pipeLine = composeFunctions(...getTransformers(options));
const finalFiles = challengeFiles.map(pipeLine);
return Promise.all(finalFiles)
.then(checkFilesErrors)
.then(challengeFiles => ({
challengeType: challengeTypes.js,
build: challengeFiles
.reduce(
(body, challengeFile) => [
...body,
challengeFile.head,
challengeFile.contents,
challengeFile.tail
],
[]
)
.join('\n'),
sources: buildSourceMap(challengeFiles)
}));
}
export function buildBackendChallenge({ url }) {
return {
challengeType: challengeTypes.backend,
build: concatHtml({ required: frameRunner }),
sources: { url }
};
}
export function updatePreview(buildData, document, proxyLogger) {
if (
buildData.challengeType === challengeTypes.html ||
buildData.challengeType === challengeTypes.multiFileCertProject
) {
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 ||
buildData.challengeType === challengeTypes.multiFileCertProject
) {
// Give iframe a title attribute for accessibility using the preview
// document's <title>.
const titleMatch = buildData?.sources?.index?.match(
/<title>(.*?)<\/title>/
);
const frameTitle = titleMatch ? titleMatch[1] + ' preview' : 'preview';
createProjectPreviewFramer(document, frameTitle)(buildData);
} else {
throw new Error(
`Cannot show preview for challenge type ${buildData.challengeType}`
);
}
}
export function challengeHasPreview({ challengeType }) {
return (
challengeType === challengeTypes.html ||
challengeType === challengeTypes.modern ||
challengeType === challengeTypes.multiFileCertProject
);
}
export function isJavaScriptChallenge({ challengeType }) {
return (
challengeType === challengeTypes.js ||
challengeType === challengeTypes.bonfire
);
}
export function isLoopProtected(challengeMeta) {
return challengeMeta.superBlock !== 'Coding Interview Prep';
}