mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-02-25 14:01:29 -05:00
feat: improve file insertion (#45942)
This commit is contained in:
committed by
GitHub
parent
0dd9fc8d4f
commit
538e7c787b
@@ -96,6 +96,7 @@ exports.createPages = function createPages({ graphql, actions, reporter }) {
|
||||
head
|
||||
tail
|
||||
history
|
||||
fileKey
|
||||
}
|
||||
solutions {
|
||||
contents
|
||||
|
||||
@@ -1,65 +1,6 @@
|
||||
import {
|
||||
cond,
|
||||
flow,
|
||||
identity,
|
||||
matchesProperty,
|
||||
partial,
|
||||
stubTrue,
|
||||
template as _template
|
||||
} from 'lodash-es';
|
||||
import { template as _template } from 'lodash-es';
|
||||
|
||||
import {
|
||||
compileHeadTail,
|
||||
setExt,
|
||||
transformContents
|
||||
} from '../../../../../utils/polyvinyl';
|
||||
|
||||
const wrapInScript = partial(
|
||||
transformContents,
|
||||
content => `<script>${content}</script>`
|
||||
);
|
||||
const wrapInStyle = partial(
|
||||
transformContents,
|
||||
content => `<style>${content}</style>`
|
||||
);
|
||||
const setExtToHTML = partial(setExt, 'html');
|
||||
const concatHeadTail = partial(compileHeadTail, '');
|
||||
|
||||
export const jsToHtml = cond([
|
||||
[
|
||||
matchesProperty('ext', 'js'),
|
||||
flow(concatHeadTail, wrapInScript, setExtToHTML)
|
||||
],
|
||||
[stubTrue, identity]
|
||||
]);
|
||||
|
||||
export const cssToHtml = cond([
|
||||
[
|
||||
matchesProperty('ext', 'css'),
|
||||
flow(concatHeadTail, wrapInStyle, setExtToHTML)
|
||||
],
|
||||
[stubTrue, identity]
|
||||
]);
|
||||
|
||||
export function findIndexHtml(challengeFiles) {
|
||||
const filtered = challengeFiles.filter(challengeFile =>
|
||||
wasHtmlFile(challengeFile)
|
||||
);
|
||||
if (filtered.length > 1) {
|
||||
throw new Error('Too many html blocks in the challenge seed');
|
||||
}
|
||||
return filtered[0];
|
||||
}
|
||||
|
||||
function wasHtmlFile(challengeFile) {
|
||||
return challengeFile.history[0] === 'index.html';
|
||||
}
|
||||
|
||||
export function concatHtml({
|
||||
required = [],
|
||||
template,
|
||||
challengeFiles = []
|
||||
} = {}) {
|
||||
export function concatHtml({ required = [], template, contents } = {}) {
|
||||
const embedSource = template ? _template(template) : ({ source }) => source;
|
||||
const head = required
|
||||
.map(({ link, src }) => {
|
||||
@@ -78,19 +19,5 @@ A required file can not have both a src and a link: src = ${src}, link = ${link}
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const indexHtml = findIndexHtml(challengeFiles);
|
||||
|
||||
const source = challengeFiles.reduce((source, challengeFile) => {
|
||||
if (!indexHtml) return source.concat(challengeFile.contents);
|
||||
if (
|
||||
indexHtml.importedFiles.includes(challengeFile.history[0]) ||
|
||||
wasHtmlFile(challengeFile)
|
||||
) {
|
||||
return source.concat(challengeFile.contents);
|
||||
} else {
|
||||
return source;
|
||||
}
|
||||
}, '');
|
||||
|
||||
return `<head>${head}</head>${embedSource({ source })}`;
|
||||
return `<head>${head}</head>${embedSource({ source: contents })}`;
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { findIndexHtml } from './builders.js';
|
||||
|
||||
const withHTML = [
|
||||
{ history: ['index.html'], contents: 'the index html' },
|
||||
{ history: ['index.css', 'index.html'], contents: 'the style file' }
|
||||
];
|
||||
|
||||
const withoutHTML = [
|
||||
{ history: ['index.css', 'index.html'], contents: 'the js file' },
|
||||
{ history: ['index.js', 'index.html'], contents: 'the style file' }
|
||||
];
|
||||
|
||||
const tooMuchHTML = [
|
||||
{ history: ['index.html'], contents: 'the index html' },
|
||||
{ history: ['index.css', 'index.html'], contents: 'index html two' },
|
||||
{ history: ['index.html'], contents: 'index html three' }
|
||||
];
|
||||
|
||||
// TODO: write tests for concatHtml instead, since findIndexHtml should not be
|
||||
// exported.
|
||||
|
||||
describe('findIndexHtml', () => {
|
||||
it('should return the index.html file from an array', () => {
|
||||
expect.assertions(1);
|
||||
|
||||
expect(findIndexHtml(withHTML)).toStrictEqual({
|
||||
history: ['index.html'],
|
||||
contents: 'the index html'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when the index.html file is missing', () => {
|
||||
expect.assertions(1);
|
||||
|
||||
expect(findIndexHtml(withoutHTML)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw if there are two or more index.htmls', () => {
|
||||
expect.assertions(1);
|
||||
|
||||
expect(() => findIndexHtml(tooMuchHTML)).toThrowError(
|
||||
'Too many html blocks in the challenge seed'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
transformContents,
|
||||
transformHeadTailAndContents,
|
||||
setExt,
|
||||
setImportedFiles,
|
||||
compileHeadTail
|
||||
} from '../../../../../utils/polyvinyl';
|
||||
import createWorker from '../utils/worker-executor';
|
||||
@@ -204,44 +203,60 @@ async function transformScript(documentElement) {
|
||||
});
|
||||
}
|
||||
|
||||
// Find if the base html refers to the css or js files and record if they do. If
|
||||
// the link or script exists we remove those elements since those files don't
|
||||
// exist on the site, only in the editor
|
||||
const addImportedFiles = async function (fileP) {
|
||||
const file = await fileP;
|
||||
const transform = documentElement => {
|
||||
// This does the final transformations of the files needed to embed them into
|
||||
// HTML.
|
||||
export const embedFilesInHtml = async function (challengeFiles) {
|
||||
const { indexHtml, stylesCss, scriptJs, indexJsx } =
|
||||
challengeFilesToObject(challengeFiles);
|
||||
|
||||
const embedStylesAndScript = (documentElement, contentDocument) => {
|
||||
const link =
|
||||
documentElement.querySelector('link[href="styles.css"]') ??
|
||||
documentElement.querySelector('link[href="./styles.css"]');
|
||||
const script =
|
||||
documentElement.querySelector('script[src="script.js"]') ??
|
||||
documentElement.querySelector('script[src="./script.js"]');
|
||||
const importedFiles = [];
|
||||
if (link) {
|
||||
importedFiles.push('styles.css');
|
||||
link.remove();
|
||||
const style = contentDocument.createElement('style');
|
||||
style.innerHTML = stylesCss?.contents;
|
||||
|
||||
link.parentNode.replaceChild(style, link);
|
||||
}
|
||||
if (script) {
|
||||
importedFiles.push('script.js');
|
||||
script.remove();
|
||||
const script = (contentDocument.createElement('script').innerHTML =
|
||||
scriptJs?.contents);
|
||||
link.parentNode.replaceChild(script, link);
|
||||
}
|
||||
return {
|
||||
contents: documentElement.innerHTML,
|
||||
importedFiles
|
||||
contents: documentElement.innerHTML
|
||||
};
|
||||
};
|
||||
|
||||
const { importedFiles, contents } = await transformWithFrame(
|
||||
transform,
|
||||
file.contents
|
||||
);
|
||||
|
||||
return flow(
|
||||
partial(setImportedFiles, importedFiles),
|
||||
partial(transformContents, () => contents)
|
||||
)(file);
|
||||
if (indexHtml) {
|
||||
const { contents } = await transformWithFrame(
|
||||
embedStylesAndScript,
|
||||
indexHtml.contents
|
||||
);
|
||||
return [challengeFiles, contents];
|
||||
} else if (indexJsx) {
|
||||
return [challengeFiles, `<script>${indexJsx.contents}</script>`];
|
||||
} else if (scriptJs) {
|
||||
return [challengeFiles, `<script>${scriptJs.contents}</script>`];
|
||||
} else {
|
||||
throw Error('No html or js(x) file found');
|
||||
}
|
||||
};
|
||||
|
||||
function challengeFilesToObject(challengeFiles) {
|
||||
const indexHtml = challengeFiles.find(file => file.fileKey === 'indexhtml');
|
||||
const indexJsx = challengeFiles.find(
|
||||
file => file.fileKey === 'indexjs' && file.history[0] === 'index.jsx'
|
||||
);
|
||||
const stylesCss = challengeFiles.find(file => file.fileKey === 'stylescss');
|
||||
const scriptJs = challengeFiles.find(file => file.fileKey === 'scriptjs');
|
||||
return { indexHtml, indexJsx, stylesCss, scriptJs };
|
||||
}
|
||||
|
||||
const transformWithFrame = async function (transform, contents) {
|
||||
// we use iframe here since file.contents is destined to be be inserted into
|
||||
// the root of an iframe.
|
||||
@@ -260,7 +275,10 @@ const transformWithFrame = async function (transform, contents) {
|
||||
// itself. It appears that the frame's documentElement can get replaced by a
|
||||
// blank documentElement without the contents. This seems only to happen on
|
||||
// Firefox.
|
||||
out = await transform(frame.contentDocument.documentElement);
|
||||
out = await transform(
|
||||
frame.contentDocument.documentElement,
|
||||
frame.contentDocument
|
||||
);
|
||||
} finally {
|
||||
document.body.removeChild(frame);
|
||||
}
|
||||
@@ -280,19 +298,14 @@ const transformHtml = async function (file) {
|
||||
return transformContents(() => contents, file);
|
||||
};
|
||||
|
||||
const composeHTML = cond([
|
||||
[testHTML, partial(compileHeadTail, '')],
|
||||
[stubTrue, identity]
|
||||
]);
|
||||
|
||||
const htmlTransformer = cond([
|
||||
[testHTML, flow(transformHtml, addImportedFiles)],
|
||||
[testHTML, flow(transformHtml)],
|
||||
[stubTrue, identity]
|
||||
]);
|
||||
|
||||
export const getTransformers = options => [
|
||||
replaceNBSP,
|
||||
babelTransformer(options ? options : {}),
|
||||
composeHTML,
|
||||
partial(compileHeadTail, ''),
|
||||
htmlTransformer
|
||||
];
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import frameRunnerData from '../../../../../config/client/frame-runner.json';
|
||||
import testEvaluatorData from '../../../../../config/client/test-evaluator.json';
|
||||
import { challengeTypes } from '../../../../utils/challenge-types';
|
||||
import { cssToHtml, jsToHtml, concatHtml } from '../rechallenge/builders.js';
|
||||
import { getTransformers } from '../rechallenge/transformers';
|
||||
import { concatHtml } from '../rechallenge/builders.js';
|
||||
import { getTransformers, embedFilesInHtml } from '../rechallenge/transformers';
|
||||
import {
|
||||
createTestFramer,
|
||||
runTestInTestFrame,
|
||||
@@ -141,19 +141,19 @@ export function buildDOMChallenge(
|
||||
const loadEnzyme = challengeFiles.some(
|
||||
challengeFile => challengeFile.ext === 'jsx'
|
||||
);
|
||||
const toHtml = [jsToHtml, cssToHtml];
|
||||
const pipeLine = composeFunctions(...getTransformers(), ...toHtml);
|
||||
const pipeLine = composeFunctions(...getTransformers());
|
||||
const finalFiles = challengeFiles.map(pipeLine);
|
||||
return Promise.all(finalFiles)
|
||||
.then(checkFilesErrors)
|
||||
.then(challengeFiles => {
|
||||
.then(embedFilesInHtml)
|
||||
.then(([challengeFiles, contents]) => {
|
||||
return {
|
||||
challengeType:
|
||||
challengeTypes.html || challengeTypes.multifileCertProject,
|
||||
build: concatHtml({
|
||||
required: finalRequires,
|
||||
template,
|
||||
challengeFiles
|
||||
contents
|
||||
}),
|
||||
sources: buildSourceMap(challengeFiles),
|
||||
loadEnzyme
|
||||
|
||||
@@ -15,7 +15,7 @@ Your code should have a `link` element.
|
||||
|
||||
```js
|
||||
// link is removed -> if exists, replaced with style
|
||||
const link = document.querySelector('body > style');
|
||||
const link = document.querySelector('head > style');
|
||||
assert(link);
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user