Files
freeCodeCamp/client/src/templates/Challenges/rechallenge/transformers.js

242 lines
6.3 KiB
JavaScript
Raw Normal View History

2018-04-06 14:51:52 +01:00
import {
attempt,
cond,
flow,
identity,
isError,
matchesProperty,
overSome,
partial,
stubTrue
} from 'lodash-es';
2018-04-06 14:51:52 +01:00
import protect from '@freecodecamp/loop-protect';
2018-04-06 14:51:52 +01:00
import {
transformContents,
transformHeadTailAndContents,
setExt,
compileHeadTail
} from '../../../../../utils/polyvinyl';
import createWorker from '../utils/worker-executor';
2018-04-06 14:51:52 +01:00
// the config files are created during the build, but not before linting
// eslint-disable-next-line import/no-unresolved
import sassData from '../../../../../config/client/sass-compile.json';
const { filename: sassCompile } = sassData;
2018-04-06 14:51:52 +01:00
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.`
);
}
2020-01-15 12:06:50 +01:00
// hold Babel, presets and options so we don't try to import them multiple times
2018-04-06 14:51:52 +01:00
2020-01-15 12:06:50 +01:00
let Babel;
let presetEnv, presetReact;
let babelOptionsJSBase, babelOptionsJS, babelOptionsJSX, babelOptionsJSPreview;
2018-11-26 02:17:38 +03:00
2020-01-15 12:06:50 +01:00
async function loadBabel() {
if (Babel) return;
/* eslint-disable no-inline-comments */
Babel = await import(
/* webpackChunkName: "@babel/standalone" */ '@babel/standalone'
);
/* eslint-enable no-inline-comments */
Babel.registerPlugin(
'loopProtection',
protect(protectTimeout, loopProtectCB)
);
Babel.registerPlugin(
'testLoopProtection',
protect(testProtectTimeout, testLoopProtectCB, loopsPerTimeoutCheck)
);
2020-01-16 11:27:26 +01:00
}
async function loadPresetEnv() {
/* eslint-disable no-inline-comments */
if (!presetEnv)
presetEnv = await import(
/* webpackChunkName: "@babel/preset-env" */ '@babel/preset-env'
);
2020-01-16 11:27:26 +01:00
/* eslint-enable no-inline-comments */
2020-01-15 12:06:50 +01:00
babelOptionsJSBase = {
presets: [presetEnv]
};
babelOptionsJS = {
...babelOptionsJSBase,
plugins: ['testLoopProtection']
};
babelOptionsJSPreview = {
...babelOptionsJSBase,
plugins: ['loopProtection']
};
}
2020-01-16 11:27:26 +01:00
async function loadPresetReact() {
/* eslint-disable no-inline-comments */
if (!presetReact)
presetReact = await import(
/* webpackChunkName: "@babel/preset-react" */ '@babel/preset-react'
);
2020-01-16 11:27:26 +01:00
if (!presetEnv)
presetEnv = await import(
/* webpackChunkName: "@babel/preset-env" */ '@babel/preset-env'
);
/* eslint-enable no-inline-comments */
babelOptionsJSX = {
plugins: ['loopProtection'],
presets: [presetEnv, presetReact]
};
}
2018-11-26 02:17:38 +03:00
const babelTransformCode = options => code =>
Babel.transform(code, options).code;
2018-04-06 14:51:52 +01:00
// const sourceReg =
// /(<!-- fcc-start-source -->)([\s\S]*?)(?=<!-- fcc-end-source -->)/g;
const NBSPReg = new RegExp(String.fromCharCode(160), 'g');
2018-11-26 02:17:38 +03:00
const testJS = matchesProperty('ext', 'js');
const testJSX = matchesProperty('ext', 'jsx');
const testHTML = matchesProperty('ext', 'html');
2018-11-26 02:17:38 +03:00
const testHTML$JS$JSX = overSome(testHTML, testJS, testJSX);
export const testJS$JSX = overSome(testJS, testJSX);
2018-04-06 14:51:52 +01:00
export const replaceNBSP = cond([
[
2018-11-26 02:17:38 +03:00
testHTML$JS$JSX,
partial(transformContents, contents => contents.replace(NBSPReg, ' '))
2018-04-06 14:51:52 +01:00
],
[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;
2018-04-06 14:51:52 +01:00
}
return result;
};
}
const babelTransformer = options => {
return cond([
[
testJS,
2020-01-15 12:06:50 +01:00
async code => {
await loadBabel();
2020-01-16 11:27:26 +01:00
await loadPresetEnv();
const babelOptions = getBabelOptions(options);
2020-01-15 12:06:50 +01:00
return partial(
transformHeadTailAndContents,
tryTransform(babelTransformCode(babelOptions))
2020-01-15 12:06:50 +01:00
)(code);
}
],
[
testJSX,
2020-01-15 12:06:50 +01:00
async code => {
await loadBabel();
2020-01-16 11:27:26 +01:00
await loadPresetReact();
2020-01-15 12:06:50 +01:00
return flow(
partial(
transformHeadTailAndContents,
2020-01-15 12:06:50 +01:00
tryTransform(babelTransformCode(babelOptionsJSX))
),
partial(setExt, 'js')
2020-01-15 12:06:50 +01:00
)(code);
}
],
[stubTrue, identity]
]);
};
2018-04-06 14:51:52 +01:00
function getBabelOptions({ preview = false, protect = true }) {
let options = babelOptionsJSBase;
// we always protect the preview, since it evaluates as the user types and
// they may briefly have infinite looping code accidentally
if (protect) {
options = preview ? babelOptionsJSPreview : babelOptionsJS;
} else {
options = preview ? babelOptionsJSPreview : options;
}
return options;
}
const sassWorker = createWorker(sassCompile);
2018-12-12 17:12:30 +03:00
async function transformSASS(element) {
// we only teach scss syntax, not sass. Also the compiler does not seem to be
// able to deal with sass.
const styleTags = element.querySelectorAll('style[type~="text/scss"]');
2018-12-12 17:12:30 +03:00
await Promise.all(
[].map.call(styleTags, async style => {
style.type = 'text/css';
style.innerHTML = await sassWorker.execute(style.innerHTML, 5000).done;
2018-12-12 17:12:30 +03:00
})
);
}
2020-01-15 12:06:50 +01:00
async function transformScript(element) {
await loadBabel();
await loadPresetEnv();
2018-12-12 17:12:30 +03:00
const scriptTags = element.querySelectorAll('script');
scriptTags.forEach(script => {
script.innerHTML = tryTransform(babelTransformCode(babelOptionsJS))(
2018-12-12 17:12:30 +03:00
script.innerHTML
);
});
}
const transformHtml = async function (file) {
const div = document.createElement('div');
div.innerHTML = file.contents;
2018-12-12 17:12:30 +03:00
await Promise.all([transformSASS(div), transformScript(div)]);
return transformContents(() => div.innerHTML, file);
};
2018-12-12 17:12:30 +03:00
export const composeHTML = cond([
[
testHTML,
flow(
partial(transformHeadTailAndContents, source => {
2018-12-10 18:06:05 +03:00
const div = document.createElement('div');
div.innerHTML = source;
return div.innerHTML;
}),
partial(compileHeadTail, '')
2018-12-12 17:12:30 +03:00
)
],
[stubTrue, identity]
]);
export const htmlTransformer = cond([
[testHTML, transformHtml],
2018-04-06 14:51:52 +01:00
[stubTrue, identity]
]);
export const getTransformers = options => [
replaceNBSP,
babelTransformer(options ? options : {}),
2018-12-12 17:12:30 +03:00
composeHTML,
htmlTransformer
];