diff --git a/.changeset/mean-bees-divide.md b/.changeset/mean-bees-divide.md new file mode 100644 index 0000000..4c801af --- /dev/null +++ b/.changeset/mean-bees-divide.md @@ -0,0 +1,5 @@ +--- +"eth-tech-tree": patch +--- + +Readme adjustments, Challenge formats revised diff --git a/src/actions/setup-challenge.ts b/src/actions/setup-challenge.ts index 0464b34..de05cf3 100644 --- a/src/actions/setup-challenge.ts +++ b/src/actions/setup-challenge.ts @@ -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 @@ -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 diff --git a/src/types.ts b/src/types.ts index 0a42e5f..cf84cd7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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; -}; - -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 { - 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 = ( - question: ExtensionQuestion -) => question; -export type Config = { - questions: ExtensionQuestion[]; -}; - -export const isDefined = (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 { diff --git a/src/utils/tree.ts b/src/utils/tree.ts index 2cb67cb..5878cb3 100644 --- a/src/utils/tree.ts +++ b/src/utils/tree.ts @@ -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"; @@ -74,10 +74,13 @@ async function selectNode(node: TreeNode): Promise { } 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 }; @@ -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); } } @@ -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) => { @@ -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 }); } @@ -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) {