chore: update eslint and stylelint configs and re-sync modern-normalize (#38982)

- update the vendored `modern-normalize` to v3.0.1
- require descriptions for lint disables in TS and CSS, same as we
already have in Go.
- disable core rules covered by `regexp/*` and `unicorn/*`, and ones
that cannot fire
- stop applying vitest rules to the playwright files in `tests/e2e`
- enable 7 stylelint rules, mostly `no-unknown` and `no-invalid` checks
- drop 2 unnecessary vendor prefixes (safari v17+, chrome v120+)
- look up ids via `querySelector` with `CSS.escape` instead of
`getElementById`
- remove stale doc about `@ts-expect-error`, it's forbidden
- misc dev doc fixes

Every declaration that `modern-normalize` v3 removes was checked against
chromium, webkit and firefox defaults first. The `hr` color and the
`:-moz-focusring` outline are kept as documented deviations, dropping
those does change rendering.

---------

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
silverwind
2026-08-20 15:57:56 +02:00
committed by GitHub
parent c778c6c920
commit 61be9fcdfa
32 changed files with 97 additions and 137 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ import {POST} from '../../modules/fetch.ts';
import {registerGlobalInitFunc} from '../../modules/observer.ts';
import {queryElems} from '../../utils/dom.ts';
import {errorMessage} from '../../modules/errors.ts';
import {submitFormFetchAction} from '../common-fetch-action.ts';
import {submitFormFetchAction} from '../../modules/fetch-action.ts';
import {cutString} from '../../utils/string.ts';
const {appSubUrl} = window.config;
+5 -8
View File
@@ -71,19 +71,16 @@ export function initGlobalDropdown() {
action: 'hide',
onShow() {
// hide associated tooltip while dropdown is open
this._tippy?.hide();
this._tippy?.disable();
el._tippy?.hide();
el._tippy?.disable();
},
onHide() {
this._tippy?.enable();
// eslint-disable-next-line unicorn/no-this-assignment
const elDropdown = this;
el._tippy?.enable();
// hide all tippy elements of items after a while. eg: use Enter to click "Copy Link" in the Issue Context Menu
// hide all tippy elements of items after a while, in case some items have tooltip popup.
setTimeout(() => {
const $dropdown = fomanticQuery(elDropdown);
if ($dropdown.dropdown('is hidden')) {
queryElems(elDropdown, '.menu > .item', (el) => el._tippy?.hide());
queryElems(el, '.menu > .item', (item) => item._tippy?.hide());
}
}, 2000);
},
+1 -1
View File
@@ -13,7 +13,7 @@ export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: st
textarea.focus();
let success = false;
try {
success = document.execCommand('insertText', false, text); // eslint-disable-line @typescript-eslint/no-deprecated
success = document.execCommand('insertText', false, text); // eslint-disable-line @typescript-eslint/no-deprecated -- no replacement keeps the undo history
} catch {}
// fall back to regular replacement
+1 -1
View File
@@ -1,6 +1,6 @@
import {toggleElem} from '../../utils/dom.ts';
import {showFomanticModal} from '../../modules/fomantic/modal.ts';
import {submitFormFetchAction} from '../common-fetch-action.ts';
import {submitFormFetchAction} from '../../modules/fetch-action.ts';
function nameHasScope(name: string): boolean {
return /.*[^/]\/[^/].*/.test(name);
+2 -4
View File
@@ -11,7 +11,7 @@ import {createTippy} from '../modules/tippy.ts';
import {invertFileFolding} from './file-fold.ts';
import {parseDom} from '../utils.ts';
import {registerGlobalEventFunc, registerGlobalInitFunc} from '../modules/observer.ts';
import {performFetchActionTrigger} from './common-fetch-action.ts';
import {performFetchActionTrigger} from '../modules/fetch-action.ts';
import {initImageDiff} from './imagediff.ts';
function initDiffFileViewToggle(el: HTMLElement) {
@@ -206,9 +206,7 @@ async function onLocationHashChange() {
const targetElementId = currentHash.substring(1);
while (currentHash === window.location.hash) {
// use getElementById to avoid querySelector throws an error when the hash is invalid
// eslint-disable-next-line unicorn/prefer-query-selector
const targetElement = document.getElementById(targetElementId);
const targetElement = document.querySelector<HTMLElement>(`#${CSS.escape(targetElementId)}`);
if (targetElement) {
// need to change hash to re-trigger ":target" CSS selector, let's manually scroll to it
targetElement.scrollIntoView();
+1 -1
View File
@@ -6,7 +6,7 @@ import {POST} from '../modules/fetch.ts';
import {initDropzone} from './dropzone.ts';
import {confirmModal} from './comp/ConfirmModal.ts';
import {applyAreYouSure, ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
import {submitFormFetchAction} from './common-fetch-action.ts';
import {submitFormFetchAction} from '../modules/fetch-action.ts';
import {dirname} from '../utils.ts';
import {pathEscapeSegments} from '../utils/url.ts';
import {showErrorToast} from '../modules/toast.ts';
+1 -1
View File
@@ -4,7 +4,7 @@ import {confirmModal} from './comp/ConfirmModal.ts';
import {createSortable} from '../modules/sortable.ts';
import {DELETE, POST} from '../modules/fetch.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {performFetchAction} from './common-fetch-action.ts';
import {performFetchAction} from '../modules/fetch-action.ts';
import type {SortableEvent} from 'sortablejs';
function initRepoIssueListCheckboxes() {
+3 -3
View File
@@ -48,7 +48,7 @@ async function initRepoProjectSortable(): Promise<void> {
handle: '.project-column-header',
delayOnTouchOnly: true,
delay: 500,
onSort: async () => { // eslint-disable-line @typescript-eslint/no-misused-promises
onSort: async () => { // eslint-disable-line @typescript-eslint/no-misused-promises -- Sortable ignores the returned promise, the body catches its own errors
boardColumns = mainBoard.querySelectorAll<HTMLElement>('.project-column');
const columnSorting = {
@@ -72,8 +72,8 @@ async function initRepoProjectSortable(): Promise<void> {
const boardCardList = boardColumn.querySelector<HTMLElement>('.cards')!;
createSortable(boardCardList, {
group: 'shared',
onAdd: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises
onUpdate: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises
onAdd: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises -- Sortable ignores the returned promise, moveIssue catches its own errors
onUpdate: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises -- Sortable ignores the returned promise, moveIssue catches its own errors
delayOnTouchOnly: true,
delay: 500,
});
+1 -1
View File
@@ -32,7 +32,7 @@ export async function attachTribute(element: HTMLElement) {
};
const mentionCollection: TributeCollection<Mention> = {
values: async (_query: string, cb: (matches: Mention[]) => void) => { // eslint-disable-line @typescript-eslint/no-misused-promises
values: async (_query: string, cb: (matches: Mention[]) => void) => { // eslint-disable-line @typescript-eslint/no-misused-promises -- tributejs ignores the returned promise, results arrive via the callback
cb(mentionsUrl ? await fetchMentions(mentionsUrl) : []);
},
requireLeadingSpace: true,
+1 -1
View File
@@ -1,4 +1,4 @@
import jquery from 'jquery'; // eslint-disable-line no-restricted-imports
import jquery from 'jquery'; // eslint-disable-line no-restricted-imports -- this is where the global $ comes from
// Some users still use inline scripts and expect jQuery to be available globally.
// To avoid breaking existing users and custom plugins, import jQuery globally without ES module.
+1 -1
View File
@@ -55,7 +55,7 @@ import {initRepositorySearch} from './features/repo-search.ts';
import {initColorPickers} from './features/colorpicker.ts';
import {initAdminSelfCheck} from './features/admin/selfcheck.ts';
import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts';
import {initGlobalFetchAction} from './features/common-fetch-action.ts';
import {initGlobalFetchAction} from './modules/fetch-action.ts';
import {initCommmPageComponents, initGlobalComponent, initGlobalDropdown, initGlobalInput} from './features/common-page.ts';
import {initGlobalButtonClickOnEnter, initGlobalButtons} from './features/common-button.ts';
import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts';
+2 -4
View File
@@ -29,15 +29,13 @@ function scrollToAnchor(encodedId?: string): void {
if (!elemId) return;
const prefixedId = addPrefix(elemId);
// eslint-disable-next-line unicorn/prefer-query-selector
let el = document.getElementById(prefixedId);
let el = document.querySelector<HTMLElement>(`#${CSS.escape(prefixedId)}`);
// check for matching user-generated `a[name]`
el = el ?? document.querySelector(`a[name="${CSS.escape(prefixedId)}"]`);
// compat for links with old 'user-content-' prefixed hashes
// eslint-disable-next-line unicorn/prefer-query-selector
el = (!el && hasPrefix(elemId)) ? document.getElementById(elemId) : el;
el = (!el && hasPrefix(elemId)) ? document.querySelector<HTMLElement>(`#${CSS.escape(elemId)}`) : el;
el?.scrollIntoView();
}
+1 -1
View File
@@ -24,7 +24,7 @@ describe('navigateToIframeLink', () => {
const navigations = captureNavigations();
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
// eslint-disable-next-line no-script-url
// eslint-disable-next-line no-script-url -- the test asserts that javascript: links are rejected
navigateToIframeLink('javascript:void(0);', '_blank');
navigateToIframeLink('data:image/svg+xml;utf8,<svg></svg>', '');
expect(openSpy).toHaveBeenCalledTimes(0);
@@ -1,4 +1,4 @@
import {execPseudoSelectorCommands, handleFetchActionErrorFields, handleFetchActionSuccessJson} from './common-fetch-action.ts';
import {execPseudoSelectorCommands, handleFetchActionErrorFields, handleFetchActionSuccessJson} from './fetch-action.ts';
import {createElementFromHTML} from '../utils/dom.ts';
import {captureNavigations, normalizeTestHtml} from '../utils/testhelper.ts';
@@ -1,10 +1,10 @@
import {GET, request} from '../modules/fetch.ts';
import {hideToastsAll, showErrorToast} from '../modules/toast.ts';
import {GET, request} from './fetch.ts';
import {hideToastsAll, showErrorToast} from './toast.ts';
import {activePageTimerRefresh, addDelegatedEventListener, createElementFromHTML, queryElems} from '../utils/dom.ts';
import {errorMessage, errorName} from '../modules/errors.ts';
import {confirmModal, createConfirmModal} from './comp/ConfirmModal.ts';
import {errorMessage, errorName} from './errors.ts';
import {confirmModal, createConfirmModal} from '../features/comp/ConfirmModal.ts';
import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
import {registerGlobalSelectorFunc} from '../modules/observer.ts';
import {registerGlobalSelectorFunc} from './observer.ts';
import {Idiomorph} from 'idiomorph';
import {parseDom} from '../utils.ts';
import {html} from '../utils/html.ts';
@@ -233,9 +233,8 @@ export async function submitFormFetchAction(elForm: HTMLFormElement, opts: Submi
async function confirmFetchAction(el: HTMLElement) {
let elModal: HTMLElement | null = null;
const dataModalConfirm = el.getAttribute('data-modal-confirm') || '';
if (dataModalConfirm.startsWith('#')) {
// eslint-disable-next-line unicorn/prefer-query-selector
elModal = document.getElementById(dataModalConfirm.substring(1));
if (dataModalConfirm.startsWith('#') && dataModalConfirm.length > 1) {
elModal = document.querySelector<HTMLElement>(`#${CSS.escape(dataModalConfirm.substring(1))}`);
if (elModal) {
elModal = createElementFromHTML(elModal.outerHTML);
elModal.removeAttribute('id');
@@ -350,8 +349,7 @@ async function fetchActionReloadOutdatedElements() {
const newPageHtml = await resp.text();
const newPageDom = parseDom(newPageHtml, 'text/html');
for (const oldEl of outdatedElems) {
// eslint-disable-next-line unicorn/prefer-query-selector
const newEl = newPageDom.getElementById(oldEl.id);
const newEl = newPageDom.querySelector<HTMLElement>(`#${CSS.escape(oldEl.id)}`);
if (newEl) {
oldEl.replaceWith(newEl);
} else {
+1 -1
View File
@@ -18,7 +18,7 @@ export function request(url: string, {method = 'GET', data, headers = {}, ...oth
if (!headers.has('content-type') && contentType) {
headers.set('content-type', contentType);
}
return fetch(url, { // eslint-disable-line no-restricted-globals
return fetch(url, { // eslint-disable-line no-restricted-globals -- this is the wrapper the rule points to
method,
headers,
...other,
+1 -1
View File
@@ -1,4 +1,4 @@
/* eslint-disable no-restricted-globals */
/* eslint-disable no-restricted-globals -- this is the wrapper the rule points to */
// Some people deploy Gitea under a subpath, so it needs prefix to avoid local storage key conflicts.
// And these keys are for user settings only, it also needs a specific prefix,
// in case in the future there are other uses of local storage, and/or we need to clear some keys when the quota is exceeded.
+1 -1
View File
@@ -24,7 +24,7 @@ let lastWorker: MockSharedWorker;
class MockSharedWorker {
port = new MockMessagePort();
// eslint-disable-next-line unicorn/no-this-assignment
// eslint-disable-next-line unicorn/no-this-assignment -- the test needs a handle on the instance the module constructs
constructor() { lastWorker = this }
addEventListener() {}
}
+1 -1
View File
@@ -4,7 +4,7 @@ import {initSwaggerUI} from './render/swagger.ts';
async function initGiteaAPIViewer() {
const elSwaggerUi = document.querySelector<HTMLElement>('#swagger-ui')!;
const url = elSwaggerUi.getAttribute('data-source')!;
const res = await fetch(url); // eslint-disable-line no-restricted-globals
const res = await fetch(url); // eslint-disable-line no-restricted-globals -- standalone entry, it must not pull in main site modules
// HINT: SWAGGER-CSS-IMPORT: this is used in the standalone page which already has the related CSS imported by `<link>`
await initSwaggerUI(elSwaggerUi, {specText: await res.text()});
}
+1 -1
View File
@@ -5,7 +5,7 @@ const pngPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91
const pngEmpty = 'data:image/png;base64,';
async function dataUriToBlob(datauri: string) {
return await (await globalThis.fetch(datauri)).blob(); // eslint-disable-line no-restricted-properties
return await (await globalThis.fetch(datauri)).blob(); // eslint-disable-line no-restricted-properties -- decodes a data URI, the fetch wrapper adds nothing here
}
test('pngChunks', async () => {
+1 -3
View File
@@ -236,9 +236,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
// check whether the mandatory `.overflow-menu-items` element is present initially which happens
// with Vue which renders differently than browsers. If it's not there, like in the case of browser
// template rendering, wait for its addition.
// The eslint rule is not sophisticated enough or aware of this problem, see
// https://github.com/43081j/eslint-plugin-wc/pull/130
const menuItemsEl = this.querySelector<HTMLElement>('.overflow-menu-items'); // eslint-disable-line wc/no-child-traversal-in-connectedcallback
const menuItemsEl = this.querySelector<HTMLElement>('.overflow-menu-items'); // eslint-disable-line wc/no-child-traversal-in-connectedcallback -- the observer below covers the case the rule warns about, see https://github.com/43081j/eslint-plugin-wc/pull/130
if (menuItemsEl) {
this.menuItemsEl = menuItemsEl;
this.init();