-
Notifications
You must be signed in to change notification settings - Fork 6
@W-20906837 feat: add command to use BYO LWR template #829
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # summary | ||
|
|
||
| Generate a Lightning Web Runtime (LWR) Build Your Own Experience Site. | ||
|
|
||
| # description | ||
|
|
||
| Creates an LWR Build Your Own Experience Site with the specified name and URL path prefix. The site includes all necessary metadata files including DigitalExperienceConfig, DigitalExperienceBundle, Network, and CustomSite. | ||
|
|
||
| # examples | ||
|
|
||
| - Generate an LWR BYO site named "mysite" with URL path prefix "mysite": | ||
|
|
||
| <%= config.bin %> <%= command.id %> --name mysite --url-path-prefix mysite | ||
|
|
||
| - Generate an LWR BYO site with a custom output directory: | ||
|
|
||
| <%= config.bin %> <%= command.id %> --name mysite --url-path-prefix mysite --output-dir force-app/main/default | ||
|
|
||
| # flags.name.summary | ||
|
|
||
| Name of the site to generate. | ||
|
|
||
| # flags.name.description | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest you remove the flag description, because it doesn't add any more info to the flag summary. Flag descriptions are optional. |
||
|
|
||
| The name of the site. | ||
|
|
||
| # flags.url-path-prefix.summary | ||
|
|
||
| URL path prefix for the site. | ||
|
|
||
| # flags.url-path-prefix.description | ||
|
|
||
| Optional. The URL path prefix for the site. This is used in the site's URL. Must contain only alphanumeric characters. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you can also remove this flag description. All flags are optional by default, unless explicitly marked required (in the code, which then automatically shows up in the --help). And the first two sentences don't really add more info to the flag summary. The last part (about only alphanumeric characters) can be tacked on the summary; i.e. "URL path prefix for the site; must contain only alphanumeric characters." |
||
|
|
||
| # flags.output-dir.summary | ||
|
|
||
| Directory to generate the site files in. | ||
|
|
||
| # flags.output-dir.description | ||
|
|
||
| The location can be an absolute path or relative to the current working directory. If not specified, the command reads your sfdx-project.json and uses the default package directory. When running outside a Salesforce DX project, defaults to the current directory. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| /* | ||
| * 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 { Flags, SfCommand, Ux } from '@salesforce/sf-plugins-core'; | ||
| import { CreateOutput, DxpSiteOptions, TemplateType } from '@salesforce/templates'; | ||
| import { Messages, SfProject } from '@salesforce/core'; | ||
| import { getCustomTemplates, runGenerator } from '../../../utils/templateCommand.js'; | ||
|
|
||
| Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); | ||
| const messages = Messages.loadMessages('@salesforce/plugin-templates', 'dxpsiteBuildYourOwnLwr'); | ||
|
|
||
| export default class BuildYourOwnLwrGenerate 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 flags = { | ||
| name: Flags.string({ | ||
| char: 'n', | ||
| summary: messages.getMessage('flags.name.summary'), | ||
| description: messages.getMessage('flags.name.description'), | ||
| required: true, | ||
| }), | ||
| 'url-path-prefix': Flags.string({ | ||
| char: 'p', | ||
| summary: messages.getMessage('flags.url-path-prefix.summary'), | ||
| description: messages.getMessage('flags.url-path-prefix.description'), | ||
| default: '', | ||
| }), | ||
| 'output-dir': Flags.directory({ | ||
| char: 'd', | ||
| summary: messages.getMessage('flags.output-dir.summary'), | ||
| description: messages.getMessage('flags.output-dir.description'), | ||
| }), | ||
| }; | ||
|
|
||
| /** | ||
| * Resolves the default output directory by reading the project's sfdx-project.json. | ||
| * Returns the path to the default package directory, | ||
| * or falls back to the current directory if not in a project context. | ||
| */ | ||
| private static async getDefaultOutputDir(): Promise<string> { | ||
| try { | ||
| const project = await SfProject.resolve(); | ||
| const defaultPackage = project.getDefaultPackage(); | ||
| return path.join(defaultPackage.path, 'main', 'default'); | ||
| } catch { | ||
| return '.'; | ||
| } | ||
| } | ||
|
|
||
| public async run(): Promise<CreateOutput> { | ||
| const { flags } = await this.parse(BuildYourOwnLwrGenerate); | ||
|
|
||
| const outputDir = flags['output-dir'] ?? (await BuildYourOwnLwrGenerate.getDefaultOutputDir()); | ||
|
|
||
| const flagsAsOptions: DxpSiteOptions = { | ||
| sitename: flags.name, | ||
| urlpathprefix: flags['url-path-prefix'], | ||
| template: 'build_your_own_lwr', | ||
| outputdir: outputDir, | ||
| }; | ||
|
|
||
| return runGenerator({ | ||
| templateType: TemplateType.DxpSite, | ||
| opts: flagsAsOptions, | ||
| ux: new Ux({ jsonEnabled: this.jsonEnabled() }), | ||
| templates: getCustomTemplates(this.configAggregator), | ||
| }); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| /* | ||
| * 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 } from 'chai'; | ||
| import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit'; | ||
| import { nls } from '@salesforce/templates/lib/i18n/index.js'; | ||
| import assert from 'yeoman-assert'; | ||
|
|
||
| describe('DXP Site build-your-own-lwr creation tests:', () => { | ||
| let session: TestSession; | ||
| before(async () => { | ||
| session = await TestSession.create({ | ||
| project: {}, | ||
| devhubAuthStrategy: 'NONE', | ||
| }); | ||
| }); | ||
| after(async () => { | ||
| await session?.clean(); | ||
| }); | ||
|
|
||
| describe('Check dxpsite creation with build-your-own-lwr template', () => { | ||
| it('should create dxpsite with all required files', () => { | ||
| const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default'); | ||
| execCmd( | ||
| `dxpsite generate build-your-own-lwr --name "My Test Site" --url-path-prefix mytestsite --output-dir "${outputDir}"`, | ||
| { | ||
| ensureExitCode: 0, | ||
| } | ||
| ); | ||
|
|
||
| const bundlePath = path.join(outputDir, 'digitalExperiences', 'site', 'My_Test_Site1'); | ||
|
|
||
| // Check top-level metadata files | ||
| assert.file([ | ||
| path.join(outputDir, 'networks', 'My Test Site.network-meta.xml'), | ||
| path.join(outputDir, 'sites', 'My_Test_Site.site-meta.xml'), | ||
| path.join(outputDir, 'digitalExperienceConfigs', 'My_Test_Site1.digitalExperienceConfig-meta.xml'), | ||
| path.join(bundlePath, 'My_Test_Site1.digitalExperience-meta.xml'), | ||
| ]); | ||
|
|
||
| // Check DEB components | ||
| assert.file([ | ||
| path.join(bundlePath, 'sfdc_cms__appPage', 'mainAppPage', 'content.json'), | ||
| path.join(bundlePath, 'sfdc_cms__appPage', 'mainAppPage', '_meta.json'), | ||
| path.join(bundlePath, 'sfdc_cms__brandingSet', 'Build_Your_Own_LWR', 'content.json'), | ||
| path.join(bundlePath, 'sfdc_cms__brandingSet', 'Build_Your_Own_LWR', '_meta.json'), | ||
| path.join(bundlePath, 'sfdc_cms__languageSettings', 'languages', 'content.json'), | ||
| path.join(bundlePath, 'sfdc_cms__languageSettings', 'languages', '_meta.json'), | ||
| path.join(bundlePath, 'sfdc_cms__mobilePublisherConfig', 'mobilePublisherConfig', 'content.json'), | ||
| path.join(bundlePath, 'sfdc_cms__mobilePublisherConfig', 'mobilePublisherConfig', '_meta.json'), | ||
| path.join(bundlePath, 'sfdc_cms__theme', 'Build_Your_Own_LWR', 'content.json'), | ||
| path.join(bundlePath, 'sfdc_cms__theme', 'Build_Your_Own_LWR', '_meta.json'), | ||
| path.join(bundlePath, 'sfdc_cms__site', 'My_Test_Site1', 'content.json'), | ||
| path.join(bundlePath, 'sfdc_cms__site', 'My_Test_Site1', '_meta.json'), | ||
| ]); | ||
|
|
||
| // Check routes | ||
| const routes = [ | ||
| 'Check_Password', | ||
| 'Error', | ||
| 'Forgot_Password', | ||
| 'Home', | ||
| 'Login', | ||
| 'News_Detail__c', | ||
| 'Register', | ||
| 'Service_Not_Available', | ||
| 'Too_Many_Requests', | ||
| ]; | ||
| for (const route of routes) { | ||
| assert.file([ | ||
| path.join(bundlePath, 'sfdc_cms__route', route, 'content.json'), | ||
| path.join(bundlePath, 'sfdc_cms__route', route, '_meta.json'), | ||
| ]); | ||
| } | ||
|
|
||
| // Check theme layouts | ||
| const layouts = ['scopedHeaderAndFooter', 'snaThemeLayout']; | ||
| for (const layout of layouts) { | ||
| assert.file([ | ||
| path.join(bundlePath, 'sfdc_cms__themeLayout', layout, 'content.json'), | ||
| path.join(bundlePath, 'sfdc_cms__themeLayout', layout, '_meta.json'), | ||
| ]); | ||
| } | ||
|
|
||
| // Check views | ||
| const views = [ | ||
| 'checkPasswordResetEmail', | ||
| 'error', | ||
| 'forgotPassword', | ||
| 'home', | ||
| 'login', | ||
| 'newsDetail', | ||
| 'register', | ||
| 'serviceNotAvailable', | ||
| 'tooManyRequests', | ||
| ]; | ||
| for (const view of views) { | ||
| assert.file([ | ||
| path.join(bundlePath, 'sfdc_cms__view', view, 'content.json'), | ||
| path.join(bundlePath, 'sfdc_cms__view', view, '_meta.json'), | ||
| ]); | ||
| } | ||
| }); | ||
|
|
||
| it('should handle site names starting with special characters', () => { | ||
| const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default'); | ||
| execCmd( | ||
| `dxpsite generate build-your-own-lwr --name "123 @ Test's Site" --url-path-prefix site123 --output-dir "${outputDir}"`, | ||
| { | ||
| ensureExitCode: 0, | ||
| } | ||
| ); | ||
|
|
||
| // Site dev name should be prefixed with X when starting with a number | ||
| const bundlePath = path.join(outputDir, 'digitalExperiences', 'site', 'X123_Test_s_Site1'); | ||
| assert.file([ | ||
| path.join(outputDir, 'networks', '123 %40 Test%27s Site.network-meta.xml'), | ||
| path.join(outputDir, 'sites', 'X123_Test_s_Site.site-meta.xml'), | ||
| path.join(outputDir, 'digitalExperienceConfigs', 'X123_Test_s_Site1.digitalExperienceConfig-meta.xml'), | ||
| path.join(bundlePath, 'X123_Test_s_Site1.digitalExperience-meta.xml'), | ||
| ]); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Check that all invalid input errors are thrown', () => { | ||
| it('should throw a missing name error', () => { | ||
| const stderr = execCmd('dxpsite generate build-your-own-lwr --url-path-prefix test').shellOutput.stderr; | ||
| expect(stderr).to.contain('Missing required flag'); | ||
| }); | ||
|
|
||
| it('should create site without url-path-prefix (uses empty default)', () => { | ||
| const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default'); | ||
| execCmd(`dxpsite generate build-your-own-lwr --name "No Prefix Site" --output-dir "${outputDir}"`, { | ||
| ensureExitCode: 0, | ||
| }); | ||
|
|
||
| const bundlePath = path.join(outputDir, 'digitalExperiences', 'site', 'No_Prefix_Site1'); | ||
| assert.file([ | ||
| path.join(outputDir, 'networks', 'No Prefix Site.network-meta.xml'), | ||
| path.join(outputDir, 'sites', 'No_Prefix_Site.site-meta.xml'), | ||
| path.join(bundlePath, 'No_Prefix_Site1.digitalExperience-meta.xml'), | ||
| ]); | ||
| }); | ||
|
|
||
| it('should throw invalid url-path-prefix error for non-alphanumeric characters', () => { | ||
| const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default'); | ||
| const stderr = execCmd( | ||
| `dxpsite generate build-your-own-lwr --name TestSite --url-path-prefix "my-prefix" --output-dir "${outputDir}"` | ||
| ).shellOutput.stderr; | ||
| expect(stderr).to.contain(nls.localize('AlphaNumericValidationError', 'url-path-prefix')); | ||
| }); | ||
|
|
||
| it('should throw invalid url-path-prefix error for underscores', () => { | ||
| const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default'); | ||
| const stderr = execCmd( | ||
| `dxpsite generate build-your-own-lwr --name TestSite --url-path-prefix "my_prefix" --output-dir "${outputDir}"` | ||
| ).shellOutput.stderr; | ||
| expect(stderr).to.contain(nls.localize('AlphaNumericValidationError', 'url-path-prefix')); | ||
| }); | ||
|
|
||
| it('should throw invalid url-path-prefix error for spaces', () => { | ||
| const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default'); | ||
| const stderr = execCmd( | ||
| `dxpsite generate build-your-own-lwr --name TestSite --url-path-prefix "my prefix" --output-dir "${outputDir}"` | ||
| ).shellOutput.stderr; | ||
| expect(stderr).to.contain(nls.localize('AlphaNumericValidationError', 'url-path-prefix')); | ||
| }); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this added/generated?I'm actually not sure if this should be inside the force folder in the test directory. WebApp template just sits outside of everything.