-
Notifications
You must be signed in to change notification settings - Fork 12
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ package-lock.json | |
|
||
# never checkin npm config | ||
.npmrc | ||
.yarnrc | ||
|
||
# debug logs | ||
npm-error.log | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,22 +6,23 @@ | |
"bugs": "https://github.com/forcedotcom/cli/issues", | ||
"dependencies": { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since I was pulling in |
||
"@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" | ||
}, | ||
|
This file was deleted.
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'), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As per latest discussion here, |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cc: @nrkruk |
||
const ldpServerUrl = PreviewUtils.generateWebSocketUrlForLocalDevServer(platform, '8081'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
maliroteh-sf marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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.'); | ||
maliroteh-sf marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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'); | ||
maliroteh-sf marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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')); | ||
} | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}'`; | ||
maliroteh-sf marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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; | ||
} | ||
} |
There was a problem hiding this comment.
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