feat(curriculum): control loop-protect (#51542)
parent
e6867fe7ba
commit
e149b09087
|
@ -78,6 +78,8 @@ exports.createPages = function createPages({ graphql, actions, reporter }) {
|
|||
certification
|
||||
challengeType
|
||||
dashedName
|
||||
disableLoopProtectTests
|
||||
disableLoopProtectPreview
|
||||
fields {
|
||||
slug
|
||||
blockHashSlug
|
||||
|
|
|
@ -312,6 +312,8 @@ export type ChallengeMeta = {
|
|||
title?: string;
|
||||
challengeType?: number;
|
||||
helpCategory: string;
|
||||
disableLoopProtectTests: boolean;
|
||||
disableLoopProtectPreview: boolean;
|
||||
};
|
||||
|
||||
export type PortfolioProjectData = {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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<BuildResult> | 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';
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"template": "",
|
||||
"required": [],
|
||||
"superBlock": "project-euler",
|
||||
"disableLoopProtectTests": true,
|
||||
"challengeOrder": [
|
||||
{
|
||||
"id": "5900f36e1000cf542c50fe80",
|
||||
|
@ -410,4 +411,4 @@
|
|||
"title": "Problem 100: Arranged probability"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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\""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"template": "",
|
||||
"required": [],
|
||||
"superBlock": "project-euler",
|
||||
"disableLoopProtectTests": true,
|
||||
"challengeOrder": [
|
||||
{
|
||||
"id": "5900f4361000cf542c50ff48",
|
||||
|
@ -410,4 +411,4 @@
|
|||
"title": "Problem 300: Protein folding"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"template": "",
|
||||
"required": [],
|
||||
"superBlock": "project-euler",
|
||||
"disableLoopProtectTests": true,
|
||||
"challengeOrder": [
|
||||
{
|
||||
"id": "5900f4991000cf542c50ffab",
|
||||
|
@ -410,4 +411,4 @@
|
|||
"title": "Problem 400: Fibonacci tree game"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"template": "",
|
||||
"required": [],
|
||||
"superBlock": "project-euler",
|
||||
"disableLoopProtectTests": true,
|
||||
"challengeOrder": [
|
||||
{
|
||||
"id": "5900f4fd1000cf542c51000f",
|
||||
|
@ -330,4 +331,4 @@
|
|||
"title": "Problem 480: The Last Question"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"template": "",
|
||||
"required": [],
|
||||
"superBlock": "coding-interview-prep",
|
||||
"disableLoopProtectTests": true,
|
||||
"challengeOrder": [
|
||||
{
|
||||
"id": "594810f028c0303b75339acb",
|
||||
|
@ -650,4 +651,4 @@
|
|||
"title": "Zig-zag matrix"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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 <number>...
|
||||
```
|
||||
|
||||
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.
|
||||
|
|
|
@ -26,7 +26,9 @@ const blockSchema = Joi.object({}).keys({
|
|||
id: Joi.string(),
|
||||
title: Joi.string()
|
||||
})
|
||||
)
|
||||
),
|
||||
disableLoopProtectTests: Joi.boolean(),
|
||||
disableLoopProtectPreview: Joi.boolean()
|
||||
})
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue