Skip to content
Merged
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -859,8 +859,8 @@ FLAG DESCRIPTIONS

Sets the default language for Lightning Web Components in this project. When set to 'typescript', generates
TypeScript configuration files (tsconfig.json, package.json with TypeScript dependencies, and TypeScript-aware
ESLint config). TypeScript projects compile locally to a dist/ folder for validation, but deploy raw .ts files to
Salesforce for server-side type stripping. Defaults to 'javascript'.
ESLint config). TypeScript files are compiled locally for validation, and the TypeScript (.ts) files are deployed
to Salesforce for server-side type stripping. Defaults to 'javascript'.
```

_See code: [src/commands/template/generate/project/index.ts](https://github.com/salesforcecli/plugin-templates/blob/56.14.0/src/commands/template/generate/project/index.ts)_
Expand Down
2 changes: 1 addition & 1 deletion messages/lightning.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ Template to use for file creation.

# flags.template.description

Supplied parameter values or default values are filled into a copy of the template.
Supplied parameter values or default values are filled into a copy of the template. For Lightning Web Components, if not specified, the CLI automatically selects the template based on the project's sfdx-project.json "defaultLwcLanguage" field: TypeScript template for "typescript", JavaScript template for "javascript".
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also list the behavior if there's no defaultLwcLanguage.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Supplied parameter values or default values are filled into a copy of the template. For Lightning Web Components, if not specified, the CLI automatically selects the template based on the project's sfdx-project.json "defaultLwcLanguage" field: TypeScript template for "typescript", JavaScript template for "javascript".
Supplied parameter values or default values are filled into a copy of the template. For Lightning Web Components, if this flag isn't specified, the CLI command automatically selects the template based on the "defaultLwcLanguage" field in the DX project's "sfdx-project.json" file.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to add the last part, because it's obvious :)

4 changes: 4 additions & 0 deletions messages/lightningCmp.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

<%= config.bin %> <%= command.id %> --name mycomponent --type lwc --output-dir force-app/main/default/lwc

- Generate a TypeScript Lightning web component:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Generate a TypeScript Lightning web component:
- Generate a TypeScript Lightning Web Component:


<%= config.bin %> <%= command.id %> --name mycomponent --type lwc --template typescript

# summary

Generate a bundle for an Aura component or a Lightning web component.
Expand Down
4 changes: 2 additions & 2 deletions messages/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ Normally defaults to https://login.salesforce.com.

# flags.lwc-language.summary

Default language for Lightning Web Components.
Language of the Lightning Web Components. Default is "javascript".
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the flag coded for a default value? I can't tell. If so, then remove the last sentence. If the flag isn't coded that way, can it be? It's nice to have stuff like default values, list of possible values, etc, be in the code itself which is then automatically displayed in the --help.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flag is not coded with a default. But that is how this is designed (for now)


# flags.lwc-language.description

Sets the default language for Lightning Web Components in this project. When set to 'typescript', generates TypeScript configuration files (tsconfig.json, package.json with TypeScript dependencies, and TypeScript-aware ESLint config). TypeScript projects compile locally to a dist/ folder for validation, but deploy raw .ts files to Salesforce for server-side type stripping. Defaults to 'javascript'.
Sets the default language for Lightning Web Components in this project. When set to `'typescript'`, generates TypeScript configuration files (tsconfig.json, package.json with TypeScript dependencies, and TypeScript-aware ESLint config). TypeScript files are compiled locally for validation, and the TypeScript (`.ts`) files are deployed to Salesforce for server-side type stripping. If not specified, the project uses JavaScript.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Sets the default language for Lightning Web Components in this project. When set to `'typescript'`, generates TypeScript configuration files (tsconfig.json, package.json with TypeScript dependencies, and TypeScript-aware ESLint config). TypeScript files are compiled locally for validation, and the TypeScript (`.ts`) files are deployed to Salesforce for server-side type stripping. If not specified, the project uses JavaScript.
When set to `'typescript'`, generates TypeScript configuration files (tsconfig.json, package.json with TypeScript dependencies, and TypeScript-aware ESLint config). TypeScript files are compiled locally for validation, and the TypeScript (`.ts`) files are deployed to Salesforce for server-side type stripping.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't repeat what's in the summary.

Also, I don't understand the last sentence. Can you reword? I didn't understand it enough to try it myself. For example, something like this (but it might be wrong, so fix as needed):

When you deploy the TypeScript-based Lightning Web Components, the TypeScript files are first compiled locally for validation and then the .ts files are deployed to your org for server-side type stripping.

28 changes: 25 additions & 3 deletions src/commands/template/generate/lightning/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import { Flags, loglevel, orgApiVersionFlagWithDeprecations, SfCommand, Ux } from '@salesforce/sf-plugins-core';
import { CreateOutput, LightningComponentOptions, TemplateType } from '@salesforce/templates';
import { Messages } from '@salesforce/core';
import { Messages, SfProject } from '@salesforce/core';
import { getCustomTemplates, runGenerator } from '../../../../utils/templateCommand.js';
import { internalFlag, outputDirFlagLightning } from '../../../../utils/flags.js';
const BUNDLE_TYPE = 'Component';
Expand Down Expand Up @@ -39,7 +39,7 @@ export default class LightningComponent extends SfCommand<CreateOutput> {
default: 'default',
// Note: keep this list here and LightningComponentOptions#template in-sync with the
// templates/lightningcomponents/[aura|lwc]/* folders
options: ['default', 'analyticsDashboard', 'analyticsDashboardWithStep'] as const,
options: ['default', 'analyticsDashboard', 'analyticsDashboardWithStep', 'typescript'] as const,
})(),
'output-dir': outputDirFlagLightning,
'api-version': orgApiVersionFlagWithDeprecations,
Expand All @@ -54,9 +54,31 @@ export default class LightningComponent extends SfCommand<CreateOutput> {

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

// Determine if user explicitly set the template flag
const userExplicitlySetTemplate = this.argv.includes('--template') || this.argv.includes('-t');
let template = flags.template;

// If template not explicitly provided and generating LWC, check project preference
if (!userExplicitlySetTemplate && flags.type === 'lwc') {
Copy link
Copy Markdown
Contributor

@iowillhoit iowillhoit Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would much rather throw an error message that try to modify these flag values.

pseudocode:

  • flags.type === 'lwc'
  • Resolve project and get defaultLwcLanguage from sfdx-project.json
  • Validate
    - if (defaultLwcLanguage === 'typescript' && flags.template !== 'typeScript') throw error: Language in 'typeScript' detected in sfdx-project.json. This requires the "--template typescript" flag

try {
const projectPath = flags['output-dir'] || process.cwd();
const project = await SfProject.resolve(projectPath);
const projectJson = await project.resolveProjectConfig();
const defaultLwcLanguage = projectJson.defaultLwcLanguage as string | undefined;

if (defaultLwcLanguage === 'typescript') {
template = 'typescript';
}
} catch (error) {
this.debug('Could not resolve project config for intelligent defaulting:', error);
}
}

const flagsAsOptions: LightningComponentOptions = {
componentname: flags.name,
template: flags.template,
// Temp re-mapping to allow lowercase typescript flag
template: template === 'typescript' ? 'typeScript' : template,
outputdir: flags['output-dir'],
apiversion: flags['api-version'],
internal: flags.internal,
Expand Down
201 changes: 201 additions & 0 deletions test/commands/template/generate/lightning/component.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,166 @@ describe('template generate lightning component:', () => {
});
});

describe('TypeScript Lightning web component generation', () => {
it('should create TypeScript LWC with explicit template flag', () => {
execCmd(
'template generate lightning component --componentname tsComponent --outputdir lwc --type lwc --template typescript',
{ ensureExitCode: 0 }
);

// Verify TypeScript files exist
assert.file(path.join(session.project.dir, 'lwc', 'tsComponent', 'tsComponent.ts'));
assert.file(path.join(session.project.dir, 'lwc', 'tsComponent', 'tsComponent.html'));
assert.file(path.join(session.project.dir, 'lwc', 'tsComponent', '__tests__', 'tsComponent.test.ts'));
assert.file(path.join(session.project.dir, 'lwc', 'tsComponent', 'tsComponent.js-meta.xml'));

// Verify no .js file in component folder
assert.noFile(path.join(session.project.dir, 'lwc', 'tsComponent', 'tsComponent.js'));

// Verify TypeScript content
assert.fileContent(
path.join(session.project.dir, 'lwc', 'tsComponent', 'tsComponent.ts'),
'export default class TsComponent extends LightningElement {}'
);

// Verify TypeScript test content
assert.fileContent(
path.join(session.project.dir, 'lwc', 'tsComponent', '__tests__', 'tsComponent.test.ts'),
"import TsComponent from 'c/tsComponent';"
);
});

it('should automatically use TypeScript template in TypeScript project', () => {
// Create a TypeScript project
execCmd('template generate project --name tsProject --lwc-language typescript', {
ensureExitCode: 0,
});

// Generate component without specifying template
execCmd(
'template generate lightning component --componentname autoTs --outputdir tsProject/force-app/main/default/lwc --type lwc',
{ ensureExitCode: 0 }
);

// Verify TypeScript files were created
assert.file(
path.join(session.project.dir, 'tsProject', 'force-app', 'main', 'default', 'lwc', 'autoTs', 'autoTs.ts')
);
assert.file(
path.join(
session.project.dir,
'tsProject',
'force-app',
'main',
'default',
'lwc',
'autoTs',
'__tests__',
'autoTs.test.ts'
)
);

// Verify no .js file
assert.noFile(
path.join(session.project.dir, 'tsProject', 'force-app', 'main', 'default', 'lwc', 'autoTs', 'autoTs.js')
);
});

it('should use JavaScript template in JavaScript project', () => {
// Create a JavaScript project
execCmd('template generate project --name jsProject --lwc-language javascript', {
ensureExitCode: 0,
});

// Generate component without specifying template
execCmd(
'template generate lightning component --componentname autoJs --outputdir jsProject/force-app/main/default/lwc --type lwc',
{ ensureExitCode: 0 }
);

// Verify JavaScript files were created
assert.file(
path.join(session.project.dir, 'jsProject', 'force-app', 'main', 'default', 'lwc', 'autoJs', 'autoJs.js')
);
assert.file(
path.join(
session.project.dir,
'jsProject',
'force-app',
'main',
'default',
'lwc',
'autoJs',
'__tests__',
'autoJs.test.js'
)
);

// Verify no .ts file
assert.noFile(
path.join(session.project.dir, 'jsProject', 'force-app', 'main', 'default', 'lwc', 'autoJs', 'autoJs.ts')
);
});

it('should allow explicit template override in TypeScript project', () => {
// Create a TypeScript project
execCmd('template generate project --name tsOverrideProject --lwc-language typescript', {
ensureExitCode: 0,
});

// Generate JavaScript component explicitly in TypeScript project
execCmd(
'template generate lightning component --componentname jsInTs --outputdir tsOverrideProject/force-app/main/default/lwc --type lwc --template default',
{ ensureExitCode: 0 }
);

// Verify JavaScript files were created (override worked)
assert.file(
path.join(
session.project.dir,
'tsOverrideProject',
'force-app',
'main',
'default',
'lwc',
'jsInTs',
'jsInTs.js'
)
);
assert.noFile(
path.join(
session.project.dir,
'tsOverrideProject',
'force-app',
'main',
'default',
'lwc',
'jsInTs',
'jsInTs.ts'
)
);
});

it('should create TypeScript component with proper class naming', () => {
execCmd(
'template generate lightning component --componentname mySpecialComponent --outputdir lwc --type lwc --template typescript',
{ ensureExitCode: 0 }
);

// Verify PascalCase class name
assert.fileContent(
path.join(session.project.dir, 'lwc', 'mySpecialComponent', 'mySpecialComponent.ts'),
'export default class MySpecialComponent extends LightningElement {}'
);

// Verify test imports use camelCase
assert.fileContent(
path.join(session.project.dir, 'lwc', 'mySpecialComponent', '__tests__', 'mySpecialComponent.test.ts'),
"import MySpecialComponent from 'c/mySpecialComponent';"
);
});
});

describe('lightning component failures', () => {
it('should throw missing component name error', () => {
const stderr = execCmd('template generate lightning component --outputdir aura').shellOutput.stderr;
Expand Down Expand Up @@ -196,5 +356,46 @@ describe('template generate lightning component:', () => {
messages.getMessage('MissingLightningComponentTemplate', ['analyticsDashboard', 'aura'])
);
});

it('should throw error when using typescript template with aura type', () => {
const stderr = execCmd(
'template generate lightning component --outputdir aura --componentname foo --type aura --template typescript'
).shellOutput.stderr;
expect(stderr).to.contain(messages.getMessage('MissingLightningComponentTemplate', ['typeScript', 'aura']));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expect(stderr).to.contain(messages.getMessage('MissingLightningComponentTemplate', ['typeScript', 'aura']));
expect(stderr).to.contain(messages.getMessage('MissingLightningComponentTemplate', ['typescript', 'aura']));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good eyes. This is duplicating an error that comes from the library. It needs to stay as typeScript for now unfortunately.

Copy link
Copy Markdown
Contributor

@soridalac soridalac Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it!! LGTM, thanks Eric for making the changes❤️

});
});

describe('Component generation outside project context', () => {
it('should create JavaScript component outside project with no template flag', () => {
// Generate component in a directory without sfdx-project.json
execCmd(
'template generate lightning component --componentname outsideComponent --outputdir standalone/lwc --type lwc',
{
ensureExitCode: 0,
}
);

// Verify JavaScript files were created (default when no project context)
assert.file(path.join(session.project.dir, 'standalone', 'lwc', 'outsideComponent', 'outsideComponent.js'));
assert.file(path.join(session.project.dir, 'standalone', 'lwc', 'outsideComponent', 'outsideComponent.html'));

// Verify no TypeScript file
assert.noFile(path.join(session.project.dir, 'standalone', 'lwc', 'outsideComponent', 'outsideComponent.ts'));
});

it('should create TypeScript component outside project with explicit template flag', () => {
// Generate TypeScript component outside project
execCmd(
'template generate lightning component --componentname outsideTsComponent --outputdir standalone/lwc --type lwc --template typescript',
{ ensureExitCode: 0 }
);

// Verify TypeScript files were created
assert.file(path.join(session.project.dir, 'standalone', 'lwc', 'outsideTsComponent', 'outsideTsComponent.ts'));
assert.file(path.join(session.project.dir, 'standalone', 'lwc', 'outsideTsComponent', 'outsideTsComponent.html'));

// Verify no JavaScript file
assert.noFile(path.join(session.project.dir, 'standalone', 'lwc', 'outsideTsComponent', 'outsideTsComponent.js'));
});
});
});
47 changes: 46 additions & 1 deletion test/commands/template/generate/project/index.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ describe('template generate project:', () => {
assert.fileContent(tsconfigPath, '**/__tests__/**');
});

it('should verify .forceignore excludes dist/ folder', () => {
it('should verify .forceignore excludes TypeScript configuration files', () => {
execCmd('template generate project --projectname forceignore-test --lwc-language typescript', {
ensureExitCode: 0,
});
Expand Down Expand Up @@ -454,6 +454,45 @@ describe('template generate project:', () => {
const sfdxContent = fs.readFileSync(sfdxProjectPath, 'utf8');
expect(sfdxContent).to.not.contain('defaultLwcLanguage');
});

it('should create TypeScript project with empty template including full toolchain', () => {
execCmd('template generate project --projectname empty-ts --template empty --lwc-language typescript', {
ensureExitCode: 0,
});

const projectDir = path.join(session.project.dir, 'empty-ts');

// Verify TypeScript-specific files exist
assert.file([path.join(projectDir, 'tsconfig.json')]);
assert.file([path.join(projectDir, 'package.json')]);
assert.file([path.join(projectDir, 'eslint.config.js')]);
assert.file([path.join(projectDir, '.forceignore')]);
assert.file([path.join(projectDir, '.gitignore')]);

// Verify Husky hooks exist for empty template
for (const file of huskyhookarray) {
assert.file([path.join(projectDir, '.husky', file)]);
}

// Verify VSCode config files exist
for (const file of vscodearray) {
assert.file([path.join(projectDir, '.vscode', `${file}.json`)]);
}

// Verify sfdx-project.json includes defaultLwcLanguage
const sfdxProjectPath = path.join(projectDir, 'sfdx-project.json');
assert.fileContent(sfdxProjectPath, '"defaultLwcLanguage": "typescript"');

// Verify package.json has TypeScript dependencies
const packageJsonPath = path.join(projectDir, 'package.json');
assert.fileContent(packageJsonPath, '"typescript"');
assert.fileContent(packageJsonPath, '"build": "tsc"');

// Verify empty template folders exist
for (const folder of emptyfolderarray) {
assert(fs.existsSync(path.join(projectDir, 'force-app', 'main', 'default', folder)));
}
});
});

describe('project creation failures', () => {
Expand All @@ -466,5 +505,11 @@ describe('template generate project:', () => {
const stderr = execCmd('template generate project --projectname foo --template foo').shellOutput.stderr;
expect(stderr).to.contain(messages.getMessage('InvalidTemplate'));
});

it('should throw error for invalid lwc-language value', () => {
const stderr = execCmd('template generate project --projectname foo --lwc-language python').shellOutput.stderr;
expect(stderr).to.contain('Expected --lwc-language');
expect(stderr).to.match(/(javascript|typescript)/);
});
});
});
Loading