├── cli ├── bash ├── src │ ├── fixtures.rs │ ├── main.rs │ ├── environment.rs │ ├── features.rs │ └── conf.rs └── Cargo.toml ├── self ├── external-seed │ └── .gitkeep ├── lesson-watch │ ├── watched.js │ └── unwatched.js ├── project-reset │ └── .gitkeep ├── learn-freecodecamp-os │ └── .gitkeep ├── build-x-using-y │ └── index.js ├── bash │ ├── sourcerer.sh │ └── .bashrc ├── curriculum │ ├── images │ │ └── fcc_primary_large.png │ ├── assertions │ │ └── afrikaans.json │ └── locales │ │ ├── english │ │ ├── external-seed-seed.md │ │ ├── lesson-watch.md │ │ ├── external-seed.md │ │ ├── project-reset.md │ │ └── build-x-using-y.md │ │ └── afrikaans │ │ └── build-x-using-y.md ├── config │ ├── state.json │ └── projects.json ├── tooling │ ├── plugins.js │ ├── helpers.js │ ├── adjust-url.js │ ├── rejig.js │ ├── camper-info.js │ └── extract-seed.js ├── package.json ├── .vscode │ ├── settings.json │ └── javascript.json.code-snippets ├── freecodecamp.conf.json ├── package-lock.json └── client │ ├── assets │ └── fcc_primary_small.svg │ └── injectable.js ├── .prettierignore ├── docs ├── src │ ├── resetting │ │ ├── reset.md │ │ └── lifecycle.md │ ├── lessoning │ │ ├── lesson.md │ │ └── lifecycle.md │ ├── roadmap.md │ ├── testing │ │ ├── test.md │ │ ├── lifecycle.md │ │ ├── globals.md │ │ └── test-utilities.md │ ├── introduction.md │ ├── examples.md │ ├── cli.md │ ├── SUMMARY.md │ ├── freecodecamp-courses.md │ ├── contributing.md │ ├── client-injection.md │ ├── plugin-system.md │ ├── getting-started.md │ └── configuration.md ├── theme │ ├── fonts │ │ ├── Lato-Black.woff │ │ ├── Lato-Bold.woff │ │ ├── Lato-Light.woff │ │ ├── Lato-Hairline.woff │ │ ├── Lato-Italic.woff │ │ ├── Lato-Regular.woff │ │ ├── Lato-BoldItalic.woff │ │ ├── Lato-BlackItalic.woff │ │ ├── Lato-LightItalic.woff │ │ ├── Hack-ZeroSlash-Bold.woff │ │ ├── Hack-ZeroSlash-Bold.woff2 │ │ ├── Lato-HairlineItalic.woff │ │ ├── Hack-ZeroSlash-Italic.woff │ │ ├── Hack-ZeroSlash-Italic.woff2 │ │ ├── Hack-ZeroSlash-Regular.woff │ │ ├── Hack-ZeroSlash-Regular.woff2 │ │ ├── Hack-ZeroSlash-BoldItalic.woff │ │ ├── Hack-ZeroSlash-BoldItalic.woff2 │ │ └── fonts.css │ ├── css │ │ ├── print.css │ │ └── general.css │ ├── highlight.css │ └── favicon.svg └── book.toml ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── .gitignore ├── Dockerfile ├── .freeCodeCamp ├── client │ ├── assets │ │ ├── Lato-Regular.woff │ │ └── fcc_primary_small.svg │ ├── components │ │ ├── tag.tsx │ │ ├── loader.tsx │ │ ├── ruler.tsx │ │ ├── description.tsx │ │ ├── progress.tsx │ │ ├── selection.tsx │ │ ├── test.tsx │ │ ├── tests.tsx │ │ ├── hints.tsx │ │ ├── console.tsx │ │ ├── header.tsx │ │ ├── error.tsx │ │ ├── checkmark.tsx │ │ ├── language-globe.tsx │ │ ├── language-list.tsx │ │ ├── heading.tsx │ │ ├── output.tsx │ │ ├── block.tsx │ │ └── controls.tsx │ ├── index.html │ ├── utils │ │ └── index.ts │ ├── templates │ │ ├── landing.tsx │ │ ├── landing.css │ │ ├── project.tsx │ │ └── project.css │ ├── types │ │ └── index.ts │ └── styles.css ├── tooling │ ├── logger.js │ ├── t.js │ ├── tests │ │ └── test-worker.js │ ├── reset.js │ ├── git │ │ ├── build.js │ │ └── gitterizer.js │ ├── seed.js │ ├── lesson.js │ ├── env.js │ ├── utils.js │ ├── client-socks.js │ ├── test-utils.js │ └── hot-reload.js ├── tsconfig.json ├── webpack.config.cjs ├── tests │ └── parser.test.js └── plugin │ └── index.js ├── renovate.json ├── .prettierrc ├── .editorconfig ├── .npmignore ├── .devcontainer └── devcontainer.json ├── .gitpod.yml ├── README.md ├── .github └── workflows │ ├── release.yml │ ├── link-check.yml │ ├── cli-release.yml │ └── mdbook.yml ├── LICENSE └── package.json /cli/bash: -------------------------------------------------------------------------------- 1 | ../self/bash/ -------------------------------------------------------------------------------- /self/external-seed/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /self/lesson-watch/watched.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /self/project-reset/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /self/lesson-watch/unwatched.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /self/learn-freecodecamp-os/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/.cache 2 | **/package-lock.json 3 | **/pkg 4 | -------------------------------------------------------------------------------- /self/build-x-using-y/index.js: -------------------------------------------------------------------------------- 1 | // I am an example boilerplate file 2 | -------------------------------------------------------------------------------- /docs/src/resetting/reset.md: -------------------------------------------------------------------------------- 1 | # Reset 2 | 3 | ```admonish todo 4 | 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/src/lessoning/lesson.md: -------------------------------------------------------------------------------- 1 | # Lesson 2 | 3 | ```admonish todo 4 | 5 | ``` 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": ["./cli/Cargo.toml"] 3 | } 4 | -------------------------------------------------------------------------------- /self/bash/sourcerer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source ./bash/.bashrc 3 | echo "BashRC Sourced" 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | See: https://opensource.freecodecamp.org/freeCodeCampOS/contributing.html 4 | -------------------------------------------------------------------------------- /docs/theme/fonts/Lato-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Lato-Black.woff -------------------------------------------------------------------------------- /docs/theme/fonts/Lato-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Lato-Bold.woff -------------------------------------------------------------------------------- /docs/theme/fonts/Lato-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Lato-Light.woff -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | node_modules 3 | Cargo.lock 4 | !.gitkeep 5 | .freeCodeCamp/dist 6 | .DS_Store 7 | /docs/book 8 | self/.logs/ -------------------------------------------------------------------------------- /docs/theme/fonts/Lato-Hairline.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Lato-Hairline.woff -------------------------------------------------------------------------------- /docs/theme/fonts/Lato-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Lato-Italic.woff -------------------------------------------------------------------------------- /docs/theme/fonts/Lato-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Lato-Regular.woff -------------------------------------------------------------------------------- /docs/theme/fonts/Lato-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Lato-BoldItalic.woff -------------------------------------------------------------------------------- /docs/theme/fonts/Lato-BlackItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Lato-BlackItalic.woff -------------------------------------------------------------------------------- /docs/theme/fonts/Lato-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Lato-LightItalic.woff -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full:2024-01-17-19-15-31 2 | 3 | WORKDIR /workspace/freeCodeCampOS 4 | 5 | COPY --chown=gitpod:gitpod . . 6 | -------------------------------------------------------------------------------- /docs/theme/fonts/Hack-ZeroSlash-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Hack-ZeroSlash-Bold.woff -------------------------------------------------------------------------------- /docs/theme/fonts/Hack-ZeroSlash-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Hack-ZeroSlash-Bold.woff2 -------------------------------------------------------------------------------- /docs/theme/fonts/Lato-HairlineItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Lato-HairlineItalic.woff -------------------------------------------------------------------------------- /.freeCodeCamp/client/assets/Lato-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/.freeCodeCamp/client/assets/Lato-Regular.woff -------------------------------------------------------------------------------- /cli/src/fixtures.rs: -------------------------------------------------------------------------------- 1 | pub static BASHRC: &str = include_str!("../bash/.bashrc"); 2 | pub static SOURCERER: &str = include_str!("../bash/sourcerer.sh"); 3 | -------------------------------------------------------------------------------- /docs/theme/fonts/Hack-ZeroSlash-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Hack-ZeroSlash-Italic.woff -------------------------------------------------------------------------------- /docs/theme/fonts/Hack-ZeroSlash-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Hack-ZeroSlash-Italic.woff2 -------------------------------------------------------------------------------- /docs/theme/fonts/Hack-ZeroSlash-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Hack-ZeroSlash-Regular.woff -------------------------------------------------------------------------------- /docs/theme/fonts/Hack-ZeroSlash-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Hack-ZeroSlash-Regular.woff2 -------------------------------------------------------------------------------- /self/curriculum/images/fcc_primary_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/self/curriculum/images/fcc_primary_large.png -------------------------------------------------------------------------------- /docs/theme/fonts/Hack-ZeroSlash-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Hack-ZeroSlash-BoldItalic.woff -------------------------------------------------------------------------------- /docs/theme/fonts/Hack-ZeroSlash-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/freeCodeCampOS/HEAD/docs/theme/fonts/Hack-ZeroSlash-BoldItalic.woff2 -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>freeCodeCamp/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/tag.tsx: -------------------------------------------------------------------------------- 1 | export const Tag = ({ text }: { text: string; margin?: string }) => { 2 | return {text}; 3 | }; 4 | -------------------------------------------------------------------------------- /self/config/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "currentProject": null, 3 | "locale": "english", 4 | "lastSeed": { 5 | "projectDashedName": null, 6 | "lessonNumber": -1 7 | } 8 | } -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/loader.tsx: -------------------------------------------------------------------------------- 1 | export const Loader = ({ size = '100' }: { size?: string }) => { 2 | return
; 3 | }; 4 | -------------------------------------------------------------------------------- /.freeCodeCamp/tooling/logger.js: -------------------------------------------------------------------------------- 1 | import { Logger } from 'logover'; 2 | 3 | export const logover = new Logger({ 4 | level: process.env.NODE_ENV === 'development' ? 'debug' : 'info' 5 | }); 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": true, 4 | "singleQuote": true, 5 | "jsxSingleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "none", 8 | "arrowParens": "avoid" 9 | } 10 | -------------------------------------------------------------------------------- /docs/src/lessoning/lifecycle.md: -------------------------------------------------------------------------------- 1 | # Lifecycle 2 | 3 | The lifecycle of a lesson follows: 4 | 5 | 1. Server parses lesson from curriculum Markdown file 6 | 2. Server sends client lesson data 7 | 3. Client renders lesson as HTML 8 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/ruler.tsx: -------------------------------------------------------------------------------- 1 | const rulerStyle = { 2 | height: '1px', 3 | backgroundColor: '#3b3b4f', 4 | margin: '0 auto' 5 | }; 6 | export const Ruler = () => { 7 | return
; 8 | }; 9 | -------------------------------------------------------------------------------- /self/curriculum/assertions/afrikaans.json: -------------------------------------------------------------------------------- 1 | { 2 | "This is a custom test assertion message. Click the > button to go to the next lesson": "Hierdie is 'n aangepaste toets bewering boodskap. Klik op die > knoppie om na die volgende les te gaan" 3 | } 4 | -------------------------------------------------------------------------------- /docs/src/roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | For the most part, this roadmap outlines todos for `freecodecamp-os`. If this roadmap is empty, then there are no todos 🎉 4 | 5 | ## Documentation 6 | 7 | ## Features 8 | 9 | - [ ] Loader to show progress of "Reset Step" 10 | - [ ] Crowdin translation integration 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [package.json] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/description.tsx: -------------------------------------------------------------------------------- 1 | interface DescriptionProps { 2 | description: string; 3 | } 4 | 5 | export const Description = ({ description }: DescriptionProps) => { 6 | return ( 7 |
11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .freeCodeCamp/client 2 | .freeCodeCamp/tests 3 | .freeCodeCamp/tsconfig.json 4 | .freeCodeCamp/webpack.config.cjs 5 | 6 | .devcontainer 7 | .github 8 | .vscode 9 | cli 10 | docs 11 | self 12 | .editorconfig 13 | .gitignore 14 | .gitpod.yml 15 | .prettierignore 16 | .prettierrc 17 | CONTRIBUTING.md 18 | Dockerfile 19 | renovate.json 20 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | freeCodeCamp: Courses 7 | 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /docs/src/testing/test.md: -------------------------------------------------------------------------------- 1 | # Test 2 | 3 | Tests are run in worker threads. For non-blocking tests, each test is run in its own worker[^1]. For blocking tests, all tests are run in the same worker - one after the other. 4 | 5 | ```admonish attention 6 | The `--before-all--`, `--after-each`, and `--after-all--` context is only available in the main thread. 7 | ``` 8 | 9 | [^1]: The operating system decides how many threads may be concurrent. 10 | -------------------------------------------------------------------------------- /docs/src/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This is the documentation for the `@freecodecamp/freecodecamp-os` package. 4 | 5 | `freecodecamp-os` is a tool for creating and running interactive courses within Visual Studio Code. 6 | 7 | The `freecodecamp-os` package is to be used in conjunction with the [freeCodeCamp - Courses](https://marketplace.visualstudio.com/items?itemName=freeCodeCamp.freecodecamp-courses) extension, for the optimal experience. 8 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/progress.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | type ProgressProps = { 4 | total: number; 5 | count: number; 6 | }; 7 | 8 | export function Progress({ total, count }: ProgressProps) { 9 | const [value, setValue] = useState(0.0); 10 | 11 | useEffect(() => { 12 | setValue(count / total); 13 | }, [count]); 14 | 15 | return ( 16 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /self/tooling/plugins.js: -------------------------------------------------------------------------------- 1 | import { pluginEvents } from '@freecodecamp/freecodecamp-os/.freeCodeCamp/plugin/index.js'; 2 | 3 | pluginEvents.onTestsStart = async (project, testsState) => {}; 4 | 5 | pluginEvents.onTestsEnd = async (project, testsState) => {}; 6 | 7 | pluginEvents.onProjectStart = async project => {}; 8 | 9 | pluginEvents.onProjectFinished = async project => {}; 10 | 11 | pluginEvents.onLessonFailed = async project => {}; 12 | 13 | pluginEvents.onLessonPassed = async project => {}; 14 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/selection.tsx: -------------------------------------------------------------------------------- 1 | import { Events, ProjectI } from '../types'; 2 | import { Block } from './block'; 3 | 4 | export interface SelectionProps { 5 | sock: (type: Events, data: {}) => void; 6 | projects: ProjectI[]; 7 | } 8 | export const Selection = ({ sock, projects }: SelectionProps) => { 9 | return ( 10 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /self/curriculum/locales/english/external-seed-seed.md: -------------------------------------------------------------------------------- 1 | ## 0 2 | 3 | ### --seed-- 4 | 5 | #### --cmd-- 6 | 7 | ```bash 8 | rm -f external-seed/index.js 9 | rm -f external-seed/log 10 | ``` 11 | 12 | ## 1 13 | 14 | ### --seed-- 15 | 16 | #### --"external-seed/index.js"-- 17 | 18 | ```js 19 | const a = 'seeding works'; 20 | console.log(a); 21 | ``` 22 | 23 | #### --cmd-- 24 | 25 | ```bash 26 | touch external-seed/log 27 | node external-seed/index.js > external-seed/log 28 | ``` 29 | 30 | ## --fcc-end-- 31 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "customizations": { 3 | "vscode": { 4 | "extensions": [ 5 | "dbaeumer.vscode-eslint", 6 | "freeCodeCamp.freecodecamp-courses@3.0.0", 7 | "freeCodeCamp.freecodecamp-dark-vscode-theme" 8 | ] 9 | } 10 | }, 11 | "forwardPorts": [8080], 12 | "workspaceFolder": "/workspace/freeCodeCampOS", 13 | "dockerFile": "../Dockerfile", 14 | "context": "..", 15 | "updateRemoteUserUID": false, 16 | "remoteUser": "gitpod", 17 | "containerUser": "gitpod" 18 | } 19 | -------------------------------------------------------------------------------- /self/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "self", 3 | "private": true, 4 | "author": "freeCodeCamp", 5 | "version": "3.4.0", 6 | "description": "Test repo for @freecodecamp/freecodecamp-os", 7 | "scripts": { 8 | "start": "node ./node_modules/@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/server.js" 9 | }, 10 | "dependencies": { 11 | "@freecodecamp/freecodecamp-os": "../" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/freeCodeCamp/freeCodeCampOS" 16 | }, 17 | "type": "module" 18 | } 19 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/test.tsx: -------------------------------------------------------------------------------- 1 | import { Loader } from './loader'; 2 | import { TestType } from '../types'; 3 | 4 | export const Test = ({ testText, passed, isLoading, testId }: TestType) => { 5 | return ( 6 |
  • 7 | 8 | {testId + 1}) {isLoading ? : passed ? '✓' : '✗'}{' '} 9 | 10 |
    14 |
  • 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: Dockerfile 3 | 4 | # Commands to start on workspace startup 5 | tasks: 6 | - init: npm ci 7 | - command: node tooling/adjust-url.js 8 | 9 | ports: 10 | - port: 8080 11 | onOpen: open-preview 12 | 13 | # TODO: See about publishing to Open VSX for smoother process 14 | vscode: 15 | extensions: 16 | - https://github.com/freeCodeCamp/courses-vscode-extension/releases/download/v3.0.0/freecodecamp-courses-3.0.0.vsix 17 | - https://github.com/freeCodeCamp/freecodecamp-dark-vscode-theme/releases/download/v1.0.0/freecodecamp-dark-vscode-theme-1.0.0.vsix 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # freeCodeCampOS 2 | 3 | This package runs the environment for courses on freeCodeCamp.org. 4 | 5 | See the documentation for more information: https://opensource.freecodecamp.org/freeCodeCampOS/ 6 | 7 | The course content served on the client is written in Markdown files. The lesson/project tests run on the Nodejs server are written in the same Markdown files. 8 | 9 | The `freecodecamp.conf.json` file is used to configure the course, and define the actions taken by the [freeCodeCamp - Courses](https://marketplace.visualstudio.com/items?itemName=freeCodeCamp.freecodecamp-courses) extension. 10 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/tests.tsx: -------------------------------------------------------------------------------- 1 | import { TestType } from '../types'; 2 | import { Test } from './test'; 3 | 4 | interface TestsProps { 5 | tests: TestType[]; 6 | } 7 | 8 | export const Tests = ({ tests }: TestsProps) => { 9 | return ( 10 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/hints.tsx: -------------------------------------------------------------------------------- 1 | export const Hints = ({ hints }: { hints: string[] }) => { 2 | return ( 3 | 8 | ); 9 | }; 10 | 11 | const HintElement = ({ hint, i }: { hint: string; i: number }) => { 12 | const details = `Hint ${i + 1} 13 | 14 | ${hint}`; 15 | return ( 16 |
    17 |
    22 |
    23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /cli/src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::pedantic)] 2 | #![allow(clippy::struct_excessive_bools)] 3 | 4 | use clap::Parser; 5 | use clapper::{add_project, create_course, Cli, SubCommand}; 6 | use inquire::error::InquireResult; 7 | 8 | mod clapper; 9 | mod conf; 10 | mod environment; 11 | mod features; 12 | mod fixtures; 13 | mod fs; 14 | 15 | fn main() -> InquireResult<()> { 16 | let args = Cli::parse(); 17 | 18 | match args.sub_commands { 19 | Some(SubCommand::AddProject) => { 20 | add_project()?; 21 | } 22 | None => { 23 | create_course()?; 24 | } 25 | } 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "create-freecodecamp-os-app" 3 | version = "3.0.2" 4 | edition = "2021" 5 | description = "CLI to create the boilerplate for a new freeCodeCamp-OS app" 6 | license = "BSD-3-Clause" 7 | documentation = "https://opensource.freecodecamp.org/freeCodeCampOS/cli.html" 8 | homepage = "https://opensource.freecodecamp.org/freeCodeCampOS/" 9 | repository = "https://github.com/freeCodeCamp/freeCodeCampOS" 10 | 11 | [dependencies] 12 | clap = { version = "4.5.4", features = ["derive"] } 13 | indicatif = "0.17.7" 14 | inquire = "0.7.0" 15 | serde = { version = "1.0.200", features = ["derive"] } 16 | serde_json = "1.0.116" 17 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/console.tsx: -------------------------------------------------------------------------------- 1 | import { ConsoleError } from '../types'; 2 | 3 | export const Console = ({ cons }: { cons: ConsoleError[] }) => { 4 | return ( 5 | 10 | ); 11 | }; 12 | 13 | const ConsoleElement = ({ testText, testId, error }: ConsoleError) => { 14 | const details = `${testId + 1} ${testText} 15 | 16 | ${error}`; 17 | return ( 18 |
    23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /docs/src/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | If you create a course using `@freecodecamp/freecodecamp-os`, open a pull request to add it to this list. 4 | 5 | ## freeCodeCamp.org 6 | 7 | ### Web3 8 | 9 | - Web3 Curriculum 10 | - NEAR Curriculum 11 | - Solana Curriculum 12 | 13 | ### Rust 14 | 15 | - Euler Rust 16 | -------------------------------------------------------------------------------- /.freeCodeCamp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | // "noImplicitAny": true, 5 | "sourceMap": true, 6 | "jsx": "react-jsx", 7 | "allowJs": true, 8 | "moduleResolution": "node", 9 | "lib": ["WebWorker", "DOM", "DOM.Iterable", "ES2015"], 10 | "target": "es5", 11 | "module": "esnext", 12 | "strict": true, 13 | "esModuleInterop": true, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "resolveJsonModule": true, 17 | // "skipLibCheck": true, 18 | "types": ["node"] 19 | }, 20 | "exclude": ["node_modules", "**/*.spec.ts"], 21 | "include": ["client/**/*"] 22 | } 23 | -------------------------------------------------------------------------------- /cli/src/environment.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display, Formatter}; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 4 | pub enum Environment { 5 | Codespaces, 6 | Gitpod, 7 | VSCode, 8 | } 9 | 10 | impl Environment { 11 | pub const VARIANTS: &'static [Environment] = &[Self::Codespaces, Self::Gitpod, Self::VSCode]; 12 | } 13 | 14 | impl Display for Environment { 15 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 16 | match self { 17 | Environment::Codespaces => write!(f, "Codespaces"), 18 | Environment::Gitpod => write!(f, "Gitpod"), 19 | Environment::VSCode => write!(f, "VSCode"), 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Shaun Hamilton"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "freeCodeCampOS" 7 | 8 | [preprocessor] 9 | 10 | [preprocessor.admonish] 11 | command = "mdbook-admonish" 12 | assets_version = "3.0.1" # do not edit: managed by `mdbook-admonish install` 13 | 14 | [output] 15 | 16 | [output.unlink] 17 | optional = true 18 | ignore-files = ["CHANGELOG.md"] 19 | 20 | [output.html] 21 | default-theme = "dark" 22 | preferred-dark-theme = "dark" 23 | additional-css = ["./mdbook-admonish.css"] 24 | mathjax-support = true 25 | site-url = "/freeCodeCampOS/" 26 | 27 | [build] 28 | build-dir = "book" # the directory where the output is placed 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npm 2 | 3 | on: 4 | # Allows you to run this workflow manually from the Actions tab 5 | workflow_dispatch: 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 13 | 14 | - name: Publish to NPM 15 | uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4 16 | with: 17 | node-version: 20 18 | registry-url: 'https://registry.npmjs.org' 19 | - run: npm ci 20 | 21 | - name: Publish to NPM 22 | run: | 23 | npm publish 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} 26 | -------------------------------------------------------------------------------- /docs/src/resetting/lifecycle.md: -------------------------------------------------------------------------------- 1 | # Lifecycle 2 | 3 | Resetting can follow one of two lifecycles: 4 | 5 | 1. Whole project reset 6 | 2. Lesson reset 7 | 8 | ## Whole Project 9 | 10 | A whole project reset is only invoked when the `Reset` button is clicked in the client. 11 | 12 | This will run a `git clean` on the project directory - removing all files (tracked and untracked), but resetting them to their last committed state. 13 | 14 | Then, the seed of each lesson will be run in order. 15 | 16 | ## Lesson 17 | 18 | A lesson reset only happens when either `seedEveryLesson` is set to `true` in the [project config](../configuration.md#projectsjson), or the [force](../project-syntax.md#--force--) flag is set on a given lessons seed. 19 | 20 | This will only run the seed for the current lesson. 21 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { marked } from 'marked'; 2 | import { markedHighlight } from 'marked-highlight'; 3 | import Prism from 'prismjs'; 4 | 5 | marked.use( 6 | markedHighlight({ 7 | highlight: (code, lang: keyof (typeof Prism)['languages']) => { 8 | if (Prism.languages[lang]) { 9 | return Prism.highlight(code, Prism.languages[lang], String(lang)); 10 | } else { 11 | return code; 12 | } 13 | } 14 | }) 15 | ); 16 | 17 | function parseMarkdown(markdown: string) { 18 | return marked.parse(markdown, { gfm: true }); 19 | } 20 | 21 | export function parse(objOrString: any) { 22 | if (typeof objOrString === 'string') { 23 | return JSON.parse(objOrString); 24 | } else { 25 | return JSON.stringify(objOrString); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/src/testing/lifecycle.md: -------------------------------------------------------------------------------- 1 | # Lifecycle 2 | 3 | The lifecycle of the testing system follows: 4 | 5 | 1. Server parses test from curriculum Markdown file 6 | 2. Server evaluates any `--before-all--` ops 7 | 8 | - If any `--before-all--` ops fail, an error is printed to the console 9 | - If any `--before-all--` ops fail, the tests stop running 10 | 11 | 3. Server evaluates all tests in parallel[^1] 12 | 1. Server evaluates any `--before-each--` ops 13 | 1. If any `--before-each--` ops fail, test code is not run 14 | 2. Server evaluates the test 15 | 3. Server evaluates any `--after-each--` ops 16 | 4. Server evaluates any `--after-all--` ops 17 | 18 | - If any `--after-all--` ops fail, an error is printed to the console 19 | 20 | [^1]: Tests can be configured to run in order, in a blocking fashion with the `blockingTests` configuration option. 21 | -------------------------------------------------------------------------------- /cli/src/features.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display, Formatter}; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 4 | pub enum Features { 5 | Helpers, 6 | PluginSystem, 7 | ScriptInjection, 8 | Terminal, 9 | } 10 | 11 | impl Features { 12 | pub const VARIANTS: &'static [Features] = &[ 13 | Self::Helpers, 14 | Self::PluginSystem, 15 | Self::ScriptInjection, 16 | Self::Terminal, 17 | ]; 18 | } 19 | 20 | impl Display for Features { 21 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 22 | match self { 23 | Features::Helpers => write!(f, "Helpers"), 24 | Features::PluginSystem => write!(f, "Plugin System"), 25 | Features::ScriptInjection => write!(f, "Script Injection"), 26 | Features::Terminal => write!(f, "Terminal"), 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/src/cli.md: -------------------------------------------------------------------------------- 1 | # CLI 2 | 3 | ## Installation 4 | 5 | ### Releases 6 | 7 | Locate your platform in the [releases](https://github.com/freeCodeCamp/freeCodeCampOS/releases) section and download the latest version. 8 | 9 | ### `cargo` 10 | 11 | ```admonish note title="" 12 | Requires Rust to be installed: https://www.rust-lang.org/tools/install 13 | ``` 14 | 15 | ```bash 16 | cargo install create-freecodecamp-os-app 17 | ``` 18 | 19 | ## Usage 20 | 21 | To create a new course with some boilerplate: 22 | 23 | ```bash 24 | create-freecodecamp-os-app 25 | ``` 26 | 27 | To add a project to an existing course: 28 | 29 | ```bash 30 | create-freecodecamp-os-app add-project 31 | ``` 32 | 33 | The version of the CLI is tied to the version of `freecodecamp-os`. Some options may not be available if the version of the CLI is not compatible with the version of `freecodecamp-os` that is installed. 34 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./introduction.md) 4 | - [Getting Started](./getting-started.md) 5 | - [CLI](./cli.md) 6 | - [Examples](./examples.md) 7 | - [Configuration](./configuration.md) 8 | - [Project Syntax](./project-syntax.md) 9 | - [freeCodeCamp - Courses](./freecodecamp-courses.md) 10 | - [Lessoning]() 11 | - [Lifecycle](./lessoning/lifecycle.md) 12 | - [Lesson](./lessoning/lesson.md) 13 | - [Testing]() 14 | - [Lifecycle](./testing/lifecycle.md) 15 | - [Test]() 16 | - [Test Utilities](./testing/test-utilities.md) 17 | - [Globals](./testing/globals.md) 18 | - [Resetting]() 19 | - [Lifecycle](./resetting/lifecycle.md) 20 | - [Reset](./resetting/reset.md) 21 | - [Plugin System](./plugin-system.md) 22 | - [Client Injection](./client-injection.md) 23 | - [Contributing](./contributing.md) 24 | - [CHANGELOG](./CHANGELOG.md) 25 | - [Roadmap](./roadmap.md) 26 | -------------------------------------------------------------------------------- /.freeCodeCamp/tooling/t.js: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { getConfig, getState } from './env.js'; 3 | import { ROOT } from './env.js'; 4 | 5 | export async function t(key, args = {}, forceLangToUse) { 6 | const { locale: loc } = await getState(); 7 | // Get key from ./locales/{locale}/comments.json 8 | // Read file and parse JSON 9 | const locale = forceLangToUse ?? loc; 10 | const config = await getConfig(); 11 | const assertions = config.curriculum?.assertions?.[locale]; 12 | if (!assertions) { 13 | return key; 14 | } 15 | const comments = await import(join(ROOT, assertions), { 16 | assert: { type: 'json' } 17 | }); 18 | 19 | // Get value from JSON 20 | const value = comments.default[key]; 21 | // Replace placeholders in value with args 22 | const result = 23 | Object.values(args)?.length > 0 24 | ? value.replace(/\{\{(\w+)\}\}/g, (_, m) => args[m]) 25 | : value; 26 | // Return value 27 | return result; 28 | } 29 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/header.tsx: -------------------------------------------------------------------------------- 1 | import { Events, FreeCodeCampConfigI, ProjectI } from '../types'; 2 | import FreeCodeCampLogo from '../assets/fcc_primary_large'; 3 | import { LanguageList } from './language-list'; 4 | 5 | interface HeaderProps { 6 | updateProject: (project: ProjectI | null) => void; 7 | freeCodeCampConfig: FreeCodeCampConfigI; 8 | sock: (type: Events, data: {}) => void; 9 | } 10 | export const Header = ({ 11 | sock, 12 | updateProject, 13 | freeCodeCampConfig 14 | }: HeaderProps) => { 15 | function returnToLanding() { 16 | updateProject(null); 17 | } 18 | 19 | const locales = freeCodeCampConfig?.curriculum?.locales 20 | ? Object.keys(freeCodeCampConfig?.curriculum?.locales) 21 | : []; 22 | return ( 23 |
    24 | 27 | {locales.length > 1 ? : null} 28 |
    29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /docs/theme/css/print.css: -------------------------------------------------------------------------------- 1 | #sidebar, 2 | #menu-bar, 3 | .nav-chapters, 4 | .mobile-nav-chapters { 5 | display: none; 6 | } 7 | 8 | #page-wrapper.page-wrapper { 9 | transform: none; 10 | margin-left: 0px; 11 | overflow-y: initial; 12 | } 13 | 14 | #content { 15 | max-width: none; 16 | margin: 0; 17 | padding: 0; 18 | } 19 | 20 | .page { 21 | overflow-y: initial; 22 | } 23 | 24 | code { 25 | background-color: #666666; 26 | border-radius: 5px; 27 | 28 | /* Force background to be printed in Chrome */ 29 | -webkit-print-color-adjust: exact; 30 | print-color-adjust: exact; 31 | } 32 | 33 | pre > .buttons { 34 | z-index: 2; 35 | } 36 | 37 | a, 38 | a:visited, 39 | a:active, 40 | a:hover { 41 | color: #4183c4; 42 | text-decoration: none; 43 | } 44 | 45 | h1, 46 | h2, 47 | h3, 48 | h4, 49 | h5, 50 | h6 { 51 | page-break-inside: avoid; 52 | page-break-after: avoid; 53 | } 54 | 55 | pre, 56 | code { 57 | page-break-inside: avoid; 58 | white-space: pre-wrap; 59 | } 60 | 61 | .fa { 62 | display: none !important; 63 | } 64 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/templates/landing.tsx: -------------------------------------------------------------------------------- 1 | import { Selection } from '../components/selection'; 2 | import { Events, FreeCodeCampConfigI, ProjectI } from '../types'; 3 | import './landing.css'; 4 | 5 | interface LandingProps { 6 | sock: (type: Events, data: {}) => void; 7 | projects: ProjectI[]; 8 | freeCodeCampConfig: FreeCodeCampConfigI; 9 | locale: string; 10 | } 11 | 12 | export const Landing = ({ 13 | sock, 14 | projects, 15 | freeCodeCampConfig, 16 | locale 17 | }: LandingProps) => { 18 | const title = freeCodeCampConfig.client?.landing?.[locale]?.title; 19 | return ( 20 | <> 21 | {title &&

    {title}

    } 22 |

    23 | {freeCodeCampConfig.client?.landing?.[locale]?.description} 24 |

    25 | 29 | {freeCodeCampConfig.client?.landing?.[locale]?.['faq-text']} 30 | 31 | 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/error.tsx: -------------------------------------------------------------------------------- 1 | export const E44o5 = ({ 2 | text, 3 | error 4 | }: { 5 | text: string; 6 | error: Error | null; 7 | }) => { 8 | return ( 9 |
    10 |
    11 |

    Error 4XX - 5XX

    12 |

    {text}

    13 | {error && ( 14 |
    15 | More Info 16 | 17 |

    {JSON.stringify(error, null, 2)}

    18 |
    19 | )} 20 |

    To Keep Learning:

    21 |
      22 |
    • First, try refresh this page
    • 23 |
    24 |

    Otherwise:

    25 |
      26 |
    1. Open the command palette
    2. 27 |
    3. 28 | Select the freeCodeCamp: Shutdown Course command 29 |
    4. 30 |
    5. Open the command palette
    6. 31 |
    7. 32 | Select the freeCodeCamp: Run Course command 33 |
    8. 34 |
    35 |
    36 |
    37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /self/curriculum/locales/english/lesson-watch.md: -------------------------------------------------------------------------------- 1 | # Lesson Watch 2 | 3 | Watch and ignore specific files for each lesson. 4 | 5 | ## 0 6 | 7 | 8 | 9 | ```json 10 | { 11 | "watch": ["lesson-watch/watched.js"] 12 | } 13 | ``` 14 | 15 | ### --description-- 16 | 17 | Making changes to `watched.js` should run the tests, but changing `unwatched.js` should do nothing. 18 | 19 | ### --tests-- 20 | 21 | Placeholder test. 22 | 23 | ```js 24 | // TODO: Test `watcher.watched()` for what should be watched 25 | assert.fail(); 26 | ``` 27 | 28 | ## 1 29 | 30 | ```json 31 | { 32 | "ignore": ["lesson-watch/unwatched.js"] 33 | } 34 | ``` 35 | 36 | ### --description-- 37 | 38 | Making any change should run the tests, but changing `unwatched.js` should do nothing. 39 | 40 | ### --tests-- 41 | 42 | Placeholder test text. 43 | 44 | ```js 45 | assert.fail(); 46 | ``` 47 | 48 | ## 2 49 | 50 | ### --description-- 51 | 52 | The default option to watch and ignore are reset. 53 | 54 | ### --tests-- 55 | 56 | This always fails. 57 | 58 | ```js 59 | assert.fail(); 60 | ``` 61 | 62 | ## --fcc-end-- 63 | -------------------------------------------------------------------------------- /docs/src/freecodecamp-courses.md: -------------------------------------------------------------------------------- 1 | # freeCodeCamp - Courses 2 | 3 | The [freeCodeCamp - Courses VSCode Extension](https://marketplace.visualstudio.com/items?itemName=freeCodeCamp.freecodecamp-courses) makes working with `freecodecamp-os` in VSCode feature-rich. 4 | 5 | ## Features 6 | 7 | ### Commands 8 | 9 | `freeCodeCamp: Develop Course` 10 | 11 | Runs the `develop-course` script in the `freecodecamp.conf.json` of the current workspace. Also, enables debug-level logging in the terminal by setting `NODE_ENV=development`. 12 | 13 | Also, with `NODE_ENV=development`, your workspace is validated following: 14 | 15 | `freeCodeCamp: Run Course` 16 | 17 | Runs the `run-course` script in the `freecodecamp.conf.json` of the current workspace. 18 | 19 | `freeCodeCamp: Shutdown Course` 20 | 21 | Disposes all terminals, and closes the visible text editors. 22 | 23 | `freeCodeCamp: Open Course` 24 | 25 | Within a new directory, this command shows the available courses to clone and then clones the selected course in the current workspace. 26 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/checkmark.tsx: -------------------------------------------------------------------------------- 1 | export const Checkmark = () => { 2 | return ( 3 | 9 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/templates/landing.css: -------------------------------------------------------------------------------- 1 | .description { 2 | max-width: 750px; 3 | margin-left: auto; 4 | margin-right: auto; 5 | } 6 | 7 | .blocks { 8 | list-style-type: none; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | padding-inline-start: 0; 14 | } 15 | 16 | .block { 17 | width: 90%; 18 | min-height: 40px; 19 | margin-top: 10px; 20 | } 21 | 22 | .block-btn { 23 | text-align: left; 24 | background-color: var(--dark-1); 25 | color: var(--light-2); 26 | padding: 14px; 27 | width: 100%; 28 | height: 100%; 29 | border: none; 30 | min-height: 40px; 31 | max-width: 750px; 32 | } 33 | 34 | .block-btn p { 35 | color: var(--light-3); 36 | } 37 | 38 | .block-btn:hover { 39 | cursor: pointer; 40 | background-color: var(--dark-3); 41 | } 42 | .tags-row { 43 | display: flex; 44 | } 45 | 46 | .tag { 47 | background-color: var(--dark-blue); 48 | color: var(--light-blue); 49 | display: block; 50 | font-size: 1rem; 51 | margin-bottom: 5px; 52 | margin-right: 5px; 53 | padding: 4px 10px; 54 | text-align: left; 55 | width: -webkit-fit-content; 56 | width: -moz-fit-content; 57 | width: fit-content; 58 | } -------------------------------------------------------------------------------- /self/tooling/helpers.js: -------------------------------------------------------------------------------- 1 | import __helpers from '@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/test-utils.js'; 2 | import { logover } from '@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/logger.js'; 3 | import { ROOT } from '@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/env.js'; 4 | import { writeFileSync } from 'fs'; 5 | import { join } from 'path'; 6 | 7 | export async function javascriptTest(filePath, test, cb) { 8 | const PATH_TO_FILE = join(ROOT, filePath); 9 | const testString = `\n${test}`; 10 | 11 | const fileContents = await __helpers.getFile(filePath); 12 | 13 | const fileWithTest = fileContents + '\n' + testString; 14 | 15 | let std; 16 | 17 | try { 18 | writeFileSync(PATH_TO_FILE, fileWithTest, 'utf-8'); 19 | 20 | std = await __helpers.getCommandOutput(`node ${PATH_TO_FILE}`); 21 | } catch (e) { 22 | logover.debug(e); 23 | } finally { 24 | const ensureFileContents = fileContents.replace(testString, ''); 25 | writeFileSync(PATH_TO_FILE, ensureFileContents, 'utf-8'); 26 | await cb(std.stdout, std.stderr); 27 | await new Promise(resolve => setTimeout(resolve, 1500)); 28 | } 29 | } 30 | 31 | export function testDynamicHelper() { 32 | return 'Helper success!'; 33 | } 34 | -------------------------------------------------------------------------------- /self/curriculum/locales/english/external-seed.md: -------------------------------------------------------------------------------- 1 | # External Seed 2 | 3 | A project to test the default parser `external seed` feature. 4 | 5 | ## 0 6 | 7 | ### --description-- 8 | 9 | The seed for this lesson deletes any `index.js` and `log` files within the `external-seed/` directory. 10 | 11 | ### --tests-- 12 | 13 | This test should pass, if the seed worked 14 | 15 | ```js 16 | const { readdir } = await import('fs/promises'); 17 | const dir = await readdir(join(ROOT, project.dashedName)); 18 | assert.equal( 19 | dir.length, 20 | 1, 21 | `"${project.dashedName}" is expected to only have the .gitkeep file.` 22 | ); 23 | ``` 24 | 25 | ## 1 26 | 27 | ### --description-- 28 | 29 | There should be a `index.js` file that was created and run when the lesson loaded. 30 | 31 | ### --tests-- 32 | 33 | The `index.js` file should be seeded for you. 34 | 35 | ```js 36 | const { access, constants } = await import('fs/promises'); 37 | await access(join(ROOT, project.dashedName, 'index.js'), constants.F_OK); 38 | ``` 39 | 40 | The `index.js` file should be run. 41 | 42 | ```js 43 | const { access, constants } = await import('fs/promises'); 44 | await access(join(ROOT, project.dashedName, 'log'), constants.F_OK); 45 | ``` 46 | 47 | ## --fcc-end-- 48 | -------------------------------------------------------------------------------- /self/tooling/adjust-url.js: -------------------------------------------------------------------------------- 1 | //! This script adjusts the preview URL for freeCodeCamp - Courses to open the correct preview. 2 | import { readFile, writeFile } from 'fs/promises'; 3 | 4 | let PREVIEW_URL = 'http://localhost:8080'; 5 | if (process.env.GITPOD_WORKSPACE_URL) { 6 | PREVIEW_URL = `https://8080-${ 7 | process.env.GITPOD_WORKSPACE_URL.split('https://')[1] 8 | }`; 9 | } else if (process.env.CODESPACE_NAME) { 10 | PREVIEW_URL = `https://${process.env.CODESPACE_NAME}-8080.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}`; 11 | } 12 | 13 | const VSCODE_SETTINGS_PATH = '.vscode/settings.json'; 14 | 15 | async function main() { 16 | const settings_file = await readFile(VSCODE_SETTINGS_PATH, 'utf-8'); 17 | const settings = JSON.parse(settings_file); 18 | 19 | let [preview] = settings?.['freecodecamp-courses.workspace.previews']; 20 | if (!preview.url) { 21 | throw new Error('.vscode setting not found'); 22 | } 23 | preview.url = PREVIEW_URL; 24 | 25 | await writeFile( 26 | VSCODE_SETTINGS_PATH, 27 | JSON.stringify(settings, null, 2), 28 | 'utf-8' 29 | ); 30 | } 31 | 32 | try { 33 | main(); 34 | } catch (e) { 35 | console.error('Unable to adjust .vscode/settings.json preview url setting:'); 36 | console.error(e); 37 | } 38 | -------------------------------------------------------------------------------- /self/tooling/rejig.js: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile, readdir } from 'fs/promises'; 2 | import { join } from 'path'; 3 | 4 | const PATH = process.argv[2]?.trim(); 5 | 6 | const CURRICULUM_PATH = 'curriculum/locales/english'; 7 | 8 | /** 9 | * Ensures all lessons are incremented by 1 10 | */ 11 | async function rejigFile(fileName) { 12 | const filePath = join(CURRICULUM_PATH, fileName); 13 | const file = await readFile(filePath, 'utf-8'); 14 | let lessonNumber = -1; 15 | const newFile = file.replace(/\n## \d+/g, () => { 16 | lessonNumber++; 17 | return `\n## ${lessonNumber}`; 18 | }); 19 | await writeFile(filePath, newFile, 'utf-8'); 20 | } 21 | 22 | try { 23 | const rejiggedFiles = []; 24 | if (PATH) { 25 | await rejigFile(PATH); 26 | rejiggedFiles.push(PATH); 27 | } else { 28 | const files = await readdir(CURRICULUM_PATH); 29 | for (const file of files) { 30 | console.log(`Rejigging '${file}'`); 31 | await rejigFile(file); 32 | rejiggedFiles.push(file); 33 | } 34 | } 35 | console.info('Successfully rejigged: ', rejiggedFiles); 36 | } catch (e) { 37 | console.error(e); 38 | console.log('Usage: npm run rejig '); 39 | console.log('Curriculum file name MUST include the `.md` extension.'); 40 | } 41 | -------------------------------------------------------------------------------- /.freeCodeCamp/tooling/tests/test-worker.js: -------------------------------------------------------------------------------- 1 | import { parentPort, workerData } from 'node:worker_threads'; 2 | // These are used in the local scope of the `eval` in `runTests` 3 | import { assert, AssertionError, expect, config as chaiConfig } from 'chai'; 4 | import __helpers_c from '../test-utils.js'; 5 | 6 | import { freeCodeCampConfig, ROOT } from '../env.js'; 7 | import { join } from 'path'; 8 | import { logover } from '../logger.js'; 9 | 10 | let __helpers = __helpers_c; 11 | 12 | // Update __helpers with dynamic utils: 13 | const helpers = freeCodeCampConfig.tooling?.['helpers']; 14 | if (helpers) { 15 | const dynamicHelpers = await import(join(ROOT, helpers)); 16 | __helpers = { ...__helpers_c, ...dynamicHelpers }; 17 | } 18 | 19 | const { beforeEach = '', project } = workerData; 20 | 21 | parentPort.on('message', async ({ testCode, testId }) => { 22 | let passed = false; 23 | let error = null; 24 | try { 25 | const _eval_out = await eval(`(async () => { 26 | ${beforeEach} 27 | ${testCode} 28 | })();`); 29 | passed = true; 30 | } catch (e) { 31 | error = {}; 32 | Object.getOwnPropertyNames(e).forEach(key => { 33 | error[key] = e[key]; 34 | }); 35 | // Cannot pass `e` "as is", because classes cannot be passed between threads 36 | error.type = e instanceof AssertionError ? 'AssertionError' : 'Error'; 37 | } 38 | parentPort.postMessage({ passed, testId, error }); 39 | }); 40 | -------------------------------------------------------------------------------- /.github/workflows/link-check.yml: -------------------------------------------------------------------------------- 1 | name: Check Links 2 | 3 | on: 4 | # Runs on PRs targeting main 5 | pull_request: 6 | branches: ['main'] 7 | paths: 8 | - 'docs/**' 9 | 10 | # Allows this workflow to be manually run from the Actions tab 11 | workflow_dispatch: 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | env: 20 | MDBOOK_VERSION: 0.4.36 21 | ADMONISH_VERSION: 1.15.0 22 | UNLINK_VERSION: 0.1.0 23 | steps: 24 | - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 25 | - name: Install mdBook 26 | run: | 27 | mkdir bin 28 | curl -sSL https://github.com/rust-lang/mdBook/releases/download/v${MDBOOK_VERSION}/mdbook-v${MDBOOK_VERSION}-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin 29 | - name: Install mdbook-admonish 30 | run: | 31 | curl -sSL https://github.com/tommilligan/mdbook-admonish/releases/download/v${ADMONISH_VERSION}/mdbook-admonish-v${ADMONISH_VERSION}-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin 32 | - name: Install mdbook-unlink 33 | run: | 34 | curl -sSL https://github.com/ShaunSHamilton/mdbook-unlink/releases/download/v${UNLINK_VERSION}/x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin 35 | echo "$PWD/bin" >> $GITHUB_PATH 36 | - name: Check Links 37 | run: cd docs && mdbook build 38 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/language-globe.tsx: -------------------------------------------------------------------------------- 1 | export function LanguageGlobe() { 2 | return ( 3 | 10 | 17 | 24 | 31 | 38 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/types/index.ts: -------------------------------------------------------------------------------- 1 | export type F = (arg: A) => R; 2 | 3 | export enum Events { 4 | CONNECT = 'connect', 5 | DISCONNECT = 'disconnect', 6 | TOGGLE_LOADER_ANIMATION = 'toggle-loader-animation', 7 | UPDATE_TESTS = 'update-tests', 8 | UPDATE_TEST = 'update-test', 9 | UPDATE_DESCRIPTION = 'update-description', 10 | UPDATE_PROJECT_HEADING = 'update-project-heading', 11 | UPDATE_PROJECTS = 'update-projects', 12 | RESET_TESTS = 'reset-tests', 13 | RUN_TESTS = 'run-tests', 14 | RESET_PROJECT = 'reset-project', 15 | REQUEST_DATA = 'request-data', 16 | GO_TO_NEXT_LESSON = 'go-to-next-lesson', 17 | GO_TO_PREVIOUS_LESSON = 'go-to-previous-lesson', 18 | SELECT_PROJECT = 'select-project', 19 | CANCEL_TESTS = 'cancel-tests', 20 | CHANGE_LANGUAGE = 'change-language' 21 | } 22 | 23 | export type TestType = { 24 | testText: string; 25 | passed: boolean; 26 | isLoading: boolean; 27 | testId: number; 28 | }; 29 | 30 | export type LoaderT = { 31 | isLoading: boolean; 32 | progress: { 33 | total: number; 34 | count: number; 35 | }; 36 | }; 37 | 38 | export interface ProjectI { 39 | id: number; 40 | title: string; 41 | description: string; 42 | isIntegrated: boolean; 43 | isPublic: boolean; 44 | currentLesson: number; 45 | numberOfLessons: number; 46 | isResetEnabled?: boolean; 47 | completedDate: null | number; 48 | tags: string[]; 49 | } 50 | 51 | export type ConsoleError = { 52 | error: string; 53 | } & TestType; 54 | 55 | export type FreeCodeCampConfigI = { 56 | [key: string]: any; 57 | }; 58 | -------------------------------------------------------------------------------- /docs/theme/highlight.css: -------------------------------------------------------------------------------- 1 | /* 2 | * An increased contrast highlighting scheme loosely based on the 3 | * "Base16 Atelier Dune Light" theme by Bram de Haan 4 | * (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) 5 | * Original Base16 color scheme by Chris Kempson 6 | * (https://github.com/chriskempson/base16) 7 | */ 8 | 9 | /* Comment */ 10 | .hljs-comment, 11 | .hljs-quote { 12 | color: #575757; 13 | } 14 | 15 | /* Red */ 16 | .hljs-variable, 17 | .hljs-template-variable, 18 | .hljs-attribute, 19 | .hljs-tag, 20 | .hljs-name, 21 | .hljs-regexp, 22 | .hljs-link, 23 | .hljs-name, 24 | .hljs-selector-id, 25 | .hljs-selector-class { 26 | color: #d70025; 27 | } 28 | 29 | /* Orange */ 30 | .hljs-number, 31 | .hljs-meta, 32 | .hljs-built_in, 33 | .hljs-builtin-name, 34 | .hljs-literal, 35 | .hljs-type, 36 | .hljs-params { 37 | color: #b21e00; 38 | } 39 | 40 | /* Green */ 41 | .hljs-string, 42 | .hljs-symbol, 43 | .hljs-bullet { 44 | color: #008200; 45 | } 46 | 47 | /* Blue */ 48 | .hljs-title, 49 | .hljs-section { 50 | color: #0030f2; 51 | } 52 | 53 | /* Purple */ 54 | .hljs-keyword, 55 | .hljs-selector-tag { 56 | color: #9d00ec; 57 | } 58 | 59 | .hljs { 60 | display: block; 61 | overflow-x: auto; 62 | background: #f6f7f6; 63 | color: #000; 64 | } 65 | 66 | .hljs-emphasis { 67 | font-style: italic; 68 | } 69 | 70 | .hljs-strong { 71 | font-weight: bold; 72 | } 73 | 74 | .hljs-addition { 75 | color: #22863a; 76 | background-color: #f0fff4; 77 | } 78 | 79 | .hljs-deletion { 80 | color: #b31d28; 81 | background-color: #ffeef0; 82 | } 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022-2024, freeCodeCamp 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /self/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | ".devcontainer": false, 4 | ".editorconfig": false, 5 | ".freeCodeCamp": false, 6 | ".gitignore": false, 7 | ".gitpod.Dockerfile": false, 8 | ".gitpod.yml": false, 9 | ".logs": false, 10 | ".prettierignore": false, 11 | ".prettierrc": false, 12 | ".vscode": false, 13 | "node_modules": false, 14 | "package.json": false, 15 | "package-lock.json": false, 16 | "LICENSE": false, 17 | "README.md": false, 18 | "renovate.json": false, 19 | "freecodecamp.conf.json": false, 20 | "bash": false, 21 | "client": false, 22 | "config": false, 23 | "curriculum": false, 24 | "tooling": false, 25 | "build-x-using-y": false, 26 | "learn-freecodecamp-os": false, 27 | "external-seed": false 28 | }, 29 | "terminal.integrated.defaultProfile.linux": "bash", 30 | "terminal.integrated.profiles.linux": { 31 | "bash": { 32 | "path": "bash", 33 | "icon": "terminal-bash", 34 | "args": [ 35 | "--init-file", 36 | "./bash/sourcerer.sh" 37 | ] 38 | } 39 | }, 40 | "freecodecamp-courses.autoStart": true, 41 | "freecodecamp-courses.prepare": "sed -i \"s#WD=.*#WD=$(pwd)#g\" ./bash/.bashrc", 42 | "freecodecamp-courses.scripts.develop-course": "NODE_ENV=development npm run start", 43 | "freecodecamp-courses.scripts.run-course": "NODE_ENV=development npm run start", 44 | "freecodecamp-courses.workspace.previews": [ 45 | { 46 | "open": true, 47 | "url": "http://localhost:8080", 48 | "showLoader": true, 49 | "timeout": 4000 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /docs/src/testing/globals.md: -------------------------------------------------------------------------------- 1 | # Globals 2 | 3 | None of the globals are within the `__helpers` module. 4 | 5 | ### `chai` 6 | 7 | #### `assert` 8 | 9 | The `assert` module: 10 | 11 | #### `expect` 12 | 13 | The `expect` module: 14 | 15 | #### `config as chaiConfig` 16 | 17 | The `config` module: 18 | 19 | #### `AssertionError` 20 | 21 | This is the `AssertionError` class from the `assert` module. 22 | 23 | ### `logover` 24 | 25 | The logger used by `freecodecamp-os`: 26 | 27 | This is mostly useful for debugging, as any logs will be output in the freeCodeCamp terminal. 28 | 29 | ### `ROOT` 30 | 31 | The root of the workspace. 32 | 33 | ### `watcher` 34 | 35 | ```admonish note 36 | This is only available in the `beforeAll` and `beforeEach` context - on the main thread. 37 | ``` 38 | 39 | The [Chokidar](https://www.npmjs.com/package/chokidar) `FSWatcher` instance. 40 | 41 | This is useful if you want to stop watching a directory during a test: 42 | 43 | ````admonish example 44 | ```javascript 45 | const DIRECTORY_PATH_RELATIVE_TO_ROOT = "example"; 46 | watcher.unwatch(DIRECTORY_PATH_RELATIVE_TO_ROOT); 47 | // Do something 48 | watcher.add(DIRECTORY_PATH_RELATIVE_TO_ROOT); 49 | ``` 50 | ```` 51 | 52 | ## Collisions 53 | 54 | As the tests are run in the `eval`ed context of the `freecodecamp-os/.freeCodeCamp/tooling/tests/test-worker.js` module, there is the possibility that variable naming collisions will occur. To avoid this, it is recommended to prefix object names with `__` (dunder). 55 | -------------------------------------------------------------------------------- /self/freecodecamp.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "port": 8080, 4 | "client": { 5 | "assets": { 6 | "header": "./client/assets/fcc_primary_large.svg", 7 | "favicon": "./client/assets/fcc_primary_small.svg" 8 | }, 9 | "landing": { 10 | "english": { 11 | "title": "freeCodeCamp-OS", 12 | "description": "Placeholder description", 13 | "faq-link": "https://freecodecamp.org", 14 | "faq-text": "Link to FAQ related to course" 15 | }, 16 | "afrikaans": { 17 | "title": "freeCodeCamp-OS", 18 | "description": "Beskrywing", 19 | "faq-link": "https://freecodecamp.org", 20 | "faq-text": "Skakel na gereelde vra" 21 | } 22 | }, 23 | "static": { 24 | "/images": "./curriculum/images", 25 | "/script/injectable.js": "./client/injectable.js" 26 | } 27 | }, 28 | "config": { 29 | "projects.json": "./config/projects.json", 30 | "state.json": "./config/state.json" 31 | }, 32 | "curriculum": { 33 | "locales": { 34 | "english": "./curriculum/locales/english", 35 | "afrikaans": "./curriculum/locales/afrikaans" 36 | }, 37 | "assertions": { 38 | "afrikaans": "./curriculum/assertions/afrikaans.json" 39 | } 40 | }, 41 | "hotReload": { 42 | "ignore": [ 43 | ".logs/.temp.log", 44 | "config/", 45 | "/node_modules/", 46 | ".git/", 47 | "/target/", 48 | "/test-ledger/", 49 | ".vscode/", 50 | "freecodecamp.conf.json" 51 | ] 52 | }, 53 | "tooling": { 54 | "helpers": "./tooling/helpers.js", 55 | "plugins": "./tooling/plugins.js" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /docs/theme/fonts/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Hack'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url('./Hack-ZeroSlash-Regular.woff2') format('woff2'); 6 | } 7 | 8 | @font-face { 9 | font-family: 'Hack'; 10 | font-style: italic; 11 | font-weight: 400; 12 | src: url('./Hack-ZeroSlash-Italic.woff2') format('woff2'); 13 | } 14 | 15 | @font-face { 16 | font-family: 'Hack'; 17 | font-style: bold; 18 | font-weight: 700; 19 | src: url('./Hack-ZeroSlash-Bold.woff2') format('woff2'); 20 | } 21 | 22 | @font-face { 23 | font-family: 'Hack'; 24 | font-style: bold italic; 25 | font-weight: 700; 26 | src: url('./Hack-ZeroSlash-BoldItalic.woff2') format('woff2'); 27 | } 28 | 29 | @font-face { 30 | font-family: 'Lato'; 31 | font-weight: 300; 32 | font-style: normal; 33 | font-display: fallback; 34 | src: url('./Lato-Light.woff') format('woff'); 35 | } 36 | 37 | @font-face { 38 | font-family: 'Lato'; 39 | font-weight: normal; 40 | font-style: normal; 41 | font-display: fallback; 42 | src: url('./Lato-Regular.woff') format('woff'); 43 | } 44 | 45 | @font-face { 46 | font-family: 'Lato'; 47 | font-weight: normal; 48 | font-style: italic; 49 | font-display: fallback; 50 | src: url('./Lato-Italic.woff') format('woff'); 51 | } 52 | 53 | @font-face { 54 | font-family: 'Lato'; 55 | font-weight: bold; 56 | font-style: normal; 57 | font-display: fallback; 58 | src: url('./Lato-Bold.woff') format('woff'); 59 | } 60 | 61 | @font-face { 62 | font-family: 'Lato'; 63 | font-weight: 900; 64 | font-style: normal; 65 | font-display: fallback; 66 | src: url('./Lato-Black.woff') format('woff'); 67 | } 68 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/language-list.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | import { LanguageGlobe } from './language-globe'; 3 | import { Events } from '../types'; 4 | 5 | type LanguageListProps = { 6 | locales: string[]; 7 | sock: (type: Events, data: {}) => void; 8 | }; 9 | 10 | export function LanguageList({ locales, sock }: LanguageListProps) { 11 | const [showList, setShowList] = useState(false); 12 | const listRef = useRef(null); 13 | 14 | const handleClick = (): void => { 15 | if (listRef.current) { 16 | if (showList) { 17 | listRef.current.classList.add('hidden'); 18 | setShowList(false); 19 | return; 20 | } 21 | listRef.current.classList.remove('hidden'); 22 | setShowList(true); 23 | } 24 | }; 25 | 26 | const handleLanguageChange = ( 27 | event: React.MouseEvent 28 | ): void => { 29 | event.preventDefault(); 30 | const selectedLanguage = event.currentTarget.dataset.value; 31 | if (selectedLanguage === undefined) return; 32 | sock(Events.CHANGE_LANGUAGE, { locale: selectedLanguage }); 33 | }; 34 | 35 | return ( 36 | <> 37 | 44 | 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /self/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "self", 3 | "version": "3.4.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "self", 9 | "version": "3.4.0", 10 | "dependencies": { 11 | "@freecodecamp/freecodecamp-os": "../" 12 | } 13 | }, 14 | "..": { 15 | "name": "@freecodecamp/freecodecamp-os", 16 | "version": "3.4.0", 17 | "dependencies": { 18 | "chai": "4.4.1", 19 | "chokidar": "3.6.0", 20 | "express": "4.18.3", 21 | "logover": "2.0.0", 22 | "marked": "9.1.6", 23 | "marked-highlight": "2.1.1", 24 | "prismjs": "1.29.0", 25 | "ws": "8.16.0" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "7.24.0", 29 | "@babel/plugin-syntax-import-assertions": "7.23.3", 30 | "@babel/preset-env": "7.24.0", 31 | "@babel/preset-react": "7.23.3", 32 | "@babel/preset-typescript": "7.23.3", 33 | "@types/marked": "5.0.2", 34 | "@types/node": "20.11.24", 35 | "@types/prismjs": "1.26.3", 36 | "@types/react": "18.2.63", 37 | "@types/react-dom": "18.2.19", 38 | "babel-loader": "9.1.3", 39 | "babel-plugin-prismjs": "2.1.0", 40 | "css-loader": "6.10.0", 41 | "file-loader": "6.2.0", 42 | "html-webpack-plugin": "5.6.0", 43 | "nodemon": "3.1.0", 44 | "react": "18.2.0", 45 | "react-dom": "18.2.0", 46 | "style-loader": "3.3.4", 47 | "ts-loader": "9.5.1", 48 | "typescript": "5.3.3", 49 | "webpack-cli": "5.1.4", 50 | "webpack-dev-server": "4.15.1" 51 | } 52 | }, 53 | "node_modules/@freecodecamp/freecodecamp-os": { 54 | "resolved": "..", 55 | "link": true 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /self/curriculum/locales/afrikaans/build-x-using-y.md: -------------------------------------------------------------------------------- 1 | # Bou 'n X met behulp van Y 2 | 3 | In hierdie kursus, sal jy 'n X bou met behulp van Y. 4 | 5 | ## 0 6 | 7 | ### --description-- 8 | 9 | 'n Beskrywing here. 10 | 11 | ```rust 12 | fn main() { 13 | println!("Hello, world!"); 14 | } 15 | ``` 16 | 17 | Hier is 'n beeld: 18 | 19 | 20 | 21 | ### --tests-- 22 | 23 | Eerste toets met Chai.js `assert`. 24 | 25 | ```js 26 | // 0 27 | // Timeout for 3 seconds 28 | await new Promise(resolve => setTimeout(resolve, 3000)); 29 | assert.equal(true, true); 30 | ``` 31 | 32 | Second test using global variables passed from `before` hook. 33 | Tweede toets met behulp van globale veranderlikes wat vanaf die `before` haak oorgedra word. 34 | 35 | ```js 36 | // 1 37 | await new Promise(resolve => setTimeout(resolve, 4000)); 38 | assert.equal(__projectLoc, 'example global variable for tests'); 39 | ``` 40 | 41 | Dynamic helpers should be imported. 42 | 43 | ```js 44 | // 2 45 | await new Promise(resolve => setTimeout(resolve, 1000)); 46 | assert.equal(__helpers.testDynamicHelper(), 'Helper success!'); 47 | // assert.fail('test'); 48 | ``` 49 | 50 | ### --before-each-- 51 | 52 | ```js 53 | await new Promise(resolve => setTimeout(resolve, 1000)); 54 | const __projectLoc = 'example global variable for tests'; 55 | ``` 56 | 57 | ### --after-each-- 58 | 59 | ```js 60 | await new Promise(resolve => setTimeout(resolve, 1000)); 61 | logover.info('after each'); 62 | ``` 63 | 64 | ### --before-all-- 65 | 66 | ```js 67 | await new Promise(resolve => setTimeout(resolve, 1000)); 68 | logover.info('before all'); 69 | ``` 70 | 71 | ### --after-all-- 72 | 73 | ```js 74 | await new Promise(resolve => setTimeout(resolve, 1000)); 75 | logover.info('after all'); 76 | ``` 77 | 78 | ## --fcc-end-- 79 | -------------------------------------------------------------------------------- /.github/workflows/cli-release.yml: -------------------------------------------------------------------------------- 1 | name: CLI Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | create-release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 15 | - uses: taiki-e/create-gh-release-action@c88e967dc754f9d45751df0de0a9e636115f121d # v1 16 | with: 17 | # (required) GitHub token for creating GitHub Releases. 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | upload-assets: 21 | needs: create-release 22 | strategy: 23 | matrix: 24 | include: 25 | - target: aarch64-unknown-linux-gnu 26 | os: ubuntu-latest 27 | - target: aarch64-apple-darwin 28 | os: macos-latest 29 | - target: x86_64-unknown-linux-gnu 30 | os: ubuntu-latest 31 | - target: x86_64-apple-darwin 32 | os: macos-latest 33 | # Universal macOS binary is supported as universal-apple-darwin. 34 | - target: universal-apple-darwin 35 | os: macos-latest 36 | runs-on: ${{ matrix.os }} 37 | steps: 38 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 39 | - uses: taiki-e/install-action@cross 40 | - uses: taiki-e/upload-rust-binary-action@116e64492098f73785ffb2cf4c498df22c85e7a5 # v1 41 | with: 42 | # (required) Comma-separated list of binary names (non-extension portion of filename) to build and upload. 43 | # Note that glob pattern is not supported yet. 44 | bin: create-freecodecamp-os-app 45 | manifest-path: ./cli/Cargo.toml 46 | target: ${{ matrix.target }} 47 | # (required) GitHub token for uploading assets to GitHub Releases. 48 | token: ${{ secrets.GITHUB_TOKEN }} 49 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/heading.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { F } from '../types'; 3 | 4 | interface HeadingProps { 5 | title: string; 6 | lessonNumber?: number; 7 | numberOfLessons?: number; 8 | goToNextLesson?: F; 9 | goToPreviousLesson?: F; 10 | } 11 | 12 | export const Heading = ({ 13 | title, 14 | lessonNumber, 15 | numberOfLessons, 16 | goToNextLesson, 17 | goToPreviousLesson 18 | }: HeadingProps) => { 19 | const [anim, setAnim] = useState(''); 20 | 21 | useEffect(() => { 22 | setAnim('fade-in'); 23 | setTimeout(() => setAnim(''), 1000); 24 | }, [lessonNumber]); 25 | 26 | const lessonNumberExists = typeof lessonNumber !== 'undefined'; 27 | const canGoBack = lessonNumberExists && lessonNumber > 0; 28 | const canGoForward = 29 | lessonNumberExists && numberOfLessons && lessonNumber < numberOfLessons - 1; 30 | 31 | const h1 = title + (lessonNumberExists ? ' - Lesson ' + lessonNumber : ''); 32 | return ( 33 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /self/config/projects.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 0, 4 | "dashedName": "learn-freecodecamp-os", 5 | "isIntegrated": false, 6 | "isPublic": true, 7 | "currentLesson": 0, 8 | "runTestsOnWatch": true, 9 | "seedEveryLesson": false, 10 | "isResetEnabled": true, 11 | "numberofLessons": null, 12 | "blockingTests": null, 13 | "breakOnFailure": null, 14 | "numberOfLessons": 27 15 | }, 16 | { 17 | "id": 1, 18 | "dashedName": "build-x-using-y", 19 | "isIntegrated": true, 20 | "isPublic": true, 21 | "currentLesson": 0, 22 | "runTestsOnWatch": null, 23 | "seedEveryLesson": null, 24 | "isResetEnabled": null, 25 | "numberofLessons": null, 26 | "blockingTests": true, 27 | "breakOnFailure": false, 28 | "numberOfLessons": 1 29 | }, 30 | { 31 | "id": 2, 32 | "dashedName": "external-seed", 33 | "isIntegrated": false, 34 | "isPublic": true, 35 | "currentLesson": 0, 36 | "runTestsOnWatch": false, 37 | "seedEveryLesson": true, 38 | "isResetEnabled": true, 39 | "numberofLessons": null, 40 | "blockingTests": false, 41 | "breakOnFailure": false, 42 | "numberOfLessons": 2 43 | }, 44 | { 45 | "id": 3, 46 | "dashedName": "project-reset", 47 | "isIntegrated": false, 48 | "isPublic": true, 49 | "currentLesson": 0, 50 | "runTestsOnWatch": false, 51 | "seedEveryLesson": false, 52 | "isResetEnabled": true, 53 | "numberofLessons": null, 54 | "blockingTests": false, 55 | "breakOnFailure": false, 56 | "numberOfLessons": 4 57 | }, 58 | { 59 | "id": 4, 60 | "dashedName": "lesson-watch", 61 | "isIntegrated": false, 62 | "isPublic": true, 63 | "currentLesson": 0, 64 | "runTestsOnWatch": true, 65 | "seedEveryLesson": false, 66 | "isResetEnabled": false, 67 | "numberofLessons": null, 68 | "blockingTests": false, 69 | "breakOnFailure": false, 70 | "numberOfLessons": 3 71 | } 72 | ] 73 | -------------------------------------------------------------------------------- /.freeCodeCamp/tooling/reset.js: -------------------------------------------------------------------------------- 1 | // Handles all the resetting of the projects 2 | import { resetBottomPanel, updateError, updateLoader } from './client-socks.js'; 3 | import { getProjectConfig, getState } from './env.js'; 4 | import { logover } from './logger.js'; 5 | import { runCommand, runLessonSeed } from './seed.js'; 6 | import { pluginEvents } from '../plugin/index.js'; 7 | 8 | /** 9 | * Resets the current project by running, in order, every seed 10 | * @param {WebSocket} ws 11 | */ 12 | export async function resetProject(ws) { 13 | resetBottomPanel(ws); 14 | // Get commands and handle file setting 15 | const { currentProject } = await getState(); 16 | const project = await getProjectConfig(currentProject); 17 | const { currentLesson } = project; 18 | updateLoader(ws, { 19 | isLoading: true, 20 | progress: { total: currentLesson, count: 0 } 21 | }); 22 | 23 | let lessonNumber = 0; 24 | try { 25 | await gitResetCurrentProjectDir(); 26 | while (lessonNumber <= currentLesson) { 27 | const { seed } = await pluginEvents.getLesson( 28 | currentProject, 29 | lessonNumber 30 | ); 31 | if (seed) { 32 | await runLessonSeed(seed, lessonNumber); 33 | } 34 | lessonNumber++; 35 | updateLoader(ws, { 36 | isLoading: true, 37 | progress: { total: currentLesson, count: lessonNumber } 38 | }); 39 | } 40 | } catch (err) { 41 | updateError(ws, err); 42 | logover.error(err); 43 | } 44 | updateLoader(ws, { 45 | isLoading: false, 46 | progress: { total: 1, count: 1 } 47 | }); 48 | } 49 | 50 | async function gitResetCurrentProjectDir() { 51 | const { currentProject } = await getState(); 52 | const project = await getProjectConfig(currentProject); 53 | try { 54 | logover.debug(`Cleaning '${project.dashedName}'`); 55 | const { stdout, stderr } = await runCommand( 56 | `git clean -f -q -- ${project.dashedName}` 57 | ); 58 | } catch (e) { 59 | logover.error(e); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/templates/project.tsx: -------------------------------------------------------------------------------- 1 | import { Description } from '../components/description'; 2 | import { Heading } from '../components/heading'; 3 | import { ConsoleError, F, LoaderT, ProjectI, TestType } from '../types'; 4 | import { Controls } from '../components/controls'; 5 | import { Output } from '../components/output'; 6 | import './project.css'; 7 | 8 | export interface ProjectProps { 9 | cancelTests: F; 10 | goToNextLesson: F; 11 | goToPreviousLesson: F; 12 | resetProject: F; 13 | runTests: F; 14 | cons: ConsoleError[]; 15 | description: string; 16 | hints: string[]; 17 | loader: LoaderT; 18 | lessonNumber: number; 19 | project: ProjectI; 20 | tests: TestType[]; 21 | } 22 | 23 | export const Project = ({ 24 | cancelTests, 25 | runTests, 26 | resetProject, 27 | goToNextLesson, 28 | goToPreviousLesson, 29 | loader, 30 | project, 31 | lessonNumber, 32 | description, 33 | tests, 34 | hints, 35 | cons 36 | }: ProjectProps) => { 37 | return ( 38 | <> 39 |
    40 | 51 | 52 | 53 | 54 | 70 | 71 | 72 |
    73 | 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/output.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { ConsoleError, TestType } from '../types'; 3 | import { Tests } from './tests'; 4 | import { Console } from './console'; 5 | import { Hints } from './hints'; 6 | 7 | interface OutputProps { 8 | hints: string[]; 9 | tests: TestType[]; 10 | cons: ConsoleError[]; 11 | } 12 | 13 | export const Output = ({ hints, tests, cons }: OutputProps) => { 14 | const [selectedBtn, setSelectedBtn] = useState('tests'); 15 | 16 | return ( 17 |
    18 |
      19 |
    • 20 | 29 |
    • 30 |
    • 31 | 40 |
    • 41 | {hints.length ? ( 42 |
    • 43 | 52 |
    • 53 | ) : null} 54 |
    55 | 56 |
    57 | {(() => { 58 | switch (selectedBtn) { 59 | case 'tests': 60 | return ; 61 | case 'console': 62 | return ; 63 | case 'hints': 64 | return ; 65 | default: 66 | return
    No content
    ; 67 | } 68 | })()} 69 |
    70 |
    71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/block.tsx: -------------------------------------------------------------------------------- 1 | import { SelectionProps } from './selection'; 2 | import { ProjectI, Events } from '../types/index'; 3 | import { Tag } from './tag'; 4 | import { Checkmark } from './checkmark'; 5 | 6 | type BlockProps = { 7 | sock: SelectionProps['sock']; 8 | } & ProjectI; 9 | 10 | export const Block = ({ 11 | id, 12 | title, 13 | description, 14 | isIntegrated, 15 | isPublic, 16 | numberOfLessons, 17 | currentLesson, 18 | completedDate, 19 | tags, 20 | sock 21 | }: BlockProps) => { 22 | function selectProject() { 23 | sock(Events.SELECT_PROJECT, { id }); 24 | } 25 | 26 | let lessonsCompleted = 0; 27 | if (completedDate) { 28 | lessonsCompleted = numberOfLessons; 29 | } else { 30 | lessonsCompleted = 31 | !isIntegrated && currentLesson === numberOfLessons - 1 32 | ? currentLesson + 1 33 | : currentLesson; 34 | } 35 | return ( 36 |
  • 37 | 77 |
  • 78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/components/controls.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { F, LoaderT, ProjectI, TestType } from '../types'; 3 | 4 | interface ControlsProps { 5 | cancelTests: F; 6 | runTests: F; 7 | resetProject?: F; 8 | isResetEnabled?: ProjectI['isResetEnabled']; 9 | tests: TestType[]; 10 | loader?: LoaderT; 11 | } 12 | 13 | // Changes the Reset button background to a filling progress bar when the seed is running 14 | function progressStyle(loader?: LoaderT) { 15 | if (!loader) { 16 | return {}; 17 | } 18 | 19 | const { 20 | isLoading, 21 | progress: { total, count } 22 | } = loader; 23 | if (isLoading) { 24 | return { 25 | background: `linear-gradient(to right, #0065A9 ${ 26 | (count / total) * 100 27 | }%, rgba(0,0,0,0) 0%)` 28 | }; 29 | } 30 | } 31 | 32 | export const Controls = ({ 33 | cancelTests, 34 | runTests, 35 | resetProject, 36 | isResetEnabled, 37 | tests, 38 | loader 39 | }: ControlsProps) => { 40 | const [isTestsRunning, setIsTestsRunning] = useState(false); 41 | 42 | useEffect(() => { 43 | if (tests.some(t => t.isLoading)) { 44 | setIsTestsRunning(true); 45 | } else { 46 | setIsTestsRunning(false); 47 | } 48 | }, [tests]); 49 | 50 | function handleTests() { 51 | if (isTestsRunning) { 52 | cancelTests(); 53 | } else { 54 | runTests(); 55 | } 56 | } 57 | 58 | const resetDisabled = !isResetEnabled || loader?.isLoading; 59 | 60 | return ( 61 |
    62 | 65 | {resetProject && ( 66 | 76 | )} 77 |
    78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@freecodecamp/freecodecamp-os", 3 | "author": "freeCodeCamp", 4 | "version": "3.5.1", 5 | "description": "Package used for freeCodeCamp projects with the freeCodeCamp Courses VSCode extension", 6 | "scripts": { 7 | "build:client": "NODE_ENV=production webpack --config ./.freeCodeCamp/webpack.config.cjs", 8 | "develop": "npm run develop:client & npm run develop:server", 9 | "develop:client": "NODE_ENV=development webpack --mode development --config ./.freeCodeCamp/webpack.config.cjs --watch", 10 | "develop:server": "nodemon --watch ./.freeCodeCamp/dist/ --watch ./.freeCodeCamp/tooling/ --watch ./tooling/ --ignore ./config/ ./.freeCodeCamp/tooling/server.js", 11 | "start": "npm run build:client && node ./.freeCodeCamp/tooling/server.js", 12 | "test": "node ./.freeCodeCamp/tests/parser.test.js", 13 | "prepublishOnly": "npm run build:client" 14 | }, 15 | "dependencies": { 16 | "chai": "4.5.0", 17 | "chokidar": "3.6.0", 18 | "express": "4.21.2", 19 | "logover": "2.0.0", 20 | "marked": "9.1.6", 21 | "marked-highlight": "2.2.3", 22 | "prismjs": "1.30.0", 23 | "ws": "8.18.3" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "7.28.5", 27 | "@babel/plugin-syntax-import-assertions": "7.27.1", 28 | "@babel/preset-env": "7.28.5", 29 | "@babel/preset-react": "7.28.5", 30 | "@babel/preset-typescript": "7.28.5", 31 | "@types/marked": "5.0.2", 32 | "@types/node": "20.19.25", 33 | "@types/prismjs": "1.26.5", 34 | "@types/react": "18.3.27", 35 | "@types/react-dom": "18.3.7", 36 | "babel-loader": "9.2.1", 37 | "babel-plugin-prismjs": "2.1.0", 38 | "css-loader": "6.11.0", 39 | "file-loader": "6.2.0", 40 | "html-webpack-plugin": "5.6.4", 41 | "nodemon": "3.1.11", 42 | "react": "18.3.1", 43 | "react-dom": "18.3.1", 44 | "style-loader": "3.3.4", 45 | "ts-loader": "9.5.4", 46 | "typescript": "5.9.3", 47 | "webpack-cli": "5.1.4", 48 | "webpack-dev-server": "4.15.2" 49 | }, 50 | "repository": { 51 | "type": "git", 52 | "url": "https://github.com/freeCodeCamp/freeCodeCampOS" 53 | }, 54 | "type": "module" 55 | } 56 | -------------------------------------------------------------------------------- /docs/theme/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /self/client/assets/fcc_primary_small.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/assets/fcc_primary_small.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/mdbook.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ['main'] 7 | paths: 8 | - 'docs/**' 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 14 | permissions: 15 | contents: read 16 | pages: write 17 | id-token: write 18 | 19 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 20 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 21 | concurrency: 22 | group: 'pages' 23 | cancel-in-progress: false 24 | 25 | jobs: 26 | # Build job 27 | build: 28 | runs-on: ubuntu-latest 29 | env: 30 | MDBOOK_VERSION: 0.4.36 31 | steps: 32 | - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 33 | - name: Install mdBook 34 | run: | 35 | curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf -y | sh 36 | rustup update 37 | cargo install --version ${MDBOOK_VERSION} mdbook 38 | cargo install --version ^1 mdbook-admonish 39 | - name: Setup Pages 40 | id: pages 41 | uses: actions/configure-pages@b8130d9ab958b325bbde9786d62f2c97a9885a0e # v3 42 | - name: Build with mdBook 43 | run: cd docs && mdbook build 44 | - name: Check Expected Files 45 | run: | 46 | if [ ! -f docs/book/html/index.html ]; then 47 | echo "Expected file docs/book/html/index.html is missing. Double-check the mdBook/Pages config." 48 | exit 1 49 | fi 50 | - name: Upload artifact 51 | uses: actions/upload-pages-artifact@84bb4cd4b733d5c320c9c9cfbc354937524f4d64 # v1 52 | with: 53 | path: ./docs/book/html 54 | 55 | # Deployment job 56 | deploy: 57 | environment: 58 | name: github-pages 59 | url: ${{ steps.deployment.outputs.page_url }} 60 | runs-on: ubuntu-latest 61 | needs: build 62 | steps: 63 | - name: Deploy to GitHub Pages 64 | id: deployment 65 | uses: actions/deploy-pages@de14547edc9944350dc0481aa5b7afb08e75f254 # v2 66 | -------------------------------------------------------------------------------- /self/curriculum/locales/english/project-reset.md: -------------------------------------------------------------------------------- 1 | # Project Reset 2 | 3 | This project tests the reset functionality of `freecodecamp-os` 4 | 5 | ## 0 6 | 7 | ### --description-- 8 | 9 | The first lesson does not necessarily need to have a seed, because, on reset, `git clean -f -q -- ` is run. 10 | 11 | ### --hints-- 12 | 13 | #### 0 14 | 15 | **Note:** `git clean` only works if Campers have not committed any changes. Otherwise, it is best to write a custom seed command for the first lesson. 16 | 17 | ### --tests-- 18 | 19 | This test always passes for testing. 20 | 21 | ```js 22 | await new Promise(resolve => setTimeout(resolve, 1000)); 23 | ``` 24 | 25 | ## 1 26 | 27 | ### --description-- 28 | 29 | This lesson's seed adds the `a.md` file, and runs a command which takes 2 seconds to complete. 30 | 31 | ### --tests-- 32 | 33 | This test always passes for testing. 34 | 35 | ```js 36 | await new Promise(resolve => setTimeout(resolve, 1000)); 37 | ``` 38 | 39 | ### --seed-- 40 | 41 | #### --"project-reset/a.md"-- 42 | 43 | ```md 44 | File from lesson 1 45 | ``` 46 | 47 | #### --cmd-- 48 | 49 | ```bash 50 | echo "Lesson 1" && sleep 2 51 | ``` 52 | 53 | ## 2 54 | 55 | ### --description-- 56 | 57 | This lesson's seed adds the `b.md` file, and runs a command which takes 2 seconds to complete. 58 | 59 | ### --tests-- 60 | 61 | This test always passes for testing. 62 | 63 | ```js 64 | await new Promise(resolve => setTimeout(resolve, 1000)); 65 | ``` 66 | 67 | ### --seed-- 68 | 69 | #### --"project-reset/b.md"-- 70 | 71 | ```md 72 | File from lesson 2 73 | ``` 74 | 75 | #### --cmd-- 76 | 77 | ```bash 78 | echo "Lesson 2" && sleep 2 79 | ``` 80 | 81 | ## 3 82 | 83 | ### --description-- 84 | 85 | This lesson's seed adds the `c.md` file, and runs a command which takes 2 seconds to complete. 86 | 87 | ### --tests-- 88 | 89 | This test always passes for testing. 90 | 91 | ```js 92 | await new Promise(resolve => setTimeout(resolve, 1000)); 93 | ``` 94 | 95 | ### --seed-- 96 | 97 | #### --"project-reset/c.md"-- 98 | 99 | ```md 100 | File from lesson 3 101 | ``` 102 | 103 | #### --cmd-- 104 | 105 | ```bash 106 | echo "Lesson 3" && sleep 2 107 | ``` 108 | 109 | ## --fcc-end-- 110 | -------------------------------------------------------------------------------- /self/curriculum/locales/english/build-x-using-y.md: -------------------------------------------------------------------------------- 1 | # Build X Using Y 2 | 3 | ```json 4 | { 5 | "tags": ["Integrated Project", "Coming soon!"] 6 | } 7 | ``` 8 | 9 | In this course, you will build x using y. 10 | 11 | ## 0 12 | 13 | ### --description-- 14 | 15 | Some description here. 16 | 17 | ```rust 18 | fn main() { 19 | println!("Hello, world!"); 20 | } 21 | ``` 22 | 23 | Here is an image: 24 | 25 | 26 | 27 | ### --tests-- 28 | 29 | First test using Chai.js `assert`. 30 | 31 | ```js 32 | // 0 33 | // Timeout for 3 seconds 34 | await new Promise(resolve => setTimeout(resolve, 3000)); 35 | assert.equal(true, true); 36 | ``` 37 | 38 | Second test using global variables passed from `before` hook. 39 | 40 | ```js 41 | // 1 42 | await new Promise(resolve => setTimeout(resolve, 4000)); 43 | assert.equal(__projectLoc, 'example global variable for tests'); 44 | ``` 45 | 46 | Dynamic helpers should be imported. 47 | 48 | ```js 49 | // 2 50 | await new Promise(resolve => setTimeout(resolve, 1000)); 51 | assert.equal(__helpers.testDynamicHelper(), 'Helper success!'); 52 | ``` 53 | 54 | ### --before-each-- 55 | 56 | ```js 57 | await new Promise(resolve => setTimeout(resolve, 1000)); 58 | const __projectLoc = 'example global variable for tests'; 59 | ``` 60 | 61 | ### --after-each-- 62 | 63 | ```js 64 | await new Promise(resolve => setTimeout(resolve, 1000)); 65 | logover.info('after each'); 66 | ``` 67 | 68 | ### --before-all-- 69 | 70 | ```js 71 | await new Promise(resolve => setTimeout(resolve, 1000)); 72 | logover.info('before all'); 73 | ``` 74 | 75 | ### --after-all-- 76 | 77 | ```js 78 | await new Promise(resolve => setTimeout(resolve, 1000)); 79 | logover.info('after all'); 80 | ``` 81 | 82 | ### --hints-- 83 | 84 | #### 0 85 | 86 | Inline hint with `some` code `blocks`. 87 | 88 | #### 1 89 | 90 | Multi-line hint with: 91 | 92 | ```js 93 | const code_block = true; 94 | ``` 95 | 96 | ### --seed-- 97 | 98 | #### --force-- 99 | 100 | #### --"build-x-using-y/readme.md"-- 101 | 102 | ```markdown 103 | # Build X Using Y 104 | 105 | In this course 106 | 107 | ## 0 108 | 109 | Hello 110 | ``` 111 | 112 | #### --cmd-- 113 | 114 | ```bash 115 | npm install 116 | ``` 117 | 118 | ## --fcc-end-- 119 | -------------------------------------------------------------------------------- /.freeCodeCamp/webpack.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | module.exports = { 4 | entry: path.join(__dirname, 'client/index.tsx'), 5 | devtool: 'inline-source-map', 6 | mode: process.env.NODE_ENV || 'development', 7 | devServer: { 8 | compress: true, 9 | port: 9000 10 | }, 11 | watch: process.env.NODE_ENV === 'development', 12 | watchOptions: { 13 | ignored: ['**/node_modules', '**/config'] 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(js|jsx|tsx|ts)$/, 19 | use: { 20 | loader: 'babel-loader', 21 | options: { 22 | presets: ['@babel/preset-env'], 23 | plugins: [ 24 | require.resolve('@babel/plugin-syntax-import-assertions'), 25 | [ 26 | 'prismjs', 27 | { 28 | languages: [ 29 | 'javascript', 30 | 'css', 31 | 'html', 32 | 'json', 33 | 'markdown', 34 | 'sql', 35 | 'rust', 36 | 'typescript', 37 | 'jsx', 38 | 'c', 39 | 'csharp', 40 | 'cpp', 41 | 'dotnet', 42 | 'python', 43 | 'pug', 44 | 'handlebars' 45 | ], 46 | plugins: [], 47 | theme: 'okaidia', 48 | css: true 49 | } 50 | ] 51 | ] 52 | } 53 | } 54 | }, 55 | { 56 | test: /\.(ts|tsx)$/, 57 | use: ['ts-loader'] 58 | }, 59 | { 60 | test: /\.(css|scss)$/, 61 | use: ['style-loader', 'css-loader'] 62 | }, 63 | { 64 | test: /\.(jpg|jpeg|png|gif|mp3|svg)$/, 65 | type: 'asset/resource' 66 | } 67 | ] 68 | }, 69 | resolve: { 70 | extensions: ['.tsx', '.ts', '.js'] 71 | }, 72 | output: { 73 | filename: 'bundle.js', 74 | path: path.resolve(__dirname, 'dist') 75 | }, 76 | plugins: [ 77 | new HtmlWebpackPlugin({ 78 | template: path.join(__dirname, 'client', 'index.html'), 79 | favicon: path.join(__dirname, 'client', 'assets/fcc_primary_small.svg') 80 | }) 81 | ] 82 | }; 83 | -------------------------------------------------------------------------------- /docs/src/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Local Development 4 | 5 | 1. Open `freeCodeCampOS/self` as a new workspace in VSCode 6 | 2. Run `npm i` 7 | 3. Run `freeCodeCamp: Develop Course` in the command palette 8 | 9 | ## Gitpod 10 | 11 | 1. Open the project in Gitpod: 12 | 13 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/freeCodeCamp/freecodecampOS) 14 | 15 | ## Opening a Pull Request 16 | 17 | 1. Fork the repository 18 | 2. Push your changes to your fork 19 | 3. Open a pull request with the recommended style 20 | 21 | ### Commit Message 22 | 23 | ```markdown 24 | (): 25 | ``` 26 | 27 | ```admonish example 28 | feat(docs): add contributing.md 29 | ``` 30 | 31 | ### Pull Request Title 32 | 33 | ```markdown 34 | (): 35 | ``` 36 | 37 | ```admonish example 38 | feat(docs): add contributing.md 39 | ``` 40 | 41 | ### Pull Request Body 42 | 43 | Answer the following questions: 44 | 45 | - What does this pull request do? 46 | - How should this be manually tested? 47 | - Any background context you want to provide? 48 | - What are the relevant issues? 49 | - Screenshots (if appropriate) 50 | 51 | ### Types 52 | 53 | - `fix` 54 | - `feat` 55 | - `refactor` 56 | - `chore` 57 | 58 | ### Scopes 59 | 60 | Any top-level directory or config file. Changing a package should have a scope of `dep` or `deps`. 61 | 62 | ## Documentation 63 | 64 | This documention is built using [mdBook](https://rust-lang.github.io/mdBook/). Read their documentation to install the latest version. 65 | 66 | Also, the documentation uses `mdbook-admonish`: 67 | 68 | ```bash 69 | cargo install mdbook-admonish 70 | ``` 71 | 72 | ### Serve the Documentation 73 | 74 | ```bash 75 | cd docs 76 | mdbook serve 77 | ``` 78 | 79 | This will spin up a local server at `http://localhost:3000`. Also, this has hot-reloading, so any changes you make will be reflected in the browser. 80 | 81 | ### Build the Documentation 82 | 83 | ```bash 84 | cd docs 85 | mdbook build 86 | ``` 87 | 88 | ## CLI (`create-freecodecamp-os-app`) 89 | 90 | The CLI is written in Rust, and is located in the `cli` directory. 91 | 92 | ### Development 93 | 94 | ```bash 95 | $ cd cli 96 | cli$ cargo run 97 | ``` 98 | 99 | --- 100 | 101 | ## Flight Manual 102 | 103 | ### Release 104 | 105 | Releases are done manually through the GitHub Actions. 106 | 107 | #### Making a Release 108 | 109 | In the `Actions` tab, select the `Publish to npm` workflow. Then, select `Run workflow`. 110 | -------------------------------------------------------------------------------- /.freeCodeCamp/tooling/git/build.js: -------------------------------------------------------------------------------- 1 | // This file handles creating the Git curriculum branches 2 | import { join } from 'path'; 3 | import { getState, setState } from '../env.js'; 4 | import { 5 | getCommands, 6 | getFilesWithSeed, 7 | getLessonFromFile, 8 | getLessonSeed 9 | } from '../parser.js'; 10 | import { runCommands, runSeed } from '../seed.js'; 11 | import { 12 | checkoutMain, 13 | commit, 14 | deleteBranch, 15 | initCurrentProjectBranch, 16 | pushProject 17 | } from './gitterizer.js'; 18 | import { logover } from '../logger.js'; 19 | 20 | const PROJECT_LIST = ['project-1']; 21 | 22 | for (const project of PROJECT_LIST) { 23 | await setState({ currentProject: project }); 24 | try { 25 | await deleteBranch(project); 26 | await buildProject(); 27 | } catch (e) { 28 | logover.error('Failed to build project: ', project); 29 | await deleteBranch(project); 30 | throw new Error(e); 31 | } finally { 32 | await checkoutMain(); 33 | logover.info('✅ Successfully built project: ', project); 34 | } 35 | } 36 | logover.info('✅ Successfully built all projects'); 37 | 38 | async function buildProject() { 39 | const { currentProject } = await getState(); 40 | const FILE = join( 41 | ROOT, 42 | freeCodeCampConfig.curriculum.locales['english'], 43 | project.dashedName + '.md' 44 | ); 45 | 46 | try { 47 | await initCurrentProjectBranch(); 48 | } catch (e) { 49 | logover.error('🔴 Failed to create a branch for ', currentProject); 50 | throw new Error(e); 51 | } 52 | 53 | let lessonNumber = 1; 54 | let lesson = await getLessonFromFile(FILE, lessonNumber); 55 | if (!lesson) { 56 | return Promise.reject( 57 | new Error(`🔴 No lesson found for ${currentProject}`) 58 | ); 59 | } 60 | while (lesson) { 61 | const seed = getLessonSeed(lesson); 62 | 63 | if (seed) { 64 | const commands = getCommands(seed); 65 | const filesWithSeed = getFilesWithSeed(seed); 66 | try { 67 | await runCommands(commands); 68 | // TODO: Not correct signature 69 | await runSeed(filesWithSeed); 70 | } catch (e) { 71 | logover.error('🔴 Failed to run seed for lesson: ', lessonNumber); 72 | throw new Error(e); 73 | } 74 | } 75 | try { 76 | // Always commit? Or, skip when seed is empty? 77 | await commit(lessonNumber); 78 | } catch (e) { 79 | throw new Error(e); 80 | } 81 | lessonNumber++; 82 | lesson = await getLessonFromFile(FILE, lessonNumber); 83 | } 84 | 85 | try { 86 | await pushProject(); 87 | } catch (e) { 88 | throw new Error(e); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /self/client/injectable.js: -------------------------------------------------------------------------------- 1 | function checkForToken() { 2 | const serverTokenCode = ` 3 | try { 4 | const {readFile} = await import('fs/promises'); 5 | const tokenFile = await readFile(join(ROOT, 'config/token.txt')); 6 | const token = tokenFile.toString(); 7 | console.log(token); 8 | __result = token; 9 | } catch (e) { 10 | __result = null; 11 | }`; 12 | socket.send( 13 | JSON.stringify({ 14 | event: '__run-client-code', 15 | data: serverTokenCode 16 | }) 17 | ); 18 | } 19 | 20 | async function askForToken() { 21 | const modal = document.createElement('dialog'); 22 | const p = document.createElement('p'); 23 | p.innerText = 'Enter your token'; 24 | p.style.color = 'black'; 25 | const input = document.createElement('input'); 26 | input.type = 'text'; 27 | input.id = 'token-input'; 28 | input.style.color = 'black'; 29 | const button = document.createElement('button'); 30 | button.innerText = 'Submit'; 31 | button.style.color = 'black'; 32 | button.onclick = async () => { 33 | const token = input.value; 34 | const serverTokenCode = ` 35 | try { 36 | const {writeFile} = await import('fs/promises'); 37 | await writeFile(join(ROOT, 'config/token.txt'), '${token}'); 38 | __result = true; 39 | } catch (e) { 40 | console.error(e); 41 | __result = false; 42 | }`; 43 | socket.send( 44 | JSON.stringify({ 45 | event: '__run-client-code', 46 | data: serverTokenCode 47 | }) 48 | ); 49 | modal.close(); 50 | }; 51 | 52 | modal.appendChild(p); 53 | modal.appendChild(input); 54 | modal.appendChild(button); 55 | document.body.appendChild(modal); 56 | modal.showModal(); 57 | } 58 | 59 | const socket = new WebSocket( 60 | `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${ 61 | window.location.host 62 | }` 63 | ); 64 | 65 | window.onload = function () { 66 | socket.onmessage = function (event) { 67 | const parsedData = JSON.parse(event.data); 68 | if ( 69 | parsedData.event === 'RESPONSE' && 70 | parsedData.data.event === '__run-client-code' 71 | ) { 72 | if (parsedData.data.error) { 73 | console.log(parsedData.data.error); 74 | return; 75 | } 76 | const { __result } = parsedData.data; 77 | if (!__result) { 78 | askForToken(); 79 | return; 80 | } 81 | window.__token = __result; 82 | } 83 | }; 84 | let interval; 85 | interval = setInterval(() => { 86 | if (socket.readyState === 1) { 87 | clearInterval(interval); 88 | checkForToken(); 89 | } 90 | }, 1000); 91 | }; 92 | -------------------------------------------------------------------------------- /self/tooling/camper-info.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Provides command-line output of useful debugging information 3 | * @example 4 | * 5 | * ```bash 6 | * node tooling/camper-info.js --history --directory 7 | * ``` 8 | */ 9 | 10 | import { 11 | getProjectConfig, 12 | getConfig, 13 | getState 14 | } from '@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/env.js'; 15 | import __helpers from '@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/test-utils.js'; 16 | import { Logger } from 'logover'; 17 | import { readdir, readFile } from 'fs/promises'; 18 | import { join } from 'path'; 19 | 20 | const logover = new Logger({ level: 'debug', timestamp: null }); 21 | 22 | const FLAGS = process.argv; 23 | 24 | async function main() { 25 | try { 26 | const handleFlag = { 27 | '--history': printCommandHistory, 28 | '--directory': printDirectoryTree 29 | }; 30 | const projectConfig = await getProjectConfig(); 31 | const config = await getConfig(); 32 | const state = await getState(); 33 | 34 | const { currentProject } = state; 35 | const { currentLesson } = projectConfig; 36 | const { version } = config; 37 | 38 | const devContainerFile = await readFile( 39 | '.devcontainer/devcontainer.json', 40 | 'utf-8' 41 | ); 42 | const devConfig = JSON.parse(devContainerFile); 43 | const coursesVersion = devConfig.extensions?.find(e => 44 | e.match('freecodecamp-courses') 45 | ); 46 | 47 | const { stdout } = await __helpers.getCommandOutput('git log -1'); 48 | 49 | logover.info('Project: ', currentProject); 50 | logover.info('Lesson Number: ', currentLesson); 51 | logover.info('Curriculum Version: ', version); 52 | logover.info('freeCodeCamp - Courses: ', coursesVersion); 53 | logover.info('Commit: ', stdout); 54 | 55 | for (const arg of FLAGS) { 56 | await handleFlag[arg]?.(); 57 | } 58 | async function printDirectoryTree() { 59 | const files = await readdir('.', { withFileTypes: true }); 60 | let depth = 0; 61 | for (const file of files) { 62 | if (file.isDirectory() && file.name === currentProject) { 63 | await recurseDirectory(file.name, depth); 64 | } 65 | } 66 | } 67 | 68 | async function printCommandHistory() { 69 | const historyCwd = await readFile('.logs/.history_cwd.log', 'utf-8'); 70 | logover.info('Command History:\n', historyCwd); 71 | } 72 | } catch (e) { 73 | logover.error(e); 74 | } 75 | } 76 | 77 | main(); 78 | 79 | const IGNORE = ['node_modules', 'target']; 80 | async function recurseDirectory(path, depth) { 81 | logover.info(`|${' '.repeat(depth * 2)}|-- ${path}`); 82 | depth++; 83 | const files = await readdir(path, { withFileTypes: true }); 84 | for (const file of files) { 85 | if (!IGNORE.includes(file.name)) { 86 | if (file.isDirectory()) { 87 | await recurseDirectory(join(path, file.name), depth); 88 | } else { 89 | logover.info(`|${' '.repeat(depth * 2)}|-- ${file.name}`); 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /self/.vscode/javascript.json.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | // Place your freeCodeCampOS workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and 3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope 4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is 5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: 6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. 7 | // Placeholders with the same ids are connected. 8 | // Example: 9 | // "Print to console": { 10 | // "scope": "javascript,typescript", 11 | // "prefix": "log", 12 | // "body": [ 13 | // "console.log('$1');", 14 | // "$2" 15 | // ], 16 | // "description": "Log output to console" 17 | // } 18 | "__helpers snippet": { 19 | "scope": "jsonc", 20 | "prefix": "__helpers", 21 | "body": [ 22 | "\"$functionName\": {", 23 | "\t\"scope\": \"javascript\",", 24 | "\t\"prefix\": \"__helpers.$functionName()\",", 25 | "\t\"body\": \"__helpers.$functionName()\",", 26 | "}," 27 | ] 28 | }, 29 | "controlWrapper": { 30 | "scope": "javascript", 31 | "prefix": "__helpers.controlWrapper()", 32 | "body": "__helpers.controlWrapper($1)" 33 | }, 34 | "getBashHistory": { 35 | "scope": "javascript", 36 | "prefix": "__helpers.getBashHistory()", 37 | "body": "__helpers.getBashHistory()" 38 | }, 39 | "getCommandOutput": { 40 | "scope": "javascript", 41 | "prefix": "__helpers.getCommandOutput()", 42 | "body": "__helpers.getCommandOutput($1)" 43 | }, 44 | "getCWD": { 45 | "scope": "javascript", 46 | "prefix": "__helpers.getCWD()", 47 | "body": "__helpers.getCWD()" 48 | }, 49 | "getLastCommand": { 50 | "scope": "javascript", 51 | "prefix": "__helpers.getLastCommand()", 52 | "body": "__helpers.getLastCommand($1)" 53 | }, 54 | "getLastCWD": { 55 | "scope": "javascript", 56 | "prefix": "__helpers.getLastCWD()", 57 | "body": "__helpers.getLastCWD($1)" 58 | }, 59 | "getTemp": { 60 | "scope": "javascript", 61 | "prefix": "__helpers.getTemp()", 62 | "body": "__helpers.getTemp()" 63 | }, 64 | "getTerminalOutput": { 65 | "scope": "javascript", 66 | "prefix": "__helpers.getTerminalOutput()", 67 | "body": "__helpers.getTerminalOutput()" 68 | }, 69 | "importSansCache": { 70 | "scope": "javascript", 71 | "prefix": "__helpers.importSansCache()", 72 | "body": "__helpers.importSansCache($1)" 73 | }, 74 | "logover": { 75 | "scope": "javascript", 76 | "prefix": "__helpers.logover()", 77 | "body": "__helpers.logover($1)" 78 | }, 79 | "makeDirectory": { 80 | "scope": "javascript", 81 | "prefix": "__helpers.makeDirectory()", 82 | "body": "__helpers.makeDirectory($1)" 83 | }, 84 | "runCommand": { 85 | "scope": "javascript", 86 | "prefix": "__helpers.runCommand()", 87 | "body": "__helpers.runCommand($1)" 88 | }, 89 | "writeJsonFile": { 90 | "scope": "javascript", 91 | "prefix": "__helpers.writeJsonFile()", 92 | "body": "__helpers.writeJsonFile($1)" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /.freeCodeCamp/tooling/seed.js: -------------------------------------------------------------------------------- 1 | // This file handles seeding the lesson contents with the seed in markdown. 2 | import { join } from 'path'; 3 | import { 4 | ROOT, 5 | getState, 6 | freeCodeCampConfig, 7 | getProjectConfig, 8 | setState 9 | } from './env.js'; 10 | import { writeFile } from 'fs/promises'; 11 | import { promisify } from 'util'; 12 | import { exec } from 'child_process'; 13 | import { logover } from './logger.js'; 14 | import { updateLoader, updateError } from './client-socks.js'; 15 | import { watcher } from './hot-reload.js'; 16 | import { pluginEvents } from '../plugin/index.js'; 17 | const execute = promisify(exec); 18 | 19 | /** 20 | * Seeds the current lesson 21 | * @param {WebSocket} ws 22 | * @param {string} projectDashedName 23 | */ 24 | export async function seedLesson(ws, projectDashedName) { 25 | updateLoader(ws, { 26 | isLoading: true, 27 | progress: { total: 2, count: 1 } 28 | }); 29 | const project = await getProjectConfig(projectDashedName); 30 | const { currentLesson } = project; 31 | 32 | try { 33 | const { seed } = await pluginEvents.getLesson( 34 | projectDashedName, 35 | currentLesson 36 | ); 37 | 38 | await runLessonSeed(seed, currentLesson); 39 | await setState({ 40 | lastSeed: { 41 | projectDashedName, 42 | lessonNumber: currentLesson 43 | } 44 | }); 45 | } catch (e) { 46 | updateError(ws, e); 47 | logover.error(e); 48 | } 49 | updateLoader(ws, { isLoading: false, progress: { total: 1, count: 1 } }); 50 | } 51 | 52 | /** 53 | * Runs the given array of commands in order 54 | * @param {string[]} commands - Array of commands to run 55 | */ 56 | export async function runCommands(commands) { 57 | // Execute the following commands in the shell 58 | for (const command of commands) { 59 | const { stdout, stderr } = await execute(command); 60 | if (stdout) { 61 | logover.debug(stdout); 62 | } 63 | if (stderr) { 64 | logover.error(stderr); 65 | return Promise.reject(stderr); 66 | } 67 | } 68 | return Promise.resolve(); 69 | } 70 | 71 | /** 72 | * Runs the given command 73 | * @param {string} command - Commands to run 74 | */ 75 | export async function runCommand(command, path = '.') { 76 | const cmdOut = await execute(command, { 77 | cwd: join(ROOT, path), 78 | shell: '/bin/bash' 79 | }); 80 | return cmdOut; 81 | } 82 | 83 | /** 84 | * Seeds the given path relative to root with the given seed 85 | */ 86 | export async function runSeed(fileSeed, filePath) { 87 | const path = join(ROOT, filePath); 88 | await writeFile(path, fileSeed); 89 | } 90 | 91 | /** 92 | * Runs the given seed for the given project and lesson number 93 | * @param {string} seed 94 | * @param {number} currentLesson 95 | */ 96 | export async function runLessonSeed(seed, currentLesson) { 97 | try { 98 | for (const cmdOrFile of seed) { 99 | if (typeof cmdOrFile === 'string') { 100 | const { stdout, stderr } = await runCommand(cmdOrFile); 101 | if (stdout || stderr) { 102 | logover.debug(stdout, stderr); 103 | } 104 | } else { 105 | const { filePath, fileSeed } = cmdOrFile; 106 | // Stop watching file being seeded to prevent triggering tests on hot reload 107 | watcher.unwatch(filePath); 108 | await runSeed(fileSeed, filePath); 109 | watcher.add(filePath); 110 | } 111 | } 112 | } catch (e) { 113 | logover.error('Failed to run seed for lesson: ', currentLesson); 114 | throw new Error(e); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /docs/src/client-injection.md: -------------------------------------------------------------------------------- 1 | # Client Injection 2 | 3 | With the [`static` config](./configuration.md#client) option, you can add a `/script/injectable.js` script to be injected in the `head` of the client. 4 | 5 | ````admonish example 6 | ```json 7 | { 8 | "client": { 9 | "static": { 10 | "/script/injectable.js": "./client/injectable.js" 11 | } 12 | } 13 | } 14 | ``` 15 | ```` 16 | 17 | There is a reserved websocket event (`__run-client-code`) that can be used to send code from the client to the server to be run in the server's context. The code has access to a few globals: 18 | 19 | - `ROOT`: The root of the course 20 | - `join`: The Node.js `path` module function 21 | 22 | This enables scripts like the following to be run: 23 | 24 | ````admonish example collapsible=true title="client/injectable.js" 25 | ```js 26 | function checkForToken() { 27 | const serverTokenCode = ` 28 | try { 29 | const {readFile} = await import('fs/promises'); 30 | const tokenFile = await readFile(join(ROOT, 'config/token.txt')); 31 | const token = tokenFile.toString(); 32 | console.log(token); 33 | __result = token; 34 | } catch (e) { 35 | console.error(e); 36 | __result = null; 37 | }`; 38 | socket.send( 39 | JSON.stringify({ 40 | event: '__run-client-code', 41 | data: serverTokenCode 42 | }) 43 | ); 44 | } 45 | 46 | async function askForToken() { 47 | const modal = document.createElement('dialog'); 48 | const p = document.createElement('p'); 49 | p.innerText = 'Enter your token'; 50 | p.style.color = 'black'; 51 | const input = document.createElement('input'); 52 | input.type = 'text'; 53 | input.id = 'token-input'; 54 | input.style.color = 'black'; 55 | const button = document.createElement('button'); 56 | button.innerText = 'Submit'; 57 | button.style.color = 'black'; 58 | button.onclick = async () => { 59 | const token = input.value; 60 | const serverTokenCode = ` 61 | try { 62 | const {writeFile} = await import('fs/promises'); 63 | await writeFile(join(ROOT, 'config/token.txt'), '${token}'); 64 | __result = true; 65 | } catch (e) { 66 | console.error(e); 67 | __result = false; 68 | }`; 69 | socket.send( 70 | JSON.stringify({ 71 | event: '__run-client-code', 72 | data: serverTokenCode 73 | }) 74 | ); 75 | modal.close(); 76 | }; 77 | 78 | modal.appendChild(p); 79 | modal.appendChild(input); 80 | modal.appendChild(button); 81 | document.body.appendChild(modal); 82 | modal.showModal(); 83 | } 84 | 85 | const socket = new WebSocket( 86 | `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${ 87 | window.location.host 88 | }` 89 | ); 90 | 91 | window.onload = function () { 92 | socket.onmessage = function (event) { 93 | const parsedData = JSON.parse(event.data); 94 | if ( 95 | parsedData.event === 'RESPONSE' && 96 | parsedData.data.event === '__run-client-code' 97 | ) { 98 | if (parsedData.data.error) { 99 | console.log(parsedData.data.error); 100 | return; 101 | } 102 | const { __result } = parsedData.data; 103 | if (!__result) { 104 | askForToken(); 105 | return; 106 | } 107 | window.__token = __result; 108 | } 109 | }; 110 | let interval; 111 | interval = setInterval(() => { 112 | if (socket.readyState === 1) { 113 | clearInterval(interval); 114 | checkForToken(); 115 | } 116 | }, 1000); 117 | }; 118 | 119 | ``` 120 | ```` 121 | -------------------------------------------------------------------------------- /.freeCodeCamp/tooling/lesson.js: -------------------------------------------------------------------------------- 1 | // This file parses answer files for lesson content 2 | import { join } from 'path'; 3 | import { 4 | updateDescription, 5 | updateProjectHeading, 6 | updateTests, 7 | updateProject, 8 | updateError, 9 | resetBottomPanel 10 | } from './client-socks.js'; 11 | import { ROOT, getState, getProjectConfig, setState } from './env.js'; 12 | import { logover } from './logger.js'; 13 | import { seedLesson } from './seed.js'; 14 | import { pluginEvents } from '../plugin/index.js'; 15 | import { 16 | unwatchAll, 17 | watchAll, 18 | watchPathRelativeToRoot, 19 | watcher 20 | } from './hot-reload.js'; 21 | 22 | /** 23 | * Runs the lesson from the `projectDashedName` config. 24 | * @param {WebSocket} ws WebSocket connection to the client 25 | * @param {string} projectDashedName 26 | */ 27 | export async function runLesson(ws, projectDashedName) { 28 | const project = await getProjectConfig(projectDashedName); 29 | const { isIntegrated, dashedName, seedEveryLesson, currentLesson } = project; 30 | const { lastSeed, lastWatchChange } = await getState(); 31 | try { 32 | const { description, seed, isForce, tests, meta } = 33 | await pluginEvents.getLesson(projectDashedName, currentLesson); 34 | 35 | // TODO: Consider performance optimizations 36 | // - Do not run at all if whole project does not contain any `meta`. 37 | await handleWatcher(meta, { lastWatchChange, currentLesson }); 38 | 39 | if (currentLesson === 0) { 40 | await pluginEvents.onProjectStart(project); 41 | } 42 | await pluginEvents.onLessonLoad(project); 43 | 44 | updateProject(ws, project); 45 | 46 | if (!isIntegrated) { 47 | updateTests( 48 | ws, 49 | tests.reduce((acc, curr, i) => { 50 | return [ 51 | ...acc, 52 | { passed: false, testText: curr[0], testId: i, isLoading: false } 53 | ]; 54 | }, []) 55 | ); 56 | } 57 | resetBottomPanel(ws); 58 | 59 | const { title } = await pluginEvents.getProjectMeta(projectDashedName); 60 | updateProjectHeading(ws, { 61 | title, 62 | lessonNumber: currentLesson 63 | }); 64 | updateDescription(ws, description); 65 | 66 | // If the current lesson has not already been seeded, seed it 67 | // Otherwise, Campers can click the "Reset Project" button to re-seed a lesson 68 | if ( 69 | lastSeed?.projectDashedName !== dashedName || 70 | (lastSeed?.projectDashedName === dashedName && 71 | lastSeed?.lessonNumber !== currentLesson) 72 | ) { 73 | if (seed) { 74 | // force flag overrides seed flag 75 | if ((seedEveryLesson && !isForce) || (!seedEveryLesson && isForce)) { 76 | await seedLesson(ws, dashedName); 77 | } 78 | } 79 | } 80 | } catch (err) { 81 | updateError(ws, err); 82 | logover.error(err); 83 | } 84 | } 85 | 86 | async function handleWatcher(meta, { lastWatchChange, currentLesson }) { 87 | // Calling `watcher` methods takes a performance hit. So, check is behind a check that the lesson has changed. 88 | if (lastWatchChange !== currentLesson) { 89 | if (meta?.watch) { 90 | unwatchAll(); 91 | for (const path of meta.watch) { 92 | const toWatch = join(ROOT, path); 93 | watchPathRelativeToRoot(toWatch); 94 | } 95 | } else if (meta?.ignore) { 96 | await watchAll(); 97 | watcher.unwatch(meta.ignore); 98 | } else { 99 | // Reset watcher back to default/freecodecamp.conf.json 100 | await watchAll(); 101 | } 102 | } 103 | await setState({ lastWatchChange: currentLesson }); 104 | } 105 | -------------------------------------------------------------------------------- /self/tooling/extract-seed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Extract seed from curriculum file to separate -seed.md file. Extracted seeds are removed from original file. 3 | * @example 4 | * 5 | * ```bash 6 | * node tooling/extract-seed.js curriculum/locales/english/learn-x-by-building-y.md 7 | * ``` 8 | */ 9 | 10 | import { copyFile, readFile, rm, writeFile } from 'fs/promises'; 11 | import { Logger } from 'logover'; 12 | import { freeCodeCampConfig } from '@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/env.js'; 13 | import { 14 | getLessonFromFile, 15 | getLessonSeed, 16 | getProjectTitle 17 | } from '@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/parser.js'; 18 | import { constants } from 'fs'; 19 | 20 | const CONFIG_PATH = freeCodeCampConfig.config['projects.json']; 21 | 22 | const END_MARKER = '## --fcc-end--'; 23 | const SEED_MARKER = '### --seed--'; 24 | 25 | const path = process.argv[2]; 26 | const noBackup = process.argv[3] === '--nobackup'; 27 | 28 | const logover = new Logger({ level: 'debug' }); 29 | 30 | async function main(filePath, noBackup = false) { 31 | const { projectTopic, currentProject } = await getProjectTitle(filePath); 32 | const projectsConfig = JSON.parse(await readFile(CONFIG_PATH, 'utf8')); 33 | const projectConfig = projectsConfig.find( 34 | ({ title }) => title === currentProject 35 | ); 36 | if (!projectConfig) { 37 | throw new Error( 38 | `No project in ${CONFIG_PATH} associated with "${filePath}".` 39 | ); 40 | } 41 | const seedFile = filePath.replace('.md', '-seed.md'); 42 | try { 43 | // If file with seed already exists, seed from it will be mangled 44 | // with seed included in project file. 45 | await rm(seedFile); 46 | } catch (err) { 47 | if (err?.code !== 'ENOENT') { 48 | throw new Error(err); 49 | } 50 | } 51 | 52 | const header = `# ${projectTopic} - ${currentProject}\n`; 53 | const seedContents = [header]; 54 | const projectWithoutSeed = [header]; 55 | 56 | let lessonNumber = 1; 57 | try { 58 | while (lessonNumber <= projectConfig.numberOfLessons) { 59 | let lesson = await getLessonFromFile(filePath, lessonNumber); 60 | const seed = getLessonSeed(lesson); 61 | if (seed) { 62 | seedContents.push(`## ${lessonNumber}\n\n${SEED_MARKER}`); 63 | seedContents.push(`${seed.trimEnd('\n')}\n`); 64 | } 65 | const lessonWithoutSeed = lesson.replace( 66 | new RegExp(`${SEED_MARKER}\n*${seed}`), 67 | '' 68 | ); 69 | projectWithoutSeed.push(`## ${lessonNumber}\n`); 70 | projectWithoutSeed.push(`${lessonWithoutSeed.trimEnd('\n')}\n`); 71 | lessonNumber++; 72 | } 73 | } catch (err) { 74 | logover.error(err); 75 | } 76 | seedContents.push(`${END_MARKER}\n`); 77 | projectWithoutSeed.push(`${END_MARKER}\n`); 78 | 79 | if (!noBackup) { 80 | const backupFile = filePath.replace('.md', '.original'); 81 | try { 82 | await copyFile(filePath, backupFile, constants.COPYFILE_EXCL); 83 | } catch (err) { 84 | logover.error(err); 85 | throw new Error(`Backup file already created at ${backupFile}`); 86 | } 87 | } 88 | 89 | try { 90 | await writeFile(seedFile, seedContents.join('\n')); 91 | } catch (err) { 92 | logover.error(err); 93 | } 94 | 95 | try { 96 | await writeFile(filePath, projectWithoutSeed.join('\n')); 97 | } catch (err) { 98 | logover.error(err); 99 | } 100 | } 101 | 102 | if (path) { 103 | try { 104 | main(path, noBackup); 105 | } catch (err) { 106 | logover.debug(err); 107 | } 108 | } else { 109 | logover.info( 110 | `Usage: node tooling/extract-seed.js path/to/curriculum/markdown/file/learn.md [--nobackup]` 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/templates/project.css: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 750px; 3 | margin: auto; 4 | padding: 20px; 5 | } 6 | 7 | .heading > button { 8 | background-color: transparent; 9 | color: var(--light-1); 10 | font-size: 1.35rem; 11 | border: none; 12 | min-height: 50px; 13 | -webkit-text-stroke: medium; 14 | } 15 | 16 | .heading > button:hover { 17 | background-color: var(--dark-4); 18 | cursor: pointer; 19 | } 20 | 21 | #project-heading { 22 | font-size: 1.5rem; 23 | width: 100%; 24 | } 25 | 26 | #lesson-number { 27 | border-radius: 50%; 28 | width: 2rem; 29 | } 30 | 31 | .fade-in { 32 | animation-duration: 1s; 33 | animation-name: fade-in; 34 | } 35 | 36 | @keyframes fade-in { 37 | 0% { 38 | opacity: 0; 39 | color: var(--dark-blue); 40 | } 41 | 100% { 42 | opacity: 100; 43 | color: var(--light-2); 44 | } 45 | } 46 | 47 | .project-controls { 48 | display: flex; 49 | width: 100%; 50 | margin: 40 0; 51 | justify-content: space-between; 52 | } 53 | .project-controls > button { 54 | width: 48%; 55 | height: 100%; 56 | background-color: var(--dark-2); 57 | border: 1px solid var(--light-2); 58 | font-size: 1.2rem; 59 | font-weight: bold; 60 | color: var(--light-2); 61 | cursor: pointer; 62 | padding: 0.5rem; 63 | } 64 | 65 | .project-controls > button:hover { 66 | background-color: var(--dark-4); 67 | } 68 | 69 | button.secondary-cta { 70 | background-color: rgb(14, 116, 184); 71 | } 72 | 73 | button.secondary-cta:hover { 74 | background-color: rgb(0, 101, 169); 75 | } 76 | 77 | .project-output { 78 | overflow-x: hidden; 79 | } 80 | 81 | .project-output ul { 82 | padding-left: 0px; 83 | } 84 | .project-output > ul { 85 | display: flex; 86 | flex-direction: row; 87 | justify-content: left; 88 | width: 100%; 89 | align-items: center; 90 | margin-bottom: 0; 91 | border-bottom: 1px solid var(--dark-4); 92 | } 93 | .project-output > ul > li { 94 | list-style: none; 95 | } 96 | 97 | .project-output-content { 98 | background-color: black; 99 | color: white; 100 | /* width: 100%; */ 101 | height: fit-content; 102 | min-height: 100px; 103 | padding: 1rem; 104 | margin-top: 0; 105 | text-align: left; 106 | } 107 | 108 | nav { 109 | display: flex; 110 | justify-content: center; 111 | align-items: center; 112 | } 113 | 114 | #description { 115 | margin: 0 auto; 116 | width: 100%; 117 | text-align: left; 118 | } 119 | 120 | #description pre { 121 | background-color: var(--dark-1); 122 | padding: 1rem; 123 | } 124 | 125 | #description > p { 126 | line-height: 2.6ch; 127 | margin: 1.3em 0; 128 | } 129 | 130 | .project-output { 131 | overflow-x: hidden; 132 | } 133 | .project-output > ul { 134 | display: flex; 135 | flex-direction: row; 136 | justify-content: left; 137 | align-items: center; 138 | margin-bottom: 0; 139 | } 140 | .project-output > ul > li { 141 | list-style: none; 142 | } 143 | .output-btn { 144 | background-color: transparent; 145 | color: var(--light-1); 146 | margin-bottom: -1px; 147 | border: 1px solid transparent; 148 | padding: 10px 18px; 149 | } 150 | .output-btn:hover { 151 | background-color: var(--dark-1); 152 | cursor: pointer; 153 | } 154 | .output-btn:disabled { 155 | background-color: var(--dark-1); 156 | color: var(--light-2); 157 | cursor: default; 158 | border: 1px solid var(--dark-4); 159 | border-bottom-color: var(--dark-1); 160 | } 161 | 162 | .project-output-content { 163 | background-color: var(--dark-1); 164 | color: white; 165 | height: fit-content; 166 | min-height: 100px; 167 | margin-top: 0; 168 | text-align: left; 169 | overflow: auto; 170 | } 171 | 172 | details > summary > p { 173 | display: inline; 174 | } 175 | -------------------------------------------------------------------------------- /.freeCodeCamp/tooling/env.js: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from 'fs/promises'; 2 | import { join } from 'path'; 3 | import { logover } from './logger.js'; 4 | import { pluginEvents } from '../plugin/index.js'; 5 | 6 | export const ROOT = process.env.INIT_CWD || process.cwd(); 7 | 8 | export async function getConfig() { 9 | const config = await readFile(join(ROOT, 'freecodecamp.conf.json'), 'utf-8'); 10 | const conf = JSON.parse(config); 11 | const defaultConfig = { 12 | curriculum: { 13 | locales: { 14 | english: 'curriculum/locales/english' 15 | } 16 | }, 17 | config: { 18 | 'projects.json': 'config/projects.json', 19 | 'state.json': 'config/state.json' 20 | } 21 | }; 22 | return { ...defaultConfig, ...conf }; 23 | } 24 | 25 | export const freeCodeCampConfig = await getConfig(); 26 | 27 | export async function getState() { 28 | let defaultState = { 29 | currentProject: null, 30 | locale: 'english', 31 | lastSeed: { 32 | projectDashedName: null, 33 | // All lessons start at 0, but the logic for whether to seed a lesson 34 | // or not is based on the current lesson matching the last seeded lesson 35 | // So, to ensure the first lesson is seeded, this is -1 36 | lessonNumber: -1 37 | }, 38 | // All lessons start at 0, but the logic for whether to run certain effects 39 | // is based on the current lesson matching the last lesson 40 | lastWatchChange: -1 41 | }; 42 | try { 43 | const state = JSON.parse( 44 | await readFile( 45 | join(ROOT, freeCodeCampConfig.config['state.json']), 46 | 'utf-8' 47 | ) 48 | ); 49 | return { ...defaultState, ...state }; 50 | } catch (err) { 51 | logover.error(err); 52 | } 53 | return defaultState; 54 | } 55 | 56 | export async function setState(obj) { 57 | const state = await getState(); 58 | const updatedState = { 59 | ...state, 60 | ...obj 61 | }; 62 | 63 | await writeFile( 64 | join(ROOT, freeCodeCampConfig.config['state.json']), 65 | JSON.stringify(updatedState, null, 2) 66 | ); 67 | } 68 | 69 | /** 70 | * @param {string} projectDashedName Project dashed name 71 | */ 72 | export async function getProjectConfig(projectDashedName) { 73 | const projects = JSON.parse( 74 | await readFile( 75 | join(ROOT, freeCodeCampConfig.config['projects.json']), 76 | 'utf-8' 77 | ) 78 | ); 79 | 80 | const project = projects.find(p => p.dashedName === projectDashedName); 81 | 82 | // Add title and description to project 83 | const { title, description } = await pluginEvents.getProjectMeta( 84 | projectDashedName 85 | ); 86 | project.title = title; 87 | project.description = description; 88 | 89 | const defaultConfig = { 90 | testPollingRate: 333, 91 | currentLesson: 0, 92 | runTestsOnWatch: false, 93 | lastKnownLessonWithHash: 0, 94 | seedEveryLesson: false, 95 | blockingTests: false, 96 | breakOnFailure: false, 97 | useGitBuildOnProduction: false // TODO: Necessary? 98 | }; 99 | if (!project) { 100 | return defaultConfig; 101 | } 102 | return { ...defaultConfig, ...project }; 103 | } 104 | 105 | /** 106 | * 107 | * @param {string} projectDashedName Project dashed name 108 | * @param {object} config Config properties to set 109 | */ 110 | export async function setProjectConfig(projectDashedName, config = {}) { 111 | const projects = JSON.parse( 112 | await readFile( 113 | join(ROOT, freeCodeCampConfig.config['projects.json']), 114 | 'utf-8' 115 | ) 116 | ); 117 | 118 | const updatedProject = { 119 | ...projects.find(p => p.dashedName === projectDashedName), 120 | ...config 121 | }; 122 | 123 | const updatedProjects = projects.map(p => 124 | p.dashedName === projectDashedName ? updatedProject : p 125 | ); 126 | 127 | await writeFile( 128 | join(ROOT, freeCodeCampConfig.config['projects.json']), 129 | JSON.stringify(updatedProjects, null, 2) 130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /.freeCodeCamp/tooling/utils.js: -------------------------------------------------------------------------------- 1 | import { cp, readdir, rm, rmdir, writeFile, readFile } from 'fs/promises'; 2 | import path, { dirname, join } from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | import { promisify } from 'util'; 5 | import { exec } from 'child_process'; 6 | import { readdirSync } from 'fs'; 7 | import { ROOT } from './env.js'; 8 | const execute = promisify(exec); 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = dirname(__filename); 12 | 13 | // Adds all existing paths at runtime 14 | const PERMANENT_PATHS_IN_ROOT = readdirSync('..'); 15 | 16 | /** 17 | * Alter the `.vscode/settings.json` file properties 18 | * @param {object} obj Object Literal to set/overwrite properties 19 | */ 20 | export async function setVSCSettings(obj) { 21 | const pathToSettings = join(ROOT, '.vscode', 'settings.json'); 22 | const settings = await getVSCSettings(); 23 | const updated = { 24 | ...settings, 25 | ...obj 26 | }; 27 | await writeFile(pathToSettings, JSON.stringify(updated, null, 2)); 28 | } 29 | 30 | /** 31 | * Get the `.vscode/settings.json` file properties 32 | * @returns The contents of the `.vscode/settings.json` file 33 | */ 34 | export async function getVSCSettings() { 35 | const pathToSettings = join(ROOT, '.vscode', 'settings.json'); 36 | return JSON.parse(await readFile(pathToSettings, 'utf8')); 37 | } 38 | 39 | /** 40 | * Toggle `[file]: true` to `.vscode/settings.json` file 41 | * @param {string} file Filename of file to hide in VSCode settings 42 | */ 43 | export async function hideFile(file) { 44 | // Get `files.exclude` 45 | const filesExclude = (await getVSCSettings())['files.exclude']; 46 | filesExclude[file] = true; 47 | await setVSCSettings({ 'files.exclude': filesExclude }); 48 | } 49 | 50 | /** 51 | * Toggle `[file]: false` to `.vscode/settings.json` file 52 | * @param {string} file Filename of file to show in VSCode settings 53 | */ 54 | export async function showFile(file) { 55 | // Get `files.exclude` 56 | const filesExclude = (await getVSCSettings())['files.exclude']; 57 | filesExclude[file] = false; 58 | await setVSCSettings({ 'files.exclude': filesExclude }); 59 | } 60 | 61 | /** 62 | * Hide all files in the `files.exclude` property of the `.vscode/settings.json` file 63 | */ 64 | export async function hideAll() { 65 | const filesExclude = (await getVSCSettings())['files.exclude']; 66 | for (const file of Object.keys(filesExclude)) { 67 | filesExclude[file] = true; 68 | } 69 | await setVSCSettings({ 'files.exclude': filesExclude }); 70 | } 71 | 72 | /** 73 | * Show all files in the `files.exclude` property of the `.vscode/settings.json` file 74 | */ 75 | export async function showAll() { 76 | const filesExclude = (await getVSCSettings())['files.exclude']; 77 | for (const file of Object.keys(filesExclude)) { 78 | filesExclude[file] = false; 79 | } 80 | await setVSCSettings({ 'files.exclude': filesExclude }); 81 | } 82 | 83 | /** 84 | * Copies all paths in the given `project.dashedName` directory to the root directory 85 | * @param {object} project Project to reset 86 | */ 87 | export async function dumpProjectDirectoryIntoRoot(project) { 88 | await cp(join(ROOT, project.dashedName), ROOT, { 89 | recursive: true 90 | }); 91 | } 92 | 93 | /** 94 | * Removes non-boilerplate paths from the root, and copies them to the project directory 95 | * @param {object} projectToCopyTo Project to copy to 96 | */ 97 | export async function cleanWorkingDirectory(projectToCopyTo) { 98 | if (projectToCopyTo) { 99 | await copyNonWDirToProject(projectToCopyTo); 100 | } 101 | const allOtherPaths = (await readdir(ROOT)).filter( 102 | p => !PERMANENT_PATHS_IN_ROOT.includes(p) 103 | ); 104 | allOtherPaths.forEach(async p => { 105 | await rm(join(ROOT, p), { recursive: true }); 106 | }); 107 | } 108 | 109 | /** 110 | * Copies all non-boilerplate paths from the root to the project directory 111 | * @param {object} project Project to copy to 112 | */ 113 | async function copyNonWDirToProject(project) { 114 | const allOtherPaths = (await readdir(ROOT)).filter( 115 | p => !PERMANENT_PATHS_IN_ROOT.includes(p) 116 | ); 117 | allOtherPaths.forEach(async p => { 118 | const relativePath = join(ROOT, p); 119 | await cp(relativePath, join(ROOT, project, p), { 120 | recursive: true, 121 | force: true 122 | }); 123 | }); 124 | } 125 | -------------------------------------------------------------------------------- /.freeCodeCamp/tests/parser.test.js: -------------------------------------------------------------------------------- 1 | /// Tests can be run from `self/` 2 | /// node ../.freeCodeCamp/tests/parser.test.js 3 | import { assert } from 'chai'; 4 | import { Logger } from 'logover'; 5 | import { pluginEvents } from '../plugin/index.js'; 6 | 7 | const logover = new Logger({ 8 | debug: '\x1b[33m[parser.test]\x1b[0m', 9 | error: '\x1b[31m[parser.test]\x1b[0m', 10 | level: 'debug', 11 | timestamp: null 12 | }); 13 | 14 | try { 15 | const { 16 | title, 17 | description: projectDescription, 18 | numberOfLessons 19 | } = await pluginEvents.getProjectMeta('build-x-using-y'); 20 | const { 21 | meta, 22 | description: lessonDescription, 23 | tests, 24 | hints, 25 | seed, 26 | isForce, 27 | beforeAll, 28 | beforeEach, 29 | afterAll, 30 | afterEach 31 | } = await pluginEvents.getLesson('build-x-using-y', 0); 32 | 33 | assert.deepEqual(title, 'Build X Using Y'); 34 | assert.deepEqual(meta, { 35 | watch: ['some/file.js'], 36 | ignore: ['another/file.js'] 37 | }); 38 | assert.deepEqual( 39 | projectDescription, 40 | '

    In this course, you will build x using y.

    ' 41 | ); 42 | assert.deepEqual(numberOfLessons, 1); 43 | 44 | assert.deepEqual( 45 | lessonDescription, 46 | `

    Some description here.

    47 |
    fn main() {
     48 |     println!("Hello, world!");
     49 | }
     50 | 

    Here is an image:

    51 | ` 52 | ); 53 | 54 | const expectedTests = [ 55 | [ 56 | '

    First test using Chai.js assert.

    ', 57 | '// 0\n// Timeout for 3 seconds\nawait new Promise(resolve => setTimeout(resolve, 3000));\nassert.equal(true, true);' 58 | ], 59 | [ 60 | '

    Second test using global variables passed from before hook.

    ', 61 | "// 1\nawait new Promise(resolve => setTimeout(resolve, 4000));\nassert.equal(__projectLoc, 'example global variable for tests');" 62 | ], 63 | [ 64 | '

    Dynamic helpers should be imported.

    ', 65 | "// 2\nawait new Promise(resolve => setTimeout(resolve, 1000));\nassert.equal(__helpers.testDynamicHelper(), 'Helper success!');" 66 | ] 67 | ]; 68 | 69 | for (const [i, [testText, testCode]] of tests.entries()) { 70 | assert.deepEqual(testText, expectedTests[i][0]); 71 | assert.deepEqual(testCode, expectedTests[i][1]); 72 | } 73 | 74 | const expectedHints = [ 75 | '

    Inline hint with some code blocks.

    ', 76 | `

    Multi-line hint with:

    77 |
    const code_block = true;
     78 | 
    ` 79 | ]; 80 | 81 | for (const [i, hint] of hints.entries()) { 82 | assert.deepEqual(hint, expectedHints[i]); 83 | } 84 | 85 | const expectedSeed = [ 86 | { 87 | filePath: 'build-x-using-y/readme.md', 88 | fileSeed: '# Build X Using Y\n\nIn this course\n\n## 0\n\nHello' 89 | }, 90 | 'npm install' 91 | ]; 92 | 93 | let i = 0; 94 | for (const s of seed) { 95 | assert.deepEqual(s, expectedSeed[i]); 96 | i++; 97 | } 98 | assert.deepEqual(i, 2); 99 | 100 | assert.deepEqual(isForce, true); 101 | 102 | assert.deepEqual( 103 | beforeEach, 104 | "await new Promise(resolve => setTimeout(resolve, 1000));\nconst __projectLoc = 'example global variable for tests';" 105 | ); 106 | assert.deepEqual( 107 | afterEach, 108 | "await new Promise(resolve => setTimeout(resolve, 1000));\nlogover.info('after each');" 109 | ); 110 | assert.deepEqual( 111 | beforeAll, 112 | "await new Promise(resolve => setTimeout(resolve, 1000));\nlogover.info('before all');" 113 | ); 114 | assert.deepEqual( 115 | afterAll, 116 | "await new Promise(resolve => setTimeout(resolve, 1000));\nlogover.info('after all');" 117 | ); 118 | } catch (e) { 119 | throw logover.error(e); 120 | } 121 | 122 | logover.debug('All tests passed! 🎉'); 123 | -------------------------------------------------------------------------------- /.freeCodeCamp/tooling/client-socks.js: -------------------------------------------------------------------------------- 1 | import { parseMarkdown } from './parser.js'; 2 | 3 | export function updateLoader(ws, loader) { 4 | ws.send(parse({ event: 'update-loader', data: { loader } })); 5 | } 6 | 7 | /** 8 | * Update all tests in the tests state 9 | * @param {WebSocket} ws WebSocket connection to the client 10 | * @param {Test[]} tests Array of Test objects 11 | */ 12 | export function updateTests(ws, tests) { 13 | ws.send(parse({ event: 'update-tests', data: { tests } })); 14 | } 15 | /** 16 | * Update single test in the tests state 17 | * @param {WebSocket} ws WebSocket connection to the client 18 | * @param {Test} test Test object 19 | */ 20 | export function updateTest(ws, test) { 21 | ws.send(parse({ event: 'update-test', data: { test } })); 22 | } 23 | /** 24 | * Update the lesson description 25 | * @param {WebSocket} ws WebSocket connection to the client 26 | * @param {string} description Lesson description 27 | */ 28 | export function updateDescription(ws, description) { 29 | ws.send( 30 | parse({ 31 | event: 'update-description', 32 | data: { description } 33 | }) 34 | ); 35 | } 36 | /** 37 | * Update the heading of the lesson 38 | * @param {WebSocket} ws WebSocket connection to the client 39 | * @param {{lessonNumber: number; title: string;}} projectHeading Project heading 40 | */ 41 | export function updateProjectHeading(ws, projectHeading) { 42 | ws.send( 43 | parse({ 44 | event: 'update-project-heading', 45 | data: projectHeading 46 | }) 47 | ); 48 | } 49 | /** 50 | * Update the project state 51 | * @param {WebSocket} ws WebSocket connection to the client 52 | * @param {Project} project Project object 53 | */ 54 | export function updateProject(ws, project) { 55 | ws.send( 56 | parse({ 57 | event: 'update-project', 58 | data: project 59 | }) 60 | ); 61 | } 62 | /** 63 | * Update the projects state 64 | * @param {WebSocket} ws WebSocket connection to the client 65 | * @param {Project[]} projects Array of Project objects 66 | */ 67 | export function updateProjects(ws, projects) { 68 | ws.send( 69 | parse({ 70 | event: 'update-projects', 71 | data: projects 72 | }) 73 | ); 74 | } 75 | /** 76 | * Update the projects state 77 | * @param {WebSocket} ws WebSocket connection to the client 78 | * @param {any} config config object 79 | */ 80 | export function updateFreeCodeCampConfig(ws, config) { 81 | ws.send( 82 | parse({ 83 | event: 'update-freeCodeCamp-config', 84 | data: config 85 | }) 86 | ); 87 | } 88 | /** 89 | * Update hints 90 | * @param {WebSocket} ws WebSocket connection to the client 91 | * @param {string[]} hints Markdown strings 92 | */ 93 | export function updateHints(ws, hints) { 94 | ws.send(parse({ event: 'update-hints', data: { hints } })); 95 | } 96 | /** 97 | * 98 | * @param {WebSocket} ws WebSocket connection to the client 99 | * @param {{error: string; testText: string; passed: boolean;isLoading: boolean;testId: number;}} cons 100 | */ 101 | export function updateConsole(ws, cons) { 102 | if (Object.keys(cons).length) { 103 | if (cons.error) { 104 | const error = `\`\`\`json\n${JSON.stringify( 105 | cons.error, 106 | null, 107 | 2 108 | )}\n\`\`\``; 109 | cons.error = parseMarkdown(error); 110 | } 111 | } 112 | ws.send(parse({ event: 'update-console', data: { cons } })); 113 | } 114 | 115 | /** 116 | * Update error 117 | * @param {WebSocket} ws WebSocket connection to the client 118 | * @param {Error} error Error object 119 | */ 120 | export function updateError(ws, error) { 121 | ws.send(parse({ event: 'update-error', data: { error } })); 122 | } 123 | 124 | /** 125 | * Update the current locale 126 | * @param {WebSocket} ws WebSocket connection to the client 127 | * @param {string} locale Locale string 128 | */ 129 | export function updateLocale(ws, locale) { 130 | ws.send(parse({ event: 'update-locale', data: locale })); 131 | } 132 | 133 | /** 134 | * Handles the case when a project is finished 135 | * @param {WebSocket} ws WebSocket connection to the client 136 | */ 137 | export function handleProjectFinish(ws) { 138 | ws.send(parse({ event: 'handle-project-finish' })); 139 | } 140 | 141 | export function parse(obj) { 142 | return JSON.stringify(obj); 143 | } 144 | 145 | /** 146 | * Resets the bottom panel (Tests, Console, Hints) of the client to empty state 147 | * @param {WebSocket} ws WebSocket connection to the client 148 | */ 149 | export function resetBottomPanel(ws) { 150 | updateHints(ws, []); 151 | updateTests(ws, []); 152 | updateConsole(ws, {}); 153 | } 154 | -------------------------------------------------------------------------------- /docs/src/plugin-system.md: -------------------------------------------------------------------------------- 1 | # Plugin System 2 | 3 | The plugin system is a way to _hook_ into events during the runtime of the application. 4 | 5 | Plugins are defined within the JS file specified by the [`tooling.plugins`](./configuration.md#tooling) configuration option. 6 | 7 | ## Hooks 8 | 9 | ### `onTestsStart` 10 | 11 | Called when the tests start running, before any `--before-` hooks. 12 | 13 | ### `onTestsEnd` 14 | 15 | Called when the tests finish running, after all `--after-` hooks. 16 | 17 | ### `onProjectStart` 18 | 19 | Called when the project first loads, before any tests are run, and only happens once per project. 20 | 21 | ### `onProjectFinished` 22 | 23 | Called when the project is finished, after all tests are run **and** passed, and only happens once per project. 24 | 25 | ### `onLessonPassed` 26 | 27 | Called when a lesson passes, after all tests are run **and** passed, and only happens once per lesson. 28 | 29 | ### `onLessonFailed` 30 | 31 | Called when a lesson fails, after all tests are run **and** any fail. 32 | 33 | ### `onLessonLoad` 34 | 35 | Called once when a lesson is loaded, after the `onProjectStart` if the first lesson. 36 | 37 | ## Parser 38 | 39 | It is possible to define a custom parser for the curriculum files. This is useful when the curriculum files are not in the default format described in the [project syntax](./project-syntax.md) section. 40 | 41 | The first parameter of the parser functions is the project dashed name. This is the same as the `dashedName` field in the `projects.json` file. 42 | 43 | It is up to the parser to read, parse, and return the data in the format expected by the application. 44 | 45 | ### `getProjectMeta` 46 | 47 | ```ts 48 | (projectDashedName: string) => 49 | Promise<{ 50 | title: string; 51 | description: string; 52 | numberOfLessons: number; 53 | tags: string[]; 54 | }>; 55 | ``` 56 | 57 | The `title`, `tags`, and `description` fields are expected to be either plain strings, or HTML strings which are then rendered in the client. 58 | 59 | ### `getLesson` 60 | 61 | ```admonish attention 62 | This function can be called multiple times per lesson. Therefore, it is expected to be idempotent. 63 | ``` 64 | 65 | ```typescript 66 | (projectDashedName: string, lessonNumber: number) => 67 | Promise<{ 68 | meta?: { watch?: string[]; ignore?: string[] }; 69 | description: string; 70 | tests: [[string, string]]; 71 | hints: string[]; 72 | seed: [{ filePath: string; fileSeed: string } | string]; 73 | isForce?: boolean; 74 | beforeAll?: string; 75 | afterAll?: string; 76 | beforeEach?: string; 77 | afterEach?: string; 78 | }>; 79 | ``` 80 | 81 | The `meta` field is expected to be an object with either a `watch` or `ignore` field. The `watch` field is expected to be an array of strings, and the `ignore` field is expected to be an array of strings. 82 | 83 | The `description` field is expected to be either a plain string, or an HTML string which is then rendered in the client. 84 | 85 | The `tests[][0]` field is the test text, and the `tests[][1]` field is the test code. The test text is expected to be either a plain string, or an HTML string. 86 | 87 | The `hints` field is expected to be an array of plain strings, or an array of HTML strings. 88 | 89 | The `seed[].filePath` field is the relative path to the file from the workspace root. The `seed[].fileSeed` field is the file content to be written to the file. 90 | 91 | The `seed[]` field can also be a plain string, which is then treated as a `bash` command to be run in the workspace root. 92 | 93 | An example of this can be seen in the default parser used: 94 | 95 | ## Example 96 | 97 | ````admonish example title=" " 98 | ```js 99 | import { pluginEvents } from "@freecodecamp/freecodecamp-os/.freeCodeCamp/plugin/index.js"; 100 | 101 | pluginEvents.onTestsStart = async (project, testsState) => { 102 | console.log('onTestsStart'); 103 | }; 104 | 105 | pluginEvents.onTestsEnd = async (project, testsState) => { 106 | console.log('onTestsEnd'); 107 | }; 108 | 109 | pluginEvents.onProjectStart = async project => { 110 | console.log('onProjectStart'); 111 | }; 112 | 113 | pluginEvents.onProjectFinished = async project => { 114 | console.log('onProjectFinished'); 115 | }; 116 | 117 | pluginEvents.onLessonFailed = async project => { 118 | console.log('onLessonFailed'); 119 | }; 120 | 121 | pluginEvents.onLessonPassed = async project => { 122 | console.log('onLessonPassed'); 123 | }; 124 | ``` 125 | ```` 126 | -------------------------------------------------------------------------------- /docs/src/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Creating a New Course 4 | 5 | Create a new project directory and install `@freecodecamp/freecodecamp-os`: 6 | 7 | ```bash 8 | mkdir 9 | cd 10 | npm init -y 11 | npm install @freecodecamp/freecodecamp-os 12 | ``` 13 | 14 | ```admonish info title=" " 15 | Feel free to replace `npm` with another package manager of your choice. 16 | ``` 17 | 18 | ## Configuring Your Course 19 | 20 | Create a `freecodecamp.conf.json` file in the project root: 21 | 22 | ```bash 23 | touch freecodecamp.conf.json 24 | ``` 25 | 26 | Add the following required configuration: 27 | 28 | ```json 29 | { 30 | "version": "0.0.1", 31 | "config": { 32 | "projects.json": "", 33 | "state.json": "" 34 | }, 35 | "curriculum": { 36 | "locales": { 37 | "": "" 38 | } 39 | } 40 | } 41 | ``` 42 | 43 | ````admonish example collapsible=true 44 | ```json 45 | { 46 | "version": "0.0.1", 47 | "config": { 48 | "projects.json": "./config/projects.json", 49 | "state.json": "./config/state.json" 50 | }, 51 | "curriculum": { 52 | "locales": { 53 | "english": "./curriculum/locales/english" 54 | } 55 | } 56 | } 57 | ``` 58 | ```` 59 | 60 | ```admonish info 61 | There are many more configuration options available. See the [configuration](./configuration.md) page for more details. 62 | ``` 63 | 64 | Create the `projects.json` file: 65 | 66 | ```json 67 | [ 68 | { 69 | "id": 0, 70 | "dashedName": "" 71 | } 72 | ] 73 | ``` 74 | 75 | ```admonish info 76 | There are many more configuration options available. See the [configuration](./configuration.md) page for more details. 77 | ``` 78 | 79 | Create the `state.json` file: 80 | 81 | ```json 82 | {} 83 | ``` 84 | 85 | Initialise this file with the initial state of the course. If you want the course to start on a project (instead of the landing page), replace `null` with the `dashedName` of the project. 86 | 87 | Create the curricula files: 88 | 89 | ```bash 90 | mkdir 91 | touch /.md 92 | ``` 93 | 94 | ````admonish example 95 | ```bash 96 | mkdir curriculum/locales/english 97 | touch curriculum/locales/english/learn-x-by-building-y.md 98 | ``` 99 | ```` 100 | 101 | Add the Markdown content to the curricula files. See the [project syntax](./project-syntax.md) page for more details. 102 | 103 | Create the project boilerplate/working directory in the root: 104 | 105 | ```bash 106 | mkdir 107 | ``` 108 | 109 | ````admonish example 110 | ```bash 111 | mkdir learn-x-by-building-y 112 | ``` 113 | ```` 114 | 115 | ````admonish attention title="Required Files" 116 | ```txt 117 | / 118 | ├── freecodecamp.conf.json 119 | ├── 120 | ├── 121 | └── / 122 | └── .md 123 | ``` 124 | If using the `terminal` feature: 125 | ```txt 126 | ├── / 127 | │ ├── 128 | │ └── 129 | ├── .logs/ 130 | │ ├── .bash_history.log 131 | │ ├── .cwd.log 132 | │ ├── .history_cwd.log 133 | │ ├── .next_command.log 134 | │ ├── .temp.log 135 | │ └── .terminal_out.log 136 | ``` 137 | If using the `tooling` feature: 138 | ```txt 139 | ├── 140 | ``` 141 | ```` 142 | 143 | Create a `.vscode/settings.json` file to configure the freeCodeCamp - Courses extension: 144 | 145 | ```json 146 | { 147 | // Open the course when the workspace is opened 148 | "freecodecamp-courses.autoStart": true, 149 | // Automatically adjust the terminal logs if used 150 | "freecodecamp-courses.prepare": "sed -i \"s#WD=.*#WD=$(pwd)#g\" ./bash/.bashrc", 151 | // Command run in terminal on `freeCodeCamp: Develop Course` 152 | "freecodecamp-courses.scripts.develop-course": "NODE_ENV=development npm run start", 153 | // Command run in terminal on `freeCodeCamp: Run Course` 154 | "freecodecamp-courses.scripts.run-course": "NODE_ENV=production npm run start", 155 | // Preview to open when course starts 156 | "freecodecamp-courses.workspace.previews": [ 157 | { 158 | "open": true, 159 | "url": "http://localhost:8080", 160 | "showLoader": true, 161 | "timeout": 4000 162 | } 163 | ], 164 | // The below settings are needed for using the terminal feature 165 | "terminal.integrated.defaultProfile.linux": "bash", 166 | "terminal.integrated.profiles.linux": { 167 | "bash": { 168 | "path": "bash", 169 | "icon": "terminal-bash", 170 | "args": ["--init-file", "./bash/sourcerer.sh"] 171 | } 172 | } 173 | } 174 | ``` 175 | 176 | A few more settings are available, and can be seen and configured from the VSCode Settings UI. 177 | -------------------------------------------------------------------------------- /.freeCodeCamp/tooling/test-utils.js: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs/promises'; 2 | import { exec } from 'child_process'; 3 | import { promisify } from 'util'; 4 | import { join } from 'path'; 5 | import { ROOT } from './env.js'; 6 | import { logover } from './logger.js'; 7 | 8 | // --------------- 9 | // GENERIC HELPERS 10 | // --------------- 11 | export const PATH_TERMINAL_OUT = join(ROOT, '.logs/.terminal_out.log'); 12 | export const PATH_BASH_HISTORY = join(ROOT, '.logs/.bash_history.log'); 13 | export const PATH_CWD = join(ROOT, '.logs/.cwd.log'); 14 | export const PATH_TEMP = join(ROOT, '.logs/.temp.log'); 15 | 16 | /** 17 | * @typedef ControlWrapperOptions 18 | * @type {object} 19 | * @property {number} timeout 20 | * @property {number} stepSize 21 | */ 22 | 23 | /** 24 | * Wraps a function in an interval to retry until it succeeds 25 | * @param {callback} cb Callback to wrap 26 | * @param {ControlWrapperOptions} options Options to pass to `ControlWrapper` 27 | * @returns {Promise} Returns the result of the callback or `null` 28 | */ 29 | async function controlWrapper(cb, { timeout = 10000, stepSize = 250 }) { 30 | return new Promise((resolve, reject) => { 31 | const interval = setInterval(async () => { 32 | try { 33 | const response = await cb(); 34 | resolve(response); 35 | } catch (e) { 36 | logover.debug(e); 37 | } 38 | }, stepSize); 39 | setTimeout(() => { 40 | clearInterval(interval); 41 | reject(null); 42 | }, timeout); 43 | }); 44 | } 45 | 46 | /** 47 | * Get the `.logs/.bash_history.log` file contents 48 | * @returns {Promise} 49 | */ 50 | async function getBashHistory() { 51 | const bashHistory = await readFile(PATH_BASH_HISTORY, { 52 | encoding: 'utf8', 53 | flag: 'a+' 54 | }); 55 | return bashHistory; 56 | } 57 | 58 | const execute = promisify(exec); 59 | /** 60 | * Returns the output of a command called from a given path 61 | * @param {string} command 62 | * @param {string} path Path relative to root of working directory 63 | * @returns {Promise<{stdout, stderr}>} 64 | */ 65 | async function getCommandOutput(command, path = '') { 66 | const cmdOut = await execute(command, { 67 | cwd: join(ROOT, path), 68 | shell: '/bin/bash' 69 | }); 70 | return cmdOut; 71 | } 72 | 73 | /** 74 | * Get the `.logs/.cwd.log` file contents 75 | * @returns {Promise} 76 | */ 77 | async function getCWD() { 78 | const cwd = await readFile(PATH_CWD, { 79 | encoding: 'utf8', 80 | flag: 'a+' 81 | }); 82 | return cwd; 83 | } 84 | 85 | /** 86 | * Get the `.logs/.bash_history.log` file contents, or `throw` is not found 87 | * @param {number} howManyBack The `nth` log from the history 88 | * @returns {Promise} 89 | */ 90 | async function getLastCommand(howManyBack = 0) { 91 | const bashLogs = await getBashHistory(); 92 | 93 | const logs = bashLogs.split('\n').filter(l => l !== ''); 94 | const lastLog = logs[logs.length - howManyBack - 1]; 95 | 96 | return lastLog; 97 | } 98 | 99 | /** 100 | * Get the `.logs/.cwd.log` file contents, or `throw` is not found 101 | * @param {number} howManyBack The `nth` log from the current working directory history 102 | * @returns {Promise} 103 | */ 104 | async function getLastCWD(howManyBack = 0) { 105 | const currentWorkingDirectory = await getCWD(); 106 | 107 | const logs = currentWorkingDirectory.split('\n').filter(l => l !== ''); 108 | const lastLog = logs[logs.length - howManyBack - 1]; 109 | 110 | return lastLog; 111 | } 112 | 113 | /** 114 | * Get the `.logs/.temp.log` file contents, or `throw` if not found 115 | * @returns {Promise} The `.temp.log` file contents 116 | */ 117 | async function getTemp() { 118 | const tempLogs = await readFile(PATH_TEMP, { 119 | encoding: 'utf8', 120 | flag: 'a+' 121 | }); 122 | return tempLogs; 123 | } 124 | 125 | /** 126 | * Get the `.logs/.terminal_out.log` file contents, or `throw` if not found 127 | * @returns {Promise} The `.terminal_out.log` file contents 128 | */ 129 | async function getTerminalOutput() { 130 | const terminalLogs = await readFile(PATH_TERMINAL_OUT, { 131 | encoding: 'utf8', 132 | flag: 'a+' 133 | }); 134 | return terminalLogs; 135 | } 136 | 137 | /** 138 | * Imports a module side-stepping Nodejs' cache 139 | * @param {string} path Path to file/module to import 140 | * @returns {Promise>} 141 | */ 142 | async function importSansCache(path) { 143 | const cacheBustingModulePath = `${join(ROOT, path)}?update=${Date.now()}`; 144 | return await import(cacheBustingModulePath); 145 | } 146 | 147 | const __helpers = { 148 | controlWrapper, 149 | getBashHistory, 150 | getCommandOutput, 151 | getCWD, 152 | getLastCommand, 153 | getLastCWD, 154 | getTemp, 155 | getTerminalOutput, 156 | importSansCache 157 | }; 158 | 159 | export default __helpers; 160 | -------------------------------------------------------------------------------- /.freeCodeCamp/tooling/hot-reload.js: -------------------------------------------------------------------------------- 1 | // This file handles the watching of the /curriculum folder for changes 2 | // and executing the command to run the tests for the next (current) lesson 3 | import { getState, getProjectConfig, ROOT, freeCodeCampConfig } from './env.js'; 4 | import { runLesson } from './lesson.js'; 5 | import { runTests } from './tests/main.js'; 6 | import { watch } from 'chokidar'; 7 | import { logover } from './logger.js'; 8 | import path from 'path'; 9 | import { readdir } from 'fs/promises'; 10 | 11 | const defaultPathsToIgnore = [ 12 | '.logs/.temp.log', 13 | 'config/', 14 | '/node_modules/', 15 | '.git/', 16 | '/target/', 17 | '/test-ledger/' 18 | ]; 19 | 20 | export const pathsToIgnore = 21 | freeCodeCampConfig.hotReload?.ignore || defaultPathsToIgnore; 22 | 23 | export const watcher = watch(ROOT, { 24 | ignoreInitial: true, 25 | ignored: path => pathsToIgnore.some(p => path.includes(p)) 26 | }); 27 | 28 | export function hotReload(ws, pathsToIgnore = defaultPathsToIgnore) { 29 | logover.info(`Watching for file changes on ${ROOT}`); 30 | let isWait = false; 31 | let testsRunning = false; 32 | let isClearConsole = false; 33 | 34 | // hotReload is called on connection, which can happen mulitple times due to client reload/disconnect. 35 | // This ensures the following does not happen: 36 | // > MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 all listeners added to [FSWatcher]. 37 | watcher.removeAllListeners('all'); 38 | 39 | watcher.on('all', async (event, name) => { 40 | if (name && !pathsToIgnore.find(p => name.includes(p))) { 41 | if (isWait) return; 42 | const { currentProject } = await getState(); 43 | if (!currentProject) { 44 | return; 45 | } 46 | 47 | const { testPollingRate, runTestsOnWatch } = await getProjectConfig( 48 | currentProject 49 | ); 50 | isWait = setTimeout(() => { 51 | isWait = false; 52 | }, testPollingRate); 53 | 54 | if (isClearConsole) { 55 | console.clear(); 56 | } 57 | 58 | await runLesson(ws, currentProject); 59 | if (runTestsOnWatch && !testsRunning) { 60 | logover.debug(`Watcher: ${event} - ${name}`); 61 | testsRunning = true; 62 | await runTests(ws, currentProject); 63 | testsRunning = false; 64 | } 65 | } 66 | }); 67 | } 68 | 69 | /** 70 | * Stops the global `watcher` from watching the entire workspace. 71 | */ 72 | export function unwatchAll() { 73 | const watched = watcher.getWatched(); 74 | for (const [dir, files] of Object.entries(watched)) { 75 | for (const file of files) { 76 | watcher.unwatch(path.join(dir, file)); 77 | } 78 | } 79 | } 80 | 81 | // Need to handle 82 | // From ROOT, must add all directories before file/s 83 | // path.dirname... all the way to ROOT 84 | // path.isAbsolute to find out if what was passed into `meta` is absolute or relative 85 | // path.parse to get the dir and base 86 | // path.relative(ROOT, path) to get the relative path from ROOT 87 | // path.resolve directly on `meta`? 88 | /** 89 | * **Example:** 90 | * - Assuming ROOT is `/home/freeCodeCampOS/self` 91 | * - Takes `lesson-watcher/src/watched.js` 92 | * - Calls `watcher.add` on each of these in order: 93 | * - `/home/freeCodeCampOS/self` 94 | * - `/home/freeCodeCampOS/self/lesson-watcher` 95 | * - `/home/freeCodeCampOS/self/lesson-watcher/src` 96 | * - `/home/freeCodeCampOS/self/lesson-watcher/src/watched.js` 97 | * @param {string} pathRelativeToRoot 98 | */ 99 | export function watchPathRelativeToRoot(pathRelativeToRoot) { 100 | const paths = getAllPathsWithRoot(pathRelativeToRoot); 101 | for (const path of paths) { 102 | watcher.add(path); 103 | } 104 | } 105 | 106 | function getAllPathsWithRoot(pathRelativeToRoot) { 107 | const paths = []; 108 | let currentPath = pathRelativeToRoot; 109 | while (currentPath !== ROOT) { 110 | paths.push(currentPath); 111 | currentPath = path.dirname(currentPath); 112 | } 113 | paths.push(ROOT); 114 | // The order does not _seem_ to matter, but the theory says it should 115 | return paths.reverse(); 116 | } 117 | 118 | /** 119 | * Adds all folders and files to the `watcher` instance. 120 | * 121 | * Does nothing with the `pathsToIgnore`, because they are already ignored by the `watcher`. 122 | */ 123 | export async function watchAll() { 124 | await watchPath(ROOT); 125 | } 126 | 127 | async function watchPath(rootPath) { 128 | const paths = await readdir(rootPath, { withFileTypes: true }); 129 | for (const p of paths) { 130 | const fullPath = path.join(rootPath, p.name); 131 | // if (pathsToIgnore.find(i => fullPath.includes(i))) { 132 | // console.log('Ignoring: ', fullPath); 133 | // continue; 134 | // } 135 | watcher.add(fullPath); 136 | if (p.isDirectory()) { 137 | await watchPath(fullPath); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /docs/src/testing/test-utilities.md: -------------------------------------------------------------------------------- 1 | # Test Utilities 2 | 3 | The test utilities are exported/global objects available in the test runner. These are referred to as _"helpers"_, and the included helpers are exported from [https://github.com/freeCodeCamp/freeCodeCampOS/blob/main/.freeCodeCamp/tooling/test-utils.js](https://github.com/freeCodeCamp/freeCodeCampOS/blob/main/.freeCodeCamp/tooling/test-utils.js). 4 | 5 | Many of the exported functions are _convinience wrappers_ around Nodejs' `fs` and `child_process` modules. Specifically, they make use of the global `ROOT` variable to run the functions relative to the root of the workspace. 6 | 7 | ## `controlWrapper` 8 | 9 | Wraps a function in an interval to retry until it does not throw or times out. 10 | 11 | ```typescript 12 | function controlWrapper( 13 | cb: () => any, 14 | { timeout = 10_000, stepSize = 250 } 15 | ): Promise | null>; 16 | ``` 17 | 18 | ```admonish attention title="" 19 | The callback function must throw for the control wrapper to re-try. 20 | ``` 21 | 22 | ```javascript 23 | const cb = async () => { 24 | const flakyFetch = await fetch('http://localhost:3123'); 25 | return flakyFetch.json(); 26 | }; 27 | const result = await __helpers.controlWrapper(cb); 28 | ``` 29 | 30 | ## `getBashHistory` 31 | 32 | Get the `.logs/.bash_history.log` file contents 33 | 34 | ```admonish danger title="Safety" 35 | Throws if file does not exist, or if read permission is denied. 36 | ``` 37 | 38 | ```typescript 39 | function getBashHistory(): Promise; 40 | ``` 41 | 42 | ```javascript 43 | const bashHistory = await __helpers.getBashHistory(); 44 | ``` 45 | 46 | ## `getCommandOutput` 47 | 48 | Returns the output of a command called from the given path relative to the root of the workspace. 49 | 50 | ```admonish danger title="Safety" 51 | Throws if path is not a valid POSIX/DOS path, and if promisified `exec` throws. 52 | ``` 53 | 54 | ```typescript 55 | function getCommandOutput( 56 | command: string, 57 | path = '' 58 | ): Promise<{ stdout: string; stderr: string } | Error>; 59 | ``` 60 | 61 | ```javascript 62 | const { stdout, stderr } = await __helpers.getCommandOutput('ls'); 63 | ``` 64 | 65 | ## `getCWD` 66 | 67 | Get the `.logs/.cwd.log` file contents 68 | 69 | ```admonish danger title="Safety" 70 | Throws if file does not exist, or if read permission is denied. 71 | ``` 72 | 73 | ```typescript 74 | function getCWD(): Promise; 75 | ``` 76 | 77 | ```javascript 78 | const cwd = await __helpers.getCWD(); 79 | ``` 80 | 81 | ## `getLastCommand` 82 | 83 | Get the \\(n^{th}\\) latest line from `.logs/.bash_history.log`. 84 | 85 | ```admonish danger title="Safety" 86 | Throws if file does not exist, or if read permission is denied. 87 | ``` 88 | 89 | ```typescript 90 | function getLastCommand(n = 0): Promise; 91 | ``` 92 | 93 | ```javascript 94 | const lastCommand = await __helpers.getLastCommand(); 95 | ``` 96 | 97 | ## `getLastCWD` 98 | 99 | Get the \\(n^{th}\\) latest line from `.logs/.cwd.log`. 100 | 101 | ```admonish danger title="Safety" 102 | Throws if file does not exist, or if read permission is denied. 103 | ``` 104 | 105 | ```typescript 106 | function getLastCWD(n = 0): Promise; 107 | ``` 108 | 109 | ```javascript 110 | const lastCWD = await __helpers.getLastCWD(); 111 | ``` 112 | 113 | ## `getTemp` 114 | 115 | Get the `.logs/.temp.log` file contents. 116 | 117 | ```admonish danger title="Safety" 118 | Throws if file does not exist, or if read permission is denied. 119 | ``` 120 | 121 | ```typescript 122 | function getTemp(): Promise; 123 | ``` 124 | 125 | ```javascript 126 | const temp = await __helpers.getTemp(); 127 | ``` 128 | 129 | ```admonish note 130 | Use the output of the `.temp.log` file at your own risk. This file is raw input from the terminal including ANSI escape codes. 131 | 132 | Output varies depending on emulator, terminal size, order text is typed, etc. For more info, see [https://github.com/freeCodeCamp/solana-curriculum/issues/159](https://github.com/freeCodeCamp/solana-curriculum/issues/159) 133 | ``` 134 | 135 | ## `getTerminalOutput` 136 | 137 | Get the `.logs/.terminal_out.log` file contents. 138 | 139 | ```admonish danger title="Safety" 140 | Throws if file does not exist, or if read permission is denied. 141 | ``` 142 | 143 | ```typescript 144 | function getTerminalOutput(): Promise; 145 | ``` 146 | 147 | ```javascript 148 | const terminalOutput = await __helpers.getTerminalOutput(); 149 | ``` 150 | 151 | ## `importSansCache` 152 | 153 | Import a module side-stepping Nodejs' cache - cache-busting imports. 154 | 155 | ```admonish danger title="Safety" 156 | Throws if path is not a valid POSIX/DOS path, and if the `import` throws. 157 | ``` 158 | 159 | ```typescript 160 | function importSansCache(path: string): Promise; 161 | ``` 162 | 163 | ```javascript 164 | const { exportedFile } = await __helpers.importSansCache( 165 | 'learn-x-by-building-y/index.js' 166 | ); 167 | ``` 168 | -------------------------------------------------------------------------------- /self/bash/.bashrc: -------------------------------------------------------------------------------- 1 | # ~/.bashrc: executed by bash(1) for non-login shells. 2 | # see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) 3 | # for examples 4 | 5 | # If not running interactively, don't do anything 6 | case $- in 7 | *i*) ;; 8 | *) return;; 9 | esac 10 | 11 | # don't put duplicate lines or lines starting with space in the history. 12 | # See bash(1) for more options 13 | HISTCONTROL=ignoreboth 14 | 15 | # append to the history file, don't overwrite it 16 | shopt -s histappend 17 | 18 | # for setting history length see HISTSIZE and HISTFILESIZE in bash(1) 19 | HISTSIZE=1000 20 | HISTFILESIZE=2000 21 | 22 | # check the window size after each command and, if necessary, 23 | # update the values of LINES and COLUMNS. 24 | shopt -s checkwinsize 25 | 26 | # If set, the pattern "**" used in a pathname expansion context will 27 | # match all files and zero or more directories and subdirectories. 28 | #shopt -s globstar 29 | 30 | # make less more friendly for non-text input files, see lesspipe(1) 31 | [ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)" 32 | 33 | # set variable identifying the chroot you work in (used in the prompt below) 34 | if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then 35 | debian_chroot=$(cat /etc/debian_chroot) 36 | fi 37 | 38 | # set a fancy prompt (non-color, unless we know we "want" color) 39 | case "$TERM" in 40 | xterm-color|*-256color) color_prompt=yes;; 41 | esac 42 | 43 | # uncomment for a colored prompt, if the terminal has the capability; turned 44 | # off by default to not distract the user: the focus in a terminal window 45 | # should be on the output of commands, not on the prompt 46 | #force_color_prompt=yes 47 | 48 | if [ -n "$force_color_prompt" ]; then 49 | if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then 50 | # We have color support; assume it's compliant with Ecma-48 51 | # (ISO/IEC-6429). (Lack of such support is extremely rare, and such 52 | # a case would tend to support setf rather than setaf.) 53 | color_prompt=yes 54 | else 55 | color_prompt= 56 | fi 57 | fi 58 | 59 | if [ "$color_prompt" = yes ]; then 60 | PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' 61 | else 62 | PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ ' 63 | fi 64 | unset color_prompt force_color_prompt 65 | 66 | # If this is an xterm set the title to user@host:dir 67 | case "$TERM" in 68 | xterm*|rxvt*) 69 | PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1" 70 | ;; 71 | *) 72 | ;; 73 | esac 74 | 75 | # enable color support of ls and also add handy aliases 76 | if [ -x /usr/bin/dircolors ]; then 77 | test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)" 78 | alias ls='ls --color=auto' 79 | #alias dir='dir --color=auto' 80 | #alias vdir='vdir --color=auto' 81 | 82 | alias grep='grep --color=auto' 83 | alias fgrep='fgrep --color=auto' 84 | alias egrep='egrep --color=auto' 85 | fi 86 | 87 | # colored GCC warnings and errors 88 | #export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01' 89 | 90 | # some more ls aliases 91 | alias ll='ls -alF' 92 | alias la='ls -A' 93 | alias l='ls -CF' 94 | 95 | # Add an "alert" alias for long running commands. Use like so: 96 | # sleep 10; alert 97 | alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"' 98 | 99 | # Alias definitions. 100 | # You may want to put all your additions into a separate file like 101 | # ~/.bash_aliases, instead of adding them here directly. 102 | # See /usr/share/doc/bash-doc/examples in the bash-doc package. 103 | 104 | if [ -f ~/.bash_aliases ]; then 105 | . ~/.bash_aliases 106 | fi 107 | 108 | # enable programmable completion features (you don't need to enable 109 | # this, if it's already enabled in /etc/bash.bashrc and /etc/profile 110 | # sources /etc/bash.bashrc). 111 | if ! shopt -oq posix; then 112 | if [ -f /usr/share/bash-completion/bash_completion ]; then 113 | . /usr/share/bash-completion/bash_completion 114 | elif [ -f /etc/bash_completion ]; then 115 | . /etc/bash_completion 116 | fi 117 | fi 118 | 119 | PS1='\[\]\u\[\] \[\]\w\[\]$(__git_ps1 " (%s)") $ ' 120 | 121 | for i in $(ls -A $HOME/.bashrc.d/); do source $HOME/.bashrc.d/$i; done 122 | 123 | 124 | # freeCodeCamp - Needed for most tests to work 125 | WD=/workspace/freeCodeCampOS 126 | 127 | # Ensure `$WD/.logs/` directory and files exist 128 | mkdir -p $WD/.logs/ 129 | touch $WD/.logs/.bash_history.log $WD/.logs/.cwd.log $WD/.logs/.history_cwd.log $WD/.logs/.terminal_out.log $WD/.logs/.temp.log 130 | 131 | PROMPT_COMMAND='>| $WD/.logs/.terminal_out.log && cat $WD/.logs/.temp.log >| $WD/.logs/.terminal_out.log && truncate -s 0 $WD/.logs/.temp.log; echo $PWD >> $WD/.logs/.cwd.log; history -a $WD/.logs/.bash_history.log; echo $PWD\$ $(history | tail -n 1) >> $WD/.logs/.history_cwd.log;' 132 | exec > >(tee -ia $WD/.logs/.temp.log) 2>&1 133 | -------------------------------------------------------------------------------- /cli/src/conf.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::Value; 5 | 6 | #[derive(Serialize, Deserialize, Debug)] 7 | pub struct Conf { 8 | pub version: Version, 9 | pub port: Option, 10 | pub client: Option, 11 | pub config: Config, 12 | pub curriculum: Curriculum, 13 | #[serde(rename = "hotReload")] 14 | pub hot_reload: Option, 15 | pub tooling: Option, 16 | } 17 | 18 | #[derive(Debug)] 19 | pub struct Version { 20 | pub major: u8, 21 | pub minor: u8, 22 | pub patch: u8, 23 | } 24 | 25 | #[derive(Serialize, Deserialize, Debug)] 26 | pub struct Client { 27 | pub assets: Option, 28 | pub landing: Option, 29 | #[serde(rename = "static")] 30 | pub static_files: Option, 31 | } 32 | 33 | #[derive(Serialize, Deserialize, Debug)] 34 | pub struct Assets { 35 | pub header: String, 36 | pub favicon: String, 37 | } 38 | 39 | #[derive(Serialize, Deserialize, Debug)] 40 | pub struct Landing { 41 | pub title: String, 42 | pub description: String, 43 | #[serde(rename = "faq-link")] 44 | pub faq_link: Option, 45 | #[serde(rename = "faq-text")] 46 | pub faq_text: Option, 47 | } 48 | 49 | #[derive(Serialize, Deserialize, Debug)] 50 | pub struct Config { 51 | #[serde(rename = "projects.json")] 52 | pub projects: String, 53 | #[serde(rename = "state.json")] 54 | pub state: String, 55 | } 56 | 57 | #[derive(Serialize, Deserialize, Debug)] 58 | pub struct Curriculum { 59 | pub locales: Locales, 60 | pub assertions: Option, 61 | } 62 | 63 | #[derive(Serialize, Deserialize, Debug)] 64 | pub struct Locales { 65 | pub english: String, 66 | } 67 | 68 | #[derive(Serialize, Deserialize, Debug)] 69 | pub struct HotReload { 70 | pub ignore: Vec, 71 | } 72 | 73 | #[derive(Serialize, Deserialize, Debug)] 74 | pub struct Tooling { 75 | pub helpers: Option, 76 | pub plugins: Option, 77 | } 78 | 79 | impl TryFrom<&str> for Version { 80 | type Error = Box; 81 | 82 | fn try_from(s: &str) -> Result { 83 | let mut split = s.split('.'); 84 | let major = split.next().unwrap().parse::()?; 85 | let minor = split.next().unwrap().parse::()?; 86 | let patch = split.next().unwrap().parse::()?; 87 | Ok(Version { 88 | major, 89 | minor, 90 | patch, 91 | }) 92 | } 93 | } 94 | 95 | impl Serialize for Version { 96 | fn serialize(&self, serializer: S) -> Result 97 | where 98 | S: serde::Serializer, 99 | { 100 | format!("{}.{}.{}", self.major, self.minor, self.patch).serialize(serializer) 101 | } 102 | } 103 | 104 | impl<'a> Deserialize<'a> for Version { 105 | fn deserialize(deserializer: D) -> Result 106 | where 107 | D: serde::Deserializer<'a>, 108 | { 109 | let s: &str = serde::Deserialize::deserialize(deserializer)?; 110 | Version::try_from(s).map_err(serde::de::Error::custom) 111 | } 112 | } 113 | 114 | #[derive(Serialize, Deserialize, Debug)] 115 | pub struct Project { 116 | pub id: u16, 117 | #[serde(rename = "dashedName")] 118 | pub dashed_name: String, 119 | #[serde(rename = "isIntegrated", default = "default_false")] 120 | pub is_integrated: bool, 121 | #[serde(rename = "isPublic", default = "default_true")] 122 | pub is_public: bool, 123 | #[serde(rename = "currentLesson", default = "default_0")] 124 | pub current_lesson: u16, 125 | #[serde(rename = "runTestsOnWatch", default = "default_false")] 126 | pub run_tests_on_watch: bool, 127 | #[serde(rename = "seedEveryLesson", default = "default_false")] 128 | pub seed_every_lesson: bool, 129 | #[serde(rename = "isResetEnabled", default = "default_false")] 130 | pub is_reset_enabled: bool, 131 | #[serde(rename = "numberofLessons", default = "default_1")] 132 | pub number_of_lessons: u16, 133 | #[serde(rename = "blockingTests", default = "default_false")] 134 | pub blocking_tests: bool, 135 | #[serde(rename = "breakOnFailure", default = "default_false")] 136 | pub break_on_failure: bool, 137 | } 138 | 139 | fn default_false() -> bool { 140 | false 141 | } 142 | 143 | fn default_true() -> bool { 144 | true 145 | } 146 | 147 | fn default_1() -> u16 { 148 | 1 149 | } 150 | 151 | fn default_0() -> u16 { 152 | 0 153 | } 154 | 155 | #[derive(Serialize, Deserialize, Debug)] 156 | pub struct State { 157 | #[serde(rename = "currentProject")] 158 | /// The current project the user is working on as a `String` or `Value::Null` 159 | pub current_project: Value, 160 | pub locale: String, 161 | #[serde(rename = "lastSeed")] 162 | pub last_seed: Option, 163 | } 164 | 165 | #[derive(Serialize, Deserialize, Debug)] 166 | pub struct LastSeed { 167 | #[serde(rename = "projectDashedName")] 168 | pub project_dashed_name: Value, 169 | #[serde(rename = "lessonNumber")] 170 | /// The lesson number last seeded 171 | /// 172 | /// Can be -1, because lessons start at 0, and -1 is used to indicate that no lesson has been seeded 173 | pub lesson_number: i16, 174 | } 175 | -------------------------------------------------------------------------------- /.freeCodeCamp/plugin/index.js: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs/promises'; 2 | import { freeCodeCampConfig, getState, ROOT } from '../tooling/env.js'; 3 | import { CoffeeDown, parseMarkdown } from '../tooling/parser.js'; 4 | import { join } from 'path'; 5 | import { logover } from '../tooling/logger.js'; 6 | 7 | /** 8 | * Project config from `config/projects.json` 9 | * @typedef {Object} Project 10 | * @property {string} id 11 | * @property {string} title 12 | * @property {string} dashedName 13 | * @property {string} description 14 | * @property {boolean} isIntegrated 15 | * @property {boolean} isPublic 16 | * @property {number} currentLesson 17 | * @property {boolean} runTestsOnWatch 18 | * @property {boolean} isResetEnabled 19 | * @property {number} numberOfLessons 20 | * @property {boolean} seedEveryLesson 21 | * @property {boolean} blockingTests 22 | * @property {boolean} breakOnFailure 23 | */ 24 | 25 | /** 26 | * @typedef {Object} TestsState 27 | * @property {boolean} passed 28 | * @property {string} testText 29 | * @property {number} testId 30 | * @property {boolean} isLoading 31 | */ 32 | 33 | /** 34 | * @typedef {Object} Lesson 35 | * @property {{watch?: string[]; ignore?: string[]} | undefined} meta 36 | * @property {string} description 37 | * @property {[[string, string]]} tests 38 | * @property {string[]} hints 39 | * @property {[{filePath: string; fileSeed: string} | string]} seed 40 | * @property {boolean?} isForce 41 | * @property {string?} beforeAll 42 | * @property {string?} afterAll 43 | * @property {string?} beforeEach 44 | * @property {string?} afterEach 45 | */ 46 | 47 | export const pluginEvents = { 48 | /** 49 | * @param {Project} project 50 | * @param {TestsState[]} testsState 51 | */ 52 | onTestsStart: async (project, testsState) => {}, 53 | 54 | /** 55 | * @param {Project} project 56 | * @param {TestsState[]} testsState 57 | */ 58 | onTestsEnd: async (project, testsState) => {}, 59 | 60 | /** 61 | * @param {Project} project 62 | */ 63 | onProjectStart: async project => {}, 64 | 65 | /** 66 | * @param {Project} project 67 | */ 68 | onProjectFinished: async project => {}, 69 | 70 | /** 71 | * @param {Project} project 72 | */ 73 | onLessonPassed: async project => {}, 74 | 75 | /** 76 | * @param {Project} project 77 | */ 78 | onLessonFailed: async project => {}, 79 | 80 | /** 81 | * @param {Project} project 82 | */ 83 | onLessonLoad: async project => {}, 84 | 85 | /** 86 | * @param {string} projectDashedName 87 | * @returns {Promise<{title: string; description: string; numberOfLessons: number; tags: string[]}>} 88 | */ 89 | getProjectMeta: async projectDashedName => { 90 | const { locale } = await getState(); 91 | const projectFilePath = join( 92 | ROOT, 93 | freeCodeCampConfig.curriculum.locales[locale], 94 | projectDashedName + '.md' 95 | ); 96 | const projectFile = await readFile(projectFilePath, 'utf8'); 97 | const coffeeDown = new CoffeeDown(projectFile); 98 | const projectMeta = coffeeDown.getProjectMeta(); 99 | // Remove `

    ` tags if present 100 | const title = parseMarkdown(projectMeta.title) 101 | .replace(/

    |<\/p>/g, '') 102 | .trim(); 103 | const description = parseMarkdown(projectMeta.description).trim(); 104 | const tags = projectMeta.tags; 105 | const numberOfLessons = projectMeta.numberOfLessons; 106 | return { title, description, numberOfLessons, tags }; 107 | }, 108 | 109 | /** 110 | * @param {string} projectDashedName 111 | * @param {number} lessonNumber 112 | * @returns {Promise} lesson 113 | */ 114 | getLesson: async (projectDashedName, lessonNumber) => { 115 | const { locale } = await getState(); 116 | const projectFilePath = join( 117 | ROOT, 118 | freeCodeCampConfig.curriculum.locales[locale], 119 | projectDashedName + '.md' 120 | ); 121 | const projectFile = await readFile(projectFilePath, 'utf8'); 122 | const coffeeDown = new CoffeeDown(projectFile); 123 | const lesson = coffeeDown.getLesson(lessonNumber); 124 | let seed = lesson.seed; 125 | if (!seed.length) { 126 | // Check for external seed file 127 | const seedFilePath = projectFilePath.replace(/.md$/, '-seed.md'); 128 | try { 129 | const seedContent = await readFile(seedFilePath, 'utf-8'); 130 | const coffeeDown = new CoffeeDown(seedContent); 131 | seed = coffeeDown.getLesson(lessonNumber).seed; 132 | } catch (e) { 133 | if (e?.code !== 'ENOENT') { 134 | logover.debug(e); 135 | throw new Error( 136 | `Error reading external seed for lesson ${lessonNumber}` 137 | ); 138 | } 139 | } 140 | } 141 | const { afterAll, afterEach, beforeAll, beforeEach, isForce, meta } = 142 | lesson; 143 | const description = parseMarkdown(lesson.description).trim(); 144 | const tests = lesson.tests.map(([testText, test]) => [ 145 | parseMarkdown(testText).trim(), 146 | test 147 | ]); 148 | const hints = lesson.hints.map(h => parseMarkdown(h).trim()); 149 | return { 150 | meta, 151 | description, 152 | tests, 153 | hints, 154 | seed, 155 | beforeAll, 156 | afterAll, 157 | beforeEach, 158 | afterEach, 159 | isForce 160 | }; 161 | } 162 | }; 163 | -------------------------------------------------------------------------------- /.freeCodeCamp/client/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --dark-1: #0a0a23; 3 | --dark-2: #1b1b32; 4 | --dark-3: #2a2a40; 5 | --dark-4: #3b3b4f; 6 | --mid: #858591; 7 | --light-1: #ffffff; 8 | --light-2: #f5f6f7; 9 | --light-3: #dfdfe2; 10 | --light-4: #d0d0d5; 11 | --light-purple: #dbb8ff; 12 | --light-yellow: #f1be32; 13 | --light-blue: #99c9ff; 14 | --light-green: #acd157; 15 | --dark-purple: #5a01a7; 16 | --dark-yellow: #ffac33; 17 | --dark-something: #4d3800; 18 | --dark-blue: #002ead; 19 | --dark-green: #00471b; 20 | } 21 | 22 | @font-face { 23 | font-family: 'Lato'; 24 | src: url('./assets/Lato-Regular.woff') format('woff'); 25 | font-weight: normal; 26 | font-style: normal; 27 | font-display: fallback; 28 | } 29 | 30 | * { 31 | color: var(--light-2); 32 | line-height: 2.2ch; 33 | } 34 | body { 35 | background-color: var(--dark-2); 36 | margin: 0; 37 | text-align: center; 38 | font-family: 'Lato', sans-serif; 39 | font-weight: 400; 40 | } 41 | 42 | header { 43 | width: 100%; 44 | height: 38px; 45 | background-color: var(--dark-1); 46 | display: flex; 47 | justify-content: center; 48 | } 49 | 50 | .header-btn { 51 | all: unset; 52 | } 53 | button { 54 | font-family: inherit; 55 | } 56 | .header-btn:hover { 57 | cursor: pointer; 58 | } 59 | 60 | #logo { 61 | width: auto; 62 | height: 38px; 63 | max-height: 100%; 64 | background-color: var(--dark-1); 65 | padding: 0.4rem; 66 | display: block; 67 | margin: 0 auto; 68 | padding-left: 20px; 69 | padding-right: 20px; 70 | } 71 | 72 | p { 73 | font-size: 16px; 74 | } 75 | 76 | .loader { 77 | --b: 10px; 78 | /* border thickness */ 79 | --n: 10; 80 | /* number of dashes*/ 81 | --g: 10deg; 82 | /* gap between dashes*/ 83 | --c: red; 84 | /* the color */ 85 | 86 | width: 100px; 87 | /* size */ 88 | margin: 0 auto; 89 | aspect-ratio: 1; 90 | border-radius: 50%; 91 | padding: 1px; 92 | /* get rid of bad outlines */ 93 | background: conic-gradient(#0000, var(--c)) content-box; 94 | -webkit-mask: 95 | /* we use +/-1deg between colors to avoid jagged edges */ repeating-conic-gradient( 96 | #0000 0deg, 97 | #000 1deg calc(360deg / var(--n) - var(--g) - 1deg), 98 | #0000 calc(360deg / var(--n) - var(--g)) calc(360deg / var(--n)) 99 | ), 100 | radial-gradient( 101 | farthest-side, 102 | #0000 calc(98% - var(--b)), 103 | #000 calc(100% - var(--b)) 104 | ); 105 | mask: repeating-conic-gradient( 106 | #0000 0deg, 107 | #000 1deg calc(360deg / var(--n) - var(--g) - 1deg), 108 | #0000 calc(360deg / var(--n) - var(--g)) calc(360deg / var(--n)) 109 | ), 110 | radial-gradient( 111 | farthest-side, 112 | #0000 calc(98% - var(--b)), 113 | #000 calc(100% - var(--b)) 114 | ); 115 | -webkit-mask-composite: destination-in; 116 | mask-composite: intersect; 117 | animation: load 1s infinite steps(var(--n)); 118 | } 119 | .width-10 { 120 | width: 10px; 121 | } 122 | .width-20 { 123 | width: 20px; 124 | } 125 | .width-30 { 126 | width: 30px; 127 | } 128 | .width-40 { 129 | width: 40px; 130 | } 131 | .width-50 { 132 | width: 50px; 133 | } 134 | .width-60 { 135 | width: 60px; 136 | } 137 | .width-70 { 138 | width: 70px; 139 | } 140 | .width-80 { 141 | width: 80px; 142 | } 143 | .width-90 { 144 | width: 90px; 145 | } 146 | [class*='width-'] { 147 | margin: 0; 148 | display: inline-block; 149 | } 150 | 151 | @keyframes load { 152 | to { 153 | transform: rotate(1turn); 154 | } 155 | } 156 | 157 | .hidden { 158 | display: none; 159 | } 160 | 161 | code { 162 | background-color: var(--dark-3); 163 | color: var(--light-1); 164 | padding: 1px; 165 | margin-top: 0.1rem; 166 | margin-bottom: 0.1rem; 167 | } 168 | 169 | #description pre { 170 | overflow-x: auto; 171 | } 172 | 173 | .test { 174 | padding-bottom: 1rem; 175 | } 176 | 177 | .test > div > p { 178 | display: inline; 179 | } 180 | 181 | details { 182 | padding-bottom: 1rem; 183 | } 184 | 185 | .e44o5 { 186 | text-align: left; 187 | } 188 | 189 | .e44o5 ul, 190 | .e44o5 ol { 191 | display: inline-block; 192 | } 193 | 194 | .e44o5 li { 195 | padding: 8px; 196 | } 197 | 198 | .block-info { 199 | display: flex; 200 | } 201 | .block-info > span { 202 | margin: auto 1em auto auto; 203 | } 204 | 205 | .sr-only { 206 | position: absolute; 207 | width: 1px; 208 | height: 1px; 209 | margin: -1px; 210 | padding: 0; 211 | overflow: hidden; 212 | clip: rect(0, 0, 0, 0); 213 | border: 0; 214 | } 215 | 216 | .block-checkmark { 217 | margin-left: 7px; 218 | position: relative; 219 | top: 1px; 220 | } 221 | 222 | #toggle-lang-button { 223 | margin: 0 0.5rem; 224 | position: absolute; 225 | right: 0; 226 | top: 0.28rem; 227 | } 228 | 229 | #toggle-lang-button:hover { 230 | cursor: pointer; 231 | } 232 | 233 | #nav-lang-list { 234 | position: absolute; 235 | right: 0; 236 | background-color: var(--dark-1); 237 | border-radius: 0.3rem; 238 | padding: 0.3rem; 239 | margin-top: 2.4rem; 240 | z-index: 1; 241 | } 242 | #nav-lang-list > li { 243 | padding: 0.28rem; 244 | list-style: none; 245 | } 246 | 247 | #nav-lang-list > li > button { 248 | width: 100%; 249 | color: var(--dark-3); 250 | } 251 | 252 | #nav-lang-list > li > button:hover { 253 | color: var(--light-2); 254 | background-color: var(--dark-3); 255 | cursor: pointer; 256 | } 257 | -------------------------------------------------------------------------------- /docs/theme/css/general.css: -------------------------------------------------------------------------------- 1 | /* Base styles and content styles */ 2 | 3 | @import 'variables.css'; 4 | 5 | :root { 6 | /* Browser default font-size is 16px, this way 1 rem = 10px */ 7 | font-size: 62.5%; 8 | } 9 | 10 | html { 11 | font-family: 'Lato', sans-serif; 12 | color: var(--fg); 13 | background-color: var(--bg); 14 | text-size-adjust: none; 15 | -webkit-text-size-adjust: none; 16 | } 17 | 18 | body { 19 | margin: 0; 20 | font-size: 1.6rem; 21 | overflow-x: hidden; 22 | } 23 | 24 | code { 25 | font-family: var(--mono-font) !important; 26 | font-size: var(--code-font-size); 27 | } 28 | 29 | /* make long words/inline code not x overflow */ 30 | main { 31 | overflow-wrap: break-word; 32 | } 33 | 34 | /* make wide tables scroll if they overflow */ 35 | .table-wrapper { 36 | overflow-x: auto; 37 | } 38 | 39 | /* Don't change font size in headers. */ 40 | h1 code, 41 | h2 code, 42 | h3 code, 43 | h4 code, 44 | h5 code, 45 | h6 code { 46 | font-size: unset; 47 | } 48 | 49 | .left { 50 | float: left; 51 | } 52 | .right { 53 | float: right; 54 | } 55 | .boring { 56 | opacity: 0.6; 57 | } 58 | .hide-boring .boring { 59 | display: none; 60 | } 61 | .hidden { 62 | display: none !important; 63 | } 64 | 65 | h2, 66 | h3 { 67 | margin-top: 2.5em; 68 | } 69 | h4, 70 | h5 { 71 | margin-top: 2em; 72 | } 73 | 74 | .header + .header h3, 75 | .header + .header h4, 76 | .header + .header h5 { 77 | margin-top: 1em; 78 | } 79 | 80 | h1:target::before, 81 | h2:target::before, 82 | h3:target::before, 83 | h4:target::before, 84 | h5:target::before, 85 | h6:target::before { 86 | display: inline-block; 87 | content: '»'; 88 | margin-left: -30px; 89 | width: 30px; 90 | } 91 | 92 | /* This is broken on Safari as of version 14, but is fixed 93 | in Safari Technology Preview 117 which I think will be Safari 14.2. 94 | https://bugs.webkit.org/show_bug.cgi?id=218076 95 | */ 96 | :target { 97 | scroll-margin-top: calc(var(--menu-bar-height) + 0.5em); 98 | } 99 | 100 | .page { 101 | outline: 0; 102 | padding: 0 var(--page-padding); 103 | margin-top: calc(0px - var(--menu-bar-height)); 104 | /* Compensate for the #menu-bar-hover-placeholder */ 105 | } 106 | .page-wrapper { 107 | box-sizing: border-box; 108 | } 109 | .js:not(.sidebar-resizing) .page-wrapper { 110 | transition: margin-left 0.3s ease, transform 0.3s ease; 111 | /* Animation: slide away */ 112 | } 113 | 114 | .content { 115 | overflow-y: auto; 116 | padding: 0 5px 50px 5px; 117 | } 118 | .content main { 119 | margin-left: auto; 120 | margin-right: auto; 121 | max-width: var(--content-max-width); 122 | } 123 | .content p { 124 | line-height: 1.45em; 125 | } 126 | .content ol { 127 | line-height: 1.45em; 128 | } 129 | .content ul { 130 | line-height: 1.45em; 131 | } 132 | .content a { 133 | text-decoration: none; 134 | } 135 | .content a:hover { 136 | text-decoration: underline; 137 | } 138 | .content img, 139 | .content video { 140 | max-width: 100%; 141 | } 142 | .content .header:link, 143 | .content .header:visited { 144 | color: var(--fg); 145 | } 146 | .content .header:link, 147 | .content .header:visited:hover { 148 | text-decoration: none; 149 | } 150 | 151 | table { 152 | margin: 0 auto; 153 | border-collapse: collapse; 154 | } 155 | table td { 156 | padding: 3px 20px; 157 | border: 1px var(--table-border-color) solid; 158 | } 159 | table thead { 160 | background: var(--table-header-bg); 161 | } 162 | table thead td { 163 | font-weight: 700; 164 | border: none; 165 | } 166 | table thead th { 167 | padding: 3px 20px; 168 | } 169 | table thead tr { 170 | border: 1px var(--table-header-bg) solid; 171 | } 172 | /* Alternate background colors for rows */ 173 | table tbody tr:nth-child(2n) { 174 | background: var(--table-alternate-bg); 175 | } 176 | 177 | blockquote { 178 | margin: 20px 0; 179 | padding: 0 20px; 180 | color: var(--fg); 181 | background-color: var(--quote-bg); 182 | border-top: 0.1em solid var(--quote-border); 183 | border-bottom: 0.1em solid var(--quote-border); 184 | } 185 | 186 | kbd { 187 | background-color: var(--table-border-color); 188 | border-radius: 4px; 189 | border: solid 1px var(--theme-popup-border); 190 | box-shadow: inset 0 -1px 0 var(--theme-hover); 191 | display: inline-block; 192 | font-size: var(--code-font-size); 193 | font-family: var(--mono-font); 194 | line-height: 10px; 195 | padding: 4px 5px; 196 | vertical-align: middle; 197 | } 198 | 199 | :not(.footnote-definition) + .footnote-definition, 200 | .footnote-definition + :not(.footnote-definition) { 201 | margin-top: 2em; 202 | } 203 | .footnote-definition { 204 | font-size: 0.9em; 205 | margin: 0.5em 0; 206 | } 207 | .footnote-definition p { 208 | display: inline; 209 | } 210 | 211 | .tooltiptext { 212 | position: absolute; 213 | visibility: hidden; 214 | color: #fff; 215 | background-color: #333; 216 | transform: translateX(-50%); 217 | /* Center by moving tooltip 50% of its width left */ 218 | left: -8px; 219 | /* Half of the width of the icon */ 220 | top: -35px; 221 | font-size: 0.8em; 222 | text-align: center; 223 | border-radius: 6px; 224 | padding: 5px 8px; 225 | margin: 5px; 226 | z-index: 1000; 227 | } 228 | .tooltipped .tooltiptext { 229 | visibility: visible; 230 | } 231 | 232 | .chapter li.part-title { 233 | color: var(--sidebar-fg); 234 | margin: 5px 0px; 235 | font-weight: bold; 236 | } 237 | 238 | .result-no-output { 239 | font-style: italic; 240 | } 241 | 242 | dfn[title] { 243 | text-decoration: underline dotted; 244 | } -------------------------------------------------------------------------------- /docs/src/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | ## `freecodecamp.conf.json` 4 | 5 | ### Required Configuration 6 | 7 | ```json 8 | { 9 | "version": "0.0.1", 10 | "config": { 11 | "projects.json": "", 12 | "state.json": "" 13 | }, 14 | "curriculum": { 15 | "locales": { 16 | "": "" 17 | } 18 | } 19 | } 20 | ``` 21 | 22 | ````admonish example collapsible=true title="Minimum Usable Example" 23 | ```json 24 | { 25 | "version": "0.0.1", 26 | "config": { 27 | "projects.json": "./config/projects.json", 28 | "state.json": "./config/state.json" 29 | }, 30 | "curriculum": { 31 | "locales": { 32 | "english": "./curriculum/locales/english" 33 | } 34 | } 35 | } 36 | ``` 37 | ```` 38 | 39 | ### Optional Configuration (Features) 40 | 41 | #### `port` 42 | 43 | By default, the server and client communicate over port `8080`. To change this, add a `port` key to the configuration file: 44 | 45 | ````admonish example 46 | ```json 47 | { 48 | "port": 8080 49 | } 50 | ``` 51 | ```` 52 | 53 | #### `client` 54 | 55 | - `assets.header`: path relative to the root of the course - `string` 56 | - `assets.favicon`: path relative to the root of the course - `string` 57 | - `landing..description`: description of the course shown on the landing page - `string` 58 | - `landing..title`: title of the course shown on the landing page - `string` 59 | - `landing..faq-link`: link to the FAQ page - `string` 60 | - `landing..faq-text`: text to display for the FAQ link - `string` 61 | - `static`: static resources to serve - `string | string[] | Record | Record[]` 62 | 63 | ````admonish example 64 | ```json 65 | { 66 | "client": { 67 | "assets": { 68 | "header": "./client/assets/header.png", 69 | "favicon": "./client/assets/favicon.ico" 70 | }, 71 | "static": ["./curriculum/", { "/images": "./curriculum/images" }] 72 | } 73 | } 74 | ``` 75 | ```` 76 | 77 | #### `config` 78 | 79 | - `projects.json`: path relative to the root of the course - `string` 80 | - `state.json`: path relative to the root of the course - `string` 81 | 82 | ````admonish example 83 | ```json 84 | { 85 | "config": { 86 | "projects.json": "./config/projects.json", 87 | "state.json": "./config/state.json" 88 | } 89 | } 90 | ``` 91 | ```` 92 | 93 | #### `curriculum` 94 | 95 | - `locales`: an object of locale names and their corresponding paths relative to the root of the course - `Record` 96 | - `assertions`: an onject of locale names and their corresponding paths to a JSON file containing custom assertions - `string` 97 | 98 | ````admonish example 99 | ```json 100 | { 101 | "curriculum": { 102 | "locales": { 103 | "english": "./curriculum/locales/english" 104 | }, 105 | "assertions": { 106 | "afrikaans": "./curriculum/assertions/afrikaans.json" 107 | } 108 | } 109 | } 110 | ``` 111 | ```` 112 | 113 | ```admonish attention 114 | Currently, `english` is a required locale, and is used as the default. 115 | ``` 116 | 117 | #### `hotReload` 118 | 119 | - `ignore`: a list of paths to ignore when hot reloading - `string[]` 120 | 121 | ````admonish example 122 | ```json 123 | { 124 | "hotReload": { 125 | "ignore": [".logs/.temp.log", "config/", "/node_modules/", ".git"] 126 | } 127 | } 128 | ``` 129 | ```` 130 | 131 | #### `tooling` 132 | 133 | - `helpers`: path relative to the root of the course - `string` 134 | - `plugins`: path relative to the root of the course - `string` 135 | 136 | ````admonish example 137 | ```json 138 | { 139 | "tooling": { 140 | "helpers": "./tooling/helpers.js", 141 | "plugins": "./tooling/plugins.js" 142 | } 143 | } 144 | ``` 145 | ```` 146 | 147 | ## `projects.json` 148 | 149 | ### Definitions 150 | 151 | - `id`: A unique, incremental integer - `number` 152 | - `dashedName`: The name of the project corresponding to the `curriculum/locales/.md` file - `string` 153 | - `isIntegrated`: Whether or not to treat the project as a single-lesson project - `boolean` (default: `false`) 154 | - `isPublic`: Whether or not to enable the project for public viewing. **Note:** the project will still be visible on the landing page, but will be disabled - `boolean` (default: `false`) 155 | - `currentLesson`: The current lesson of the project - `number` (default: `1`) 156 | - `runTestsOnWatch`: Whether or not to run tests on file change - `boolean` (default: `false`) 157 | - `isResetEnabled`: Whether or not to enable the reset button - `boolean` (default: `false`) 158 | - `numberOfLessons`: The number of lessons in the project - `number`[^1] 159 | - `seedEveryLesson`: Whether or not to run the seed on lesson load - `boolean` (default: `false`) 160 | - `blockingTests`: Run tests synchronously - `boolean` (default: `false`) 161 | - `breakOnFailure`: Stop running tests on the first failure - `boolean` (default: `false`) 162 | 163 | [^1]: This is automagically calculated when the app is launched. 164 | 165 | ### Required Configuration 166 | 167 | ```json 168 | [ 169 | { 170 | "id": 0, // Unique ID 171 | "dashedName": "" 172 | } 173 | ] 174 | ``` 175 | 176 | ### Optional Configuration 177 | 178 | ````admonish example 179 | ```json 180 | [ 181 | { 182 | "id": 0, 183 | "dashedName": "learn-x-by-building-y", 184 | "isIntegrated": false, 185 | "isPublic": false, 186 | "currentLesson": 1, 187 | "runTestsOnWatch": false, 188 | "isResetEnabled": false, 189 | "numberOfLessons": 10, 190 | "seedEveryLesson": false, 191 | "blockingTests": false, 192 | "breakOnFailure": false 193 | } 194 | ] 195 | ``` 196 | ```` 197 | 198 | ## `.gitignore` 199 | 200 | ### Retaining Files When a Step is Reset 201 | 202 | ```admonish warning 203 | Resetting a step removes all untracked files from the project directory. To prevent this for specific files, add them to a boilerplate `.gitignore` file, or the one in root. 204 | ``` 205 | -------------------------------------------------------------------------------- /.freeCodeCamp/tooling/git/gitterizer.js: -------------------------------------------------------------------------------- 1 | // This file handles the fetching/parsing of the Git status of the project 2 | import { promisify } from 'util'; 3 | import { exec } from 'child_process'; 4 | import { getState, setState } from '../env.js'; 5 | import { logover } from '../logger.js'; 6 | const execute = promisify(exec); 7 | 8 | /** 9 | * Runs the following commands: 10 | * 11 | * ```bash 12 | * git add . 13 | * git commit --allow-empty -m "()" 14 | * ``` 15 | * 16 | * @param {number} lessonNumber 17 | * @returns {Promise} 18 | */ 19 | export async function commit(lessonNumber) { 20 | try { 21 | const { stdout, stderr } = await execute( 22 | `git add . && git commit --allow-empty -m "(${lessonNumber})"` 23 | ); 24 | if (stderr) { 25 | logover.error('Failed to commit lesson: ', lessonNumber); 26 | throw new Error(stderr); 27 | } 28 | } catch (e) { 29 | return Promise.reject(e); 30 | } 31 | return Promise.resolve(); 32 | } 33 | 34 | /** 35 | * Initialises a new branch for the `CURRENT_PROJECT` 36 | * @returns {Promise} 37 | */ 38 | export async function initCurrentProjectBranch() { 39 | const { currentProject } = await getState(); 40 | try { 41 | const { stdout, stderr } = await execute( 42 | `git checkout -b ${currentProject}` 43 | ); 44 | // SILlY GIT PUTS A BRANCH SWITCH INTO STDERR!!! 45 | // if (stderr) { 46 | // throw new Error(stderr); 47 | // } 48 | } catch (e) { 49 | return Promise.reject(e); 50 | } 51 | return Promise.resolve(); 52 | } 53 | 54 | /** 55 | * Returns the commit hash of the branch `origin/` 56 | * @param {number} number 57 | * @returns {Promise} 58 | */ 59 | export async function getCommitHashByNumber(number) { 60 | const { lastKnownLessonWithHash, currentProject } = await getState(); 61 | try { 62 | const { stdout, stderr } = await execute( 63 | `git log origin/${currentProject} --oneline --grep="(${number})" --` 64 | ); 65 | if (stderr) { 66 | throw new Error(stderr); 67 | } 68 | const hash = stdout.match(/\w+/)?.[0]; 69 | // This keeps track of the latest known commit in case there are no commits from one lesson to the next 70 | if (!hash) { 71 | return getCommitHashByNumber(lastKnownLessonWithHash); 72 | } 73 | await setState({ lastKnownLessonWithHash: number }); 74 | return hash; 75 | } catch (e) { 76 | throw new Error(e); 77 | } 78 | } 79 | 80 | /** 81 | * Aborts and in-progress `cherry-pick` 82 | * @returns {Promise} 83 | */ 84 | async function ensureNoUnfinishedGit() { 85 | try { 86 | const { stdout, stderr } = await execute(`git cherry-pick --abort`); 87 | // Throwing in a `try` probably does not make sense 88 | if (stderr) { 89 | throw new Error(stderr); 90 | } 91 | } catch (e) { 92 | return Promise.reject(e); 93 | } 94 | return Promise.resolve(); 95 | } 96 | 97 | /** 98 | * Git cleans the current branch, then `cherry-pick`s the commit hash found by `lessonNumber` 99 | * @param {number} lessonNumber 100 | * @returns {Promise} 101 | */ 102 | export async function setFileSystemToLessonNumber(lessonNumber) { 103 | await ensureNoUnfinishedGit(); 104 | const endHash = await getCommitHashByNumber(lessonNumber); 105 | const firstHash = await getCommitHashByNumber(1); 106 | try { 107 | // TODO: Continue on this error? Or, bail? 108 | if (!endHash || !firstHash) { 109 | throw new Error('Could not find commit hash'); 110 | } 111 | // VOLUME BINDING? 112 | // 113 | // TODO: Probably do not want to always completely clean for each lesson 114 | if (firstHash === endHash) { 115 | await execute(`git clean -f -q -- . && git cherry-pick ${endHash}`); 116 | } else { 117 | // TODO: Why not git checkout ${endHash} 118 | const { stdout, stderr } = await execute( 119 | `git clean -f -q -- . && git cherry-pick ${firstHash}^..${endHash}` 120 | ); 121 | if (stderr) { 122 | throw new Error(stderr); 123 | } 124 | } 125 | } catch (e) { 126 | return Promise.reject(e); 127 | } 128 | return Promise.resolve(); 129 | } 130 | 131 | /** 132 | * Pushes the `` branch to `origin` 133 | * @returns {Promise} 134 | */ 135 | export async function pushProject() { 136 | const { currentProject } = await getState(); 137 | try { 138 | const { stdout, stderr } = await execute( 139 | `git push origin ${currentProject} --force` 140 | ); 141 | // if (stderr) { 142 | // throw new Error(stderr); 143 | // } 144 | } catch (e) { 145 | logover.error('Failed to push project ', currentProject); 146 | return Promise.reject(e); 147 | } 148 | return Promise.resolve(); 149 | } 150 | 151 | /** 152 | * Checks out the `main` branch 153 | * 154 | * **IMPORTANT**: This function restores any/all git changes that are uncommitted. 155 | * @returns {Promise} 156 | */ 157 | export async function checkoutMain() { 158 | try { 159 | await execute('git restore .'); 160 | const { stdout, stderr } = await execute(`git checkout main`); 161 | // if (stderr) { 162 | // throw new Error(stderr); 163 | // } 164 | } catch (e) { 165 | return Promise.reject(e); 166 | } 167 | return Promise.resolve(); 168 | } 169 | 170 | /** 171 | * If the given branch is found to exist, deletes the branch 172 | * @param {string} branch 173 | * @returns {Promise} 174 | */ 175 | export async function deleteBranch(branch) { 176 | const isBranchExists = await branchExists(branch); 177 | if (!isBranchExists) { 178 | return Promise.resolve(); 179 | } 180 | logover.warn('Deleting branch ', branch); 181 | try { 182 | await checkoutMain(); 183 | const { stdout, stderr } = await execute(`git branch -D ${branch}`); 184 | logover.info(stdout); 185 | // if (stderr) { 186 | // throw new Error(stderr); 187 | // } 188 | } catch (e) { 189 | logover.error('Failed to delete branch: ', branch); 190 | return Promise.reject(e); 191 | } 192 | return Promise.resolve(); 193 | } 194 | 195 | /** 196 | * Checks if the given branch exists 197 | * @param {string} branch 198 | * @returns {Promise} 199 | */ 200 | export async function branchExists(branch) { 201 | try { 202 | const { stdout, stderr } = await execute(`git branch --list ${branch}`); 203 | return Promise.resolve(stdout.includes(branch)); 204 | } catch (e) { 205 | return Promise.reject(e); 206 | } 207 | } 208 | --------------------------------------------------------------------------------