local-compile-context.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891
  1. import {
  2. FC,
  3. createContext,
  4. useCallback,
  5. useContext,
  6. useEffect,
  7. useMemo,
  8. useRef,
  9. useState,
  10. Dispatch,
  11. SetStateAction,
  12. } from 'react'
  13. import useScopeValue from '../hooks/use-scope-value'
  14. import useScopeValueSetterOnly from '../hooks/use-scope-value-setter-only'
  15. import usePersistedState from '../hooks/use-persisted-state'
  16. import useAbortController from '../hooks/use-abort-controller'
  17. import DocumentCompiler from '../../features/pdf-preview/util/compiler'
  18. import {
  19. send,
  20. sendMB,
  21. sendMBOnce,
  22. sendMBSampled,
  23. } from '../../infrastructure/event-tracking'
  24. import {
  25. buildLogEntryAnnotations,
  26. buildRuleCounts,
  27. buildRuleDeltas,
  28. handleLogFiles,
  29. handleOutputFiles,
  30. } from '../../features/pdf-preview/util/output-files'
  31. import { useProjectContext } from './project-context'
  32. import { useEditorContext } from './editor-context'
  33. import { buildFileList } from '../../features/pdf-preview/util/file-list'
  34. import { useLayoutContext } from './layout-context'
  35. import { useUserContext } from './user-context'
  36. import { useFileTreeData } from '@/shared/context/file-tree-data-context'
  37. import { useDetachContext } from '@/shared/context/detach-context'
  38. import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
  39. import { useUserSettingsContext } from '@/shared/context/user-settings-context'
  40. import { useFeatureFlag } from '@/shared/context/split-test-context'
  41. import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
  42. import { getJSON } from '@/infrastructure/fetch-json'
  43. import { CompileResponseData } from '../../../../types/compile'
  44. import {
  45. PdfScrollPosition,
  46. usePdfScrollPosition,
  47. } from '@/shared/hooks/use-pdf-scroll-position'
  48. import { PdfFileDataList } from '@/features/pdf-preview/util/types'
  49. import { isSplitTestEnabled } from '@/utils/splitTestUtils'
  50. import { captureException } from '@/infrastructure/error-reporter'
  51. import OError from '@overleaf/o-error'
  52. import getMeta from '@/utils/meta'
  53. import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
  54. import { useRailContext } from '@/features/ide-redesign/contexts/rail-context'
  55. type PdfFile = Record<string, any>
  56. export type CompileContext = {
  57. autoCompile: boolean
  58. clearingCache: boolean
  59. clsiServerId?: string
  60. codeCheckFailed: boolean
  61. compiling: boolean
  62. deliveryLatencies: Record<string, any>
  63. draft: boolean
  64. error?: string
  65. fileList?: PdfFileDataList
  66. hasChanges: boolean
  67. hasShortCompileTimeout: boolean
  68. highlights?: Record<string, any>[]
  69. isProjectOwner: boolean
  70. logEntries?: Record<string, any>
  71. logEntryAnnotations?: Record<string, any>
  72. outputFilesArchive?: string
  73. pdfDownloadUrl?: string
  74. pdfFile?: PdfFile
  75. pdfUrl?: string
  76. pdfViewer?: string
  77. position?: PdfScrollPosition
  78. rawLog?: string
  79. setAutoCompile: (value: boolean) => void
  80. setDraft: (value: any) => void
  81. setError: (value: any) => void
  82. setHasLintingError: (value: any) => void // only for storybook
  83. setHighlights: (value: any) => void
  84. setPosition: Dispatch<SetStateAction<PdfScrollPosition>>
  85. setShowCompileTimeWarning: (value: any) => void
  86. setShowLogs: (value: boolean) => void
  87. toggleLogs: () => void
  88. setStopOnFirstError: (value: boolean) => void
  89. setStopOnValidationError: (value: boolean) => void
  90. showCompileTimeWarning: boolean
  91. showLogs: boolean
  92. stopOnFirstError: boolean
  93. stopOnValidationError: boolean
  94. stoppedOnFirstError: boolean
  95. uncompiled?: boolean
  96. validationIssues?: Record<string, any>
  97. firstRenderDone: (metrics: {
  98. latencyFetch: number
  99. latencyRender: number | undefined
  100. pdfCachingMetrics: { viewerId: string }
  101. }) => void
  102. cleanupCompileResult?: () => void
  103. animateCompileDropdownArrow: boolean
  104. editedSinceCompileStarted: boolean
  105. lastCompileOptions: any
  106. setAnimateCompileDropdownArrow: (value: boolean) => void
  107. recompileFromScratch: () => void
  108. setCompiling: (value: boolean) => void
  109. startCompile: (options?: any) => void
  110. stopCompile: () => void
  111. setChangedAt: (value: any) => void
  112. clearCache: () => void
  113. syncToEntry: (value: any, keepCurrentView?: boolean) => void
  114. recordAction: (action: string) => void
  115. }
  116. export const LocalCompileContext = createContext<CompileContext | undefined>(
  117. undefined
  118. )
  119. export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
  120. children,
  121. }) => {
  122. const { hasPremiumCompile, isProjectOwner } = useEditorContext()
  123. const { openDocWithId, openDocs, currentDocument } = useEditorManagerContext()
  124. const { role } = useDetachContext()
  125. const newEditor = useIsNewEditorEnabled()
  126. const {
  127. _id: projectId,
  128. rootDocId,
  129. joinedOnce,
  130. imageName,
  131. compiler: compilerName,
  132. } = useProjectContext()
  133. const { pdfPreviewOpen } = useLayoutContext()
  134. const { openTab: openRailTab } = useRailContext()
  135. const { features, alphaProgram, labsProgram } = useUserContext()
  136. const { fileTreeData } = useFileTreeData()
  137. const { findEntityByPath } = useFileTreePathContext()
  138. // whether a compile is in progress
  139. const [compiling, setCompiling] = useState(false)
  140. // whether to show the compile time warning
  141. const [showCompileTimeWarning, setShowCompileTimeWarning] = useState(false)
  142. const [hasShortCompileTimeout, setHasShortCompileTimeout] = useState(false)
  143. // the log entries parsed from the compile output log
  144. const [logEntries, setLogEntries] = useScopeValueSetterOnly('pdf.logEntries')
  145. // annotations for display in the editor, built from the log entries
  146. const [logEntryAnnotations, setLogEntryAnnotations] = useScopeValue(
  147. 'pdf.logEntryAnnotations'
  148. )
  149. // the PDF viewer and whether syntax validation is enabled globally
  150. const { userSettings } = useUserSettingsContext()
  151. const { pdfViewer, syntaxValidation } = userSettings
  152. // the URL for downloading the PDF
  153. const [, setPdfDownloadUrl] =
  154. useScopeValueSetterOnly<string>('pdf.downloadUrl')
  155. // the URL for loading the PDF in the preview pane
  156. const [, setPdfUrl] = useScopeValueSetterOnly<string>('pdf.url')
  157. // low level details for metrics
  158. const [pdfFile, setPdfFile] = useState<PdfFile | undefined>()
  159. useEffect(() => {
  160. setPdfDownloadUrl(pdfFile?.pdfDownloadUrl)
  161. setPdfUrl(pdfFile?.pdfUrl)
  162. }, [pdfFile, setPdfDownloadUrl, setPdfUrl])
  163. // the project is considered to be "uncompiled" if a doc has changed, or finished saving, since the last compile started.
  164. const [uncompiled, setUncompiled] = useScopeValue('pdf.uncompiled')
  165. // whether a doc has been edited since the last compile started
  166. const [editedSinceCompileStarted, setEditedSinceCompileStarted] =
  167. useState(false)
  168. // the id of the CLSI server which ran the compile
  169. const [clsiServerId, setClsiServerId] = useState<string>()
  170. // data received in response to a compile request
  171. const [data, setData] = useState<CompileResponseData>()
  172. // the rootDocId used in the most recent compile request, which may not be the
  173. // same as the project rootDocId. This is used to calculate correct paths when
  174. // parsing the compile logs
  175. const lastCompileRootDocId = data ? (data.rootDocId ?? rootDocId) : null
  176. // callback to be invoked for PdfJsMetrics
  177. const [firstRenderDone, setFirstRenderDone] = useState(() => () => {})
  178. // latencies of compile/pdf download/rendering
  179. const [deliveryLatencies, setDeliveryLatencies] = useState({})
  180. // whether the project has been compiled yet
  181. const [compiledOnce, setCompiledOnce] = useState(false)
  182. // fetch initial compile response from cache
  183. const [initialCompileFromCache, setInitialCompileFromCache] = useState(
  184. getMeta('ol-projectOwnerHasPremiumOnPageLoad') &&
  185. isSplitTestEnabled('populate-clsi-cache') &&
  186. // Avoid fetching the initial compile from cache in PDF detach tab
  187. role !== 'detached'
  188. )
  189. // fetch of initial compile from cache is pending
  190. const [pendingInitialCompileFromCache, setPendingInitialCompileFromCache] =
  191. useState(false)
  192. // Raw data from clsi-cache, will need post-processing and check settings
  193. const [dataFromCache, setDataFromCache] = useState<CompileResponseData>()
  194. // whether the cache is being cleared
  195. const [clearingCache, setClearingCache] = useState(false)
  196. // whether the logs should be visible
  197. const [showLogs, setShowLogs] = useState(false)
  198. // whether the compile dropdown arrow should be animated
  199. const [animateCompileDropdownArrow, setAnimateCompileDropdownArrow] =
  200. useState(false)
  201. const toggleLogs = useCallback(() => {
  202. setShowLogs(prev => {
  203. if (!prev) {
  204. sendMBOnce('ide-open-logs-once')
  205. }
  206. return !prev
  207. })
  208. }, [setShowLogs])
  209. // an error that occurred
  210. const [error, setError] = useState<string>()
  211. // the list of files that can be downloaded
  212. const [fileList, setFileList] = useState<PdfFileDataList>()
  213. // the raw contents of the log file
  214. const [rawLog, setRawLog] = useState<string>()
  215. // validation issues from CLSI
  216. const [validationIssues, setValidationIssues] = useState()
  217. // areas to highlight on the PDF, from synctex
  218. const [highlights, setHighlights] = useState()
  219. const [position, setPosition] = usePdfScrollPosition(lastCompileRootDocId)
  220. // whether autocompile is switched on
  221. const [autoCompile, setAutoCompile] = usePersistedState(
  222. `autocompile_enabled:${projectId}`,
  223. false,
  224. true
  225. )
  226. // whether the compile should run in draft mode
  227. const [draft, setDraft] = usePersistedState(`draft:${projectId}`, false, true)
  228. // whether compiling should stop on first error
  229. const [stopOnFirstError, setStopOnFirstError] = usePersistedState(
  230. `stop_on_first_error:${projectId}`,
  231. false,
  232. true
  233. )
  234. // whether the last compiles stopped on first error
  235. const [stoppedOnFirstError, setStoppedOnFirstError] = useState(false)
  236. // whether compiling should be prevented if there are linting errors
  237. const [stopOnValidationError, setStopOnValidationError] = usePersistedState(
  238. `stop_on_validation_error:${projectId}`,
  239. true,
  240. true
  241. )
  242. // whether the editor linter found errors
  243. const [hasLintingError, setHasLintingError] = useScopeValue('hasLintingError')
  244. // the timestamp that a doc was last changed
  245. const [changedAt, setChangedAt] = useState(0)
  246. const { signal } = useAbortController()
  247. const cleanupCompileResult = useCallback(() => {
  248. setPdfFile(undefined)
  249. setLogEntries(null)
  250. setLogEntryAnnotations({})
  251. }, [setPdfFile, setLogEntries, setLogEntryAnnotations])
  252. const compilingRef = useRef(false)
  253. useEffect(() => {
  254. compilingRef.current = compiling
  255. }, [compiling])
  256. const _buildLogEntryAnnotations = useCallback(
  257. (entries: any) =>
  258. buildLogEntryAnnotations(entries, fileTreeData, lastCompileRootDocId),
  259. [fileTreeData, lastCompileRootDocId]
  260. )
  261. const buildLogEntryAnnotationsRef = useRef(_buildLogEntryAnnotations)
  262. useEffect(() => {
  263. buildLogEntryAnnotationsRef.current = _buildLogEntryAnnotations
  264. }, [_buildLogEntryAnnotations])
  265. // the document compiler
  266. const [compiler] = useState(() => {
  267. return new DocumentCompiler({
  268. projectId,
  269. setChangedAt,
  270. setCompiling,
  271. setData,
  272. setFirstRenderDone,
  273. setDeliveryLatencies,
  274. setError,
  275. cleanupCompileResult,
  276. compilingRef,
  277. signal,
  278. openDocs,
  279. })
  280. })
  281. // keep currentDoc in sync with the compiler
  282. useEffect(() => {
  283. compiler.currentDoc = currentDocument
  284. }, [compiler, currentDocument])
  285. // keep the project rootDocId in sync with the compiler
  286. useEffect(() => {
  287. compiler.projectRootDocId = rootDocId
  288. }, [compiler, rootDocId])
  289. // keep draft setting in sync with the compiler
  290. useEffect(() => {
  291. compiler.setOption('draft', draft)
  292. }, [compiler, draft])
  293. // keep stop on first error setting in sync with the compiler
  294. useEffect(() => {
  295. compiler.setOption('stopOnFirstError', stopOnFirstError)
  296. }, [compiler, stopOnFirstError])
  297. useEffect(() => {
  298. setUncompiled(changedAt > 0)
  299. }, [setUncompiled, changedAt])
  300. useEffect(() => {
  301. setEditedSinceCompileStarted(changedAt > 0)
  302. }, [setEditedSinceCompileStarted, changedAt])
  303. // try to fetch the last compile result after opening the project, potentially before joining the project.
  304. useEffect(() => {
  305. if (initialCompileFromCache && !pendingInitialCompileFromCache) {
  306. setPendingInitialCompileFromCache(true)
  307. getJSON(`/project/${projectId}/output/cached/output.overleaf.json`)
  308. .then((data: any) => {
  309. // Hand data over to next effect, it will wait for project/doc loading.
  310. setDataFromCache(data)
  311. })
  312. .catch(() => {
  313. // Let the isAutoCompileOnLoad effect take over
  314. setInitialCompileFromCache(false)
  315. setPendingInitialCompileFromCache(false)
  316. })
  317. }
  318. }, [projectId, initialCompileFromCache, pendingInitialCompileFromCache])
  319. // Maybe adopt the compile from cache
  320. useEffect(() => {
  321. if (!dataFromCache) return // no compile from cache available
  322. if (!joinedOnce) return // wait for joinProject, it populates the file-tree.
  323. if (!currentDocument) return // wait for current doc to load, it affects the rootDoc override
  324. if (compiledOnce) return // regular compile triggered
  325. // Gracefully access file-tree and getRootDocOverride
  326. let settingsUpToDate = false
  327. try {
  328. dataFromCache.rootDocId = findEntityByPath(
  329. dataFromCache.options?.rootResourcePath || ''
  330. )?.entity?._id
  331. const rootDocOverride = compiler.getRootDocOverrideId() || rootDocId
  332. settingsUpToDate =
  333. rootDocOverride === dataFromCache.rootDocId &&
  334. dataFromCache.options.imageName === imageName &&
  335. dataFromCache.options.compiler === compilerName &&
  336. dataFromCache.options.draft === draft &&
  337. // Allow stopOnFirstError to be enabled in the compile from cache and disabled locally.
  338. // Compiles that passed with stopOnFirstError=true will also pass with stopOnFirstError=false. The inverse does not hold, and we need to recompile.
  339. !!dataFromCache.options.stopOnFirstError >= stopOnFirstError
  340. } catch (err) {
  341. captureException(
  342. OError.tag(err as unknown as Error, 'validate compile options', {
  343. options: dataFromCache.options,
  344. })
  345. )
  346. }
  347. if (settingsUpToDate) {
  348. sendMB('compile-from-cache', { projectId })
  349. setData(dataFromCache)
  350. setCompiledOnce(true)
  351. }
  352. setDataFromCache(undefined)
  353. setInitialCompileFromCache(false)
  354. setPendingInitialCompileFromCache(false)
  355. }, [
  356. projectId,
  357. dataFromCache,
  358. joinedOnce,
  359. currentDocument,
  360. compiledOnce,
  361. rootDocId,
  362. findEntityByPath,
  363. compiler,
  364. compilerName,
  365. imageName,
  366. stopOnFirstError,
  367. draft,
  368. ])
  369. // always compile the PDF once after opening the project, after the doc has loaded
  370. useEffect(() => {
  371. if (
  372. !compiledOnce &&
  373. currentDocument &&
  374. !initialCompileFromCache &&
  375. !pendingInitialCompileFromCache
  376. ) {
  377. setCompiledOnce(true)
  378. compiler.compile({ isAutoCompileOnLoad: true })
  379. }
  380. }, [
  381. compiledOnce,
  382. currentDocument,
  383. initialCompileFromCache,
  384. pendingInitialCompileFromCache,
  385. compiler,
  386. ])
  387. useEffect(() => {
  388. setHasShortCompileTimeout(
  389. features?.compileTimeout !== undefined && features.compileTimeout <= 60
  390. )
  391. }, [features])
  392. useEffect(() => {
  393. if (hasShortCompileTimeout && compiling && isProjectOwner) {
  394. const timeout = window.setTimeout(() => {
  395. setShowCompileTimeWarning(true)
  396. }, 30000)
  397. return () => {
  398. window.clearTimeout(timeout)
  399. }
  400. }
  401. }, [compiling, isProjectOwner, hasShortCompileTimeout])
  402. const hasCompileLogsEvents = useFeatureFlag('compile-log-events')
  403. // compare log entry counts with the previous compile, and record actions between compiles
  404. // these are refs rather than state so they don't trigger the effect to run
  405. const previousRuleCountsRef = useRef<{
  406. ruleCounts: Record<string, number>
  407. rootDocId: string
  408. } | null>(null)
  409. const recordedActionsRef = useRef<Record<string, boolean>>({})
  410. const recordAction = useCallback((action: string) => {
  411. recordedActionsRef.current[action] = true
  412. }, [])
  413. // handle the data returned from a compile request
  414. // note: this should _only_ run when `data` changes,
  415. // the other dependencies must all be static
  416. useEffect(() => {
  417. if (!joinedOnce) return // wait for joinProject, it populates the premium flags.
  418. const abortController = new AbortController()
  419. const recordedActions = recordedActionsRef.current
  420. recordedActionsRef.current = {}
  421. if (data) {
  422. if (data.clsiServerId) {
  423. setClsiServerId(data.clsiServerId) // set in scope, for PdfSynctexController
  424. }
  425. if (data.outputFiles) {
  426. const outputFiles = new Map()
  427. for (const outputFile of data.outputFiles) {
  428. // Use a shadow-copy, we will update it in place and append to .url.
  429. outputFiles.set(outputFile.path, { ...outputFile })
  430. }
  431. // set the PDF context
  432. if (data.status === 'success') {
  433. setPdfFile(handleOutputFiles(outputFiles, projectId, data))
  434. }
  435. setFileList(buildFileList(outputFiles, data))
  436. // handle log files
  437. // asynchronous (TODO: cancel on new compile?)
  438. setLogEntryAnnotations(null)
  439. setLogEntries(null)
  440. setRawLog(undefined)
  441. handleLogFiles(outputFiles, data, abortController.signal).then(
  442. (result: Record<string, any>) => {
  443. setRawLog(result.log)
  444. setLogEntries(result.logEntries)
  445. setLogEntryAnnotations(
  446. buildLogEntryAnnotationsRef.current(result.logEntries.all)
  447. )
  448. // sample compile stats for real users
  449. if (!alphaProgram) {
  450. if (['success', 'stopped-on-first-error'].includes(data.status)) {
  451. sendMBSampled(
  452. 'compile-result',
  453. {
  454. errors: result.logEntries.errors.length,
  455. warnings: result.logEntries.warnings.length,
  456. typesetting: result.logEntries.typesetting.length,
  457. newPdfPreview: true, // TODO: is this useful?
  458. stopOnFirstError: data.options.stopOnFirstError,
  459. },
  460. 0.01
  461. )
  462. }
  463. if (hasCompileLogsEvents || labsProgram) {
  464. const ruleCounts = buildRuleCounts(
  465. result.logEntries.all
  466. ) as Record<string, number>
  467. const rootDocId = data.rootDocId || compiler.projectRootDocId
  468. const previousRuleCounts = previousRuleCountsRef.current
  469. previousRuleCountsRef.current = { ruleCounts, rootDocId }
  470. const ruleDeltas =
  471. previousRuleCounts &&
  472. previousRuleCounts.rootDocId === rootDocId
  473. ? buildRuleDeltas(ruleCounts, previousRuleCounts.ruleCounts)
  474. : {}
  475. sendMB('compile-log-entries', {
  476. status: data.status,
  477. stopOnFirstError: data.options.stopOnFirstError,
  478. isAutoCompileOnLoad: !!data.options.isAutoCompileOnLoad,
  479. isAutoCompileOnChange: !!data.options.isAutoCompileOnChange,
  480. rootDocId,
  481. ...recordedActions,
  482. ...ruleCounts,
  483. ...ruleDeltas,
  484. })
  485. }
  486. }
  487. }
  488. )
  489. }
  490. switch (data.status) {
  491. case 'success':
  492. setError(undefined)
  493. setShowLogs(false)
  494. break
  495. case 'stopped-on-first-error':
  496. setError(undefined)
  497. setShowLogs(true)
  498. break
  499. case 'clsi-maintenance':
  500. case 'compile-in-progress':
  501. case 'exited':
  502. case 'failure':
  503. case 'project-too-large':
  504. case 'rate-limited':
  505. case 'terminated':
  506. case 'too-recently-compiled':
  507. setError(data.status)
  508. break
  509. case 'timedout':
  510. setError('timedout')
  511. if (!hasPremiumCompile && isProjectOwner) {
  512. send(
  513. 'subscription-funnel',
  514. 'editor-click-feature',
  515. 'compile-timeout'
  516. )
  517. }
  518. break
  519. case 'autocompile-backoff':
  520. if (!data.options.isAutoCompileOnLoad) {
  521. setError('autocompile-disabled')
  522. setAutoCompile(false)
  523. }
  524. break
  525. case 'unavailable':
  526. setError('clsi-unavailable')
  527. break
  528. case 'validation-problems':
  529. setError('validation-problems')
  530. setValidationIssues(data.validationProblems)
  531. break
  532. default:
  533. setError('error')
  534. break
  535. }
  536. setStoppedOnFirstError(data.status === 'stopped-on-first-error')
  537. }
  538. return () => {
  539. abortController.abort()
  540. }
  541. }, [
  542. joinedOnce,
  543. data,
  544. alphaProgram,
  545. labsProgram,
  546. features,
  547. hasCompileLogsEvents,
  548. hasPremiumCompile,
  549. isProjectOwner,
  550. projectId,
  551. setAutoCompile,
  552. setClsiServerId,
  553. setLogEntries,
  554. setLogEntryAnnotations,
  555. setPdfFile,
  556. compiler,
  557. ])
  558. // switch to logs if there's an error
  559. useEffect(() => {
  560. if (error) {
  561. setShowLogs(true)
  562. }
  563. }, [error])
  564. // whether there has been an autocompile linting error, if syntax validation is switched on
  565. const autoCompileLintingError = Boolean(
  566. autoCompile && syntaxValidation && hasLintingError
  567. )
  568. const codeCheckFailed = stopOnValidationError && autoCompileLintingError
  569. // the project is available for auto-compiling
  570. // (autocompile is enabled, the PDF preview is open, and the code check (if enabled) hasn't failed)
  571. const canAutoCompile = Boolean(
  572. autoCompile && pdfPreviewOpen && !codeCheckFailed
  573. )
  574. // show that the project has pending changes
  575. const hasChanges = Boolean(canAutoCompile && uncompiled && compiledOnce)
  576. // call the debounced autocompile function if the project is available for auto-compiling and it has changed
  577. useEffect(() => {
  578. if (canAutoCompile) {
  579. if (changedAt > 0) {
  580. compiler.debouncedAutoCompile()
  581. }
  582. } else {
  583. compiler.debouncedAutoCompile.cancel()
  584. }
  585. }, [compiler, canAutoCompile, changedAt])
  586. // cancel debounced recompile on unmount
  587. useEffect(() => {
  588. return () => {
  589. compiler.debouncedAutoCompile.cancel()
  590. }
  591. }, [compiler])
  592. // start a compile manually
  593. const startCompile = useCallback(
  594. (options: any) => {
  595. setCompiledOnce(true)
  596. compiler.compile(options)
  597. },
  598. [compiler, setCompiledOnce]
  599. )
  600. // stop a compile manually
  601. const stopCompile = useCallback(() => {
  602. compiler.stopCompile()
  603. }, [compiler])
  604. // clear the compile cache
  605. const clearCache = useCallback(() => {
  606. setClearingCache(true)
  607. return compiler
  608. .clearCache()
  609. .then(() => {
  610. setFileList(undefined)
  611. setPdfFile(undefined)
  612. })
  613. .finally(() => {
  614. setClearingCache(false)
  615. })
  616. }, [compiler])
  617. const syncToEntry = useCallback(
  618. (entry: any, keepCurrentView = false) => {
  619. const result = findEntityByPath(entry.file)
  620. if (result && result.type === 'doc') {
  621. openDocWithId(result.entity._id, {
  622. gotoLine: entry.line ?? undefined,
  623. gotoColumn: entry.column ?? undefined,
  624. keepCurrentView,
  625. })
  626. }
  627. },
  628. [findEntityByPath, openDocWithId]
  629. )
  630. // clear the cache then run a compile, triggered by a menu item
  631. const recompileFromScratch = useCallback(() => {
  632. clearCache().then(() => {
  633. compiler.compile()
  634. })
  635. }, [clearCache, compiler])
  636. // After a compile, the compiler sets `data.options` to the options that were
  637. // used for that compile.
  638. const lastCompileOptions = useMemo(() => data?.options || {}, [data])
  639. useEffect(() => {
  640. const listener = () => {
  641. if (newEditor) {
  642. openRailTab('errors')
  643. } else {
  644. setShowLogs(true)
  645. }
  646. }
  647. window.addEventListener('editor:show-logs', listener)
  648. return () => {
  649. window.removeEventListener('editor:show-logs', listener)
  650. }
  651. }, [newEditor, openRailTab])
  652. const value = useMemo(
  653. () => ({
  654. animateCompileDropdownArrow,
  655. autoCompile,
  656. clearCache,
  657. clearingCache,
  658. clsiServerId,
  659. codeCheckFailed,
  660. compiling,
  661. deliveryLatencies,
  662. draft,
  663. editedSinceCompileStarted,
  664. error,
  665. fileList,
  666. hasChanges,
  667. hasShortCompileTimeout,
  668. highlights,
  669. isProjectOwner,
  670. lastCompileOptions,
  671. logEntryAnnotations,
  672. logEntries,
  673. pdfDownloadUrl: pdfFile?.pdfDownloadUrl,
  674. pdfFile,
  675. pdfUrl: pdfFile?.pdfUrl,
  676. pdfViewer,
  677. position,
  678. rawLog,
  679. recompileFromScratch,
  680. setAnimateCompileDropdownArrow,
  681. setAutoCompile,
  682. setCompiling,
  683. setDraft,
  684. setError,
  685. setHasLintingError, // only for stories
  686. setHighlights,
  687. setPosition,
  688. showCompileTimeWarning,
  689. setShowCompileTimeWarning,
  690. setShowLogs,
  691. toggleLogs,
  692. setStopOnFirstError,
  693. setStopOnValidationError,
  694. showLogs,
  695. startCompile,
  696. stopCompile,
  697. stopOnFirstError,
  698. stopOnValidationError,
  699. stoppedOnFirstError,
  700. uncompiled,
  701. validationIssues,
  702. firstRenderDone,
  703. setChangedAt,
  704. cleanupCompileResult,
  705. syncToEntry,
  706. recordAction,
  707. }),
  708. [
  709. animateCompileDropdownArrow,
  710. autoCompile,
  711. clearCache,
  712. clearingCache,
  713. clsiServerId,
  714. codeCheckFailed,
  715. compiling,
  716. deliveryLatencies,
  717. draft,
  718. editedSinceCompileStarted,
  719. error,
  720. fileList,
  721. hasChanges,
  722. hasShortCompileTimeout,
  723. highlights,
  724. isProjectOwner,
  725. lastCompileOptions,
  726. logEntries,
  727. logEntryAnnotations,
  728. position,
  729. pdfFile,
  730. pdfViewer,
  731. rawLog,
  732. recompileFromScratch,
  733. setAnimateCompileDropdownArrow,
  734. setAutoCompile,
  735. setDraft,
  736. setError,
  737. setHasLintingError, // only for stories
  738. setHighlights,
  739. setPosition,
  740. setShowCompileTimeWarning,
  741. setStopOnFirstError,
  742. setStopOnValidationError,
  743. showCompileTimeWarning,
  744. showLogs,
  745. startCompile,
  746. stopCompile,
  747. stopOnFirstError,
  748. stopOnValidationError,
  749. stoppedOnFirstError,
  750. uncompiled,
  751. validationIssues,
  752. firstRenderDone,
  753. setChangedAt,
  754. cleanupCompileResult,
  755. setShowLogs,
  756. toggleLogs,
  757. syncToEntry,
  758. recordAction,
  759. ]
  760. )
  761. return (
  762. <LocalCompileContext.Provider value={value}>
  763. {children}
  764. </LocalCompileContext.Provider>
  765. )
  766. }
  767. export function useLocalCompileContext() {
  768. const context = useContext(LocalCompileContext)
  769. if (!context) {
  770. throw new Error(
  771. 'useLocalCompileContext is only available inside LocalCompileProvider'
  772. )
  773. }
  774. return context
  775. }