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/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/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..c5b66b3f 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, @@ -23,20 +23,23 @@ export default defineConfig( prettier, ...svelte.configs.prettier, { + linterOptions: { reportUnusedDisableDirectives: 'error' }, languageOptions: { globals: { ...globals.browser, ...globals.node } }, rules: { 'no-undef': 'off', - 'no-console': '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 - '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 .' } ], - // 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 +76,8 @@ export default defineConfig( 'tab-active', 'tab-strip', 'preview-scroll', - 'modal-top-search' + 'modal-top-search', + 'table-pin-rows' ] } ], @@ -88,8 +92,14 @@ 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' } + }, + { + // 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'], @@ -131,15 +141,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/package.json b/package.json index f77f39fb..82b40966 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/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/components/layout/sidebar/Sidebar.svelte b/src/lib/components/layout/sidebar/Sidebar.svelte index ef737206..9c754cd9 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 7fecc5d5..9aa79a4e 100644 --- a/src/lib/components/storage/landing/RecentItems.svelte +++ b/src/lib/components/storage/landing/RecentItems.svelte @@ -1,5 +1,4 @@