Skip to content

Commit 6e68f35

Browse files
committed
Allow generating new exercises in nested directories
Exercises dirs will no longer all be in the repo root but will be nested in dirs relating to the curriculum structure.
1 parent 41c43b3 commit 6e68f35

File tree

2 files changed

+124
-41
lines changed

2 files changed

+124
-41
lines changed

generators/helpers.js

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,56 @@
11
const { readdir } = require("fs/promises");
2+
const { basename, dirname, join } = require("path");
23

34
function splitDirectoryName(directoryName) {
5+
const exerciseDirectoryName = directoryName.endsWith("solution")
6+
? basename(dirname(directoryName))
7+
: basename(directoryName);
48
return {
5-
exerciseNumber: directoryName.match(/\d+/),
6-
exerciseName: directoryName.match(/[a-z]+/i),
9+
exerciseNumber: exerciseDirectoryName.match(/\d+/),
10+
exerciseName: exerciseDirectoryName.match(/[a-z]+/i),
711
};
812
}
913

10-
async function getLatestExerciseDirectory() {
14+
async function getDirsWithExercises(path) {
15+
const ignoredDirs = ["archive", "node_modules", "generators"];
1116
try {
12-
const files = await readdir("./");
17+
const dirs = await readdir(join(process.cwd(), path), {
18+
withFileTypes: true,
19+
});
20+
const exerciseDirs = dirs.filter(
21+
(entry) =>
22+
entry.isDirectory() &&
23+
!entry.name.startsWith(".") &&
24+
!ignoredDirs.includes(entry.name),
25+
);
26+
return exerciseDirs.map((dir) => dir.name);
27+
} catch {
28+
return [];
29+
}
30+
}
31+
32+
async function getLatestExerciseDirectory(path) {
33+
try {
34+
const files = await readdir(join(process.cwd(), path));
1335
return files.findLast((file) => /^\d+_\w+$/.test(file));
14-
} catch (err) {
15-
console.error(err);
36+
} catch {
37+
return "0";
1638
}
1739
}
1840

19-
async function createExerciseDirectoryName(directoryName) {
20-
const latestExerciseDirectory = await getLatestExerciseDirectory();
41+
async function createExerciseDirectoryName(exerciseName, path) {
42+
const latestExerciseDirectory = await getLatestExerciseDirectory(path);
2143
const latestExerciseNumber = parseInt(latestExerciseDirectory.match(/^\d+/));
2244

23-
if (latestExerciseDirectory === `${latestExerciseNumber}_${directoryName}`) {
24-
throw new Error(`Exercise already exists with name "${directoryName}"`);
45+
if (latestExerciseDirectory === `${latestExerciseNumber}_${exerciseName}`) {
46+
throw new Error(`Exercise already exists with name "${exerciseName}"`);
2547
}
2648

27-
return `${latestExerciseNumber + 1}_${directoryName}`;
49+
return `${latestExerciseNumber + 1}_${exerciseName}`;
2850
}
2951

30-
module.exports = { createExerciseDirectoryName, splitDirectoryName };
52+
module.exports = {
53+
getDirsWithExercises,
54+
createExerciseDirectoryName,
55+
splitDirectoryName,
56+
};

plopFile.js

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,103 @@
11
const { mkdir } = require("fs/promises");
22
const { join } = require("path");
33
const { camelCase } = require("case-anything");
4-
const { createExerciseDirectoryName } = require("./generators/helpers");
4+
const {
5+
createExerciseDirectoryName,
6+
getDirsWithExercises,
7+
} = require("./generators/helpers");
58
const { writeReadme } = require("./generators/writeReadme");
69
const { writeExercise } = require("./generators/writeExercise");
710
const { writeExerciseSpec } = require("./generators/writeExerciseSpec");
811

12+
/**
13+
* @typedef {import('plop').NodePlopAPI} Plop
14+
* @param {Plop} plop
15+
*/
916
module.exports = function (plop) {
10-
plop.setActionType("createExercise", async function (answers) {
11-
const { exerciseName } = answers;
12-
if (!exerciseName) {
13-
throw new Error(
14-
`Invalid exerciseName. Expected: valid string. Actual: "${exerciseName}"`
17+
const NEW_DIR_OPTION = "<Make new directory>";
18+
19+
plop.setActionType(
20+
"createExercise",
21+
async function ({ pathForExercise, exerciseName }) {
22+
if (!exerciseName) {
23+
throw new Error(
24+
`Invalid exerciseName. Expected: valid string. Actual: "${exerciseName}"`,
25+
);
26+
} else if (!pathForExercise.length) {
27+
throw new Error(
28+
"The new exercise cannot be placed in the project root",
29+
);
30+
}
31+
32+
const camelExerciseName = camelCase(exerciseName);
33+
const exerciseDirectoryName = await createExerciseDirectoryName(
34+
camelExerciseName,
35+
join(...pathForExercise),
1536
);
16-
}
17-
18-
const camelExerciseName = camelCase(exerciseName);
19-
const exerciseDirectoryName = await createExerciseDirectoryName(
20-
camelExerciseName
21-
);
22-
const basePath = join("./", exerciseDirectoryName);
23-
const solutionPath = join(basePath, "solution");
24-
25-
await mkdir(basePath);
26-
await mkdir(solutionPath);
27-
28-
await writeReadme(basePath);
29-
await writeExercise(basePath);
30-
await writeExercise(solutionPath);
31-
await writeExerciseSpec(basePath);
32-
await writeExerciseSpec(solutionPath);
33-
});
37+
const basePath = join(
38+
process.cwd(),
39+
...pathForExercise,
40+
exerciseDirectoryName,
41+
);
42+
const solutionPath = join(basePath, "solution");
43+
44+
await mkdir(basePath, { recursive: true });
45+
await mkdir(solutionPath);
46+
47+
await writeReadme(basePath);
48+
await writeExercise(basePath);
49+
await writeExercise(solutionPath);
50+
await writeExerciseSpec(basePath);
51+
await writeExerciseSpec(solutionPath);
52+
},
53+
);
3454

3555
plop.setGenerator("Basic", {
3656
description: "Create a basic JavaScript exercise.",
37-
prompts: [
38-
{
57+
prompts: async function (inquirer) {
58+
async function getPathForExercise(dirPath = []) {
59+
const exerciseDirs = await getDirsWithExercises(dirPath.join("/"));
60+
61+
// Will only be empty when entering a new dir on a recursive call
62+
// Recursive call only happens when new dir required which can bypass this question
63+
const { dir } = exerciseDirs.length
64+
? await inquirer.prompt({
65+
type: "list",
66+
name: "dir",
67+
message: "Which directory should this exercise go in?",
68+
choices: [NEW_DIR_OPTION, ...exerciseDirs],
69+
})
70+
: { dir: NEW_DIR_OPTION };
71+
72+
if (dir === NEW_DIR_OPTION) {
73+
const { newDirName } = await inquirer.prompt({
74+
type: "input",
75+
name: "newDirName",
76+
message: "What is the name of the new directory?",
77+
});
78+
dirPath.push(newDirName);
79+
} else {
80+
dirPath.push(dir);
81+
}
82+
83+
const { needMoreDirs } = await inquirer.prompt({
84+
type: "confirm",
85+
name: "needMoreDirs",
86+
message: "Does this exercise need to be nested in a subdirectory?",
87+
});
88+
89+
return needMoreDirs ? await getPathForExercise(dirPath) : dirPath;
90+
}
91+
92+
const pathForExercise = await getPathForExercise();
93+
const { exerciseName } = await inquirer.prompt({
3994
type: "input",
4095
name: "exerciseName",
41-
message: "What is the name of the exercise? (camelCase)",
42-
},
43-
],
96+
message: "What is the name of the new exercise (in camelCase)?",
97+
});
98+
99+
return { pathForExercise, exerciseName };
100+
},
44101
actions: [{ type: "createExercise" }],
45102
});
46103
};

0 commit comments

Comments
 (0)