local-compile-context.tsx 26 KB

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