From f86dc7e192cfd299a9047ffb14421c5f4b3b6c18 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Fri, 27 Mar 2026 13:30:00 +0530 Subject: [PATCH 1/6] refactor: rename webapp template generate command to ui-bundle Rename `sf template generate webapp` to `sf template generate ui-bundle`: - Rename src/commands/template/generate/webapp/ to ui-bundle/ - Rename messages/webApplication.md to messages/ui-bundle.generate.md - Extract UI_BUNDLES_DIR constant for the uiBundles folder name - Update command-snapshot.json with new ui-bundle command entry - Update package.json oclif topics to reflect ui-bundle namespace - Update CLAUDE.md and add-template-generator skill with ui-bundle examples W-21575871 Made-with: Cursor --- .../skills/add-template-generator/SKILL.md | 16 ++- CLAUDE.md | 11 +- command-snapshot.json | 4 +- messages/ui-bundle.generate.md | 62 +++++++++ messages/webApplication.md | 61 --------- package.json | 19 +-- .../generate/{webapp => ui-bundle}/index.ts | 17 +-- .../template/generate/ui-bundle/index.nut.ts | 123 ++++++++++++++++++ .../template/generate/webapp/index.nut.ts | 120 ----------------- 9 files changed, 226 insertions(+), 207 deletions(-) create mode 100644 messages/ui-bundle.generate.md delete mode 100644 messages/webApplication.md rename src/commands/template/generate/{webapp => ui-bundle}/index.ts (84%) create mode 100644 test/commands/template/generate/ui-bundle/index.nut.ts delete mode 100644 test/commands/template/generate/webapp/index.nut.ts diff --git a/.claude/skills/add-template-generator/SKILL.md b/.claude/skills/add-template-generator/SKILL.md index 8e462544..5623554b 100644 --- a/.claude/skills/add-template-generator/SKILL.md +++ b/.claude/skills/add-template-generator/SKILL.md @@ -2,6 +2,7 @@ name: add-template-generator description: Add a new template generator command to the CLI --- + Use this workflow whenever exposing a generator from salesforcedx-templates to CLI users. # Add Template Generator Command Workflow @@ -24,7 +25,8 @@ sf dev generate command -n template:generate:{metadataType}:{optionalSubTemplate ``` **Notes:** -- Replace `{metadataType}` with your metadata type (e.g., `flexipage`, `apex`) + +- Replace `{metadataType}` with your metadata type (e.g., `flexipage`, `ui-bundle`, `apex`) - Only add `{optionalSubTemplate}` if you need nested generators (e.g., `digital-experience:site`) - This creates the command file, updates oclif metadata, and adds NUTs @@ -61,6 +63,7 @@ public static readonly state = 'beta'; // or 'preview' ``` **State options:** + - `beta`: Shows beta warning to users - `preview`: Shows preview warning to users - No state: Command is GA (requires backwards compatibility) @@ -100,11 +103,13 @@ sf dev generate flag ``` This will: + - Add the flag to your command's `flags` object - Generate TypeScript types - Add entries to the `messages.md` file **Common flags to consider:** + - `--name` / `-n`: Name of the generated item (usually required) - `--output-dir` / `-d`: Output directory (default: '.') - `--template` / `-t`: Template type selection (if multiple templates) @@ -113,6 +118,7 @@ This will: ## Step 6: Review Message Files Check `messages/{metadataType}.md` (merge from `template.generate.{metadataType}.md` if generator created a separate file) and ensure: + - Summary is clear and concise - Description provides helpful context - Flag descriptions are detailed and explain constraints @@ -129,9 +135,9 @@ import { runGenerator } from '../../utils/templateCommand.js'; public async run(): Promise { const { flags } = await this.parse(CommandClass); - + // Add any pre-processing or validation here - + return runGenerator({ templateType: TemplateType.{YourMetadataType}, opts: flags, @@ -143,6 +149,7 @@ public async run(): Promise { ## Step 8: Write/Update NUTs Review the auto-generated NUTs in `test/commands/template/generate/{metadataType}/`. Add tests to validate: + - Required flags work correctly - Optional flags are respected - Correct files are created in the right locations @@ -164,6 +171,7 @@ Test your command: ```bash sf template generate {metadataType} --name TestExample --output-dir ./test-output +# e.g. sf template generate ui-bundle --name MyApp --output-dir ./test-output ``` Verify the generated files are correct. @@ -212,4 +220,4 @@ Before opening PR ensure: - flags validated - messages documented - NUTs pass -- topics updated \ No newline at end of file +- topics updated diff --git a/CLAUDE.md b/CLAUDE.md index 6da899cb..91ceedf5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,6 +30,7 @@ Command structure: src/commands/template/generate/{metadataType}/ Files: + - index.ts → top-level generator - {subTemplate}.ts → nested generator @@ -38,7 +39,9 @@ Naming pattern: sf template generate {metadataType} {optionalSubTemplate} Examples: + - sf template generate flexipage +- sf template generate ui-bundle - sf template generate digital-experience site --- @@ -92,13 +95,13 @@ Only GA commands require permanent backwards compatibility. All generators should call: runGenerator({ - templateType: TemplateType.X, - opts: flags, - ux +templateType: TemplateType.X, +opts: flags, +ux }) --- ## Reference Docs -Use official Salesforce CLI docs when needed. \ No newline at end of file +Use official Salesforce CLI docs when needed. diff --git a/command-snapshot.json b/command-snapshot.json index ca0bd5b6..d627cd56 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -148,8 +148,8 @@ "plugin": "@salesforce/plugin-templates" }, { - "alias": ["webapp:generate"], - "command": "template:generate:webapp", + "alias": ["ui-bundle:generate"], + "command": "template:generate:ui-bundle", "flagAliases": [], "flagChars": ["d", "l", "n", "t"], "flags": ["api-version", "flags-dir", "json", "label", "name", "output-dir", "template"], diff --git a/messages/ui-bundle.generate.md b/messages/ui-bundle.generate.md new file mode 100644 index 00000000..4714db01 --- /dev/null +++ b/messages/ui-bundle.generate.md @@ -0,0 +1,62 @@ +# summary + +Generate a UI bundle. + +# description + +Generates a UI bundle in the specified directory or the current working directory. The UI bundle files are created in a folder with the designated name. UI bundle files must be contained in a parent directory called "uiBundles" in your package directory. Either run this command from an existing directory of this name, or use the --output-dir flag to create one or point to an existing one. + +# examples + +- Generate a UI bundle called MyUiBundle in the current directory: + + <%= config.bin %> <%= command.id %> --name MyUiBundle + +- Generate a React-based UI bundle: + + <%= config.bin %> <%= command.id %> --name MyReactApp --template reactbasic + +- Generate the UI bundle in the "force-app/main/default/uiBundles" directory: + + <%= config.bin %> <%= command.id %> --name MyUiBundle --output-dir force-app/main/default/uiBundles + +# flags.name.summary + +Name of the generated UI bundle. + +# flags.name.description + +This name can contain only underscores and alphanumeric characters, and must be unique in your org. It must begin with a letter, not include spaces, not end with an underscore, and not contain two consecutive underscores. + +# flags.template.summary + +Template to use for file creation. + +# flags.template.description + +Supplied parameter values or default values are filled into a copy of the template. + +# flags.label.summary + +Master label for the UI bundle. + +# flags.label.description + +If not specified, the label is derived from the name. + +# flags.output-dir.summary + +Directory for saving the created files. + +# flags.output-dir.description + +The location can be an absolute path or relative to the current working directory. + +**Important:** The generator automatically ensures the output directory ends with "uiBundles". If your specified path doesn't end with "uiBundles", it's automatically appended. The UI bundle is created at "/". + +**Examples:** + +- "--output-dir force-app/main/default" → Creates a UI bundle at "force-app/main/default/uiBundles/MyUiBundle/" +- "--output-dir force-app/main/default/uiBundles" → Creates a UI bundle at "force-app/main/default/uiBundles/MyUiBundle/" (no change) + +If not specified, the command reads your sfdx-project.json and defaults to "uiBundles" directory within your default package directory. When running outside a Salesforce DX project, defaults to the current directory. diff --git a/messages/webApplication.md b/messages/webApplication.md deleted file mode 100644 index 178b4580..00000000 --- a/messages/webApplication.md +++ /dev/null @@ -1,61 +0,0 @@ -# summary - -Generate a web application. - -# description - -Generates a web application in the specified directory or the current working directory. The web application files are created in a folder with the designated name. Web application files must be contained in a parent directory called "webapplications" in your package directory. Either run this command from an existing directory of this name, or use the --output-dir flag to create one or point to an existing one. - -# examples - -- Generate a web application called MyWebApp in the current directory: - - <%= config.bin %> <%= command.id %> --name MyWebApp - -- Generate a React-based web application: - - <%= config.bin %> <%= command.id %> --name MyReactApp --template reactbasic - -- Generate the web application in the "force-app/main/default/webapplications" directory: - - <%= config.bin %> <%= command.id %> --name MyWebApp --output-dir force-app/main/default/webapplications - -# flags.name.summary - -Name of the generated web application. - -# flags.name.description - -This name can contain only underscores and alphanumeric characters, and must be unique in your org. It must begin with a letter, not include spaces, not end with an underscore, and not contain two consecutive underscores. - -# flags.template.summary - -Template to use for file creation. - -# flags.template.description - -Supplied parameter values or default values are filled into a copy of the template. - -# flags.label.summary - -Master label for the web application. - -# flags.label.description - -If not specified, the label is derived from the name. - -# flags.output-dir.summary - -Directory for saving the created files. - -# flags.output-dir.description - -The location can be an absolute path or relative to the current working directory. - -**Important:** The generator automatically ensures the output directory ends with "webapplications". If your specified path doesn't end with "webapplications", it's automatically appended. The web application is created at "/". - -**Examples:** -- "--output-dir force-app/main/default" → Creates a web application at "force-app/main/default/webapplications/MyWebApp/" -- "--output-dir force-app/main/default/webapplications" → Creates a web application at "force-app/main/default/webapplications/MyWebApp/" (no change) - -If not specified, the command reads your sfdx-project.json and defaults to "webapplications" directory within your default package directory. When running outside a Salesforce DX project, defaults to the current directory. diff --git a/package.json b/package.json index 8e348b93..2f1c4357 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,24 @@ { "name": "@salesforce/plugin-templates", "description": "Commands to create metadata from a default or custom template", - "version": "56.10.7", + "version": "56.11.5", "author": "Salesforce", "bugs": "https://github.com/salesforcecli/plugin-templates/issues", "enableO11y": true, "o11yUploadEndpoint": "https://794testsite.my.site.com/byolwr/webruntime/log/metrics", "dependencies": { - "@salesforce/core": "^8.27.0", + "@salesforce/core": "^8.27.1", "@salesforce/sf-plugins-core": "^12", - "@salesforce/templates": "^66.4.1" + "@salesforce/templates": "^66.5.6" }, "devDependencies": { - "@oclif/plugin-command-snapshot": "^5.3.12", + "@oclif/plugin-command-snapshot": "^5.3.13", "@salesforce/cli-plugins-testkit": "^5.3.41", "@salesforce/dev-scripts": "^11.0.4", - "@salesforce/plugin-command-reference": "^3.1.81", + "@salesforce/plugin-command-reference": "^3.1.82", "@types/yeoman-assert": "^3.1.4", "eslint-plugin-sf-plugin": "^1.20.33", - "oclif": "^4.22.87", + "oclif": "^4.22.96", "ts-node": "^10.9.2", "typescript": "^5.9.3", "yeoman-assert": "^3.1.1" @@ -84,12 +84,15 @@ "visualforce": { "description": "Create a visualforce page or component." }, - "webapp": { - "description": "Create a web application." + "ui-bundle": { + "description": "Generate a UI bundle." } } } } + }, + "ui-bundle": { + "description": "Work with UI bundles." } }, "flexibleTaxonomy": true, diff --git a/src/commands/template/generate/webapp/index.ts b/src/commands/template/generate/ui-bundle/index.ts similarity index 84% rename from src/commands/template/generate/webapp/index.ts rename to src/commands/template/generate/ui-bundle/index.ts index e91d2b57..7868f707 100644 --- a/src/commands/template/generate/webapp/index.ts +++ b/src/commands/template/generate/ui-bundle/index.ts @@ -12,15 +12,16 @@ 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', 'webApplication'); +const messages = Messages.loadMessages('@salesforce/plugin-templates', 'ui-bundle.generate'); -export default class WebAppGenerate extends SfCommand { +export const UI_BUNDLES_DIR = 'uiBundles'; + +export default class UiBundleGenerate 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 hidden = true; // Hide from external developers until GA - public static readonly aliases = ['webapp:generate']; - public static readonly deprecateAliases = true; + public static readonly aliases = ['ui-bundle:generate']; public static readonly flags = { name: Flags.string({ char: 'n', @@ -50,23 +51,23 @@ export default class WebAppGenerate extends SfCommand { /** * Resolves the default output directory by reading the project's sfdx-project.json. - * Returns the path to webapplications under the default package directory, + * Returns the path to uiBundles under the default package directory, * or falls back to the current directory if not in a project context. */ private static async getDefaultOutputDir(): Promise { try { const project = await SfProject.resolve(); const defaultPackage = project.getDefaultPackage(); - return path.join(defaultPackage.path, 'main', 'default', 'webapplications'); + return path.join(defaultPackage.path, 'main', 'default', UI_BUNDLES_DIR); } catch { return '.'; } } public async run(): Promise { - const { flags } = await this.parse(WebAppGenerate); + const { flags } = await this.parse(UiBundleGenerate); - const outputDir = flags['output-dir'] ?? (await WebAppGenerate.getDefaultOutputDir()); + const outputDir = flags['output-dir'] ?? (await UiBundleGenerate.getDefaultOutputDir()); const flagsAsOptions: WebApplicationOptions = { webappname: flags.name, diff --git a/test/commands/template/generate/ui-bundle/index.nut.ts b/test/commands/template/generate/ui-bundle/index.nut.ts new file mode 100644 index 00000000..d31e343b --- /dev/null +++ b/test/commands/template/generate/ui-bundle/index.nut.ts @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2025, 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'; +const UI_BUNDLES_DIR = 'uiBundles'; + +describe('template generate ui-bundle:', () => { + let session: TestSession; + let projectDir: string; + before(async () => { + session = await TestSession.create({ + project: {}, + devhubAuthStrategy: 'NONE', + }); + projectDir = session.project.dir; + }); + after(async () => { + await session?.clean(); + }); + + describe('Check UI bundle creation with default template', () => { + it('should create UI bundle using default template in uiBundles directory', () => { + const outputDir = path.join(projectDir, 'force-app', 'main', 'default', UI_BUNDLES_DIR); + execCmd(`template generate ui-bundle --name MyUiBundle --output-dir "${outputDir}"`, { ensureExitCode: 0 }); + assert.file([ + path.join(outputDir, 'MyUiBundle', 'MyUiBundle.webapplication-meta.xml'), + path.join(outputDir, 'MyUiBundle', 'src', 'index.html'), + path.join(outputDir, 'MyUiBundle', 'webapplication.json'), + ]); + assert.fileContent( + path.join(outputDir, 'MyUiBundle', 'MyUiBundle.webapplication-meta.xml'), + 'My Ui Bundle' + ); + }); + + it('should default to project uiBundles directory when --output-dir is omitted', () => { + const expectedOutputDir = path.join(projectDir, 'force-app', 'main', 'default', UI_BUNDLES_DIR); + execCmd('template generate ui-bundle --name DefaultDirApp', { ensureExitCode: 0 }); + assert.file([ + path.join(expectedOutputDir, 'DefaultDirApp', 'DefaultDirApp.webapplication-meta.xml'), + path.join(expectedOutputDir, 'DefaultDirApp', 'src', 'index.html'), + path.join(expectedOutputDir, 'DefaultDirApp', 'webapplication.json'), + ]); + }); + + it('should create UI bundle with custom label', () => { + const outputDir = path.join(projectDir, 'force-app', 'main', 'default', UI_BUNDLES_DIR); + execCmd(`template generate ui-bundle --name TestApp --label "Custom Label" --output-dir "${outputDir}"`, { + ensureExitCode: 0, + }); + assert.file([ + path.join(outputDir, 'TestApp', 'TestApp.webapplication-meta.xml'), + path.join(outputDir, 'TestApp', 'src', 'index.html'), + ]); + assert.fileContent(path.join(outputDir, 'TestApp', 'src', 'index.html'), 'Welcome to Web App'); + }); + }); + + describe('Check UI bundle creation with reactbasic template', () => { + it('should create React UI bundle with all required files', () => { + const outputDir = path.join(projectDir, 'force-app', 'main', 'default', UI_BUNDLES_DIR); + execCmd(`template generate ui-bundle --name MyReactApp --template reactbasic --output-dir "${outputDir}"`, { + ensureExitCode: 0, + }); + assert.file([ + path.join(outputDir, 'MyReactApp', 'MyReactApp.webapplication-meta.xml'), + path.join(outputDir, 'MyReactApp', 'index.html'), + path.join(outputDir, 'MyReactApp', 'webapplication.json'), + path.join(outputDir, 'MyReactApp', 'package.json'), + ]); + assert.fileContent(path.join(outputDir, 'MyReactApp', 'package.json'), '"name": "base-react-app"'); + }); + }); + + describe('Check that all invalid name errors are thrown', () => { + it('should throw a missing name error', () => { + const stderr = execCmd('template generate ui-bundle').shellOutput.stderr; + expect(stderr).to.contain('Missing required flag'); + }); + + it('should throw invalid non alphanumeric name error', () => { + const outputDir = path.join(projectDir, 'force-app', 'main', 'default', UI_BUNDLES_DIR); + const stderr = execCmd(`template generate ui-bundle --name /a --output-dir "${outputDir}"`).shellOutput.stderr; + expect(stderr).to.contain(nls.localize('AlphaNumericNameError')); + }); + + it('should throw invalid name starting with numeric error', () => { + const outputDir = path.join(projectDir, 'force-app', 'main', 'default', UI_BUNDLES_DIR); + const stderr = execCmd(`template generate ui-bundle --name 3aa --output-dir "${outputDir}"`).shellOutput.stderr; + expect(stderr).to.contain(nls.localize('NameMustStartWithLetterError')); + }); + + it('should throw invalid name ending with underscore error', () => { + const outputDir = path.join(projectDir, 'force-app', 'main', 'default', UI_BUNDLES_DIR); + const stderr = execCmd(`template generate ui-bundle --name a_ --output-dir "${outputDir}"`).shellOutput.stderr; + expect(stderr).to.contain(nls.localize('EndWithUnderscoreError')); + }); + + it('should throw invalid name with double underscore error', () => { + const outputDir = path.join(projectDir, 'force-app', 'main', 'default', UI_BUNDLES_DIR); + const stderr = execCmd(`template generate ui-bundle --name a__a --output-dir "${outputDir}"`).shellOutput.stderr; + expect(stderr).to.contain(nls.localize('DoubleUnderscoreError')); + }); + + it('should auto-append uiBundles folder when output dir does not end with uiBundles', () => { + const outputDir = path.join(projectDir, 'force-app', 'main', 'default', 'test-dir'); + const expectedOutputDir = path.join(outputDir, UI_BUNDLES_DIR); + execCmd(`template generate ui-bundle --name TestApp --output-dir "${outputDir}"`, { ensureExitCode: 0 }); + assert.file([ + path.join(expectedOutputDir, 'TestApp', 'TestApp.webapplication-meta.xml'), + path.join(expectedOutputDir, 'TestApp', 'src', 'index.html'), + path.join(expectedOutputDir, 'TestApp', 'webapplication.json'), + ]); + }); + }); +}); diff --git a/test/commands/template/generate/webapp/index.nut.ts b/test/commands/template/generate/webapp/index.nut.ts deleted file mode 100644 index efc2168c..00000000 --- a/test/commands/template/generate/webapp/index.nut.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2025, 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('template generate web application:', () => { - let session: TestSession; - before(async () => { - session = await TestSession.create({ - project: {}, - devhubAuthStrategy: 'NONE', - }); - }); - after(async () => { - await session?.clean(); - }); - - describe('Check webapp creation with default template', () => { - it('should create webapp using default template in webapplications directory', () => { - const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'webapplications'); - execCmd(`template generate webapp --name MyWebApp --output-dir "${outputDir}"`, { ensureExitCode: 0 }); - assert.file([ - path.join(outputDir, 'MyWebApp', 'MyWebApp.webapplication-meta.xml'), - path.join(outputDir, 'MyWebApp', 'src', 'index.html'), - path.join(outputDir, 'MyWebApp', 'webapplication.json'), - ]); - assert.fileContent( - path.join(outputDir, 'MyWebApp', 'MyWebApp.webapplication-meta.xml'), - 'My Web App' - ); - }); - - it('should default to project webapplications directory when --output-dir is omitted', () => { - const expectedOutputDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'webapplications'); - execCmd('template generate webapp --name DefaultDirApp', { ensureExitCode: 0 }); - assert.file([ - path.join(expectedOutputDir, 'DefaultDirApp', 'DefaultDirApp.webapplication-meta.xml'), - path.join(expectedOutputDir, 'DefaultDirApp', 'src', 'index.html'), - path.join(expectedOutputDir, 'DefaultDirApp', 'webapplication.json'), - ]); - }); - - it('should create webapp with custom label', () => { - const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'webapplications'); - execCmd(`template generate webapp --name TestApp --label "Custom Label" --output-dir "${outputDir}"`, { - ensureExitCode: 0, - }); - assert.file([ - path.join(outputDir, 'TestApp', 'TestApp.webapplication-meta.xml'), - path.join(outputDir, 'TestApp', 'src', 'index.html'), - ]); - assert.fileContent(path.join(outputDir, 'TestApp', 'src', 'index.html'), 'Welcome to Web App'); - }); - }); - - describe('Check webapp creation with reactbasic template', () => { - it('should create React webapp with all required files', () => { - const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'webapplications'); - execCmd(`template generate webapp --name MyReactApp --template reactbasic --output-dir "${outputDir}"`, { - ensureExitCode: 0, - }); - assert.file([ - path.join(outputDir, 'MyReactApp', 'MyReactApp.webapplication-meta.xml'), - path.join(outputDir, 'MyReactApp', 'index.html'), - path.join(outputDir, 'MyReactApp', 'webapplication.json'), - path.join(outputDir, 'MyReactApp', 'package.json'), - ]); - assert.fileContent(path.join(outputDir, 'MyReactApp', 'package.json'), '"name": "base-react-app"'); - }); - }); - - describe('Check that all invalid name errors are thrown', () => { - it('should throw a missing name error', () => { - const stderr = execCmd('template generate webapp').shellOutput.stderr; - expect(stderr).to.contain('Missing required flag'); - }); - - it('should throw invalid non alphanumeric webapp name error', () => { - const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'webapplications'); - const stderr = execCmd(`template generate webapp --name /a --output-dir "${outputDir}"`).shellOutput.stderr; - expect(stderr).to.contain(nls.localize('AlphaNumericNameError')); - }); - - it('should throw invalid webapp name starting with numeric error', () => { - const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'webapplications'); - const stderr = execCmd(`template generate webapp --name 3aa --output-dir "${outputDir}"`).shellOutput.stderr; - expect(stderr).to.contain(nls.localize('NameMustStartWithLetterError')); - }); - - it('should throw invalid webapp name ending with underscore error', () => { - const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'webapplications'); - const stderr = execCmd(`template generate webapp --name a_ --output-dir "${outputDir}"`).shellOutput.stderr; - expect(stderr).to.contain(nls.localize('EndWithUnderscoreError')); - }); - - it('should throw invalid webapp name with double underscore error', () => { - const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'webapplications'); - const stderr = execCmd(`template generate webapp --name a__a --output-dir "${outputDir}"`).shellOutput.stderr; - expect(stderr).to.contain(nls.localize('DoubleUnderscoreError')); - }); - - it('should auto-append webapplications folder when output dir does not end with webapplications', () => { - const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'test-dir'); - const expectedOutputDir = path.join(outputDir, 'webapplications'); - execCmd(`template generate webapp --name TestApp --output-dir "${outputDir}"`, { ensureExitCode: 0 }); - assert.file([ - path.join(expectedOutputDir, 'TestApp', 'TestApp.webapplication-meta.xml'), - path.join(expectedOutputDir, 'TestApp', 'src', 'index.html'), - path.join(expectedOutputDir, 'TestApp', 'webapplication.json'), - ]); - }); - }); -}); From 541a7d9eb0e4c0c1465b5148370e73c34df72b92 Mon Sep 17 00:00:00 2001 From: Deepu Mungamuri Date: Fri, 27 Mar 2026 19:07:43 +0530 Subject: [PATCH 2/6] feat: rename react project templates from reactb2e/reactb2x to reactinternalapp/reactexternalapp - Replace deprecated reactb2e and reactb2x template names with reactinternalapp and reactexternalapp in the project generate command options, improving clarity by reflecting the internal vs external audience distinction instead of opaque B2E/B2X codes - Add nativemobile as a new supported template option - Bump @salesforce/templates to 66.6.2 to pick up the uiBundles output directory rename (previously webapplications) and aligned internal template identifiers - Update NUT tests to use the new template names and uiBundles path - Update messages to document the renamed templates correctly Made-with: Cursor --- messages/project.md | 2 +- package.json | 2 +- .../template/generate/project/index.ts | 10 +++++++++- .../template/generate/project/index.nut.ts | 20 +++++++++---------- yarn.lock | 8 ++++---- 5 files changed, 25 insertions(+), 17 deletions(-) diff --git a/messages/project.md b/messages/project.md index 4417e539..a6d28c09 100644 --- a/messages/project.md +++ b/messages/project.md @@ -58,7 +58,7 @@ The standard template provides a complete force-app directory structure so you k The analytics template provides similar files and the force-app/main/default/waveTemplates directory. -The reactb2e and reactb2x templates provide React-based project scaffolding for B2E and B2X web application use cases. +The reactinternalapp and reactexternalapp templates provide React-based project scaffolding for internal and external web application use cases. The agent template provides project scaffolding for building Agentforce agents and includes a sample agent called Local Info Agent. diff --git a/package.json b/package.json index 2f1c4357..0ba72fc6 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "dependencies": { "@salesforce/core": "^8.27.1", "@salesforce/sf-plugins-core": "^12", - "@salesforce/templates": "^66.5.6" + "@salesforce/templates": "^66.6.2" }, "devDependencies": { "@oclif/plugin-command-snapshot": "^5.3.13", diff --git a/src/commands/template/generate/project/index.ts b/src/commands/template/generate/project/index.ts index 38d6420c..2ab31722 100644 --- a/src/commands/template/generate/project/index.ts +++ b/src/commands/template/generate/project/index.ts @@ -33,7 +33,15 @@ export default class Project extends SfCommand { summary: messages.getMessage('flags.template.summary'), description: messages.getMessage('flags.template.description'), default: 'standard', - options: ['standard', 'empty', 'analytics', 'reactb2e', 'reactb2x', 'agent'] as const, + options: [ + 'standard', + 'empty', + 'analytics', + 'reactinternalapp', + 'reactexternalapp', + 'agent', + 'nativemobile', + ] as const, })(), 'output-dir': outputDirFlag, namespace: Flags.string({ diff --git a/test/commands/template/generate/project/index.nut.ts b/test/commands/template/generate/project/index.nut.ts index 4c613d6a..1c4d68f0 100644 --- a/test/commands/template/generate/project/index.nut.ts +++ b/test/commands/template/generate/project/index.nut.ts @@ -243,10 +243,10 @@ describe('template generate project:', () => { assert.file([path.join(session.project.dir, 'analytics1', 'eslint.config.js')]); }); - it('should create project with reactb2e template', () => { - const projectName = 'react-b2e-test'; - const alphanumericName = 'reactb2etest'; - execCmd(`template generate project --projectname ${projectName} --template reactb2e`, { + it('should create project with reactexternalapp template', () => { + const projectName = 'react-externalapp-test'; + const alphanumericName = 'reactexternalapptest'; + execCmd(`template generate project --projectname ${projectName} --template reactexternalapp`, { ensureExitCode: 0, }); const projectDir = path.join(session.project.dir, projectName); @@ -257,7 +257,7 @@ describe('template generate project:', () => { 'force-app', 'main', 'default', - 'webapplications', + 'uiBundles', alphanumericName, `${alphanumericName}.webapplication-meta.xml` ); @@ -265,10 +265,10 @@ describe('template generate project:', () => { assert.fileContent(webappMetaPath, alphanumericName); }); - it('should create project with reactb2x template', () => { - const projectName = 'react-b2x-test'; - const alphanumericName = 'reactb2xtest'; - execCmd(`template generate project --projectname ${projectName} --template reactb2x`, { + it('should create project with reactinternalapp template', () => { + const projectName = 'react-internalapp-test'; + const alphanumericName = 'reactinternalapptest'; + execCmd(`template generate project --projectname ${projectName} --template reactinternalapp`, { ensureExitCode: 0, }); const projectDir = path.join(session.project.dir, projectName); @@ -279,7 +279,7 @@ describe('template generate project:', () => { 'force-app', 'main', 'default', - 'webapplications', + 'uiBundles', alphanumericName, `${alphanumericName}.webapplication-meta.xml` ); diff --git a/yarn.lock b/yarn.lock index a695e619..9b0be05d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1649,10 +1649,10 @@ cli-progress "^3.12.0" terminal-link "^3.0.0" -"@salesforce/templates@^66.5.6": - version "66.5.6" - resolved "https://registry.yarnpkg.com/@salesforce/templates/-/templates-66.5.6.tgz#1fcece13106882ded54fcd96a5d9bf49fd31a546" - integrity sha512-k18ROus8XUskIT6n2eXsHvoZK3NmrVePSPduCfjWEohMEJ/qy9ltdyMos/eebzc3ejooLBu40/22zKjVZFoFfA== +"@salesforce/templates@^66.6.2": + version "66.6.2" + resolved "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@salesforce/templates/-/templates-66.6.2.tgz#efc37965d41ba81791d08ecb733a327f4e9f8536" + integrity sha512-ACrKYF586OtPD9PfDivFL9p66Bb08wbVOlp/FnlphjLFmcVNGxNlmG0bJnZqjHQnckZOvtrVfwBaHxS4iFObtg== dependencies: "@salesforce/kit" "^3.2.4" ejs "^3.1.10" From 05437833356bc02e6fa5c009549468094208e42b Mon Sep 17 00:00:00 2001 From: gary-chang Date: Sat, 28 Mar 2026 17:37:37 -0700 Subject: [PATCH 3/6] fix: update command snapshot with webapp:generate alias and update project messages --- command-snapshot.json | 2 +- messages/project.md | 2 +- package.json | 14 +++++++++++++- src/commands/template/generate/ui-bundle/index.ts | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/command-snapshot.json b/command-snapshot.json index d627cd56..a91bf327 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -148,7 +148,7 @@ "plugin": "@salesforce/plugin-templates" }, { - "alias": ["ui-bundle:generate"], + "alias": ["ui-bundle:generate", "webapp:generate"], "command": "template:generate:ui-bundle", "flagAliases": [], "flagChars": ["d", "l", "n", "t"], diff --git a/messages/project.md b/messages/project.md index a6d28c09..2373b891 100644 --- a/messages/project.md +++ b/messages/project.md @@ -58,7 +58,7 @@ The standard template provides a complete force-app directory structure so you k The analytics template provides similar files and the force-app/main/default/waveTemplates directory. -The reactinternalapp and reactexternalapp templates provide React-based project scaffolding for internal and external web application use cases. +The reactinternalapp and reactexternalapp templates provide React-based project scaffolding for internal and external UI bundle use cases. The agent template provides project scaffolding for building Agentforce agents and includes a sample agent called Local Info Agent. diff --git a/package.json b/package.json index 0ba72fc6..e37e0bf2 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,14 @@ }, "ui-bundle": { "description": "Work with UI bundles." + }, + "webapp": { + "description": "Work with UI bundles.", + "subtopics": { + "generate": { + "description": "Generate a UI bundle." + } + } } }, "flexibleTaxonomy": true, @@ -250,5 +258,9 @@ } }, "exports": "./lib/index.js", - "type": "module" + "type": "module", + "volta": { + "node": "24.14.1", + "yarn": "1.22.22" + } } diff --git a/src/commands/template/generate/ui-bundle/index.ts b/src/commands/template/generate/ui-bundle/index.ts index 7868f707..26819a23 100644 --- a/src/commands/template/generate/ui-bundle/index.ts +++ b/src/commands/template/generate/ui-bundle/index.ts @@ -21,7 +21,7 @@ export default class UiBundleGenerate extends SfCommand { public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); public static readonly hidden = true; // Hide from external developers until GA - public static readonly aliases = ['ui-bundle:generate']; + public static readonly aliases = ['ui-bundle:generate', 'webapp:generate']; public static readonly flags = { name: Flags.string({ char: 'n', From 28894e17c53a38de1ee5e1ae36a4dcd252515213 Mon Sep 17 00:00:00 2001 From: k-j-kim Date: Sun, 29 Mar 2026 22:16:10 -0700 Subject: [PATCH 4/6] fix: align with upstream salesforcedx-templates renames and incorporate PR review feedback --- .../skills/add-template-generator/SKILL.md | 3 +-- command-snapshot.json | 2 +- package.json | 8 -------- .../template/generate/project/index.ts | 10 +--------- .../template/generate/ui-bundle/index.ts | 10 +++++----- .../template/generate/project/index.nut.ts | 4 ++-- .../template/generate/ui-bundle/index.nut.ts | 20 +++++++++---------- 7 files changed, 20 insertions(+), 37 deletions(-) diff --git a/.claude/skills/add-template-generator/SKILL.md b/.claude/skills/add-template-generator/SKILL.md index 5623554b..0ce77cca 100644 --- a/.claude/skills/add-template-generator/SKILL.md +++ b/.claude/skills/add-template-generator/SKILL.md @@ -26,7 +26,7 @@ sf dev generate command -n template:generate:{metadataType}:{optionalSubTemplate **Notes:** -- Replace `{metadataType}` with your metadata type (e.g., `flexipage`, `ui-bundle`, `apex`) +- Replace `{metadataType}` with your metadata type (e.g., `flexipage`, `apex`) - Only add `{optionalSubTemplate}` if you need nested generators (e.g., `digital-experience:site`) - This creates the command file, updates oclif metadata, and adds NUTs @@ -171,7 +171,6 @@ Test your command: ```bash sf template generate {metadataType} --name TestExample --output-dir ./test-output -# e.g. sf template generate ui-bundle --name MyApp --output-dir ./test-output ``` Verify the generated files are correct. diff --git a/command-snapshot.json b/command-snapshot.json index a91bf327..d627cd56 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -148,7 +148,7 @@ "plugin": "@salesforce/plugin-templates" }, { - "alias": ["ui-bundle:generate", "webapp:generate"], + "alias": ["ui-bundle:generate"], "command": "template:generate:ui-bundle", "flagAliases": [], "flagChars": ["d", "l", "n", "t"], diff --git a/package.json b/package.json index e37e0bf2..7d620078 100644 --- a/package.json +++ b/package.json @@ -93,14 +93,6 @@ }, "ui-bundle": { "description": "Work with UI bundles." - }, - "webapp": { - "description": "Work with UI bundles.", - "subtopics": { - "generate": { - "description": "Generate a UI bundle." - } - } } }, "flexibleTaxonomy": true, diff --git a/src/commands/template/generate/project/index.ts b/src/commands/template/generate/project/index.ts index 2ab31722..c44fb6aa 100644 --- a/src/commands/template/generate/project/index.ts +++ b/src/commands/template/generate/project/index.ts @@ -33,15 +33,7 @@ export default class Project extends SfCommand { summary: messages.getMessage('flags.template.summary'), description: messages.getMessage('flags.template.description'), default: 'standard', - options: [ - 'standard', - 'empty', - 'analytics', - 'reactinternalapp', - 'reactexternalapp', - 'agent', - 'nativemobile', - ] as const, + options: ['standard', 'empty', 'analytics', 'reactinternalapp', 'reactexternalapp', 'agent'] as const, })(), 'output-dir': outputDirFlag, namespace: Flags.string({ diff --git a/src/commands/template/generate/ui-bundle/index.ts b/src/commands/template/generate/ui-bundle/index.ts index 26819a23..ca7b4e1a 100644 --- a/src/commands/template/generate/ui-bundle/index.ts +++ b/src/commands/template/generate/ui-bundle/index.ts @@ -7,7 +7,7 @@ import path from 'node:path'; import { Flags, SfCommand, Ux } from '@salesforce/sf-plugins-core'; -import { CreateOutput, WebApplicationOptions, TemplateType } from '@salesforce/templates'; +import { CreateOutput, UIBundleOptions, TemplateType } from '@salesforce/templates'; import { Messages, SfProject } from '@salesforce/core'; import { getCustomTemplates, runGenerator } from '../../../../utils/templateCommand.js'; @@ -21,7 +21,7 @@ export default class UiBundleGenerate extends SfCommand { public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); public static readonly hidden = true; // Hide from external developers until GA - public static readonly aliases = ['ui-bundle:generate', 'webapp:generate']; + public static readonly aliases = ['ui-bundle:generate']; public static readonly flags = { name: Flags.string({ char: 'n', @@ -69,8 +69,8 @@ export default class UiBundleGenerate extends SfCommand { const outputDir = flags['output-dir'] ?? (await UiBundleGenerate.getDefaultOutputDir()); - const flagsAsOptions: WebApplicationOptions = { - webappname: flags.name, + const flagsAsOptions: UIBundleOptions = { + bundlename: flags.name, template: flags.template, masterlabel: flags.label, outputdir: outputDir, @@ -78,7 +78,7 @@ export default class UiBundleGenerate extends SfCommand { }; return runGenerator({ - templateType: TemplateType.WebApplication, + templateType: TemplateType.UIBundle, opts: flagsAsOptions, ux: new Ux({ jsonEnabled: this.jsonEnabled() }), templates: getCustomTemplates(this.configAggregator), diff --git a/test/commands/template/generate/project/index.nut.ts b/test/commands/template/generate/project/index.nut.ts index 1c4d68f0..cd0a0b28 100644 --- a/test/commands/template/generate/project/index.nut.ts +++ b/test/commands/template/generate/project/index.nut.ts @@ -259,7 +259,7 @@ describe('template generate project:', () => { 'default', 'uiBundles', alphanumericName, - `${alphanumericName}.webapplication-meta.xml` + `${alphanumericName}.uibundle-meta.xml` ); assert.file([webappMetaPath]); assert.fileContent(webappMetaPath, alphanumericName); @@ -281,7 +281,7 @@ describe('template generate project:', () => { 'default', 'uiBundles', alphanumericName, - `${alphanumericName}.webapplication-meta.xml` + `${alphanumericName}.uibundle-meta.xml` ); assert.file([webappMetaPath]); assert.fileContent(webappMetaPath, alphanumericName); diff --git a/test/commands/template/generate/ui-bundle/index.nut.ts b/test/commands/template/generate/ui-bundle/index.nut.ts index d31e343b..17275b7f 100644 --- a/test/commands/template/generate/ui-bundle/index.nut.ts +++ b/test/commands/template/generate/ui-bundle/index.nut.ts @@ -30,12 +30,12 @@ describe('template generate ui-bundle:', () => { const outputDir = path.join(projectDir, 'force-app', 'main', 'default', UI_BUNDLES_DIR); execCmd(`template generate ui-bundle --name MyUiBundle --output-dir "${outputDir}"`, { ensureExitCode: 0 }); assert.file([ - path.join(outputDir, 'MyUiBundle', 'MyUiBundle.webapplication-meta.xml'), + path.join(outputDir, 'MyUiBundle', 'MyUiBundle.uibundle-meta.xml'), path.join(outputDir, 'MyUiBundle', 'src', 'index.html'), - path.join(outputDir, 'MyUiBundle', 'webapplication.json'), + path.join(outputDir, 'MyUiBundle', 'ui-bundle.json'), ]); assert.fileContent( - path.join(outputDir, 'MyUiBundle', 'MyUiBundle.webapplication-meta.xml'), + path.join(outputDir, 'MyUiBundle', 'MyUiBundle.uibundle-meta.xml'), 'My Ui Bundle' ); }); @@ -44,9 +44,9 @@ describe('template generate ui-bundle:', () => { const expectedOutputDir = path.join(projectDir, 'force-app', 'main', 'default', UI_BUNDLES_DIR); execCmd('template generate ui-bundle --name DefaultDirApp', { ensureExitCode: 0 }); assert.file([ - path.join(expectedOutputDir, 'DefaultDirApp', 'DefaultDirApp.webapplication-meta.xml'), + path.join(expectedOutputDir, 'DefaultDirApp', 'DefaultDirApp.uibundle-meta.xml'), path.join(expectedOutputDir, 'DefaultDirApp', 'src', 'index.html'), - path.join(expectedOutputDir, 'DefaultDirApp', 'webapplication.json'), + path.join(expectedOutputDir, 'DefaultDirApp', 'ui-bundle.json'), ]); }); @@ -56,7 +56,7 @@ describe('template generate ui-bundle:', () => { ensureExitCode: 0, }); assert.file([ - path.join(outputDir, 'TestApp', 'TestApp.webapplication-meta.xml'), + path.join(outputDir, 'TestApp', 'TestApp.uibundle-meta.xml'), path.join(outputDir, 'TestApp', 'src', 'index.html'), ]); assert.fileContent(path.join(outputDir, 'TestApp', 'src', 'index.html'), 'Welcome to Web App'); @@ -70,9 +70,9 @@ describe('template generate ui-bundle:', () => { ensureExitCode: 0, }); assert.file([ - path.join(outputDir, 'MyReactApp', 'MyReactApp.webapplication-meta.xml'), + path.join(outputDir, 'MyReactApp', 'MyReactApp.uibundle-meta.xml'), path.join(outputDir, 'MyReactApp', 'index.html'), - path.join(outputDir, 'MyReactApp', 'webapplication.json'), + path.join(outputDir, 'MyReactApp', 'ui-bundle.json'), path.join(outputDir, 'MyReactApp', 'package.json'), ]); assert.fileContent(path.join(outputDir, 'MyReactApp', 'package.json'), '"name": "base-react-app"'); @@ -114,9 +114,9 @@ describe('template generate ui-bundle:', () => { const expectedOutputDir = path.join(outputDir, UI_BUNDLES_DIR); execCmd(`template generate ui-bundle --name TestApp --output-dir "${outputDir}"`, { ensureExitCode: 0 }); assert.file([ - path.join(expectedOutputDir, 'TestApp', 'TestApp.webapplication-meta.xml'), + path.join(expectedOutputDir, 'TestApp', 'TestApp.uibundle-meta.xml'), path.join(expectedOutputDir, 'TestApp', 'src', 'index.html'), - path.join(expectedOutputDir, 'TestApp', 'webapplication.json'), + path.join(expectedOutputDir, 'TestApp', 'ui-bundle.json'), ]); }); }); From dc6fa3bc5766c1ed442c618003da5b3303f3b877 Mon Sep 17 00:00:00 2001 From: k-j-kim Date: Sun, 29 Mar 2026 22:21:26 -0700 Subject: [PATCH 5/6] chore: bump @salesforce/templates to ^66.7.1 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7d620078..a43c6dad 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "dependencies": { "@salesforce/core": "^8.27.1", "@salesforce/sf-plugins-core": "^12", - "@salesforce/templates": "^66.6.2" + "@salesforce/templates": "^66.7.1" }, "devDependencies": { "@oclif/plugin-command-snapshot": "^5.3.13", diff --git a/yarn.lock b/yarn.lock index 9b0be05d..1f618c1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1649,10 +1649,10 @@ cli-progress "^3.12.0" terminal-link "^3.0.0" -"@salesforce/templates@^66.6.2": - version "66.6.2" - resolved "https://nexus-proxy.repo.local.sfdc.net/nexus/content/groups/npm-all/@salesforce/templates/-/templates-66.6.2.tgz#efc37965d41ba81791d08ecb733a327f4e9f8536" - integrity sha512-ACrKYF586OtPD9PfDivFL9p66Bb08wbVOlp/FnlphjLFmcVNGxNlmG0bJnZqjHQnckZOvtrVfwBaHxS4iFObtg== +"@salesforce/templates@^66.7.1": + version "66.7.1" + resolved "https://registry.yarnpkg.com/@salesforce/templates/-/templates-66.7.1.tgz#042a8dd786e544aa5772ca361fe010d23b5bf12f" + integrity sha512-MCHKy2Fjll528Yoxg7WiSHx3yWKanxFFtpW3IYQQhBpjyh4lkbwT5/iOMfihkqdD9HjzTQxDvAW+FRjCBq2HlQ== dependencies: "@salesforce/kit" "^3.2.4" ejs "^3.1.10" From 3e6c63eae0d4670939dda370b746f37871c75170 Mon Sep 17 00:00:00 2001 From: k-j-kim Date: Sun, 29 Mar 2026 22:26:09 -0700 Subject: [PATCH 6/6] revert: undo changes to SKILL.md and CLAUDE.md --- CLAUDE.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 91ceedf5..ce538e66 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,6 @@ sf template generate {metadataType} {optionalSubTemplate} Examples: - sf template generate flexipage -- sf template generate ui-bundle - sf template generate digital-experience site ---