From 0807a584c4294266bb673a9c9d3717bc9adc9072 Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Wed, 20 Oct 2021 13:07:00 +0200 Subject: [PATCH] feat: make project preview data available for frame --- client/gatsby-node.js | 7 +++ .../Challenges/redux/action-types.js | 1 + .../redux/execute-challenge-saga.js | 38 ++++++++++- .../src/templates/Challenges/redux/index.js | 8 ++- .../src/templates/Challenges/utils/build.js | 25 ++++++-- client/utils/gatsby/challenge-page-creator.js | 63 ++++++++++++++----- 6 files changed, 120 insertions(+), 22 deletions(-) diff --git a/client/gatsby-node.js b/client/gatsby-node.js index 753cc90e97..f6f4bbfac3 100644 --- a/client/gatsby-node.js +++ b/client/gatsby-node.js @@ -87,6 +87,13 @@ exports.createPages = function createPages({ graphql, actions, reporter }) { src } challengeOrder + challengeFiles { + name + ext + contents + head + tail + } solutions { contents ext diff --git a/client/src/templates/Challenges/redux/action-types.js b/client/src/templates/Challenges/redux/action-types.js index d938afde03..5b37a40cfc 100644 --- a/client/src/templates/Challenges/redux/action-types.js +++ b/client/src/templates/Challenges/redux/action-types.js @@ -32,6 +32,7 @@ export const actionTypes = createTypes( 'openModal', 'previewMounted', + 'projectPreviewMounted', 'challengeMounted', 'checkChallenge', 'executeChallenge', diff --git a/client/src/templates/Challenges/redux/execute-challenge-saga.js b/client/src/templates/Challenges/redux/execute-challenge-saga.js index c6161a14ff..e7d0438833 100644 --- a/client/src/templates/Challenges/redux/execute-challenge-saga.js +++ b/client/src/templates/Challenges/redux/execute-challenge-saga.js @@ -236,6 +236,41 @@ function* previewChallengeSaga({ flushLogs = true } = {}) { } } +// TODO: DRY this and previewChallengeSaga +// TODO: remove everything about proxyLogger here +function* previewProjectSolutionSaga({ payload }) { + if (!payload) return; + const { isFirstChallengeInBlock, challengeData } = payload; + if (!isFirstChallengeInBlock) return; + + const logProxy = yield channel(); + const proxyLogger = args => logProxy.put(args); + + try { + yield fork(takeEveryConsole, logProxy); + if (canBuildChallenge(challengeData)) { + const challengeMeta = yield select(challengeMetaSelector); + const protect = isLoopProtected(challengeMeta); + const buildData = yield buildChallengeData(challengeData, { + preview: true, + protect + }); + if (challengeHasPreview(challengeData)) { + const document = yield getContext('document'); + yield call(updatePreview, buildData, document, proxyLogger, true); + } + } + } catch (err) { + if (err === 'timeout') { + // TODO: translate the error + // eslint-disable-next-line no-ex-assign + err = `The code you have written is taking longer than the ${previewTimeout}ms our challenges allow. You may have created an infinite loop or need to write a more efficient algorithm`; + } + console.log(err); + yield put(updateConsole(escape(err))); + } +} + export function createExecuteChallengeSaga(types) { return [ takeLatest(types.executeChallenge, executeCancellableChallengeSaga), @@ -247,6 +282,7 @@ export function createExecuteChallengeSaga(types) { types.resetChallenge ], executeCancellablePreviewSaga - ) + ), + takeLatest(types.projectPreviewMounted, previewProjectSolutionSaga) ]; } diff --git a/client/src/templates/Challenges/redux/index.js b/client/src/templates/Challenges/redux/index.js index 8639edbc99..1030aaba4e 100644 --- a/client/src/templates/Challenges/redux/index.js +++ b/client/src/templates/Challenges/redux/index.js @@ -39,7 +39,8 @@ const initialState = { completion: false, help: false, video: false, - reset: false + reset: false, + projectPreview: false }, projectFormValues: {}, successMessage: 'Happy Coding!' @@ -114,6 +115,9 @@ export const closeModal = createAction(actionTypes.closeModal); export const openModal = createAction(actionTypes.openModal); export const previewMounted = createAction(actionTypes.previewMounted); +export const projectPreviewMounted = createAction( + actionTypes.projectPreviewMounted +); export const challengeMounted = createAction(actionTypes.challengeMounted); export const checkChallenge = createAction(actionTypes.checkChallenge); export const executeChallenge = createAction(actionTypes.executeChallenge); @@ -148,6 +152,8 @@ export const isCompletionModalOpenSelector = state => export const isHelpModalOpenSelector = state => state[ns].modal.help; export const isVideoModalOpenSelector = state => state[ns].modal.video; export const isResetModalOpenSelector = state => state[ns].modal.reset; +export const isProjectPreviewModalOpenSelector = state => + state[ns].modal.projectPreview; export const isResettingSelector = state => state[ns].isResetting; export const isBuildEnabledSelector = state => state[ns].isBuildEnabled; diff --git a/client/src/templates/Challenges/utils/build.js b/client/src/templates/Challenges/utils/build.js index 038d0303e5..85a79a34e3 100644 --- a/client/src/templates/Challenges/utils/build.js +++ b/client/src/templates/Challenges/utils/build.js @@ -9,7 +9,8 @@ import { getTransformers } from '../rechallenge/transformers'; import { createTestFramer, runTestInTestFrame, - createMainFramer + createMainPreviewFramer, + createProjectPreviewFramer } from './frame'; import createWorker from './worker-executor'; @@ -197,13 +198,27 @@ export function buildBackendChallenge({ url }) { }; } -export async function updatePreview(buildData, document, proxyLogger) { +// TODO: use fewer arguments (and no boolean ones!) +export async function updatePreview( + buildData, + document, + proxyLogger, + previewProjectSolution +) { const { challengeType } = buildData; + // TODO: no need to await here, nothing is waiting for the preview to be + // visible if (challengeType === challengeTypes.html) { - await new Promise(resolve => - createMainFramer(document, resolve, proxyLogger)(buildData) - ); + if (previewProjectSolution) { + await new Promise(resolve => + createProjectPreviewFramer(document, resolve, proxyLogger)(buildData) + ); + } else { + await new Promise(resolve => + createMainPreviewFramer(document, resolve, proxyLogger)(buildData) + ); + } } else { throw new Error(`Cannot show preview for challenge type ${challengeType}`); } diff --git a/client/utils/gatsby/challenge-page-creator.js b/client/utils/gatsby/challenge-page-creator.js index aa1325fd7f..d3d328613a 100644 --- a/client/utils/gatsby/challenge-page-creator.js +++ b/client/utils/gatsby/challenge-page-creator.js @@ -1,6 +1,7 @@ const path = require('path'); +const { createPoly } = require('../../../utils/polyvinyl'); const { dasherize } = require('../../../utils/slugs'); - +const { sortChallengeFiles } = require('../../../utils/sort-challengefiles'); const { viewTypes } = require('../challenge-types'); const backend = path.resolve( @@ -57,7 +58,7 @@ function getTemplateComponent(challengeType) { } exports.createChallengePages = function (createPage) { - return function ({ node }, index, allChallenges) { + return function ({ node: challenge }, index, allChallengeEdges) { const { superBlock, block, @@ -65,17 +66,11 @@ exports.createChallengePages = function (createPage) { required = [], template, challengeType, - id, - challengeOrder - } = node; + id + } = challenge; // TODO: challengeType === 7 and isPrivate are the same, right? If so, we // should remove one of them. - const challengesInBlock = allChallenges.filter( - ({ node }) => node.block === block - ); - const allSolutionsToLastChallenge = - challengesInBlock[challengesInBlock.length - 1].node.solutions; createPage({ path: slug, component: getTemplateComponent(challengeType), @@ -85,18 +80,56 @@ exports.createChallengePages = function (createPage) { block, template, required, - nextChallengePath: getNextChallengePath(node, index, allChallenges), - prevChallengePath: getPrevChallengePath(node, index, allChallenges), - id, - isFirstChallengeInBlock: challengeOrder === 0, - solutionToLastChallenge: allSolutionsToLastChallenge[0] + nextChallengePath: getNextChallengePath( + challenge, + index, + allChallengeEdges + ), + prevChallengePath: getPrevChallengePath( + challenge, + index, + allChallengeEdges + ), + id }, + projectPreview: getProjectPreviewConfig(challenge, allChallengeEdges), slug } }); }; }; +function getProjectPreviewConfig(challenge, allChallengeEdges) { + const { block, challengeOrder } = challenge; + + const challengesInBlock = allChallengeEdges + .filter(({ node }) => node.block === block) + .map(({ node }) => node); + const lastChallenge = challengesInBlock[challengesInBlock.length - 1]; + const solutionToLastChallenge = sortChallengeFiles( + lastChallenge.solutions[0] ?? [] + ); + const lastChallengeFiles = sortChallengeFiles( + lastChallenge.challengeFiles ?? [] + ); + const projectPreviewChallengeFiles = lastChallengeFiles.map((file, id) => + createPoly({ + ...file, + contents: solutionToLastChallenge[id]?.contents ?? file.contents + }) + ); + + return { + isFirstChallengeInBlock: challengeOrder === 0, + challengeData: { + challengeType: lastChallenge.challengeType, + challengeFiles: projectPreviewChallengeFiles, + required: lastChallenge.required, + template: lastChallenge.template + } + }; +} + exports.createBlockIntroPages = function (createPage) { return function (edge) { const {