feat: remove protection from interview prep (#38136)

The interview prep section includes many challenges that require long
running calculations which can be mistaken for infinite loops. This
removes the loop protection from those challenges, while the tests are
being evaluated.

It keeps the protection for the preview, since it is easy to create
broken code while working on a challenge and that should not crash the
site.
This commit is contained in:
Oliver Eyton-Williams 2020-02-04 06:03:56 +01:00 committed by GitHub
parent 8b692560e8
commit c6eb40ceef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 53 additions and 30 deletions

View File

@ -44,18 +44,22 @@ Babel.registerPlugin(
protect(testProtectTimeout, testLoopProtectCB, loopsPerTimeoutCheck) protect(testProtectTimeout, testLoopProtectCB, loopsPerTimeoutCheck)
); );
const babelOptionsJSBase = {
presets: [presetEnv]
};
const babelOptionsJSX = { const babelOptionsJSX = {
plugins: ['loopProtection'], plugins: ['loopProtection'],
presets: [presetEnv, presetReact] presets: [presetEnv, presetReact]
}; };
const babelOptionsJS = { const babelOptionsJS = {
plugins: ['testLoopProtection'], ...babelOptionsJSBase,
presets: [presetEnv] plugins: ['testLoopProtection']
}; };
const babelOptionsJSPreview = { const babelOptionsJSPreview = {
...babelOptionsJS, ...babelOptionsJSBase,
plugins: ['loopProtection'] plugins: ['loopProtection']
}; };
@ -94,16 +98,22 @@ function tryTransform(wrap = identity) {
}; };
} }
const babelTransformer = (preview = false) => const babelTransformer = ({ preview = false, protect = true }) => {
cond([ let options = babelOptionsJSBase;
// we always protect the preview, since it evaluates as the user types and
// they may briefly have infinite looping code accidentally
if (protect) {
options = preview ? babelOptionsJSPreview : babelOptionsJS;
} else {
options = preview ? babelOptionsJSPreview : options;
}
return cond([
[ [
testJS, testJS,
flow( flow(
partial( partial(
vinyl.transformHeadTailAndContents, vinyl.transformHeadTailAndContents,
tryTransform( tryTransform(babelTransformCode(options))
babelTransformCode(preview ? babelOptionsJSPreview : babelOptionsJS)
)
) )
) )
], ],
@ -119,6 +129,7 @@ const babelTransformer = (preview = false) =>
], ],
[stubTrue, identity] [stubTrue, identity]
]); ]);
};
const sassWorker = createWorker(sassCompile); const sassWorker = createWorker(sassCompile);
async function transformSASS(element) { async function transformSASS(element) {
@ -167,16 +178,9 @@ export const htmlTransformer = cond([
[stubTrue, identity] [stubTrue, identity]
]); ]);
export const transformers = [ export const getTransformers = config => [
replaceNBSP, replaceNBSP,
babelTransformer(), babelTransformer(config ? config : {}),
composeHTML,
htmlTransformer
];
export const transformersPreview = [
replaceNBSP,
babelTransformer(true),
composeHTML, composeHTML,
htmlTransformer htmlTransformer
]; ];

View File

@ -15,6 +15,7 @@ import escape from 'lodash/escape';
import { import {
challengeDataSelector, challengeDataSelector,
challengeMetaSelector,
challengeTestsSelector, challengeTestsSelector,
initConsole, initConsole,
updateConsole, updateConsole,
@ -33,7 +34,8 @@ import {
getTestRunner, getTestRunner,
challengeHasPreview, challengeHasPreview,
updatePreview, updatePreview,
isJavaScriptChallenge isJavaScriptChallenge,
isLoopProtected
} from '../utils/build'; } from '../utils/build';
// How long before bailing out of a preview. // How long before bailing out of a preview.
@ -67,7 +69,12 @@ export function* executeChallengeSaga() {
const proxyLogger = args => consoleProxy.put(args); const proxyLogger = args => consoleProxy.put(args);
const challengeData = yield select(challengeDataSelector); const challengeData = yield select(challengeDataSelector);
const buildData = yield buildChallengeData(challengeData); const challengeMeta = yield select(challengeMetaSelector);
const protect = isLoopProtected(challengeMeta);
const buildData = yield buildChallengeData(challengeData, {
preview: false,
protect
});
const document = yield getContext('document'); const document = yield getContext('document');
const testRunner = yield call( const testRunner = yield call(
getTestRunner, getTestRunner,
@ -103,9 +110,9 @@ function* takeEveryConsole(channel) {
}); });
} }
function* buildChallengeData(challengeData, preview) { function* buildChallengeData(challengeData, options) {
try { try {
return yield call(buildChallenge, challengeData, preview); return yield call(buildChallenge, challengeData, options);
} catch (e) { } catch (e) {
yield put(disableBuildOnError()); yield put(disableBuildOnError());
throw e; throw e;
@ -167,8 +174,14 @@ function* previewChallengeSaga() {
yield fork(takeEveryConsole, logProxy); yield fork(takeEveryConsole, logProxy);
const challengeData = yield select(challengeDataSelector); const challengeData = yield select(challengeDataSelector);
if (canBuildChallenge(challengeData)) { if (canBuildChallenge(challengeData)) {
const buildData = yield buildChallengeData(challengeData, true); const challengeMeta = yield select(challengeMetaSelector);
const protect = isLoopProtected(challengeMeta);
const buildData = yield buildChallengeData(challengeData, {
preview: true,
protect
});
// evaluate the user code in the preview frame or in the worker // evaluate the user code in the preview frame or in the worker
if (challengeHasPreview(challengeData)) { if (challengeHasPreview(challengeData)) {
const document = yield getContext('document'); const document = yield getContext('document');

View File

@ -21,6 +21,7 @@ const initialState = {
canFocusEditor: true, canFocusEditor: true,
challengeFiles: {}, challengeFiles: {},
challengeMeta: { challengeMeta: {
superBlock: '',
block: '', block: '',
id: '', id: '',
nextChallengePath: '/', nextChallengePath: '/',

View File

@ -1,4 +1,4 @@
import { transformers, transformersPreview } from '../rechallenge/transformers'; import { getTransformers } from '../rechallenge/transformers';
import { cssToHtml, jsToHtml, concatHtml } from '../rechallenge/builders.js'; import { cssToHtml, jsToHtml, concatHtml } from '../rechallenge/builders.js';
import { challengeTypes } from '../../../../utils/challengeTypes'; import { challengeTypes } from '../../../../utils/challengeTypes';
import createWorker from './worker-executor'; import createWorker from './worker-executor';
@ -76,11 +76,11 @@ export function canBuildChallenge(challengeData) {
return buildFunctions.hasOwnProperty(challengeType); return buildFunctions.hasOwnProperty(challengeType);
} }
export async function buildChallenge(challengeData, preview = false) { export async function buildChallenge(challengeData, options) {
const { challengeType } = challengeData; const { challengeType } = challengeData;
let build = buildFunctions[challengeType]; let build = buildFunctions[challengeType];
if (build) { if (build) {
return build(challengeData, preview); return build(challengeData, options);
} }
throw new Error(`Cannot build challenge of type ${challengeType}`); throw new Error(`Cannot build challenge of type ${challengeType}`);
} }
@ -123,7 +123,7 @@ export function buildDOMChallenge({ files, required = [], template = '' }) {
const finalRequires = [...globalRequires, ...required, ...frameRunner]; const finalRequires = [...globalRequires, ...required, ...frameRunner];
const loadEnzyme = Object.keys(files).some(key => files[key].ext === 'jsx'); const loadEnzyme = Object.keys(files).some(key => files[key].ext === 'jsx');
const toHtml = [jsToHtml, cssToHtml]; const toHtml = [jsToHtml, cssToHtml];
const pipeLine = composeFunctions(...transformers, ...toHtml); const pipeLine = composeFunctions(...getTransformers(), ...toHtml);
const finalFiles = Object.keys(files) const finalFiles = Object.keys(files)
.map(key => files[key]) .map(key => files[key])
.map(pipeLine); .map(pipeLine);
@ -137,10 +137,9 @@ export function buildDOMChallenge({ files, required = [], template = '' }) {
})); }));
} }
export function buildJSChallenge({ files }, preview = false) { export function buildJSChallenge({ files }, options) {
const pipeLine = preview const pipeLine = composeFunctions(...getTransformers(options));
? composeFunctions(...transformersPreview)
: composeFunctions(...transformers);
const finalFiles = Object.keys(files) const finalFiles = Object.keys(files)
.map(key => files[key]) .map(key => files[key])
.map(pipeLine); .map(pipeLine);
@ -191,3 +190,7 @@ export function isJavaScriptChallenge({ challengeType }) {
challengeType === challengeTypes.bonfire challengeType === challengeTypes.bonfire
); );
} }
export function isLoopProtected(challengeMeta) {
return challengeMeta.superBlock !== 'Coding Interview Prep';
}

View File

@ -61,6 +61,7 @@ const getIntroIfRequired = (node, index, nodeArray) => {
exports.createChallengePages = createPage => ({ node }, index, thisArray) => { exports.createChallengePages = createPage => ({ node }, index, thisArray) => {
const { const {
superBlock,
block, block,
fields: { slug }, fields: { slug },
required = [], required = [],
@ -77,6 +78,7 @@ exports.createChallengePages = createPage => ({ node }, index, thisArray) => {
component: getTemplateComponent(challengeType), component: getTemplateComponent(challengeType),
context: { context: {
challengeMeta: { challengeMeta: {
superBlock,
block: block, block: block,
introPath: getIntroIfRequired(node, index, thisArray), introPath: getIntroIfRequired(node, index, thisArray),
template, template,