Skip to content

Commit ea13492

Browse files
@W-21111861@ feat: Skip standalone proxy when Vite WebApp proxy is active (#23)
* feat: add Vite proxy detection and TTY-aware messaging - Detect Vite WebApp proxy via health check, skip standalone proxy when active - Use getErrorPageTemplate from @salesforce/webapp-experimental/proxy - TTY-aware stop message (Ctrl+C in terminal vs VS Code command palette) - Add info.ready-for-development-vite for Vite proxy case - Simplify ready-for-development to show only URL to open - Resolve PR review comments on messaging Co-authored-by: Cursor <cursoragent@cursor.com> * fix: remove label/version to match @salesforce/webapp-experimental 1.x type - Run yarn install to resolve 1.23.0+ (was 0.2.0 from stale lock) - ProxyServer: use minimal fallback { name, outputDir } per PR #22 - Tests: remove label/version from manifest fixtures - ManifestWatcher tests: fix assertions for simplified type Co-authored-by: Cursor <cursoragent@cursor.com> * fix: group ready-for-development messages, simplify Vite message format Co-authored-by: Cursor <cursoragent@cursor.com> * fix: update server-running message - remove bold, add quotes Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 96be9de commit ea13492

9 files changed

Lines changed: 671 additions & 153 deletions

File tree

README.md

Lines changed: 392 additions & 82 deletions
Large diffs are not rendered by default.

messages/webapp.dev.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,26 @@ Dev server URL: %s
8282

8383
# info.proxy-url
8484

85-
Proxy URL: %s (open this in your browser)
85+
Proxy URL: %s (open this URL in your browser)
8686

8787
# info.ready-for-development
8888

8989
✅ Ready for development!
9090
→ %s (open this URL in your browser)
9191

92+
# info.ready-for-development-vite
93+
94+
✅ Ready for development!
95+
→ %s (Vite proxy active - open this URL in your browser)
96+
9297
# info.press-ctrl-c
9398

9499
Press Ctrl+C to stop the proxy server
95100

101+
# info.server-running
102+
103+
Dev server is running. Stop it by running "SFDX: Close Live Preview" from the VS Code command palette.
104+
96105
# info.dev-server-healthy
97106

98107
✓ Dev server is responding at: %s
@@ -178,3 +187,7 @@ Using default dev command: %s
178187

179188
⚠️ The --url flag (%s) does not match the actual dev server URL (%s).
180189
The proxy will use the actual dev server URL.
190+
191+
# info.vite-proxy-detected
192+
193+
Vite WebApp proxy detected at %s - using Vite's built-in proxy (standalone proxy skipped)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"@oclif/plugin-command-snapshot": "^5.3.8",
2222
"@salesforce/cli-plugins-testkit": "^5.3.41",
2323
"@salesforce/dev-scripts": "^11.0.4",
24-
"@salesforce/plugin-command-reference": "^3.1.79",
24+
"@salesforce/plugin-command-reference": "^3.1.77",
2525
"@types/http-proxy": "^1.17.14",
2626
"@types/micromatch": "^4.0.10",
2727
"eslint-plugin-sf-plugin": "^1.20.33",

src/commands/webapp/dev.ts

Lines changed: 81 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,30 @@ export default class WebappDev extends SfCommand<WebAppDevResult> {
118118
}
119119
}
120120

121+
/**
122+
* Check if Vite's WebAppProxyHandler is active at the dev server URL.
123+
* The Vite plugin responds to a health check query parameter with a custom header
124+
* when the proxy middleware is active.
125+
*
126+
* @param devServerUrl - The dev server URL to check
127+
* @returns true if Vite's proxy is handling requests, false otherwise
128+
*/
129+
private static async checkViteProxyActive(devServerUrl: string): Promise<boolean> {
130+
try {
131+
// The Vite plugin uses a query parameter for health checks, not a path
132+
const healthUrl = new URL(devServerUrl);
133+
healthUrl.searchParams.set('sfProxyHealthCheck', 'true');
134+
const response = await fetch(healthUrl.toString(), {
135+
method: 'GET',
136+
signal: AbortSignal.timeout(3000), // 3 second timeout
137+
});
138+
return response.headers.get('X-Salesforce-WebApp-Proxy') === 'true';
139+
} catch {
140+
// Health check failed - Vite proxy not active
141+
return false;
142+
}
143+
}
144+
121145
// eslint-disable-next-line complexity
122146
public async run(): Promise<WebAppDevResult> {
123147
const { flags } = await this.parse(WebappDev);
@@ -290,7 +314,7 @@ export default class WebappDev extends SfCommand<WebAppDevResult> {
290314
const actualDevServerUrl = await new Promise<string>((resolve, reject) => {
291315
const timeout = setTimeout(() => {
292316
reject(
293-
new SfError('Dev server did not start within 30 seconds.', 'DevServerTimeoutError', [
317+
new SfError('Dev server did not start within 30 seconds.', 'DevServerTimeoutError', [
294318
'The dev server may be taking longer than expected to start',
295319
'Check if the dev server command is correct in webapplication.json',
296320
'Try running the dev server command manually to see if it starts',
@@ -327,51 +351,78 @@ export default class WebappDev extends SfCommand<WebAppDevResult> {
327351
// Ensure devServerUrl is set (should always be set by step 3)
328352
if (!devServerUrl) {
329353
throw new SfError(
330-
'Unable to determine dev server URL. Please specify --url or configure dev.url in webapplication.json.',
354+
'Unable to determine dev server URL. Please specify --url or configure dev.url in webapplication.json.',
331355
'DevServerUrlError'
332356
);
333357
}
334358

335-
// Step 5: Start proxy server
336-
this.logger.debug(`Starting proxy server on port ${flags.port}...`);
337-
const salesforceInstanceUrl = orgConnection.instanceUrl;
338-
this.proxyServer = new ProxyServer({
339-
devServerUrl,
340-
salesforceInstanceUrl,
341-
port: flags.port,
342-
manifest: manifest ?? undefined,
343-
orgAlias: orgUsername,
344-
});
359+
// Step 5: Check for Vite proxy and conditionally start standalone proxy
360+
this.logger.debug('Checking if Vite WebApp proxy is active...');
361+
const viteProxyActive = await WebappDev.checkViteProxyActive(devServerUrl);
345362

346-
await this.proxyServer.start();
347-
const proxyUrl = this.proxyServer.getProxyUrl();
348-
this.logger.debug(`Proxy server running on ${proxyUrl}`);
363+
// Track the final URL to open in browser (either proxy or dev server)
364+
let finalUrl: string;
349365

350-
// Listen for dev server status changes (minimal output)
351-
this.proxyServer.on('dev-server-up', (url: string) => {
352-
this.logger?.debug(messages.getMessage('info.dev-server-detected', [url]));
353-
});
366+
if (viteProxyActive) {
367+
// Vite's WebAppProxyHandler is handling the proxy - skip standalone proxy
368+
this.log(messages.getMessage('info.vite-proxy-detected', [devServerUrl]));
369+
this.logger.debug('Vite proxy detected, skipping standalone proxy server');
370+
finalUrl = devServerUrl;
371+
} else {
372+
// Start standalone proxy server
373+
this.logger.debug(`Starting proxy server on port ${flags.port}...`);
374+
const salesforceInstanceUrl = orgConnection.instanceUrl;
375+
this.proxyServer = new ProxyServer({
376+
devServerUrl,
377+
salesforceInstanceUrl,
378+
port: flags.port,
379+
manifest: manifest ?? undefined,
380+
orgAlias: orgUsername,
381+
});
354382

355-
this.proxyServer.on('dev-server-down', (url: string) => {
356-
this.log(messages.getMessage('warning.dev-server-unreachable-status', [url]));
357-
this.log(messages.getMessage('info.start-dev-server-hint'));
358-
});
383+
await this.proxyServer.start();
384+
const proxyUrl = this.proxyServer.getProxyUrl();
385+
this.logger.debug(`Proxy server running on ${proxyUrl}`);
386+
387+
// Listen for dev server status changes (minimal output)
388+
this.proxyServer.on('dev-server-up', (url: string) => {
389+
this.logger?.debug(messages.getMessage('info.dev-server-detected', [url]));
390+
});
391+
392+
this.proxyServer.on('dev-server-down', (url: string) => {
393+
this.log(messages.getMessage('warning.dev-server-unreachable-status', [url]));
394+
this.log(messages.getMessage('info.start-dev-server-hint'));
395+
});
359396

360-
// Step 6: Check if dev server is reachable (non-blocking warning)
361-
if (devServerUrl) {
397+
finalUrl = proxyUrl;
398+
}
399+
400+
// Step 6: Check if dev server is reachable (non-blocking warning) - only when using standalone proxy
401+
if (!viteProxyActive && devServerUrl) {
362402
await this.checkDevServerHealth(devServerUrl);
363403
}
364404

365405
// Step 7: Open browser if requested
366406
if (flags.open) {
367407
this.logger.debug('Opening browser...');
368-
await WebappDev.openBrowser(proxyUrl);
408+
await WebappDev.openBrowser(finalUrl);
369409
}
370410

371411
// Display usage instructions
372412
this.log('');
373-
this.log(messages.getMessage('info.ready-for-development', [proxyUrl]));
374-
this.log(messages.getMessage('info.press-ctrl-c'));
413+
if (viteProxyActive) {
414+
this.log(messages.getMessage('info.ready-for-development-vite', [devServerUrl]));
415+
} else {
416+
this.log(messages.getMessage('info.ready-for-development', [finalUrl]));
417+
}
418+
// Show appropriate stop message based on execution context
419+
// In TTY (interactive terminal): show "Press Ctrl+C to stop"
420+
// In non-TTY (IDE, CI, piped): show generic "Server running" message
421+
if (process.stdout.isTTY) {
422+
this.log(messages.getMessage('info.press-ctrl-c'));
423+
} else {
424+
this.log(messages.getMessage('info.server-running'));
425+
}
375426
this.log('');
376427

377428
// Keep the command running until interrupted or dev server exits
@@ -403,7 +454,7 @@ export default class WebappDev extends SfCommand<WebAppDevResult> {
403454

404455
// Return result (never reached, but required for type safety)
405456
return {
406-
url: proxyUrl,
457+
url: finalUrl,
407458
devServerUrl: devServerUrl ?? '',
408459
};
409460
} catch (error) {
@@ -417,7 +468,7 @@ export default class WebappDev extends SfCommand<WebAppDevResult> {
417468

418469
// Wrap unknown errors
419470
const errorMessage = error instanceof Error ? error.message : String(error);
420-
throw new SfError(`Failed to start webapp dev command: ${errorMessage}`, 'UnexpectedError', [
471+
throw new SfError(`Failed to start webapp dev command: ${errorMessage}`, 'UnexpectedError', [
421472
'This is an unexpected error',
422473
'Please try again',
423474
'If the problem persists, check the command logs with SF_LOG_LEVEL=debug',

src/proxy/ProxyServer.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,6 @@ export class ProxyServer extends EventEmitter {
176176

177177
this.server.on('connection', (socket) => {
178178
this.activeConnections.add(socket);
179-
socket.on('error', (err) => {
180-
// Handle ECONNRESET and other socket errors gracefully
181-
// These can happen when the dev server crashes or a client disconnects abruptly
182-
this.logger.debug(`Socket error (${err.message}), cleaning up connection`);
183-
});
184179
socket.once('close', () => {
185180
this.activeConnections.delete(socket);
186181
});

src/server/DevServerManager.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ export class DevServerManager extends EventEmitter {
210210
// Validate that command is provided
211211
if (!this.options.command) {
212212
throw new SfError(
213-
'Dev server command is required when explicit URL is not provided',
213+
'Dev server command is required when explicit URL is not provided',
214214
'DevServerCommandRequired',
215215
['Provide a "command" in DevServerOptions', 'Or provide an "explicitUrl" to skip spawning']
216216
);
@@ -232,7 +232,7 @@ export class DevServerManager extends EventEmitter {
232232
} catch (error) {
233233
const sfError =
234234
error instanceof Error ? error : new Error(error instanceof Object ? JSON.stringify(error) : String(error));
235-
throw new SfError(`Failed to spawn dev server process: ${sfError.message}`, 'DevServerSpawnError', [
235+
throw new SfError(`Failed to spawn dev server process: ${sfError.message}`, 'DevServerSpawnError', [
236236
`Verify the command is correct: ${this.options.command}`,
237237
'Check that the executable exists in your PATH',
238238
'Ensure you have the necessary dependencies installed',
@@ -430,10 +430,12 @@ export class DevServerManager extends EventEmitter {
430430
this.logger.error(`Dev server error: ${parsedError.title}`);
431431
this.logger.debug(`Error type: ${parsedError.type}`);
432432

433-
// Emit the parsed DevServerError directly so the receiver (dev.ts)
434-
// can access stderrLines, title, and type for the error page.
435-
// Previously this was wrapped in SfError which lost those properties.
436-
this.emit('error', parsedError);
433+
// Convert to SfError for proper error handling
434+
// Use just the message (not title) since title will be shown separately
435+
// Prefix with ❌ for visual consistency with success messages (✅)
436+
const sfError = new SfError(`❌ ${parsedError.message}`, 'DevServerError', parsedError.suggestions);
437+
438+
this.emit('error', sfError);
437439
}
438440

439441
// Reset state
@@ -450,7 +452,7 @@ export class DevServerManager extends EventEmitter {
450452
private handleProcessError(error: Error): void {
451453
this.logger.error(`Dev server process error: ${error.message}`);
452454

453-
const sfError = new SfError(`Dev server process error: ${error.message}`, 'DevServerProcessError', [
455+
const sfError = new SfError(`Dev server process error: ${error.message}`, 'DevServerProcessError', [
454456
'Check that the command is correct in webapplication.json',
455457
'Verify all dependencies are installed',
456458
'Try running the command manually to see the error',
@@ -469,7 +471,7 @@ export class DevServerManager extends EventEmitter {
469471
this.logger.error('Dev server failed to start within timeout period');
470472

471473
const error = new SfError(
472-
`Dev server did not start within ${this.options.startupTimeout / 1000} seconds`,
474+
`Dev server did not start within ${this.options.startupTimeout / 1000} seconds`,
473475
'DevServerStartupTimeout',
474476
[
475477
'The dev server may be taking longer than expected to start',

0 commit comments

Comments
 (0)