Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions apps/desktop/scripts/dev.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,20 @@ if (!devUrl) {
process.exit(1);
}

// Let Vite finish the initial dependency crawl + optimizer commit before the
// window loads (issue #4775). `warmupRequest` on the renderer entry kicks the
// recursive pre-transform of the static import graph (which registers every
// reachable dep with the optimizer), and `waitForRequestsIdle` resolves at
// crawl end — the same signal the dep optimizer waits on before committing
// node_modules/.vite/deps. Loading Electron before that commit let the page
// execute chunks from a previous optimizer generation alongside fresh ones —
// two React instances, a null hook dispatcher, and a renderer crash on the
// first lazy component. warmupRequest swallows transform errors itself, so
// this can never abort the launch; it only reorders the startup race away.
log('vite', 'warming renderer entry and waiting for the dep crawl to settle...');
await server.environments.client.warmupRequest('/main.tsx');
await server.environments.client.waitForRequestsIdle();

log('electron', `launching against ${devUrl} (renderer HMR live)`);

// Created before launch so signals during codesign/preparation are durable.
Expand Down
48 changes: 48 additions & 0 deletions apps/desktop/src/main/__tests__/main-renderer-dev-cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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 { describe, it } from 'node:test';
import { clearDevRendererHttpCache } from '../main-renderer-dev-cache.js';

describe('dev renderer HTTP cache hygiene (issue #4775)', () => {
it('clears the session HTTP cache before loading a Vite dev server', async () => {
let cleared = 0;
const session = { async clearCache() { cleared += 1; } };
const didClear = await clearDevRendererHttpCache(session, { useDevServer: true });
assert.equal(didClear, true);
assert.equal(cleared, 1);
});

it('leaves the cache alone for packaged file:// builds', async () => {
let cleared = 0;
const session = { async clearCache() { cleared += 1; } };
const didClear = await clearDevRendererHttpCache(session, { useDevServer: false });
assert.equal(didClear, false);
assert.equal(cleared, 0);
});

it('downgrades a clearCache failure to a warning instead of blocking load', async () => {
const session = {
async clearCache(): Promise<void> { throw new Error('cache locked'); },
};
const didClear = await clearDevRendererHttpCache(session, { useDevServer: true });
assert.equal(didClear, false);
});
});
68 changes: 68 additions & 0 deletions apps/desktop/src/main/main-renderer-dev-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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.
*/

/**
* Dev-server cache hygiene (issue #4775).
*
* Vite serves optimized deps as `Cache-Control: immutable` keyed by per-dep
* `?v=` browserHash labels, and the optimizer preserves a dep's label across
* re-optimization commits — including ones where the dep's bundle bytes
* changed (Vite upgrade, patch re-application, interrupted commit). The
* persistent session cache then resurrects a previous generation's
* react/react-dom chunk graph next to freshly transformed sources that import
* today's react copy: two React instances in one page, a null hook
* dispatcher, and `Cannot read properties of null (reading 'useRef')`
* killing the renderer on the first lazily loaded component.
*
* Clearing the HTTP cache before loading the dev server keeps every dev
* session on exactly one optimizer generation. Packaged builds load file://
* and never touch this path; localStorage/cookies are not part of the HTTP
* cache and survive.
*
* Structural interfaces instead of `electron` imports: the module stays
* loadable in plain node tests (same pattern as main-renderer-loader.ts).
*/
export interface DevRendererCacheSession {
clearCache(): Promise<void>;
}

export interface DevRendererCacheEntry {
readonly useDevServer: boolean;
}

/**
* Clears the renderer session's HTTP cache when — and only when — the window
* is about to load a Vite dev server. Returns whether the cache was cleared.
* A clearCache failure downgrades to a warning: the duplicate-React crash it
* guards against is recoverable by reload, but a window that never opens is
* not.
*/
export async function clearDevRendererHttpCache(
session: DevRendererCacheSession,
rendererEntry: DevRendererCacheEntry,
): Promise<boolean> {
if (!rendererEntry.useDevServer) return false;
try {
await session.clearCache();
return true;
} catch (error) {
console.warn('[main] failed to clear dev renderer HTTP cache:', error);
return false;
}
}
6 changes: 6 additions & 0 deletions apps/desktop/src/main/main-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ 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 { clearDevRendererHttpCache } from './main-renderer-dev-cache.js';
import {
type MainRendererFrameIdentity,
observeMainRendererProcessGone,
Expand Down Expand Up @@ -522,6 +523,11 @@ export function createMainWindowController(deps: MainWindowControllerDeps): Main
void writeSavedBounds(workspaceRoot, final);
});

// Dev-server cache hygiene (issue #4775) — see main-renderer-dev-cache.ts
// for why a stale immutable dep-chunk graph must never survive into a new
// dev session (duplicate React instances → null hook dispatcher crash).
await clearDevRendererHttpCache(mainWindow.webContents.session, rendererEntry);

await loadMainRenderer(mainWindow, rendererEntry);

// PR-SHOW-AFTER-FIRST-COMMIT: reveal fallback. Start this budget only once
Expand Down