-
Notifications
You must be signed in to change notification settings - Fork 14
W-21523405 feat: Add TypeScript component generation with intelligent defaulting #877
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0ff51ca
b0c304a
a89b175
01deb89
c9e1dc5
5dd1c71
1d76655
08481d9
6f0d2ed
19144cf
7ddc316
258bafd
7edf8b9
055d291
da86f64
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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". | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to add the last part, because it's obvious :) |
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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: | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| <%= config.bin %> <%= command.id %> --name mycomponent --type lwc --template typescript | ||||||
|
|
||||||
| # summary | ||||||
|
|
||||||
| Generate a bundle for an Aura component or a Lightning web component. | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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". | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'; | ||
|
|
@@ -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, | ||
|
|
@@ -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') { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
|
||
| 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, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||
|
|
@@ -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'])); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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')); | ||||||
| }); | ||||||
| }); | ||||||
| }); | ||||||
There was a problem hiding this comment.
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.