2018-04-06 14:51:52 +01:00
|
|
|
import {
|
|
|
|
attempt,
|
|
|
|
cond,
|
|
|
|
flow,
|
|
|
|
identity,
|
|
|
|
isError,
|
|
|
|
matchesProperty,
|
|
|
|
overSome,
|
|
|
|
partial,
|
|
|
|
stubTrue
|
2021-04-28 23:11:20 +02:00
|
|
|
} from 'lodash-es';
|
2018-04-06 14:51:52 +01:00
|
|
|
|
2019-11-27 02:51:43 +01:00
|
|
|
import protect from '@freecodecamp/loop-protect';
|
2018-04-06 14:51:52 +01:00
|
|
|
|
2021-05-31 17:46:26 +02:00
|
|
|
import {
|
|
|
|
transformContents,
|
|
|
|
transformHeadTailAndContents,
|
|
|
|
setExt,
|
|
|
|
compileHeadTail
|
|
|
|
} from '../../../../../utils/polyvinyl';
|
2019-01-03 01:50:28 +03:00
|
|
|
import createWorker from '../utils/worker-executor';
|
2018-04-06 14:51:52 +01:00
|
|
|
|
2019-11-14 21:13:44 +01:00
|
|
|
// the config files are created during the build, but not before linting
|
|
|
|
// eslint-disable-next-line import/no-unresolved
|
2021-03-26 00:43:43 +05:30
|
|
|
import sassData from '../../../../../config/client/sass-compile.json';
|
|
|
|
|
|
|
|
const { filename: sassCompile } = sassData;
|
2019-11-14 21:13:44 +01:00
|
|
|
|
2018-04-06 14:51:52 +01:00
|
|
|
const protectTimeout = 100;
|
2019-11-27 02:51:43 +01:00
|
|
|
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 */
|
2020-08-19 22:30:46 +02:00
|
|
|
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']
|
|
|
|
};
|
|
|
|
}
|
2019-11-27 02:51:43 +01:00
|
|
|
|
2020-01-16 11:27:26 +01:00
|
|
|
async function loadPresetReact() {
|
|
|
|
/* eslint-disable no-inline-comments */
|
2021-02-23 15:52:20 +01:00
|
|
|
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');
|
2018-07-08 22:12:38 +03:00
|
|
|
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,
|
2021-05-31 17:46:26 +02:00
|
|
|
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)) {
|
2018-05-26 21:32:40 +01:00
|
|
|
// 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...)
|
2018-07-04 16:38:42 +03:00
|
|
|
throw result;
|
2018-04-06 14:51:52 +01:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-02-11 14:35:55 +01:00
|
|
|
const babelTransformer = options => {
|
2020-02-04 06:03:56 +01:00
|
|
|
return cond([
|
2019-11-27 02:51:43 +01:00
|
|
|
[
|
|
|
|
testJS,
|
2020-01-15 12:06:50 +01:00
|
|
|
async code => {
|
|
|
|
await loadBabel();
|
2020-01-16 11:27:26 +01:00
|
|
|
await loadPresetEnv();
|
2020-02-11 14:35:55 +01:00
|
|
|
const babelOptions = getBabelOptions(options);
|
2020-01-15 12:06:50 +01:00
|
|
|
return partial(
|
2021-05-31 17:46:26 +02:00
|
|
|
transformHeadTailAndContents,
|
2020-02-11 14:35:55 +01:00
|
|
|
tryTransform(babelTransformCode(babelOptions))
|
2020-01-15 12:06:50 +01:00
|
|
|
)(code);
|
|
|
|
}
|
2019-11-27 02:51:43 +01:00
|
|
|
],
|
|
|
|
[
|
|
|
|
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(
|
2021-05-31 17:46:26 +02:00
|
|
|
transformHeadTailAndContents,
|
2020-01-15 12:06:50 +01:00
|
|
|
tryTransform(babelTransformCode(babelOptionsJSX))
|
|
|
|
),
|
2021-05-31 17:46:26 +02:00
|
|
|
partial(setExt, 'js')
|
2020-01-15 12:06:50 +01:00
|
|
|
)(code);
|
|
|
|
}
|
2019-11-27 02:51:43 +01:00
|
|
|
],
|
|
|
|
[stubTrue, identity]
|
|
|
|
]);
|
2020-02-04 06:03:56 +01:00
|
|
|
};
|
2018-04-06 14:51:52 +01:00
|
|
|
|
2020-02-11 14:35:55 +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;
|
|
|
|
}
|
|
|
|
|
2019-11-14 21:13:44 +01:00
|
|
|
const sassWorker = createWorker(sassCompile);
|
2018-12-12 17:12:30 +03:00
|
|
|
async function transformSASS(element) {
|
2020-05-09 16:31:18 +02:00
|
|
|
// 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';
|
2019-03-14 12:08:15 +03:00
|
|
|
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();
|
2020-01-16 11:41:36 +01:00
|
|
|
await loadPresetEnv();
|
2018-12-12 17:12:30 +03:00
|
|
|
const scriptTags = element.querySelectorAll('script');
|
|
|
|
scriptTags.forEach(script => {
|
2020-01-16 11:41:36 +01:00
|
|
|
script.innerHTML = tryTransform(babelTransformCode(babelOptionsJS))(
|
2018-12-12 17:12:30 +03:00
|
|
|
script.innerHTML
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
2018-11-11 16:49:20 +03:00
|
|
|
|
2021-03-11 00:31:46 +05:30
|
|
|
const transformHtml = async function (file) {
|
2018-11-11 16:49:20 +03:00
|
|
|
const div = document.createElement('div');
|
|
|
|
div.innerHTML = file.contents;
|
2018-12-12 17:12:30 +03:00
|
|
|
await Promise.all([transformSASS(div), transformScript(div)]);
|
2021-05-31 17:46:26 +02:00
|
|
|
return transformContents(() => div.innerHTML, file);
|
2018-07-08 22:12:38 +03:00
|
|
|
};
|
|
|
|
|
2018-12-12 17:12:30 +03:00
|
|
|
export const composeHTML = cond([
|
|
|
|
[
|
|
|
|
testHTML,
|
|
|
|
flow(
|
2021-05-31 17:46:26 +02:00
|
|
|
partial(transformHeadTailAndContents, source => {
|
2018-12-10 18:06:05 +03:00
|
|
|
const div = document.createElement('div');
|
|
|
|
div.innerHTML = source;
|
|
|
|
return div.innerHTML;
|
|
|
|
}),
|
2021-05-31 17:46:26 +02:00
|
|
|
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]
|
|
|
|
]);
|
|
|
|
|
2020-02-11 14:35:55 +01:00
|
|
|
export const getTransformers = options => [
|
2019-11-27 02:51:43 +01:00
|
|
|
replaceNBSP,
|
2020-02-11 14:35:55 +01:00
|
|
|
babelTransformer(options ? options : {}),
|
2018-12-12 17:12:30 +03:00
|
|
|
composeHTML,
|
|
|
|
htmlTransformer
|
2018-07-08 22:12:38 +03:00
|
|
|
];
|