diff --git a/package-lock.json b/package-lock.json index 39390c6..18ee46c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "ace-builds": "^1.19.0", "autoprefixer": "^10.4.7", "clsx": "^1.2.0", + "comlink": "^4.4.1", "fast-glob": "^3.2.11", "feed": "^4.2.2", "focus-visible": "^5.2.0", diff --git a/package.json b/package.json index 65f0045..267cd3c 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "ace-builds": "^1.19.0", "autoprefixer": "^10.4.7", "clsx": "^1.2.0", + "comlink": "^4.4.1", "fast-glob": "^3.2.11", "feed": "^4.2.2", "focus-visible": "^5.2.0", diff --git a/src/components/PythonContext/Go.tsx b/src/components/PythonContext/Go.tsx new file mode 100644 index 0000000..2da421f --- /dev/null +++ b/src/components/PythonContext/Go.tsx @@ -0,0 +1,36 @@ +import React, { useEffect } from 'react' +import { useWorker } from './WorkerContext' + +export default function Go() { + const { runner, output, runCode } = useWorker() + const [count, setCount] = React.useState(0) + const [stdout, setStdout] = React.useState('') + const id = '4022iiuj' + useEffect(() => { + // check that length is greater than 0 to avoid the initial empty array + if (output.length > 0 && output[0] === id) { + // join them all with newlines except the first one + setStdout(output.slice(1).join('\n')) + } + }, [output]) + // useEffect to check the state of stdout and stderr + useEffect(() => { + if (stdout) { + console.log(stdout) + console.log(output) + } + }, [stdout]) + + return ( +
+

{ + setCount(count + 1) + await runCode('print(1+2)', '4022iiuj') + }} + > + Go +

+
+ ) +} diff --git a/src/components/PythonContext/WorkerContext.tsx b/src/components/PythonContext/WorkerContext.tsx new file mode 100644 index 0000000..aae67a1 --- /dev/null +++ b/src/components/PythonContext/WorkerContext.tsx @@ -0,0 +1,109 @@ +import React, { + createContext, + useContext, + useRef, + useEffect, + ReactNode, + useState, + useCallback, +} from 'react' +import { proxy, Remote, wrap } from 'comlink' +import { Runner, PythonRunner } from './types' + +interface WorkerContextProps { + runner: React.MutableRefObject | undefined> + runCode: (code: string, id: string) => Promise + isLoading: boolean + pyodideVersion: string | undefined + output: string[] +} + +// Create the context +const WorkerContext = createContext(undefined) + +interface WorkerProviderProps { + children: ReactNode +} + +// Create a provider component +export const WorkerProvider: React.FC = ({ children }) => { + const workerRef = useRef(null) + const runnerRef = useRef>() + const [isLoading, setIsLoading] = useState(false) + const [pyodideVersion, setPyodideVersion] = useState() + const [output, setOutput] = useState([]) + const [stderr, setStderr] = useState('') + + const runCode = useCallback(async (code: string, id: string) => { + if (!runnerRef.current) { + throw new Error('Pyodide is not loaded') + } + try { + setOutput([id]) + await runnerRef.current.run(code) + } catch (error) { + console.error('Error:', error) + setStderr('Traceback (most recent call): \n' + error.message) + } + }, []) + // useEffect to check on stdErr + useEffect(() => { + if (stderr) { + console.log(stderr) + } + }, [stderr]) + + useEffect(() => { + const worker = new Worker(new URL('./python-worker', import.meta.url)) + workerRef.current = worker + const init = async () => { + try { + setIsLoading(true) + const runner: Remote = wrap(workerRef.current as Worker) + runnerRef.current = runner + + await runner.init( + proxy((msg: string) => { + setOutput((prev) => [...prev, msg]) + }), + proxy(({ version }) => { + // The runner is ready once the Pyodide version has been set + setPyodideVersion(version) + console.debug('Loaded pyodide version:', version) + }), + [[], []] + ) + } catch (error) { + console.error('Error loading Pyodide:', error) + } finally { + setIsLoading(false) + } + } + init() + // Cleanup worker on unmount + return () => { + if (workerRef.current) { + workerRef.current.terminate() + workerRef.current = null + } + } + }, []) + + // The value provided by the context includes both the worker and functions to interact with it + return ( + + {children} + + ) +} + +// Create a custom hook to access the worker +export const useWorker = (): WorkerContextProps => { + const context = useContext(WorkerContext) + if (context === undefined) { + throw new Error('useWorker must be used within a WorkerProvider') + } + return context +} diff --git a/src/components/PythonContext/python-worker.ts b/src/components/PythonContext/python-worker.ts new file mode 100644 index 0000000..f3d7e73 --- /dev/null +++ b/src/components/PythonContext/python-worker.ts @@ -0,0 +1,78 @@ +importScripts('https://cdn.jsdelivr.net/pyodide/v0.22.0/full/pyodide.js') + +interface Pyodide { + loadPackage: (packages: string[]) => Promise + pyimport: (pkg: string) => micropip + // eslint-disable-next-line @typescript-eslint/no-explicit-any + runPythonAsync: (code: string, namespace?: any) => Promise + version: string + FS: { + readFile: (name: string, options: unknown) => void + writeFile: (name: string, data: string, options: unknown) => void + mkdir: (name: string) => void + rmdir: (name: string) => void + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + globals: any + isPyProxy: (value: unknown) => boolean +} + +interface micropip { + install: (packages: string[]) => Promise +} + +declare global { + interface Window { + loadPyodide: ({ + stdout, + }: { + stdout?: (msg: string) => void + }) => Promise + pyodide: Pyodide + } +} + +// Monkey patch console.log to prevent the script from outputting logs +// eslint-disable-next-line @typescript-eslint/no-empty-function +console.log = () => {} + +import { expose } from 'comlink' + +const python = { + async init( + stdout: (msg: string) => void, + onLoad: ({ version, banner }: { version: string; banner?: string }) => void, + packages: string[][] + ) { + self.pyodide = await self.loadPyodide({ + stdout, + }) + if (packages[0].length > 0) { + await self.pyodide.loadPackage(packages[0]) + } + if (packages[1].length > 0) { + await self.pyodide.loadPackage(['micropip']) + const micropip = self.pyodide.pyimport('micropip') + await micropip.install(packages[1]) + } + const version = self.pyodide.version + onLoad({ version }) + }, + async run(code: string) { + await self.pyodide.runPythonAsync(code) + }, + readFile(name: string) { + return self.pyodide.FS.readFile(name, { encoding: 'utf8' }) + }, + writeFile(name: string, data: string) { + return self.pyodide.FS.writeFile(name, data, { encoding: 'utf8' }) + }, + mkdir(name: string) { + self.pyodide.FS.mkdir(name) + }, + rmdir(name: string) { + self.pyodide.FS.rmdir(name) + }, +} + +expose(python) diff --git a/src/components/PythonContext/types.ts b/src/components/PythonContext/types.ts new file mode 100644 index 0000000..6c245c5 --- /dev/null +++ b/src/components/PythonContext/types.ts @@ -0,0 +1,20 @@ +export interface Runner { + init: ( + stdout: (msg: string) => void, + onLoad: ({ version, banner }: { version: string; banner?: string }) => void, + packages?: string[][] + ) => Promise + interruptExecution: () => void + readFile: (name: string) => void + writeFile: (name: string, data: string) => void + mkdir: (name: string) => void + rmdir: (name: string) => void +} + +export interface PythonRunner extends Runner { + run: (code: string) => Promise +} + +export interface PythonConsoleRunner extends Runner { + run: (code: string) => Promise<{ state: string; error?: string }> +} diff --git a/src/components/Sidebar/SidebarContext.tsx b/src/components/Sidebar/SidebarContext.tsx index 6b67090..43876c1 100644 --- a/src/components/Sidebar/SidebarContext.tsx +++ b/src/components/Sidebar/SidebarContext.tsx @@ -1,4 +1,4 @@ -import { useReducer, createContext, useContext, Reducer } from 'react' +import React, { useReducer, createContext, useContext, Reducer } from 'react' import { produce, Draft } from 'immer' export type RawStatefulNode = { diff --git a/src/pages/f.tsx b/src/pages/f.tsx new file mode 100644 index 0000000..100aae7 --- /dev/null +++ b/src/pages/f.tsx @@ -0,0 +1,13 @@ +import React from 'react' +import { WorkerProvider } from '../components/PythonContext/WorkerContext' +import Go from '../components/PythonContext/Go' + +export default function f() { + return ( +
+ + + +
+ ) +}