feat: handle python input synchronously (#52526)

Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
Oliver Eyton-Williams
2023-12-18 11:22:26 -08:00
committed by GitHub
co-authored by Shaun Hamilton
parent 8e457e7789
commit 583745e6ca
24 changed files with 760 additions and 660 deletions
+3 -1
View File
@@ -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",
+1 -1
View File
@@ -20,7 +20,7 @@
]
},
{
"source": "{misc/*.js,sw.js}",
"source": "{misc/*.js,sw.js,python-input-sw.js}",
"headers": [
{
"key": "Cache-Control",
@@ -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 => {
)}
</ReflexContainer>
{displayPreviewPortal && (
<PreviewPortal windowTitle={windowTitle}>{preview}</PreviewPortal>
<PreviewPortal onResize={onPreviewResize} windowTitle={windowTitle}>
{preview}
</PreviewPortal>
)}
</div>
);
@@ -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<MobileLayoutProps, MobileLayoutState> {
hasPreview,
notes,
preview,
onPreviewResize,
showPreviewPane,
showPreviewPortal,
removePortalWindow,
@@ -328,7 +330,9 @@ class MobileLayout extends Component<MobileLayoutProps, MobileLayoutState> {
)}
</Tabs>
{displayPreviewPortal && (
<PreviewPortal windowTitle={windowTitle}>{preview}</PreviewPortal>
<PreviewPortal onResize={onPreviewResize} windowTitle={windowTitle}>
{preview}
</PreviewPortal>
)}
</>
);
@@ -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<PreviewProps, 'disableIframe' | 'previewMounted'>) => {
return (
previewMounted,
challengeType,
xtermFitRef
}: Pick<PreviewProps, 'disableIframe' | 'previewMounted'> & {
challengeType: number;
xtermFitRef: React.MutableRefObject<FitAddon | null>;
}) => {
return challengeType === challengeTypes.python ? (
<XtermTerminal xtermFitRef={xtermFitRef} />
) : (
<Preview
className='full-height'
disableIframe={disableIframe}
@@ -214,6 +224,7 @@ function ShowClassic({
const containerRef = useRef<HTMLElement>(null);
const editorRef = useRef<editor.IStandaloneCodeEditor>();
const instructionsPanelRef = useRef<HTMLDivElement>(null);
const xtermFitRef = useRef<FitAddon | null>(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={<Notes notes={notes} />}
onPreviewResize={onPreviewResize}
preview={
<StepPreview
challengeType={challengeType}
disableIframe={resizing}
previewMounted={previewMounted}
xtermFitRef={xtermFitRef}
/>
}
windowTitle={windowTitle}
@@ -470,10 +486,13 @@ function ShowClassic({
isFirstStep={isFirstStep}
layoutState={layout}
notes={<Notes notes={notes} />}
onPreviewResize={onPreviewResize}
preview={
<StepPreview
challengeType={challengeType}
disableIframe={resizing}
previewMounted={previewMounted}
xtermFitRef={xtermFitRef}
/>
}
resizeProps={resizeProps}
@@ -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<FitAddon | null>;
}) => {
const termContainerRef = useRef<HTMLDivElement | null>(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 (
<div ref={termContainerRef}>
<link rel='stylesheet' href='/js/xterm.css' />
</div>
);
};
@@ -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<PreviewPortalProps> {
this.props.removePortalWindow();
});
this.externalWindow?.addEventListener('resize', () => {
this.props.onResize();
});
this.props.storePortalWindow(this.externalWindow);
// close the portal if the main window closes
@@ -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');
}
@@ -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);
});
});
});
@@ -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, '')
];
@@ -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 }) {
+40 -21
View File
@@ -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<ChallengeFile>;
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<BuildChallengeData, 'build' | 'sources'>,
{ 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<typeof createWorker>;
@@ -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<BuildResult> | 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)
}))
);
}
@@ -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();
@@ -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);
}
+23
View File
@@ -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);
}
});
@@ -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--
+45 -45
View File
@@ -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
);
}
+136 -81
View File
@@ -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
-1
View File
@@ -22,7 +22,6 @@ export interface InitTestFrameArg {
};
getUserInput?: (fileName: string) => string;
loadEnzyme?: () => void;
transformedPython?: string;
}
export type FrameWindow = Window &
@@ -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<string>;
type Print = (...args: unknown[]) => void;
type ResetTerminal = () => void;
type EvaluatedTeststring = {
input: string[];
test: () => Promise<unknown>;
};
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 evaluatedTestString = await new 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);
}
})
);
// 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
}
};
}
};
}
@@ -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<unknown>;
};
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<unknown>(
(resolve, reject) => {
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);
}
}
);
// 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
}
});
}
};
@@ -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);
};
@@ -2,6 +2,7 @@
"compilerOptions": {
"target": "es2022",
"module": "CommonJS",
"lib": ["WebWorker", "DOM"],
"allowJs": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
@@ -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'