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,