chore: upgrade eslint plugins, remove eslint-plugin-github

- Upgrade `typescript-eslint` to 8.61 and `eslint-plugin-unicorn` to 65,
  configuring the rules introduced in unicorn 65.
- Remove `eslint-plugin-github` and its workarounds: rules, the type-stub
  module declaration, the pnpm peer-dependency override, and the in-code
  `eslint-disable` comments that referenced its rules.
- Remove rules deprecated or deleted in their plugins and switch to their
  replacements (e.g. `prefer-single-call`, `no-object-constructor`,
  `id-denylist`, `no-unnecessary-slice-end`).
- Apply the resulting autofixes, mainly wrapping interpolated values in CSS
  selectors with `CSS.escape()`.

Assisted-by: claude-code:opus-4.8
This commit is contained in:
silverwind
2026-06-09 12:04:49 +02:00
parent 49a0d19fa3
commit 1b23df42df
18 changed files with 334 additions and 626 deletions
+2 -2
View File
@@ -5,14 +5,14 @@ export function initAdminUserListSearchForm(): void {
const form = document.querySelector<HTMLFormElement>('#user-list-search-form');
if (!form) return;
for (const button of form.querySelectorAll(`button[name=sort][value="${searchForm.SortType}"]`)) {
for (const button of form.querySelectorAll(`button[name=sort][value="${CSS.escape(searchForm.SortType)}"]`)) {
button.classList.add('active');
}
if (searchForm.StatusFilterMap) {
for (const [k, v] of Object.entries(searchForm.StatusFilterMap)) {
if (!v) continue;
for (const input of form.querySelectorAll<HTMLInputElement>(`input[name="status_filter[${k}]"][value="${v}"]`)) {
for (const input of form.querySelectorAll<HTMLInputElement>(`input[name="status_filter[${CSS.escape(k)}]"][value="${CSS.escape(v)}"]`)) {
input.checked = true;
}
}
+2 -2
View File
@@ -24,7 +24,7 @@ export function initGlobalDeleteButton(): void {
btn.addEventListener('click', (e) => {
e.preventDefault();
// eslint-disable-next-line github/no-dataset -- code depends on the camel-casing
// `dataset` is used here because the code below depends on it
const dataObj = btn.dataset;
const modalId = btn.getAttribute('data-modal-id');
@@ -156,7 +156,7 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) {
const [attrTargetName, attrTargetProp] = attrTargetCombo.split('.');
// try to find target by: "#target" -> "[name=target]" -> ".target" -> "<target> tag", and then try the modal itself
const attrTarget = elModal.querySelector(`#${attrTargetName}`) ||
elModal.querySelector(`[name=${attrTargetName}]`) ||
elModal.querySelector(`[name=${CSS.escape(attrTargetName)}]`) ||
elModal.querySelector(`.${attrTargetName}`) ||
elModal.querySelector(`${attrTargetName}`) ||
(elModal.matches(`${attrTargetName}`) || elModal.matches(`#${attrTargetName}`) || elModal.matches(`.${attrTargetName}`) ? elModal : null);
+1 -1
View File
@@ -245,7 +245,7 @@ async function onLocationHashChange() {
const issueCommentPrefix = '#issuecomment-';
if (currentHash.startsWith(issueCommentPrefix)) {
const commentId = currentHash.substring(issueCommentPrefix.length);
const expandButton = document.querySelector<HTMLElement>(`.code-expander-button[data-hidden-comment-ids*=",${commentId},"]`);
const expandButton = document.querySelector<HTMLElement>(`.code-expander-button[data-hidden-comment-ids*=",${CSS.escape(commentId)},"]`);
if (expandButton) {
// avoid infinite loop, do not re-click the button if already clicked
const attrAutoLoadClicked = 'data-auto-load-clicked';
+1 -1
View File
@@ -192,7 +192,7 @@ function initPinRemoveButton() {
// Delete the tooltip
el._tippy.destroy();
// Remove the Card
el.closest(`div.issue-card[data-issue-id="${id}"]`)!.remove();
el.closest(`div.issue-card[data-issue-id="${CSS.escape(String(id))}"]`)!.remove();
}
});
}
+2 -2
View File
@@ -130,9 +130,9 @@ export function initRepoIssueCommentDelete() {
// on the Conversation page, there is no parent "tr", so no need to do anything for "add-code-comment"
if (lineType) {
if (lineType === 'same') {
document.querySelector(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`)!.classList.remove('tw-invisible');
document.querySelector(`[data-path="${CSS.escape(String(path))}"] .add-code-comment[data-idx="${CSS.escape(String(idx))}"]`)!.classList.remove('tw-invisible');
} else {
document.querySelector(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`)!.classList.remove('tw-invisible');
document.querySelector(`[data-path="${CSS.escape(String(path))}"] .add-code-comment[data-side="${CSS.escape(String(side))}"][data-idx="${CSS.escape(String(idx))}"]`)!.classList.remove('tw-invisible');
}
}
conversationHolder.remove();
+2 -2
View File
@@ -120,11 +120,11 @@ function initRepoProjectColumnEdit(writableProjectBoard: Element): void {
}
// update the newly saved column title and color in the project board (to avoid reload)
const elEditButton = writableProjectBoard.querySelector<HTMLButtonElement>(`.show-project-column-modal-edit[${attrDataColumnId}="${columnId}"]`)!;
const elEditButton = writableProjectBoard.querySelector<HTMLButtonElement>(`.show-project-column-modal-edit[${CSS.escape(attrDataColumnId)}="${CSS.escape(columnId)}"]`)!;
elEditButton.setAttribute(attrDataColumnTitle, elColumnTitle.value);
elEditButton.setAttribute(attrDataColumnColor, elColumnColor.value);
const elBoardColumn = writableProjectBoard.querySelector<HTMLElement>(`.project-column[data-id="${columnId}"]`)!;
const elBoardColumn = writableProjectBoard.querySelector<HTMLElement>(`.project-column[data-id="${CSS.escape(columnId)}"]`)!;
const elBoardColumnTitle = elBoardColumn.querySelector<HTMLElement>(`.project-column-title-text`)!;
elBoardColumnTitle.textContent = elColumnTitle.value;
if (elColumnColor.value) {
+1 -1
View File
@@ -12,7 +12,7 @@ export function initRepoReleaseNew() {
registerGlobalEventFunc('click', 'onReleaseEditAttachmentDelete', (el) => {
const uuid = el.getAttribute('data-uuid')!;
const id = el.getAttribute('data-id')!;
document.querySelector<HTMLInputElement>(`input[name='attachment-del-${uuid}']`)!.value = 'true';
document.querySelector<HTMLInputElement>(`input[name='attachment-del-${CSS.escape(uuid)}']`)!.value = 'true';
hideElem(`#attachment-${id}`);
});
registerGlobalInitFunc('initReleaseEditForm', (elForm: HTMLFormElement) => {
+1 -1
View File
@@ -188,7 +188,7 @@ function webAuthnError(errorType: ErrorType, message:string = '') {
if (errorType === 'general') {
elErrorMsg.textContent = message || 'unknown error';
} else {
const elTypedError = document.querySelector(`#webauthn-error [data-webauthn-error-msg=${errorType}]`);
const elTypedError = document.querySelector(`#webauthn-error [data-webauthn-error-msg=${CSS.escape(errorType)}]`);
if (elTypedError) {
elErrorMsg.textContent = `${elTypedError.textContent}${message ? ` ${message}` : ''}`;
} else {
+1 -1
View File
@@ -20,7 +20,7 @@ export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error', d
}
// compact the message to a data attribute to avoid too many duplicated messages
const msgCompact = `${msgType}-${msg.trim()}`.replace(/[^-\w\u{80}-\u{10FFFF}]+/gu, '');
let msgContainer = parentContainer.querySelector<HTMLDivElement>(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
let msgContainer = parentContainer.querySelector<HTMLDivElement>(`.js-global-error[data-global-error-msg-compact="${CSS.escape(msgCompact)}"]`);
if (!msgContainer) {
const el = document.createElement('div');
el.innerHTML = html`<div class="ui container js-global-error tw-my-[--page-spacing]"><details class="ui ${msgType} message"><summary></summary></details></div>`;
+1 -1
View File
@@ -8,7 +8,7 @@ export function initTabSwitcher(tabItemContainer: Element) {
for (const elItem of tabItems) {
const tabName = elItem.getAttribute('data-tab')!;
elItem.addEventListener('click', () => {
const elPanel = document.querySelector(`.ui.tab[data-tab="${tabName}"]`)!;
const elPanel = document.querySelector(`.ui.tab[data-tab="${CSS.escape(tabName)}"]`)!;
queryElemSiblings(elPanel, '.ui.tab', (el) => el.classList.remove('active'));
queryElemSiblings(elItem, '.item[data-tab]', (el) => el.classList.remove('active'));
elItem.classList.add('active');
+2 -3
View File
@@ -9,8 +9,8 @@ type ElementsCallback<T extends Element> = (el: T) => Promisable<any>;
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]): ArrayLikeIterable<Element> {
if (typeof el === 'string' || el instanceof String) {
el = document.querySelectorAll(el as string);
if (typeof el === 'string') {
el = document.querySelectorAll(el);
}
if (el instanceof Node) {
func(el, ...args);
@@ -268,7 +268,6 @@ export function isElemVisible(el: HTMLElement): boolean {
export function createElementFromHTML<T extends Element>(htmlString: string): T {
htmlString = htmlString.trim();
// There is no way to create some elements without a proper parent, jQuery's approach: https://github.com/jquery/jquery/blob/main/src/manipulation/wrapMap.js
// eslint-disable-next-line github/unescaped-html-literal
if (htmlString.startsWith('<tr')) {
const container = document.createElement('table');
container.innerHTML = htmlString;
+1 -1
View File
@@ -37,7 +37,7 @@ export function firstStartDateAfterDate(inputDate: Date): number {
}
const dayOfWeek = inputDate.getUTCDay();
const daysUntilSunday = 7 - dayOfWeek;
const resultDate = new Date(inputDate.getTime());
const resultDate = new Date(inputDate);
resultDate.setUTCDate(resultDate.getUTCDate() + daysUntilSunday);
return resultDate.valueOf();
}
+1 -1
View File
@@ -54,6 +54,6 @@ export function linkifyURLs(html: string): string {
const cleanUrl = trailingPunct ? url.slice(0, -trailingPunct[0].length) : url;
const trailing = trailingPunct ? trailingPunct[0] : '';
// safe because regexp only matches valid URLs (no quotes or angle brackets)
return `<a href="${cleanUrl}" target="_blank">${cleanUrl}</a>${trailing}`; // eslint-disable-line github/unescaped-html-literal
return `<a href="${cleanUrl}" target="_blank">${cleanUrl}</a>${trailing}`;
});
}