feat: update challenge helpers to handle id filenames (#44769)
* refactor: light tweaks for readability * refactor: simplify metadata functions * fix: most tests * test: fix utils tests * test: simplify mocks * WIP: update get-last-step-file-content * feat: finish create-next-step * fix: type error * test: provide mock meta.json for test * refactor: get meta path from project path * refactor: get project name from path * refactor: simplify getProjectMetaPath further Also removes some excessive mocking * refactor: remove more mocks, always clear .env * feat: update create-next-step * feat: update create-empty steps Also refactors slightly, so it's easier to insert steps into the meta * docs: update challenge-helper-script docs * feat: create-step-between * refactor: allow metadata parse errors to propagate * fix: convert reorderSteps to renameSteps * refactor: create-step-between -> insert-step * feat: update delete-step * refactor: consolidate commands into commands.ts * refactor: clean up and consolidation * refactor: more cleanup * fix: make cli args consistent Everything accepts a single integer and nothing else * refactor: renameSteps -> updateStepTitles * docs: update with the names and args * feat: add step validating meta + files are synced
This commit is contained in:
committed by
GitHub
parent
16e7cdedb1
commit
339c6713d2
@ -21,10 +21,10 @@
|
|||||||
"build": "ts-node --project ../tsconfig.json ../tools/scripts/build/build-curriculum",
|
"build": "ts-node --project ../tsconfig.json ../tools/scripts/build/build-curriculum",
|
||||||
"create-empty-steps": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/create-empty-steps",
|
"create-empty-steps": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/create-empty-steps",
|
||||||
"create-next-step": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/create-next-step",
|
"create-next-step": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/create-next-step",
|
||||||
"create-step-between": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/create-step-between",
|
"insert-step": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/insert-step",
|
||||||
"delete-step": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/delete-step",
|
"delete-step": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/delete-step",
|
||||||
"lint": "ts-node --project ../tsconfig.json lint-localized",
|
"lint": "ts-node --project ../tsconfig.json lint-localized",
|
||||||
"reorder-steps": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/reorder-steps",
|
"update-step-titles": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/update-step-titles",
|
||||||
"test": "mocha --delay --exit --reporter progress --bail",
|
"test": "mocha --delay --exit --reporter progress --bail",
|
||||||
"test:full-output": "cross-env FULL_OUTPUT=true mocha --delay --reporter progress"
|
"test:full-output": "cross-env FULL_OUTPUT=true mocha --delay --reporter progress"
|
||||||
},
|
},
|
||||||
|
@ -10,9 +10,7 @@ If you want to create new steps, the following tools simplify that process.
|
|||||||
|
|
||||||
## create-next-step
|
## create-next-step
|
||||||
|
|
||||||
A one-off script that will automatically add the next step based on the last step numbered as `step-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.
|
A one-off script that will automatically add the next step based on the last step in the project. 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](#reorder-steps).
|
|
||||||
|
|
||||||
### How to run script:
|
### How to run script:
|
||||||
|
|
||||||
@ -25,9 +23,7 @@ npm run create-next-step
|
|||||||
|
|
||||||
## create-empty-steps
|
## create-empty-steps
|
||||||
|
|
||||||
A one-off script that automatically adds a specified number of steps at a specific starting step number. The challenge seed code for all steps created will be empty.
|
A one-off script that automatically adds a specified number of steps. The challenge seed code for all steps created will be empty.
|
||||||
|
|
||||||
**Note:** This script also runs [reorder-steps](#reorder-steps).
|
|
||||||
|
|
||||||
### How to run script:
|
### How to run script:
|
||||||
|
|
||||||
@ -35,14 +31,14 @@ A one-off script that automatically adds a specified number of steps at a specif
|
|||||||
2. Run the following npm command:
|
2. Run the following npm command:
|
||||||
|
|
||||||
```bash
|
```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.
|
npm run create-empty-steps X # where X is the number of steps to create.
|
||||||
```
|
```
|
||||||
|
|
||||||
## create-step-between
|
## insert-step
|
||||||
|
|
||||||
A one-off script that automatically adds a new step between two existing consecutive steps. The challenge seed code will use the existing starting step's challenge seed code with the editable region markers (ERMs) removed.
|
A one-off script that automatically adds a new step at a specified position, incrementing all subsequent steps (both their titles and in their meta.json). 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](#reorder-steps).
|
**Note:** This script also runs [update-step-titles](#update-step-titles).
|
||||||
|
|
||||||
### How to run script:
|
### How to run script:
|
||||||
|
|
||||||
@ -50,12 +46,14 @@ A one-off script that automatically adds a new step between two existing consecu
|
|||||||
2. Run the following npm command:
|
2. Run the following npm command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run create-step-between start=X # where X is the starting step number
|
npm run insert-step X # where X is the position to insert the new step.
|
||||||
```
|
```
|
||||||
|
|
||||||
## delete-step
|
## delete-step
|
||||||
|
|
||||||
A one-off script that deletes an existing step and then reorders the remaining step files in the project's folder as well as updates the `challengeOrder` property array in the project's `meta.json` with the new order of the steps.
|
A one-off script that deletes an existing step, decrementing all subsequent steps (both their titles and in their meta.json)
|
||||||
|
|
||||||
|
**Note:** This script also runs [update-step-titles](#update-step-titles).
|
||||||
|
|
||||||
### How to run script
|
### How to run script
|
||||||
|
|
||||||
@ -63,55 +61,12 @@ A one-off script that deletes an existing step and then reorders the remaining s
|
|||||||
2. Run the following npm command:
|
2. Run the following npm command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run delete-step num=x # where x is the step number to be deleted.
|
npm run delete-step X # where X is the step number to be deleted.
|
||||||
```
|
```
|
||||||
|
|
||||||
## reorder-steps
|
## update-step-titles
|
||||||
|
|
||||||
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.
|
A one-off script that automatically updates the frontmatter in a project's markdown files so that they are consistent with the project's meta.json. It ensures that each step's title (and dashedName) match the meta's challengeOrder.
|
||||||
|
|
||||||
### Working Example
|
|
||||||
|
|
||||||
Let's say you start with the following project structure:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
step-001.md
|
|
||||||
step-002.md
|
|
||||||
step-003.md
|
|
||||||
step-004.md
|
|
||||||
step-005.md
|
|
||||||
step-006.md
|
|
||||||
```
|
|
||||||
|
|
||||||
At some point you decide you need to delete `step-002.md`, because that step is no longer needed. Also, you decide to break down `step-004.md` into three steps instead of just one.
|
|
||||||
|
|
||||||
To accomplish this restructure, you would need to delete `step-002.md` and then add a `step-004a.md` and a `step-004b.md`. The new folder structure would look like the following:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
step-001.md
|
|
||||||
step-003.md
|
|
||||||
step-004.md
|
|
||||||
step-004a.md
|
|
||||||
step-004b.md
|
|
||||||
step-005.md
|
|
||||||
step-006.md
|
|
||||||
```
|
|
||||||
|
|
||||||
You now need the file names to be `step-001.md` through `step-007.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 `step-3.md` to `step-2.md`, you would need to change `step-2.md`'s title from `Step 03` to `Step 02`.
|
|
||||||
|
|
||||||
See below for the actual project folder changes needed:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
step-001.md
|
|
||||||
step-003.md renamed to step-002.md and title changes to "Step 2"
|
|
||||||
step-004.md renames to step-003.md and title changes to "Step 3"
|
|
||||||
step-004a.md renames to step-004.md and title changes to "Step 4"
|
|
||||||
step-004b.md renames to step-005.md and title changes to "Step 5"
|
|
||||||
step-005.md renames to step-006.md and title changes to "Step 6"
|
|
||||||
step-006.md renames to step-007.md and title changes to "Step 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 addition changes the `title` associated with each of the affected step's challenge `id`.
|
|
||||||
|
|
||||||
### How to run script
|
### How to run script
|
||||||
|
|
||||||
@ -119,5 +74,5 @@ Along with the above changes, the `challengeOrder` key in the project's `meta.js
|
|||||||
2. Run the following npm command:
|
2. Run the following npm command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run reorder-steps
|
npm run update-step-titles
|
||||||
```
|
```
|
||||||
|
73
tools/challenge-helper-scripts/commands.ts
Normal file
73
tools/challenge-helper-scripts/commands.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import { getProjectPath } from './helpers/get-project-info';
|
||||||
|
import { getMetaData } from './helpers/project-metadata';
|
||||||
|
import {
|
||||||
|
createStepFile,
|
||||||
|
deleteStepFromMeta,
|
||||||
|
getChallengeSeeds,
|
||||||
|
insertStepIntoMeta,
|
||||||
|
updateStepTitles
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
function deleteStep(stepNum: number) {
|
||||||
|
if (stepNum < 1) {
|
||||||
|
throw 'Step not deleted. Step num must be a number greater than 0.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const challengeOrder = getMetaData().challengeOrder;
|
||||||
|
|
||||||
|
if (stepNum > challengeOrder.length)
|
||||||
|
throw `Step # ${stepNum} not deleted. Largest step number is ${challengeOrder.length}.`;
|
||||||
|
|
||||||
|
const stepId = challengeOrder[stepNum - 1][0];
|
||||||
|
|
||||||
|
fs.unlinkSync(`${getProjectPath()}${stepId}.md`);
|
||||||
|
deleteStepFromMeta({ stepNum });
|
||||||
|
updateStepTitles();
|
||||||
|
|
||||||
|
console.log(`Sucessfully deleted step #${stepNum}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertStep(stepNum: number) {
|
||||||
|
if (stepNum < 1) {
|
||||||
|
throw 'Step not inserted. New step number must be greater than 0.';
|
||||||
|
}
|
||||||
|
const challengeOrder = getMetaData().challengeOrder;
|
||||||
|
|
||||||
|
if (stepNum > challengeOrder.length + 1)
|
||||||
|
throw `Step not inserted. New step number must be less than ${
|
||||||
|
challengeOrder.length + 2
|
||||||
|
}.`;
|
||||||
|
|
||||||
|
const challengeSeeds =
|
||||||
|
stepNum > 1
|
||||||
|
? getChallengeSeeds(
|
||||||
|
`${getProjectPath()}${challengeOrder[stepNum - 2][0]}.md`
|
||||||
|
)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const stepId = createStepFile({
|
||||||
|
stepNum,
|
||||||
|
challengeSeeds
|
||||||
|
});
|
||||||
|
|
||||||
|
insertStepIntoMeta({ stepNum, stepId });
|
||||||
|
updateStepTitles();
|
||||||
|
console.log(`Sucessfully inserted new step #${stepNum}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptySteps(num: number) {
|
||||||
|
if (num < 1 || num > 1000) {
|
||||||
|
throw `No steps created. arg 'num' must be between 1 and 1000 inclusive`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStepNum = getMetaData().challengeOrder.length + 1;
|
||||||
|
|
||||||
|
for (let stepNum = nextStepNum; stepNum < nextStepNum + num; stepNum++) {
|
||||||
|
const stepId = createStepFile({ stepNum });
|
||||||
|
insertStepIntoMeta({ stepNum, stepId });
|
||||||
|
}
|
||||||
|
console.log(`Sucessfully added ${num} steps`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { deleteStep, insertStep, createEmptySteps };
|
@ -1,38 +1,6 @@
|
|||||||
import { getArgValues } from './helpers/get-arg-values';
|
import { getArgValue } from './helpers/get-arg-value';
|
||||||
import { getExistingStepNums } from './helpers/get-existing-step-nums';
|
import { createEmptySteps } from './commands';
|
||||||
import { getProjectPath } from './helpers/get-project-path';
|
import { validateMetaData } from './helpers/project-metadata';
|
||||||
import { createStepFile, reorderSteps } from './utils';
|
|
||||||
|
|
||||||
const anyStepExists = (steps: number[], stepsToFind: number[]) =>
|
validateMetaData();
|
||||||
stepsToFind.some(num => steps.includes(num));
|
createEmptySteps(getArgValue(process.argv));
|
||||||
|
|
||||||
const projectPath = getProjectPath();
|
|
||||||
const args = getArgValues(process.argv);
|
|
||||||
|
|
||||||
const { num: numString, start: startString } = args;
|
|
||||||
if (!startString) {
|
|
||||||
throw `No steps created. start arg val must be specified.`;
|
|
||||||
}
|
|
||||||
if (!numString) {
|
|
||||||
throw `No steps created. num arg val must be specified.`;
|
|
||||||
}
|
|
||||||
const num = parseInt(numString, 10);
|
|
||||||
const stepStart = parseInt(startString, 10);
|
|
||||||
|
|
||||||
if (num < 1 || num > 1000) {
|
|
||||||
throw `No steps created. arg 'num' must be between 1 and 1000 inclusive`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxStepNum = stepStart + num - 1;
|
|
||||||
|
|
||||||
const existingSteps = getExistingStepNums(projectPath);
|
|
||||||
|
|
||||||
if (anyStepExists(existingSteps, [stepStart, maxStepNum])) {
|
|
||||||
throw `Step not created. At least one of the steps specified between ${startString} and ${maxStepNum} already exists.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let stepNum = stepStart; stepNum <= maxStepNum; stepNum++) {
|
|
||||||
createStepFile({ stepNum, projectPath });
|
|
||||||
}
|
|
||||||
console.log(`Sucessfully added ${numString} steps`);
|
|
||||||
reorderSteps();
|
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import { getLastStepFileContent } from './helpers/get-last-step-file-content';
|
import { getLastStep } from './helpers/get-last-step-file-number';
|
||||||
import { getProjectPath } from './helpers/get-project-path';
|
import { insertStep } from './commands';
|
||||||
import { reorderSteps, createStepFile } from './utils';
|
import { validateMetaData } from './helpers/project-metadata';
|
||||||
|
|
||||||
const projectPath = getProjectPath();
|
validateMetaData();
|
||||||
const { nextStepNum, challengeSeeds } = getLastStepFileContent();
|
insertStep(getLastStep().stepNum + 1);
|
||||||
|
|
||||||
createStepFile({ stepNum: nextStepNum, projectPath, challengeSeeds });
|
|
||||||
console.log(`Sucessfully added step #${nextStepNum}`);
|
|
||||||
reorderSteps();
|
|
||||||
|
@ -22,7 +22,7 @@ type SuperBlockInfo = {
|
|||||||
|
|
||||||
type IntroJson = Record<SuperBlocks, SuperBlockInfo>;
|
type IntroJson = Record<SuperBlocks, SuperBlockInfo>;
|
||||||
|
|
||||||
type Meta = {
|
export type Meta = {
|
||||||
name: string;
|
name: string;
|
||||||
isUpcomingChange: boolean;
|
isUpcomingChange: boolean;
|
||||||
dashedName: string;
|
dashedName: string;
|
||||||
@ -210,8 +210,7 @@ async function createFirstChallenge(
|
|||||||
return createStepFile({
|
return createStepFile({
|
||||||
projectPath: newChallengeDir + '/',
|
projectPath: newChallengeDir + '/',
|
||||||
stepNum: 1,
|
stepNum: 1,
|
||||||
challengeSeeds,
|
challengeSeeds
|
||||||
stepBetween: false
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
import { getArgValues } from './helpers/get-arg-values';
|
|
||||||
import { getExistingStepNums } from './helpers/get-existing-step-nums';
|
|
||||||
import { getProjectPath } from './helpers/get-project-path';
|
|
||||||
import { padWithLeadingZeros } from './helpers/pad-with-leading-zeros';
|
|
||||||
import { createStepFile, getChallengeSeeds, reorderSteps } from './utils';
|
|
||||||
|
|
||||||
const allStepsExist = (steps: number[], stepsToFind: number[]) =>
|
|
||||||
stepsToFind.every(num => steps.includes(num));
|
|
||||||
|
|
||||||
const projectPath = getProjectPath();
|
|
||||||
const args = getArgValues(process.argv);
|
|
||||||
|
|
||||||
const start = parseInt(args.start, 10);
|
|
||||||
|
|
||||||
if (!Number.isInteger(start) || start < 1) {
|
|
||||||
throw 'Step not created. Start step must be greater than 0.';
|
|
||||||
}
|
|
||||||
|
|
||||||
const end = start + 1;
|
|
||||||
|
|
||||||
const existingSteps = getExistingStepNums(projectPath);
|
|
||||||
if (!allStepsExist(existingSteps, [start, end])) {
|
|
||||||
throw `Step not created. Both start step, ${start}, and end step, ${end}, must exist`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const challengeSeeds = getChallengeSeeds(
|
|
||||||
`${projectPath}step-${padWithLeadingZeros(start)}.md`
|
|
||||||
);
|
|
||||||
createStepFile({
|
|
||||||
stepNum: start,
|
|
||||||
projectPath,
|
|
||||||
challengeSeeds,
|
|
||||||
stepBetween: true
|
|
||||||
});
|
|
||||||
console.log(`Sucessfully added step between step #${start} and step #${end}`);
|
|
||||||
reorderSteps();
|
|
@ -1,32 +1,6 @@
|
|||||||
import fs from 'fs';
|
import { deleteStep } from './commands';
|
||||||
import { getArgValues } from './helpers/get-arg-values';
|
import { getArgValue } from './helpers/get-arg-value';
|
||||||
import { getExistingStepNums } from './helpers/get-existing-step-nums';
|
import { validateMetaData } from './helpers/project-metadata';
|
||||||
import { getProjectPath } from './helpers/get-project-path';
|
|
||||||
import { padWithLeadingZeros } from './helpers/pad-with-leading-zeros';
|
|
||||||
import { reorderSteps } from './utils';
|
|
||||||
|
|
||||||
const stepExists = (steps: number[], stepToFind: number) =>
|
validateMetaData();
|
||||||
steps.includes(stepToFind);
|
deleteStep(getArgValue(process.argv));
|
||||||
|
|
||||||
const projectPath = getProjectPath();
|
|
||||||
const args = getArgValues(process.argv);
|
|
||||||
|
|
||||||
const num = parseInt(args.num, 10);
|
|
||||||
|
|
||||||
if (!Number.isInteger(num) || num < 1) {
|
|
||||||
throw 'Step not deleted. Step num must be a number greater than 0.';
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingSteps = getExistingStepNums(projectPath);
|
|
||||||
if (!stepExists(existingSteps, num)) {
|
|
||||||
throw `Step # ${num} not deleted because it does not exist.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stepFileToDelete = `${projectPath}step-${padWithLeadingZeros(num)}.md`;
|
|
||||||
try {
|
|
||||||
fs.unlinkSync(stepFileToDelete);
|
|
||||||
console.log(`Sucessfully deleted step #${num}`);
|
|
||||||
reorderSteps();
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
|
18
tools/challenge-helper-scripts/helpers/get-arg-value.test.ts
Normal file
18
tools/challenge-helper-scripts/helpers/get-arg-value.test.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { getArgValue } from './get-arg-value';
|
||||||
|
|
||||||
|
describe('getArgValue helper', () => {
|
||||||
|
it('should throw if there no arguments', () => {
|
||||||
|
const args = ['/Path/to/node', '/Path/to/script'];
|
||||||
|
expect(() => getArgValue(args)).toThrow('only one argument allowed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the argument is not an integer', () => {
|
||||||
|
expect.assertions(3);
|
||||||
|
const args = ['/Path/to/node', '/Path/to/script', 'num=4'];
|
||||||
|
expect(() => getArgValue(args)).toThrow('argument must be an integer');
|
||||||
|
const args2 = ['/Path/to/node', '/Path/to/script', '4.1'];
|
||||||
|
expect(() => getArgValue(args2)).toThrow('argument must be an integer');
|
||||||
|
const args3 = ['/Path/to/node', '/Path/to/script', '4'];
|
||||||
|
expect(getArgValue(args3)).toBe(4);
|
||||||
|
});
|
||||||
|
});
|
11
tools/challenge-helper-scripts/helpers/get-arg-value.ts
Normal file
11
tools/challenge-helper-scripts/helpers/get-arg-value.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const isIntRE = /^\d+$/;
|
||||||
|
function getArgValue(argv: string[] = []): number {
|
||||||
|
if (argv.length !== 3) throw `only one argument allowed`;
|
||||||
|
const value = argv[2];
|
||||||
|
const intValue = parseInt(value, 10);
|
||||||
|
if (!isIntRE.test(value) || !Number.isInteger(intValue))
|
||||||
|
throw `argument must be an integer`;
|
||||||
|
return intValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getArgValue };
|
@ -1,34 +0,0 @@
|
|||||||
import { getArgValues } from './get-arg-values';
|
|
||||||
|
|
||||||
describe('getArgValues helper', () => {
|
|
||||||
it('should be able to run if there are no values to process', () => {
|
|
||||||
const args = ['/Path/to/node', '/Path/to/script'];
|
|
||||||
expect(getArgValues(args)).toEqual({});
|
|
||||||
expect(getArgValues()).toEqual({});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse the third element (key/value) if provided', () => {
|
|
||||||
const args = ['/Path/to/node', '/Path/to/script', 'num=4'];
|
|
||||||
expect(getArgValues(args)).toEqual({ num: '4' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse multiple arguments (key/value) if provided', () => {
|
|
||||||
const args = ['/Path/to/node', '/Path/to/script', 'num=4', 'another=5'];
|
|
||||||
expect(getArgValues(args)).toEqual({ another: '5', num: '4' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse the arguments with spaces (key/value) if provided', () => {
|
|
||||||
const args = ['/Path/to/node', '/Path/to/script', 'num = 3'];
|
|
||||||
expect(getArgValues(args)).toEqual({ num: '3' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw error on invalid key/value arguments', () => {
|
|
||||||
const items = [
|
|
||||||
['/Path/to/node', '/Path/to/script', 'num='],
|
|
||||||
['/Path/to/node', '/Path/to/script', '='],
|
|
||||||
['/Path/to/node', '/Path/to/script', 'num=2', '= 3']
|
|
||||||
];
|
|
||||||
|
|
||||||
items.forEach(item => expect(() => getArgValues(item)).toThrow());
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,15 +0,0 @@
|
|||||||
// Creates an object with the values starting at the third position of argv,
|
|
||||||
// ['lorem', 'ipsum', 'one=1', 'two=2', ...] => { one: 1, two: 2, ...}
|
|
||||||
function getArgValues(argv: string[] = []): Record<string, string> {
|
|
||||||
return argv.slice(2).reduce((argsObj, arg) => {
|
|
||||||
const [argument, value] = arg.replace(/\s/g, '').split('=');
|
|
||||||
|
|
||||||
if (!argument || !value) {
|
|
||||||
throw `Invalid argument/value specified: ${arg}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...argsObj, [argument]: value };
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { getArgValues };
|
|
@ -1,74 +0,0 @@
|
|||||||
import mock from 'mock-fs';
|
|
||||||
import { getExistingStepNums } from './get-existing-step-nums';
|
|
||||||
|
|
||||||
// NOTE:
|
|
||||||
// Use `console.log()` before mocking the filesystem or use
|
|
||||||
// `process.stdout.write()` instead. There are issues when using `mock-fs` and
|
|
||||||
// `require`.
|
|
||||||
|
|
||||||
describe('getExistingStepNums helper', () => {
|
|
||||||
it('should return the number portion of the project paths', () => {
|
|
||||||
mock({
|
|
||||||
'mock-project': {
|
|
||||||
'step-001.md': 'Lorem ipsum...',
|
|
||||||
'step-002.md': 'Lorem ipsum...'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const folder = `${process.cwd()}/mock-project/`;
|
|
||||||
const steps = getExistingStepNums(folder);
|
|
||||||
const expected = [1, 2];
|
|
||||||
|
|
||||||
expect(steps).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ignore text formatting and files named final.md', () => {
|
|
||||||
mock({
|
|
||||||
'mock-project': {
|
|
||||||
'final.md': 'Lorem ipsum...',
|
|
||||||
'step-001.md': 'Lorem ipsum...'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const folder = `${process.cwd()}/mock-project/`;
|
|
||||||
const steps = getExistingStepNums(folder);
|
|
||||||
const expected = [1];
|
|
||||||
|
|
||||||
expect(steps).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw if file names do not follow naming convention', () => {
|
|
||||||
mock({
|
|
||||||
'mock-project': {
|
|
||||||
'step-001.md': 'Lorem ipsum...',
|
|
||||||
'step-002.md': 'Lorem ipsum...',
|
|
||||||
'step002.md': 'Lorem ipsum...'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const folder = `${process.cwd()}/mock-project/`;
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
getExistingStepNums(folder);
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return empty array if there are no markdown files', () => {
|
|
||||||
mock({
|
|
||||||
'mock-project': {
|
|
||||||
'step-001.js': 'Lorem ipsum...',
|
|
||||||
'step-002.css': 'Lorem ipsum...'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const folder = `${process.cwd()}/mock-project/`;
|
|
||||||
const steps = getExistingStepNums(folder);
|
|
||||||
const expected: number[] = [];
|
|
||||||
|
|
||||||
expect(steps).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
mock.restore();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,30 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
// Generates an array with the output of processing filenames with an expected
|
|
||||||
// format (`step-###.md`).
|
|
||||||
// ['step-001.md', 'step-002.md'] => [1, 2]
|
|
||||||
function getExistingStepNums(projectPath: string): number[] {
|
|
||||||
return fs.readdirSync(projectPath).reduce((stepNums, fileName) => {
|
|
||||||
if (
|
|
||||||
path.extname(fileName).toLowerCase() === '.md' &&
|
|
||||||
!fileName.endsWith('final.md')
|
|
||||||
) {
|
|
||||||
const stepNumString = fileName.split('.')[0].split('-')[1];
|
|
||||||
|
|
||||||
if (!/^\d{3}$/.test(stepNumString)) {
|
|
||||||
throw (
|
|
||||||
`Step not created. File ${fileName} has a step number containing non-digits.` +
|
|
||||||
' Please run reorder-steps script first.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const stepNum = parseInt(stepNumString, 10);
|
|
||||||
stepNums.push(stepNum);
|
|
||||||
}
|
|
||||||
|
|
||||||
return stepNums;
|
|
||||||
}, [] as number[]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { getExistingStepNums };
|
|
@ -1,57 +0,0 @@
|
|||||||
import mock from 'mock-fs';
|
|
||||||
import { getLastStepFileContent } from './get-last-step-file-content';
|
|
||||||
|
|
||||||
jest.mock('./get-project-path', () => {
|
|
||||||
return {
|
|
||||||
getProjectPath: jest.fn(() => 'mock-project/')
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock('../utils', () => {
|
|
||||||
return {
|
|
||||||
getChallengeSeeds: jest.fn(() => {
|
|
||||||
return {
|
|
||||||
lorem: 'ipsum'
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getLastStepFileContent helper', () => {
|
|
||||||
it('should throw if last step count does not match with numbers of steps', () => {
|
|
||||||
mock({
|
|
||||||
'mock-project/': {
|
|
||||||
'step-001.md': 'Lorem ipsum...',
|
|
||||||
'step-004.md': 'Lorem ipsum...',
|
|
||||||
'final.md': 'Lorem ipsum...'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
getLastStepFileContent();
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return information if steps count is correct', () => {
|
|
||||||
mock({
|
|
||||||
'mock-project': {
|
|
||||||
'step-001.md': 'Lorem ipsum...',
|
|
||||||
'step-002.md': 'Lorem ipsum...',
|
|
||||||
'final.md': 'Lorem ipsum...'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
nextStepNum: 3,
|
|
||||||
challengeSeeds: {
|
|
||||||
lorem: 'ipsum'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(getLastStepFileContent()).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
mock.restore();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,41 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import { getChallengeSeeds } from '../utils';
|
|
||||||
import { getProjectPath } from './get-project-path';
|
|
||||||
import { ChallengeSeed } from './get-step-template';
|
|
||||||
|
|
||||||
// Looks up the last file found with format `step-###.md` in a directory and
|
|
||||||
// returns associated information to it. At the same time validates that the
|
|
||||||
// number of files match the names used to name these.
|
|
||||||
function getLastStepFileContent(): {
|
|
||||||
challengeSeeds: Record<string, ChallengeSeed>;
|
|
||||||
nextStepNum: number;
|
|
||||||
} {
|
|
||||||
const filesArr: string[] = [];
|
|
||||||
const projectPath = getProjectPath();
|
|
||||||
|
|
||||||
fs.readdirSync(projectPath).forEach(fileName => {
|
|
||||||
if (
|
|
||||||
path.extname(fileName).toLowerCase() === '.md' &&
|
|
||||||
!fileName.endsWith('final.md')
|
|
||||||
) {
|
|
||||||
filesArr.push(fileName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const fileName = filesArr[filesArr.length - 1];
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
||||||
const lastStepFileString: string = fileName.split('.')[0].split('-')[1];
|
|
||||||
const lastStepFileNum = parseInt(lastStepFileString, 10);
|
|
||||||
if (filesArr.length !== lastStepFileNum) {
|
|
||||||
throw `Error: The last file step is ${lastStepFileNum} and there are ${filesArr.length} files.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
challengeSeeds: getChallengeSeeds(projectPath + fileName),
|
|
||||||
nextStepNum: lastStepFileNum + 1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { getLastStepFileContent };
|
|
@ -0,0 +1,13 @@
|
|||||||
|
import { last } from 'lodash';
|
||||||
|
import { getMetaData } from './project-metadata';
|
||||||
|
|
||||||
|
function getLastStep(): { stepNum: number } {
|
||||||
|
const meta = getMetaData();
|
||||||
|
const challengeOrder: string[][] = meta.challengeOrder;
|
||||||
|
const step = last(challengeOrder);
|
||||||
|
if (!step) throw new Error('No steps found');
|
||||||
|
|
||||||
|
return { stepNum: challengeOrder.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getLastStep };
|
@ -1,4 +1,4 @@
|
|||||||
import { getProjectPath } from './get-project-path';
|
import { getProjectName, getProjectPath } from './get-project-info';
|
||||||
|
|
||||||
describe('getProjectPath helper', () => {
|
describe('getProjectPath helper', () => {
|
||||||
it('should return the calling dir path', () => {
|
it('should return the calling dir path', () => {
|
||||||
@ -20,3 +20,18 @@ describe('getProjectPath helper', () => {
|
|||||||
expect(getProjectPath()).toEqual(expected);
|
expect(getProjectPath()).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getProjectName helper', () => {
|
||||||
|
it('should return the last path segment of the calling dir', () => {
|
||||||
|
const mockCallingDir = 'calling/dir';
|
||||||
|
const expected = `dir`;
|
||||||
|
|
||||||
|
// Add mock to test condition
|
||||||
|
process.env.CALLING_DIR = mockCallingDir;
|
||||||
|
|
||||||
|
expect(getProjectName()).toEqual(expected);
|
||||||
|
|
||||||
|
// Remove mock to not affect other tests
|
||||||
|
delete process.env.CALLING_DIR;
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,9 @@
|
|||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export function getProjectPath(): string {
|
||||||
|
return (process.env.CALLING_DIR || process.cwd()) + path.sep;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProjectName(): string {
|
||||||
|
return getProjectPath().split(path.sep).slice(-2)[0];
|
||||||
|
}
|
@ -1,19 +0,0 @@
|
|||||||
import { getProjectMetaPath } from './get-project-meta-path';
|
|
||||||
|
|
||||||
describe('getProjectMetaPath helper', () => {
|
|
||||||
it('should throw if args are invalid', () => {
|
|
||||||
expect(() => {
|
|
||||||
getProjectMetaPath('', '');
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the meta path', () => {
|
|
||||||
const curriculum = 'test-curriculum';
|
|
||||||
const project = 'test-project';
|
|
||||||
const expected = `${process.cwd()}/${curriculum}/challenges/_meta/${project}/meta.json`;
|
|
||||||
const expectedB = `${process.cwd()}/challenges/_meta/${project}/meta.json`;
|
|
||||||
|
|
||||||
expect(getProjectMetaPath(curriculum, project)).toEqual(expected);
|
|
||||||
expect(getProjectMetaPath('', project)).toEqual(expectedB);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,25 +0,0 @@
|
|||||||
import path from 'path';
|
|
||||||
|
|
||||||
// Returns the path of the meta file associated to arguments provided.
|
|
||||||
const getProjectMetaPath = (
|
|
||||||
curriculumPath: string,
|
|
||||||
projectName: string
|
|
||||||
): string => {
|
|
||||||
if (typeof curriculumPath !== 'string' || typeof projectName !== 'string') {
|
|
||||||
throw `${curriculumPath} and ${projectName} should be of type string`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!projectName) {
|
|
||||||
throw `${projectName} can't be an empty string`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.resolve(
|
|
||||||
curriculumPath,
|
|
||||||
'challenges',
|
|
||||||
'_meta',
|
|
||||||
projectName,
|
|
||||||
'meta.json'
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { getProjectMetaPath };
|
|
@ -1,36 +0,0 @@
|
|||||||
import { readFileSync } from 'fs';
|
|
||||||
import { getMetaData } from './get-project-path-metadata';
|
|
||||||
|
|
||||||
jest.mock('fs', () => {
|
|
||||||
return {
|
|
||||||
readFileSync: jest.fn()
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockPath = '/mock/path';
|
|
||||||
|
|
||||||
describe('getMetaData helper', () => {
|
|
||||||
it('should process requested file', () => {
|
|
||||||
// @ts-expect-error - readFileSync is mocked
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
||||||
readFileSync.mockImplementation(() => '{"name": "Test Project"}');
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
name: 'Test Project'
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(getMetaData(mockPath)).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw if file is not found', () => {
|
|
||||||
// @ts-expect-error - readFileSync is mocked
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
||||||
readFileSync.mockImplementation(() => {
|
|
||||||
throw new Error();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
getMetaData(mockPath);
|
|
||||||
}).toThrowError(new Error(`No _meta.json file exists at ${mockPath}`));
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,16 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
// Process the contents of a argument (json) to an Object
|
|
||||||
function getMetaData(file: string): Record<string, unknown> {
|
|
||||||
let metaData;
|
|
||||||
|
|
||||||
try {
|
|
||||||
metaData = fs.readFileSync(file, 'utf8');
|
|
||||||
} catch (err) {
|
|
||||||
throw `No _meta.json file exists at ${file}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.parse(metaData) as Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { getMetaData };
|
|
@ -1,8 +0,0 @@
|
|||||||
import path from 'path';
|
|
||||||
|
|
||||||
// Returns the path of a project
|
|
||||||
function getProjectPath(): string {
|
|
||||||
return (process.env.CALLING_DIR || process.cwd()) + path.sep;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { getProjectPath };
|
|
@ -48,7 +48,6 @@ Test 1
|
|||||||
tail: ''
|
tail: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
stepBetween: false,
|
|
||||||
stepNum: 5
|
stepNum: 5
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -22,7 +22,6 @@ ${content}`
|
|||||||
type StepOptions = {
|
type StepOptions = {
|
||||||
challengeId: ObjectID;
|
challengeId: ObjectID;
|
||||||
challengeSeeds: Record<string, ChallengeSeed>;
|
challengeSeeds: Record<string, ChallengeSeed>;
|
||||||
stepBetween: boolean;
|
|
||||||
stepNum: number;
|
stepNum: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -38,7 +37,6 @@ export interface ChallengeSeed {
|
|||||||
function getStepTemplate({
|
function getStepTemplate({
|
||||||
challengeId,
|
challengeId,
|
||||||
challengeSeeds,
|
challengeSeeds,
|
||||||
stepBetween,
|
|
||||||
stepNum
|
stepNum
|
||||||
}: StepOptions): string {
|
}: StepOptions): string {
|
||||||
const seedTexts = Object.values(challengeSeeds)
|
const seedTexts = Object.values(challengeSeeds)
|
||||||
@ -61,12 +59,7 @@ function getStepTemplate({
|
|||||||
.map(({ ext, tail }: ChallengeSeed) => getCodeBlock(ext, tail))
|
.map(({ ext, tail }: ChallengeSeed) => getCodeBlock(ext, tail))
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
const descStepNum = stepBetween ? stepNum + 1 : stepNum;
|
const stepDescription = `step ${stepNum} instructions`;
|
||||||
|
|
||||||
const stepDescription = `${
|
|
||||||
stepBetween ? 'new ' : ''
|
|
||||||
}step ${descStepNum} instructions`;
|
|
||||||
|
|
||||||
const seedChallengeSection = getSeedSection(seedTexts, 'seed-contents');
|
const seedChallengeSection = getSeedSection(seedTexts, 'seed-contents');
|
||||||
const seedHeadSection = getSeedSection(seedHeads, 'before-user-code');
|
const seedHeadSection = getSeedSection(seedHeads, 'before-user-code');
|
||||||
const seedTailSection = getSeedSection(seedTails, 'after-user-code');
|
const seedTailSection = getSeedSection(seedTails, 'after-user-code');
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
const { padWithLeadingZeros } = require('./pad-with-leading-zeros');
|
|
||||||
|
|
||||||
describe('padWithLeadingZeros helper', () => {
|
|
||||||
it('should return a string of 3 digits for valid values', () => {
|
|
||||||
const items = ['1', '11', '111'];
|
|
||||||
|
|
||||||
items.forEach(item => expect(padWithLeadingZeros(item).length).toEqual(3));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should prepend 0s on valid values while length is less than 3', () => {
|
|
||||||
expect(padWithLeadingZeros('1')).toEqual('001');
|
|
||||||
expect(padWithLeadingZeros('11')).toEqual('011');
|
|
||||||
expect(padWithLeadingZeros('111')).toEqual('111');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw on valid values that are longer that 3 characters', () => {
|
|
||||||
expect(() => {
|
|
||||||
padWithLeadingZeros('19850809');
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,14 +0,0 @@
|
|||||||
// Prepends zero's to the given value until length is equal to 3:
|
|
||||||
// '1' -> '001', '12' -> '012', ...
|
|
||||||
// Note: always want file step numbers 3 digits
|
|
||||||
function padWithLeadingZeros(value: number): string {
|
|
||||||
const valueString = value.toString();
|
|
||||||
|
|
||||||
if (valueString.length > 3) {
|
|
||||||
throw `${valueString} should be less than 4 characters`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return valueString.padStart(3, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
export { padWithLeadingZeros };
|
|
161
tools/challenge-helper-scripts/helpers/project-metadata.test.ts
Normal file
161
tools/challenge-helper-scripts/helpers/project-metadata.test.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import mock from 'mock-fs';
|
||||||
|
import {
|
||||||
|
getMetaData,
|
||||||
|
getProjectMetaPath,
|
||||||
|
validateMetaData
|
||||||
|
} from './project-metadata';
|
||||||
|
|
||||||
|
describe('getProjectMetaPath helper', () => {
|
||||||
|
it('should return the meta path', () => {
|
||||||
|
const expected = path.join(
|
||||||
|
'curriculum',
|
||||||
|
'challenges',
|
||||||
|
`_meta/mock-project/meta.json`
|
||||||
|
);
|
||||||
|
|
||||||
|
process.env.CALLING_DIR =
|
||||||
|
'curriculum/challenges/english/superblock/mock-project';
|
||||||
|
|
||||||
|
expect(getProjectMetaPath()).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete process.env.CALLING_DIR;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMetaData helper', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mock({
|
||||||
|
curriculum: {
|
||||||
|
challenges: {
|
||||||
|
english: {
|
||||||
|
superblock: {
|
||||||
|
'mock-project': {
|
||||||
|
'step-001.md': 'Lorem ipsum...',
|
||||||
|
'step-002.md': 'Lorem ipsum...',
|
||||||
|
'step-003.md': 'Lorem ipsum...'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
'mock-project': {
|
||||||
|
'meta.json': `{
|
||||||
|
"id": "mock-id",
|
||||||
|
"challengeOrder": [["1","step1"], ["2","step2"], ["1","step3"]]}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process requested file', () => {
|
||||||
|
const expected = {
|
||||||
|
id: 'mock-id',
|
||||||
|
challengeOrder: [
|
||||||
|
['1', 'step1'],
|
||||||
|
['2', 'step2'],
|
||||||
|
['1', 'step3']
|
||||||
|
]
|
||||||
|
};
|
||||||
|
process.env.CALLING_DIR =
|
||||||
|
'curriculum/challenges/english/superblock/mock-project';
|
||||||
|
expect(getMetaData()).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if file is not found', () => {
|
||||||
|
process.env.CALLING_DIR =
|
||||||
|
'curriculum/challenges/english/superblock/mick-priject';
|
||||||
|
expect(() => {
|
||||||
|
getMetaData();
|
||||||
|
}).toThrowError(
|
||||||
|
new Error(
|
||||||
|
`ENOENT: no such file or directory, open 'curriculum/challenges/_meta/mick-priject/meta.json'`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.restore();
|
||||||
|
delete process.env.CALLING_DIR;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateMetaData helper', () => {
|
||||||
|
it('should throw if a stepfile is missing', () => {
|
||||||
|
mock({
|
||||||
|
'_meta/project/': {
|
||||||
|
'meta.json':
|
||||||
|
'{"id": "mock-id", "challengeOrder": [["id-1", "Step 1"], ["id-3", "Step 2"], ["id-2", "Step 3"]]}'
|
||||||
|
},
|
||||||
|
'english/superblock/project/': {
|
||||||
|
'id-1.md': `---
|
||||||
|
id: id-1
|
||||||
|
title: Step 2
|
||||||
|
challengeType: a
|
||||||
|
dashedName: step-2
|
||||||
|
---
|
||||||
|
`,
|
||||||
|
'id-3.md': `---
|
||||||
|
id: id-3
|
||||||
|
title: Step 3
|
||||||
|
challengeType: c
|
||||||
|
dashedName: step-3
|
||||||
|
---
|
||||||
|
`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.env.CALLING_DIR = 'english/superblock/project';
|
||||||
|
|
||||||
|
expect(() => validateMetaData()).toThrow(
|
||||||
|
"ENOENT: no such file or directory, access 'english/superblock/project/id-2.md'"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if a step is present in the project, but not the meta', () => {
|
||||||
|
mock({
|
||||||
|
'_meta/project/': {
|
||||||
|
'meta.json':
|
||||||
|
'{"id": "mock-id", "challengeOrder": [["id-1", "Step 1"], ["id-2", "Step 3"]]}'
|
||||||
|
},
|
||||||
|
'english/superblock/project/': {
|
||||||
|
'id-1.md': `---
|
||||||
|
id: id-1
|
||||||
|
title: Step 2
|
||||||
|
challengeType: a
|
||||||
|
dashedName: step-2
|
||||||
|
---
|
||||||
|
`,
|
||||||
|
'id-2.md': `---
|
||||||
|
id: id-2
|
||||||
|
title: Step 1
|
||||||
|
challengeType: b
|
||||||
|
dashedName: step-1
|
||||||
|
---
|
||||||
|
`,
|
||||||
|
'id-3.md': `---
|
||||||
|
id: id-3
|
||||||
|
title: Step 3
|
||||||
|
challengeType: c
|
||||||
|
dashedName: step-3
|
||||||
|
---
|
||||||
|
`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.env.CALLING_DIR = 'english/superblock/project';
|
||||||
|
|
||||||
|
expect(() => validateMetaData()).toThrow(
|
||||||
|
"File english/superblock/project/id-3.md should be in the meta.json's challengeOrder"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.restore();
|
||||||
|
delete process.env.CALLING_DIR;
|
||||||
|
});
|
||||||
|
});
|
46
tools/challenge-helper-scripts/helpers/project-metadata.ts
Normal file
46
tools/challenge-helper-scripts/helpers/project-metadata.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import glob from 'glob';
|
||||||
|
import { Meta } from '../create-project';
|
||||||
|
import { getProjectName, getProjectPath } from './get-project-info';
|
||||||
|
|
||||||
|
function getMetaData(): Meta {
|
||||||
|
const metaData = fs.readFileSync(getProjectMetaPath(), 'utf8');
|
||||||
|
return JSON.parse(metaData) as Meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMetaData(newMetaData: Record<string, unknown>): void {
|
||||||
|
fs.writeFileSync(getProjectMetaPath(), JSON.stringify(newMetaData, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProjectMetaPath(): string {
|
||||||
|
return path.join(
|
||||||
|
getProjectPath(),
|
||||||
|
'../../..',
|
||||||
|
'_meta',
|
||||||
|
getProjectName(),
|
||||||
|
'meta.json'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This (and everything else) should be async, but it's fast enough
|
||||||
|
// for the moment.
|
||||||
|
function validateMetaData(): void {
|
||||||
|
const { challengeOrder } = getMetaData();
|
||||||
|
|
||||||
|
// each step in the challengeOrder should correspond to a file
|
||||||
|
challengeOrder.forEach(([id]) => {
|
||||||
|
fs.accessSync(`${getProjectPath()}${id}.md`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// each file should have a corresponding step in the challengeOrder
|
||||||
|
glob.sync(`${getProjectPath()}/*.md`).forEach(file => {
|
||||||
|
const id = path.basename(file, '.md');
|
||||||
|
if (!challengeOrder.find(([stepId]) => stepId === id))
|
||||||
|
throw new Error(
|
||||||
|
`File ${file} should be in the meta.json's challengeOrder`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getMetaData, updateMetaData, getProjectMetaPath, validateMetaData };
|
6
tools/challenge-helper-scripts/insert-step.ts
Normal file
6
tools/challenge-helper-scripts/insert-step.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { getArgValue } from './helpers/get-arg-value';
|
||||||
|
import { insertStep } from './commands';
|
||||||
|
import { validateMetaData } from './helpers/project-metadata';
|
||||||
|
|
||||||
|
validateMetaData();
|
||||||
|
insertStep(getArgValue(process.argv));
|
@ -1,3 +0,0 @@
|
|||||||
const { reorderSteps } = require('./utils');
|
|
||||||
|
|
||||||
reorderSteps();
|
|
5
tools/challenge-helper-scripts/update-step-titles.ts
Normal file
5
tools/challenge-helper-scripts/update-step-titles.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { validateMetaData } from './helpers/project-metadata';
|
||||||
|
import { updateStepTitles } from './utils';
|
||||||
|
|
||||||
|
validateMetaData();
|
||||||
|
updateStepTitles();
|
@ -1,6 +1,8 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import ObjectID from 'bson-objectid';
|
import ObjectID from 'bson-objectid';
|
||||||
import glob from 'glob';
|
import glob from 'glob';
|
||||||
|
import * as matter from 'gray-matter';
|
||||||
import mock from 'mock-fs';
|
import mock from 'mock-fs';
|
||||||
|
|
||||||
// NOTE:
|
// NOTE:
|
||||||
@ -18,39 +20,9 @@ jest.mock('./helpers/get-step-template', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('./helpers/get-project-meta-path', () => {
|
|
||||||
return {
|
|
||||||
getProjectMetaPath: jest.fn(() => '_meta/project/meta.json')
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock('./helpers/get-project-path', () => {
|
|
||||||
return {
|
|
||||||
getProjectPath: jest.fn(() => 'project/')
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock('gray-matter', () => {
|
|
||||||
return {
|
|
||||||
read: jest.fn(() => ({
|
|
||||||
data: { id: mockChallengeId }
|
|
||||||
})),
|
|
||||||
stringify: jest.fn(() => 'Lorem ipsum...')
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock(
|
|
||||||
'../challenge-helper-scripts/helpers/get-project-path-metadata',
|
|
||||||
() => ({
|
|
||||||
getMetaData: jest.fn(() => ({
|
|
||||||
id: 'mock-id'
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const mockChallengeId = '60d35cf3fe32df2ce8e31b03';
|
const mockChallengeId = '60d35cf3fe32df2ce8e31b03';
|
||||||
import { getStepTemplate } from './helpers/get-step-template';
|
import { getStepTemplate } from './helpers/get-step-template';
|
||||||
import { createStepFile, reorderSteps } from './utils';
|
import { createStepFile, insertStepIntoMeta, updateStepTitles } from './utils';
|
||||||
|
|
||||||
describe('Challenge utils helper scripts', () => {
|
describe('Challenge utils helper scripts', () => {
|
||||||
describe('createStepFile util', () => {
|
describe('createStepFile util', () => {
|
||||||
@ -78,61 +50,111 @@ describe('Challenge utils helper scripts', () => {
|
|||||||
const files = glob.sync(`project/*.md`);
|
const files = glob.sync(`project/*.md`);
|
||||||
|
|
||||||
expect(files).toEqual([
|
expect(files).toEqual([
|
||||||
|
`project/${mockChallengeId}.md`,
|
||||||
`project/step-001.md`,
|
`project/step-001.md`,
|
||||||
`project/step-002.md`,
|
`project/step-002.md`
|
||||||
`project/step-003.md`
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('reorderSteps util', () => {
|
describe('insertStepIntoMeta util', () => {
|
||||||
it('should sort files found in given path', () => {
|
it('should update the meta with a new file id and name', () => {
|
||||||
mock({
|
mock({
|
||||||
'_meta/project/': {
|
'_meta/project/': {
|
||||||
'meta.json': 'Lorem ipsum meta content...'
|
'meta.json': `{"id": "mock-id",
|
||||||
},
|
|
||||||
'project/': {
|
|
||||||
'step-001.md': 'Lorem ipsum 1...',
|
|
||||||
'step-002.md': 'Lorem ipsum 2...',
|
|
||||||
'step-002b.md': 'Lorem ipsum 3...'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
reorderSteps();
|
|
||||||
|
|
||||||
// - Should write a file with a given name and template
|
|
||||||
const files = glob.sync(`project/*.md`);
|
|
||||||
|
|
||||||
expect(files).toEqual([
|
|
||||||
'project/step-001.md',
|
|
||||||
'project/step-002.md',
|
|
||||||
'project/step-003.md'
|
|
||||||
]);
|
|
||||||
|
|
||||||
const result = fs.readFileSync('_meta/project/meta.json', 'utf8');
|
|
||||||
|
|
||||||
const expectedResult = `{
|
|
||||||
"id": "mock-id",
|
|
||||||
"challengeOrder": [
|
"challengeOrder": [
|
||||||
[
|
[
|
||||||
"60d35cf3fe32df2ce8e31b03",
|
"id-1",
|
||||||
"Step 1"
|
"Step 1"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"60d35cf3fe32df2ce8e31b03",
|
"id-2",
|
||||||
"Step 2"
|
"Step 2"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"60d35cf3fe32df2ce8e31b03",
|
"id-3",
|
||||||
"Step 3"
|
"Step 3"
|
||||||
]
|
]
|
||||||
]
|
]}`
|
||||||
}`;
|
}
|
||||||
|
});
|
||||||
|
process.env.CALLING_DIR = 'english/superblock/project';
|
||||||
|
|
||||||
expect(result).toEqual(expectedResult);
|
insertStepIntoMeta({ stepNum: 3, stepId: new ObjectID(mockChallengeId) });
|
||||||
|
|
||||||
|
const meta = JSON.parse(
|
||||||
|
fs.readFileSync('_meta/project/meta.json', 'utf8')
|
||||||
|
);
|
||||||
|
expect(meta).toEqual({
|
||||||
|
id: 'mock-id',
|
||||||
|
challengeOrder: [
|
||||||
|
['id-1', 'Step 1'],
|
||||||
|
['id-2', 'Step 2'],
|
||||||
|
[mockChallengeId, 'Step 3'],
|
||||||
|
['id-3', 'Step 4']
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateStepTitles util', () => {
|
||||||
|
it('should apply meta.challengeOrder to step files', () => {
|
||||||
|
mock({
|
||||||
|
'_meta/project/': {
|
||||||
|
'meta.json':
|
||||||
|
'{"id": "mock-id", "challengeOrder": [["id-1", "Step 1"], ["id-3", "Step 2"], ["id-2", "Step 3"]]}'
|
||||||
|
},
|
||||||
|
'english/superblock/project/': {
|
||||||
|
'id-1.md': `---
|
||||||
|
id: id-1
|
||||||
|
title: Step 2
|
||||||
|
challengeType: a
|
||||||
|
dashedName: step-2
|
||||||
|
---
|
||||||
|
`,
|
||||||
|
'id-2.md': `---
|
||||||
|
id: id-2
|
||||||
|
title: Step 1
|
||||||
|
challengeType: b
|
||||||
|
dashedName: step-1
|
||||||
|
---
|
||||||
|
`,
|
||||||
|
'id-3.md': `---
|
||||||
|
id: id-3
|
||||||
|
title: Step 3
|
||||||
|
challengeType: c
|
||||||
|
dashedName: step-3
|
||||||
|
---
|
||||||
|
`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.env.CALLING_DIR = 'english/superblock/project';
|
||||||
|
|
||||||
|
updateStepTitles();
|
||||||
|
|
||||||
|
expect(matter.read('english/superblock/project/id-1.md').data).toEqual({
|
||||||
|
id: 'id-1',
|
||||||
|
title: 'Step 1',
|
||||||
|
challengeType: 'a',
|
||||||
|
dashedName: 'step-1'
|
||||||
|
});
|
||||||
|
expect(matter.read('english/superblock/project/id-2.md').data).toEqual({
|
||||||
|
id: 'id-2',
|
||||||
|
title: 'Step 3',
|
||||||
|
challengeType: 'b',
|
||||||
|
dashedName: 'step-3'
|
||||||
|
});
|
||||||
|
expect(matter.read('english/superblock/project/id-3.md').data).toEqual({
|
||||||
|
id: 'id-3',
|
||||||
|
title: 'Step 2',
|
||||||
|
challengeType: 'c',
|
||||||
|
dashedName: 'step-2'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mock.restore();
|
mock.restore();
|
||||||
|
delete process.env.CALLING_DIR;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,120 +2,91 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import ObjectID from 'bson-objectid';
|
import ObjectID from 'bson-objectid';
|
||||||
import * as matter from 'gray-matter';
|
import * as matter from 'gray-matter';
|
||||||
import { getMetaData } from '../challenge-helper-scripts/helpers/get-project-path-metadata';
|
|
||||||
import { parseMDSync } from '../challenge-parser/parser';
|
import { parseMDSync } from '../challenge-parser/parser';
|
||||||
import { getProjectMetaPath } from './helpers/get-project-meta-path';
|
import { getMetaData, updateMetaData } from './helpers/project-metadata';
|
||||||
import { getProjectPath } from './helpers/get-project-path';
|
import { getProjectPath } from './helpers/get-project-info';
|
||||||
import { ChallengeSeed, getStepTemplate } from './helpers/get-step-template';
|
import { ChallengeSeed, getStepTemplate } from './helpers/get-step-template';
|
||||||
import { padWithLeadingZeros } from './helpers/pad-with-leading-zeros';
|
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
projectPath: string;
|
|
||||||
stepNum: number;
|
stepNum: number;
|
||||||
|
projectPath?: string;
|
||||||
challengeSeeds?: Record<string, ChallengeSeed>;
|
challengeSeeds?: Record<string, ChallengeSeed>;
|
||||||
stepBetween?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createStepFile = ({
|
const createStepFile = ({
|
||||||
projectPath,
|
|
||||||
stepNum,
|
stepNum,
|
||||||
challengeSeeds = {},
|
projectPath = getProjectPath(),
|
||||||
stepBetween = false
|
challengeSeeds = {}
|
||||||
}: Options) => {
|
}: Options) => {
|
||||||
const challengeId = new ObjectID();
|
const challengeId = new ObjectID();
|
||||||
|
|
||||||
let finalStepNum = padWithLeadingZeros(stepNum);
|
|
||||||
finalStepNum += stepBetween ? 'a' : '';
|
|
||||||
|
|
||||||
const template = getStepTemplate({
|
const template = getStepTemplate({
|
||||||
challengeId,
|
challengeId,
|
||||||
challengeSeeds,
|
challengeSeeds,
|
||||||
stepBetween,
|
|
||||||
stepNum
|
stepNum
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.writeFileSync(`${projectPath}step-${finalStepNum}.md`, template);
|
fs.writeFileSync(`${projectPath}${challengeId.toString()}.md`, template);
|
||||||
|
|
||||||
return challengeId;
|
return challengeId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const reorderSteps = () => {
|
interface InsertOptions {
|
||||||
const projectPath = getProjectPath();
|
stepNum: number;
|
||||||
|
stepId: ObjectID;
|
||||||
|
}
|
||||||
|
|
||||||
const projectName = process.env.CALLING_DIR
|
function insertStepIntoMeta({ stepNum, stepId }: InsertOptions) {
|
||||||
? process.env.CALLING_DIR.split(path.sep).slice(-1).toString()
|
const existingMeta = getMetaData();
|
||||||
: process.cwd().split(path.sep).slice(-1).toString();
|
const oldOrder = [...existingMeta.challengeOrder];
|
||||||
|
oldOrder.splice(stepNum - 1, 0, [stepId.toString()]);
|
||||||
|
// rename all the files in challengeOrder
|
||||||
|
const challengeOrder = oldOrder.map(([id], index) => [
|
||||||
|
id,
|
||||||
|
`Step ${index + 1}`
|
||||||
|
]);
|
||||||
|
|
||||||
const curriculumPath = process.env.CALLING_DIR
|
updateMetaData({ ...existingMeta, challengeOrder });
|
||||||
? ''
|
}
|
||||||
: path.join(__dirname, '../');
|
|
||||||
|
|
||||||
const projectMetaPath = getProjectMetaPath(curriculumPath, projectName);
|
function deleteStepFromMeta({ stepNum }: { stepNum: number }) {
|
||||||
|
const existingMeta = getMetaData();
|
||||||
|
const oldOrder = [...existingMeta.challengeOrder];
|
||||||
|
oldOrder.splice(stepNum - 1, 1);
|
||||||
|
// rename all the files in challengeOrder
|
||||||
|
const challengeOrder = oldOrder.map(([id], index) => [
|
||||||
|
id,
|
||||||
|
`Step ${index + 1}`
|
||||||
|
]);
|
||||||
|
|
||||||
const parsedData = getMetaData(projectMetaPath);
|
updateMetaData({ ...existingMeta, challengeOrder });
|
||||||
|
}
|
||||||
|
|
||||||
let foundFinal = false;
|
const updateStepTitles = () => {
|
||||||
const filesArr = [];
|
const meta = getMetaData();
|
||||||
fs.readdirSync(projectPath).forEach(fileName => {
|
|
||||||
|
const fileNames: string[] = [];
|
||||||
|
fs.readdirSync(getProjectPath()).forEach(fileName => {
|
||||||
if (path.extname(fileName).toLowerCase() === '.md') {
|
if (path.extname(fileName).toLowerCase() === '.md') {
|
||||||
if (!fileName.endsWith('final.md')) {
|
fileNames.push(fileName);
|
||||||
filesArr.push(fileName);
|
|
||||||
} else {
|
|
||||||
foundFinal = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (foundFinal) {
|
fileNames.forEach(fileName => {
|
||||||
filesArr.push('final.md');
|
const filePath = `${getProjectPath()}${fileName}`;
|
||||||
}
|
|
||||||
|
|
||||||
const filesToReorder = filesArr.map((fileName, i) => {
|
|
||||||
const newStepNum = i + 1;
|
|
||||||
const newFileName =
|
|
||||||
fileName !== 'final.md'
|
|
||||||
? `step-${padWithLeadingZeros(newStepNum)}.md`
|
|
||||||
: 'final.md';
|
|
||||||
return {
|
|
||||||
oldFileName: fileName,
|
|
||||||
newFileName,
|
|
||||||
newStepNum
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const challengeOrder: string[][] = [];
|
|
||||||
|
|
||||||
filesToReorder.forEach(({ oldFileName, newFileName, newStepNum }) => {
|
|
||||||
fs.renameSync(
|
|
||||||
`${projectPath}${oldFileName}`,
|
|
||||||
`${projectPath}${newFileName}.tmp`
|
|
||||||
);
|
|
||||||
const filePath = `${projectPath}${newFileName}.tmp`;
|
|
||||||
const frontMatter = matter.read(filePath);
|
const frontMatter = matter.read(filePath);
|
||||||
const challengeID =
|
const newStepNum =
|
||||||
(frontMatter.data.id as string) || new ObjectID().toString();
|
meta.challengeOrder.findIndex(elem => elem[0] === frontMatter.data.id) +
|
||||||
const title =
|
1;
|
||||||
newFileName === 'final.md' ? 'Final Prototype' : `Step ${newStepNum}`;
|
const title = `Step ${newStepNum}`;
|
||||||
const dashedName = `step-${newStepNum}`;
|
const dashedName = `step-${newStepNum}`;
|
||||||
challengeOrder.push(['' + challengeID, title]);
|
|
||||||
const newData = {
|
const newData = {
|
||||||
...frontMatter.data,
|
...frontMatter.data,
|
||||||
id: challengeID,
|
|
||||||
title,
|
title,
|
||||||
dashedName
|
dashedName
|
||||||
};
|
};
|
||||||
fs.writeFileSync(filePath, matter.stringify(frontMatter.content, newData));
|
fs.writeFileSync(filePath, matter.stringify(frontMatter.content, newData));
|
||||||
});
|
});
|
||||||
|
|
||||||
filesToReorder.forEach(({ newFileName }) => {
|
|
||||||
fs.renameSync(
|
|
||||||
`${projectPath}${newFileName}.tmp`,
|
|
||||||
`${projectPath}${newFileName}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const newMeta = { ...parsedData, challengeOrder };
|
|
||||||
fs.writeFileSync(projectMetaPath, JSON.stringify(newMeta, null, 2));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getChallengeSeeds = (
|
const getChallengeSeeds = (
|
||||||
@ -125,4 +96,10 @@ const getChallengeSeeds = (
|
|||||||
return parseMDSync(challengeFilePath).challengeFiles;
|
return parseMDSync(challengeFilePath).challengeFiles;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { createStepFile, reorderSteps, getChallengeSeeds };
|
export {
|
||||||
|
createStepFile,
|
||||||
|
updateStepTitles,
|
||||||
|
getChallengeSeeds,
|
||||||
|
insertStepIntoMeta,
|
||||||
|
deleteStepFromMeta
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user