diff --git a/common/app/routes/Challenges/Preview.jsx b/common/app/routes/Challenges/Preview.jsx index a69b225fc8..0ca2decdbb 100644 --- a/common/app/routes/Challenges/Preview.jsx +++ b/common/app/routes/Challenges/Preview.jsx @@ -1,4 +1,5 @@ -import React, { PropTypes, PureComponent } from 'react'; +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import ns from './ns.json'; diff --git a/common/app/routes/Challenges/rechallenge/transformers.js b/common/app/routes/Challenges/rechallenge/transformers.js index c0b3a9a94a..82c55e5141 100644 --- a/common/app/routes/Challenges/rechallenge/transformers.js +++ b/common/app/routes/Challenges/rechallenge/transformers.js @@ -1,7 +1,9 @@ import { + attempt, cond, flow, identity, + isError, matchesProperty, overEvery, overSome, @@ -35,7 +37,7 @@ const NBSPReg = new RegExp(String.fromCharCode(160), 'g'); const isJS = matchesProperty('ext', 'js'); const testHTMLJS = overSome(isJS, matchesProperty('ext', 'html')); -const testJS$JSX = overSome(isJS, matchesProperty('ext', 'jsx')); +export const testJS$JSX = overSome(isJS, matchesProperty('ext', 'jsx')); // work around the absence of multi-flile editing // this can be replaced with `matchesProperty('ext', 'sass')` @@ -105,13 +107,26 @@ export const replaceNBSP = cond([ [ stubTrue, identity ] ]); +function tryTransform(wrap = identity) { + return function transformWrappedPoly(source) { + const result = attempt(wrap, source); + if (isError(result)) { + const friendlyError = `${result}` + .match(/[\w\W]+?\n/)[0] + .replace(' unknown:', ''); + throw new Error(friendlyError); + } + return result; + }; +} + export const babelTransformer = cond([ [ testJS$JSX, flow( partial( vinyl.transformHeadTailAndContents, - babelTransformCode + tryTransform(babelTransformCode) ), partial(vinyl.setExt, 'js') ) diff --git a/common/app/routes/Challenges/redux/execute-challenge-epic.js b/common/app/routes/Challenges/redux/execute-challenge-epic.js index 10e24f9269..9913711ebd 100644 --- a/common/app/routes/Challenges/redux/execute-challenge-epic.js +++ b/common/app/routes/Challenges/redux/execute-challenge-epic.js @@ -12,7 +12,8 @@ import { codeLockedSelector, showPreviewSelector, - testsSelector + testsSelector, + disableJSOnError } from './'; import { buildFromFiles, @@ -26,7 +27,8 @@ import { import { createErrorObservable, - challengeSelector + challengeSelector, + doActionOnError } from '../../../redux'; @@ -55,13 +57,12 @@ export function updateMainEpic(actions, { getState }, { document }) { .flatMapLatest(() => buildFromFiles(getState(), true) .map(frameMain) .ignoreElements() - .catch(createErrorObservable) + .catch(doActionOnError(() => disableJSOnError())) ); return Observable.merge(buildAndFrameMain, proxyLogger.map(updateOutput)); }); } - export function executeChallengeEpic(actions, { getState }, { document }) { return Observable.of(document) // if document is not defined then none of this epic will run diff --git a/common/app/routes/Challenges/redux/index.js b/common/app/routes/Challenges/redux/index.js index 7d92931a69..86593feea9 100644 --- a/common/app/routes/Challenges/redux/index.js +++ b/common/app/routes/Challenges/redux/index.js @@ -77,6 +77,7 @@ export const types = createTypes([ 'checkChallenge', createAsyncTypes('submitChallenge'), 'moveToNextChallenge', + 'disableJSOnError', 'checkForNextBlock', // help @@ -153,6 +154,8 @@ export const submitChallengeComplete = createAction( export const moveToNextChallenge = createAction(types.moveToNextChallenge); export const checkForNextBlock = createAction(types.checkForNextBlock); +export const disableJSOnError = createAction(types.disableJSOnError); + // help export const openHelpModal = createAction(types.openHelpModal); export const closeHelpModal = createAction(types.closeHelpModal); @@ -308,6 +311,7 @@ export default combineReducers( [ combineActions( types.classicEditorUpdated, + types.disableJSOnError, types.modernEditorUpdated ) ]: state => ({ diff --git a/common/app/routes/Challenges/utils/build.js b/common/app/routes/Challenges/utils/build.js index a1b9d02424..61a196abf5 100644 --- a/common/app/routes/Challenges/utils/build.js +++ b/common/app/routes/Challenges/utils/build.js @@ -6,11 +6,13 @@ import throwers from '../rechallenge/throwers'; import { backendFormValuesSelector, challengeTemplateSelector, - challengeRequiredSelector + challengeRequiredSelector, + isJSEnabledSelector } from '../redux'; import { applyTransformers, - proxyLoggerTransformer + proxyLoggerTransformer, + testJS$JSX } from '../rechallenge/transformers'; import { cssToHtml, @@ -42,11 +44,25 @@ const globalRequires = [ jQuery ]; +function filterJSIfDisabled(state) { + const isJSEnabled = isJSEnabledSelector(state); + return file => { + if (testJS$JSX(file) && !isJSEnabled) { + return null; + } + return file; + }; +} + export function buildFromFiles(state, shouldProxyConsole) { const files = filesSelector(state); const required = challengeRequiredSelector(state); const finalRequires = [...globalRequires, ...required ]; - return createFileStream(files) + const requiredFiles = Object.keys(files) + .map(key => files[key]) + .filter(filterJSIfDisabled(state)) + .filter(Boolean); + return createFileStream(requiredFiles) ::pipe(throwers) ::pipe(applyTransformers) ::pipe(shouldProxyConsole ? proxyLoggerTransformer : identity) diff --git a/common/utils/polyvinyl.js b/common/utils/polyvinyl.js index 881b2709be..18e5b15ea7 100644 --- a/common/utils/polyvinyl.js +++ b/common/utils/polyvinyl.js @@ -5,11 +5,11 @@ import castToObservable from '../app/utils/cast-to-observable.js'; // createFileStream( -// files: Dictionary[Path, PolyVinyl] +// files: [...PolyVinyl] // ) => Observable[...Observable[...PolyVinyl]] -export function createFileStream(files = {}) { +export function createFileStream(files = []) { return Observable.of( - Observable.from(Object.keys(files).map(key => files[key])) + Observable.from(files) ); } @@ -116,7 +116,7 @@ export function setContent(contents, poly) { }; } -// setExt(contents: String, poly: PolyVinyl) => PolyVinyl +// setExt(ext: String, poly: PolyVinyl) => PolyVinyl export function setExt(ext, poly) { checkPoly(poly); const newPoly = { @@ -129,7 +129,7 @@ export function setExt(ext, poly) { return newPoly; } -// setName(contents: String, poly: PolyVinyl) => PolyVinyl +// setName(name: String, poly: PolyVinyl) => PolyVinyl export function setName(name, poly) { checkPoly(poly); const newPoly = { @@ -142,7 +142,7 @@ export function setName(name, poly) { return newPoly; } -// setError(contents: String, poly: PolyVinyl) => PolyVinyl +// setError(error: Object, poly: PolyVinyl) => PolyVinyl export function setError(error, poly) { invariant( typeof error === 'object', @@ -166,6 +166,7 @@ export function clearHeadTail(poly) { }; } +// appendToTail (tail: String, poly: PolyVinyl) => PolyVinyl export function appendToTail(tail, poly) { checkPoly(poly); return { @@ -174,7 +175,7 @@ export function appendToTail(tail, poly) { }; } -// compileHeadTail(contents: String, poly: PolyVinyl) => PolyVinyl +// compileHeadTail(padding: String, poly: PolyVinyl) => PolyVinyl export function compileHeadTail(padding = '', poly) { return clearHeadTail(transformContents( () => [ poly.head, poly.contents, poly.tail ].join(padding),