diff --git a/messages/lightningOut.md b/messages/lightningOut.md index a0e64938..20083094 100644 --- a/messages/lightningOut.md +++ b/messages/lightningOut.md @@ -38,7 +38,11 @@ Developer name of the External Client Application associated with the app. # flags.runtime.summary -Runtime the app targets. LWR_CORE serves from your Salesforce org for authenticated users; CLWR serves from an Experience Cloud site (guest access, extra site-deployment step). +Runtime the app targets: LWR_CORE or CLWR. + +# flags.runtime.description + +LWR_CORE serves the app from your Salesforce org to authenticated users. CLWR serves it from an Experience Cloud site to guest and authenticated users. After you deploy a CLWR app, link it to your site in Setup. # flags.host-domains.summary @@ -80,17 +84,21 @@ Ignoring unrecognized key "%s" in --definition-file. Your local project's sourceApiVersion (%s) is below 68.0, the minimum API version this scaffold supports for deployment. Set sourceApiVersion to 68.0 or later in sfdx-project.json, or pass --api-version 68.0 or later when you deploy. +# warning.org-settings + +The generated MyDomain and Security settings apply to the whole org when you deploy them: isFirstPartyCookieUseRequired is set to false and enableOauthCorsPolicy is set to true. Review both settings before deploying to a production org. + # success.next-step Scaffold generated in %s. Deploy it with: sf project deploy start --source-dir %s%s # success.app-id -After deploying, note the LightningOutApp's App ID from Setup — your host page needs it to embed the app. +After deploying, copy the App ID from Lightning Out 2.0 App Manager in Setup, and set it as the "app-id" attribute on the tag on your host page. For LWR_CORE apps, the "app-id" attribute is what adds your host domains to the CSP frame-ancestors directive. # success.dont-delete -Don't delete the generated ExternalClientApplication after deploying — Lightning Out uses it for OAuth at runtime. +Don't delete the generated ExternalClientApplication after deploying. Lightning Out 2.0 uses it for OAuth at runtime. # success.eca-overwrite @@ -98,7 +106,11 @@ Re-running this command overwrites the generated files for the "%s" External Cli # success.components-exist -Verify that every component you referenced already exists and is exposed for Lightning Out before deploying. +Verify that every component you referenced already exists and is exposed for Lightning Out 2.0 before deploying. + +# success.clwr-site + +For CLWR, link the app to your Experience Cloud site after deploying. From Setup, in the Quick Find box, enter Lightning Out, and then select Lightning Out 2.0 App Manager. Edit the app, select your site on the "Linked Communities Site" tab, and save. Then follow the steps in the "Complete Deployment in Experience Builder" dialog, which include publishing the site. # success.frontdoor diff --git a/src/commands/template/generate/lightning-out/index.ts b/src/commands/template/generate/lightning-out/index.ts index 096010bd..2c09b437 100644 --- a/src/commands/template/generate/lightning-out/index.ts +++ b/src/commands/template/generate/lightning-out/index.ts @@ -149,8 +149,6 @@ export default class LightningOut extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); - public static readonly state = 'beta'; - public static readonly hidden = true; public static readonly flags = { 'app-name': Flags.string({ summary: messages.getMessage('flags.app-name.summary'), required: false }), @@ -158,6 +156,7 @@ export default class LightningOut extends SfCommand { runtime: Flags.option({ options: ['LWR_CORE', 'CLWR'] as const, summary: messages.getMessage('flags.runtime.summary'), + description: messages.getMessage('flags.runtime.description'), })(), 'host-domains': Flags.string({ summary: messages.getMessage('flags.host-domains.summary'), multiple: true }), components: Flags.string({ summary: messages.getMessage('flags.components.summary'), multiple: true }), @@ -190,17 +189,23 @@ export default class LightningOut extends SfCommand { this.warn(messages.getMessage('warning.source-api-version', [String(projApi)])); } - // Success guidance (suppressed automatically under --json). + // The org-wide settings notice is a warning, so it also appears in the --json `warnings` array. + this.warn(messages.getMessage('warning.org-settings')); + + // Success guidance (log/info are suppressed automatically under --json). // Only pin --api-version to the 68.0 deploy floor when the project is below it; otherwise omit // the optional flag so the user's own (>= floor) project default applies rather than being downgraded. const outputDir = opts.outputdir ?? '.'; const apiVersionSuffix = belowFloor ? ' --api-version 68.0' : ''; this.log(messages.getMessage('success.next-step', [outputDir, shellQuoteArg(outputDir), apiVersionSuffix])); + if (opts.runtime === 'CLWR') { + this.info(messages.getMessage('success.clwr-site')); + } this.info(messages.getMessage('success.app-id')); this.info(messages.getMessage('success.dont-delete')); this.info(messages.getMessage('success.eca-overwrite', [opts.eca.name ?? ''])); this.info(messages.getMessage('success.components-exist')); - // frontdoor.jsp handoff is a CLWR-only concern; don't show it for LWR_CORE runs. + // The frontdoor.jsp handoff is a CLWR-only concern; don't show it for LWR_CORE runs. if (opts.runtime === 'CLWR') { this.info(messages.getMessage('success.frontdoor')); } diff --git a/test/commands/template/generate/lightning-out/index.test.ts b/test/commands/template/generate/lightning-out/index.test.ts index 8e2d92e2..b7388659 100644 --- a/test/commands/template/generate/lightning-out/index.test.ts +++ b/test/commands/template/generate/lightning-out/index.test.ts @@ -20,7 +20,8 @@ import * as path from 'node:path'; import { Messages } from '@salesforce/core'; import { TestContext } from '@salesforce/core/testSetup'; import { expect } from 'chai'; -import { stubSfCommandUx } from '@salesforce/sf-plugins-core'; +import { stubSfCommandUx, stubUx } from '@salesforce/sf-plugins-core'; +import { TemplateService } from '@salesforce/templates'; import LightningOut, { mergeLightningOutInputs, readDefinition, @@ -35,16 +36,17 @@ const messages = Messages.loadMessages('@salesforce/plugin-templates', 'lightnin describe('template generate lightning-out (unit)', () => { const $$ = new TestContext(); + let cmdStubs: ReturnType; beforeEach(() => { - stubSfCommandUx($$.SANDBOX); + cmdStubs = stubSfCommandUx($$.SANDBOX); }); afterEach(() => { $$.restore(); }); - it('is a beta, hidden command', () => { - expect(LightningOut.state).to.equal('beta'); - expect(LightningOut.hidden).to.equal(true); + it('is a GA command (neither beta nor hidden)', () => { + expect(LightningOut.state).to.equal(undefined); + expect(LightningOut.hidden).to.not.equal(true); }); it('renders the output dir into success.next-step and includes --api-version only when the suffix is supplied', () => { @@ -61,6 +63,70 @@ describe('template generate lightning-out (unit)', () => { expect(atOrAboveFloor).to.not.include('--api-version'); }); + describe('post-run output', () => { + const base = [ + '--app-name', + 'MyLoApp', + '--eca-name', + 'MyLoAppEca', + '--eca-contact-email', + 'dev@example.com', + '--eca-callback-url', + 'https://example.com/cb', + '--host-domains', + 'https://example.com', + ]; + + beforeEach(() => { + stubUx($$.SANDBOX); + $$.SANDBOX.stub(TemplateService.prototype, 'create').resolves({ + outputDir: '.', + created: [], + rawOutput: '', + }); + }); + + it('shows the site-link and frontdoor guidance for CLWR', async () => { + await LightningOut.run([...base, '--runtime', 'CLWR']); + const info = cmdStubs.info.args.flat(); + expect(info).to.include(messages.getMessage('success.clwr-site')); + expect(info).to.include(messages.getMessage('success.frontdoor')); + expect(messages.getMessage('success.clwr-site')).to.include('Linked Communities Site'); + }); + + it('omits the CLWR guidance for LWR_CORE', async () => { + await LightningOut.run([...base, '--runtime', 'LWR_CORE']); + const info = cmdStubs.info.args.flat(); + expect(info).to.not.include(messages.getMessage('success.clwr-site')); + expect(info).to.not.include(messages.getMessage('success.frontdoor')); + expect(info).to.include(messages.getMessage('success.app-id')); + }); + + it('warns that the generated settings are org-wide for every runtime', async () => { + for (const runtime of ['LWR_CORE', 'CLWR']) { + cmdStubs.warn.resetHistory(); + // eslint-disable-next-line no-await-in-loop + await LightningOut.run([...base, '--runtime', runtime]); + const warned = cmdStubs.warn.args.flat(); + expect(warned).to.include(messages.getMessage('warning.org-settings')); + } + expect(messages.getMessage('warning.org-settings')) + .to.include('isFirstPartyCookieUseRequired') + .and.to.include('enableOauthCorsPolicy'); + }); + + it('points to the App ID location and the app-id attribute', () => { + expect(messages.getMessage('success.app-id')) + .to.include('Lightning Out 2.0 App Manager') + .and.to.include('"app-id"'); + }); + }); + + it('keeps the runtime flag summary short and puts the detail in its description', () => { + expect(LightningOut.flags.runtime.summary).to.not.include('Setup'); + expect(LightningOut.flags.runtime.description).to.include('link it to your site in Setup'); + }); + describe('mergeLightningOutInputs', () => { it('takes appName/eca.name from flags over the definition file', () => { const { opts } = mergeLightningOutInputs(