diff --git a/config/scripts/renderer-boot-graph.mjs b/config/scripts/renderer-boot-graph.mjs
index d99c5468e78..03cc9db7739 100644
--- a/config/scripts/renderer-boot-graph.mjs
+++ b/config/scripts/renderer-boot-graph.mjs
@@ -105,16 +105,16 @@ export function verifyRendererBootGraph(root = process.cwd()) {
console.error(` ${chunk}`)
}
console.error('Escape them in the source (e.g. \\u2014 inside String.raw regex sources).')
- return 1
}
- if (violations.length === 0) {
- return 0
+ if (violations.length > 0) {
+ console.error('Payloads that must stay off the renderer boot graph are preloaded again:')
+ for (const violation of violations) {
+ console.error(` ${violation.label} -> ${violation.chunk}`)
+ }
+ console.error('')
+ console.error(
+ 'Load them after first paint (see primeTerminalWebglAddon) or from a route chunk.'
+ )
}
- console.error('Payloads that must stay off the renderer boot graph are preloaded again:')
- for (const violation of violations) {
- console.error(` ${violation.label} -> ${violation.chunk}`)
- }
- console.error('')
- console.error('Load them after first paint (see primeTerminalWebglAddon) or from a route chunk.')
- return 1
+ return twoByteChunks.length > 0 || violations.length > 0 ? 1 : 0
}
diff --git a/config/scripts/renderer-boot-graph.test.mjs b/config/scripts/renderer-boot-graph.test.mjs
new file mode 100644
index 00000000000..d4784af213a
--- /dev/null
+++ b/config/scripts/renderer-boot-graph.test.mjs
@@ -0,0 +1,41 @@
+import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { expect, it, vi } from 'vitest'
+import { verifyRendererBootGraph } from './renderer-boot-graph.mjs'
+
+it.each([
+ [false, false],
+ [true, false],
+ [false, true],
+ [true, true]
+])('reports both boot guards independently (two-byte=%s, payload=%s)', (twoByte, payload) => {
+ const root = mkdtempSync(join(tmpdir(), 'orca-boot-guards-'))
+ const errors = vi.spyOn(console, 'error').mockImplementation(() => {})
+ const log = vi.spyOn(console, 'log').mockImplementation(() => {})
+ try {
+ const renderer = join(root, 'out', 'renderer')
+ const i18n = join(root, 'src', 'renderer', 'src', 'i18n')
+ mkdirSync(join(renderer, 'assets'), { recursive: true })
+ mkdirSync(join(i18n, 'locales'), { recursive: true })
+ writeFileSync(join(i18n, 'locales', 'en.json'), JSON.stringify({ pruned: 'x'.repeat(50) }))
+ writeFileSync(join(i18n, 'en-runtime-required.json'), '{}')
+ writeFileSync(
+ join(renderer, 'index.html'),
+ ''
+ )
+ writeFileSync(
+ join(renderer, 'assets', 'index.js'),
+ JSON.stringify(`${twoByte ? '\u2014' : ''}${payload ? 'WebGL2 not supported' : ''}`)
+ )
+
+ expect(verifyRendererBootGraph(root)).toBe(twoByte || payload ? 1 : 0)
+ const output = errors.mock.calls.flat().join('\n')
+ expect(output.includes('Boot chunks contain chars above U+00FF')).toBe(twoByte)
+ expect(output.includes('@xterm/addon-webgl -> index.js')).toBe(payload)
+ } finally {
+ errors.mockRestore()
+ log.mockRestore()
+ rmSync(root, { recursive: true, force: true })
+ }
+})
diff --git a/src/main/daemon/macos-daemon-launchd.test.ts b/src/main/daemon/macos-daemon-launchd.test.ts
index 981860aafd3..922a920c2ee 100644
--- a/src/main/daemon/macos-daemon-launchd.test.ts
+++ b/src/main/daemon/macos-daemon-launchd.test.ts
@@ -4,7 +4,6 @@ import { join } from 'node:path'
import type * as TimerPromises from 'node:timers/promises'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import type { DaemonEndpointIdentity } from './daemon-hello-protocol'
-import { DAEMON_V8_FLAGS } from './daemon-launched-child-spawn'
import type { ProcessResult, ProcessSpec } from '@orca/process-host/process-spec'
const { state, runProcessMock, materializeMock, ensureWithinMock, disconnectMock, recoverMock } =
@@ -233,7 +232,7 @@ it('launches the copied helper on plain Node and leaves no inherited credentials
: []
expect(args.slice(0, 5)).toEqual([
helperExec(),
- ...DAEMON_V8_FLAGS,
+ '--max-semi-space-size=4',
helperEntry(),
'--socket',
options.socketPath
diff --git a/src/renderer/src/lib/monaco-ts-worker-idle-stop.test.ts b/src/renderer/src/lib/monaco-ts-worker-idle-stop.test.ts
index 36238fba759..7e6e0c5aee6 100644
--- a/src/renderer/src/lib/monaco-ts-worker-idle-stop.test.ts
+++ b/src/renderer/src/lib/monaco-ts-worker-idle-stop.test.ts
@@ -1,30 +1,42 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { installMonacoTsWorkerIdleStop } from './monaco-ts-worker-idle-stop'
+function createModel(languageId: string) {
+ return { getLanguageId: () => languageId }
+}
+
function createFakeMonaco(languages: string[]) {
- const listeners: (() => void)[] = []
+ const models = languages.map(createModel)
const defaults = () => ({
getCompilerOptions: vi.fn(() => ({ jsx: 1 })),
setCompilerOptions: vi.fn()
})
const typescriptDefaults = defaults()
const javascriptDefaults = defaults()
- const subscribe = (listener: () => void) => {
- listeners.push(listener)
- return { dispose: vi.fn() }
- }
+ const onWillDisposeModel = vi.fn<
+ (listener: (model: ReturnType) => void) => { dispose(): void }
+ >(() => ({ dispose: vi.fn() }))
+ const onDidChangeModelLanguage = vi.fn<
+ (listener: (event: { model: ReturnType; oldLanguage: string }) => void) => {
+ dispose(): void
+ }
+ >(() => ({ dispose: vi.fn() }))
const monaco = {
- editor: {
- getModels: () => languages.map((id) => ({ getLanguageId: () => id })),
- onWillDisposeModel: subscribe,
- onDidChangeModelLanguage: subscribe
- },
+ editor: { getModels: () => models, onWillDisposeModel, onDidChangeModelLanguage },
typescript: { typescriptDefaults, javascriptDefaults }
}
return {
monaco,
- languages,
- listeners,
+ models,
+ disposeModel(model: ReturnType) {
+ onWillDisposeModel.mock.calls[0][0](model)
+ models.splice(models.indexOf(model), 1)
+ },
+ changeLanguage(model: ReturnType, language: string) {
+ const oldLanguage = model.getLanguageId()
+ model.getLanguageId = () => language
+ onDidChangeModelLanguage.mock.calls[0][0]({ model, oldLanguage })
+ },
typescriptDefaults,
javascriptDefaults
}
@@ -33,27 +45,51 @@ function createFakeMonaco(languages: string[]) {
afterEach(() => vi.useRealTimers())
describe('installMonacoTsWorkerIdleStop', () => {
- it('stops the TS/JS workers once no TS/JS model remains for the idle delay', () => {
+ it('stops on the original deadline despite unrelated model events', () => {
vi.useFakeTimers()
- const fake = createFakeMonaco(['typescript'])
+ const fake = createFakeMonaco(['typescript', 'markdown', 'plaintext'])
installMonacoTsWorkerIdleStop(fake.monaco, 1000)
- fake.languages.splice(0)
- fake.listeners[0]()
- vi.advanceTimersByTime(999)
+ fake.disposeModel(fake.models[0])
+ vi.advanceTimersByTime(500)
+ fake.disposeModel(fake.models[0])
+ fake.changeLanguage(fake.models[0], 'markdown')
+ vi.advanceTimersByTime(499)
expect(fake.typescriptDefaults.setCompilerOptions).not.toHaveBeenCalled()
vi.advanceTimersByTime(1)
expect(fake.typescriptDefaults.setCompilerOptions).toHaveBeenCalledWith({ jsx: 1 })
expect(fake.javascriptDefaults.setCompilerOptions).toHaveBeenCalledWith({ jsx: 1 })
})
- it('keeps the worker while a TS/JS model is still open', () => {
+ it('rechecks reopened TS/JS models and waits for their departure', () => {
vi.useFakeTimers()
- const fake = createFakeMonaco(['typescript', 'markdown'])
+ const fake = createFakeMonaco(['typescript'])
installMonacoTsWorkerIdleStop(fake.monaco, 1000)
- fake.listeners[0]()
+ fake.disposeModel(fake.models[0])
+ fake.models.push(createModel('javascript'))
vi.advanceTimersByTime(1000)
expect(fake.typescriptDefaults.setCompilerOptions).not.toHaveBeenCalled()
+ fake.changeLanguage(fake.models[0], 'markdown')
+ vi.advanceTimersByTime(999)
+ expect(fake.javascriptDefaults.setCompilerOptions).not.toHaveBeenCalled()
+ vi.advanceTimersByTime(1)
+ expect(fake.javascriptDefaults.setCompilerOptions).toHaveBeenCalledTimes(1)
+ })
+
+ it('cancels the pending stop and model subscriptions on cleanup', () => {
+ vi.useFakeTimers()
+ const fake = createFakeMonaco(['javascript'])
+ const dispose = installMonacoTsWorkerIdleStop(fake.monaco, 1000)
+ fake.disposeModel(fake.models[0])
+ dispose()
+ vi.advanceTimersByTime(1000)
+ expect(fake.javascriptDefaults.setCompilerOptions).not.toHaveBeenCalled()
+ for (const event of [
+ fake.monaco.editor.onWillDisposeModel,
+ fake.monaco.editor.onDidChangeModelLanguage
+ ]) {
+ expect(event.mock.results[0].value.dispose).toHaveBeenCalledOnce()
+ }
})
})
diff --git a/src/renderer/src/lib/monaco-ts-worker-idle-stop.ts b/src/renderer/src/lib/monaco-ts-worker-idle-stop.ts
index 87bab1977dc..2a977439c33 100644
--- a/src/renderer/src/lib/monaco-ts-worker-idle-stop.ts
+++ b/src/renderer/src/lib/monaco-ts-worker-idle-stop.ts
@@ -3,7 +3,8 @@ export const TS_WORKER_IDLE_STOP_MS = 60_000
const TS_LANGUAGE_IDS = new Set(['typescript', 'javascript'])
-type ModelEvent = (listener: () => void) => { dispose(): void }
+type Model = { getLanguageId(): string }
+type ModelEvent = (listener: (event: Event) => void) => { dispose(): void }
type LanguageDefaults = {
getCompilerOptions(): Options
setCompilerOptions(options: Options): void
@@ -11,9 +12,9 @@ type LanguageDefaults = {
/** The slice of the monaco namespace this reads; the real namespace satisfies it structurally. */
export type MonacoTsWorkerHost = {
editor: {
- getModels(): readonly { getLanguageId(): string }[]
- onWillDisposeModel: ModelEvent
- onDidChangeModelLanguage: ModelEvent
+ getModels(): readonly Model[]
+ onWillDisposeModel: ModelEvent
+ onDidChangeModelLanguage: ModelEvent<{ model: Model; oldLanguage: string }>
}
typescript: {
typescriptDefaults: LanguageDefaults
@@ -49,8 +50,16 @@ export function installMonacoTsWorkerIdleStop(
}
const subscriptions = [
- monaco.editor.onWillDisposeModel(schedule),
- monaco.editor.onDidChangeModelLanguage(schedule)
+ monaco.editor.onWillDisposeModel((model) => {
+ if (TS_LANGUAGE_IDS.has(model.getLanguageId())) {
+ schedule()
+ }
+ }),
+ monaco.editor.onDidChangeModelLanguage(({ model, oldLanguage }) => {
+ if (TS_LANGUAGE_IDS.has(oldLanguage) && !TS_LANGUAGE_IDS.has(model.getLanguageId())) {
+ schedule()
+ }
+ })
]
return () => {
if (timer !== null) {