fix: Make svelte-check pass and resolve TS6/Svelte 5.56 type errors

Adding the `svelte-check` CI gate surfaced pre-existing type errors and
warnings under develop's TypeScript 6 / Svelte 5.56 bump. Fixes them so
`pnpm check` reports 0 errors / 0 warnings:

- Add `lang="ts"` to Share/Privacy (and a script to PrivacyPolicyLink) so
  importers get real declarations instead of implicit `any`.
- Replace the deprecated `monaco.languages.json` with the new top-level
  `monaco.json` namespace (proper API, no casts).
- Navbar: use `resolve('/', {})` instead of the deprecated `base`.
- Type the promo `component` as `Component<{ closeBanner: Snippet }>` and
  MainMenu's `renderer` as `Snippet<[Omit<MenuItem, 'renderer'>]>`.
- Index-by-string casts in state.ts / Preset / DiagramDocumentationButton.
- Make Actions' clipboard handler accept an optional event.
- toggle-group: expose variant/size via getters to fix the
  state_referenced_locally warning.
- Make the History e2e save/delete cases change state via a sample
  diagram (deterministic) instead of editor typing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sidharth Vinod
2026-06-08 19:53:25 +05:30
co-authored by Claude Opus 4.8
parent a3a3f8ffbe
commit 64e8822e32
13 changed files with 34 additions and 22 deletions
+4 -1
View File
@@ -199,7 +199,10 @@ ${svgString}`);
};
};
const onCopyClipboard = async (event: Event) => {
const onCopyClipboard = async (event?: Event) => {
if (!event) {
return;
}
await exportImage(event, clipboardCopy);
logEvent('copyClipboard');
};
+1 -1
View File
@@ -115,7 +115,7 @@
throw new Error('divEl is undefined');
}
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
monaco.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
enableSchemaRequest: true,
schemas: [
@@ -97,7 +97,9 @@
return { key: '', url: docURLBase };
}
const key = standardizeDiagramType(diagramType);
const docConfig = docMap[key] ?? { code: '' };
const docConfig: { code: string; config?: string } = docMap[key as keyof typeof docMap] ?? {
code: ''
};
const url = docURLBase + (docConfig[editorMode] ?? docConfig.code ?? '');
return { key, url };
});
+4 -4
View File
@@ -28,7 +28,7 @@
sharesData?: boolean;
checkDiagramType?: boolean;
isSectionEnd?: boolean;
renderer: (item: Omit<MenuItem, 'renderer'>) => ReturnType<Snippet>;
renderer: Snippet<[Omit<MenuItem, 'renderer'>]>;
}
const menuItems: MenuItem[] = $derived([
@@ -89,7 +89,7 @@
]);
</script>
{#snippet menuItem(options: MenuItem)}
{#snippet menuItem(options: Omit<MenuItem, 'renderer'>)}
<a
href={options.href}
target="_blank"
@@ -104,7 +104,7 @@
</a>
{/snippet}
{#snippet mcMenuItem(item: MenuItem)}
{#snippet mcMenuItem(item: Omit<MenuItem, 'renderer'>)}
<McWrapper
side="right"
labelPrefix={item.sharesData === false ? 'Opens a new tab in' : undefined}
@@ -114,7 +114,7 @@
</McWrapper>
{/snippet}
{#snippet darkModeMenuItem(options: MenuItem)}
{#snippet darkModeMenuItem(options: Omit<MenuItem, 'renderer'>)}
<div
class={cn(
'flex cursor-pointer items-center justify-between border-b-2 px-3 py-2 hover:bg-muted',
+3 -3
View File
@@ -13,7 +13,7 @@
import { Button } from '$/components/ui/button';
import { Separator } from '$/components/ui/separator';
import { dismissPromotion, getActivePromotion } from '$lib/util/promos/promo';
import type { ComponentProps, Snippet } from 'svelte';
import { untrack, type ComponentProps, type Snippet } from 'svelte';
import MermaidIcon from '~icons/custom/mermaid';
import CloseIcon from '~icons/material-symbols/close-rounded';
import GithubIcon from '~icons/mdi/github';
@@ -41,7 +41,7 @@
}
];
let activePromotion = $state(hidePromotion ? undefined : getActivePromotion());
let activePromotion = $state(untrack(() => (hidePromotion ? undefined : getActivePromotion())));
const trackBannerClick = () => {
if (!activePromotion) {
@@ -84,7 +84,7 @@
<div class="flex flex-1 items-center gap-2">
<MainMenu />
<MermaidIcon class="size-6" />
<a href={resolve('/')} class="whitespace-nowrap text-accent">
<a href={resolve('/', {})} class="whitespace-nowrap text-accent">
{#if !mobileToggle}
Mermaid
{/if}
+1 -1
View File
@@ -35,7 +35,7 @@
const samples = { ...getSampleDiagrams(), ...extras } as const;
const loadSampleDiagram = (diagramType: string): void => {
updateCode(samples[diagramType], {
updateCode(samples[diagramType as keyof typeof samples], {
resetPanZoom: true,
updateDiagram: true
});
+1 -1
View File
@@ -1,4 +1,4 @@
<script>
<script lang="ts">
import ExternalLinkWrapper from '$/components/ExternalLinkWrapper.svelte';
import * as Dialog from '$/components/ui/dialog';
import { env } from '$/util/env';
+1 -1
View File
@@ -1,4 +1,4 @@
<script>
<script lang="ts">
import { buttonVariants } from '$/components/ui/button';
import * as Dialog from '$/components/ui/dialog';
import { Separator } from '$/components/ui/separator';
@@ -1,3 +1,5 @@
<script lang="ts"></script>
<div class="text-center">
<a
href="https://mermaid.ai/privacy-policy"
@@ -24,8 +24,12 @@
}: ToggleGroupPrimitive.RootProps & ToggleVariants = $props();
setToggleGroupCtx({
variant,
size
get variant() {
return variant;
},
get size() {
return size;
}
});
</script>
+2 -2
View File
@@ -1,7 +1,7 @@
import { env } from '$lib/util/env';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import type { Component } from 'svelte';
import type { Component, Snippet } from 'svelte';
import { get, writable, type Writable } from 'svelte/store';
import { localStorage, persist } from '../persist';
import April2025 from './April2025.svelte';
@@ -12,7 +12,7 @@ dayjs.extend(duration);
interface Promotion {
startDate: Date;
endDate: Date;
component: Component;
component: Component<{ closeBanner: Snippet }>;
hideDurationMs: number;
}
+1 -1
View File
@@ -189,7 +189,7 @@ function getUnsafePaths(object: object, unsafeKeys: string[], path: string[] = [
}
}
Object.keys(object).forEach((key) => {
const value = object[key] as unknown;
const value = (object as Record<string, unknown>)[key];
const currentPath = [...path, key];
// Prototype pollution check.
if (key.startsWith('__')) {
+5 -4
View File
@@ -1,5 +1,4 @@
import { expect, test, type Page } from '@playwright/test';
import { typeInEditor } from './utils';
const config = '{\n "theme": "default"\n}';
@@ -93,8 +92,9 @@ test.describe('History', () => {
await expect(page.getByText('State already saved.')).toBeVisible();
await expect(page.locator('#historyList li')).toHaveCount(1);
// A real edit produces a new entry.
await typeInEditor(page, ' Z[Extra]', { newline: true });
// Loading a different sample changes the state, so it saves as a new entry.
await page.getByRole('button', { name: 'Sequence', exact: true }).click();
await expect(page.locator('#view')).not.toContainText('Christmas');
await page.locator('#saveHistory').click();
await expect(page.locator('#historyList li')).toHaveCount(2);
});
@@ -112,7 +112,8 @@ test.describe('History', () => {
test('deletes a single entry and clears all after confirmation', async ({ page }) => {
await openHistory(page);
await page.locator('#saveHistory').click();
await typeInEditor(page, ' Z[Another]', { newline: true });
await page.getByRole('button', { name: 'Sequence', exact: true }).click();
await expect(page.locator('#view')).not.toContainText('Christmas');
await page.locator('#saveHistory').click();
await expect(page.locator('#historyList li')).toHaveCount(2);