mirror of
https://github.com/zhukunpenglinyutong/jetbrains-cc-gui.git
synced 2026-11-04 07:27:06 +08:00
fix(webview): preserve entry module identity for usage dashboard
This commit is contained in:
@@ -20,8 +20,8 @@ test('packaged chat tabs bootstrap compressed assets and preserve input and lazy
|
||||
const loadedAssets: string[] = [];
|
||||
const startupMs: number[] = [];
|
||||
|
||||
// Mirror JCEF's binary resource handler: .gz is fetched as-is, while lazy modules
|
||||
// and styles are decompressed by Java before reaching Chromium.
|
||||
// Mirror JCEF's binary resource handler: modules and styles are stored as
|
||||
// gzip and decompressed by Java before reaching Chromium at their stable URL.
|
||||
await context.route('https://cc-gui-webview.local/assets/**', async (route) => {
|
||||
const fileName = path.posix.basename(new URL(route.request().url()).pathname);
|
||||
const compressed = fileName.endsWith('.js') || fileName.endsWith('.css');
|
||||
@@ -55,8 +55,18 @@ test('packaged chat tabs bootstrap compressed assets and preserve input and lazy
|
||||
}));
|
||||
const bridge = window as PackagedWindow;
|
||||
bridge.__packagedBridgeEvents = [];
|
||||
localStorage.setItem('language', 'en');
|
||||
bridge.sendToJava = (message) => {
|
||||
bridge.__packagedBridgeEvents.push(message);
|
||||
if (message.startsWith('tt_ensure_server:') || message.startsWith('tt_proxy:')) {
|
||||
const request = JSON.parse(message.slice(message.indexOf(':') + 1));
|
||||
const data = message.startsWith('tt_ensure_server:')
|
||||
? { running: true, port: 7680 }
|
||||
: { body: JSON.stringify({ data: [], totals: {}, projects: [], token: 'fixture' }) };
|
||||
setTimeout(() => window.onTokenTrackerResponse?.(JSON.stringify({
|
||||
requestId: request.requestId, ok: true, data,
|
||||
})), 0);
|
||||
}
|
||||
if (message.startsWith('get_dependency_status:')) {
|
||||
setTimeout(() => {
|
||||
const callback = (window as unknown as Record<string, unknown>).updateDependencyStatus;
|
||||
@@ -112,12 +122,29 @@ test('packaged chat tabs bootstrap compressed assets and preserve input and lazy
|
||||
await expect(input).toBeEmpty();
|
||||
|
||||
if (tab === 1) {
|
||||
// This lazy chunk imports React from the entry module. Reopening it must
|
||||
// reuse the running app rather than evaluate a second entry/React copy.
|
||||
for (let visit = 0; visit < 2; visit++) {
|
||||
await page.locator('.header button:has(.codicon-settings-gear)').click();
|
||||
await page.locator('.codicon-graph').click();
|
||||
await expect(page.locator('.tt-dashboard')).toBeVisible();
|
||||
await expect(page.locator('.tt-dashboard')).toContainText('Daily Breakdown');
|
||||
await expect(page.locator('.tt-dashboard').getByRole('button', { name: 'Refresh', exact: true })).toBeEnabled();
|
||||
await expect.poll(() => page.evaluate(() => (window as PackagedWindow).__packagedBridgeEvents
|
||||
.filter((event) => event.startsWith('frontend_ready:')).length)).toBe(1);
|
||||
if (visit === 0) {
|
||||
await page.screenshot({ path: testInfo.outputPath('packaged-usage.png') });
|
||||
}
|
||||
await page.locator('button:has(.codicon-arrow-left)').click();
|
||||
await expect(input).toBeVisible();
|
||||
}
|
||||
await page.screenshot({ path: testInfo.outputPath('packaged-chat.png') });
|
||||
}
|
||||
await page.close();
|
||||
}
|
||||
|
||||
expect(loadedAssets.some((name) => /^index-.*\.js\.gz$/.test(name))).toBe(true);
|
||||
expect(loadedAssets.some((name) => /^index-.*\.js$/.test(name))).toBe(true);
|
||||
expect(loadedAssets.some((name) => name.endsWith('.js.gz'))).toBe(false);
|
||||
expect(loadedAssets.some((name) => /^flowDiagram-.*\.js$/.test(name))).toBe(true);
|
||||
expect(errors).toEqual([]);
|
||||
await testInfo.attach('startup-metrics', {
|
||||
|
||||
@@ -33,29 +33,19 @@ const rewriteRelativeModuleImports = async (source) => {
|
||||
return rewritten;
|
||||
};
|
||||
|
||||
const injectCompressedModuleBootstrap = (html) => {
|
||||
const injectModuleBootstrap = (html) => {
|
||||
const moduleScriptPattern = /<script type="module" crossorigin src="([^"]+)"><\/script>/;
|
||||
const match = html.match(moduleScriptPattern);
|
||||
if (!match) {
|
||||
throw new Error('Unable to find the Vite entry module');
|
||||
}
|
||||
|
||||
const compressedModuleUrl = `${match[1]}.gz`;
|
||||
// Use the same URL as imports from lazy chunks so the browser evaluates the
|
||||
// entry (and its React instance) exactly once. A blob URL creates a second
|
||||
// module identity when a lazy chunk imports the canonical entry URL.
|
||||
// The JCEF asset handler decompresses the stored .js.gz for this .js URL.
|
||||
const bootstrap = `<script type="module">
|
||||
(async () => {
|
||||
const response = await fetch(${JSON.stringify(compressedModuleUrl)});
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error(\`Failed to load compressed webview module: \${response.status}\`);
|
||||
}
|
||||
const sourceStream = response.body.pipeThrough(new DecompressionStream('gzip'));
|
||||
const source = await new Response(sourceStream).text();
|
||||
const objectUrl = URL.createObjectURL(new Blob([source], { type: 'text/javascript' }));
|
||||
try {
|
||||
await import(objectUrl);
|
||||
} finally {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
})().catch((error) => {
|
||||
import(${JSON.stringify(match[1])}).catch((error) => {
|
||||
console.error('[CC GUI] Webview bootstrap failed', error);
|
||||
// A blank chat tab is indistinguishable from a slow one; surface the failure.
|
||||
const fallback = document.createElement('p');
|
||||
@@ -89,7 +79,7 @@ const copyAsset = async (fileName) => {
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const html = injectCompressedModuleBootstrap(await readFile(distFile, 'utf-8'));
|
||||
const html = injectModuleBootstrap(await readFile(distFile, 'utf-8'));
|
||||
await mkdir(path.dirname(targetFile), { recursive: true });
|
||||
await writeFile(targetFile, html, 'utf-8');
|
||||
|
||||
|
||||
@@ -52,9 +52,8 @@ test('packages a small bootstrap and lossless compressed assets with valid modul
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const bootstrap = await readFile(path.join(resources, 'html/claude-chat.html'), 'utf8');
|
||||
assert.ok(Buffer.byteLength(bootstrap) < 32_768, 'per-tab HTML must not inline the bundle');
|
||||
assert.ok(bootstrap.includes(`${origin}/assets/index-entry.js.gz`));
|
||||
assert.ok(bootstrap.includes("DecompressionStream('gzip')"));
|
||||
assert.ok(bootstrap.includes('URL.revokeObjectURL(objectUrl)'));
|
||||
assert.ok(bootstrap.includes(`import("${origin}/assets/index-entry.js")`));
|
||||
assert.ok(!bootstrap.includes('createObjectURL'), 'entry and lazy imports must share one module URL');
|
||||
|
||||
const packaged = path.join(resources, 'webview/assets');
|
||||
const compressed = await readFile(path.join(packaged, 'index-entry.js.gz'));
|
||||
|
||||
Reference in New Issue
Block a user