From e257c2969e88b4c9c036be593bf1dead288e96c3 Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Fri, 5 Sep 2025 15:22:38 +0200 Subject: [PATCH] fix: rebuild challenge pages if source is updated (#62056) --- client/gatsby-config.js | 4 +- client/utils/build-challenges.js | 13 +++- curriculum/build-curriculum.js | 21 +++++- curriculum/build-curriculum.test.js | 66 ++++++++++++++++++- .../gatsby-source-challenges/gatsby-node.js | 16 +++-- 5 files changed, 107 insertions(+), 13 deletions(-) diff --git a/client/gatsby-config.js b/client/gatsby-config.js index af5a4fcd6e8..19a51798cc5 100644 --- a/client/gatsby-config.js +++ b/client/gatsby-config.js @@ -2,7 +2,7 @@ const path = require('path'); const envData = require('./config/env.json'); const { buildChallenges, - replaceChallengeNode, + replaceChallengeNodes, localeChallengesRootDir } = require('./utils/build-challenges'); @@ -57,7 +57,7 @@ module.exports = { options: { name: 'challenges', source: buildChallenges, - onSourceChange: replaceChallengeNode(curriculumLocale), + onSourceChange: replaceChallengeNodes(curriculumLocale), curriculumPath: localeChallengesRootDir } }, diff --git a/client/utils/build-challenges.js b/client/utils/build-challenges.js index 3bf1f6ad9c9..04e0d8c86a8 100644 --- a/client/utils/build-challenges.js +++ b/client/utils/build-challenges.js @@ -10,6 +10,7 @@ const { getBlockCreator } = require('../../curriculum/build-curriculum'); const { getBlockStructure } = require('../../curriculum/file-handler'); +const { getSuperblocks } = require('../../curriculum/build-curriculum'); const { curriculumLocale } = envData; @@ -17,21 +18,27 @@ exports.localeChallengesRootDir = getContentDir(curriculumLocale); const blockCreator = getBlockCreator(curriculumLocale); -exports.replaceChallengeNode = () => { - return async function replaceChallengeNode(filePath) { +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}`); const meta = getBlockStructure(block); + const superblocks = getSuperblocks(block); - return await blockCreator.createChallenge({ + const challenge = await blockCreator.createChallenge({ filename, block, meta, isAudited: true }); + + return superblocks.map(superBlock => ({ + ...challenge, + superBlock + })); }; }; diff --git a/curriculum/build-curriculum.js b/curriculum/build-curriculum.js index dff383f3a88..b068bfdd7a5 100644 --- a/curriculum/build-curriculum.js +++ b/curriculum/build-curriculum.js @@ -244,6 +244,24 @@ function addBlockStructure( })); } +/** + * Returns a list of all the superblocks that contain the given block + * @param {string} block + */ +function getSuperblocks( + block, + _addSuperblockStructure = addSuperblockStructure +) { + const { superblocks } = getCurriculumStructure(); + const withStructure = _addSuperblockStructure(superblocks); + + return withStructure + .filter(({ blocks }) => + blocks.some(({ dashedName }) => dashedName === block) + ) + .map(({ name }) => name); +} + async function buildCurriculum(lang, filters) { const contentDir = getContentDir(lang); const builder = new SuperblockCreator({ @@ -289,5 +307,6 @@ module.exports = { getBlockStructure, getSuperblockStructure, createCommentMap, - superBlockToFilename + superBlockToFilename, + getSuperblocks }; diff --git a/curriculum/build-curriculum.test.js b/curriculum/build-curriculum.test.js index f78fe56afb2..29ff9347ae4 100644 --- a/curriculum/build-curriculum.test.js +++ b/curriculum/build-curriculum.test.js @@ -1,6 +1,15 @@ +jest.mock('./file-handler'); + const path = require('node:path'); -const { createCommentMap, addBlockStructure } = require('./build-curriculum'); +const { + createCommentMap, + addBlockStructure, + getSuperblocks +} = require('./build-curriculum'); +const { getCurriculumStructure } = require('./file-handler'); + +const mockGetCurriculumStructure = getCurriculumStructure; describe('createCommentMap', () => { const dictionaryDir = path.resolve(__dirname, '__fixtures__', 'dictionaries'); @@ -103,3 +112,58 @@ describe('addBlockStructure', () => { ]); }); }); + +describe('getSuperblocks', () => { + it('returns an empty array if no superblocks contain the given block', () => { + mockGetCurriculumStructure.mockReturnValue({ + superblocks: ['superblock-1'] // doesn't matter what this is, but must be defined + }); + const mockAddSuperblockStructure = () => [ + { + blocks: [{ dashedName: 'block-1' }], + name: 'superblock-1' + } + ]; + + expect( + getSuperblocks('nonexistent-block', mockAddSuperblockStructure) + ).toEqual([]); + }); + + it('returns an array with one superblock if one superblock contains the given block', () => { + mockGetCurriculumStructure.mockReturnValue({ + superblocks: ['superblock-1'] // doesn't matter what this is, but must be defined + }); + const mockAddSuperblockStructure = () => [ + { + blocks: [{ dashedName: 'block-1' }, { dashedName: 'block-2' }], + name: 'superblock-1' + } + ]; + + expect(getSuperblocks('block-1', mockAddSuperblockStructure)).toEqual([ + 'superblock-1' + ]); + }); + + it('returns an array with multiple superblocks if multiple superblocks contain the given block', () => { + mockGetCurriculumStructure.mockReturnValue({ + superblocks: ['superblock-1'] // doesn't matter what this is, but must be defined + }); + const mockAddSuperblockStructure = () => [ + { + blocks: [{ dashedName: 'block-1' }, { dashedName: 'block-2' }], + name: 'superblock-1' + }, + { + blocks: [{ dashedName: 'block-3' }, { dashedName: 'block-1' }], + name: 'superblock-2' + } + ]; + + expect(getSuperblocks('block-1', mockAddSuperblockStructure)).toEqual([ + 'superblock-1', + 'superblock-2' + ]); + }); +}); diff --git a/tools/client-plugins/gatsby-source-challenges/gatsby-node.js b/tools/client-plugins/gatsby-source-challenges/gatsby-node.js index 181325d4c03..54e2c5248c1 100644 --- a/tools/client-plugins/gatsby-source-challenges/gatsby-node.js +++ b/tools/client-plugins/gatsby-source-challenges/gatsby-node.js @@ -39,13 +39,15 @@ exports.sourceNodes = function sourceChallengesSourceNodes( watcher.on('change', filePath => /\.md?$/.test(filePath) ? onSourceChange(filePath) - .then(challenge => { + .then(challenges => { reporter.info( ` -File changed at ${filePath}, replacing challengeNode id ${challenge.id} +File changed at ${filePath}, replacing challengeNodes with ids ${challenges.map(({ id }) => id).join(', ')} ` ); - createVisibleChallenge(challenge, { isReloading: true }); + challenges.forEach(challenge => + createVisibleChallenge(challenge, { isReloading: true }) + ); }) .catch(e => reporter.error(`fcc-replace-challenge @@ -74,13 +76,15 @@ File changed at ${filePath}, replacing challengeNode id ${challenge.id} const { path: siblingPath } = entry; const relativePath = path.join(blockPath, siblingPath); onSourceChange(relativePath) - .then(challenge => { + .then(challenges => { reporter.info( ` -File changed at ${relativePath}, replacing challengeNode id ${challenge.id} +File changed at ${relativePath}, replacing challengeNodes with ids ${challenges.map(({ id }) => id).join(', ')} ` ); - createVisibleChallenge(challenge); + challenges.forEach(challenge => + createVisibleChallenge(challenge) + ); }) .catch(e => reporter.error(`fcc-replace-challenge