commit 1919bce72913319e5c3d6d795c2095bd108f6e86
parent 4204c8c64d437f70aad84f6814d02a9ad93cec28
Author: Florian Dold <dold@taler.net>
Date: Thu, 27 Aug 2026 00:00:49 +0200
web-util: remove browser storage sync and guard local storage
Also add tests.
Diffstat:
7 files changed, 427 insertions(+), 289 deletions(-)
diff --git a/packages/web-util/package.json b/packages/web-util/package.json
@@ -59,7 +59,6 @@
"ws": "7.4.5"
},
"dependencies": {
- "@types/chrome": "0.0.197",
"qrcode-generator": "^1.4.4",
"tailwindcss": "3.4.17"
},
diff --git a/packages/web-util/src/hooks/useLocalStorage.test.tsx b/packages/web-util/src/hooks/useLocalStorage.test.tsx
@@ -0,0 +1,215 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { Window } from "happy-dom";
+import { h } from "preact";
+import { act } from "preact/test-utils";
+import { buildStorageKey, useLocalStorage } from "./useLocalStorage.js";
+
+function installDom(storage?: Storage): Window {
+ const window = new Window({ url: "https://storage.example/" });
+ if (storage) {
+ Object.defineProperty(window, "localStorage", {
+ configurable: true,
+ value: storage,
+ });
+ }
+ for (const [key, value] of Object.entries({
+ window,
+ document: window.document,
+ navigator: window.navigator,
+ Node: window.Node,
+ Element: window.Element,
+ Event: window.Event,
+ StorageEvent: window.StorageEvent,
+ HTMLElement: window.HTMLElement,
+ MutationObserver: window.MutationObserver,
+ })) {
+ Object.defineProperty(globalThis, key, {
+ configurable: true,
+ writable: true,
+ value,
+ });
+ }
+ return window;
+}
+
+function throwingWriteStorage(): Storage {
+ const content = new Map<string, string>();
+ return {
+ get length() {
+ return content.size;
+ },
+ clear() {
+ content.clear();
+ },
+ getItem(key: string) {
+ return content.get(key) ?? null;
+ },
+ key(index: number) {
+ return [...content.keys()][index] ?? null;
+ },
+ removeItem(key: string) {
+ content.delete(key);
+ },
+ setItem() {
+ throw new DOMException("Storage quota exceeded", "QuotaExceededError");
+ },
+ };
+}
+
+async function eventually(assertion: () => void): Promise<void> {
+ let lastError: unknown;
+ for (let attempt = 0; attempt < 50; attempt++) {
+ try {
+ assertion();
+ return;
+ } catch (error) {
+ lastError = error;
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ }
+ }
+ throw lastError;
+}
+
+test("rapid updates always render the latest value", async () => {
+ const window = installDom();
+ const originalDate = globalThis.Date;
+ class FixedDate extends originalDate {
+ constructor() {
+ super(1_000);
+ }
+ }
+ globalThis.Date = FixedDate as DateConstructor;
+ const { cleanup, render } = await import("@testing-library/preact");
+ const key = buildStorageKey("rapid-update-test");
+ let update = (_value: string): void => undefined;
+
+ function Harness() {
+ const state = useLocalStorage(key, "initial");
+ update = state.update;
+ return <output>{state.value}</output>;
+ }
+
+ try {
+ const view = render(<Harness />);
+ await act(() => {
+ update("first");
+ update("second");
+ });
+ assert.equal(view.getByText("second").textContent, "second");
+ } finally {
+ cleanup();
+ globalThis.Date = originalDate;
+ await window.happyDOM.abort();
+ }
+});
+
+test("an update between render and subscription is observed", async () => {
+ const window = installDom();
+ const { cleanup, render } = await import("@testing-library/preact");
+ const key = buildStorageKey("subscription-race-test");
+ window.localStorage.setItem(key.id, JSON.stringify("before"));
+ let changeDuringRender = true;
+
+ function Harness() {
+ const state = useLocalStorage(key, "initial");
+ if (changeDuringRender) {
+ changeDuringRender = false;
+ window.localStorage.setItem(key.id, JSON.stringify("after"));
+ }
+ return <output>{state.value}</output>;
+ }
+
+ try {
+ const view = render(<Harness />);
+ await eventually(() =>
+ assert.equal(view.getByText("after").textContent, "after"),
+ );
+ } finally {
+ cleanup();
+ await window.happyDOM.abort();
+ }
+});
+
+test("storage events update the subscribed value", async () => {
+ const window = installDom();
+ const { cleanup, render } = await import("@testing-library/preact");
+ const key = buildStorageKey("cross-window-update-test");
+
+ function Harness() {
+ const state = useLocalStorage(key, "initial");
+ return <output>{state.value}</output>;
+ }
+
+ try {
+ const view = render(<Harness />);
+ const value = JSON.stringify("from-another-window");
+ window.localStorage.setItem(key.id, value);
+ await act(() => {
+ window.dispatchEvent(
+ new window.StorageEvent("storage", {
+ key: key.id,
+ newValue: value,
+ storageArea: window.localStorage,
+ }),
+ );
+ });
+ assert.equal(
+ view.getByText("from-another-window").textContent,
+ "from-another-window",
+ );
+ } finally {
+ cleanup();
+ await window.happyDOM.abort();
+ }
+});
+
+test("write failures fall back to in-memory storage", async () => {
+ const window = installDom(throwingWriteStorage());
+ const { cleanup, render } = await import("@testing-library/preact");
+ const key = buildStorageKey("write-failure-test");
+ let update = (_value: string): void => undefined;
+
+ function Harness() {
+ const state = useLocalStorage(key, "initial");
+ update = state.update;
+ return <output>{state.value}</output>;
+ }
+
+ try {
+ const view = render(<Harness />);
+ await act(() => update("session-only"));
+ assert.equal(view.getByText("session-only").textContent, "session-only");
+ } finally {
+ cleanup();
+ await window.happyDOM.abort();
+ }
+});
+
+test("an unavailable localStorage getter falls back to memory", async () => {
+ const window = installDom();
+ Object.defineProperty(window, "localStorage", {
+ configurable: true,
+ get() {
+ throw new DOMException("Storage access denied", "SecurityError");
+ },
+ });
+ const { cleanup, render } = await import("@testing-library/preact");
+ const key = buildStorageKey("blocked-storage-test");
+ let update = (_value: string): void => undefined;
+
+ function Harness() {
+ const state = useLocalStorage(key, "initial");
+ update = state.update;
+ return <output>{state.value}</output>;
+ }
+
+ try {
+ const view = render(<Harness />);
+ await act(() => update("session-only"));
+ assert.equal(view.getByText("session-only").textContent, "session-only");
+ } finally {
+ cleanup();
+ await window.happyDOM.abort();
+ }
+});
diff --git a/packages/web-util/src/hooks/useLocalStorage.ts b/packages/web-util/src/hooks/useLocalStorage.ts
@@ -19,14 +19,9 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { AbsoluteTime, Codec, codecForString } from "@gnu-taler/taler-util";
+import { Codec, codecForString } from "@gnu-taler/taler-util";
import { useEffect, useState } from "preact/hooks";
-import {
- ObservableMap,
- browserStorageMap,
- localStorageMap,
- memoryMap,
-} from "../utils/observable.js";
+import { localStorageMap } from "../utils/observable.js";
declare const opaque_StorageKey: unique symbol;
@@ -57,32 +52,7 @@ export interface StorageState<Type = string> {
reset: () => void;
}
-const supportLocalStorage = typeof window !== "undefined";
-const supportBrowserStorage =
- typeof chrome !== "undefined" && typeof chrome.storage !== "undefined";
-
-/**
- * Build setting storage
- */
-const storage: ObservableMap<string, string> = (function buildStorage() {
- if (supportBrowserStorage) {
- //browser storage is like local storage but
- //with app sync.
- //Works for almost every browser
- if (supportLocalStorage) {
- return browserStorageMap(localStorageMap());
- } else {
- // service worker doesn't have local storage
- return browserStorageMap(memoryMap<string>());
- }
- } else if (supportLocalStorage) {
- // fallback if browser is too old
- return localStorageMap();
- } else {
- // new need to save settings somewhere
- return memoryMap<string>();
- }
-})();
+const storage = localStorageMap();
//with initial value
export function useLocalStorage<Type = string>(
key: StorageKey<Type>,
@@ -97,25 +67,28 @@ export function useLocalStorage<Type = string>(
key: StorageKey<Type>,
defaultValue?: Type,
): StorageState<Type> {
- const current = convert(storage.get(key.id), key, defaultValue);
+ const renderedValue = storage.get(key.id);
+ const current = convert(renderedValue, key, defaultValue);
- const [_, setStoredValue] = useState(AbsoluteTime.now().t_ms);
+ const [_, setVersion] = useState(0);
useEffect(() => {
- return storage.onUpdate(key.id, () => {
- // const newValue = storage.get(key.id);
- setStoredValue(AbsoluteTime.now().t_ms);
+ const unsubscribe = storage.onUpdate(key.id, () => {
+ setVersion((version) => version + 1);
});
+ // The value can change after render but before this effect subscribes.
+ // Re-read it after subscribing so that such an update is not lost.
+ if (storage.get(key.id) !== renderedValue) {
+ setVersion((version) => version + 1);
+ }
+ return unsubscribe;
}, [key.id]);
const setValue = (value?: Type): void => {
if (value === undefined) {
storage.delete(key.id);
} else {
- storage.set(
- key.id,
- key.codec ? JSON.stringify(value) : (value as string),
- );
+ storage.set(key.id, JSON.stringify(value));
}
};
diff --git a/packages/web-util/src/utils/observable.test.ts b/packages/web-util/src/utils/observable.test.ts
@@ -0,0 +1,29 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { memoryMap } from "./observable.js";
+
+test("memory maps notify key and map subscribers", () => {
+ const storage = memoryMap<string>();
+ let keyUpdates = 0;
+ let anyUpdates = 0;
+ const unsubscribeKey = storage.onUpdate("selected", () => keyUpdates++);
+ const unsubscribeAny = storage.onAnyUpdate(() => anyUpdates++);
+
+ storage.set("selected", "first");
+ assert.equal(keyUpdates, 1);
+ assert.equal(anyUpdates, 1);
+
+ storage.set("other", "second");
+ assert.equal(keyUpdates, 1);
+ assert.equal(anyUpdates, 2);
+
+ storage.clear();
+ assert.equal(keyUpdates, 2);
+ assert.equal(anyUpdates, 3);
+
+ unsubscribeKey();
+ unsubscribeAny();
+ storage.set("selected", "third");
+ assert.equal(keyUpdates, 2);
+ assert.equal(anyUpdates, 3);
+});
diff --git a/packages/web-util/src/utils/observable.ts b/packages/web-util/src/utils/observable.ts
@@ -3,43 +3,58 @@ export type ObservableMap<K, V> = Map<K, V> & {
onUpdate: (key: string, callback: () => void) => () => void;
};
+function createObservableCallbacks() {
+ const anyUpdateHandlers = new Set<() => void>();
+ const keyUpdateHandlers = new Map<string, Set<() => void>>();
+
+ return {
+ onAnyUpdate(handler: () => void): () => void {
+ anyUpdateHandlers.add(handler);
+ return () => anyUpdateHandlers.delete(handler);
+ },
+ onUpdate(key: string, handler: () => void): () => void {
+ const handlers = keyUpdateHandlers.get(key) ?? new Set<() => void>();
+ handlers.add(handler);
+ keyUpdateHandlers.set(key, handlers);
+ return () => {
+ handlers.delete(handler);
+ if (handlers.size === 0) keyUpdateHandlers.delete(key);
+ };
+ },
+ notifyUpdate(key: string): void {
+ for (const handler of keyUpdateHandlers.get(key) ?? []) handler();
+ for (const handler of anyUpdateHandlers) handler();
+ },
+ notifyClear(): void {
+ for (const handlers of keyUpdateHandlers.values()) {
+ for (const handler of handlers) handler();
+ }
+ for (const handler of anyUpdateHandlers) handler();
+ },
+ };
+}
+
//FIXME: allow different type for different properties
export function memoryMap<T>(
backend: Map<string, T> = new Map<string, T>(),
): ObservableMap<string, T> {
- const obs = new EventTarget();
+ const callbacks = createObservableCallbacks();
const theMemoryMap: ObservableMap<string, T> = {
- onAnyUpdate: (handler) => {
- obs.addEventListener(`update`, handler);
- obs.addEventListener(`clear`, handler);
- return () => {
- obs.removeEventListener(`update`, handler);
- obs.removeEventListener(`clear`, handler);
- };
- },
- onUpdate: (key, handler) => {
- obs.addEventListener(`update-${key}`, handler);
- obs.addEventListener(`clear`, handler);
- return () => {
- obs.removeEventListener(`update-${key}`, handler);
- obs.removeEventListener(`clear`, handler);
- };
- },
+ onAnyUpdate: callbacks.onAnyUpdate,
+ onUpdate: callbacks.onUpdate,
delete: (key: string) => {
const result = backend.delete(key);
- obs.dispatchEvent(new Event(`update-${key}`));
- obs.dispatchEvent(new Event(`update`));
+ callbacks.notifyUpdate(key);
return result;
},
set: (key: string, value: T) => {
backend.set(key, value);
- obs.dispatchEvent(new Event(`update-${key}`));
- obs.dispatchEvent(new Event(`update`));
+ callbacks.notifyUpdate(key);
return theMemoryMap;
},
clear: () => {
backend.clear();
- obs.dispatchEvent(new Event(`clear`));
+ callbacks.notifyClear();
},
entries: backend.entries.bind(backend),
forEach: backend.forEach.bind(backend),
@@ -56,148 +71,161 @@ export function memoryMap<T>(
return theMemoryMap;
}
-//FIXME: change this implementation to match the
-// browser storage. instead of creating a sync implementation
-// of observable map it should reuse the memoryMap and
-// sync the state with local storage
-export function localStorageMap(): ObservableMap<string, string> {
- const obs = new EventTarget();
+type StorageProvider = () => Storage | undefined;
+
+function defaultStorageProvider(): Storage | undefined {
+ if (typeof window === "undefined") return undefined;
+ return window.localStorage;
+}
+
+/**
+ * An observable local-storage map with an in-memory fallback.
+ *
+ * Browser storage can be unavailable or start throwing at any time, for
+ * example in privacy-restricted contexts or after exceeding its quota. The
+ * fallback keeps the UI operational for the remainder of the page session.
+ */
+export function localStorageMap(
+ storageProvider: StorageProvider = defaultStorageProvider,
+): ObservableMap<string, string> {
+ const callbacks = createObservableCallbacks();
+ const fallback = new Map<string, string>();
+ let disabledBackend: Storage | undefined;
+
+ function getBackend(): Storage | undefined {
+ try {
+ const backend = storageProvider();
+ return backend === disabledBackend ? undefined : backend;
+ } catch {
+ return undefined;
+ }
+ }
+
+ function disableBackend(backend: Storage): void {
+ disabledBackend = backend;
+ }
+
+ function get(key: string): string | undefined {
+ const backend = getBackend();
+ if (!backend) return fallback.get(key);
+ try {
+ const value = backend.getItem(key);
+ if (value === null) {
+ fallback.delete(key);
+ return undefined;
+ }
+ fallback.set(key, value);
+ return value;
+ } catch {
+ disableBackend(backend);
+ return fallback.get(key);
+ }
+ }
+
+ function snapshot(): Map<string, string> {
+ const backend = getBackend();
+ if (!backend) return new Map(fallback);
+ try {
+ const content = new Map<string, string>();
+ for (let index = 0; index < backend.length; index++) {
+ const key = backend.key(index);
+ if (key === null) continue;
+ const value = backend.getItem(key);
+ if (value !== null) content.set(key, value);
+ }
+ fallback.clear();
+ for (const [key, value] of content) fallback.set(key, value);
+ return content;
+ } catch {
+ disableBackend(backend);
+ return new Map(fallback);
+ }
+ }
+
+ function storageEventMatches(event: StorageEvent): boolean {
+ if (event.storageArea === null) return true;
+ return event.storageArea === getBackend();
+ }
+
const theLocalStorageMap: ObservableMap<string, string> = {
onAnyUpdate: (handler) => {
- obs.addEventListener(`update`, handler);
- obs.addEventListener(`clear`, handler);
- window.addEventListener("storage", handler);
+ const eventWindow = typeof window === "undefined" ? undefined : window;
+ function handleStorageEvent(event: StorageEvent) {
+ if (storageEventMatches(event)) handler();
+ }
+ const unsubscribe = callbacks.onAnyUpdate(handler);
+ eventWindow?.addEventListener("storage", handleStorageEvent);
return () => {
- window.removeEventListener("storage", handler);
- obs.removeEventListener(`update`, handler);
- obs.removeEventListener(`clear`, handler);
+ eventWindow?.removeEventListener("storage", handleStorageEvent);
+ unsubscribe();
};
},
onUpdate: (key, handler) => {
- obs.addEventListener(`update-${key}`, handler);
- obs.addEventListener(`clear`, handler);
function handleStorageEvent(ev: StorageEvent) {
- if (ev.key === null || ev.key === key) {
+ if (storageEventMatches(ev) && (ev.key === null || ev.key === key)) {
handler();
}
}
- window.addEventListener("storage", handleStorageEvent);
+ const eventWindow = typeof window === "undefined" ? undefined : window;
+ const unsubscribe = callbacks.onUpdate(key, handler);
+ eventWindow?.addEventListener("storage", handleStorageEvent);
return () => {
- window.removeEventListener("storage", handleStorageEvent);
- obs.removeEventListener(`update-${key}`, handler);
- obs.removeEventListener(`clear`, handler);
+ eventWindow?.removeEventListener("storage", handleStorageEvent);
+ unsubscribe();
};
},
delete: (key: string) => {
- const exists = localStorage.getItem(key) !== null;
- localStorage.removeItem(key);
- obs.dispatchEvent(new Event(`update-${key}`));
- obs.dispatchEvent(new Event(`update`));
+ const exists = get(key) !== undefined;
+ fallback.delete(key);
+ const backend = getBackend();
+ if (backend) {
+ try {
+ backend.removeItem(key);
+ } catch {
+ disableBackend(backend);
+ }
+ }
+ callbacks.notifyUpdate(key);
return exists;
},
set: (key: string, v: string) => {
- localStorage.setItem(key, v);
- obs.dispatchEvent(new Event(`update-${key}`));
- obs.dispatchEvent(new Event(`update`));
+ fallback.set(key, v);
+ const backend = getBackend();
+ if (backend) {
+ try {
+ backend.setItem(key, v);
+ } catch {
+ disableBackend(backend);
+ }
+ }
+ callbacks.notifyUpdate(key);
return theLocalStorageMap;
},
clear: () => {
- localStorage.clear();
- obs.dispatchEvent(new Event(`clear`));
- },
- entries: (): IterableIterator<[string, string]> => {
- let index = 0;
- const total = localStorage.length;
- return {
- next() {
- if (index === total) return { done: true, value: undefined };
- const key = localStorage.key(index);
- if (key === null) {
- //we are going from 0 until last, this should not happen
- throw Error("key cant be null");
- }
- const item = localStorage.getItem(key);
- if (item === null) {
- //the key exist, this should not happen
- throw Error("value cant be null");
- }
- index = index + 1;
- return { done: false, value: [key, item] };
- },
- [Symbol.iterator]() {
- return this;
- },
- };
- },
- forEach: (cb) => {
- for (let index = 0; index < localStorage.length; index++) {
- const key = localStorage.key(index);
- if (key === null) {
- //we are going from 0 until last, this should not happen
- throw Error("key cant be null");
+ fallback.clear();
+ const backend = getBackend();
+ if (backend) {
+ try {
+ backend.clear();
+ } catch {
+ disableBackend(backend);
}
- const item = localStorage.getItem(key);
- if (item === null) {
- //the key exist, this should not happen
- throw Error("value cant be null");
- }
- cb(item, key, theLocalStorageMap);
}
+ callbacks.notifyClear();
},
- get: (key: string) => {
- const item = localStorage.getItem(key);
- if (item === null) return undefined;
- return item;
- },
- has: (key: string) => {
- return localStorage.getItem(key) !== null;
- },
- keys: () => {
- let index = 0;
- const total = localStorage.length;
- return {
- next() {
- if (index === total) return { done: true, value: undefined };
- const key = localStorage.key(index);
- if (key === null) {
- //we are going from 0 until last, this should not happen
- throw Error("key cant be null");
- }
- index = index + 1;
- return { done: false, value: key };
- },
- [Symbol.iterator]() {
- return this;
- },
- };
+ entries: () => snapshot().entries(),
+ forEach: (cb, thisArg) => {
+ snapshot().forEach((value, key) =>
+ cb.call(thisArg, value, key, theLocalStorageMap),
+ );
},
+ get,
+ has: (key: string) => get(key) !== undefined,
+ keys: () => snapshot().keys(),
get size() {
- return localStorage.length;
- },
- values: () => {
- let index = 0;
- const total = localStorage.length;
- return {
- next() {
- if (index === total) return { done: true, value: undefined };
- const key = localStorage.key(index);
- if (key === null) {
- //we are going from 0 until last, this should not happen
- throw Error("key cant be null");
- }
- const item = localStorage.getItem(key);
- if (item === null) {
- //the key exist, this should not happen
- throw Error("value cant be null");
- }
- index = index + 1;
- return { done: false, value: item };
- },
- [Symbol.iterator]() {
- return this;
- },
- };
+ return snapshot().size;
},
+ values: () => snapshot().values(),
[Symbol.iterator]: function (): IterableIterator<[string, string]> {
return theLocalStorageMap.entries();
},
@@ -205,97 +233,3 @@ export function localStorageMap(): ObservableMap<string, string> {
};
return theLocalStorageMap;
}
-
-const isFirefox =
- typeof (window as any) !== "undefined" &&
- typeof (window as any)["InstallTrigger"] !== "undefined";
-
-async function getAllContent() {
- //Firefox and Chrome has different storage api
- if (isFirefox) {
- // @ts-ignore
- return browser.storage.local.get();
- } else {
- return chrome.storage.local.get();
- }
-}
-
-async function updateContent(obj: Record<string, any>) {
- if (isFirefox) {
- // @ts-ignore
- return browser.storage.local.set(obj);
- } else {
- return chrome.storage.local.set(obj);
- }
-}
-async function removeContent(keys: string[]) {
- if (keys.length === 0) return;
- if (isFirefox) {
- // @ts-ignore
- return browser.storage.local.remove(keys);
- }
- return chrome.storage.local.remove(keys);
-}
-type Changes = { [key: string]: { oldValue?: any; newValue?: any } };
-function onBrowserStorageUpdate(cb: (changes: Changes) => void): void {
- if (isFirefox) {
- // @ts-ignore
- browser.storage.local.onChanged.addListener(cb);
- } else {
- chrome.storage.local.onChanged.addListener(cb);
- }
-}
-
-export function browserStorageMap(
- backend: ObservableMap<string, string>,
-): ObservableMap<string, string> {
- let applyingRemoteChange = false;
- getAllContent().then((content) => {
- applyingRemoteChange = true;
- Object.entries(content ?? {}).forEach(([k, v]) => {
- backend.set(k, v as string);
- });
- applyingRemoteChange = false;
- });
-
- let pendingSync = Promise.resolve();
- backend.onAnyUpdate(() => {
- if (applyingRemoteChange) return;
- pendingSync = pendingSync
- .then(async () => {
- const result: Record<string, string> = {};
- for (const [key, value] of backend.entries()) result[key] = value;
- const existing = await getAllContent();
- const deletedKeys = Object.keys(existing ?? {}).filter(
- (key) => !(key in result),
- );
- await removeContent(deletedKeys);
- await updateContent(result);
- })
- .catch((error) => {
- console.error("Could not synchronize browser storage", error);
- });
- });
-
- onBrowserStorageUpdate((changes) => {
- //another chrome instance made the change
- applyingRemoteChange = true;
- const changedItems = Object.keys(changes);
- if (changedItems.length === 0) {
- backend.clear();
- } else {
- for (const key of changedItems) {
- if (changes[key].newValue === undefined) {
- backend.delete(key);
- } else {
- if (changes[key].newValue !== changes[key].oldValue) {
- backend.set(key, changes[key].newValue);
- }
- }
- }
- }
- applyingRemoteChange = false;
- });
-
- return backend;
-}
diff --git a/packages/web-util/tsconfig.json b/packages/web-util/tsconfig.json
@@ -7,10 +7,9 @@
"lib": ["DOM", "ES2023"],
"outDir": "lib",
"paths": {
- "@assets/*": ["../../contrib/taler-assets/*"],
+ "@assets/*": ["../../contrib/taler-assets/*"]
},
- "rootDir": "./src",
- "types": ["chrome"],
+ "rootDir": "./src"
},
"references": [
{
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
@@ -755,9 +755,6 @@ importers:
packages/web-util:
dependencies:
- '@types/chrome':
- specifier: 0.0.197
- version: 0.0.197
qrcode-generator:
specifier: ^1.4.4
version: 1.4.4
@@ -1413,9 +1410,6 @@ packages:
'@types/chai@4.3.3':
resolution: {integrity: sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==}
- '@types/chrome@0.0.197':
- resolution: {integrity: sha512-m1NfS5bOjaypyqQfaX6CxmJodZVcvj5+Mt/K94EBHkflYjPNmXHAzbxfifdLMa0YM3PDyOxohoTS5ug/e6p5jA==}
-
'@types/chrome@0.0.326':
resolution: {integrity: sha512-WS7jKf3ZRZFHOX7dATCZwqNJgdfiSF0qBRFxaO0LhIOvTNBrfkab26bsZwp6EBpYtqp8loMHJTnD6vDTLWPKYw==}
@@ -4600,11 +4594,6 @@ snapshots:
'@types/chai@4.3.3': {}
- '@types/chrome@0.0.197':
- dependencies:
- '@types/filesystem': 0.0.32
- '@types/har-format': 1.2.9
-
'@types/chrome@0.0.326':
dependencies:
'@types/filesystem': 0.0.32