Code like `var xs = []; while(true){ xs.push(1) }` can quickly run the browser out of memory causing it to crash. These changes stop user loops from running indefinitely so that common mistakes will no longer cause the browser to crash. Also, the user is informed if a long running loop is detected (js and jsx challenges) during preview or testing. Before this there was no protection for js challenges and no information was given to the user if they had created such a loop. Co-Authored-By: Tom <20648924+moT01@users.noreply.github.com> Co-Authored-By: mrugesh <1884376+raisedadead@users.noreply.github.com> Co-Authored-By: Randell Dawson <5313213+RandellDawson@users.noreply.github.com>
183 lines
4.5 KiB
JavaScript
183 lines
4.5 KiB
JavaScript
import {
|
|
attempt,
|
|
cond,
|
|
flow,
|
|
identity,
|
|
isError,
|
|
matchesProperty,
|
|
overSome,
|
|
partial,
|
|
stubTrue
|
|
} from 'lodash';
|
|
|
|
import * as Babel from '@babel/standalone';
|
|
import presetEnv from '@babel/preset-env';
|
|
import presetReact from '@babel/preset-react';
|
|
import protect from '@freecodecamp/loop-protect';
|
|
|
|
import * as vinyl from '../utils/polyvinyl.js';
|
|
import createWorker from '../utils/worker-executor';
|
|
|
|
// the config files are created during the build, but not before linting
|
|
// eslint-disable-next-line import/no-unresolved
|
|
import { filename as sassCompile } from '../../../../config/sass-compile';
|
|
|
|
const protectTimeout = 100;
|
|
const testProtectTimeout = 1500;
|
|
const loopsPerTimeoutCheck = 2000;
|
|
|
|
function loopProtectCB(line) {
|
|
console.log(
|
|
`Potential infinite loop detected on line ${line}. Tests may fail if this is not changed.`
|
|
);
|
|
}
|
|
|
|
function testLoopProtectCB(line) {
|
|
console.log(
|
|
`Potential infinite loop detected on line ${line}. Tests may be failing because of this.`
|
|
);
|
|
}
|
|
|
|
Babel.registerPlugin('loopProtection', protect(protectTimeout, loopProtectCB));
|
|
Babel.registerPlugin(
|
|
'testLoopProtection',
|
|
protect(testProtectTimeout, testLoopProtectCB, loopsPerTimeoutCheck)
|
|
);
|
|
|
|
const babelOptionsJSX = {
|
|
plugins: ['loopProtection'],
|
|
presets: [presetEnv, presetReact]
|
|
};
|
|
|
|
const babelOptionsJS = {
|
|
plugins: ['testLoopProtection'],
|
|
presets: [presetEnv]
|
|
};
|
|
|
|
const babelOptionsJSPreview = {
|
|
...babelOptionsJS,
|
|
plugins: ['loopProtection']
|
|
};
|
|
|
|
const babelTransformCode = options => code =>
|
|
Babel.transform(code, options).code;
|
|
|
|
// const sourceReg =
|
|
// /(<!-- fcc-start-source -->)([\s\S]*?)(?=<!-- fcc-end-source -->)/g;
|
|
const NBSPReg = new RegExp(String.fromCharCode(160), 'g');
|
|
|
|
const testJS = matchesProperty('ext', 'js');
|
|
const testJSX = matchesProperty('ext', 'jsx');
|
|
const testHTML = matchesProperty('ext', 'html');
|
|
const testHTML$JS$JSX = overSome(testHTML, testJS, testJSX);
|
|
export const testJS$JSX = overSome(testJS, testJSX);
|
|
|
|
export const replaceNBSP = cond([
|
|
[
|
|
testHTML$JS$JSX,
|
|
partial(vinyl.transformContents, contents => contents.replace(NBSPReg, ' '))
|
|
],
|
|
[stubTrue, identity]
|
|
]);
|
|
|
|
function tryTransform(wrap = identity) {
|
|
return function transformWrappedPoly(source) {
|
|
const result = attempt(wrap, source);
|
|
if (isError(result)) {
|
|
// note(Bouncey): Error thrown here to collapse the build pipeline
|
|
// At the minute, it will not bubble up
|
|
// We collapse the pipeline so the app doesn't fall over trying
|
|
// parse bad code (syntax/type errors etc...)
|
|
throw result;
|
|
}
|
|
return result;
|
|
};
|
|
}
|
|
|
|
const babelTransformer = (preview = false) =>
|
|
cond([
|
|
[
|
|
testJS,
|
|
flow(
|
|
partial(
|
|
vinyl.transformHeadTailAndContents,
|
|
tryTransform(
|
|
babelTransformCode(preview ? babelOptionsJSPreview : babelOptionsJS)
|
|
)
|
|
)
|
|
)
|
|
],
|
|
[
|
|
testJSX,
|
|
flow(
|
|
partial(
|
|
vinyl.transformHeadTailAndContents,
|
|
tryTransform(babelTransformCode(babelOptionsJSX))
|
|
),
|
|
partial(vinyl.setExt, 'js')
|
|
)
|
|
],
|
|
[stubTrue, identity]
|
|
]);
|
|
|
|
const sassWorker = createWorker(sassCompile);
|
|
async function transformSASS(element) {
|
|
const styleTags = element.querySelectorAll('style[type="text/sass"]');
|
|
await Promise.all(
|
|
[].map.call(styleTags, async style => {
|
|
style.type = 'text/css';
|
|
style.innerHTML = await sassWorker.execute(style.innerHTML, 5000).done;
|
|
})
|
|
);
|
|
}
|
|
|
|
function transformScript(element) {
|
|
const scriptTags = element.querySelectorAll('script');
|
|
scriptTags.forEach(script => {
|
|
script.innerHTML = tryTransform(babelTransformCode(babelOptionsJSX))(
|
|
script.innerHTML
|
|
);
|
|
});
|
|
}
|
|
|
|
const transformHtml = async function(file) {
|
|
const div = document.createElement('div');
|
|
div.innerHTML = file.contents;
|
|
await Promise.all([transformSASS(div), transformScript(div)]);
|
|
return vinyl.transformContents(() => div.innerHTML, file);
|
|
};
|
|
|
|
export const composeHTML = cond([
|
|
[
|
|
testHTML,
|
|
flow(
|
|
partial(vinyl.transformHeadTailAndContents, source => {
|
|
const div = document.createElement('div');
|
|
div.innerHTML = source;
|
|
return div.innerHTML;
|
|
}),
|
|
partial(vinyl.compileHeadTail, '')
|
|
)
|
|
],
|
|
[stubTrue, identity]
|
|
]);
|
|
|
|
export const htmlTransformer = cond([
|
|
[testHTML, transformHtml],
|
|
[stubTrue, identity]
|
|
]);
|
|
|
|
export const transformers = [
|
|
replaceNBSP,
|
|
babelTransformer(),
|
|
composeHTML,
|
|
htmlTransformer
|
|
];
|
|
|
|
export const transformersPreview = [
|
|
replaceNBSP,
|
|
babelTransformer(true),
|
|
composeHTML,
|
|
htmlTransformer
|
|
];
|