diff --git a/api-server/package-lock.json b/api-server/package-lock.json index 2373bd874c..a40b8173f5 100644 --- a/api-server/package-lock.json +++ b/api-server/package-lock.json @@ -824,11 +824,6 @@ "to-fast-properties": "^2.0.0" } }, - "@freecodecamp/curriculum": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@freecodecamp/curriculum/-/curriculum-3.1.2.tgz", - "integrity": "sha512-buBBtAcGKagoUxSr3SuCycPlCySV9buou1Aod1uSbrKyNWS7hAIGdyloJiqPotEUfjh23uXm+ogvL9/Z4+QWhQ==" - }, "@freecodecamp/loopback-component-passport": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@freecodecamp/loopback-component-passport/-/loopback-component-passport-1.0.0.tgz", diff --git a/api-server/package.json b/api-server/package.json index 129655bd78..f29353b8af 100644 --- a/api-server/package.json +++ b/api-server/package.json @@ -27,7 +27,6 @@ }, "license": "(BSD-3-Clause AND CC-BY-SA-4.0)", "dependencies": { - "@freecodecamp/curriculum": "^3.1.1", "@freecodecamp/loopback-component-passport": "^1.0.0", "accepts": "^1.3.0", "auth0-js": "^9.5.1", diff --git a/client/gatsby-config.js b/client/gatsby-config.js index 535f12a86e..d0236b4018 100644 --- a/client/gatsby-config.js +++ b/client/gatsby-config.js @@ -1,6 +1,10 @@ const path = require('path'); -const { buildChallenges } = require('./utils/buildChallenges'); +const { + buildChallenges, + replaceChallengeNode, + localeChallengesRootDir +} = require('./utils/buildChallenges'); const { NODE_ENV: env, LOCALE: locale = 'english' } = process.env; @@ -36,7 +40,9 @@ module.exports = { resolve: 'fcc-source-challenges', options: { name: 'challenges', - source: buildChallenges + source: buildChallenges, + onSourceChange: replaceChallengeNode, + curriculumPath: localeChallengesRootDir } }, { diff --git a/client/package.json b/client/package.json index 1092c73986..9ed46318a1 100644 --- a/client/package.json +++ b/client/package.json @@ -13,7 +13,6 @@ "@fortawesome/free-regular-svg-icons": "^5.2.0", "@fortawesome/free-solid-svg-icons": "^5.2.0", "@fortawesome/react-fontawesome": "0.0.20", - "@freecodecamp/curriculum": "0.0.0-next.4", "@freecodecamp/react-bootstrap": "^0.32.3", "@reach/router": "^1.1.1", "axios": "^0.18.0", @@ -82,6 +81,7 @@ }, "devDependencies": { "babel-plugin-transform-imports": "^1.5.0", + "chokidar": "^2.0.4", "eslint": "^5.5.0", "eslint-config-freecodecamp": "^1.1.1", "jest": "^23.6.0", diff --git a/client/plugins/fcc-source-challenges/create-Challenge-nodes.js b/client/plugins/fcc-source-challenges/create-Challenge-nodes.js index e8e77fc633..59718353e6 100644 --- a/client/plugins/fcc-source-challenges/create-Challenge-nodes.js +++ b/client/plugins/fcc-source-challenges/create-Challenge-nodes.js @@ -1,6 +1,6 @@ const crypto = require('crypto'); -function createChallengeNodes(challenge, reporter) { +function createChallengeNode(challenge, reporter) { if (typeof challenge.description[0] !== 'string') { reporter.warn(` @@ -35,4 +35,4 @@ function createChallengeNodes(challenge, reporter) { ); } -exports.createChallengeNodes = createChallengeNodes; +exports.createChallengeNode = createChallengeNode; diff --git a/client/plugins/fcc-source-challenges/gatsby-node.js b/client/plugins/fcc-source-challenges/gatsby-node.js index 0d93484f9e..79ef44913e 100644 --- a/client/plugins/fcc-source-challenges/gatsby-node.js +++ b/client/plugins/fcc-source-challenges/gatsby-node.js @@ -1,31 +1,70 @@ -const { createChallengeNodes } = require('./create-Challenge-nodes'); +const chokidar = require('chokidar'); + +const { createChallengeNode } = require('./create-Challenge-nodes'); exports.sourceNodes = function sourceChallengesSourceNodes( { actions, reporter }, pluginOptions ) { - if (typeof pluginOptions.source !== 'function') { + const { source, onSourceChange, curriculumPath } = pluginOptions; + if (typeof source !== 'function') { reporter.panic(` -"source" is a required option for fcc-source-challenges. It must be a function -that delivers challenge files to the plugin - `); + "source" is a required option for fcc-source-challenges. It must be a + function that delivers challenge objects to the plugin + `); + } + if (typeof onSourceChange !== 'function') { + reporter.panic(` + "onSourceChange" is a required option for fcc-source-challenges. It must be + a function that delivers a new challenge object to the plugin + `); + } + if (typeof curriculumPath !== 'string') { + reporter.panic(` + "curriculumPath" is a required option for fcc-source-challenges. It must be + a path to a curriculum directory + `); } - // TODO: Add live seed updates const { createNode } = actions; + const watcher = chokidar.watch(curriculumPath, { + ignored: /(^|[\/\\])\../, + persistent: true + }); - const { source } = pluginOptions; - return source() - .then(challenges => - challenges - .filter(challenge => challenge.superBlock !== 'Certificates') - .map(challenge => createChallengeNodes(challenge, reporter)) - .map(node => createNode(node)) - ) - .catch(e => - reporter.panic(`fcc-source-challenges + watcher.on('ready', sourceAndCreateNodes).on( + 'change', + filePath => + (/\.md$/).test(filePath) + ? onSourceChange(filePath) + .then(challenge => { + reporter.info( + `File changed at ${filePath}, replacing challengeNode id ${ + challenge.id + }` + ); + return createChallengeNode(challenge, reporter); + }) + .then(createNode) + : null + ); + + function sourceAndCreateNodes() { + return source() + .then(challenges => Promise.all(challenges)) + .then(challenges => + challenges + .filter( + challenge => challenge.superBlock.toLowerCase() !== 'certificates' + ) + .map(challenge => createChallengeNode(challenge, reporter)) + .map(node => createNode(node)) + ) + .catch(e => + reporter.panic(`fcc-source-challenges ${e.message} `) - ); + ); + } }; diff --git a/client/utils/buildChallenges.js b/client/utils/buildChallenges.js index d0fdb313d9..44a19873dd 100644 --- a/client/utils/buildChallenges.js +++ b/client/utils/buildChallenges.js @@ -1,7 +1,12 @@ -const { getChallengesForLang } = require('@freecodecamp/curriculum'); +const path = require('path'); const _ = require('lodash'); -const utils = require('../utils'); +const { + getChallengesForLang, + createChallenge, + localeChallengesRootDir +} = require('../../curriculum/getChallenges'); +const utils = require('./'); const { locale } = require('../config/env.json'); const dasherize = utils.dasherize; @@ -10,8 +15,18 @@ const nameify = utils.nameify; const arrToString = arr => Array.isArray(arr) ? arr.join('\n') : _.toString(arr); +exports.localeChallengesRootDir = localeChallengesRootDir; + +exports.replaceChallengeNode = function replaceChallengeNode(fullFilePath) { + const relativeChallengePath = fullFilePath.replace( + localeChallengesRootDir + path.sep, + '' + ); + return createChallenge(relativeChallengePath); +}; + exports.buildChallenges = async function buildChallenges() { - const curriculum = await getChallengesForLang( locale ); + const curriculum = await getChallengesForLang(locale); const superBlocks = Object.keys(curriculum); const blocks = superBlocks .map(superBlock => curriculum[superBlock].blocks) @@ -20,54 +35,57 @@ exports.buildChallenges = async function buildChallenges() { return blocks.concat(_.flatten(currentBlocks)); }, []); - const builtChallenges = blocks.filter(block => !block.isPrivate).map(({ meta, challenges }) => { - const { - order, - time, - template, - required, - superBlock, - superOrder, - isPrivate, - dashedName: blockDashedName, - fileName - } = meta; + const builtChallenges = blocks + .filter(block => !block.isPrivate) + .map(({ meta, challenges }) => { + const { + order, + time, + template, + required, + superBlock, + superOrder, + isPrivate, + dashedName: blockDashedName, + fileName + } = meta; - return challenges.map(challenge => { - challenge.name = nameify(challenge.title); + return challenges.map(challenge => { + challenge.name = nameify(challenge.title); - challenge.dashedName = dasherize(challenge.name); + challenge.dashedName = dasherize(challenge.name); - if (challenge.files) { - challenge.files = _.reduce( - challenge.files, - (map, file) => { - map[file.key] = { - ...file, - head: arrToString(file.head), - contents: arrToString(file.contents), - tail: arrToString(file.tail) - }; - return map; - }, - {} - ); - } - challenge.fileName = fileName; - challenge.order = order; - challenge.block = blockDashedName; - challenge.isPrivate = challenge.isPrivate || isPrivate; - challenge.isRequired = !!challenge.isRequired; - challenge.time = time; - challenge.superOrder = superOrder; - challenge.superBlock = superBlock - .split('-') - .map(word => _.capitalize(word)) - .join(' '); - challenge.required = required; - challenge.template = template; - return challenge; - }); - }).reduce((accu, current) => accu.concat(current), []) + if (challenge.files) { + challenge.files = _.reduce( + challenge.files, + (map, file) => { + map[file.key] = { + ...file, + head: arrToString(file.head), + contents: arrToString(file.contents), + tail: arrToString(file.tail) + }; + return map; + }, + {} + ); + } + challenge.fileName = fileName; + challenge.order = order; + challenge.block = blockDashedName; + challenge.isPrivate = challenge.isPrivate || isPrivate; + challenge.isRequired = !!challenge.isRequired; + challenge.time = time; + challenge.superOrder = superOrder; + challenge.superBlock = superBlock + .split('-') + .map(word => _.capitalize(word)) + .join(' '); + challenge.required = required; + challenge.template = template; + return challenge; + }); + }) + .reduce((accu, current) => accu.concat(current), []); return builtChallenges; }; diff --git a/curriculum/getChallenges.js b/curriculum/getChallenges.js index 0173784775..0f44922ecf 100644 --- a/curriculum/getChallenges.js +++ b/curriculum/getChallenges.js @@ -1,12 +1,17 @@ const path = require('path'); const { findIndex } = require('lodash'); const readDirP = require('readdirp-walk'); - const { parseMarkdown } = require('@freecodecamp/challenge-md-parser'); const { dasherize } = require('./utils'); +const { locale } = require('../config/env.json'); const challengesDir = path.resolve(__dirname, './challenges'); +const localeChallengesRootDir = path.resolve(challengesDir, `./${locale}`); +const metaDir = path.resolve(challengesDir, '_meta'); +exports.challengesDir = challengesDir; +exports.localeChallengesRootDir = localeChallengesRootDir; +exports.metaDir = metaDir; exports.getChallengesForLang = function getChallengesForLang(lang) { let curriculum = {}; @@ -18,8 +23,7 @@ exports.getChallengesForLang = function getChallengesForLang(lang) { }; async function buildCurriculum(file, curriculum) { - - const { name, depth, path: filePath, fullPath, stat } = file; + const { name, depth, path: filePath, stat } = file; if (depth === 1 && stat.isDirectory()) { // extract the superBlock info const { order, name: superBlock } = superBlockInfo(name); @@ -41,9 +45,9 @@ async function buildCurriculum(file, curriculum) { if (name === 'meta.json' || name === '.DS_Store') { return; } + const block = getBlockNameFromPath(filePath); const { name: superBlock } = superBlockInfoFromPath(filePath); - const challenge = await parseMarkdown(fullPath); let challengeBlock; try { challengeBlock = curriculum[superBlock].blocks[block]; @@ -53,6 +57,26 @@ async function buildCurriculum(file, curriculum) { process.exit(0); } const { meta } = challengeBlock; + + const challenge = await createChallenge(filePath, meta); + + challengeBlock.challenges = [...challengeBlock.challenges, challenge]; +} + +async function createChallenge(challengeFilePath, maybeMeta) { + const fullPath = path.resolve(localeChallengesRootDir, challengeFilePath); + const metaPath = path.resolve( + metaDir, + `./${getBlockNameFromFullPath(fullPath)}/meta.json` + ); + let meta; + if (maybeMeta) { + meta = maybeMeta; + } else { + meta = require(metaPath); + } + const { name: superBlock } = superBlockInfoFromPath(challengeFilePath); + const challenge = await parseMarkdown(fullPath); const challengeOrder = findIndex( meta.challengeOrder, ([id]) => id === challenge.id @@ -64,9 +88,12 @@ async function buildCurriculum(file, curriculum) { challenge.superOrder = superOrder; challenge.superBlock = superBlock; challenge.challengeOrder = challengeOrder; - challengeBlock.challenges = [...challengeBlock.challenges, challenge]; + + return challenge; } +exports.createChallenge = createChallenge; + function superBlockInfoFromPath(filePath) { const [maybeSuper] = filePath.split(path.sep); return superBlockInfo(maybeSuper); @@ -89,3 +116,8 @@ function getBlockNameFromPath(filePath) { const [, block] = filePath.split(path.sep); return block; } + +function getBlockNameFromFullPath(fullFilePath) { + const [, block] = fullFilePath.split(path.sep).reverse(); + return block; +} diff --git a/curriculum/package.json b/curriculum/package.json index f86aa3b389..1177645426 100644 --- a/curriculum/package.json +++ b/curriculum/package.json @@ -14,11 +14,9 @@ "version": "0.0.0-next.4", "main": "lib.js", "scripts": { - "build": "gulp build", "develop": "gulp", "format": "prettier --write es5 './**/*.{js,json}' && npm run lint", "lint": "eslint ./**/*.js --fix", - "prepare": "npm run build", "repack": "babel-node ./repack.js", "semantic-release": "semantic-release", "test": "mocha --delay --reporter progress --bail", diff --git a/package.json b/package.json index 882c8e7622..a9d06c5a86 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "scripts": { "postinstall": "npm run bootstrap", "prebootstrap": "npm run ensure-env", - "bootstrap": "lerna bootstrap && lerna run build --scope @freecodecamp/curriculum", + "bootstrap": "lerna bootstrap", "clean": "lerna clean", "develop": "npm-run-all -s ensure-env start-develop", "ensure-env": "cross-env DEBUG=fcc:* node ./tools/scripts/ensure-env.js", diff --git a/tools/scripts/seed/package.json b/tools/scripts/seed/package.json index eac20c8bc8..0f42d5f853 100644 --- a/tools/scripts/seed/package.json +++ b/tools/scripts/seed/package.json @@ -17,7 +17,6 @@ }, "homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme", "devDependencies": { - "@freecodecamp/curriculum": "0.0.0-next.4", "debug": "^4.0.1", "dotenv": "^6.0.0", "jest": "^23.6.0", diff --git a/tools/scripts/seed/seedChallenges.js b/tools/scripts/seed/seedChallenges.js index e34beccf57..63aed02de3 100644 --- a/tools/scripts/seed/seedChallenges.js +++ b/tools/scripts/seed/seedChallenges.js @@ -2,10 +2,10 @@ const path = require('path'); const fs = require('fs'); require('dotenv').config({ path: path.resolve(__dirname, '../../../.env') }); const { MongoClient, ObjectID } = require('mongodb'); -const { getChallengesForLang } = require('@freecodecamp/curriculum'); const { flatten } = require('lodash'); const debug = require('debug'); +const { getChallengesForLang } = require('../../../curriculum/getChallenges'); const { createPathMigrationMap } = require('./createPathMigrationMap'); const log = debug('fcc:tools:seedChallenges');