From c6eb40ceeffcbfa1ff92d09a1d78e08cfc92603d Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Tue, 4 Feb 2020 06:03:56 +0100 Subject: [PATCH] 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. --- .../Challenges/rechallenge/transformers.js | 38 ++++++++++--------- .../redux/execute-challenge-saga.js | 23 ++++++++--- .../src/templates/Challenges/redux/index.js | 1 + .../src/templates/Challenges/utils/build.js | 19 ++++++---- client/utils/gatsby/challengePageCreator.js | 2 + 5 files changed, 53 insertions(+), 30 deletions(-) diff --git a/client/src/templates/Challenges/rechallenge/transformers.js b/client/src/templates/Challenges/rechallenge/transformers.js index a47a1e4f0d..e0774287b9 100644 --- a/client/src/templates/Challenges/rechallenge/transformers.js +++ b/client/src/templates/Challenges/rechallenge/transformers.js @@ -44,18 +44,22 @@ Babel.registerPlugin( protect(testProtectTimeout, testLoopProtectCB, loopsPerTimeoutCheck) ); +const babelOptionsJSBase = { + presets: [presetEnv] +}; + const babelOptionsJSX = { plugins: ['loopProtection'], presets: [presetEnv, presetReact] }; const babelOptionsJS = { - plugins: ['testLoopProtection'], - presets: [presetEnv] + ...babelOptionsJSBase, + plugins: ['testLoopProtection'] }; const babelOptionsJSPreview = { - ...babelOptionsJS, + ...babelOptionsJSBase, plugins: ['loopProtection'] }; @@ -94,16 +98,22 @@ function tryTransform(wrap = identity) { }; } -const babelTransformer = (preview = false) => - cond([ +const babelTransformer = ({ preview = false, protect = true }) => { + 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, flow( partial( vinyl.transformHeadTailAndContents, - tryTransform( - babelTransformCode(preview ? babelOptionsJSPreview : babelOptionsJS) - ) + tryTransform(babelTransformCode(options)) ) ) ], @@ -119,6 +129,7 @@ const babelTransformer = (preview = false) => ], [stubTrue, identity] ]); +}; const sassWorker = createWorker(sassCompile); async function transformSASS(element) { @@ -167,16 +178,9 @@ export const htmlTransformer = cond([ [stubTrue, identity] ]); -export const transformers = [ +export const getTransformers = config => [ replaceNBSP, - babelTransformer(), - composeHTML, - htmlTransformer -]; - -export const transformersPreview = [ - replaceNBSP, - babelTransformer(true), + babelTransformer(config ? config : {}), composeHTML, htmlTransformer ]; diff --git a/client/src/templates/Challenges/redux/execute-challenge-saga.js b/client/src/templates/Challenges/redux/execute-challenge-saga.js index b5048181f1..d3dac046ad 100644 --- a/client/src/templates/Challenges/redux/execute-challenge-saga.js +++ b/client/src/templates/Challenges/redux/execute-challenge-saga.js @@ -15,6 +15,7 @@ import escape from 'lodash/escape'; import { challengeDataSelector, + challengeMetaSelector, challengeTestsSelector, initConsole, updateConsole, @@ -33,7 +34,8 @@ import { getTestRunner, challengeHasPreview, updatePreview, - isJavaScriptChallenge + isJavaScriptChallenge, + isLoopProtected } from '../utils/build'; // How long before bailing out of a preview. @@ -67,7 +69,12 @@ export function* executeChallengeSaga() { const proxyLogger = args => consoleProxy.put(args); 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 testRunner = yield call( getTestRunner, @@ -103,9 +110,9 @@ function* takeEveryConsole(channel) { }); } -function* buildChallengeData(challengeData, preview) { +function* buildChallengeData(challengeData, options) { try { - return yield call(buildChallenge, challengeData, preview); + return yield call(buildChallenge, challengeData, options); } catch (e) { yield put(disableBuildOnError()); throw e; @@ -167,8 +174,14 @@ function* previewChallengeSaga() { yield fork(takeEveryConsole, logProxy); const challengeData = yield select(challengeDataSelector); + 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 if (challengeHasPreview(challengeData)) { const document = yield getContext('document'); diff --git a/client/src/templates/Challenges/redux/index.js b/client/src/templates/Challenges/redux/index.js index e8b1ff0444..8cb92f4a43 100644 --- a/client/src/templates/Challenges/redux/index.js +++ b/client/src/templates/Challenges/redux/index.js @@ -21,6 +21,7 @@ const initialState = { canFocusEditor: true, challengeFiles: {}, challengeMeta: { + superBlock: '', block: '', id: '', nextChallengePath: '/', diff --git a/client/src/templates/Challenges/utils/build.js b/client/src/templates/Challenges/utils/build.js index 0121cd957f..99304b35ee 100644 --- a/client/src/templates/Challenges/utils/build.js +++ b/client/src/templates/Challenges/utils/build.js @@ -1,4 +1,4 @@ -import { transformers, transformersPreview } from '../rechallenge/transformers'; +import { getTransformers } from '../rechallenge/transformers'; import { cssToHtml, jsToHtml, concatHtml } from '../rechallenge/builders.js'; import { challengeTypes } from '../../../../utils/challengeTypes'; import createWorker from './worker-executor'; @@ -76,11 +76,11 @@ export function canBuildChallenge(challengeData) { return buildFunctions.hasOwnProperty(challengeType); } -export async function buildChallenge(challengeData, preview = false) { +export async function buildChallenge(challengeData, options) { const { challengeType } = challengeData; let build = buildFunctions[challengeType]; if (build) { - return build(challengeData, preview); + return build(challengeData, options); } 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 loadEnzyme = Object.keys(files).some(key => files[key].ext === 'jsx'); const toHtml = [jsToHtml, cssToHtml]; - const pipeLine = composeFunctions(...transformers, ...toHtml); + const pipeLine = composeFunctions(...getTransformers(), ...toHtml); const finalFiles = Object.keys(files) .map(key => files[key]) .map(pipeLine); @@ -137,10 +137,9 @@ export function buildDOMChallenge({ files, required = [], template = '' }) { })); } -export function buildJSChallenge({ files }, preview = false) { - const pipeLine = preview - ? composeFunctions(...transformersPreview) - : composeFunctions(...transformers); +export function buildJSChallenge({ files }, options) { + const pipeLine = composeFunctions(...getTransformers(options)); + const finalFiles = Object.keys(files) .map(key => files[key]) .map(pipeLine); @@ -191,3 +190,7 @@ export function isJavaScriptChallenge({ challengeType }) { challengeType === challengeTypes.bonfire ); } + +export function isLoopProtected(challengeMeta) { + return challengeMeta.superBlock !== 'Coding Interview Prep'; +} diff --git a/client/utils/gatsby/challengePageCreator.js b/client/utils/gatsby/challengePageCreator.js index 651f2399e9..9685196bb5 100644 --- a/client/utils/gatsby/challengePageCreator.js +++ b/client/utils/gatsby/challengePageCreator.js @@ -61,6 +61,7 @@ const getIntroIfRequired = (node, index, nodeArray) => { exports.createChallengePages = createPage => ({ node }, index, thisArray) => { const { + superBlock, block, fields: { slug }, required = [], @@ -77,6 +78,7 @@ exports.createChallengePages = createPage => ({ node }, index, thisArray) => { component: getTemplateComponent(challengeType), context: { challengeMeta: { + superBlock, block: block, introPath: getIntroIfRequired(node, index, thisArray), template,