Skip to content

feat: initial revision for app preview on desktop #42

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
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package-lock.json

# never checkin npm config
.npmrc
.yarnrc

# debug logs
npm-error.log
Expand Down
8 changes: 4 additions & 4 deletions command-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
[
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auto-generated after running yarn update-snapshots

{
"alias": [],
"command": "lightning:preview:component",
"command": "lightning:preview:app",
"flagAliases": [],
"flagChars": ["n"],
"flags": ["flags-dir", "json", "name"],
"flagChars": ["i", "n", "o", "t"],
"flags": ["device-id", "device-type", "flags-dir", "json", "name", "target-org"],
"plugin": "@salesforce/plugin-lightning-dev"
},
{
"alias": [],
"command": "lightning:preview:app",
"command": "lightning:preview:component",
"flagAliases": [],
"flagChars": ["n"],
"flags": ["flags-dir", "json", "name"],
Expand Down
20 changes: 16 additions & 4 deletions messages/lightning.preview.app.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Preview Lightning Experience Applications.

# description

Preview components, org, and sites. If no topic is specified, the default action is to preview the org.
Preview components, apps, and sites. If no topic is specified, the default action is to preview the org.

In dev preview mode, you can edit local files and see these changes to your Lightning Web Components (LWC) within your {org name} org:

Expand All @@ -21,11 +21,23 @@ Use the appropriate topic to preview specific aspects of the development environ

# flags.name.summary

Description of a flag.
Name of the Lightning Experience application to preview.

# flags.name.description
# flags.target-org.summary

More information about a flag. Don't repeat the summary.
Specify the org to preview.

# flags.device-type.summary

Type of device to emulate in preview.

# flags.device-id.summary

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

# error.fetching.app-id

Unable to determine App Id for %s

# examples

Expand Down
25 changes: 13 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,23 @@
"bugs": "https://github.com/forcedotcom/cli/issues",
"dependencies": {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I was pulling in @salesforce/lwc-dev-mobile-core I also decided to update the rest of the dependencies to the latest version.

"@oclif/core": "^3.26.6",
"@salesforce/core": "^7.3.2",
"@salesforce/kit": "^3.1.0",
"@salesforce/sf-plugins-core": "^9.0.7",
"tar": "^7.1.0",
"lwc": "6.6.2",
"lwr": "0.13.0-alpha.6",
"@lwrjs/api": "0.13.0-alpha.6"
"@salesforce/core": "^7.3.9",
"@salesforce/kit": "^3.1.2",
"@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.1",
"@salesforce/sf-plugins-core": "^9.0.14",
"tar": "^7.2.0",
"lwc": "6.6.4",
"lwr": "0.13.0-alpha.12",
"@lwrjs/api": "0.13.0-alpha.12"
},
"devDependencies": {
"@oclif/plugin-command-snapshot": "^5.1.9",
"@salesforce/cli-plugins-testkit": "^5.3.5",
"@salesforce/dev-scripts": "^9.0.0",
"@salesforce/plugin-command-reference": "^3.0.82",
"@salesforce/cli-plugins-testkit": "^5.3.8",
"@salesforce/dev-scripts": "^9.1.2",
"@salesforce/plugin-command-reference": "^3.0.88",
"@types/tar": "^6.1.13",
"eslint-plugin-sf-plugin": "^1.18.3",
"oclif": "^4.10.15",
"eslint-plugin-sf-plugin": "^1.18.5",
"oclif": "^4.11.3",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
},
Expand Down
16 changes: 0 additions & 16 deletions schemas/lightning-preview-app.json

This file was deleted.

122 changes: 109 additions & 13 deletions src/commands/lightning/preview/app.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,137 @@
/*
* Copyright (c) 2023, salesforce.com, inc.
* Copyright (c) 2024, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import { Messages, Logger, Org } from '@salesforce/core';
import { PreviewUtils } from '@salesforce/lwc-dev-mobile-core';
import { OrgUtils } from '../../../shared/orgUtils.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.preview.app');

export type LightningPreviewAppResult = {
path: string;
enum Platform {
desktop = 'desktop',
ios = 'ios',
android = 'android',
}

type LightningPreviewAppFlags = {
name: string | undefined;
'target-org': Org;
'device-type': Platform;
'device-id': string | undefined;
};

export default class LightningPreviewApp extends SfCommand<LightningPreviewAppResult> {
export default class LightningPreviewApp extends SfCommand<void> {
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
public static readonly examples = messages.getMessages('examples');

public static readonly flags = {
name: Flags.string({
summary: messages.getMessage('flags.name.summary'),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per latest discussion here, --name is now optional.

description: messages.getMessage('flags.name.description'),
char: 'n',
required: false,
}),
'target-org': Flags.requiredOrg({
summary: messages.getMessage('flags.target-org.summary'),
}),
'device-type': Flags.option({
summary: messages.getMessage('flags.device-type.summary'),
char: 't',
options: [Platform.desktop, Platform.ios, Platform.android] as const,
default: Platform.desktop,
})(),
'device-id': Flags.string({
summary: messages.getMessage('flags.device-id.summary'),
char: 'i',
}),
};

public async run(): Promise<LightningPreviewAppResult> {
public async run(): Promise<void> {
const { flags } = await this.parse(LightningPreviewApp);
const logger = await Logger.child(this.ctor.name);

if (flags['device-type'] === Platform.desktop) {
await this.desktopPreview(logger, flags);
} else if (flags['device-type'] === Platform.ios) {
await this.iosPreview(logger, flags);
} else if (flags['device-type'] === Platform.android) {
await this.androidPreview(logger, flags);
}
}

private async desktopPreview(logger: Logger, flags: LightningPreviewAppFlags): Promise<void> {
const appName = flags['name'];
const targetOrg = flags['target-org'];
const platform = flags['device-type'];

logger.debug('Determining Local Dev Server url');
// todo: figure out how to make the port dynamic instead of hard-coded value here
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the work for automatically starting the dev server progresses, we should have a better idea on how to achieve this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cc: @nrkruk

const ldpServerUrl = PreviewUtils.generateWebSocketUrlForLocalDevServer(platform, '8081');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nrkruk et al: I just want to call out that with this line we are (in this moment anyway) officially putting LDP-related utility functionality into the lwc-dev-mobile-core project/package. I think such functionality could live in there, or we could create a new independent package, or we could re-brand the existing one. No action necessary here: I just wanted to call it out for discussion.

logger.debug(`Local Dev Server url is ${ldpServerUrl}`);

// appPath will resolve to one of the following:
//
// lightning/app/<appId> => when the user is targetting a specific LEX app
// lightning => when the user is not targetting a specific LEX app
//
let appPath = '';
if (appName) {
logger.debug(`Determining App Id for ${appName}`);

// The appName is optional but if the user did provide an appName then it must be
// a valid one.... meaning that it should resolve to a valid appId.
const appId = await OrgUtils.getAppId(targetOrg.getConnection(undefined), appName);
if (!appId) {
throw new Error(messages.getMessage('error.fetching.app-id', [appName]));
}

logger.debug(`App Id is ${appId}`);

appPath = `lightning/app/${appId}`;
} else {
logger.debug('No Lightning Experience application name provided.... using the default app instead.');
appPath = 'lightning';
}

// we prepend a '0.' to all of the params to ensure they will persist across redirects
const orgOpenCommandArgs = ['--path', `${appPath}?0.aura.ldpServerUrl=${ldpServerUrl}&0.aura.mode=DEVPREVIEW`];

// There are various ways to pass in a target org (as an alias, as a username, etc).
// We could have LightningPreviewApp parse its --target-org flag which will be resolved
// to an Org object (see https://github.com/forcedotcom/sfdx-core/blob/main/src/org/org.ts)
// then write a bunch of code to look at this Org object to try to determine whether
// it was initialized using Alis, Username, etc. and get a string representation of the
// org to be forwarded to OrgOpenCommand.
//
// Or we could simply look at the raw arguments passed to the LightningPreviewApp command,
// find the raw value for --target-org flag and forward that raw value to OrgOpenCommand.
// The OrgOpenCommand will then parse the raw value automatically. If the value is
// valid then OrgOpenCommand will consume it and continue. And if the value is invalid then
// OrgOpenCommand simply throws an error which will get bubbled up to LightningPreviewApp.
//
// Here we've chosen the second approach
const idx = this.argv.findIndex((item) => item.toLowerCase() === '-o' || item.toLowerCase() === '--target-org');
if (idx >= 0 && idx < this.argv.length - 1) {
orgOpenCommandArgs.push('--target-org', this.argv[idx + 1]);
}

await this.config.runCommand('org:open', orgOpenCommandArgs);
}

// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars
private async iosPreview(logger: Logger, flags: LightningPreviewAppFlags): Promise<void> {
logger.info('Preview on iOS Not Implemented Yet');
return Promise.reject(new Error('Preview on iOS Not Implemented Yet'));
}

const name = flags.name ?? 'world';
this.log(`hello ${name} from /Users/nkruk/git/plugin-lightning-dev/src/commands/lightning/preview/org.ts`);
return {
path: '/Users/nkruk/git/plugin-lightning-dev/src/commands/lightning/preview/org.ts',
};
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars
private async androidPreview(logger: Logger, flags: LightningPreviewAppFlags): Promise<void> {
logger.info('Preview on Android Not Implemented Yet');
return Promise.reject(new Error('Preview on Android Not Implemented Yet'));
}
}
41 changes: 41 additions & 0 deletions src/shared/orgUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2024, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { Connection } from '@salesforce/core';

export class OrgUtils {
/**
* Given an app name, it queries the org to find the matching app id. To do so,
* it will first attempt at finding the app with a matching DeveloperName. If
* no match is found, it will then attempt at finding the app with a matching
* Label. If multiple matches are found, then the first match is returned.
*
Comment on lines +12 to +16
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See this discussion which lead to the implementation here.

* @param connection the connection to the org
* @param appName the name of the app
* @returns the app id or undefined if no match is found
*/
public static async getAppId(connection: Connection, appName: string): Promise<string | undefined> {
// NOTE: We have to break up the query and run against different columns separately instead
// of using OR statement, otherwise we'll get the error 'Disjunctions not supported'
const devNameQuery = `SELECT DurableId FROM AppDefinition WHERE DeveloperName LIKE '${appName}'`;
const labelQuery = `SELECT DurableId FROM AppDefinition WHERE Label LIKE '${appName}'`;

// First attempt to resolve using DeveloperName
let result = await connection.query<{ DurableId: string }>(devNameQuery);
if (result.totalSize > 0) {
return result.records[0].DurableId;
}

// If no matches, then resolve using Label
result = await connection.query<{ DurableId: string }>(labelQuery);
if (result.totalSize > 0) {
return result.records[0].DurableId;
}

return undefined;
}
}
Loading
Loading