chore: improve linting (#7244)

This commit is contained in:
Noel
2022-01-11 12:30:08 +01:00
committed by GitHub
parent ff3a8b8323
commit 16938da355
57 changed files with 527 additions and 440 deletions

View File

@@ -125,6 +125,7 @@ function audioCycleStep() {
// eslint-disable-next-line @typescript-eslint/dot-notation
available.forEach((player) => player['_stepDispatch']());
// eslint-disable-next-line @typescript-eslint/no-use-before-define
prepareNextAudioFrame(available);
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/prefer-ts-expect-error */
import type { GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispatchData } from 'discord-api-types/v9';
import type { CreateVoiceConnectionOptions } from '.';
import type { AudioPlayer } from './audio/AudioPlayer';
@@ -261,11 +262,11 @@ export class VoiceConnection extends TypedEmitter<VoiceConnectionEvents> {
*/
public set state(newState: VoiceConnectionState) {
const oldState = this._state;
const oldNetworking: Networking | undefined = Reflect.get(oldState, 'networking');
const newNetworking: Networking | undefined = Reflect.get(newState, 'networking');
const oldNetworking = Reflect.get(oldState, 'networking') as Networking | undefined;
const newNetworking = Reflect.get(newState, 'networking') as Networking | undefined;
const oldSubscription: PlayerSubscription | undefined = Reflect.get(oldState, 'subscription');
const newSubscription: PlayerSubscription | undefined = Reflect.get(newState, 'subscription');
const oldSubscription = Reflect.get(oldState, 'subscription') as PlayerSubscription | undefined;
const newSubscription = Reflect.get(newState, 'subscription') as PlayerSubscription | undefined;
if (oldNetworking !== newNetworking) {
if (oldNetworking) {
@@ -365,6 +366,7 @@ export class VoiceConnection extends TypedEmitter<VoiceConnectionEvents> {
newUdp?.on('message', this.receiver.onUdpMessage);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
this.receiver.connectionData = Reflect.get(newState, 'connectionData') ?? {};
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/prefer-ts-expect-error */
import { addAudioPlayer, deleteAudioPlayer } from '../DataStore';
import { Awaited, noop } from '../util/util';
import { VoiceConnection, VoiceConnectionStatus } from '../VoiceConnection';
@@ -163,6 +164,19 @@ export type AudioPlayerEvents = {
) => Awaited<void>;
};
/**
* Stringifies an AudioPlayerState instance.
*
* @param state - The state to stringify
*/
function stringifyState(state: AudioPlayerState) {
return JSON.stringify({
...state,
resource: Reflect.has(state, 'resource'),
stepTimeout: Reflect.has(state, 'stepTimeout'),
});
}
/**
* Used to play audio resources (i.e. tracks, streams) to voice connections.
*
@@ -606,19 +620,6 @@ export class AudioPlayer extends TypedEmitter<AudioPlayerEvents> {
}
}
/**
* Stringifies an AudioPlayerState instance.
*
* @param state - The state to stringify
*/
function stringifyState(state: AudioPlayerState) {
return JSON.stringify({
...state,
resource: Reflect.has(state, 'resource'),
stepTimeout: Reflect.has(state, 'stepTimeout'),
});
}
/**
* Creates a new AudioPlayer to be used.
*/

View File

@@ -151,7 +151,7 @@ export class AudioResource<T = unknown> {
this.silenceRemaining--;
return SILENCE_FRAME;
}
const packet: Buffer | null = this.playStream.read();
const packet = this.playStream.read() as Buffer | null;
if (packet) {
this.playbackDuration += 20;
}

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/dot-notation */
import { AudioResource } from '../../audio/AudioResource';
import { createAudioPlayer, AudioPlayerStatus, AudioPlayer, SILENCE_FRAME } from '../AudioPlayer';
@@ -372,7 +374,7 @@ test('play() throws when playing a resource that has already ended', async () =>
expect(resource.playStream.readableEnded).toBe(true);
player.stop(true);
expect(player.state.status).toBe(AudioPlayerStatus.Idle);
expect(() => player.play(resource)).toThrow();
expect(() => player?.play(resource)).toThrow();
});
test('Propagates errors from streams', async () => {

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { opus, VolumeTransformer } from 'prism-media';
import { PassThrough, Readable } from 'node:stream';
import { SILENCE_FRAME } from '../AudioPlayer';

View File

@@ -157,6 +157,41 @@ export interface NetworkingEvents {
close: (code: number) => Awaited<void>;
}
/**
* Stringifies a NetworkingState.
*
* @param state - The state to stringify
*/
function stringifyState(state: NetworkingState) {
return JSON.stringify({
...state,
ws: Reflect.has(state, 'ws'),
udp: Reflect.has(state, 'udp'),
});
}
/**
* Chooses an encryption mode from a list of given options. Chooses the most preferred option.
*
* @param options - The available encryption options
*/
function chooseEncryptionMode(options: string[]): string {
const option = options.find((option) => SUPPORTED_ENCRYPTION_MODES.includes(option));
if (!option) {
throw new Error(`No compatible encryption modes. Available include: ${options.join(', ')}`);
}
return option;
}
/**
* Returns a random number that is in the range of n bits.
*
* @param n - The number of bits
*/
function randomNBit(n: number) {
return Math.floor(Math.random() * 2 ** n);
}
/**
* Manages the networking required to maintain a voice connection and dispatch audio packets
*/
@@ -348,12 +383,16 @@ export class Networking extends TypedEmitter<NetworkingEvents> {
* @param packet - The received packet
*/
private onWsPacket(packet: any) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (packet.op === VoiceOpcodes.Hello && this.state.code !== NetworkingStatusCode.Closed) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
this.state.ws.setHeartbeatInterval(packet.d.heartbeat_interval);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
} else if (packet.op === VoiceOpcodes.Ready && this.state.code === NetworkingStatusCode.Identifying) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const { ip, port, ssrc, modes } = packet.d;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const udp = new VoiceUDPSocket({ ip, port });
udp.on('error', this.onChildError);
udp.on('debug', this.onUdpDebug);
@@ -387,19 +426,23 @@ export class Networking extends TypedEmitter<NetworkingEvents> {
code: NetworkingStatusCode.UdpHandshaking,
udp,
connectionData: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
ssrc,
},
};
} else if (
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
packet.op === VoiceOpcodes.SessionDescription &&
this.state.code === NetworkingStatusCode.SelectingProtocol
) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const { mode: encryptionMode, secret_key: secretKey } = packet.d;
this.state = {
...this.state,
code: NetworkingStatusCode.Ready,
connectionData: {
...this.state.connectionData,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
encryptionMode,
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
secretKey: new Uint8Array(secretKey),
@@ -411,6 +454,7 @@ export class Networking extends TypedEmitter<NetworkingEvents> {
packetsPlayed: 0,
},
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
} else if (packet.op === VoiceOpcodes.Resumed && this.state.code === NetworkingStatusCode.Resuming) {
this.state = {
...this.state,
@@ -557,38 +601,3 @@ export class Networking extends TypedEmitter<NetworkingEvents> {
return [secretbox.methods.close(opusPacket, nonce, secretKey)];
}
}
/**
* Returns a random number that is in the range of n bits.
*
* @param n - The number of bits
*/
function randomNBit(n: number) {
return Math.floor(Math.random() * 2 ** n);
}
/**
* Stringifies a NetworkingState.
*
* @param state - The state to stringify
*/
function stringifyState(state: NetworkingState) {
return JSON.stringify({
...state,
ws: Reflect.has(state, 'ws'),
udp: Reflect.has(state, 'udp'),
});
}
/**
* Chooses an encryption mode from a list of given options. Chooses the most preferred option.
*
* @param options - The available encryption options
*/
function chooseEncryptionMode(options: string[]): string {
const option = options.find((option) => SUPPORTED_ENCRYPTION_MODES.includes(option));
if (!option) {
throw new Error(`No compatible encryption modes. Available include: ${options.join(', ')}`);
}
return option;
}

View File

@@ -24,6 +24,25 @@ export interface VoiceUDPSocketEvents {
message: (message: Buffer) => Awaited<void>;
}
/**
* Parses the response from Discord to aid with local IP discovery.
*
* @param message - The received message
*/
export function parseLocalPacket(message: Buffer): SocketConfig {
const packet = Buffer.from(message);
const ip = packet.slice(8, packet.indexOf(0, 8)).toString('utf-8');
if (!isIPv4(ip)) {
throw new Error('Malformed IP address');
}
const port = packet.readUInt16BE(packet.length - 2);
return { ip, port };
}
/**
* The interval in milliseconds at which keep alive datagrams are sent.
*/
@@ -191,22 +210,3 @@ export class VoiceUDPSocket extends TypedEmitter<VoiceUDPSocketEvents> {
});
}
}
/**
* Parses the response from Discord to aid with local IP discovery.
*
* @param message - The received message
*/
export function parseLocalPacket(message: Buffer): SocketConfig {
const packet = Buffer.from(message);
const ip = packet.slice(8, packet.indexOf(0, 8)).toString('utf-8');
if (!isIPv4(ip)) {
throw new Error('Malformed IP address');
}
const port = packet.readUInt16BE(packet.length - 2);
return { ip, port };
}

View File

@@ -107,6 +107,7 @@ export class VoiceWebSocket extends TypedEmitter<VoiceWebSocketEvents> {
let packet: any;
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
packet = JSON.parse(event.data);
} catch (error) {
const e = error as Error;
@@ -114,6 +115,7 @@ export class VoiceWebSocket extends TypedEmitter<VoiceWebSocketEvents> {
return;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (packet.op === VoiceOpcodes.HeartbeatAck) {
this.lastHeartbeatAck = Date.now();
this.missedHeartbeats = 0;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { createSocket as _createSocket } from 'node:dgram';
@@ -21,7 +22,6 @@ class FakeSocket extends EventEmitter {
}
// ip = 91.90.123.93, port = 54148
// eslint-disable-next-line prettier/prettier
const VALID_RESPONSE = Buffer.from([
0x0, 0x2, 0x0, 0x46, 0x0, 0x4, 0xeb, 0x23, 0x39, 0x31, 0x2e, 0x39, 0x30, 0x2e, 0x31, 0x32, 0x33, 0x2e, 0x39, 0x33,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { VoiceOpcodes } from 'discord-api-types/voice/v4';
import EventEmitter, { once } from 'node:events';
import WS from 'jest-websocket-mock';

View File

@@ -63,23 +63,34 @@ export class VoiceReceiver {
* @internal
*/
public onWsPacket(packet: any) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (packet.op === VoiceOpcodes.ClientDisconnect && typeof packet.d?.user_id === 'string') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
this.ssrcMap.delete(packet.d.user_id);
} else if (
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
packet.op === VoiceOpcodes.Speaking &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
typeof packet.d?.user_id === 'string' &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
typeof packet.d?.ssrc === 'number'
) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
this.ssrcMap.update({ userId: packet.d.user_id, audioSSRC: packet.d.ssrc });
} else if (
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
packet.op === VoiceOpcodes.ClientConnect &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
typeof packet.d?.user_id === 'string' &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
typeof packet.d?.audio_ssrc === 'number'
) {
this.ssrcMap.update({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
userId: packet.d.user_id,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
audioSSRC: packet.d.audio_ssrc,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
videoSSRC: packet.d.video_ssrc === 0 ? undefined : packet.d.video_ssrc,
});
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import EventEmitter, { once } from 'node:events';
import { SSRCMap, VoiceUserData } from '../SSRCMap';

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/dot-notation */
import { VoiceReceiver } from '../VoiceReceiver';
import { VoiceConnection as _VoiceConnection, VoiceConnectionStatus } from '../../VoiceConnection';

View File

@@ -1,28 +1,37 @@
interface Methods {
open(buffer: Buffer, nonce: Buffer, secretKey: Uint8Array): Buffer | null;
close(opusPacket: Buffer, nonce: Buffer, secretKey: Uint8Array): Buffer;
random(bytes: number, nonce: Buffer): Buffer;
open: (buffer: Buffer, nonce: Buffer, secretKey: Uint8Array) => Buffer | null;
close: (opusPacket: Buffer, nonce: Buffer, secretKey: Uint8Array) => Buffer;
random: (bytes: number, nonce: Buffer) => Buffer;
}
const libs = {
sodium: (sodium: any): Methods => ({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
open: sodium.api.crypto_secretbox_open_easy,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
close: sodium.api.crypto_secretbox_easy,
random: (n: any, buffer?: Buffer) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
if (!buffer) buffer = Buffer.allocUnsafe(n);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
sodium.api.randombytes_buf(buffer);
return buffer;
},
}),
'libsodium-wrappers': (sodium: any): Methods => ({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
open: sodium.crypto_secretbox_open_easy,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
close: sodium.crypto_secretbox_easy,
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
random: (n: any) => sodium.randombytes_buf(n),
}),
tweetnacl: (tweetnacl: any): Methods => ({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
open: tweetnacl.secretbox.open,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
close: tweetnacl.secretbox,
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
random: (n: any) => tweetnacl.randomBytes(n),
}),
} as const;
@@ -46,6 +55,7 @@ void (async () => {
try {
// eslint-disable-next-line
const lib = require(libName);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (libName === 'libsodium-wrappers' && lib.ready) await lib.ready;
Object.assign(methods, libs[libName](lib));
break;

View File

@@ -1,3 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { demuxProbe } from '../demuxProbe';
import { opus as _opus } from 'prism-media';
import { Readable } from 'node:stream';
@@ -9,6 +12,10 @@ jest.mock('prism-media');
const WebmDemuxer = _opus.WebmDemuxer as unknown as jest.Mock<_opus.WebmDemuxer>;
const OggDemuxer = _opus.OggDemuxer as unknown as jest.Mock<_opus.OggDemuxer>;
function nextTick() {
return new Promise((resolve) => process.nextTick(resolve));
}
async function* gen(n: number) {
for (let i = 0; i < n; i++) {
yield Buffer.from([i]);
@@ -37,10 +44,6 @@ async function collectStream(stream: Readable): Promise<Buffer> {
return output;
}
function nextTick() {
return new Promise((resolve) => process.nextTick(resolve));
}
describe('demuxProbe', () => {
const webmWrite: jest.Mock<(buffer: Buffer) => void> = jest.fn();
const oggWrite: jest.Mock<(buffer: Buffer) => void> = jest.fn();

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import EventEmitter from 'node:events';
import { VoiceConnection, VoiceConnectionStatus } from '../../VoiceConnection';
import { entersState } from '../entersState';

View File

@@ -6,7 +6,8 @@
export function abortAfter(delay: number): [AbortController, AbortSignal] {
const ac = new AbortController();
const timeout = setTimeout(() => ac.abort(), delay);
// @ts-ignore
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
ac.signal.addEventListener('abort', () => clearTimeout(timeout));
return [ac, ac.signal];
}

View File

@@ -10,17 +10,17 @@ export interface DiscordGatewayAdapterLibraryMethods {
*
* @param data - The inner data of the VOICE_SERVER_UPDATE payload
*/
onVoiceServerUpdate(data: GatewayVoiceServerUpdateDispatchData): void;
onVoiceServerUpdate: (data: GatewayVoiceServerUpdateDispatchData) => void;
/**
* Call this when you receive a VOICE_STATE_UPDATE payload that is relevant to the adapter.
*
* @param data - The inner data of the VOICE_STATE_UPDATE payload
*/
onVoiceStateUpdate(data: GatewayVoiceStateUpdateDispatchData): void;
onVoiceStateUpdate: (data: GatewayVoiceStateUpdateDispatchData) => void;
/**
* Call this when the adapter can no longer be used (e.g. due to a disconnect from the main gateway)
*/
destroy(): void;
destroy: () => void;
}
/**
@@ -34,12 +34,12 @@ export interface DiscordGatewayAdapterImplementerMethods {
*
* @returns `false` if the payload definitely failed to send - in this case, the voice connection disconnects
*/
sendPayload(payload: any): boolean;
sendPayload: (payload: any) => boolean;
/**
* This will be called by @discordjs/voice when the adapter can safely be destroyed as it will no
* longer be used.
*/
destroy(): void;
destroy: () => void;
}
/**

View File

@@ -56,8 +56,11 @@ export function demuxProbe(
let resolved: StreamType | undefined = undefined;
const finish = (type: StreamType) => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
stream.off('data', onData);
// eslint-disable-next-line @typescript-eslint/no-use-before-define
stream.off('close', onClose);
// eslint-disable-next-line @typescript-eslint/no-use-before-define
stream.off('end', onClose);
stream.pause();
resolved = type;

View File

@@ -3,6 +3,51 @@
import { resolve, dirname } from 'node:path';
import prism from 'prism-media';
/**
* Tries to find the package.json file for a given module.
*
* @param dir - The directory to look in
* @param packageName - The name of the package to look for
* @param depth - The maximum recursion depth
*/
function findPackageJSON(
dir: string,
packageName: string,
depth: number,
): { name: string; version: string } | undefined {
if (depth === 0) return undefined;
const attemptedPath = resolve(dir, './package.json');
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const pkg = require(attemptedPath);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (pkg.name !== packageName) throw new Error('package.json does not match');
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return pkg;
} catch (err) {
return findPackageJSON(resolve(dir, '..'), packageName, depth - 1);
}
}
/**
* Tries to find the version of a dependency.
*
* @param name - The package to find the version of
*/
function version(name: string): string {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const pkg =
name === '@discordjs/voice'
? require('../../package.json')
: findPackageJSON(dirname(require.resolve(name)), name, 3);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
return pkg?.version ?? 'not found';
} catch (err) {
return 'not found';
}
}
/**
* Generates a report of the dependencies used by the \@discordjs/voice module.
* Useful for debugging.
@@ -41,43 +86,3 @@ export function generateDependencyReport() {
return ['-'.repeat(50), ...report, '-'.repeat(50)].join('\n');
}
/**
* Tries to find the package.json file for a given module.
*
* @param dir - The directory to look in
* @param packageName - The name of the package to look for
* @param depth - The maximum recursion depth
*/
function findPackageJSON(
dir: string,
packageName: string,
depth: number,
): { name: string; version: string } | undefined {
if (depth === 0) return undefined;
const attemptedPath = resolve(dir, './package.json');
try {
const pkg = require(attemptedPath);
if (pkg.name !== packageName) throw new Error('package.json does not match');
return pkg;
} catch (err) {
return findPackageJSON(resolve(dir, '..'), packageName, depth - 1);
}
}
/**
* Tries to find the version of a dependency.
*
* @param name - The package to find the version of
*/
function version(name: string): string {
try {
const pkg =
name === '@discordjs/voice'
? require('../../package.json')
: findPackageJSON(dirname(require.resolve(name)), name, 3);
return pkg?.version ?? 'not found';
} catch (err) {
return 'not found';
}
}