local-compile-context.tsx 26 KB

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