diff --git a/client/package.json b/client/package.json
index 2bb60e985f0..520c4d6f102 100644
--- a/client/package.json
+++ b/client/package.json
@@ -136,7 +136,9 @@
"typescript": "5.2.2",
"util": "0.12.5",
"uuid": "8.3.2",
- "validator": "13.11.0"
+ "validator": "13.11.0",
+ "xterm": "^5.2.1",
+ "xterm-addon-fit": "^0.8.0"
},
"devDependencies": {
"@babel/plugin-syntax-dynamic-import": "7.8.3",
diff --git a/client/serve/serve.json b/client/serve/serve.json
index 66c25ffdb32..51935a93b9d 100644
--- a/client/serve/serve.json
+++ b/client/serve/serve.json
@@ -20,7 +20,7 @@
]
},
{
- "source": "{misc/*.js,sw.js}",
+ "source": "{misc/*.js,sw.js,python-input-sw.js}",
"headers": [
{
"key": "Cache-Control",
diff --git a/client/src/templates/Challenges/classic/desktop-layout.tsx b/client/src/templates/Challenges/classic/desktop-layout.tsx
index ad38207c31b..82ddedc9485 100644
--- a/client/src/templates/Challenges/classic/desktop-layout.tsx
+++ b/client/src/templates/Challenges/classic/desktop-layout.tsx
@@ -45,6 +45,7 @@ interface DesktopLayoutProps {
testsPane: Pane;
};
notes: ReactElement;
+ onPreviewResize: () => void;
preview: ReactElement;
resizeProps: ResizeProps;
testOutput: ReactElement;
@@ -149,6 +150,7 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
isFirstStep,
layoutState,
notes,
+ onPreviewResize,
preview,
hasEditableBoundaries,
windowTitle
@@ -285,7 +287,9 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
)}
{displayPreviewPortal && (
- {preview}
+
+ {preview}
+
)}
);
diff --git a/client/src/templates/Challenges/classic/mobile-layout.tsx b/client/src/templates/Challenges/classic/mobile-layout.tsx
index ec8a679ede7..b2a4af0655a 100644
--- a/client/src/templates/Challenges/classic/mobile-layout.tsx
+++ b/client/src/templates/Challenges/classic/mobile-layout.tsx
@@ -30,6 +30,7 @@ interface MobileLayoutProps {
instructions: JSX.Element;
notes: ReactElement;
preview: JSX.Element;
+ onPreviewResize: () => void;
windowTitle: string;
showPreviewPortal: boolean;
showPreviewPane: boolean;
@@ -157,6 +158,7 @@ class MobileLayout extends Component {
hasPreview,
notes,
preview,
+ onPreviewResize,
showPreviewPane,
showPreviewPortal,
removePortalWindow,
@@ -328,7 +330,9 @@ class MobileLayout extends Component {
)}
{displayPreviewPortal && (
- {preview}
+
+ {preview}
+
)}
>
);
diff --git a/client/src/templates/Challenges/classic/show.tsx b/client/src/templates/Challenges/classic/show.tsx
index e831ff40bd8..1c9776867d2 100644
--- a/client/src/templates/Challenges/classic/show.tsx
+++ b/client/src/templates/Challenges/classic/show.tsx
@@ -9,6 +9,8 @@ import { bindActionCreators, Dispatch } from 'redux';
import { createStructuredSelector } from 'reselect';
import store from 'store';
import { editor } from 'monaco-editor';
+import type { FitAddon } from 'xterm-addon-fit';
+
import { challengeTypes } from '../../../../../shared/config/challenge-types';
import LearnLayout from '../../../components/layouts/learn';
import { MAX_MOBILE_WIDTH } from '../../../../config/misc';
@@ -56,6 +58,7 @@ import {
} from '../redux/selectors';
import { savedChallengesSelector } from '../../../redux/selectors';
import { getGuideUrl } from '../utils';
+import { XtermTerminal } from './xterm';
import MultifileEditor from './multifile-editor';
import DesktopLayout from './desktop-layout';
import MobileLayout from './mobile-layout';
@@ -148,9 +151,16 @@ const handleContentWidgetEvents = (e: MouseEvent | TouchEvent): void => {
const StepPreview = ({
disableIframe,
- previewMounted
-}: Pick) => {
- return (
+ previewMounted,
+ challengeType,
+ xtermFitRef
+}: Pick & {
+ challengeType: number;
+ xtermFitRef: React.MutableRefObject;
+}) => {
+ return challengeType === challengeTypes.python ? (
+
+ ) : (
(null);
const editorRef = useRef();
const instructionsPanelRef = useRef(null);
+ const xtermFitRef = useRef(null);
const isMobile = useMediaQuery({
query: `(max-width: ${MAX_MOBILE_WIDTH}px)`
});
@@ -248,6 +259,8 @@ function ShowClassic({
return isValidLayout ? reflexLayout : BASE_LAYOUT;
};
+ const onPreviewResize = () => xtermFitRef.current?.fit();
+
// layout: Holds the information of the panes sizes for desktop view
const [layout, setLayout] = useState(getLayoutState());
@@ -438,10 +451,13 @@ function ShowClassic({
showToolPanel: false
})}
notes={}
+ onPreviewResize={onPreviewResize}
preview={
}
windowTitle={windowTitle}
@@ -470,10 +486,13 @@ function ShowClassic({
isFirstStep={isFirstStep}
layoutState={layout}
notes={}
+ onPreviewResize={onPreviewResize}
preview={
}
resizeProps={resizeProps}
diff --git a/client/src/templates/Challenges/classic/xterm.tsx b/client/src/templates/Challenges/classic/xterm.tsx
new file mode 100644
index 00000000000..b33ec8fa53c
--- /dev/null
+++ b/client/src/templates/Challenges/classic/xterm.tsx
@@ -0,0 +1,102 @@
+import React, { MutableRefObject, useEffect, useRef } from 'react';
+import type { IDisposable, Terminal } from 'xterm';
+import type { FitAddon } from 'xterm-addon-fit';
+
+import { registerTerminal } from '../utils/python-worker-handler';
+
+const registerServiceWorker = async () => {
+ if ('serviceWorker' in navigator) {
+ try {
+ await navigator.serviceWorker.register('/python-input-sw.js');
+ } catch (error) {
+ console.error(`Registration failed`);
+ console.error(error);
+ }
+ }
+};
+
+export const XtermTerminal = ({
+ xtermFitRef
+}: {
+ xtermFitRef: MutableRefObject;
+}) => {
+ const termContainerRef = useRef(null);
+
+ useEffect(() => {
+ void registerServiceWorker();
+
+ let term: Terminal | null;
+
+ async function createTerminal() {
+ const disposables: IDisposable[] = [];
+ const { Terminal } = await import('xterm');
+ const { FitAddon } = await import('xterm-addon-fit');
+ // 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.
+ term = new Terminal({ convertEol: true });
+ const fitAddon = new FitAddon();
+ xtermFitRef.current = fitAddon;
+ term.loadAddon(fitAddon);
+ if (termContainerRef.current) term.open(termContainerRef.current);
+ fitAddon.fit();
+
+ const print = (text: string) => term?.writeln(`>>> ${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 input = (text: string) => {
+ print(text);
+ 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();
+ navigator.serviceWorker.controller?.postMessage(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
+ if (disposable) disposables.push(disposable);
+ };
+ const reset = () => {
+ term?.reset();
+ disposables.forEach(disposable => disposable.dispose());
+ disposables.length = 0;
+ };
+ registerTerminal({ print, input }, reset);
+ }
+
+ void createTerminal();
+
+ return () => {
+ term?.dispose();
+ };
+ }, [xtermFitRef]);
+
+ return (
+
+
+
+ );
+};
diff --git a/client/src/templates/Challenges/components/preview-portal.tsx b/client/src/templates/Challenges/components/preview-portal.tsx
index 208fd6a0b41..32a7efa7b18 100644
--- a/client/src/templates/Challenges/components/preview-portal.tsx
+++ b/client/src/templates/Challenges/components/preview-portal.tsx
@@ -34,6 +34,7 @@ interface PreviewPortalProps {
isAdvancing: boolean;
setChapterSlug: (arg: string) => void;
chapterSlug: string;
+ onResize: () => void;
}
const mapDispatchToProps = {
@@ -136,6 +137,10 @@ class PreviewPortal extends Component {
this.props.removePortalWindow();
});
+ this.externalWindow?.addEventListener('resize', () => {
+ this.props.onResize();
+ });
+
this.props.storePortalWindow(this.externalWindow);
// close the portal if the main window closes
diff --git a/client/src/templates/Challenges/rechallenge/transform-python.js b/client/src/templates/Challenges/rechallenge/transform-python.js
deleted file mode 100644
index abbcd162411..00000000000
--- a/client/src/templates/Challenges/rechallenge/transform-python.js
+++ /dev/null
@@ -1,120 +0,0 @@
-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');
-}
diff --git a/client/src/templates/Challenges/rechallenge/transform-python.test.ts b/client/src/templates/Challenges/rechallenge/transform-python.test.ts
deleted file mode 100644
index 515874c9bd2..00000000000
--- a/client/src/templates/Challenges/rechallenge/transform-python.test.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/* 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);
- });
- });
-});
diff --git a/client/src/templates/Challenges/rechallenge/transformers.js b/client/src/templates/Challenges/rechallenge/transformers.js
index f368311b187..2beebf9161a 100644
--- a/client/src/templates/Challenges/rechallenge/transformers.js
+++ b/client/src/templates/Challenges/rechallenge/transformers.js
@@ -19,7 +19,6 @@ import {
compileHeadTail
} from '../../../../../shared/utils/polyvinyl';
import createWorker from '../utils/worker-executor';
-import { makeCancellable, makeInputAwaitable } from './transform-python';
const { filename: sassCompile } = sassData;
@@ -100,7 +99,6 @@ const NBSPReg = new RegExp(String.fromCharCode(160), 'g');
const testJS = matchesProperty('ext', 'js');
const testJSX = matchesProperty('ext', 'jsx');
const testHTML = matchesProperty('ext', 'html');
-const testPython = matchesProperty('ext', 'py');
const testHTML$JS$JSX = overSome(testHTML, testJS, testJSX);
const replaceNBSP = cond([
@@ -306,17 +304,6 @@ const htmlTransformer = cond([
[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 => [
replaceNBSP,
babelTransformer(loopProtectOptions),
@@ -326,6 +313,5 @@ export const getTransformers = loopProtectOptions => [
export const getPythonTransformers = () => [
replaceNBSP,
- partial(compileHeadTail, ''),
- pythonTransformer
+ partial(compileHeadTail, '')
];
diff --git a/client/src/templates/Challenges/redux/execute-challenge-saga.js b/client/src/templates/Challenges/redux/execute-challenge-saga.js
index 608f791df21..81532ad1085 100644
--- a/client/src/templates/Challenges/redux/execute-challenge-saga.js
+++ b/client/src/templates/Challenges/redux/execute-challenge-saga.js
@@ -33,7 +33,10 @@ import {
updatePreview,
updateProjectPreview
} from '../utils/build';
-import { runPythonInFrame, mainPreviewId } from '../utils/frame';
+import {
+ getPythonWorker,
+ resetPythonWorker
+} from '../utils/python-worker-handler';
import { executeGA } from '../../../redux/actions';
import { fireConfetti } from '../../../utils/fire-confetti';
import { actionTypes } from './action-types';
@@ -264,15 +267,13 @@ function* previewChallengeSaga({ flushLogs = true } = {}) {
const portalDocument = yield select(portalDocumentSelector);
const finalDocument = portalDocument || document;
- 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.
+ // Python challenges do not use the preview frame, they use a web worker
+ // to run the code. The UI is handled by the xterm component, so there
+ // is no need to update the preview frame.
if (challengeData.challengeType === challengeTypes.python) {
yield updatePython(challengeData);
+ } else {
+ yield call(updatePreview, buildData, finalDocument, proxyLogger);
}
} else if (isJavaScriptChallenge(challengeData)) {
const runUserCode = getTestRunner(buildData, {
@@ -306,19 +307,19 @@ function* updatePreviewSaga() {
}
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;
+ resetPythonWorker();
+ const worker = getPythonWorker();
+ const code = {
+ contents: buildData.sources.index,
+ editableContents: buildData.sources.editableContents,
+ original: buildData.sources.original
+ };
+ worker.postMessage({ code });
// 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 }) {
diff --git a/client/src/templates/Challenges/utils/build.ts b/client/src/templates/Challenges/utils/build.ts
index 97d4c276f63..7db9d5aa89b 100644
--- a/client/src/templates/Challenges/utils/build.ts
+++ b/client/src/templates/Challenges/utils/build.ts
@@ -1,13 +1,13 @@
import { challengeTypes } from '../../../../../shared/config/challenge-types';
import frameRunnerData from '../../../../../client/config/browser-scripts/frame-runner.json';
-import testEvaluatorData from '../../../../../client/config/browser-scripts/test-evaluator.json';
-import pythonRunnerData from '../../../../../client/config/browser-scripts/python-runner.json';
+import jsTestEvaluatorData from '../../../../../client/config/browser-scripts/test-evaluator.json';
+import pyTestEvaluatorData from '../../../../../client/config/browser-scripts/python-test-evaluator.json';
import {
ChallengeFile as PropTypesChallengeFile,
ChallengeMeta
} from '../../../redux/prop-types';
-import { concatHtml, createPythonTerminal } from '../rechallenge/builders';
+import { concatHtml } from '../rechallenge/builders';
import {
getTransformers,
embedFilesInHtml,
@@ -48,12 +48,16 @@ interface BuildOptions {
usesTestRunner?: boolean;
}
-const { filename: testEvaluator } = testEvaluatorData;
+interface WorkerConfig {
+ terminateWorker: boolean;
+ testEvaluator: string;
+}
+
+const { filename: jsTestEvaluator } = jsTestEvaluatorData;
+const { filename: pyTestEvaluator } = pyTestEvaluatorData;
const frameRunnerSrc = `/js/${frameRunnerData.filename}.js`;
-const pythonRunnerSrc = `/js/${pythonRunnerData.filename}.js`;
-
type ApplyFunctionProps = (file: ChallengeFile) => Promise;
const applyFunction =
@@ -143,6 +147,7 @@ const testRunners = {
[challengeTypes.html]: getDOMTestRunner,
[challengeTypes.backend]: getDOMTestRunner,
[challengeTypes.pythonProject]: getDOMTestRunner,
+ [challengeTypes.python]: getPyTestRunner,
[challengeTypes.multifileCertProject]: getDOMTestRunner
};
// TODO: Figure out and (hopefully) simplify the return type.
@@ -163,13 +168,37 @@ export function getTestRunner(
function getJSTestRunner(
{ build, sources }: BuildChallengeData,
{ proxyLogger, removeComments }: TestRunnerConfig
+) {
+ return getWorkerTestRunner(
+ { build, sources },
+ { proxyLogger, removeComments },
+ { testEvaluator: jsTestEvaluator, terminateWorker: true }
+ );
+}
+
+function getPyTestRunner(
+ { build, sources }: BuildChallengeData,
+ { proxyLogger, removeComments }: TestRunnerConfig
+) {
+ return getWorkerTestRunner(
+ { build, sources },
+ { proxyLogger, removeComments },
+ { testEvaluator: pyTestEvaluator, terminateWorker: false }
+ );
+}
+
+function getWorkerTestRunner(
+ { build, sources }: Pick,
+ { proxyLogger, removeComments }: TestRunnerConfig,
+ { testEvaluator, terminateWorker }: WorkerConfig
) {
const code = {
contents: sources.index,
- editableContents: sources.editableContents
+ editableContents: sources.editableContents,
+ original: sources.original
};
- const testWorker = createWorker(testEvaluator, { terminateWorker: true });
+ const testWorker = createWorker(testEvaluator, { terminateWorker });
type CreateWorker = ReturnType;
@@ -203,7 +232,7 @@ async function getDOMTestRunner(
type BuildResult = {
challengeType: number;
- build: string;
+ build?: string;
sources: Source | undefined;
};
@@ -282,10 +311,6 @@ function buildBackendChallenge({ url }: BuildChallengeData) {
};
}
-function getTransformedPython(challengeFiles: ChallengeFiles) {
- return challengeFiles[0].contents;
-}
-
export function buildPythonChallenge({
challengeFiles
}: BuildChallengeData): Promise | undefined {
@@ -298,14 +323,8 @@ export function buildPythonChallenge({
.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)
+ challengeType: challengeTypes.python,
+ sources: buildSourceMap(challengeFiles)
}))
);
}
diff --git a/client/src/templates/Challenges/utils/frame.ts b/client/src/templates/Challenges/utils/frame.ts
index decfdc3bc7c..8c033266fc0 100644
--- a/client/src/templates/Challenges/utils/frame.ts
+++ b/client/src/templates/Challenges/utils/frame.ts
@@ -24,7 +24,6 @@ export interface Context {
build: string;
sources: Source;
loadEnzyme?: () => void;
- transformedPython?: string;
}
export interface TestRunnerConfig {
@@ -268,15 +267,14 @@ const updateWindowI18next = () => (frameContext: Context) => {
const initTestFrame = (frameReady?: () => void) => (frameContext: Context) => {
waitForFrame(frameContext)
.then(async () => {
- const { sources, loadEnzyme, transformedPython } = frameContext;
+ const { sources, loadEnzyme } = frameContext;
// provide the file name and get the original source
const getUserInput = (fileName: string) =>
toString(sources[fileName as keyof typeof sources]);
await frameContext.document?.__initTestFrame({
code: sources,
getUserInput,
- loadEnzyme,
- transformedPython
+ loadEnzyme
});
if (frameReady) frameReady();
diff --git a/client/src/templates/Challenges/utils/python-worker-handler.ts b/client/src/templates/Challenges/utils/python-worker-handler.ts
new file mode 100644
index 00000000000..8c3ec31f0a6
--- /dev/null
+++ b/client/src/templates/Challenges/utils/python-worker-handler.ts
@@ -0,0 +1,65 @@
+import pythonWorkerData from '../../../../config/browser-scripts/python-worker.json';
+
+const pythonWorkerSrc = `/js/${pythonWorkerData.filename}.js`;
+
+let worker: Worker | null = null;
+let testWorker: Worker | null = null;
+let listener: ((event: MessageEvent) => void) | null = null;
+let resetTerminal: (() => void) | null = null;
+
+export function getPythonWorker(): Worker {
+ if (!worker) {
+ worker = new Worker(pythonWorkerSrc);
+ }
+ return worker;
+}
+
+export function getPythonTestWorker(): Worker {
+ if (!testWorker) {
+ testWorker = new Worker(pythonWorkerSrc);
+ }
+ return testWorker;
+}
+
+type PythonWorkerEvent = {
+ data: {
+ type: 'print' | 'input' | 'contentLoaded';
+ text: string;
+ };
+};
+
+/**
+ * Registers a terminal to receive print and input messages from the python worker.
+ * @param handlers
+ * @param handlers.print - A function that handles print messages from the python worker
+ * @param handlers.input - A function that handles input messages from the python worker
+ * @param reset - A function that resets the terminal
+ */
+export function registerTerminal(
+ handlers: {
+ print: (text: string) => void;
+ input: (text: string) => void;
+ },
+ reset: () => void
+): void {
+ const pythonWorker = getPythonWorker();
+ if (listener) pythonWorker.removeEventListener('message', listener);
+ listener = (event: PythonWorkerEvent) => {
+ const { type, text } = event.data;
+ // Ignore contentLoaded messages for now.
+ if (type === 'contentLoaded') return;
+ handlers[type](text);
+ };
+ pythonWorker.addEventListener('message', listener);
+ resetTerminal = reset;
+}
+
+/**
+ * Terminates the existing python worker and creates a new one.
+ */
+export function resetPythonWorker(): void {
+ if (resetTerminal) resetTerminal();
+ worker?.terminate();
+ worker = new Worker(pythonWorkerSrc);
+ if (listener) worker.addEventListener('message', listener);
+}
diff --git a/client/static/python-input-sw.js b/client/static/python-input-sw.js
new file mode 100644
index 00000000000..cdd4fcdf196
--- /dev/null
+++ b/client/static/python-input-sw.js
@@ -0,0 +1,23 @@
+self.addEventListener('install', function() {
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', function() {
+ self.clients.claim();
+})
+
+let resolver;
+
+self.onmessage = function(event) {
+ resolver(event.data);
+}
+
+self.addEventListener('fetch', (event) => {
+ const url = new URL(event.request.url);
+ if (url.pathname === '/python/intercept-input/') {
+ const response = new Promise((resolve) => {
+ resolver = (data) => resolve(new Response(data));
+ });
+ event.respondWith(response);
+ }
+});
diff --git a/curriculum/challenges/english/20-upcoming-python/learn-python-by-building-a-blackjack-game/64b163c20e59cbd4a64940b0.md b/curriculum/challenges/english/20-upcoming-python/learn-python-by-building-a-blackjack-game/64b163c20e59cbd4a64940b0.md
index 36ad3da4198..020d23938c1 100644
--- a/curriculum/challenges/english/20-upcoming-python/learn-python-by-building-a-blackjack-game/64b163c20e59cbd4a64940b0.md
+++ b/curriculum/challenges/english/20-upcoming-python/learn-python-by-building-a-blackjack-game/64b163c20e59cbd4a64940b0.md
@@ -13,11 +13,42 @@ Create a function to add two numbers together.
Adding 3 and 5 should return 8.
+```js
+({
+ test: () => assert.equal(__userGlobals.get("add")(3,5), 8)
+})
+```
+
+Test value of `__name__`
+
+```js
+({
+ test: () => assert(__pyodide.runPython(`__name__ == '__main__'`))
+})
+```
+
+Test __locals and __pyodide
+
+```js
+({
+ test: () => assert(__pyodide.runPython(`__locals.get('add')(4,5) == 9`))
+})
+```
+
+Test cleaner syntax
+
+```js
+({
+ test: () => assert(runPython(`add(4,5) == 9`))
+})
+```
+
+Testing getDef
+
```js
({
test: () => {
- assert.equal(__userGlobals.get("add")(3,5), 8);
- const add = __helpers.python.getDef(e.code.original["main.py"].replaceAll(/\r/g, ""), "add");
+ const add = __helpers.python.getDef(code, "add");
assert.deepEqual(add, {
def: "def add(a, b):\n return a + b\n",
function_parameters: "a, b",
@@ -28,7 +59,6 @@ Adding 3 and 5 should return 8.
})
```
-
# --seed--
## --seed-contents--
diff --git a/curriculum/test/test-challenges.js b/curriculum/test/test-challenges.js
index b49dab5b838..5592b558951 100644
--- a/curriculum/test/test-challenges.js
+++ b/curriculum/test/test-challenges.js
@@ -33,8 +33,10 @@ const {
} = require('../../client/src/templates/Challenges/utils/worker-executor');
const { challengeTypes } = require('../../shared/config/challenge-types');
// the config files are created during the build, but not before linting
-const testEvaluator =
+const javaScriptTestEvaluator =
require('../../client/config/browser-scripts/test-evaluator.json').filename;
+const pythonTestEvaluator =
+ require('../../client/config/browser-scripts/python-test-evaluator.json').filename;
const { getLines } = require('../../shared/utils/get-lines');
@@ -531,28 +533,39 @@ async function createTestRunner(
solutionFiles
);
- const { build, sources, loadEnzyme, transformedPython } =
- await buildChallenge(
- {
- challengeFiles,
- required,
- template
- },
- { usesTestRunner: true }
- );
+ const { build, sources, loadEnzyme } = await buildChallenge(
+ {
+ challengeFiles,
+ required,
+ template
+ },
+ { usesTestRunner: true }
+ );
const code = {
contents: sources.index,
- editableContents: sources.editableContents
+ editableContents: sources.editableContents,
+ original: sources.original
};
- const runsInBrowser =
- buildChallenge === buildDOMChallenge ||
- buildChallenge === buildPythonChallenge;
+ const runsInBrowser = buildChallenge === buildDOMChallenge;
+ const runsInPythonWorker = buildChallenge === buildPythonChallenge;
+
+ const testEvaluator = runsInPythonWorker
+ ? pythonTestEvaluator
+ : javaScriptTestEvaluator;
+
+ // The python worker clears the globals between tests, so it should be fine
+ // to use the same evaluator for all tests. TODO: check if this is true for
+ // sys, since sys.modules is not being reset.
+ const workerConfig = {
+ testEvaluator,
+ options: { terminateWorker: !runsInPythonWorker }
+ };
const evaluator = await (runsInBrowser
- ? getContextEvaluator(build, sources, code, loadEnzyme, transformedPython)
- : getWorkerEvaluator(build, sources, code, removeComments));
+ ? getContextEvaluator(build, sources, code, loadEnzyme)
+ : getWorkerEvaluator(build, sources, code, removeComments, workerConfig));
return async ({ text, testString }) => {
try {
@@ -596,20 +609,8 @@ function replaceChallengeFilesContentsWithSolutions(
});
}
-async function getContextEvaluator(
- build,
- sources,
- code,
- loadEnzyme,
- transformedPython
-) {
- await initializeTestRunner(
- build,
- sources,
- code,
- loadEnzyme,
- transformedPython
- );
+async function getContextEvaluator(build, sources, code, loadEnzyme) {
+ await initializeTestRunner(build, sources, code, loadEnzyme);
return {
evaluate: async (testString, timeout) =>
@@ -624,8 +625,15 @@ async function getContextEvaluator(
};
}
-async function getWorkerEvaluator(build, sources, code, removeComments) {
- const testWorker = createWorker(testEvaluator, { terminateWorker: true });
+async function getWorkerEvaluator(
+ build,
+ sources,
+ code,
+ removeComments,
+ workerConfig
+) {
+ const { testEvaluator, options } = workerConfig;
+ const testWorker = createWorker(testEvaluator, options);
return {
evaluate: async (testString, timeout) =>
await testWorker.execute(
@@ -635,30 +643,22 @@ async function getWorkerEvaluator(build, sources, code, removeComments) {
};
}
-async function initializeTestRunner(
- build,
- sources,
- code,
- loadEnzyme,
- transformedPython
-) {
+async function initializeTestRunner(build, sources, code, loadEnzyme) {
await page.reload();
await page.setContent(build);
await page.evaluate(
- async (code, sources, loadEnzyme, transformedPython) => {
+ async (code, sources, loadEnzyme) => {
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.
+ // client.
await document.__initTestFrame({
code: sources,
getUserInput,
- loadEnzyme,
- transformedPython
+ loadEnzyme
});
},
code,
sources,
- loadEnzyme,
- transformedPython
+ loadEnzyme
);
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6b69bcdab1f..b9d22e0aa4b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -595,7 +595,7 @@ importers:
version: 4.20.10
gatsby:
specifier: 3.15.0
- version: 3.15.0(@types/node@20.8.2)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
+ version: 3.15.0(@types/node@18.18.9)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
gatsby-cli:
specifier: 3.15.0
version: 3.15.0
@@ -782,6 +782,12 @@ importers:
validator:
specifier: 13.11.0
version: 13.11.0
+ xterm:
+ specifier: ^5.2.1
+ version: 5.2.1
+ xterm-addon-fit:
+ specifier: ^0.8.0
+ version: 0.8.0(xterm@5.2.1)
devDependencies:
'@babel/plugin-syntax-dynamic-import':
specifier: 7.8.3
@@ -947,7 +953,7 @@ importers:
version: 13.0.4
ts-node:
specifier: 10.9.1
- version: 10.9.1(@types/node@20.8.2)(typescript@5.2.2)
+ version: 10.9.1(@types/node@18.18.9)(typescript@5.2.2)
webpack:
specifier: 5.89.0
version: 5.89.0(webpack-cli@4.10.0)
@@ -2650,7 +2656,7 @@ packages:
'@babel/traverse': 7.23.2
'@babel/types': 7.23.0
convert-source-map: 1.9.0
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
gensync: 1.0.0-beta.2
json5: 2.2.3
lodash: 4.17.21
@@ -2722,7 +2728,7 @@ packages:
'@babel/traverse': 7.23.2
'@babel/types': 7.23.0
convert-source-map: 2.0.0
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -2744,7 +2750,7 @@ packages:
'@babel/traverse': 7.23.3
'@babel/types': 7.23.3
convert-source-map: 2.0.0
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -2946,7 +2952,7 @@ packages:
'@babel/core': 7.23.3
'@babel/helper-compilation-targets': 7.22.15
'@babel/helper-plugin-utils': 7.22.5
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
lodash.debounce: 4.0.8
resolve: 1.22.6
transitivePeerDependencies:
@@ -2961,7 +2967,7 @@ packages:
'@babel/core': 7.23.0
'@babel/helper-compilation-targets': 7.22.15
'@babel/helper-plugin-utils': 7.22.5
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
lodash.debounce: 4.0.8
resolve: 1.22.6
transitivePeerDependencies:
@@ -2975,7 +2981,7 @@ packages:
'@babel/core': 7.23.3
'@babel/helper-compilation-targets': 7.22.15
'@babel/helper-plugin-utils': 7.22.5
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
lodash.debounce: 4.0.8
resolve: 1.22.6
transitivePeerDependencies:
@@ -6408,7 +6414,7 @@ packages:
'@babel/helper-split-export-declaration': 7.22.6
'@babel/parser': 7.23.0
'@babel/types': 7.23.0
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@@ -6425,7 +6431,7 @@ packages:
'@babel/helper-split-export-declaration': 7.22.6
'@babel/parser': 7.23.0
'@babel/types': 7.23.0
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@@ -6442,7 +6448,7 @@ packages:
'@babel/helper-split-export-declaration': 7.22.6
'@babel/parser': 7.23.3
'@babel/types': 7.23.3
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@@ -6812,7 +6818,7 @@ packages:
engines: {node: ^10.12.0 || >=12.0.0}
dependencies:
ajv: 6.12.6
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
espree: 7.3.1
globals: 13.22.0
ignore: 4.0.6
@@ -6828,7 +6834,7 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
ajv: 6.12.6
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
espree: 9.6.1
globals: 13.22.0
ignore: 5.2.4
@@ -7197,7 +7203,7 @@ packages:
tslib: 2.2.0
value-or-promise: 1.0.6
- /@graphql-tools/url-loader@6.10.1(@types/node@20.8.2)(graphql@15.8.0):
+ /@graphql-tools/url-loader@6.10.1(@types/node@18.18.9)(graphql@15.8.0):
resolution: {integrity: sha512-DSDrbhQIv7fheQ60pfDpGD256ixUQIR6Hhf9Z5bRjVkXOCvO5XrkwoWLiU7iHL81GB1r0Ba31bf+sl+D4nyyfw==}
peerDependencies:
graphql: ^14.0.0 || ^15.0.0
@@ -7216,7 +7222,7 @@ packages:
is-promise: 4.0.0
isomorphic-ws: 4.0.1(ws@7.4.5)
lodash: 4.17.21
- meros: 1.1.4(@types/node@20.8.2)
+ meros: 1.1.4(@types/node@18.18.9)
subscriptions-transport-ws: 0.9.19(graphql@15.8.0)
sync-fetch: 0.3.0
tslib: 2.2.0
@@ -7335,7 +7341,7 @@ packages:
engines: {node: '>=10.10.0'}
dependencies:
'@humanwhocodes/object-schema': 2.0.1
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@@ -7345,7 +7351,7 @@ packages:
engines: {node: '>=10.10.0'}
dependencies:
'@humanwhocodes/object-schema': 1.2.1
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@@ -11251,7 +11257,7 @@ packages:
'@typescript-eslint/experimental-utils': 4.33.0(eslint@7.32.0)(typescript@5.2.2)
'@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@5.2.2)
'@typescript-eslint/scope-manager': 4.33.0
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
eslint: 7.32.0
functional-red-black-tree: 1.0.1
ignore: 5.2.4
@@ -11279,7 +11285,7 @@ packages:
'@typescript-eslint/type-utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2)
'@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2)
'@typescript-eslint/visitor-keys': 6.10.0
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
eslint: 8.53.0
graphemer: 1.4.0
ignore: 5.2.4
@@ -11337,7 +11343,7 @@ packages:
'@typescript-eslint/scope-manager': 4.33.0
'@typescript-eslint/types': 4.33.0
'@typescript-eslint/typescript-estree': 4.33.0(typescript@5.2.2)
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
eslint: 7.32.0
typescript: 5.2.2
transitivePeerDependencies:
@@ -11357,7 +11363,7 @@ packages:
'@typescript-eslint/types': 6.10.0
'@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2)
'@typescript-eslint/visitor-keys': 6.10.0
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
eslint: 8.53.0
typescript: 5.2.2
transitivePeerDependencies:
@@ -11397,7 +11403,7 @@ packages:
dependencies:
'@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2)
'@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2)
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
eslint: 8.53.0
ts-api-utils: 1.0.3(typescript@5.2.2)
typescript: 5.2.2
@@ -11433,7 +11439,7 @@ packages:
dependencies:
'@typescript-eslint/types': 3.10.1
'@typescript-eslint/visitor-keys': 3.10.1
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
glob: 7.2.3
is-glob: 4.0.3
lodash: 4.17.21
@@ -11454,7 +11460,7 @@ packages:
dependencies:
'@typescript-eslint/types': 4.33.0
'@typescript-eslint/visitor-keys': 4.33.0
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
semver: 7.5.4
@@ -11474,7 +11480,7 @@ packages:
dependencies:
'@typescript-eslint/types': 5.62.0
'@typescript-eslint/visitor-keys': 5.62.0
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
semver: 7.5.4
@@ -11495,7 +11501,7 @@ packages:
dependencies:
'@typescript-eslint/types': 6.10.0
'@typescript-eslint/visitor-keys': 6.10.0
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
semver: 7.5.4
@@ -11978,7 +11984,7 @@ packages:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
dependencies:
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
transitivePeerDependencies:
- supports-color
@@ -13201,7 +13207,7 @@ packages:
'@babel/core': 7.23.0
'@babel/runtime': 7.23.1
'@babel/types': 7.23.0
- gatsby: 3.15.0(@types/node@20.8.2)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
+ gatsby: 3.15.0(@types/node@18.18.9)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
gatsby-core-utils: 2.15.0
/babel-plugin-remove-graphql-queries@3.15.0(@babel/core@7.23.3)(gatsby@3.15.0):
@@ -13214,7 +13220,7 @@ packages:
'@babel/core': 7.23.3
'@babel/runtime': 7.23.1
'@babel/types': 7.23.0
- gatsby: 3.15.0(@types/node@20.8.2)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
+ gatsby: 3.15.0(@types/node@18.18.9)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
gatsby-core-utils: 2.15.0
/babel-plugin-syntax-async-functions@6.13.0:
@@ -15977,6 +15983,16 @@ packages:
ms: 2.0.0
dev: false
+ /debug@3.2.7:
+ resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ dependencies:
+ ms: 2.1.3
+
/debug@3.2.7(supports-color@5.5.0):
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
@@ -16011,6 +16027,17 @@ packages:
ms: 2.1.2
dev: true
+ /debug@4.3.4:
+ resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ dependencies:
+ ms: 2.1.2
+
/debug@4.3.4(supports-color@8.1.1):
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
@@ -16308,7 +16335,7 @@ packages:
hasBin: true
dependencies:
address: 1.1.2
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
transitivePeerDependencies:
- supports-color
@@ -16336,7 +16363,7 @@ packages:
'@types/tmp': 0.0.33
application-config-path: 0.1.1
command-exists: 1.2.9
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7
eol: 0.9.1
get-port: 3.2.0
glob: 7.2.3
@@ -16464,7 +16491,7 @@ packages:
/docsify-server-renderer@4.13.1:
resolution: {integrity: sha512-XNJeCK3zp+mVO7JZFn0bH4hNBAMMC1MbuCU7CBsjLHYn4NHrjIgCBGmylzEan3/4Qm6kbSzQx8XzUK5T7GQxHw==}
dependencies:
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
docsify: 4.13.1
node-fetch: 2.7.0
resolve-pathname: 3.0.0
@@ -16765,7 +16792,7 @@ packages:
dependencies:
base64-arraybuffer: 0.1.4
component-emitter: 1.3.0
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
engine.io-parser: 4.0.3
has-cors: 1.1.0
parseqs: 0.0.6
@@ -16792,7 +16819,7 @@ packages:
base64id: 2.0.0
cookie: 0.4.2
cors: 2.8.5
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
engine.io-parser: 4.0.3
ws: 7.4.6
transitivePeerDependencies:
@@ -17198,7 +17225,7 @@ packages:
/eslint-import-resolver-node@0.3.9:
resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
dependencies:
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7
is-core-module: 2.13.1
resolve: 1.22.6
transitivePeerDependencies:
@@ -17211,10 +17238,10 @@ packages:
eslint: '*'
eslint-plugin-import: '*'
dependencies:
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
enhanced-resolve: 5.15.0
eslint: 8.53.0
- eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.53.0)
+ eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.53.0)
eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.53.0)
get-tsconfig: 4.7.2
globby: 13.2.2
@@ -17249,7 +17276,7 @@ packages:
optional: true
dependencies:
'@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@5.2.2)
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7
eslint: 7.32.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@6.10.0)(eslint-plugin-import@2.29.0)(eslint@8.53.0)
@@ -17278,13 +17305,41 @@ packages:
optional: true
dependencies:
'@typescript-eslint/parser': 6.10.0(eslint@8.53.0)(typescript@5.2.2)
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7
eslint: 8.53.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@6.10.0)(eslint-plugin-import@2.29.0)(eslint@8.53.0)
transitivePeerDependencies:
- supports-color
+ /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.53.0):
+ resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
+ engines: {node: '>=4'}
+ peerDependencies:
+ '@typescript-eslint/parser': '*'
+ eslint: '*'
+ eslint-import-resolver-node: '*'
+ eslint-import-resolver-typescript: '*'
+ eslint-import-resolver-webpack: '*'
+ peerDependenciesMeta:
+ '@typescript-eslint/parser':
+ optional: true
+ eslint:
+ optional: true
+ eslint-import-resolver-node:
+ optional: true
+ eslint-import-resolver-typescript:
+ optional: true
+ eslint-import-resolver-webpack:
+ optional: true
+ dependencies:
+ '@typescript-eslint/parser': 6.10.0(eslint@8.53.0)(typescript@5.2.2)
+ debug: 3.2.7
+ eslint: 8.53.0
+ eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@6.10.0)(eslint-plugin-import@2.29.0)(eslint@8.53.0)
+ transitivePeerDependencies:
+ - supports-color
+
/eslint-plugin-filenames-simple@0.8.0(eslint@8.53.0):
resolution: {integrity: sha512-8+uBzNBE5gSUMQv7bmMBiOD26eKzD4/5flPtD5Vl3dzZLXotSwXK3W7ZZqKQfU0Qyoborh+LqbN76EfmbBcU8A==}
engines: {node: ^14.17.0 || ^16.0.0 || ^18.0.0}
@@ -17305,7 +17360,7 @@ packages:
lodash: 4.17.21
string-natural-compare: 3.0.1
- /eslint-plugin-graphql@4.0.0(@types/node@20.8.2)(graphql@15.8.0)(typescript@5.2.2):
+ /eslint-plugin-graphql@4.0.0(@types/node@18.18.9)(graphql@15.8.0)(typescript@5.2.2):
resolution: {integrity: sha512-d5tQm24YkVvCEk29ZR5ScsgXqAGCjKlMS8lx3mS7FS/EKsWbkvXQImpvic03EpMIvNTBW5e+2xnHzXB/VHNZJw==}
engines: {node: '>=10.0'}
peerDependencies:
@@ -17313,7 +17368,7 @@ packages:
dependencies:
'@babel/runtime': 7.23.1
graphql: 15.8.0
- graphql-config: 3.4.1(@types/node@20.8.2)(graphql@15.8.0)(typescript@5.2.2)
+ graphql-config: 3.4.1(@types/node@18.18.9)(graphql@15.8.0)(typescript@5.2.2)
lodash.flatten: 4.4.0
lodash.without: 4.4.0
transitivePeerDependencies:
@@ -17338,7 +17393,7 @@ packages:
array.prototype.findlastindex: 1.2.3
array.prototype.flat: 1.3.2
array.prototype.flatmap: 1.3.2
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7
doctrine: 2.1.0
eslint: 7.32.0
eslint-import-resolver-node: 0.3.9
@@ -17372,7 +17427,7 @@ packages:
array.prototype.findlastindex: 1.2.3
array.prototype.flat: 1.3.2
array.prototype.flatmap: 1.3.2
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7
doctrine: 2.1.0
eslint: 8.53.0
eslint-import-resolver-node: 0.3.9
@@ -17412,7 +17467,7 @@ packages:
'@es-joy/jsdoccomment': 0.39.4
are-docs-informative: 0.0.2
comment-parser: 1.3.1
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
escape-string-regexp: 4.0.0
eslint: 8.53.0
esquery: 1.5.0
@@ -17643,7 +17698,7 @@ packages:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.3
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
doctrine: 3.0.0
enquirer: 2.4.1
escape-string-regexp: 4.0.0
@@ -17696,7 +17751,7 @@ packages:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.3
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
doctrine: 3.0.0
escape-string-regexp: 4.0.0
eslint-scope: 7.2.2
@@ -18616,7 +18671,7 @@ packages:
debug:
optional: true
dependencies:
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7
/follow-redirects@1.15.3(debug@4.3.4):
resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==}
@@ -18627,7 +18682,7 @@ packages:
debug:
optional: true
dependencies:
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
/for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
@@ -19091,7 +19146,7 @@ packages:
dependencies:
'@babel/runtime': 7.14.0
fs-extra: 10.0.1
- gatsby: 3.15.0(@types/node@20.8.2)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
+ gatsby: 3.15.0(@types/node@18.18.9)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
lodash: 4.17.21
moment: 2.29.1
pify: 5.0.0
@@ -19105,7 +19160,7 @@ packages:
gatsby: ^3.0.0-next.0
dependencies:
'@babel/runtime': 7.20.13
- gatsby: 3.15.0(@types/node@20.8.2)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
+ gatsby: 3.15.0(@types/node@18.18.9)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
dev: false
/gatsby-plugin-manifest@3.15.0(gatsby@3.15.0)(graphql@15.8.0):
@@ -19115,7 +19170,7 @@ packages:
gatsby: ^3.0.0-next.0
dependencies:
'@babel/runtime': 7.20.13
- gatsby: 3.15.0(@types/node@20.8.2)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
+ gatsby: 3.15.0(@types/node@18.18.9)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
gatsby-core-utils: 2.15.0
gatsby-plugin-utils: 1.15.0(gatsby@3.15.0)(graphql@15.8.0)
semver: 7.5.4
@@ -19136,7 +19191,7 @@ packages:
chokidar: 3.5.3
fs-exists-cached: 1.0.0
fs-extra: 10.1.0
- gatsby: 3.15.0(@types/node@20.8.2)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
+ gatsby: 3.15.0(@types/node@18.18.9)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
gatsby-core-utils: 2.15.0
gatsby-page-utils: 1.15.0
gatsby-plugin-utils: 1.15.0(gatsby@3.15.0)(graphql@15.8.0)
@@ -19153,7 +19208,7 @@ packages:
peerDependencies:
gatsby: ~2.x.x || ~3.x.x || ~4.x.x
dependencies:
- gatsby: 3.15.0(@types/node@20.8.2)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
+ gatsby: 3.15.0(@types/node@18.18.9)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
lodash.get: 4.4.2
lodash.uniq: 4.5.0
dev: false
@@ -19166,7 +19221,7 @@ packages:
postcss: ^8.0.5
dependencies:
'@babel/runtime': 7.20.13
- gatsby: 3.15.0(@types/node@20.8.2)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
+ gatsby: 3.15.0(@types/node@18.18.9)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
postcss: 8.4.31
postcss-loader: 4.3.0(postcss@8.4.31)(webpack@5.89.0)
transitivePeerDependencies:
@@ -19181,7 +19236,7 @@ packages:
react-helmet: ^5.1.3 || ^6.0.0
dependencies:
'@babel/runtime': 7.20.13
- gatsby: 3.15.0(@types/node@20.8.2)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
+ gatsby: 3.15.0(@types/node@18.18.9)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
react-helmet: 6.1.0(react@16.14.0)
dev: false
@@ -19202,7 +19257,7 @@ packages:
'@babel/preset-typescript': 7.23.3(@babel/core@7.23.3)
'@babel/runtime': 7.23.1
babel-plugin-remove-graphql-queries: 3.15.0(@babel/core@7.23.3)(gatsby@3.15.0)
- gatsby: 3.15.0(@types/node@20.8.2)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
+ gatsby: 3.15.0(@types/node@18.18.9)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
transitivePeerDependencies:
- supports-color
@@ -19215,7 +19270,7 @@ packages:
dependencies:
'@babel/runtime': 7.23.1
fastq: 1.15.0
- gatsby: 3.15.0(@types/node@20.8.2)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
+ gatsby: 3.15.0(@types/node@18.18.9)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
graphql: 15.8.0
joi: 17.11.0
@@ -19225,7 +19280,7 @@ packages:
gatsby: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
dependencies:
'@babel/runtime': 7.23.1
- gatsby: 3.15.0(@types/node@20.8.2)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
+ gatsby: 3.15.0(@types/node@18.18.9)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
webpack-bundle-analyzer: 4.9.1
transitivePeerDependencies:
- bufferutil
@@ -19266,7 +19321,7 @@ packages:
chokidar: 3.5.3
contentful-management: 7.54.2(debug@4.3.4)
cors: 2.8.5
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
detect-port: 1.5.1
dotenv: 8.6.0
execa: 5.1.1
@@ -19322,7 +19377,7 @@ packages:
prismjs: ^1.15.0
dependencies:
'@babel/runtime': 7.20.13
- gatsby: 3.15.0(@types/node@20.8.2)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
+ gatsby: 3.15.0(@types/node@18.18.9)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
parse-numeric-range: 1.3.0
prismjs: 1.29.0
unist-util-visit: 2.0.3
@@ -19339,7 +19394,7 @@ packages:
fastq: 1.15.0
file-type: 16.5.4
fs-extra: 10.1.0
- gatsby: 3.15.0(@types/node@20.8.2)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
+ gatsby: 3.15.0(@types/node@18.18.9)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
gatsby-core-utils: 2.15.0
got: 9.6.0
md5-file: 5.0.0
@@ -19379,7 +19434,7 @@ packages:
gatsby: ^4.0.0-next
dependencies:
'@babel/runtime': 7.23.1
- gatsby: 3.15.0(@types/node@20.8.2)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
+ gatsby: 3.15.0(@types/node@18.18.9)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0)
gatsby-core-utils: 3.25.0
gray-matter: 4.0.3
hast-util-raw: 6.1.0
@@ -19414,7 +19469,7 @@ packages:
transitivePeerDependencies:
- supports-color
- /gatsby@3.15.0(@types/node@20.8.2)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0):
+ /gatsby@3.15.0(@types/node@18.18.9)(babel-eslint@10.1.0)(eslint-import-resolver-typescript@3.5.5)(eslint-plugin-testing-library@3.9.0)(react-dom@16.14.0)(react@16.14.0)(typescript@5.2.2)(webpack-cli@4.10.0):
resolution: {integrity: sha512-zZrHYZtBksrWkOvIJIsaOdfT6rTd5g+HclsWO25H3kTecaPGm5wiKrTtEDPePHWNqEM1V0rLJ/I97/N5tS+7Lw==}
engines: {node: '>=12.13.0'}
hasBin: true
@@ -19465,7 +19520,7 @@ packages:
css-minimizer-webpack-plugin: 2.0.0(webpack@5.89.0)
css.escape: 1.5.1
date-fns: 2.30.0
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7
deepmerge: 4.3.1
del: 5.1.0
detect-port: 1.5.1
@@ -19474,7 +19529,7 @@ packages:
eslint: 7.32.0
eslint-config-react-app: 6.0.0(@typescript-eslint/eslint-plugin@4.33.0)(@typescript-eslint/parser@4.33.0)(babel-eslint@10.1.0)(eslint-plugin-flowtype@5.10.0)(eslint-plugin-import@2.28.1)(eslint-plugin-jsx-a11y@6.7.1)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2)(eslint-plugin-testing-library@3.9.0)(eslint@7.32.0)(typescript@5.2.2)
eslint-plugin-flowtype: 5.10.0(eslint@7.32.0)
- eslint-plugin-graphql: 4.0.0(@types/node@20.8.2)(graphql@15.8.0)(typescript@5.2.2)
+ eslint-plugin-graphql: 4.0.0(@types/node@18.18.9)(graphql@15.8.0)(typescript@5.2.2)
eslint-plugin-import: 2.28.1(@typescript-eslint/parser@4.33.0)(eslint-import-resolver-typescript@3.5.5)(eslint@7.32.0)
eslint-plugin-jsx-a11y: 6.7.1(eslint@7.32.0)
eslint-plugin-react: 7.33.2(eslint@7.32.0)
@@ -19992,7 +20047,7 @@ packages:
graphql-type-json: 0.3.2(graphql@15.8.0)
object-path: 0.11.5
- /graphql-config@3.4.1(@types/node@20.8.2)(graphql@15.8.0)(typescript@5.2.2):
+ /graphql-config@3.4.1(@types/node@18.18.9)(graphql@15.8.0)(typescript@5.2.2):
resolution: {integrity: sha512-g9WyK4JZl1Ko++FSyE5Ir2g66njfxGzrDDhBOwnkoWf/t3TnnZG6BBkWP+pkqVJ5pqMJGPKHNrbew8jRxStjhw==}
engines: {node: '>= 10.0.0'}
peerDependencies:
@@ -20003,7 +20058,7 @@ packages:
'@graphql-tools/json-file-loader': 6.2.6(graphql@15.8.0)
'@graphql-tools/load': 6.2.8(graphql@15.8.0)
'@graphql-tools/merge': 6.2.14(graphql@15.8.0)
- '@graphql-tools/url-loader': 6.10.1(@types/node@20.8.2)(graphql@15.8.0)
+ '@graphql-tools/url-loader': 6.10.1(@types/node@18.18.9)(graphql@15.8.0)
'@graphql-tools/utils': 7.10.0(graphql@15.8.0)
cosmiconfig: 7.0.0
cosmiconfig-toml-loader: 1.0.0
@@ -20648,7 +20703,7 @@ packages:
dependencies:
'@tootallnate/once': 2.0.0
agent-base: 6.0.2
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
transitivePeerDependencies:
- supports-color
dev: true
@@ -20725,7 +20780,7 @@ packages:
engines: {node: '>= 6'}
dependencies:
agent-base: 6.0.2
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
transitivePeerDependencies:
- supports-color
@@ -21657,7 +21712,7 @@ packages:
resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==}
engines: {node: '>=10'}
dependencies:
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
istanbul-lib-coverage: 3.2.0
source-map: 0.6.1
transitivePeerDependencies:
@@ -22816,7 +22871,7 @@ packages:
cli-truncate: 3.1.0
colorette: 2.0.20
commander: 9.5.0
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
execa: 6.1.0
lilconfig: 2.0.6
listr2: 5.0.8
@@ -23908,7 +23963,7 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
- /meros@1.1.4(@types/node@20.8.2):
+ /meros@1.1.4(@types/node@18.18.9):
resolution: {integrity: sha512-E9ZXfK9iQfG9s73ars9qvvvbSIkJZF5yOo9j4tcwM5tN8mUKfj/EKN5PzOr3ZH0y5wL7dLAHw3RVEfpQV9Q7VQ==}
engines: {node: '>=12'}
peerDependencies:
@@ -23917,7 +23972,7 @@ packages:
'@types/node':
optional: true
dependencies:
- '@types/node': 20.8.2
+ '@types/node': 18.18.9
/method-override@3.0.0:
resolution: {integrity: sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==}
@@ -24366,7 +24421,7 @@ packages:
/micromark@2.11.4:
resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==}
dependencies:
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
parse-entities: 2.0.0
transitivePeerDependencies:
- supports-color
@@ -24376,7 +24431,7 @@ packages:
resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==}
dependencies:
'@types/debug': 4.1.9
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
decode-named-character-reference: 1.0.2
micromark-core-commonmark: 1.1.0
micromark-factory-space: 1.1.0
@@ -24399,7 +24454,7 @@ packages:
resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==}
dependencies:
'@types/debug': 4.1.9
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
decode-named-character-reference: 1.0.2
devlop: 1.1.0
micromark-core-commonmark: 2.0.0
@@ -29383,7 +29438,7 @@ packages:
'@types/component-emitter': 1.2.12
backo2: 1.0.2
component-emitter: 1.3.0
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
engine.io-client: 4.1.4
parseuri: 0.0.6
socket.io-parser: 4.0.5
@@ -29398,7 +29453,7 @@ packages:
dependencies:
'@types/component-emitter': 1.2.12
component-emitter: 1.3.0
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
transitivePeerDependencies:
- supports-color
@@ -29411,7 +29466,7 @@ packages:
'@types/node': 14.18.63
accepts: 1.3.8
base64id: 2.0.0
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
engine.io: 4.1.2
socket.io-adapter: 2.1.0
socket.io-parser: 4.0.5
@@ -29649,7 +29704,7 @@ packages:
arg: 5.0.2
bluebird: 3.7.2
check-more-types: 2.24.0
- debug: 4.3.4(supports-color@8.1.1)
+ debug: 4.3.4
execa: 5.1.1
lazy-ass: 1.6.0
ps-tree: 1.2.0
@@ -32189,7 +32244,7 @@ packages:
/webpack-virtual-modules@0.3.2:
resolution: {integrity: sha512-RXQXioY6MhzM4CNQwmBwKXYgBs6ulaiQ8bkNQEl2J6Z+V+s7lgl/wGvaI/I0dLnYKB8cKsxQc17QOAVIphPLDw==}
dependencies:
- debug: 3.2.7(supports-color@8.1.1)
+ debug: 3.2.7
transitivePeerDependencies:
- supports-color
diff --git a/tools/client-plugins/browser-scripts/index.d.ts b/tools/client-plugins/browser-scripts/index.d.ts
index dc041d22eee..10969a45a82 100644
--- a/tools/client-plugins/browser-scripts/index.d.ts
+++ b/tools/client-plugins/browser-scripts/index.d.ts
@@ -22,7 +22,6 @@ export interface InitTestFrameArg {
};
getUserInput?: (fileName: string) => string;
loadEnzyme?: () => void;
- transformedPython?: string;
}
export type FrameWindow = Window &
diff --git a/tools/client-plugins/browser-scripts/python-runner.ts b/tools/client-plugins/browser-scripts/python-runner.ts
deleted file mode 100644
index 04e8fc04c2c..00000000000
--- a/tools/client-plugins/browser-scripts/python-runner.ts
+++ /dev/null
@@ -1,299 +0,0 @@
-// 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;
-type Print = (...args: unknown[]) => void;
-type ResetTerminal = () => void;
-type EvaluatedTeststring = {
- input: string[];
- test: () => Promise;
-};
-
-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 =>
- 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, b: Record) =>
- JSON.stringify(a) === JSON.stringify(b);
-
- // Hardcode Deep Freeze dependency
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const DeepFreeze = (o: Record) => {
- 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 evaluatedTestString = await new Promise(
- (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 } =
- eval(testString);
- resolve(test);
- } catch (err) {
- reject(err);
- }
- })
- );
-
- // If the test string does not evaluate to an object, then we assume that
- // it's a standard JS test and any assertions have already passed.
- if (typeof evaluatedTestString !== 'object') {
- return { pass: true };
- }
-
- if (!evaluatedTestString || !('test' in evaluatedTestString)) {
- throw new Error(
- 'Test string did not evaluate to an object with the test property'
- );
- }
-
- const { input, test } = evaluatedTestString as EvaluatedTeststring;
-
- // 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
- });
-
- // We have to declare these variables in the scope of 'eval', so that they
- // exist when the `testString` is evaluated. Otherwise, they will be
- // undefined when `test` is called and the tests will not be able to use
- // __pyodide or __userGlobals.
- const __pyodide = await this.__runPython(code);
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
- const __userGlobals = __pyodide.globals.get('__locals') as unknown;
- 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
- }
- };
- }
- };
-}
diff --git a/tools/client-plugins/browser-scripts/python-test-evaluator.ts b/tools/client-plugins/browser-scripts/python-test-evaluator.ts
new file mode 100644
index 00000000000..839e7f62a8c
--- /dev/null
+++ b/tools/client-plugins/browser-scripts/python-test-evaluator.ts
@@ -0,0 +1,166 @@
+// 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 type { PyProxy } from 'pyodide/ffi';
+import pkg from 'pyodide/package.json';
+import * as helpers from '@freecodecamp/curriculum-helpers';
+import chai from 'chai';
+
+const ctx: Worker & typeof globalThis = self as unknown as Worker &
+ typeof globalThis;
+
+let pyodide: PyodideInterface;
+
+interface PythonRunEvent extends MessageEvent {
+ data: {
+ code: {
+ contents: string;
+ editableContents: string;
+ original: { [id: string]: string };
+ };
+ removeComments: boolean;
+ firstTest: unknown;
+ testString: string;
+ build: string;
+ sources: {
+ [fileName: string]: unknown;
+ };
+ };
+}
+
+type EvaluatedTeststring = {
+ input: string[];
+ test: () => Promise;
+};
+
+async function setupPyodide() {
+ if (pyodide) return pyodide;
+
+ pyodide = await loadPyodide({
+ // TODO: host this ourselves
+ indexURL: `https://cdn.jsdelivr.net/pyodide/v${pkg.version}/full/`
+ });
+
+ // We freeze this to prevent learners from getting the worker into a
+ // weird state. NOTE: this has to come after pyodide is loaded, because
+ // pyodide modifies self while loading.
+ Object.freeze(self);
+
+ ctx.postMessage({ type: 'contentLoaded' });
+
+ return pyodide;
+}
+
+void setupPyodide();
+
+ctx.onmessage = async (e: PythonRunEvent) => {
+ const pyodide = await setupPyodide();
+ // TODO: Use removeComments when we have it
+ /* eslint-disable @typescript-eslint/no-unused-vars */
+ const code = (e.data.code.contents || '').slice();
+ const editableContents = (e.data.code.editableContents || '').slice();
+ const testString = e.data.testString;
+
+ const assert = chai.assert;
+ const __helpers = helpers;
+ /* eslint-enable @typescript-eslint/no-unused-vars */
+ // 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 evaluatedTestString = await new Promise(
+ (resolve, reject) => {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const test: { input: string[]; test: () => Promise } =
+ eval(testString);
+ resolve(test);
+ } catch (err) {
+ reject(err);
+ }
+ }
+ );
+
+ // If the test string does not evaluate to an object, then we assume that
+ // it's a standard JS test and any assertions have already passed.
+ if (typeof evaluatedTestString !== 'object') {
+ ctx.postMessage({ pass: true });
+ return;
+ }
+
+ if (!evaluatedTestString || !('test' in evaluatedTestString)) {
+ throw new Error(
+ 'Test string did not evaluate to an object with the test property'
+ );
+ }
+
+ const { input, test } = evaluatedTestString as EvaluatedTeststring;
+
+ const inputIterator = (input ?? []).values();
+ const testInput = () => {
+ const next = inputIterator.next();
+ if (next.done) {
+ // TODO: handle this error in the UI
+ throw new Error('Too many input calls');
+ } else {
+ return next.value;
+ }
+ };
+
+ // Make input available to python (print is not used yet)
+ pyodide.registerJsModule('jscustom', {
+ input: testInput
+ // print: () => {}
+ });
+ // Create fresh globals for each test
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+ const __userGlobals = pyodide.globals.get('dict')() as PyProxy;
+ // Some tests rely on __name__ being set to __main__ and we new dicts do not
+ // have this set by default.
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+ __userGlobals.set('__name__', '__main__');
+
+ // The runPython helper is a shortcut for running python code with our
+ // custom globals.
+ const runPython = (pyCode: string) =>
+ pyodide.runPython(pyCode, { globals: __userGlobals }) as unknown;
+ // TODO: remove __pyodide once all the test use runPython.
+ const __pyodide = {
+ runPython
+ };
+
+ runPython(
+ `
+ import jscustom
+ from jscustom import input
+ `
+ );
+
+ // Evaluates the learner's code so that any variables they define are
+ // available to the test.
+ runPython(code);
+ // TODO: remove the next line, creating __locals, once all the tests access
+ // variables directly.
+ runPython('__locals = globals()');
+ await test();
+
+ ctx.postMessage({ 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
+ ctx.postMessage({
+ err: {
+ message: (err as Error).message,
+ stack: (err as Error).stack,
+ expected: (err as { expected?: string }).expected,
+ actual: (err as { actual?: string }).actual
+ }
+ });
+ }
+};
diff --git a/tools/client-plugins/browser-scripts/python-worker.ts b/tools/client-plugins/browser-scripts/python-worker.ts
new file mode 100644
index 00000000000..5ea2c26ef70
--- /dev/null
+++ b/tools/client-plugins/browser-scripts/python-worker.ts
@@ -0,0 +1,81 @@
+// 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';
+
+const ctx: Worker & typeof globalThis = self as unknown as Worker &
+ typeof globalThis;
+
+let pyodide: PyodideInterface;
+
+interface PythonRunEvent extends MessageEvent {
+ data: {
+ code: {
+ contents: string;
+ editableContents: string;
+ original: { [id: string]: string };
+ };
+ };
+}
+
+async function setupPyodide() {
+ if (pyodide) return pyodide;
+
+ pyodide = await loadPyodide({
+ // TODO: host this ourselves
+ indexURL: `https://cdn.jsdelivr.net/pyodide/v${pkg.version}/full/`
+ });
+
+ // We freeze this to prevent learners from getting the worker into a
+ // weird state. NOTE: this has to come after pyodide is loaded, because
+ // pyodide modifies self while loading.
+ Object.freeze(self);
+
+ // 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(' ');
+ postMessage({ type: 'print', text });
+ }
+
+ function input(text: string) {
+ // TODO: send unique ids to the main thread and the service worker, so we
+ // can have multiple concurrent input requests.
+ postMessage({ type: 'input', text });
+ const request = new XMLHttpRequest();
+ request.open('POST', '/python/intercept-input/', false);
+ request.send(null);
+
+ return request.responseText;
+ }
+
+ // 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.
+
+ // Make print available to python
+ pyodide.registerJsModule('jscustom', {
+ print,
+ input
+ });
+ // TODO: use a fresh global object for each runPython call if we stop terminating
+ // the worker when the user input changes. (See python-test-evaluator.ts)
+ pyodide.runPython(`
+ import jscustom
+ from jscustom import print
+ from jscustom import input
+`);
+
+ return pyodide;
+}
+
+void setupPyodide();
+
+ctx.onmessage = async (e: PythonRunEvent) => {
+ const code = (e.data.code.contents || '').slice();
+ const pyodide = await setupPyodide();
+ // use pyodide.runPythonAsync if we want top-level await
+ pyodide.runPython(code);
+};
diff --git a/tools/client-plugins/browser-scripts/tsconfig.json b/tools/client-plugins/browser-scripts/tsconfig.json
index 7a6d59c7f6f..af99915a71e 100644
--- a/tools/client-plugins/browser-scripts/tsconfig.json
+++ b/tools/client-plugins/browser-scripts/tsconfig.json
@@ -2,6 +2,7 @@
"compilerOptions": {
"target": "es2022",
"module": "CommonJS",
+ "lib": ["WebWorker", "DOM"],
"allowJs": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
diff --git a/tools/client-plugins/browser-scripts/webpack.config.js b/tools/client-plugins/browser-scripts/webpack.config.js
index bd509976c09..447ba57259b 100644
--- a/tools/client-plugins/browser-scripts/webpack.config.js
+++ b/tools/client-plugins/browser-scripts/webpack.config.js
@@ -17,7 +17,8 @@ module.exports = (env = {}) => {
'frame-runner': './frame-runner.ts',
'sass-compile': './sass-compile.ts',
'test-evaluator': './test-evaluator.ts',
- 'python-runner': './python-runner.ts'
+ 'python-worker': './python-worker.ts',
+ 'python-test-evaluator': './python-test-evaluator.ts'
},
devtool: __DEV__ ? 'inline-source-map' : 'source-map',
output: {
@@ -61,17 +62,16 @@ module.exports = (env = {}) => {
]
}
}
- },
- // xterm doesn't bundle its css, so we need to load it ourselves
- {
- test: /\.css$/,
- use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new CopyWebpackPlugin({
- patterns: ['./node_modules/sass.js/dist/sass.sync.js']
+ patterns: [
+ './node_modules/sass.js/dist/sass.sync.js',
+ // TODO: copy this into the css folder, not the js folder
+ './node_modules/xterm/css/xterm.css'
+ ]
}),
new webpack.ProvidePlugin({
process: 'process/browser'