Move to @pezkuwi/dev 0.86 (eslint 10 toolchain)

- @pezkuwi/dev, dev-test, dev-ts ^0.86.3
- react-qr Display: the frames follow the value's content (xxhash), are
  reset while rendering instead of in an effect, and the frame timer is
  keyed on the frame set. Before, a value that went from one frame to
  several never started animating; Display.spec covers single,
  multi, single-to-multi and unmount. tsconfig build/spec know *.spec.tsx.
- optional calls instead of guard expressions (ScanAddress, stores)
- KeyringOptions as a Record; type assertions the receiver already
  accepts removed (Settings, vue-identicon)
- disable directives narrowed to the rules that still report
This commit is contained in:
Satoshi Qazi Muhammed committed 2026-10-09 16:14:33 +04:00
1 parent 6ee748883a
commit 5a542f2805
18 files changed
+2861 -3172

No files matched your search

+1 -1
View File
@@ -33,7 +33,7 @@
"test:one": "pezkuwi-dev-run-test --env browser" "test:one": "pezkuwi-dev-run-test --env browser"
}, },
"devDependencies": { "devDependencies": {
"@pezkuwi/dev": "^0.85.12", "@pezkuwi/dev": "^0.86.3",
"@pezkuwi/x-bundle": "^14.0.26", "@pezkuwi/x-bundle": "^14.0.26",
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"react": "^18.2.0", "react": "^18.2.0",
+135
View File
@@ -0,0 +1,135 @@
// Copyright 2017-2026 @pezkuwi/react-qr authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { QrDisplay } from './Display.js';
// Bytes, so that a payload of this size is split into several QR frames.
const MULTI = new Uint8Array(4096).map((_, i) => i % 256);
const SINGLE = new Uint8Array([1, 2, 3, 4]);
const DELAY = 40;
function sleep (ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function setup () {
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const el = document.createElement('div');
const root = createRoot(el);
const src = (): string | null => el.querySelector('img')?.getAttribute('src') ?? null;
return { el, root, src };
}
/** Every distinct image shown while waiting `ms`. */
async function imagesDuring (src: () => string | null, ms: number): Promise<Set<string>> {
const seen = new Set<string>();
for (let t = 0; t < ms; t += DELAY / 2) {
await act(async () => {
await sleep(DELAY / 2);
});
const s = src();
s && seen.add(s);
}
return seen;
}
describe('QrDisplay', (): void => {
it('shows a single frame and keeps it', async (): Promise<void> => {
const { root, src } = setup();
act(() => {
root.render(
<QrDisplay
timerDelay={DELAY}
value={SINGLE}
/>
);
});
expect({ image: src()?.startsWith('data:image/'), test: 'single' }).toEqual({ image: true, test: 'single' });
expect({ frames: (await imagesDuring(src, DELAY * 4)).size, test: 'single' }).toEqual({ frames: 1, test: 'single' });
act(() => {
root.unmount();
});
});
it('cycles through the frames of a multi-frame payload', async (): Promise<void> => {
const { root, src } = setup();
act(() => {
root.render(
<QrDisplay
skipEncoding={false}
timerDelay={DELAY}
value={MULTI}
/>
);
});
expect({ cycles: (await imagesDuring(src, DELAY * 6)).size > 1, test: 'multi' }).toEqual({ cycles: true, test: 'multi' });
act(() => {
root.unmount();
});
});
it('starts cycling when a single-frame value becomes a multi-frame one', async (): Promise<void> => {
const { root, src } = setup();
act(() => {
root.render(
<QrDisplay
timerDelay={DELAY}
value={SINGLE}
/>
);
});
await imagesDuring(src, DELAY * 3);
act(() => {
root.render(
<QrDisplay
timerDelay={DELAY}
value={MULTI}
/>
);
});
expect({ cycles: (await imagesDuring(src, DELAY * 6)).size > 1, test: 'single-to-multi' }).toEqual({ cycles: true, test: 'single-to-multi' });
act(() => {
root.unmount();
});
});
it('stops its timer on unmount', async (): Promise<void> => {
const { el, root } = setup();
act(() => {
root.render(
<QrDisplay
timerDelay={DELAY}
value={MULTI}
/>
);
});
act(() => {
root.unmount();
});
// a timer left running would render into the unmounted root and throw
await sleep(DELAY * 3);
expect(el.querySelector('img')).toEqual(null);
});
});
+62 -65
View File
@@ -1,9 +1,8 @@
// Copyright 2017-2026 @pezkuwi/react-qr authors & contributors // Copyright 2017-2026 @pezkuwi/react-qr authors & contributors
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { objectSpread } from '@pezkuwi/util';
import { xxhashAsHex } from '@pezkuwi/util-crypto'; import { xxhashAsHex } from '@pezkuwi/util-crypto';
import { qrcode } from './qrcode.js'; import { qrcode } from './qrcode.js';
@@ -19,16 +18,11 @@ interface Props {
value: Uint8Array; value: Uint8Array;
} }
interface FrameState { interface Shown {
frames: Uint8Array[];
frameIdx: number; frameIdx: number;
image: string | null; frames: Uint8Array[];
valueHash: string | null; skipEncoding: boolean;
} valueHash: string;
interface TimerState {
timerDelay: number;
timerId: ReturnType<typeof setTimeout> | null;
} }
const DEFAULT_FRAME_DELAY = 2750; const DEFAULT_FRAME_DELAY = 2750;
@@ -45,75 +39,78 @@ function getDataUrl (value: Uint8Array): string {
return qr.createDataURL(16, 0); return qr.createDataURL(16, 0);
} }
function Display ({ className = '', size, skipEncoding, style = {}, timerDelay = DEFAULT_FRAME_DELAY, value }: Props): React.ReactElement<Props> | null { function encode (value: Uint8Array, skipEncoding: boolean, valueHash: string): Shown {
const [{ image }, setFrameState] = useState<FrameState>({ frameIdx: 0, frames: [], image: null, valueHash: null }); return {
const timerRef = useRef<TimerState>({ timerDelay, timerId: null }); frameIdx: 0,
frames: skipEncoding
? [value]
: createFrames(value),
skipEncoding,
valueHash
};
}
function Display ({ className = '', size, skipEncoding = false, style = {}, timerDelay = DEFAULT_FRAME_DELAY, value }: Props): React.ReactElement<Props> | null {
// The frames follow the content of value (by hash), not the identity of the
// array, so a new array with the same bytes does not restart the display.
const valueHash = useMemo(() => xxhashAsHex(value), [value]);
const [shown, setShown] = useState<Shown>(() => encode(value, skipEncoding, valueHash));
// A new value (or encoding) starts again at its first frame. Adjusting state
// while rendering, as React documents for state that follows props, rather
// than in an effect, which renders the stale frame once more first.
if (shown.valueHash !== valueHash || shown.skipEncoding !== skipEncoding) {
setShown(encode(value, skipEncoding, valueHash));
}
const containerStyle = useMemo( const containerStyle = useMemo(
() => createImgSize(size), () => createImgSize(size),
[size] [size]
); );
// run on initial load to setup the global timer and provide and unsubscribe // Frames are encoded on demand, not up front: for a large payload that
useEffect((): () => void => { // keeps the first frame quick.
const nextFrame = () => setFrameState((state): FrameState => { const image = useMemo(
// when we have a single frame, we only ever fire once () => getDataUrl(shown.frames[shown.frameIdx]),
if (state.frames.length <= 1) { [shown]
return state; );
}
let frameIdx = state.frameIdx + 1; // Step through the frames of the current value. Each full cycle slows the
// display a little. A single frame needs no timer; a new set of frames gets
// a new one, so the display animates whenever the value has several frames.
useEffect((): (() => void) | undefined => {
const frames = shown.frames;
// when we overflow, skip to the first and slightly increase the delay between frames if (frames.length <= 1) {
if (frameIdx === state.frames.length) { return undefined;
}
let frameIdx = 0;
let delay = timerDelay;
let timerId: ReturnType<typeof setTimeout>;
const nextFrame = (): void => {
frameIdx = frameIdx + 1;
if (frameIdx === frames.length) {
frameIdx = 0; frameIdx = 0;
timerRef.current.timerDelay = timerRef.current.timerDelay + TIMER_INC; delay = delay + TIMER_INC;
} }
// only encode the frames on demand, not above as part of the setShown((state) =>
// state derivation - in the case of large payloads, this should state.frames === frames
// be slightly more responsive on initial load ? { ...state, frameIdx }
const newState = objectSpread<FrameState>({}, state, { : state
frameIdx, );
image: getDataUrl(state.frames[frameIdx]) timerId = setTimeout(nextFrame, delay);
}); };
// set the new timer last timerId = setTimeout(nextFrame, delay);
timerRef.current.timerId = setTimeout(nextFrame, timerRef.current.timerDelay);
return newState;
});
timerRef.current.timerId = setTimeout(nextFrame, timerRef.current.timerDelay);
return (): void => { return (): void => {
// eslint-disable-next-line react-hooks/exhaustive-deps clearTimeout(timerId);
timerRef.current.timerId && clearTimeout(timerRef.current.timerId);
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps }, [shown.frames, timerDelay]);
}, []);
useEffect((): void => {
setFrameState((state): FrameState => {
const valueHash = xxhashAsHex(value);
if (valueHash === state.valueHash) {
return state;
}
const frames: Uint8Array[] = skipEncoding
? [value]
: createFrames(value);
// encode on demand
return {
frameIdx: 0,
frames,
image: getDataUrl(frames[0]),
valueHash
};
});
}, [skipEncoding, value]);
if (!image) { if (!image) {
return null; return null;
+1 -1
View File
@@ -59,7 +59,7 @@ function ScanAddress ({ className, isEthereum, onError, onScan, size, style }: P
onScan({ content, genesisHash: genesisHash as HexString, isAddress, name: name?.length ? name.join(':') : undefined }); onScan({ content, genesisHash: genesisHash as HexString, isAddress, name: name?.length ? name.join(':') : undefined });
} catch (error) { } catch (error) {
onError && onError(error as Error); onError?.(error as Error);
console.error('@pezkuwi/react-qr:QrScanAddress', (error as Error).message, data); console.error('@pezkuwi/react-qr:QrScanAddress', (error as Error).message, data);
} }
+2 -1
View File
@@ -6,7 +6,8 @@
"rootDir": "./src" "rootDir": "./src"
}, },
"exclude": [ "exclude": [
"**/*.spec.ts" "**/*.spec.ts",
"**/*.spec.tsx"
], ],
"references": [ "references": [
{ "path": "../ui-settings/tsconfig.build.json" } { "path": "../ui-settings/tsconfig.build.json" }
+2 -1
View File
@@ -8,7 +8,8 @@
"noEmit": true "noEmit": true
}, },
"include": [ "include": [
"**/*.spec.ts" "**/*.spec.ts",
"**/*.spec.tsx"
], ],
"references": [ "references": [
{ "path": "../react-qr/tsconfig.build.json" } { "path": "../react-qr/tsconfig.build.json" }
+1 -3
View File
@@ -11,9 +11,7 @@ export interface KeyringSectionOption {
export type KeyringSectionOptions = KeyringSectionOption[]; export type KeyringSectionOptions = KeyringSectionOption[];
export type KeyringOptions = { export type KeyringOptions = Record<KeyringItemType | 'all' | 'allPlus' | 'recent' | 'testing', KeyringSectionOptions>;
[type in KeyringItemType | 'all' | 'allPlus' | 'recent' | 'testing']: KeyringSectionOptions
};
export type KeyringOption$Type = keyof KeyringOptions; export type KeyringOption$Type = keyof KeyringOptions;
+2 -2
View File
@@ -18,11 +18,11 @@ export class BrowserStore implements KeyringStore {
public remove (key: string, fn?: () => void): void { public remove (key: string, fn?: () => void): void {
store.remove(key); store.remove(key);
fn && fn(); fn?.();
} }
public set (key: string, value: KeyringJson, fn?: () => void): void { public set (key: string, value: KeyringJson, fn?: () => void): void {
store.set(key, value); store.set(key, value);
fn && fn(); fn?.();
} }
} }
+2 -2
View File
@@ -64,12 +64,12 @@ export class FileStore implements KeyringStore {
public remove (key: string, fn?: () => void): void { public remove (key: string, fn?: () => void): void {
fs.unlinkSync(this._getPath(key)); fs.unlinkSync(this._getPath(key));
fn && fn(); fn?.();
} }
public set (key: string, value: KeyringJson, fn?: () => void): void { public set (key: string, value: KeyringJson, fn?: () => void): void {
fs.writeFileSync(this._getPath(key), Buffer.from(JSON.stringify(value), 'utf-8')); fs.writeFileSync(this._getPath(key), Buffer.from(JSON.stringify(value), 'utf-8'));
fn && fn(); fn?.();
} }
private _getPath (key: string): string { private _getPath (key: string): string {
+2 -2
View File
@@ -1,7 +1,7 @@
// Copyright 2017-2026 @pezkuwi/ui-settings authors & contributors // Copyright 2017-2026 @pezkuwi/ui-settings authors & contributors
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import type { Endpoint, EndpointType, Option, SettingsStruct } from './types.js'; import type { Endpoint, Option, SettingsStruct } from './types.js';
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import store from 'store'; import store from 'store';
@@ -60,7 +60,7 @@ export class Settings implements SettingsStruct {
// will become deprecated for supporting substrate connect light clients. apiType structure should be used instead // will become deprecated for supporting substrate connect light clients. apiType structure should be used instead
this.#apiUrl = (typeof settings.apiUrl === 'string' && settings.apiUrl) || (hasProcess && process.env?.['WS_URL']) || (ENDPOINT_DEFAULT.value as string); this.#apiUrl = (typeof settings.apiUrl === 'string' && settings.apiUrl) || (hasProcess && process.env?.['WS_URL']) || (ENDPOINT_DEFAULT.value as string);
this.#apiType = { param: this.#apiUrl, type: 'json-rpc' as EndpointType }; this.#apiType = { param: this.#apiUrl, type: 'json-rpc' };
this.#camera = withDefault(CAMERA, settings.camera, CAMERA_DEFAULT); this.#camera = withDefault(CAMERA, settings.camera, CAMERA_DEFAULT);
this.#ledgerApp = withDefault(LEDGER_APP, settings.ledgerApp, LEDGER_APP_DEFAULT); this.#ledgerApp = withDefault(LEDGER_APP, settings.ledgerApp, LEDGER_APP_DEFAULT);
this.#ledgerConn = withDefault(LEDGER_CONN, settings.ledgerConn, LEDGER_CONN_DEFAULT); this.#ledgerConn = withDefault(LEDGER_CONN, settings.ledgerConn, LEDGER_CONN_DEFAULT);
@@ -11,7 +11,7 @@ describe('identicon', (): void => {
it('generates a basic [0,..,0] identicon', (): void => { it('generates a basic [0,..,0] identicon', (): void => {
expect( expect(
xmlserializer.serializeToString( xmlserializer.serializeToString(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
beachballIcon(new Uint8Array(32), { isAlternative: false, size: 256 }) as any beachballIcon(new Uint8Array(32), { isAlternative: false, size: 256 }) as any
) )
).toEqual( ).toEqual(
@@ -24,7 +24,7 @@ describe('identicon', (): void => {
it('allows overrides', (): void => { it('allows overrides', (): void => {
expect( expect(
xmlserializer.serializeToString( xmlserializer.serializeToString(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
beachballIcon(new Uint8Array(32), { isAlternative: false, size: 100 }, 'testClass', { display: 'block' }) as any beachballIcon(new Uint8Array(32), { isAlternative: false, size: 100 }, 'testClass', { display: 'block' }) as any
) )
).toEqual( ).toEqual(
@@ -12,7 +12,7 @@ describe('circle', (): void => {
it('creates a circle shape', (): void => { it('creates a circle shape', (): void => {
expect( expect(
xmlserializer.serializeToString( xmlserializer.serializeToString(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
circle(seeder(), 'blue', 50, 2) as any circle(seeder(), 'blue', 50, 2) as any
) )
).toEqual('<circle xmlns="http://www.w3.org/2000/svg" cx="25" cy="32.5" r="15" fill="blue"/>'); ).toEqual('<circle xmlns="http://www.w3.org/2000/svg" cx="25" cy="32.5" r="15" fill="blue"/>');
@@ -11,7 +11,7 @@ describe('circle', (): void => {
it('creates a basic SVG circle element', (): void => { it('creates a basic SVG circle element', (): void => {
expect( expect(
xs.serializeToString( xs.serializeToString(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
circle(123, 12, 34) as any circle(123, 12, 34) as any
) )
).toEqual('<circle xmlns="http://www.w3.org/2000/svg" cx="12" cy="34" r="123"/>'); ).toEqual('<circle xmlns="http://www.w3.org/2000/svg" cx="12" cy="34" r="123"/>');
@@ -11,7 +11,7 @@ describe('element', (): void => {
it('creates a basic SVG element', (): void => { it('creates a basic SVG element', (): void => {
expect( expect(
xs.serializeToString( xs.serializeToString(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
element(123) as any element(123) as any
) )
).toEqual('<svg xmlns="http://www.w3.org/2000/svg" x="0" y="0" width="123" height="123"/>'); ).toEqual('<svg xmlns="http://www.w3.org/2000/svg" x="0" y="0" width="123" height="123"/>');
@@ -11,7 +11,7 @@ describe('rect', (): void => {
it('creates a basic SVG rect element', (): void => { it('creates a basic SVG rect element', (): void => {
expect( expect(
xs.serializeToString( xs.serializeToString(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
rect(123) as any rect(123) as any
) )
).toEqual('<rect xmlns="http://www.w3.org/2000/svg" x="0" y="0" width="123" height="123" rx="7.6875" ry="7.6875"/>'); ).toEqual('<rect xmlns="http://www.w3.org/2000/svg" x="0" y="0" width="123" height="123" rx="7.6875" ry="7.6875"/>');
@@ -11,7 +11,7 @@ describe('svg', (): void => {
it('creates a basic SVG element', (): void => { it('creates a basic SVG element', (): void => {
expect( expect(
xs.serializeToString( xs.serializeToString(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
svg('rect') as any svg('rect') as any
) )
).toEqual('<rect xmlns="http://www.w3.org/2000/svg"/>'); ).toEqual('<rect xmlns="http://www.w3.org/2000/svg"/>');
+1 -1
View File
@@ -33,7 +33,7 @@ function resolvePublicKey (value: string | Uint8Array, prefix?: Prefix): string
} }
return isU8a(value) || isHex(value) return isU8a(value) || isHex(value)
? encodeAddress(value as string, prefix) ? encodeAddress(value, prefix)
: value; : value;
} }
+2643 -3086
View File
File diff suppressed because it is too large. Load diff