feat: add usage examples to CLI help output#262
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an examples framework to the SPFx CLI help output by extending the shared action base, and provides initial examples for spfx create. The PR also includes additional behavior/documentation changes to list-templates and create parameter requirements that go beyond the stated PR scope.
Changes:
- Add
getExamples()+renderHelpText()override inSPFxActionBaseto append anEXAMPLESsection to action help output. - Add
CreateActionexamples and make--library-nameoptional (with a default). - Update
ListTemplatesActionhelp text and add--spfx-versionfiltering behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| apps/spfx-cli/src/cli/actions/SPFxActionBase.ts | Adds the base help-text rendering extension point for appending formatted usage examples. |
| apps/spfx-cli/src/cli/actions/CreateAction.ts | Makes --library-name optional, defaults it during context generation, and supplies initial usage examples. |
| apps/spfx-cli/src/cli/actions/ListTemplatesAction.ts | Updates command documentation and filters displayed templates by --spfx-version. |
Comments suppressed due to low confidence (1)
apps/spfx-cli/src/cli/actions/CreateAction.ts:212
buildBuiltInContext()requireslibraryName: string, butthis._libraryNameParameter.value || rawSolutionNamecan evaluate toundefinedwhen the user omits both--library-nameand--solution-name. This will either fail type-checking or produce runtime issues in template rendering. Since the parameter description says it defaults to the solution name, use the already-computedsolutionName(always defined) and trim the CLI value.
const builtInContext: ISPFxBuiltInContext = buildBuiltInContext(
{
componentName,
libraryName: this._libraryNameParameter.value || rawSolutionName,
spfxVersion: template.spfxVersion,
solutionName: rawSolutionName || undefined,
componentAlias: this._componentAliasParameter.value?.trim() || undefined,
componentDescription: this._componentDescriptionParameter.value?.trim() || undefined
},
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public renderHelpText(): string { | ||
| const base: string = super.renderHelpText(); | ||
| const examples: string[] = this.getExamples(); | ||
| if (examples.length === 0) { | ||
| return base; | ||
| } | ||
|
|
||
| // Append examples after the last line of base help | ||
| const lines: string[] = base.split('\n'); | ||
| lines.push(''); | ||
| lines.push('EXAMPLES'); | ||
| for (const example of examples) { | ||
| lines.push(''); | ||
| lines.push(` ${example}`); | ||
| } | ||
| lines.push(''); | ||
| return lines.join('\n'); | ||
| } |
| // Apply --spfx-version filter if provided (user expects filter, not just branch selection) | ||
| const spfxVersion: string | undefined = this._spfxVersionParameter.value?.trim(); | ||
| if (spfxVersion) { | ||
| const filteredTemplates = [...templates.values()].filter( | ||
| (t) => t.spfxVersion && t.spfxVersion.startsWith(spfxVersion) | ||
| ); |
| protected getExamples(): string[] { | ||
| return [ | ||
| 'Scaffold a new React web part in the current directory:\n spfx create --template webpart-react --component-name "HelloWorld" --library-name "helloworld-library" --solution-name "helloworld" --component-description "A Hello World web part"', | ||
| 'Scaffold an extension with pnpm (skipping npm install):\n spfx create --template extension-field --component-name "ColorField" --library-name "color-field-lib" --solution-name "color-field" --package-manager pnpm --skip-install', | ||
| 'Scaffold a web part without npm install, specifying a local template source:\n spfx create --template webpart-react --component-name "MyWebPart" --library-name "mywebpart" --solution-name "mywebpart" --skip-install --local-source ./my-templates', | ||
| 'Scaffold a web part using a specific SPFx version:\n spfx create --template webpart-react --component-name "MyWP" --component-description "Description" --spfx-version 1.22 --skip-install' | ||
| ]; | ||
| } |
| const templates: SPFxTemplateCollection = await this._fetchTemplatesAsync(manager); | ||
|
|
||
| const formattedTable: string = await templates.toFormattedStringAsync(); | ||
| terminal.writeLine(formattedTable); | ||
| // Apply --spfx-version filter if provided (user expects filter, not just branch selection) | ||
| const spfxVersion: string | undefined = this._spfxVersionParameter.value?.trim(); | ||
| if (spfxVersion) { | ||
| const filteredTemplates = [...templates.values()].filter( |
| // Append examples after the last line of base help | ||
| const lines: string[] = base.split('\n'); | ||
| lines.push(''); | ||
| lines.push('EXAMPLES'); | ||
| for (const example of examples) { | ||
| lines.push(''); | ||
| lines.push(` ${example}`); | ||
| } | ||
| lines.push(''); | ||
| return lines.join('\n'); |
| { | ||
| componentName, | ||
| libraryName: this._libraryNameParameter.value, | ||
| libraryName: this._libraryNameParameter.value || rawSolutionName, |
| return [ | ||
| 'Scaffold a new React web part in the current directory:\n spfx create --template webpart-react --component-name "HelloWorld" --library-name "helloworld-library" --solution-name "helloworld" --component-description "A Hello World web part"', | ||
| 'Scaffold an extension with pnpm (skipping npm install):\n spfx create --template extension-field --component-name "ColorField" --package-manager pnpm', | ||
| 'Scaffold a web part without npm install, specifying a local template source:\n spfx create --template webpart-react --component-name "MyWebPart" --library-name "mywebpart" --solution-name "mywebpart" --local-source ./my-templates', | ||
| 'Scaffold a web part using a specific SPFx version:\n spfx create --template webpart-react --component-name "MyWP" --component-description "Description" --spfx-version 1.22' | ||
| ]; | ||
| } |
| // Apply --spfx-version filter if provided (user expects filter, not just branch selection) | ||
| const spfxVersion: string | undefined = this._spfxVersionParameter.value?.trim(); |
| documentation: | ||
| 'This command lists all available templates from the default GitHub source ' + | ||
| 'and any additional sources specified with --local-source or --remote-source.' | ||
| 'and any additional sources specified with --local-source or --remote-source. ' + | ||
| 'Use --spfx-version to filter templates by version (e.g., "--spfx-version 1.22").' | ||
| }, |
| this._libraryNameParameter = this.defineStringParameter({ | ||
| parameterLongName: '--library-name', | ||
| argumentName: 'LIBRARY_NAME', | ||
| description: 'The library name for the component', | ||
| required: true | ||
| description: 'The library name for the component. Defaults to the solution name if not specified.', | ||
| required: false | ||
| }); |
| this._libraryNameParameter = this.defineStringParameter({ | ||
| parameterLongName: '--library-name', | ||
| argumentName: 'LIBRARY_NAME', | ||
| description: 'The library name for the component', | ||
| required: true | ||
| description: 'The library name for the component. Defaults to the solution name if not specified.', | ||
| required: false | ||
| }); |
| // Apply --spfx-version filter if provided (user expects filter, not just branch selection) | ||
| const spfxVersion: string | undefined = this._spfxVersionParameter.value?.trim(); | ||
| if (spfxVersion) { | ||
| const filteredTemplates = [...templates.values()].filter( | ||
| (t) => t.spfxVersion && t.spfxVersion.startsWith(spfxVersion) | ||
| ); | ||
| if (filteredTemplates.length === 0) { | ||
| terminal.writeLine( | ||
| `No templates found for SPFx version "${spfxVersion}". ` + | ||
| `Use "spfx list-templates" (without --spfx-version) to see all available versions.` | ||
| ); | ||
| return; | ||
| } | ||
| const displayCollection: SPFxTemplateCollection = new SPFxTemplateCollection(filteredTemplates); |
| documentation: | ||
| 'This command lists all available templates from the default GitHub source ' + | ||
| 'and any additional sources specified with --local-source or --remote-source.' | ||
| 'and any additional sources specified with --local-source or --remote-source. ' + | ||
| 'Use --spfx-version to filter templates by version (e.g., "--spfx-version 1.22").' | ||
| }, |
| } | ||
|
|
||
| // Append examples after the last line of base help | ||
| const lines: string[] = base.replace(/\r$/, '').split('\n'); |
| componentName, | ||
| libraryName: this._libraryNameParameter.value, | ||
| libraryName: this._libraryNameParameter.value || rawSolutionName, | ||
| spfxVersion: template.spfxVersion, | ||
| solutionName: rawSolutionName || undefined, | ||
| componentAlias: this._componentAliasParameter.value?.trim() || undefined, |
| const spfxVersion: string | undefined = this._spfxVersionParameter.value?.trim(); | ||
| if (spfxVersion) { | ||
| const filteredTemplates = [...templates.values()].filter( | ||
| (t) => t.spfxVersion && t.spfxVersion.startsWith(spfxVersion) | ||
| ); |
| summary: 'Lists available SPFx templates from configured sources', | ||
| documentation: | ||
| 'This command lists all available templates from the default GitHub source ' + | ||
| 'and any additional sources specified with --local-source or --remote-source.' | ||
| 'and any additional sources specified with --local-source or --remote-source. ' + | ||
| 'Use --spfx-version to filter templates by version (e.g., "--spfx-version 1.22").' |
| { | ||
| componentName, | ||
| libraryName: this._libraryNameParameter.value, | ||
| libraryName: this._libraryNameParameter.value || rawSolutionName, |
| 'Scaffold a new React web part in the current working directory:\n spfx create --template webpart-react --component-name "HelloWorld" --library-name "helloworld-library" --solution-name "helloworld" --component-description "A Hello World web part"', | ||
| 'Scaffold an extension with pnpm (skipping npm install):\n spfx create --template extension-field --component-name "ColorField" --package-manager pnpm', |
| const spfxVersion: string | undefined = this._spfxVersionParameter.value?.trim(); | ||
| if (spfxVersion) { | ||
| const filteredTemplates = [...templates.values()].filter( | ||
| (t) => t.spfxVersion && t.spfxVersion.startsWith(spfxVersion) | ||
| ); |
| for (const example of examples) { | ||
| lines.push(''); | ||
| lines.push(` ${example}`); | ||
| } |
| // Apply --spfx-version filter if provided (user expects filter, not just branch selection) | ||
| const spfxVersion: string | undefined = this._spfxVersionParameter.value?.trim(); |
Fix SummaryPushed at [
Reviewers: Copilot |
|
@vystartasv seems like this PR has commits from #259 PR |
Adds an abstract getExamples() method to SPFxActionBase that subclasses override to provide usage examples. The renderHelpText() method is overridden to append a formatted EXAMPLES section. Includes 4 examples for the 'spfx create' command covering: - Basic React web part scaffolding - Extension scaffolding with pnpm - Local template source usage - SPFx version targeting Partially addresses SharePoint#238
- Add override keyword to renderHelpText() (required by noImplicitOverride setting) - Add override keyword to CreateAction.getExamples() - Replace --skip-install examples with --package-manager pnpm - Remove unnecessary parameters from examples
The previous fix used /\r$/ which only strips a trailing CR from the last line. For CRLF content, every line except the last retains \r, producing malformed output. Use /\r\n/g to normalize all pairs.
466c7c4 to
01eeccd
Compare
| 'Scaffold a new React web part in the current working directory:\n spfx create --template webpart-react --component-name "HelloWorld" --library-name "helloworld-library" --solution-name "helloworld" --component-description "A Hello World web part"', | ||
| 'Scaffold an extension with pnpm (skipping npm install):\n spfx create --template extension-field --component-name "ColorField" --package-manager pnpm', | ||
| 'Scaffold a web part without npm install, specifying a local template source:\n spfx create --template webpart-react --component-name "MyWebPart" --library-name "mywebpart" --solution-name "mywebpart" --local-source ./my-templates', | ||
| 'Scaffold a web part using a specific SPFx version:\n spfx create --template webpart-react --component-name "MyWP" --component-description "Description" --spfx-version 1.22' |
| public override renderHelpText(): string { | ||
| const base: string = super.renderHelpText(); | ||
| const examples: string[] = this.getExamples(); | ||
| if (examples.length === 0) { | ||
| return base; | ||
| } |
| for (const example of examples) { | ||
| lines.push(''); | ||
| lines.push(` ${example}`); | ||
| } |
| { | ||
| "changes": [ | ||
| { | ||
| "packageName": "@microsoft/spfx-cli", | ||
| "comment": "", |
| protected getExamples(): string[] { | ||
| return []; | ||
| } |
| public override renderHelpText(): string { | ||
| const base: string = super.renderHelpText(); | ||
| const examples: string[] = this.getExamples(); | ||
| if (examples.length === 0) { | ||
| return base; |
| // Append examples after the last line of base help | ||
| // Normalize Windows CRLF to LF before splitting so every line is clean | ||
| const lines: string[] = base.replace(/\r\n/g, '\n').split('\n'); | ||
| lines.push(''); | ||
| lines.push('EXAMPLES'); | ||
| for (const example of examples) { | ||
| lines.push(''); | ||
| lines.push(` ${example}`); | ||
| } | ||
| lines.push(''); | ||
| return lines.join('\n'); |
| 'Scaffold a new React web part in the current working directory:\n spfx create --template webpart-react --component-name "HelloWorld" --library-name "helloworld-library" --solution-name "helloworld" --component-description "A Hello World web part"', | ||
| 'Scaffold an extension with pnpm (skipping npm install):\n spfx create --template extension-field --component-name "ColorField" --package-manager pnpm', | ||
| 'Scaffold a web part without npm install, specifying a local template source:\n spfx create --template webpart-react --component-name "MyWebPart" --library-name "mywebpart" --solution-name "mywebpart" --local-source ./my-templates', | ||
| 'Scaffold a web part using a specific SPFx version:\n spfx create --template webpart-react --component-name "MyWP" --component-description "Description" --spfx-version 1.22' |
| protected override getExamples(): string[] { | ||
| return [ | ||
| 'Scaffold a new React web part in the current working directory:\n spfx create --template webpart-react --component-name "HelloWorld" --library-name "helloworld-library" --solution-name "helloworld" --component-description "A Hello World web part"', | ||
| 'Scaffold an extension with pnpm (skipping npm install):\n spfx create --template extension-field --component-name "ColorField" --package-manager pnpm', | ||
| 'Scaffold a web part without npm install, specifying a local template source:\n spfx create --template webpart-react --component-name "MyWebPart" --library-name "mywebpart" --solution-name "mywebpart" --local-source ./my-templates', | ||
| 'Scaffold a web part using a specific SPFx version:\n spfx create --template webpart-react --component-name "MyWP" --component-description "Description" --spfx-version 1.22' | ||
| ]; | ||
| } |
| return [ | ||
| 'Scaffold a new React web part in the current working directory:\n spfx create --template webpart-react --component-name "HelloWorld" --library-name "helloworld-library" --solution-name "helloworld" --component-description "A Hello World web part"', | ||
| 'Scaffold an extension with pnpm (skipping npm install):\n spfx create --template extension-field --component-name "ColorField" --package-manager pnpm', | ||
| 'Scaffold a web part without npm install, specifying a local template source:\n spfx create --template webpart-react --component-name "MyWebPart" --library-name "mywebpart" --solution-name "mywebpart" --local-source ./my-templates', | ||
| 'Scaffold a web part using a specific SPFx version:\n spfx create --template webpart-react --component-name "MyWP" --component-description "Description" --spfx-version 1.22' | ||
| ]; |
There was a problem hiding this comment.
| return [ | |
| 'Scaffold a new React web part in the current working directory:\n spfx create --template webpart-react --component-name "HelloWorld" --library-name "helloworld-library" --solution-name "helloworld" --component-description "A Hello World web part"', | |
| 'Scaffold an extension with pnpm (skipping npm install):\n spfx create --template extension-field --component-name "ColorField" --package-manager pnpm', | |
| 'Scaffold a web part without npm install, specifying a local template source:\n spfx create --template webpart-react --component-name "MyWebPart" --library-name "mywebpart" --solution-name "mywebpart" --local-source ./my-templates', | |
| 'Scaffold a web part using a specific SPFx version:\n spfx create --template webpart-react --component-name "MyWP" --component-description "Description" --spfx-version 1.22' | |
| ]; | |
| const templateParameterName: string = this._solutionNameParameter.longName; | |
| const solutionNameParmeterName: string = this._solutionNameParameter.longName; | |
| const componentDescriptionParameterName: string = this._componentDescriptionParameter.longName; | |
| const componentNameParameterName: string = this._componentNameParameter.longName; | |
| const libraryNameParameterName: string this._libraryNameParameter.longName; | |
| const packageManagerParameterName: string = this._packageManagerParameter.longName; | |
| const spfxVersionParameterName: string = this._spfxVersionParameter.longName; | |
| const localSourceParameterName: string = this._localSourceParameter.longName; | |
| return [ | |
| `Scaffold a new React web part in the current working directory:\n spfx create ${templateParameterName} webpart-react ${componentNameParameterName} "HelloWorld" ${libraryNameParameterName} "helloworld-library" ${solutionNameParmeterName} "helloworld" ${componentDescriptionParameterName} "A Hello World web part"`, | |
| `Scaffold an extension with pnpm (skipping npm install):\n spfx create ${templateParameterName} extension-field ${componentNameParameterName} "ColorField" ${packageManagerParameterName} pnpm`, | |
| `Scaffold a web part without npm install, specifying a local template source:\n spfx create ${templateParameterName} webpart-react ${componentNameParameterName} "MyWebPart" ${libraryNameParameterName} "mywebpart" ${solutionNameParmeterName} "mywebpart" ${localSourceParameterName} ./my-templates`, | |
| `Scaffold a web part using a specific SPFx version:\n spfx create ${templateParameterName} webpart-react ${componentNameParameterName} "MyWP" ${componentDescriptionParameterName} "Description" ${spfxVersionParameterName} 1.22` | |
| ]; |
|
|
||
| // Append examples after the last line of base help | ||
| // Normalize Windows CRLF to LF before splitting so every line is clean | ||
| const lines: string[] = base.replace(/\r\n/g, '\n').split('\n'); |
There was a problem hiding this comment.
Why bother splitting this? Just append \nEXAMPLES\n${exampleLines}\n to the base text.
The new EXAMPLES section in renderHelpText() causes the 'create' action help snapshot to diverge from the updated output. Regenerated with --updateSnapshot.
| public override renderHelpText(): string { | ||
| const base: string = super.renderHelpText(); | ||
| const examples: string[] = this.getExamples(); | ||
| if (examples.length === 0) { | ||
| return base; | ||
| } | ||
|
|
||
| // Append examples after the last line of base help | ||
| // Normalize Windows CRLF to LF before splitting so every line is clean | ||
| const lines: string[] = base.replace(/\r\n/g, '\n').split('\n'); | ||
| lines.push(''); | ||
| lines.push('EXAMPLES'); | ||
| for (const example of examples) { | ||
| lines.push(''); | ||
| lines.push(` ${example}`); | ||
| } | ||
| lines.push(''); | ||
| return lines.join('\n'); | ||
| } |
Fixes two issues: 1. Reverts --library-name back to optional (regression from merge conflict with already-merged PR SharePoint#260 that made it optional on main) 2. Fixes example text to accurately describe CLI behavior: - Subfolder output, not CWD - Use --package-manager none (not pnpm) to skip install - Include required params in all examples now that --library-name is optional, examples show the minimal arguments needed Co-Authored-By: Claw <claw@hermes>
| EXAMPLES | ||
|
|
||
| Create a new React web part. Scaffolds into a subfolder named after the solution: | ||
| spfx create --template webpart-react --component-name \"HelloWorld\" --solution-name \"helloworld\" --component-description \"A Hello World web part\" |
| this._libraryNameParameter = this.defineStringParameter({ | ||
| parameterLongName: '--library-name', | ||
| argumentName: 'LIBRARY_NAME', | ||
| description: 'The library name for the component', | ||
| required: true | ||
| description: 'The library name for the component. Defaults to the solution name if not specified.', | ||
| required: false |
Description
Adds an extensible usage examples framework to the SPFx CLI help output:
SPFxActionBase — new abstract method
getExamples(): string[]that subclasses override to provide usage examples. OverridesrenderHelpText()to append a formatted EXAMPLES section after the default help text.CreateAction — 4 examples covering the key scaffolding scenarios:
Partially addresses #238
How was this tested?
mainto remove earlier cross-PR contaminationType of change