Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ EXAMPLES
$ sf lightning dev app --target-org myOrg --device-type ios --device-id "iPhone 15 Pro Max"
```

_See code: [src/commands/lightning/dev/app.ts](https://github.com/salesforcecli/plugin-lightning-dev/blob/4.5.1/src/commands/lightning/dev/app.ts)_
_See code: [src/commands/lightning/dev/app.ts](https://github.com/salesforcecli/plugin-lightning-dev/blob/4.5.2-alpha.0/src/commands/lightning/dev/app.ts)_

## `sf lightning dev component`

Expand Down Expand Up @@ -249,7 +249,7 @@ EXAMPLES
$ sf lightning dev component --name myComponent
```

_See code: [src/commands/lightning/dev/component.ts](https://github.com/salesforcecli/plugin-lightning-dev/blob/4.5.1/src/commands/lightning/dev/component.ts)_
_See code: [src/commands/lightning/dev/component.ts](https://github.com/salesforcecli/plugin-lightning-dev/blob/4.5.2-alpha.0/src/commands/lightning/dev/component.ts)_

## `sf lightning dev site`

Expand Down Expand Up @@ -305,6 +305,6 @@ EXAMPLES
$ sf lightning dev site --name "Partner Central" --target-org myOrg --get-latest
```

_See code: [src/commands/lightning/dev/site.ts](https://github.com/salesforcecli/plugin-lightning-dev/blob/4.5.1/src/commands/lightning/dev/site.ts)_
_See code: [src/commands/lightning/dev/site.ts](https://github.com/salesforcecli/plugin-lightning-dev/blob/4.5.2-alpha.0/src/commands/lightning/dev/site.ts)_

<!-- commandsstop -->
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@salesforce/plugin-lightning-dev",
"description": "Lightning development tools for LEX, Mobile, and Experience Sites",
"version": "4.5.1",
"version": "4.5.2-alpha.0",
"author": "Salesforce",
"bugs": "https://github.com/forcedotcom/cli/issues",
"dependencies": {
Expand Down
17 changes: 15 additions & 2 deletions src/commands/lightning/dev/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ComponentUtils } from '../../../shared/componentUtils.js';
import { PromptUtils } from '../../../shared/promptUtils.js';
import { PreviewUtils } from '../../../shared/previewUtils.js';
import { startLWCServer } from '../../../lwc-dev-server/index.js';
import { MetaUtils } from '../../../shared/metaUtils.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.dev.component');
Expand Down Expand Up @@ -65,6 +66,15 @@ export default class LightningDevComponent extends SfCommand<ComponentPreviewRes
const targetOrg = flags['target-org'];
const apiVersion = flags['api-version'];

// Auto enable local dev
if (process.env.AUTO_ENABLE_LOCAL_DEV === 'true') {
try {
await MetaUtils.ensureLightningPreviewEnabled(targetOrg.getConnection(undefined));
} catch (error) {
this.log('Error autoenabling local dev', error);
}
}

const { ldpServerId, ldpServerToken } = await PreviewUtils.initializePreviewConnection(targetOrg);

logger.debug('Determining the next available port for Local Dev Server');
Expand Down Expand Up @@ -154,8 +164,11 @@ export default class LightningDevComponent extends SfCommand<ComponentPreviewRes
// Construct and log the full URL that will be opened
const connection = targetOrg.getConnection(apiVersion);

// strip trailing slashes
const instanceUrl = connection.instanceUrl.replace(/\/$/, '');

const previewUrl = PreviewUtils.generateComponentPreviewUrl(
connection.instanceUrl,
instanceUrl,
ldpServerUrl,
ldpServerId,
componentName,
Expand All @@ -164,7 +177,7 @@ export default class LightningDevComponent extends SfCommand<ComponentPreviewRes

// Prepare the result for JSON output
const result: ComponentPreviewResult = {
instanceUrl: connection.instanceUrl,
instanceUrl,
ldpServerUrl,
ldpServerId,
componentName: componentName ?? '',
Expand Down
230 changes: 230 additions & 0 deletions src/shared/metaUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/*
* 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, Logger } from '@salesforce/core';

type LightningExperienceSettingsMetadata = {
[key: string]: unknown;
fullName?: string;
enableLightningPreviewPref?: string | boolean;
};

type MyDomainSettingsMetadata = {
[key: string]: unknown;
fullName?: string;
isFirstPartyCookieUseRequired?: string | boolean;
};

type MetadataUpdateResult = {
success: boolean;
fullName: string;
errors?: Array<{ message: string }>;
};

/**
* Utility class for managing Salesforce metadata settings related to Lightning Development.
*/
export class MetaUtils {
private static logger = Logger.childFromRoot('metaUtils');

/**
* Retrieves the Lightning Experience Settings metadata from the org.
*
* @param connection the connection to the org
* @returns LightningExperienceSettingsMetadata object containing the settings
* @throws Error if unable to retrieve the metadata
*/
public static async getLightningExperienceSettings(
connection: Connection
): Promise<LightningExperienceSettingsMetadata> {
this.logger.debug('Retrieving Lightning Experience Settings metadata');

const metadata = await connection.metadata.read('LightningExperienceSettings', 'enableLightningPreviewPref');

if (!metadata) {
throw new Error('Unable to retrieve Lightning Experience Settings metadata.');
}

if (Array.isArray(metadata)) {
if (metadata.length === 0) {
throw new Error('Lightning Experience Settings metadata response was empty.');
}
return metadata[0] as LightningExperienceSettingsMetadata;
}

return metadata as LightningExperienceSettingsMetadata;
}

/**
* Checks if Lightning Preview (Local Dev) is enabled for the org.
*
* @param connection the connection to the org
* @returns boolean indicating whether Lightning Preview is enabled
*/
public static async isLightningPreviewEnabled(connection: Connection): Promise<boolean> {
try {
const settings = await this.getLightningExperienceSettings(connection);
const flagValue = settings.enableLightningPreviewPref ?? 'false';
const enabled = String(flagValue).toLowerCase().trim() === 'true';
this.logger.debug(`Lightning Preview enabled: ${enabled}`);
return enabled;
} catch (error) {
this.logger.warn('Error checking Lightning Preview status, assuming disabled:', error);
return false;
}
}

/**
* Enables or disables Lightning Preview (Local Dev) for the org by updating the metadata.
*
* @param connection the connection to the org
* @param enable boolean indicating whether to enable (true) or disable (false) Lightning Preview
* @throws Error if the metadata update fails
*/
public static async setLightningPreviewEnabled(connection: Connection, enable: boolean): Promise<void> {
this.logger.debug(`Setting Lightning Preview enabled to: ${enable}`);

const updateResult = await connection.metadata.update('LightningExperienceSettings', {
fullName: 'enableLightningPreviewPref',
enableLightningPreviewPref: enable ? 'true' : 'false',
});

const results = Array.isArray(updateResult) ? updateResult : [updateResult];
const typedResults = results as MetadataUpdateResult[];
const errors = typedResults.filter((result) => !result.success);

if (errors.length > 0) {
const message = errors
.flatMap((result) => (Array.isArray(result.errors) ? result.errors : result.errors ? [result.errors] : []))
.filter((error): error is { message: string } => Boolean(error))
.map((error) => error.message)
.join(' ');

throw new Error(message || 'Failed to update Lightning Preview setting.');
}

this.logger.debug('Successfully updated Lightning Preview setting');
}

/**
* Retrieves the My Domain Settings metadata from the org.
*
* @param connection the connection to the org
* @returns MyDomainSettingsMetadata object containing the settings
* @throws Error if unable to retrieve the metadata
*/
public static async getMyDomainSettings(connection: Connection): Promise<MyDomainSettingsMetadata> {
this.logger.debug('Retrieving My Domain Settings metadata');

const metadata = await connection.metadata.read('MyDomainSettings', 'MyDomain');

if (!metadata) {
throw new Error('Unable to retrieve My Domain settings metadata.');
}

if (Array.isArray(metadata)) {
if (metadata.length === 0) {
throw new Error('My Domain settings metadata response was empty.');
}
return metadata[0] as MyDomainSettingsMetadata;
}

return metadata as MyDomainSettingsMetadata;
}

/**
* Checks if first-party cookies are required for the org.
*
* @param connection the connection to the org
* @returns boolean indicating whether first-party cookies are required
*/
public static async isFirstPartyCookieRequired(connection: Connection): Promise<boolean> {
try {
const settings = await this.getMyDomainSettings(connection);
const flagValue = settings.isFirstPartyCookieUseRequired ?? 'false';
const required = String(flagValue).toLowerCase().trim() === 'true';
this.logger.debug(`First-party cookie required: ${required}`);
return required;
} catch (error) {
this.logger.warn('Error checking first-party cookie requirement, assuming not required:', error);
return false;
}
}

/**
* Updates the My Domain setting that controls whether first-party cookies are required.
*
* @param connection the connection to the org
* @param requireFirstPartyCookies boolean indicating whether to require first-party cookies
* @throws Error if the metadata update fails
*/
public static async setMyDomainFirstPartyCookieRequirement(
connection: Connection,
requireFirstPartyCookies: boolean
): Promise<void> {
this.logger.debug(`Setting first-party cookie requirement to: ${requireFirstPartyCookies}`);

const updateResult = await connection.metadata.update('MyDomainSettings', {
fullName: 'MyDomain',
isFirstPartyCookieUseRequired: requireFirstPartyCookies ? 'true' : 'false',
});

const results = Array.isArray(updateResult) ? updateResult : [updateResult];
const typedResults = results as MetadataUpdateResult[];
const errors = typedResults.filter((result) => !result.success);

if (errors.length > 0) {
const message = errors
.flatMap((result) => (Array.isArray(result.errors) ? result.errors : result.errors ? [result.errors] : []))
.filter((error): error is { message: string } => Boolean(error))
.map((error) => error.message)
.join(' ');

throw new Error(message || 'Failed to update My Domain first-party cookie requirement.');
}

this.logger.debug('Successfully updated first-party cookie requirement');
}

/**
* Ensures Lightning Preview is enabled for the org. If it's not enabled, this method will enable it.
*
* @param connection the connection to the org
* @returns boolean indicating whether Lightning Preview was already enabled (true) or had to be enabled (false)
*/
public static async ensureLightningPreviewEnabled(connection: Connection): Promise<boolean> {
const isEnabled = await this.isLightningPreviewEnabled(connection);

if (!isEnabled) {
this.logger.info('Lightning Preview is not enabled. Enabling it now...');
await this.setLightningPreviewEnabled(connection, true);
return false;
}

this.logger.debug('Lightning Preview is already enabled');
return true;
}

/**
* Ensures first-party cookies are not required for the org. If they are required, this method will disable the requirement.
*
* @param connection the connection to the org
* @returns boolean indicating whether first-party cookies were already not required (true) or had to be disabled (false)
*/
public static async ensureFirstPartyCookiesNotRequired(connection: Connection): Promise<boolean> {
const isRequired = await this.isFirstPartyCookieRequired(connection);

if (isRequired) {
this.logger.info('First-party cookies are required. Disabling requirement...');
await this.setMyDomainFirstPartyCookieRequirement(connection, false);
return false;
}

this.logger.debug('First-party cookies are not required');
return true;
}
}