Skip to content

Commit da39c4c

Browse files
Address review comments: URL mismatch tests, multi-meta warning, 60s timeout
- Add comprehensive tests for devServerUrl vs actualDevServerUrl mismatch combinations (explicit URL match/mismatch, manifest, skipDevServer cases) - Warn when multiple .webapplication-meta.xml files found in directory; use first match for backward compatibility - Increase dev server startup timeout from 30 to 60 seconds to align with VS Code extension and support slower dev server startups Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ea13492 commit da39c4c

4 files changed

Lines changed: 191 additions & 6 deletions

File tree

src/commands/webapp/dev.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ export default class WebappDev extends SfCommand<WebAppDevResult> {
286286
this.devServerManager = new DevServerManager({
287287
command: devCommand,
288288
cwd: webappDir,
289+
startupTimeout: 60_000, // 60 seconds - aligned with VS Code extension
289290
});
290291

291292
// Setup dev server event handlers
@@ -314,13 +315,13 @@ export default class WebappDev extends SfCommand<WebAppDevResult> {
314315
const actualDevServerUrl = await new Promise<string>((resolve, reject) => {
315316
const timeout = setTimeout(() => {
316317
reject(
317-
new SfError('❌ Dev server did not start within 30 seconds.', 'DevServerTimeoutError', [
318+
new SfError('❌ Dev server did not start within 60 seconds.', 'DevServerTimeoutError', [
318319
'The dev server may be taking longer than expected to start',
319320
'Check if the dev server command is correct in webapplication.json',
320321
'Try running the dev server command manually to see if it starts',
321322
])
322323
);
323-
}, 30_000);
324+
}, 60_000);
324325

325326
this.devServerManager?.on('ready', (url: string) => {
326327
clearTimeout(timeout);

src/config/webappDiscovery.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616

1717
import { access, readdir, readFile } from 'node:fs/promises';
1818
import { basename, dirname, join, relative } from 'node:path';
19-
import { SfError, SfProject } from '@salesforce/core';
19+
import { Logger, SfError, SfProject } from '@salesforce/core';
2020
import type { WebAppManifest } from './manifest.js';
2121

22+
const logger = Logger.childFromRoot('WebappDiscovery');
23+
2224
/**
2325
* Default command to run when no webapplication.json manifest is found
2426
*/
@@ -76,18 +78,32 @@ function isWebapplicationsFolder(folderName: string): boolean {
7678

7779
/**
7880
* Check if a directory contains a {name}.webapplication-meta.xml file
79-
* Returns the webapp name extracted from the filename, or null if not found
81+
* Returns the webapp name extracted from the filename, or null if not found.
82+
* Logs a warning if multiple metadata files are found (uses first match).
8083
*/
8184
async function findWebappMetaXml(dirPath: string): Promise<string | null> {
8285
try {
8386
const entries = await readdir(dirPath);
87+
const matches: string[] = [];
88+
8489
for (const entry of entries) {
8590
const match = WEBAPP_META_XML_PATTERN.exec(entry);
8691
if (match) {
87-
return match[1]; // Return the webapp name from the filename
92+
matches.push(match[1]);
8893
}
8994
}
90-
return null;
95+
96+
if (matches.length === 0) {
97+
return null;
98+
}
99+
100+
if (matches.length > 1) {
101+
logger.warn(
102+
`Multiple .webapplication-meta.xml files found in ${dirPath}: ${matches.join(', ')}. Using "${matches[0]}".`
103+
);
104+
}
105+
106+
return matches[0];
91107
} catch {
92108
return null;
93109
}

test/commands/webapp/dev.test.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,4 +258,156 @@ describe('webapp:dev command integration', () => {
258258
expect(manifest.dev?.command).to.not.be.empty;
259259
});
260260
});
261+
262+
/**
263+
* Tests for devServerUrl vs actualDevServerUrl mismatch handling.
264+
* Mirrors the logic in dev.ts (lines 335-342) to enumerate all combinations.
265+
*
266+
* Combinations:
267+
* - explicitUrlProvided: --url flag was passed
268+
* - skipDevServer: --url was reachable, so we never start dev server
269+
* - actualDevServerUrl: URL from DevServerManager 'ready' event (only when we start dev server)
270+
*/
271+
describe('Dev Server URL Mismatch', () => {
272+
/**
273+
* Helper that mirrors the mismatch check logic from dev.ts:
274+
* if (explicitUrlProvided && flags.url && flags.url !== actualDevServerUrl) { this.warn(...) }
275+
*/
276+
function shouldWarnUrlMismatch(
277+
explicitUrlProvided: boolean,
278+
flagsUrl: string | undefined,
279+
actualDevServerUrl: string
280+
): boolean {
281+
return !!(explicitUrlProvided && flagsUrl && flagsUrl !== actualDevServerUrl);
282+
}
283+
284+
/**
285+
* Helper that mirrors the final devServerUrl assignment when dev server is started:
286+
* devServerUrl = actualDevServerUrl
287+
*/
288+
function getFinalDevServerUrlWhenStarted(actualDevServerUrl: string): string {
289+
return actualDevServerUrl;
290+
}
291+
292+
describe('when --url is provided and dev server is started (explicitUrlProvided=true)', () => {
293+
it('should NOT warn when flags.url matches actualDevServerUrl', () => {
294+
const flagsUrl = 'http://localhost:5173';
295+
const actualDevServerUrl = 'http://localhost:5173';
296+
297+
expect(shouldWarnUrlMismatch(true, flagsUrl, actualDevServerUrl)).to.be.false;
298+
expect(getFinalDevServerUrlWhenStarted(actualDevServerUrl)).to.equal(flagsUrl);
299+
});
300+
301+
it('should warn when flags.url differs from actualDevServerUrl (different port)', () => {
302+
const flagsUrl = 'http://localhost:3000';
303+
const actualDevServerUrl = 'http://localhost:5173';
304+
305+
expect(shouldWarnUrlMismatch(true, flagsUrl, actualDevServerUrl)).to.be.true;
306+
expect(getFinalDevServerUrlWhenStarted(actualDevServerUrl)).to.equal(actualDevServerUrl);
307+
});
308+
309+
it('should warn when flags.url differs from actualDevServerUrl (localhost vs 127.0.0.1)', () => {
310+
const flagsUrl = 'http://localhost:5173';
311+
const actualDevServerUrl = 'http://127.0.0.1:5173';
312+
313+
expect(shouldWarnUrlMismatch(true, flagsUrl, actualDevServerUrl)).to.be.true;
314+
expect(getFinalDevServerUrlWhenStarted(actualDevServerUrl)).to.equal(actualDevServerUrl);
315+
});
316+
317+
it('should warn when flags.url differs from actualDevServerUrl (trailing slash)', () => {
318+
const flagsUrl = 'http://localhost:5173';
319+
const actualDevServerUrl = 'http://localhost:5173/';
320+
321+
expect(shouldWarnUrlMismatch(true, flagsUrl, actualDevServerUrl)).to.be.true;
322+
expect(getFinalDevServerUrlWhenStarted(actualDevServerUrl)).to.equal(actualDevServerUrl);
323+
});
324+
325+
it('should always use actualDevServerUrl as final devServerUrl when dev server is started', () => {
326+
const actualDevServerUrl = 'http://localhost:8080';
327+
expect(getFinalDevServerUrlWhenStarted(actualDevServerUrl)).to.equal(actualDevServerUrl);
328+
});
329+
});
330+
331+
describe('when --url is NOT provided (explicitUrlProvided=false)', () => {
332+
it('should NOT warn - no mismatch check when no explicit URL', () => {
333+
const actualDevServerUrl = 'http://localhost:5173';
334+
335+
expect(shouldWarnUrlMismatch(false, undefined, actualDevServerUrl)).to.be.false;
336+
expect(getFinalDevServerUrlWhenStarted(actualDevServerUrl)).to.equal(actualDevServerUrl);
337+
});
338+
339+
it('should use actualDevServerUrl from dev server (manifest has dev.command)', () => {
340+
const actualDevServerUrl = 'http://localhost:3000';
341+
expect(getFinalDevServerUrlWhenStarted(actualDevServerUrl)).to.equal(actualDevServerUrl);
342+
});
343+
});
344+
345+
describe('when --url is reachable (skipDevServer=true)', () => {
346+
it('should use flags.url as devServerUrl - no actualDevServerUrl, no mismatch possible', () => {
347+
const flagsUrl = 'http://localhost:5173';
348+
// When skipDevServer, we never get actualDevServerUrl - devServerUrl stays as flags.url
349+
expect(flagsUrl).to.equal('http://localhost:5173');
350+
});
351+
});
352+
353+
describe('when manifest has dev.url and no --url (skipDevServer=false, no dev server start)', () => {
354+
it('should use manifest.dev.url as devServerUrl - no actualDevServerUrl', () => {
355+
const manifestUrl = 'http://localhost:5173';
356+
// When manifest.dev.url && !explicitUrlProvided, we use manifest url, don't start dev server
357+
expect(manifestUrl).to.equal('http://localhost:5173');
358+
});
359+
});
360+
361+
describe('combination matrix: explicitUrlProvided × match/mismatch', () => {
362+
const combinations: Array<{
363+
explicitUrlProvided: boolean;
364+
flagsUrl: string | undefined;
365+
actualDevServerUrl: string;
366+
expectMismatch: boolean;
367+
description: string;
368+
}> = [
369+
{
370+
explicitUrlProvided: true,
371+
flagsUrl: 'http://localhost:5173',
372+
actualDevServerUrl: 'http://localhost:5173',
373+
expectMismatch: false,
374+
description: 'explicit URL matches actual',
375+
},
376+
{
377+
explicitUrlProvided: true,
378+
flagsUrl: 'http://localhost:3000',
379+
actualDevServerUrl: 'http://localhost:5173',
380+
expectMismatch: true,
381+
description: 'explicit URL different port',
382+
},
383+
{
384+
explicitUrlProvided: true,
385+
flagsUrl: 'http://127.0.0.1:5173',
386+
actualDevServerUrl: 'http://localhost:5173',
387+
expectMismatch: true,
388+
description: 'explicit URL different host',
389+
},
390+
{
391+
explicitUrlProvided: true,
392+
flagsUrl: 'https://localhost:5173',
393+
actualDevServerUrl: 'http://localhost:5173',
394+
expectMismatch: true,
395+
description: 'explicit URL different protocol',
396+
},
397+
{
398+
explicitUrlProvided: false,
399+
flagsUrl: undefined,
400+
actualDevServerUrl: 'http://localhost:5173',
401+
expectMismatch: false,
402+
description: 'no explicit URL - no mismatch check',
403+
},
404+
];
405+
406+
combinations.forEach(({ explicitUrlProvided, flagsUrl, actualDevServerUrl, expectMismatch, description }) => {
407+
it(`should ${expectMismatch ? 'warn' : 'not warn'} when ${description}`, () => {
408+
expect(shouldWarnUrlMismatch(explicitUrlProvided, flagsUrl, actualDevServerUrl)).to.equal(expectMismatch);
409+
});
410+
});
411+
});
412+
});
261413
});

test/config/webappDiscovery.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,5 +285,21 @@ describe('webappDiscovery', () => {
285285
expect(result.webapp?.name).to.equal('standalone-app');
286286
expect(result.allWebapps).to.have.length(1);
287287
});
288+
289+
it('should warn and use first match when directory has multiple .webapplication-meta.xml files', async () => {
290+
setupSfdxProject();
291+
292+
// Create webapp directory with multiple metadata files (misconfiguration)
293+
const multiMetaPath = join(sfdxWebappsPath, 'multi-meta-app');
294+
mkdirSync(multiMetaPath, { recursive: true });
295+
writeFileSync(join(multiMetaPath, 'alpha.webapplication-meta.xml'), '<WebApplication/>');
296+
writeFileSync(join(multiMetaPath, 'beta.webapplication-meta.xml'), '<WebApplication/>');
297+
298+
const result = await discoverWebapp(undefined, testDir);
299+
300+
// Discovery should succeed - uses first match (order depends on readdir)
301+
expect(result.allWebapps).to.have.length(1);
302+
expect(['alpha', 'beta']).to.include(result.allWebapps[0].name);
303+
});
288304
});
289305
});

0 commit comments

Comments
 (0)