diff --git a/README.md b/README.md index 4627ff86..2e832c19 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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` @@ -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)_ diff --git a/package.json b/package.json index fa113e54..eb70cfda 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/commands/lightning/dev/component.ts b/src/commands/lightning/dev/component.ts index 0b40dd36..1aca172e 100644 --- a/src/commands/lightning/dev/component.ts +++ b/src/commands/lightning/dev/component.ts @@ -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'); @@ -65,6 +66,15 @@ export default class LightningDevComponent extends SfCommand; +}; + +/** + * 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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; + } +}