feat: refactor and test tools steps utilities (#42693)
This commit is contained in:
6
package-lock.json
generated
6
package-lock.json
generated
@ -19336,6 +19336,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mock-fs": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-A5mm/SpSDwwc/klSaEvvKMGQQtiGiQy8UcDAd/vpVO1fV+4zaHjt39yKgCSErFzv2zYxZIUx9Ud/7ybeHBf8Fg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"modify-values": {
|
"modify-values": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz",
|
||||||
|
@ -132,6 +132,7 @@
|
|||||||
"lint-staged": "11.0.0",
|
"lint-staged": "11.0.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"markdownlint": "0.23.1",
|
"markdownlint": "0.23.1",
|
||||||
|
"mock-fs": "^5.0.0",
|
||||||
"npm-run-all": "4.1.5",
|
"npm-run-all": "4.1.5",
|
||||||
"ora": "5.4.1",
|
"ora": "5.4.1",
|
||||||
"prettier": "2.3.2",
|
"prettier": "2.3.2",
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
const {
|
const { getArgValues } = require('./helpers/get-arg-values');
|
||||||
reorderSteps,
|
const { getExistingStepNums } = require('./helpers/get-existing-step-nums');
|
||||||
createStepFile,
|
const { getProjectPath } = require('./helpers/get-project-path');
|
||||||
getExistingStepNums,
|
const { createStepFile, reorderSteps } = require('./utils');
|
||||||
getProjectPath,
|
|
||||||
getArgValues
|
|
||||||
} = require('./utils');
|
|
||||||
|
|
||||||
const anyStepExists = (steps, stepsToFind) =>
|
const anyStepExists = (steps, stepsToFind) =>
|
||||||
stepsToFind.some(num => steps.includes(num));
|
stepsToFind.some(num => steps.includes(num));
|
||||||
|
@ -1,39 +1,10 @@
|
|||||||
const fs = require('fs');
|
const { getProjectPath } = require('./helpers/get-project-path');
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
reorderSteps,
|
getLastStepFileContent
|
||||||
createStepFile,
|
} = require('./helpers/get-last-step-file-content');
|
||||||
getChallengeSeeds,
|
const { reorderSteps, createStepFile } = require('./utils');
|
||||||
getProjectPath
|
|
||||||
} = 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.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
nextStepNum: lastStepFileNum + 1,
|
|
||||||
challengeSeeds: getChallengeSeeds(projectPath + fileName)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const projectPath = getProjectPath();
|
const projectPath = getProjectPath();
|
||||||
|
|
||||||
const { nextStepNum, challengeSeeds } = getLastStepFileContent();
|
const { nextStepNum, challengeSeeds } = getLastStepFileContent();
|
||||||
|
|
||||||
createStepFile({ stepNum: nextStepNum, projectPath, challengeSeeds });
|
createStepFile({ stepNum: nextStepNum, projectPath, challengeSeeds });
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
const {
|
const { getArgValues } = require('./helpers/get-arg-values');
|
||||||
reorderSteps,
|
const { getExistingStepNums } = require('./helpers/get-existing-step-nums');
|
||||||
createStepFile,
|
const { getProjectPath } = require('./helpers/get-project-path');
|
||||||
getChallengeSeeds,
|
const { padWithLeadingZeros } = require('./helpers/pad-with-leading-zeros');
|
||||||
padWithLeadingZeros,
|
const { createStepFile, getChallengeSeeds, reorderSteps } = require('./utils');
|
||||||
getExistingStepNums,
|
|
||||||
getProjectPath,
|
|
||||||
getArgValues
|
|
||||||
} = require('./utils');
|
|
||||||
|
|
||||||
const allStepsExist = (steps, stepsToFind) =>
|
const allStepsExist = (steps, stepsToFind) =>
|
||||||
stepsToFind.every(num => steps.includes(num));
|
stepsToFind.every(num => steps.includes(num));
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const { getArgValues } = require('./helpers/get-arg-values');
|
||||||
const {
|
const { getExistingStepNums } = require('./helpers/get-existing-step-nums');
|
||||||
reorderSteps,
|
const { getProjectPath } = require('./helpers/get-project-path');
|
||||||
padWithLeadingZeros,
|
const { padWithLeadingZeros } = require('./helpers/pad-with-leading-zeros');
|
||||||
getExistingStepNums,
|
const { reorderSteps } = require('./utils');
|
||||||
getProjectPath,
|
|
||||||
getArgValues
|
|
||||||
} = require('./utils');
|
|
||||||
|
|
||||||
const stepExists = (steps, stepToFind) => steps.includes(stepToFind);
|
const stepExists = (steps, stepToFind) => steps.includes(stepToFind);
|
||||||
|
|
||||||
|
1
tools/challenge-helper-scripts/helpers/get-arg-values.d.ts
vendored
Normal file
1
tools/challenge-helper-scripts/helpers/get-arg-values.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export declare function getArgValues(argv: string[]): Record<string, string>;
|
15
tools/challenge-helper-scripts/helpers/get-arg-values.js
Normal file
15
tools/challenge-helper-scripts/helpers/get-arg-values.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// 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 = []) {
|
||||||
|
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 };
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getArgValues = getArgValues;
|
@ -0,0 +1,34 @@
|
|||||||
|
const { getArgValues } = require('./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
tools/challenge-helper-scripts/helpers/get-existing-step-nums.d.ts
vendored
Normal file
1
tools/challenge-helper-scripts/helpers/get-existing-step-nums.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export declare function getExistingStepNums(options: string[]): number[];
|
@ -0,0 +1,30 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Generates an array with the output of processing filenames with an expected
|
||||||
|
// format (`part-###.md`).
|
||||||
|
// ['part-001.md', 'part-002.md'] => [1, 2]
|
||||||
|
function getExistingStepNums(projectPath) {
|
||||||
|
return fs.readdirSync(projectPath).reduce((stepNums, fileName) => {
|
||||||
|
if (
|
||||||
|
path.extname(fileName).toLowerCase() === '.md' &&
|
||||||
|
!fileName.endsWith('final.md')
|
||||||
|
) {
|
||||||
|
let stepNum = fileName.split('.')[0].split('-')[1];
|
||||||
|
|
||||||
|
if (!/^\d{3}$/.test(stepNum)) {
|
||||||
|
throw (
|
||||||
|
`Step not created. File ${fileName} has a step number containing non-digits.` +
|
||||||
|
' Please run reorder-steps script first.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
stepNum = parseInt(stepNum, 10);
|
||||||
|
stepNums.push(stepNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stepNums;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getExistingStepNums = getExistingStepNums;
|
@ -0,0 +1,74 @@
|
|||||||
|
const mock = require('mock-fs');
|
||||||
|
const { getExistingStepNums } = require('./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': {
|
||||||
|
'part-001.md': 'Lorem ipsum...',
|
||||||
|
'part-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...',
|
||||||
|
'part-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': {
|
||||||
|
'part-001.md': 'Lorem ipsum...',
|
||||||
|
'part-002.md': 'Lorem ipsum...',
|
||||||
|
'part002.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': {
|
||||||
|
'part-001.js': 'Lorem ipsum...',
|
||||||
|
'part-002.css': 'Lorem ipsum...'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const folder = `${process.cwd()}/mock-project/`;
|
||||||
|
const steps = getExistingStepNums(folder);
|
||||||
|
const expected = [];
|
||||||
|
|
||||||
|
expect(steps).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
mock.restore();
|
||||||
|
});
|
||||||
|
});
|
1
tools/challenge-helper-scripts/helpers/get-last-step-file-content.d.ts
vendored
Normal file
1
tools/challenge-helper-scripts/helpers/get-last-step-file-content.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export declare function getLastStepFileContent(): Record<string, unknown>;
|
@ -0,0 +1,35 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { getChallengeSeeds } = require('../utils');
|
||||||
|
const { getProjectPath } = require('./get-project-path');
|
||||||
|
|
||||||
|
// Looks up the last file found with format `part-###.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() {
|
||||||
|
const filesArr = [];
|
||||||
|
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];
|
||||||
|
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.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
challengeSeeds: getChallengeSeeds(projectPath + fileName),
|
||||||
|
nextStepNum: lastStepFileNum + 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getLastStepFileContent = getLastStepFileContent;
|
@ -0,0 +1,57 @@
|
|||||||
|
const mock = require('mock-fs');
|
||||||
|
const { getLastStepFileContent } = require('./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/': {
|
||||||
|
'part-001.md': 'Lorem ipsum...',
|
||||||
|
'part-004.md': 'Lorem ipsum...',
|
||||||
|
'final.md': 'Lorem ipsum...'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
getLastStepFileContent();
|
||||||
|
}).toThrow();
|
||||||
|
|
||||||
|
mock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return information if steps count is correct', () => {
|
||||||
|
mock({
|
||||||
|
'mock-project': {
|
||||||
|
'part-001.md': 'Lorem ipsum...',
|
||||||
|
'part-002.md': 'Lorem ipsum...',
|
||||||
|
'final.md': 'Lorem ipsum...'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
nextStepNum: 3,
|
||||||
|
challengeSeeds: {
|
||||||
|
lorem: 'ipsum'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getLastStepFileContent()).toEqual(expected);
|
||||||
|
|
||||||
|
mock.restore();
|
||||||
|
});
|
||||||
|
});
|
4
tools/challenge-helper-scripts/helpers/get-project-meta-path.d.ts
vendored
Normal file
4
tools/challenge-helper-scripts/helpers/get-project-meta-path.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export declare function getProjectMetaPath(
|
||||||
|
curriculumPath: string,
|
||||||
|
projectName: string
|
||||||
|
): string;
|
@ -0,0 +1,22 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Returns the path of the meta file associated to arguments provided.
|
||||||
|
const getProjectMetaPath = (curriculumPath, projectName) => {
|
||||||
|
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'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getProjectMetaPath = getProjectMetaPath;
|
@ -0,0 +1,31 @@
|
|||||||
|
const { getProjectMetaPath } = require('./get-project-meta-path');
|
||||||
|
|
||||||
|
describe('getProjectMetaPath helper', () => {
|
||||||
|
it('should throw if args are invalid', () => {
|
||||||
|
expect(() => {
|
||||||
|
getProjectMetaPath();
|
||||||
|
}).toThrow();
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
getProjectMetaPath('test-curriculum', {});
|
||||||
|
}).toThrow();
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
getProjectMetaPath([], []);
|
||||||
|
}).toThrow();
|
||||||
|
|
||||||
|
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
tools/challenge-helper-scripts/helpers/get-project-path-metadata.d.ts
vendored
Normal file
1
tools/challenge-helper-scripts/helpers/get-project-path-metadata.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export declare function getMetaData(string): Record<string, unknown>;
|
@ -0,0 +1,16 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// Process the contents of a argument (json) to an Object
|
||||||
|
function getMetaData(file) {
|
||||||
|
let metaData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
metaData = fs.readFileSync(file);
|
||||||
|
} catch (err) {
|
||||||
|
throw `No _meta.json file exists at ${file}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(metaData);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getMetaData = getMetaData;
|
@ -0,0 +1,31 @@
|
|||||||
|
jest.mock('fs', () => {
|
||||||
|
return {
|
||||||
|
readFileSync: jest.fn()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockPath = '/mock/path';
|
||||||
|
const { readFileSync } = require('fs');
|
||||||
|
const { getMetaData } = require('./get-project-path-metadata');
|
||||||
|
|
||||||
|
describe('getMetaData helper', () => {
|
||||||
|
it('should process requested file', () => {
|
||||||
|
readFileSync.mockImplementation(() => '{"name": "Test Project"}');
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
name: 'Test Project'
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getMetaData(mockPath)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if file is not found', () => {
|
||||||
|
readFileSync.mockImplementation(() => {
|
||||||
|
throw new Error();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
getMetaData(mockPath);
|
||||||
|
}).toThrowError(new Error(`No _meta.json file exists at ${mockPath}`));
|
||||||
|
});
|
||||||
|
});
|
1
tools/challenge-helper-scripts/helpers/get-project-path.d.ts
vendored
Normal file
1
tools/challenge-helper-scripts/helpers/get-project-path.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export declare function getProjectPath(): string;
|
@ -0,0 +1,8 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Returns the path of a project
|
||||||
|
function getProjectPath() {
|
||||||
|
return (process.env.CALLING_DIR || process.cwd()) + path.sep;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getProjectPath = getProjectPath;
|
@ -0,0 +1,22 @@
|
|||||||
|
const { getProjectPath } = require('./get-project-path');
|
||||||
|
|
||||||
|
describe('getProjectPath helper', () => {
|
||||||
|
it('should return the calling dir path', () => {
|
||||||
|
const mockCallingDir = 'calling/dir';
|
||||||
|
const expected = `${mockCallingDir}/`;
|
||||||
|
|
||||||
|
// Add mock to test condition
|
||||||
|
process.env.CALLING_DIR = mockCallingDir;
|
||||||
|
|
||||||
|
expect(getProjectPath()).toEqual(expected);
|
||||||
|
|
||||||
|
// Remove mock to not affect other tests
|
||||||
|
delete process.env.CALLING_DIR;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the projects absolute path', () => {
|
||||||
|
const expected = `${process.cwd()}/`;
|
||||||
|
|
||||||
|
expect(getProjectPath()).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
8
tools/challenge-helper-scripts/helpers/get-step-template.d.ts
vendored
Normal file
8
tools/challenge-helper-scripts/helpers/get-step-template.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
type StepOptions = {
|
||||||
|
challengeId: string;
|
||||||
|
challengeSeeds: unknown;
|
||||||
|
stepBetween: boolean;
|
||||||
|
stepNum: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export declare function getStepTemplate(options: StepOptions): string;
|
80
tools/challenge-helper-scripts/helpers/get-step-template.js
Normal file
80
tools/challenge-helper-scripts/helpers/get-step-template.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
const { insertErms } = require('./insert-erms');
|
||||||
|
|
||||||
|
// Builds a block
|
||||||
|
function getCodeBlock(label, content) {
|
||||||
|
return `\`\`\`${label}
|
||||||
|
${typeof content !== 'undefined' ? content : ''}
|
||||||
|
\`\`\``;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds a section
|
||||||
|
function getSeedSection(content, label) {
|
||||||
|
return content
|
||||||
|
? `
|
||||||
|
|
||||||
|
## --${label}--
|
||||||
|
|
||||||
|
${content}`
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the base markdown for a step
|
||||||
|
function getStepTemplate({
|
||||||
|
challengeId,
|
||||||
|
challengeSeeds,
|
||||||
|
stepBetween,
|
||||||
|
stepNum
|
||||||
|
}) {
|
||||||
|
const seedTexts = Object.values(challengeSeeds)
|
||||||
|
.map(({ contents, ext, editableRegionBoundaries }) => {
|
||||||
|
const fullContents = insertErms(contents, editableRegionBoundaries);
|
||||||
|
return getCodeBlock(ext, fullContents);
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const seedHeads = Object.values(challengeSeeds)
|
||||||
|
.filter(({ head }) => head)
|
||||||
|
.map(({ ext, head }) => getCodeBlock(ext, head))
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const seedTails = Object.values(challengeSeeds)
|
||||||
|
.filter(({ tail }) => tail)
|
||||||
|
.map(({ ext, tail }) => getCodeBlock(ext, tail))
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const descStepNum = stepBetween ? stepNum + 1 : stepNum;
|
||||||
|
|
||||||
|
const stepDescription = `${
|
||||||
|
stepBetween ? 'new ' : ''
|
||||||
|
}step ${descStepNum} instructions`;
|
||||||
|
|
||||||
|
const seedChallengeSection = getSeedSection(seedTexts, 'seed-contents');
|
||||||
|
const seedHeadSection = getSeedSection(seedHeads, 'before-user-code');
|
||||||
|
const seedTailSection = getSeedSection(seedTails, 'after-user-code');
|
||||||
|
|
||||||
|
return (
|
||||||
|
`---
|
||||||
|
id: ${challengeId}
|
||||||
|
title: Part ${stepNum}
|
||||||
|
challengeType: 0
|
||||||
|
dashedName: part-${stepNum}
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
${stepDescription}
|
||||||
|
|
||||||
|
# --hints--
|
||||||
|
|
||||||
|
Test 1
|
||||||
|
|
||||||
|
${getCodeBlock('js')}
|
||||||
|
|
||||||
|
# --seed--` +
|
||||||
|
seedChallengeSection +
|
||||||
|
seedHeadSection +
|
||||||
|
seedTailSection
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getStepTemplate = getStepTemplate;
|
@ -0,0 +1,56 @@
|
|||||||
|
const { getStepTemplate } = require('./get-step-template');
|
||||||
|
|
||||||
|
// Note: evaluates at highlevel the process, but seedHeads and seedTails could
|
||||||
|
// be tested if more specifics are needed.
|
||||||
|
describe('getStepTemplate util', () => {
|
||||||
|
it('should be able to create a markdown', () => {
|
||||||
|
const baseOutput = `---
|
||||||
|
id: 60d4ebe4801158d1abe1b18f
|
||||||
|
title: Part 5
|
||||||
|
challengeType: 0
|
||||||
|
dashedName: part-5
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
step 5 instructions
|
||||||
|
|
||||||
|
# --hints--
|
||||||
|
|
||||||
|
Test 1
|
||||||
|
|
||||||
|
\`\`\`js
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
# --seed--
|
||||||
|
|
||||||
|
## --seed-contents--
|
||||||
|
|
||||||
|
\`\`\`html
|
||||||
|
--fcc-editable-region--
|
||||||
|
|
||||||
|
--fcc-editable-region--
|
||||||
|
\`\`\``;
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
challengeId: '60d4ebe4801158d1abe1b18f',
|
||||||
|
challengeSeeds: {
|
||||||
|
indexhtml: {
|
||||||
|
contents: '',
|
||||||
|
editableRegionBoundaries: [0, 2],
|
||||||
|
ext: 'html',
|
||||||
|
head: '',
|
||||||
|
id: '',
|
||||||
|
key: 'indexhtml',
|
||||||
|
name: 'index',
|
||||||
|
tail: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stepBetween: false,
|
||||||
|
stepNum: 5
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getStepTemplate(props)).toEqual(baseOutput);
|
||||||
|
});
|
||||||
|
});
|
1
tools/challenge-helper-scripts/helpers/insert-erms.d.ts
vendored
Normal file
1
tools/challenge-helper-scripts/helpers/insert-erms.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export declare function insertErms(seedCode: string, erms: number[]): string;
|
30
tools/challenge-helper-scripts/helpers/insert-erms.js
Normal file
30
tools/challenge-helper-scripts/helpers/insert-erms.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Update given value with markers (labels)
|
||||||
|
const insertErms = (seedCode, erms) => {
|
||||||
|
if (!erms || erms.length <= 1) {
|
||||||
|
throw `erms should be provided`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (erms.length <= 1) {
|
||||||
|
throw `erms should contain 2 elements`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const separator = '\n';
|
||||||
|
const lines = seedCode.split(separator);
|
||||||
|
const markerLabel = '--fcc-editable-region--';
|
||||||
|
|
||||||
|
// Generate a version of seed code with the erm markers
|
||||||
|
const newSeedCode = erms
|
||||||
|
.slice(0, 2)
|
||||||
|
.reduce((acc, erm) => {
|
||||||
|
if (Number.isInteger(erm)) {
|
||||||
|
acc.splice(erm, 0, markerLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, lines)
|
||||||
|
.join(separator);
|
||||||
|
|
||||||
|
return newSeedCode;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.insertErms = insertErms;
|
55
tools/challenge-helper-scripts/helpers/insert-erms.test.js
Normal file
55
tools/challenge-helper-scripts/helpers/insert-erms.test.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
const { insertErms } = require('./insert-erms');
|
||||||
|
|
||||||
|
describe('insertErms helper', () => {
|
||||||
|
const code = `<h1>Hello World</h1>
|
||||||
|
<main>
|
||||||
|
<h2>CatPhotoApp</h2>
|
||||||
|
<img src="https://www.bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
|
||||||
|
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
|
||||||
|
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
|
||||||
|
</main>`;
|
||||||
|
|
||||||
|
it('should throw error if erm is undefined', () => {
|
||||||
|
expect(() => {
|
||||||
|
insertErms(code);
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if erm length is less than 2', () => {
|
||||||
|
const items = [[], [1]];
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
expect(() => {
|
||||||
|
insertErms(code, item);
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update code with markers if provided', () => {
|
||||||
|
const newCode = `--fcc-editable-region--
|
||||||
|
<h1>Hello World</h1>
|
||||||
|
<main>
|
||||||
|
<h2>CatPhotoApp</h2>
|
||||||
|
<img src="https://www.bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
|
||||||
|
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
|
||||||
|
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
|
||||||
|
--fcc-editable-region--
|
||||||
|
</main>`;
|
||||||
|
|
||||||
|
expect(insertErms(code, [0, 7])).toEqual(newCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update code with 2 markers if more are provided', () => {
|
||||||
|
const newCode = `<h1>Hello World</h1>
|
||||||
|
<main>
|
||||||
|
--fcc-editable-region--
|
||||||
|
<h2>CatPhotoApp</h2>
|
||||||
|
--fcc-editable-region--
|
||||||
|
<img src="https://www.bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
|
||||||
|
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
|
||||||
|
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
|
||||||
|
</main>`;
|
||||||
|
|
||||||
|
expect(insertErms(code, [2, 4, 6, 7])).toEqual(newCode);
|
||||||
|
});
|
||||||
|
});
|
1
tools/challenge-helper-scripts/helpers/pad-with-leading-zeros.d.ts
vendored
Normal file
1
tools/challenge-helper-scripts/helpers/pad-with-leading-zeros.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export declare function padWithLeadingZeros(value: number | string): string;
|
@ -0,0 +1,18 @@
|
|||||||
|
// 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) {
|
||||||
|
if (!(typeof value === 'number' || typeof value === 'string')) {
|
||||||
|
throw `${value} should be of type number or string`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newValue = '' + value;
|
||||||
|
|
||||||
|
if (newValue.length > 3) {
|
||||||
|
throw `${newValue} should be less than 4 characters`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newValue.padStart(3, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.padWithLeadingZeros = padWithLeadingZeros;
|
@ -0,0 +1,35 @@
|
|||||||
|
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 invalid values', () => {
|
||||||
|
const items = ['undefined', null, []];
|
||||||
|
|
||||||
|
items.forEach(item =>
|
||||||
|
expect(() => {
|
||||||
|
if (item !== 'undefined') {
|
||||||
|
padWithLeadingZeros(item);
|
||||||
|
} else {
|
||||||
|
padWithLeadingZeros();
|
||||||
|
}
|
||||||
|
}).toThrow()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw on valid values that are longer that 3 characters', () => {
|
||||||
|
expect(() => {
|
||||||
|
padWithLeadingZeros('19850809');
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
8
tools/challenge-helper-scripts/utils.d.ts
vendored
8
tools/challenge-helper-scripts/utils.d.ts
vendored
@ -1,10 +1,10 @@
|
|||||||
type CreateStepOptions = {
|
type CreateStepOptions = {
|
||||||
projectPath: string;
|
|
||||||
stepNum: number;
|
|
||||||
challengeSeeds: Record<string, unknown>;
|
challengeSeeds: Record<string, unknown>;
|
||||||
|
projectPath: string;
|
||||||
stepBetween: boolean;
|
stepBetween: boolean;
|
||||||
|
stepNum: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare function createStepFile(options: CreateStepOptions): string;
|
export declare function createStepFile(options: CreateStepOptions): string;
|
||||||
|
export declare function reorderSteps(): void;
|
||||||
// TODO: the rest of the functions
|
export declare function getChallengeSeeds(string): Record<string, unknown>;
|
||||||
|
@ -3,22 +3,13 @@ const path = require('path');
|
|||||||
const matter = require('gray-matter');
|
const matter = require('gray-matter');
|
||||||
const ObjectID = require('bson-objectid');
|
const ObjectID = require('bson-objectid');
|
||||||
const { parseMDSync } = require('../challenge-parser/parser');
|
const { parseMDSync } = require('../challenge-parser/parser');
|
||||||
|
const {
|
||||||
const padWithLeadingZeros = originalNum => {
|
getMetaData
|
||||||
/* always want file step numbers 3 digits */
|
} = require('../challenge-helper-scripts/helpers/get-project-path-metadata');
|
||||||
return ('' + originalNum).padStart(3, '0');
|
const { getStepTemplate } = require('./helpers/get-step-template');
|
||||||
};
|
const { getProjectMetaPath } = require('./helpers/get-project-meta-path');
|
||||||
|
const { getProjectPath } = require('./helpers/get-project-path');
|
||||||
const insertErms = (seedCode, erms) => {
|
const { padWithLeadingZeros } = require('./helpers/pad-with-leading-zeros');
|
||||||
const lines = seedCode.split('\n');
|
|
||||||
if (Number.isInteger(erms[0])) {
|
|
||||||
lines.splice(erms[0], 0, '--fcc-editable-region--');
|
|
||||||
}
|
|
||||||
if (Number.isInteger(erms[1])) {
|
|
||||||
lines.splice(erms[1], 0, '--fcc-editable-region--');
|
|
||||||
}
|
|
||||||
return lines.join('\n');
|
|
||||||
};
|
|
||||||
|
|
||||||
const createStepFile = ({
|
const createStepFile = ({
|
||||||
projectPath,
|
projectPath,
|
||||||
@ -26,88 +17,20 @@ const createStepFile = ({
|
|||||||
challengeSeeds = {},
|
challengeSeeds = {},
|
||||||
stepBetween = false
|
stepBetween = false
|
||||||
}) => {
|
}) => {
|
||||||
const seedTexts = Object.values(challengeSeeds).map(
|
|
||||||
({ contents, ext, editableRegionBoundaries }) => {
|
|
||||||
const fullContents = insertErms(contents, editableRegionBoundaries);
|
|
||||||
return `\`\`\`${ext}
|
|
||||||
${fullContents}
|
|
||||||
\`\`\``;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const seedHeads = Object.values(challengeSeeds)
|
|
||||||
.filter(({ head }) => head)
|
|
||||||
.map(
|
|
||||||
({ ext, head }) => `\`\`\`${ext}
|
|
||||||
${head}
|
|
||||||
\`\`\``
|
|
||||||
)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
const seedTails = Object.values(challengeSeeds)
|
|
||||||
.filter(({ tail }) => tail)
|
|
||||||
.map(
|
|
||||||
({ ext, tail }) => `\`\`\`${ext}
|
|
||||||
${tail}
|
|
||||||
\`\`\``
|
|
||||||
)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
const descStepNum = stepBetween ? stepNum + 1 : stepNum;
|
|
||||||
const stepDescription = `${
|
|
||||||
stepBetween ? 'new' : ''
|
|
||||||
} step ${descStepNum} instructions`;
|
|
||||||
const challengeSeedSection = `
|
|
||||||
# --seed--
|
|
||||||
|
|
||||||
## --seed-contents--
|
|
||||||
|
|
||||||
${seedTexts.join('\n')}`;
|
|
||||||
|
|
||||||
const seedHeadSection = seedHeads
|
|
||||||
? `
|
|
||||||
|
|
||||||
## --before-user-code--
|
|
||||||
|
|
||||||
${seedHeads}`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const seedTailSection = seedTails
|
|
||||||
? `
|
|
||||||
|
|
||||||
## --after-user-code--
|
|
||||||
|
|
||||||
${seedTails}`
|
|
||||||
: '';
|
|
||||||
const challengeId = ObjectID();
|
const challengeId = ObjectID();
|
||||||
|
|
||||||
const template =
|
|
||||||
`---
|
|
||||||
id: ${challengeId}
|
|
||||||
title: Part ${stepNum}
|
|
||||||
challengeType: 0
|
|
||||||
dashedName: part-${stepNum}
|
|
||||||
---
|
|
||||||
|
|
||||||
# --description--
|
|
||||||
|
|
||||||
${stepDescription}
|
|
||||||
|
|
||||||
# --hints--
|
|
||||||
|
|
||||||
Test 1
|
|
||||||
|
|
||||||
\`\`\`js
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
` +
|
|
||||||
challengeSeedSection +
|
|
||||||
seedHeadSection +
|
|
||||||
seedTailSection;
|
|
||||||
|
|
||||||
let finalStepNum = padWithLeadingZeros(stepNum);
|
let finalStepNum = padWithLeadingZeros(stepNum);
|
||||||
finalStepNum += stepBetween ? 'a' : '';
|
finalStepNum += stepBetween ? 'a' : '';
|
||||||
|
|
||||||
|
const template = getStepTemplate({
|
||||||
|
challengeId,
|
||||||
|
challengeSeeds,
|
||||||
|
stepBetween,
|
||||||
|
stepNum
|
||||||
|
});
|
||||||
|
|
||||||
fs.writeFileSync(`${projectPath}part-${finalStepNum}.md`, template);
|
fs.writeFileSync(`${projectPath}part-${finalStepNum}.md`, template);
|
||||||
|
|
||||||
return challengeId;
|
return challengeId;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -122,20 +45,9 @@ const reorderSteps = () => {
|
|||||||
? ''
|
? ''
|
||||||
: path.join(__dirname, '../');
|
: path.join(__dirname, '../');
|
||||||
|
|
||||||
const projectMetaPath = path.resolve(
|
const projectMetaPath = getProjectMetaPath(curriculumPath, projectName);
|
||||||
curriculumPath,
|
|
||||||
'challenges',
|
|
||||||
'_meta',
|
|
||||||
projectName,
|
|
||||||
'meta.json'
|
|
||||||
);
|
|
||||||
|
|
||||||
let metaData;
|
const parsedData = getMetaData(projectMetaPath);
|
||||||
try {
|
|
||||||
metaData = fs.readFileSync(projectMetaPath);
|
|
||||||
} catch (err) {
|
|
||||||
throw `No _meta.json file exists at ${projectMetaPath}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let foundFinal = false;
|
let foundFinal = false;
|
||||||
const filesArr = [];
|
const filesArr = [];
|
||||||
@ -167,7 +79,6 @@ const reorderSteps = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const challengeOrder = [];
|
const challengeOrder = [];
|
||||||
const parsedData = JSON.parse(metaData);
|
|
||||||
|
|
||||||
filesToReorder.forEach(({ oldFileName, newFileName, newStepNum }) => {
|
filesToReorder.forEach(({ oldFileName, newFileName, newStepNum }) => {
|
||||||
fs.renameSync(
|
fs.renameSync(
|
||||||
@ -199,52 +110,14 @@ const reorderSteps = () => {
|
|||||||
|
|
||||||
const newMeta = { ...parsedData, challengeOrder };
|
const newMeta = { ...parsedData, challengeOrder };
|
||||||
fs.writeFileSync(projectMetaPath, JSON.stringify(newMeta, null, 2));
|
fs.writeFileSync(projectMetaPath, JSON.stringify(newMeta, null, 2));
|
||||||
console.log('Reordered steps');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getChallengeSeeds = challengeFilePath => {
|
const getChallengeSeeds = challengeFilePath => {
|
||||||
return parseMDSync(challengeFilePath).files;
|
return parseMDSync(challengeFilePath).files;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getExistingStepNums = projectPath => {
|
|
||||||
return fs.readdirSync(projectPath).reduce((stepNums, fileName) => {
|
|
||||||
if (
|
|
||||||
path.extname(fileName).toLowerCase() === '.md' &&
|
|
||||||
!fileName.endsWith('final.md')
|
|
||||||
) {
|
|
||||||
let stepNum = fileName.split('.')[0].split('-')[1];
|
|
||||||
if (!/^\d{3}$/.test(stepNum)) {
|
|
||||||
throw (
|
|
||||||
`Step not created. File ${fileName} has a step number containing non-digits.` +
|
|
||||||
' Please run reorder-steps script first.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
stepNum = parseInt(stepNum, 10);
|
|
||||||
stepNums.push(stepNum);
|
|
||||||
}
|
|
||||||
return stepNums;
|
|
||||||
}, []);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getProjectPath = () =>
|
|
||||||
(process.env.CALLING_DIR || process.cwd()) + path.sep;
|
|
||||||
|
|
||||||
const getArgValues = argv => {
|
|
||||||
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 };
|
|
||||||
}, {});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createStepFile,
|
createStepFile,
|
||||||
getChallengeSeeds,
|
getChallengeSeeds,
|
||||||
padWithLeadingZeros,
|
reorderSteps
|
||||||
reorderSteps,
|
|
||||||
getExistingStepNums,
|
|
||||||
getProjectPath,
|
|
||||||
getArgValues
|
|
||||||
};
|
};
|
||||||
|
139
tools/challenge-helper-scripts/utils.test.js
Normal file
139
tools/challenge-helper-scripts/utils.test.js
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const mock = require('mock-fs');
|
||||||
|
const glob = require('glob');
|
||||||
|
|
||||||
|
// NOTE:
|
||||||
|
// Use `console.log()` before mocking the filesystem or use
|
||||||
|
// `process.stdout.write()` instead. There are issues when using `mock-fs` and
|
||||||
|
// `require`.
|
||||||
|
|
||||||
|
jest.mock('bson-objectid', () => {
|
||||||
|
return jest.fn(() => mockChallengeId);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('./helpers/get-step-template', () => {
|
||||||
|
return {
|
||||||
|
getStepTemplate: jest.fn(() => 'Mock 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 ObjectID = require('bson-objectid');
|
||||||
|
const { getStepTemplate } = require('./helpers/get-step-template');
|
||||||
|
const { createStepFile, reorderSteps } = require('./utils');
|
||||||
|
|
||||||
|
describe('Challenge utils helper scripts', () => {
|
||||||
|
describe('createStepFile util', () => {
|
||||||
|
it('should create next step and return its identifier', () => {
|
||||||
|
mock({
|
||||||
|
'project/': {
|
||||||
|
'part-001.md': 'Lorem ipsum...',
|
||||||
|
'part-002.md': 'Lorem ipsum...'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const step = createStepFile({
|
||||||
|
projectPath: 'project/',
|
||||||
|
stepNum: 3
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(step).toEqual(mockChallengeId);
|
||||||
|
expect(ObjectID).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Internal tasks
|
||||||
|
// - Should generate a template for the step that is being created
|
||||||
|
expect(getStepTemplate).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// - Should write a file with a given name and template
|
||||||
|
const files = glob.sync(`project/*.md`);
|
||||||
|
|
||||||
|
expect(files).toEqual([
|
||||||
|
`project/part-001.md`,
|
||||||
|
`project/part-002.md`,
|
||||||
|
`project/part-003.md`
|
||||||
|
]);
|
||||||
|
|
||||||
|
mock.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('reorderSteps util', () => {
|
||||||
|
it('should sort files found in given path', () => {
|
||||||
|
mock({
|
||||||
|
'_meta/project/': {
|
||||||
|
'meta.json': 'Lorem ipsum meta content...'
|
||||||
|
},
|
||||||
|
'project/': {
|
||||||
|
'part-001.md': 'Lorem ipsum 1...',
|
||||||
|
'part-002.md': 'Lorem ipsum 2...',
|
||||||
|
'part-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/part-001.md',
|
||||||
|
'project/part-002.md',
|
||||||
|
'project/part-003.md'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = fs.readFileSync('_meta/project/meta.json', 'utf8');
|
||||||
|
|
||||||
|
const expectedResult = `{
|
||||||
|
"id": "mock-id",
|
||||||
|
"challengeOrder": [
|
||||||
|
[
|
||||||
|
"60d35cf3fe32df2ce8e31b03",
|
||||||
|
"Part 1"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"60d35cf3fe32df2ce8e31b03",
|
||||||
|
"Part 2"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"60d35cf3fe32df2ce8e31b03",
|
||||||
|
"Part 3"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}`;
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedResult);
|
||||||
|
|
||||||
|
mock.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Reference in New Issue
Block a user