From 6394af0a59927da497c2f828f6a2f2d971db27da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kuna?= <1282324+codecalm@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:55:13 +0200 Subject: [PATCH] Add ESLint with typescript-eslint and fix its findings (#3256) --- .build/check-compat-fixture.ts | 6 +- .build/check-compat.ts | 16 +- .build/check-html.ts | 8 +- .build/check-markup-classes.ts | 14 +- .build/generate-tokens.ts | 5 +- .build/html-diff.ts | 7 +- .build/reformat-mdx.mts | 8 +- .build/vite.config.helper.mts | 1 - .changeset/eslint-js-linter.md | 7 + .eslintrc | 10 - core/js/src/bootstrap/button.ts | 2 +- core/js/src/bootstrap/carousel.ts | 2 +- core/js/src/bootstrap/dom/data.ts | 1 - core/js/src/bootstrap/dom/event-handler.ts | 4 +- core/js/src/bootstrap/dom/manipulator.ts | 2 +- core/js/src/bootstrap/dropdown.ts | 18 +- core/js/src/bootstrap/scrollspy.ts | 8 +- core/js/src/bootstrap/tab.ts | 2 +- core/js/src/bootstrap/tooltip.ts | 8 +- core/js/src/bootstrap/util/config.ts | 4 +- core/js/src/bootstrap/util/focustrap.ts | 2 +- core/js/src/bootstrap/util/index.ts | 4 +- core/js/src/bootstrap/util/sanitizer.ts | 2 +- core/js/src/bootstrap/util/scrollbar.ts | 2 + core/js/src/confetti.ts | 6 +- core/js/src/datepicker.ts | 15 +- core/js/src/deprecated.ts | 2 +- core/js/src/sparkline.ts | 4 +- core/js/tests/unit/tab.spec.ts | 3 +- core/js/tests/unit/util/index.spec.ts | 2 +- core/package.json | 2 +- docs/lib/llms.ts | 6 +- docs/lib/scss-docs.ts | 2 +- eslint.config.mjs | 118 +++ package.json | 14 +- pnpm-lock.yaml | 933 +++++++++++++++------ screenshots/.build/capture.ts | 4 +- screenshots/.build/record.ts | 16 +- shared/lib/chart-script.ts | 2 +- shared/lib/theme-config.ts | 2 +- 40 files changed, 935 insertions(+), 339 deletions(-) create mode 100644 .changeset/eslint-js-linter.md delete mode 100644 .eslintrc create mode 100644 eslint.config.mjs diff --git a/.build/check-compat-fixture.ts b/.build/check-compat-fixture.ts index 027cb2d18..a216ce9dd 100644 --- a/.build/check-compat-fixture.ts +++ b/.build/check-compat-fixture.ts @@ -27,7 +27,7 @@ import { pkgName, releasedPackage } from './released-package' const fixtureDir = '.build/compat-fixture' const packageArg = process.argv.indexOf('--package') -const coreDir = packageArg > -1 ? process.argv[packageArg + 1]! : 'core' +const coreDir = packageArg > -1 ? process.argv[packageArg + 1] : 'core' const MIME: Record = { '.html': 'text/html', '.css': 'text/css', '.js': 'text/javascript', '.svg': 'image/svg+xml', '.map': 'application/json' } @@ -61,7 +61,7 @@ function sassResults(pkg: string): Result { function serve(pkg: string): Promise { const server = createServer((request, response) => { - const path = decodeURIComponent((request.url ?? '/').split('?')[0]!) + const path = decodeURIComponent((request.url ?? '/').split('?')[0]) const file = path === '/' ? resolve(fixtureDir, 'page.html') : resolve(pkg, `.${path}`) if (!existsSync(file) || !statSync(file).isFile()) { response.writeHead(404).end() @@ -86,7 +86,7 @@ async function pageResults(pkg: string, page: Page): Promise { await page.goto(`http://127.0.0.1:${port}/`, { waitUntil: 'networkidle' }) await page.addStyleTag({ content: '*, *::before, *::after { transition: none !important; }' }) - const style = (selector: string, property: string): Promise => page.evaluate(([s, p]) => getComputedStyle(document.querySelector(s!)!).getPropertyValue(p!), [selector, property]) + const style = (selector: string, property: string): Promise => page.evaluate(([s, p]) => getComputedStyle(document.querySelector(s)!).getPropertyValue(p), [selector, property]) results['page: no script error and no 404'] = problems.length === 0 ? 'yes' : problems.join('; ') results['page: rgba(var(--tblr-primary-rgb), .5) paints'] = await style('#highlight', 'background-color') diff --git a/.build/check-compat.ts b/.build/check-compat.ts index ae3efa014..7401c97d9 100644 --- a/.build/check-compat.ts +++ b/.build/check-compat.ts @@ -48,7 +48,7 @@ function walk(root: string, dir = root): string[] { }) } -const matches = (source: string, pattern: RegExp): Set => new Set([...source.matchAll(pattern)].map((match) => match[1]!)) +const matches = (source: string, pattern: RegExp): Set => new Set([...source.matchAll(pattern)].map((match) => match[1])) // --- css ------------------------------------------------------------------- @@ -106,7 +106,7 @@ function splitParams(source: string | undefined): string[] { } params.push(current) // `$color: rgba($white, 0.15)` → `$color`; `$args...` → `$args...` - return params.map((param) => param.trim().split(':')[0]!.trim()).filter(Boolean) + return params.map((param) => param.trim().split(':')[0].trim()).filter(Boolean) } function sassSurface(root: string): { variables: Set; callables: Map } { @@ -117,8 +117,8 @@ function sassSurface(root: string): { variables: Set; callables: Map { } }` const { css } = compileString(source, { loadPaths: [root, 'node_modules'], logger: { warn() {}, debug() {} } }) - return new Map([...css.matchAll(/--([\w-]+): (\w+);/g)].map((match) => [match[1]!, match[2]!])) + return new Map([...css.matchAll(/--([\w-]+): (\w+);/g)].map((match) => [match[1], match[2]])) } function sassTypeBreaks(released: string, current: string): string[] { @@ -175,7 +175,7 @@ function esmExports(file: string): Set { const source = readFileSync(file, 'utf8') const names = new Set() for (const [, list] of source.matchAll(/^export\s*\{([^}]*)\}/gm)) { - for (const entry of list!.split(',')) { + for (const entry of list.split(',')) { const name = entry .trim() .split(/\s+as\s+/) @@ -220,7 +220,7 @@ async function main() { const lines = readFileSync(baselineFile, 'utf8').split('\n') const againstIndex = lines.findIndex((line) => againstPattern.test(line)) if (againstIndex === -1) throw new Error(`${baselineFile} has no \`# against: \` line`) - const against = againstPattern.exec(lines[againstIndex]!)![1]! + const against = againstPattern.exec(lines[againstIndex])![1] if (reset) { writeFileSync(baselineFile, [...lines.slice(0, againstIndex), `# against: ${version}`, ''].join('\n')) @@ -233,7 +233,7 @@ async function main() { const baseline = new Map() // key → accepted for (const line of stale ? [] : lines) { const [entry, comment = ''] = line.split(' # ') - const key = entry!.trim() + const key = entry.trim() if (!key || key.startsWith('#')) continue baseline.set(key, comment.trim().startsWith('accepted:')) } diff --git a/.build/check-html.ts b/.build/check-html.ts index 18c5d253f..336215832 100644 --- a/.build/check-html.ts +++ b/.build/check-html.ts @@ -52,7 +52,7 @@ const CONSOLE_IGNORE = [/GL Driver Message/, /Google Maps JavaScript API/, /allo type PageReport = { url: string; errors: string[]; warnings: string[] } const resolveFile = (root: string, urlPath: string): string | null => { - const clean = decodeURIComponent(urlPath.split('?')[0]!.split('#')[0]!) + const clean = decodeURIComponent(urlPath.split('?')[0].split('#')[0]) // `/x/` and `/x/index.html` also resolve to x.html: Astro's file format // collapses x/index.astro to x.html and the hosting serves it under both. const collapsed = clean.replace(/\/(index\.html)?$/, '') @@ -87,7 +87,7 @@ const serve = (root: string): Promise<{ server: Server; base: string }> => const sitemapPaths = async (base: string, dir?: string): Promise => { const xml = dir ? readFileSync(join(dir, 'sitemap.xml'), 'utf8') : await (await fetch(`${base}/sitemap.xml`)).text() - return [...xml.matchAll(/([^<]+)<\/loc>/g)].map((m) => m[1]!.replace(/^https?:\/\/[^/]+/, '')).filter((p) => p.startsWith('/')) + return [...xml.matchAll(/([^<]+)<\/loc>/g)].map((m) => m[1].replace(/^https?:\/\/[^/]+/, '')).filter((p) => p.startsWith('/')) } // Runs in the page as plain JavaScript (a string, so no bundler helpers such as @@ -162,14 +162,14 @@ const crawl = async (browser: Browser, base: string, paths: string[]) => { await page.goto(base + path, { waitUntil: 'load', timeout: 60_000 }) await page.waitForTimeout(500) if (onSite()) { - const r = (await page.evaluate(INSPECT)) as Inspection + const r = await page.evaluate(INSPECT) for (const id of r.duplicateIds) if (!VENDOR_ID_PREFIXES.some((p) => id.startsWith(p))) report.errors.push(`duplicate id "${id}"`) for (const t of r.missingTargets) report.errors.push(`no element for ${t}`) for (const n of r.nested) report.errors.push(`nested interactive: ${n}`) for (const src of r.imgNoAlt) report.warnings.push(`img without alt: ${src}`) for (const html of r.unnamed) report.warnings.push(`no accessible name: ${html}`) for (const href of r.links) { - const key = href.split(/[?#]/)[0]! + const key = href.split(/[?#]/)[0] if (!links.has(key)) links.set(key, path) } } diff --git a/.build/check-markup-classes.ts b/.build/check-markup-classes.ts index 21276bd92..0ceb548cf 100644 --- a/.build/check-markup-classes.ts +++ b/.build/check-markup-classes.ts @@ -23,8 +23,8 @@ const htmlTargets: string[] = [] const cssDirs: string[] = [] let updateBaseline = false for (let i = 0; i < args.length; i++) { - if (args[i] === '--html') htmlTargets.push(args[++i]!) - else if (args[i] === '--css') cssDirs.push(args[++i]!) + if (args[i] === '--html') htmlTargets.push(args[++i]) + else if (args[i] === '--css') cssDirs.push(args[++i]) else if (args[i] === '--update-baseline') updateBaseline = true } if (htmlTargets.length === 0 || cssDirs.length === 0) { @@ -41,14 +41,14 @@ const walk = (dir: string, ext: string): string[] => const defined = new Set() for (const dir of cssDirs) { for (const file of walk(resolve(dir), '.css')) { - for (const m of readFileSync(file, 'utf8').matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)) defined.add(m[1]!) + for (const m of readFileSync(file, 'utf8').matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)) defined.add(m[1]) } } const pagesOf = async (target: string): Promise<{ path: string; html: string }[]> => { if (/^https?:/.test(target)) { const xml = await (await fetch(`${target}/sitemap.xml`)).text() - const paths = [...xml.matchAll(/([^<]+)<\/loc>/g)].map((m) => m[1]!.replace(/^https?:\/\/[^/]+/, '')) + const paths = [...xml.matchAll(/([^<]+)<\/loc>/g)].map((m) => m[1].replace(/^https?:\/\/[^/]+/, '')) const pages = [] for (const path of paths) pages.push({ path, html: await (await fetch(target + path)).text() }) return pages @@ -56,7 +56,7 @@ const pagesOf = async (target: string): Promise<{ path: string; html: string }[] const root = resolve(target) const xml = readFileSync(join(root, 'sitemap.xml'), 'utf8') return [...xml.matchAll(/([^<]+)<\/loc>/g)].map((m) => { - const path = m[1]!.replace(/^https?:\/\/[^/]+/, '') + const path = m[1].replace(/^https?:\/\/[^/]+/, '') const file = [join(root, path), join(root, `${path}.html`), join(root, path, 'index.html')].find((f) => existsSync(f) && statSync(f).isFile()) return { path, html: file ? readFileSync(file, 'utf8') : '' } }) @@ -70,9 +70,9 @@ const main = async () => { pageCount++ // Page-level