-
Notifications
You must be signed in to change notification settings - Fork 5
fix: use pnpm'n main help - pnpm's official descriptions for cmds and opts #46
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
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
7773f09
update
AmirSa12 ece6de5
trigger ci
AmirSa12 f14d7d5
machanism change - parse --help
AmirSa12 35dbac8
types
AmirSa12 3291105
fix/descriptions
AmirSa12 9a4d53f
big update
AmirSa12 4a26c65
update
AmirSa12 eb86cfe
update
AmirSa12 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import type { Complete } from '../../src/t.js'; | ||
| import { | ||
| getPackageJsonScripts, | ||
| getPackageJsonDependencies, | ||
| } from '../utils/package-json-utils.js'; | ||
|
|
||
| // provides completions for npm scripts from package.json.. like: start,dev,build | ||
| export const packageJsonScriptCompletion = async ( | ||
| complete: Complete | ||
| ): Promise<void> => { | ||
| getPackageJsonScripts().forEach((script) => | ||
| complete(script, `Run ${script} script`) | ||
| ); | ||
| }; | ||
|
|
||
| // provides completions for package dependencies from package.json.. for commands like remove `pnpm remove <dependency>` | ||
| export const packageJsonDependencyCompletion = async ( | ||
| complete: Complete | ||
| ): Promise<void> => { | ||
| getPackageJsonDependencies().forEach((dep) => complete(dep, '')); | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import type { PackageManagerCompletion } from '../package-manager-completion.js'; | ||
|
|
||
| export async function setupBunCompletions( | ||
| completion: PackageManagerCompletion | ||
| ): Promise<void> {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import type { PackageManagerCompletion } from '../package-manager-completion.js'; | ||
|
|
||
| export async function setupNpmCompletions( | ||
| completion: PackageManagerCompletion | ||
| ): Promise<void> {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,233 @@ | ||
| import { promisify } from 'node:util'; | ||
| import child_process from 'node:child_process'; | ||
|
|
||
| const exec = promisify(child_process.exec); | ||
| const { execSync } = child_process; | ||
| import type { PackageManagerCompletion } from '../package-manager-completion.js'; | ||
| import { Command, Option } from '../../src/t.js'; | ||
|
|
||
| interface LazyCommand extends Command { | ||
| _lazyCommand?: string; | ||
| _optionsLoaded?: boolean; | ||
| optionsRaw?: Map<string, Option>; | ||
| } | ||
|
|
||
| import { | ||
| packageJsonScriptCompletion, | ||
| packageJsonDependencyCompletion, | ||
| } from '../completions/completion-producers.js'; | ||
| import { | ||
| stripAnsiEscapes, | ||
| measureIndent, | ||
| parseAliasList, | ||
| COMMAND_ROW_RE, | ||
| OPTION_ROW_RE, | ||
| OPTION_HEAD_RE, | ||
| type ParsedOption, | ||
| } from '../utils/text-utils.js'; | ||
|
|
||
| // regex to detect options section in help text | ||
| const OPTIONS_SECTION_RE = /^\s*Options:/i; | ||
|
|
||
| // we parse the pnpm help text to extract commands and their descriptions! | ||
| export function parsePnpmHelp(helpText: string): Record<string, string> { | ||
| const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/); | ||
|
|
||
| // we find the earliest description column across command rows. | ||
| let descColumnIndex = Number.POSITIVE_INFINITY; | ||
| for (const line of helpLines) { | ||
| const rowMatch = line.match(COMMAND_ROW_RE); | ||
| if (!rowMatch) continue; | ||
| const descColumnIndexOnThisLine = line.indexOf(rowMatch[2]); | ||
| if ( | ||
| descColumnIndexOnThisLine >= 0 && | ||
| descColumnIndexOnThisLine < descColumnIndex | ||
| ) { | ||
| descColumnIndex = descColumnIndexOnThisLine; | ||
| } | ||
| } | ||
| if (!Number.isFinite(descColumnIndex)) return {}; | ||
|
|
||
| // we fold rows, and join continuation lines aligned to descColumnIndex or deeper. | ||
| type PendingRow = { names: string[]; desc: string } | null; | ||
| let pendingRow: PendingRow = null; | ||
|
|
||
| const commandMap = new Map<string, string>(); | ||
| const flushPendingRow = () => { | ||
| if (!pendingRow) return; | ||
| const desc = pendingRow.desc.trim(); | ||
| for (const name of pendingRow.names) commandMap.set(name, desc); | ||
| pendingRow = null; | ||
| }; | ||
|
|
||
| for (const line of helpLines) { | ||
| if (OPTIONS_SECTION_RE.test(line)) break; // we stop at options | ||
|
|
||
| // we match the command row | ||
| const rowMatch = line.match(COMMAND_ROW_RE); | ||
| if (rowMatch) { | ||
| flushPendingRow(); | ||
| pendingRow = { | ||
| names: parseAliasList(rowMatch[1]), | ||
| desc: rowMatch[2].trim(), | ||
| }; | ||
| continue; | ||
| } | ||
|
|
||
| // we join continuation lines aligned to descColumnIndex or deeper | ||
| if (pendingRow) { | ||
| const indentWidth = measureIndent(line); | ||
| if (indentWidth >= descColumnIndex && line.trim()) { | ||
| pendingRow.desc += ' ' + line.trim(); | ||
| } | ||
| } | ||
| } | ||
| // we flush the pending row and return the command map | ||
| flushPendingRow(); | ||
|
|
||
| return Object.fromEntries(commandMap); | ||
| } | ||
|
|
||
| // now we get the pnpm commands from the main help output | ||
| export async function getPnpmCommandsFromMainHelp(): Promise< | ||
| Record<string, string> | ||
| > { | ||
| try { | ||
| const { stdout } = await exec('pnpm --help', { | ||
| encoding: 'utf8', | ||
| timeout: 500, | ||
| maxBuffer: 4 * 1024 * 1024, | ||
| }); | ||
| return parsePnpmHelp(stdout); | ||
| } catch { | ||
| return {}; | ||
| } | ||
| } | ||
|
|
||
| // here we parse the pnpm options from the help text | ||
| export function parsePnpmOptions( | ||
| helpText: string, | ||
| { flagsOnly = true }: { flagsOnly?: boolean } = {} | ||
| ): ParsedOption[] { | ||
| // we strip the ANSI escapes from the help text | ||
| const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/); | ||
|
|
||
| // we find the earliest description column among option rows we care about | ||
| let descColumnIndex = Number.POSITIVE_INFINITY; | ||
| for (const line of helpLines) { | ||
| const optionMatch = line.match(OPTION_ROW_RE); | ||
| if (!optionMatch) continue; | ||
| if (flagsOnly && optionMatch.groups?.val) continue; // skip value-taking options, we will add them manually with their value | ||
| const descColumnIndexOnThisLine = line.indexOf(optionMatch.groups!.desc); | ||
| if ( | ||
| descColumnIndexOnThisLine >= 0 && | ||
| descColumnIndexOnThisLine < descColumnIndex | ||
| ) { | ||
| descColumnIndex = descColumnIndexOnThisLine; | ||
| } | ||
| } | ||
| if (!Number.isFinite(descColumnIndex)) return []; | ||
|
|
||
| // we fold the option rows and join the continuations | ||
| const optionsOut: ParsedOption[] = []; | ||
| let pendingOption: ParsedOption | null = null; | ||
|
|
||
| const flushPendingOption = () => { | ||
| if (!pendingOption) return; | ||
| pendingOption.desc = pendingOption.desc.trim(); | ||
| optionsOut.push(pendingOption); | ||
| pendingOption = null; | ||
| }; | ||
|
|
||
| // we match the option row | ||
| for (const line of helpLines) { | ||
| const optionMatch = line.match(OPTION_ROW_RE); | ||
| if (optionMatch) { | ||
| if (flagsOnly && optionMatch.groups?.val) continue; | ||
| flushPendingOption(); | ||
| pendingOption = { | ||
| short: optionMatch.groups?.short || undefined, | ||
| long: optionMatch.groups!.long, | ||
| desc: optionMatch.groups!.desc.trim(), | ||
| }; | ||
| continue; | ||
| } | ||
|
|
||
| // we join the continuations | ||
| if (pendingOption) { | ||
| const indentWidth = measureIndent(line); | ||
| const startsNewOption = OPTION_HEAD_RE.test(line); | ||
| if (indentWidth >= descColumnIndex && line.trim() && !startsNewOption) { | ||
| pendingOption.desc += ' ' + line.trim(); | ||
| } | ||
| } | ||
| } | ||
| // we flush the pending option | ||
| flushPendingOption(); | ||
|
|
||
| return optionsOut; | ||
| } | ||
|
|
||
| // we load the dynamic options synchronously when requested ( separated from the command loading ) | ||
| export function loadDynamicOptionsSync( | ||
| cmd: LazyCommand, | ||
| command: string | ||
| ): void { | ||
| try { | ||
| const stdout = execSync(`pnpm ${command} --help`, { | ||
| encoding: 'utf8', | ||
| timeout: 500, | ||
| }); | ||
|
|
||
| const parsedOptions = parsePnpmOptions(stdout, { flagsOnly: true }); | ||
|
|
||
| for (const { long, short, desc } of parsedOptions) { | ||
| const alreadyDefined = cmd.optionsRaw?.get?.(long); | ||
| if (!alreadyDefined) cmd.option(long, desc, short); | ||
| } | ||
| } catch (_err) {} | ||
| } | ||
|
|
||
| // we setup the lazy option loading for a command | ||
|
|
||
| function setupLazyOptionLoading(cmd: LazyCommand, command: string): void { | ||
| cmd._lazyCommand = command; | ||
| cmd._optionsLoaded = false; | ||
|
|
||
| const optionsStore = cmd.options; | ||
| cmd.optionsRaw = optionsStore; | ||
|
|
||
| Object.defineProperty(cmd, 'options', { | ||
| get() { | ||
| if (!this._optionsLoaded) { | ||
| this._optionsLoaded = true; | ||
| loadDynamicOptionsSync(this, this._lazyCommand); // block until filled | ||
| } | ||
| return optionsStore; | ||
| }, | ||
| configurable: true, | ||
| }); | ||
| } | ||
|
|
||
| export async function setupPnpmCompletions( | ||
| completion: PackageManagerCompletion | ||
| ): Promise<void> { | ||
| try { | ||
| const commandsWithDescriptions = await getPnpmCommandsFromMainHelp(); | ||
|
|
||
| for (const [command, description] of Object.entries( | ||
| commandsWithDescriptions | ||
| )) { | ||
| const cmd = completion.command(command, description); | ||
|
|
||
| if (['remove', 'rm', 'update', 'up'].includes(command)) { | ||
| cmd.argument('package', packageJsonDependencyCompletion); | ||
| } | ||
| if (command === 'run') { | ||
| cmd.argument('script', packageJsonScriptCompletion, true); | ||
| } | ||
|
|
||
| setupLazyOptionLoading(cmd, command); | ||
| } | ||
| } catch (_err) {} | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import type { PackageManagerCompletion } from '../package-manager-completion.js'; | ||
|
|
||
| export async function setupYarnCompletions( | ||
| completion: PackageManagerCompletion | ||
| ): Promise<void> {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { readFileSync } from 'fs'; | ||
|
|
||
| export function getPackageJsonScripts(): string[] { | ||
| try { | ||
| const packageJson = JSON.parse(readFileSync('package.json', 'utf8')); | ||
| return Object.keys(packageJson.scripts || {}); | ||
| } catch { | ||
| return []; | ||
| } | ||
| } | ||
|
|
||
| export function getPackageJsonDependencies(): string[] { | ||
| try { | ||
| const packageJson = JSON.parse(readFileSync('package.json', 'utf8')); | ||
| const deps = { | ||
| ...packageJson.dependencies, | ||
| ...packageJson.devDependencies, | ||
| ...packageJson.peerDependencies, | ||
| ...packageJson.optionalDependencies, | ||
| }; | ||
| return Object.keys(deps); | ||
| } catch { | ||
| return []; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| // regex for parsing help text | ||
| export const ANSI_ESCAPE_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g; | ||
|
|
||
| // Command row: <indent><names><>=2 spaces><desc> | ||
| // e.g. " install, i Install all dependencies" | ||
| export const COMMAND_ROW_RE = /^\s+([a-z][a-z\s,-]*?)\s{2,}(\S.*)$/i; | ||
|
|
||
| // Option row (optional value part captured in (?<val>)): | ||
| // [indent][-x, ]--long[ <value>| [value]] <>=2 spaces> <desc> | ||
| export const OPTION_ROW_RE = | ||
| /^\s*(?:-(?<short>[A-Za-z]),\s*)?--(?<long>[a-z0-9-]+)(?<val>\s+(?:<[^>]+>|\[[^\]]+\]))?\s{2,}(?<desc>\S.*)$/i; | ||
|
|
||
| // we detect the start of a new option head (used to stop continuation) | ||
| export const OPTION_HEAD_RE = /^\s*(?:-[A-Za-z],\s*)?--[a-z0-9-]+/i; | ||
|
|
||
| // we remove the ANSI escape sequences from a string | ||
| export const stripAnsiEscapes = (s: string): string => | ||
| s.replace(ANSI_ESCAPE_RE, ''); | ||
|
|
||
| // measure the indentation level of a string | ||
| export const measureIndent = (s: string): number => | ||
| (s.match(/^\s*/) || [''])[0].length; | ||
|
|
||
| // parse a comma-separated list of aliases | ||
| export const parseAliasList = (s: string): string[] => | ||
| s | ||
| .split(',') | ||
| .map((t) => t.trim()) | ||
| .filter(Boolean); | ||
|
|
||
| export type ParsedOption = { | ||
| long: string; | ||
| short?: string; | ||
| desc: string; | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
remove the description here, it's not needed!
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.
or maybe we can use the content of the script as the description of it, up to you!