Introduction

This is the documentation for the @freecodecamp/freecodecamp-os package.

freecodecamp-os is a tool for creating and running interactive courses within Visual Studio Code.

The freecodecamp-os package is to be used in conjunction with the freeCodeCamp - Courses extension, for the optimal experience.

Getting Started

Creating a New Course

Create a new project directory and install @freecodecamp/freecodecamp-os:

mkdir <COURSE_DIR>
cd <COURSE_DIR>
npm init -y
npm install @freecodecamp/freecodecamp-os

Feel free to replace npm with another package manager of your choice.

Configuring Your Course

Create a freecodecamp.conf.json file in the project root:

touch freecodecamp.conf.json

Add the following required configuration:

{
  "version": "0.0.1",
  "config": {
    "projects.json": "<PROJECTS_JSON>",
    "state.json": "<STATE_JSON>"
  },
  "curriculum": {
    "locales": {
      "<LOCALE>": "<LOCALE_DIR>"
    }
  }
}

Example

{
  "version": "0.0.1",
  "config": {
    "projects.json": "./config/projects.json",
    "state.json": "./config/state.json"
  },
  "curriculum": {
    "locales": {
      "english": "./curriculum/locales/english"
    }
  }
}

Info

There are many more configuration options available. See the configuration page for more details.

Create the projects.json file:

[
  {
    "id": 0,
    "dashedName": "<PROJECT_DASHED_NAME>"
  }
]

Info

There are many more configuration options available. See the configuration page for more details.

Create the state.json file:

{}

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.

Create the curricula files:

mkdir <LOCALE_DIR>
touch <LOCALE_DIR>/<PROJECT_DASHED_NAME>.md

Example

mkdir curriculum/locales/english
touch curriculum/locales/english/learn-x-by-building-y.md

Add the Markdown content to the curricula files. See the project syntax page for more details.

Create the project boilerplate/working directory in the root:

mkdir <PROJECT_DASHED_NAME>

Example

mkdir learn-x-by-building-y

Required Files

<COURSE_DIR>/
├── freecodecamp.conf.json
├── <PROJECTS_JSON>
├── <STATE_JSON>
└── <LOCALE_DIR>/
    └── <PROJECT_DASHED_NAME>.md

If using the terminal feature:

├── <CONFIG_BASH>/
│   ├── <CONFIG_BASH_BASHRC>
│   └── <CONFIG_BASH_SOURCERER>
├── .logs/
│   ├── .bash_history.log
│   ├── .cwd.log
│   ├── .history_cwd.log
│   ├── .next_command.log
│   ├── .temp.log
│   └── .terminal_out.log

If using the tooling feature:

├── <CONFIG_TOOLING_HELPERS>

Create a .vscode/settings.json file to configure the freeCodeCamp - Courses extension:

{
  // Open the course when the workspace is opened
  "freecodecamp-courses.autoStart": true,
  // Automatically adjust the terminal logs if used
  "freecodecamp-courses.prepare": "sed -i \"s#WD=.*#WD=$(pwd)#g\" ./bash/.bashrc",
  // Command run in terminal on `freeCodeCamp: Develop Course`
  "freecodecamp-courses.scripts.develop-course": "NODE_ENV=development npm run start",
  // Command run in terminal on `freeCodeCamp: Run Course`
  "freecodecamp-courses.scripts.run-course": "NODE_ENV=production npm run start",
  // Preview to open when course starts
  "freecodecamp-courses.workspace.previews": [
    {
      "open": true,
      "url": "http://localhost:8080",
      "showLoader": true,
      "timeout": 4000
    }
  ],
  // The below settings are needed for using the terminal feature
  "terminal.integrated.defaultProfile.linux": "bash",
  "terminal.integrated.profiles.linux": {
    "bash": {
      "path": "bash",
      "icon": "terminal-bash",
      "args": ["--init-file", "./bash/sourcerer.sh"]
    }
  }
}

A few more settings are available, and can be seen and configured from the VSCode Settings UI.

CLI

Installation

Releases

Locate your platform in the releases section and download the latest version.

cargo

Requires Rust to be installed: https://www.rust-lang.org/tools/install

cargo install create-freecodecamp-os-app

Usage

To create a new course with some boilerplate:

create-freecodecamp-os-app

To add a project to an existing course:

create-freecodecamp-os-app add-project

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.

Examples

If you create a course using @freecodecamp/freecodecamp-os, open a pull request to add it to this list.

freeCodeCamp.org

Web3

Rust

Configuration

freecodecamp.conf.json

Required Configuration

{
  "version": "0.0.1",
  "config": {
    "projects.json": "<PROJECTS_JSON>",
    "state.json": "<STATE_JSON>"
  },
  "curriculum": {
    "locales": {
      "<LOCALE>": "<LOCALE_DIR>"
    }
  }
}

Minimum Usable Example

{
  "version": "0.0.1",
  "config": {
    "projects.json": "./config/projects.json",
    "state.json": "./config/state.json"
  },
  "curriculum": {
    "locales": {
      "english": "./curriculum/locales/english"
    }
  }
}

Optional Configuration (Features)

port

By default, the server and client communicate over port 8080. To change this, add a port key to the configuration file:

Example

{
  "port": 8080
}

client

  • assets.header: path relative to the root of the course - string
  • assets.favicon: path relative to the root of the course - string
  • landing.<locale>.description: description of the course shown on the landing page - string
  • landing.<locale>.title: title of the course shown on the landing page - string
  • landing.<locale>.faq-link: link to the FAQ page - string
  • landing.<locale>.faq-text: text to display for the FAQ link - string
  • static: static resources to serve - string | string[] | Record<string, string> | Record<string, string>[]

Example

{
  "client": {
    "assets": {
      "header": "./client/assets/header.png",
      "favicon": "./client/assets/favicon.ico"
    },
    "static": ["./curriculum/", { "/images": "./curriculum/images" }]
  }
}

config

  • projects.json: path relative to the root of the course - string
  • state.json: path relative to the root of the course - string

Example

{
  "config": {
    "projects.json": "./config/projects.json",
    "state.json": "./config/state.json"
  }
}

curriculum

  • locales: an object of locale names and their corresponding paths relative to the root of the course - Record<string, string>
  • assertions: an onject of locale names and their corresponding paths to a JSON file containing custom assertions - string

Example

{
  "curriculum": {
    "locales": {
      "english": "./curriculum/locales/english"
    },
    "assertions": {
      "afrikaans": "./curriculum/assertions/afrikaans.json"
    }
  }
}

Attention

Currently, english is a required locale, and is used as the default.

hotReload

  • ignore: a list of paths to ignore when hot reloading - string[]

Example

{
  "hotReload": {
    "ignore": [".logs/.temp.log", "config/", "/node_modules/", ".git"]
  }
}

tooling

  • helpers: path relative to the root of the course - string
  • plugins: path relative to the root of the course - string

Example

{
  "tooling": {
    "helpers": "./tooling/helpers.js",
    "plugins": "./tooling/plugins.js"
  }
}

projects.json

Definitions

  • id: A unique, incremental integer - number
  • dashedName: The name of the project corresponding to the curriculum/locales/<PROJECT_DASHED_NAME>.md file - string
  • isIntegrated: Whether or not to treat the project as a single-lesson project - boolean (default: false)
  • 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)
  • currentLesson: The current lesson of the project - number (default: 1)
  • runTestsOnWatch: Whether or not to run tests on file change - boolean (default: false)
  • isResetEnabled: Whether or not to enable the reset button - boolean (default: false)
  • numberOfLessons: The number of lessons in the project - number1
  • seedEveryLesson: Whether or not to run the seed on lesson load - boolean (default: false)
  • blockingTests: Run tests synchronously - boolean (default: false)
    • breakOnFailure: Stop running tests on the first failure - boolean (default: false)
1

This is automagically calculated when the app is launched.

Required Configuration

[
  {
    "id": 0, // Unique ID
    "dashedName": "<PROJECT_DASHED_NAME>"
  }
]

Optional Configuration

Example

[
  {
    "id": 0,
    "dashedName": "learn-x-by-building-y",
    "isIntegrated": false,
    "isPublic": false,
    "currentLesson": 1,
    "runTestsOnWatch": false,
    "isResetEnabled": false,
    "numberOfLessons": 10,
    "seedEveryLesson": false,
    "blockingTests": false,
    "breakOnFailure": false
  }
]

.gitignore

Retaining Files When a Step is Reset

Warning

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.

Project Syntax

This is the Markdown syntax used to create projects in the curriculum using the default parser. The parser can be configured using the plugin-system.

Markers

# <TITLE>

# <TITLE>

The first paragraph is used as the description of the project. The first json code block is used for extra metadata such as tags:

Example

# Learn X by Building Y

```json
{
  "tags": ["Coming Soon!"]
}
```

This is a description.

## <N>

## <LESSON_NUMBER>

Zero-based numbering, because of course

Example

## 0

meta

## <N>

```json
{
  "watch": ["path/relative/to/root"],
  "ignore": ["path/relative/to/root"]
}
```

The meta.watch field is used to specify specific files to watch during a lesson. The meta.ignore field is used to specify specific files to ignore during a lesson. The watcher is affected once on lesson load.

The watch and ignore fields are optional. It does not make sense to provide both at the same time.

### --description--

### --description--

<DESCRIPTION_CONTENT>

Example

### --description--

This is the description content.

### --tests--

### --tests--

<TEST_TEXT>

```js
<TEST_CODE>
```

Example

### --tests--

You should ...

```js
await new Promise(resolve => setTimeout(resolve, 2000));
assert.equal(true, true);
```

### --seed--

### --seed--

#### --"<FILE_PATH>"--

```<FILE_EXTENSION>
<FILE_CONTENT>
```

#### --cmd--

```bash
<COMMAND>
```

Example

#### --seed--

#### --"index.js"--

```js
// I am an example boilerplate file
```

#### --cmd--

```bash
npm install
```

### --hints--

### --hints--

#### 0

Markdown-valid hint

#### 1

A second Markdown hint with code:

```rust
impl Developer for Camper {
  fn can_code(&self) -> bool {
    true
  }
}
```

#### --force--

Any seed marked with the force flag will overwrite the seedEveryLesson configuration option. Specifically, the force flag causes the seed to run, if it were not going to, and it prevents the seed from running, if it were going to.

### --seed--

#### --force--

<!-- Rest of seed -->

Attention

The force flag is ignored when the whole project is reset.

## --fcc-end--

An EOF discriminator.

## --fcc-end--

Example

curriculum/locales/english/learn-x-by-building-y.md

# Learn X by Building Y

In this course, you will learn X by building y.

## 0

### --description--

Declare a variable `a` with value `1`, in `index.js`.

```javascript
const a = 1;
```

### --tests--

You should declare a variable named `a`.

```js
const test = `console.assert(typeof a);`;
const filePath = 'learn-x-by-building-y/index.js';
const cb = (stdout, stderr) => {
  assert.isEmpty(stderr);
  assert.exists(stdout);
};
await __helpers.javascriptTest(filePath, test, cb);
```

You should give `a` a value of `1`.

```js
const test = `console.assert(a === 1, \`expected \${a} to equal 1\`);`;
const filePath = 'learn-x-by-building-y/index.js';
const cb = (stdout, stderr) => {
  assert.isEmpty(stderr);
  assert.exists(stdout);
};
await __helpers.javascriptTest(filePath, test, cb);
```

### --seed--

#### --"index.js"--

```js
// I am an example boilerplate file
```

## 1

```json
{
  "watch": ["learn-x-by-building-y/test/index.js"]
}
```

### --description--

Create a new directory named `test`, and create a file `test/index.ts`.

Then add the following:

```ts
const test: string = 'test';
```

### --tests--

You should ...

```js
await new Promise(resolve => setTimeout(resolve, 2000));
assert.equal(true, true);
```

### --seed--

#### --"index.js"--

```javascript
// I am an example boilerplate file
const a = 1;
```

## 2

### --description--

Description.

### --tests--

You should ...

```js
await new Promise(resolve => setTimeout(resolve, 2000));
assert.equal(true, true);
```

### --seed--

#### --cmd--

```bash
echo "I should run first"
```

#### --cmd--

```bash
mkdir test
```

#### --cmd--

```bash
touch test/index.ts
```

#### --"test/index.ts"--

```ts
const test: string = 'test';
```

## 3

### --description--

Description.

### --tests--

You should ...

```js
await new Promise(resolve => setTimeout(resolve, 2000));
assert.equal(true, true);
```

## 4

### --description--

Description.

### --tests--

You should ...

```js
await new Promise(resolve => setTimeout(resolve, 2000));
assert.equal(true, true);
```

## 5

### --description--

Description.

### --tests--

This test will pass after 5 seconds.

```js
await new Promise(resolve => setTimeout(resolve, 5000));
assert.equal(1, 1);
```

## --fcc-end--

It is also possible to add the seed for a lesson in a separate file named <PROJECT_DASHED_NAME>-seed.md within the locales directory.

curriculum/locales/english/learn-x-by-building-y-seed.md

## 0

### --seed--

#### --"index.js"--

```javascript
// Seed in a separate file
```

## --fcc-end--

Note

If seed for the same lesson is included in both the project file and a seed file, the seed in the project file will be used.

freeCodeCamp - Courses

The freeCodeCamp - Courses VSCode Extension makes working with freecodecamp-os in VSCode feature-rich.

Features

Commands

freeCodeCamp: Develop Course

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.

Also, with NODE_ENV=development, your workspace is validated following: https://github.com/freeCodeCamp/freeCodeCampOS/blob/main/.freeCodeCamp/tooling/validate.js

freeCodeCamp: Run Course

Runs the run-course script in the freecodecamp.conf.json of the current workspace.

freeCodeCamp: Shutdown Course

Disposes all terminals, and closes the visible text editors.

freeCodeCamp: Open Course

Within a new directory, this command shows the available courses to clone and then clones the selected course in the current workspace.

Lifecycle

The lifecycle of a lesson follows:

  1. Server parses lesson from curriculum Markdown file
  2. Server sends client lesson data
  3. Client renders lesson as HTML

Lesson

Todo

Lifecycle

The lifecycle of the testing system follows:

  1. Server parses test from curriculum Markdown file
  2. Server evaluates any --before-all-- ops
  • If any --before-all-- ops fail, an error is printed to the console
  • If any --before-all-- ops fail, the tests stop running
  1. Server evaluates all tests in parallel1
    1. Server evaluates any --before-each-- ops
      1. If any --before-each-- ops fail, test code is not run
    2. Server evaluates the test
    3. Server evaluates any --after-each-- ops
  2. Server evaluates any --after-all-- ops
  • If any --after-all-- ops fail, an error is printed to the console
1

Tests can be configured to run in order, in a blocking fashion with the blockingTests configuration option.

Test Utilities

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.

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.

controlWrapper

Wraps a function in an interval to retry until it does not throw or times out.

function controlWrapper(
  cb: () => any,
  { timeout = 10_000, stepSize = 250 }
): Promise<ReturnType<cb> | null>;

The callback function must throw for the control wrapper to re-try.

const cb = async () => {
  const flakyFetch = await fetch('http://localhost:3123');
  return flakyFetch.json();
};
const result = await __helpers.controlWrapper(cb);

getBashHistory

Get the .logs/.bash_history.log file contents

Safety

Throws if file does not exist, or if read permission is denied.

function getBashHistory(): Promise<string>;
const bashHistory = await __helpers.getBashHistory();

getCommandOutput

Returns the output of a command called from the given path relative to the root of the workspace.

Safety

Throws if path is not a valid POSIX/DOS path, and if promisified exec throws.

function getCommandOutput(
  command: string,
  path = ''
): Promise<{ stdout: string; stderr: string } | Error>;
const { stdout, stderr } = await __helpers.getCommandOutput('ls');

getCWD

Get the .logs/.cwd.log file contents

Safety

Throws if file does not exist, or if read permission is denied.

function getCWD(): Promise<string>;
const cwd = await __helpers.getCWD();

getLastCommand

Get the \(n^{th}\) latest line from .logs/.bash_history.log.

Safety

Throws if file does not exist, or if read permission is denied.

function getLastCommand(n = 0): Promise<string>;
const lastCommand = await __helpers.getLastCommand();

getLastCWD

Get the \(n^{th}\) latest line from .logs/.cwd.log.

Safety

Throws if file does not exist, or if read permission is denied.

function getLastCWD(n = 0): Promise<string>;
const lastCWD = await __helpers.getLastCWD();

getTemp

Get the .logs/.temp.log file contents.

Safety

Throws if file does not exist, or if read permission is denied.

function getTemp(): Promise<string>;
const temp = await __helpers.getTemp();

Note

Use the output of the .temp.log file at your own risk. This file is raw input from the terminal including ANSI escape codes.

Output varies depending on emulator, terminal size, order text is typed, etc. For more info, see https://github.com/freeCodeCamp/solana-curriculum/issues/159

getTerminalOutput

Get the .logs/.terminal_out.log file contents.

Safety

Throws if file does not exist, or if read permission is denied.

function getTerminalOutput(): Promise<string>;
const terminalOutput = await __helpers.getTerminalOutput();

importSansCache

Import a module side-stepping Nodejs' cache - cache-busting imports.

Safety

Throws if path is not a valid POSIX/DOS path, and if the import throws.

function importSansCache(path: string): Promise<any>;
const { exportedFile } = await __helpers.importSansCache(
  'learn-x-by-building-y/index.js'
);

Globals

None of the globals are within the __helpers module.

chai

assert

The assert module: https://www.chaijs.com/api/assert/

expect

The expect module: https://www.chaijs.com/api/bdd/

config as chaiConfig

The config module: https://www.chaijs.com/guide/styles/#configuration

AssertionError

This is the AssertionError class from the assert module.

logover

The logger used by freecodecamp-os: https://www.npmjs.com/package/logover

This is mostly useful for debugging, as any logs will be output in the freeCodeCamp terminal.

ROOT

The root of the workspace.

watcher

Note

This is only available in the beforeAll and beforeEach context - on the main thread.

The Chokidar FSWatcher instance.

This is useful if you want to stop watching a directory during a test:

Example

const DIRECTORY_PATH_RELATIVE_TO_ROOT = "example";
watcher.unwatch(DIRECTORY_PATH_RELATIVE_TO_ROOT);
// Do something
watcher.add(DIRECTORY_PATH_RELATIVE_TO_ROOT);

Collisions

As the tests are run in the evaled 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).

Lifecycle

Resetting can follow one of two lifecycles:

  1. Whole project reset
  2. Lesson reset

Whole Project

A whole project reset is only invoked when the Reset button is clicked in the client.

This will run a git clean on the project directory - removing all files (tracked and untracked), but resetting them to their last committed state.

Then, the seed of each lesson will be run in order.

Lesson

A lesson reset only happens when either seedEveryLesson is set to true in the project config, or the force flag is set on a given lessons seed.

This will only run the seed for the current lesson.

Reset

Todo

Plugin System

The plugin system is a way to hook into events during the runtime of the application.

Plugins are defined within the JS file specified by the tooling.plugins configuration option.

Hooks

onTestsStart

Called when the tests start running, before any --before- hooks.

onTestsEnd

Called when the tests finish running, after all --after- hooks.

onProjectStart

Called when the project first loads, before any tests are run, and only happens once per project.

onProjectFinished

Called when the project is finished, after all tests are run and passed, and only happens once per project.

onLessonPassed

Called when a lesson passes, after all tests are run and passed, and only happens once per lesson.

onLessonFailed

Called when a lesson fails, after all tests are run and any fail.

onLessonLoad

Called once when a lesson is loaded, after the onProjectStart if the first lesson.

Parser

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 section.

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.

It is up to the parser to read, parse, and return the data in the format expected by the application.

getProjectMeta

(projectDashedName: string) =>
  Promise<{
    title: string;
    description: string;
    numberOfLessons: number;
    tags: string[];
  }>;

The title, tags, and description fields are expected to be either plain strings, or HTML strings which are then rendered in the client.

getLesson

Attention

This function can be called multiple times per lesson. Therefore, it is expected to be idempotent.

(projectDashedName: string, lessonNumber: number) =>
  Promise<{
    meta?: { watch?: string[]; ignore?: string[] };
    description: string;
    tests: [[string, string]];
    hints: string[];
    seed: [{ filePath: string; fileSeed: string } | string];
    isForce?: boolean;
    beforeAll?: string;
    afterAll?: string;
    beforeEach?: string;
    afterEach?: string;
  }>;

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.

The description field is expected to be either a plain string, or an HTML string which is then rendered in the client.

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.

The hints field is expected to be an array of plain strings, or an array of HTML strings.

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.

The seed[] field can also be a plain string, which is then treated as a bash command to be run in the workspace root.

An example of this can be seen in the default parser used: https://github.com/freeCodeCamp/freeCodeCampOS/blob/main/.freeCodeCamp/plugin/index.js

Example

import { pluginEvents } from "@freecodecamp/freecodecamp-os/.freeCodeCamp/plugin/index.js";

pluginEvents.onTestsStart = async (project, testsState) => {
  console.log('onTestsStart');
};

pluginEvents.onTestsEnd = async (project, testsState) => {
  console.log('onTestsEnd');
};

pluginEvents.onProjectStart = async project => {
  console.log('onProjectStart');
};

pluginEvents.onProjectFinished = async project => {
  console.log('onProjectFinished');
};

pluginEvents.onLessonFailed = async project => {
  console.log('onLessonFailed');
};

pluginEvents.onLessonPassed = async project => {
  console.log('onLessonPassed');
};

Client Injection

With the static config option, you can add a /script/injectable.js script to be injected in the head of the client.

Example

{
  "client": {
    "static": {
      "/script/injectable.js": "./client/injectable.js"
    }
  }
}

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:

  • ROOT: The root of the course
  • join: The Node.js path module function

This enables scripts like the following to be run:

client/injectable.js

function checkForToken() {
  const serverTokenCode = `
    try {
      const {readFile} = await import('fs/promises');
      const tokenFile = await readFile(join(ROOT, 'config/token.txt'));
      const token = tokenFile.toString();
      console.log(token);
      __result = token;
    } catch (e) {
      console.error(e);
      __result = null;
    }`;
  socket.send(
    JSON.stringify({
      event: '__run-client-code',
      data: serverTokenCode
    })
  );
}

async function askForToken() {
  const modal = document.createElement('dialog');
  const p = document.createElement('p');
  p.innerText = 'Enter your token';
  p.style.color = 'black';
  const input = document.createElement('input');
  input.type = 'text';
  input.id = 'token-input';
  input.style.color = 'black';
  const button = document.createElement('button');
  button.innerText = 'Submit';
  button.style.color = 'black';
  button.onclick = async () => {
    const token = input.value;
    const serverTokenCode = `
      try {
        const {writeFile} = await import('fs/promises');
        await writeFile(join(ROOT, 'config/token.txt'), '${token}');
        __result = true;
      } catch (e) {
        console.error(e);
        __result = false;
      }`;
    socket.send(
      JSON.stringify({
        event: '__run-client-code',
        data: serverTokenCode
      })
    );
    modal.close();
  };

  modal.appendChild(p);
  modal.appendChild(input);
  modal.appendChild(button);
  document.body.appendChild(modal);
  modal.showModal();
}

const socket = new WebSocket(
  `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${
    window.location.host
  }`
);

window.onload = function () {
  socket.onmessage = function (event) {
    const parsedData = JSON.parse(event.data);
    if (
      parsedData.event === 'RESPONSE' &&
      parsedData.data.event === '__run-client-code'
    ) {
      if (parsedData.data.error) {
        console.log(parsedData.data.error);
        return;
      }
      const { __result } = parsedData.data;
      if (!__result) {
        askForToken();
        return;
      }
      window.__token = __result;
    }
  };
  let interval;
  interval = setInterval(() => {
    if (socket.readyState === 1) {
      clearInterval(interval);
      checkForToken();
    }
  }, 1000);
};

Contributing

Local Development

  1. Open freeCodeCampOS/self as a new workspace in VSCode
  2. Run npm i
  3. Run freeCodeCamp: Develop Course in the command palette

Gitpod

  1. Open the project in Gitpod:

Open in Gitpod

Opening a Pull Request

  1. Fork the repository
  2. Push your changes to your fork
  3. Open a pull request with the recommended style

Commit Message

<type>(<scope>): <description>

Example

feat(docs): add contributing.md

Pull Request Title

<type>(<scope>): <description>

Example

feat(docs): add contributing.md

Pull Request Body

Answer the following questions:

  • What does this pull request do?
  • How should this be manually tested?
  • Any background context you want to provide?
  • What are the relevant issues?
  • Screenshots (if appropriate)

Types

  • fix
  • feat
  • refactor
  • chore

Scopes

Any top-level directory or config file. Changing a package should have a scope of dep or deps.

Documentation

This documention is built using mdBook. Read their documentation to install the latest version.

Also, the documentation uses mdbook-admonish:

cargo install mdbook-admonish

Serve the Documentation

cd docs
mdbook serve

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.

Build the Documentation

cd docs
mdbook build

CLI (create-freecodecamp-os-app)

The CLI is written in Rust, and is located in the cli directory.

Development

$ cd cli
cli$ cargo run

Flight Manual

Release

Releases are done manually through the GitHub Actions.

Making a Release

In the Actions tab, select the Publish to npm workflow. Then, select Run workflow.

Changelog

[3.6.0] -

Add

  • Use bash's script command to record terminal input and output
    • Gated behind a feature flag
    • Existing .logs/ files will be deprecated in favour of script command in 4.0

[3.5.1] - 2024-03-19

Fix

  • Remove watcher from context in worker

[3.5.0] - 2024-03-18

Add

  • meta to getLesson
    • meta.watch and meta.ignore to alter watch behaviour when lesson loads

Fix

  • Add / to end of .git in defaultPathsToIgnore to prevent files starting with .git from being ignored
  • Trim description and title fields from parser

[3.4.1] - 2024-03-11

Fix

  • Convert Error class to object in worker before sending to parent

[3.4.0] - 2024-03-05

Add

  • pluginEvents.onLessonLoad
  • Loader showing progress for reset step

[3.3.0] - 2024-02-15

Add

  • tags to getProjectMeta

[3.2.0] - 2024-02-12

Add

  • Parser API for curriculum files
    • pluginEvents.getProjectMeta
    • pluginEvents.getLesson

Fix

  • Refactor original regex Markdown parser to use marked.lexer
    • This allows for more complex Markdown files to be parsed
    • Render Markdown in server
    • Pass HTML string to client to render

[3.1.0] - 2024-02-05

Add

  • client.landing.locale.title field for landing page h1

Fix

  • Give .description element a max-width of 750px
  • Add ws as dependency

[3.0.0] - 2024-02-01

Change

  • Remove freecodecamp.conf.json fields controlled by freecodecamp-courses extension
  • Allow hints for integrated projects
  • Replace use of FCC_OS_PORT with port field in freecodecamp.conf.json
  • Make version field required in freecodecamp.conf.json
  • Move project title and description to curriculum markdown files
  • Rename .terminal-out.log to .terminal_out.log

Migration Guide

  1. Configure the following settings for the freeCodeCamp - Courses extension, and remove them from the freecodecamp.conf.json file:
  • "freecodecamp-courses.autoStart"
  • "freecodecamp-courses.path"
  • "freecodecamp-courses.prepare"
  • "freecodecamp-courses.scripts.develop-course"
  • "freecodecamp-courses.scripts.run-course"
  • "freecodecamp-courses.workspace.files"
  • "freecodecamp-courses.workspace.previews"
  • "freecodecamp-courses.workspace.terminals"
  1. Instead of FCC_OS_PORT environment variable, use port field in freecodecamp.conf.json file
  2. Add a SemVer compliant version field to freecodecamp.conf.json file
  3. Remove the title and description fields in the project.json, and add the description to each corresponding Markdown file immediately after the title
  4. Rename the .terminal-out.log file to .terminal_out.log

[2.1.0] - 2024-01-23

Add

  • Worker threads to run tests in parallel
  • ### --after-each-- to run code after each test
  • Cancel Tests button that terminates all workers
  • Plugin system for events:
    • onTestsStart
    • onTestsEnd
    • onProjectStart
    • onLessonPassed
    • onLessonFailed
    • onProjectFinished
  • /script/injectable.js static file to inject a JS script into the client
  • __run-client-code websocket event to run code in the server's context
  • Add create-freecodecamp-os-app cli for creating the boilerplate

Update

  • dependency @types/node to v18.18.13
  • dependency @types/react-dom to v18.2.17
  • dependency typescript to v5.3.2
  • dependency ts-loader to v9.5.1
  • dependency marked-highlight to v2.0.7
  • dependency marked to v9.1.6
  • babel monorepo to v7.23.3
  • dependency @types/prismjs to v1.26.3
  • dependency @types/react to v18.2.36
  • actions/setup-node digest to 1a4442c (#380)
  • dependency chai to v4.3.10
  • dependency @types/marked to v5.0.2

[2.0.0 - deprecated] - 2023-09-25

Add

  • make watcher global
  • process.env.FCC_OS_PORT || 8080 for server listen port
  • working hints

Change

  • remove __helpers.makeDirectory
  • remove __helpers.runCommand
  • remove __helpers.writeJsonFile
  • remove __helpers.getDirectory
  • remove __helpers.getFile
  • remove __helpers.getJsonFile
  • remove __helpers.copyDirectory
  • remove __helpers.copyProjectFiles
  • remove __helpers.fileExists
  • update controlWrapper to match documented API
  • start lessons at 0 instead of 1
  • remove landing page topic (h2)
  • config.path is no longer required
  • Remove postinstall script
  • Tests no longer have --before-all-- context

Update

  • dependency babel-loader to v9.1.2
  • dependency marked to v9
    • Markedjs had multiple major releases within 2 months
  • dependency typescript to v5.0.4
  • dependency webpack-cli to v5.1.1

Migration Guide

  1. Refactor tests to use Nodejs API instead of removed __helpers functions.
  2. Change all lesson numbers to be zero-based (start at 0)
  3. Manually build client before running tooling server (npm run build:client)
    1. Suggestion: Add cd ./node_modules/@freecodecamp/freecodecamp-os/ && npm run build:client to freecodecamp.conf.json > prepare
  4. Change --before-all-- into --before-each--
    1. Probably remove --after-all--
    2. No longer use global in tests

[1.10.0] - 2023-08-08

Add

  • config.client.static to serve files (e.g. images) in client

[1.9.2] - 2023-06-17

Fix

  • remove seeding files from watch during seeding

Update

  • react monorepo (#313)
  • github actions (#311)
  • react monorepo
  • dependency @types/node to v18.16.18

[1.9.1] - 2023-05-30

Fix

  • fix 1.9.0 introduced bug of hanging tests

Update

  • dependency webpack-dev-server to v4.15.0
  • dependency @types/react to v18.2.6
  • dependency @types/marked to v4.3.0
  • react monorepo
  • dependency @types/node to v18.16.5
  • dependency @babel/core to v7.21.8
  • babel monorepo to v7.21.5
  • pin dependencies (#241)

[1.9.0] - 2023-05-20

Fix

  • adjust build path
  • set $HOME for Gitpod

Add

  • add blockingTests flag
  • add breakOnFailure flag

Bugs

  • when blockingTests && breakOnFailure, proceeding tests appear to hang in client

[1.8.4] - 2023-04-19

Fix

  • seed files on lesson (#237)

Update

  • dependency webpack-dev-server to v4.13.3
  • dependency html-webpack-plugin to v5.5.1
  • dependency @types/react to v18.0.35
  • dependency @types/node to v18.15.11
  • babel monorepo to v7.21.4

[1.8.3] - 2023-03-30

Fix

  • adjust import pathing (#225)

Update

  • dependency @types/node to v18.15.10
  • dependency marked to v4.3.0
  • dependency nodemon to v2.0.22
  • dependency @types/node to v18.15.9
  • dependency @types/node to v18.15.8
  • dependency @types/react to v18.0.29
  • dependency webpack-dev-server to v4.13.1
  • dependency webpack-dev-server to v4.13.0
  • dependency style-loader to v3.3.2
  • dependency @types/node to v18.15.3
  • dependency @babel/core to v7.21.3
  • dependency @types/node to v18.15.0
  • dependency nodemon to v2.0.21
  • dependency @types/node to v18.14.6

[1.8.2] - 2023-03-03

Fix

  • parse crlf line endings (#210)

[1.8.1] - 2023-03-02

Fix

  • use node_module pathing (#209)

[1.8.0] - 2023-03-02

Change

  • do not copy .freeCodeCamp into root (#208)

Update

  • dependency @types/node to v18.14.2
  • babel monorepo to v7.21.0
  • dependency @types/node to v18.14.1
  • dependency @types/node to v18.14.0
  • dependency @types/react-dom to v18.0.11
  • dependency @types/react to v18.0.28
  • dependency @types/node to v18.13.0

[1.7.3] - 2023-02-08

Fix

  • handle completed projects (#197)
  • set default terminal (#200)

Update

  • dependency @types/node to v18.11.19
  • dependency typescript to v4.9.5

[1.7.2] - 2023-01-31

Fix

  • apply css to all code blocks (#196)

[1.7.1] - 2023-01-31

Add

  • validate curriculum on develop (#182)

Update

  • dependency @types/react to v18.0.27
  • dependency marked to v4.2.12
  • dependency @babel/core to v7.20.12
  • dependency @types/node to v18.11.18
  • dependency marked to v4.2.5
  • dependency @types/react-dom to v18.0.10
  • dependency @babel/core to v7.20.7
  • dependency @types/node to v18.11.17
  • dependency css-loader to v6.7.3
  • dependency @types/node to v18.11.16

[1.7.0] - 2023-01-31

Add

  • create cache-busting helper (#181)

Update

  • dependency @types/node to v18.11.13
  • dependency typescript to v4.9.4
  • dependency marked to v4.2.4
  • dependency @types/node to v18.11.12

[1.6.9] - 2022-12-08

Fix

  • pass WebSocket to reset function (#174)

[1.6.8] - 2022-12-07

Change

  • remove .env file from npm (#173)

[1.6.7] - 2022-12-07

Add

  • create mkdir helper function (#172)

[1.6.6] - 2022-12-06

Fix

  • handle file parser errors (#166)

Update

  • readme docs (#160)
  • dependency ts-loader to v9.4.2
  • dependency @types/react to v18.0.26
  • dependency @types/node to v18.11.10
  • dependency @babel/core to v7.20.5
  • dependency typescript to v4.9.3
  • dependency marked to v4.2.3
  • dependency @types/react-dom to v18.0.9
  • dependency css-loader to v6.7.2
  • dependency chai to v4.3.7
  • dependency marked to v4.2.2
  • babel monorepo
  • dependency @types/react to v18.0.25
  • dependency @types/node to v18.11.9
  • dependency @types/node to v18.11.8
  • dependency @types/node to v18.11.7
  • dependency @babel/plugin-syntax-import-assertions to v7.20.0
  • dependency @types/react-dom to v18.0.8
  • dependency @types/react to v18.0.24
  • dependency @babel/core to v7.19.6
  • dependency @babel/preset-env to v7.19.4
  • dependency express to v4.18.2
  • dependency typescript to v4.8.4
  • dependency marked to v4.1.1
  • babel monorepo to v7.19.3

[1.6.5] - 2022-09-28

Fix

  • reset project, prevent lesson under/over -flow (#139)
  • patch and enable reset button (#133)
  • step skipping buttons (#138)
  • docker settings (#137)

Add

  • feat: add script for camper info (#132)

Update

  • dependency ts-loader to v9.4.1
  • dependency webpack-dev-server to v4.11.1
  • dependency @types/react to v18.0.21

Change

  • updated styles for error page (#131)

[1.6.4] - 2022-09-19

Add

  • progress to projects (#121)

Change

  • disable reset button (#128)

[1.6.3] - 2022-09-19

Add

  • create test-util to get .temp.log file (#127)

Update

  • dependency nodemon to v2.0.20
  • dependency @types/react to v18.0.20
  • babel monorepo to v7.19.1
  • pin dependency logover to 2.0.0

[1.6.2] - 2022-09-16

Fix

  • fetch project on hot-reload

[1.6.1] - 2022-09-15

Fix

  • optional chaining to hot-reload in server (#119)

[1.6.0] - 2022-09-14

Add

  • versioning
  • hot-ignore (#118)

[1.5.5] - 2022-09-12

Change

  • replace spark with fade in (#116)

Update

  • dependency webpack-dev-server to v4.11.0
  • dependency @types/react to v18.0.19
  • babel monorepo to v7.19.0
  • dependency typescript to v4.8.3
  • dependency @types/react to v18.0.18
  • dependency @types/marked to v4.0.7
  • pin dependency @types/node to 18.7.15

[1.5.4] - 2022-09-09

Change

  • UI revamp (#108)

[1.5.3] - 2022-09-07

Fix

  • correctly show/hide files in production mode (#107)

[1.5.2] - 2022-09-07

Fix

  • add controlWrapper function (#106)

[1.5.1] - 2022-09-07

Fix

  • fix npm deps and installation (#105)

[1.5.0] - 2022-09-06

Add

  • before-all and before-each hooks (#82)
  • git build script for multiple projects (#58)
  • loader config for version 1.1.1 (#44)

Change

  • all major changes (#97)
  • with small improvements and updates (#54)
  • Improved client styling (#42)

Fix

  • ignoring of Mac files (#60)
  • pinning Ubuntu to version 20.04 (#53)
  • updating of dependency marked to v4.0.19 (598e2e7)
  • updating of dependency logover to v1.3.5 (1866487)
  • updating of dependency logover to v1.3.4 (513d189)
  • updating of dependency ws to v8.8.1 (ece46b5)
  • updating of dependency marked to v4.0.18 (a13a865)
  • updating of dependency logover to v1.3.2 (e1e6b10)
  • updating of dependency nodemon to v2.0.19 (459d450)
  • updating of dependency marked to v4.0.17 (bce9486)
  • updating of dependency ws to v8.8.0 (fc52646)
  • updating of dependency ws to v8.7.0 (36fc630)
  • updating of dependency ws to v8.6.0 (9ad962a)
  • updating of dependency express to v4.18.1 (0b7e21f)
  • updating of dependency prismjs to v1.28.0 (29734ff)
  • updating of dependency marked to v4.0.14 (17856ed)
  • updating of dependency marked to v4.0.13 (e482c37)
  • pinning of dependency nodemon to v2.0.15 (73e53e5)

Update

  • dependency marked to v4.0.19 (598e2e7)
  • dependency @types/react to v18.0.17 (4ca81e9)
  • dependency @types/react to v17.0.48 (7e68a76)
  • dependency @types/react to v17.0.47 (f419a80)
  • dependency @babel/core to v7.18.5 (f94eec6)
  • dependency @types/react to v17.0.45 (f44ab1c)
  • dependency @types/react-dom to v17.0.17 (014323f)
  • dependency @types/react-dom to v17.0.16 (f496e14)
  • dependency ts-loader to v9.3.1 (a4bb535)
  • dependency nodemon to v2.0.18 (c1540dd)
  • dependency typescript to v4.7.4 (a2ba270)
  • dependency typescript to v4.7.3 (f5cb880)
  • dependency typescript to v4.7.2 (29e5897)
  • dependency ts-loader to v9.3.0 (5081424)
  • dependency typescript to v4.6.4 (b1e8fe5)
  • dependency nodemon to v2.0.16 (68b6da7)
  • dependency @types/react-dom to v17.0.16 (f496e14)
  • dependency webpack-cli to v4.10.0 (85e4326)
  • dependency webpack-dev-server to v4.10.0 (b36bd79)

Roadmap

For the most part, this roadmap outlines todos for freecodecamp-os. If this roadmap is empty, then there are no todos 🎉

Documentation

Features

  • Loader to show progress of "Reset Step"
  • Crowdin translation integration