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
16 changes: 12 additions & 4 deletions .claude/skills/add-template-generator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`, `multi-framework`, `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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -129,9 +135,9 @@ import { runGenerator } from '../../utils/templateCommand.js';

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

// Add any pre-processing or validation here

return runGenerator({
templateType: TemplateType.{YourMetadataType},
opts: flags,
Expand All @@ -143,6 +149,7 @@ public async run(): Promise<CreateOutput> {
## 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
Expand All @@ -164,6 +171,7 @@ Test your command:

```bash
sf template generate {metadataType} --name TestExample --output-dir ./test-output
# e.g. sf template generate multi-framework --name MyApp --output-dir ./test-output
```

Verify the generated files are correct.
Expand Down Expand Up @@ -212,4 +220,4 @@ Before opening PR ensure:
- flags validated
- messages documented
- NUTs pass
- topics updated
- topics updated
11 changes: 7 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Command structure:
src/commands/template/generate/{metadataType}/

Files:

- index.ts → top-level generator
- {subTemplate}.ts → nested generator

Expand All @@ -38,7 +39,9 @@ Naming pattern:
sf template generate {metadataType} {optionalSubTemplate}

Examples:

- sf template generate flexipage
- sf template generate multi-framework
- sf template generate digital-experience site

---
Expand Down Expand Up @@ -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.
Use official Salesforce CLI docs when needed.
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": ["webui:generate"],
"command": "template:generate:webui",
"flagAliases": [],
"flagChars": ["d", "l", "n", "t"],
"flags": ["api-version", "flags-dir", "json", "label", "name", "output-dir", "template"],
Expand Down
17 changes: 9 additions & 8 deletions messages/webApplication.md → messages/webui.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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.
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 "webui" 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

Expand All @@ -16,9 +16,9 @@ Generates a web application in the specified directory or the current working di

<%= config.bin %> <%= command.id %> --name MyReactApp --template reactbasic

- Generate the web application in the "force-app/main/default/webapplications" directory:
- Generate the web application in the "force-app/main/default/webui" directory:

<%= config.bin %> <%= command.id %> --name MyWebApp --output-dir force-app/main/default/webapplications
<%= config.bin %> <%= command.id %> --name MyWebApp --output-dir force-app/main/default/webui

# flags.name.summary

Expand Down Expand Up @@ -50,12 +50,13 @@ Directory for saving the created files.

# flags.output-dir.description

The location can be an absolute path or relative to the current working directory.
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 "<output-dir>/<webappname>".
**Important:** The generator automatically ensures the output directory ends with "webui". If your specified path doesn't end with "webui", it's automatically appended. The web application is created at "<output-dir>/<webappname>".

**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.
- "--output-dir force-app/main/default" → Creates a web application at "force-app/main/default/webui/MyWebApp/"
- "--output-dir force-app/main/default/webui" → Creates a web application at "force-app/main/default/webui/MyWebApp/" (no change)

If not specified, the command reads your sfdx-project.json and defaults to "webui" directory within your default package directory. When running outside a Salesforce DX project, defaults to the current directory.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@
"visualforce": {
"description": "Create a visualforce page or component."
},
"webapp": {
"description": "Create a web application."
"multi-framework": {
"description": "Create a multi-framework web application."
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ 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', 'webui');

export default class WebAppGenerate extends SfCommand<CreateOutput> {
export default class WebUIGenerate 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 = ['webui:generate'];
public static readonly flags = {
name: Flags.string({
char: 'n',
Expand Down Expand Up @@ -50,23 +49,23 @@ 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 webui 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', 'webui');
} catch {
return '.';
}
}

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

const outputDir = flags['output-dir'] ?? (await WebAppGenerate.getDefaultOutputDir());
const outputDir = flags['output-dir'] ?? (await WebUIGenerate.getDefaultOutputDir());

const flagsAsOptions: WebApplicationOptions = {
webappname: flags.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ describe('template generate web application:', () => {
});

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 });
it('should create webapp using default template in webui directory', () => {
const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'webui');
execCmd(`template generate webui --name MyWebApp --output-dir "${outputDir}"`, { ensureExitCode: 0 });
assert.file([
path.join(outputDir, 'MyWebApp', 'MyWebApp.webapplication-meta.xml'),
path.join(outputDir, 'MyWebApp', 'src', 'index.html'),
Expand All @@ -37,9 +37,9 @@ describe('template generate web application:', () => {
);
});

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 });
it('should default to project webui directory when --output-dir is omitted', () => {
const expectedOutputDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'webui');
execCmd('template generate webui --name DefaultDirApp', { ensureExitCode: 0 });
assert.file([
path.join(expectedOutputDir, 'DefaultDirApp', 'DefaultDirApp.webapplication-meta.xml'),
path.join(expectedOutputDir, 'DefaultDirApp', 'src', 'index.html'),
Expand All @@ -48,8 +48,8 @@ describe('template generate web application:', () => {
});

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}"`, {
const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'webui');
execCmd(`template generate webui --name TestApp --label "Custom Label" --output-dir "${outputDir}"`, {
ensureExitCode: 0,
});
assert.file([
Expand All @@ -62,8 +62,8 @@ describe('template generate web application:', () => {

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}"`, {
const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'webui');
execCmd(`template generate webui --name MyReactApp --template reactbasic --output-dir "${outputDir}"`, {
ensureExitCode: 0,
});
assert.file([
Expand All @@ -78,38 +78,38 @@ describe('template generate web application:', () => {

describe('Check that all invalid name errors are thrown', () => {
it('should throw a missing name error', () => {
const stderr = execCmd('template generate webapp').shellOutput.stderr;
const stderr = execCmd('template generate webui').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;
const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'webui');
const stderr = execCmd(`template generate webui --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;
const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'webui');
const stderr = execCmd(`template generate webui --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;
const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'webui');
const stderr = execCmd(`template generate webui --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;
const outputDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'webui');
const stderr = execCmd(`template generate webui --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', () => {
it('should auto-append webui folder when output dir does not end with webui', () => {
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 });
const expectedOutputDir = path.join(outputDir, 'webui');
execCmd(`template generate webui --name TestApp --output-dir "${outputDir}"`, { ensureExitCode: 0 });
assert.file([
path.join(expectedOutputDir, 'TestApp', 'TestApp.webapplication-meta.xml'),
path.join(expectedOutputDir, 'TestApp', 'src', 'index.html'),
Expand Down
Loading