Skip to content

Commit b91e2f1

Browse files
committed
feat: move component preview to org
1 parent 1e291fe commit b91e2f1

File tree

3 files changed

+137
-69
lines changed

3 files changed

+137
-69
lines changed

src/commands/lightning/dev/app.ts

Lines changed: 3 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,12 @@ import {
1717
Platform,
1818
} from '@salesforce/lwc-dev-mobile-core';
1919
import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
20-
import { OrgUtils } from '../../../shared/orgUtils.js';
2120
import { startLWCServer } from '../../../lwc-dev-server/index.js';
2221
import { PreviewUtils } from '../../../shared/previewUtils.js';
2322
import { PromptUtils } from '../../../shared/promptUtils.js';
2423

2524
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
2625
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.dev.app');
27-
const sharedMessages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'shared.utils');
2826

2927
export const iOSSalesforceAppPreviewConfig = {
3028
name: 'Salesforce Mobile App',
@@ -77,29 +75,11 @@ export default class LightningDevApp extends SfCommand<void> {
7775
return Promise.reject(new Error(messages.getMessage('error.no-project', [(error as Error)?.message ?? ''])));
7876
}
7977

80-
const connection = targetOrg.getConnection(undefined);
81-
const username = connection.getUsername();
82-
if (!username) {
83-
return Promise.reject(new Error(messages.getMessage('error.username')));
84-
}
85-
86-
const localDevEnabled = await OrgUtils.isLocalDevEnabled(connection);
87-
if (!localDevEnabled) {
88-
return Promise.reject(new Error(sharedMessages.getMessage('error.localdev.not.enabled')));
89-
}
90-
91-
OrgUtils.ensureMatchingAPIVersion(connection);
78+
logger.debug('Initalizing preview connection and configuring local web server identity');
79+
const { connection, ldpServerId, ldpServerToken } = await PreviewUtils.initializePreviewConnection(targetOrg);
9280

9381
const platform = flags['device-type'] ?? (await PromptUtils.promptUserToSelectPlatform());
9482

95-
logger.debug('Configuring local web server identity');
96-
const appServerIdentity = await PreviewUtils.getOrCreateAppServerIdentity(connection);
97-
const ldpServerToken = appServerIdentity.identityToken;
98-
const ldpServerId = appServerIdentity.usernameToServerEntityIdMap[username];
99-
if (!ldpServerId) {
100-
return Promise.reject(new Error(messages.getMessage('error.identitydata.entityid')));
101-
}
102-
10383
const appId = await PreviewUtils.getLightningExperienceAppId(connection, appName, logger);
10484

10585
logger.debug('Determining the next available port for Local Dev Server');
@@ -149,25 +129,7 @@ export default class LightningDevApp extends SfCommand<void> {
149129
logger.debug('No Lightning Experience application name provided.... using the default app instead.');
150130
}
151131

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

172134
if (ldpServerUrl.startsWith('wss')) {
173135
this.log(`\n${messages.getMessage('trust.local.dev.server')}`);

src/commands/lightning/dev/component.ts

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,18 @@
66
*/
77

88
import path from 'node:path';
9-
import url from 'node:url';
109
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
1110
import { Messages, SfProject } from '@salesforce/core';
12-
import { cmpDev } from '@lwrjs/api';
1311
import { ComponentUtils } from '../../../shared/componentUtils.js';
1412
import { PromptUtils } from '../../../shared/promptUtils.js';
13+
import { PreviewUtils } from '../../../shared/previewUtils.js';
1514

1615
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
1716
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.dev.component');
1817

18+
// TODO: generate ldp server url
19+
const ldpServerUrl = 'http://localhost:3000';
20+
1921
export default class LightningDevComponent extends SfCommand<void> {
2022
public static readonly summary = messages.getMessage('summary');
2123
public static readonly description = messages.getMessage('description');
@@ -32,15 +34,19 @@ export default class LightningDevComponent extends SfCommand<void> {
3234
char: 'c',
3335
default: false,
3436
}),
35-
// TODO should this be required or optional?
36-
// We don't technically need this if your components are simple / don't need any data from your org
37-
'target-org': Flags.optionalOrg(),
37+
'target-org': Flags.requiredOrg(),
3838
};
3939

4040
public async run(): Promise<void> {
4141
const { flags } = await this.parse(LightningDevComponent);
4242
const project = await SfProject.resolve();
4343

44+
let componentName = flags['name'];
45+
const clientSelect = flags['client-select'];
46+
const targetOrg = flags['target-org'];
47+
48+
const { ldpServerId } = await PreviewUtils.initializePreviewConnection(targetOrg);
49+
4450
const namespacePaths = await ComponentUtils.getNamespacePaths(project);
4551
const componentPaths = await ComponentUtils.getAllComponentPaths(namespacePaths);
4652
if (!componentPaths) {
@@ -63,47 +69,49 @@ export default class LightningDevComponent extends SfCommand<void> {
6369
return undefined;
6470
}
6571

66-
const componentName = path.basename(componentPath);
67-
const label = ComponentUtils.componentNameToTitleCase(componentName);
72+
const name = path.basename(componentPath);
73+
const label = ComponentUtils.componentNameToTitleCase(name);
6874

6975
return {
70-
name: componentName,
76+
name,
7177
label: xml.LightningComponentBundle.masterLabel ?? label,
7278
description: xml.LightningComponentBundle.description ?? '',
7379
};
7480
})
7581
)
7682
).filter((component) => !!component);
7783

78-
let name = flags.name;
79-
if (!flags['client-select']) {
80-
if (name) {
84+
if (!clientSelect) {
85+
if (componentName) {
8186
// validate that the component exists before launching the server
82-
const match = components.find((component) => name === component.name || name === component.label);
87+
const match = components.find(
88+
(component) => componentName === component.name || componentName === component.label
89+
);
8390
if (!match) {
84-
throw new Error(messages.getMessage('error.component-not-found', [name]));
91+
throw new Error(messages.getMessage('error.component-not-found', [componentName]));
8592
}
8693

87-
name = match.name;
94+
componentName = match.name;
8895
} else {
8996
// prompt the user for a name if one was not provided
90-
name = await PromptUtils.promptUserToSelectComponent(components);
91-
if (!name) {
97+
componentName = await PromptUtils.promptUserToSelectComponent(components);
98+
if (!componentName) {
9299
throw new Error(messages.getMessage('error.component'));
93100
}
94101
}
95102
}
96103

97-
const dirname = path.dirname(url.fileURLToPath(import.meta.url));
98-
const rootDir = path.resolve(dirname, '../../../..');
99-
const port = parseInt(process.env.PORT ?? '3000', 10);
100-
101-
await cmpDev({
102-
rootDir,
103-
mode: 'dev',
104-
port,
105-
name: name ? `c/${name}` : undefined,
106-
namespacePaths,
107-
});
104+
// TODO: launch the local dev server
105+
106+
const targetOrgArg = PreviewUtils.getTargetOrgFromArguments(this.argv);
107+
const launchArguments = PreviewUtils.generateComponentPreviewLaunchArguments(
108+
ldpServerUrl,
109+
ldpServerId,
110+
componentName,
111+
targetOrgArg
112+
);
113+
114+
// Open the browser and navigate to the right page
115+
await this.config.runCommand('org:open', launchArguments);
108116
}
109117
}

src/shared/previewUtils.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import fs from 'node:fs';
1414
import os from 'node:os';
1515
import path from 'node:path';
16-
import { Connection, Logger, Messages } from '@salesforce/core';
16+
import { Connection, Logger, Messages, Org } from '@salesforce/core';
1717
import {
1818
AndroidDeviceManager,
1919
AppleDeviceManager,
@@ -33,8 +33,15 @@ import { PromptUtils } from './promptUtils.js';
3333

3434
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
3535
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.dev.app');
36+
const sharedMessages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'shared.utils');
3637
const DevPreviewAuraMode = 'DEVPREVIEW';
3738

39+
export type PreviewConnection = {
40+
connection: Connection;
41+
ldpServerId: string;
42+
ldpServerToken: string;
43+
};
44+
3845
export class PreviewUtils {
3946
public static generateWebSocketUrlForLocalDevServer(
4047
platform: string,
@@ -139,6 +146,37 @@ export class PreviewUtils {
139146
}
140147
}
141148

149+
/**
150+
* Extracts the target org from command line arguments.
151+
*
152+
* There are various ways to pass in a target org (as an alias, as a username, etc).
153+
* We could have LightningPreviewApp parse its --target-org flag which will be resolved
154+
* to an Org object (see https://github.com/forcedotcom/sfdx-core/blob/main/src/org/org.ts)
155+
* then write a bunch of code to look at this Org object to try to determine whether
156+
* it was initialized using Alias, Username, etc. and get a string representation of the
157+
* org to be forwarded to OrgOpenCommand.
158+
*
159+
* Or we could simply look at the raw arguments passed to the LightningPreviewApp command,
160+
* find the raw value for --target-org flag and forward that raw value to OrgOpenCommand.
161+
* The OrgOpenCommand will then parse the raw value automatically. If the value is
162+
* valid then OrgOpenCommand will consume it and continue. And if the value is invalid then
163+
* OrgOpenCommand simply throws an error which will get bubbled up to LightningPreviewApp.
164+
*
165+
* Here we've chosen the second approach.
166+
*
167+
* @param args - Array of command line arguments
168+
* @returns The target org identifier if found, undefined otherwise
169+
*/
170+
public static getTargetOrgFromArguments(args: string[]): string | undefined {
171+
const idx = args.findIndex((item) => item.toLowerCase() === '-o' || item.toLowerCase() === '--target-org');
172+
let targetOrg: string | undefined;
173+
if (idx >= 0 && idx < args.length - 1) {
174+
targetOrg = args[idx + 1];
175+
}
176+
177+
return targetOrg;
178+
}
179+
142180
/**
143181
* Generates the proper set of arguments to be used for launching desktop browser and navigating to the right location.
144182
*
@@ -176,6 +214,38 @@ export class PreviewUtils {
176214
return launchArguments;
177215
}
178216

217+
/**
218+
* Generates the proper set of arguments to be used for launching a component preview in the browser.
219+
*
220+
* @param ldpServerUrl The URL for the local dev server
221+
* @param ldpServerId Record ID for the identity token
222+
* @param componentName The name of the component to preview
223+
* @param targetOrg An optional org id
224+
* @returns Array of arguments to be used by Org:Open command for launching the component preview
225+
*/
226+
public static generateComponentPreviewLaunchArguments(
227+
ldpServerUrl: string,
228+
ldpServerId: string,
229+
componentName?: string,
230+
targetOrg?: string
231+
): string[] {
232+
// TODO: vanity application target
233+
let appPath = `lwr/application/ai/${encodeURIComponent(
234+
'localdev%2Fpreview'
235+
)}?ldpServerUrl=${ldpServerUrl}&ldpServerId=${ldpServerId}`;
236+
if (componentName) {
237+
appPath += `&specifier=c/${componentName}`;
238+
}
239+
240+
const launchArguments = ['--path', appPath];
241+
242+
if (targetOrg) {
243+
launchArguments.push('--target-org', targetOrg);
244+
}
245+
246+
return launchArguments;
247+
}
248+
179249
/**
180250
* Generates the proper set of arguments to be used for launching a mobile app with custom launch arguments.
181251
*
@@ -324,6 +394,34 @@ export class PreviewUtils {
324394
});
325395
}
326396

397+
public static async initializePreviewConnection(targetOrg: Org): Promise<PreviewConnection> {
398+
const connection = targetOrg.getConnection(undefined);
399+
const username = connection.getUsername();
400+
if (!username) {
401+
return Promise.reject(new Error(messages.getMessage('error.username')));
402+
}
403+
404+
const localDevEnabled = await OrgUtils.isLocalDevEnabled(connection);
405+
if (!localDevEnabled) {
406+
return Promise.reject(new Error(sharedMessages.getMessage('error.localdev.not.enabled')));
407+
}
408+
409+
OrgUtils.ensureMatchingAPIVersion(connection);
410+
411+
const appServerIdentity = await PreviewUtils.getOrCreateAppServerIdentity(connection);
412+
const ldpServerToken = appServerIdentity.identityToken;
413+
const ldpServerId = appServerIdentity.usernameToServerEntityIdMap[username];
414+
if (!ldpServerId) {
415+
return Promise.reject(new Error(messages.getMessage('error.identitydata.entityid')));
416+
}
417+
418+
return {
419+
connection,
420+
ldpServerId,
421+
ldpServerToken,
422+
};
423+
}
424+
327425
public static async getOrCreateAppServerIdentity(connection: Connection): Promise<LocalWebServerIdentityData> {
328426
const username = connection.getUsername()!;
329427

0 commit comments

Comments
 (0)