mirror of
https://github.com/freeCodeCamp/web3-curriculum.git
synced 2026-01-10 14:08:11 -05:00
feat: add freecodecamp-os package (#50)
* feat: add freecodecamp-os package * fix: add state that fell out * fix: add necessary files to .vscode/settings.json * fix: format files * fix: helper imports Co-authored-by: Tom <20648924+moT01@users.noreply.github.com> * fix: hide by default, fix tests Co-authored-by: Tom <20648924+moT01@users.noreply.github.com>
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"freeCodeCamp.freecodecamp-courses@1.4.3",
|
||||
"freeCodeCamp.freecodecamp-courses@1.5.0",
|
||||
"freeCodeCamp.freecodecamp-dark-vscode-theme"
|
||||
],
|
||||
"forwardPorts": [3000],
|
||||
"forwardPorts": [8080],
|
||||
"workspaceFolder": "/workspace/web3-curriculum",
|
||||
"dockerFile": "../.freeCodeCamp/Dockerfile",
|
||||
"dockerFile": "../Dockerfile",
|
||||
"context": ".."
|
||||
}
|
||||
|
||||
7
.freeCodeCamp/client/components/congratulations.tsx
Normal file
7
.freeCodeCamp/client/components/congratulations.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export const Congratulations = () => {
|
||||
return (
|
||||
<li className='test'>
|
||||
<span className='passed'>🎉 Congratulations! You passed all tests.</span>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
@@ -2,17 +2,23 @@ import { ConsoleError } from '../types';
|
||||
import { parseMarkdown } from '../utils';
|
||||
|
||||
export const Console = ({ cons }: { cons: ConsoleError[] }) => {
|
||||
const consoleMarkdown = cons
|
||||
.map(({ id, hint, error }) => {
|
||||
return `<details>\n<summary>${id + 1}) ${parseMarkdown(
|
||||
hint
|
||||
)}</summary>\n\n\`\`\`json\n${JSON.stringify(
|
||||
error,
|
||||
null,
|
||||
2
|
||||
)}\n\`\`\`\n\n</details>`;
|
||||
})
|
||||
.join('\n\n');
|
||||
return (
|
||||
<ul style={{ listStyle: 'none' }}>
|
||||
{cons.map(con => (
|
||||
<ConsoleElement key={con.testId} {...con} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
const ConsoleElement = ({ testText, testId, error }: ConsoleError) => {
|
||||
const consoleMarkdown = `<details>\n<summary>${testId + 1}) ${parseMarkdown(
|
||||
testText
|
||||
)}</summary>\n\n\`\`\`json\n${JSON.stringify(
|
||||
error,
|
||||
null,
|
||||
2
|
||||
)}\n\`\`\`\n\n</details>`;
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: parseMarkdown(consoleMarkdown) }}
|
||||
|
||||
@@ -16,15 +16,24 @@ export const Heading = ({
|
||||
goToPreviousLesson
|
||||
}: HeadingProps) => {
|
||||
return (
|
||||
<nav>
|
||||
<nav className='heading'>
|
||||
{goToPreviousLesson && (
|
||||
<button onClick={() => goToPreviousLesson()}><</button>
|
||||
<button
|
||||
className='previous-lesson-btn'
|
||||
onClick={() => goToPreviousLesson()}
|
||||
>
|
||||
←
|
||||
</button>
|
||||
)}
|
||||
<h1 id='project-heading'>
|
||||
{topic} - {title}
|
||||
{lessonNumber && <LessonNumber lessonNumber={lessonNumber} />}
|
||||
</h1>
|
||||
{goToNextLesson && <button onClick={() => goToNextLesson()}>></button>}
|
||||
{goToNextLesson && (
|
||||
<button className='next-lesson-btn' onClick={() => goToNextLesson()}>
|
||||
→
|
||||
</button>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Events } from '../types';
|
||||
import projects from '../../config/projects.json' assert { type: 'json' };
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Events, ProjectI } from '../types';
|
||||
// import projects from '../../config/projects.json' assert { type: 'json' };
|
||||
import { Block } from './block';
|
||||
|
||||
export interface SelectionProps {
|
||||
sock: (type: Events, data: {}) => void;
|
||||
projects: ProjectI[];
|
||||
}
|
||||
export const Selection = ({ sock }: SelectionProps) => {
|
||||
export const Selection = ({ sock, projects }: SelectionProps) => {
|
||||
return (
|
||||
<ul className='blocks'>
|
||||
{projects.map((p, i) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { TestType } from '../types';
|
||||
import { Congratulations } from './congratulations';
|
||||
import { Test } from './test';
|
||||
|
||||
interface TestsProps {
|
||||
@@ -34,11 +35,3 @@ export const Tests = ({ tests }: TestsProps) => {
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
const Congratulations = () => {
|
||||
return (
|
||||
<li className='test'>
|
||||
<span className='passed'>🎉 Congratulations! You passed all tests.</span>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Suspense, useState, useEffect } from 'react';
|
||||
import { ConsoleError, Events, ProjectI, TestType } from './types/index';
|
||||
import {
|
||||
ConsoleError,
|
||||
Events,
|
||||
FreeCodeCampConfigI,
|
||||
ProjectI,
|
||||
TestType
|
||||
} from './types/index';
|
||||
import { Loader } from './components/loader';
|
||||
import { Landing } from './templates/landing';
|
||||
import { Project } from './templates/project';
|
||||
@@ -19,6 +25,9 @@ if (process.env.GITPOD_WORKSPACE_URL) {
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const [projects, setProjects] = useState<ProjectI[]>([]);
|
||||
const [freeCodeCampConfig, setFreeCodeCampConfig] =
|
||||
useState<FreeCodeCampConfigI>({});
|
||||
const [project, setProject] = useState<ProjectI | null>(null);
|
||||
const [topic, setTopic] = useState('');
|
||||
|
||||
@@ -30,6 +39,8 @@ const App = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [alertCamper, setAlertCamper] = useState<null | string>(null);
|
||||
|
||||
const [debouncers, setDebouncers] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
socket.onopen = function (_event) {
|
||||
sock(Events.CONNECT);
|
||||
@@ -61,10 +72,23 @@ const App = () => {
|
||||
'update-description': updateDescription,
|
||||
'update-project-heading': updateProjectHeading,
|
||||
'update-project': setProject,
|
||||
'reset-tests': resetTests
|
||||
'update-projects': setProjects,
|
||||
'update-freeCodeCamp-config': setFreeCodeCampConfig,
|
||||
'reset-tests': resetTests,
|
||||
RESPONSE: debounce
|
||||
};
|
||||
|
||||
function debounce({ event }: { event: string }) {
|
||||
const debouncerRemoved = debouncers.filter(d => d !== event);
|
||||
setDebouncers(() => debouncerRemoved);
|
||||
}
|
||||
|
||||
function sock(type: Events, data = {}) {
|
||||
if (debouncers.includes(type)) {
|
||||
return;
|
||||
}
|
||||
const newDebouncers = [...debouncers, type];
|
||||
setDebouncers(() => newDebouncers);
|
||||
socket.send(parse({ event: type, data }));
|
||||
}
|
||||
|
||||
@@ -100,12 +124,15 @@ const App = () => {
|
||||
}
|
||||
|
||||
function updateConsole({ cons }: { cons: ConsoleError }) {
|
||||
if (!Object.keys(cons).length) {
|
||||
return setCons([]);
|
||||
}
|
||||
// Insert cons in array at index `id`
|
||||
setCons(prev => {
|
||||
const sorted = [
|
||||
...prev.slice(0, cons.id),
|
||||
...prev.slice(0, cons.testId),
|
||||
cons,
|
||||
...prev.slice(cons.id)
|
||||
...prev.slice(cons.testId)
|
||||
].filter(Boolean);
|
||||
return sorted;
|
||||
});
|
||||
@@ -162,7 +189,7 @@ const App = () => {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Landing {...{ topic, sock }} />
|
||||
<Landing {...{ topic, sock, projects, freeCodeCampConfig }} />
|
||||
)}
|
||||
</Suspense>
|
||||
</>
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#description {
|
||||
margin: 0 auto;
|
||||
width: 80%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#description > pre {
|
||||
background-color: black !important;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
#description > p {
|
||||
line-height: 2.6ch;
|
||||
margin: 1.3em 0;
|
||||
}
|
||||
|
||||
.integrated-project-controls {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
width: 80%;
|
||||
margin: 1rem auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
.integrated-project-controls > button {
|
||||
width: 30%;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
background-image: linear-gradient(var(--light-yellow), var(--dark-yellow));
|
||||
border-width: 3px;
|
||||
border-color: var(--dark-yellow);
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: var(--dark-1);
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.integrated-project-controls > button:hover {
|
||||
background-image: none;
|
||||
background-color: var(--light-yellow);
|
||||
}
|
||||
|
||||
.integrated-project-output {
|
||||
max-width: 95%;
|
||||
margin: 1rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.integrated-project-output > ul {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.integrated-project-output > ul > li {
|
||||
list-style: none;
|
||||
}
|
||||
.output-btn {
|
||||
background-color: var(--dark-3);
|
||||
color: var(--light-1);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.output-btn:hover {
|
||||
background-color: var(--light-blue);
|
||||
color: var(--dark-3);
|
||||
cursor: pointer;
|
||||
}
|
||||
.output-btn:disabled {
|
||||
background-color: var(--light-blue);
|
||||
color: var(--dark-3);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.integrated-project-output-content {
|
||||
background-color: black;
|
||||
color: white;
|
||||
height: fit-content;
|
||||
min-height: 100px;
|
||||
padding: 1rem 2rem;
|
||||
margin-top: 0;
|
||||
text-align: left;
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -1,33 +1,33 @@
|
||||
import { Selection } from '../components/selection';
|
||||
import { Events } from '../types';
|
||||
import { Events, FreeCodeCampConfigI, ProjectI } from '../types';
|
||||
import './landing.css';
|
||||
|
||||
interface LandingProps {
|
||||
topic: string;
|
||||
sock: (type: Events, data: {}) => void;
|
||||
projects: ProjectI[];
|
||||
freeCodeCampConfig: FreeCodeCampConfigI;
|
||||
}
|
||||
|
||||
export const Landing = ({ topic, sock }: LandingProps) => {
|
||||
export const Landing = ({
|
||||
topic,
|
||||
sock,
|
||||
projects,
|
||||
freeCodeCampConfig
|
||||
}: LandingProps) => {
|
||||
return (
|
||||
<>
|
||||
<h2>{topic}</h2>
|
||||
<p className='description'>
|
||||
For these courses, you will use your local development environment to
|
||||
complete interactive tutorials and build projects.
|
||||
{freeCodeCampConfig.client?.landing?.description}
|
||||
</p>
|
||||
<p>
|
||||
These courses start off with basic cryptographic concepts. Using Nodejs,
|
||||
you will learn everything from cryptographic hash functions to building
|
||||
your own blockchain.
|
||||
</p>
|
||||
<p>Next, you will learn about different consensus mechanisms.</p>
|
||||
<p>
|
||||
Finally, you will learn Rust, and WASM in the context of a blockchain.
|
||||
</p>
|
||||
<a className='faq' href='#'>
|
||||
Link to FAQ related to course
|
||||
<a
|
||||
className='faq'
|
||||
href={freeCodeCampConfig.client?.landing?.['faq-link']}
|
||||
>
|
||||
{freeCodeCampConfig.client?.landing?.['faq-text']}
|
||||
</a>
|
||||
<Selection {...{ topic, sock }} />
|
||||
<Selection {...{ topic, sock, projects }} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
/* nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.heading > button {
|
||||
background-color: var(--dark-blue);
|
||||
margin: 0 0.5rem;
|
||||
color: var(--light-1);
|
||||
font-size: 1.35rem;
|
||||
-webkit-text-stroke: medium;
|
||||
}
|
||||
|
||||
nav > button {
|
||||
background-color: var(--dark-3);
|
||||
color: rgb(230, 230, 230);
|
||||
font-size: 1.8rem;
|
||||
font-weight: bold;
|
||||
width: 50px;
|
||||
height: 100%;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
nav > button:hover {
|
||||
.heading > button:hover {
|
||||
background-color: var(--light-blue);
|
||||
color: var(--dark-3);
|
||||
} */
|
||||
color: var(--dark-blue);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#project-heading {
|
||||
font-size: 1.5rem;
|
||||
@@ -67,17 +58,6 @@ nav > button:hover {
|
||||
}
|
||||
}
|
||||
|
||||
/* #description {
|
||||
margin: 0 auto;
|
||||
width: 80%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#description > pre {
|
||||
background-color: black !important;
|
||||
padding: 1rem;
|
||||
} */
|
||||
|
||||
.project-controls {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
@@ -231,7 +211,6 @@ nav {
|
||||
color: white;
|
||||
height: fit-content;
|
||||
min-height: 100px;
|
||||
/* padding: 1rem 2rem; */
|
||||
margin-top: 0;
|
||||
text-align: left;
|
||||
overflow: auto;
|
||||
|
||||
@@ -8,6 +8,7 @@ export enum Events {
|
||||
UPDATE_TEST = 'update-test',
|
||||
UPDATE_DESCRIPTION = 'update-description',
|
||||
UPDATE_PROJECT_HEADING = 'update-project-heading',
|
||||
UPDATE_PROJECTS = 'update-projects',
|
||||
RESET_TESTS = 'reset-tests',
|
||||
RUN_TESTS = 'run-tests',
|
||||
RESET_PROJECT = 'reset-project',
|
||||
@@ -31,8 +32,10 @@ export interface ProjectI {
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
export interface ConsoleError {
|
||||
id: number;
|
||||
hint: string;
|
||||
export type ConsoleError = {
|
||||
error: string;
|
||||
}
|
||||
} & TestType;
|
||||
|
||||
export type FreeCodeCampConfigI = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"path": ".freeCodeCamp",
|
||||
"prepare": "\\cp sample.env .env && npm ci && cd .. && npm ci",
|
||||
"scripts": {
|
||||
"develop-course": "npm run develop",
|
||||
"run-course": "npm run start",
|
||||
"test": {
|
||||
"functionName": "handleMessage",
|
||||
"arguments": [
|
||||
{
|
||||
"message": "Hello World!",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"previews": [
|
||||
{
|
||||
"open": true,
|
||||
"url": "http://localhost:8080",
|
||||
"showLoader": true,
|
||||
"timeout": 20000
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
22
.freeCodeCamp/npm/postinstall.js
Normal file
22
.freeCodeCamp/npm/postinstall.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { cp } from 'fs/promises';
|
||||
import { warn } from 'logover';
|
||||
import { join } from 'path';
|
||||
|
||||
const ROOT = process.env.INIT_CWD || process.cwd();
|
||||
|
||||
copyDotFreeCodeCampToRoot();
|
||||
|
||||
async function copyDotFreeCodeCampToRoot() {
|
||||
try {
|
||||
await cp(
|
||||
join(ROOT, 'node_modules/@freecodecamp/freecodecamp-os/.freeCodeCamp'),
|
||||
join(ROOT, '.freeCodeCamp'),
|
||||
{
|
||||
recursive: true,
|
||||
force: true
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
warn(e);
|
||||
}
|
||||
}
|
||||
1
.freeCodeCamp/npm/uninstall.js
Normal file
1
.freeCodeCamp/npm/uninstall.js
Normal file
@@ -0,0 +1 @@
|
||||
console.log("freeCodeCampOS: Uninstalling...");
|
||||
13086
.freeCodeCamp/package-lock.json
generated
13086
.freeCodeCamp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"name": "external-project",
|
||||
"author": "freeCodeCamp",
|
||||
"description": "Template used for freeCodeCamp projects with the freeCodeCamp Courses VSCode extension",
|
||||
"scripts": {
|
||||
"start": "npm run build:client && node ./tooling/server.js",
|
||||
"develop": "npm run develop:client & npm run develop:server",
|
||||
"build:git": "node ./tooling/git/build.js",
|
||||
"build:client": "NODE_ENV=production webpack",
|
||||
"develop:client": "webpack --watch --mode development",
|
||||
"develop:server": "nodemon --watch ./dist/ --watch ./tooling/ ./tooling/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"chai": "4.3.6",
|
||||
"chokidar": "3.5.3",
|
||||
"express": "4.18.1",
|
||||
"logover": "^1.3.1",
|
||||
"marked": "4.0.16",
|
||||
"prismjs": "^1.28.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/freeCodeCamp/external-project"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.18.2",
|
||||
"@babel/plugin-syntax-import-assertions": "^7.18.6",
|
||||
"@babel/preset-env": "7.18.2",
|
||||
"@babel/preset-react": "7.17.12",
|
||||
"@babel/preset-typescript": "7.17.12",
|
||||
"@types/marked": "4.0.3",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/react": "18.0.15",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"babel-loader": "8.2.5",
|
||||
"babel-plugin-prismjs": "^2.1.0",
|
||||
"css-loader": "6.7.1",
|
||||
"dotenv-webpack": "^7.1.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"nodemon": "2.0.16",
|
||||
"os-browserify": "^0.3.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"process": "^0.11.10",
|
||||
"style-loader": "3.3.1",
|
||||
"ts-loader": "9.3.0",
|
||||
"typescript": "4.7.3",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-dev-server": "4.9.2"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all"
|
||||
],
|
||||
"type": "module"
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
CURRENT_PROJECT=
|
||||
LOCALE=english
|
||||
# TEST_POLLING_RATE=100
|
||||
# RUN_TESTS_ON_WATCH=false
|
||||
# SEED_EVERY_LESSON=false
|
||||
# USE_GIT_BUILD_ON_PRODUCTION=false
|
||||
44
.freeCodeCamp/tests/fixtures/expected-format.md
vendored
44
.freeCodeCamp/tests/fixtures/expected-format.md
vendored
@@ -1,44 +0,0 @@
|
||||
# Title - Project
|
||||
|
||||
## 1
|
||||
|
||||
### --description--
|
||||
|
||||
Some description.
|
||||
|
||||
Maybe some code:
|
||||
|
||||
```js
|
||||
const a = 1;
|
||||
// A comment at the end?
|
||||
```
|
||||
|
||||
### --tests--
|
||||
|
||||
Test text with many words.
|
||||
|
||||
```js
|
||||
// First test code
|
||||
const a = 'test';
|
||||
```
|
||||
|
||||
Second test text with `inline-code`.
|
||||
|
||||
```js
|
||||
const a = 'test2';
|
||||
// Second test code;
|
||||
```
|
||||
|
||||
### --before-all--
|
||||
|
||||
```js
|
||||
global.__beforeAll = 'before-all';
|
||||
```
|
||||
|
||||
### --before-each--
|
||||
|
||||
```js
|
||||
global.__beforeEach = 'before-each';
|
||||
```
|
||||
|
||||
## --fcc-end--
|
||||
@@ -1,30 +0,0 @@
|
||||
# Title - Project
|
||||
## 1
|
||||
### --description--
|
||||
This description has no spaces between the heading.
|
||||
```rs
|
||||
|
||||
//Same goes for this code.
|
||||
let mut a = 1;
|
||||
// comment
|
||||
```
|
||||
### --tests--
|
||||
Test text at top.
|
||||
```js
|
||||
// First test no space
|
||||
// No code?
|
||||
|
||||
```
|
||||
|
||||
Second test text with `inline-code`.
|
||||
|
||||
|
||||
```js
|
||||
// Too many spaces?
|
||||
const a = 'test2';
|
||||
```
|
||||
### --before-all--
|
||||
```js
|
||||
global.__beforeAll = 'before-all';
|
||||
```
|
||||
## --fcc-end--
|
||||
@@ -1,139 +0,0 @@
|
||||
import {
|
||||
getProjectTitle,
|
||||
getLessonFromFile,
|
||||
getLessonDescription,
|
||||
getLessonHintsAndTests,
|
||||
getLessonSeed,
|
||||
getBeforeAll,
|
||||
getBeforeEach,
|
||||
getCommands,
|
||||
getFilesWithSeed,
|
||||
isForceFlag,
|
||||
extractStringFromCode
|
||||
} from '../tooling/parser.js';
|
||||
import { assert } from 'chai';
|
||||
import logover, { debug, error } from 'logover';
|
||||
|
||||
logover({
|
||||
debug: '\x1b[33m[parser.test]\x1b[0m',
|
||||
error: '\x1b[31m[parser.test]\x1b[0m',
|
||||
level: 'debug',
|
||||
timestamp: null
|
||||
});
|
||||
|
||||
const EXPECTED_PATH = './tests/fixtures/expected-format.md';
|
||||
const POOR_PATH = './tests/fixtures/valid-poor-format.md';
|
||||
|
||||
try {
|
||||
const projectTitle = await getProjectTitle(EXPECTED_PATH);
|
||||
assert.deepEqual(projectTitle, {
|
||||
projectTopic: 'Title',
|
||||
currentProject: 'Project'
|
||||
});
|
||||
const lesson = await getLessonFromFile(EXPECTED_PATH);
|
||||
|
||||
const lessonDescription = getLessonDescription(lesson);
|
||||
assert.equal(
|
||||
lessonDescription,
|
||||
'\nSome description.\n\nMaybe some code:\n\n```js\nconst a = 1;\n// A comment at the end?\n```\n'
|
||||
);
|
||||
|
||||
const lessonHintsAndTests = getLessonHintsAndTests(lesson);
|
||||
|
||||
assert.equal(lessonHintsAndTests[0][0], 'Test text with many words.');
|
||||
assert.equal(
|
||||
lessonHintsAndTests[0][1],
|
||||
"// First test code\nconst a = 'test';\n"
|
||||
);
|
||||
assert.equal(
|
||||
lessonHintsAndTests[1][0],
|
||||
'Second test text with `inline-code`.'
|
||||
);
|
||||
assert.equal(
|
||||
lessonHintsAndTests[1][1],
|
||||
"const a = 'test2';\n// Second test code;\n"
|
||||
);
|
||||
|
||||
const lessonSeed = getLessonSeed(lesson);
|
||||
|
||||
const beforeAll = getBeforeAll(lesson);
|
||||
assert.equal(beforeAll, "global.__beforeAll = 'before-all';\n\n\n");
|
||||
|
||||
const beforeEach = getBeforeEach(lesson);
|
||||
assert.equal(beforeEach, "global.__beforeEach = 'before-each';\n");
|
||||
|
||||
const commands = getCommands(lessonSeed);
|
||||
|
||||
const filesWithSeed = getFilesWithSeed(lessonSeed);
|
||||
|
||||
const isForce = isForceFlag(lessonSeed);
|
||||
} catch (e) {
|
||||
throw error(e);
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// VALID POOR FORMAT
|
||||
// -----------------
|
||||
|
||||
try {
|
||||
const projectTitle = await getProjectTitle(POOR_PATH);
|
||||
assert.deepEqual(projectTitle, {
|
||||
projectTopic: 'Title',
|
||||
currentProject: 'Project'
|
||||
});
|
||||
const lesson = await getLessonFromFile(POOR_PATH);
|
||||
|
||||
const lessonDescription = getLessonDescription(lesson);
|
||||
assert.equal(
|
||||
lessonDescription,
|
||||
'This description has no spaces between the heading.\n```rs\n\n//Same goes for this code.\nlet mut a = 1;\n// comment\n```'
|
||||
);
|
||||
|
||||
const lessonHintsAndTests = getLessonHintsAndTests(lesson);
|
||||
|
||||
assert.equal(lessonHintsAndTests[0][0], 'Test text at top.');
|
||||
assert.equal(
|
||||
lessonHintsAndTests[0][1],
|
||||
'// First test no space\n// No code?\n\n'
|
||||
);
|
||||
assert.equal(
|
||||
lessonHintsAndTests[1][0],
|
||||
'Second test text with `inline-code`.'
|
||||
);
|
||||
assert.equal(
|
||||
lessonHintsAndTests[1][1],
|
||||
"// Too many spaces?\nconst a = 'test2';\n"
|
||||
);
|
||||
|
||||
const lessonSeed = getLessonSeed(lesson);
|
||||
|
||||
const beforeAll = getBeforeAll(lesson);
|
||||
assert.equal(beforeAll, "global.__beforeAll = 'before-all';\n");
|
||||
|
||||
const beforeEach = getBeforeEach(lesson);
|
||||
|
||||
const commands = getCommands(lessonSeed);
|
||||
|
||||
const filesWithSeed = getFilesWithSeed(lessonSeed);
|
||||
|
||||
const isForce = isForceFlag(lessonSeed);
|
||||
} catch (e) {
|
||||
throw error(e);
|
||||
}
|
||||
|
||||
try {
|
||||
let stringFromCode = extractStringFromCode(`\`\`\`js
|
||||
const a = 1;
|
||||
\`\`\``);
|
||||
assert.equal(stringFromCode, 'const a = 1;\n');
|
||||
stringFromCode = extractStringFromCode(`\`\`\`js
|
||||
const a = 1;
|
||||
// comment
|
||||
\`\`\`
|
||||
`);
|
||||
assert.equal(stringFromCode, 'const a = 1;\n// comment\n\n');
|
||||
} catch (e) {
|
||||
throw error(e);
|
||||
}
|
||||
|
||||
debug('All tests passed! 🎉');
|
||||
@@ -2,17 +2,35 @@ export function toggleLoaderAnimation(ws) {
|
||||
ws.send(parse({ event: 'toggle-loader-animation' }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all tests in the tests state
|
||||
* @param {WebSocket} ws WebSocket connection to the client
|
||||
* @param {Test[]} tests Array of Test objects
|
||||
*/
|
||||
export function updateTests(ws, tests) {
|
||||
ws.send(parse({ event: 'update-tests', data: { tests } }));
|
||||
}
|
||||
/**
|
||||
* Update single test in the tests state
|
||||
* @param {WebSocket} ws WebSocket connection to the client
|
||||
* @param {Test} test Test object
|
||||
*/
|
||||
export function updateTest(ws, test) {
|
||||
ws.send(parse({ event: 'update-test', data: { test } }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the lesson description
|
||||
* @param {WebSocket} ws WebSocket connection to the client
|
||||
* @param {string} description Lesson description
|
||||
*/
|
||||
export function updateDescription(ws, description) {
|
||||
ws.send(parse({ event: 'update-description', data: { description } }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the heading of the lesson
|
||||
* @param {WebSocket} ws WebSocket connection to the client
|
||||
* @param {string} projectHeading Project heading
|
||||
*/
|
||||
export function updateProjectHeading(ws, projectHeading) {
|
||||
ws.send(
|
||||
parse({
|
||||
@@ -21,7 +39,11 @@ export function updateProjectHeading(ws, projectHeading) {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the project state
|
||||
* @param {WebSocket} ws WebSocket connection to the client
|
||||
* @param {Project} project Project object
|
||||
*/
|
||||
export function updateProject(ws, project) {
|
||||
ws.send(
|
||||
parse({
|
||||
@@ -30,11 +52,45 @@ export function updateProject(ws, project) {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the projects state
|
||||
* @param {WebSocket} ws WebSocket connection to the client
|
||||
* @param {Project[]} projects Array of Project objects
|
||||
*/
|
||||
export function updateProjects(ws, projects) {
|
||||
ws.send(
|
||||
parse({
|
||||
event: 'update-projects',
|
||||
data: projects
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Update the projects state
|
||||
* @param {WebSocket} ws WebSocket connection to the client
|
||||
* @param {any} config config object
|
||||
*/
|
||||
export function updateFreeCodeCampConfig(ws, config) {
|
||||
ws.send(
|
||||
parse({
|
||||
event: 'update-freeCodeCamp-config',
|
||||
data: config
|
||||
})
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Update hints
|
||||
* @param {WebSocket} ws WebSocket connection to the client
|
||||
* @param {string} hints `\n` separated string
|
||||
*/
|
||||
export function updateHints(ws, hints) {
|
||||
ws.send(parse({ event: 'update-hints', data: { hints } }));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {WebSocket} ws WebSocket connection to the client
|
||||
* @param {*} cons
|
||||
*/
|
||||
export function updateConsole(ws, cons) {
|
||||
ws.send(parse({ event: 'update-console', data: { cons } }));
|
||||
}
|
||||
|
||||
@@ -1,51 +1,63 @@
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
import { join } from 'path';
|
||||
|
||||
export const ROOT = join(__dirname, '../..');
|
||||
export const ROOT = process.env.INIT_CWD || process.cwd();
|
||||
|
||||
export async function readEnv() {
|
||||
let meta = {
|
||||
CURRENT_PROJECT: 'calculator',
|
||||
LOCALE: 'english'
|
||||
export async function getConfig() {
|
||||
const config = await readFile(join(ROOT, 'freecodecamp.conf.json'), 'utf-8');
|
||||
return JSON.parse(config);
|
||||
}
|
||||
|
||||
export const freeCodeCampConfig = await getConfig();
|
||||
|
||||
export async function getState() {
|
||||
let defaultState = {
|
||||
currentProject: null,
|
||||
locale: 'english'
|
||||
};
|
||||
try {
|
||||
const META = await readFile(join(ROOT, '.freeCodeCamp/.env'), 'utf8');
|
||||
const metaArr = META.split('\n').filter(Boolean);
|
||||
const new_meta = metaArr.reduce((meta, line) => {
|
||||
const [key, value] = line.split('=');
|
||||
return { ...meta, [key]: value };
|
||||
}, '');
|
||||
meta = { ...meta, ...new_meta };
|
||||
const state = JSON.parse(
|
||||
await readFile(
|
||||
join(ROOT, freeCodeCampConfig.config['state.json']),
|
||||
'utf-8'
|
||||
)
|
||||
);
|
||||
return { ...defaultState, ...state };
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
return meta;
|
||||
return defaultState;
|
||||
}
|
||||
|
||||
export async function updateEnv(obj) {
|
||||
// TODO: Maybe not completely overwrite the file?
|
||||
const env = { ...(await readEnv()), ...obj };
|
||||
export async function setState(obj) {
|
||||
const state = await getState();
|
||||
const updatedState = {
|
||||
...state,
|
||||
...obj
|
||||
};
|
||||
|
||||
await writeFile(
|
||||
join(ROOT, '.freeCodeCamp/.env'),
|
||||
Object.entries(env).reduce((acc, [key, value]) => {
|
||||
return `${acc}\n${key}=${value}`;
|
||||
}, '')
|
||||
join(ROOT, freeCodeCampConfig.config['state.json']),
|
||||
JSON.stringify(updatedState, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} project Project dashed name
|
||||
*/
|
||||
export async function getProjectConfig(project) {
|
||||
const projects = (
|
||||
await import(join(ROOT, '.freeCodeCamp/config/projects.json'), {
|
||||
assert: { type: 'json' }
|
||||
})
|
||||
).default;
|
||||
const projects = JSON.parse(
|
||||
await readFile(
|
||||
join(ROOT, freeCodeCampConfig.config['projects.json']),
|
||||
'utf-8'
|
||||
)
|
||||
);
|
||||
|
||||
const proj = projects.find(p => p.dashedName === project);
|
||||
|
||||
const defaultConfig = {
|
||||
testPollingRate: 100,
|
||||
testPollingRate: 333,
|
||||
currentLesson: 1,
|
||||
runTestsOnWatch: false,
|
||||
lastKnownLessonWithHash: 1,
|
||||
seedEveryLesson: false,
|
||||
@@ -63,20 +75,24 @@ export async function getProjectConfig(project) {
|
||||
* @param {object} config Config properties to set
|
||||
*/
|
||||
export async function setProjectConfig(project, config = {}) {
|
||||
const projects = (
|
||||
await import(join(ROOT, '.freeCodeCamp/config/projects.json'), {
|
||||
assert: { type: 'json' }
|
||||
})
|
||||
).default;
|
||||
const projects = JSON.parse(
|
||||
await readFile(
|
||||
join(ROOT, freeCodeCampConfig.config['projects.json']),
|
||||
'utf-8'
|
||||
)
|
||||
);
|
||||
|
||||
const updatedProject = {
|
||||
...projects.find(p => p.dashedName === project),
|
||||
...config
|
||||
};
|
||||
|
||||
const updatedProjects = projects.map(p =>
|
||||
p.dashedName === project ? updatedProject : p
|
||||
);
|
||||
|
||||
await writeFile(
|
||||
join(ROOT, '.freeCodeCamp/config/projects.json'),
|
||||
updatedProjects
|
||||
join(ROOT, freeCodeCampConfig.config['projects.json']),
|
||||
JSON.stringify(updatedProjects, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// This file handles creating the Git curriculum branches
|
||||
import { join } from 'path';
|
||||
import { PATH, readEnv, updateEnv } from '../env.js';
|
||||
import { getState, setState } from '../env.js';
|
||||
import {
|
||||
getCommands,
|
||||
getFilesWithSeed,
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
const PROJECT_LIST = ['project-1'];
|
||||
|
||||
for (const project of PROJECT_LIST) {
|
||||
await updateEnv({ CURRENT_PROJECT: project });
|
||||
await setState({ currentProject: project });
|
||||
try {
|
||||
await deleteBranch(project);
|
||||
await buildProject();
|
||||
@@ -35,13 +35,17 @@ for (const project of PROJECT_LIST) {
|
||||
console.log('✅ Successfully built all projects');
|
||||
|
||||
async function buildProject() {
|
||||
const { CURRENT_PROJECT } = await readEnv();
|
||||
const FILE = join(`${PATH}/tooling/locales/english/${CURRENT_PROJECT}.md`);
|
||||
const { currentProject } = await getState();
|
||||
const FILE = join(
|
||||
ROOT,
|
||||
freeCodeCampConfig.curriculum.locales['english'],
|
||||
project.dashedName + '.md'
|
||||
);
|
||||
|
||||
try {
|
||||
await initCurrentProjectBranch();
|
||||
} catch (e) {
|
||||
console.error('🔴 Failed to create a branch for ', CURRENT_PROJECT);
|
||||
console.error('🔴 Failed to create a branch for ', currentProject);
|
||||
throw new Error(e);
|
||||
}
|
||||
|
||||
@@ -49,7 +53,7 @@ async function buildProject() {
|
||||
let lesson = await getLessonFromFile(FILE, lessonNumber);
|
||||
if (!lesson) {
|
||||
return Promise.reject(
|
||||
new Error(`🔴 No lesson found for ${CURRENT_PROJECT}`)
|
||||
new Error(`🔴 No lesson found for ${currentProject}`)
|
||||
);
|
||||
}
|
||||
while (lesson) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// This file handles the fetching/parsing of the Git status of the project
|
||||
import { promisify } from 'util';
|
||||
import { exec } from 'child_process';
|
||||
import { readEnv, updateEnv } from '../env.js';
|
||||
import { getState, setState } from '../env.js';
|
||||
const execute = promisify(exec);
|
||||
|
||||
/**
|
||||
@@ -35,10 +35,10 @@ export async function commit(lessonNumber) {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function initCurrentProjectBranch() {
|
||||
const { CURRENT_PROJECT } = await readEnv();
|
||||
const { currentProject } = await getState();
|
||||
try {
|
||||
const { stdout, stderr } = await execute(
|
||||
`git checkout -b ${CURRENT_PROJECT}`
|
||||
`git checkout -b ${currentProject}`
|
||||
);
|
||||
// SILlY GIT PUTS A BRANCH SWITCH INTO STDERR!!!
|
||||
// if (stderr) {
|
||||
@@ -56,10 +56,10 @@ export async function initCurrentProjectBranch() {
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function getCommitHashByNumber(number) {
|
||||
const { LAST_KNOWN_LESSON_WITH_HASH, CURRENT_PROJECT } = await readEnv();
|
||||
const { lastKnownLessonWithHash, currentProject } = await getState();
|
||||
try {
|
||||
const { stdout, stderr } = await execute(
|
||||
`git log origin/${CURRENT_PROJECT} --oneline --grep="(${number})" --`
|
||||
`git log origin/${currentProject} --oneline --grep="(${number})" --`
|
||||
);
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
@@ -67,9 +67,9 @@ export async function getCommitHashByNumber(number) {
|
||||
const hash = stdout.match(/\w+/)?.[0];
|
||||
// This keeps track of the latest known commit in case there are no commits from one lesson to the next
|
||||
if (!hash) {
|
||||
return getCommitHashByNumber(LAST_KNOWN_LESSON_WITH_HASH);
|
||||
return getCommitHashByNumber(lastKnownLessonWithHash);
|
||||
}
|
||||
await updateEnv({ LAST_KNOWN_LESSON_WITH_HASH: number });
|
||||
await setState({ lastKnownLessonWithHash: number });
|
||||
return hash;
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
@@ -132,16 +132,16 @@ export async function setFileSystemToLessonNumber(lessonNumber) {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function pushProject() {
|
||||
const { CURRENT_PROJECT } = await readEnv();
|
||||
const { currentProject } = await getState();
|
||||
try {
|
||||
const { stdout, stderr } = await execute(
|
||||
`git push origin ${CURRENT_PROJECT} --force`
|
||||
`git push origin ${currentProject} --force`
|
||||
);
|
||||
// if (stderr) {
|
||||
// throw new Error(stderr);
|
||||
// }
|
||||
} catch (e) {
|
||||
console.error('🔴 Failed to push project ', CURRENT_PROJECT);
|
||||
console.error('🔴 Failed to push project ', currentProject);
|
||||
return Promise.reject(e);
|
||||
}
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -1,45 +1,57 @@
|
||||
// This file handles the watching of the /curriculum folder for changes
|
||||
// and executing the command to run the tests for the next (current) lesson
|
||||
import { readEnv, getProjectConfig, ROOT } from './env.js';
|
||||
import { getState, getProjectConfig, ROOT } from './env.js';
|
||||
import runLesson from './lesson.js';
|
||||
import runTests from './test.js';
|
||||
import { watch } from 'chokidar';
|
||||
import { join } from 'path';
|
||||
const { CURRENT_PROJECT } = await readEnv();
|
||||
const { currentProject } = await getState();
|
||||
const { testPollingRate, runTestsOnWatch } = await getProjectConfig(
|
||||
CURRENT_PROJECT
|
||||
currentProject
|
||||
);
|
||||
|
||||
function hotReload(ws) {
|
||||
console.log(`Watching for file changes on ${ROOT}`);
|
||||
let isWait = false;
|
||||
let testsRunning = false;
|
||||
let isClearConsole = false;
|
||||
|
||||
watch(ROOT, { ignored: join(ROOT, '.logs/.temp.log') }).on(
|
||||
'all',
|
||||
async (event, name) => {
|
||||
if (name) {
|
||||
if (isWait) return;
|
||||
isWait = setTimeout(() => {
|
||||
isWait = false;
|
||||
}, testPollingRate);
|
||||
// TODO: Migrate list to config
|
||||
const pathsToIgnore = [
|
||||
'.logs/.temp.log',
|
||||
'config/',
|
||||
'/node_modules/',
|
||||
'.git',
|
||||
'/target/',
|
||||
'/test-ledger/'
|
||||
];
|
||||
|
||||
const { CURRENT_PROJECT } = await readEnv();
|
||||
if (!CURRENT_PROJECT) {
|
||||
return;
|
||||
}
|
||||
const project = await getProjectConfig(CURRENT_PROJECT);
|
||||
if (isClearConsole) {
|
||||
console.clear();
|
||||
}
|
||||
runLesson(ws, project);
|
||||
// console.log(`Watcher: ${event} - ${name}`);
|
||||
if (runTestsOnWatch) {
|
||||
runTests(ws, project);
|
||||
}
|
||||
watch(ROOT, {
|
||||
ignoreInitial: true
|
||||
// `ignored` appears to do nothing. Have tried multiple permutations
|
||||
// ignored: pathsToIgnore.join('|') //p => pathsToIgnore.includes(p)
|
||||
}).on('all', async (event, name) => {
|
||||
if (name && !pathsToIgnore.find(p => name.includes(p))) {
|
||||
if (isWait) return;
|
||||
isWait = setTimeout(() => {
|
||||
isWait = false;
|
||||
}, testPollingRate);
|
||||
|
||||
const { currentProject } = await getState();
|
||||
if (!currentProject) {
|
||||
return;
|
||||
}
|
||||
if (isClearConsole) {
|
||||
console.clear();
|
||||
}
|
||||
await runLesson(ws, currentProject);
|
||||
if (runTestsOnWatch && !testsRunning) {
|
||||
console.log(`Watcher: ${event} - ${name}`);
|
||||
testsRunning = true;
|
||||
await runTests(ws, currentProject);
|
||||
testsRunning = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default hotReload;
|
||||
|
||||
@@ -8,22 +8,26 @@ import {
|
||||
getLessonSeed,
|
||||
isForceFlag
|
||||
} from './parser.js';
|
||||
import { LOCALE } from './t.js';
|
||||
import {
|
||||
updateDescription,
|
||||
updateProjectHeading,
|
||||
updateTests,
|
||||
updateProject
|
||||
} from './client-socks.js';
|
||||
import { ROOT, readEnv } from './env.js';
|
||||
import { ROOT, getState, getProjectConfig, freeCodeCampConfig } from './env.js';
|
||||
import seedLesson from './seed.js';
|
||||
|
||||
async function runLesson(ws, project) {
|
||||
const locale = LOCALE === 'undefined' ? 'english' : LOCALE ?? 'english';
|
||||
/**
|
||||
* Runs the lesson from the `project` config.
|
||||
* @param {WebSocket} ws WebSocket connection to the client
|
||||
* @param {object} projectDashedName Project dashed-name
|
||||
*/
|
||||
async function runLesson(ws, projectDashedName) {
|
||||
const project = await getProjectConfig(projectDashedName);
|
||||
const { locale } = await getState();
|
||||
const projectFile = join(
|
||||
ROOT,
|
||||
'.freeCodeCamp/tooling/locales',
|
||||
locale,
|
||||
freeCodeCampConfig.curriculum.locales[locale],
|
||||
project.dashedName + '.md'
|
||||
);
|
||||
const lessonNumber = project.currentLesson;
|
||||
@@ -32,7 +36,6 @@ async function runLesson(ws, project) {
|
||||
|
||||
updateProject(ws, project);
|
||||
|
||||
const { SEED_EVERY_LESSON } = await readEnv();
|
||||
if (!project.isIntegrated) {
|
||||
const hintsAndTestsArr = getLessonHintsAndTests(lesson);
|
||||
updateTests(
|
||||
@@ -54,8 +57,8 @@ async function runLesson(ws, project) {
|
||||
const isForce = isForceFlag(seed);
|
||||
// force flag overrides seed flag
|
||||
if (
|
||||
(SEED_EVERY_LESSON === 'true' && !isForce) ||
|
||||
(SEED_EVERY_LESSON !== 'true' && isForce)
|
||||
(project.seedEveryLesson && !isForce) ||
|
||||
(!project.seedEveryLesson && isForce)
|
||||
) {
|
||||
await seedLesson(ws, project, lessonNumber);
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export const locales = ['english', 'spanish'];
|
||||
|
||||
export const translatedLocales = {
|
||||
english: 'English',
|
||||
spanish: 'Español'
|
||||
};
|
||||
@@ -1,37 +1,15 @@
|
||||
{
|
||||
"greeting": "Welcome. This course is available in multiple languages. Type one from below:",
|
||||
"call-to-action": "The language {{locale}} is not yet supported. Help us translate this course: https://contribute.freecodecamp.org/",
|
||||
"task": "Task",
|
||||
"lesson": "LESSON",
|
||||
"next-lesson": "When you are done, type the following for the next lesson:",
|
||||
"reset-error": "An error occured trying to reset your progress",
|
||||
"lesson-reset": "Lesson #{{lessonNumber}} reset",
|
||||
"switch-success": "Successfully switched to project: {{project}}",
|
||||
"access-lessons": "You should be able to access the lessons with:",
|
||||
"switch-error": "An error has occured trying to switch to the chosen project:",
|
||||
"switch-navigate": "Please navigate to the '{{location}}' file, and add the following line:",
|
||||
"valid-project": "a valid project",
|
||||
"create-new-project-error": "It looks like you have not created a new project with:",
|
||||
"lesson-correct": "Lesson #{{lessonNumber}} is correct",
|
||||
"tests-error": "An error occured trying to run the tests:",
|
||||
"shell-permission": "Gives the shell permission to run fcc",
|
||||
"fcc-n": "Runs the nth lesson",
|
||||
"fcc-reset-n": "Resets the nth lesson",
|
||||
"fcc solution-n": "Prints the solution for the nth lesson",
|
||||
"fcc-switch-project": "Switches between the lessons for <project>",
|
||||
"fcc-test-n": "Runs the regex tests for the nth lesson",
|
||||
"fcc-help": "Prints this help message",
|
||||
"fcc-locale": "Changes language to <locale>",
|
||||
"cargo-run": "Runs the <project>/src/main.rs binary",
|
||||
"rust-docs": "Rust documentation",
|
||||
"rust-book": "Rust book",
|
||||
"file-error": "Error reading {{file}}",
|
||||
"not-enough-arguments": "Not enough arguments given",
|
||||
"too-many-arguments": "Too many arguments given",
|
||||
"already-on-project": "Already on project {{project}}",
|
||||
"project-not-exist": "Project {{project}} does not exist. Here are the available projects:",
|
||||
"invalid-argument": "Invalid argument",
|
||||
"welcome": "Welcome to the freeCodeCamp Rust in Replit course!\n\nYou will be using this console (or the Shell) to read majority of the instructions, throughout this course.\n\nTo get your first lesson, type the following in the prompt:\n\n\t$ fcc 1\n\nYou can click the Run button at any time to get back to this screen.\n\nOnce the course is set up, you can get help and see options by running:\n\n\t$ fcc help\n\n\nIf at any point you get stuck, you can either:\n\t- Reset your code with: $ fcc reset <n>\n\t- View the answer to the code with: $ fcc solution <n>",
|
||||
"set-locale-success": "Successfully set the locale to {{locale}}",
|
||||
"set-locale-error": "An error occured trying to set the locale to {{locale}}"
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@ import {
|
||||
getCommands,
|
||||
getFilesWithSeed
|
||||
} from './parser.js';
|
||||
import { LOCALE } from './t.js';
|
||||
import { ROOT } from './env.js';
|
||||
import { ROOT, getState, freeCodeCampConfig } from './env.js';
|
||||
import { writeFile } from 'fs/promises';
|
||||
import { promisify } from 'util';
|
||||
import { exec } from 'child_process';
|
||||
@@ -16,11 +15,10 @@ const execute = promisify(exec);
|
||||
export default async function seedLesson(ws, project) {
|
||||
// TODO: Use ws to display loader whilst seeding
|
||||
const lessonNumber = project.currentLesson;
|
||||
const locale = LOCALE === 'undefined' ? 'english' : LOCALE ?? 'english';
|
||||
const { locale } = await getState();
|
||||
const projectFile = join(
|
||||
ROOT,
|
||||
'.freeCodeCamp/tooling/locales',
|
||||
locale,
|
||||
freeCodeCampConfig.curriculum.locales[locale],
|
||||
project.dashedName + '.md'
|
||||
);
|
||||
const lesson = await getLessonFromFile(projectFile, lessonNumber);
|
||||
|
||||
@@ -1,79 +1,112 @@
|
||||
import express from 'express';
|
||||
import { readFile } from 'fs/promises';
|
||||
import runTests from './test.js';
|
||||
import {
|
||||
getProjectConfig,
|
||||
readEnv,
|
||||
getState,
|
||||
ROOT,
|
||||
setProjectConfig,
|
||||
updateEnv
|
||||
setState,
|
||||
getConfig
|
||||
} from './env.js';
|
||||
import logover, { debug, error, warn } from 'logover';
|
||||
|
||||
import { WebSocketServer } from 'ws';
|
||||
import runLesson from './lesson.js';
|
||||
import { updateTests, updateHints, updateConsole } from './client-socks.js';
|
||||
import {
|
||||
updateTests,
|
||||
updateHints,
|
||||
updateConsole,
|
||||
updateProjects,
|
||||
updateFreeCodeCampConfig
|
||||
} from './client-socks.js';
|
||||
import hotReload from './hot-reload.js';
|
||||
import projects from '../config/projects.json' assert { 'type': 'json' };
|
||||
import { hideAll, showFile } from './utils.js';
|
||||
import { hideAll, showFile, showAll } from './utils.js';
|
||||
import { join } from 'path';
|
||||
logover({
|
||||
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug'
|
||||
});
|
||||
const freeCodeCampConfig = await getConfig();
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(express.static('./dist'));
|
||||
app.use(express.static(join(ROOT, '.freeCodeCamp/dist')));
|
||||
|
||||
async function handleRunTests(ws, data) {
|
||||
const { CURRENT_PROJECT } = await readEnv();
|
||||
const project = await getProjectConfig(CURRENT_PROJECT);
|
||||
runTests(ws, project);
|
||||
const { currentProject } = await getState();
|
||||
await runTests(ws, currentProject);
|
||||
ws.send(parse({ data: { event: data.event }, event: 'RESPONSE' }));
|
||||
}
|
||||
|
||||
function handleResetProject(ws, data) {}
|
||||
function handleResetLesson(ws, data) {}
|
||||
|
||||
async function handleGoToNextLesson(ws, data) {
|
||||
const { CURRENT_PROJECT } = await readEnv();
|
||||
const project = await getProjectConfig(CURRENT_PROJECT);
|
||||
const { currentProject } = await getState();
|
||||
const project = await getProjectConfig(currentProject);
|
||||
const nextLesson = project.currentLesson + 1;
|
||||
setProjectConfig(CURRENT_PROJECT, { currentLesson: nextLesson });
|
||||
runLesson(ws, project);
|
||||
await setProjectConfig(currentProject, { currentLesson: nextLesson });
|
||||
await runLesson(ws, project.dashedName);
|
||||
updateHints(ws, '');
|
||||
updateTests(ws, []);
|
||||
updateConsole(ws, '');
|
||||
ws.send(parse({ data: { event: data.event }, event: 'RESPONSE' }));
|
||||
}
|
||||
|
||||
async function handleGoToPreviousLesson(ws, data) {
|
||||
const { CURRENT_PROJECT } = await readEnv();
|
||||
const project = await getProjectConfig(CURRENT_PROJECT);
|
||||
const { currentProject } = await getState();
|
||||
const project = await getProjectConfig(currentProject);
|
||||
const prevLesson = project.currentLesson - 1;
|
||||
setProjectConfig(CURRENT_PROJECT, { CURRENT_LESSON: prevLesson });
|
||||
runLesson(ws, project);
|
||||
await setProjectConfig(currentProject, { currentLesson: prevLesson });
|
||||
await runLesson(ws, project.dashedName);
|
||||
updateTests(ws, []);
|
||||
updateHints(ws, '');
|
||||
updateConsole(ws, '');
|
||||
ws.send(parse({ data: { event: data.event }, event: 'RESPONSE' }));
|
||||
}
|
||||
|
||||
async function handleConnect(ws) {
|
||||
const { CURRENT_PROJECT } = await readEnv();
|
||||
if (!CURRENT_PROJECT) {
|
||||
const projects = JSON.parse(
|
||||
await readFile(
|
||||
join(ROOT, freeCodeCampConfig.config['projects.json']),
|
||||
'utf-8'
|
||||
)
|
||||
);
|
||||
updateProjects(ws, projects);
|
||||
updateFreeCodeCampConfig(ws, freeCodeCampConfig);
|
||||
const { currentProject } = await getState();
|
||||
if (!currentProject) {
|
||||
return;
|
||||
}
|
||||
const project = await getProjectConfig(CURRENT_PROJECT);
|
||||
runLesson(ws, project);
|
||||
const project = await getProjectConfig(currentProject);
|
||||
runLesson(ws, project.dashedName);
|
||||
}
|
||||
|
||||
async function handleSelectProject(ws, data) {
|
||||
const projects = JSON.parse(
|
||||
await readFile(
|
||||
join(ROOT, freeCodeCampConfig.config['projects.json']),
|
||||
'utf-8'
|
||||
)
|
||||
);
|
||||
const selectedProject = projects.find(p => p.id === data?.data?.id);
|
||||
// TODO: Should this set the CURRENT_PROJECT to `null` (empty string)?
|
||||
// TODO: Should this set the currentProject to `null` (empty string)?
|
||||
// for the case where the Camper has navigated to the landing page.
|
||||
await updateEnv({ CURRENT_PROJECT: selectedProject?.dashedName ?? '' });
|
||||
if (!selectedProject) {
|
||||
await setState({ currentProject: selectedProject?.dashedName ?? null });
|
||||
if (!selectedProject && !data?.data?.id) {
|
||||
warn('Selected project does not exist: ', data);
|
||||
return;
|
||||
return ws.send(parse({ data: { event: data.event }, event: 'RESPONSE' }));
|
||||
}
|
||||
await hideAll();
|
||||
await showFile(selectedProject.dashedName);
|
||||
runLesson(ws, selectedProject);
|
||||
|
||||
// Disabled whilst in development because it is annoying
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
await hideAll();
|
||||
await showFile(selectedProject.dashedName);
|
||||
} else {
|
||||
await showAll();
|
||||
}
|
||||
await runLesson(ws, selectedProject.dashedName);
|
||||
return ws.send(parse({ data: { event: data.event }, event: 'RESPONSE' }));
|
||||
}
|
||||
|
||||
const server = app.listen(8080, () => {
|
||||
@@ -99,6 +132,16 @@ wss.on('connection', function connection(ws) {
|
||||
const parsedData = parseBuffer(data);
|
||||
handle[parsedData.event]?.(ws, parsedData);
|
||||
});
|
||||
(async () => {
|
||||
const projects = JSON.parse(
|
||||
await readFile(
|
||||
join(ROOT, freeCodeCampConfig.config['projects.json']),
|
||||
'utf-8'
|
||||
)
|
||||
);
|
||||
updateProjects(ws, projects);
|
||||
updateFreeCodeCampConfig(ws, freeCodeCampConfig);
|
||||
})();
|
||||
sock('connect', { message: "Server says 'Hello!'" });
|
||||
|
||||
function sock(type, data = {}) {
|
||||
@@ -113,3 +156,28 @@ function parse(obj) {
|
||||
function parseBuffer(buf) {
|
||||
return JSON.parse(buf.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Files currently under ownership by another thread.
|
||||
*/
|
||||
const RACING_FILES = new Set();
|
||||
const FREEDOM_TIMEOUT = 100;
|
||||
|
||||
/**
|
||||
* Adds an operation to the race queue. If a file is already in the queue, the op is delayed until the file is released.
|
||||
* @param {string} filepath Path to file to move
|
||||
* @param {*} cb Callback to call once file is free
|
||||
*/
|
||||
async function addToRaceQueue(filepath, cb) {
|
||||
const isFileFree = await new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
if (!RACING_FILES.has(filepath)) {
|
||||
resolve(true);
|
||||
}
|
||||
}, FREEDOM_TIMEOUT);
|
||||
});
|
||||
if (isFileFree) {
|
||||
RACING_FILES.add(filepath);
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { readEnv, ROOT } from './env.js';
|
||||
|
||||
export const LOCALE = await readEnv('.meta').LOCALE;
|
||||
import { getState } from './env.js';
|
||||
|
||||
export async function t(key, args = {}, forceLangToUse) {
|
||||
const loc = await readEnv().LOCALE;
|
||||
const { locale: loc } = await getState();
|
||||
// Get key from ./locales/{locale}/comments.json
|
||||
// Read file and parse JSON
|
||||
const locale =
|
||||
forceLangToUse ?? loc === 'undefined' ? 'english' : loc ?? 'english';
|
||||
const locale = forceLangToUse ?? loc;
|
||||
const comments = import(
|
||||
join(ROOT, `.freeCodeCamp/tooling/locales/${locale}/comments.json`),
|
||||
`.freeCodeCamp/tooling/locales/${locale}/comments.json`,
|
||||
{
|
||||
assert: { type: 'json' }
|
||||
}
|
||||
|
||||
@@ -1,50 +1,97 @@
|
||||
import { readFile, readdir } from 'fs/promises';
|
||||
import { exec, execSync } from 'child_process';
|
||||
import sha256 from 'crypto-js/sha256.js';
|
||||
import { promisify } from 'util';
|
||||
import elliptic from 'elliptic';
|
||||
import { join } from 'path';
|
||||
import fs from 'fs';
|
||||
import { ROOT } from './env.js';
|
||||
import WebSocket, { WebSocketServer } from 'ws';
|
||||
|
||||
// ---------------
|
||||
// GENERIC HELPERS
|
||||
// ---------------
|
||||
const PATH_TERMINAL_OUT = join(ROOT, '.logs/.terminal-out.log');
|
||||
const PATH_BASH_HISTORY = join(ROOT, '.logs/.bash_history.log');
|
||||
const PATH_CWD = join(ROOT, '.logs/.cwd.log');
|
||||
|
||||
const execute = promisify(exec);
|
||||
/**
|
||||
* Get the `.logs/.terminal-out.log` file contents, or `throw` if not found
|
||||
* @returns {Promise<string>} The `.terminal-out.log` file contents
|
||||
*/
|
||||
async function getTerminalOutput() {
|
||||
const terminalLogs = await readFile(PATH_TERMINAL_OUT, 'utf8');
|
||||
if (!terminalLogs) {
|
||||
throw new Error(`Could not find ${PATH_TERMINAL_OUT}`);
|
||||
}
|
||||
return terminalLogs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `.logs/.bash_history.log` file contents
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function getBashHistory() {
|
||||
const bashHistory = await readFile(PATH_BASH_HISTORY, 'utf8');
|
||||
if (!bashHistory) {
|
||||
throw new Error(`Could not find ${PATH_CWD}`);
|
||||
}
|
||||
return bashHistory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `.logs/.bash_history.log` file contents, or `throw` is not found
|
||||
* @param {number?} howManyBack The `nth` log from the history
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function getLastCommand(howManyBack = 0) {
|
||||
const bashLogs = await getBashHistory();
|
||||
|
||||
const logs = bashLogs.split('\n').filter(l => l !== '');
|
||||
const lastLog = logs[logs.length - howManyBack - 1];
|
||||
|
||||
return lastLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `.logs/.cwd.log` file contents
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function getCWD() {
|
||||
const cwd = await readFile(PATH_CWD, 'utf8');
|
||||
if (!cwd) {
|
||||
throw new Error(`Could not find ${PATH_CWD}`);
|
||||
}
|
||||
return cwd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `.logs/.cwd.log` file contents, or `throw` is not found
|
||||
* @param {number} howManyBack The `nth` log from the current working directory history
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function getLastCWD(howManyBack = 0) {
|
||||
const currentWorkingDirectory = await getCWD();
|
||||
|
||||
const logs = currentWorkingDirectory.split('\n').filter(l => l !== '');
|
||||
const lastLog = logs[logs.length - howManyBack - 1];
|
||||
|
||||
return lastLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of a directory
|
||||
* @param {string} path Path relative to root of working directory
|
||||
* @returns {string[]} An array of file names
|
||||
* @returns {Promise<string[]>} An array of file names
|
||||
*/
|
||||
async function getDirectory(path) {
|
||||
const files = await readdir(join(ROOT, path));
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `.logs/.terminal-out.log` file contents, or `throw` if not found
|
||||
* @returns {string} The `.terminal-out.log` file contents
|
||||
*/
|
||||
async function getTerminalOutput() {
|
||||
const pathToTerminalLogs = join(ROOT, '.logs/.terminal-out.log');
|
||||
const terminalLogs = await readFile(pathToTerminalLogs, 'utf8');
|
||||
|
||||
// TODO: Throwing is probably an anti-pattern?
|
||||
if (!terminalLogs) {
|
||||
throw new Error('No terminal logs found');
|
||||
}
|
||||
|
||||
return terminalLogs;
|
||||
}
|
||||
|
||||
const execute = promisify(exec);
|
||||
/**
|
||||
* Returns the output of a command called from a given path
|
||||
* @param {string} command
|
||||
* @param {string} path Path relative to root of working directory
|
||||
* @returns {{stdout, stderr}}
|
||||
* @returns {Promise<{stdout, stderr}>}
|
||||
*/
|
||||
async function getCommandOutput(command, path = '') {
|
||||
try {
|
||||
@@ -58,40 +105,10 @@ async function getCommandOutput(command, path = '') {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `.logs/.bash_history.log` file contents, or `throw` is not found
|
||||
* @param {number?} howManyBack The `nth` log from the history
|
||||
* @returns {string}
|
||||
*/
|
||||
async function getLastCommand(howManyBack = 0) {
|
||||
const pathToBashLogs = join(ROOT, '.logs/.bash_history.log');
|
||||
const bashLogs = await readFile(pathToBashLogs, 'utf8');
|
||||
|
||||
if (!bashLogs) {
|
||||
throw new Error(`Could not find ${pathToBashLogs}`);
|
||||
}
|
||||
|
||||
const logs = bashLogs.split('\n');
|
||||
const lastLog = logs[logs.length - howManyBack - 2];
|
||||
|
||||
return lastLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `.logs/.cwd.log` file contents
|
||||
* @returns {string}
|
||||
*/
|
||||
async function getCWD() {
|
||||
// TODO: Do not return whole file?
|
||||
const pathToCWD = join(ROOT, '.logs/.cwd.log');
|
||||
const cwd = await readFile(pathToCWD, 'utf8');
|
||||
return cwd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a file from the given `path`
|
||||
* @param {string} path Path relative to root of working directory
|
||||
* @returns {string}
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function getFile(path) {
|
||||
const file = await readFile(join(ROOT, path), 'utf8');
|
||||
@@ -103,7 +120,7 @@ async function getFile(path) {
|
||||
* @param {string} path Path relative to root of working directory
|
||||
* @returns {boolean}
|
||||
*/
|
||||
async function fileExists(path) {
|
||||
function fileExists(path) {
|
||||
return fs.existsSync(join(ROOT, path));
|
||||
}
|
||||
|
||||
@@ -112,7 +129,7 @@ async function fileExists(path) {
|
||||
* @param {string} folderToCopyPath Path to folder to copy relative to root
|
||||
* @param {string} destinationFolderPath Path to folder destination relative to root
|
||||
*/
|
||||
async function copyDirectory(folderToCopyPath, destinationFolderPath) {
|
||||
function copyDirectory(folderToCopyPath, destinationFolderPath) {
|
||||
const folderToCopy = join(ROOT, folderToCopyPath);
|
||||
const destinationFolder = join(ROOT, destinationFolderPath);
|
||||
|
||||
@@ -125,7 +142,7 @@ async function copyDirectory(folderToCopyPath, destinationFolderPath) {
|
||||
});
|
||||
}
|
||||
|
||||
async function copyProjectFiles(
|
||||
function copyProjectFiles(
|
||||
projectFolderPath,
|
||||
testsFolderPath,
|
||||
arrayOfFiles = []
|
||||
@@ -147,7 +164,7 @@ async function copyProjectFiles(
|
||||
* @param {string} command Command string to run
|
||||
* @param {string} path Path relative to root to run command in
|
||||
*/
|
||||
async function runCommand(command, path) {
|
||||
function runCommand(command, path) {
|
||||
execSync(command, {
|
||||
cwd: join(ROOT, path),
|
||||
shell: '/bin/bash'
|
||||
@@ -159,7 +176,7 @@ async function runCommand(command, path) {
|
||||
* @param {string} filePath Path to JSON file relative to root
|
||||
* @returns {object} `JSON.parse` file contents
|
||||
*/
|
||||
async function getJsonFile(filePath) {
|
||||
function getJsonFile(filePath) {
|
||||
const fileString = fs.readFileSync(join(ROOT, filePath));
|
||||
return JSON.parse(fileString);
|
||||
}
|
||||
@@ -169,165 +186,59 @@ async function getJsonFile(filePath) {
|
||||
* @param {string} path Path to JSON file relative to root
|
||||
* @param {any} content Stringifiable content to write to `path`
|
||||
*/
|
||||
async function writeJsonFile(path, content) {
|
||||
function writeJsonFile(path, content) {
|
||||
fs.writeFileSync(join(ROOT, path), JSON.stringify(content, null, 2));
|
||||
}
|
||||
|
||||
// ------------------
|
||||
// BLOCKCHAIN HELPERS
|
||||
// ------------------
|
||||
/**
|
||||
* @typedef ControlWrapperOptions
|
||||
* @type {object}
|
||||
* @property {number} timeout
|
||||
* @property {number} stepSize
|
||||
*/
|
||||
|
||||
const ec = new elliptic.ec('p192');
|
||||
|
||||
async function generateHash(content) {
|
||||
const hash = sha256(content).toString();
|
||||
return hash;
|
||||
}
|
||||
|
||||
async function generateSignature(privateKey, content) {
|
||||
const keyPair = ec.keyFromPrivate(privateKey, 'hex');
|
||||
const signature = keyPair.sign(content).toDER('hex');
|
||||
return signature;
|
||||
}
|
||||
|
||||
async function validateSignature(publicKey, content, signature) {
|
||||
const keyPair = ec.keyFromPublic(publicKey, 'hex');
|
||||
const verifiedSignature = keyPair.verify(content, signature);
|
||||
return verifiedSignature;
|
||||
}
|
||||
|
||||
async function getPublicKeyFromPrivate(privateKey) {
|
||||
const keyPair = ec.keyFromPrivate(privateKey, 'hex');
|
||||
const publicKey = keyPair.getPublic('hex');
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// TODO: @moT01 should move these into their respective files
|
||||
// ----------------------------------------------------------
|
||||
|
||||
// used in fundraising contract project
|
||||
async function getContract(contractAddress, cwd, includePool = true) {
|
||||
// get the latest contract state from the blockchain
|
||||
const blockchain = await getJsonFile(`${cwd}/blockchain.json`);
|
||||
const latestContract = blockchain.reduce((currentContract, nextBlock) => {
|
||||
if (nextBlock.smartContracts) {
|
||||
nextBlock.smartContracts.forEach(contract => {
|
||||
if (contract.address === contractAddress) {
|
||||
// first occurrence of contract
|
||||
if (!currentContract.hasOwnProperty('address')) {
|
||||
Object.keys(contract).forEach(
|
||||
key => (currentContract[key] = contract[key])
|
||||
);
|
||||
|
||||
// contract found and added, only update state after that
|
||||
} else if (contract.hasOwnProperty('state')) {
|
||||
currentContract.state = contract.state;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return currentContract;
|
||||
}, {});
|
||||
|
||||
if (includePool) {
|
||||
// add contract pool to latest contract state
|
||||
const smartContracts = await getJsonFile(`${cwd}/smart-contracts.json`);
|
||||
smartContracts.forEach(contract => {
|
||||
if (contract.address === contractAddress) {
|
||||
if (!latestContract.hasOwnProperty('address')) {
|
||||
Object.keys(contract).forEach(
|
||||
key => (latestContract[key] = contract[key])
|
||||
);
|
||||
} else if (latestContract.hasOwnProperty('state')) {
|
||||
latestContract.state = contract.state;
|
||||
/**
|
||||
* Wraps a function in an interval to retry until it succeeds
|
||||
* @param {callback} cb Callback to wrap
|
||||
* @param {ControlWrapperOptions} options Options to pass to `ControlWrapper`
|
||||
* @returns {Promise<any>} Returns the result of the callback or `null`
|
||||
*/
|
||||
async function controlWrapper(cb, { timeout = 10000, stepSize = 250 }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const response = await cb();
|
||||
if (response) {
|
||||
clearInterval(interval);
|
||||
resolve(response);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return latestContract.hasOwnProperty('address') ? latestContract : null;
|
||||
}
|
||||
|
||||
// for p2p network project
|
||||
async function canConnectToSocket(address) {
|
||||
return await new Promise(resolve => {
|
||||
const socket = new WebSocket(address, { shouldKeepAlive: false });
|
||||
socket.on('open', () => {
|
||||
socket.close();
|
||||
resolve(true);
|
||||
});
|
||||
socket.on('error', () => resolve(false));
|
||||
});
|
||||
}
|
||||
|
||||
async function startSocketServerAndHandshake({
|
||||
myPort: port,
|
||||
theirAddress = 'ws://localhost:4001',
|
||||
connectOnly = false
|
||||
}) {
|
||||
return await new Promise(resolve => {
|
||||
const address = `ws://localhost:${port}`;
|
||||
|
||||
const server = new WebSocketServer({ port });
|
||||
server.on('connection', externalSocket => {
|
||||
if (connectOnly) {
|
||||
externalSocket.close();
|
||||
server.close();
|
||||
resolve(true);
|
||||
}
|
||||
|
||||
externalSocket.on('message', messageString => {
|
||||
const message = JSON.parse(messageString);
|
||||
|
||||
if (message.hasOwnProperty('type') && message.type === 'HANDSHAKE') {
|
||||
externalSocket.close();
|
||||
server.close();
|
||||
resolve(message);
|
||||
} else {
|
||||
externalSocket.close();
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}, stepSize);
|
||||
setTimeout(() => {
|
||||
server.close();
|
||||
resolve();
|
||||
}, 5000);
|
||||
|
||||
server.on('error', () => server.close());
|
||||
|
||||
const socket = new WebSocket(theirAddress, { shouldKeepAlive: false });
|
||||
socket.on('open', () => {
|
||||
socket.send(JSON.stringify({ type: 'HANDSHAKE', data: [address] }));
|
||||
socket.close();
|
||||
});
|
||||
|
||||
socket.on('error', () => resolve());
|
||||
clearInterval(interval);
|
||||
reject(null);
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
const __helpers = {
|
||||
getDirectory,
|
||||
getFile,
|
||||
fileExists,
|
||||
getTerminalOutput,
|
||||
getCommandOutput,
|
||||
getLastCommand,
|
||||
getCWD,
|
||||
controlWrapper,
|
||||
copyDirectory,
|
||||
copyProjectFiles,
|
||||
runCommand,
|
||||
fileExists,
|
||||
getBashHistory,
|
||||
getCommandOutput,
|
||||
getCWD,
|
||||
getDirectory,
|
||||
getFile,
|
||||
getJsonFile,
|
||||
writeJsonFile,
|
||||
generateHash,
|
||||
generateSignature,
|
||||
validateSignature,
|
||||
getPublicKeyFromPrivate,
|
||||
getContract,
|
||||
canConnectToSocket,
|
||||
startSocketServerAndHandshake
|
||||
getLastCommand,
|
||||
getLastCWD,
|
||||
getTerminalOutput,
|
||||
runCommand,
|
||||
writeJsonFile
|
||||
};
|
||||
|
||||
export default __helpers;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// These are used in the local scope of the `eval` in `runTests`
|
||||
import fs from 'fs';
|
||||
import { assert, AssertionError } from 'chai';
|
||||
import __helpers from './test-utils.js';
|
||||
import __helpers_c from './test-utils.js';
|
||||
|
||||
import {
|
||||
getLessonHintsAndTests,
|
||||
@@ -11,8 +11,13 @@ import {
|
||||
getAfterAll
|
||||
} from './parser.js';
|
||||
|
||||
import { LOCALE } from './t.js';
|
||||
import { ROOT, setProjectConfig } from './env.js';
|
||||
import {
|
||||
freeCodeCampConfig,
|
||||
getProjectConfig,
|
||||
getState,
|
||||
ROOT,
|
||||
setProjectConfig
|
||||
} from './env.js';
|
||||
import runLesson from './lesson.js';
|
||||
import {
|
||||
toggleLoaderAnimation,
|
||||
@@ -27,14 +32,22 @@ logover({
|
||||
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug'
|
||||
});
|
||||
|
||||
export default async function runTests(ws, project) {
|
||||
const locale = LOCALE === 'undefined' ? 'english' : LOCALE ?? 'english';
|
||||
toggleLoaderAnimation(ws);
|
||||
let __helpers = __helpers_c;
|
||||
|
||||
export default async function runTests(ws, projectDashedName) {
|
||||
// Update __helpers with dynamic utils:
|
||||
const helpers = freeCodeCampConfig.tooling?.['helpers'];
|
||||
if (helpers) {
|
||||
const dynamicHelpers = await import(join(ROOT, helpers));
|
||||
__helpers = { ...__helpers_c, ...dynamicHelpers };
|
||||
}
|
||||
const project = await getProjectConfig(projectDashedName);
|
||||
const { locale } = await getState();
|
||||
// toggleLoaderAnimation(ws);
|
||||
const lessonNumber = project.currentLesson;
|
||||
const projectFile = join(
|
||||
ROOT,
|
||||
'.freeCodeCamp/tooling/locales',
|
||||
locale,
|
||||
freeCodeCampConfig.curriculum.locales[locale],
|
||||
project.dashedName + '.md'
|
||||
);
|
||||
try {
|
||||
@@ -53,6 +66,7 @@ export default async function runTests(ws, project) {
|
||||
error(e);
|
||||
}
|
||||
}
|
||||
// toggleLoaderAnimation(ws);
|
||||
|
||||
const hintsAndTestsArr = getLessonHintsAndTests(lesson);
|
||||
updateTests(
|
||||
@@ -65,7 +79,7 @@ export default async function runTests(ws, project) {
|
||||
}, [])
|
||||
);
|
||||
updateConsole(ws, '');
|
||||
const testPromises = hintsAndTestsArr.map(async ([hint, test], i) => {
|
||||
const testPromises = hintsAndTestsArr.map(async ([hint, testCode], i) => {
|
||||
if (beforeEach) {
|
||||
try {
|
||||
debug('Starting: --before-each-- hook');
|
||||
@@ -79,7 +93,7 @@ export default async function runTests(ws, project) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
const _testOutput = await eval(`(async () => {${test}})();`);
|
||||
const _testOutput = await eval(`(async () => {${testCode}})();`);
|
||||
updateTest(ws, {
|
||||
passed: true,
|
||||
testText: hint,
|
||||
@@ -90,15 +104,17 @@ export default async function runTests(ws, project) {
|
||||
if (!(e instanceof AssertionError)) {
|
||||
error(e);
|
||||
}
|
||||
const consoleError = { id: i, hint, error: e };
|
||||
|
||||
updateConsole(ws, consoleError);
|
||||
updateTest(ws, {
|
||||
const testState = {
|
||||
passed: false,
|
||||
testText: hint,
|
||||
isLoading: false,
|
||||
testId: i
|
||||
});
|
||||
};
|
||||
const consoleError = { ...testState, error: e };
|
||||
|
||||
updateConsole(ws, consoleError);
|
||||
updateTest(ws, testState);
|
||||
return Promise.reject(`- ${hint}\n`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
@@ -112,7 +128,7 @@ export default async function runTests(ws, project) {
|
||||
await setProjectConfig(project.dashedName, {
|
||||
currentLesson: lessonNumber + 1
|
||||
});
|
||||
runLesson(ws, project);
|
||||
await runLesson(ws, projectDashedName);
|
||||
updateHints(ws, '');
|
||||
}
|
||||
} else {
|
||||
@@ -142,7 +158,5 @@ export default async function runTests(ws, project) {
|
||||
} catch (e) {
|
||||
error('Test Error: ');
|
||||
debug(e);
|
||||
} finally {
|
||||
toggleLoaderAnimation(ws);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { cp, readdir, rm, rmdir, writeFile } from 'fs/promises';
|
||||
import { cp, readdir, rm, rmdir, writeFile, readFile } from 'fs/promises';
|
||||
import path, { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { promisify } from 'util';
|
||||
import { exec } from 'child_process';
|
||||
import { readdirSync } from 'fs';
|
||||
import { ROOT } from './env.js';
|
||||
const execute = promisify(exec);
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -12,76 +13,110 @@ const __dirname = dirname(__filename);
|
||||
// Adds all existing paths at runtime
|
||||
const PERMANENT_PATHS_IN_ROOT = readdirSync('..');
|
||||
|
||||
/**
|
||||
* Alter the `.vscode/settings.json` file properties
|
||||
* @param {object} obj Object Literal to set/overwrite properties
|
||||
*/
|
||||
export async function setVSCSettings(obj) {
|
||||
const settings = (
|
||||
await import('../../.vscode/settings.json', {
|
||||
assert: { type: 'json' }
|
||||
})
|
||||
).default;
|
||||
const pathToSettings = join(ROOT, '.vscode', 'settings.json');
|
||||
const settings = await getVSCSettings();
|
||||
const updated = {
|
||||
...settings,
|
||||
...obj
|
||||
};
|
||||
const pathToSettings = join(__dirname, '../..', '.vscode', 'settings.json');
|
||||
await writeFile(pathToSettings, JSON.stringify(updated, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `.vscode/settings.json` file properties
|
||||
* @returns The contents of the `.vscode/settings.json` file
|
||||
*/
|
||||
export async function getVSCSettings() {
|
||||
const pathToSettings = join(ROOT, '.vscode', 'settings.json');
|
||||
return JSON.parse(await readFile(pathToSettings, 'utf8'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle `[file]: true` to `.vscode/settings.json` file
|
||||
* @param {string} file Filename of file to hide in VSCode settings
|
||||
*/
|
||||
export async function hideFile(file) {
|
||||
// Get `files.exclude`
|
||||
const filesExclude = (
|
||||
await import('../../.vscode/settings.json', { assert: { type: 'json' } })
|
||||
).default['files.exclude'];
|
||||
const filesExclude = (await getVSCSettings())['files.exclude'];
|
||||
filesExclude[file] = true;
|
||||
await setVSCSettings({ 'files.exclude': filesExclude });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle `[file]: false` to `.vscode/settings.json` file
|
||||
* @param {string} file Filename of file to show in VSCode settings
|
||||
*/
|
||||
export async function showFile(file) {
|
||||
// Get `files.exclude`
|
||||
const filesExclude = (
|
||||
await import('../../.vscode/settings.json', { assert: { type: 'json' } })
|
||||
).default['files.exclude'];
|
||||
const filesExclude = (await getVSCSettings())['files.exclude'];
|
||||
filesExclude[file] = false;
|
||||
await setVSCSettings({ 'files.exclude': filesExclude });
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide all files in the `files.exclude` property of the `.vscode/settings.json` file
|
||||
*/
|
||||
export async function hideAll() {
|
||||
const filesExclude = (
|
||||
await import('../../.vscode/settings.json', { assert: { type: 'json' } })
|
||||
).default['files.exclude'];
|
||||
const filesExclude = (await getVSCSettings())['files.exclude'];
|
||||
for (const file of Object.keys(filesExclude)) {
|
||||
filesExclude[file] = true;
|
||||
}
|
||||
await setVSCSettings({ 'files.exclude': filesExclude });
|
||||
}
|
||||
|
||||
/**
|
||||
* Show all files in the `files.exclude` property of the `.vscode/settings.json` file
|
||||
*/
|
||||
export async function showAll() {
|
||||
const filesExclude = (await getVSCSettings())['files.exclude'];
|
||||
for (const file of Object.keys(filesExclude)) {
|
||||
filesExclude[file] = false;
|
||||
}
|
||||
await setVSCSettings({ 'files.exclude': filesExclude });
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies all paths in the given `project.dashedName` directory to the root directory
|
||||
* @param {object} project Project to reset
|
||||
*/
|
||||
export async function dumpProjectDirectoryIntoRoot(project) {
|
||||
const pathToRoot = join(__dirname, '../..');
|
||||
await cp(join(pathToRoot, project.dashedName), pathToRoot, {
|
||||
await cp(join(ROOT, project.dashedName), ROOT, {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes non-boilerplate paths from the root, and copies them to the project directory
|
||||
* @param {object} projectToCopyTo Project to copy to
|
||||
*/
|
||||
export async function cleanWorkingDirectory(projectToCopyTo) {
|
||||
if (projectToCopyTo) {
|
||||
await copyNonWDirToProject(projectToCopyTo);
|
||||
}
|
||||
const pathToRoot = join(__dirname, '../..');
|
||||
const allOtherPaths = (await readdir(pathToRoot)).filter(
|
||||
const allOtherPaths = (await readdir(ROOT)).filter(
|
||||
p => !PERMANENT_PATHS_IN_ROOT.includes(p)
|
||||
);
|
||||
allOtherPaths.forEach(async p => {
|
||||
await rm(join(pathToRoot, p), { recursive: true });
|
||||
await rm(join(ROOT, p), { recursive: true });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies all non-boilerplate paths from the root to the project directory
|
||||
* @param {object} project Project to copy to
|
||||
*/
|
||||
async function copyNonWDirToProject(project) {
|
||||
const pathToRoot = join(__dirname, '../..');
|
||||
const allOtherPaths = (await readdir(pathToRoot)).filter(
|
||||
const allOtherPaths = (await readdir(ROOT)).filter(
|
||||
p => !PERMANENT_PATHS_IN_ROOT.includes(p)
|
||||
);
|
||||
allOtherPaths.forEach(async p => {
|
||||
const relativePath = join(pathToRoot, p);
|
||||
await cp(relativePath, join(pathToRoot, project, p), {
|
||||
const relativePath = join(ROOT, p);
|
||||
await cp(relativePath, join(ROOT, project, p), {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
const path = require('path');
|
||||
const { DefinePlugin } = require('webpack');
|
||||
const Dotenv = require('dotenv-webpack');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: './client/index.tsx',
|
||||
entry: path.join(__dirname, 'client/index.tsx'),
|
||||
devtool: 'inline-source-map',
|
||||
mode: process.env.NODE_ENV || 'development',
|
||||
devServer: {
|
||||
compress: true,
|
||||
port: 9000
|
||||
},
|
||||
watch: process.env.NODE_ENV === 'development',
|
||||
watchOptions: {
|
||||
ignored: ['**/node_modules', 'config']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx|tsx|ts)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
@@ -46,7 +48,6 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
test: /\.(ts|tsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: ['ts-loader']
|
||||
},
|
||||
{
|
||||
@@ -81,7 +82,6 @@ module.exports = {
|
||||
'process.env.GITPOD_WORKSPACE_URL': JSON.stringify(
|
||||
process.env.GITPOD_WORKSPACE_URL
|
||||
)
|
||||
}),
|
||||
new Dotenv()
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ image:
|
||||
|
||||
# Commands to start on workspace startup
|
||||
tasks:
|
||||
- init: cd .freeCodeCamp && cp sample.env .env && npm ci
|
||||
- init: npm ci
|
||||
|
||||
ports:
|
||||
- port: 8080
|
||||
@@ -12,5 +12,5 @@ ports:
|
||||
# TODO: See about publishing to Open VSX for smoother process
|
||||
vscode:
|
||||
extensions:
|
||||
- https://github.com/freeCodeCamp/courses-vscode-extension/releases/download/v1.4.3/freecodecamp-courses-1.4.3.vsix
|
||||
- https://github.com/freeCodeCamp/courses-vscode-extension/releases/download/v1.5.0/freecodecamp-courses-1.5.0.vsix
|
||||
- https://github.com/freeCodeCamp/freecodecamp-dark-vscode-theme/releases/download/v1.0.0/freecodecamp-dark-vscode-theme-1.0.0.vsix
|
||||
|
||||
22
.vscode/settings.json
vendored
22
.vscode/settings.json
vendored
@@ -8,6 +8,7 @@
|
||||
".devcontainer": true,
|
||||
".editorconfig": true,
|
||||
".freeCodeCamp": true,
|
||||
".git": true,
|
||||
".gitignore": true,
|
||||
".gitpod.Dockerfile": true,
|
||||
".gitpod.yml": true,
|
||||
@@ -15,6 +16,12 @@
|
||||
".prettierignore": true,
|
||||
".prettierrc": true,
|
||||
".vscode": true,
|
||||
"bash": true,
|
||||
"config": true,
|
||||
"curriculum": true,
|
||||
"tooling": true,
|
||||
"Dockerfile": true,
|
||||
"freecodecamp.conf.json": true,
|
||||
"node_modules": true,
|
||||
"package.json": true,
|
||||
"package-lock.json": true,
|
||||
@@ -30,5 +37,16 @@
|
||||
"learn-proof-of-stake-consensus-by-completing-a-web3-game": true,
|
||||
"learn-smart-contracts-by-building-a-dumb-contract": true,
|
||||
"learn-websockets-by-building-a-blockchain-explorer": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"bash": {
|
||||
"path": "bash",
|
||||
"icon": "terminal-bash",
|
||||
"args": [
|
||||
"--init-file",
|
||||
"./bash/sourcerer.sh"
|
||||
]
|
||||
}
|
||||
},
|
||||
"workbench.colorTheme": "freeCodeCamp Dark Theme"
|
||||
}
|
||||
@@ -47,3 +47,5 @@ RUN npm config set prefix '~/.npm-global'
|
||||
# Configure course-specific environment
|
||||
COPY . .
|
||||
WORKDIR ${HOMEDIR}
|
||||
|
||||
RUN cd ${HOMEDIR} && npm install
|
||||
@@ -124,5 +124,7 @@ for i in $(ls -A $HOME/.bashrc.d/); do source $HOME/.bashrc.d/$i; done
|
||||
|
||||
# freeCodeCamp - Needed for most tests to work
|
||||
|
||||
PROMPT_COMMAND='>| /workspace/web3-curriculum/.logs/.terminal-out.log && cat /workspace/web3-curriculum/.logs/.temp.log >| /workspace/web3-curriculum/.logs/.terminal-out.log && truncate -s 0 /workspace/web3-curriculum/.logs/.temp.log; echo $PWD >> /workspace/web3-curriculum/.logs/.cwd; history -a /workspace/web3-curriculum/.logs/.bash_history'
|
||||
exec > >(tee -ia /workspace/web3-curriculum/.logs/.temp.log) 2>&1
|
||||
WD=/workspace/web3-curriculum
|
||||
|
||||
PROMPT_COMMAND='>| $WD/.logs/.terminal-out.log && cat $WD/.logs/.temp.log >| $WD/.logs/.terminal-out.log && truncate -s 0 $WD/.logs/.temp.log; echo $PWD >> $WD/.logs/.cwd.log; history -a $WD/.logs/.bash_history.log'
|
||||
exec > >(tee -ia $WD/.logs/.temp.log) 2>&1
|
||||
3
bash/sourcerer.sh
Normal file
3
bash/sourcerer.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
source ./bash/.bashrc
|
||||
echo "BashRC Sourced"
|
||||
4
config/state.json
Normal file
4
config/state.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"currentProject": null,
|
||||
"locale": "english"
|
||||
}
|
||||
@@ -599,7 +599,7 @@ try {
|
||||
// Stop and start node in background
|
||||
const { spawn } = await import('child_process');
|
||||
const _node = spawn('node', ['node/provider.js'], {
|
||||
cwd: '../build-a-web3-client-side-package-for-your-dapp'
|
||||
cwd: './build-a-web3-client-side-package-for-your-dapp'
|
||||
});
|
||||
|
||||
await new Promise(resolve => {
|
||||
53
freecodecamp.conf.json
Normal file
53
freecodecamp.conf.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"path": ".",
|
||||
"scripts": {
|
||||
"develop-course": "NODE_ENV=development node ./.freeCodeCamp/tooling/server.js",
|
||||
"run-course": "NODE_ENV=production node ./.freeCodeCamp/tooling/server.js",
|
||||
"test": {
|
||||
"functionName": "handleMessage",
|
||||
"arguments": [
|
||||
{
|
||||
"message": "Hello World!",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"previews": [
|
||||
{
|
||||
"open": true,
|
||||
"url": "http://localhost:8080",
|
||||
"showLoader": true,
|
||||
"timeout": 10000
|
||||
}
|
||||
]
|
||||
},
|
||||
"bash": {
|
||||
".bashrc": "./bash/.bashrc",
|
||||
"sourcerer.sh": "./bash/sourcerer.sh"
|
||||
},
|
||||
"client": {
|
||||
"assets": {
|
||||
"header": "./client/assets/fcc_primary_large.svg",
|
||||
"favicon": "./client/assets/fcc_primary_small.svg"
|
||||
},
|
||||
"landing": {
|
||||
"description": "For these courses, you will use your local development environment to complete interactive tutorials and build projects.\n\nThese courses start off with basic cryptographic concepts. Using Nodejs, you will learn everything from cryptographic hash functions to building your own blockchain.\n\nNext, you will learn about different consensus mechanisms.\n\nFinally, you will learn Rust, and WASM in the context of a blockchain.",
|
||||
"faq-link": "#",
|
||||
"faq-text": "Link to FAQ related to course"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"projects.json": "./config/projects.json",
|
||||
"state.json": "./config/state.json"
|
||||
},
|
||||
"curriculum": {
|
||||
"locales": {
|
||||
"english": "./curriculum/locales/english"
|
||||
}
|
||||
},
|
||||
"tooling": {
|
||||
"helpers": "./tooling/helpers.js"
|
||||
}
|
||||
}
|
||||
9948
package-lock.json
generated
9948
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -12,12 +12,12 @@
|
||||
"author": "Shaun Hamilton",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"crypto-js": "^4.1.1",
|
||||
"dotenv": "^16.0.1",
|
||||
"elliptic": "^6.5.4",
|
||||
"express": "^4.18.1",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"@freecodecamp/freecodecamp-os": "^1.5.3",
|
||||
"crypto-js": "4.1.1",
|
||||
"dotenv": "16.0.2",
|
||||
"elliptic": "6.5.4",
|
||||
"express": "4.18.1",
|
||||
"logover": "^1.3.5",
|
||||
"ws": "^8.8.0"
|
||||
"ws": "8.8.1"
|
||||
}
|
||||
}
|
||||
|
||||
133
tooling/helpers.js
Normal file
133
tooling/helpers.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import __helpers from '../.freeCodeCamp/tooling/test-utils.js';
|
||||
import sha256 from 'crypto-js/sha256.js';
|
||||
import elliptic from 'elliptic';
|
||||
import WebSocket, { WebSocketServer } from 'ws';
|
||||
|
||||
const ec = new elliptic.ec('p192');
|
||||
|
||||
export function generateHash(content) {
|
||||
const hash = sha256(content).toString();
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function generateSignature(privateKey, content) {
|
||||
const keyPair = ec.keyFromPrivate(privateKey, 'hex');
|
||||
const signature = keyPair.sign(content).toDER('hex');
|
||||
return signature;
|
||||
}
|
||||
|
||||
export function validateSignature(publicKey, content, signature) {
|
||||
const keyPair = ec.keyFromPublic(publicKey, 'hex');
|
||||
const verifiedSignature = keyPair.verify(content, signature);
|
||||
return verifiedSignature;
|
||||
}
|
||||
|
||||
export function getPublicKeyFromPrivate(privateKey) {
|
||||
const keyPair = ec.keyFromPrivate(privateKey, 'hex');
|
||||
const publicKey = keyPair.getPublic('hex');
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
// used in fundraising contract project
|
||||
export async function getContract(contractAddress, cwd, includePool = true) {
|
||||
// get the latest contract state from the blockchain
|
||||
const blockchain = await __helpers.getJsonFile(`${cwd}/blockchain.json`);
|
||||
const latestContract = blockchain.reduce((currentContract, nextBlock) => {
|
||||
if (nextBlock.smartContracts) {
|
||||
nextBlock.smartContracts.forEach(contract => {
|
||||
if (contract.address === contractAddress) {
|
||||
// first occurrence of contract
|
||||
if (!currentContract.hasOwnProperty('address')) {
|
||||
Object.keys(contract).forEach(
|
||||
key => (currentContract[key] = contract[key])
|
||||
);
|
||||
|
||||
// contract found and added, only update state after that
|
||||
} else if (contract.hasOwnProperty('state')) {
|
||||
currentContract.state = contract.state;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return currentContract;
|
||||
}, {});
|
||||
|
||||
if (includePool) {
|
||||
// add contract pool to latest contract state
|
||||
const smartContracts = await __helpers.getJsonFile(
|
||||
`${cwd}/smart-contracts.json`
|
||||
);
|
||||
smartContracts.forEach(contract => {
|
||||
if (contract.address === contractAddress) {
|
||||
if (!latestContract.hasOwnProperty('address')) {
|
||||
Object.keys(contract).forEach(
|
||||
key => (latestContract[key] = contract[key])
|
||||
);
|
||||
} else if (latestContract.hasOwnProperty('state')) {
|
||||
latestContract.state = contract.state;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return latestContract.hasOwnProperty('address') ? latestContract : null;
|
||||
}
|
||||
|
||||
// for p2p network project
|
||||
export async function canConnectToSocket(address) {
|
||||
return await new Promise(resolve => {
|
||||
const socket = new WebSocket(address, { shouldKeepAlive: false });
|
||||
socket.on('open', () => {
|
||||
socket.close();
|
||||
resolve(true);
|
||||
});
|
||||
socket.on('error', () => resolve(false));
|
||||
});
|
||||
}
|
||||
|
||||
export async function startSocketServerAndHandshake({
|
||||
myPort: port,
|
||||
theirAddress = 'ws://localhost:4001',
|
||||
connectOnly = false
|
||||
}) {
|
||||
return await new Promise(resolve => {
|
||||
const address = `ws://localhost:${port}`;
|
||||
|
||||
const server = new WebSocketServer({ port });
|
||||
server.on('connection', externalSocket => {
|
||||
if (connectOnly) {
|
||||
externalSocket.close();
|
||||
server.close();
|
||||
resolve(true);
|
||||
}
|
||||
|
||||
externalSocket.on('message', messageString => {
|
||||
const message = JSON.parse(messageString);
|
||||
|
||||
if (message.hasOwnProperty('type') && message.type === 'HANDSHAKE') {
|
||||
externalSocket.close();
|
||||
server.close();
|
||||
resolve(message);
|
||||
} else {
|
||||
externalSocket.close();
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
server.close();
|
||||
resolve();
|
||||
}, 5000);
|
||||
|
||||
server.on('error', () => server.close());
|
||||
|
||||
const socket = new WebSocket(theirAddress, { shouldKeepAlive: false });
|
||||
socket.on('open', () => {
|
||||
socket.send(JSON.stringify({ type: 'HANDSHAKE', data: [address] }));
|
||||
socket.close();
|
||||
});
|
||||
|
||||
socket.on('error', () => resolve());
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user