Skip to content

Commit 9905d55

Browse files
committed
add yarn handler
1 parent a942235 commit 9905d55

File tree

1 file changed

+154
-1
lines changed

1 file changed

+154
-1
lines changed

bin/handlers/yarn-handler.ts

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,158 @@
11
import type { PackageManagerCompletion } from '../package-manager-completion.js';
2+
import { stripAnsiEscapes, type ParsedOption } from '../utils/text-utils.js';
3+
import {
4+
LazyCommand,
5+
OptionHandlers,
6+
commonOptionHandlers,
7+
setupLazyOptionLoading,
8+
setupCommandArguments,
9+
safeExec,
10+
safeExecSync,
11+
} from '../utils/shared.js';
12+
13+
const OPTIONS_SECTION_RE = /^\s*Options:\s*$/i;
14+
const COMMANDS_SECTION_RE = /^\s*Commands:\s*$/i;
15+
const SECTION_END_RE = /^(Run `yarn help|Visit https:\/\/)/i;
16+
const LINE_SPLIT_RE = /\r?\n/;
17+
const YARN_OPTION_RE =
18+
/^\s*(?:-([a-zA-Z]),?\s*)?--([a-z][a-z0-9-]*)(?:\s+<[^>]+>|\s+\[[^\]]+\])?\s+(.+)$/;
19+
const YARN_COMMAND_RE = /^\s*-\s+([a-z][a-z0-9-]*(?:\s*\/\s*[a-z][a-zA-Z]*)*)/;
20+
21+
function toLines(text: string): string[] {
22+
return stripAnsiEscapes(text).split(LINE_SPLIT_RE);
23+
}
24+
25+
function findSectionStart(lines: string[], header: RegExp): number {
26+
for (let i = 0; i < lines.length; i++) {
27+
if (header.test(lines[i].trim())) return i + 1;
28+
}
29+
return -1;
30+
}
31+
32+
const yarnOptionHandlers: OptionHandlers = {
33+
...commonOptionHandlers,
34+
35+
emoji(complete) {
36+
complete('true', ' ');
37+
complete('false', ' ');
38+
},
39+
40+
production(complete) {
41+
complete('true', ' ');
42+
complete('false', ' ');
43+
},
44+
45+
'scripts-prepend-node-path'(complete) {
46+
complete('true', ' ');
47+
complete('false', ' ');
48+
},
49+
};
50+
51+
export function parseYarnHelp(helpText: string): Record<string, string> {
52+
const lines = toLines(helpText);
53+
const commands: Record<string, string> = {};
54+
55+
const startIndex = findSectionStart(lines, COMMANDS_SECTION_RE);
56+
if (startIndex === -1) return commands;
57+
58+
for (let i = startIndex; i < lines.length; i++) {
59+
const line = lines[i];
60+
61+
// Stop at section end
62+
if (SECTION_END_RE.test(line)) break;
63+
64+
if (!line.trim()) continue;
65+
66+
const match = line.match(YARN_COMMAND_RE);
67+
if (match) {
68+
const [, commandWithAliases] = match;
69+
// handle commands with aliases like "generate-lock-entry / generateLockEntry"
70+
const commands_parts = commandWithAliases.split(/\s*\/\s*/);
71+
const mainCommand = commands_parts[0].trim();
72+
73+
if (mainCommand) {
74+
// yarn doesn't provide descriptions in main help, use empty string
75+
commands[mainCommand] = '';
76+
77+
// add aliases if they exist
78+
for (let j = 1; j < commands_parts.length; j++) {
79+
const alias = commands_parts[j].trim();
80+
if (alias) {
81+
commands[alias] = '';
82+
}
83+
}
84+
}
85+
}
86+
}
87+
88+
return commands;
89+
}
90+
91+
export async function getYarnCommandsFromMainHelp(): Promise<
92+
Record<string, string>
93+
> {
94+
const output = await safeExec('cd /tmp && yarn --help');
95+
return output ? parseYarnHelp(output) : {};
96+
}
97+
98+
export function parseYarnOptions(
99+
helpText: string,
100+
{ flagsOnly = true }: { flagsOnly?: boolean } = {}
101+
): ParsedOption[] {
102+
const lines = toLines(helpText);
103+
const out: ParsedOption[] = [];
104+
105+
const start = findSectionStart(lines, OPTIONS_SECTION_RE);
106+
if (start === -1) return out;
107+
108+
for (let i = start; i < lines.length; i++) {
109+
const line = lines[i];
110+
if (SECTION_END_RE.test(line.trim())) break;
111+
112+
const m = line.match(YARN_OPTION_RE);
113+
if (!m) continue;
114+
115+
const [, short, long, desc] = m;
116+
const takesValue = line.includes('<') || line.includes('[');
117+
if (flagsOnly && takesValue) continue;
118+
119+
out.push({
120+
short: short || undefined,
121+
long,
122+
desc: desc.trim(),
123+
});
124+
}
125+
126+
return out;
127+
}
128+
129+
function loadYarnOptionsSync(cmd: LazyCommand, command: string): void {
130+
// Use cd /tmp to avoid packageManager constraints
131+
const output = safeExecSync(`cd /tmp && yarn ${command} --help`);
132+
if (!output) return;
133+
134+
const options = parseYarnOptions(output, { flagsOnly: false });
135+
136+
for (const { long, short, desc } of options) {
137+
const exists = cmd.optionsRaw?.get?.(long);
138+
if (exists) continue;
139+
140+
const handler = yarnOptionHandlers[long];
141+
if (handler) cmd.option(long, desc, handler, short);
142+
else cmd.option(long, desc, short);
143+
}
144+
}
2145

3146
export async function setupYarnCompletions(
4147
completion: PackageManagerCompletion
5-
): Promise<void> {}
148+
): Promise<void> {
149+
try {
150+
const commands = await getYarnCommandsFromMainHelp();
151+
152+
for (const [command, description] of Object.entries(commands)) {
153+
const c = completion.command(command, description);
154+
setupCommandArguments(c, command, 'yarn');
155+
setupLazyOptionLoading(c, command, 'yarn', loadYarnOptionsSync);
156+
}
157+
} catch {}
158+
}

0 commit comments

Comments
 (0)