python-execution-context.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import {
  2. createContext,
  3. FC,
  4. PropsWithChildren,
  5. useCallback,
  6. useContext,
  7. useEffect,
  8. useMemo,
  9. useRef,
  10. } from 'react'
  11. import getMeta from '@/utils/meta'
  12. import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
  13. import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
  14. import { useProjectContext } from '@/shared/context/project-context'
  15. import {
  16. PythonRunner,
  17. ExecutionContext,
  18. } from '@/features/ide-react/components/editor/python/python-runner'
  19. // Worker factory lives here (a .tsx file) so that the full
  20. // `new Worker(new URL(..., import.meta.url))` expression is in a single place
  21. // where webpack 5 can statically detect it and create a proper worker bundle.
  22. // Keeping import.meta.url out of .ts files also avoids Node.js 24 switching to
  23. // ESM mode and breaking CJS-based test loading via @babel/register.
  24. const createPyodideWorker = () =>
  25. new Worker(
  26. /* webpackChunkName: "pyodide-worker" */
  27. new URL('../components/editor/python/pyodide.worker.ts', import.meta.url),
  28. { type: 'module' }
  29. )
  30. export interface PythonExecutionContextValue {
  31. getPythonRunner: (fileId: string) => PythonRunner
  32. }
  33. export const PythonExecutionContext = createContext<
  34. PythonExecutionContextValue | undefined
  35. >(undefined)
  36. export const PythonExecutionProvider: FC<PropsWithChildren> = ({
  37. children,
  38. }) => {
  39. const { openDocs } = useEditorManagerContext()
  40. const { projectSnapshot } = useProjectContext()
  41. const { pathInFolder } = useFileTreePathContext()
  42. const runnersRef = useRef(new Map<string, PythonRunner>())
  43. const baseAssetPathRef = useRef<string | null>(null)
  44. const pathInFolderRef = useRef(pathInFolder)
  45. pathInFolderRef.current = pathInFolder
  46. // Refreshes the project snapshot and resolves the source code and all project
  47. // files for the given fileId, to be passed to the executor for running.
  48. const getExecutionContext = useCallback(
  49. async (fileId: string): Promise<ExecutionContext | null> => {
  50. await openDocs.awaitBufferedOps(AbortSignal.timeout(5000))
  51. await projectSnapshot.refresh()
  52. const relativePath = pathInFolderRef.current(fileId)
  53. if (!relativePath) {
  54. return null
  55. }
  56. const code = projectSnapshot.getDocContents(relativePath)
  57. if (code == null) {
  58. return null
  59. }
  60. const docPaths = projectSnapshot.getDocPaths()
  61. const files = docPaths
  62. .map(docPath => {
  63. const content = projectSnapshot.getDocContents(docPath)
  64. return content != null ? { relativePath: docPath, content } : null
  65. })
  66. .filter(
  67. (f): f is { relativePath: string; content: string } => f != null
  68. )
  69. return { code, files }
  70. },
  71. [openDocs, projectSnapshot]
  72. )
  73. const getPythonRunner = useCallback(
  74. (fileId: string): PythonRunner => {
  75. const existing = runnersRef.current.get(fileId)
  76. if (existing) {
  77. return existing
  78. }
  79. if (!baseAssetPathRef.current) {
  80. baseAssetPathRef.current = new URL(
  81. getMeta('ol-baseAssetPath'),
  82. window.location.href
  83. ).toString()
  84. }
  85. const runner = new PythonRunner(
  86. fileId,
  87. baseAssetPathRef.current,
  88. () => getExecutionContext(fileId),
  89. createPyodideWorker
  90. )
  91. runner.init()
  92. runnersRef.current.set(fileId, runner)
  93. return runner
  94. },
  95. [getExecutionContext]
  96. )
  97. useEffect(() => {
  98. const runners = runnersRef.current
  99. return () => {
  100. for (const runner of runners.values()) {
  101. runner.destroy()
  102. }
  103. runners.clear()
  104. }
  105. }, [])
  106. const value = useMemo(() => ({ getPythonRunner }), [getPythonRunner])
  107. return (
  108. <PythonExecutionContext.Provider value={value}>
  109. {children}
  110. </PythonExecutionContext.Provider>
  111. )
  112. }
  113. export const usePythonExecutionContext = (): PythonExecutionContextValue => {
  114. const context = useContext(PythonExecutionContext)
  115. if (!context) {
  116. throw new Error(
  117. 'usePythonExecutionContext is only available inside PythonExecutionContext.Provider'
  118. )
  119. }
  120. return context
  121. }