mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-09-24 23:01:18 +08:00
feat: handle python input synchronously (#52526)
Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
co-authored by
Shaun Hamilton
parent
8e457e7789
commit
583745e6ca
+3
-1
@@ -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",
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
+33
-3
@@ -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--
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
Generated
+136
-81
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user