Skip to content
Open
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
19 changes: 19 additions & 0 deletions command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,25 @@
"flags": ["api-version", "flags-dir", "internal", "json", "loglevel", "name", "output-dir", "template", "type"],
"plugin": "@salesforce/plugin-templates"
},
{
"alias": [],
"command": "template:generate:lightning:embedding",
"flagAliases": ["apiversion", "outputdir"],
"flagChars": ["d", "i", "n", "s"],
"flags": [
"api-version",
"flags-dir",
"internal",
"json",
"loglevel",
"name",
"output-dir",
"sandbox",
"shell-title",
"src"
],
"plugin": "@salesforce/plugin-templates"
},
{
"alias": ["force:lightning:event:create", "lightning:generate:event"],
"command": "template:generate:lightning:event",
Expand Down
55 changes: 55 additions & 0 deletions messages/lightningEmbedding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# examples

- Generate an embedding wrapper LWC in the current directory:

<%= config.bin %> <%= command.id %> --name MyEmbeddingWrapper --src https://app.example.com --sandbox allow-forms --shell-title "Expense Report Widget"

- Generate an embedding wrapper LWC in the "force-app/main/default/lwc" directory with multiple sandbox tokens:

<%= config.bin %> <%= command.id %> --name MyEmbeddingWrapper --src https://app.example.com --sandbox allow-forms --sandbox allow-scripts --shell-title "Expense Report Widget" --output-dir force-app/main/default/lwc

# summary

Generate a Lightning Web Component (LWC) bundle that wraps the lightning-embedding base component.

# description

The generated LWC bundle consumes the first-party <lightning-embedding> component, which is pre-wired with the three required attributes: the widget URL (src), iframe sandbox tokens, and an accessible iframe title (shell-title).

The generated LWC bundle contains four files (.html, .js, .js-meta.xml, .css) in a directory named with the camelCased component name. The bundle must live under a parent folder named "lwc".

# flags.name.summary

Name of the generated component; must be in PascalCase format.

# flags.name.description

The component name is also used (camelCased) as the LWC folder name and file stem. Must contain only alphanumeric characters and start with a letter.

# flags.src.summary

Absolute HTTPS URL that the iframe will load.

# flags.src.description

The URL is bound to the <lightning-embedding> "src" attribute as a reactive property in the generated LWC. Must use HTTPS; plain HTTP is allowed only for localhost or 127.0.0.1 (for local development).

# flags.src.error

The --src flag must be an absolute HTTPS URL, such as https://app.example.com. Plain HTTP is allowed only for localhost or 127.0.0.1.

# flags.sandbox.summary

Iframe sandbox token. Specify this flag multiple times to set more than one token.

# flags.sandbox.description

Each token is written into the space-separated "sandbox" attribute on <lightning-embedding>. Only W3C-defined sandbox tokens are accepted.

# flags.shell-title.summary

Accessible title for the embedded iframe.

# flags.shell-title.description

Written to the "shell-title" attribute on <lightning-embedding> and used as the iframe's accessible name (announced by screen readers).
87 changes: 87 additions & 0 deletions src/commands/template/generate/lightning/embedding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright (c) 2026, 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 { Flags, loglevel, orgApiVersionFlagWithDeprecations, SfCommand, Ux } from '@salesforce/sf-plugins-core';
import {
CreateOutput,
isAllowedLightningEmbeddingSrcUrl,
LIGHTNING_EMBEDDING_SANDBOX_TOKENS,
LightningEmbeddingOptions,
TemplateType,
} from '@salesforce/templates';
import { Messages } from '@salesforce/core';
import { getCustomTemplates, runGenerator } from '../../../../utils/templateCommand.js';
import { internalFlag, outputDirFlagLightning } from '../../../../utils/flags.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-templates', 'lightningEmbedding');

export default class LightningEmbedding 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 = {
name: Flags.string({
char: 'n',
summary: messages.getMessage('flags.name.summary'),
description: messages.getMessage('flags.name.description'),
required: true,
}),
src: Flags.string({
Comment thread
iowillhoit marked this conversation as resolved.
char: 's',
summary: messages.getMessage('flags.src.summary'),
description: messages.getMessage('flags.src.description'),
required: true,
parse: (input: string) => {
if (!isAllowedLightningEmbeddingSrcUrl(input)) {
throw new Error(messages.getMessage('flags.src.error'));
}
return Promise.resolve(input);
},
}),
sandbox: Flags.option({
summary: messages.getMessage('flags.sandbox.summary'),
description: messages.getMessage('flags.sandbox.description'),
options: LIGHTNING_EMBEDDING_SANDBOX_TOKENS,
multiple: true,
required: true,
})(),
'shell-title': Flags.string({
summary: messages.getMessage('flags.shell-title.summary'),
description: messages.getMessage('flags.shell-title.description'),
required: true,
}),
'output-dir': outputDirFlagLightning,
'api-version': orgApiVersionFlagWithDeprecations,
internal: internalFlag,
loglevel,
};

public async run(): Promise<CreateOutput> {
const { flags } = await this.parse(LightningEmbedding);

const flagsAsOptions: LightningEmbeddingOptions = {
componentname: flags.name,
src: flags.src,
sandbox: flags.sandbox.join(' '),
shellTitle: flags['shell-title'],
outputdir: flags['output-dir'],
apiversion: flags['api-version'],
internal: flags.internal,
};

return runGenerator({
templateType: TemplateType.LightningEmbedding,
opts: flagsAsOptions,
ux: new Ux({ jsonEnabled: this.jsonEnabled() }),
templates: getCustomTemplates(this.configAggregator),
});
}
}
152 changes: 152 additions & 0 deletions test/commands/template/generate/lightning/embedding.nut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* Copyright (c) 2026, 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 path from 'node:path';
import { expect, config } from 'chai';
import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit';
import assert from 'yeoman-assert';

config.truncateThreshold = 0;

describe('template generate lightning embedding:', () => {
let session: TestSession;
before(async () => {
session = await TestSession.create({
project: {},
devhubAuthStrategy: 'NONE',
});
});
after(async () => {
await session?.clean();
});

const lwcDir = (): string => path.join(session.project.dir, 'lwc');
const bundleFiles = (componentName: string): string[] => {
const camel = componentName.charAt(0).toLowerCase() + componentName.slice(1);
return ['.html', '.js', '.css', '.js-meta.xml'].map((suffix) => path.join(lwcDir(), camel, camel + suffix));
};

describe('Check lightning embedding creation', () => {
const name = 'MyEmbedding';
const src = 'https://app.example.com';
const shellTitle = 'Demo Embedding';

it('should scaffold an embedding LWC bundle with all four files', () => {
execCmd(
`template generate lightning embedding --name ${name} --src ${src} --sandbox allow-forms --shell-title "${shellTitle}" --output-dir ${lwcDir()}`,
{ ensureExitCode: 0 }
);
assert.file(bundleFiles(name));
});

it('should emit a <lightning-embedding> element in the generated html', () => {
execCmd(
`template generate lightning embedding --name ${name} --src ${src} --sandbox allow-forms --shell-title "${shellTitle}" --output-dir ${lwcDir()}`,
{ ensureExitCode: 0 }
);
const camel = name.charAt(0).toLowerCase() + name.slice(1);
assert.fileContent(path.join(lwcDir(), camel, `${camel}.html`), '<lightning-embedding');
});

it('should join multiple --sandbox tokens into a single space-separated attribute', () => {
execCmd(
`template generate lightning embedding --name MultiSandbox --src ${src} --sandbox allow-forms --sandbox allow-scripts --shell-title "${shellTitle}" --output-dir ${lwcDir()}`,
{ ensureExitCode: 0 }
);
assert.fileContent(
path.join(lwcDir(), 'multiSandbox', 'multiSandbox.html'),
'sandbox="allow-forms allow-scripts"'
);
});

it('should bind the src URL into the generated js as a reactive property', () => {
execCmd(
`template generate lightning embedding --name SrcBinding --src ${src} --sandbox allow-forms --shell-title "${shellTitle}" --output-dir ${lwcDir()}`,
{ ensureExitCode: 0 }
);
assert.fileContent(path.join(lwcDir(), 'srcBinding', 'srcBinding.js'), src);
});

it('should accept http URLs on localhost for local development', () => {
execCmd(
`template generate lightning embedding --name LocalDev --src http://localhost:3000 --sandbox allow-forms --shell-title "${shellTitle}" --output-dir ${lwcDir()}`,
{ ensureExitCode: 0 }
);
assert.fileContent(path.join(lwcDir(), 'localDev', 'localDev.js'), 'http://localhost:3000');
});
});

describe('lightning embedding failures', () => {
const baseFlags = '--name Foo --sandbox allow-forms --shell-title "Demo"';

it('should throw missing --name error', () => {
const stderr = execCmd(
'template generate lightning embedding --src https://app.example.com --sandbox allow-forms --shell-title "Demo"'
).shellOutput.stderr;
expect(stderr).to.contain('Missing required flag');
});

it('should throw missing --src error', () => {
const stderr = execCmd(
'template generate lightning embedding --name Foo --sandbox allow-forms --shell-title "Demo"'
).shellOutput.stderr;
expect(stderr).to.contain('Missing required flag');
});

it('should throw missing --sandbox error', () => {
const stderr = execCmd(
'template generate lightning embedding --name Foo --src https://app.example.com --shell-title "Demo"'
).shellOutput.stderr;
expect(stderr).to.contain('Missing required flag');
});

it('should throw missing --shell-title error (no fallback to --name)', () => {
const stderr = execCmd(
'template generate lightning embedding --name Foo --src https://app.example.com --sandbox allow-forms'
).shellOutput.stderr;
expect(stderr).to.contain('Missing required flag');
});

it('should reject http src on a non-localhost host', () => {
const stderr = execCmd(
`template generate lightning embedding --name Foo --src http://attacker.com --sandbox allow-forms --shell-title "Demo" --output-dir ${lwcDir()}`
).shellOutput.stderr;
expect(stderr).to.contain('HTTPS URL');
});

it('should reject non-http(s) protocols', () => {
const stderr = execCmd(
`template generate lightning embedding --name Foo --src ftp://example.com --sandbox allow-forms --shell-title "Demo" --output-dir ${lwcDir()}`
).shellOutput.stderr;
expect(stderr).to.contain('HTTPS URL');
});

it('should reject malformed --src input', () => {
const stderr = execCmd(
`template generate lightning embedding --name Foo --src not-a-url --sandbox allow-forms --shell-title "Demo" --output-dir ${lwcDir()}`
).shellOutput.stderr;
expect(stderr).to.contain('HTTPS URL');
});

it('should reject an unknown sandbox token', () => {
const stderr = execCmd(
`template generate lightning embedding ${baseFlags} --src https://app.example.com --sandbox allow-everything --output-dir ${lwcDir()}`,
{ ensureExitCode: 'nonZero' }
).shellOutput.stderr;
expect(stderr).to.contain('Expected --sandbox');
});

it('should throw missing lwc parent folder error when output-dir is not under lwc/', () => {
const stderr = execCmd(
`template generate lightning embedding --name Foo --src https://app.example.com --sandbox allow-forms --shell-title "Demo" --output-dir ${path.join(
session.project.dir,
'somewhere-else'
)}`
).shellOutput.stderr;
expect(stderr).to.match(/lwc/i);
});
});
});