Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
4 changes: 2 additions & 2 deletions command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
2 changes: 1 addition & 1 deletion messages/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
62 changes: 62 additions & 0 deletions messages/ui-bundle.generate.md
Original file line number Diff line number Diff line change
@@ -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 "<output-dir>/<name>".

**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.
61 changes: 0 additions & 61 deletions messages/webApplication.md

This file was deleted.

9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"dependencies": {
"@salesforce/core": "^8.27.1",
"@salesforce/sf-plugins-core": "^12",
"@salesforce/templates": "^66.6.2"
"@salesforce/templates": "^66.7.2"
},
"devDependencies": {
"@oclif/plugin-command-snapshot": "^5.3.13",
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@

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';

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<CreateOutput> {
export const UI_BUNDLES_DIR = 'uiBundles';

export default class UiBundleGenerate 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 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',
Expand Down Expand Up @@ -50,34 +51,34 @@ export default class WebAppGenerate extends SfCommand<CreateOutput> {

/**
* 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<string> {
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<CreateOutput> {
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,
const flagsAsOptions: UIBundleOptions = {
bundlename: flags.name,
template: flags.template,
masterlabel: flags.label,
outputdir: outputDir,
apiversion: flags['api-version'],
};

return runGenerator({
templateType: TemplateType.WebApplication,
templateType: TemplateType.UIBundle,
opts: flagsAsOptions,
ux: new Ux({ jsonEnabled: this.jsonEnabled() }),
templates: getCustomTemplates(this.configAggregator),
Expand Down
36 changes: 18 additions & 18 deletions test/commands/template/generate/project/index.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,48 +243,48 @@ describe('template generate project:', () => {
assert.file([path.join(session.project.dir, 'analytics1', 'eslint.config.js')]);
});

it('should create project with reactinternalapp template', () => {
const projectName = 'react-internal-app-test';
const alphanumericName = 'reactinternalapptest';
execCmd(`template generate project --projectname ${projectName} --template reactinternalapp`, {
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);
assert.file([path.join(projectDir, 'sfdx-project.json')]);
assert.fileContent(path.join(projectDir, 'sfdx-project.json'), 'sourceApiVersion');
const webappMetaPath = path.join(
const uiBundleMetaPath = path.join(
projectDir,
'force-app',
'main',
'default',
'webapplications',
'uiBundles',
alphanumericName,
`${alphanumericName}.webapplication-meta.xml`
`${alphanumericName}.uibundle-meta.xml`
);
assert.file([webappMetaPath]);
assert.fileContent(webappMetaPath, alphanumericName);
assert.file([uiBundleMetaPath]);
assert.fileContent(uiBundleMetaPath, alphanumericName);
});

it('should create project with reactexternalapp template', () => {
const projectName = 'react-b2x-test';
const alphanumericName = 'reactb2xtest';
execCmd(`template generate project --projectname ${projectName} --template reactexternalapp`, {
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);
assert.file([path.join(projectDir, 'sfdx-project.json')]);
assert.fileContent(path.join(projectDir, 'sfdx-project.json'), 'sourceApiVersion');
const webappMetaPath = path.join(
const uiBundleMetaPath = path.join(
projectDir,
'force-app',
'main',
'default',
'webapplications',
'uiBundles',
alphanumericName,
`${alphanumericName}.webapplication-meta.xml`
`${alphanumericName}.uibundle-meta.xml`
);
assert.file([webappMetaPath]);
assert.fileContent(webappMetaPath, alphanumericName);
assert.file([uiBundleMetaPath]);
assert.fileContent(uiBundleMetaPath, alphanumericName);
});

it('should create project with agent template', () => {
Expand Down
Loading
Loading