diff --git a/client/utils/build-challenges.js b/client/utils/build-challenges.js index 04e0d8c86a8..207330f7782 100644 --- a/client/utils/build-challenges.js +++ b/client/utils/build-challenges.js @@ -2,43 +2,69 @@ const path = require('path'); const _ = require('lodash'); -const envData = require('../config/env.json'); const { getChallengesForLang } = require('../../curriculum/get-challenges'); const { getContentDir, - getBlockCreator + getBlockCreator, + getSuperblocks, + superBlockToFilename } = require('../../curriculum/build-curriculum'); -const { getBlockStructure } = require('../../curriculum/file-handler'); -const { getSuperblocks } = require('../../curriculum/build-curriculum'); +const { + getBlockStructure, + getSuperblockStructure +} = require('../../curriculum/file-handler'); +const { transformSuperBlock } = require('../../curriculum/build-superblock'); +const { getSuperOrder } = require('../../curriculum/utils'); -const { curriculumLocale } = envData; +const curriculumLocale = process.env.CURRICULUM_LOCALE || 'english'; exports.localeChallengesRootDir = getContentDir(curriculumLocale); const blockCreator = getBlockCreator(curriculumLocale); +function getBlockMetadata(block, superBlock) { + // Compute metadata for the given block in the specified superblock + const sbFilename = superBlockToFilename[superBlock]; + const sbData = getSuperblockStructure(sbFilename); + const blocks = transformSuperBlock(sbData, { + showComingSoon: process.env.SHOW_UPCOMING_CHANGES === 'true' + }); + + const order = blocks.findIndex(b => b.dashedName === block); + const superOrder = getSuperOrder(superBlock); + + if (order === -1) { + throw new Error(`Block ${block} not found in superblock ${superBlock}`); + } + + return { order, superOrder }; +} + exports.replaceChallengeNodes = () => { return async function replaceChallengeNodes(filePath) { const parentDir = path.dirname(filePath); const block = path.basename(parentDir); const filename = path.basename(filePath); - console.log(`Replacing challenge node for ${filePath}`); + console.log(`Replacing challenge nodes for ${filePath}`); const meta = getBlockStructure(block); const superblocks = getSuperblocks(block); - const challenge = await blockCreator.createChallenge({ - filename, - block, - meta, - isAudited: true - }); + // Create a challenge for each superblock containing this block + const challenges = await Promise.all( + superblocks.map(async superBlock => { + const { order, superOrder } = getBlockMetadata(block, superBlock); + return blockCreator.createChallenge({ + filename, + block, + meta: { ...meta, superBlock, order, superOrder }, + isAudited: true + }); + }) + ); - return superblocks.map(superBlock => ({ - ...challenge, - superBlock - })); + return challenges; }; }; diff --git a/tools/client-plugins/gatsby-source-challenges/gatsby-node.js b/tools/client-plugins/gatsby-source-challenges/gatsby-node.js index 54e2c5248c1..a646bfe2f23 100644 --- a/tools/client-plugins/gatsby-source-challenges/gatsby-node.js +++ b/tools/client-plugins/gatsby-source-challenges/gatsby-node.js @@ -1,6 +1,4 @@ -const path = require('path'); const chokidar = require('chokidar'); -const readdirp = require('readdirp'); const { createChallengeNode } = require('./create-challenge-nodes'); @@ -36,68 +34,34 @@ exports.sourceNodes = function sourceChallengesSourceNodes( cwd: curriculumPath }); + function handleChallengeUpdate(filePath, action = 'changed') { + return onSourceChange(filePath) + .then(challenges => { + const actionText = action === 'added' ? 'creating' : 'replacing'; + reporter.info( + `Challenge file ${action}: ${filePath}, ${actionText} challengeNodes with ids ${challenges.map(({ id }) => id).join(', ')}` + ); + challenges.forEach(challenge => + createVisibleChallenge(challenge, { isReloading: true }) + ); + }) + .catch(e => + reporter.error( + `fcc-replace-challenge\nattempting to replace ${filePath}\n\n${e.message}\n${e.stack ? ` ${e.stack}` : ''}` + ) + ); + } + + // On file change, replace only the changed challenge. The key is ensuring + // onSourceChange returns a challenge with complete metadata. watcher.on('change', filePath => - /\.md?$/.test(filePath) - ? onSourceChange(filePath) - .then(challenges => { - reporter.info( - ` -File changed at ${filePath}, replacing challengeNodes with ids ${challenges.map(({ id }) => id).join(', ')} - ` - ); - challenges.forEach(challenge => - createVisibleChallenge(challenge, { isReloading: true }) - ); - }) - .catch(e => - reporter.error(`fcc-replace-challenge - attempting to replace ${filePath} - - ${e.message} - ${e.stack} - - `) - ) - : null + /\.md?$/.test(filePath) ? handleChallengeUpdate(filePath, 'changed') : null ); - // if a file is added, that might change the order of the challenges in the - // containing block, so we recreate them all + // On file add, replace just the new challenge. watcher.on('add', filePath => { - if (/\.md?$/.test(filePath)) { - const blockPath = path.dirname(filePath); - const fullBlockPath = path.join( - __dirname, - '../../../curriculum/challenges/english/', - blockPath - ); - readdirp(fullBlockPath, { fileFilter: '*.md' }) - .on('data', entry => { - const { path: siblingPath } = entry; - const relativePath = path.join(blockPath, siblingPath); - onSourceChange(relativePath) - .then(challenges => { - reporter.info( - ` -File changed at ${relativePath}, replacing challengeNodes with ids ${challenges.map(({ id }) => id).join(', ')} - ` - ); - challenges.forEach(challenge => - createVisibleChallenge(challenge) - ); - }) - .catch(e => - reporter.error(`fcc-replace-challenge -attempting to replace ${relativePath} - -${e.message} - -`) - ); - }) - .on('warn', error => console.error('non-fatal error', error)) - .on('error', error => console.error('fatal error', error)); - } + if (!/\.md?$/.test(filePath)) return; + handleChallengeUpdate(filePath, 'added'); }); function sourceAndCreateNodes() {