From 6524a37bac9d9705c83cb3d03529d84e23c922eb Mon Sep 17 00:00:00 2001 From: Johannes Fleck Date: Mon, 26 Oct 2026 16:10:43 +0100 Subject: [PATCH 1/6] chore(lint): lint e2e/support and migrate.ts Both were excluded from ESLint. Drop the ignores, remove the now redundant detect-object-injection disables in garage.ts and log migration errors via the pino logger instead of console. Refs #344 --- e2e/support/garage.ts | 3 --- eslint.config.js | 2 +- src/lib/server/migrate.ts | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/e2e/support/garage.ts b/e2e/support/garage.ts index 9989bf78..3d4142b0 100644 --- a/e2e/support/garage.ts +++ b/e2e/support/garage.ts @@ -37,7 +37,6 @@ function readString(value: unknown, keys: string[]): string | null { } for (const key of keys) { - // eslint-disable-next-line security/detect-object-injection const candidate = record[key]; if (typeof candidate === 'string' && candidate.length > 0) { return candidate; @@ -54,7 +53,6 @@ function readStringArray(value: unknown, keys: string[]): string[] { } for (const key of keys) { - // eslint-disable-next-line security/detect-object-injection const candidate = record[key]; if (Array.isArray(candidate)) { return candidate.filter((item): item is string => typeof item === 'string'); @@ -75,7 +73,6 @@ function extractAdminList(value: unknown): JsonRecord[] { } for (const key of ADMIN_LIST_KEYS) { - // eslint-disable-next-line security/detect-object-injection const candidate = record[key]; if (Array.isArray(candidate)) { return candidate diff --git a/eslint.config.js b/eslint.config.js index e9db4541..fbcd1f49 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -15,7 +15,7 @@ const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); export default defineConfig( includeIgnoreFile(gitignorePath), - { ignores: ['src/lib/editor/generated/**', 'src/lib/server/migrate.ts', 'e2e/support/**'] }, + { ignores: ['src/lib/editor/generated/**'] }, js.configs.recommended, ...ts.configs.recommended, ...svelte.configs.recommended, diff --git a/src/lib/server/migrate.ts b/src/lib/server/migrate.ts index 1758acb4..22ead3ea 100644 --- a/src/lib/server/migrate.ts +++ b/src/lib/server/migrate.ts @@ -30,7 +30,7 @@ if (import.meta.url === `file://${process.argv[1]}`) { process.exit(success ? 0 : 1); }) .catch((error) => { - console.error('Migration error:', error); + log.error({ err: error }, 'Migration error'); process.exit(1); }); } From cc7ca9f9c2e786a7aea08e16af22004cc0c315fa Mon Sep 17 00:00:00 2001 From: Johannes Fleck Date: Mon, 26 Oct 2026 16:14:28 +0100 Subject: [PATCH 2/6] chore(lint): enable no-console outside CLI and E2E helper scripts Only server code had no-console enabled. Enable it everywhere and keep it off for the E2E helper processes and the ANTLR patch script, which run without the app's logger. Refs #344 --- eslint.config.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index fbcd1f49..197c14c8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -28,7 +28,7 @@ export default defineConfig( }, rules: { 'no-undef': 'off', - 'no-console': 'off', + 'no-console': 'error', // It will currently also error on external links or links with query parameters. // https://github.com/sveltejs/eslint-plugin-svelte/issues/1353 'svelte/no-navigation-without-resolve': 'warn', @@ -88,8 +88,9 @@ export default defineConfig( } }, { - files: ['src/lib/server/**/*.ts', 'src/routes/**/*.server.ts'], - rules: { 'no-console': 'error' } + // CLI scripts and E2E helper processes that run without the app's logger. + files: ['e2e/support/**/*.ts', 'src/lib/editor/grammar/patch-generated.mjs'], + rules: { 'no-console': 'off' } }, { files: ['src/**/*.ts'], From d44ecc816b359fdf45587d577945368608fe69bf Mon Sep 17 00:00:00 2001 From: Johannes Fleck Date: Mon, 26 Oct 2026 16:17:13 +0100 Subject: [PATCH 3/6] chore(lint): drop fs-filename check for tests, document production disables Tests only touch fixtures and temp files they create, so turn security/detect-non-literal-fs-filename off for unit, E2E and architecture tests instead of disabling it line by line. Give each remaining disable in production code a reason. Refs #344 --- e2e/i18n.spec.ts | 1 - eslint.config.js | 5 +++++ src/architecture-tests/helpers.ts | 3 --- src/lib/server/db-tls.ts | 2 +- src/lib/server/db.test.ts | 1 - src/lib/server/storage/archive.test.ts | 12 ------------ src/lib/server/storage/archive.ts | 24 ++++++++++++------------ src/lib/server/storage/s3-client.test.ts | 1 - src/lib/server/storage/s3-client.ts | 2 +- src/lib/server/trino/client.ts | 2 +- 10 files changed, 20 insertions(+), 33 deletions(-) diff --git a/e2e/i18n.spec.ts b/e2e/i18n.spec.ts index 75ac399b..81bbc507 100644 --- a/e2e/i18n.spec.ts +++ b/e2e/i18n.spec.ts @@ -7,7 +7,6 @@ const DE_DASHBOARD_HEADING = 'Stackable Unified Data Platform – Übersicht'; /** Load the saved auth storage state, optionally stripping the locale cookie. */ function loadAuthState(authFile: string, { withoutLocale = false } = {}) { - // eslint-disable-next-line security/detect-non-literal-fs-filename const state = JSON.parse(fs.readFileSync(authFile, 'utf-8')); if (withoutLocale) { state.cookies = state.cookies.filter((c: { name: string }) => c.name !== 'PARAGLIDE_LOCALE'); diff --git a/eslint.config.js b/eslint.config.js index 197c14c8..0f5b3c80 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -92,6 +92,11 @@ export default defineConfig( files: ['e2e/support/**/*.ts', 'src/lib/editor/grammar/patch-generated.mjs'], rules: { 'no-console': 'off' } }, + { + // Tests read and write fixtures and temp files they create themselves. + files: ['**/*.{test,spec}.ts', 'e2e/**/*.ts', 'src/architecture-tests/**/*.ts'], + rules: { 'security/detect-non-literal-fs-filename': 'off' } + }, { files: ['src/**/*.ts'], ignores: ['src/**/*.test.ts', 'src/**/*.spec.ts'], diff --git a/src/architecture-tests/helpers.ts b/src/architecture-tests/helpers.ts index d2cb4d68..2b7a6a07 100644 --- a/src/architecture-tests/helpers.ts +++ b/src/architecture-tests/helpers.ts @@ -13,9 +13,6 @@ export const defaultOptions = { logging: { enabled: false, level: 'warn' as const } }; -// These tests deliberately inspect a discovered source tree rather than -// application-controlled input paths. -/* eslint-disable security/detect-non-literal-fs-filename */ export function readTextFile(file: string): string { return readFileSync(file, 'utf-8'); } diff --git a/src/lib/server/db-tls.ts b/src/lib/server/db-tls.ts index 3ad3577b..91bd27a8 100644 --- a/src/lib/server/db-tls.ts +++ b/src/lib/server/db-tls.ts @@ -44,7 +44,7 @@ export function databaseSsl(env: DatabaseTlsEnv, mode: DatabaseTlsMode): Connect if (mode === 'disable') return false; return { rejectUnauthorized: mode === 'verify-full', - // eslint-disable-next-line security/detect-non-literal-fs-filename + // eslint-disable-next-line security/detect-non-literal-fs-filename -- CA certificate path from the deployment configuration ...(caCertPath ? { ca: readFileSync(caCertPath) } : {}) }; } diff --git a/src/lib/server/db.test.ts b/src/lib/server/db.test.ts index 4a9dff1d..8b763acb 100644 --- a/src/lib/server/db.test.ts +++ b/src/lib/server/db.test.ts @@ -124,7 +124,6 @@ describe('database environment', () => { it('trusts the configured CA certificate', async () => { const caCertPath = join(tempDir, 'ca.crt'); - // eslint-disable-next-line security/detect-non-literal-fs-filename writeFileSync(caCertPath, 'test-ca'); env.DATABASE_TLS_CA_CERT = caCertPath; diff --git a/src/lib/server/storage/archive.test.ts b/src/lib/server/storage/archive.test.ts index dd366a5a..3fa69c4d 100644 --- a/src/lib/server/storage/archive.test.ts +++ b/src/lib/server/storage/archive.test.ts @@ -54,7 +54,6 @@ function makeZip(path: string, entries: Record): void { function makeTar(path: string, entries: Record): Promise { return new Promise((resolve, reject) => { const pack = tar.pack(); - // eslint-disable-next-line security/detect-non-literal-fs-filename const ws = createWriteStream(path); pack.pipe(ws); for (const [name, content] of Object.entries(entries)) { @@ -70,7 +69,6 @@ function makeTarGz(path: string, entries: Record): Promise return new Promise((resolve, reject) => { const pack = tar.pack(); const gz = createGzip(); - // eslint-disable-next-line security/detect-non-literal-fs-filename const ws = createWriteStream(path); pack.pipe(gz).pipe(ws); for (const [name, content] of Object.entries(entries)) { @@ -111,7 +109,6 @@ function bufferStream(data: Buffer): ReadableStream { } const dummyDownloadFn: ArchiveDownloadFn = (key: string) => - // eslint-disable-next-line security/detect-non-literal-fs-filename Promise.resolve({ stream: bufferStream(readFileSync(key)) }); describe('getArchiveFormat', () => { @@ -158,14 +155,12 @@ describe('listArchiveContents', () => { clearArchiveCache(); for (const p of cleanupPaths) { try { - // eslint-disable-next-line security/detect-non-literal-fs-filename if (existsSync(p)) unlinkSync(p); } catch { /* noop */ } } try { - // eslint-disable-next-line security/detect-non-literal-fs-filename if (existsSync(testDir)) unlinkSync(testDir); } catch { /* noop */ @@ -693,13 +688,10 @@ describe('Cache invalidation', () => { /** Number of archive files this test worker currently has on disk. */ function cachedFileCount(): number { const root = join(tmpdir(), 'cockpit-archive-cache'); - // eslint-disable-next-line security/detect-non-literal-fs-filename if (!existsSync(root)) return 0; let count = 0; - // eslint-disable-next-line security/detect-non-literal-fs-filename for (const name of readdirSync(root)) { if (!name.startsWith(`${process.pid}.${threadId}-`)) continue; - // eslint-disable-next-line security/detect-non-literal-fs-filename count += readdirSync(join(root, name)).length; } return count; @@ -848,10 +840,8 @@ describe('Cache limits', () => { makeZip(path, { 'random.bin': randomBytes(1000) }); return path; }); - // eslint-disable-next-line security/detect-non-literal-fs-filename const sizes = paths.map((path) => statSync(path).size); flags.archiveCacheMaxBytes = sizes[0] + sizes[1] + 10; - // eslint-disable-next-line security/detect-non-literal-fs-filename const metadataFn = vi.fn(async (key: string) => ({ size: statSync(key).size, etag: '"v1"' })); const downloadSpy = vi.fn(dummyDownloadFn); @@ -888,14 +878,12 @@ describe('Cache limits', () => { it('counts nested archives towards the cache size', async () => { const outerPath = makeNestedZip(); - // eslint-disable-next-line security/detect-non-literal-fs-filename const innerSize = statSync(testPath('inner.zip')).size; await listArchiveContents('test-bucket', outerPath, '', dummyDownloadFn, head(), { nestedArchivePath: 'archives/inner.zip' }); - // eslint-disable-next-line security/detect-non-literal-fs-filename expect(archiveCacheBytes()).toBe(statSync(outerPath).size + innerSize); expect(cachedFileCount()).toBe(2); }); diff --git a/src/lib/server/storage/archive.ts b/src/lib/server/storage/archive.ts index 1b19b9a8..a51bf1e6 100644 --- a/src/lib/server/storage/archive.ts +++ b/src/lib/server/storage/archive.ts @@ -145,7 +145,7 @@ function isProcessAlive(pid: number): boolean { * gets the same PID, so directories of this PID and thread are stale as well. */ function removeStaleCacheDirs(): void { - // eslint-disable-next-line security/detect-non-literal-fs-filename + // eslint-disable-next-line security/detect-non-literal-fs-filename -- fixed cache root under tmpdir() for (const name of readdirSync(CACHE_ROOT)) { const match = /^(\d+)\.(\d+)-/.exec(name); if (match) { @@ -165,7 +165,7 @@ function removeStaleCacheDirs(): void { function cacheDir(): string { if (!processCacheDir) { - // eslint-disable-next-line security/detect-non-literal-fs-filename + // eslint-disable-next-line security/detect-non-literal-fs-filename -- fixed cache root under tmpdir() mkdirSync(CACHE_ROOT, { recursive: true }); removeStaleCacheDirs(); processCacheDir = mkdtempSync(join(CACHE_ROOT, `${process.pid}.${threadId}-`)); @@ -200,9 +200,9 @@ function removeEntry(key: string, entry: CacheEntry, reason: EvictionReason) { function acquire(key: string, ownFill = false): ArchiveLease | null { const entry = archiveCache.get(key); if (!entry || (entry.retired && !ownFill)) return null; - // eslint-disable-next-line security/detect-non-literal-fs-filename + // eslint-disable-next-line security/detect-non-literal-fs-filename -- server-generated path inside the archive cache directory if (Date.now() > entry.expiresAt || !existsSync(entry.path)) { - // eslint-disable-next-line security/detect-non-literal-fs-filename + // eslint-disable-next-line security/detect-non-literal-fs-filename -- server-generated path inside the archive cache directory if (entry.leases === 0) removeEntry(key, entry, existsSync(entry.path) ? 'expired' : 'missing'); return null; } @@ -280,7 +280,7 @@ function addEntry(id: EntryId, dir: string, path: string, singleUse: boolean): v if ( !existing.retired && Date.now() <= existing.expiresAt && - // eslint-disable-next-line security/detect-non-literal-fs-filename + // eslint-disable-next-line security/detect-non-literal-fs-filename -- server-generated path inside the archive cache directory existsSync(existing.path) ) { log.debug({ cache_key: key }, 'archive already cached, discarding duplicate download'); @@ -299,7 +299,7 @@ function addEntry(id: EntryId, dir: string, path: string, singleUse: boolean): v } } - // eslint-disable-next-line security/detect-non-literal-fs-filename + // eslint-disable-next-line security/detect-non-literal-fs-filename -- server-generated path inside the archive cache directory const size = statSync(path).size; const now = Date.now(); archiveCache.set(key, { @@ -639,7 +639,7 @@ function listTar( }); extract.on('error', reject); - // eslint-disable-next-line security/detect-non-literal-fs-filename + // eslint-disable-next-line security/detect-non-literal-fs-filename -- server-generated path inside the archive cache directory createReadStream(tempPath).pipe(extract); }); } @@ -690,7 +690,7 @@ function extractTarEntry( extract.on('finish', () => resolve(found)); extract.on('error', reject); - // eslint-disable-next-line security/detect-non-literal-fs-filename + // eslint-disable-next-line security/detect-non-literal-fs-filename -- server-generated path inside the archive cache directory createReadStream(tempPath).pipe(extract); }); } @@ -731,7 +731,7 @@ function listTarGz( extract.on('error', reject); gunzip.on('error', reject); - // eslint-disable-next-line security/detect-non-literal-fs-filename + // eslint-disable-next-line security/detect-non-literal-fs-filename -- server-generated path inside the archive cache directory createReadStream(tempPath).pipe(gunzip).pipe(extract); }); } @@ -784,7 +784,7 @@ function extractTarGzEntry( extract.on('error', reject); gunzip.on('error', reject); - // eslint-disable-next-line security/detect-non-literal-fs-filename + // eslint-disable-next-line security/detect-non-literal-fs-filename -- server-generated path inside the archive cache directory createReadStream(tempPath).pipe(gunzip).pipe(extract); }); } @@ -916,7 +916,7 @@ function leaseOuterArchive( await pipeline( Readable.fromWeb(download.stream as import('stream/web').ReadableStream), byteLimit(limit), - // eslint-disable-next-line security/detect-non-literal-fs-filename + // eslint-disable-next-line security/detect-non-literal-fs-filename -- server-generated path inside the archive cache directory createWriteStream(path), { signal: jobSignal } ); @@ -984,7 +984,7 @@ async function leaseArchive( extname(nestedArchivePath) || '.bin', nestedData.length, async (path) => { - // eslint-disable-next-line security/detect-non-literal-fs-filename + // eslint-disable-next-line security/detect-non-literal-fs-filename -- server-generated path inside the archive cache directory await writeFile(path, nestedData, { signal: jobSignal }); // Extracted from an outer download that is not cached either. return { singleUse: outer.singleUse }; diff --git a/src/lib/server/storage/s3-client.test.ts b/src/lib/server/storage/s3-client.test.ts index b9a155f8..5a8c52d3 100644 --- a/src/lib/server/storage/s3-client.test.ts +++ b/src/lib/server/storage/s3-client.test.ts @@ -163,7 +163,6 @@ describe('createS3Client custom CA certificate', () => { it('trusts only the configured CA certificate for verified connections', async () => { dir = mkdtempSync(join(tmpdir(), 's3-ca-')); const caPath = join(dir, 'ca.crt'); - // eslint-disable-next-line security/detect-non-literal-fs-filename writeFileSync(caPath, 'test-ca'); mockEnv.STACKABLE_COCKPIT_STORAGE_TLS_CA_CERT = caPath; diff --git a/src/lib/server/storage/s3-client.ts b/src/lib/server/storage/s3-client.ts index 5aba9972..368930cd 100644 --- a/src/lib/server/storage/s3-client.ts +++ b/src/lib/server/storage/s3-client.ts @@ -18,7 +18,7 @@ function loadCaCert(): Buffer | undefined { if (!path) return undefined; let ca: Buffer; try { - // eslint-disable-next-line security/detect-non-literal-fs-filename + // eslint-disable-next-line security/detect-non-literal-fs-filename -- CA certificate path from the deployment configuration ca = readFileSync(path); } catch (err) { throw new Error( diff --git a/src/lib/server/trino/client.ts b/src/lib/server/trino/client.ts index b51ebd35..0fb801ab 100644 --- a/src/lib/server/trino/client.ts +++ b/src/lib/server/trino/client.ts @@ -195,7 +195,7 @@ if (trinoConfigured) { dispatcher = new Agent({ connect: { rejectUnauthorized: !tlsInsecure, - // eslint-disable-next-line security/detect-non-literal-fs-filename + // eslint-disable-next-line security/detect-non-literal-fs-filename -- CA certificate path from the deployment configuration ...(tlsCaCertPath ? { ca: readFileSync(tlsCaCertPath) } : {}) } }); From 37c8e2aa5bb0f11062c94e2462c95fb19bc82f52 Mon Sep 17 00:00:00 2001 From: Johannes Fleck Date: Mon, 26 Oct 2026 16:24:53 +0100 Subject: [PATCH 4/6] chore(lint): make no-navigation-without-resolve an error Replace the file-level overrides with disables that state why the path is already resolved. The existing disable-next-line comments in Sidebar and RecentItems sat above the element instead of the href line and had no effect. Drop the second resolve() on already resolved paths in the storage layout and the connection edit page. Refs #344 --- eslint.config.js | 11 +---------- src/lib/components/layout/sidebar/Sidebar.svelte | 4 +++- .../components/storage/landing/RecentItems.svelte | 8 +++----- .../settings/connections/[id]/edit/+page.svelte | 5 ++--- src/routes/(app)/storage/+layout.svelte | 15 ++++----------- 5 files changed, 13 insertions(+), 30 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 0f5b3c80..3fe689d1 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -31,7 +31,7 @@ export default defineConfig( 'no-console': 'error', // It will currently also error on external links or links with query parameters. // https://github.com/sveltejs/eslint-plugin-svelte/issues/1353 - 'svelte/no-navigation-without-resolve': 'warn', + 'svelte/no-navigation-without-resolve': 'error', 'svelte/no-restricted-html-elements': [ 'error', { elements: ['dialog'], message: 'Use the shared Modal component instead of .' } @@ -137,15 +137,6 @@ export default defineConfig( files: ['src/lib/components/Modal.svelte'], rules: { 'svelte/no-restricted-html-elements': 'off' } }, - { - files: [ - 'src/lib/components/layout/sidebar/Sidebar.svelte', - 'src/lib/components/storage/landing/RecentItems.svelte', - 'src/routes/**/edit/+page.svelte', - 'src/routes/(app)/storage/+layout.svelte' - ], - rules: { 'svelte/no-navigation-without-resolve': 'off' } - }, { files: [ 'src/lib/client/**/*.ts', diff --git a/src/lib/components/layout/sidebar/Sidebar.svelte b/src/lib/components/layout/sidebar/Sidebar.svelte index 3ae277d3..273f69e0 100644 --- a/src/lib/components/layout/sidebar/Sidebar.svelte +++ b/src/lib/components/layout/sidebar/Sidebar.svelte @@ -119,8 +119,9 @@ {#snippet navItem(item: NavItem)} {@const active = isActive((resolve as (pathname: string) => string)(item.route))}
  • - + + string)(item.route)} data-sveltekit-preload-data="hover" @@ -136,6 +137,7 @@ aria-current={active ? 'page' : undefined} aria-disabled={item.disabled ? 'true' : undefined} > + {@render navIcon(item.icon)} {#if !collapsed} {item.label} diff --git a/src/lib/components/storage/landing/RecentItems.svelte b/src/lib/components/storage/landing/RecentItems.svelte index 455aa126..825fccab 100644 --- a/src/lib/components/storage/landing/RecentItems.svelte +++ b/src/lib/components/storage/landing/RecentItems.svelte @@ -106,6 +106,8 @@ {/if} {/snippet} + + {#snippet filesTable()} @@ -127,7 +129,6 @@
    - string)( fileHref(storage.connectionHostname, file) @@ -156,7 +157,6 @@ - string)( locationHref(storage.connectionHostname, loc) @@ -223,7 +221,6 @@
    {/snippet} +
    diff --git a/src/routes/(app)/settings/connections/[id]/edit/+page.svelte b/src/routes/(app)/settings/connections/[id]/edit/+page.svelte index dc82a773..13522f19 100644 --- a/src/routes/(app)/settings/connections/[id]/edit/+page.svelte +++ b/src/routes/(app)/settings/connections/[id]/edit/+page.svelte @@ -2,7 +2,6 @@ import { onMount } from 'svelte'; import { goto, beforeNavigate } from '$app/navigation'; import { resolve } from '$app/paths'; - import type { Pathname } from '$app/types'; import { superForm } from 'sveltekit-superforms'; import { zod4Client as zod } from 'sveltekit-superforms/adapters'; import { untrack } from 'svelte'; @@ -74,8 +73,8 @@ confirmLeaveOpen = false; pendingNavigation = null; bypassDirtyCheck = true; - // The URL comes from SvelteKit's beforeNavigate callback. - void goto((resolve as (pathname: Pathname) => string)(dest as Pathname)); + // eslint-disable-next-line svelte/no-navigation-without-resolve -- URL of a navigation SvelteKit already resolved + void goto(dest); } function cancelLeave() { diff --git a/src/routes/(app)/storage/+layout.svelte b/src/routes/(app)/storage/+layout.svelte index ef0849c3..cd9cdf79 100644 --- a/src/routes/(app)/storage/+layout.svelte +++ b/src/routes/(app)/storage/+layout.svelte @@ -39,19 +39,12 @@ persistEnabled: storageRestoreTabsEnabled, connectionId: storage.connectionId, navigateToLocation: (connection, bucket, prefix) => { - void goto( - (resolve as (pathname: import('$app/types').Pathname) => string)( - locationPath(connection, bucket, prefix) as import('$app/types').Pathname - ) - ); + // eslint-disable-next-line svelte/no-navigation-without-resolve -- path is built with resolve() in locationPath() + void goto(locationPath(connection, bucket, prefix)); }, replaceLocationUrl: (connection, bucket, prefix) => { - void goto( - (resolve as (pathname: import('$app/types').Pathname) => string)( - locationPath(connection, bucket, prefix) as import('$app/types').Pathname - ), - { replaceState: true } - ); + // eslint-disable-next-line svelte/no-navigation-without-resolve -- path is built with resolve() in locationPath() + void goto(locationPath(connection, bucket, prefix), { replaceState: true }); } }); setTabsState(tabsState); From 2f2b0989fc420c749f144edc853e2f2bae2b78cd Mon Sep 17 00:00:00 2001 From: Johannes Fleck Date: Mon, 26 Oct 2026 16:33:15 +0100 Subject: [PATCH 5/6] chore(lint): fail on lint and svelte-check warnings Warnings never failed lint, so they piled up unnoticed. Resolve the remaining ones (false-positive regex findings, table-pin-rows), then run eslint with --max-warnings=0, svelte-check with --fail-on-warnings and report unused eslint-disable directives as errors. Refs #344 --- eslint.config.js | 6 ++++-- package.json | 6 +++--- src/lib/storage/search-filter.ts | 3 +++ src/lib/storage/search-regex.ts | 1 + 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 3fe689d1..1467741c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -23,6 +23,7 @@ export default defineConfig( prettier, ...svelte.configs.prettier, { + linterOptions: { reportUnusedDisableDirectives: 'error' }, languageOptions: { globals: { ...globals.browser, ...globals.node } }, @@ -36,7 +37,7 @@ export default defineConfig( 'error', { elements: ['dialog'], message: 'Use the shared Modal component instead of .' } ], - // Too common occurrence in this project, disabling for now. + // Flags every computed member access; nearly all hits are array indices or typed Record lookups. 'security/detect-object-injection': 'off' } }, @@ -73,7 +74,8 @@ export default defineConfig( 'tab-active', 'tab-strip', 'preview-scroll', - 'modal-top-search' + 'modal-top-search', + 'table-pin-rows' ] } ], diff --git a/package.json b/package.json index 0e08978b..caf8d9d7 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,11 @@ "build": "vite build", "preview": "vite preview --host", "prepare": "svelte-kit sync || echo '' && npm run generate:antlr", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --fail-on-warnings", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "format": "prettier --write --ignore-path .prettierignore .", - "lint": "prettier --check --ignore-path .prettierignore . && eslint . && npm run check && npm run knip && npm run test:arch", - "lint:fix": "prettier --write --ignore-path .prettierignore . && eslint . --fix && npm run check && npm run knip && npm run test:arch", + "lint": "prettier --check --ignore-path .prettierignore . && eslint . --max-warnings=0 && npm run check && npm run knip && npm run test:arch", + "lint:fix": "prettier --write --ignore-path .prettierignore . && eslint . --fix --max-warnings=0 && npm run check && npm run knip && npm run test:arch", "knip": "knip", "knip:fix": "knip --fix", "test:unit": "vitest run", diff --git a/src/lib/storage/search-filter.ts b/src/lib/storage/search-filter.ts index 30a102d2..d4bab0c8 100644 --- a/src/lib/storage/search-filter.ts +++ b/src/lib/storage/search-filter.ts @@ -50,6 +50,7 @@ const MB = 1024 ** 2; * (KB, GB, …) is rejected. Returns null when the input cannot be parsed. */ export function parseSizeInput(input: string): number | null { + // eslint-disable-next-line security/detect-unsafe-regex -- linear: the optional fraction must start with [.,] const match = /^(\d+(?:[.,]\d+)?)\s*([a-zA-Z]*)$/.exec(input.trim()); if (!match) return null; const [, numberPart, unitRaw] = match; @@ -70,6 +71,7 @@ export interface ParseDateOptions { preferDayFirst?: boolean; } +// eslint-disable-next-line security/detect-unsafe-regex -- bounded repetition only const DATE_PART_RE = /^(?:(\d{1,4})([./-]))?(\d{1,2})\2?(\d{1,4})$/; /** @@ -153,6 +155,7 @@ function parseDateParts(input: string, options: ParseDateOptions): DateParts | n let minutes = 0; let seconds = 0; if (timePart !== undefined) { + // eslint-disable-next-line security/detect-unsafe-regex -- bounded repetition only const timeMatch = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/.exec(timePart); if (timeMatch === null) return null; hours = Number(timeMatch[1]); diff --git a/src/lib/storage/search-regex.ts b/src/lib/storage/search-regex.ts index 397d329c..7d21af61 100644 --- a/src/lib/storage/search-regex.ts +++ b/src/lib/storage/search-regex.ts @@ -38,6 +38,7 @@ export function createSafeSearchRegex(pattern: string): RegExp { assertSafe(ast); try { + // eslint-disable-next-line security/detect-non-literal-regexp -- pattern is checked by assertSafe() above return new RegExp(pattern, 'i'); } catch { throw new UnsafeSearchRegexError('Regular expression is invalid'); From 9914cb56e1fd9a3fd2e3d18b89aed790022fa98d Mon Sep 17 00:00:00 2001 From: Johannes Fleck Date: Mon, 26 Oct 2026 16:41:54 +0100 Subject: [PATCH 6/6] fix(storage): stop resolving recent-item links twice fileHref() and locationHref() already return resolved paths. Passing them through resolve() again dropped path segments shaped like a route group, so a recent folder such as logs/(2024)/ linked to logs/. Use the helpers directly, and reuse storageHref() in the storage layout instead of a local copy. Also report unused eslint-disable comments in Svelte templates via svelte/comment-directive and remove the ones that were not needed. Refs #344 --- e2e/storage/recent-items.spec.ts | 37 +++++++++++++++++++ eslint.config.js | 2 + .../storage/landing/RecentItems.svelte | 24 +++--------- .../storage/sidebar/BucketList.svelte | 2 - .../components/trino/StatementResult.svelte | 2 - src/routes/(app)/storage/+layout.svelte | 16 ++------ 6 files changed, 47 insertions(+), 36 deletions(-) diff --git a/e2e/storage/recent-items.spec.ts b/e2e/storage/recent-items.spec.ts index 5dd2284f..2fc571c6 100644 --- a/e2e/storage/recent-items.spec.ts +++ b/e2e/storage/recent-items.spec.ts @@ -5,6 +5,7 @@ import { requireGarageCredentials } from '../support/garage.js'; import { + bucketRoute, connectAndOpenPrefix, deleteKnownKeys, putTextObject, @@ -68,6 +69,42 @@ test.describe('Storage S3 — Recent Items', () => { } }); + test('links to folders with parenthesised names keep the full path', async ({ + page + }, testInfo) => { + const credentials = requireGarageCredentials(); + const client = createS3Client(credentials); + const folder = `${uniquePrefix(testInfo, 'recent-parens')}(2024)/`; + const key = `${folder}report.txt`; + const folderRoute = bucketRoute( + new URL(credentials.endpoint).hostname, + credentials.bucket, + folder + ); + + try { + await putTextObject(client, credentials.bucket, key, 'parenthesised folder'); + + await connectAndOpenPrefix(page, credentials, folder); + await previewFile(page, 'report.txt'); + + await page.goto('/storage'); + await waitForStorageConnected(page); + await expect(page.getByRole('link', { name: 'Open folder — report.txt' })).toHaveAttribute( + 'href', + folderRoute + ); + + await page.getByRole('tab', { name: 'Recent Locations' }).click(); + await expect(page.getByRole('link', { name: 'Go to location — (2024)' })).toHaveAttribute( + 'href', + folderRoute + ); + } finally { + await deleteKnownKeys(client, credentials.bucket, [key]); + } + }); + test('removes a deleted file from Recent Files', async ({ page }, testInfo) => { test.slow(); // multiple navigations + deletion; Firefox is slow in CI const credentials = requireGarageCredentials(); diff --git a/eslint.config.js b/eslint.config.js index 1467741c..c5b66b3f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -29,6 +29,8 @@ export default defineConfig( }, rules: { 'no-undef': 'off', + // linterOptions.reportUnusedDisableDirectives does not cover comments in Svelte templates. + 'svelte/comment-directive': ['error', { reportUnusedDisableDirectives: true }], 'no-console': 'error', // It will currently also error on external links or links with query parameters. // https://github.com/sveltejs/eslint-plugin-svelte/issues/1353 diff --git a/src/lib/components/storage/landing/RecentItems.svelte b/src/lib/components/storage/landing/RecentItems.svelte index 825fccab..44cabee0 100644 --- a/src/lib/components/storage/landing/RecentItems.svelte +++ b/src/lib/components/storage/landing/RecentItems.svelte @@ -1,5 +1,4 @@