feat(learn): Added project-based curriculum tools (#39448)
* feat: added project-based curriculum tools * fix: allow script to work /w or /wo npm run * fix: moved console.log to reorder-steps function * fix: integrated bson-objectid library
This commit is contained in:
committed by
Mrugesh Mohapatra
parent
b9c9a95223
commit
7565849d7d
45
curriculum/package-lock.json
generated
45
curriculum/package-lock.json
generated
@ -3954,6 +3954,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"bson-objectid": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bson-objectid/-/bson-objectid-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-eQBNQXsisEAXlwiSy8zRNZdW2xDBJaEVkTPbodYR9hGxxtE548Qq7ilYOd8WAQ86xF7NRUdiWSQ1pa/TkKiE2A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"btoa-lite": {
|
"btoa-lite": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz",
|
||||||
@ -7420,6 +7426,18 @@
|
|||||||
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
|
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"gray-matter": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-7hB/+LxrOjq/dd8APlK0r24uL/67w7SkYnfwhNFwg/VDIGWGmduTDYf3WNstLW2fbbmRwrDGCVSJ2isuf2+4Hw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"js-yaml": "^3.11.0",
|
||||||
|
"kind-of": "^6.0.2",
|
||||||
|
"section-matter": "^1.0.0",
|
||||||
|
"strip-bom-string": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"growl": {
|
"growl": {
|
||||||
"version": "1.10.5",
|
"version": "1.10.5",
|
||||||
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
|
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
|
||||||
@ -14214,6 +14232,27 @@
|
|||||||
"xmlchars": "^2.1.1"
|
"xmlchars": "^2.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"section-matter": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"extend-shallow": "^2.0.1",
|
||||||
|
"kind-of": "^6.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"extend-shallow": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"is-extendable": "^0.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"semantic-release": {
|
"semantic-release": {
|
||||||
"version": "15.14.0",
|
"version": "15.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-15.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-15.14.0.tgz",
|
||||||
@ -15291,6 +15330,12 @@
|
|||||||
"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
|
"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"strip-bom-string": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"strip-eof": {
|
"strip-eof": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
|
||||||
|
@ -14,6 +14,9 @@
|
|||||||
"version": "0.0.0-next.4",
|
"version": "0.0.0-next.4",
|
||||||
"main": "lib.js",
|
"main": "lib.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"create-empty-steps": "cross-env CALLING_DIR=$INIT_CWD node ./tools/create-empty-steps",
|
||||||
|
"create-next-step": "cross-env CALLING_DIR=$INIT_CWD node ./tools/create-next-step",
|
||||||
|
"reorder-steps": "cross-env CALLING_DIR=$INIT_CWD node ./tools/reorder-steps",
|
||||||
"develop": "gulp",
|
"develop": "gulp",
|
||||||
"lint": "gulp lint",
|
"lint": "gulp lint",
|
||||||
"semantic-release": "semantic-release",
|
"semantic-release": "semantic-release",
|
||||||
@ -45,9 +48,11 @@
|
|||||||
"babel-preset-stage-0": "^6.3.13",
|
"babel-preset-stage-0": "^6.3.13",
|
||||||
"babel-preset-stage-3": "^6.24.1",
|
"babel-preset-stage-3": "^6.24.1",
|
||||||
"babel-standalone": "^6.26.0",
|
"babel-standalone": "^6.26.0",
|
||||||
|
"bson-objectid": "^1.3.1",
|
||||||
"chai": "4.2.0",
|
"chai": "4.2.0",
|
||||||
"cross-env": "^7.0.2",
|
"cross-env": "^7.0.2",
|
||||||
"fs-extra": "^6.0.1",
|
"fs-extra": "^6.0.1",
|
||||||
|
"gray-matter": "^4.0.2",
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.2",
|
||||||
"joi": "^13.3.0",
|
"joi": "^13.3.0",
|
||||||
"joi-objectid": "^2.0.0",
|
"joi-objectid": "^2.0.0",
|
||||||
|
74
curriculum/tools/README.md
Normal file
74
curriculum/tools/README.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# Free Code Camp Project-based Curriculum Tools
|
||||||
|
|
||||||
|
This repo is for tools to help facilitate the creation and maintenance of the Free Code Camp project-based curriculum.
|
||||||
|
|
||||||
|
## [create-next-step.js](create-next-step.js)
|
||||||
|
A one-off script that will automatically add the next step based on the last step numbered as `part-xxx.md` where `xxx` represents the 3-digit step number of the last step. The challenge seed code will use the previous step's challenge seed code with the editable region markers (ERMs) removed.
|
||||||
|
|
||||||
|
**Note:** This script also runs [reorder-steps.js](reorder-steps.js).
|
||||||
|
|
||||||
|
### How to run script:
|
||||||
|
1. Change to the directory of the project.
|
||||||
|
2. Run the following npm command:
|
||||||
|
```bash
|
||||||
|
npm run create-next-step
|
||||||
|
```
|
||||||
|
|
||||||
|
## [create-next-step.js](create-next-step.js)
|
||||||
|
A one-off script that will automatically add a specified numbers of steps at a specific starting step number. The challenge seed code will be empty.
|
||||||
|
|
||||||
|
**Note:** This script also runs [reorder-steps.js](reorder-steps.js).
|
||||||
|
|
||||||
|
### How to run script:
|
||||||
|
1. Change to the directory of the project.
|
||||||
|
2. Run the following npm command:
|
||||||
|
```bash
|
||||||
|
npm run create-empty-steps start=X num=Y # where X is the starting step number and Y is the number of steps to create.
|
||||||
|
```
|
||||||
|
|
||||||
|
## [reorder-steps.js](reorder-steps.js)
|
||||||
|
A one-off script that automatically reorders the step files in a project's markdown files based on the filename. It also updates the `challengeOrder` property array in the project's `meta.json` with the new order of the steps.
|
||||||
|
|
||||||
|
### Working Example
|
||||||
|
Let's say you start with the following project structure:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
part-1.md
|
||||||
|
part-2.md
|
||||||
|
part-3.md
|
||||||
|
part-4.md
|
||||||
|
part-5.md
|
||||||
|
part-6.md
|
||||||
|
```
|
||||||
|
At some point you decide you need to delete `part-2.md`, because that step is no longer needed. Also, you decide to break down `part-4.md` into three steps instead of just one.
|
||||||
|
|
||||||
|
To accomplish the this restructure, you would need to delete `part-2.md` and then add a `part-4a.md` and a `part=5b.md`. The new folder structure would look like the following:
|
||||||
|
```bash
|
||||||
|
part-001.md
|
||||||
|
part-003.md
|
||||||
|
part-004.md
|
||||||
|
part-004a.md
|
||||||
|
part-004b.md
|
||||||
|
part-005.md
|
||||||
|
part-006.md
|
||||||
|
```
|
||||||
|
You now need the file names to be `part-1.md` through `part-7.md`, because you removed one but gained two more for a net difference of one file. Also, the frontmatter of each file below a deleted step or added step will need to be modified by making the `title` key value match the new step number. For example, after renaming `part-3.md` to `part-2.md`, you would need to change `part-2.md`'s title from `Part 03` to `Part 02`.
|
||||||
|
|
||||||
|
See below for the actual project folder changes needed:
|
||||||
|
```bash
|
||||||
|
part-001.md
|
||||||
|
part-003.md renamed to part-002.md and title changes to "Part 2"
|
||||||
|
part-004.md renames to part-003.md and title changes to "Part 3"
|
||||||
|
part-004a.md renames to part-004.md and title changes to "Part 4"
|
||||||
|
part-004b.md renames to part-005.md and title changes to "Part 5"
|
||||||
|
part-005.md renames to part-006.md and title changes to "Part 6"
|
||||||
|
part-006.md renames to part-007.md and title changes to "Part 7"
|
||||||
|
```
|
||||||
|
Along with the above changes, the `challengeOrder` key in the project's `meta.json` file needs to reflect the new step order. This is needed because each step below a step deletion and/or step addtion changes the `title` assoiciated with each of the affected step's challenge `id`.
|
||||||
|
|
||||||
|
### Solution Steps
|
||||||
|
1. Change to the directory of the project.
|
||||||
|
2. Run the following npm command:
|
||||||
|
```bash
|
||||||
|
npm run reorder-steps
|
||||||
|
```
|
30
curriculum/tools/create-empty-steps.js
Normal file
30
curriculum/tools/create-empty-steps.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const { reorderSteps, createStepFile } = require('./utils');
|
||||||
|
|
||||||
|
const projectPath = (process.env.CALLING_DIR || process.cwd()) + path.sep;
|
||||||
|
const argValuePairs = process.argv.slice(2);
|
||||||
|
|
||||||
|
const args = argValuePairs.reduce((argsObj, arg) => {
|
||||||
|
const [argument, value] = arg.replace(/\s/g, '').split('=');
|
||||||
|
if (!argument || !value) {
|
||||||
|
throw `Invalid argument/value specified: ${arg}`;
|
||||||
|
}
|
||||||
|
return { ...argsObj, [argument]: value };
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
let { num, start } = args;
|
||||||
|
num = parseInt(num, 10);
|
||||||
|
const stepStart = parseInt(start, 10);
|
||||||
|
|
||||||
|
if (num > 20) {
|
||||||
|
throw 'No steps created. num arg val must be less than or equal to 20.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxStepNum = stepStart + num - 1;
|
||||||
|
|
||||||
|
for (let stepNum = stepStart; stepNum <= maxStepNum; stepNum++) {
|
||||||
|
createStepFile({ stepNum, projectPath });
|
||||||
|
}
|
||||||
|
console.log(`Sucessfully added ${num} steps`);
|
||||||
|
reorderSteps();
|
46
curriculum/tools/create-next-step.js
Normal file
46
curriculum/tools/create-next-step.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const { reorderSteps, createStepFile } = require('./utils');
|
||||||
|
|
||||||
|
const getLastStepFileContent = () => {
|
||||||
|
const filesArr = [];
|
||||||
|
fs.readdirSync(projectPath).forEach(fileName => {
|
||||||
|
if (
|
||||||
|
path.extname(fileName).toLowerCase() === '.md' &&
|
||||||
|
!fileName.endsWith('final.md')
|
||||||
|
) {
|
||||||
|
filesArr.push(fileName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileName = filesArr[filesArr.length - 1];
|
||||||
|
let lastStepFileNum = fileName.split('.')[0].split('-')[1];
|
||||||
|
lastStepFileNum = parseInt(lastStepFileNum, 10);
|
||||||
|
if (filesArr.length !== lastStepFileNum) {
|
||||||
|
throw `Error: The last file step is ${lastStepFileNum} and there are ${filesArr.length} files.`;
|
||||||
|
}
|
||||||
|
const fileContent = fs.readFileSync(projectPath + fileName, 'utf8');
|
||||||
|
const matchedSection = fileContent
|
||||||
|
.toString()
|
||||||
|
.match(/<section id='challengeSeed'>(?<challengeSeed>[\s\S]+)<\/section>/);
|
||||||
|
let finalChallengeSeed;
|
||||||
|
if (matchedSection) {
|
||||||
|
let {
|
||||||
|
groups: { challengeSeed }
|
||||||
|
} = matchedSection;
|
||||||
|
finalChallengeSeed = challengeSeed ? challengeSeed : '';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
nextStepNum: lastStepFileNum + 1,
|
||||||
|
challengeSeed: finalChallengeSeed
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const projectPath = (process.env.CALLING_DIR || process.cwd()) + path.sep;
|
||||||
|
|
||||||
|
const { nextStepNum, challengeSeed } = getLastStepFileContent();
|
||||||
|
|
||||||
|
createStepFile({ stepNum: nextStepNum, projectPath, challengeSeed });
|
||||||
|
console.log(`Sucessfully added step #${nextStepNum}`);
|
||||||
|
reorderSteps();
|
3
curriculum/tools/reorder-steps.js
Normal file
3
curriculum/tools/reorder-steps.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
const { reorderSteps } = require('./utils');
|
||||||
|
|
||||||
|
reorderSteps();
|
168
curriculum/tools/utils.js
Normal file
168
curriculum/tools/utils.js
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const matter = require('gray-matter');
|
||||||
|
const ObjectID = require('bson-objectid');
|
||||||
|
|
||||||
|
const padWithLeadingZeros = originalNum => {
|
||||||
|
/* always want file step numbers 3 digits */
|
||||||
|
const maxDigits = 3;
|
||||||
|
let paddedNum = '' + originalNum;
|
||||||
|
while (paddedNum.length < maxDigits) {
|
||||||
|
paddedNum = '0' + paddedNum;
|
||||||
|
}
|
||||||
|
return paddedNum;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeErms = seedCode => {
|
||||||
|
const lines = seedCode.split('\n');
|
||||||
|
return lines
|
||||||
|
.filter(line => !line.includes('--fcc-editable-region--'))
|
||||||
|
.join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
const createStepFile = ({ projectPath, stepNum, challengeSeed = '' }) => {
|
||||||
|
if (challengeSeed) {
|
||||||
|
challengeSeed = removeErms(challengeSeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
const challengeSeedSection = `<section id='challengeSeed'>
|
||||||
|
|
||||||
|
${challengeSeed.trim()}
|
||||||
|
|
||||||
|
</section>`;
|
||||||
|
|
||||||
|
const template = `---
|
||||||
|
id: ${ObjectID.generate()}
|
||||||
|
title: Part ${stepNum}
|
||||||
|
challengeType: 0
|
||||||
|
isHidden: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
<section id='description'>
|
||||||
|
|
||||||
|
step ${stepNum} instructions
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
<section id='tests'>
|
||||||
|
|
||||||
|
\`\`\`yml
|
||||||
|
tests:
|
||||||
|
- text: Test 1
|
||||||
|
testString: ''
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
## Challenge Seed
|
||||||
|
${challengeSeedSection}
|
||||||
|
`;
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
`${projectPath}part-${padWithLeadingZeros(stepNum)}.md`,
|
||||||
|
template
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reorderSteps = () => {
|
||||||
|
const projectPath = (process.env.CALLING_DIR || process.cwd()) + path.sep;
|
||||||
|
|
||||||
|
const projectName = process.env.CALLING_DIR
|
||||||
|
? process.env.CALLING_DIR.split(path.sep)
|
||||||
|
.slice(-1)
|
||||||
|
.toString()
|
||||||
|
: process
|
||||||
|
.cwd()
|
||||||
|
.split(path.sep)
|
||||||
|
.slice(-1)
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
const curriculumPath = process.env.CALLING_DIR
|
||||||
|
? ''
|
||||||
|
: '../../../../../curriculum';
|
||||||
|
|
||||||
|
const projectMetaPath = path.resolve(
|
||||||
|
curriculumPath,
|
||||||
|
'challenges',
|
||||||
|
'_meta',
|
||||||
|
projectName,
|
||||||
|
'meta.json'
|
||||||
|
);
|
||||||
|
|
||||||
|
let metaData;
|
||||||
|
try {
|
||||||
|
metaData = fs.readFileSync(projectMetaPath);
|
||||||
|
} catch (err) {
|
||||||
|
throw `No _meta.json file exists at ${projectMetaPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let foundFinal = false;
|
||||||
|
const filesArr = [];
|
||||||
|
fs.readdirSync(projectPath).forEach(fileName => {
|
||||||
|
if (path.extname(fileName).toLowerCase() === '.md') {
|
||||||
|
if (!fileName.endsWith('final.md')) {
|
||||||
|
filesArr.push(fileName);
|
||||||
|
} else {
|
||||||
|
foundFinal = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (foundFinal) {
|
||||||
|
filesArr.push('final.md');
|
||||||
|
}
|
||||||
|
|
||||||
|
const filesToReorder = filesArr.map((fileName, i) => {
|
||||||
|
const newStepNum = i + 1;
|
||||||
|
const newFileName =
|
||||||
|
fileName !== 'final.md'
|
||||||
|
? `part-${padWithLeadingZeros(newStepNum)}.md`
|
||||||
|
: 'final.md';
|
||||||
|
return {
|
||||||
|
oldFileName: fileName,
|
||||||
|
newFileName,
|
||||||
|
newStepNum
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const challengeOrder = [];
|
||||||
|
const parsedData = JSON.parse(metaData);
|
||||||
|
|
||||||
|
filesToReorder.forEach(({ oldFileName, newFileName, newStepNum }) => {
|
||||||
|
fs.renameSync(
|
||||||
|
`${projectPath}${oldFileName}`,
|
||||||
|
`${projectPath}${newFileName}.tmp`
|
||||||
|
);
|
||||||
|
const filePath = `${projectPath}${newFileName}.tmp`;
|
||||||
|
const frontMatter = matter.read(filePath);
|
||||||
|
const challengeID = frontMatter.data.id || ObjectID.generate();
|
||||||
|
const title =
|
||||||
|
newFileName === 'final.md' ? 'Final Prototype' : `Part ${newStepNum}`;
|
||||||
|
challengeOrder.push(['' + challengeID, title]);
|
||||||
|
const newData = {
|
||||||
|
...frontMatter.data,
|
||||||
|
id: challengeID,
|
||||||
|
title
|
||||||
|
};
|
||||||
|
fs.writeFileSync(filePath, frontMatter.stringify(newData));
|
||||||
|
});
|
||||||
|
|
||||||
|
filesToReorder.forEach(({ newFileName }) => {
|
||||||
|
fs.renameSync(
|
||||||
|
`${projectPath}${newFileName}.tmp`,
|
||||||
|
`${projectPath}${newFileName}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const newMeta = { ...parsedData, challengeOrder };
|
||||||
|
fs.writeFileSync(projectMetaPath, JSON.stringify(newMeta, null, 2));
|
||||||
|
console.log('Reordered steps');
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
padWithLeadingZeros,
|
||||||
|
createStepFile,
|
||||||
|
reorderSteps
|
||||||
|
};
|
Reference in New Issue
Block a user