From e2dfbd66c3b60eacd6f0b82d38e0d9cf342c78af Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Fri, 13 Jun 2025 15:54:59 +0100 Subject: [PATCH] ci: browser compatibility linting (#19132) --- web/.browserslistrc | 5 ++ web/eslint.config.js | 33 ++++++++ web/package-lock.json | 83 +++++++++++++++++++ web/package.json | 3 + .../asset-viewer/asset-viewer.svelte | 2 + .../face-editor/face-editor.svelte | 1 + .../asset-viewer/slideshow-bar.svelte | 2 + .../drag-and-drop-upload-overlay.svelte | 2 + .../scrubber/scrubber.svelte | 2 + .../timeline-manager.svelte.ts | 6 +- web/src/lib/utils/navigation.ts | 3 +- 11 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 web/.browserslistrc diff --git a/web/.browserslistrc b/web/.browserslistrc new file mode 100644 index 0000000000..c3a939f1b7 --- /dev/null +++ b/web/.browserslistrc @@ -0,0 +1,5 @@ +> 0.2% and last 4 major versions +> 0.5% +not dead +edge >= 135 +not edge < 135 diff --git a/web/eslint.config.js b/web/eslint.config.js index 9a545fcbc7..55d39e637f 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -1,4 +1,6 @@ import js from '@eslint/js'; +import tslintPluginCompat from '@koddsson/eslint-plugin-tscompat'; +import eslintPluginCompat from 'eslint-plugin-compat'; import eslintPluginSvelte from 'eslint-plugin-svelte'; import eslintPluginUnicorn from 'eslint-plugin-unicorn'; import globals from 'globals'; @@ -14,6 +16,37 @@ export default typescriptEslint.config( ...eslintPluginSvelte.configs.recommended, eslintPluginUnicorn.configs.recommended, js.configs.recommended, + { + plugins: { + tscompat: tslintPluginCompat, + }, + rules: { + 'tscompat/tscompat': [ + 'error', + { browserslist: ['> 0.2% and last 4 major versions', '> 0.5%', 'not dead', 'edge >= 135', 'not edge < 135'] }, + ], + }, + languageOptions: { + parser, + parserOptions: { + project: ['./tsconfig.json'], + tsconfigRootDir: __dirname, + }, + }, + ignores: ['**/service-worker/**'], + }, + { + plugins: { + compat: eslintPluginCompat, + }, + settings: { + polyfills: [], + lintAllEsApis: true, + }, + rules: { + 'compat/compat': 'error', + }, + }, { ignores: [ '**/.DS_Store', diff --git a/web/package-lock.json b/web/package-lock.json index 09514baa4e..7a36685cdb 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -42,6 +42,7 @@ "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.18.0", "@faker-js/faker": "^9.3.0", + "@koddsson/eslint-plugin-tscompat": "^0.2.0", "@socket.io/component-emitter": "^3.1.0", "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/enhanced-img": "^0.6.0", @@ -63,6 +64,7 @@ "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.0", "eslint-p": "^0.23.0", + "eslint-plugin-compat": "^6.0.2", "eslint-plugin-svelte": "^3.9.0", "eslint-plugin-unicorn": "^59.0.0", "factory.ts": "^1.4.1", @@ -74,6 +76,7 @@ "rollup-plugin-visualizer": "^6.0.0", "svelte": "^5.25.3", "svelte-check": "^4.1.5", + "svelte-eslint-parser": "^1.2.0", "tailwindcss": "^4.1.7", "tslib": "^2.6.2", "typescript": "^5.7.3", @@ -1537,6 +1540,26 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@koddsson/eslint-plugin-tscompat": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@koddsson/eslint-plugin-tscompat/-/eslint-plugin-tscompat-0.2.0.tgz", + "integrity": "sha512-Oqd4kWSX0LiO9wWHjcmDfXZNC7TotFV/tLRhwCFU3XUeb//KYvJ75c9OmeSJ+vBv5lkCeB+xYsqyNrBc5j18XA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@mdn/browser-compat-data": "^6.0.17", + "@typescript-eslint/type-utils": "^8.0.1", + "@typescript-eslint/utils": "^8.0.0", + "browserslist": "^4.23.0" + } + }, + "node_modules/@koddsson/eslint-plugin-tscompat/node_modules/@mdn/browser-compat-data": { + "version": "6.0.22", + "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-6.0.22.tgz", + "integrity": "sha512-zhgOBTouJOd8IbE5dEEcfzg83l+nxKL/7Ru2HPeCVbog9I0JGHg3QZab9IxZquKFTUsc+c7QqU4EVENeZzZWRg==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/@mapbox/geojson-rewind": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", @@ -1687,6 +1710,13 @@ "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==", "license": "Apache-2.0" }, + "node_modules/@mdn/browser-compat-data": { + "version": "5.7.6", + "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.7.6.tgz", + "integrity": "sha512-7xdrMX0Wk7grrTZQwAoy1GkvPMFoizStUoL+VmtUkAxegbCCec+3FKwOM6yc/uGU5+BEczQHXAlWiqvM8JeENg==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/@namnode/store": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@namnode/store/-/store-0.1.0.tgz", @@ -3505,6 +3535,16 @@ "node": ">=12" } }, + "node_modules/ast-metadata-inferer": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/ast-metadata-inferer/-/ast-metadata-inferer-0.8.1.tgz", + "integrity": "sha512-ht3Dm6Zr7SXv6t1Ra6gFo0+kLDglHGrEbYihTkcycrbHw7WCcuhBzPlJYHEsIpycaUwzsJHje+vUcxXUX4ztTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mdn/browser-compat-data": "^5.6.19" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4717,6 +4757,42 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/eslint-plugin-compat": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-6.0.2.tgz", + "integrity": "sha512-1ME+YfJjmOz1blH0nPZpHgjMGK4kjgEeoYqGCqoBPQ/mGu/dJzdoP0f1C8H2jcWZjzhZjAMccbM/VdXhPORIfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mdn/browser-compat-data": "^5.5.35", + "ast-metadata-inferer": "^0.8.1", + "browserslist": "^4.24.2", + "caniuse-lite": "^1.0.30001687", + "find-up": "^5.0.0", + "globals": "^15.7.0", + "lodash.memoize": "^4.1.2", + "semver": "^7.6.2" + }, + "engines": { + "node": ">=18.x" + }, + "peerDependencies": { + "eslint": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-compat/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-plugin-svelte": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.9.0.tgz", @@ -6494,6 +6570,13 @@ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "license": "MIT" }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", diff --git a/web/package.json b/web/package.json index 4a15dc1634..a06b12f826 100644 --- a/web/package.json +++ b/web/package.json @@ -59,6 +59,7 @@ "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.18.0", "@faker-js/faker": "^9.3.0", + "@koddsson/eslint-plugin-tscompat": "^0.2.0", "@socket.io/component-emitter": "^3.1.0", "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/enhanced-img": "^0.6.0", @@ -80,6 +81,7 @@ "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.0", "eslint-p": "^0.23.0", + "eslint-plugin-compat": "^6.0.2", "eslint-plugin-svelte": "^3.9.0", "eslint-plugin-unicorn": "^59.0.0", "factory.ts": "^1.4.1", @@ -91,6 +93,7 @@ "rollup-plugin-visualizer": "^6.0.0", "svelte": "^5.25.3", "svelte-check": "^4.1.5", + "svelte-eslint-parser": "^1.2.0", "tailwindcss": "^4.1.7", "tslib": "^2.6.2", "typescript": "^5.7.3", diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index b6daa4d384..34e00625b5 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -300,8 +300,10 @@ const handleStopSlideshow = async () => { try { + // eslint-disable-next-line tscompat/tscompat if (document.fullscreenElement) { document.body.style.cursor = ''; + // eslint-disable-next-line tscompat/tscompat await document.exitFullscreen(); } } catch (error) { diff --git a/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte b/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte index c88afc765a..0336e0a74d 100644 --- a/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte +++ b/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte @@ -57,6 +57,7 @@ canvas = new Canvas(canvasEl); configureControlStyle(); + // eslint-disable-next-line tscompat/tscompat faceRect = new Rect({ fill: 'rgba(66,80,175,0.25)', stroke: 'rgb(66,80,175)', diff --git a/web/src/lib/components/asset-viewer/slideshow-bar.svelte b/web/src/lib/components/asset-viewer/slideshow-bar.svelte index 0bb81ea44b..0f7d0a565f 100644 --- a/web/src/lib/components/asset-viewer/slideshow-bar.svelte +++ b/web/src/lib/components/asset-viewer/slideshow-bar.svelte @@ -101,7 +101,9 @@ }; const onShowSettings = async () => { + // eslint-disable-next-line tscompat/tscompat if (document.fullscreenElement) { + // eslint-disable-next-line tscompat/tscompat await document.exitFullscreen(); } await modalManager.show(SlideshowSettingsModal); diff --git a/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte b/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte index 3af6a49d0b..4b8046b082 100644 --- a/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte +++ b/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte @@ -51,6 +51,7 @@ const entries: FileSystemEntry[] = []; const files: File[] = []; for (const item of dataTransfer.items) { + // eslint-disable-next-line tscompat/tscompat const entry = item.webkitGetAsEntry(); if (entry) { entries.push(entry); @@ -67,6 +68,7 @@ return handleFiles([...files, ...directoryFiles]); }; + // eslint-disable-next-line tscompat/tscompat const browserSupportsDirectoryUpload = () => typeof DataTransferItem.prototype.webkitGetAsEntry === 'function'; const getAllFilesFromTransferEntries = async (transferEntries: FileSystemEntry[]): Promise => { diff --git a/web/src/lib/components/shared-components/scrubber/scrubber.svelte b/web/src/lib/components/shared-components/scrubber/scrubber.svelte index 17e39f1739..730ff39c56 100644 --- a/web/src/lib/components/shared-components/scrubber/scrubber.svelte +++ b/web/src/lib/components/shared-components/scrubber/scrubber.svelte @@ -310,6 +310,7 @@ void onScrub?.(segmentDate!, scrollPercent, monthGroupPercentY); }; + /* eslint-disable tscompat/tscompat */ const getTouch = (event: TouchEvent) => { if (event.touches.length === 1) { return event.touches[0]; @@ -354,6 +355,7 @@ isHover = false; } }; + /* eslint-enable tscompat/tscompat */ onMount(() => { document.addEventListener('touchmove', onTouchMove, true); return () => { diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts index 98e8438b17..8aacd0a90a 100644 --- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts +++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts @@ -470,7 +470,11 @@ export class TimelineManager { }, { order: this.#options.order ?? AssetOrder.Desc }, ); - return unprocessedIds.values().map((id) => lookup.get(id)!); + const result: TimelineAsset[] = []; + for (const id of unprocessedIds.values()) { + result.push(lookup.get(id)!); + } + return result; } removeAssets(ids: string[]) { diff --git a/web/src/lib/utils/navigation.ts b/web/src/lib/utils/navigation.ts index 89e1f9f5f0..2e5a353cf8 100644 --- a/web/src/lib/utils/navigation.ts +++ b/web/src/lib/utils/navigation.ts @@ -40,7 +40,8 @@ export function currentUrlReplaceAssetId(assetId: string) { const params = new URLSearchParams($page.url.search); // always remove the assetGridScrollTargetParams params.delete('at'); - const searchparams = params.size > 0 ? '?' + params.toString() : ''; + const paramsString = params.toString(); + const searchparams = paramsString == '' ? '' : '?' + params.toString(); // this contains special casing for the /photos/:assetId photos route, which hangs directly // off / instead of a subpath, unlike every other asset-containing route. return isPhotosRoute($page.route.id)