chore: Cleanup potentially

problematic code.
This commit is contained in:
Sidharth Vinod
2022-11-15 18:48:16 +05:30
parent 60c352a849
commit 85ad89b915
16 changed files with 104 additions and 73 deletions
+5 -1
View File
@@ -5,6 +5,7 @@ module.exports = {
'eslint:recommended', 'eslint:recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking', 'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/strict',
'prettier' 'prettier'
], ],
plugins: ['svelte3', 'tailwindcss', '@typescript-eslint', 'es', 'vitest'], plugins: ['svelte3', 'tailwindcss', '@typescript-eslint', 'es', 'vitest'],
@@ -19,7 +20,10 @@ module.exports = {
'package.json', 'package.json',
'tsconfig.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: { settings: {
'svelte3/typescript': () => require('typescript') 'svelte3/typescript': () => require('typescript')
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"cSpell.words": ["asyncable", "mindmap", "pako", "Serde", "serdes"], "cSpell.words": ["asyncable", "mindmap", "pako", "Serde", "serdes", "tailwindcss"],
"vitest.commandLine": "yarn test:unit", "vitest.commandLine": "yarn test:unit",
"vitest.enable": true, "vitest.enable": true,
"testing.autoRun.mode": "rerun", "testing.autoRun.mode": "rerun",
+2 -1
View File
@@ -9,7 +9,8 @@ declare global {
namespace jest { namespace jest {
interface Matchers<R = void> interface Matchers<R = void>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-expect-error
// eslint-disable-next-line no-undef
extends TestingLibraryMatchers<typeof expect.stringContaining, R> {} extends TestingLibraryMatchers<typeof expect.stringContaining, R> {}
} }
} }
+15 -9
View File
@@ -15,8 +15,8 @@
`mermaid-diagram-${moment().format('YYYY-MM-DD-HHmmss')}.${ext}`; `mermaid-diagram-${moment().format('YYYY-MM-DD-HHmmss')}.${ext}`;
const getBase64SVG = (svg?: HTMLElement, width?: number, height?: number): string => { const getBase64SVG = (svg?: HTMLElement, width?: number, height?: number): string => {
svg?.setAttribute('height', `${height}px`); height && 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 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) { if (!svg) {
svg = getSvgEl(); svg = getSvgEl();
} }
@@ -28,7 +28,10 @@
const exportImage = (event: Event, exporter: Exporter) => { const exportImage = (event: Event, exporter: Exporter) => {
const canvas: HTMLCanvasElement = document.createElement('canvas'); 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(); const box: DOMRect = svg.getBoundingClientRect();
canvas.width = box.width; canvas.width = box.width;
canvas.height = box.height; canvas.height = box.height;
@@ -43,6 +46,9 @@
} }
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
if (!context) {
throw new Error('context not found');
}
context.fillStyle = 'white'; context.fillStyle = 'white';
context.fillRect(0, 0, canvas.width, canvas.height); context.fillRect(0, 0, canvas.width, canvas.height);
@@ -56,12 +62,12 @@
const getSvgEl = () => { const getSvgEl = () => {
const svgEl: HTMLElement = document const svgEl: HTMLElement = document
.querySelector('#container svg') .querySelector('#container svg')!
.cloneNode(true) as HTMLElement; .cloneNode(true) as HTMLElement;
svgEl.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); svgEl.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
const fontAwesomeCdnUrl = Array.from(document.head.getElementsByTagName('link')) const fontAwesomeCdnUrl = Array.from(document.head.getElementsByTagName('link'))
.map((l) => l.href) .map((l) => l.href)
.find((h) => h && h.includes('font-awesome')); .find((h) => h.includes('font-awesome'));
if (fontAwesomeCdnUrl == null) { if (fontAwesomeCdnUrl == null) {
return svgEl; return svgEl;
} }
@@ -99,10 +105,10 @@
context.drawImage(image, 0, 0, canvas.width, canvas.height); context.drawImage(image, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => { canvas.toBlob((blob) => {
try { 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([ void navigator.clipboard.write([
/* eslint-disable no-undef */
// @ts-ignore: https://github.com/microsoft/TypeScript/issues/43821
new ClipboardItem({ new ClipboardItem({
[blob.type]: blob [blob.type]: blob
}) })
@@ -142,7 +148,7 @@
let gistURL = ''; let gistURL = '';
stateStore.subscribe(({ loader }) => { stateStore.subscribe(({ loader }) => {
if (loader?.type === 'gist') { if (loader?.type === 'gist') {
// @ts-ignore Gist will have url // @ts-expect-error Gist will have url
gistURL = loader.config.url; gistURL = loader.config.url;
} }
}); });
+27 -14
View File
@@ -8,9 +8,9 @@
import initEditor from 'monaco-mermaid'; import initEditor from 'monaco-mermaid';
import { logEvent } from '$lib/util/stats'; import { logEvent } from '$lib/util/stats';
let divEl: HTMLDivElement = null; let divEl: HTMLDivElement | undefined = undefined;
let editor: monaco.editor.IStandaloneCodeEditor; let editor: monaco.editor.IStandaloneCodeEditor | undefined;
let Monaco: typeof monaco; let Monaco: typeof monaco | undefined;
let editorOptions: monaco.editor.IStandaloneEditorConstructionOptions = { let editorOptions: monaco.editor.IStandaloneEditorConstructionOptions = {
minimap: { minimap: {
enabled: false enabled: false
@@ -22,7 +22,7 @@
stateStore.subscribe(({ errorMarkers, editorMode, code, mermaid }) => { stateStore.subscribe(({ errorMarkers, editorMode, code, mermaid }) => {
console.log('editor store subscription', { code, mermaid }); console.log('editor store subscription', { code, mermaid });
if (!editor) { if (!editor || !Monaco) {
return; return;
} }
@@ -36,12 +36,17 @@
// Update editor mode if it's different // Update editor mode if it's different
const language = editorMode === 'code' ? 'mermaid' : 'json'; const language = editorMode === 'code' ? 'mermaid' : 'json';
if (editor.getModel().getLanguageId() !== language) { const model = editor.getModel();
Monaco?.editor.setModelLanguage(editor.getModel(), language); if (!model) {
console.error("editor model doesn't exist");
return;
}
if (model.getLanguageId() !== language) {
Monaco.editor.setModelLanguage(model, language);
} }
// Display/clear errors // Display/clear errors
Monaco?.editor.setModelMarkers(editor.getModel(), 'mermaid', errorMarkers); Monaco.editor.setModelMarkers(model, 'mermaid', errorMarkers);
}); });
themeStore.subscribe(({ isDark }) => { themeStore.subscribe(({ isDark }) => {
@@ -62,7 +67,7 @@
// errorDebug(); // errorDebug();
let i = 0; let i = 0;
while (i++ < 500) { 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; Monaco = window.monaco;
if (Monaco !== undefined) { if (Monaco !== undefined) {
return; return;
@@ -74,14 +79,20 @@
onMount(async () => { onMount(async () => {
await loadMonaco(); // Fix https://github.com/mermaid-js/mermaid-live-editor/issues/175 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 // eslint-disable-next-line @typescript-eslint/no-unsafe-call
initEditor(Monaco); initEditor(Monaco);
errorDebug(100); errorDebug(100);
editor = Monaco.editor.create(divEl, editorOptions); editor = Monaco.editor.create(divEl, editorOptions);
editor.onDidChangeModelContent(({ isFlush, changes }) => { editor.onDidChangeModelContent(({ isFlush, changes }) => {
const newText = editor.getValue(); const newText = editor?.getValue();
console.log('editor onDidChangeModelContent', { text, newText, isFlush, changes }); console.log('editor onDidChangeModelContent', { text, newText, isFlush, changes });
if (text === newText || isFlush) { if (!newText || text === newText || isFlush) {
return; return;
} }
text = newText; 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) => { const resizeObserver = new ResizeObserver((entries) => {
editor.layout({ editor!.layout({
height: entries[0].contentRect.height, height: entries[0].contentRect.height,
width: entries[0].contentRect.width width: entries[0].contentRect.width
}); });
}); });
resizeObserver.observe(divEl.parentElement); if (divEl.parentElement) {
resizeObserver.observe(divEl.parentElement);
}
console.log(`editor mounted`); console.log(`editor mounted`);
return () => { return () => {
console.log(`editor disposed`); console.log(`editor disposed`);
editor.dispose(); editor?.dispose();
}; };
}); });
</script> </script>
+2 -2
View File
@@ -114,8 +114,8 @@
}; };
// Adding in this array will add an icon to the preset menu // Adding in this array will add an icon to the preset menu
const newDiagrams: Array<SampleTypes> = ['Mindmap']; const newDiagrams: SampleTypes[] = ['Mindmap'];
const diagramOrder: Array<SampleTypes> = [ const diagramOrder: SampleTypes[] = [
'Sequence', 'Sequence',
'Flow', 'Flow',
'Class', 'Class',
+1 -1
View File
@@ -25,7 +25,7 @@
'🧛‍♂️ dracula' '🧛‍♂️ dracula'
]; ];
function checkTheme(theme: string) { function checkTheme(theme: string): boolean {
return theme.includes($themeStore.theme); return theme.includes($themeStore.theme);
} }
</script> </script>
+12 -3
View File
@@ -17,9 +17,12 @@
let hide = false; let hide = false;
let manualUpdate = true; let manualUpdate = true;
let panZoomEnabled = $stateStore.panZoom; let panZoomEnabled = $stateStore.panZoom;
let pzoom: typeof panzoom; let pzoom: typeof panzoom | undefined;
const handlePanZoomChange = () => { const handlePanZoomChange = () => {
if (!pzoom) {
return;
}
const pan = pzoom.getPan(); const pan = pzoom.getPan();
const zoom = pzoom.getZoom(); const zoom = pzoom.getZoom();
updateCodeStore({ pan, zoom }); updateCodeStore({ pan, zoom });
@@ -35,6 +38,9 @@
pzoom = undefined; pzoom = undefined;
void Promise.resolve().then(() => { void Promise.resolve().then(() => {
const graphDiv = document.getElementById('graph-div'); const graphDiv = document.getElementById('graph-div');
if (!graphDiv) {
return;
}
pzoom = panzoom(graphDiv, { pzoom = panzoom(graphDiv, {
onPan: handlePanZoomChange, onPan: handlePanZoomChange,
onZoom: handlePanZoomChange, onZoom: handlePanZoomChange,
@@ -71,7 +77,7 @@
code = state.code; code = state.code;
config = state.mermaid; config = state.mermaid;
panZoomEnabled = state.panZoom; panZoomEnabled = state.panZoom;
const scroll = view.parentElement.scrollTop; const scroll = view.parentElement!.scrollTop;
delete container.dataset.processed; delete container.dataset.processed;
await renderDiagram( await renderDiagram(
Object.assign({}, JSON.parse(state.mermaid)) as MermaidConfig, Object.assign({}, JSON.parse(state.mermaid)) as MermaidConfig,
@@ -83,6 +89,9 @@
container.innerHTML = svgCode; container.innerHTML = svgCode;
// console.log(container.innerHTML); // console.log(container.innerHTML);
const graphDiv = document.getElementById('graph-div'); const graphDiv = document.getElementById('graph-div');
if (!graphDiv) {
throw new Error('graph-div not found');
}
graphDiv.setAttribute('height', '100%'); graphDiv.setAttribute('height', '100%');
graphDiv.style.maxWidth = '100%'; graphDiv.style.maxWidth = '100%';
if (bindFunctions) { if (bindFunctions) {
@@ -91,7 +100,7 @@
} }
} }
); );
view.parentElement.scrollTop = scroll; view.parentElement!.scrollTop = scroll;
error = false; error = false;
} else if (manualUpdate) { } else if (manualUpdate) {
manualUpdate = false; manualUpdate = false;
+5 -4
View File
@@ -73,12 +73,13 @@ export type HistoryEntry = { id: string; state: State; time: number; url?: strin
} }
); );
export interface DocConfig { export type DocConfig = Record<
[key: string]: { string,
{
code: string; code: string;
config?: string; config?: string;
}; }
} >;
export type EditorMode = 'code' | 'config'; export type EditorMode = 'code' | 'config';
+7 -6
View File
@@ -8,10 +8,11 @@ const loaders: Record<string, Loader> = {
export const loadDataFromUrl = async (): Promise<void> => { export const loadDataFromUrl = async (): Promise<void> => {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
let state: Partial<State> = defaultState; let state: Partial<State> = defaultState;
let code: string, config: string; let code: string | undefined = undefined;
let config: string | undefined = undefined;
let loaded = false; let loaded = false;
const codeURL: string = searchParams.get('code'); const codeURL: string | undefined = searchParams.get('code') ?? undefined;
const configURL: string = searchParams.get('config'); const configURL: string | undefined = searchParams.get('config') ?? undefined;
if (codeURL) { if (codeURL) {
code = await (await fetch(codeURL)).text(); code = await (await fetch(codeURL)).text();
@@ -23,9 +24,6 @@ export const loadDataFromUrl = async (): Promise<void> => {
config = defaultState.mermaid; config = defaultState.mermaid;
} }
if (!code) { 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()) { for (const [key, value] of searchParams.entries()) {
if (key in loaders) { if (key in loaders) {
try { try {
@@ -38,6 +36,9 @@ export const loadDataFromUrl = async (): Promise<void> => {
} }
} }
} else { } else {
if (!codeURL) {
throw new Error('Code URL is not defined');
}
state = { state = {
code, code,
mermaid: config, mermaid: config,
+1 -1
View File
@@ -7,7 +7,7 @@ interface MigrationState {
version: number; version: number;
} }
const migrations: { [key: string]: () => void } = { const migrations: Record<string, () => void> = {
injectHistoryIDs injectHistoryIDs
}; };
+12 -18
View File
@@ -39,16 +39,16 @@ let noWarnings = false;
/** /**
* List of storages where the warning have already been displayed. * List of storages where the warning have already been displayed.
*/ */
const alreadyWarnFor: Array<string> = []; const alreadyWarnFor: string[] = [];
/** /**
* Add a log to indicate that the requested Storage have not been found. * Add a log to indicate that the requested Storage have not been found.
* @param {string} storageName * @param {string} storageName
*/ */
const warnStorageNotFound = (storageName: string) => { 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.`; let message = `Unable to find the ${storageName}. No data will be persisted.`;
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
message += 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 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 // @TODO: to remove in the next major
if (value === 'undefined') { if (value === 'undefined') {
return undefined; return undefined;
@@ -78,7 +69,7 @@ const deserialize = (value: string): unknown => {
if (value !== null && value !== undefined) { if (value !== null && value !== undefined) {
try { try {
return ESSerializer.deserialize(value, allowedClasses); return ESSerializer.deserialize(value);
} catch (e) { } catch (e) {
// Do nothing // Do nothing
// use the value "as is" // use the value "as is"
@@ -184,7 +175,7 @@ function getBrowserStorage(
browserStorage: Storage, browserStorage: Storage,
listenExternalChanges = false listenExternalChanges = false
): SelfUpdateStorageInterface<any> { ): SelfUpdateStorageInterface<any> {
const listeners: Array<{ key: string; listener: (newValue: any) => void }> = []; const listeners: { key: string; listener: (newValue: any) => void }[] = [];
const listenerFunction = (event: StorageEvent) => { const listenerFunction = (event: StorageEvent) => {
const eventKey = event.key; const eventKey = event.key;
if (event.storageArea === browserStorage) { if (event.storageArea === browserStorage) {
@@ -196,12 +187,14 @@ function getBrowserStorage(
} }
}; };
const connect = () => { 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); window.addEventListener('storage', listenerFunction);
} }
}; };
const disconnect = () => { 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); window.removeEventListener('storage', listenerFunction);
} }
}; };
@@ -240,7 +233,8 @@ function getBrowserStorage(
* @param listenExternalChanges - Update the store if the localStorage is updated from another page * @param listenExternalChanges - Update the store if the localStorage is updated from another page
*/ */
export function localStorage<T>(listenExternalChanges = false): StorageInterface<T> { export function localStorage<T>(listenExternalChanges = false): StorageInterface<T> {
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); return getBrowserStorage(window.localStorage, listenExternalChanges);
} }
warnStorageNotFound('window.localStorage'); warnStorageNotFound('window.localStorage');
+1 -1
View File
@@ -111,7 +111,7 @@ export const loadState = (data: string): void => {
console.log(`Loading '${data}'`); console.log(`Loading '${data}'`);
try { try {
state = deserializeState(data); state = deserializeState(data);
const mermaidConfig: { [key: string]: string } = const mermaidConfig: Record<string, string> =
typeof state.mermaid === 'string' ? JSON.parse(state.mermaid) : state.mermaid; typeof state.mermaid === 'string' ? JSON.parse(state.mermaid) : state.mermaid;
if ( if (
mermaidConfig.securityLevel && mermaidConfig.securityLevel &&
+10 -9
View File
@@ -1,6 +1,6 @@
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import type { AnalyticsInstance } from 'analytics'; import type { AnalyticsInstance } from 'analytics';
export let analytics: AnalyticsInstance; export let analytics: AnalyticsInstance | undefined;
export const initAnalytics = async (): Promise<void> => { export const initAnalytics = async (): Promise<void> => {
if (browser && !analytics) { if (browser && !analytics) {
@@ -27,7 +27,7 @@ export const initAnalytics = async (): Promise<void> => {
} }
}; };
export const detectType = (text: string): string => { export const detectType = (text: string): string | undefined => {
const possibleDiagramTypes = [ const possibleDiagramTypes = [
'classDiagram', 'classDiagram',
'erDiagram', 'erDiagram',
@@ -49,7 +49,7 @@ export const detectType = (text: string): string => {
}; };
export const countLines = (code: string): number => { 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 => { export const saveStatistics = (graph: string): void => {
@@ -80,19 +80,20 @@ const delaysPerEvent = {
themeChange: defaultDelay themeChange: defaultDelay
}; };
export type AnalyticsEvent = keyof typeof delaysPerEvent; export type AnalyticsEvent = keyof typeof delaysPerEvent;
const timeouts: Record<string, number> = {}; const timeouts: Map<string, number> = new Map<string, number>();
// manual debounce to reduce the number of events sent to analytics // manual debounce to reduce the number of events sent to analytics
export const logEvent = (name: AnalyticsEvent, data?: unknown): void => { export const logEvent = (name: AnalyticsEvent, data?: unknown): void => {
if (!analytics) { if (!analytics) {
return; return;
} }
const key = data ? JSON.stringify({ name, data }) : name; const key = data ? JSON.stringify({ name, data }) : name;
if (timeouts[key] === undefined) { if (!timeouts.has(key)) {
void analytics.track(name, data); void analytics.track(name, data);
} else { } else {
clearTimeout(timeouts[key]); clearTimeout(timeouts.get(key));
} }
timeouts[key] = window.setTimeout(() => { timeouts.set(
delete timeouts[key]; key,
}, delaysPerEvent[name]); window.setTimeout(() => timeouts.delete(key), delaysPerEvent[name])
);
}; };
+2 -2
View File
@@ -23,10 +23,10 @@ export const initHandler = async (): Promise<void> => {
syncDiagram(); syncDiagram();
initURLSubscription(); initURLSubscription();
await initAnalytics(); 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'; export const cmdKey = isMac ? 'Cmd' : 'Ctrl';
let count = 0; let count = 0;
+1
View File
@@ -12,6 +12,7 @@
"compilerOptions": { "compilerOptions": {
"resolveJsonModule": true, "resolveJsonModule": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strictNullChecks": true,
"types": ["vitest/importMeta"] "types": ["vitest/importMeta"]
}, },
"extends": "./.svelte-kit/tsconfig.json" "extends": "./.svelte-kit/tsconfig.json"