Skip to content
Closed
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
20 changes: 16 additions & 4 deletions messages/lightningOut.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -80,25 +84,33 @@ 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 <lightning-out-application> 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

Re-running this command overwrites the generated files for the "%s" External Client Application; back up local edits first.

# 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

Expand Down
13 changes: 9 additions & 4 deletions src/commands/template/generate/lightning-out/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,15 +149,14 @@ export default class LightningOut extends SfCommand<CreateOutput> {
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 }),
'eca-name': Flags.string({ summary: messages.getMessage('flags.eca-name.summary'), required: false }),
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 }),
Expand Down Expand Up @@ -190,17 +189,23 @@ export default class LightningOut extends SfCommand<CreateOutput> {
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'));
}
Expand Down
76 changes: 71 additions & 5 deletions test/commands/template/generate/lightning-out/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -35,16 +36,17 @@ const messages = Messages.loadMessages('@salesforce/plugin-templates', 'lightnin

describe('template generate lightning-out (unit)', () => {
const $$ = new TestContext();
let cmdStubs: ReturnType<typeof stubSfCommandUx>;
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', () => {
Expand All @@ -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(
Expand Down