diff --git a/packages/client/package.json b/packages/client/package.json index 8d92a66a..9a20ebbf 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -30,6 +30,10 @@ } } }, + "browser": { + "./cjs/methods/files/_platform.js": "./cjs/methods/files/_platform.web.js", + "./esm/methods/files/_platform.js": "./esm/methods/files/_platform.web.js" + }, "dependencies": { "@types/node": "18.16.0", "@mtcute/core": "workspace:^1.0.0", diff --git a/packages/client/src/methods/files/_platform.ts b/packages/client/src/methods/files/_platform.ts new file mode 100644 index 00000000..9097634c --- /dev/null +++ b/packages/client/src/methods/files/_platform.ts @@ -0,0 +1,32 @@ +import { createReadStream, promises, ReadStream } from 'node:fs' +import { basename } from 'node:path' +import { Readable } from 'node:stream' + +import { nodeReadableToWeb } from '../../utils/stream-utils.js' + +/** @internal */ +export function _createFileStream(path: string): ReadStream { + return createReadStream(path) +} + +/** @internal */ +export function _isFileStream(stream: unknown): stream is ReadStream { + return stream instanceof ReadStream +} + +/** @internal */ +export async function _extractFileStreamMeta(stream: ReadStream): Promise<[string, number]> { + const fileName = basename(stream.path.toString()) + const fileSize = await promises.stat(stream.path.toString()).then((stat) => stat.size) + + return [fileName, fileSize] +} + +/** @internal */ +export function _handleNodeStream(val: T | Readable): T | ReadableStream { + if (val instanceof Readable) { + return nodeReadableToWeb(val) + } + + return val +} diff --git a/packages/client/src/methods/files/_platform.web.ts b/packages/client/src/methods/files/_platform.web.ts new file mode 100644 index 00000000..3fad2b9f --- /dev/null +++ b/packages/client/src/methods/files/_platform.web.ts @@ -0,0 +1,23 @@ +import { MtArgumentError } from '@mtcute/core' + +/** @internal */ +export function _createFileStream(): never { + throw new MtArgumentError('Cannot create file stream on web platform') +} + +/** @internal */ +export function _isFileStream() { + return false +} + +/** @internal */ +export function _extractFileStreamMeta(): never { + throw new Error('UNREACHABLE') +} + +/** @internal */ +export function _handleNodeStream(val: unknown) { + return val +} + +// all the above functions shall be inlined by terser diff --git a/packages/client/src/methods/files/upload-file.ts b/packages/client/src/methods/files/upload-file.ts index a5699b17..09724eee 100644 --- a/packages/client/src/methods/files/upload-file.ts +++ b/packages/client/src/methods/files/upload-file.ts @@ -1,30 +1,15 @@ import fileType from 'file-type' -// eslint-disable-next-line no-restricted-imports -import type { ReadStream } from 'fs' -import { createRequire } from 'module' import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core' import { randomLong } from '@mtcute/core/utils.js' import { UploadedFile, UploadFileLike } from '../../types/index.js' import { determinePartSize, isProbablyPlainText } from '../../utils/file-utils.js' -import { bufferToStream, createChunkedReader, nodeReadableToWeb, streamToBuffer } from '../../utils/stream-utils.js' +import { bufferToStream, createChunkedReader, streamToBuffer } from '../../utils/stream-utils.js' +import { _createFileStream, _extractFileStreamMeta, _handleNodeStream, _isFileStream } from './_platform.js' const { fromBuffer: fileTypeFromBuffer } = fileType -let fs: typeof import('fs') | null = null -let path: typeof import('path') | null = null -let nodeStream: typeof import('stream') | null = null - -try { - // @only-if-esm - const require = createRequire(import.meta.url) - // @/only-if-esm - fs = require('fs') as typeof import('fs') - path = require('path') as typeof import('path') - nodeStream = require('stream') as typeof import('stream') -} catch (e) {} - const OVERRIDE_MIME: Record = { // tg doesn't interpret `audio/opus` files as voice messages for some reason 'audio/opus': 'audio/ogg', @@ -134,20 +119,11 @@ export async function uploadFile( } if (typeof file === 'string') { - if (!fs) { - throw new MtArgumentError('Local paths are only supported for NodeJS!') - } - file = fs.createReadStream(file) + file = _createFileStream(file) } - if (fs && file instanceof fs.ReadStream) { - fileName = path!.basename(file.path.toString()) - fileSize = await new Promise((res, rej) => { - fs!.stat((file as ReadStream).path.toString(), (err, stat) => { - if (err) rej(err) - res(stat.size) - }) - }) + if (_isFileStream(file)) { + [fileName, fileSize] = await _extractFileStreamMeta(file) // fs.ReadStream is a subclass of Readable, will be handled below } @@ -186,9 +162,7 @@ export async function uploadFile( file = file.body } - if (nodeStream && file instanceof nodeStream.Readable) { - file = nodeReadableToWeb(file) - } + file = _handleNodeStream(file) if (!(file instanceof ReadableStream)) { throw new MtArgumentError('Could not convert input `file` to stream!') diff --git a/packages/client/src/utils/inspectable.ts b/packages/client/src/utils/inspectable.ts index 8408c41b..253cd5ef 100644 --- a/packages/client/src/utils/inspectable.ts +++ b/packages/client/src/utils/inspectable.ts @@ -1,18 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-argument */ -import { createRequire } from 'module' - import { base64Encode } from '@mtcute/core/utils.js' -let util: typeof import('util') | null = null - -try { - // @only-if-esm - const require = createRequire(import.meta.url) - // @/only-if-esm - util = require('util') as typeof import('util') -} catch (e) {} +const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom') // get all property names. unlike Object.getOwnPropertyNames, // also gets inherited property names @@ -83,7 +74,5 @@ export function makeInspectable(obj: new (...args: any[]) => T, props?: (keyo // eslint-disable-next-line @typescript-eslint/no-unsafe-return return ret } - if (util) { - obj.prototype[util.inspect.custom] = obj.prototype.toJSON - } + obj.prototype[customInspectSymbol] = obj.prototype.toJSON } diff --git a/packages/core/src/network/transports/index.ts b/packages/core/src/network/transports/index.ts index 29580033..cdcb1bc9 100644 --- a/packages/core/src/network/transports/index.ts +++ b/packages/core/src/network/transports/index.ts @@ -4,8 +4,6 @@ export * from './abstract.js' export * from './intermediate.js' export * from './obfuscated.js' export * from './streamed.js' -export * from './tcp.js' -export * from './websocket.js' export * from './wrapped.js' import { _defaultTransportFactory } from '../../utils/platform/transport.js' diff --git a/packages/core/src/network/transports/websocket.ts b/packages/core/src/network/transports/websocket.ts index b9cfeed0..72876ab5 100644 --- a/packages/core/src/network/transports/websocket.ts +++ b/packages/core/src/network/transports/websocket.ts @@ -1,5 +1,4 @@ import EventEmitter from 'events' -import { createRequire } from 'module' import { tl } from '@mtcute/tl' @@ -9,22 +8,8 @@ import { IPacketCodec, ITelegramTransport, TransportState } from './abstract.js' import { IntermediatePacketCodec } from './intermediate.js' import { ObfuscatedPacketCodec } from './obfuscated.js' -let ws: { - new (address: string, options?: string): WebSocket -} | null - -if (typeof window === 'undefined' || typeof window.WebSocket === 'undefined') { - try { - // @only-if-esm - const require = createRequire(import.meta.url) - // @/only-if-esm - // eslint-disable-next-line - ws = require('ws') - } catch (e) { - ws = null - } -} else { - ws = window.WebSocket +export type WebSocketConstructor = { + new (address: string, protocol?: string): WebSocket } const subdomainsMap: Record = { @@ -51,20 +36,36 @@ export abstract class BaseWebSocketTransport extends EventEmitter implements ITe private _baseDomain: string private _subdomains: Record + private _WebSocket: WebSocketConstructor - /** - * @param baseDomain Base WebSocket domain - * @param subdomains Map of sub-domains (key is DC ID, value is string) - */ - constructor(baseDomain = 'web.telegram.org', subdomains = subdomainsMap) { + constructor({ + ws = WebSocket, + baseDomain = 'web.telegram.org', + subdomains = subdomainsMap, + }: { + /** Custom implementation of WebSocket (e.g. https://npm.im/ws) */ + ws?: WebSocketConstructor + /** Base WebSocket domain */ + baseDomain?: string + /** Map of sub-domains (key is DC ID, value is string) */ + subdomains?: Record + } = {}) { super() if (!ws) { - throw new MtUnsupportedError('To use WebSocket transport with NodeJS, install `ws` package.') + throw new MtUnsupportedError( + 'To use WebSocket transport with NodeJS, install `ws` package and pass it to constructor', + ) + } + + // gotta love cjs/esm compat + if ('default' in ws) { + ws = ws.default as WebSocketConstructor } this._baseDomain = baseDomain this._subdomains = subdomains + this._WebSocket = ws this.close = this.close.bind(this) } @@ -104,7 +105,7 @@ export abstract class BaseWebSocketTransport extends EventEmitter implements ITe this._state = TransportState.Connecting this._currentDc = dc - this._socket = new ws!( + this._socket = new this._WebSocket( `wss://${this._subdomains[dc.id]}.${this._baseDomain}/apiws${testMode ? '_test' : ''}`, 'binary', )