Skip to content

feat: configure LDP server with certificate + some cleanup #72

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 10 commits into from
Jun 28, 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
1 change: 0 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ on:
push:
branches-ignore: [main]
workflow_dispatch:
pull_request:

env:
UT_DISABLE_NODE_CURRENT: true
Expand Down
20 changes: 15 additions & 5 deletions messages/lightning.preview.app.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ Type of device to emulate in preview.

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

# error.no-project

This command is required to run from within a Salesforce project directory. %s

# error.fetching.app-id

Unable to determine App Id for %s
Expand Down Expand Up @@ -63,15 +67,21 @@ Preparing to download

Downloading

# certificate.attention
# trust.local.dev.server

Note: Your desktop browser requires additional configuration to trust the local development server. See the documentation for more details.

# certificate.installation.notice

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.

# certificate.installation.skip.message

╔═══════════╗
║ ATTENTION ║
╚═══════════╝
Do you want to skip this step

# certificate.installation.description

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

`%s`

Expand Down
20 changes: 14 additions & 6 deletions messages/shared.utils.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
# lwc-dev-server-utils.port-desc
# config-utils.port-desc

The port number of the local dev server

# lwc-dev-server-utils.port-message
# config-utils.port-error-message

Must be a number between 1 and 65535
The port number must be a number between 1 and 65535

# lwc-dev-server-utils.workspace-desc
# config-utils.workspace-desc

The workspace name of the local lwc dev server

# lwc-dev-server-utils.workspace-message
# config-utils.workspace-error-message

Valid workspace value is "SalesforceCLI" OR "mrt"

# identity-utils.token-desc
# config-utils.token-desc

The Base64-encoded identity token of the local web server

# config-utils.cert-desc

The SSL certificate data to be used by the local dev server for secure connections

# config-utils.cert-error-message

You must provide valid SSL certificate data
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
"@lwrjs/api": "0.13.0-alpha.22",
"@lwc/lwc-dev-server": "^8.1.1",
"@lwc/sfdc-lwc-compiler": "^8.1.1",
"@oclif/core": "^3.26.6",
"@salesforce/core": "^7.3.9",
"@oclif/core": "^4.0.7",
"@salesforce/core": "^8.1.0",
"@salesforce/kit": "^3.1.6",
"@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.4",
"@salesforce/sf-plugins-core": "^9.1.1",
"@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.5",
"@salesforce/sf-plugins-core": "^11.1.2",
"@inquirer/select": "^2.3.7",
"chalk": "^5.3.0",
"lwc": "7.0.0",
Expand All @@ -23,7 +23,7 @@
"devDependencies": {
"@oclif/plugin-command-snapshot": "^5.2.3",
"@salesforce/cli-plugins-testkit": "^5.3.16",
"@salesforce/dev-scripts": "^9.1.2",
"@salesforce/dev-scripts": "^10.2.2",
"@salesforce/plugin-command-reference": "^3.1.6",
"@types/node-fetch": "^2.6.11",
"@types/tar": "^6.1.13",
Expand Down
164 changes: 99 additions & 65 deletions src/commands/lightning/preview/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import path from 'node:path';
import * as readline from 'node:readline';
import { Logger, Messages } from '@salesforce/core';
import { Logger, Messages, SfProject } from '@salesforce/core';
import {
AndroidAppPreviewConfig,
AndroidVirtualDevice,
Expand All @@ -21,7 +21,6 @@ import chalk from 'chalk';
import { OrgUtils } from '../../../shared/orgUtils.js';
import { startLWCServer } from '../../../lwc-dev-server/index.js';
import { PreviewUtils } from '../../../shared/previewUtils.js';
import { LwcDevServerUtils } from '../../../shared/lwcDevServerUtils.js';

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

const maxInt32 = 2_147_483_647; // maximum 32-bit signed integer value

export default class LightningPreviewApp extends SfCommand<void> {
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
Expand All @@ -62,15 +63,44 @@ export default class LightningPreviewApp extends SfCommand<void> {
}),
};

public static async waitForUserToInstallCert(
private static async waitForKeyPress(): Promise<void> {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

// eslint-disable-next-line no-console
console.log(`\n${messages.getMessage('certificate.waiting')}\n`);

process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.once('data', () => {
process.stdin.setRawMode(false);
process.stdin.pause();
rl.close();
resolve();
});
});
}

public async waitForUserToInstallCert(
platform: Platform.ios | Platform.android,
device: IOSSimulatorDevice | AndroidVirtualDevice,
certFilePath: string
): Promise<void> {
let attention = `\n${messages.getMessage('certificate.attention')}`;
attention = chalk.red(attention);
// eslint-disable-next-line no-console
console.log(attention);
console.log(`\n${messages.getMessage('certificate.installation.notice')}`);

const skipInstall = await this.confirm({
message: messages.getMessage('certificate.installation.skip.message'),
defaultAnswer: true,
ms: maxInt32, // simulate no timeout and wait for user to answer
});

if (skipInstall) {
return;
}

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

private static async waitForKeyPress(): Promise<void> {
return new Promise((resolve) => {
// Emit keypress events on stdin
readline.emitKeypressEvents(process.stdin);
// Set stdin to raw mode
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}

// eslint-disable-next-line no-console
console.log(`\n${messages.getMessage('certificate.waiting')}\n`);

// Function to handle key press
function onKeyPress(): void {
// Restore stdin settings
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
process.stdin.removeListener('keypress', onKeyPress);
process.stdin.pause();
resolve();
}

// Add keypress listener
process.stdin.on('keypress', onKeyPress);
});
}

public async run(): Promise<void> {
const { flags } = await this.parse(LightningPreviewApp);
const logger = await Logger.child(this.ctor.name);
Expand All @@ -148,13 +150,12 @@ export default class LightningPreviewApp extends SfCommand<void> {
const targetOrg = flags['target-org'];
const deviceId = flags['device-id'];

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

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

logger.debug('Determining the next available port for Local Dev Server');
const serverPort = await PreviewUtils.getNextAvailablePort();
logger.debug(`Next available port is ${serverPort}`);

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

if (platform === Platform.desktop) {
await this.desktopPreview(ldpServerUrl, appId, logger);
await this.desktopPreview(sfdxProjectRootPath, serverPort, ldpServerUrl, appId, logger);
} else {
await this.mobilePreview(platform, ldpServerUrl, appName, appId, deviceId, logger);
await this.mobilePreview(
platform,
sfdxProjectRootPath,
serverPort,
ldpServerUrl,
appName,
appId,
deviceId,
logger
);
}
}

private async desktopPreview(ldpServerUrl: string, appId?: string, logger?: Logger): Promise<void> {
private async desktopPreview(
sfdxProjectRootPath: string,
serverPort: number,
ldpServerUrl: string,
appId: string | undefined,
logger: Logger
): Promise<void> {
if (!appId) {
logger?.debug('No Lightning Experience application name provided.... using the default app instead.');
logger.debug('No Lightning Experience application name provided.... using the default app instead.');
}

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

const protocol = new URL(ldpServerUrl).protocol.replace(':', '').toLowerCase();
if (protocol === 'wss') {
this.log(`\n${messages.getMessage('trust.local.dev.server')}`);
}

const launchArguments = PreviewUtils.generateDesktopPreviewLaunchArguments(ldpServerUrl, appId, targetOrg);

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

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

private async mobilePreview(
platform: Platform.ios | Platform.android,
sfdxProjectRootPath: string,
serverPort: number,
ldpServerUrl: string,
appName?: string,
appId?: string,
deviceId?: string,
logger?: Logger
appName: string | undefined,
appId: string | undefined,
deviceId: string | undefined,
logger: Logger
): Promise<void> {
try {
// 1. Verify that user environment is set up for mobile (i.e. has right tooling)
// Verify that user environment is set up for mobile (i.e. has right tooling)
await this.verifyMobileRequirements(platform, logger);

// 2. Fetch the target device
// Fetch the target device
const device = await PreviewUtils.getMobileDevice(platform, deviceId, logger);
if (!device) {
throw new Error(messages.getMessage('error.device.notfound', [deviceId ?? '']));
}

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

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

// 5. Check if Salesforce Mobile App is installed on the device
// Show message and wait for user to install the certificate on their device
await this.waitForUserToInstallCert(platform, device, certFilePath);

// Check if Salesforce Mobile App is installed on the device
const appConfig = platform === Platform.ios ? iOSSalesforceAppPreviewConfig : androidSalesforceAppPreviewConfig;
const appInstalled = await PreviewUtils.verifyMobileAppInstalled(
platform,
Expand All @@ -251,10 +284,9 @@ export default class LightningPreviewApp extends SfCommand<void> {
logger
);

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

// Start the LWC Dev Server
await startLWCServer(process.cwd(), logger ? logger : await Logger.child(this.ctor.name));
// 7. Launch the native app for previewing (launchMobileApp will show its own spinner)
const protocol = new URL(ldpServerUrl).protocol.replace(':', '').toLowerCase();
await startLWCServer(logger, sfdxProjectRootPath, serverPort, protocol, certData);

// Launch the native app for previewing (launchMobileApp will show its own spinner)
// eslint-disable-next-line camelcase
appConfig.launch_arguments = PreviewUtils.generateMobileAppPreviewLaunchArguments(ldpServerUrl, appName, appId);
await PreviewUtils.launchMobileApp(platform, appConfig, resolvedDeviceId, emulatorPort, bundlePath, logger);
Expand All @@ -305,13 +339,13 @@ export default class LightningPreviewApp extends SfCommand<void> {
* @param platform A mobile platform (iOS or Android)
* @param logger An optional logger to be used for logging
*/
private async verifyMobileRequirements(platform: Platform.ios | Platform.android, logger?: Logger): Promise<void> {
logger?.debug(`Verifying environment meets requirements for previewing on ${platform}`);
private async verifyMobileRequirements(platform: Platform.ios | Platform.android, logger: Logger): Promise<void> {
logger.debug(`Verifying environment meets requirements for previewing on ${platform}`);

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

logger?.debug('Requirements are met'); // if we make it here then all is good
logger.debug('Requirements are met'); // if we make it here then all is good
}
}
Loading