Skip to content

Commit 2d9fb17

Browse files
authored
feat: move component preview to org (#447)
1 parent ebb9fad commit 2d9fb17

File tree

5 files changed

+364
-72
lines changed

5 files changed

+364
-72
lines changed

src/commands/lightning/dev/app.ts

Lines changed: 3 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ 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';
@@ -77,29 +76,11 @@ export default class LightningDevApp extends SfCommand<void> {
7776
throw new Error(sharedMessages.getMessage('error.no-project', [(error as Error)?.message ?? '']));
7877
}
7978

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

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

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-
throw new Error(sharedMessages.getMessage('error.identitydata.entityid'));
101-
}
102-
10384
const appId = await PreviewUtils.getLightningExperienceAppId(connection, appName, logger);
10485

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

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-
}
133+
const targetOrg = PreviewUtils.getTargetOrgFromArguments(this.argv);
171134

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

src/commands/lightning/dev/component.ts

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@
66
*/
77

88
import path from 'node:path';
9-
import url from 'node:url';
109
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
11-
import { Messages, SfProject } from '@salesforce/core';
12-
import { cmpDev } from '@lwrjs/api';
10+
import { Messages, SfProject, Logger } from '@salesforce/core';
11+
import { Platform } from '@salesforce/lwc-dev-mobile-core';
1312
import { ComponentUtils } from '../../../shared/componentUtils.js';
1413
import { PromptUtils } from '../../../shared/promptUtils.js';
14+
import { PreviewUtils } from '../../../shared/previewUtils.js';
15+
import { startLWCServer } from '../../../lwc-dev-server/index.js';
1516

1617
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
1718
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.dev.component');
19+
const sharedMessages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'shared.utils');
1820

1921
export default class LightningDevComponent extends SfCommand<void> {
2022
public static readonly summary = messages.getMessage('summary');
@@ -32,15 +34,37 @@ 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);
42+
const logger = await Logger.child(this.ctor.name);
4243
const project = await SfProject.resolve();
4344

45+
let sfdxProjectRootPath = '';
46+
try {
47+
sfdxProjectRootPath = await SfProject.resolveProjectPath();
48+
} catch (error) {
49+
return Promise.reject(
50+
new Error(sharedMessages.getMessage('error.no-project', [(error as Error)?.message ?? '']))
51+
);
52+
}
53+
54+
let componentName = flags['name'];
55+
const clientSelect = flags['client-select'];
56+
const targetOrg = flags['target-org'];
57+
58+
const { ldpServerId, ldpServerToken } = await PreviewUtils.initializePreviewConnection(targetOrg);
59+
60+
logger.debug('Determining the next available port for Local Dev Server');
61+
const serverPorts = await PreviewUtils.getNextAvailablePorts();
62+
logger.debug(`Next available ports are http=${serverPorts.httpPort} , https=${serverPorts.httpsPort}`);
63+
64+
logger.debug('Determining Local Dev Server url');
65+
const ldpServerUrl = PreviewUtils.generateWebSocketUrlForLocalDevServer(Platform.desktop, serverPorts, logger);
66+
logger.debug(`Local Dev Server url is ${ldpServerUrl}`);
67+
4468
const namespacePaths = await ComponentUtils.getNamespacePaths(project);
4569
const componentPaths = await ComponentUtils.getAllComponentPaths(namespacePaths);
4670
if (!componentPaths) {
@@ -63,48 +87,49 @@ export default class LightningDevComponent extends SfCommand<void> {
6387
return undefined;
6488
}
6589

66-
const componentName = path.basename(componentPath);
67-
const label = ComponentUtils.componentNameToTitleCase(componentName);
90+
const name = path.basename(componentPath);
91+
const label = ComponentUtils.componentNameToTitleCase(name);
6892

6993
return {
70-
name: componentName,
94+
name,
7195
label: xml.LightningComponentBundle.masterLabel ?? label,
7296
description: xml.LightningComponentBundle.description ?? '',
7397
};
7498
})
7599
)
76100
).filter((component) => !!component);
77101

78-
let name = flags.name;
79-
if (!flags['client-select']) {
80-
if (name) {
102+
if (!clientSelect) {
103+
if (componentName) {
81104
// validate that the component exists before launching the server
82-
const match = components.find((component) => name === component.name || name === component.label);
105+
const match = components.find(
106+
(component) => componentName === component.name || componentName === component.label
107+
);
83108
if (!match) {
84-
throw new Error(messages.getMessage('error.component-not-found', [name]));
109+
throw new Error(messages.getMessage('error.component-not-found', [componentName]));
85110
}
86111

87-
name = match.name;
112+
componentName = match.name;
88113
} else {
89114
// prompt the user for a name if one was not provided
90-
name = await PromptUtils.promptUserToSelectComponent(components);
91-
if (!name) {
115+
componentName = await PromptUtils.promptUserToSelectComponent(components);
116+
if (!componentName) {
92117
throw new Error(messages.getMessage('error.component'));
93118
}
94119
}
95120
}
96121

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-
open: process.env.OPEN_BROWSER === 'false' ? false : true,
108-
});
122+
await startLWCServer(logger, sfdxProjectRootPath, ldpServerToken, Platform.desktop, serverPorts);
123+
124+
const targetOrgArg = PreviewUtils.getTargetOrgFromArguments(this.argv);
125+
const launchArguments = PreviewUtils.generateComponentPreviewLaunchArguments(
126+
ldpServerUrl,
127+
ldpServerId,
128+
componentName,
129+
targetOrgArg
130+
);
131+
132+
// Open the browser and navigate to the right page
133+
await this.config.runCommand('org:open', launchArguments);
109134
}
110135
}

src/lwc-dev-server/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ async function createLWCServerConfig(
3030
const { namespace } = projectJson;
3131

3232
// e.g. lwc folders in force-app/main/default/lwc, package-dir/lwc
33-
const namespacePaths = (await Promise.all(packageDirs.map((dir) => glob(`${dir.fullPath}/**/lwc`, { absolute: true })))).flat();
33+
const namespacePaths = (
34+
await Promise.all(packageDirs.map((dir) => glob(`${dir.fullPath}/**/lwc`, { absolute: true })))
35+
).flat();
3436

3537
const ports = serverPorts ??
3638
(await ConfigUtils.getLocalDevServerPorts()) ?? {

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+
let appPath = `lwr/application/e/devpreview/ai/${encodeURIComponent(
233+
'localdev%2Fpreview'
234+
)}?ldpServerUrl=${ldpServerUrl}&ldpServerId=${ldpServerId}`;
235+
if (componentName) {
236+
// TODO: support other namespaces
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(sharedMessages.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(sharedMessages.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)