Skip to content

Commit 3b4fb42

Browse files
authored
Merge pull request #877 from salesforcecli/feat/typescript-component-generation
W-21523405 feat: Add TypeScript component generation with intelligent defaulting
2 parents 45d39af + da86f64 commit 3b4fb42

7 files changed

Lines changed: 281 additions & 9 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -859,8 +859,8 @@ FLAG DESCRIPTIONS
859859
860860
Sets the default language for Lightning Web Components in this project. When set to 'typescript', generates
861861
TypeScript configuration files (tsconfig.json, package.json with TypeScript dependencies, and TypeScript-aware
862-
ESLint config). TypeScript projects compile locally to a dist/ folder for validation, but deploy raw .ts files to
863-
Salesforce for server-side type stripping. Defaults to 'javascript'.
862+
ESLint config). TypeScript files are compiled locally for validation, and the TypeScript (.ts) files are deployed
863+
to Salesforce for server-side type stripping. Defaults to 'javascript'.
864864
```
865865

866866
_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)_

messages/lightning.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ Template to use for file creation.
2424

2525
# flags.template.description
2626

27-
Supplied parameter values or default values are filled into a copy of the template.
27+
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".

messages/lightningCmp.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

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

19+
- Generate a TypeScript Lightning web component:
20+
21+
<%= config.bin %> <%= command.id %> --name mycomponent --type lwc --template typescript
22+
1923
# summary
2024

2125
Generate a bundle for an Aura component or a Lightning web component.

messages/project.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ Normally defaults to https://login.salesforce.com.
100100

101101
# flags.lwc-language.summary
102102

103-
Default language for Lightning Web Components.
103+
Language of the Lightning Web Components. Default is "javascript".
104104

105105
# flags.lwc-language.description
106106

107-
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'.
107+
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.

src/commands/template/generate/lightning/component.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

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

5555
public async run(): Promise<CreateOutput> {
5656
const { flags } = await this.parse(LightningComponent);
57+
58+
// Determine if user explicitly set the template flag
59+
const userExplicitlySetTemplate = this.argv.includes('--template') || this.argv.includes('-t');
60+
let template = flags.template;
61+
62+
// If template not explicitly provided and generating LWC, check project preference
63+
if (!userExplicitlySetTemplate && flags.type === 'lwc') {
64+
try {
65+
const projectPath = flags['output-dir'] || process.cwd();
66+
const project = await SfProject.resolve(projectPath);
67+
const projectJson = await project.resolveProjectConfig();
68+
const defaultLwcLanguage = projectJson.defaultLwcLanguage as string | undefined;
69+
70+
if (defaultLwcLanguage === 'typescript') {
71+
template = 'typescript';
72+
}
73+
} catch (error) {
74+
this.debug('Could not resolve project config for intelligent defaulting:', error);
75+
}
76+
}
77+
5778
const flagsAsOptions: LightningComponentOptions = {
5879
componentname: flags.name,
59-
template: flags.template,
80+
// Temp re-mapping to allow lowercase typescript flag
81+
template: template === 'typescript' ? 'typeScript' : template,
6082
outputdir: flags['output-dir'],
6183
apiversion: flags['api-version'],
6284
internal: flags.internal,

test/commands/template/generate/lightning/component.nut.ts

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,166 @@ describe('template generate lightning component:', () => {
165165
});
166166
});
167167

168+
describe('TypeScript Lightning web component generation', () => {
169+
it('should create TypeScript LWC with explicit template flag', () => {
170+
execCmd(
171+
'template generate lightning component --componentname tsComponent --outputdir lwc --type lwc --template typescript',
172+
{ ensureExitCode: 0 }
173+
);
174+
175+
// Verify TypeScript files exist
176+
assert.file(path.join(session.project.dir, 'lwc', 'tsComponent', 'tsComponent.ts'));
177+
assert.file(path.join(session.project.dir, 'lwc', 'tsComponent', 'tsComponent.html'));
178+
assert.file(path.join(session.project.dir, 'lwc', 'tsComponent', '__tests__', 'tsComponent.test.ts'));
179+
assert.file(path.join(session.project.dir, 'lwc', 'tsComponent', 'tsComponent.js-meta.xml'));
180+
181+
// Verify no .js file in component folder
182+
assert.noFile(path.join(session.project.dir, 'lwc', 'tsComponent', 'tsComponent.js'));
183+
184+
// Verify TypeScript content
185+
assert.fileContent(
186+
path.join(session.project.dir, 'lwc', 'tsComponent', 'tsComponent.ts'),
187+
'export default class TsComponent extends LightningElement {}'
188+
);
189+
190+
// Verify TypeScript test content
191+
assert.fileContent(
192+
path.join(session.project.dir, 'lwc', 'tsComponent', '__tests__', 'tsComponent.test.ts'),
193+
"import TsComponent from 'c/tsComponent';"
194+
);
195+
});
196+
197+
it('should automatically use TypeScript template in TypeScript project', () => {
198+
// Create a TypeScript project
199+
execCmd('template generate project --name tsProject --lwc-language typescript', {
200+
ensureExitCode: 0,
201+
});
202+
203+
// Generate component without specifying template
204+
execCmd(
205+
'template generate lightning component --componentname autoTs --outputdir tsProject/force-app/main/default/lwc --type lwc',
206+
{ ensureExitCode: 0 }
207+
);
208+
209+
// Verify TypeScript files were created
210+
assert.file(
211+
path.join(session.project.dir, 'tsProject', 'force-app', 'main', 'default', 'lwc', 'autoTs', 'autoTs.ts')
212+
);
213+
assert.file(
214+
path.join(
215+
session.project.dir,
216+
'tsProject',
217+
'force-app',
218+
'main',
219+
'default',
220+
'lwc',
221+
'autoTs',
222+
'__tests__',
223+
'autoTs.test.ts'
224+
)
225+
);
226+
227+
// Verify no .js file
228+
assert.noFile(
229+
path.join(session.project.dir, 'tsProject', 'force-app', 'main', 'default', 'lwc', 'autoTs', 'autoTs.js')
230+
);
231+
});
232+
233+
it('should use JavaScript template in JavaScript project', () => {
234+
// Create a JavaScript project
235+
execCmd('template generate project --name jsProject --lwc-language javascript', {
236+
ensureExitCode: 0,
237+
});
238+
239+
// Generate component without specifying template
240+
execCmd(
241+
'template generate lightning component --componentname autoJs --outputdir jsProject/force-app/main/default/lwc --type lwc',
242+
{ ensureExitCode: 0 }
243+
);
244+
245+
// Verify JavaScript files were created
246+
assert.file(
247+
path.join(session.project.dir, 'jsProject', 'force-app', 'main', 'default', 'lwc', 'autoJs', 'autoJs.js')
248+
);
249+
assert.file(
250+
path.join(
251+
session.project.dir,
252+
'jsProject',
253+
'force-app',
254+
'main',
255+
'default',
256+
'lwc',
257+
'autoJs',
258+
'__tests__',
259+
'autoJs.test.js'
260+
)
261+
);
262+
263+
// Verify no .ts file
264+
assert.noFile(
265+
path.join(session.project.dir, 'jsProject', 'force-app', 'main', 'default', 'lwc', 'autoJs', 'autoJs.ts')
266+
);
267+
});
268+
269+
it('should allow explicit template override in TypeScript project', () => {
270+
// Create a TypeScript project
271+
execCmd('template generate project --name tsOverrideProject --lwc-language typescript', {
272+
ensureExitCode: 0,
273+
});
274+
275+
// Generate JavaScript component explicitly in TypeScript project
276+
execCmd(
277+
'template generate lightning component --componentname jsInTs --outputdir tsOverrideProject/force-app/main/default/lwc --type lwc --template default',
278+
{ ensureExitCode: 0 }
279+
);
280+
281+
// Verify JavaScript files were created (override worked)
282+
assert.file(
283+
path.join(
284+
session.project.dir,
285+
'tsOverrideProject',
286+
'force-app',
287+
'main',
288+
'default',
289+
'lwc',
290+
'jsInTs',
291+
'jsInTs.js'
292+
)
293+
);
294+
assert.noFile(
295+
path.join(
296+
session.project.dir,
297+
'tsOverrideProject',
298+
'force-app',
299+
'main',
300+
'default',
301+
'lwc',
302+
'jsInTs',
303+
'jsInTs.ts'
304+
)
305+
);
306+
});
307+
308+
it('should create TypeScript component with proper class naming', () => {
309+
execCmd(
310+
'template generate lightning component --componentname mySpecialComponent --outputdir lwc --type lwc --template typescript',
311+
{ ensureExitCode: 0 }
312+
);
313+
314+
// Verify PascalCase class name
315+
assert.fileContent(
316+
path.join(session.project.dir, 'lwc', 'mySpecialComponent', 'mySpecialComponent.ts'),
317+
'export default class MySpecialComponent extends LightningElement {}'
318+
);
319+
320+
// Verify test imports use camelCase
321+
assert.fileContent(
322+
path.join(session.project.dir, 'lwc', 'mySpecialComponent', '__tests__', 'mySpecialComponent.test.ts'),
323+
"import MySpecialComponent from 'c/mySpecialComponent';"
324+
);
325+
});
326+
});
327+
168328
describe('lightning component failures', () => {
169329
it('should throw missing component name error', () => {
170330
const stderr = execCmd('template generate lightning component --outputdir aura').shellOutput.stderr;
@@ -196,5 +356,46 @@ describe('template generate lightning component:', () => {
196356
messages.getMessage('MissingLightningComponentTemplate', ['analyticsDashboard', 'aura'])
197357
);
198358
});
359+
360+
it('should throw error when using typescript template with aura type', () => {
361+
const stderr = execCmd(
362+
'template generate lightning component --outputdir aura --componentname foo --type aura --template typescript'
363+
).shellOutput.stderr;
364+
expect(stderr).to.contain(messages.getMessage('MissingLightningComponentTemplate', ['typeScript', 'aura']));
365+
});
366+
});
367+
368+
describe('Component generation outside project context', () => {
369+
it('should create JavaScript component outside project with no template flag', () => {
370+
// Generate component in a directory without sfdx-project.json
371+
execCmd(
372+
'template generate lightning component --componentname outsideComponent --outputdir standalone/lwc --type lwc',
373+
{
374+
ensureExitCode: 0,
375+
}
376+
);
377+
378+
// Verify JavaScript files were created (default when no project context)
379+
assert.file(path.join(session.project.dir, 'standalone', 'lwc', 'outsideComponent', 'outsideComponent.js'));
380+
assert.file(path.join(session.project.dir, 'standalone', 'lwc', 'outsideComponent', 'outsideComponent.html'));
381+
382+
// Verify no TypeScript file
383+
assert.noFile(path.join(session.project.dir, 'standalone', 'lwc', 'outsideComponent', 'outsideComponent.ts'));
384+
});
385+
386+
it('should create TypeScript component outside project with explicit template flag', () => {
387+
// Generate TypeScript component outside project
388+
execCmd(
389+
'template generate lightning component --componentname outsideTsComponent --outputdir standalone/lwc --type lwc --template typescript',
390+
{ ensureExitCode: 0 }
391+
);
392+
393+
// Verify TypeScript files were created
394+
assert.file(path.join(session.project.dir, 'standalone', 'lwc', 'outsideTsComponent', 'outsideTsComponent.ts'));
395+
assert.file(path.join(session.project.dir, 'standalone', 'lwc', 'outsideTsComponent', 'outsideTsComponent.html'));
396+
397+
// Verify no JavaScript file
398+
assert.noFile(path.join(session.project.dir, 'standalone', 'lwc', 'outsideTsComponent', 'outsideTsComponent.js'));
399+
});
199400
});
200401
});

test/commands/template/generate/project/index.nut.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ describe('template generate project:', () => {
368368
assert.fileContent(tsconfigPath, '**/__tests__/**');
369369
});
370370

371-
it('should verify .forceignore excludes dist/ folder', () => {
371+
it('should verify .forceignore excludes TypeScript configuration files', () => {
372372
execCmd('template generate project --projectname forceignore-test --lwc-language typescript', {
373373
ensureExitCode: 0,
374374
});
@@ -454,6 +454,45 @@ describe('template generate project:', () => {
454454
const sfdxContent = fs.readFileSync(sfdxProjectPath, 'utf8');
455455
expect(sfdxContent).to.not.contain('defaultLwcLanguage');
456456
});
457+
458+
it('should create TypeScript project with empty template including full toolchain', () => {
459+
execCmd('template generate project --projectname empty-ts --template empty --lwc-language typescript', {
460+
ensureExitCode: 0,
461+
});
462+
463+
const projectDir = path.join(session.project.dir, 'empty-ts');
464+
465+
// Verify TypeScript-specific files exist
466+
assert.file([path.join(projectDir, 'tsconfig.json')]);
467+
assert.file([path.join(projectDir, 'package.json')]);
468+
assert.file([path.join(projectDir, 'eslint.config.js')]);
469+
assert.file([path.join(projectDir, '.forceignore')]);
470+
assert.file([path.join(projectDir, '.gitignore')]);
471+
472+
// Verify Husky hooks exist for empty template
473+
for (const file of huskyhookarray) {
474+
assert.file([path.join(projectDir, '.husky', file)]);
475+
}
476+
477+
// Verify VSCode config files exist
478+
for (const file of vscodearray) {
479+
assert.file([path.join(projectDir, '.vscode', `${file}.json`)]);
480+
}
481+
482+
// Verify sfdx-project.json includes defaultLwcLanguage
483+
const sfdxProjectPath = path.join(projectDir, 'sfdx-project.json');
484+
assert.fileContent(sfdxProjectPath, '"defaultLwcLanguage": "typescript"');
485+
486+
// Verify package.json has TypeScript dependencies
487+
const packageJsonPath = path.join(projectDir, 'package.json');
488+
assert.fileContent(packageJsonPath, '"typescript"');
489+
assert.fileContent(packageJsonPath, '"build": "tsc"');
490+
491+
// Verify empty template folders exist
492+
for (const folder of emptyfolderarray) {
493+
assert(fs.existsSync(path.join(projectDir, 'force-app', 'main', 'default', folder)));
494+
}
495+
});
457496
});
458497

459498
describe('project creation failures', () => {
@@ -466,5 +505,11 @@ describe('template generate project:', () => {
466505
const stderr = execCmd('template generate project --projectname foo --template foo').shellOutput.stderr;
467506
expect(stderr).to.contain(messages.getMessage('InvalidTemplate'));
468507
});
508+
509+
it('should throw error for invalid lwc-language value', () => {
510+
const stderr = execCmd('template generate project --projectname foo --lwc-language python').shellOutput.stderr;
511+
expect(stderr).to.contain('Expected --lwc-language');
512+
expect(stderr).to.match(/(javascript|typescript)/);
513+
});
469514
});
470515
});

0 commit comments

Comments
 (0)