diff --git a/api-server/common/utils/polyvinyl.js b/api-server/common/utils/polyvinyl.js deleted file mode 100644 index 790f8bd508..0000000000 --- a/api-server/common/utils/polyvinyl.js +++ /dev/null @@ -1,206 +0,0 @@ -// originally based off of https://github.com/gulpjs/vinyl -import invariant from 'invariant'; -import { Observable } from 'rx'; -import castToObservable from '../../server/utils/cast-to-observable'; - -// createFileStream( -// files: [...PolyVinyl] -// ) => Observable[...Observable[...PolyVinyl]] -export function createFileStream(files = []) { - return Observable.of(Observable.from(files)); -} - -// Observable::pipe( -// project( -// file: PolyVinyl -// ) => PolyVinyl|Observable[PolyVinyl]|Promise[PolyVinyl] -// ) => Observable[...Observable[...PolyVinyl]] -export function pipe(project) { - const source = this; - return source.map(files => - files.flatMap(file => castToObservable(project(file))) - ); -} - -// interface PolyVinyl { -// source: String, -// contents: String, -// name: String, -// ext: String, -// path: String, -// key: String, -// head: String, -// tail: String, -// history: [...String], -// error: Null|Object|Error -// } - -// createPoly({ -// name: String, -// ext: String, -// contents: String, -// history?: [...String], -// }) => PolyVinyl, throws -export function createPoly({ name, ext, contents, history, ...rest } = {}) { - invariant(typeof name === 'string', 'name must be a string but got %s', name); - - invariant(typeof ext === 'string', 'ext must be a string, but was %s', ext); - - invariant( - typeof contents === 'string', - 'contents must be a string but got %s', - contents - ); - - return { - ...rest, - history: Array.isArray(history) ? history : [name + ext], - name, - ext, - path: name + '.' + ext, - key: name + ext, - contents, - error: null - }; -} - -// isPoly(poly: Any) => Boolean -export function isPoly(poly) { - return ( - poly && - typeof poly.contents === 'string' && - typeof poly.name === 'string' && - typeof poly.ext === 'string' && - Array.isArray(poly.history) - ); -} - -// checkPoly(poly: Any) => Void, throws -export function checkPoly(poly) { - invariant( - isPoly(poly), - 'function should receive a PolyVinyl, but got %s', - poly - ); -} - -// isEmpty(poly: PolyVinyl) => Boolean, throws -export function isEmpty(poly) { - checkPoly(poly); - return !!poly.contents; -} - -// setContent(contents: String, poly: PolyVinyl) => PolyVinyl -// setContent will loose source if set -export function setContent(contents, poly) { - checkPoly(poly); - return { - ...poly, - contents, - source: null - }; -} - -// setExt(ext: String, poly: PolyVinyl) => PolyVinyl -export function setExt(ext, poly) { - checkPoly(poly); - const newPoly = { - ...poly, - ext, - path: poly.name + '.' + ext, - key: poly.name + ext - }; - newPoly.history = [...poly.history, newPoly.path]; - return newPoly; -} - -// setName(name: String, poly: PolyVinyl) => PolyVinyl -export function setName(name, poly) { - checkPoly(poly); - const newPoly = { - ...poly, - name, - path: name + '.' + poly.ext, - key: name + poly.ext - }; - newPoly.history = [...poly.history, newPoly.path]; - return newPoly; -} - -// setError(error: Object, poly: PolyVinyl) => PolyVinyl -export function setError(error, poly) { - invariant( - typeof error === 'object', - 'error must be an object or null, but got %', - error - ); - checkPoly(poly); - return { - ...poly, - error - }; -} - -// clearHeadTail(poly: PolyVinyl) => PolyVinyl -export function clearHeadTail(poly) { - checkPoly(poly); - return { - ...poly, - head: '', - tail: '' - }; -} - -// appendToTail (tail: String, poly: PolyVinyl) => PolyVinyl -export function appendToTail(tail, poly) { - checkPoly(poly); - return { - ...poly, - tail: poly.tail.concat(tail) - }; -} - -// compileHeadTail(padding: String, poly: PolyVinyl) => PolyVinyl -export function compileHeadTail(padding = '', poly) { - return clearHeadTail( - transformContents( - () => [poly.head, poly.contents, poly.tail].join(padding), - poly - ) - ); -} - -// transformContents( -// wrap: (contents: String) => String, -// poly: PolyVinyl -// ) => PolyVinyl -// transformContents will keep a copy of the original -// code in the `source` property. If the original polyvinyl -// already contains a source, this version will continue as -// the source property -export function transformContents(wrap, poly) { - const newPoly = setContent(wrap(poly.contents), poly); - // if no source exist, set the original contents as source - newPoly.source = poly.source || poly.contents; - return newPoly; -} - -// transformHeadTailAndContents( -// wrap: (source: String) => String, -// poly: PolyVinyl -// ) => PolyVinyl -export function transformHeadTailAndContents(wrap, poly) { - return { - ...transformContents(wrap, poly), - head: wrap(poly.head), - tail: wrap(poly.tail) - }; -} - -export function testContents(predicate, poly) { - return !!predicate(poly.contents); -} - -export function updateFileFromSpec(spec, poly) { - return setContent(poly.contents, createPoly(spec)); -} diff --git a/api-server/server/middlewares/validator.js b/api-server/server/middlewares/validator.js index 84bdefd7f5..8a703ec64c 100644 --- a/api-server/server/middlewares/validator.js +++ b/api-server/server/middlewares/validator.js @@ -1,5 +1,5 @@ import validator from 'express-validator'; -import { isPoly } from '../../common/utils/polyvinyl'; +import { isPoly } from '../../../utils/polyvinyl'; const isObject = val => !!val && typeof val === 'object'; diff --git a/client/gatsby-node.js b/client/gatsby-node.js index cb55e2a8d0..bd51d687ba 100644 --- a/client/gatsby-node.js +++ b/client/gatsby-node.js @@ -3,7 +3,7 @@ const env = require('../config/env'); const { createFilePath } = require('gatsby-source-filesystem'); const { dasherize } = require('../utils/slugs'); -const { blockNameify } = require('./utils/blockNameify'); +const { blockNameify } = require('../utils/block-nameify'); const { createChallengePages, createBlockIntroPages, diff --git a/client/src/components/Donation/DonationModal.js b/client/src/components/Donation/DonationModal.js index fdde372a5e..4f52394526 100644 --- a/client/src/components/Donation/DonationModal.js +++ b/client/src/components/Donation/DonationModal.js @@ -6,7 +6,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { Modal, Button, Col, Row } from '@freecodecamp/react-bootstrap'; import { Spacer } from '../helpers'; -import { blockNameify } from '../../../utils/blockNameify'; +import { blockNameify } from '../../../../utils/block-nameify'; import Heart from '../../assets/icons/Heart'; import Cup from '../../assets/icons/Cup'; import MinimalDonateForm from './MinimalDonateForm'; diff --git a/client/src/components/Map/components/Block.js b/client/src/components/Map/components/Block.js index 437178342b..12e23ab9eb 100644 --- a/client/src/components/Map/components/Block.js +++ b/client/src/components/Map/components/Block.js @@ -8,7 +8,7 @@ import { Link } from 'gatsby'; import { makeExpandedBlockSelector, toggleBlock } from '../redux'; import { completedChallengesSelector, executeGA } from '../../../redux'; import Caret from '../../../assets/icons/Caret'; -import { blockNameify } from '../../../../utils/blockNameify'; +import { blockNameify } from '../../../../../utils/block-nameify'; import GreenPass from '../../../assets/icons/GreenPass'; import GreenNotCompleted from '../../../assets/icons/GreenNotCompleted'; import IntroInformation from '../../../assets/icons/IntroInformation'; diff --git a/client/src/templates/Challenges/rechallenge/builders.js b/client/src/templates/Challenges/rechallenge/builders.js index 6e20ab18bb..f3f7db4356 100644 --- a/client/src/templates/Challenges/rechallenge/builders.js +++ b/client/src/templates/Challenges/rechallenge/builders.js @@ -8,7 +8,11 @@ import { template as _template } from 'lodash'; -import { compileHeadTail, setExt, transformContents } from '../utils/polyvinyl'; +import { + compileHeadTail, + setExt, + transformContents +} from '../../../../../utils/polyvinyl'; const htmlCatch = '\n\n'; const jsCatch = '\n;/*fcc*/\n'; diff --git a/client/src/templates/Challenges/rechallenge/transformers.js b/client/src/templates/Challenges/rechallenge/transformers.js index 90fd88394f..0dc65c2a0f 100644 --- a/client/src/templates/Challenges/rechallenge/transformers.js +++ b/client/src/templates/Challenges/rechallenge/transformers.js @@ -12,7 +12,7 @@ import { import protect from '@freecodecamp/loop-protect'; -import * as vinyl from '../utils/polyvinyl.js'; +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 diff --git a/client/src/templates/Challenges/redux/code-storage-epic.js b/client/src/templates/Challenges/redux/code-storage-epic.js index 5e74323f0b..4c7f300ca6 100644 --- a/client/src/templates/Challenges/redux/code-storage-epic.js +++ b/client/src/templates/Challenges/redux/code-storage-epic.js @@ -14,7 +14,7 @@ import { import { types as appTypes } from '../../../redux'; -import { setContent, isPoly } from '../utils/polyvinyl'; +import { setContent, isPoly } from '../../../../../utils/polyvinyl'; import { createFlashMessage } from '../../../components/Flash/redux'; diff --git a/client/src/templates/Challenges/redux/index.js b/client/src/templates/Challenges/redux/index.js index b8e94f9880..3ae8e8c452 100644 --- a/client/src/templates/Challenges/redux/index.js +++ b/client/src/templates/Challenges/redux/index.js @@ -2,7 +2,7 @@ import { createAction, handleActions } from 'redux-actions'; import { createTypes } from '../../../../utils/stateManagement'; -import { createPoly } from '../utils/polyvinyl'; +import { createPoly } from '../../../../../utils/polyvinyl'; import challengeModalEpic from './challenge-modal-epic'; import completionEpic from './completion-epic'; import codeLockEpic from './code-lock-epic'; diff --git a/client/utils/buildChallenges.js b/client/utils/buildChallenges.js index 9c02ffcbc8..90ab93665a 100644 --- a/client/utils/buildChallenges.js +++ b/client/utils/buildChallenges.js @@ -7,7 +7,7 @@ const { } = require('../../curriculum/getChallenges'); const { dasherize, nameify } = require('../../utils/slugs'); const { locale } = require('../config/env.json'); -const { blockNameify } = require('./blockNameify'); +const { blockNameify } = require('../../utils/block-nameify'); const arrToString = arr => Array.isArray(arr) ? arr.join('\n') : _.toString(arr); diff --git a/curriculum/getChallenges.js b/curriculum/getChallenges.js index e7fa732bd2..f0c42cdc55 100644 --- a/curriculum/getChallenges.js +++ b/curriculum/getChallenges.js @@ -6,8 +6,6 @@ const fs = require('fs'); const { dasherize } = require('../utils/slugs'); -const { challengeSchemaValidator } = require('./schema/challengeSchema'); - const challengesDir = path.resolve(__dirname, './challenges'); const metaDir = path.resolve(challengesDir, '_meta'); exports.challengesDir = challengesDir; @@ -38,13 +36,13 @@ exports.getChallengesForLang = function getChallengesForLang(lang) { readDirP({ root: getChallengesDirForLang(lang) }) .on('data', file => { running++; - buildCurriculum(file, curriculum, lang).then(done); + buildCurriculum(file, curriculum).then(done); }) .on('end', done); }); }; -async function buildCurriculum(file, curriculum, lang) { +async function buildCurriculum(file, curriculum) { const { name, depth, path: filePath, fullPath, stat } = file; if (depth === 1 && stat.isDirectory()) { // extract the superBlock info @@ -80,12 +78,12 @@ async function buildCurriculum(file, curriculum, lang) { } const { meta } = challengeBlock; - const challenge = await createChallenge(fullPath, meta, lang); + const challenge = await createChallenge(fullPath, meta); challengeBlock.challenges = [...challengeBlock.challenges, challenge]; } -async function createChallenge(fullPath, maybeMeta, lang) { +async function createChallenge(fullPath, maybeMeta) { let meta; if (maybeMeta) { meta = maybeMeta; @@ -98,11 +96,6 @@ async function createChallenge(fullPath, maybeMeta, lang) { } const { name: superBlock } = superBlockInfoFromFullPath(fullPath); const challenge = await parseMarkdown(fullPath); - const result = challengeSchemaValidator(lang)(challenge); - if (result.error) { - console.log(result.value); - throw new Error(result.error); - } const challengeOrder = findIndex( meta.challengeOrder, ([id]) => id === challenge.id diff --git a/curriculum/test/test-challenges.js b/curriculum/test/test-challenges.js index dcfc44495b..1a9f074162 100644 --- a/curriculum/test/test-challenges.js +++ b/curriculum/test/test-challenges.js @@ -53,9 +53,7 @@ const { buildJSChallenge } = require('../../client/src/templates/Challenges/utils/build'); -const { - createPoly -} = require('../../client/src/templates/Challenges/utils/polyvinyl'); +const { createPoly } = require('../../utils/polyvinyl'); const testEvaluator = require('../../client/config/test-evaluator').filename; @@ -323,24 +321,24 @@ function populateTestsForLang({ lang, challenges }, meta) { }); let { files = [] } = challenge; - let createTestRunner; if (challengeType === challengeTypes.backend) { it('Check tests is not implemented.'); return; - } else if ( - challengeType === challengeTypes.js || - challengeType === challengeTypes.bonfire - ) { - createTestRunner = createTestRunnerForJSChallenge; - } else if (files.length === 1) { - createTestRunner = createTestRunnerForDOMChallenge; - } else { + } + + if (files.length > 1) { it('Check tests.', () => { throw new Error('Seed file should be only the one.'); }); return; } + const buildChallenge = + challengeType === challengeTypes.js || + challengeType === challengeTypes.bonfire + ? buildJSChallenge + : buildDOMChallenge; + files = files.map(createPoly); it('Test suite must fail on the initial contents', async function() { this.timeout(5000 * tests.length + 1000); @@ -353,7 +351,7 @@ function populateTestsForLang({ lang, challenges }, meta) { testRunner = await createTestRunner( { ...challenge, files }, '', - page + buildChallenge ); } catch { fails = true; @@ -390,7 +388,7 @@ function populateTestsForLang({ lang, challenges }, meta) { const testRunner = await createTestRunner( { ...challenge, files }, solution, - page + buildChallenge ); for (const test of tests) { await testRunner(test); @@ -404,41 +402,29 @@ function populateTestsForLang({ lang, challenges }, meta) { }); } -async function createTestRunnerForDOMChallenge( +async function createTestRunner( { required = [], template, files }, solution, - context + buildChallenge ) { if (solution) { files[0].contents = solution; } - const { build, sources, loadEnzyme } = await buildDOMChallenge({ + const { build, sources, loadEnzyme } = await buildChallenge({ files, required, template }); + const code = sources && 'index' in sources ? sources['index'] : ''; - await context.reload(); - await context.setContent(build); - await context.evaluate( - async (sources, loadEnzyme) => { - const code = sources && 'index' in sources ? sources['index'] : ''; - const getUserInput = fileName => sources[fileName]; - await document.__initTestFrame({ code, getUserInput, loadEnzyme }); - }, - sources, - loadEnzyme - ); + const evaluator = await (buildChallenge === buildDOMChallenge + ? getContextEvaluator(build, sources, code, loadEnzyme) + : getWorkerEvaluator(build, sources, code)); return async ({ text, testString }) => { try { - const { pass, err } = await Promise.race([ - new Promise((_, reject) => setTimeout(() => reject('timeout'), 5000)), - await context.evaluate(async testString => { - return await document.__runTest(testString); - }, testString) - ]); + const { pass, err } = await evaluator.evaluate(testString, 5000); if (!pass) { throw new AssertionError(err.message); } @@ -448,30 +434,45 @@ async function createTestRunnerForDOMChallenge( }; } -async function createTestRunnerForJSChallenge({ files }, solution) { - if (solution) { - files[0].contents = solution; - } +async function getContextEvaluator(build, sources, code, loadEnzyme) { + await initializeTestRunner(build, sources, code, loadEnzyme); - const { build, sources } = await buildJSChallenge({ files }); - const code = sources && 'index' in sources ? sources['index'] : ''; - - const testWorker = createWorker(testEvaluator, { terminateWorker: true }); - return async ({ text, testString }) => { - try { - const { pass, err } = await testWorker.execute( - { testString, build, code, sources }, - 5000 - ).done; - if (!pass) { - throw new AssertionError(err.message); - } - } catch (err) { - reThrow(err, text); - } + return { + evaluate: async (testString, timeout) => + Promise.race([ + new Promise((_, reject) => + setTimeout(() => reject('timeout'), timeout) + ), + await page.evaluate(async testString => { + return await document.__runTest(testString); + }, testString) + ]) }; } +async function getWorkerEvaluator(build, sources, code) { + const testWorker = createWorker(testEvaluator, { terminateWorker: true }); + return { + evaluate: async (testString, timeout) => + await testWorker.execute({ testString, build, code, sources }, timeout) + .done + }; +} + +async function initializeTestRunner(build, sources, code, loadEnzyme) { + await page.reload(); + await page.setContent(build); + await page.evaluate( + async (code, sources, loadEnzyme) => { + const getUserInput = fileName => sources[fileName]; + await document.__initTestFrame({ code, getUserInput, loadEnzyme }); + }, + code, + sources, + loadEnzyme + ); +} + function reThrow(err, text) { if (typeof err === 'string') { throw new AssertionError( diff --git a/client/utils/blockNameify.js b/utils/block-nameify.js similarity index 100% rename from client/utils/blockNameify.js rename to utils/block-nameify.js diff --git a/client/src/templates/Challenges/utils/polyvinyl.js b/utils/polyvinyl.js similarity index 76% rename from client/src/templates/Challenges/utils/polyvinyl.js rename to utils/polyvinyl.js index dd2594ff36..cc725f65fb 100644 --- a/client/src/templates/Challenges/utils/polyvinyl.js +++ b/utils/polyvinyl.js @@ -1,14 +1,17 @@ // originally based off of https://github.com/gulpjs/vinyl -import invariant from 'invariant'; -import { of, from, isObservable } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; +const invariant = require('invariant'); +const { of, from, isObservable } = require('rxjs'); +const { map, switchMap } = require('rxjs/operators'); -export const isPromise = value => - value && - typeof value.subscribe !== 'function' && - typeof value.then === 'function'; +function isPromise(value) { + return ( + value && + typeof value.subscribe !== 'function' && + typeof value.then === 'function' + ); +} -export function castToObservable(maybe) { +function castToObservable(maybe) { if (isObservable(maybe)) { return maybe; } @@ -21,7 +24,7 @@ export function castToObservable(maybe) { // createFileStream( // files: [...PolyVinyl] // ) => Observable[...Observable[...PolyVinyl]] -export function createFileStream(files = []) { +function createFileStream(files = []) { return of(from(files)); } @@ -30,7 +33,7 @@ export function createFileStream(files = []) { // file: PolyVinyl // ) => PolyVinyl|Observable[PolyVinyl]|Promise[PolyVinyl] // ) => Observable[...Observable[...PolyVinyl]] -export function pipe(project) { +function pipe(project) { const source = this; return source.pipe( map(files => { @@ -58,7 +61,7 @@ export function pipe(project) { // contents: String, // history?: [...String], // }) => PolyVinyl, throws -export function createPoly({ name, ext, contents, history, ...rest } = {}) { +function createPoly({ name, ext, contents, history, ...rest } = {}) { invariant(typeof name === 'string', 'name must be a string but got %s', name); invariant(typeof ext === 'string', 'ext must be a string, but was %s', ext); @@ -82,7 +85,7 @@ export function createPoly({ name, ext, contents, history, ...rest } = {}) { } // isPoly(poly: Any) => Boolean -export function isPoly(poly) { +function isPoly(poly) { return ( poly && typeof poly.contents === 'string' && @@ -93,7 +96,7 @@ export function isPoly(poly) { } // checkPoly(poly: Any) => Void, throws -export function checkPoly(poly) { +function checkPoly(poly) { invariant( isPoly(poly), 'function should receive a PolyVinyl, but got %s', @@ -102,14 +105,14 @@ export function checkPoly(poly) { } // isEmpty(poly: PolyVinyl) => Boolean, throws -export function isEmpty(poly) { +function isEmpty(poly) { checkPoly(poly); return !!poly.contents; } // setContent(contents: String, poly: PolyVinyl) => PolyVinyl // setContent will loose source if set -export function setContent(contents, poly) { +function setContent(contents, poly) { checkPoly(poly); return { ...poly, @@ -119,7 +122,7 @@ export function setContent(contents, poly) { } // setExt(ext: String, poly: PolyVinyl) => PolyVinyl -export function setExt(ext, poly) { +function setExt(ext, poly) { checkPoly(poly); const newPoly = { ...poly, @@ -132,7 +135,7 @@ export function setExt(ext, poly) { } // setName(name: String, poly: PolyVinyl) => PolyVinyl -export function setName(name, poly) { +function setName(name, poly) { checkPoly(poly); const newPoly = { ...poly, @@ -145,7 +148,7 @@ export function setName(name, poly) { } // setError(error: Object, poly: PolyVinyl) => PolyVinyl -export function setError(error, poly) { +function setError(error, poly) { invariant( typeof error === 'object', 'error must be an object or null, but got %', @@ -159,7 +162,7 @@ export function setError(error, poly) { } // clearHeadTail(poly: PolyVinyl) => PolyVinyl -export function clearHeadTail(poly) { +function clearHeadTail(poly) { checkPoly(poly); return { ...poly, @@ -169,7 +172,7 @@ export function clearHeadTail(poly) { } // appendToTail (tail: String, poly: PolyVinyl) => PolyVinyl -export function appendToTail(tail, poly) { +function appendToTail(tail, poly) { checkPoly(poly); return { ...poly, @@ -178,7 +181,7 @@ export function appendToTail(tail, poly) { } // compileHeadTail(padding: String, poly: PolyVinyl) => PolyVinyl -export function compileHeadTail(padding = '', poly) { +function compileHeadTail(padding = '', poly) { return clearHeadTail( transformContents( () => [poly.head, poly.contents, poly.tail].join(padding), @@ -195,7 +198,7 @@ export function compileHeadTail(padding = '', poly) { // code in the `source` property. If the original polyvinyl // already contains a source, this version will continue as // the source property -export function transformContents(wrap, poly) { +function transformContents(wrap, poly) { const newPoly = setContent(wrap(poly.contents), poly); // if no source exist, set the original contents as source newPoly.source = poly.source || poly.contents; @@ -206,7 +209,7 @@ export function transformContents(wrap, poly) { // wrap: (source: String) => String, // poly: PolyVinyl // ) => PolyVinyl -export function transformHeadTailAndContents(wrap, poly) { +function transformHeadTailAndContents(wrap, poly) { return { ...transformContents(wrap, poly), head: wrap(poly.head), @@ -214,10 +217,32 @@ export function transformHeadTailAndContents(wrap, poly) { }; } -export function testContents(predicate, poly) { +function testContents(predicate, poly) { return !!predicate(poly.contents); } -export function updateFileFromSpec(spec, poly) { +function updateFileFromSpec(spec, poly) { return setContent(poly.contents, createPoly(spec)); } + +module.exports = { + isPromise, + castToObservable, + createFileStream, + pipe, + createPoly, + isPoly, + checkPoly, + isEmpty, + setContent, + setExt, + setName, + setError, + clearHeadTail, + appendToTail, + compileHeadTail, + transformContents, + transformHeadTailAndContents, + testContents, + updateFileFromSpec +};