Skip to content

Commit 4b24953

Browse files
authored
chore: enable strict index checks (#582)
1 parent 8f1c380 commit 4b24953

26 files changed

Lines changed: 150 additions & 49 deletions

.changeset/dirty-rats-change.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@clack/prompts": patch
3+
"@clack/core": patch
4+
---
5+
6+
Handle empty arrays in various prompts and utilities.

packages/core/src/prompts/autocomplete.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,10 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
8686
if (this._cursor >= this.userInput.length) {
8787
return `${this.userInput}█`;
8888
}
89-
const s1 = this.userInput.slice(0, this._cursor);
90-
const [s2, ...s3] = this.userInput.slice(this._cursor);
91-
return `${s1}${styleText('inverse', s2)}${s3.join('')}`;
89+
const preCursor = this.userInput.slice(0, this.cursor);
90+
const cursorChar = this.userInput.slice(this.cursor, this.cursor + 1);
91+
const rest = this.userInput.slice(this.cursor + 1);
92+
return `${preCursor}${styleText('inverse', cursorChar)}${rest}`;
9293
}
9394

9495
get options(): T[] {
@@ -117,7 +118,7 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
117118
}
118119
} else {
119120
if (!this.multiple && this.options.length > 0) {
120-
initialValues = [this.options[0].value];
121+
initialValues = [this.options[0]?.value];
121122
}
122123
}
123124

packages/core/src/prompts/date.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ const SEGMENTS: Record<string, SegmentConfig> = {
2222
} as const;
2323

2424
function segmentsFor(fmt: DateFormat): SegmentConfig[] {
25-
return [...fmt].map((c) => SEGMENTS[c as keyof typeof SEGMENTS]);
25+
// biome-ignore lint/style/noNonNullAssertion: DateFormat only contains Y/M/D keys present in SEGMENTS
26+
return [...fmt].map((c) => SEGMENTS[c as keyof typeof SEGMENTS]!);
2627
}
2728

2829
function detectLocaleFormat(locale?: string): { segments: SegmentConfig[]; separator: string } {

packages/core/src/prompts/group-multiselect.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export default class GroupMultiSelectPrompt<T extends { value: any }> extends Pr
2828

2929
private toggleValue() {
3030
const item = this.options[this.cursor];
31+
if (item === undefined) {
32+
return;
33+
}
3134
if (this.value === undefined) {
3235
this.value = [];
3336
}

packages/core/src/prompts/multi-line.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ export default class MultiLinePrompt extends Prompt<string> {
2525
if (this.cursor >= userInput.length) {
2626
return `${userInput}█`;
2727
}
28-
const s1 = userInput.slice(0, this.cursor);
29-
const s2 = userInput[this.cursor];
30-
const s3 = userInput.slice(this.cursor + 1);
31-
if (s2 === '\n') return `${s1}█\n${s3}`;
32-
return `${s1}${styleText('inverse', s2)}${s3}`;
28+
const preCursor = userInput.slice(0, this.cursor);
29+
const cursorChar = userInput.slice(this.cursor, this.cursor + 1);
30+
const rest = userInput.slice(this.cursor + 1);
31+
if (cursorChar === '\n') return `${preCursor}█\n${rest}`;
32+
return `${preCursor}${styleText('inverse', cursorChar)}${rest}`;
3333
}
3434
get cursor() {
3535
return this._cursor;

packages/core/src/prompts/multi-select.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default class MultiSelectPrompt<T extends OptionLike> extends Prompt<T['v
1818
cursor = 0;
1919

2020
private get _value(): T['value'] {
21-
return this.options[this.cursor].value;
21+
return this.options[this.cursor]?.value;
2222
}
2323

2424
private get _enabledOptions(): T[] {
@@ -59,7 +59,7 @@ export default class MultiSelectPrompt<T extends OptionLike> extends Prompt<T['v
5959
this.options.findIndex(({ value }) => value === opts.cursorAt),
6060
0
6161
);
62-
this.cursor = this.options[cursor].disabled ? findCursor<T>(cursor, 1, this.options) : cursor;
62+
this.cursor = this.options[cursor]?.disabled ? findCursor<T>(cursor, 1, this.options) : cursor;
6363
this.on('key', (_char, key) => {
6464
if (key.name === 'a') {
6565
this.toggleAll();

packages/core/src/prompts/password.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ export default class PasswordPrompt extends Prompt<string> {
2121
return `${this.masked}${styleText(['inverse', 'hidden'], '_')}`;
2222
}
2323
const masked = this.masked;
24-
const s1 = masked.slice(0, this.cursor);
25-
const s2 = masked.slice(this.cursor);
26-
return `${s1}${styleText('inverse', s2[0])}${s2.slice(1)}`;
24+
const preCursor = masked.slice(0, this.cursor);
25+
const cursorChar = masked.slice(this.cursor, this.cursor + 1);
26+
const rest = masked.slice(this.cursor + 1);
27+
return `${preCursor}${styleText('inverse', cursorChar)}${rest}`;
2728
}
2829
clear() {
2930
this._clearUserInput();

packages/core/src/prompts/select.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export default class SelectPrompt<T extends { value: any; disabled?: boolean }>
1717
}
1818

1919
private changeValue() {
20-
this.value = this._selectedValue.value;
20+
const selectedValue = this._selectedValue;
21+
this.value = selectedValue === undefined ? undefined : selectedValue.value;
2122
}
2223

2324
constructor(opts: SelectOptions<T>) {
@@ -27,7 +28,7 @@ export default class SelectPrompt<T extends { value: any; disabled?: boolean }>
2728

2829
const initialCursor = this.options.findIndex(({ value }) => value === opts.initialValue);
2930
const cursor = initialCursor === -1 ? 0 : initialCursor;
30-
this.cursor = this.options[cursor].disabled ? findCursor<T>(cursor, 1, this.options) : cursor;
31+
this.cursor = this.options[cursor]?.disabled ? findCursor<T>(cursor, 1, this.options) : cursor;
3132
this.changeValue();
3233

3334
this.on('cursor', (key) => {

packages/core/src/prompts/text.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ export default class TextPrompt extends Prompt<string> {
1515
if (this.cursor >= userInput.length) {
1616
return `${this.userInput}█`;
1717
}
18-
const s1 = userInput.slice(0, this.cursor);
19-
const [s2, ...s3] = userInput.slice(this.cursor);
20-
return `${s1}${styleText('inverse', s2)}${s3.join('')}`;
18+
const preCursor = userInput.slice(0, this.cursor);
19+
const cursorChar = userInput.slice(this.cursor, this.cursor + 1);
20+
const rest = userInput.slice(this.cursor + 1);
21+
return `${preCursor}${styleText('inverse', cursorChar)}${rest}`;
2122
}
2223
get cursor() {
2324
return this._cursor;

packages/core/src/utils/cursor.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export function findCursor<T extends { disabled?: boolean }>(
1111
const maxCursor = Math.max(options.length - 1, 0);
1212
const clampedCursor = newCursor < 0 ? maxCursor : newCursor > maxCursor ? 0 : newCursor;
1313
const newOption = options[clampedCursor];
14-
if (newOption.disabled) {
14+
if (newOption?.disabled) {
1515
return findCursor(clampedCursor, delta < 0 ? -1 : 1, options);
1616
}
1717
return clampedCursor;
@@ -37,20 +37,22 @@ export function findTextCursor(
3737

3838
cursorY = Math.max(0, Math.min(lines.length - 1, cursorY + deltaY));
3939

40-
cursorX = Math.min(cursorX, lines[cursorY].length) + deltaX;
40+
// biome-ignore-start lint/style/noNonNullAssertion: cursorY/i are always kept within lines bounds
41+
cursorX = Math.min(cursorX, lines[cursorY]!.length) + deltaX;
4142
while (cursorX < 0 && cursorY > 0) {
4243
cursorY--;
43-
cursorX += lines[cursorY].length + 1;
44+
cursorX += lines[cursorY]!.length + 1;
4445
}
45-
while (cursorX > lines[cursorY].length && cursorY < lines.length - 1) {
46-
cursorX -= lines[cursorY].length + 1;
46+
while (cursorX > lines[cursorY]!.length && cursorY < lines.length - 1) {
47+
cursorX -= lines[cursorY]!.length + 1;
4748
cursorY++;
4849
}
49-
cursorX = Math.max(0, Math.min(lines[cursorY].length, cursorX));
50+
cursorX = Math.max(0, Math.min(lines[cursorY]!.length, cursorX));
5051

5152
let newCursor = 0;
5253
for (let i = 0; i < cursorY; i++) {
53-
newCursor += lines[i].length + 1;
54+
newCursor += lines[i]!.length + 1;
5455
}
56+
// biome-ignore-end lint/style/noNonNullAssertion: end of clamped cursor block
5557
return newCursor + cursorX;
5658
}

0 commit comments

Comments
 (0)