Skip to content

Commit a942235

Browse files
authored
feat: add bun handler (#51)
* add bun handler * update
1 parent 578f633 commit a942235

File tree

1 file changed

+190
-1
lines changed

1 file changed

+190
-1
lines changed

bin/handlers/bun-handler.ts

Lines changed: 190 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,194 @@
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 COMMANDS_SECTION_RE = /^Commands:\s*$/i;
14+
const FLAGS_SECTION_RE = /^Flags:\s*$/i;
15+
const SECTION_END_RE = /^(Examples|Full documentation|Learn more)/i;
16+
const COMMAND_VALIDATION_RE = /^[a-z][a-z0-9-]*$/;
17+
const BUN_OPTION_RE =
18+
/^\s*(?:-([a-zA-Z]),?\s*)?--([a-z][a-z0-9-]*)(?:=<[^>]+>)?\s+(.+)$/;
19+
const MAIN_COMMAND_RE = /^ ([a-z][a-z0-9-]*)\s+(.+)$/;
20+
const CONTINUATION_COMMAND_RE = /^\s{12,}([a-z][a-z0-9-]*)\s+(.+)$/;
21+
const EMPTY_LINE_FOLLOWED_BY_NON_COMMAND_RE = /^\s+[a-z]/;
22+
const DESCRIPTION_SPLIT_RE = /\s{2,}/;
23+
const CAPITAL_LETTER_START_RE = /^[A-Z]/;
24+
const LINE_SPLIT_RE = /\r?\n/;
25+
26+
function toLines(text: string): string[] {
27+
return stripAnsiEscapes(text).split(LINE_SPLIT_RE);
28+
}
29+
30+
function findSectionStart(lines: string[], header: RegExp): number {
31+
for (let i = 0; i < lines.length; i++) {
32+
if (header.test(lines[i].trim())) return i + 1;
33+
}
34+
return -1;
35+
}
36+
37+
const bunOptionHandlers: OptionHandlers = {
38+
...commonOptionHandlers,
39+
40+
backend(complete) {
41+
complete('clonefile', ' ');
42+
complete('hardlink', ' ');
43+
complete('symlink', ' ');
44+
complete('copyfile', ' ');
45+
},
46+
47+
linker(complete) {
48+
complete('isolated', ' ');
49+
complete('hoisted', ' ');
50+
},
51+
52+
omit(complete) {
53+
complete('dev', ' ');
54+
complete('optional', ' ');
55+
complete('peer', ' ');
56+
},
57+
58+
shell(complete) {
59+
complete('bun', ' ');
60+
complete('system', ' ');
61+
},
62+
63+
'unhandled-rejections'(complete) {
64+
complete('strict', ' ');
65+
complete('throw', ' ');
66+
complete('warn', ' ');
67+
complete('none', ' ');
68+
complete('warn-with-error-code', ' ');
69+
},
70+
};
71+
72+
export function parseBunHelp(helpText: string): Record<string, string> {
73+
const lines = toLines(helpText);
74+
75+
const startIndex = findSectionStart(lines, COMMANDS_SECTION_RE);
76+
if (startIndex === -1) return {};
77+
78+
const commands: Record<string, string> = {};
79+
80+
for (let i = startIndex; i < lines.length; i++) {
81+
const line = lines[i];
82+
83+
// stop when we hit Flags section or empty line followed by non-command content
84+
if (
85+
FLAGS_SECTION_RE.test(line.trim()) ||
86+
(line.trim() === '' &&
87+
i + 1 < lines.length &&
88+
!lines[i + 1].match(EMPTY_LINE_FOLLOWED_BY_NON_COMMAND_RE))
89+
) {
90+
break;
91+
}
92+
93+
if (!line.trim()) continue;
94+
95+
// main command row
96+
const main = line.match(MAIN_COMMAND_RE);
97+
if (main) {
98+
const [, command, rest] = main;
99+
if (COMMAND_VALIDATION_RE.test(command)) {
100+
const parts = rest.split(DESCRIPTION_SPLIT_RE);
101+
let desc = parts[parts.length - 1];
102+
103+
if (desc && CAPITAL_LETTER_START_RE.test(desc)) {
104+
commands[command] = desc.trim();
105+
} else if (parts.length > 1) {
106+
for (const p of parts) {
107+
if (CAPITAL_LETTER_START_RE.test(p)) {
108+
commands[command] = p.trim();
109+
break;
110+
}
111+
}
112+
}
113+
}
114+
}
115+
116+
const cont = line.match(CONTINUATION_COMMAND_RE);
117+
if (cont) {
118+
const [, command, description] = cont;
119+
if (COMMAND_VALIDATION_RE.test(command)) {
120+
commands[command] = description.trim();
121+
}
122+
}
123+
}
124+
125+
return commands;
126+
}
127+
128+
export async function getBunCommandsFromMainHelp(): Promise<
129+
Record<string, string>
130+
> {
131+
const output = await safeExec('bun --help');
132+
return output ? parseBunHelp(output) : {};
133+
}
134+
135+
export function parseBunOptions(
136+
helpText: string,
137+
{ flagsOnly = true }: { flagsOnly?: boolean } = {}
138+
): ParsedOption[] {
139+
const lines = toLines(helpText);
140+
const out: ParsedOption[] = [];
141+
142+
const start = findSectionStart(lines, FLAGS_SECTION_RE);
143+
if (start === -1) return out;
144+
145+
for (let i = start; i < lines.length; i++) {
146+
const line = lines[i];
147+
if (SECTION_END_RE.test(line.trim())) break;
148+
149+
const m = line.match(BUN_OPTION_RE);
150+
if (!m) continue;
151+
152+
const [, short, long, desc] = m;
153+
const takesValue = line.includes('=<'); // bun shows value as --opt=<val>
154+
if (flagsOnly && takesValue) continue;
155+
156+
out.push({
157+
short: short || undefined,
158+
long,
159+
desc: desc.trim(),
160+
});
161+
}
162+
163+
return out;
164+
}
165+
166+
function loadBunOptionsSync(cmd: LazyCommand, command: string): void {
167+
const output = safeExecSync(`bun ${command} --help`);
168+
if (!output) return;
169+
170+
const options = parseBunOptions(output, { flagsOnly: false });
171+
172+
for (const { long, short, desc } of options) {
173+
const exists = cmd.optionsRaw?.get?.(long);
174+
if (exists) continue;
175+
176+
const handler = bunOptionHandlers[long];
177+
if (handler) cmd.option(long, desc, handler, short);
178+
else cmd.option(long, desc, short);
179+
}
180+
}
2181

3182
export async function setupBunCompletions(
4183
completion: PackageManagerCompletion
5-
): Promise<void> {}
184+
): Promise<void> {
185+
try {
186+
const commands = await getBunCommandsFromMainHelp();
187+
188+
for (const [command, description] of Object.entries(commands)) {
189+
const c = completion.command(command, description);
190+
setupCommandArguments(c, command, 'bun');
191+
setupLazyOptionLoading(c, command, 'bun', loadBunOptionsSync);
192+
}
193+
} catch {}
194+
}

0 commit comments

Comments
 (0)