Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
36 changes: 36 additions & 0 deletions src/components/PythonContext/Go.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="mt-20">
<h1
onClick={async () => {
setCount(count + 1)
await runCode('print(1+2)', '4022iiuj')
}}
>
Go
</h1>
</div>
)
}
109 changes: 109 additions & 0 deletions src/components/PythonContext/WorkerContext.tsx
Original file line number Diff line number Diff line change
@@ -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<Remote<PythonRunner> | undefined>
runCode: (code: string, id: string) => Promise<void>
isLoading: boolean
pyodideVersion: string | undefined
output: string[]
}

// Create the context
const WorkerContext = createContext<WorkerContextProps | undefined>(undefined)

interface WorkerProviderProps {
children: ReactNode
}

// Create a provider component
export const WorkerProvider: React.FC<WorkerProviderProps> = ({ children }) => {
const workerRef = useRef<Worker | null>(null)
const runnerRef = useRef<Remote<PythonRunner>>()
const [isLoading, setIsLoading] = useState(false)
const [pyodideVersion, setPyodideVersion] = useState<string | undefined>()
const [output, setOutput] = useState<string[]>([])
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<PythonRunner> = 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 (
<WorkerContext.Provider
value={{ runner: runnerRef, isLoading, pyodideVersion, output, runCode }}
>
{children}
</WorkerContext.Provider>
)
}

// 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
}
78 changes: 78 additions & 0 deletions src/components/PythonContext/python-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
importScripts('https://cdn.jsdelivr.net/pyodide/v0.22.0/full/pyodide.js')

interface Pyodide {
loadPackage: (packages: string[]) => Promise<void>
pyimport: (pkg: string) => micropip
// eslint-disable-next-line @typescript-eslint/no-explicit-any
runPythonAsync: (code: string, namespace?: any) => Promise<void>
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<void>
}

declare global {
interface Window {
loadPyodide: ({
stdout,
}: {
stdout?: (msg: string) => void
}) => Promise<Pyodide>
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)
20 changes: 20 additions & 0 deletions src/components/PythonContext/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export interface Runner {
init: (
stdout: (msg: string) => void,
onLoad: ({ version, banner }: { version: string; banner?: string }) => void,
packages?: string[][]
) => Promise<void>
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<void>
}

export interface PythonConsoleRunner extends Runner {
run: (code: string) => Promise<{ state: string; error?: string }>
}
2 changes: 1 addition & 1 deletion src/components/Sidebar/SidebarContext.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down
13 changes: 13 additions & 0 deletions src/pages/f.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="mt-20">
<WorkerProvider>
<Go />
</WorkerProvider>
</div>
)
}