From 02f7808de43482f9f72e8d12b8cb17532b95db75 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 30 Oct 2025 19:08:27 +0100 Subject: [PATCH 01/11] Fix a number of `strictNullChecks`-related issues --- eslint.config.ts | 1 + tailwind.config.ts | 5 +++-- tests/e2e/example.test.e2e.ts | 3 ++- tools/generate-svg.ts | 21 ++++++++++++++++----- tools/utils.ts | 3 +++ tsconfig.json | 1 + web_src/js/features/repo-issue.ts | 2 +- webpack.config.ts | 7 ++++--- 8 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 tools/utils.ts diff --git a/eslint.config.ts b/eslint.config.ts index ed2bc6524584b..9557f5caaf668 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -905,6 +905,7 @@ export default defineConfig([ files: ['tests/e2e/**'], rules: { ...playwright.configs['flat/recommended'].rules, + 'playwright/no-conditional-in-test': [0], }, }, { diff --git a/tailwind.config.ts b/tailwind.config.ts index 624ae47a5cd63..3c8ed3ce8122f 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -2,6 +2,7 @@ import {readFileSync} from 'node:fs'; import {env} from 'node:process'; import {parse} from 'postcss'; import plugin from 'tailwindcss/plugin.js'; +import {isTruthy} from './tools/utils.ts'; import type {Config} from 'tailwindcss'; const isProduction = env.NODE_ENV !== 'development'; @@ -37,7 +38,7 @@ export default { './{build,models,modules,routers,services}/**/*.go', './templates/**/*.tmpl', './web_src/js/**/*.{ts,js,vue}', - ].filter(Boolean), + ].filter(isTruthy), blocklist: [ // classes that don't work without CSS variables from "@tailwind base" which we don't use 'transform', 'shadow', 'ring', 'blur', 'grayscale', 'invert', '!invert', 'filter', '!filter', @@ -121,4 +122,4 @@ export default { }); }), ], -} satisfies Config; +} satisfies Config as Config; diff --git a/tests/e2e/example.test.e2e.ts b/tests/e2e/example.test.e2e.ts index 1689f1b8efc7e..14c34f96955c0 100644 --- a/tests/e2e/example.test.e2e.ts +++ b/tests/e2e/example.test.e2e.ts @@ -45,8 +45,9 @@ test('login', async ({page}, workerInfo) => { test('logged in user', async ({browser}, workerInfo) => { const context = await load_logged_in_context(browser, workerInfo, 'user2'); - const page = await context.newPage(); + if (!context) return; + const page = await context.newPage(); await page.goto('/'); // Make sure we routed to the home page. Else login failed. diff --git a/tools/generate-svg.ts b/tools/generate-svg.ts index b1dc46d4511b7..082c35429b50a 100755 --- a/tools/generate-svg.ts +++ b/tools/generate-svg.ts @@ -47,6 +47,11 @@ function processAssetsSvgFiles(pattern: string, opts: Opts = {}) { return glob(pattern).map((path) => processAssetsSvgFile(path, opts)); } +function lowercaseKeys(obj: Record | undefined) { + if (!obj) return obj; + return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value])); +} + async function processMaterialFileIcons() { const paths = glob('node_modules/material-icon-theme/icons/*.svg'); const svgSymbols: Record = {}; @@ -76,18 +81,24 @@ async function processMaterialFileIcons() { // * https://code.visualstudio.com/docs/languages/identifiers#_known-language-identifiers // * https://github.com/microsoft/vscode/tree/1.98.0/extensions delete iconRules.iconDefinitions; - for (const [k, v] of Object.entries(iconRules.fileNames)) iconRules.fileNames[k.toLowerCase()] = v; - for (const [k, v] of Object.entries(iconRules.folderNames)) iconRules.folderNames[k.toLowerCase()] = v; - for (const [k, v] of Object.entries(iconRules.fileExtensions)) iconRules.fileExtensions[k.toLowerCase()] = v; + + iconRules.fileNames = lowercaseKeys(iconRules.fileNames); + iconRules.folderNames = lowercaseKeys(iconRules.fileNames); + iconRules.fileExtensions = lowercaseKeys(iconRules.fileNames); + // Use VSCode's "Language ID" mapping from its extensions for (const [_, langIdExtMap] of Object.entries(vscodeExtensions)) { for (const [langId, names] of Object.entries(langIdExtMap)) { for (const name of names) { const nameLower = name.toLowerCase(); if (nameLower[0] === '.') { - iconRules.fileExtensions[nameLower.substring(1)] ??= langId; + if (iconRules.fileExtensions) { + iconRules.fileExtensions[nameLower.substring(1)] ??= langId; + } } else { - iconRules.fileNames[nameLower] ??= langId; + if (iconRules.fileNames) { + iconRules.fileNames[nameLower] ??= langId; + } } } } diff --git a/tools/utils.ts b/tools/utils.ts new file mode 100644 index 0000000000000..6b73b0e301708 --- /dev/null +++ b/tools/utils.ts @@ -0,0 +1,3 @@ +export function isTruthy(value: T): value is T extends false | '' | 0 | null | undefined ? never : T { // eslint-disable-line unicorn/prefer-native-coercion-functions + return Boolean(value); +} diff --git a/tsconfig.json b/tsconfig.json index 3bc6065647e4e..1daf4b7233b60 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -40,6 +40,7 @@ "strictBindCallApply": true, "strictBuiltinIteratorReturn": true, "strictFunctionTypes": true, + "strictNullChecks": false, "stripInternal": true, "verbatimModuleSyntax": true, "types": [ diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts index 58274b95c0a0c..bacdae53504be 100644 --- a/web_src/js/features/repo-issue.ts +++ b/web_src/js/features/repo-issue.ts @@ -261,7 +261,7 @@ export function initRepoPullRequestReview() { if (commentDiv) { // get the name of the parent id const groupID = commentDiv.closest('div[id^="code-comments-"]')?.getAttribute('id'); - if (groupID && groupID.startsWith('code-comments-')) { + if (groupID?.startsWith('code-comments-')) { const id = groupID.slice(14); const ancestorDiffBox = commentDiv.closest('.diff-file-box'); diff --git a/webpack.config.ts b/webpack.config.ts index 9f31c01e8865e..2d62e4870da46 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -12,6 +12,7 @@ import {readFileSync, globSync} from 'node:fs'; import {env} from 'node:process'; import tailwindcss from 'tailwindcss'; import tailwindConfig from './tailwind.config.ts'; +import {isTruthy} from './tools/utils.ts'; const {EsbuildPlugin} = EsBuildLoader; const {SourceMapDevToolPlugin, DefinePlugin, EnvironmentPlugin} = webpack; @@ -30,7 +31,7 @@ const isProduction = env.NODE_ENV !== 'development'; // false - all disabled let sourceMaps; if ('ENABLE_SOURCEMAP' in env) { - sourceMaps = ['true', 'false'].includes(env.ENABLE_SOURCEMAP) ? env.ENABLE_SOURCEMAP : 'reduced'; + sourceMaps = ['true', 'false'].includes(env.ENABLE_SOURCEMAP || '') ? env.ENABLE_SOURCEMAP : 'reduced'; } else { sourceMaps = isProduction ? 'reduced' : 'true'; } @@ -95,7 +96,7 @@ export default { path: fileURLToPath(new URL('public/assets', import.meta.url)), filename: () => 'js/[name].js', chunkFilename: ({chunk}) => { - const language = (/monaco.*languages?_.+?_(.+?)_/.exec(String(chunk.id)) || [])[1]; + const language = (/monaco.*languages?_.+?_(.+?)_/.exec(String(chunk?.id)) || [])[1]; return `js/${language ? `monaco-language-${language.toLowerCase()}` : `[name]`}.[contenthash:8].js`; }, }, @@ -270,7 +271,7 @@ export default { excludeAssets: [ /^js\/monaco-language-.+\.js$/, !isProduction && /^licenses.txt$/, - ].filter(Boolean), + ].filter(isTruthy), groupAssetsByChunk: false, groupAssetsByEmitStatus: false, groupAssetsByInfo: false, From bd95b698d69a353518b91715a63a4822807d0191 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 30 Oct 2025 19:15:40 +0100 Subject: [PATCH 02/11] use typeof --- webpack.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webpack.config.ts b/webpack.config.ts index 2d62e4870da46..33f850c9a7b42 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -30,8 +30,8 @@ const isProduction = env.NODE_ENV !== 'development'; // reduced - minimal sourcemaps, the default in production // false - all disabled let sourceMaps; -if ('ENABLE_SOURCEMAP' in env) { - sourceMaps = ['true', 'false'].includes(env.ENABLE_SOURCEMAP || '') ? env.ENABLE_SOURCEMAP : 'reduced'; +if (typeof env.ENABLE_SOURCEMAP === 'string') { + sourceMaps = ['true', 'false'].includes(env.ENABLE_SOURCEMAP) ? env.ENABLE_SOURCEMAP : 'reduced'; } else { sourceMaps = isProduction ? 'reduced' : 'true'; } From a106694b2681fc09006cdc6d7b0c18da456f4c5d Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 30 Oct 2025 19:18:50 +0100 Subject: [PATCH 03/11] Revert "use typeof" This reverts commit bd95b698d69a353518b91715a63a4822807d0191. --- webpack.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webpack.config.ts b/webpack.config.ts index 33f850c9a7b42..2d62e4870da46 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -30,8 +30,8 @@ const isProduction = env.NODE_ENV !== 'development'; // reduced - minimal sourcemaps, the default in production // false - all disabled let sourceMaps; -if (typeof env.ENABLE_SOURCEMAP === 'string') { - sourceMaps = ['true', 'false'].includes(env.ENABLE_SOURCEMAP) ? env.ENABLE_SOURCEMAP : 'reduced'; +if ('ENABLE_SOURCEMAP' in env) { + sourceMaps = ['true', 'false'].includes(env.ENABLE_SOURCEMAP || '') ? env.ENABLE_SOURCEMAP : 'reduced'; } else { sourceMaps = isProduction ? 'reduced' : 'true'; } From 6b2216558f0ad740fcf73b0572511753739a6e53 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 31 Oct 2025 08:33:22 +0100 Subject: [PATCH 04/11] use as any --- tailwind.config.ts | 3 +-- tools/utils.ts | 3 --- webpack.config.ts | 3 +-- 3 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 tools/utils.ts diff --git a/tailwind.config.ts b/tailwind.config.ts index 3c8ed3ce8122f..39abba2da0e97 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -2,7 +2,6 @@ import {readFileSync} from 'node:fs'; import {env} from 'node:process'; import {parse} from 'postcss'; import plugin from 'tailwindcss/plugin.js'; -import {isTruthy} from './tools/utils.ts'; import type {Config} from 'tailwindcss'; const isProduction = env.NODE_ENV !== 'development'; @@ -38,7 +37,7 @@ export default { './{build,models,modules,routers,services}/**/*.go', './templates/**/*.tmpl', './web_src/js/**/*.{ts,js,vue}', - ].filter(isTruthy), + ].filter(Boolean as any), blocklist: [ // classes that don't work without CSS variables from "@tailwind base" which we don't use 'transform', 'shadow', 'ring', 'blur', 'grayscale', 'invert', '!invert', 'filter', '!filter', diff --git a/tools/utils.ts b/tools/utils.ts deleted file mode 100644 index 6b73b0e301708..0000000000000 --- a/tools/utils.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function isTruthy(value: T): value is T extends false | '' | 0 | null | undefined ? never : T { // eslint-disable-line unicorn/prefer-native-coercion-functions - return Boolean(value); -} diff --git a/webpack.config.ts b/webpack.config.ts index 2d62e4870da46..5291480655424 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -12,7 +12,6 @@ import {readFileSync, globSync} from 'node:fs'; import {env} from 'node:process'; import tailwindcss from 'tailwindcss'; import tailwindConfig from './tailwind.config.ts'; -import {isTruthy} from './tools/utils.ts'; const {EsbuildPlugin} = EsBuildLoader; const {SourceMapDevToolPlugin, DefinePlugin, EnvironmentPlugin} = webpack; @@ -271,7 +270,7 @@ export default { excludeAssets: [ /^js\/monaco-language-.+\.js$/, !isProduction && /^licenses.txt$/, - ].filter(isTruthy), + ].filter(Boolean as any), groupAssetsByChunk: false, groupAssetsByEmitStatus: false, groupAssetsByInfo: false, From ab525622bfbb79b27fb43b6bac4cedf53b29c04d Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 31 Oct 2025 08:39:07 +0100 Subject: [PATCH 05/11] alter type --- tailwind.config.ts | 2 +- webpack.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tailwind.config.ts b/tailwind.config.ts index 39abba2da0e97..b4c0dc6d261ec 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -37,7 +37,7 @@ export default { './{build,models,modules,routers,services}/**/*.go', './templates/**/*.tmpl', './web_src/js/**/*.{ts,js,vue}', - ].filter(Boolean as any), + ].filter(Boolean as unknown as (x: T | boolean) => x is T), blocklist: [ // classes that don't work without CSS variables from "@tailwind base" which we don't use 'transform', 'shadow', 'ring', 'blur', 'grayscale', 'invert', '!invert', 'filter', '!filter', diff --git a/webpack.config.ts b/webpack.config.ts index 5291480655424..f0528a7331567 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -270,7 +270,7 @@ export default { excludeAssets: [ /^js\/monaco-language-.+\.js$/, !isProduction && /^licenses.txt$/, - ].filter(Boolean as any), + ].filter(Boolean as unknown as (x: T | boolean) => x is T), groupAssetsByChunk: false, groupAssetsByEmitStatus: false, groupAssetsByInfo: false, From 52236a78c6784447f4eace9258b34aa656f901a8 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 31 Oct 2025 08:47:33 +0100 Subject: [PATCH 06/11] apply same method on all .filter(Boolean) --- web_src/js/features/repo-settings.ts | 2 +- web_src/js/svg.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web_src/js/features/repo-settings.ts b/web_src/js/features/repo-settings.ts index 43a236b0c6858..7a8df8af0f13c 100644 --- a/web_src/js/features/repo-settings.ts +++ b/web_src/js/features/repo-settings.ts @@ -101,7 +101,7 @@ function initRepoSettingsBranches() { // show the `Matched` mark for the status checks that match the pattern const markMatchedStatusChecks = () => { const patterns = (document.querySelector('#status_check_contexts').value || '').split(/[\r\n]+/); - const validPatterns = patterns.map((item) => item.trim()).filter(Boolean); + const validPatterns = patterns.map((item) => item.trim()).filter(Boolean as unknown as (x: T | boolean) => x is T); const marks = document.querySelectorAll('.status-check-matched-mark'); for (const el of marks) { diff --git a/web_src/js/svg.ts b/web_src/js/svg.ts index 2ad9bffd51ac4..3d3e6530728d4 100644 --- a/web_src/js/svg.ts +++ b/web_src/js/svg.ts @@ -181,7 +181,7 @@ export function svg(name: SvgName, size = 16, classNames?: string | string[]): s svgNode.setAttribute('width', String(size)); svgNode.setAttribute('height', String(size)); } - if (className) svgNode.classList.add(...className.split(/\s+/).filter(Boolean)); + if (className) svgNode.classList.add(...className.split(/\s+/).filter(Boolean as unknown as (x: T | boolean) => x is T)); return serializeXml(svgNode); } From aa1184aec5976c5940b79f74da033c5395d22fcd Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 31 Oct 2025 08:51:18 +0100 Subject: [PATCH 07/11] better fix --- tools/generate-svg.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tools/generate-svg.ts b/tools/generate-svg.ts index 082c35429b50a..c18eacc86fd44 100755 --- a/tools/generate-svg.ts +++ b/tools/generate-svg.ts @@ -47,8 +47,7 @@ function processAssetsSvgFiles(pattern: string, opts: Opts = {}) { return glob(pattern).map((path) => processAssetsSvgFile(path, opts)); } -function lowercaseKeys(obj: Record | undefined) { - if (!obj) return obj; +function lowercaseKeys(obj: Record) { return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value])); } @@ -82,9 +81,15 @@ async function processMaterialFileIcons() { // * https://github.com/microsoft/vscode/tree/1.98.0/extensions delete iconRules.iconDefinitions; - iconRules.fileNames = lowercaseKeys(iconRules.fileNames); - iconRules.folderNames = lowercaseKeys(iconRules.fileNames); - iconRules.fileExtensions = lowercaseKeys(iconRules.fileNames); + if (iconRules.fileNames) { + iconRules.fileNames = lowercaseKeys(iconRules.fileNames); + } + if (iconRules.folderNames) { + iconRules.folderNames = lowercaseKeys(iconRules.folderNames); + } + if (iconRules.fileExtensions) { + iconRules.fileExtensions = lowercaseKeys(iconRules.fileExtensions); + } // Use VSCode's "Language ID" mapping from its extensions for (const [_, langIdExtMap] of Object.entries(vscodeExtensions)) { From 9d36023a67455e78bcc9a27a27ed8fd7d3ed8030 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 31 Oct 2025 10:14:29 +0100 Subject: [PATCH 08/11] better fix for playwright issue --- tests/e2e/example.test.e2e.ts | 2 -- tests/e2e/utils_e2e.ts | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/e2e/example.test.e2e.ts b/tests/e2e/example.test.e2e.ts index 14c34f96955c0..4bf2b54670457 100644 --- a/tests/e2e/example.test.e2e.ts +++ b/tests/e2e/example.test.e2e.ts @@ -45,8 +45,6 @@ test('login', async ({page}, workerInfo) => { test('logged in user', async ({browser}, workerInfo) => { const context = await load_logged_in_context(browser, workerInfo, 'user2'); - if (!context) return; - const page = await context.newPage(); await page.goto('/'); diff --git a/tests/e2e/utils_e2e.ts b/tests/e2e/utils_e2e.ts index 3e92e0d3c2d82..0973f0838c8e3 100644 --- a/tests/e2e/utils_e2e.ts +++ b/tests/e2e/utils_e2e.ts @@ -33,15 +33,15 @@ export async function login_user(browser: Browser, workerInfo: WorkerInfo, user: } export async function load_logged_in_context(browser: Browser, workerInfo: WorkerInfo, user: string) { - let context; try { - context = await browser.newContext({storageState: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`}); + return await browser.newContext({storageState: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`}); } catch (err) { if (err.code === 'ENOENT') { throw new Error(`Could not find state for '${user}'. Did you call login_user(browser, workerInfo, '${user}') in test.beforeAll()?`); + } else { + throw err; } } - return context; } export async function save_visual(page: Page) { From d620c6f07e43b36f609b9f9dab0c641385a9e77e Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 3 Nov 2025 20:51:33 +0100 Subject: [PATCH 09/11] revert eslint change Signed-off-by: silverwind --- eslint.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/eslint.config.ts b/eslint.config.ts index 9557f5caaf668..ed2bc6524584b 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -905,7 +905,6 @@ export default defineConfig([ files: ['tests/e2e/**'], rules: { ...playwright.configs['flat/recommended'].rules, - 'playwright/no-conditional-in-test': [0], }, }, { From a733b93b07fbf200858118ce725d202429d225e2 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 3 Nov 2025 20:52:09 +0100 Subject: [PATCH 10/11] revert whitespace Signed-off-by: silverwind --- tests/e2e/example.test.e2e.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/example.test.e2e.ts b/tests/e2e/example.test.e2e.ts index 4bf2b54670457..1689f1b8efc7e 100644 --- a/tests/e2e/example.test.e2e.ts +++ b/tests/e2e/example.test.e2e.ts @@ -46,6 +46,7 @@ test('login', async ({page}, workerInfo) => { test('logged in user', async ({browser}, workerInfo) => { const context = await load_logged_in_context(browser, workerInfo, 'user2'); const page = await context.newPage(); + await page.goto('/'); // Make sure we routed to the home page. Else login failed. From b0f6a439cabfb36b2e9bb01fef690e01e5a65ce1 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 3 Nov 2025 20:54:20 +0100 Subject: [PATCH 11/11] revert unnecessary change --- tailwind.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tailwind.config.ts b/tailwind.config.ts index b4c0dc6d261ec..8693208e13c59 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -121,4 +121,4 @@ export default { }); }), ], -} satisfies Config as Config; +} satisfies Config;