Skip to content

Commit fa4c3e8

Browse files
feat: configure LDP server with certificate + some cleanup (#72)
* feat: configure LDP server with certificate + some cleanup * chore: refactor * chore: implement getNextAvailablePort() * chore: refactor to pass cert data * Update messages/lightning.preview.app.md Co-authored-by: moorejacqueline <[email protected]> * Update messages/lightning.preview.app.md Co-authored-by: moorejacqueline <[email protected]> * chore: pr feedback * ci: disable builds on fork PRs * chore: remove encoding option * chore: remove encoding option --------- Co-authored-by: moorejacqueline <[email protected]>
1 parent b316dfd commit fa4c3e8

File tree

17 files changed

+915
-758
lines changed

17 files changed

+915
-758
lines changed

.github/workflows/test.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ on:
33
push:
44
branches-ignore: [main]
55
workflow_dispatch:
6-
pull_request:
76

87
env:
98
UT_DISABLE_NODE_CURRENT: true

messages/lightning.preview.app.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ Type of device to emulate in preview.
3535

3636
For mobile virtual devices, specify the device ID to preview. If omitted, the first available virtual device will be used.
3737

38+
# error.no-project
39+
40+
This command is required to run from within a Salesforce project directory. %s
41+
3842
# error.fetching.app-id
3943

4044
Unable to determine App Id for %s
@@ -63,15 +67,21 @@ Preparing to download
6367

6468
Downloading
6569

66-
# certificate.attention
70+
# trust.local.dev.server
71+
72+
Note: Your desktop browser requires additional configuration to trust the local development server. See the documentation for more details.
73+
74+
# certificate.installation.notice
75+
76+
To use local preview on your device, you have to install a self-signed certificate on it. If you previously set up a certificate for your device, you can skip this step.
77+
78+
# certificate.installation.skip.message
6779

68-
╔═══════════╗
69-
║ ATTENTION ║
70-
╚═══════════╝
80+
Do you want to skip this step
7181

7282
# certificate.installation.description
7383

74-
If you have not done so already, please install the self-signed certificate on your device before proceeding. The certificate file is located at
84+
Before proceeding, install the self-signed certificate on your device. The certificate file is located at
7585

7686
`%s`
7787

messages/shared.utils.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
1-
# lwc-dev-server-utils.port-desc
1+
# config-utils.port-desc
22

33
The port number of the local dev server
44

5-
# lwc-dev-server-utils.port-message
5+
# config-utils.port-error-message
66

7-
Must be a number between 1 and 65535
7+
The port number must be a number between 1 and 65535
88

9-
# lwc-dev-server-utils.workspace-desc
9+
# config-utils.workspace-desc
1010

1111
The workspace name of the local lwc dev server
1212

13-
# lwc-dev-server-utils.workspace-message
13+
# config-utils.workspace-error-message
1414

1515
Valid workspace value is "SalesforceCLI" OR "mrt"
1616

17-
# identity-utils.token-desc
17+
# config-utils.token-desc
1818

1919
The Base64-encoded identity token of the local web server
20+
21+
# config-utils.cert-desc
22+
23+
The SSL certificate data to be used by the local dev server for secure connections
24+
25+
# config-utils.cert-error-message
26+
27+
You must provide valid SSL certificate data

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
"@lwrjs/api": "0.13.0-alpha.22",
99
"@lwc/lwc-dev-server": "^8.1.1",
1010
"@lwc/sfdc-lwc-compiler": "^8.1.1",
11-
"@oclif/core": "^3.26.6",
12-
"@salesforce/core": "^7.3.9",
11+
"@oclif/core": "^4.0.7",
12+
"@salesforce/core": "^8.1.0",
1313
"@salesforce/kit": "^3.1.6",
14-
"@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.4",
15-
"@salesforce/sf-plugins-core": "^9.1.1",
14+
"@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.5",
15+
"@salesforce/sf-plugins-core": "^11.1.2",
1616
"@inquirer/select": "^2.3.7",
1717
"chalk": "^5.3.0",
1818
"lwc": "7.0.0",
@@ -23,7 +23,7 @@
2323
"devDependencies": {
2424
"@oclif/plugin-command-snapshot": "^5.2.3",
2525
"@salesforce/cli-plugins-testkit": "^5.3.16",
26-
"@salesforce/dev-scripts": "^9.1.2",
26+
"@salesforce/dev-scripts": "^10.2.2",
2727
"@salesforce/plugin-command-reference": "^3.1.6",
2828
"@types/node-fetch": "^2.6.11",
2929
"@types/tar": "^6.1.13",

src/commands/lightning/preview/app.ts

Lines changed: 99 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import path from 'node:path';
99
import * as readline from 'node:readline';
10-
import { Logger, Messages } from '@salesforce/core';
10+
import { Logger, Messages, SfProject } from '@salesforce/core';
1111
import {
1212
AndroidAppPreviewConfig,
1313
AndroidVirtualDevice,
@@ -21,7 +21,6 @@ import chalk from 'chalk';
2121
import { OrgUtils } from '../../../shared/orgUtils.js';
2222
import { startLWCServer } from '../../../lwc-dev-server/index.js';
2323
import { PreviewUtils } from '../../../shared/previewUtils.js';
24-
import { LwcDevServerUtils } from '../../../shared/lwcDevServerUtils.js';
2524

2625
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
2726
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.preview.app');
@@ -37,6 +36,8 @@ export const androidSalesforceAppPreviewConfig = {
3736
activity: 'com.salesforce.chatter.Chatter',
3837
} as AndroidAppPreviewConfig;
3938

39+
const maxInt32 = 2_147_483_647; // maximum 32-bit signed integer value
40+
4041
export default class LightningPreviewApp extends SfCommand<void> {
4142
public static readonly summary = messages.getMessage('summary');
4243
public static readonly description = messages.getMessage('description');
@@ -62,15 +63,44 @@ export default class LightningPreviewApp extends SfCommand<void> {
6263
}),
6364
};
6465

65-
public static async waitForUserToInstallCert(
66+
private static async waitForKeyPress(): Promise<void> {
67+
return new Promise((resolve) => {
68+
const rl = readline.createInterface({
69+
input: process.stdin,
70+
output: process.stdout,
71+
});
72+
73+
// eslint-disable-next-line no-console
74+
console.log(`\n${messages.getMessage('certificate.waiting')}\n`);
75+
76+
process.stdin.setRawMode(true);
77+
process.stdin.resume();
78+
process.stdin.once('data', () => {
79+
process.stdin.setRawMode(false);
80+
process.stdin.pause();
81+
rl.close();
82+
resolve();
83+
});
84+
});
85+
}
86+
87+
public async waitForUserToInstallCert(
6688
platform: Platform.ios | Platform.android,
6789
device: IOSSimulatorDevice | AndroidVirtualDevice,
6890
certFilePath: string
6991
): Promise<void> {
70-
let attention = `\n${messages.getMessage('certificate.attention')}`;
71-
attention = chalk.red(attention);
7292
// eslint-disable-next-line no-console
73-
console.log(attention);
93+
console.log(`\n${messages.getMessage('certificate.installation.notice')}`);
94+
95+
const skipInstall = await this.confirm({
96+
message: messages.getMessage('certificate.installation.skip.message'),
97+
defaultAnswer: true,
98+
ms: maxInt32, // simulate no timeout and wait for user to answer
99+
});
100+
101+
if (skipInstall) {
102+
return;
103+
}
74104

75105
let installationSteps = '';
76106
if (platform === Platform.ios) {
@@ -111,34 +141,6 @@ export default class LightningPreviewApp extends SfCommand<void> {
111141
return LightningPreviewApp.waitForKeyPress();
112142
}
113143

114-
private static async waitForKeyPress(): Promise<void> {
115-
return new Promise((resolve) => {
116-
// Emit keypress events on stdin
117-
readline.emitKeypressEvents(process.stdin);
118-
// Set stdin to raw mode
119-
if (process.stdin.isTTY) {
120-
process.stdin.setRawMode(true);
121-
}
122-
123-
// eslint-disable-next-line no-console
124-
console.log(`\n${messages.getMessage('certificate.waiting')}\n`);
125-
126-
// Function to handle key press
127-
function onKeyPress(): void {
128-
// Restore stdin settings
129-
if (process.stdin.isTTY) {
130-
process.stdin.setRawMode(false);
131-
}
132-
process.stdin.removeListener('keypress', onKeyPress);
133-
process.stdin.pause();
134-
resolve();
135-
}
136-
137-
// Add keypress listener
138-
process.stdin.on('keypress', onKeyPress);
139-
});
140-
}
141-
142144
public async run(): Promise<void> {
143145
const { flags } = await this.parse(LightningPreviewApp);
144146
const logger = await Logger.child(this.ctor.name);
@@ -148,13 +150,12 @@ export default class LightningPreviewApp extends SfCommand<void> {
148150
const targetOrg = flags['target-org'];
149151
const deviceId = flags['device-id'];
150152

151-
logger.debug('Determining Local Dev Server url');
152-
// todo: figure out how to make the port dynamic instead of hard-coded value here
153-
const ldpServerUrl = PreviewUtils.generateWebSocketUrlForLocalDevServer(
154-
platform,
155-
`${await LwcDevServerUtils.getLocalDevServerPort()}`
156-
);
157-
logger.debug(`Local Dev Server url is ${ldpServerUrl}`);
153+
let sfdxProjectRootPath = '';
154+
try {
155+
sfdxProjectRootPath = await SfProject.resolveProjectPath();
156+
} catch (error) {
157+
return Promise.reject(new Error(messages.getMessage('error.no-project', [(error as Error)?.message ?? ''])));
158+
}
158159

159160
let appId: string | undefined;
160161
if (appName) {
@@ -170,16 +171,39 @@ export default class LightningPreviewApp extends SfCommand<void> {
170171
logger.debug(`App Id is ${appId}`);
171172
}
172173

174+
logger.debug('Determining the next available port for Local Dev Server');
175+
const serverPort = await PreviewUtils.getNextAvailablePort();
176+
logger.debug(`Next available port is ${serverPort}`);
177+
178+
logger.debug('Determining Local Dev Server url');
179+
const ldpServerUrl = PreviewUtils.generateWebSocketUrlForLocalDevServer(platform, serverPort);
180+
logger.debug(`Local Dev Server url is ${ldpServerUrl}`);
181+
173182
if (platform === Platform.desktop) {
174-
await this.desktopPreview(ldpServerUrl, appId, logger);
183+
await this.desktopPreview(sfdxProjectRootPath, serverPort, ldpServerUrl, appId, logger);
175184
} else {
176-
await this.mobilePreview(platform, ldpServerUrl, appName, appId, deviceId, logger);
185+
await this.mobilePreview(
186+
platform,
187+
sfdxProjectRootPath,
188+
serverPort,
189+
ldpServerUrl,
190+
appName,
191+
appId,
192+
deviceId,
193+
logger
194+
);
177195
}
178196
}
179197

180-
private async desktopPreview(ldpServerUrl: string, appId?: string, logger?: Logger): Promise<void> {
198+
private async desktopPreview(
199+
sfdxProjectRootPath: string,
200+
serverPort: number,
201+
ldpServerUrl: string,
202+
appId: string | undefined,
203+
logger: Logger
204+
): Promise<void> {
181205
if (!appId) {
182-
logger?.debug('No Lightning Experience application name provided.... using the default app instead.');
206+
logger.debug('No Lightning Experience application name provided.... using the default app instead.');
183207
}
184208

185209
// There are various ways to pass in a target org (as an alias, as a username, etc).
@@ -202,46 +226,55 @@ export default class LightningPreviewApp extends SfCommand<void> {
202226
targetOrg = this.argv[idx + 1];
203227
}
204228

229+
const protocol = new URL(ldpServerUrl).protocol.replace(':', '').toLowerCase();
230+
if (protocol === 'wss') {
231+
this.log(`\n${messages.getMessage('trust.local.dev.server')}`);
232+
}
233+
205234
const launchArguments = PreviewUtils.generateDesktopPreviewLaunchArguments(ldpServerUrl, appId, targetOrg);
206235

207236
// Start the LWC Dev Server
208-
await startLWCServer(process.cwd(), logger ? logger : await Logger.child(this.ctor.name));
237+
await startLWCServer(logger, sfdxProjectRootPath, serverPort, protocol);
209238

239+
// Open the browser and navigate to the right page
210240
await this.config.runCommand('org:open', launchArguments);
211241
}
212242

213243
private async mobilePreview(
214244
platform: Platform.ios | Platform.android,
245+
sfdxProjectRootPath: string,
246+
serverPort: number,
215247
ldpServerUrl: string,
216-
appName?: string,
217-
appId?: string,
218-
deviceId?: string,
219-
logger?: Logger
248+
appName: string | undefined,
249+
appId: string | undefined,
250+
deviceId: string | undefined,
251+
logger: Logger
220252
): Promise<void> {
221253
try {
222-
// 1. Verify that user environment is set up for mobile (i.e. has right tooling)
254+
// Verify that user environment is set up for mobile (i.e. has right tooling)
223255
await this.verifyMobileRequirements(platform, logger);
224256

225-
// 2. Fetch the target device
257+
// Fetch the target device
226258
const device = await PreviewUtils.getMobileDevice(platform, deviceId, logger);
227259
if (!device) {
228260
throw new Error(messages.getMessage('error.device.notfound', [deviceId ?? '']));
229261
}
230262

231-
// 3. Boot the device if not already booted
263+
// Boot the device if not already booted
232264
this.spinner.start(messages.getMessage('spinner.device.boot', [device.toString()]));
233265
const resolvedDeviceId = platform === Platform.ios ? (device as IOSSimulatorDevice).udid : device.name;
234266
const emulatorPort = await PreviewUtils.bootMobileDevice(platform, resolvedDeviceId, logger);
235267
this.spinner.stop();
236268

237-
// 4. Generate self-signed certificate and wait for user to install it
238-
// TODO: update the save location to be the same as server config file path
269+
// Configure certificates for dev server secure connection
239270
this.spinner.start(messages.getMessage('spinner.cert.gen'));
240-
const certFilePath = PreviewUtils.generateSelfSignedCert(platform, '~/Desktop/cert');
271+
const { certData, certFilePath } = await PreviewUtils.generateSelfSignedCert(platform, sfdxProjectRootPath);
241272
this.spinner.stop();
242-
await LightningPreviewApp.waitForUserToInstallCert(platform, device, certFilePath);
243273

244-
// 5. Check if Salesforce Mobile App is installed on the device
274+
// Show message and wait for user to install the certificate on their device
275+
await this.waitForUserToInstallCert(platform, device, certFilePath);
276+
277+
// Check if Salesforce Mobile App is installed on the device
245278
const appConfig = platform === Platform.ios ? iOSSalesforceAppPreviewConfig : androidSalesforceAppPreviewConfig;
246279
const appInstalled = await PreviewUtils.verifyMobileAppInstalled(
247280
platform,
@@ -251,10 +284,9 @@ export default class LightningPreviewApp extends SfCommand<void> {
251284
logger
252285
);
253286

254-
// 6. If Salesforce Mobile App is not installed, download and install it
287+
// If Salesforce Mobile App is not installed, download and install it
255288
let bundlePath: string | undefined;
256289
if (!appInstalled) {
257-
const maxInt32 = 2_147_483_647; // maximum 32-bit signed integer value
258290
const proceedWithDownload = await this.confirm({
259291
message: messages.getMessage('mobileapp.download', [appConfig.name]),
260292
defaultAnswer: false,
@@ -285,8 +317,10 @@ export default class LightningPreviewApp extends SfCommand<void> {
285317
}
286318

287319
// Start the LWC Dev Server
288-
await startLWCServer(process.cwd(), logger ? logger : await Logger.child(this.ctor.name));
289-
// 7. Launch the native app for previewing (launchMobileApp will show its own spinner)
320+
const protocol = new URL(ldpServerUrl).protocol.replace(':', '').toLowerCase();
321+
await startLWCServer(logger, sfdxProjectRootPath, serverPort, protocol, certData);
322+
323+
// Launch the native app for previewing (launchMobileApp will show its own spinner)
290324
// eslint-disable-next-line camelcase
291325
appConfig.launch_arguments = PreviewUtils.generateMobileAppPreviewLaunchArguments(ldpServerUrl, appName, appId);
292326
await PreviewUtils.launchMobileApp(platform, appConfig, resolvedDeviceId, emulatorPort, bundlePath, logger);
@@ -305,13 +339,13 @@ export default class LightningPreviewApp extends SfCommand<void> {
305339
* @param platform A mobile platform (iOS or Android)
306340
* @param logger An optional logger to be used for logging
307341
*/
308-
private async verifyMobileRequirements(platform: Platform.ios | Platform.android, logger?: Logger): Promise<void> {
309-
logger?.debug(`Verifying environment meets requirements for previewing on ${platform}`);
342+
private async verifyMobileRequirements(platform: Platform.ios | Platform.android, logger: Logger): Promise<void> {
343+
logger.debug(`Verifying environment meets requirements for previewing on ${platform}`);
310344

311345
const setupCommand = new LwcDevMobileCoreSetup(['-p', platform], this.config);
312346
await setupCommand.init();
313347
await setupCommand.run();
314348

315-
logger?.debug('Requirements are met'); // if we make it here then all is good
349+
logger.debug('Requirements are met'); // if we make it here then all is good
316350
}
317351
}

0 commit comments

Comments
 (0)