diff --git a/apps/desktop/scripts/dev-renderer-warmup.mjs b/apps/desktop/scripts/dev-renderer-warmup.mjs new file mode 100644 index 0000000000..d4bcfcf6c2 --- /dev/null +++ b/apps/desktop/scripts/dev-renderer-warmup.mjs @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Resolve the renderer entry before Electron starts. This lets Vite finish + * dependency optimization and prevents a first navigation from combining + * modules from two optimizer generations. + */ +export async function warmupDevRenderer(server, entry = '/main.tsx') { + const environment = server.environments?.client; + if (environment?.warmupRequest && environment?.waitForRequestsIdle) { + await environment.warmupRequest(entry); + await environment.waitForRequestsIdle(); + return; + } + + // Vite 7 and earlier expose the same operation on the server. Keep this + // fallback for local installs that have not moved to per-environment APIs. + await server.warmupRequest(entry); +} diff --git a/apps/desktop/scripts/dev-renderer-warmup.test.mjs b/apps/desktop/scripts/dev-renderer-warmup.test.mjs new file mode 100644 index 0000000000..cf97003ad4 --- /dev/null +++ b/apps/desktop/scripts/dev-renderer-warmup.test.mjs @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { warmupDevRenderer } from './dev-renderer-warmup.mjs'; + +test('warms the client environment and waits for its imports to settle', async () => { + const calls = []; + const server = { + environments: { + client: { + async warmupRequest(url) { calls.push(`warmup:${url}`); }, + async waitForRequestsIdle() { calls.push('idle'); }, + }, + }, + async warmupRequest() { throw new Error('legacy server API should not be used'); }, + }; + + await warmupDevRenderer(server); + + assert.deepEqual(calls, ['warmup:/main.tsx', 'idle']); +}); + +test('falls back to the server warmup API for older Vite versions', async () => { + const calls = []; + const server = { + async warmupRequest(url) { calls.push(url); }, + }; + + await warmupDevRenderer(server, '/legacy.tsx'); + + assert.deepEqual(calls, ['/legacy.tsx']); +}); diff --git a/apps/desktop/scripts/dev.mjs b/apps/desktop/scripts/dev.mjs index 56e7ec7e31..304f7090c0 100644 --- a/apps/desktop/scripts/dev.mjs +++ b/apps/desktop/scripts/dev.mjs @@ -47,6 +47,7 @@ import { handleDevelopmentLaunchOutcome, waitForDevelopmentLaunchVerdict, } from './dev-app-runtime.mjs'; +import { warmupDevRenderer } from './dev-renderer-warmup.mjs'; const DESKTOP_DIR = resolve(fileURLToPath(new URL('..', import.meta.url))); const REPO_ROOT = resolve(DESKTOP_DIR, '..', '..'); @@ -151,6 +152,7 @@ process.chdir(DESKTOP_DIR); log('vite', 'starting dev server...'); const server = await createServer(); await server.listen(); +await warmupDevRenderer(server); server.printUrls(); const devUrl = server.resolvedUrls?.local?.[0]?.replace(/\/$/, ''); diff --git a/apps/desktop/src/main/__tests__/main-renderer-loader.test.ts b/apps/desktop/src/main/__tests__/main-renderer-loader.test.ts new file mode 100644 index 0000000000..79ba648a31 --- /dev/null +++ b/apps/desktop/src/main/__tests__/main-renderer-loader.test.ts @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { clearDevRendererCache, type MainRendererCacheOwner } from '../main-renderer-loader.js'; + +test('clears the renderer session cache only for a Vite development entry', async () => { + let clearCount = 0; + const owner: MainRendererCacheOwner = { + webContents: { + session: { + async clearCache() { clearCount += 1; }, + }, + }, + }; + + await clearDevRendererCache(owner, { + filePath: '/app/dist-renderer/index.html', + url: 'http://127.0.0.1:5173', + useDevServer: true, + }); + await clearDevRendererCache(owner, { + filePath: '/app/dist-renderer/index.html', + url: 'file:///app/dist-renderer/index.html', + useDevServer: false, + }); + + assert.equal(clearCount, 1); +}); diff --git a/apps/desktop/src/main/main-renderer-loader.ts b/apps/desktop/src/main/main-renderer-loader.ts index 5b97a43564..fa1bc4450d 100644 --- a/apps/desktop/src/main/main-renderer-loader.ts +++ b/apps/desktop/src/main/main-renderer-loader.ts @@ -31,6 +31,21 @@ export interface MainRendererEntry { readonly useDevServer: boolean; } +export interface MainRendererCacheOwner { + webContents: { + session: { + clearCache(): Promise; + }; + }; +} + +export async function clearDevRendererCache( + mainWindow: MainRendererCacheOwner, + rendererEntry: MainRendererEntry, +): Promise { + if (rendererEntry.useDevServer) await mainWindow.webContents.session.clearCache(); +} + export function resolveMainRendererEntry( mainModuleDirectory: string, viteDevServerUrl: string | undefined, diff --git a/apps/desktop/src/main/main-window.ts b/apps/desktop/src/main/main-window.ts index 9f8fbc1e84..e1bec8686a 100644 --- a/apps/desktop/src/main/main-window.ts +++ b/apps/desktop/src/main/main-window.ts @@ -28,7 +28,11 @@ import { BrowserViewController } from './browser/controller.js'; import { BrowserViewManager } from './browser/view-manager.js'; import type { E2eFixture } from './e2e-fixture.js'; import { installMainWindowPermissionPolicy } from './main-window-permission-policy.js'; -import { loadMainRenderer, resolveMainRendererEntry } from './main-renderer-loader.js'; +import { + clearDevRendererCache, + loadMainRenderer, + resolveMainRendererEntry, +} from './main-renderer-loader.js'; import { type MainRendererFrameIdentity, observeMainRendererProcessGone, @@ -522,6 +526,7 @@ export function createMainWindowController(deps: MainWindowControllerDeps): Main void writeSavedBounds(workspaceRoot, final); }); + await clearDevRendererCache(mainWindow, rendererEntry); await loadMainRenderer(mainWindow, rendererEntry); // PR-SHOW-AFTER-FIRST-COMMIT: reveal fallback. Start this budget only once