feat: improve file insertion (#45942)

This commit is contained in:
Oliver Eyton-Williams
2022-05-14 06:36:26 +02:00
committed by GitHub
parent 0dd9fc8d4f
commit 538e7c787b
6 changed files with 55 additions and 159 deletions

View File

@@ -96,6 +96,7 @@ exports.createPages = function createPages({ graphql, actions, reporter }) {
head
tail
history
fileKey
}
solutions {
contents

View File

@@ -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 })}`;
}

View File

@@ -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'
);
});
});

View File

@@ -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
];

View File

@@ -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

View File

@@ -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);
```