Skip to content

Commit

Permalink
update to latest release (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
escottalexander authored Nov 1, 2024
2 parents 64c94b4 + c433b91 commit af1e165
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 154 deletions.
5 changes: 5 additions & 0 deletions .changeset/mean-bees-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eth-tech-tree": patch
---

Readme adjustments, Challenge formats revised
6 changes: 4 additions & 2 deletions src/actions/setup-challenge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ const README_CONTENT = {
- [Requirements](#requirements)
- [Start Here](#start-here)
- [Challenge Description](#challenge-description)
- [Testing Your Progress](#testing-your-progress)
- [Solved! (Final Steps)](#solved-final-steps)
## Requirements
Expand All @@ -178,9 +179,10 @@ Run the following commands in your terminal:
yarn install
foundryup
\`\`\``,
BOTTOM_CONTENT: `**Don't change any existing method names** as it will break tests but feel free to add additional methods if it helps you complete the task.
BOTTOM_CONTENT: `## Testing Your Progress
Use your skills to build out the above requirements in whatever way you choose. You are encouraged to run tests periodically to visualize your progress.
Start by using \`yarn foundry:test\` to run a set of tests against the contract code. You will see several failing tests. As you add functionality to the contract, periodically run the tests so you can see your progress and address blind spots. If you struggle to understand why some are returning errors then you might find it useful to run the command with the extra logging verbosity flag \`-vvvv\` (\`yarn foundry:test -vvvv\`) as this will show you very detailed information about where tests are failing. Learn how to read the traces [here](https://book.getfoundry.sh/forge/traces). You can also use the \`--match-test "TestName"\` flag to only run a single test. Of course you can chain both to include a higher verbosity and only run a specific test by including both flags \`yarn foundry:test -vvvv --match-test "TestName"\`. You will also see we have included an import of \`console2.sol\` which allows you to use \`console.log()\` type functionality inside your contracts to know what a value is at a specific time of execution. You can read more about how to use that at [FoundryBook](https://book.getfoundry.sh/reference/forge-std/console-log).
Run tests using \`yarn foundry:test\` to run a set of tests against the contract code. Initially you will see build errors but as you complete the requirements you will start to pass tests. If you struggle to understand why some tests are returning errors then you might find it useful to run the command with the extra logging verbosity flag \`-vvvv\` (\`yarn foundry:test -vvvv\`) as this will show you very detailed information about where tests are failing. Learn how to read the traces [here](https://book.getfoundry.sh/forge/traces). You can also use the \`--match-test "TestName"\` flag to only run a single test. Of course you can chain both to include a higher verbosity and only run a specific test by including both flags \`yarn foundry:test -vvvv --match-test "TestName"\`. You will also see we have included an import of \`console2.sol\` which allows you to use \`console.log()\` type functionality inside your contracts to know what a value is at a specific time of execution. You can read more about how to use that at [FoundryBook](https://book.getfoundry.sh/reference/forge-std/console-log).
For a more "hands on" approach you can try testing your contract with the provided front end interface by running the following:
\`\`\`bash
Expand Down
76 changes: 0 additions & 76 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,9 @@
import type { Question } from "inquirer";

export type Args = string[];

export type RawOptions = {
project: string | null;
install: boolean | null;
dev: boolean;
extensions: Extension[] | null;
};

type NonNullableRawOptions = {
[Prop in keyof RawOptions]: NonNullable<RawOptions[Prop]>;
};

export type Options = NonNullableRawOptions;

export type Extension =
| "hardhat"
| "foundry"
type NullExtension = null;
export type ExtensionOrNull = Extension | NullExtension;
// corresponds to inquirer question types:
// - multi-select -> checkbox
// - single-select -> list
type QuestionType = "multi-select" | "single-select";
interface ExtensionQuestion<T extends ExtensionOrNull[] = ExtensionOrNull[]> {
type: QuestionType;
extensions: T;
name: string;
message: Question["message"];
default?: T[number];
}

export const isExtension = (item: ExtensionOrNull): item is Extension =>
item !== null;

/**
* This function makes sure that the `T` generic type is narrowed down to
* whatever `extensions` are passed in the question prop. That way we can type
* check the `default` prop is not using any valid extension, but only one
* already provided in the `extensions` prop.
*
* Questions can be created without this function, just using a normal object,
* but `default` type will be any valid Extension.
*/
export const typedQuestion = <T extends ExtensionOrNull[]>(
question: ExtensionQuestion<T>
) => question;
export type Config = {
questions: ExtensionQuestion[];
};

export const isDefined = <T>(item: T | undefined | null): item is T =>
item !== undefined && item !== null;

export type ExtensionDescriptor = {
name: string;
value: Extension;
path: string;
extensions?: Extension[];
extends?: Extension;
};

export type ExtensionBranch = ExtensionDescriptor & {
extensions: Extension[];
};
export type ExtensionDict = {
[extension in Extension]: ExtensionDescriptor;
};

export const extensionWithSubextensions = (
extension: ExtensionDescriptor | undefined
): extension is ExtensionBranch => {
return Object.prototype.hasOwnProperty.call(extension, "extensions");
};

export type TemplateDescriptor = {
path: string;
fileUrl: string;
relativePath: string;
source: string;
};

export interface IUserChallenge {
Expand Down
162 changes: 86 additions & 76 deletions src/utils/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import inquirer from "inquirer";
import chalk from "chalk";
import { loadChallenges, loadUserState, saveUserState } from "./stateManager";
import { testChallenge, submitChallenge, setupChallenge } from "../actions";
import { IChallenge, IUserChallenge } from "../types";
import { IChallenge, IUser, IUserChallenge } from "../types";
import fs from "fs";
import { pressEnterToContinue } from "./helpers";
import { getUser } from "../modules/api";
Expand Down Expand Up @@ -74,10 +74,13 @@ async function selectNode(node: TreeNode): Promise<void> {
}
const actions = [backAction].concat((node.actions as Action[]).map(action => action));
const choices = actions.map(action => action.label);
const message = `${chalk.red(node.label)}
${node.message}
`;
const actionPrompt = {
type: "list",
name: "selectedAction",
message: "What would you like to do?",
message,
choices,
default: 1
};
Expand Down Expand Up @@ -193,12 +196,12 @@ function findHeader(allNodes: TreeNode, targetNode: TreeNode): TreeNode | undefi
}

// Nesting Magic - Recursive function to build nested tree structure
function NestingMagic(challenges: any[], parentName: string | undefined = undefined): TreeNode[] {
function nestingMagic(challenges: any[], parentName: string | undefined = undefined): TreeNode[] {
const tree: TreeNode[] = [];
for (let challenge of challenges) {
if (challenge.parentName === parentName) {
// Recursively call NestingMagic for each child
challenge.children = NestingMagic(challenges, challenge.name);
challenge.children = nestingMagic(challenges, challenge.name);
tree.push(challenge);
}
}
Expand All @@ -207,7 +210,7 @@ function NestingMagic(challenges: any[], parentName: string | undefined = undefi

export function buildTree(): TreeNode {
const userState = loadUserState();
const { address, installLocation, challenges: userChallenges } = userState;
const { challenges: userChallenges } = userState;
const tree: TreeNode[] = [];
const challenges = loadChallenges();
const tags = challenges.reduce((acc: string[], challenge: any) => {
Expand All @@ -218,90 +221,24 @@ export function buildTree(): TreeNode {
const filteredChallenges = challenges.filter((challenge: IChallenge) => challenge.tags.includes(tag));
let completedCount = 0;
const transformedChallenges = filteredChallenges.map((challenge: IChallenge) => {
const { label, name, level, type, repo, childrenNames, enabled: unlocked } = challenge;
const { label, name, level, type, childrenNames, enabled: unlocked, description } = challenge;
const parentName = challenges.find((c: any) => c.childrenNames?.includes(name))?.name;
const completed = userChallenges.find((c: IUserChallenge) => c.challengeName === name)?.status === "success";
if (completed) {
completedCount++;
}
// Build selection actions
const actions: Action[] = [];
if (type === "challenge") {
const targetDir = `${installLocation}/${name}`;
if (!fs.existsSync(targetDir)) {
actions.push({
label: "Setup Challenge Repository",
action: async () => {
console.clear();
await setupChallenge(name, installLocation);
// Rebuild the tree
globalTree = buildTree();
// Wait for enter key
await pressEnterToContinue();
// Return to challenge menu
const challengeNode = findNode(globalTree, name) as TreeNode;
await selectNode(challengeNode);
}
});
} else {
actions.push({
label: "Test Challenge",
action: async () => {
console.clear();
await testChallenge(name);
// Wait for enter key
await pressEnterToContinue();
// Return to challenge menu
const challengeNode = findNode(globalTree, name) as TreeNode;
await selectNode(challengeNode);
}
});
actions.push({
label: "Submit Completed Challenge",
action: async () => {
console.clear();
// Submit the challenge
await submitChallenge(name);
// Fetch users challenge state from the server
const newUserState = await getUser(address);
userState.challenges = newUserState.challenges;
// Save the new user state locally
await saveUserState(userState);
// Rebuild the tree
globalTree = buildTree();
// Wait for enter key
await pressEnterToContinue();
// Return to challenge menu
const challengeNode = findNode(globalTree, name) as TreeNode;
await selectNode(challengeNode);
}
});
}
} else if (type === "quiz") {
actions.push({
label: "Mark as Read",
action: async () => {
console.log("Marking as read...");
}
});
} else if (type === "capstone-project") {
actions.push({
label: "Submit Project",
action: async () => {
console.log("Submitting project...");
}
});
}
const actions: Action[] = getActions(userState, challenge);

return { label, name, level, type, actions, completed, childrenNames, parentName, unlocked };
return { label, name, level, type, actions, completed, childrenNames, parentName, unlocked, message: description };
});
const NestingChallenges = NestingMagic(transformedChallenges);
const nestedChallenges = nestingMagic(transformedChallenges);

tree.push({
type: "header",
label: `${tag} ${chalk.green(`(${completedCount}/${filteredChallenges.length})`)}`,
name: `${tag.toLowerCase()}`,
children: NestingChallenges,
children: nestedChallenges,
recursive: true
});
}
Expand All @@ -317,6 +254,79 @@ export function buildTree(): TreeNode {
return mainMenu;
}

function getActions(userState: IUser, challenge: IChallenge): Action[] {
const actions: Action[] = [];
const { address, installLocation } = userState;
const { type, name } = challenge;
if (type === "challenge") {
const targetDir = `${installLocation}/${name}`;
if (!fs.existsSync(targetDir)) {
actions.push({
label: "Setup Challenge Repository",
action: async () => {
console.clear();
await setupChallenge(name, installLocation);
// Rebuild the tree
globalTree = buildTree();
// Wait for enter key
await pressEnterToContinue();
// Return to challenge menu
const challengeNode = findNode(globalTree, name) as TreeNode;
await selectNode(challengeNode);
}
});
} else {
actions.push({
label: "Test Challenge",
action: async () => {
console.clear();
await testChallenge(name);
// Wait for enter key
await pressEnterToContinue();
// Return to challenge menu
const challengeNode = findNode(globalTree, name) as TreeNode;
await selectNode(challengeNode);
}
});
actions.push({
label: "Submit Completed Challenge",
action: async () => {
console.clear();
// Submit the challenge
await submitChallenge(name);
// Fetch users challenge state from the server
const newUserState = await getUser(address);
userState.challenges = newUserState.challenges;
// Save the new user state locally
await saveUserState(userState);
// Rebuild the tree
globalTree = buildTree();
// Wait for enter key
await pressEnterToContinue();
// Return to challenge menu
const challengeNode = findNode(globalTree, name) as TreeNode;
await selectNode(challengeNode);
}
});
}
} else if (type === "quiz") {
actions.push({
label: "Mark as Read",
action: async () => {
console.log("Marking as read...");
}
});
} else if (type === "capstone-project") {
actions.push({
label: "Submit Project",
action: async () => {
console.log("Submitting project...");
}
});
}
return actions;
};

function findNode(globalTree: TreeNode, name: string): TreeNode | undefined {
// Descend the tree until the node is found
if (globalTree.name === name) {
Expand Down

0 comments on commit af1e165

Please sign in to comment.