mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
feat: python in the browser (#50913)
Co-authored-by: Beau Carnes <1513130+beaucarnes@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
0caaf7ec66
commit
69d6ee32bf
@@ -70,6 +70,7 @@
|
|||||||
"./api/tsconfig.json",
|
"./api/tsconfig.json",
|
||||||
"./config/tsconfig.json",
|
"./config/tsconfig.json",
|
||||||
"./tools/ui-components/tsconfig.json",
|
"./tools/ui-components/tsconfig.json",
|
||||||
|
"./tools/client-plugins/browser-scripts/tsconfig.json",
|
||||||
"./utils/tsconfig.json",
|
"./utils/tsconfig.json",
|
||||||
"./web/tsconfig.json",
|
"./web/tsconfig.json",
|
||||||
"./curriculum-server/tsconfig.json",
|
"./curriculum-server/tsconfig.json",
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -161,6 +161,7 @@ config/env.json
|
|||||||
config/client/sass-compile.json
|
config/client/sass-compile.json
|
||||||
config/client/frame-runner.json
|
config/client/frame-runner.json
|
||||||
config/client/test-evaluator.json
|
config/client/test-evaluator.json
|
||||||
|
config/client/python-runner.json
|
||||||
config/curriculum.json
|
config/curriculum.json
|
||||||
config/i18n.js
|
config/i18n.js
|
||||||
config/misc.js
|
config/misc.js
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resolve: 'fcc-source-challenges',
|
resolve: require.resolve(
|
||||||
|
'../tools/client-plugins/gatsby-source-challenges'
|
||||||
|
),
|
||||||
options: {
|
options: {
|
||||||
name: 'challenges',
|
name: 'challenges',
|
||||||
source: buildChallenges,
|
source: buildChallenges,
|
||||||
@@ -70,7 +72,9 @@ module.exports = {
|
|||||||
resolve: 'gatsby-transformer-remark'
|
resolve: 'gatsby-transformer-remark'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resolve: 'gatsby-remark-node-identity',
|
resolve: require.resolve(
|
||||||
|
'../tools/client-plugins/gatsby-remark-node-identity'
|
||||||
|
),
|
||||||
options: {
|
options: {
|
||||||
identity: 'blockIntroMarkdown',
|
identity: 'blockIntroMarkdown',
|
||||||
predicate: ({ frontmatter }) => {
|
predicate: ({ frontmatter }) => {
|
||||||
@@ -83,7 +87,9 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resolve: 'gatsby-remark-node-identity',
|
resolve: require.resolve(
|
||||||
|
'../tools/client-plugins/gatsby-remark-node-identity'
|
||||||
|
),
|
||||||
options: {
|
options: {
|
||||||
identity: 'superBlockIntroMarkdown',
|
identity: 'superBlockIntroMarkdown',
|
||||||
predicate: ({ frontmatter }) => {
|
predicate: ({ frontmatter }) => {
|
||||||
|
|||||||
@@ -1057,6 +1057,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"upcoming-python": {
|
||||||
|
"title": "Upcoming Python",
|
||||||
|
"intro": ["placeholder"],
|
||||||
|
"blocks": {
|
||||||
|
"learn-python-by-building-a-blackjack-game": {
|
||||||
|
"title": "Learn Python by Building a Blackjack Game",
|
||||||
|
"intro": ["Learn Python.", ""]
|
||||||
|
},
|
||||||
|
"upcoming-python-project": {
|
||||||
|
"title": "Upcoming Python Project",
|
||||||
|
"intro": ["placeholder"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"example-certification": {
|
"example-certification": {
|
||||||
"title": "Example Certification",
|
"title": "Example Certification",
|
||||||
"intro": ["placeholder"],
|
"intro": ["placeholder"],
|
||||||
|
|||||||
@@ -19,11 +19,11 @@
|
|||||||
"author": "freeCodeCamp <team@freecodecamp.org>",
|
"author": "freeCodeCamp <team@freecodecamp.org>",
|
||||||
"main": "none",
|
"main": "none",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prebuild": "pnpm -w run create:config && pnpm run build:workers --env production && pnpm run build:components-library",
|
"prebuild": "pnpm -w run create:config && pnpm run build:scripts --env production && pnpm run build:components-library",
|
||||||
"build": "cross-env NODE_OPTIONS=\"--max-old-space-size=7168\" gatsby build --prefix-paths",
|
"build": "cross-env NODE_OPTIONS=\"--max-old-space-size=7168\" gatsby build --prefix-paths",
|
||||||
"build:workers": "cross-env NODE_OPTIONS=\"--max-old-space-size=7168\" webpack --config ./webpack-workers.js",
|
"build:scripts": "pnpm run -F=browser-scripts build",
|
||||||
"clean": "gatsby clean",
|
"clean": "gatsby clean",
|
||||||
"predevelop": "pnpm run build:workers --env development && pnpm run build:components-library",
|
"predevelop": "pnpm run build:scripts --env development && pnpm run build:components-library",
|
||||||
"build:components-library": "pnpm run -F=@freecodecamp/ui build",
|
"build:components-library": "pnpm run -F=@freecodecamp/ui build",
|
||||||
"develop": "cross-env NODE_OPTIONS=\"--max-old-space-size=5000\" gatsby develop --inspect=9230",
|
"develop": "cross-env NODE_OPTIONS=\"--max-old-space-size=5000\" gatsby develop --inspect=9230",
|
||||||
"lint": "ts-node ./i18n/schema-validation.ts",
|
"lint": "ts-node ./i18n/schema-validation.ts",
|
||||||
@@ -71,7 +71,6 @@
|
|||||||
"crypto-browserify": "3.12.0",
|
"crypto-browserify": "3.12.0",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"enzyme": "3.11.0",
|
"enzyme": "3.11.0",
|
||||||
"enzyme-adapter-react-16": "1.15.7",
|
|
||||||
"final-form": "4.20.9",
|
"final-form": "4.20.9",
|
||||||
"gatsby": "3.15.0",
|
"gatsby": "3.15.0",
|
||||||
"gatsby-cli": "3.15.0",
|
"gatsby-cli": "3.15.0",
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ const iconMap = {
|
|||||||
[SuperBlocks.ProjectEuler]: Graduation,
|
[SuperBlocks.ProjectEuler]: Graduation,
|
||||||
[SuperBlocks.CollegeAlgebraPy]: CollegeAlgebra,
|
[SuperBlocks.CollegeAlgebraPy]: CollegeAlgebra,
|
||||||
[SuperBlocks.FoundationalCSharp]: CSharpLogo,
|
[SuperBlocks.FoundationalCSharp]: CSharpLogo,
|
||||||
[SuperBlocks.ExampleCertification]: ResponsiveDesign
|
[SuperBlocks.ExampleCertification]: ResponsiveDesign,
|
||||||
|
[SuperBlocks.UpcomingPython]: PythonIcon
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateIconComponent = (
|
const generateIconComponent = (
|
||||||
|
|||||||
9
client/src/pages/learn/upcoming-python/index.md
Normal file
9
client/src/pages/learn/upcoming-python/index.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: Upcoming Python Certification
|
||||||
|
superBlock: upcoming-python
|
||||||
|
certification: upcoming-python
|
||||||
|
---
|
||||||
|
|
||||||
|
## Upcoming Python Certification
|
||||||
|
|
||||||
|
Learn the basics of Python.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: Introduction to Learn Python by Building a Blackjack Game
|
||||||
|
block: learn-python-by-building-a-blackjack-game
|
||||||
|
superBlock: scientific-computing-with-python
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction to Learn Python by Building a Blackjack Game
|
||||||
|
|
||||||
|
Learn Python!
|
||||||
@@ -28,6 +28,7 @@ const machineLearningPyBase =
|
|||||||
const collegeAlgebraPyBase = '/learn/college-algebra-with-python';
|
const collegeAlgebraPyBase = '/learn/college-algebra-with-python';
|
||||||
const takeHomeBase = '/learn/coding-interview-prep/take-home-projects';
|
const takeHomeBase = '/learn/coding-interview-prep/take-home-projects';
|
||||||
const foundationalCSharpBase = '/learn/foundational-c-sharp-with-microsoft';
|
const foundationalCSharpBase = '/learn/foundational-c-sharp-with-microsoft';
|
||||||
|
const upcomingPythonBase = '/learn/upcoming-python';
|
||||||
const exampleCertBase = '/learn/example-certification';
|
const exampleCertBase = '/learn/example-certification';
|
||||||
const legacyFrontEndBase = feLibsBase;
|
const legacyFrontEndBase = feLibsBase;
|
||||||
const legacyFrontEndResponsiveBase = responsiveWebBase;
|
const legacyFrontEndResponsiveBase = responsiveWebBase;
|
||||||
@@ -769,6 +770,19 @@ const upcomingCertMap = [
|
|||||||
certSlug: 'example-certification-v8'
|
certSlug: 'example-certification-v8'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '64afc4e8f3b37856e035b85f',
|
||||||
|
title: 'Upcoming Python Certification',
|
||||||
|
certSlug: 'upcoming-python-v8',
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
id: '64afc37bf3b37856e035b85e',
|
||||||
|
title: 'Upcoming Python Project',
|
||||||
|
link: `${upcomingPythonBase}/upcoming-python-project`,
|
||||||
|
certSlug: 'upcoming-python-v8'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ import {
|
|||||||
challengeFilesSelector
|
challengeFilesSelector
|
||||||
} from '../redux/selectors';
|
} from '../redux/selectors';
|
||||||
|
|
||||||
type VisibleEditors = {
|
import type { VisibleEditors } from './multifile-editor';
|
||||||
[key: string]: boolean;
|
|
||||||
};
|
|
||||||
interface EditorTabsProps {
|
interface EditorTabsProps {
|
||||||
challengeFiles: ChallengeFiles;
|
challengeFiles: ChallengeFiles;
|
||||||
toggleVisibleEditor: typeof toggleVisibleEditor;
|
toggleVisibleEditor: typeof toggleVisibleEditor;
|
||||||
@@ -43,7 +42,13 @@ class EditorTabs extends Component<EditorTabsProps> {
|
|||||||
{sortChallengeFiles(challengeFiles).map(
|
{sortChallengeFiles(challengeFiles).map(
|
||||||
(challengeFile: ChallengeFile) => (
|
(challengeFile: ChallengeFile) => (
|
||||||
<button
|
<button
|
||||||
aria-expanded={visibleEditors[challengeFile.fileKey] ?? 'false'}
|
aria-expanded={
|
||||||
|
// @ts-expect-error TODO: validate challengeFile on io-boundary,
|
||||||
|
// then we won't need to ignore this error and we can drop the
|
||||||
|
// nullish handling.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
visibleEditors[challengeFile.fileKey] ?? 'false'
|
||||||
|
}
|
||||||
key={challengeFile.fileKey}
|
key={challengeFile.fileKey}
|
||||||
data-cy={`editor-tab-${challengeFile.fileKey}`}
|
data-cy={`editor-tab-${challengeFile.fileKey}`}
|
||||||
onClick={() => toggleVisibleEditor(challengeFile.fileKey)}
|
onClick={() => toggleVisibleEditor(challengeFile.fileKey)}
|
||||||
|
|||||||
@@ -187,7 +187,9 @@ const modeMap = {
|
|||||||
css: 'css',
|
css: 'css',
|
||||||
html: 'html',
|
html: 'html',
|
||||||
js: 'javascript',
|
js: 'javascript',
|
||||||
jsx: 'javascript'
|
jsx: 'javascript',
|
||||||
|
py: 'python',
|
||||||
|
python: 'python'
|
||||||
};
|
};
|
||||||
|
|
||||||
let monacoThemesDefined = false;
|
let monacoThemesDefined = false;
|
||||||
|
|||||||
@@ -17,8 +17,12 @@ import { FileKey } from '../../../redux/prop-types';
|
|||||||
import { Themes } from '../../../components/settings/theme';
|
import { Themes } from '../../../components/settings/theme';
|
||||||
import Editor, { type EditorProps } from './editor';
|
import Editor, { type EditorProps } from './editor';
|
||||||
|
|
||||||
type VisibleEditors = {
|
export type VisibleEditors = {
|
||||||
[key: string]: boolean;
|
indexhtml?: boolean;
|
||||||
|
indexjsx?: boolean;
|
||||||
|
stylescss?: boolean;
|
||||||
|
scriptjs?: boolean;
|
||||||
|
mainpy?: boolean;
|
||||||
};
|
};
|
||||||
type MultifileEditorProps = Pick<
|
type MultifileEditorProps = Pick<
|
||||||
EditorProps,
|
EditorProps,
|
||||||
@@ -36,12 +40,7 @@ type MultifileEditorProps = Pick<
|
|||||||
// We use dimensions to trigger a re-render of the editor
|
// We use dimensions to trigger a re-render of the editor
|
||||||
| 'dimensions'
|
| 'dimensions'
|
||||||
> & {
|
> & {
|
||||||
visibleEditors: {
|
visibleEditors: VisibleEditors;
|
||||||
indexhtml?: boolean;
|
|
||||||
indexjsx?: boolean;
|
|
||||||
stylescss?: boolean;
|
|
||||||
scriptjs?: boolean;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
visibleEditorsSelector,
|
visibleEditorsSelector,
|
||||||
@@ -74,7 +73,7 @@ const MultifileEditor = (props: MultifileEditorProps) => {
|
|||||||
isUsingKeyboardInTablist,
|
isUsingKeyboardInTablist,
|
||||||
resizeProps,
|
resizeProps,
|
||||||
title,
|
title,
|
||||||
visibleEditors: { stylescss, indexhtml, scriptjs, indexjsx },
|
visibleEditors: { stylescss, indexhtml, scriptjs, indexjsx, mainpy },
|
||||||
usesMultifileEditor,
|
usesMultifileEditor,
|
||||||
showProjectPreview
|
showProjectPreview
|
||||||
} = props;
|
} = props;
|
||||||
@@ -98,6 +97,7 @@ const MultifileEditor = (props: MultifileEditorProps) => {
|
|||||||
if (indexhtml) editorKeys.push('indexhtml');
|
if (indexhtml) editorKeys.push('indexhtml');
|
||||||
if (stylescss) editorKeys.push('stylescss');
|
if (stylescss) editorKeys.push('stylescss');
|
||||||
if (scriptjs) editorKeys.push('scriptjs');
|
if (scriptjs) editorKeys.push('scriptjs');
|
||||||
|
if (mainpy) editorKeys.push('mainpy');
|
||||||
|
|
||||||
const editorAndSplitterKeys = editorKeys.reduce((acc: string[] | [], key) => {
|
const editorAndSplitterKeys = editorKeys.reduce((acc: string[] | [], key) => {
|
||||||
if (acc.length === 0) {
|
if (acc.length === 0) {
|
||||||
|
|||||||
@@ -222,10 +222,14 @@ function ShowClassic({
|
|||||||
`intro:${superBlock}.blocks.${block}.title`
|
`intro:${superBlock}.blocks.${block}.title`
|
||||||
)}: ${title}`;
|
)}: ${title}`;
|
||||||
const windowTitle = `${blockNameTitle} | freeCodeCamp.org`;
|
const windowTitle = `${blockNameTitle} | freeCodeCamp.org`;
|
||||||
|
// TODO: show preview should NOT be computed like this. That determination is
|
||||||
|
// made during the build (at least twice!). It should be either a prop or
|
||||||
|
// computed from challengeType
|
||||||
const showPreview =
|
const showPreview =
|
||||||
challengeType === challengeTypes.html ||
|
challengeType === challengeTypes.html ||
|
||||||
challengeType === challengeTypes.modern ||
|
challengeType === challengeTypes.modern ||
|
||||||
challengeType === challengeTypes.multifileCertProject;
|
challengeType === challengeTypes.multifileCertProject ||
|
||||||
|
challengeType === challengeTypes.python;
|
||||||
|
|
||||||
const getLayoutState = () => {
|
const getLayoutState = () => {
|
||||||
const reflexLayout = store.get(REFLEX_LAYOUT) as ReflexLayout;
|
const reflexLayout = store.get(REFLEX_LAYOUT) as ReflexLayout;
|
||||||
|
|||||||
@@ -42,3 +42,11 @@ A required file can not have both a src and a link: src = ${src}, link = ${link}
|
|||||||
embedSource({ source: contents }) || ''
|
embedSource({ source: contents }) || ''
|
||||||
}${testRunnerScript}`;
|
}${testRunnerScript}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createPythonTerminal(pythonRunnerSrc: string): string {
|
||||||
|
const head =
|
||||||
|
'<head><style>#terminal { margin-top: 10px; width: 100%; height: 350px; background-color: #000; color: #00ff00; padding: 5px; overflow: auto; border: 1px solid #ccc; border-radius: 3px; }</style></head>';
|
||||||
|
|
||||||
|
const body = `<body><div id='terminal'></div><script src='${pythonRunnerSrc}' type='text/javascript'></script></body>`;
|
||||||
|
return `<html>${head}${body}</html>`;
|
||||||
|
}
|
||||||
|
|||||||
120
client/src/templates/Challenges/rechallenge/transform-python.js
Normal file
120
client/src/templates/Challenges/rechallenge/transform-python.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
export const indent = (code, spaces) => {
|
||||||
|
const lines = code.split('\n');
|
||||||
|
return lines.map(line => `${' '.repeat(spaces)}${line}`).join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Requirements:
|
||||||
|
// - run in a single instance of pyodide (because loadPyodide is slow)
|
||||||
|
// - be able to stop execution of learner code
|
||||||
|
//
|
||||||
|
// This wrapper lets us meet the second requirement, since tasks are
|
||||||
|
// cancellable. This creates a second issue: the learner code no longer modifies
|
||||||
|
// the global scope, so we need to copy the locals to globals.
|
||||||
|
//
|
||||||
|
// Finally, we have to await the task, or there's no way for the JavaScript
|
||||||
|
// context to know when the task is complete.
|
||||||
|
export const makeCancellable = code => `import asyncio
|
||||||
|
async def cancellable_coroutine():
|
||||||
|
try:
|
||||||
|
${indent(code, 8)}
|
||||||
|
globals()['__locals'] = locals()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
__task = asyncio.create_task(cancellable_coroutine())
|
||||||
|
|
||||||
|
def __cancel():
|
||||||
|
__task.cancel()
|
||||||
|
await __task`;
|
||||||
|
|
||||||
|
export function modifyInputStatements(line) {
|
||||||
|
// Use a regular expression to match input statements with chained methods
|
||||||
|
const inputRegex = /(.*=\s*)input\((["'].*?["']\))(\.\w+\([^)]*\))*/;
|
||||||
|
const match = line.match(inputRegex);
|
||||||
|
if (match) {
|
||||||
|
const inputStatement = match[0];
|
||||||
|
const varAssignment = match[1];
|
||||||
|
const inputCall =
|
||||||
|
'input' +
|
||||||
|
inputStatement
|
||||||
|
.slice(varAssignment.length)
|
||||||
|
.split('input')[1]
|
||||||
|
.split('.')[0];
|
||||||
|
const methods = inputStatement
|
||||||
|
.slice(varAssignment.length + inputCall.length)
|
||||||
|
.split('.')
|
||||||
|
.slice(1);
|
||||||
|
const tempVar = '_temp_input_var';
|
||||||
|
const newStatements = [
|
||||||
|
`${tempVar} = ${inputCall}`,
|
||||||
|
...methods.map(method => `${tempVar} = ${tempVar}.${method}`),
|
||||||
|
`${varAssignment.trim()} ${tempVar}`
|
||||||
|
];
|
||||||
|
// Get the indentation of the original line
|
||||||
|
const indentation = line.match(/^\s*/)[0];
|
||||||
|
// Apply the same indentation to each new statement
|
||||||
|
const indentedStatements = newStatements.map(stmt => indentation + stmt);
|
||||||
|
// Replace the original input statement in the line with the temporary variable
|
||||||
|
const updatedLine = line.replace(
|
||||||
|
inputStatement,
|
||||||
|
indentedStatements.join('\n')
|
||||||
|
);
|
||||||
|
return updatedLine.split('\n');
|
||||||
|
}
|
||||||
|
return [line];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeInputAwaitable(code) {
|
||||||
|
const lines = code.split('\n');
|
||||||
|
const asyncFunctions = new Set();
|
||||||
|
const modifiedLines = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
let line = lines[i];
|
||||||
|
|
||||||
|
// Modify input statements with chained methods
|
||||||
|
const updatedLines = modifyInputStatements(line);
|
||||||
|
|
||||||
|
// If the line contains an input statement, update it to use "await"
|
||||||
|
if (updatedLines.some(updatedLine => updatedLine.includes('input('))) {
|
||||||
|
updatedLines.forEach((updatedLine, index) => {
|
||||||
|
if (updatedLine.includes('input(')) {
|
||||||
|
updatedLines[index] = updatedLine.replace('input(', 'await input(');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the outer function definition and make it async
|
||||||
|
for (let j = i - 1; j >= 0; j--) {
|
||||||
|
if (lines[j].includes('def ')) {
|
||||||
|
if (!modifiedLines[j].includes('async def ')) {
|
||||||
|
const functionName = lines[j].match(
|
||||||
|
/def\s+([a-zA-Z_][a-zA-Z_0-9]*)/
|
||||||
|
)[1];
|
||||||
|
asyncFunctions.add(functionName);
|
||||||
|
modifiedLines[j] = modifiedLines[j].replace('def ', 'async def ');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update function calls to include 'await' for async functions
|
||||||
|
asyncFunctions.forEach(funcName => {
|
||||||
|
updatedLines.forEach((updatedLine, index) => {
|
||||||
|
if (
|
||||||
|
updatedLine.includes(` ${funcName}(`) &&
|
||||||
|
!updatedLine.includes(`await ${funcName}(`)
|
||||||
|
) {
|
||||||
|
updatedLines[index] = updatedLine.replace(
|
||||||
|
`${funcName}(`,
|
||||||
|
`await ${funcName}(`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modifiedLines.push(...updatedLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
return modifiedLines.join('\n');
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
import { indent, makeCancellable } from './transform-python';
|
||||||
|
|
||||||
|
describe('transform-python', () => {
|
||||||
|
describe('indent', () => {
|
||||||
|
it('should indent n spaces', () => {
|
||||||
|
const inputCode = `def foo():
|
||||||
|
print('bar')`;
|
||||||
|
const fourSpaces = ` def foo():
|
||||||
|
print('bar')`;
|
||||||
|
const eightSpaces = ` def foo():
|
||||||
|
print('bar')`;
|
||||||
|
|
||||||
|
expect(indent(inputCode, 4)).toEqual(fourSpaces);
|
||||||
|
expect(indent(inputCode, 8)).toEqual(eightSpaces);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('makeCancellable', () => {
|
||||||
|
it('should wrap a code string in a cancellable coroutine', () => {
|
||||||
|
const inputCode = `def foo():
|
||||||
|
print('bar')`;
|
||||||
|
const wrappedCode = `import asyncio
|
||||||
|
async def cancellable_coroutine():
|
||||||
|
try:
|
||||||
|
def foo():
|
||||||
|
print('bar')
|
||||||
|
globals()['__locals'] = locals()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
__task = asyncio.create_task(cancellable_coroutine())
|
||||||
|
|
||||||
|
def __cancel():
|
||||||
|
__task.cancel()
|
||||||
|
await __task`;
|
||||||
|
|
||||||
|
expect(makeCancellable(inputCode)).toEqual(wrappedCode);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
compileHeadTail
|
compileHeadTail
|
||||||
} from '../../../../../utils/polyvinyl';
|
} from '../../../../../utils/polyvinyl';
|
||||||
import createWorker from '../utils/worker-executor';
|
import createWorker from '../utils/worker-executor';
|
||||||
|
import { makeCancellable, makeInputAwaitable } from './transform-python';
|
||||||
|
|
||||||
const { filename: sassCompile } = sassData;
|
const { filename: sassCompile } = sassData;
|
||||||
|
|
||||||
@@ -98,6 +99,7 @@ const NBSPReg = new RegExp(String.fromCharCode(160), 'g');
|
|||||||
const testJS = matchesProperty('ext', 'js');
|
const testJS = matchesProperty('ext', 'js');
|
||||||
const testJSX = matchesProperty('ext', 'jsx');
|
const testJSX = matchesProperty('ext', 'jsx');
|
||||||
const testHTML = matchesProperty('ext', 'html');
|
const testHTML = matchesProperty('ext', 'html');
|
||||||
|
const testPython = matchesProperty('ext', 'py');
|
||||||
const testHTML$JS$JSX = overSome(testHTML, testJS, testJSX);
|
const testHTML$JS$JSX = overSome(testHTML, testJS, testJSX);
|
||||||
|
|
||||||
const replaceNBSP = cond([
|
const replaceNBSP = cond([
|
||||||
@@ -297,9 +299,26 @@ const htmlTransformer = cond([
|
|||||||
[stubTrue, identity]
|
[stubTrue, identity]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const transformPython = async function (file) {
|
||||||
|
const awaitableCode = makeInputAwaitable(file.contents);
|
||||||
|
const cancellableCode = makeCancellable(awaitableCode);
|
||||||
|
return transformContents(() => cancellableCode, file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pythonTransformer = cond([
|
||||||
|
[testPython, transformPython],
|
||||||
|
[stubTrue, identity]
|
||||||
|
]);
|
||||||
|
|
||||||
export const getTransformers = loopProtectOptions => [
|
export const getTransformers = loopProtectOptions => [
|
||||||
replaceNBSP,
|
replaceNBSP,
|
||||||
babelTransformer(loopProtectOptions),
|
babelTransformer(loopProtectOptions),
|
||||||
partial(compileHeadTail, ''),
|
partial(compileHeadTail, ''),
|
||||||
htmlTransformer
|
htmlTransformer
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const getPythonTransformers = () => [
|
||||||
|
replaceNBSP,
|
||||||
|
partial(compileHeadTail, ''),
|
||||||
|
pythonTransformer
|
||||||
|
];
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
updatePreview,
|
updatePreview,
|
||||||
updateProjectPreview
|
updateProjectPreview
|
||||||
} from '../utils/build';
|
} from '../utils/build';
|
||||||
|
import { runPythonInFrame, mainPreviewId } from '../utils/frame';
|
||||||
import { actionTypes } from './action-types';
|
import { actionTypes } from './action-types';
|
||||||
import {
|
import {
|
||||||
disableBuildOnError,
|
disableBuildOnError,
|
||||||
@@ -241,6 +242,15 @@ function* previewChallengeSaga({ flushLogs = true } = {}) {
|
|||||||
const finalDocument = portalDocument || document;
|
const finalDocument = portalDocument || document;
|
||||||
|
|
||||||
yield call(updatePreview, buildData, finalDocument, proxyLogger);
|
yield call(updatePreview, buildData, finalDocument, proxyLogger);
|
||||||
|
|
||||||
|
// Python challenges need to be created in two steps:
|
||||||
|
// 1) build the frame
|
||||||
|
// 2) evaluate the code in the frame. This is necessary to avoid
|
||||||
|
// recreating the frame (which is slow since loadPyodide takes a long
|
||||||
|
// time)on every change.
|
||||||
|
if (challengeData.challengeType === challengeTypes.python) {
|
||||||
|
yield updatePython(challengeData);
|
||||||
|
}
|
||||||
} else if (isJavaScriptChallenge(challengeData)) {
|
} else if (isJavaScriptChallenge(challengeData)) {
|
||||||
const runUserCode = getTestRunner(buildData, {
|
const runUserCode = getTestRunner(buildData, {
|
||||||
proxyLogger,
|
proxyLogger,
|
||||||
@@ -251,6 +261,7 @@ function* previewChallengeSaga({ flushLogs = true } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.log('previewChallengeSaga error', err);
|
||||||
if (err[0] === 'timeout') {
|
if (err[0] === 'timeout') {
|
||||||
// TODO: translate the error
|
// TODO: translate the error
|
||||||
// eslint-disable-next-line no-ex-assign
|
// eslint-disable-next-line no-ex-assign
|
||||||
@@ -261,6 +272,32 @@ function* previewChallengeSaga({ flushLogs = true } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function* updatePreviewSaga() {
|
||||||
|
const challengeData = yield select(challengeDataSelector);
|
||||||
|
if (challengeData.challengeType === challengeTypes.python) {
|
||||||
|
yield updatePython(challengeData);
|
||||||
|
} else {
|
||||||
|
// all other challenges have to recreate the preview
|
||||||
|
yield previewChallengeSaga();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function* updatePython(challengeData) {
|
||||||
|
const document = yield getContext('document');
|
||||||
|
// TODO: refactor the build pipeline so that we have discrete, composable
|
||||||
|
// functions to handle transforming code, embedding it and building the
|
||||||
|
// final html. Then we can just use the transformation function here.
|
||||||
|
const buildData = yield buildChallengeData(challengeData);
|
||||||
|
const code = buildData.transformedPython;
|
||||||
|
// TODO: proxy errors to the console
|
||||||
|
try {
|
||||||
|
yield call(runPythonInFrame, document, code, mainPreviewId);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error evaluating python code', code);
|
||||||
|
console.log('Message:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function* previewProjectSolutionSaga({ payload }) {
|
function* previewProjectSolutionSaga({ payload }) {
|
||||||
if (!payload) return;
|
if (!payload) return;
|
||||||
const { showProjectPreview, challengeData } = payload;
|
const { showProjectPreview, challengeData } = payload;
|
||||||
@@ -282,8 +319,9 @@ function* previewProjectSolutionSaga({ payload }) {
|
|||||||
export function createExecuteChallengeSaga(types) {
|
export function createExecuteChallengeSaga(types) {
|
||||||
return [
|
return [
|
||||||
takeLatest(types.executeChallenge, executeCancellableChallengeSaga),
|
takeLatest(types.executeChallenge, executeCancellableChallengeSaga),
|
||||||
|
takeLatest(types.updateFile, updatePreviewSaga),
|
||||||
takeLatest(
|
takeLatest(
|
||||||
[types.updateFile, types.challengeMounted, types.resetChallenge],
|
[types.challengeMounted, types.resetChallenge],
|
||||||
previewChallengeSaga
|
previewChallengeSaga
|
||||||
),
|
),
|
||||||
takeLatest(types.previewMounted, previewChallengeSaga, {
|
takeLatest(types.previewMounted, previewChallengeSaga, {
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ export const challengeDataSelector = state => {
|
|||||||
} else if (
|
} else if (
|
||||||
challengeType === challengeTypes.html ||
|
challengeType === challengeTypes.html ||
|
||||||
challengeType === challengeTypes.modern ||
|
challengeType === challengeTypes.modern ||
|
||||||
challengeType === challengeTypes.multifileCertProject
|
challengeType === challengeTypes.multifileCertProject ||
|
||||||
|
challengeType === challengeTypes.python
|
||||||
) {
|
) {
|
||||||
const { required = [], template = '' } = challengeMetaSelector(state);
|
const { required = [], template = '' } = challengeMetaSelector(state);
|
||||||
challengeData = {
|
challengeData = {
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
|
import { challengeTypes } from '../../../../../config/challenge-types';
|
||||||
import frameRunnerData from '../../../../../config/client/frame-runner.json';
|
import frameRunnerData from '../../../../../config/client/frame-runner.json';
|
||||||
import testEvaluatorData from '../../../../../config/client/test-evaluator.json';
|
import testEvaluatorData from '../../../../../config/client/test-evaluator.json';
|
||||||
import { challengeTypes } from '../../../../../config/challenge-types';
|
import pythonRunnerData from '../../../../../config/client/python-runner.json';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChallengeFile as PropTypesChallengeFile,
|
ChallengeFile as PropTypesChallengeFile,
|
||||||
ChallengeMeta
|
ChallengeMeta
|
||||||
} from '../../../redux/prop-types';
|
} from '../../../redux/prop-types';
|
||||||
import { concatHtml } from '../rechallenge/builders';
|
import { concatHtml, createPythonTerminal } from '../rechallenge/builders';
|
||||||
import { getTransformers, embedFilesInHtml } from '../rechallenge/transformers';
|
import {
|
||||||
|
getTransformers,
|
||||||
|
embedFilesInHtml,
|
||||||
|
getPythonTransformers
|
||||||
|
} from '../rechallenge/transformers';
|
||||||
import {
|
import {
|
||||||
createTestFramer,
|
createTestFramer,
|
||||||
runTestInTestFrame,
|
runTestInTestFrame,
|
||||||
@@ -41,10 +47,11 @@ interface BuildOptions {
|
|||||||
usesTestRunner: boolean;
|
usesTestRunner: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { filename: runner } = frameRunnerData;
|
|
||||||
const { filename: testEvaluator } = testEvaluatorData;
|
const { filename: testEvaluator } = testEvaluatorData;
|
||||||
|
|
||||||
const frameRunnerSrc = `/js/${runner}.js`;
|
const frameRunnerSrc = `/js/${frameRunnerData.filename}.js`;
|
||||||
|
|
||||||
|
const pythonRunnerSrc = `/js/${pythonRunnerData.filename}.js`;
|
||||||
|
|
||||||
type ApplyFunctionProps = (file: ChallengeFile) => Promise<ChallengeFile>;
|
type ApplyFunctionProps = (file: ChallengeFile) => Promise<ChallengeFile>;
|
||||||
|
|
||||||
@@ -67,6 +74,8 @@ const applyFunction =
|
|||||||
const composeFunctions = (...fns: ApplyFunctionProps[]) =>
|
const composeFunctions = (...fns: ApplyFunctionProps[]) =>
|
||||||
fns.map(applyFunction).reduce((f, g) => x => f(x).then(g));
|
fns.map(applyFunction).reduce((f, g) => x => f(x).then(g));
|
||||||
|
|
||||||
|
// TODO: split this into at least two functions. One to create 'original' i.e.
|
||||||
|
// the source and another to create the contents.
|
||||||
function buildSourceMap(challengeFiles: ChallengeFiles): Source | undefined {
|
function buildSourceMap(challengeFiles: ChallengeFiles): Source | undefined {
|
||||||
// TODO: rename sources.index to sources.contents.
|
// TODO: rename sources.index to sources.contents.
|
||||||
const source: Source | undefined = challengeFiles?.reduce(
|
const source: Source | undefined = challengeFiles?.reduce(
|
||||||
@@ -77,7 +86,11 @@ function buildSourceMap(challengeFiles: ChallengeFiles): Source | undefined {
|
|||||||
sources.editableContents += challengeFile.editableContents || '';
|
sources.editableContents += challengeFile.editableContents || '';
|
||||||
return sources;
|
return sources;
|
||||||
},
|
},
|
||||||
{ index: '', editableContents: '', original: {} } as Source
|
{
|
||||||
|
index: '',
|
||||||
|
editableContents: '',
|
||||||
|
original: {}
|
||||||
|
} as Source
|
||||||
);
|
);
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
@@ -101,7 +114,8 @@ const buildFunctions = {
|
|||||||
[challengeTypes.backEndProject]: buildBackendChallenge,
|
[challengeTypes.backEndProject]: buildBackendChallenge,
|
||||||
[challengeTypes.pythonProject]: buildBackendChallenge,
|
[challengeTypes.pythonProject]: buildBackendChallenge,
|
||||||
[challengeTypes.multifileCertProject]: buildDOMChallenge,
|
[challengeTypes.multifileCertProject]: buildDOMChallenge,
|
||||||
[challengeTypes.colab]: buildBackendChallenge
|
[challengeTypes.colab]: buildBackendChallenge,
|
||||||
|
[challengeTypes.python]: buildPythonChallenge
|
||||||
};
|
};
|
||||||
|
|
||||||
export function canBuildChallenge(challengeData: BuildChallengeData): boolean {
|
export function canBuildChallenge(challengeData: BuildChallengeData): boolean {
|
||||||
@@ -192,6 +206,9 @@ type BuildResult = {
|
|||||||
sources: Source | undefined;
|
sources: Source | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: All the buildXChallenge files have a similar structure, so make that
|
||||||
|
// abstraction (function, class, whatever) and then create the various functions
|
||||||
|
// out of it.
|
||||||
export function buildDOMChallenge(
|
export function buildDOMChallenge(
|
||||||
{ challengeFiles, required = [], template = '' }: BuildChallengeData,
|
{ challengeFiles, required = [], template = '' }: BuildChallengeData,
|
||||||
{ usesTestRunner } = { usesTestRunner: false }
|
{ usesTestRunner } = { usesTestRunner: false }
|
||||||
@@ -206,24 +223,24 @@ export function buildDOMChallenge(
|
|||||||
if (finalFiles) {
|
if (finalFiles) {
|
||||||
return Promise.all(finalFiles)
|
return Promise.all(finalFiles)
|
||||||
.then(checkFilesErrors)
|
.then(checkFilesErrors)
|
||||||
.then(embedFilesInHtml)
|
.then(
|
||||||
.then(([_challengeFiles, _contents]) => {
|
embedFilesInHtml as (
|
||||||
const challengeFiles = _challengeFiles as ChallengeFiles;
|
x: ChallengeFiles
|
||||||
const contents = _contents as string;
|
) => Promise<[ChallengeFiles, string]>
|
||||||
|
)
|
||||||
return {
|
.then(([challengeFiles, contents]) => ({
|
||||||
challengeType:
|
// TODO: Stop overwriting challengeType with 'html'. Figure out why it's
|
||||||
challengeTypes.html || challengeTypes.multifileCertProject,
|
// necessary at the moment.
|
||||||
build: concatHtml({
|
challengeType: challengeTypes.html,
|
||||||
required,
|
build: concatHtml({
|
||||||
template,
|
required,
|
||||||
contents,
|
template,
|
||||||
...(usesTestRunner && { testRunner: frameRunnerSrc })
|
contents,
|
||||||
}),
|
...(usesTestRunner && { testRunner: frameRunnerSrc })
|
||||||
sources: buildSourceMap(challengeFiles),
|
}),
|
||||||
loadEnzyme
|
sources: buildSourceMap(challengeFiles),
|
||||||
};
|
loadEnzyme
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,20 +280,57 @@ function buildBackendChallenge({ url }: BuildChallengeData) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTransformedPython(challengeFiles: ChallengeFiles) {
|
||||||
|
return challengeFiles[0].contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPythonChallenge({
|
||||||
|
challengeFiles
|
||||||
|
}: BuildChallengeData): Promise<BuildResult> | undefined {
|
||||||
|
const pipeLine = composeFunctions(...getPythonTransformers());
|
||||||
|
const finalFiles = challengeFiles.map(pipeLine);
|
||||||
|
|
||||||
|
if (finalFiles) {
|
||||||
|
return (
|
||||||
|
Promise.all(finalFiles)
|
||||||
|
.then(checkFilesErrors)
|
||||||
|
// Unlike the DOM challenges, there's no need to embed the files in HTML
|
||||||
|
.then(challengeFiles => ({
|
||||||
|
// TODO: Stop overwriting challengeType with 'html'. Figure out why it's
|
||||||
|
// necessary at the moment.
|
||||||
|
challengeType: challengeTypes.html,
|
||||||
|
// Both the terminal and pyodide are loaded into the browser, so we
|
||||||
|
// still need to build the HTML.
|
||||||
|
build: createPythonTerminal(pythonRunnerSrc),
|
||||||
|
sources: buildSourceMap(challengeFiles),
|
||||||
|
transformedPython: getTransformedPython(challengeFiles)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function updatePreview(
|
export function updatePreview(
|
||||||
buildData: BuildChallengeData,
|
buildData: BuildChallengeData,
|
||||||
document: Document,
|
document: Document,
|
||||||
proxyLogger: ProxyLogger
|
proxyLogger: ProxyLogger
|
||||||
): void {
|
): Promise<void> {
|
||||||
|
// TODO: either create a 'buildType' or use the real challengeType here
|
||||||
|
// (buildData.challengeType is set to 'html' for challenges that can be
|
||||||
|
// previewed, hence this being true for python challenges, multifile steps and
|
||||||
|
// so on).
|
||||||
|
|
||||||
if (
|
if (
|
||||||
buildData.challengeType === challengeTypes.html ||
|
buildData.challengeType === challengeTypes.html ||
|
||||||
buildData.challengeType === challengeTypes.multifileCertProject
|
buildData.challengeType === challengeTypes.multifileCertProject
|
||||||
) {
|
) {
|
||||||
createMainPreviewFramer(
|
return new Promise<void>(resolve =>
|
||||||
document,
|
createMainPreviewFramer(
|
||||||
proxyLogger,
|
document,
|
||||||
getDocumentTitle(buildData)
|
proxyLogger,
|
||||||
)(buildData);
|
getDocumentTitle(buildData),
|
||||||
|
resolve
|
||||||
|
)(buildData)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Cannot show preview for challenge type ${buildData.challengeType}`
|
`Cannot show preview for challenge type ${buildData.challengeType}`
|
||||||
@@ -318,7 +372,8 @@ export function challengeHasPreview({ challengeType }: ChallengeMeta): boolean {
|
|||||||
return (
|
return (
|
||||||
challengeType === challengeTypes.html ||
|
challengeType === challengeTypes.html ||
|
||||||
challengeType === challengeTypes.modern ||
|
challengeType === challengeTypes.modern ||
|
||||||
challengeType === challengeTypes.multifileCertProject
|
challengeType === challengeTypes.multifileCertProject ||
|
||||||
|
challengeType === challengeTypes.python
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
import { toString, flow } from 'lodash-es';
|
import { toString, flow } from 'lodash-es';
|
||||||
import i18next, { i18n } from 'i18next';
|
import i18next, { type i18n } from 'i18next';
|
||||||
|
|
||||||
import { format } from '../../../utils/format';
|
import { format } from '../../../utils/format';
|
||||||
|
import type {
|
||||||
|
FrameDocument,
|
||||||
|
PythonDocument
|
||||||
|
} from '../../../../../tools/client-plugins/browser-scripts';
|
||||||
|
|
||||||
const utilsFormat: <T>(x: T) => string = format;
|
const utilsFormat: <T>(x: T) => string = format;
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
console: {
|
|
||||||
log: () => void;
|
|
||||||
info: () => void;
|
|
||||||
warn: () => void;
|
|
||||||
error: () => void;
|
|
||||||
};
|
|
||||||
i18nContent: i18n;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Source {
|
export interface Source {
|
||||||
index: string;
|
index: string;
|
||||||
contents?: string;
|
contents?: string;
|
||||||
@@ -24,12 +17,15 @@ export interface Source {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Context {
|
export interface Context {
|
||||||
window: Window;
|
window?: Window &
|
||||||
document: Document;
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
typeof globalThis & { i18nContent?: i18n; __pyodide: unknown };
|
||||||
|
document?: FrameDocument | PythonDocument;
|
||||||
element: HTMLIFrameElement;
|
element: HTMLIFrameElement;
|
||||||
build: string;
|
build: string;
|
||||||
sources: Source;
|
sources: Source;
|
||||||
loadEnzyme?: () => void;
|
loadEnzyme?: () => void;
|
||||||
|
transformedPython?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TestRunnerConfig {
|
export interface TestRunnerConfig {
|
||||||
@@ -139,24 +135,44 @@ type TestResult =
|
|||||||
| { pass: boolean }
|
| { pass: boolean }
|
||||||
| { err: { message: string; stack?: string } };
|
| { err: { message: string; stack?: string } };
|
||||||
|
|
||||||
|
function getContentDocument<T extends Document = FrameDocument>(
|
||||||
|
document: Document,
|
||||||
|
id: string
|
||||||
|
) {
|
||||||
|
const frame = document.getElementById(id);
|
||||||
|
if (!frame) return null;
|
||||||
|
const frameDocument = (frame as HTMLIFrameElement).contentDocument;
|
||||||
|
return frameDocument as T;
|
||||||
|
}
|
||||||
|
|
||||||
export const runTestInTestFrame = async function (
|
export const runTestInTestFrame = async function (
|
||||||
document: Document,
|
document: Document,
|
||||||
test: string,
|
test: string,
|
||||||
timeout: number
|
timeout: number
|
||||||
): Promise<TestResult | undefined> {
|
): Promise<TestResult | undefined> {
|
||||||
const { contentDocument: frame } = document.getElementById(
|
const contentDocument = getContentDocument(document, testId);
|
||||||
testId
|
if (contentDocument) {
|
||||||
) as HTMLIFrameElement;
|
|
||||||
if (frame !== null) {
|
|
||||||
return await Promise.race([
|
return await Promise.race([
|
||||||
new Promise<
|
new Promise<
|
||||||
{ pass: boolean } | { err: { message: string; stack?: string } }
|
{ pass: boolean } | { err: { message: string; stack?: string } }
|
||||||
>((_, reject) => setTimeout(() => reject('timeout'), timeout)),
|
>((_, reject) => setTimeout(() => reject('timeout'), timeout)),
|
||||||
frame.__runTest(test)
|
contentDocument.__runTest(test)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const runPythonInFrame = function (
|
||||||
|
document: Document,
|
||||||
|
code: string,
|
||||||
|
previewId: string
|
||||||
|
): void {
|
||||||
|
const contentDocument = getContentDocument<PythonDocument>(
|
||||||
|
document,
|
||||||
|
previewId
|
||||||
|
);
|
||||||
|
void contentDocument?.__runPython(code);
|
||||||
|
};
|
||||||
|
|
||||||
const createFrame =
|
const createFrame =
|
||||||
(document: Document, id: string, title?: string) =>
|
(document: Document, id: string, title?: string) =>
|
||||||
(frameContext: Context) => {
|
(frameContext: Context) => {
|
||||||
@@ -253,45 +269,56 @@ const updateWindowI18next = () => (frameContext: Context) => {
|
|||||||
const initTestFrame = (frameReady?: () => void) => (frameContext: Context) => {
|
const initTestFrame = (frameReady?: () => void) => (frameContext: Context) => {
|
||||||
waitForFrame(frameContext)
|
waitForFrame(frameContext)
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
const { sources, loadEnzyme } = frameContext;
|
const { sources, loadEnzyme, transformedPython } = frameContext;
|
||||||
// provide the file name and get the original source
|
// provide the file name and get the original source
|
||||||
const getUserInput = (fileName: string) =>
|
const getUserInput = (fileName: string) =>
|
||||||
toString(sources[fileName as keyof typeof sources]);
|
toString(sources[fileName as keyof typeof sources]);
|
||||||
await frameContext.document.__initTestFrame({
|
await frameContext.document?.__initTestFrame({
|
||||||
code: sources,
|
code: sources,
|
||||||
getUserInput,
|
getUserInput,
|
||||||
loadEnzyme
|
loadEnzyme,
|
||||||
|
transformedPython
|
||||||
});
|
});
|
||||||
if (frameReady) {
|
|
||||||
frameReady();
|
if (frameReady) frameReady();
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(handleDocumentNotFound);
|
.catch(handleDocumentNotFound);
|
||||||
return frameContext;
|
return frameContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initMainFrame =
|
const initMainFrame =
|
||||||
(_: unknown, proxyLogger?: ProxyLogger) => (frameContext: Context) => {
|
(frameReady?: () => void, proxyLogger?: ProxyLogger) =>
|
||||||
|
(frameContext: Context) => {
|
||||||
waitForFrame(frameContext)
|
waitForFrame(frameContext)
|
||||||
.then(() => {
|
.then(async () => {
|
||||||
// Overwriting the onerror added by createHeader to catch any errors thrown
|
// Overwriting the onerror added by createHeader to catch any errors thrown
|
||||||
// after the frame is ready. It has to be overwritten, as proxyLogger cannot
|
// after the frame is ready. It has to be overwritten, as proxyLogger cannot
|
||||||
// be added as part of createHeader.
|
// be added as part of createHeader.
|
||||||
|
|
||||||
frameContext.window.onerror = function (msg) {
|
if (frameContext.window) {
|
||||||
if (typeof msg === 'string') {
|
frameContext.window.onerror = function (msg) {
|
||||||
const string = msg.toLowerCase();
|
if (typeof msg === 'string') {
|
||||||
if (string.includes('script error')) {
|
const string = msg.toLowerCase();
|
||||||
msg = 'Error, open your browser console to learn more.';
|
if (string.includes('script error')) {
|
||||||
|
msg = 'Error, open your browser console to learn more.';
|
||||||
|
}
|
||||||
|
if (proxyLogger) {
|
||||||
|
proxyLogger(msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (proxyLogger) {
|
// let the error propagate so it appears in the browser console, otherwise
|
||||||
proxyLogger(msg);
|
// an error from a cross origin script just appears as 'Script error.'
|
||||||
}
|
return false;
|
||||||
}
|
};
|
||||||
// let the error propagate so it appears in the browser console, otherwise
|
}
|
||||||
// an error from a cross origin script just appears as 'Script error.'
|
|
||||||
return false;
|
if (
|
||||||
};
|
frameContext.document &&
|
||||||
|
'__initPythonFrame' in frameContext.document
|
||||||
|
) {
|
||||||
|
await frameContext.document?.__initPythonFrame();
|
||||||
|
}
|
||||||
|
if (frameReady) frameReady();
|
||||||
})
|
})
|
||||||
.catch(handleDocumentNotFound);
|
.catch(handleDocumentNotFound);
|
||||||
return frameContext;
|
return frameContext;
|
||||||
@@ -305,6 +332,21 @@ function handleDocumentNotFound(err: string) {
|
|||||||
|
|
||||||
const initPreviewFrame = () => (frameContext: Context) => frameContext;
|
const initPreviewFrame = () => (frameContext: Context) => frameContext;
|
||||||
|
|
||||||
|
// TODO: reimplement when ready to preview python challenges
|
||||||
|
// const initPreviewFrame = () => (frameContext: Context) => {
|
||||||
|
// waitForFrame(frameContext)
|
||||||
|
// .then(() => {
|
||||||
|
// if (
|
||||||
|
// frameContext.document &&
|
||||||
|
// '__initPythonFrame' in frameContext.document
|
||||||
|
// ) {
|
||||||
|
// void frameContext.document?.__initPythonFrame();
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .catch(handleDocumentNotFound);
|
||||||
|
// return frameContext;
|
||||||
|
// };
|
||||||
|
|
||||||
const waitForFrame = (frameContext: Context) => {
|
const waitForFrame = (frameContext: Context) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!frameContext.document) {
|
if (!frameContext.document) {
|
||||||
@@ -317,7 +359,7 @@ const waitForFrame = (frameContext: Context) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function writeToFrame(content: string, frame: Document | null) {
|
function writeToFrame(content: string, frame?: FrameDocument) {
|
||||||
// it's possible, if the preview is rapidly opened and closed, for the frame
|
// it's possible, if the preview is rapidly opened and closed, for the frame
|
||||||
// to be null at this point.
|
// to be null at this point.
|
||||||
if (frame) {
|
if (frame) {
|
||||||
@@ -344,14 +386,15 @@ const writeContentToFrame = (frameContext: Context) => {
|
|||||||
export const createMainPreviewFramer = (
|
export const createMainPreviewFramer = (
|
||||||
document: Document,
|
document: Document,
|
||||||
proxyLogger: ProxyLogger,
|
proxyLogger: ProxyLogger,
|
||||||
frameTitle: string
|
frameTitle: string,
|
||||||
|
frameReady?: () => void
|
||||||
): ((args: Context) => void) =>
|
): ((args: Context) => void) =>
|
||||||
createFramer(
|
createFramer(
|
||||||
document,
|
document,
|
||||||
mainPreviewId,
|
mainPreviewId,
|
||||||
initMainFrame,
|
initMainFrame,
|
||||||
proxyLogger,
|
proxyLogger,
|
||||||
undefined,
|
frameReady,
|
||||||
frameTitle
|
frameTitle
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -80,8 +80,9 @@ const CertChallenge = ({
|
|||||||
const [isCertified, setIsCertified] = useState(false);
|
const [isCertified, setIsCertified] = useState(false);
|
||||||
const [userLoaded, setUserLoaded] = useState(false);
|
const [userLoaded, setUserLoaded] = useState(false);
|
||||||
|
|
||||||
// @ts-expect-error Typescript is confused
|
const cert = fullCertMap.find(x => x.title === title);
|
||||||
const certSlug = fullCertMap.find(x => x.title === title).certSlug;
|
if (!cert) throw Error(`Certification ${title} not found`);
|
||||||
|
const certSlug = cert.certSlug;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { pending, complete } = fetchState;
|
const { pending, complete } = fetchState;
|
||||||
|
|||||||
@@ -147,7 +147,9 @@ function getProjectPreviewConfig(challenge, allChallengeEdges) {
|
|||||||
showProjectPreview:
|
showProjectPreview:
|
||||||
challengeOrder === 0 &&
|
challengeOrder === 0 &&
|
||||||
usesMultifileEditor &&
|
usesMultifileEditor &&
|
||||||
challengeType !== challengeTypes.multifileCertProject,
|
challengeType !== challengeTypes.multifileCertProject &&
|
||||||
|
// TODO: revert this to enable project previews for python challenges
|
||||||
|
challengeType !== challengeTypes.python,
|
||||||
challengeData: {
|
challengeData: {
|
||||||
challengeType: lastChallenge.challengeType,
|
challengeType: lastChallenge.challengeType,
|
||||||
challengeFiles: projectPreviewChallengeFiles
|
challengeFiles: projectPreviewChallengeFiles
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const colab = 16;
|
|||||||
const exam = 17;
|
const exam = 17;
|
||||||
const msTrophyUrl = 18;
|
const msTrophyUrl = 18;
|
||||||
const multipleChoice = 19;
|
const multipleChoice = 19;
|
||||||
|
const python = 20;
|
||||||
|
|
||||||
export const challengeTypes = {
|
export const challengeTypes = {
|
||||||
html,
|
html,
|
||||||
@@ -41,7 +42,8 @@ export const challengeTypes = {
|
|||||||
colab,
|
colab,
|
||||||
exam,
|
exam,
|
||||||
msTrophyUrl,
|
msTrophyUrl,
|
||||||
multipleChoice
|
multipleChoice,
|
||||||
|
python
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isFinalProject = (challengeType: number) => {
|
export const isFinalProject = (challengeType: number) => {
|
||||||
@@ -91,7 +93,8 @@ export const viewTypes = {
|
|||||||
[colab]: 'frontend',
|
[colab]: 'frontend',
|
||||||
[exam]: 'exam',
|
[exam]: 'exam',
|
||||||
[msTrophyUrl]: 'frontend',
|
[msTrophyUrl]: 'frontend',
|
||||||
[multipleChoice]: 'video'
|
[multipleChoice]: 'video',
|
||||||
|
[python]: 'modern'
|
||||||
};
|
};
|
||||||
|
|
||||||
// determine the type of submit function to use for the challenge on completion
|
// determine the type of submit function to use for the challenge on completion
|
||||||
@@ -118,5 +121,6 @@ export const submitTypes = {
|
|||||||
[colab]: 'project.backEnd',
|
[colab]: 'project.backEnd',
|
||||||
[exam]: 'exam',
|
[exam]: 'exam',
|
||||||
[msTrophyUrl]: 'project.frontEnd',
|
[msTrophyUrl]: 'project.frontEnd',
|
||||||
[multipleChoice]: 'tests'
|
[multipleChoice]: 'tests',
|
||||||
|
[python]: 'tests'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ export enum SuperBlocks {
|
|||||||
ProjectEuler = 'project-euler',
|
ProjectEuler = 'project-euler',
|
||||||
CollegeAlgebraPy = 'college-algebra-with-python',
|
CollegeAlgebraPy = 'college-algebra-with-python',
|
||||||
FoundationalCSharp = 'foundational-c-sharp-with-microsoft',
|
FoundationalCSharp = 'foundational-c-sharp-with-microsoft',
|
||||||
ExampleCertification = 'example-certification'
|
ExampleCertification = 'example-certification',
|
||||||
|
UpcomingPython = 'upcoming-python'
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -75,7 +76,8 @@ export const superBlockOrder: SuperBlockOrder = {
|
|||||||
SuperBlocks.JsAlgoDataStructNew,
|
SuperBlocks.JsAlgoDataStructNew,
|
||||||
SuperBlocks.TheOdinProject,
|
SuperBlocks.TheOdinProject,
|
||||||
SuperBlocks.FoundationalCSharp,
|
SuperBlocks.FoundationalCSharp,
|
||||||
SuperBlocks.ExampleCertification
|
SuperBlocks.ExampleCertification,
|
||||||
|
SuperBlocks.UpcomingPython
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,7 +99,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
|
|||||||
SuperBlocks.ProjectEuler,
|
SuperBlocks.ProjectEuler,
|
||||||
SuperBlocks.JsAlgoDataStructNew,
|
SuperBlocks.JsAlgoDataStructNew,
|
||||||
SuperBlocks.TheOdinProject,
|
SuperBlocks.TheOdinProject,
|
||||||
SuperBlocks.FoundationalCSharp
|
SuperBlocks.FoundationalCSharp,
|
||||||
|
SuperBlocks.UpcomingPython
|
||||||
],
|
],
|
||||||
[Languages.Chinese]: [
|
[Languages.Chinese]: [
|
||||||
SuperBlocks.CollegeAlgebraPy,
|
SuperBlocks.CollegeAlgebraPy,
|
||||||
@@ -105,7 +108,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
|
|||||||
SuperBlocks.ProjectEuler,
|
SuperBlocks.ProjectEuler,
|
||||||
SuperBlocks.JsAlgoDataStructNew,
|
SuperBlocks.JsAlgoDataStructNew,
|
||||||
SuperBlocks.TheOdinProject,
|
SuperBlocks.TheOdinProject,
|
||||||
SuperBlocks.FoundationalCSharp
|
SuperBlocks.FoundationalCSharp,
|
||||||
|
SuperBlocks.UpcomingPython
|
||||||
],
|
],
|
||||||
[Languages.ChineseTraditional]: [
|
[Languages.ChineseTraditional]: [
|
||||||
SuperBlocks.CollegeAlgebraPy,
|
SuperBlocks.CollegeAlgebraPy,
|
||||||
@@ -113,29 +117,34 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
|
|||||||
SuperBlocks.ProjectEuler,
|
SuperBlocks.ProjectEuler,
|
||||||
SuperBlocks.JsAlgoDataStructNew,
|
SuperBlocks.JsAlgoDataStructNew,
|
||||||
SuperBlocks.TheOdinProject,
|
SuperBlocks.TheOdinProject,
|
||||||
SuperBlocks.FoundationalCSharp
|
SuperBlocks.FoundationalCSharp,
|
||||||
|
SuperBlocks.UpcomingPython
|
||||||
],
|
],
|
||||||
[Languages.Italian]: [
|
[Languages.Italian]: [
|
||||||
SuperBlocks.JsAlgoDataStructNew,
|
SuperBlocks.JsAlgoDataStructNew,
|
||||||
SuperBlocks.TheOdinProject,
|
SuperBlocks.TheOdinProject,
|
||||||
SuperBlocks.FoundationalCSharp
|
SuperBlocks.FoundationalCSharp,
|
||||||
|
SuperBlocks.UpcomingPython
|
||||||
],
|
],
|
||||||
[Languages.Portuguese]: [
|
[Languages.Portuguese]: [
|
||||||
SuperBlocks.JsAlgoDataStructNew,
|
SuperBlocks.JsAlgoDataStructNew,
|
||||||
SuperBlocks.FoundationalCSharp
|
SuperBlocks.FoundationalCSharp,
|
||||||
|
SuperBlocks.UpcomingPython
|
||||||
],
|
],
|
||||||
[Languages.Ukrainian]: [
|
[Languages.Ukrainian]: [
|
||||||
SuperBlocks.CodingInterviewPrep,
|
SuperBlocks.CodingInterviewPrep,
|
||||||
SuperBlocks.ProjectEuler,
|
SuperBlocks.ProjectEuler,
|
||||||
SuperBlocks.JsAlgoDataStructNew,
|
SuperBlocks.JsAlgoDataStructNew,
|
||||||
SuperBlocks.FoundationalCSharp
|
SuperBlocks.FoundationalCSharp,
|
||||||
|
SuperBlocks.UpcomingPython
|
||||||
],
|
],
|
||||||
[Languages.Japanese]: [
|
[Languages.Japanese]: [
|
||||||
SuperBlocks.CollegeAlgebraPy,
|
SuperBlocks.CollegeAlgebraPy,
|
||||||
SuperBlocks.ProjectEuler,
|
SuperBlocks.ProjectEuler,
|
||||||
SuperBlocks.JsAlgoDataStructNew,
|
SuperBlocks.JsAlgoDataStructNew,
|
||||||
SuperBlocks.TheOdinProject,
|
SuperBlocks.TheOdinProject,
|
||||||
SuperBlocks.FoundationalCSharp
|
SuperBlocks.FoundationalCSharp,
|
||||||
|
SuperBlocks.UpcomingPython
|
||||||
],
|
],
|
||||||
[Languages.German]: [
|
[Languages.German]: [
|
||||||
SuperBlocks.RespWebDesignNew,
|
SuperBlocks.RespWebDesignNew,
|
||||||
@@ -152,7 +161,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
|
|||||||
SuperBlocks.ProjectEuler,
|
SuperBlocks.ProjectEuler,
|
||||||
SuperBlocks.JsAlgoDataStructNew,
|
SuperBlocks.JsAlgoDataStructNew,
|
||||||
SuperBlocks.TheOdinProject,
|
SuperBlocks.TheOdinProject,
|
||||||
SuperBlocks.FoundationalCSharp
|
SuperBlocks.FoundationalCSharp,
|
||||||
|
SuperBlocks.UpcomingPython
|
||||||
],
|
],
|
||||||
[Languages.Arabic]: [
|
[Languages.Arabic]: [
|
||||||
SuperBlocks.DataVis,
|
SuperBlocks.DataVis,
|
||||||
@@ -168,7 +178,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
|
|||||||
SuperBlocks.ProjectEuler,
|
SuperBlocks.ProjectEuler,
|
||||||
SuperBlocks.JsAlgoDataStructNew,
|
SuperBlocks.JsAlgoDataStructNew,
|
||||||
SuperBlocks.TheOdinProject,
|
SuperBlocks.TheOdinProject,
|
||||||
SuperBlocks.FoundationalCSharp
|
SuperBlocks.FoundationalCSharp,
|
||||||
|
SuperBlocks.UpcomingPython
|
||||||
],
|
],
|
||||||
[Languages.Swahili]: [
|
[Languages.Swahili]: [
|
||||||
SuperBlocks.DataVis,
|
SuperBlocks.DataVis,
|
||||||
@@ -187,7 +198,8 @@ export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
|
|||||||
SuperBlocks.RespWebDesign,
|
SuperBlocks.RespWebDesign,
|
||||||
SuperBlocks.FrontEndDevLibs,
|
SuperBlocks.FrontEndDevLibs,
|
||||||
SuperBlocks.JsAlgoDataStructNew,
|
SuperBlocks.JsAlgoDataStructNew,
|
||||||
SuperBlocks.JsAlgoDataStruct
|
SuperBlocks.JsAlgoDataStruct,
|
||||||
|
SuperBlocks.UpcomingPython
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "Learn Python By Building a Blackjack Game",
|
||||||
|
"isUpcomingChange": true,
|
||||||
|
"usesMultifileEditor": true,
|
||||||
|
"hasEditableBoundaries": true,
|
||||||
|
"dashedName": "learn-python-by-building-a-blackjack-game",
|
||||||
|
"helpCategory": "Python",
|
||||||
|
"order": 0,
|
||||||
|
"time": "2 hours",
|
||||||
|
"template": "",
|
||||||
|
"required": [],
|
||||||
|
"superBlock": "upcoming-python",
|
||||||
|
"isBeta": true,
|
||||||
|
"challengeOrder": [
|
||||||
|
{
|
||||||
|
"id": "5daa813381b9e3db6c126b43",
|
||||||
|
"title": "Step 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "64a5229b99ff0e8250cd9a72",
|
||||||
|
"title": "Step 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "64b163c20e59cbd4a64940b0",
|
||||||
|
"title": "Step 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "64b171849f925b0773aa434c",
|
||||||
|
"title": "Step 4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -232,4 +232,4 @@
|
|||||||
"title": "Data Visualization: Mailing Lists"
|
"title": "Data Visualization: Mailing Lists"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,4 +28,4 @@
|
|||||||
"title": "Probability Calculator"
|
"title": "Probability Calculator"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "Upcoming Python Project",
|
||||||
|
"isUpcomingChange": true,
|
||||||
|
"dashedName": "upcoming-python-project",
|
||||||
|
"helpCategory": "Python",
|
||||||
|
"order": 1,
|
||||||
|
"time": "2 hours",
|
||||||
|
"template": "",
|
||||||
|
"required": [],
|
||||||
|
"superBlock": "upcoming-python",
|
||||||
|
"challengeOrder": [
|
||||||
|
{
|
||||||
|
"id": "64afc37bf3b37856e035b85e",
|
||||||
|
"title": "Upcoming Python Project"
|
||||||
|
}
|
||||||
|
]}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
id: 64afc4e8f3b37856e035b85f
|
||||||
|
title: Upcoming Python Certification
|
||||||
|
certification: upcoming-python-certification
|
||||||
|
challengeType: 7
|
||||||
|
isPrivate: true
|
||||||
|
tests:
|
||||||
|
- id: 64afc37bf3b37856e035b85e
|
||||||
|
title: Upcoming Python Project
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
id: 5daa813381b9e3db6c126b43
|
||||||
|
title: Step 1
|
||||||
|
challengeType: 20
|
||||||
|
dashedName: step-1
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
PYTHON
|
||||||
|
|
||||||
|
Set the `hello` variable to "world". Then print the value.
|
||||||
|
|
||||||
|
# --hints--
|
||||||
|
|
||||||
|
The `hello` variable should equal "world".
|
||||||
|
|
||||||
|
```js
|
||||||
|
({ test: () => assert.equal(__userGlobals.get("hello"), "world") })
|
||||||
|
```
|
||||||
|
|
||||||
|
# --seed--
|
||||||
|
|
||||||
|
## --seed-contents--
|
||||||
|
|
||||||
|
```py
|
||||||
|
--fcc-editable-region--
|
||||||
|
one = 1
|
||||||
|
hello = "goodbye"
|
||||||
|
def a_function():
|
||||||
|
local_thing = "world"
|
||||||
|
print(local_thing)
|
||||||
|
|
||||||
|
a_function()
|
||||||
|
|
||||||
|
print(hello)
|
||||||
|
--fcc-editable-region--
|
||||||
|
```
|
||||||
|
|
||||||
|
# --solutions--
|
||||||
|
|
||||||
|
```py
|
||||||
|
one = 1
|
||||||
|
hello = "world"
|
||||||
|
print(hello)
|
||||||
|
```
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
id: 64a5229b99ff0e8250cd9a72
|
||||||
|
title: Step 2
|
||||||
|
challengeType: 20
|
||||||
|
dashedName: step-2
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
step 2 instructions
|
||||||
|
|
||||||
|
# --hints--
|
||||||
|
|
||||||
|
Test 1
|
||||||
|
|
||||||
|
```js
|
||||||
|
({ input: ["Beau", "Carnes"], test: () => {
|
||||||
|
assert.equal( "Beau", __userGlobals.get("name"));
|
||||||
|
assert.equal( "Carnes", __userGlobals.get("last_name"));
|
||||||
|
} })
|
||||||
|
```
|
||||||
|
|
||||||
|
# --seed--
|
||||||
|
|
||||||
|
## --seed-contents--
|
||||||
|
|
||||||
|
```py
|
||||||
|
--fcc-editable-region--
|
||||||
|
name = input('What is your name?')
|
||||||
|
print('Hi ' + name)
|
||||||
|
--fcc-editable-region--
|
||||||
|
```
|
||||||
|
|
||||||
|
# --solutions--
|
||||||
|
|
||||||
|
```py
|
||||||
|
name = input('What is your name?')
|
||||||
|
last_name = input('What is your last name?')
|
||||||
|
print('Hi ' + name + ' ' + last_name)
|
||||||
|
```
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
id: 64b163c20e59cbd4a64940b0
|
||||||
|
title: Step 3
|
||||||
|
challengeType: 20
|
||||||
|
dashedName: step-3
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
Create a function to add two numbers together.
|
||||||
|
|
||||||
|
# --hints--
|
||||||
|
|
||||||
|
Adding 3 and 5 should return 8.
|
||||||
|
|
||||||
|
```js
|
||||||
|
({ test: () => assert.equal(__userGlobals.get("add")(3,5), 8) })
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# --seed--
|
||||||
|
|
||||||
|
## --seed-contents--
|
||||||
|
|
||||||
|
```py
|
||||||
|
--fcc-editable-region--
|
||||||
|
|
||||||
|
--fcc-editable-region--
|
||||||
|
```
|
||||||
|
|
||||||
|
# --solutions--
|
||||||
|
|
||||||
|
```py
|
||||||
|
def add(a, b):
|
||||||
|
return a + b
|
||||||
|
```
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
id: 64b171849f925b0773aa434c
|
||||||
|
title: Step 4
|
||||||
|
challengeType: 20
|
||||||
|
dashedName: step-4
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
Create an list of numbers from 1 to 3 and assign it to the variable `xs`.
|
||||||
|
|
||||||
|
# --hints--
|
||||||
|
|
||||||
|
`xs` should be the list `[1,2,3]`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
({ test: () => assert.deepEqual(Array.from(__userGlobals.get("xs")), [1,2,3])})
|
||||||
|
```
|
||||||
|
|
||||||
|
# --seed--
|
||||||
|
|
||||||
|
## --seed-contents--
|
||||||
|
|
||||||
|
```py
|
||||||
|
--fcc-editable-region--
|
||||||
|
|
||||||
|
--fcc-editable-region--
|
||||||
|
```
|
||||||
|
|
||||||
|
# --solutions--
|
||||||
|
|
||||||
|
```py
|
||||||
|
xs = [1,2,3]
|
||||||
|
```
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
id: 64afc37bf3b37856e035b85e
|
||||||
|
title: Upcoming Python Project
|
||||||
|
challengeType: 20
|
||||||
|
dashedName: upcoming-python-project
|
||||||
|
---
|
||||||
|
|
||||||
|
# --description--
|
||||||
|
|
||||||
|
Python Project instructions
|
||||||
|
|
||||||
|
# --hints--
|
||||||
|
|
||||||
|
Test 1
|
||||||
|
|
||||||
|
```js
|
||||||
|
({ input: ["Beau", "Carnes"], test: () => {
|
||||||
|
assert.equal( "Beau", __userGlobals.get("name"));
|
||||||
|
assert.equal( "Carnes", __userGlobals.get("last_name"));
|
||||||
|
} })
|
||||||
|
```
|
||||||
|
|
||||||
|
# --seed--
|
||||||
|
|
||||||
|
## --seed-contents--
|
||||||
|
|
||||||
|
```py
|
||||||
|
--fcc-editable-region--
|
||||||
|
name = input('What is your name?')
|
||||||
|
print('Hi ' + name)
|
||||||
|
--fcc-editable-region--
|
||||||
|
```
|
||||||
|
|
||||||
|
# --solutions--
|
||||||
|
|
||||||
|
```py
|
||||||
|
name = input('What is your name?')
|
||||||
|
last_name = input('What is your last name?')
|
||||||
|
print('Hi ' + name + ' ' + last_name)
|
||||||
|
```
|
||||||
@@ -33,7 +33,7 @@ const schema = Joi.object()
|
|||||||
challengeOrder: Joi.number(),
|
challengeOrder: Joi.number(),
|
||||||
removeComments: Joi.bool().required(),
|
removeComments: Joi.bool().required(),
|
||||||
certification: Joi.string().regex(slugRE),
|
certification: Joi.string().regex(slugRE),
|
||||||
challengeType: Joi.number().min(0).max(19).required(),
|
challengeType: Joi.number().min(0).max(20).required(),
|
||||||
checksum: Joi.number(),
|
checksum: Joi.number(),
|
||||||
// TODO: require this only for normal challenges, not certs
|
// TODO: require this only for normal challenges, not certs
|
||||||
dashedName: Joi.string().regex(slugRE),
|
dashedName: Joi.string().regex(slugRE),
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ require('@babel/register')({
|
|||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
buildDOMChallenge,
|
buildDOMChallenge,
|
||||||
buildJSChallenge
|
buildJSChallenge,
|
||||||
|
buildPythonChallenge
|
||||||
} = require('../../client/src/templates/Challenges/utils/build');
|
} = require('../../client/src/templates/Challenges/utils/build');
|
||||||
const {
|
const {
|
||||||
default: createWorker
|
default: createWorker
|
||||||
@@ -319,12 +320,14 @@ function populateTestsForLang({ lang, challenges, meta }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { challengeType } = challenge;
|
const { challengeType } = challenge;
|
||||||
|
// TODO: shouldn't this be a function in challenge-types.js?
|
||||||
if (
|
if (
|
||||||
challengeType !== challengeTypes.html &&
|
challengeType !== challengeTypes.html &&
|
||||||
challengeType !== challengeTypes.js &&
|
challengeType !== challengeTypes.js &&
|
||||||
challengeType !== challengeTypes.jsProject &&
|
challengeType !== challengeTypes.jsProject &&
|
||||||
challengeType !== challengeTypes.modern &&
|
challengeType !== challengeTypes.modern &&
|
||||||
challengeType !== challengeTypes.backend
|
challengeType !== challengeTypes.backend &&
|
||||||
|
challengeType !== challengeTypes.python
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -349,14 +352,21 @@ function populateTestsForLang({ lang, challenges, meta }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(after python PR): simplify pipeline and sync with client.
|
||||||
|
// buildChallengeData should be called and any errors handled.
|
||||||
|
// canBuildChallenge does not need to exist independently.
|
||||||
const buildChallenge =
|
const buildChallenge =
|
||||||
challengeType === challengeTypes.js ||
|
{
|
||||||
challengeType === challengeTypes.jsProject
|
[challengeTypes.js]: buildJSChallenge,
|
||||||
? buildJSChallenge
|
[challengeTypes.jsProject]: buildJSChallenge,
|
||||||
: buildDOMChallenge;
|
[challengeTypes.python]: buildPythonChallenge
|
||||||
|
}[challengeType] ?? buildDOMChallenge;
|
||||||
|
|
||||||
|
// The python tests are (currently) slow, so we give them more time.
|
||||||
|
const timePerTest =
|
||||||
|
challengeType === challengeTypes.python ? 10000 : 5000;
|
||||||
it('Test suite must fail on the initial contents', async function () {
|
it('Test suite must fail on the initial contents', async function () {
|
||||||
this.timeout(5000 * tests.length + 1000);
|
this.timeout(timePerTest * tests.length + 1000);
|
||||||
// suppress errors in the console.
|
// suppress errors in the console.
|
||||||
const oldConsoleError = console.error;
|
const oldConsoleError = console.error;
|
||||||
console.error = () => {};
|
console.error = () => {};
|
||||||
@@ -445,7 +455,7 @@ function populateTestsForLang({ lang, challenges, meta }) {
|
|||||||
it(`Solution ${
|
it(`Solution ${
|
||||||
index + 1
|
index + 1
|
||||||
} must pass the tests`, async function () {
|
} must pass the tests`, async function () {
|
||||||
this.timeout(5000 * tests.length + 2000);
|
this.timeout(timePerTest * tests.length + 2000);
|
||||||
const testRunner = await createTestRunner(
|
const testRunner = await createTestRunner(
|
||||||
challenge,
|
challenge,
|
||||||
solution,
|
solution,
|
||||||
@@ -477,22 +487,27 @@ async function createTestRunner(
|
|||||||
solutionFiles
|
solutionFiles
|
||||||
);
|
);
|
||||||
|
|
||||||
const { build, sources, loadEnzyme } = await buildChallenge(
|
const { build, sources, loadEnzyme, transformedPython } =
|
||||||
{
|
await buildChallenge(
|
||||||
challengeFiles,
|
{
|
||||||
required,
|
challengeFiles,
|
||||||
template
|
required,
|
||||||
},
|
template
|
||||||
{ usesTestRunner: true }
|
},
|
||||||
);
|
{ usesTestRunner: true }
|
||||||
|
);
|
||||||
|
|
||||||
const code = {
|
const code = {
|
||||||
contents: sources.index,
|
contents: sources.index,
|
||||||
editableContents: sources.editableContents
|
editableContents: sources.editableContents
|
||||||
};
|
};
|
||||||
|
|
||||||
const evaluator = await (buildChallenge === buildDOMChallenge
|
const runsInBrowser =
|
||||||
? getContextEvaluator(build, sources, code, loadEnzyme)
|
buildChallenge === buildDOMChallenge ||
|
||||||
|
buildChallenge === buildPythonChallenge;
|
||||||
|
|
||||||
|
const evaluator = await (runsInBrowser
|
||||||
|
? getContextEvaluator(build, sources, code, loadEnzyme, transformedPython)
|
||||||
: getWorkerEvaluator(build, sources, code, removeComments));
|
: getWorkerEvaluator(build, sources, code, removeComments));
|
||||||
|
|
||||||
return async ({ text, testString }) => {
|
return async ({ text, testString }) => {
|
||||||
@@ -537,8 +552,20 @@ function replaceChallengeFilesContentsWithSolutions(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getContextEvaluator(build, sources, code, loadEnzyme) {
|
async function getContextEvaluator(
|
||||||
await initializeTestRunner(build, sources, code, loadEnzyme);
|
build,
|
||||||
|
sources,
|
||||||
|
code,
|
||||||
|
loadEnzyme,
|
||||||
|
transformedPython
|
||||||
|
) {
|
||||||
|
await initializeTestRunner(
|
||||||
|
build,
|
||||||
|
sources,
|
||||||
|
code,
|
||||||
|
loadEnzyme,
|
||||||
|
transformedPython
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
evaluate: async (testString, timeout) =>
|
evaluate: async (testString, timeout) =>
|
||||||
@@ -564,20 +591,30 @@ async function getWorkerEvaluator(build, sources, code, removeComments) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initializeTestRunner(build, sources, code, loadEnzyme) {
|
async function initializeTestRunner(
|
||||||
|
build,
|
||||||
|
sources,
|
||||||
|
code,
|
||||||
|
loadEnzyme,
|
||||||
|
transformedPython
|
||||||
|
) {
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await page.setContent(build);
|
await page.setContent(build);
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
async (code, sources, loadEnzyme) => {
|
async (code, sources, loadEnzyme, transformedPython) => {
|
||||||
const getUserInput = fileName => sources[fileName];
|
const getUserInput = fileName => sources[fileName];
|
||||||
|
// TODO: use frame's functions directly, so it behaves more like the
|
||||||
|
// client. Also, keep an eye on performance - loading pyodide is slow.
|
||||||
await document.__initTestFrame({
|
await document.__initTestFrame({
|
||||||
code: sources,
|
code: sources,
|
||||||
getUserInput,
|
getUserInput,
|
||||||
loadEnzyme
|
loadEnzyme,
|
||||||
|
transformedPython
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
code,
|
code,
|
||||||
sources,
|
sources,
|
||||||
loadEnzyme
|
loadEnzyme,
|
||||||
|
transformedPython
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ const directoryToSuperblock = {
|
|||||||
'18-project-euler': 'project-euler',
|
'18-project-euler': 'project-euler',
|
||||||
'19-foundational-c-sharp-with-microsoft':
|
'19-foundational-c-sharp-with-microsoft':
|
||||||
'foundational-c-sharp-with-microsoft',
|
'foundational-c-sharp-with-microsoft',
|
||||||
|
'20-upcoming-python': 'upcoming-python',
|
||||||
'99-example-certification': 'example-certification'
|
'99-example-certification': 'example-certification'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ describe('getSuperBlockFromPath', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it('handles all the directories in ./challenges/english', () => {
|
it('handles all the directories in ./challenges/english', () => {
|
||||||
expect.assertions(20);
|
expect.assertions(21);
|
||||||
|
|
||||||
for (const directory of directories) {
|
for (const directory of directories) {
|
||||||
expect(() => getSuperBlockFromDir(directory)).not.toThrow();
|
expect(() => getSuperBlockFromDir(directory)).not.toThrow();
|
||||||
@@ -151,7 +151,7 @@ describe('getSuperBlockFromPath', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns valid superblocks (or 'certifications') for all valid arguments", () => {
|
it("returns valid superblocks (or 'certifications') for all valid arguments", () => {
|
||||||
expect.assertions(20);
|
expect.assertions(21);
|
||||||
|
|
||||||
const superBlockPaths = directories.filter(x => x !== '00-certifications');
|
const superBlockPaths = directories.filter(x => x !== '00-certifications');
|
||||||
|
|
||||||
|
|||||||
798
pnpm-lock.yaml
generated
798
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,13 +2,12 @@ packages:
|
|||||||
- 'api'
|
- 'api'
|
||||||
- 'api-server'
|
- 'api-server'
|
||||||
- 'client'
|
- 'client'
|
||||||
- 'client/plugins/fcc-source-challenges'
|
|
||||||
- 'client/plugins/gatsby-remark-node-identity'
|
|
||||||
- 'curriculum'
|
- 'curriculum'
|
||||||
- 'tools/challenge-editor/api'
|
- 'tools/challenge-editor/api'
|
||||||
- 'tools/challenge-editor/client'
|
- 'tools/challenge-editor/client'
|
||||||
- 'tools/challenge-helper-scripts'
|
- 'tools/challenge-helper-scripts'
|
||||||
- 'tools/challenge-parser'
|
- 'tools/challenge-parser'
|
||||||
|
- 'tools/client-plugins/*'
|
||||||
- 'tools/crowdin'
|
- 'tools/crowdin'
|
||||||
- 'tools/scripts/build'
|
- 'tools/scripts/build'
|
||||||
- 'tools/scripts/seed'
|
- 'tools/scripts/seed'
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const superBlockFolderMap = {
|
|||||||
'project-euler': '18-project-euler',
|
'project-euler': '18-project-euler',
|
||||||
'foundational-c-sharp-with-microsoft':
|
'foundational-c-sharp-with-microsoft':
|
||||||
'19-foundational-c-sharp-with-microsoft',
|
'19-foundational-c-sharp-with-microsoft',
|
||||||
|
'upcoming-python': '20-upcoming-python',
|
||||||
'example-certification': '99-example-certification'
|
'example-certification': '99-example-certification'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -66,5 +66,13 @@ export const superBlockList = [
|
|||||||
{
|
{
|
||||||
name: 'Project Euler',
|
name: 'Project Euler',
|
||||||
path: '18-project-euler'
|
path: '18-project-euler'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Foundational C# with Microsoft',
|
||||||
|
path: '19-foundational-c-sharp-with-microsoft'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Upcoming Python',
|
||||||
|
path: '20-upcoming-python'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"@testing-library/jest-dom": "5.17.0",
|
"@testing-library/jest-dom": "5.17.0",
|
||||||
"@testing-library/react": "12.1.5",
|
"@testing-library/react": "12.1.5",
|
||||||
"@testing-library/user-event": "13.5.0",
|
"@testing-library/user-event": "13.5.0",
|
||||||
"codemirror": "5",
|
"codemirror": "5.65.13",
|
||||||
"react": "16.14.0",
|
"react": "16.14.0",
|
||||||
"react-dom": "16.14.0",
|
"react-dom": "16.14.0",
|
||||||
"react-router-dom": "6.14.2",
|
"react-router-dom": "6.14.2",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function getSuperBlockSubPath(superBlock: SuperBlocks): string {
|
|||||||
[SuperBlocks.CollegeAlgebraPy]: '17-college-algebra-with-python',
|
[SuperBlocks.CollegeAlgebraPy]: '17-college-algebra-with-python',
|
||||||
[SuperBlocks.ProjectEuler]: '18-project-euler',
|
[SuperBlocks.ProjectEuler]: '18-project-euler',
|
||||||
[SuperBlocks.FoundationalCSharp]: '19-foundational-c-sharp-with-microsoft',
|
[SuperBlocks.FoundationalCSharp]: '19-foundational-c-sharp-with-microsoft',
|
||||||
|
[SuperBlocks.UpcomingPython]: '20-upcoming-python',
|
||||||
[SuperBlocks.ExampleCertification]: '99-example-certification'
|
[SuperBlocks.ExampleCertification]: '99-example-certification'
|
||||||
};
|
};
|
||||||
return pathMap[superBlock];
|
return pathMap[superBlock];
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ function defaultFile(lang, id) {
|
|||||||
function getFilenames(lang) {
|
function getFilenames(lang) {
|
||||||
const langToFilename = {
|
const langToFilename = {
|
||||||
js: 'script',
|
js: 'script',
|
||||||
css: 'styles'
|
css: 'styles',
|
||||||
|
py: 'main',
|
||||||
|
python: 'main'
|
||||||
};
|
};
|
||||||
return langToFilename[lang] ?? 'index';
|
return langToFilename[lang] ?? 'index';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,13 @@
|
|||||||
import jQuery from 'jquery';
|
import jQuery from 'jquery';
|
||||||
import * as helpers from '@freecodecamp/curriculum-helpers';
|
import * as helpers from '@freecodecamp/curriculum-helpers';
|
||||||
|
|
||||||
declare global {
|
import type { FrameDocument, FrameWindow, InitTestFrameArg } from '.';
|
||||||
interface Window {
|
|
||||||
$: JQueryStatic;
|
|
||||||
}
|
|
||||||
interface Document {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
__initTestFrame: (e: InitTestFrameArg) => Promise<void>;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
__runTest: (
|
|
||||||
testString: string
|
|
||||||
) => Promise<
|
|
||||||
{ pass: boolean } | { err: { message: string; stack?: string } }
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.$ = jQuery;
|
(window as FrameWindow).$ = jQuery;
|
||||||
|
|
||||||
document.__initTestFrame = initTestFrame;
|
const frameDocument = document as FrameDocument;
|
||||||
|
|
||||||
interface InitTestFrameArg {
|
frameDocument.__initTestFrame = initTestFrame;
|
||||||
code: {
|
|
||||||
contents?: string;
|
|
||||||
editableContents?: string;
|
|
||||||
original?: { [id: string]: string };
|
|
||||||
};
|
|
||||||
getUserInput?: (fileName: string) => string;
|
|
||||||
loadEnzyme?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initTestFrame(e: InitTestFrameArg = { code: {} }) {
|
async function initTestFrame(e: InitTestFrameArg = { code: {} }) {
|
||||||
const code = (e.code.contents || '').slice();
|
const code = (e.code.contents || '').slice();
|
||||||
@@ -44,12 +22,12 @@ async function initTestFrame(e: InitTestFrameArg = { code: {} }) {
|
|||||||
// __testEditable allows test authors to run tests against a transitory dom
|
// __testEditable allows test authors to run tests against a transitory dom
|
||||||
// element built using only the code in the editable region.
|
// element built using only the code in the editable region.
|
||||||
const __testEditable = (cb: () => () => unknown) => {
|
const __testEditable = (cb: () => () => unknown) => {
|
||||||
const div = document.createElement('div');
|
const div = frameDocument.createElement('div');
|
||||||
div.id = 'editable-only';
|
div.id = 'editable-only';
|
||||||
div.innerHTML = editableContents;
|
div.innerHTML = editableContents;
|
||||||
document.body.appendChild(div);
|
frameDocument.body.appendChild(div);
|
||||||
const out = cb();
|
const out = cb();
|
||||||
document.body.removeChild(div);
|
frameDocument.body.removeChild(div);
|
||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,7 +78,7 @@ async function initTestFrame(e: InitTestFrameArg = { code: {} }) {
|
|||||||
/* eslint-enable prefer-const */
|
/* eslint-enable prefer-const */
|
||||||
}
|
}
|
||||||
|
|
||||||
document.__runTest = async function runTests(testString: string) {
|
frameDocument.__runTest = async function runTests(testString: string) {
|
||||||
// uncomment the following line to inspect
|
// uncomment the following line to inspect
|
||||||
// the frame-runner as it runs tests
|
// the frame-runner as it runs tests
|
||||||
// make sure the dev tools console is open
|
// make sure the dev tools console is open
|
||||||
@@ -111,7 +89,7 @@ async function initTestFrame(e: InitTestFrameArg = { code: {} }) {
|
|||||||
// i.e. function() { assert(true, 'happy coding'); }
|
// i.e. function() { assert(true, 'happy coding'); }
|
||||||
const testPromise = new Promise((resolve, reject) =>
|
const testPromise = new Promise((resolve, reject) =>
|
||||||
// To avoid race conditions, we have to run the test in a final
|
// To avoid race conditions, we have to run the test in a final
|
||||||
// document ready:
|
// frameDocument ready:
|
||||||
$(() => {
|
$(() => {
|
||||||
try {
|
try {
|
||||||
const test: unknown = eval(testString);
|
const test: unknown = eval(testString);
|
||||||
33
tools/client-plugins/browser-scripts/index.d.ts
vendored
Normal file
33
tools/client-plugins/browser-scripts/index.d.ts
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
|
import { PyodideInterface } from 'pyodide';
|
||||||
|
|
||||||
|
export interface FrameDocument extends Document {
|
||||||
|
__initTestFrame: (e: InitTestFrameArg) => Promise<void>;
|
||||||
|
__runTest: (
|
||||||
|
testString: string
|
||||||
|
) => Promise<
|
||||||
|
{ pass: boolean } | { err: { message: string; stack?: string } }
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PythonDocument extends FrameDocument {
|
||||||
|
__initPythonFrame: () => Promise<void>;
|
||||||
|
__runPython: (code: string) => Promise<PyodideInterface>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InitTestFrameArg {
|
||||||
|
code: {
|
||||||
|
contents?: string;
|
||||||
|
editableContents?: string;
|
||||||
|
original?: { [id: string]: string };
|
||||||
|
};
|
||||||
|
getUserInput?: (fileName: string) => string;
|
||||||
|
loadEnzyme?: () => void;
|
||||||
|
transformedPython?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FrameWindow = Window &
|
||||||
|
typeof globalThis & {
|
||||||
|
$: typeof $;
|
||||||
|
};
|
||||||
60
tools/client-plugins/browser-scripts/package.json
Normal file
60
tools/client-plugins/browser-scripts/package.json
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"name": "@freecodecamp/browser-scripts",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "The freeCodeCamp.org open-source codebase and curriculum",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"private": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18",
|
||||||
|
"pnpm": "8"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/freeCodeCamp/freeCodeCamp.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/freeCodeCamp/freeCodeCamp/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
|
||||||
|
"author": "freeCodeCamp <team@freecodecamp.org>",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "cross-env NODE_OPTIONS=\"--max-old-space-size=7168\" webpack -c webpack.config.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||||
|
"@babel/plugin-transform-runtime": "7.19.6",
|
||||||
|
"@babel/preset-env": "7.22.5",
|
||||||
|
"@babel/preset-typescript": "7.22.5",
|
||||||
|
"@freecodecamp/curriculum-helpers": "1.1.0",
|
||||||
|
"@types/chai": "4.3.4",
|
||||||
|
"@types/copy-webpack-plugin": "^8.0.1",
|
||||||
|
"@types/enzyme": "3.10.12",
|
||||||
|
"@types/enzyme-adapter-react-16": "1.0.6",
|
||||||
|
"@types/jquery": "3.5.16",
|
||||||
|
"@types/lodash-es": "4.17.6",
|
||||||
|
"babel-loader": "8.3.0",
|
||||||
|
"buffer": "6.0.3",
|
||||||
|
"chai": "4.3.7",
|
||||||
|
"copy-webpack-plugin": "9.1.0",
|
||||||
|
"css-loader": "^6.8.1",
|
||||||
|
"enzyme": "3.11.0",
|
||||||
|
"enzyme-adapter-react-16": "1.15.7",
|
||||||
|
"jquery": "3.6.4",
|
||||||
|
"lodash-es": "4.17.21",
|
||||||
|
"process": "0.11.10",
|
||||||
|
"pyodide": "^0.23.3",
|
||||||
|
"sass.js": "0.11.1",
|
||||||
|
"style-loader": "^3.3.3",
|
||||||
|
"util": "0.12.5",
|
||||||
|
"webpack": "5.76.2",
|
||||||
|
"webpack-cli": "4.10.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.20.13",
|
||||||
|
"xterm": "^5.2.1",
|
||||||
|
"xterm-addon-fit": "^0.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
280
tools/client-plugins/browser-scripts/python-runner.ts
Normal file
280
tools/client-plugins/browser-scripts/python-runner.ts
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
// We have to specify pyodide.js because we need to import that file (not .mjs)
|
||||||
|
// and 'import' defaults to .mjs
|
||||||
|
import { loadPyodide, type PyodideInterface } from 'pyodide/pyodide.js';
|
||||||
|
import pkg from 'pyodide/package.json';
|
||||||
|
import { IDisposable, Terminal } from 'xterm';
|
||||||
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
|
import jQuery from 'jquery'; // TODO: is jQuery needed for the python runner?
|
||||||
|
import * as helpers from '@freecodecamp/curriculum-helpers';
|
||||||
|
|
||||||
|
import type { PythonDocument, FrameWindow, InitTestFrameArg } from '.';
|
||||||
|
|
||||||
|
import 'xterm/css/xterm.css';
|
||||||
|
|
||||||
|
(window as FrameWindow).$ = jQuery;
|
||||||
|
|
||||||
|
// This will be running in an iframe, so document will be
|
||||||
|
// element.contentDocument. This declaration is just to add properties we know
|
||||||
|
// exist on this document (but not on the parent)
|
||||||
|
const contentDocument = document as PythonDocument;
|
||||||
|
|
||||||
|
function createTerminal(disposables: IDisposable[]) {
|
||||||
|
const terminalContainer = document.getElementById('terminal');
|
||||||
|
if (!terminalContainer) throw Error('Could not find terminal container');
|
||||||
|
|
||||||
|
// Setting convertEol so that \n is converted to \r\n. Otherwise the terminal
|
||||||
|
// will interpret \n as line feed and just move the cursor to the next line.
|
||||||
|
// convertEol makes every \n a \r\n.
|
||||||
|
const term = new Terminal({ convertEol: true });
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
term.loadAddon(fitAddon);
|
||||||
|
term.open(terminalContainer);
|
||||||
|
fitAddon.fit();
|
||||||
|
|
||||||
|
const resetTerminal = () => {
|
||||||
|
term.reset();
|
||||||
|
disposables.forEach(disposable => disposable.dispose());
|
||||||
|
disposables.length = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { term, resetTerminal };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupPyodide() {
|
||||||
|
// I tried setting jsglobals here, to provide 'input' and 'print' to python,
|
||||||
|
// without having to modify the global window object. However, it didn't work
|
||||||
|
// because pyodide needs access to that object. Instead, I used
|
||||||
|
// registerJsModule when setting up runPython.
|
||||||
|
return await loadPyodide({
|
||||||
|
indexURL: `https://cdn.jsdelivr.net/pyodide/v${pkg.version}/full/`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type Input = (text: string) => Promise<string>;
|
||||||
|
type Print = (...args: unknown[]) => void;
|
||||||
|
type ResetTerminal = () => void;
|
||||||
|
|
||||||
|
function createJSFunctionsForPython(
|
||||||
|
term: Terminal,
|
||||||
|
disposables: IDisposable[],
|
||||||
|
pyodide: PyodideInterface
|
||||||
|
) {
|
||||||
|
const writeLine = (text: string) => term.writeln(`>>> ${text}`);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
|
const str = pyodide.globals.get('str') as (x: unknown) => string;
|
||||||
|
function print(...args: unknown[]) {
|
||||||
|
const text = args.map(x => str(x)).join(' ');
|
||||||
|
writeLine(text);
|
||||||
|
}
|
||||||
|
// TODO: prevent user from moving cursor outside the current input line and
|
||||||
|
// handle insertion and deletion properly. While backspace and delete don't
|
||||||
|
// seem to work, we can use "\x1b[0K" to clear from the cursor to the end.
|
||||||
|
// Also, we should not add special characters to the userinput string.
|
||||||
|
const waitForInput = (): Promise<string> =>
|
||||||
|
new Promise(resolve => {
|
||||||
|
let userinput = '';
|
||||||
|
// Eslint is correct that this only gets assigned once, but we can't use
|
||||||
|
// const because the declaration (before keyListener is defined) and
|
||||||
|
// assignment (after keyListener is defined) must be separate.
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
let disposable: IDisposable | undefined;
|
||||||
|
|
||||||
|
const done = () => {
|
||||||
|
disposable?.dispose();
|
||||||
|
resolve(userinput);
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyListener = (key: string) => {
|
||||||
|
if (key === '\u007F' || key === '\b') {
|
||||||
|
// Backspace or delete key
|
||||||
|
term.write('\b \b'); // Move cursor back, replace character with space, then move cursor back again
|
||||||
|
userinput = userinput.slice(0, -1); // Remove the last character from userinput
|
||||||
|
}
|
||||||
|
if (key == '\r') {
|
||||||
|
term.write('\r\n');
|
||||||
|
done();
|
||||||
|
} else {
|
||||||
|
userinput += key;
|
||||||
|
term.write(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
disposable = term.onData(keyListener); // Listen for key events and store the disposable
|
||||||
|
disposables.push(disposable);
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = async (text: string) => {
|
||||||
|
writeLine(text);
|
||||||
|
return await waitForInput();
|
||||||
|
};
|
||||||
|
|
||||||
|
return { print, input };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupRunPython(
|
||||||
|
pyodide: PyodideInterface,
|
||||||
|
{
|
||||||
|
input,
|
||||||
|
print,
|
||||||
|
resetTerminal
|
||||||
|
}: { input: Input; print: Print; resetTerminal: ResetTerminal }
|
||||||
|
) {
|
||||||
|
// Make print and input available to python
|
||||||
|
pyodide.registerJsModule('jscustom', {
|
||||||
|
input,
|
||||||
|
print
|
||||||
|
});
|
||||||
|
pyodide.runPython(`
|
||||||
|
import jscustom
|
||||||
|
from jscustom import print
|
||||||
|
from jscustom import input
|
||||||
|
`);
|
||||||
|
|
||||||
|
async function runPython(code: string) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
|
pyodide.globals.get('__cancel')?.();
|
||||||
|
resetTerminal();
|
||||||
|
|
||||||
|
// There's no need to clear out globals between runs, because the user's
|
||||||
|
// code is always run in a coroutine and shouldn't pollute them. If we
|
||||||
|
// subsequently want to run code that does interact with globals, we can
|
||||||
|
// revisit this.
|
||||||
|
await pyodide.runPythonAsync(code);
|
||||||
|
return pyodide;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentDocument.__runPython = runPython;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initPythonFrame() {
|
||||||
|
const disposables: IDisposable[] = [];
|
||||||
|
const { term, resetTerminal } = createTerminal(disposables);
|
||||||
|
const pyodide = await setupPyodide();
|
||||||
|
const { print, input } = createJSFunctionsForPython(
|
||||||
|
term,
|
||||||
|
disposables,
|
||||||
|
pyodide
|
||||||
|
);
|
||||||
|
setupRunPython(pyodide, { input, print, resetTerminal });
|
||||||
|
}
|
||||||
|
|
||||||
|
contentDocument.__initPythonFrame = initPythonFrame;
|
||||||
|
contentDocument.__initTestFrame = initTestFrame;
|
||||||
|
|
||||||
|
// TODO: DRY this and frame-runner.ts's initTestFrame
|
||||||
|
async function initTestFrame(e: InitTestFrameArg) {
|
||||||
|
const pyodide = await setupPyodide();
|
||||||
|
|
||||||
|
// transformedPython is used here not because it's necessary (it's not since
|
||||||
|
// the transformation converts `input` into `await input` and the tests
|
||||||
|
// provide a synchronous `input` function), but because we want to run the
|
||||||
|
// tests against exactly the same code that runs in the preview.
|
||||||
|
const code = (e.transformedPython || '').slice();
|
||||||
|
const __file = (id?: string) => {
|
||||||
|
if (id && e.code.original) {
|
||||||
|
return e.code.original[id];
|
||||||
|
} else {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!e.getUserInput) {
|
||||||
|
e.getUserInput = () => code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
// Fake Deep Equal dependency
|
||||||
|
const DeepEqual = (a: Record<string, unknown>, b: Record<string, unknown>) =>
|
||||||
|
JSON.stringify(a) === JSON.stringify(b);
|
||||||
|
|
||||||
|
// Hardcode Deep Freeze dependency
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const DeepFreeze = (o: Record<string, any>) => {
|
||||||
|
Object.freeze(o);
|
||||||
|
Object.getOwnPropertyNames(o).forEach(function (prop) {
|
||||||
|
if (
|
||||||
|
Object.prototype.hasOwnProperty.call(o, prop) &&
|
||||||
|
o[prop] !== null &&
|
||||||
|
(typeof o[prop] === 'object' || typeof o[prop] === 'function') &&
|
||||||
|
!Object.isFrozen(o[prop])
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
DeepFreeze(o[prop]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return o;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { default: chai } = await import(/* webpackChunkName: "chai" */ 'chai');
|
||||||
|
const assert = chai.assert;
|
||||||
|
const __helpers = helpers;
|
||||||
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
|
|
||||||
|
contentDocument.__runTest = async function runTests(testString: string) {
|
||||||
|
// uncomment the following line to inspect
|
||||||
|
// the frame-runner as it runs tests
|
||||||
|
// make sure the dev tools console is open
|
||||||
|
// debugger;
|
||||||
|
try {
|
||||||
|
// eval test string to get the dummy input and actual test
|
||||||
|
const { input, test } = await new Promise<{
|
||||||
|
input: string[];
|
||||||
|
test: () => Promise<unknown>;
|
||||||
|
}>((resolve, reject) =>
|
||||||
|
// To avoid race conditions, we have to run the test in a final
|
||||||
|
// frameDocument ready:
|
||||||
|
$(() => {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
const test: { input: string[]; test: () => Promise<unknown> } =
|
||||||
|
eval(testString);
|
||||||
|
resolve(test);
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// TODO: throw helpful error if we run out of input values, since it's likely
|
||||||
|
// that the user added too many input statements.
|
||||||
|
const inputIterator = input ? input.values() : null;
|
||||||
|
setupRunPython(pyodide, {
|
||||||
|
input: () => {
|
||||||
|
return Promise.resolve(
|
||||||
|
inputIterator ? inputIterator.next().value : ''
|
||||||
|
);
|
||||||
|
},
|
||||||
|
// We don't, currently, care what print is called with, but it does need
|
||||||
|
// to exist
|
||||||
|
print: () => void 0,
|
||||||
|
// resetTerminal is only necessary when calling __runPython more than
|
||||||
|
// once, which we don't do in the test frame
|
||||||
|
resetTerminal: () => void 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make __pyodide available to the test code
|
||||||
|
const __pyodide: PyodideInterface = await this.__runPython(code);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
||||||
|
const __userGlobals = __pyodide.globals.get('__locals');
|
||||||
|
await test();
|
||||||
|
|
||||||
|
return { pass: true };
|
||||||
|
} catch (err) {
|
||||||
|
if (!(err instanceof chai.AssertionError)) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
// to provide useful debugging information when debugging the tests, we
|
||||||
|
// have to extract the message, stack and, if they exist, expected and
|
||||||
|
// actual before returning
|
||||||
|
return {
|
||||||
|
err: {
|
||||||
|
message: (err as Error).message,
|
||||||
|
stack: (err as Error).stack,
|
||||||
|
expected: (err as { expected?: string }).expected,
|
||||||
|
actual: (err as { actual?: string }).actual
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import chai from 'chai';
|
import chai from 'chai';
|
||||||
import { toString as __toString } from 'lodash-es';
|
import { toString as __toString } from 'lodash-es';
|
||||||
import * as helpers from '@freecodecamp/curriculum-helpers';
|
import * as helpers from '@freecodecamp/curriculum-helpers';
|
||||||
import { format as __format } from '../../utils/format';
|
import { format as __format } from './utils/format';
|
||||||
|
|
||||||
const ctx: Worker & typeof globalThis = self as unknown as Worker &
|
const ctx: Worker & typeof globalThis = self as unknown as Worker &
|
||||||
typeof globalThis;
|
typeof globalThis;
|
||||||
12
tools/client-plugins/browser-scripts/tsconfig.json
Normal file
12
tools/client-plugins/browser-scripts/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2022",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"allowJs": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
||||||
22
tools/client-plugins/browser-scripts/utils/format.js
Normal file
22
tools/client-plugins/browser-scripts/utils/format.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// TODO: this is a straight up copy of the format function from the client.
|
||||||
|
// Figure out a way to share it.
|
||||||
|
|
||||||
|
import { inspect } from 'util/util';
|
||||||
|
|
||||||
|
export function format(x) {
|
||||||
|
// we're trying to mimic console.log, so we avoid wrapping strings in quotes:
|
||||||
|
if (typeof x === 'string') return x;
|
||||||
|
else if (x instanceof Set) {
|
||||||
|
return `Set(${x.size}) {${Array.from(x).join(', ')}}`;
|
||||||
|
} else if (x instanceof Map) {
|
||||||
|
return `Map(${x.size}) {${Array.from(
|
||||||
|
x.entries(),
|
||||||
|
([k, v]) => `${k} => ${v}`
|
||||||
|
).join(', ')}})`;
|
||||||
|
} else if (typeof x === 'bigint') {
|
||||||
|
return x.toString() + 'n';
|
||||||
|
} else if (typeof x === 'symbol') {
|
||||||
|
return x.toString();
|
||||||
|
}
|
||||||
|
return inspect(x);
|
||||||
|
}
|
||||||
@@ -5,15 +5,16 @@ const webpack = require('webpack');
|
|||||||
|
|
||||||
module.exports = (env = {}) => {
|
module.exports = (env = {}) => {
|
||||||
const __DEV__ = env.production !== true;
|
const __DEV__ = env.production !== true;
|
||||||
const staticPath = path.join(__dirname, './static/js');
|
const staticPath = path.join(__dirname, '../../../client/static/js');
|
||||||
const configPath = path.join(__dirname, '../config/client');
|
const configPath = path.join(__dirname, '../../../config/client');
|
||||||
return {
|
return {
|
||||||
cache: __DEV__ ? { type: 'filesystem' } : false,
|
cache: __DEV__ ? { type: 'filesystem' } : false,
|
||||||
mode: __DEV__ ? 'development' : 'production',
|
mode: __DEV__ ? 'development' : 'production',
|
||||||
entry: {
|
entry: {
|
||||||
'frame-runner': './src/client/frame-runner.ts',
|
'frame-runner': './frame-runner.ts',
|
||||||
'sass-compile': './src/client/workers/sass-compile.ts',
|
'sass-compile': './sass-compile.ts',
|
||||||
'test-evaluator': './src/client/workers/test-evaluator.ts'
|
'test-evaluator': './test-evaluator.ts',
|
||||||
|
'python-runner': './python-runner.ts'
|
||||||
},
|
},
|
||||||
devtool: __DEV__ ? 'inline-source-map' : 'source-map',
|
devtool: __DEV__ ? 'inline-source-map' : 'source-map',
|
||||||
output: {
|
output: {
|
||||||
@@ -57,6 +58,11 @@ module.exports = (env = {}) => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
// xterm doesn't bundle its css, so we need to load it ourselves
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: ['style-loader', 'css-loader']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -76,7 +82,7 @@ module.exports = (env = {}) => {
|
|||||||
buffer: require.resolve('buffer'),
|
buffer: require.resolve('buffer'),
|
||||||
util: false,
|
util: false,
|
||||||
stream: false,
|
stream: false,
|
||||||
process: require.resolve('process/browser')
|
process: require.resolve('process/browser.js')
|
||||||
},
|
},
|
||||||
extensions: ['.js', '.ts']
|
extensions: ['.js', '.ts']
|
||||||
}
|
}
|
||||||
@@ -4,12 +4,12 @@ exports.onCreateNode = function remarkNodeIdentityOnCreateNode(
|
|||||||
) {
|
) {
|
||||||
if (typeof predicate !== 'function') {
|
if (typeof predicate !== 'function') {
|
||||||
reporter.panic(
|
reporter.panic(
|
||||||
'Please supply a predicate function to `gatsby-plugin-identity`'
|
'Please supply a predicate function to `gatsby-remark-node-identity`'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (typeof identity !== 'string' || identity.length === 0) {
|
if (typeof identity !== 'string' || identity.length === 0) {
|
||||||
reporter.panic(
|
reporter.panic(
|
||||||
'`gatsby-plugin-identity` requires an identify string to add to nodes ' +
|
'`gatsby-remark-node-identity` requires an identify string to add to nodes ' +
|
||||||
'that match the predicate'
|
'that match the predicate'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@freecodecamp/gatsby-plugin-node-identity",
|
"name": "@freecodecamp/gatsby-remark-node-identity",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "The freeCodeCamp.org open-source codebase and curriculum",
|
"description": "The freeCodeCamp.org open-source codebase and curriculum",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
@@ -17,5 +17,5 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
|
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
|
||||||
"author": "freeCodeCamp <team@freecodecamp.org>",
|
"author": "freeCodeCamp <team@freecodecamp.org>",
|
||||||
"main": "none"
|
"main": "gatsby-node.js"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@freecodecamp/fcc-source-challenges",
|
"name": "@freecodecamp/gatsby-source-challenges",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "The freeCodeCamp.org open-source codebase and curriculum",
|
"description": "The freeCodeCamp.org open-source codebase and curriculum",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
|
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
|
||||||
"author": "freeCodeCamp <team@freecodecamp.org>",
|
"author": "freeCodeCamp <team@freecodecamp.org>",
|
||||||
"main": "none",
|
"main": "gatsby-node.js",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chokidar": "3.5.3",
|
"chokidar": "3.5.3",
|
||||||
"readdirp": "3.6.0"
|
"readdirp": "3.6.0"
|
||||||
@@ -92,6 +92,7 @@ if (envData.clientLocale == 'english' && !envData.showUpcomingChanges) {
|
|||||||
'college-algebra-with-python',
|
'college-algebra-with-python',
|
||||||
'the-odin-project',
|
'the-odin-project',
|
||||||
'foundational-c-sharp-with-microsoft',
|
'foundational-c-sharp-with-microsoft',
|
||||||
|
'upcoming-python',
|
||||||
'example-certification'
|
'example-certification'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user