Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"command": "lightning:dev:site",
"flagAliases": [],
"flagChars": ["l", "n", "o"],
"flags": ["flags-dir", "get-latest", "guest", "name", "target-org"],
"flags": ["flags-dir", "get-latest", "guest", "name", "target-org", "ssr"],
"plugin": "@salesforce/plugin-lightning-dev"
}
]
16 changes: 0 additions & 16 deletions messages/lightning.dev.app.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,6 @@ Type of device to display the app preview.

ID of the mobile device to display the preview if device type is set to `ios` or `android`. The default value is the ID of the first available mobile device.

# error.username

Org must have a valid user

# error.identitydata

Couldn't find identity data while generating preview arguments

# error.identitydata.entityid

Couldn't find entity ID while generating preview arguments

# 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
4 changes: 4 additions & 0 deletions messages/lightning.dev.site.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ Download the latest version of the specified site from your org, instead of usin

Preview the site as a guest user (rather than an authenticated user).

# flags.ssr.summary

Preview the SSR bundle

# examples

- Select a site to preview from the org "myOrg":
Expand Down
16 changes: 16 additions & 0 deletions messages/shared.utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,19 @@ Your org is on API version %s, but this version of the CLI plugin supports API v
# error.org.api-mismatch.remediation

To use the plugin with this org, you can reinstall or update the plugin using the "%s" tag. For example: "sf plugins install %s".

# error.username

Org must have a valid user

# error.identitydata

Couldn't find identity data while generating preview arguments

# error.identitydata.entityid

Couldn't find entity ID while generating preview arguments

# error.no-project

This command is required to run from within a Salesforce project directory. %s
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"lightning-base-components": "1.27.2-alpha",
"lwc": "~8.20.1",
"node-fetch": "^3.3.2",
"open": "^10.1.0",
"xml2js": "^0.6.2"
},
"devDependencies": {
Expand Down
8 changes: 4 additions & 4 deletions src/commands/lightning/dev/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,18 @@ export default class LightningDevApp extends SfCommand<void> {
try {
sfdxProjectRootPath = await SfProject.resolveProjectPath();
} catch (error) {
return Promise.reject(new Error(messages.getMessage('error.no-project', [(error as Error)?.message ?? ''])));
throw new Error(sharedMessages.getMessage('error.no-project', [(error as Error)?.message ?? '']));
}

const connection = targetOrg.getConnection(undefined);
const username = connection.getUsername();
if (!username) {
return Promise.reject(new Error(messages.getMessage('error.username')));
throw new Error(sharedMessages.getMessage('error.username'));
}

const localDevEnabled = await OrgUtils.isLocalDevEnabled(connection);
if (!localDevEnabled) {
return Promise.reject(new Error(sharedMessages.getMessage('error.localdev.not.enabled')));
throw new Error(sharedMessages.getMessage('error.localdev.not.enabled'));
}

OrgUtils.ensureMatchingAPIVersion(connection);
Expand All @@ -97,7 +97,7 @@ export default class LightningDevApp extends SfCommand<void> {
const ldpServerToken = appServerIdentity.identityToken;
const ldpServerId = appServerIdentity.usernameToServerEntityIdMap[username];
if (!ldpServerId) {
return Promise.reject(new Error(messages.getMessage('error.identitydata.entityid')));
throw new Error(sharedMessages.getMessage('error.identitydata.entityid'));
}

const appId = await PreviewUtils.getLightningExperienceAppId(connection, appName, logger);
Expand Down
51 changes: 50 additions & 1 deletion src/commands/lightning/dev/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
*/
import fs from 'node:fs';
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import { Logger, Messages, SfProject } from '@salesforce/core';
import { Platform } from '@salesforce/lwc-dev-mobile-core';
import { expDev, SitesLocalDevOptions, setupDev } from '@lwrjs/api';
import open from 'open';
import { OrgUtils } from '../../../shared/orgUtils.js';
import { PromptUtils } from '../../../shared/promptUtils.js';
import { ExperienceSite } from '../../../shared/experience/expSite.js';
import { PreviewUtils } from '../../../shared/previewUtils.js';
import { startLWCServer } from '../../../lwc-dev-server/index.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.dev.site');
Expand All @@ -36,6 +40,10 @@ export default class LightningDevSite extends SfCommand<void> {
summary: messages.getMessage('flags.guest.summary'),
default: false,
}),
ssr: Flags.boolean({
summary: messages.getMessage('flags.ssr.summary'),
default: false,
}),
};

public async run(): Promise<void> {
Expand All @@ -45,6 +53,7 @@ export default class LightningDevSite extends SfCommand<void> {
const org = flags['target-org'];
const getLatest = flags['get-latest'];
const guest = flags.guest;
const ssr = flags.ssr;
let siteName = flags.name;

const connection = org.getConnection(undefined);
Expand All @@ -63,6 +72,46 @@ export default class LightningDevSite extends SfCommand<void> {
}

const selectedSite = new ExperienceSite(org, siteName);

if (!ssr) {
let sfdxProjectRootPath = '';
try {
sfdxProjectRootPath = await SfProject.resolveProjectPath();
} catch (error) {
throw new Error(sharedMessages.getMessage('error.no-project', [(error as Error)?.message ?? '']));
}
const previewUrl = await selectedSite.getPreviewUrl();
const username = connection.getUsername();
if (!username) {
throw new Error(sharedMessages.getMessage('error.username'));
}

this.log('Configuring local web server identity');
const appServerIdentity = await PreviewUtils.getOrCreateAppServerIdentity(connection);
const ldpServerToken = appServerIdentity.identityToken;
const ldpServerId = appServerIdentity.usernameToServerEntityIdMap[username];
if (!ldpServerId) {
throw new Error(sharedMessages.getMessage('error.identitydata.entityid'));
}

this.log('Determining the next available port for Local Dev Server');
const serverPorts = await PreviewUtils.getNextAvailablePorts();
this.log(`Next available ports are http=${serverPorts.httpPort} , https=${serverPorts.httpsPort}`);

this.log('Determining Local Dev Server url');
const ldpServerUrl = PreviewUtils.generateWebSocketUrlForLocalDevServer(Platform.desktop, serverPorts);
this.log(`Local Dev Server url is ${ldpServerUrl}`);

const logger = await Logger.child(this.ctor.name);
await startLWCServer(logger, sfdxProjectRootPath, ldpServerToken, Platform.desktop, serverPorts);
const url = new URL(previewUrl);
url.searchParams.set('aura.lwcDevServerUrl', ldpServerUrl);
url.searchParams.set('aura.lwcDevServerId', ldpServerId);
url.searchParams.set('lwc.mode', 'dev');
await open(url.toString());
return;
}

@nrkruk nrkruk Jul 10, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: could we separate this out in into two different function calls? Run method then just calls previewHMR() / previewSSR() based on the 'ssr' cli arg being there?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done


let siteZip: string | undefined;

// If the site is not setup / is not based on the current release / or get-latest is requested ->
Expand Down
41 changes: 41 additions & 0 deletions src/shared/experience/expSite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,47 @@ export class ExperienceSite {
return retVal;
}

public async getPreviewUrl(): Promise<string> {
// Get the community ID
const communityId = await this.getNetworkId();
const conn = this.org.getConnection();
const accessToken = conn.accessToken;
const instanceUrl = conn.instanceUrl;

if (!accessToken) {
throw new SfError(`Invalid access token, unable to get preview URL for: ${this.siteDisplayName}`);
}

try {
// Call the communities API to get the preview URL
const apiUrl = `${instanceUrl}/services/data/v64.0/connect/communities/${communityId}/preview-url/pages/Home`;
const response = await axios.get<{ previewUrl: string }>(apiUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});

if (response.data?.previewUrl) {
return response.data.previewUrl;
} else {
throw new SfError(`Invalid response from communities API for site: ${this.siteDisplayName}`);
}
} catch (error) {
// Handle axios errors
if (axios.isAxiosError(error)) {
if (error.response) {
// Server responded with non-200 status
throw new SfError(
`Failed to get preview URL: Server responded with status ${error.response.status} - ${error.response.statusText}`
);
} else if (error.request) {
// Request was made but no response received
throw new SfError('Failed to get preview URL: No response received from server');
}
}
throw new SfError(`Failed to get preview URL for site: ${this.siteDisplayName}`);
}
}
/**
* Generate a site bundle on demand and download it
*
Expand Down
4 changes: 2 additions & 2 deletions src/shared/previewUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ export class PreviewUtils {
const userConfiguredPorts = await ConfigUtils.getLocalDevServerPorts();

if (userConfiguredPorts) {
return Promise.resolve(userConfiguredPorts);
return userConfiguredPorts;
}

const httpPort = await this.doGetNextAvailablePort(LOCAL_DEV_SERVER_DEFAULT_HTTP_PORT);
const httpsPort = await this.doGetNextAvailablePort(httpPort + 1);

return Promise.resolve({ httpPort, httpsPort });
return { httpPort, httpsPort };
}

/**
Expand Down
2 changes: 1 addition & 1 deletion test/commands/lightning/dev/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ describe('lightning dev app', () => {
$$.SANDBOX.stub(Connection.prototype, 'getUsername').returns(undefined);
await MockedLightningPreviewApp.run(['--name', 'blah', '-o', testOrgData.username, '-t', Platform.desktop]);
} catch (err) {
expect(err).to.be.an('error').with.property('message', messages.getMessage('error.username'));
expect(err).to.be.an('error').with.property('message', sharedMessages.getMessage('error.username'));
}
});

Expand Down
59 changes: 59 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5266,6 +5266,13 @@ builtin-modules@^3.3.0:
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==

bundle-name@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889"
integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==
dependencies:
run-applescript "^7.0.0"

bytes@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
Expand Down Expand Up @@ -5933,6 +5940,19 @@ deepmerge@^4.2.2:
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==

default-browser-id@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.0.tgz#a1d98bf960c15082d8a3fa69e83150ccccc3af26"
integrity sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==

default-browser@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.2.1.tgz#7b7ba61204ff3e425b556869ae6d3e9d9f1712cf"
integrity sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==
dependencies:
bundle-name "^4.1.0"
default-browser-id "^5.0.0"

default-require-extensions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.1.tgz#bfae00feeaeada68c2ae256c62540f60b80625bd"
Expand All @@ -5954,6 +5974,11 @@ define-data-property@^1.0.1, define-data-property@^1.1.4:
es-errors "^1.3.0"
gopd "^1.0.1"

define-lazy-prop@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f"
integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==

define-properties@^1.2.0, define-properties@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c"
Expand Down Expand Up @@ -8013,6 +8038,11 @@ is-docker@^2.0.0:
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==

is-docker@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200"
integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==

is-extendable@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
Expand Down Expand Up @@ -8074,6 +8104,13 @@ is-in-ci@^0.1.0:
resolved "https://registry.yarnpkg.com/is-in-ci/-/is-in-ci-0.1.0.tgz#5e07d6a02ec3a8292d3f590973357efa3fceb0d3"
integrity sha512-d9PXLEY0v1iJ64xLiQMJ51J128EYHAaOR4yZqQi8aHGfw6KgifM3/Viw1oZZ1GCVmb3gBuyhLyHj0HgR2DhSXQ==

is-inside-container@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4"
integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==
dependencies:
is-docker "^3.0.0"

is-map@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e"
Expand Down Expand Up @@ -8232,6 +8269,13 @@ is-wsl@^2.2.0:
dependencies:
is-docker "^2.0.0"

is-wsl@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2"
integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==
dependencies:
is-inside-container "^1.0.0"

isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
Expand Down Expand Up @@ -9666,6 +9710,16 @@ only@~0.0.2:
resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==

open@^10.1.0:
version "10.1.2"
resolved "https://registry.yarnpkg.com/open/-/open-10.1.2.tgz#d5df40984755c9a9c3c93df8156a12467e882925"
integrity sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==
dependencies:
default-browser "^5.2.1"
define-lazy-prop "^3.0.0"
is-inside-container "^1.0.0"
is-wsl "^3.1.0"

optionator@^0.9.3:
version "0.9.4"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734"
Expand Down Expand Up @@ -10541,6 +10595,11 @@ rollup@^2.79.2:
optionalDependencies:
fsevents "~2.3.2"

run-applescript@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.0.0.tgz#e5a553c2bffd620e169d276c1cd8f1b64778fbeb"
integrity sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==

run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
Expand Down
Loading