local-compile-context.tsx 24 KB

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