├── .eslintrc.json ├── .github ├── CODEOWNERS ├── release.yml └── workflows │ ├── build.yml │ └── deploy.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── images ├── courses-list.png ├── landing-page.png ├── logo-128X128.png ├── open-course.png ├── opening-example-course.png ├── opening-rust-in-replit.png └── solana-project.png ├── package-lock.json ├── package.json ├── renovate.json ├── resources └── courses.json ├── src ├── commands │ ├── collapse.ts │ ├── develop-course.ts │ ├── open-course.ts │ └── run-course.ts ├── components.ts ├── extension.ts ├── fixture.ts ├── flash.ts ├── handles.ts ├── index.ts ├── inputs.ts ├── loader.ts ├── typings.ts └── usefuls.ts ├── tsconfig.json └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------- 2 | # CODEOWNERS - For automated review request for 3 | # high impact files. 4 | # 5 | # Important: The order in this file cascades. 6 | # 7 | # https://help.github.com/articles/about-codeowners 8 | # ------------------------------------------------- 9 | 10 | # ------------------------------------------------- 11 | # All files are owned by Shaun 12 | # ------------------------------------------------- 13 | 14 | * @ShaunSHamilton 15 | 16 | # --- Owned by none (negate rule above) --- 17 | 18 | package.json 19 | package-lock.json 20 | 21 | # ------------------------------------------------- 22 | # All files in the root are owned by Shaun 23 | # ------------------------------------------------- 24 | 25 | /* @ShaunSHamilton 26 | 27 | # --- Owned by none (negate rule above) --- 28 | 29 | /package.json 30 | /package-lock.json 31 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | authors: 6 | - renovate 7 | - renovate[bot] 8 | categories: 9 | - title: Breaking Changes 🛠 10 | labels: 11 | - major 12 | - title: Exciting New Features and Bugs 🎉 13 | labels: 14 | - minor 15 | - title: Bug Removals 16 | labels: 17 | - patch 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Test 2 | 3 | # On push to main, when one of the following labels exists: `patch`, `minor`, `major` 4 | on: 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | if: ${{ contains(github.event.pull_request.labels.*.name, 'patch') || contains(github.event.pull_request.labels.*.name, 'minor') || contains(github.event.pull_request.labels.*.name, 'major') }} 12 | name: Build Test 13 | runs-on: ubuntu-22.04 14 | 15 | steps: 16 | - name: Checkout Source Files 17 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 18 | - name: Use Node.js 22.x 19 | uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3 20 | with: 21 | node-version: 22.x 22 | cache: "npm" 23 | 24 | - name: Install Dependencies 25 | run: npm install 26 | - name: Build Extension 27 | run: npm run vsce -- ls 28 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | env: 8 | # Specific name is required to login: https://code.visualstudio.com/api/working-with-extensions/continuous-integration 9 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 10 | 11 | jobs: 12 | build: 13 | name: Publish Extension from Release 14 | runs-on: ubuntu-22.04 15 | 16 | steps: 17 | - name: Checkout Source Files 18 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 19 | - name: Use Node.js 22.x 20 | uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3 21 | with: 22 | node-version: 22.x 23 | 24 | - name: Install Dependencies 25 | run: npm install 26 | 27 | - name: Publish 28 | run: npm run deploy 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .act.json 4 | freecodecamp-courses-*.vsix 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/**/*.js", 30 | "${workspaceFolder}/dist/**/*.js" 31 | ], 32 | "preLaunchTask": "tasks: watch-tests" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off" 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": ["$ts-webpack-watch", "$tslint-webpack-watch"], 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never", 13 | "group": "watchers" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch-tests", 23 | "problemMatcher": "$tsc-watch", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "never", 27 | "group": "watchers" 28 | }, 29 | "group": "build" 30 | }, 31 | { 32 | "label": "tasks: watch-tests", 33 | "dependsOn": ["npm: watch", "npm: watch-tests"], 34 | "problemMatcher": [] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | webpack.config.js 9 | vsc-extension-quickstart.md 10 | **/tsconfig.json 11 | **/.eslintrc.json 12 | **/*.map 13 | **/*.ts 14 | .github/** 15 | renovate.json 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the `freecodecamp-courses` extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Released] 8 | 9 | ##[3.0.1](#v3.0.1) (2025-05-20) 10 | 11 | ### Updated 12 | 13 | - `@types/node`: `22.15.20` 14 | - `@typescript-eslint/eslint-plugin`: `8.32.1` 15 | - `@typescript-eslint/parser`: `8.32.1` 16 | - `@vscode/vsce`: `3.4.1` 17 | - `eslint`: `9.27.0` 18 | - `webpack-cli`: `6.0.1` 19 | 20 | ##[3.0.0](#v3.0.0) (2024-02-01) 21 | 22 | ### Changed 23 | 24 | - `activationEvents`: 25 | - `"workspaceContains:**/freecodecamp.conf.json"` to `"onStartupFinished"` 26 | 27 | ### Added 28 | 29 | - Configuration settings in package.json: 30 | - `"freecodecamp-courses.autoStart"`: Automatically start the course when opened in VS Code (boolean, default: false) 31 | - `"freecodecamp-courses.path"`: Relative path to the directory where scripts will be run (string, default: ".") 32 | - `"freecodecamp-courses.prepare"`: Command to run on the first opening of a course (string, default: "npm install") 33 | - `"freecodecamp-courses.scripts.develop-course"`: Command to run when developing a course (string, default: "npm run develop") 34 | - `"freecodecamp-courses.scripts.run-course"`: Command to run when running a course in production (string, default: "npm run start") 35 | - `"freecodecamp-courses.workspace.files"`: Files to open in the workspace when opening a course (array of objects with "path" property) 36 | - `"freecodecamp-courses.workspace.previews"`: Previews to open in the workspace when opening a course (array of objects with "open", "showLoader", "url", and "timeout" properties) 37 | - `"freecodecamp-courses.workspace.terminals"`: Terminals to open in the workspace when opening a course (array of objects with "directory", "message", "name", and "show" properties) 38 | 39 | ### Removed 40 | 41 | - Removed the `create-new-course.ts` file and related command registration from extension.ts. 42 | - Delete `schema.json` for `freecodecamp.conf.json` 43 | 44 | ##[2.1.0](#v2.1.0) (2024-01-18) 45 | 46 | ### Added 47 | 48 | - `workspace.autoStart` configuration option 49 | - Extension activates and auto-starts if `workspace.autoStart` is `true` 50 | 51 | ##[2.0.0](#v2.0.0) (2023-09-28) 52 | 53 | ### Removed 54 | 55 | - `freeCodeCamp: Test` 56 | - `config.scripts.test` 57 | - `config.bash` 58 | 59 | ### Changed 60 | 61 | - `freeCodeCamp: Collapse` -> `freeCodeCamp Dev: Collapse` 62 | - `freeCodeCamp: Create New Course` 63 | - Does not clone a repo. Instead, uses the terminal to clone the `minimal-example` folder from `freecodecamp-os` 64 | - Require `freecodecamp.conf.json` to be in workspace root 65 | - `config.path` defaults to current workspace root 66 | 67 | ### Updated 68 | 69 | - `@types/node`: `20.7.1`, 70 | - `@types/vscode`: `1.82.0`, 71 | - `@typescript-eslint/eslint-plugin`: `6.7.3`, 72 | - `@typescript-eslint/parser`: `6.7.3`, 73 | - `@vscode/vsce`: `2.21.0`, 74 | - `eslint`: `8.50.0`, 75 | - `ts-loader`: `9.4.4`, 76 | - `typescript`: `5.2.2`, 77 | - `webpack`: `5.88.2`, 78 | - `node-fetch`: `3.3.2` 79 | 80 | ##[1.7.5](#v1.7.5) (2023-05-19) 81 | 82 | ### Updated 83 | 84 | - `@types/node` to `18.16.9` 85 | - `@types/vscode` to `1.78.0` 86 | - `@typescript-eslint/eslint-plugin` to `5.59.6` 87 | - `@typescript-eslint/parser` to `5.59.6` 88 | - `typescript` to `5.0.4` 89 | - `webpack` to `5.83.1` 90 | 91 | ##[1.7.4](#v1.7.4) (2022-03-30) 92 | 93 | ### Updated 94 | 95 | - Use `@vscode/vsce` instead of `vsce` 96 | - Update all dependencies 97 | - Year in `LICENSE` 98 | - Remove `activationEvents` from `package.json` 99 | 100 | ##[1.7.3](#v1.7.3) (2022-01-26) 101 | 102 | ### Fixed 103 | 104 | - `schema.json` definition for `freecodecamp.conf.json` 105 | 106 | ### Removed 107 | 108 | - `glob` 109 | - `@types/glob` 110 | - `mocha` 111 | - `@types/mocha` 112 | - `vscode-test-electron` 113 | - `src/test` directory 114 | 115 | ### Updated 116 | 117 | - `webpack-cli` to `5.0.1` 118 | 119 | ##[1.7.2](#v1.7.2) (2022-12-15) 120 | 121 | ### Added 122 | 123 | - `schema.json` for `freecodecamp.conf.json` 124 | 125 | ### Updated 126 | 127 | - `vscode` engine to `1.74.0` 128 | 129 | ##[1.7.1](#v1.7.1) (2022-11-07) 130 | 131 | ### Fixed 132 | 133 | - `freeCodeCamp: Run Course` checks for new version of curriculum from `raw.githubusercontent.com`. Prevent fail-fast if fetch fails. 134 | 135 | ##[1.7.0](#v1.7.0) (2022-10-24) 136 | 137 | ### Added 138 | 139 | - `freeCodeCamp: Collapse` command to collapse all matched text levels in the editor 140 | 141 | ##[1.6.1](#v1.6.1) (2022-10-22) 142 | 143 | ### Updated 144 | 145 | - `vscode` engine to `^1.72.0` 146 | - `vsce` to `2.13.0` 147 | - `typescript-eslint` monorepo to `5.40.0` 148 | - `eslint` to `8.25.0` 149 | - `typescript` to `4.8.4` 150 | 151 | ##[1.6.0](#v1.6.0) (2022-09-14) 152 | 153 | ### Added 154 | 155 | - New `freecodecamp.conf.json` property: 156 | - `version` 157 | - `hotReload.ignore` 158 | - On `freeCodeCamp: Run Course`, if a course version has been updated, a Camper is warned. 159 | 160 | ### Fixed 161 | 162 | - Added `node-fetch` dependency, because `fetch` does not appear in `vscode@1.71.0` 163 | 164 | ##[1.5.1](#v1.5.1) (2022-09-13) 165 | 166 | ### Updated 167 | 168 | - `vscode` engine to `^1.71.1` 169 | 170 | ##[1.5.0](#v1.5.0) (2022-09-06) 171 | 172 | ### Changed 173 | 174 | - `config.prepare` is no longer required 175 | - freeCodeCamp - Courses extension version is now tied to [freeCodeCampOS](https://github.com/freeCodeCamp/freeCodeCampOS) version 176 | 177 | ### Added 178 | 179 | - New `freecodecamp.conf.json` properties: 180 | - `bash` 181 | - `client` 182 | - `config` 183 | - `curriculum` 184 | - `tooling` 185 | 186 | ### Updated 187 | 188 | - `vscode` engine to `1.71.0` 189 | 190 | ##[1.4.4](#v1.4.4) (2022-08-19) 191 | 192 | ### Updated 193 | 194 | - `vscode` engine to `1.70.0` 195 | - `node` engine to `>=18` 196 | 197 | ### Changed 198 | 199 | - Removed `axios` for native Node.js `fetch` 200 | 201 | ##[1.4.0](#v1.4.0) (2022-07-23) 202 | 203 | ### Added 204 | 205 | - feat: previews ping `url` until it is available 206 | - feat: previews have `timeout` to wait for `url` to be available 207 | 208 | ### Changed 209 | 210 | - feat: loader looks more fCC -like 211 | 212 | ##[1.3.2](#v1.3.2) (2022-07-23) 213 | 214 | - chore(patch): update dependencies 215 | 216 | ##[1.3.1](#v1.3.1) (2022-06-20) 217 | 218 | ### Changed 219 | 220 | - chore(minor): minor package updates 221 | 222 | ##[1.2.3](#v1.2.3) (2022-06-20) 223 | 224 | ### Test 225 | 226 | ##[1.2.2](#v1.2.2) (2022-06-20) 227 | 228 | ### Added 229 | 230 | - fix(minor): change terminal directory if `!term` 231 | 232 | ### Fixed 233 | 234 | - chore(patch): update dependencies 235 | 236 | ##[1.2.1](#v1.2.1) (2022-06-10) 237 | 238 | ### Updated 239 | 240 | - chore(deps): update dependency @types/vscode to v1.68.0 241 | - chore(deps): update typescript-eslint monorepo to v5.27.0 242 | - chore(deps): update dependency webpack to v5.73.0 243 | - chore(deps): update dependency vsce to v2.9.1 244 | - chore(deps): update dependency eslint to v8.17.0 245 | - chore(deps): update dependency typescript to v4.7.3 246 | - chore(deps): update dependency @types/node to v14.18.20 247 | - chore(deps): update dependency glob to v7.2.3 248 | 249 | ##[1.2.0](#v1.2.0) (2022-04-30) 250 | 251 | ### Changed 252 | 253 | - Removed idea of `order` in workspace - too difficult to implement 254 | 255 | ### Fixed 256 | 257 | - Do not try change directory, if terminal has already existed 258 | 259 | ##[1.1.1](#v1.1.1) (2022-04-28) 260 | 261 | ### Fixed 262 | 263 | - Did you know, in HTML, the `className` attribute is just `class`... 🤦‍♂️ 264 | 265 | ### Changed 266 | 267 | - Re-used terminal logic to simplify loader logic 268 | 269 | ##[1.1.0](#v1.1.0) (2022-04-28) 270 | 271 | ### Added 272 | 273 | - Loader Screen to conf 274 | - `prepare` script to conf 275 | 276 | ##[1.0.2](#v1.0.2) (2022-04-27) 277 | 278 | ### Changed 279 | 280 | - `freeCodeCamp: Develop Course` and `freeCodeCamp: Start Course` 281 | - Re-uses the same terminal, if it exists 282 | 283 | ##[1.0.1](#v1.0.1) (2022-03-02) 284 | 285 | ### Added 286 | 287 | - General first patch for release 288 | - `freeCodeCamp: Test` for development 289 | 290 | ##[1.0.0](#v1.0.0) (2022-03-02) 291 | 292 | ### Added 293 | 294 | - Initial Release 🎉 🚀 295 | - Added License, README, and CHANGELOG 296 | - Added `resources/courses.json` to list available courses 297 | - Added `freecodecamp.conf.json` usage 298 | - Added the following commands: 299 | - `freeCodeCamp: Open Course` 300 | - `freeCodeCamp: Run Course` 301 | - `freeCodeCamp: Develop Course` 302 | - `freeCodeCamp: Shutdown Course` 303 | - `freeCodeCamp: Create New Course` 304 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # freeCodeCamp Courses Extension 2 | 3 | freeCodeCamp contributor's guide: https://contribute.freecodecamp.org/ 4 | 5 | To contribute a new course, see the [external-project](https://github.com/freeCodeCamp/external-project) repository. 6 | 7 | To contribute to this extension, see below. 8 | 9 | ### Developing Locally 10 | 11 | Fork the repository, then clone it to your local machine: 12 | 13 | ```bash 14 | git clone https://github.com/freeCodeCamp/courses-vscode-extension.git 15 | ``` 16 | 17 | Change into the directory, and install the dependencies: 18 | 19 | ```bash 20 | cd courses-vscode-extension 21 | npm install 22 | ``` 23 | 24 | Run the development script: 25 | 26 | ```bash 27 | npm run watch 28 | ``` 29 | 30 | `F5` should open a new VSCode window with the extension running. 31 | 32 | ### Submitting a Pull Request 33 | 34 | #### Naming Convention 35 | 36 | **VSIX Commit Message**: `patch(1.0.1): update courses url` 37 | 38 | **Pull Request Title**: `fix(patch): update courses url` 39 | 40 | Include the _vsix_ file, and specify if change is patch (`0.0.x`), minor (`0.x.0`), or major (`x.0.0`). 41 | 42 | #### Building the VSIX 43 | 44 | Run the following command with the argument being either `patch`, `minor`, or `major`: 45 | 46 | ```bash 47 | npm run pack 48 | ``` 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019-2024, freeCodeCamp.org 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # freeCodeCamp - Courses 2 | 3 | 4 | 5 | This extension helps run the freeCodeCamp courses found here: [./resources/courses.json](resources/courses.json) 6 | 7 | **NOTE TO COURSE CREATORS:** To be used in conjunction with [freeCodeCampOS](https://github.com/freeCodeCamp/freeCodeCampOS). 8 | 9 | ## Features 10 | 11 | ### How to Open A Course 12 | 13 | 1. Press `Ctrl + Shift + P` and select `freeCodeCamp: Open Course` 14 | 15 | ![Open Course](images/open-course.png) 16 | 17 | 3. Select a course from the list 18 | 19 | ![Courses List](images/courses-list.png) 20 | 21 | 4. `Ctrl + Shift + P` and select `Remote-Containers: Rebuild and Reopen in Container` 22 | 23 | 5. `Ctrl + Shift + P` and select `freeCodeCamp: Run Course` 24 | 25 | ![Opening Example Course](images/landing-page.png) 26 | 27 | ![Example Project](images/solana-project.png) 28 | 29 | --- 30 | 31 | ## Creating a Course 32 | 33 | See https://opensource.freecodecamp.org/freeCodeCampOS/ 34 | -------------------------------------------------------------------------------- /images/courses-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/courses-vscode-extension/b1cd28595b7b94fe4bd8fa41ebbe509de54095a8/images/courses-list.png -------------------------------------------------------------------------------- /images/landing-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/courses-vscode-extension/b1cd28595b7b94fe4bd8fa41ebbe509de54095a8/images/landing-page.png -------------------------------------------------------------------------------- /images/logo-128X128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/courses-vscode-extension/b1cd28595b7b94fe4bd8fa41ebbe509de54095a8/images/logo-128X128.png -------------------------------------------------------------------------------- /images/open-course.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/courses-vscode-extension/b1cd28595b7b94fe4bd8fa41ebbe509de54095a8/images/open-course.png -------------------------------------------------------------------------------- /images/opening-example-course.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/courses-vscode-extension/b1cd28595b7b94fe4bd8fa41ebbe509de54095a8/images/opening-example-course.png -------------------------------------------------------------------------------- /images/opening-rust-in-replit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/courses-vscode-extension/b1cd28595b7b94fe4bd8fa41ebbe509de54095a8/images/opening-rust-in-replit.png -------------------------------------------------------------------------------- /images/solana-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCodeCamp/courses-vscode-extension/b1cd28595b7b94fe4bd8fa41ebbe509de54095a8/images/solana-project.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "activationEvents": [ 3 | "onStartupFinished" 4 | ], 5 | "author": "freeCodeCamp", 6 | "bugs": { 7 | "url": "https://github.com/freeCodeCamp/courses-vscode-extension/issues" 8 | }, 9 | "categories": [ 10 | "Education" 11 | ], 12 | "contributes": { 13 | "commands": [ 14 | { 15 | "command": "freecodecamp-courses.openCourse", 16 | "title": "freeCodeCamp: Open Course" 17 | }, 18 | { 19 | "command": "freecodecamp-courses.runCourse", 20 | "title": "freeCodeCamp: Run Course" 21 | }, 22 | { 23 | "command": "freecodecamp-courses.developCourse", 24 | "title": "freeCodeCamp: Develop Course" 25 | }, 26 | { 27 | "command": "freecodecamp-courses.collapse", 28 | "title": "freeCodeCamp Dev: Collapse" 29 | }, 30 | { 31 | "command": "freecodecamp-courses.shutdownCourse", 32 | "title": "freeCodeCamp: Shutdown Course" 33 | } 34 | ], 35 | "configuration": { 36 | "title": "freeCodeCamp - Courses", 37 | "properties": { 38 | "freecodecamp-courses.autoStart": { 39 | "type": "boolean", 40 | "default": false, 41 | "description": "Automatically start the course when opened in VS Code" 42 | }, 43 | "freecodecamp-courses.path": { 44 | "type": "string", 45 | "default": ".", 46 | "description": "Relative path to directory where scripts will be run" 47 | }, 48 | "freecodecamp-courses.prepare": { 49 | "type": "string", 50 | "default": "npm install", 51 | "description": "Command to run on first opening a course" 52 | }, 53 | "freecodecamp-courses.scripts.develop-course": { 54 | "type": "string", 55 | "default": "npm run develop", 56 | "description": "Command to run when developing a course" 57 | }, 58 | "freecodecamp-courses.scripts.run-course": { 59 | "type": "string", 60 | "default": "npm run start", 61 | "description": "Command to run when running a course in production" 62 | }, 63 | "freecodecamp-courses.workspace.files": { 64 | "type": "array", 65 | "default": [], 66 | "description": "Files to open in the workspace when opening a course", 67 | "items": { 68 | "type": "object", 69 | "properties": { 70 | "path": { 71 | "type": "string", 72 | "description": "Relative path to file" 73 | } 74 | } 75 | } 76 | }, 77 | "freecodecamp-courses.workspace.previews": { 78 | "type": "array", 79 | "default": [ 80 | { 81 | "open": true, 82 | "showLoader": true, 83 | "url": "http://localhost:8080", 84 | "timeout": 4000 85 | } 86 | ], 87 | "description": "Previews to open in the workspace when opening a course", 88 | "items": { 89 | "type": "object", 90 | "properties": { 91 | "open": { 92 | "type": "boolean", 93 | "default": true, 94 | "description": "Whether to open the preview" 95 | }, 96 | "showLoader": { 97 | "type": "boolean", 98 | "default": true, 99 | "description": "Whether to show the loading screen" 100 | }, 101 | "url": { 102 | "type": "string", 103 | "description": "URL to open in the preview" 104 | }, 105 | "timeout": { 106 | "type": "number", 107 | "default": 4000, 108 | "description": "Timeout for URL to respond with 200" 109 | } 110 | } 111 | } 112 | }, 113 | "freecodecamp-courses.workspace.terminals": { 114 | "type": "array", 115 | "default": [], 116 | "items": { 117 | "type": "object", 118 | "properties": { 119 | "directory": { 120 | "type": "string", 121 | "description": "Relative path to directory where scripts will be run" 122 | }, 123 | "message": { 124 | "type": [ 125 | "string", 126 | "null" 127 | ], 128 | "default": null, 129 | "description": "Message to display in terminal" 130 | }, 131 | "name": { 132 | "type": "string", 133 | "description": "Name of terminal" 134 | }, 135 | "show": { 136 | "type": "boolean", 137 | "description": "Whether to show the terminal" 138 | } 139 | } 140 | }, 141 | "description": "Terminals to open in the workspace when opening a course" 142 | } 143 | } 144 | } 145 | }, 146 | "description": "Provides tooling for quick and easy selection of courses offered by freeCodeCamp", 147 | "devDependencies": { 148 | "@types/node": "22.15.29", 149 | "@types/vscode": "1.100.0", 150 | "@typescript-eslint/eslint-plugin": "8.33.0", 151 | "@typescript-eslint/parser": "8.33.0", 152 | "@vscode/vsce": "3.4.2", 153 | "eslint": "9.28.0", 154 | "ts-loader": "9.5.2", 155 | "typescript": "5.8.3", 156 | "webpack": "5.99.9", 157 | "webpack-cli": "6.0.1" 158 | }, 159 | "displayName": "freeCodeCamp - Courses", 160 | "engines": { 161 | "node": "^22.0.0", 162 | "vscode": "^1.100.0" 163 | }, 164 | "galleryBanner": { 165 | "color": "#0a0a23", 166 | "theme": "dark" 167 | }, 168 | "icon": "images/logo-128X128.png", 169 | "keywords": [ 170 | "freecodecamp", 171 | "courses", 172 | "web3", 173 | "rust", 174 | "backend" 175 | ], 176 | "license": "BSD-3-Clause", 177 | "main": "./dist/extension.js", 178 | "name": "freecodecamp-courses", 179 | "publisher": "freeCodeCamp", 180 | "repository": { 181 | "type": "git", 182 | "url": "https://github.com/freeCodeCamp/courses-vscode-extension" 183 | }, 184 | "scripts": { 185 | "compile": "webpack", 186 | "deploy": "vsce publish", 187 | "lint": "eslint src --ext ts", 188 | "pack": "vsce package", 189 | "package": "webpack --mode production --devtool hidden-source-map", 190 | "pretest": "npm run compile-tests && npm run compile && npm run lint", 191 | "test:pack": "npm run pack -- --no-git-tag-version --no-update-package-json 1.0.0 -o freecodecamp-courses-test.vsix", 192 | "vsce": "vsce", 193 | "vscode:prepublish": "npm run package", 194 | "watch": "webpack --watch" 195 | }, 196 | "version": "3.0.1" 197 | } 198 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>freeCodeCamp/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /resources/courses.json: -------------------------------------------------------------------------------- 1 | { 2 | "courses": [ 3 | { 4 | "name": "Project Euler: Rust", 5 | "githubLink": "https://github.com/freeCodeCamp/euler-rust", 6 | "tags": ["rust"] 7 | }, 8 | { 9 | "name": "Web3", 10 | "githubLink": "https://github.com/freeCodeCamp/web3-curriculum", 11 | "tags": ["web3"] 12 | }, 13 | { 14 | "name": "Solana", 15 | "githubLink": "https://github.com/freeCodeCamp/solana-curriculum", 16 | "tags": ["web3"] 17 | }, 18 | { 19 | "name": "NEAR", 20 | "githubLink": "https://github.com/freeCodeCamp/near-curriculum", 21 | "tags": ["web3"] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/collapse.ts: -------------------------------------------------------------------------------- 1 | import { window, commands } from "vscode"; 2 | 3 | export default async function collapse() { 4 | const editor = window.activeTextEditor; 5 | if (!editor) { 6 | return; 7 | } 8 | const selections = editor.selections; 9 | try { 10 | await commands.executeCommand("editor.action.selectHighlights"); 11 | } catch (e) { 12 | console.error("freeCodeCamp Courses: ", e); 13 | } 14 | selections.forEach(async (selection) => { 15 | try { 16 | await commands.executeCommand("editor.fold", selection.start); 17 | } catch (e) { 18 | console.error("freeCodeCamp Courses: ", e); 19 | } 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/develop-course.ts: -------------------------------------------------------------------------------- 1 | import { handleConfig } from "../handles"; 2 | import { handleMessage } from "../flash"; 3 | import { FlashTypes } from "../typings"; 4 | import { workspace } from "vscode"; 5 | 6 | export default async function developCourse() { 7 | try { 8 | const config = workspace.getConfiguration("freecodecamp-courses"); 9 | handleConfig(config, "develop-course"); 10 | } catch (e) { 11 | console.error("freeCodeCamp > runCourse: ", e); 12 | return handleMessage({ 13 | message: "Unable to develop course. See dev console for more details.", 14 | type: FlashTypes.ERROR, 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/open-course.ts: -------------------------------------------------------------------------------- 1 | import { window } from "vscode"; 2 | import { handleEmptyDirectory, createBackgroundTerminal } from "../handles"; 3 | 4 | import { Courses } from "../typings"; 5 | import { promptQuickPick } from "../inputs"; 6 | import { currentDirectoryCourse } from "../components"; 7 | import { gitClone } from "../usefuls"; 8 | 9 | export default async function openCourse() { 10 | try { 11 | const { courses } = (await ( 12 | await fetch( 13 | "https://raw.githubusercontent.com/freeCodeCamp/freecodecamp-courses/main/resources/courses.json" 14 | ) 15 | ).json()) as Courses; 16 | // Check if course is already downloaded 17 | const courseGitDownloaded = await currentDirectoryCourse(); 18 | 19 | const courseNames = courses.map((course) => { 20 | if (courseGitDownloaded === course.githubLink) { 21 | return `Re-download: ${course.name}`; 22 | } else { 23 | return course.name; 24 | } 25 | }); 26 | const coursePicked = await promptQuickPick(courseNames, { 27 | placeHolder: "Select a course", 28 | canPickMany: false, 29 | }); 30 | 31 | if (coursePicked) { 32 | window.showInformationMessage(`Downloading Course: ${coursePicked}`); 33 | 34 | const course = courses.find( 35 | ({ name }) => name === coursePicked.replace("Re-download: ", "") 36 | ); 37 | if (course?.githubLink !== courseGitDownloaded) { 38 | await handleEmptyDirectory(); 39 | await createBackgroundTerminal( 40 | "freeCodeCamp: Git Course", 41 | // @ts-expect-error TODO: strongly type this 42 | gitClone(course.githubLink) 43 | ); 44 | } else { 45 | await createBackgroundTerminal("freeCodeCamp: Re-Git", "git pull"); 46 | } 47 | 48 | // TODO: This does not work for some reason 49 | // await rebuildAndReopenInContainer(); 50 | } 51 | } catch (e) { 52 | console.error(e); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/commands/run-course.ts: -------------------------------------------------------------------------------- 1 | import { handleConfig } from "../handles"; 2 | import { handleMessage } from "../flash"; 3 | import { FlashTypes } from "../typings"; 4 | import { workspace } from "vscode"; 5 | 6 | export default async function runCourse() { 7 | try { 8 | const config = workspace.getConfiguration("freecodecamp-courses"); 9 | handleConfig(config, "run-course"); 10 | } catch (e) { 11 | console.error("freeCodeCamp > runCourse: ", e); 12 | return handleMessage({ 13 | message: "Unable to run course. See dev console for more details.", 14 | type: FlashTypes.ERROR, 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components.ts: -------------------------------------------------------------------------------- 1 | import { commands, Uri, workspace, window } from "vscode"; 2 | import { Course, FlashTypes } from "./typings"; 3 | import { handleMessage } from "./flash"; 4 | 5 | export async function openTerminal() { 6 | const terminal = window.createTerminal("freeCodeCamp"); 7 | terminal.sendText(`source ~/.bashrc`, true); 8 | // TODO: clear terminal 9 | terminal.show(); 10 | } 11 | 12 | /** 13 | * This function opens the built-in VSCode Simple Browser, and loads the local port started by live-server 14 | */ 15 | export function openSimpleBrowser(url: string) { 16 | commands.executeCommand("simpleBrowser.show", url); 17 | } 18 | 19 | /** 20 | * Returns the current working directory `package.json > repository.url` or `null` 21 | */ 22 | export async function currentDirectoryCourse(): Promise< 23 | Course["githubLink"] | null 24 | > { 25 | try { 26 | const work = workspace.workspaceFolders?.[0]?.uri?.fsPath ?? ""; 27 | const path = Uri.file(work); 28 | const bin = await workspace.fs.readFile(Uri.joinPath(path, "package.json")); 29 | const fileData = JSON.parse(bin.toString()); 30 | const courseGithubLink = fileData?.repository?.url ?? null; 31 | return Promise.resolve(courseGithubLink); 32 | } catch (e) { 33 | console.error(e); 34 | handleMessage({ message: e as string, type: FlashTypes.INFO }); 35 | return Promise.resolve(null); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import { commands, ExtensionContext, window, workspace } from "vscode"; 2 | import openCourse from "./commands/open-course"; 3 | import runCourse from "./commands/run-course"; 4 | import developCourse from "./commands/develop-course"; 5 | import collapse from "./commands/collapse"; 6 | 7 | export async function activate(context: ExtensionContext) { 8 | console.log("freeCodeCamp Courses extension is now active!"); 9 | 10 | // Get extension settings 11 | const configuration = workspace.getConfiguration("freecodecamp-courses"); 12 | try { 13 | if (configuration.get("autoStart")) { 14 | runCourse(); 15 | } 16 | } catch (e) { 17 | console.debug(e); 18 | } 19 | 20 | context.subscriptions.push( 21 | commands.registerCommand("freecodecamp-courses.openCourse", async () => { 22 | openCourse(); 23 | }) 24 | ); 25 | context.subscriptions.push( 26 | commands.registerCommand("freecodecamp-courses.runCourse", async () => { 27 | runCourse(); 28 | }) 29 | ); 30 | context.subscriptions.push( 31 | commands.registerCommand("freecodecamp-courses.developCourse", async () => { 32 | developCourse(); 33 | }) 34 | ); 35 | context.subscriptions.push( 36 | commands.registerCommand("freecodecamp-courses.collapse", async () => { 37 | collapse(); 38 | }) 39 | ); 40 | context.subscriptions.push( 41 | commands.registerCommand( 42 | "freecodecamp-courses.shutdownCourse", 43 | async () => { 44 | shutdownCourse(); 45 | } 46 | ) 47 | ); 48 | } 49 | 50 | function shutdownCourse() { 51 | // End all running terminal processes 52 | window.terminals.forEach((terminal) => { 53 | terminal.dispose(); 54 | }); 55 | 56 | // This is a bit of a hack to close the Simple Browser window 57 | // So, it might not work well. 58 | try { 59 | window.visibleTextEditors.forEach((editor) => { 60 | window.showTextDocument(editor.document).then((_) => { 61 | return commands.executeCommand("workbench.action.closeActiveEditor"); 62 | }); 63 | }); 64 | } catch {} 65 | } 66 | 67 | // this method is called when your extension is deactivated 68 | export function deactivate() {} 69 | -------------------------------------------------------------------------------- /src/fixture.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import { Config } from "./typings"; 3 | 4 | export const exampleConfig: Config = { 5 | autoStart: false, // Whether or not to automatically start course on open of VSCode 6 | path: ".freeCodeCamp", // Relative path to tooling directory where scripts will be run 7 | prepare: "cp sample.env .env && source ./tooling/.bashrc && npm ci", // Run before running scripts 8 | scripts: { 9 | // Scripts linked to extension commands 10 | "develop-course": "npm run develop", // Run when `Develop Course` command is executed 11 | "run-course": "npm run start", // Run when `Run Course` command is executed 12 | }, 13 | workspace: { 14 | // Workspace settings 15 | files: [ 16 | // Files to be opened in workspace 17 | { 18 | path: "README.md", // Relative path to file 19 | }, 20 | ], 21 | previews: [ 22 | // Previews to be opened in workspace 23 | { 24 | open: true, // Whether or not to open preview 25 | showLoader: true, // Whether or not to show loading indicator 26 | url: "https://www.freecodecamp.org/", // URL to open 27 | timeout: 10000, // Timeout for URL to respond with 200 28 | }, 29 | ], 30 | terminals: [ 31 | // Terminals to be opened in workspace 32 | { 33 | directory: ".", // Relative path to open terminal with 34 | message: "'Hello World!'", // Message to display in terminal 35 | name: "Camper", // Name of terminal 36 | show: true, // Whether or not to show terminal 37 | }, 38 | ], 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/flash.ts: -------------------------------------------------------------------------------- 1 | import { window } from "vscode"; 2 | import { FlashTypes, Flash } from "./typings"; 3 | 4 | export function showMessage(shower: Function) { 5 | return (s: string, opts: Flash["opts"]) => shower(s, opts); 6 | } 7 | 8 | export const flasher = { 9 | [FlashTypes.INFO]: showMessage(window.showInformationMessage), 10 | [FlashTypes.WARNING]: showMessage(window.showWarningMessage), 11 | [FlashTypes.ERROR]: showMessage(window.showErrorMessage), 12 | }; 13 | 14 | export function handleMessage(flash: Flash) { 15 | flasher[flash.type](flash.message, flash.opts); 16 | } 17 | -------------------------------------------------------------------------------- /src/handles.ts: -------------------------------------------------------------------------------- 1 | import { Config, FlashTypes } from "./typings"; 2 | import { 3 | commands, 4 | Terminal, 5 | TerminalExitStatus, 6 | window, 7 | WorkspaceConfiguration, 8 | } from "vscode"; 9 | import { openSimpleBrowser } from "./components"; 10 | import { cd, checkIfURLIsAvailable, ensureDirectoryIsEmpty } from "./usefuls"; 11 | import { handleMessage } from "./flash"; 12 | import { createLoaderWebView } from "./loader"; 13 | 14 | /** 15 | * Creates a terminal with the given name and executes the given commands. 16 | * @example 17 | * handleTerminal(".freeCodeCamp", "freeCodeCamp: Open Course", "npm install", "live-server .") 18 | */ 19 | export function handleTerminal( 20 | path: string, 21 | name: string, 22 | ...commands: string[] 23 | ) { 24 | const commandString = commands 25 | .join(" && ") 26 | .replace(/ ?([^&]+) && & && ([^&]+)/g, " ($1 & $2)"); 27 | 28 | // If terminal already exists, then re-use it: 29 | const existingTerminal = window.terminals.find( 30 | (terminal) => terminal.name === name 31 | ); 32 | if (existingTerminal) { 33 | existingTerminal.sendText(commandString); 34 | return existingTerminal; 35 | } 36 | const terminal = window.createTerminal(name); 37 | terminal.sendText(cd(path, commandString), true); 38 | return terminal; 39 | } 40 | 41 | export async function createBackgroundTerminal(name: string, command: string) { 42 | const terminal = window.createTerminal(name); 43 | terminal.sendText(`${command} && exit`, true); 44 | const exitStatus = await pollTerminal(terminal); 45 | if (exitStatus) { 46 | terminal.dispose(); 47 | return Promise.resolve(exitStatus); 48 | } 49 | return Promise.reject(); 50 | } 51 | 52 | export async function pollTerminal( 53 | terminal: Terminal 54 | ): Promise { 55 | // Every 400ms, check if `terminal.exitStatus` is `undefined`. If it is not `undefined`, resolve promise to `terminal.exitStatus`. 56 | return new Promise((resolve) => { 57 | const interval = setInterval(() => { 58 | if (terminal.exitStatus) { 59 | resolve(terminal.exitStatus); 60 | clearInterval(interval); 61 | } 62 | }, 400); 63 | }); 64 | } 65 | 66 | // Does not work. Unsure why not. 67 | export function rebuildAndReopenInContainer() { 68 | commands.executeCommand("remote-containers.rebuildAndReopenInContainer"); 69 | } 70 | 71 | export async function handleEmptyDirectory() { 72 | const isEmpty = await ensureDirectoryIsEmpty(); 73 | if (!isEmpty) { 74 | handleMessage({ 75 | message: "Directory is not empty.", 76 | type: FlashTypes.WARNING, 77 | opts: { 78 | detail: "Please empty working directory, and try again.", 79 | modal: true, 80 | }, 81 | }); 82 | 83 | return Promise.reject(); 84 | } 85 | return Promise.resolve(); 86 | } 87 | 88 | const scripts = { 89 | // eslint-disable-next-line @typescript-eslint/naming-convention 90 | "develop-course": (path: string, val: string) => { 91 | handleTerminal(path, "freeCodeCamp: Develop Course", val); 92 | }, 93 | // eslint-disable-next-line @typescript-eslint/naming-convention 94 | "run-course": (path: string, val: string) => { 95 | handleTerminal(path, "freeCodeCamp: Run Course", val); 96 | }, 97 | }; 98 | 99 | export async function handleWorkspace( 100 | workspace: Config["workspace"], 101 | prepareTerminalClose: ReturnType 102 | ): Promise { 103 | if (workspace!.previews) { 104 | for (const preview of workspace!.previews) { 105 | if (preview.showLoader) { 106 | const panel = createLoaderWebView(); 107 | // TODO: could use result here to show error in loader webview 108 | await prepareTerminalClose; 109 | 110 | // Wait for the port to be available, before disposing the panel. 111 | new Promise(async (resolve) => { 112 | if (preview.url) { 113 | await checkIfURLIsAvailable(preview.url, preview.timeout); 114 | } 115 | if (preview?.open) { 116 | setTimeout(() => { 117 | openSimpleBrowser(preview.url); 118 | }, 500); 119 | } 120 | panel.dispose(); 121 | }); 122 | } 123 | if (!preview.url && preview?.open) { 124 | setTimeout(() => { 125 | openSimpleBrowser(preview.url); 126 | }, 500); 127 | } 128 | } 129 | } 130 | if (workspace!.terminals) { 131 | for (const term of workspace!.terminals) { 132 | if (term?.name) { 133 | const t = handleTerminal( 134 | term.directory, 135 | term.name, 136 | `echo ${term.message || ""}` 137 | ); 138 | if (term?.show) { 139 | t.show(); 140 | } 141 | } 142 | } 143 | } 144 | if (workspace!.files) { 145 | for (const _file of workspace!.files) { 146 | // TODO: Open file 147 | } 148 | } 149 | return Promise.resolve(); 150 | } 151 | 152 | export async function handleConfig( 153 | config: WorkspaceConfiguration, 154 | caller: keyof Config["scripts"] 155 | ) { 156 | // Ensure compulsory keys and values are set 157 | const path = config.path || "."; 158 | // Run prepare script 159 | let prepareTerminalClose: Promise = new Promise( 160 | (resolve) => resolve({ code: 0 } as TerminalExitStatus) 161 | ); 162 | if (config.get("prepare")) { 163 | prepareTerminalClose = createBackgroundTerminal( 164 | "freeCodeCamp: Preparing Course", 165 | cd(path, config.prepare) 166 | ); 167 | } 168 | 169 | if (config.get("workspace")) { 170 | await handleWorkspace(config.workspace, prepareTerminalClose); 171 | } 172 | 173 | const calledScript = config.scripts[caller]; 174 | if (typeof calledScript === "string") { 175 | scripts[caller](path, calledScript); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | openTerminal, 3 | openSimpleBrowser, 4 | currentDirectoryCourse, 5 | } from "./components"; 6 | import { showInputBox, promptQuickPick } from "./inputs"; 7 | import { ensureDirectoryIsEmpty, cd } from "./usefuls"; 8 | 9 | import { handleMessage, showMessage } from "./flash"; 10 | 11 | export const everythingButHandles = { 12 | currentDirectoryCourse, 13 | ensureDirectoryIsEmpty, 14 | promptQuickPick, 15 | openSimpleBrowser, 16 | openTerminal, 17 | showInputBox, 18 | handleMessage, 19 | showMessage, 20 | cd, 21 | }; 22 | -------------------------------------------------------------------------------- /src/inputs.ts: -------------------------------------------------------------------------------- 1 | import { QuickPickOptions, window } from "vscode"; 2 | 3 | export async function promptQuickPick( 4 | selections: string[], 5 | options: QuickPickOptions & { 6 | canPickMany: boolean; 7 | } 8 | ) { 9 | return await window.showQuickPick(selections, options); 10 | } 11 | 12 | /** 13 | * Shows an input box using window.showInputBox(). 14 | */ 15 | export async function showInputBox(placeHolder: string) { 16 | const result = await window.showInputBox({ 17 | placeHolder, 18 | }); 19 | return result; 20 | } 21 | -------------------------------------------------------------------------------- /src/loader.ts: -------------------------------------------------------------------------------- 1 | import { ViewColumn, window } from "vscode"; 2 | 3 | export async function isInstallFinished() { 4 | return new Promise((resolve) => { 5 | setTimeout(() => { 6 | resolve(true); 7 | }, 1000); 8 | }); 9 | } 10 | 11 | export function createLoaderWebView() { 12 | const panel = window.createWebviewPanel( 13 | "Loader", 14 | "Loading", 15 | ViewColumn.One, 16 | {} 17 | ); 18 | panel.webview.html = getLoaderHTML(); 19 | return panel; 20 | } 21 | 22 | function getLoaderHTML() { 23 | return ` 24 | 25 | 26 | 27 | 28 | 29 | Loading 30 | 31 | 32 | 112 | 113 | 114 |
115 | 116 |
117 |

Preparing the course...

118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | 126 | 127 | `; 128 | } 129 | -------------------------------------------------------------------------------- /src/typings.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | export enum FlashTypes { 3 | ERROR = "error", 4 | INFO = "info", 5 | WARNING = "warning", 6 | } 7 | 8 | export type Flash = { 9 | message: string; 10 | opts?: { 11 | detail: string; 12 | modal?: boolean; 13 | }; 14 | type: FlashTypes; 15 | }; 16 | 17 | export interface Course { 18 | githubLink: string; 19 | name: string; 20 | tags: string[]; 21 | } 22 | 23 | export interface Courses { 24 | courses: Course[]; 25 | } 26 | 27 | type Preview = { 28 | open: boolean; 29 | showLoader: boolean; 30 | url: string; 31 | timeout: number; 32 | }; 33 | 34 | type Terminal = { 35 | directory: string; 36 | message: string | null; 37 | name: string; 38 | show: boolean; 39 | }; 40 | 41 | type File = { path: string }; 42 | 43 | export interface Config { 44 | autoStart: boolean; 45 | path: string; 46 | prepare: string; 47 | scripts: { 48 | "develop-course": string; 49 | "run-course": string; 50 | }; 51 | workspace: { 52 | files: File[]; 53 | previews: Preview[]; 54 | terminals: Terminal[]; 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/usefuls.ts: -------------------------------------------------------------------------------- 1 | import { workspace, Uri } from "vscode"; 2 | 3 | export const gitClone = (githubLink: string) => `git clone ${githubLink}.git .`; 4 | export const cd = (path: string, cmd: string) => `cd ${path} && ${cmd}`; 5 | 6 | export async function ensureDirectoryIsEmpty(): Promise { 7 | try { 8 | const arrOfArrs = await workspace.fs.readDirectory( 9 | Uri.file(workspace.workspaceFolders?.[0]?.uri?.fsPath ?? "") 10 | ); 11 | if (arrOfArrs.length === 0) { 12 | return Promise.resolve(true); 13 | } else { 14 | return Promise.resolve(false); 15 | } 16 | } catch (e) { 17 | console.error("freeCodeCamp > ensureDirectoryIsEmpty: ", e); 18 | return Promise.reject(false); 19 | } 20 | } 21 | 22 | /** 23 | * Requests the argument URL every 250ms until it returns a 200 status code, or until the timeout is reached. 24 | * @param url The URL to ping. 25 | * @param timeout The timeout in milliseconds. 26 | */ 27 | export async function checkIfURLIsAvailable( 28 | url: string, 29 | timeout: number = 10000 30 | ): Promise { 31 | try { 32 | return new Promise((resolve, reject) => { 33 | const interval = setInterval(async () => { 34 | try { 35 | const response = await fetchWithTimeout(url, 250); 36 | if (response.status === 200) { 37 | clearInterval(interval); 38 | resolve(true); 39 | } 40 | } catch (e) { 41 | // Do nothing. 42 | console.debug("freeCodeCamp > checkIfURLIsAvailable: ", e); 43 | } 44 | }, 250); 45 | setTimeout(() => { 46 | clearInterval(interval); 47 | resolve(false); 48 | }, timeout); 49 | }); 50 | } catch (e) { 51 | console.error("freeCodeCamp > checkIfURLIsAvailable: ", e); 52 | return Promise.reject(false); 53 | } 54 | } 55 | 56 | async function fetchWithTimeout(url: string, timeout: number) { 57 | const controller = new AbortController(); 58 | const id = setTimeout(() => controller.abort(), timeout); 59 | const response = await fetch(url, { 60 | signal: controller.signal, 61 | }); 62 | clearTimeout(id); 63 | return response; 64 | } 65 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2022", 5 | "lib": ["ES2022", "DOM"], 6 | "sourceMap": true, 7 | "rootDir": "src", 8 | "strict": true /* enable all strict type-checking options */ 9 | /* Additional Checks */ 10 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 11 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 12 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 13 | }, 14 | "exclude": ["node_modules", ".vscode-test"] 15 | } 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | "use strict"; 4 | 5 | const path = require("path"); 6 | 7 | //@ts-check 8 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 9 | 10 | /** @type WebpackConfig */ 11 | const extensionConfig = { 12 | target: "node", // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 13 | mode: "none", // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 14 | 15 | entry: "./src/extension.ts", // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 16 | output: { 17 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 18 | path: path.resolve(__dirname, "dist"), 19 | filename: "extension.js", 20 | libraryTarget: "commonjs2", 21 | }, 22 | externals: { 23 | vscode: "commonjs vscode", // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 24 | // modules added here also need to be added in the .vscodeignore file 25 | }, 26 | resolve: { 27 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 28 | extensions: [".ts", ".js"], 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.ts$/, 34 | exclude: /node_modules/, 35 | use: [ 36 | { 37 | loader: "ts-loader", 38 | }, 39 | ], 40 | }, 41 | ], 42 | }, 43 | devtool: "inline-source-map", 44 | infrastructureLogging: { 45 | level: "log", // enables logging required for problem matchers 46 | }, 47 | }; 48 | module.exports = [extensionConfig]; 49 | --------------------------------------------------------------------------------