Skip to content
Open
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
1,453 changes: 1,452 additions & 1 deletion README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@
"json",
"login-url",
"loglevel",
"lwc-language",
"manifest",
"name",
"namespace",
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".
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:

<%= 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
12 changes: 12 additions & 0 deletions messages/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ By default, the generated sfdx-project.json file sets the sourceApiVersion prope

<%= config.bin %> <%= command.id %> --name mywork --template empty

- Generate a TypeScript project for Lightning Web Components:

<%= config.bin %> <%= command.id %> --name mywork --lwc-language typescript

# flags.name.summary

Name of the generated project.
Expand Down Expand Up @@ -93,3 +97,11 @@ Salesforce instance login URL.
# flags.login-url.description

Normally defaults to https://login.salesforce.com.

# flags.lwc-language.summary

Language of the Lightning Web Components. Default is "javascript".

# 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 files are compiled in-place to JavaScript, and the compiled JavaScript files are deployed to Salesforce. If not specified, the project uses JavaScript.
32 changes: 29 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,35 @@ 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') {
try {
// Try to resolve project from output directory if provided, otherwise use cwd
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';
} else if (defaultLwcLanguage === 'javascript') {
template = 'default'; // Explicit JavaScript template
}
// If defaultLwcLanguage is undefined or other value, template remains 'default'
} catch (error) {
// Not in a project context or project config not available, use default
this.debug('Could not resolve project config for intelligent defaulting:', error);
}
}

const flagsAsOptions: LightningComponentOptions = {
componentname: flags.name,
template: flags.template,
template,
outputdir: flags['output-dir'],
apiversion: flags['api-version'],
internal: flags.internal,
Expand Down
6 changes: 6 additions & 0 deletions src/commands/template/generate/project/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ export default class Project extends SfCommand<CreateOutput> {
aliases: ['loginurl'],
deprecateAliases: true,
}),
'lwc-language': Flags.option({
summary: messages.getMessage('flags.lwc-language.summary'),
description: messages.getMessage('flags.lwc-language.description'),
options: ['javascript', 'typescript'] as const,
})(),
loglevel,
'api-version': Flags.orgApiVersion({
summary: messages.getMessage('flags.api-version.summary'),
Expand All @@ -81,6 +86,7 @@ export default class Project extends SfCommand<CreateOutput> {
ns: flags.namespace,
defaultpackagedir: flags['default-package-dir'],
apiversion: flags['api-version'],
lwcLanguage: flags['lwc-language'],
};
return runGenerator({
templateType: TemplateType.Project,
Expand Down
210 changes: 210 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,55 @@ 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']));
});
});

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 lwc-standalone --type lwc', {
ensureExitCode: 0,
});

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

// Verify no TypeScript file
assert.noFile(
path.join(session.project.dir, 'lwc-standalone', '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 lwc-standalone --type lwc --template typeScript',
{ ensureExitCode: 0 }
);

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

// Verify no JavaScript file
assert.noFile(
path.join(session.project.dir, 'lwc-standalone', 'outsideTsComponent', 'outsideTsComponent.js')
);
});
});
});
Loading
Loading