diff --git a/client/gatsby-node.js b/client/gatsby-node.js index db1765d8936..e3913c64f2a 100644 --- a/client/gatsby-node.js +++ b/client/gatsby-node.js @@ -78,6 +78,8 @@ exports.createPages = function createPages({ graphql, actions, reporter }) { certification challengeType dashedName + disableLoopProtectTests + disableLoopProtectPreview fields { slug blockHashSlug diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts index c8291ee9fcc..46a8b5c26f5 100644 --- a/client/src/redux/prop-types.ts +++ b/client/src/redux/prop-types.ts @@ -312,6 +312,8 @@ export type ChallengeMeta = { title?: string; challengeType?: number; helpCategory: string; + disableLoopProtectTests: boolean; + disableLoopProtectPreview: boolean; }; export type PortfolioProjectData = { diff --git a/client/src/templates/Challenges/rechallenge/transformers.js b/client/src/templates/Challenges/rechallenge/transformers.js index 451f320732b..91aec07b0ca 100644 --- a/client/src/templates/Challenges/rechallenge/transformers.js +++ b/client/src/templates/Challenges/rechallenge/transformers.js @@ -159,12 +159,18 @@ const babelTransformer = loopProtectOptions => { function getBabelOptions( presets, - { preview, protect } = { preview: false, protect: true } + { preview, disableLoopProtectTests, disableLoopProtectPreview } = { + preview: false, + disableLoopProtectTests: false, + disableLoopProtectPreview: false + } ) { // we always protect the preview, since it evaluates as the user types and // they may briefly have infinite looping code accidentally - if (preview) return { ...presets, plugins: ['loopProtection'] }; - if (protect) return { ...presets, plugins: ['testLoopProtection'] }; + if (preview && !disableLoopProtectPreview) + return { ...presets, plugins: ['loopProtection'] }; + if (!disableLoopProtectTests) + return { ...presets, plugins: ['testLoopProtection'] }; return presets; } diff --git a/client/src/templates/Challenges/redux/execute-challenge-saga.js b/client/src/templates/Challenges/redux/execute-challenge-saga.js index a81058ac502..bf778a7adad 100644 --- a/client/src/templates/Challenges/redux/execute-challenge-saga.js +++ b/client/src/templates/Challenges/redux/execute-challenge-saga.js @@ -30,7 +30,6 @@ import { challengeHasPreview, getTestRunner, isJavaScriptChallenge, - isLoopProtected, updatePreview, updateProjectPreview } from '../utils/build'; @@ -107,10 +106,10 @@ function* executeChallengeSaga({ payload }) { const challengeData = yield select(challengeDataSelector); const challengeMeta = yield select(challengeMetaSelector); - const protect = isLoopProtected(challengeMeta); const buildData = yield buildChallengeData(challengeData, { preview: false, - protect, + disableLoopProtectTests: challengeMeta.disableLoopProtectTests, + disableLoopProtectPreview: challengeMeta.disableLoopProtectPreview, usesTestRunner: true }); const document = yield getContext('document'); @@ -230,10 +229,10 @@ function* previewChallengeSaga({ flushLogs = true } = {}) { if (canBuildChallenge(challengeData)) { const challengeMeta = yield select(challengeMetaSelector); - const protect = isLoopProtected(challengeMeta); const buildData = yield buildChallengeData(challengeData, { preview: true, - protect + disableLoopProtectTests: challengeMeta.disableLoopProtectTests, + disableLoopProtectPreview: challengeMeta.disableLoopProtectPreview }); // evaluate the user code in the preview frame or in the worker if (challengeHasPreview(challengeData)) { diff --git a/client/src/templates/Challenges/utils/build.ts b/client/src/templates/Challenges/utils/build.ts index bb848c5acf1..97d4c276f63 100644 --- a/client/src/templates/Challenges/utils/build.ts +++ b/client/src/templates/Challenges/utils/build.ts @@ -43,8 +43,9 @@ interface BuildChallengeData extends Context { interface BuildOptions { preview: boolean; - protect: boolean; - usesTestRunner: boolean; + disableLoopProtectTests: boolean; + disableLoopProtectPreview: boolean; + usesTestRunner?: boolean; } const { filename: testEvaluator } = testEvaluatorData; @@ -211,14 +212,15 @@ type BuildResult = { // out of it. export function buildDOMChallenge( { challengeFiles, required = [], template = '' }: BuildChallengeData, - { usesTestRunner } = { usesTestRunner: false } + options?: BuildOptions ): Promise | undefined { const loadEnzyme = challengeFiles?.some( challengeFile => challengeFile.ext === 'jsx' ); - const pipeLine = composeFunctions(...getTransformers()); + const pipeLine = composeFunctions(...getTransformers(options)); const finalFiles = challengeFiles?.map(pipeLine); + const usesTestRunner = options?.usesTestRunner ?? false; if (finalFiles) { return Promise.all(finalFiles) @@ -385,7 +387,3 @@ export function isJavaScriptChallenge({ challengeType === challengeTypes.jsProject ); } - -export function isLoopProtected(challengeMeta: ChallengeMeta): boolean { - return challengeMeta.superBlock !== 'coding-interview-prep'; -} diff --git a/client/utils/gatsby/challenge-page-creator.js b/client/utils/gatsby/challenge-page-creator.js index 6c153acc890..d16e90b22b8 100644 --- a/client/utils/gatsby/challenge-page-creator.js +++ b/client/utils/gatsby/challenge-page-creator.js @@ -93,6 +93,8 @@ exports.createChallengePages = function (createPage) { return function ({ node: { challenge } }, index, allChallengeEdges) { const { dashedName, + disableLoopProtectTests, + disableLoopProtectPreview, certification, superBlock, block, @@ -113,6 +115,8 @@ exports.createChallengePages = function (createPage) { blockHashSlug, dashedName, certification, + disableLoopProtectTests, + disableLoopProtectPreview, superBlock, block, isFirstStep: getIsFirstStepInBlock(index, allChallengeEdges), diff --git a/curriculum/challenges/_meta/project-euler-problems-1-to-100/meta.json b/curriculum/challenges/_meta/project-euler-problems-1-to-100/meta.json index a643b76795a..402a4ae870b 100644 --- a/curriculum/challenges/_meta/project-euler-problems-1-to-100/meta.json +++ b/curriculum/challenges/_meta/project-euler-problems-1-to-100/meta.json @@ -8,6 +8,7 @@ "template": "", "required": [], "superBlock": "project-euler", + "disableLoopProtectTests": true, "challengeOrder": [ { "id": "5900f36e1000cf542c50fe80", @@ -410,4 +411,4 @@ "title": "Problem 100: Arranged probability" } ] -} \ No newline at end of file +} diff --git a/curriculum/challenges/_meta/project-euler-problems-101-to-200/meta.json b/curriculum/challenges/_meta/project-euler-problems-101-to-200/meta.json index b76684f6274..f01103def3b 100644 --- a/curriculum/challenges/_meta/project-euler-problems-101-to-200/meta.json +++ b/curriculum/challenges/_meta/project-euler-problems-101-to-200/meta.json @@ -8,6 +8,7 @@ "template": "", "required": [], "superBlock": "project-euler", + "disableLoopProtectTests": true, "challengeOrder": [ { "id": "5900f3d21000cf542c50fee4", @@ -410,4 +411,4 @@ "title": "Problem 200: Find the 200th prime-proof sqube containing the contiguous sub-string \"200\"" } ] -} \ No newline at end of file +} diff --git a/curriculum/challenges/_meta/project-euler-problems-201-to-300/meta.json b/curriculum/challenges/_meta/project-euler-problems-201-to-300/meta.json index 244f0957c9e..97c24b8c825 100644 --- a/curriculum/challenges/_meta/project-euler-problems-201-to-300/meta.json +++ b/curriculum/challenges/_meta/project-euler-problems-201-to-300/meta.json @@ -8,6 +8,7 @@ "template": "", "required": [], "superBlock": "project-euler", + "disableLoopProtectTests": true, "challengeOrder": [ { "id": "5900f4361000cf542c50ff48", @@ -410,4 +411,4 @@ "title": "Problem 300: Protein folding" } ] -} \ No newline at end of file +} diff --git a/curriculum/challenges/_meta/project-euler-problems-301-to-400/meta.json b/curriculum/challenges/_meta/project-euler-problems-301-to-400/meta.json index fd8d790c1ca..032258b34c0 100644 --- a/curriculum/challenges/_meta/project-euler-problems-301-to-400/meta.json +++ b/curriculum/challenges/_meta/project-euler-problems-301-to-400/meta.json @@ -8,6 +8,7 @@ "template": "", "required": [], "superBlock": "project-euler", + "disableLoopProtectTests": true, "challengeOrder": [ { "id": "5900f4991000cf542c50ffab", @@ -410,4 +411,4 @@ "title": "Problem 400: Fibonacci tree game" } ] -} \ No newline at end of file +} diff --git a/curriculum/challenges/_meta/project-euler-problems-401-to-480/meta.json b/curriculum/challenges/_meta/project-euler-problems-401-to-480/meta.json index 93b6f68fc81..042371b73eb 100644 --- a/curriculum/challenges/_meta/project-euler-problems-401-to-480/meta.json +++ b/curriculum/challenges/_meta/project-euler-problems-401-to-480/meta.json @@ -8,6 +8,7 @@ "template": "", "required": [], "superBlock": "project-euler", + "disableLoopProtectTests": true, "challengeOrder": [ { "id": "5900f4fd1000cf542c51000f", @@ -330,4 +331,4 @@ "title": "Problem 480: The Last Question" } ] -} \ No newline at end of file +} diff --git a/curriculum/challenges/_meta/rosetta-code/meta.json b/curriculum/challenges/_meta/rosetta-code/meta.json index c54cec3a9ac..4c11d2975d7 100644 --- a/curriculum/challenges/_meta/rosetta-code/meta.json +++ b/curriculum/challenges/_meta/rosetta-code/meta.json @@ -8,6 +8,7 @@ "template": "", "required": [], "superBlock": "coding-interview-prep", + "disableLoopProtectTests": true, "challengeOrder": [ { "id": "594810f028c0303b75339acb", @@ -650,4 +651,4 @@ "title": "Zig-zag matrix" } ] -} \ No newline at end of file +} diff --git a/curriculum/get-challenges.js b/curriculum/get-challenges.js index 05fb381e040..7b2e1cddd04 100644 --- a/curriculum/get-challenges.js +++ b/curriculum/get-challenges.js @@ -326,6 +326,8 @@ ${getFullPath('english', filePath)} showUpcomingChanges: process.env.SHOW_UPCOMING_CHANGES === 'true' }); challenge.usesMultifileEditor = !!meta.usesMultifileEditor; + challenge.disableLoopProtectTests = !!meta.disableLoopProtectTests; + challenge.disableLoopProtectPreview = !!meta.disableLoopProtectPreview; } function fixChallengeProperties(challenge) { diff --git a/curriculum/schema/challenge-schema.js b/curriculum/schema/challenge-schema.js index 877356d3aa0..437dbfe45bd 100644 --- a/curriculum/schema/challenge-schema.js +++ b/curriculum/schema/challenge-schema.js @@ -42,6 +42,8 @@ const schema = Joi.object() then: Joi.string().allow(''), otherwise: Joi.string().required() }), + disableLoopProtectTests: Joi.boolean().required(), + disableLoopProtectPreview: Joi.boolean().required(), challengeFiles: Joi.array().items(fileJoi), guideUrl: Joi.string().uri({ scheme: 'https' }), hasEditableBoundaries: Joi.boolean(), diff --git a/docs/how-to-work-on-coding-challenges.md b/docs/how-to-work-on-coding-challenges.md index 61fc6bbe423..e570278bb7c 100644 --- a/docs/how-to-work-on-coding-challenges.md +++ b/docs/how-to-work-on-coding-challenges.md @@ -566,3 +566,19 @@ pnpm run update-challenge-order ``` This will take you through an interactive process to select the order of the challenges. + +## Troubleshooting + +### Infinite Loop Detected + +If you see the following error in the console while previewing a challenge: + +```text +Potential infinite loop detected on line ... +``` + +This means that the loop-protect plugin has found a long-running loop or recursive function. If your challenge needs to do that (e.g. it contains an event loop that is supposed to run indefinitely), then you can prevent the plugin from being used in the preview. To do so, add `disableLoopProtectPreview: true` to the block's `meta.json` file. + +If your tests are computationally intensive, then you may see this error when they run. If this happens then you can add `disableLoopProtectTests: true` to the block's `meta.json` file. + +It's not typically necessary to have both set to true, so only set them as needed. diff --git a/tools/scripts/build/external-data-schema.js b/tools/scripts/build/external-data-schema.js index 0462e89ff2a..f984999923f 100644 --- a/tools/scripts/build/external-data-schema.js +++ b/tools/scripts/build/external-data-schema.js @@ -26,7 +26,9 @@ const blockSchema = Joi.object({}).keys({ id: Joi.string(), title: Joi.string() }) - ) + ), + disableLoopProtectTests: Joi.boolean(), + disableLoopProtectPreview: Joi.boolean() }) });