Skip to content

feat: use both http and https ports when configuring local dev server #118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 29, 2024
Merged
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
30 changes: 15 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,33 @@
"author": "Salesforce",
"bugs": "https://github.com/forcedotcom/cli/issues",
"dependencies": {
"@lwrjs/api": "0.13.1",
"@lwc/lwc-dev-server": "^9.3.0",
"@lwc/sfdc-lwc-compiler": "^9.3.0",
"@oclif/core": "^4.0.12",
"@salesforce/core": "^8.2.3",
"@lwrjs/api": "0.13.3",
"@lwc/lwc-dev-server": "^9.4.0",
"@lwc/sfdc-lwc-compiler": "^9.4.0",
"@oclif/core": "^4.0.17",
"@salesforce/core": "^8.2.7",
"@salesforce/kit": "^3.1.6",
"@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.5",
"@salesforce/sf-plugins-core": "^11.2.1",
"@inquirer/select": "^2.4.2",
"@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.6",
"@salesforce/sf-plugins-core": "^11.2.4",
"@inquirer/select": "^2.4.3",
"chalk": "^5.3.0",
"lwc": "7.1.2",
"lwc": "7.2.0",
"lwr": "0.13.3",
"node-fetch": "^3.3.2",
"tar": "^7.4.3"
},
"devDependencies": {
"@oclif/plugin-command-snapshot": "^5.2.10",
"@salesforce/cli-plugins-testkit": "^5.3.17",
"@salesforce/dev-scripts": "^10.2.2",
"@salesforce/plugin-command-reference": "^3.1.8",
"@salesforce/cli-plugins-testkit": "^5.3.20",
"@salesforce/dev-scripts": "^10.2.7",
"@salesforce/plugin-command-reference": "^3.1.13",
"@types/node-fetch": "^2.6.11",
"@types/tar": "^6.1.13",
"eslint-plugin-sf-plugin": "^1.18.11",
"eslint-plugin-sf-plugin": "^1.20.1",
"esmock": "^2.6.7",
"oclif": "^4.13.10",
"oclif": "^4.14.12",
"ts-node": "^10.9.2",
"typescript": "^5.5.3"
"typescript": "^5.5.4"
},
"engines": {
"node": ">=18.0.0 <22"
Expand Down
18 changes: 9 additions & 9 deletions src/commands/lightning/preview/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,22 +199,22 @@ export default class LightningPreviewApp extends SfCommand<void> {
}

logger.debug('Determining the next available port for Local Dev Server');
const serverPort = await PreviewUtils.getNextAvailablePort();
logger.debug(`Next available port is ${serverPort}`);
const serverPorts = await PreviewUtils.getNextAvailablePorts();
logger.debug(`Next available ports are http=${serverPorts.httpPort} , https=${serverPorts.httpsPort}`);

logger.debug('Determining Local Dev Server url');
const ldpServerUrl = PreviewUtils.generateWebSocketUrlForLocalDevServer(platform, serverPort, logger);
const ldpServerUrl = PreviewUtils.generateWebSocketUrlForLocalDevServer(platform, serverPorts, logger);
logger.debug(`Local Dev Server url is ${ldpServerUrl}`);

const entityId = await PreviewUtils.getEntityId(username);

if (platform === Platform.desktop) {
await this.desktopPreview(sfdxProjectRootPath, serverPort, token, entityId, ldpServerUrl, appId, logger);
await this.desktopPreview(sfdxProjectRootPath, serverPorts, token, entityId, ldpServerUrl, appId, logger);
} else {
await this.mobilePreview(
platform,
sfdxProjectRootPath,
serverPort,
serverPorts,
token,
entityId,
ldpServerUrl,
Expand All @@ -228,7 +228,7 @@ export default class LightningPreviewApp extends SfCommand<void> {

private async desktopPreview(
sfdxProjectRootPath: string,
serverPort: number,
serverPorts: { httpPort: number; httpsPort: number },
token: string,
entityId: string,
ldpServerUrl: string,
Expand Down Expand Up @@ -271,7 +271,7 @@ export default class LightningPreviewApp extends SfCommand<void> {
);

// Start the LWC Dev Server
await startLWCServer(logger, sfdxProjectRootPath, token, serverPort);
await startLWCServer(logger, sfdxProjectRootPath, token, serverPorts);

// Open the browser and navigate to the right page
await this.config.runCommand('org:open', launchArguments);
Expand All @@ -280,7 +280,7 @@ export default class LightningPreviewApp extends SfCommand<void> {
private async mobilePreview(
platform: Platform.ios | Platform.android,
sfdxProjectRootPath: string,
serverPort: number,
serverPorts: { httpPort: number; httpsPort: number },
token: string,
entityId: string,
ldpServerUrl: string,
Expand Down Expand Up @@ -357,7 +357,7 @@ export default class LightningPreviewApp extends SfCommand<void> {

// Start the LWC Dev Server

await startLWCServer(logger, sfdxProjectRootPath, token, serverPort, certData);
await startLWCServer(logger, sfdxProjectRootPath, token, serverPorts, certData);

// Launch the native app for previewing (launchMobileApp will show its own spinner)
// eslint-disable-next-line camelcase
Expand Down
17 changes: 12 additions & 5 deletions src/lwc-dev-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Logger } from '@salesforce/core';
import { SSLCertificateData } from '@salesforce/lwc-dev-mobile-core';
import {
ConfigUtils,
LOCAL_DEV_SERVER_DEFAULT_PORT,
LOCAL_DEV_SERVER_DEFAULT_HTTP_PORT,
LOCAL_DEV_SERVER_DEFAULT_WORKSPACE,
} from '../shared/configUtils.js';

Expand Down Expand Up @@ -47,7 +47,7 @@ async function createLWCServerConfig(
logger: Logger,
rootDir: string,
token: string,
serverPort?: number,
serverPorts?: { httpPort: number; httpsPort: number },
certData?: SSLCertificateData,
workspace?: Workspace
): Promise<ServerConfig> {
Expand Down Expand Up @@ -76,10 +76,16 @@ async function createLWCServerConfig(
}
}

const ports = serverPorts ??
(await ConfigUtils.getLocalDevServerPorts()) ?? {
httpPort: LOCAL_DEV_SERVER_DEFAULT_HTTP_PORT,
httpsPort: LOCAL_DEV_SERVER_DEFAULT_HTTP_PORT + 1,
};

const serverConfig: ServerConfig = {
rootDir,
// use custom port if any is provided, or fetch from config file (if any), otherwise use the default port
port: serverPort ?? (await ConfigUtils.getLocalDevServerPort()) ?? LOCAL_DEV_SERVER_DEFAULT_PORT,
port: ports.httpPort,
paths: namespacePaths,
// use custom workspace if any is provided, or fetch from config file (if any), otherwise use the default workspace
workspace: workspace ?? (await ConfigUtils.getLocalDevServerWorkspace()) ?? LOCAL_DEV_SERVER_DEFAULT_WORKSPACE,
Expand All @@ -91,6 +97,7 @@ async function createLWCServerConfig(
serverConfig.https = {
cert: certData.pemCertificate,
key: certData.pemPrivateKey,
port: ports.httpsPort,
};
}

Expand All @@ -101,11 +108,11 @@ export async function startLWCServer(
logger: Logger,
rootDir: string,
token: string,
serverPort?: number,
serverPorts?: { httpPort: number; httpsPort: number },
certData?: SSLCertificateData,
workspace?: Workspace
): Promise<LWCServer> {
const config = await createLWCServerConfig(logger, rootDir, token, serverPort, certData, workspace);
const config = await createLWCServerConfig(logger, rootDir, token, serverPorts, certData, workspace);

logger.trace(`Starting LWC Dev Server with config: ${JSON.stringify(config)}`);
let lwcDevServer: LWCServer | null = await startLwcDevServer(config);
Expand Down
8 changes: 4 additions & 4 deletions src/shared/configUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type IdentityTokenService = {
saveTokenToServer(token: string): Promise<string>;
};

export const LOCAL_DEV_SERVER_DEFAULT_PORT = 8081;
export const LOCAL_DEV_SERVER_DEFAULT_HTTP_PORT = 8081;
export const LOCAL_DEV_SERVER_DEFAULT_WORKSPACE = Workspace.SfCli;

export type LocalWebServerIdentityData = {
Expand Down Expand Up @@ -104,11 +104,11 @@ export class ConfigUtils {
await config.write();
}

public static async getLocalDevServerPort(): Promise<number | undefined> {
public static async getLocalDevServerPorts(): Promise<{ httpPort: number; httpsPort: number } | undefined> {
const config = await this.getLocalConfig();
const configPort = config.get(ConfigVars.LOCAL_DEV_SERVER_PORT) as number;
const ports = config.get(ConfigVars.LOCAL_DEV_SERVER_PORT) as { httpPort: number; httpsPort: number };

return configPort;
return ports;
}

public static async getLocalDevServerWorkspace(): Promise<Workspace | undefined> {
Expand Down
69 changes: 38 additions & 31 deletions src/shared/previewUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
} from '@salesforce/lwc-dev-mobile-core';
import { Progress, Spinner } from '@salesforce/sf-plugins-core';
import fetch from 'node-fetch';
import { ConfigUtils, LOCAL_DEV_SERVER_DEFAULT_PORT } from './configUtils.js';
import { ConfigUtils, LOCAL_DEV_SERVER_DEFAULT_HTTP_PORT } from './configUtils.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.preview.app');
Expand All @@ -40,14 +40,14 @@ const DevPreviewAuraMode = 'DEVPREVIEW';
export class PreviewUtils {
public static generateWebSocketUrlForLocalDevServer(
platform: string,
port: string | number,
ports: { httpPort: number; httpsPort: number },
logger?: Logger
): string {
return LwcDevMobileCorePreviewUtils.generateWebSocketUrlForLocalDevServer(platform, port.toString(), logger);
return LwcDevMobileCorePreviewUtils.generateWebSocketUrlForLocalDevServer(platform, ports, logger);
}

/**
* Returns a port number to be used by the local dev server.
* Returns a pair of port numbers to be used by the local dev server for http and https.
*
* It starts by checking whether the user has configured a port in their config file.
* If so then we are only allowed to use that port, regardless of whether it is in use
Expand All @@ -58,38 +58,19 @@ export class PreviewUtils {
* If it is in use then we increment the port number by 2 and check if it is in use or not.
* This process is repeated until a port that is not in use is found.
*
* @returns a port number to be used by the local dev server.
* @returns a pair of port numbers to be used by the local dev server for http and https.
*/
public static async getNextAvailablePort(): Promise<number> {
const userConfiguredPort = await ConfigUtils.getLocalDevServerPort();
public static async getNextAvailablePorts(): Promise<{ httpPort: number; httpsPort: number }> {
const userConfiguredPorts = await ConfigUtils.getLocalDevServerPorts();

if (userConfiguredPort) {
return Promise.resolve(userConfiguredPort);
if (userConfiguredPorts) {
return Promise.resolve(userConfiguredPorts);
}

let port = LOCAL_DEV_SERVER_DEFAULT_PORT;
let done = false;

while (!done) {
const cmd =
process.platform === 'win32' ? `netstat -an | find "LISTENING" | find ":${port}"` : `lsof -i :${port}`;
const httpPort = await this.doGetNextAvailablePort(LOCAL_DEV_SERVER_DEFAULT_HTTP_PORT);
const httpsPort = await this.doGetNextAvailablePort(httpPort + 1);

try {
const result = CommonUtils.executeCommandSync(cmd);
if (result.trim()) {
port = port + 2; // that port is in use so try another
} else {
done = true;
}
} catch (error) {
// On some platforms (like mac) if the command doesn't produce
// any results then that is considered an error but in our case
// that means the port is not in use and is ready for us to use.
done = true;
}
}

return Promise.resolve(port);
return Promise.resolve({ httpPort, httpsPort });
}

/**
Expand Down Expand Up @@ -516,4 +497,30 @@ export class PreviewUtils {
return entityId;
}
}

private static async doGetNextAvailablePort(startingPort: number): Promise<number> {
let port = startingPort;
let done = false;

while (!done) {
const cmd =
process.platform === 'win32' ? `netstat -an | find "LISTENING" | find ":${port}"` : `lsof -i :${port}`;

try {
const result = CommonUtils.executeCommandSync(cmd);
if (result.trim()) {
port = port + 2; // that port is in use so try another
} else {
done = true;
}
} catch (error) {
// On some platforms (like mac) if the command doesn't produce
// any results then that is considered an error but in our case
// that means the port is not in use and is ready for us to use.
done = true;
}
}

return Promise.resolve(port);
}
}
5 changes: 4 additions & 1 deletion test/commands/lightning/preview/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,10 @@ describe('lightning preview app', () => {
const expectedOutputDir = path.dirname(testBundleArchive);
const expectedFinalBundlePath =
platform === Platform.ios ? path.join(expectedOutputDir, 'Chatter.app') : testBundleArchive;
const expectedLdpServerUrl = PreviewUtils.generateWebSocketUrlForLocalDevServer(platform, '8081');
const expectedLdpServerUrl = PreviewUtils.generateWebSocketUrlForLocalDevServer(platform, {
httpPort: 8081,
httpsPort: 8082,
});
const expectedDeviceId = platform === Platform.ios ? testIOSDevice.udid : testAndroidDevice.name;
const expectedAppConfig =
platform === Platform.ios ? iOSSalesforceAppPreviewConfig : androidSalesforceAppPreviewConfig;
Expand Down
2 changes: 1 addition & 1 deletion test/lwc-dev-server/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('lwc-dev-server', () => {

beforeEach(async () => {
$$.SANDBOX.stub(ConfigUtils, 'getOrCreateIdentityToken').resolves('testIdentityToken');
$$.SANDBOX.stub(ConfigUtils, 'getLocalDevServerPort').resolves(1234);
$$.SANDBOX.stub(ConfigUtils, 'getLocalDevServerPorts').resolves({ httpPort: 1234, httpsPort: 5678 });
$$.SANDBOX.stub(ConfigUtils, 'getLocalDevServerWorkspace').resolves(Workspace.SfCli);
$$.SANDBOX.stub(ConfigUtils, 'getCertData').resolves(undefined);
});
Expand Down
14 changes: 8 additions & 6 deletions test/shared/configUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,22 +140,24 @@ describe('configUtils', () => {
expect(resolved).to.equal(undefined);
});

it('getLocalDevServerPort returns undefined when value not found in config', async () => {
it('getLocalDevServerPorts returns undefined when value not found in config', async () => {
$$.SANDBOX.stub(Config, 'create').withArgs($$.SANDBOX.match.any).resolves(Config.prototype);
$$.SANDBOX.stub(Config, 'addAllowedProperties').withArgs($$.SANDBOX.match.any);
$$.SANDBOX.stub(Config.prototype, 'get').withArgs(ConfigVars.LOCAL_DEV_SERVER_PORT).returns(undefined);
const resolved = await ConfigUtils.getLocalDevServerPort();
const resolved = await ConfigUtils.getLocalDevServerPorts();

expect(resolved).to.be.undefined;
});

it('getLocalDevServerPort resolves to port value in config', async () => {
it('getLocalDevServerPorts resolves to port values in config', async () => {
$$.SANDBOX.stub(Config, 'create').withArgs($$.SANDBOX.match.any).resolves(Config.prototype);
$$.SANDBOX.stub(Config, 'addAllowedProperties').withArgs($$.SANDBOX.match.any);
$$.SANDBOX.stub(Config.prototype, 'get').withArgs(ConfigVars.LOCAL_DEV_SERVER_PORT).returns(123);
const resolved = await ConfigUtils.getLocalDevServerPort();
$$.SANDBOX.stub(Config.prototype, 'get')
.withArgs(ConfigVars.LOCAL_DEV_SERVER_PORT)
.returns({ httpPort: 123, httpsPort: 456 });
const resolved = await ConfigUtils.getLocalDevServerPorts();

expect(resolved).to.equal(123);
expect(resolved).to.deep.equal({ httpPort: 123, httpsPort: 456 });
});

it('getLocalDevServerWorkspace returns undefined when value not found in config', async () => {
Expand Down
Loading