local-compile-context.tsx 26 KB

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