mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
* [web] extract PythonExecutionContext and PythonRunner to manage pyodide execution per file * [web] define worker URL in python execution context in order to avoid breaking cjs-based tests * [web] use null check for doc contents to allow running empty python files * [web] flush buffered editor ops before refreshing snapshot for python execution * [web] catch getExecutionContext errors in python runner to prevent unhandled rejections * [web] add PythonRunner unit tests and extract shared WorkerMock * refactor: rename snapshot to state in PythonRunner * fix: remove unnecessary path normalization in PythonExecutionProvider * fix cypress tests GitOrigin-RevId: 9c55586d982fe8df5b90374227005c6b83e94d1f
141 lines
4.0 KiB
TypeScript
141 lines
4.0 KiB
TypeScript
import {
|
|
createContext,
|
|
FC,
|
|
PropsWithChildren,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
} from 'react'
|
|
import getMeta from '@/utils/meta'
|
|
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
|
|
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
|
|
import { useProjectContext } from '@/shared/context/project-context'
|
|
import {
|
|
PythonRunner,
|
|
ExecutionContext,
|
|
} from '@/features/ide-react/components/editor/python/python-runner'
|
|
|
|
// Worker factory lives here (a .tsx file) so that the full
|
|
// `new Worker(new URL(..., import.meta.url))` expression is in a single place
|
|
// where webpack 5 can statically detect it and create a proper worker bundle.
|
|
// Keeping import.meta.url out of .ts files also avoids Node.js 24 switching to
|
|
// ESM mode and breaking CJS-based test loading via @babel/register.
|
|
const createPyodideWorker = () =>
|
|
new Worker(
|
|
/* webpackChunkName: "pyodide-worker" */
|
|
new URL('../components/editor/python/pyodide.worker.ts', import.meta.url),
|
|
{ type: 'module' }
|
|
)
|
|
|
|
export interface PythonExecutionContextValue {
|
|
getPythonRunner: (fileId: string) => PythonRunner
|
|
}
|
|
|
|
export const PythonExecutionContext = createContext<
|
|
PythonExecutionContextValue | undefined
|
|
>(undefined)
|
|
|
|
export const PythonExecutionProvider: FC<PropsWithChildren> = ({
|
|
children,
|
|
}) => {
|
|
const { openDocs } = useEditorManagerContext()
|
|
const { projectSnapshot } = useProjectContext()
|
|
const { pathInFolder } = useFileTreePathContext()
|
|
const runnersRef = useRef(new Map<string, PythonRunner>())
|
|
const baseAssetPathRef = useRef<string | null>(null)
|
|
|
|
const pathInFolderRef = useRef(pathInFolder)
|
|
pathInFolderRef.current = pathInFolder
|
|
|
|
// Refreshes the project snapshot and resolves the source code and all project
|
|
// files for the given fileId, to be passed to the executor for running.
|
|
const getExecutionContext = useCallback(
|
|
async (fileId: string): Promise<ExecutionContext | null> => {
|
|
await openDocs.awaitBufferedOps(AbortSignal.timeout(5000))
|
|
await projectSnapshot.refresh()
|
|
|
|
const relativePath = pathInFolderRef.current(fileId)
|
|
if (!relativePath) {
|
|
return null
|
|
}
|
|
|
|
const code = projectSnapshot.getDocContents(relativePath)
|
|
if (code == null) {
|
|
return null
|
|
}
|
|
|
|
const docPaths = projectSnapshot.getDocPaths()
|
|
const files = docPaths
|
|
.map(docPath => {
|
|
const content = projectSnapshot.getDocContents(docPath)
|
|
return content != null ? { relativePath: docPath, content } : null
|
|
})
|
|
.filter(
|
|
(f): f is { relativePath: string; content: string } => f != null
|
|
)
|
|
|
|
return { code, files }
|
|
},
|
|
[openDocs, projectSnapshot]
|
|
)
|
|
|
|
const getPythonRunner = useCallback(
|
|
(fileId: string): PythonRunner => {
|
|
const existing = runnersRef.current.get(fileId)
|
|
if (existing) {
|
|
return existing
|
|
}
|
|
|
|
if (!baseAssetPathRef.current) {
|
|
baseAssetPathRef.current = new URL(
|
|
getMeta('ol-baseAssetPath'),
|
|
window.location.href
|
|
).toString()
|
|
}
|
|
|
|
const runner = new PythonRunner(
|
|
fileId,
|
|
baseAssetPathRef.current,
|
|
() => getExecutionContext(fileId),
|
|
createPyodideWorker
|
|
)
|
|
runner.init()
|
|
runnersRef.current.set(fileId, runner)
|
|
return runner
|
|
},
|
|
[getExecutionContext]
|
|
)
|
|
|
|
useEffect(() => {
|
|
const runners = runnersRef.current
|
|
return () => {
|
|
for (const runner of runners.values()) {
|
|
runner.destroy()
|
|
}
|
|
runners.clear()
|
|
}
|
|
}, [])
|
|
|
|
const value = useMemo(() => ({ getPythonRunner }), [getPythonRunner])
|
|
|
|
return (
|
|
<PythonExecutionContext.Provider value={value}>
|
|
{children}
|
|
</PythonExecutionContext.Provider>
|
|
)
|
|
}
|
|
|
|
export const usePythonExecutionContext = (): PythonExecutionContextValue => {
|
|
const context = useContext(PythonExecutionContext)
|
|
|
|
if (!context) {
|
|
throw new Error(
|
|
'usePythonExecutionContext is only available inside PythonExecutionContext.Provider'
|
|
)
|
|
}
|
|
|
|
return context
|
|
}
|