diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 2be1587c..f21ee3c8 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -5,6 +5,7 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'plugin:@typescript-eslint/strict', 'prettier' ], plugins: ['svelte3', 'tailwindcss', '@typescript-eslint', 'es', 'vitest'], @@ -19,7 +20,10 @@ module.exports = { 'package.json', 'tsconfig.json' ], - overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], + overrides: [ + { files: ['*.svelte'], processor: 'svelte3/svelte3' } + // { files: ['*.ts'], extends: ['plugin:@typescript-eslint/recommended-requiring-type-checking'] } + ], settings: { 'svelte3/typescript': () => require('typescript') }, diff --git a/.vscode/settings.json b/.vscode/settings.json index ff638551..28b8758f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "editor.formatOnSave": true, - "cSpell.words": ["asyncable", "mindmap", "pako", "Serde", "serdes"], + "cSpell.words": ["asyncable", "mindmap", "pako", "Serde", "serdes", "tailwindcss"], "vitest.commandLine": "yarn test:unit", "vitest.enable": true, "testing.autoRun.mode": "rerun", diff --git a/src/global.d.ts b/src/global.d.ts index 81ca3d20..95ecd6a0 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -9,7 +9,8 @@ declare global { namespace jest { interface Matchers // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore + // @ts-expect-error + // eslint-disable-next-line no-undef extends TestingLibraryMatchers {} } } diff --git a/src/lib/components/Actions.svelte b/src/lib/components/Actions.svelte index 6121fd51..166a58cd 100644 --- a/src/lib/components/Actions.svelte +++ b/src/lib/components/Actions.svelte @@ -15,8 +15,8 @@ `mermaid-diagram-${moment().format('YYYY-MM-DD-HHmmss')}.${ext}`; const getBase64SVG = (svg?: HTMLElement, width?: number, height?: number): string => { - svg?.setAttribute('height', `${height}px`); - svg?.setAttribute('width', `${width}px`); // Workaround https://stackoverflow.com/questions/28690643/firefox-error-rendering-an-svg-image-to-html5-canvas-with-drawimage + height && svg?.setAttribute('height', `${height}px`); + width && svg?.setAttribute('width', `${width}px`); // Workaround https://stackoverflow.com/questions/28690643/firefox-error-rendering-an-svg-image-to-html5-canvas-with-drawimage if (!svg) { svg = getSvgEl(); } @@ -28,7 +28,10 @@ const exportImage = (event: Event, exporter: Exporter) => { const canvas: HTMLCanvasElement = document.createElement('canvas'); - const svg: HTMLElement = document.querySelector('#container svg'); + const svg: HTMLElement | null = document.querySelector('#container svg'); + if (!svg) { + throw new Error('svg not found'); + } const box: DOMRect = svg.getBoundingClientRect(); canvas.width = box.width; canvas.height = box.height; @@ -43,6 +46,9 @@ } const context = canvas.getContext('2d'); + if (!context) { + throw new Error('context not found'); + } context.fillStyle = 'white'; context.fillRect(0, 0, canvas.width, canvas.height); @@ -56,12 +62,12 @@ const getSvgEl = () => { const svgEl: HTMLElement = document - .querySelector('#container svg') + .querySelector('#container svg')! .cloneNode(true) as HTMLElement; svgEl.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); const fontAwesomeCdnUrl = Array.from(document.head.getElementsByTagName('link')) .map((l) => l.href) - .find((h) => h && h.includes('font-awesome')); + .find((h) => h.includes('font-awesome')); if (fontAwesomeCdnUrl == null) { return svgEl; } @@ -99,10 +105,10 @@ context.drawImage(image, 0, 0, canvas.width, canvas.height); canvas.toBlob((blob) => { try { - // @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1004/files + if (!blob) { + throw new Error('blob is empty'); + } void navigator.clipboard.write([ - /* eslint-disable no-undef */ - // @ts-ignore: https://github.com/microsoft/TypeScript/issues/43821 new ClipboardItem({ [blob.type]: blob }) @@ -142,7 +148,7 @@ let gistURL = ''; stateStore.subscribe(({ loader }) => { if (loader?.type === 'gist') { - // @ts-ignore Gist will have url + // @ts-expect-error Gist will have url gistURL = loader.config.url; } }); diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index 19011ed4..5525afe9 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -8,9 +8,9 @@ import initEditor from 'monaco-mermaid'; import { logEvent } from '$lib/util/stats'; - let divEl: HTMLDivElement = null; - let editor: monaco.editor.IStandaloneCodeEditor; - let Monaco: typeof monaco; + let divEl: HTMLDivElement | undefined = undefined; + let editor: monaco.editor.IStandaloneCodeEditor | undefined; + let Monaco: typeof monaco | undefined; let editorOptions: monaco.editor.IStandaloneEditorConstructionOptions = { minimap: { enabled: false @@ -22,7 +22,7 @@ stateStore.subscribe(({ errorMarkers, editorMode, code, mermaid }) => { console.log('editor store subscription', { code, mermaid }); - if (!editor) { + if (!editor || !Monaco) { return; } @@ -36,12 +36,17 @@ // Update editor mode if it's different const language = editorMode === 'code' ? 'mermaid' : 'json'; - if (editor.getModel().getLanguageId() !== language) { - Monaco?.editor.setModelLanguage(editor.getModel(), language); + const model = editor.getModel(); + if (!model) { + console.error("editor model doesn't exist"); + return; + } + if (model.getLanguageId() !== language) { + Monaco.editor.setModelLanguage(model, language); } // Display/clear errors - Monaco?.editor.setModelMarkers(editor.getModel(), 'mermaid', errorMarkers); + Monaco.editor.setModelMarkers(model, 'mermaid', errorMarkers); }); themeStore.subscribe(({ isDark }) => { @@ -62,7 +67,7 @@ // errorDebug(); let i = 0; while (i++ < 500) { - // @ts-ignore : This is a hack to handle a svelte-kit error when importing monaco. + // @ts-expect-error : This is a hack to handle a svelte-kit error when importing monaco. Monaco = window.monaco; if (Monaco !== undefined) { return; @@ -74,14 +79,20 @@ onMount(async () => { await loadMonaco(); // Fix https://github.com/mermaid-js/mermaid-live-editor/issues/175 + if (!Monaco) { + throw new Error('Monaco failed to load'); + } + if (!divEl) { + throw new Error('divEl is undefined'); + } // eslint-disable-next-line @typescript-eslint/no-unsafe-call initEditor(Monaco); errorDebug(100); editor = Monaco.editor.create(divEl, editorOptions); editor.onDidChangeModelContent(({ isFlush, changes }) => { - const newText = editor.getValue(); + const newText = editor?.getValue(); console.log('editor onDidChangeModelContent', { text, newText, isFlush, changes }); - if (text === newText || isFlush) { + if (!newText || text === newText || isFlush) { return; } text = newText; @@ -98,19 +109,21 @@ }); } }); - Monaco?.editor.setTheme($themeStore.isDark ? 'mermaid-dark' : 'mermaid'); + Monaco.editor.setTheme($themeStore.isDark ? 'mermaid-dark' : 'mermaid'); const resizeObserver = new ResizeObserver((entries) => { - editor.layout({ + editor!.layout({ height: entries[0].contentRect.height, width: entries[0].contentRect.width }); }); - resizeObserver.observe(divEl.parentElement); + if (divEl.parentElement) { + resizeObserver.observe(divEl.parentElement); + } console.log(`editor mounted`); return () => { console.log(`editor disposed`); - editor.dispose(); + editor?.dispose(); }; }); diff --git a/src/lib/components/Preset.svelte b/src/lib/components/Preset.svelte index 0eae7dc5..ad3831ee 100644 --- a/src/lib/components/Preset.svelte +++ b/src/lib/components/Preset.svelte @@ -114,8 +114,8 @@ }; // Adding in this array will add an icon to the preset menu - const newDiagrams: Array = ['Mindmap']; - const diagramOrder: Array = [ + const newDiagrams: SampleTypes[] = ['Mindmap']; + const diagramOrder: SampleTypes[] = [ 'Sequence', 'Flow', 'Class', diff --git a/src/lib/components/Theme.svelte b/src/lib/components/Theme.svelte index 6f456a68..4226159d 100644 --- a/src/lib/components/Theme.svelte +++ b/src/lib/components/Theme.svelte @@ -25,7 +25,7 @@ '🧛‍♂️ dracula' ]; - function checkTheme(theme: string) { + function checkTheme(theme: string): boolean { return theme.includes($themeStore.theme); } diff --git a/src/lib/components/View.svelte b/src/lib/components/View.svelte index 46f0cf0d..28fa976f 100644 --- a/src/lib/components/View.svelte +++ b/src/lib/components/View.svelte @@ -17,9 +17,12 @@ let hide = false; let manualUpdate = true; let panZoomEnabled = $stateStore.panZoom; - let pzoom: typeof panzoom; + let pzoom: typeof panzoom | undefined; const handlePanZoomChange = () => { + if (!pzoom) { + return; + } const pan = pzoom.getPan(); const zoom = pzoom.getZoom(); updateCodeStore({ pan, zoom }); @@ -35,6 +38,9 @@ pzoom = undefined; void Promise.resolve().then(() => { const graphDiv = document.getElementById('graph-div'); + if (!graphDiv) { + return; + } pzoom = panzoom(graphDiv, { onPan: handlePanZoomChange, onZoom: handlePanZoomChange, @@ -71,7 +77,7 @@ code = state.code; config = state.mermaid; panZoomEnabled = state.panZoom; - const scroll = view.parentElement.scrollTop; + const scroll = view.parentElement!.scrollTop; delete container.dataset.processed; await renderDiagram( Object.assign({}, JSON.parse(state.mermaid)) as MermaidConfig, @@ -83,6 +89,9 @@ container.innerHTML = svgCode; // console.log(container.innerHTML); const graphDiv = document.getElementById('graph-div'); + if (!graphDiv) { + throw new Error('graph-div not found'); + } graphDiv.setAttribute('height', '100%'); graphDiv.style.maxWidth = '100%'; if (bindFunctions) { @@ -91,7 +100,7 @@ } } ); - view.parentElement.scrollTop = scroll; + view.parentElement!.scrollTop = scroll; error = false; } else if (manualUpdate) { manualUpdate = false; diff --git a/src/lib/types.d.ts b/src/lib/types.d.ts index 30f94b9c..33a3fc69 100644 --- a/src/lib/types.d.ts +++ b/src/lib/types.d.ts @@ -73,12 +73,13 @@ export type HistoryEntry = { id: string; state: State; time: number; url?: strin } ); -export interface DocConfig { - [key: string]: { +export type DocConfig = Record< + string, + { code: string; config?: string; - }; -} + } +>; export type EditorMode = 'code' | 'config'; diff --git a/src/lib/util/fileLoaders/loader.ts b/src/lib/util/fileLoaders/loader.ts index e40a988f..596d02a2 100644 --- a/src/lib/util/fileLoaders/loader.ts +++ b/src/lib/util/fileLoaders/loader.ts @@ -8,10 +8,11 @@ const loaders: Record = { export const loadDataFromUrl = async (): Promise => { const searchParams = new URLSearchParams(window.location.search); let state: Partial = defaultState; - let code: string, config: string; + let code: string | undefined = undefined; + let config: string | undefined = undefined; let loaded = false; - const codeURL: string = searchParams.get('code'); - const configURL: string = searchParams.get('config'); + const codeURL: string | undefined = searchParams.get('code') ?? undefined; + const configURL: string | undefined = searchParams.get('config') ?? undefined; if (codeURL) { code = await (await fetch(codeURL)).text(); @@ -23,9 +24,6 @@ export const loadDataFromUrl = async (): Promise => { config = defaultState.mermaid; } if (!code) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-call for (const [key, value] of searchParams.entries()) { if (key in loaders) { try { @@ -38,6 +36,9 @@ export const loadDataFromUrl = async (): Promise => { } } } else { + if (!codeURL) { + throw new Error('Code URL is not defined'); + } state = { code, mermaid: config, diff --git a/src/lib/util/migrations.ts b/src/lib/util/migrations.ts index ba6a7c53..7851270b 100644 --- a/src/lib/util/migrations.ts +++ b/src/lib/util/migrations.ts @@ -7,7 +7,7 @@ interface MigrationState { version: number; } -const migrations: { [key: string]: () => void } = { +const migrations: Record void> = { injectHistoryIDs }; diff --git a/src/lib/util/persist.ts b/src/lib/util/persist.ts index 3beb4704..584a0f76 100644 --- a/src/lib/util/persist.ts +++ b/src/lib/util/persist.ts @@ -39,16 +39,16 @@ let noWarnings = false; /** * List of storages where the warning have already been displayed. */ -const alreadyWarnFor: Array = []; +const alreadyWarnFor: string[] = []; /** * Add a log to indicate that the requested Storage have not been found. * @param {string} storageName */ const warnStorageNotFound = (storageName: string) => { - const isProduction = typeof process !== 'undefined' && process.env?.NODE_ENV === 'production'; + const isProduction = typeof process !== 'undefined' && process.env.NODE_ENV === 'production'; - if (!noWarnings && alreadyWarnFor.indexOf(storageName) === -1 && !isProduction) { + if (!noWarnings && !alreadyWarnFor.includes(storageName) && !isProduction) { let message = `Unable to find the ${storageName}. No data will be persisted.`; if (typeof window === 'undefined') { message += @@ -60,17 +60,8 @@ const warnStorageNotFound = (storageName: string) => { } }; -const allowedClasses = []; -/** - * Add a class to the allowed list of classes to be serialized - * @param classDef The class to add to the list - */ -export const addSerializableClass = (classDef: () => unknown): void => { - allowedClasses.push(classDef); -}; - const serialize = (value: unknown): string => ESSerializer.serialize(value); -const deserialize = (value: string): unknown => { +const deserialize = (value?: string | null): unknown => { // @TODO: to remove in the next major if (value === 'undefined') { return undefined; @@ -78,7 +69,7 @@ const deserialize = (value: string): unknown => { if (value !== null && value !== undefined) { try { - return ESSerializer.deserialize(value, allowedClasses); + return ESSerializer.deserialize(value); } catch (e) { // Do nothing // use the value "as is" @@ -184,7 +175,7 @@ function getBrowserStorage( browserStorage: Storage, listenExternalChanges = false ): SelfUpdateStorageInterface { - const listeners: Array<{ key: string; listener: (newValue: any) => void }> = []; + const listeners: { key: string; listener: (newValue: any) => void }[] = []; const listenerFunction = (event: StorageEvent) => { const eventKey = event.key; if (event.storageArea === browserStorage) { @@ -196,12 +187,14 @@ function getBrowserStorage( } }; const connect = () => { - if (listenExternalChanges && typeof window !== 'undefined' && window?.addEventListener) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (listenExternalChanges && typeof window !== 'undefined' && window.addEventListener) { window.addEventListener('storage', listenerFunction); } }; const disconnect = () => { - if (listenExternalChanges && typeof window !== 'undefined' && window?.removeEventListener) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (listenExternalChanges && typeof window !== 'undefined' && window.removeEventListener) { window.removeEventListener('storage', listenerFunction); } }; @@ -240,7 +233,8 @@ function getBrowserStorage( * @param listenExternalChanges - Update the store if the localStorage is updated from another page */ export function localStorage(listenExternalChanges = false): StorageInterface { - if (typeof window !== 'undefined' && window?.localStorage) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (typeof window !== 'undefined' && window.localStorage) { return getBrowserStorage(window.localStorage, listenExternalChanges); } warnStorageNotFound('window.localStorage'); diff --git a/src/lib/util/state.ts b/src/lib/util/state.ts index 4d0387bd..dd298275 100644 --- a/src/lib/util/state.ts +++ b/src/lib/util/state.ts @@ -111,7 +111,7 @@ export const loadState = (data: string): void => { console.log(`Loading '${data}'`); try { state = deserializeState(data); - const mermaidConfig: { [key: string]: string } = + const mermaidConfig: Record = typeof state.mermaid === 'string' ? JSON.parse(state.mermaid) : state.mermaid; if ( mermaidConfig.securityLevel && diff --git a/src/lib/util/stats.ts b/src/lib/util/stats.ts index b86405ce..c349088d 100644 --- a/src/lib/util/stats.ts +++ b/src/lib/util/stats.ts @@ -1,6 +1,6 @@ import { browser } from '$app/environment'; import type { AnalyticsInstance } from 'analytics'; -export let analytics: AnalyticsInstance; +export let analytics: AnalyticsInstance | undefined; export const initAnalytics = async (): Promise => { if (browser && !analytics) { @@ -27,7 +27,7 @@ export const initAnalytics = async (): Promise => { } }; -export const detectType = (text: string): string => { +export const detectType = (text: string): string | undefined => { const possibleDiagramTypes = [ 'classDiagram', 'erDiagram', @@ -49,7 +49,7 @@ export const detectType = (text: string): string => { }; export const countLines = (code: string): number => { - return (code.match(/\n/g) || '').length + 1; + return (code.match(/\n/g)?.length ?? 0) + 1; }; export const saveStatistics = (graph: string): void => { @@ -80,19 +80,20 @@ const delaysPerEvent = { themeChange: defaultDelay }; export type AnalyticsEvent = keyof typeof delaysPerEvent; -const timeouts: Record = {}; +const timeouts: Map = new Map(); // manual debounce to reduce the number of events sent to analytics export const logEvent = (name: AnalyticsEvent, data?: unknown): void => { if (!analytics) { return; } const key = data ? JSON.stringify({ name, data }) : name; - if (timeouts[key] === undefined) { + if (!timeouts.has(key)) { void analytics.track(name, data); } else { - clearTimeout(timeouts[key]); + clearTimeout(timeouts.get(key)); } - timeouts[key] = window.setTimeout(() => { - delete timeouts[key]; - }, delaysPerEvent[name]); + timeouts.set( + key, + window.setTimeout(() => timeouts.delete(key), delaysPerEvent[name]) + ); }; diff --git a/src/lib/util/util.ts b/src/lib/util/util.ts index 1f8588e0..c762bc03 100644 --- a/src/lib/util/util.ts +++ b/src/lib/util/util.ts @@ -23,10 +23,10 @@ export const initHandler = async (): Promise => { syncDiagram(); initURLSubscription(); await initAnalytics(); - await analytics?.page(); + await analytics.page(); }; -export const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; +export const isMac = navigator.platform.toUpperCase().includes('MAC'); export const cmdKey = isMac ? 'Cmd' : 'Ctrl'; let count = 0; diff --git a/tsconfig.json b/tsconfig.json index 38b29b7f..91263b2e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "compilerOptions": { "resolveJsonModule": true, "allowSyntheticDefaultImports": true, + "strictNullChecks": true, "types": ["vitest/importMeta"] }, "extends": "./.svelte-kit/tsconfig.json"