From a2cdc737351b657266a417a68ba46259eaeeef05 Mon Sep 17 00:00:00 2001 From: alina sireneva Date: Sun, 28 Apr 2024 22:41:28 +0300 Subject: [PATCH] feat: initial deno support --- .config/eslint.cjs | 4 +- .github/workflows/test.yaml | 2 +- .npmrc | 1 + package.json | 2 +- packages/bun/src/platform.ts | 6 + packages/bun/src/worker.ts | 4 +- .../highlevel/methods/files/upload-file.ts | 3 +- .../core/src/highlevel/types/files/utils.ts | 21 ++- .../core/src/highlevel/worker/protocol.ts | 1 + .../src/storage/sqlite/repository/peers.ts | 3 +- packages/core/src/types/utils.ts | 2 + packages/core/src/utils/error-reporting.ts | 3 +- packages/deno/README.md | 25 +++ packages/deno/build.config.cjs | 1 + packages/deno/package.json | 30 ++++ packages/deno/src/client.ts | 144 ++++++++++++++++++ packages/deno/src/common-internals-web | 1 + packages/deno/src/index.ts | 9 ++ packages/deno/src/methods.ts | 2 + packages/deno/src/methods/download-file.ts | 39 +++++ packages/deno/src/platform.ts | 48 ++++++ packages/deno/src/sqlite/driver.ts | 38 +++++ packages/deno/src/sqlite/index.ts | 14 ++ packages/deno/src/sqlite/sqlite.test.ts | 31 ++++ packages/deno/src/utils.ts | 1 + packages/deno/src/utils/crypto.test.ts | 13 ++ packages/deno/src/utils/crypto.ts | 97 ++++++++++++ packages/deno/src/utils/exit-hook.ts | 21 +++ packages/deno/src/utils/normalize-file.ts | 50 ++++++ packages/deno/src/utils/tcp.ts | 135 ++++++++++++++++ packages/deno/src/worker.ts | 71 +++++++++ packages/deno/tsconfig.json | 16 ++ packages/deno/typedoc.cjs | 10 ++ packages/dispatcher/tsconfig.json | 3 + .../src/common-internals-node/platform.ts | 2 +- packages/node/src/utils/crypto.ts | 3 +- .../base64.ts | 0 .../hex.ts | 0 .../src/{ => common-internals-web}/logging.ts | 0 .../web/src/common-internals-web/readme.md | 2 + .../utf8.ts | 2 +- packages/web/src/idb/driver.ts | 2 + packages/web/src/platform.ts | 9 +- packages/web/src/worker.ts | 2 + pnpm-lock.yaml | 105 +++++++++++++ scripts/build-package.js | 4 + scripts/fetch-deno-dts.mjs | 33 ++++ scripts/publish.js | 1 + scripts/remove-jsr-sourcefiles.mjs | 23 +++ tsconfig.json | 1 + 50 files changed, 1013 insertions(+), 27 deletions(-) create mode 100644 .npmrc create mode 100644 packages/deno/README.md create mode 100644 packages/deno/build.config.cjs create mode 100644 packages/deno/package.json create mode 100644 packages/deno/src/client.ts create mode 120000 packages/deno/src/common-internals-web create mode 100644 packages/deno/src/index.ts create mode 100644 packages/deno/src/methods.ts create mode 100644 packages/deno/src/methods/download-file.ts create mode 100644 packages/deno/src/platform.ts create mode 100644 packages/deno/src/sqlite/driver.ts create mode 100644 packages/deno/src/sqlite/index.ts create mode 100644 packages/deno/src/sqlite/sqlite.test.ts create mode 100644 packages/deno/src/utils.ts create mode 100644 packages/deno/src/utils/crypto.test.ts create mode 100644 packages/deno/src/utils/crypto.ts create mode 100644 packages/deno/src/utils/exit-hook.ts create mode 100644 packages/deno/src/utils/normalize-file.ts create mode 100644 packages/deno/src/utils/tcp.ts create mode 100644 packages/deno/src/worker.ts create mode 100644 packages/deno/tsconfig.json create mode 100644 packages/deno/typedoc.cjs rename packages/web/src/{encodings => common-internals-web}/base64.ts (100%) rename packages/web/src/{encodings => common-internals-web}/hex.ts (100%) rename packages/web/src/{ => common-internals-web}/logging.ts (100%) create mode 100644 packages/web/src/common-internals-web/readme.md rename packages/web/src/{encodings => common-internals-web}/utf8.ts (90%) create mode 100644 scripts/fetch-deno-dts.mjs create mode 100644 scripts/remove-jsr-sourcefiles.mjs diff --git a/.config/eslint.cjs b/.config/eslint.cjs index 2a1ea066..02b8bd5b 100644 --- a/.config/eslint.cjs +++ b/.config/eslint.cjs @@ -279,7 +279,7 @@ module.exports = { }, }, { - files: ['packages/bun/**'], + files: ['packages/bun/**', 'packages/deno/**'], rules: { 'import/no-unresolved': 'off', 'no-restricted-imports': 'off', @@ -291,7 +291,7 @@ module.exports = { rules: { 'import/no-unresolved': 'off', } - } + }, ], settings: { 'import/resolver': { diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2377652e..aae1b802 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -67,7 +67,7 @@ jobs: - name: 'Build tests' run: pnpm exec vite build -c .config/vite.deno.mts - name: 'Run tests' - run: cd dist/tests && deno test + run: cd dist/tests && deno test -A --unstable-ffi test-web: runs-on: ubuntu-latest diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..41583e36 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/package.json b/package.json index ebd5c48a..ff338996 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ ], "scripts": { "prepare": "husky install .config/husky", - "postinstall": "node scripts/validate-deps-versions.mjs", + "postinstall": "node scripts/validate-deps-versions.mjs && node scripts/fetch-deno-dts.mjs && node scripts/remove-jsr-sourcefiles.mjs", "test": "pnpm run -r test && vitest --config .config/vite.mts run", "test:dev": "vitest --config .config/vite.mts watch", "test:ui": "vitest --config .config/vite.mts --ui", diff --git a/packages/bun/src/platform.ts b/packages/bun/src/platform.ts index ef805446..7af79895 100644 --- a/packages/bun/src/platform.ts +++ b/packages/bun/src/platform.ts @@ -1,8 +1,14 @@ +import * as os from 'os' + import { NodePlatform } from './common-internals-node/platform.js' import { normalizeFile } from './utils/normalize-file.js' export class BunPlatform extends NodePlatform { declare normalizeFile: typeof normalizeFile + + getDeviceModel(): string { + return `Bun/${process.version} (${os.type()} ${os.arch()})` + } } BunPlatform.prototype.normalizeFile = normalizeFile diff --git a/packages/bun/src/worker.ts b/packages/bun/src/worker.ts index 0c953a52..877db612 100644 --- a/packages/bun/src/worker.ts +++ b/packages/bun/src/worker.ts @@ -14,7 +14,7 @@ import { WorkerMessageHandler, } from '@mtcute/core/worker.js' -import { NodePlatform } from './common-internals-node/platform.js' +import { BunPlatform } from './platform.js' export type { TelegramWorkerOptions, TelegramWorkerPortOptions, WorkerCustomMethods } @@ -44,7 +44,7 @@ export class TelegramWorker extends TelegramWorke export class TelegramWorkerPort extends TelegramWorkerPortBase { constructor(readonly options: TelegramWorkerPortOptions) { - setPlatform(new NodePlatform()) + setPlatform(new BunPlatform()) super(options) } diff --git a/packages/core/src/highlevel/methods/files/upload-file.ts b/packages/core/src/highlevel/methods/files/upload-file.ts index eb74837d..0b4b5658 100644 --- a/packages/core/src/highlevel/methods/files/upload-file.ts +++ b/packages/core/src/highlevel/methods/files/upload-file.ts @@ -171,7 +171,8 @@ export async function uploadFile( throw new MtArgumentError('Fetch response contains `null` body') } - file = file.body + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + file = file.body as ReadableStream } if (!(file instanceof ReadableStream)) { diff --git a/packages/core/src/highlevel/types/files/utils.ts b/packages/core/src/highlevel/types/files/utils.ts index 0eac28bf..a339fc21 100644 --- a/packages/core/src/highlevel/types/files/utils.ts +++ b/packages/core/src/highlevel/types/files/utils.ts @@ -1,9 +1,7 @@ -/* eslint-disable no-restricted-imports */ -import type { ReadStream } from 'node:fs' - import { tdFileId } from '@mtcute/file-id' import { tl } from '@mtcute/tl' +import { AnyToNever } from '../../../types/utils.js' import { FileLocation } from './file-location.js' import { UploadedFile } from './uploaded-file.js' @@ -14,8 +12,9 @@ import { UploadedFile } from './uploaded-file.js' * - `File`, `Blob` (from the Web API) * - `string`, which will be interpreted as file path (**non-browser only!**) * - `URL` (from the Web API, will be `fetch()`-ed; `file://` URLs are not available in browsers) - * - `ReadStream` (for Node.js/Bun, from the `fs` module) + * - `ReadStream` (for Node.js/Bun, from the `node:fs` module) * - `BunFile` (from `Bun.file()`) + * - `Deno.FsFile` (from `Deno.open()` in Deno) * - `ReadableStream` (Web API readable stream) * - `Readable` (Node.js/Bun readable stream) * - `Response` (from `window.fetch`) @@ -26,10 +25,14 @@ export type UploadFileLike = | File | Blob | string - | ReadStream - | ReadableStream - | NodeJS.ReadableStream - | Response + | AnyToNever + | AnyToNever> + | AnyToNever + | AnyToNever + | AnyToNever + +// AnyToNever in the above type ensures we don't make the entire type `any` +// if some of the types are not available in the current environment /** * Describes types that can be used as an input @@ -41,6 +44,8 @@ export type UploadFileLike = * - `ReadStream` (for Node.js/Bun, from the `fs` module) * - `ReadableStream` (from the Web API, base readable stream) * - `Readable` (for Node.js/Bun, base readable stream) + * - `BunFile` (from `Bun.file()`) + * - `Deno.FsFile` (from `Deno.open()` in Deno) * - {@link UploadedFile} returned from {@link TelegramClient.uploadFile} * - `tl.TypeInputFile` and `tl.TypeInputMedia` TL objects * - `string` with a path to a local file prepended with `file:` (non-browser only) (e.g. `file:image.jpg`) diff --git a/packages/core/src/highlevel/worker/protocol.ts b/packages/core/src/highlevel/worker/protocol.ts index 4a603578..2f9b61d8 100644 --- a/packages/core/src/highlevel/worker/protocol.ts +++ b/packages/core/src/highlevel/worker/protocol.ts @@ -1,3 +1,4 @@ +/// import type { Worker as NodeWorker } from 'node:worker_threads' import { tl } from '@mtcute/tl' diff --git a/packages/core/src/storage/sqlite/repository/peers.ts b/packages/core/src/storage/sqlite/repository/peers.ts index 2e48b9ad..3426e0c8 100644 --- a/packages/core/src/storage/sqlite/repository/peers.ts +++ b/packages/core/src/storage/sqlite/repository/peers.ts @@ -62,10 +62,9 @@ export class SqlitePeersRepository implements IPeersRepository { this._driver._writeLater(this._store, [ peer.id, peer.accessHash, - // add commas to make it easier to search with LIKE JSON.stringify(peer.usernames), peer.updated, - peer.phone, + peer.phone ?? null, peer.complete, ]) } diff --git a/packages/core/src/types/utils.ts b/packages/core/src/types/utils.ts index 666c6914..2f58e50f 100644 --- a/packages/core/src/types/utils.ts +++ b/packages/core/src/types/utils.ts @@ -1,6 +1,8 @@ export type MaybePromise = T | Promise export type PartialExcept = Partial> & Pick export type PartialOnly = Partial> & Omit +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyToNever = any extends T ? never : T export type MaybeArray = T | T[] diff --git a/packages/core/src/utils/error-reporting.ts b/packages/core/src/utils/error-reporting.ts index 7c9470e9..0f219153 100644 --- a/packages/core/src/utils/error-reporting.ts +++ b/packages/core/src/utils/error-reporting.ts @@ -7,7 +7,8 @@ export function reportUnknownError(log: Logger, error: tl.RpcError, method: stri fetch(`https://rpc.pwrtelegram.xyz/?code=${error.code}&method=${method}&error=${error.text}`) .then((r) => r.json()) - .then((r) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .then((r: any) => { if (r.ok) { log.info('telerpc responded with error info for %s: %s', error.text, r.result) } else { diff --git a/packages/deno/README.md b/packages/deno/README.md new file mode 100644 index 00000000..2d2baff9 --- /dev/null +++ b/packages/deno/README.md @@ -0,0 +1,25 @@ +# @mtcute/bun + +📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_deno.html) + +‼️ **Experimental** Deno support package for mtcute. Includes: +- SQLite storage (based on [`@db/sqlite`](https://jsr.io/@db/sqlite)) +- TCP transport (based on Deno-native APIs) +- `TelegramClient` implementation using the above +- HTML and Markdown parsers + +## Usage + +```typescript +import { TelegramClient } from '@mtcute/deno' + +const tg = new TelegramClient({ + apiId: 12345, + apiHash: 'abcdef', + storage: 'my-account' +}) + +tg.run(async (user) => { + console.log(`✨ logged in as ${user.displayName}`) +}) +``` diff --git a/packages/deno/build.config.cjs b/packages/deno/build.config.cjs new file mode 100644 index 00000000..17718026 --- /dev/null +++ b/packages/deno/build.config.cjs @@ -0,0 +1 @@ +module.exports = () => ({ buildCjs: false }) diff --git a/packages/deno/package.json b/packages/deno/package.json new file mode 100644 index 00000000..fa148f60 --- /dev/null +++ b/packages/deno/package.json @@ -0,0 +1,30 @@ +{ + "name": "@mtcute/deno", + "private": true, + "version": "0.11.0", + "description": "Meta-package for Deno", + "author": "alina sireneva ", + "license": "MIT", + "main": "src/index.ts", + "type": "module", + "sideEffects": false, + "scripts": { + "docs": "typedoc", + "build": "pnpm run -w build-package deno" + }, + "exports": { + ".": "./src/index.ts", + "./utils.js": "./src/utils.ts" + }, + "dependencies": { + "@mtcute/core": "workspace:^", + "@mtcute/wasm": "workspace:^", + "@mtcute/markdown-parser": "workspace:^", + "@mtcute/html-parser": "workspace:^", + "@db/sqlite": "npm:@jsr/db__sqlite@0.11.1", + "@std/io": "npm:@jsr/std__io@0.223.0" + }, + "devDependencies": { + "@mtcute/test": "workspace:^" + } +} diff --git a/packages/deno/src/client.ts b/packages/deno/src/client.ts new file mode 100644 index 00000000..32a03e96 --- /dev/null +++ b/packages/deno/src/client.ts @@ -0,0 +1,144 @@ +import { createInterface, Interface as RlInterface } from 'node:readline' +import { Readable, Writable } from 'node:stream' + +import { FileDownloadLocation, FileDownloadParameters, ITelegramStorageProvider, PartialOnly, User } from '@mtcute/core' +import { + BaseTelegramClient as BaseTelegramClientBase, + BaseTelegramClientOptions as BaseTelegramClientOptionsBase, + TelegramClient as TelegramClientBase, + TelegramClientOptions, +} from '@mtcute/core/client.js' +import { setPlatform } from '@mtcute/core/platform.js' + +import { downloadToFile } from './methods/download-file.js' +import { DenoPlatform } from './platform.js' +import { SqliteStorage } from './sqlite/index.js' +import { DenoCryptoProvider } from './utils/crypto.js' +import { TcpTransport } from './utils/tcp.js' + +export type { TelegramClientOptions } + +export interface BaseTelegramClientOptions + extends PartialOnly, 'transport' | 'crypto'> { + /** + * Storage to use for this client. + * + * If a string is passed, it will be used as + * a name for an SQLite database file. + * + * @default `"client.session"` + */ + storage?: string | ITelegramStorageProvider + + /** + * **ADVANCED USE ONLY** + * + * Whether to not set up the platform. + * This is useful if you call `setPlatform` yourself. + */ + platformless?: boolean +} + +export class BaseTelegramClient extends BaseTelegramClientBase { + constructor(opts: BaseTelegramClientOptions) { + if (!opts.platformless) setPlatform(new DenoPlatform()) + + super({ + crypto: new DenoCryptoProvider(), + transport: () => new TcpTransport(), + ...opts, + storage: + typeof opts.storage === 'string' ? + new SqliteStorage(opts.storage) : + opts.storage ?? new SqliteStorage('client.session'), + }) + } +} + +/** + * Telegram client for use in Node.js + */ +export class TelegramClient extends TelegramClientBase { + constructor(opts: TelegramClientOptions) { + if ('client' in opts) { + super(opts) + + return + } + + super({ + client: new BaseTelegramClient(opts), + }) + } + + private _rl?: RlInterface + + /** + * Tiny wrapper over Node `readline` package + * for simpler user input for `.run()` method. + * + * Associated `readline` interface is closed + * after `run()` returns, or with the client. + * + * @param text Text of the question + */ + input(text: string): Promise { + if (!this._rl) { + this._rl = createInterface({ + // eslint-disable-next-line + input: Readable.fromWeb(Deno.stdin.readable as any), + output: Writable.fromWeb(Deno.stdout.writable), + }) + } + + return new Promise((res) => this._rl?.question(text, res)) + } + + close(): Promise { + this._rl?.close() + + return super.close() + } + + start(params: Parameters[0] = {}): Promise { + if (!params.botToken) { + if (!params.phone) params.phone = () => this.input('phone > ') + if (!params.code) params.code = () => this.input('code > ') + + if (!params.password) { + params.password = () => this.input('2fa password > ') + } + } + + return super.start(params).then((user) => { + if (this._rl) { + this._rl.close() + delete this._rl + } + + return user + }) + } + + run( + params: Parameters[0] | ((user: User) => void | Promise), + then?: (user: User) => void | Promise, + ): void { + if (typeof params === 'function') { + then = params + params = {} + } + + this.start(params) + .then(then) + .catch((err) => this.emitError(err)) + } + + downloadToFile( + filename: string, + location: FileDownloadLocation, + params?: FileDownloadParameters | undefined, + ): Promise { + return downloadToFile(this, filename, location, params) + } +} diff --git a/packages/deno/src/common-internals-web b/packages/deno/src/common-internals-web new file mode 120000 index 00000000..3f10a5de --- /dev/null +++ b/packages/deno/src/common-internals-web @@ -0,0 +1 @@ +../../web/src/common-internals-web \ No newline at end of file diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts new file mode 100644 index 00000000..87f84c57 --- /dev/null +++ b/packages/deno/src/index.ts @@ -0,0 +1,9 @@ +export * from './client.js' +export * from './platform.js' +export * from './sqlite/index.js' +export * from './utils/crypto.js' +export * from './utils/tcp.js' +export * from './worker.js' +export * from '@mtcute/core' +export * from '@mtcute/html-parser' +export * from '@mtcute/markdown-parser' diff --git a/packages/deno/src/methods.ts b/packages/deno/src/methods.ts new file mode 100644 index 00000000..f25b163c --- /dev/null +++ b/packages/deno/src/methods.ts @@ -0,0 +1,2 @@ +export { downloadToFile } from './methods/download-file.js' +export * from '@mtcute/core/methods.js' diff --git a/packages/deno/src/methods/download-file.ts b/packages/deno/src/methods/download-file.ts new file mode 100644 index 00000000..fc2adf48 --- /dev/null +++ b/packages/deno/src/methods/download-file.ts @@ -0,0 +1,39 @@ +import { FileDownloadLocation, FileDownloadParameters, FileLocation, ITelegramClient } from '@mtcute/core' +import { downloadAsIterable } from '@mtcute/core/methods.js' + +import { writeAll } from '@std/io/write-all' + +/** + * Download a remote file to a local file (only for NodeJS). + * Promise will resolve once the download is complete. + * + * @param filename Local file name to which the remote file will be downloaded + * @param params File download parameters + */ +export async function downloadToFile( + client: ITelegramClient, + filename: string, + location: FileDownloadLocation, + params?: FileDownloadParameters, +): Promise { + if (location instanceof FileLocation && ArrayBuffer.isView(location.location)) { + // early return for inline files + await Deno.writeFile(filename, location.location) + } + + const fd = await Deno.open(filename, { write: true, create: true, truncate: true }) + + if (params?.abortSignal) { + params.abortSignal.addEventListener('abort', () => { + client.log.debug('aborting file download %s - cleaning up', filename) + fd.close() + Deno.removeSync(filename) + }) + } + + for await (const chunk of downloadAsIterable(client, location, params)) { + await writeAll(fd, chunk) + } + + fd.close() +} diff --git a/packages/deno/src/platform.ts b/packages/deno/src/platform.ts new file mode 100644 index 00000000..aaff526a --- /dev/null +++ b/packages/deno/src/platform.ts @@ -0,0 +1,48 @@ +import { ICorePlatform } from '@mtcute/core/platform.js' + +import { base64Decode, base64Encode } from './common-internals-web/base64.js' +import { hexDecode, hexEncode } from './common-internals-web/hex.js' +import { defaultLoggingHandler } from './common-internals-web/logging.js' +import { utf8ByteLength, utf8Decode, utf8Encode } from './common-internals-web/utf8.js' +import { beforeExit } from './utils/exit-hook.js' +import { normalizeFile } from './utils/normalize-file.js' + +export class DenoPlatform implements ICorePlatform { + declare log: typeof defaultLoggingHandler + declare beforeExit: typeof beforeExit + declare normalizeFile: typeof normalizeFile + + getDeviceModel(): string { + return `Deno/${Deno.version.deno} (${Deno.build.os} ${Deno.build.arch})` + } + + getDefaultLogLevel(): number | null { + const envLogLevel = parseInt(Deno.env.get('MTCUTE_LOG_LEVEL') ?? '') + + if (!isNaN(envLogLevel)) { + return envLogLevel + } + + return null + } + + // ITlPlatform + declare utf8ByteLength: typeof utf8ByteLength + declare utf8Encode: typeof utf8Encode + declare utf8Decode: typeof utf8Decode + declare hexEncode: typeof hexEncode + declare hexDecode: typeof hexDecode + declare base64Encode: typeof base64Encode + declare base64Decode: typeof base64Decode +} + +DenoPlatform.prototype.utf8ByteLength = utf8ByteLength +DenoPlatform.prototype.utf8Encode = utf8Encode +DenoPlatform.prototype.utf8Decode = utf8Decode +DenoPlatform.prototype.hexEncode = hexEncode +DenoPlatform.prototype.hexDecode = hexDecode +DenoPlatform.prototype.base64Encode = base64Encode +DenoPlatform.prototype.base64Decode = base64Decode +DenoPlatform.prototype.log = defaultLoggingHandler +DenoPlatform.prototype.beforeExit = beforeExit +DenoPlatform.prototype.normalizeFile = normalizeFile diff --git a/packages/deno/src/sqlite/driver.ts b/packages/deno/src/sqlite/driver.ts new file mode 100644 index 00000000..e983f027 --- /dev/null +++ b/packages/deno/src/sqlite/driver.ts @@ -0,0 +1,38 @@ +import { BaseSqliteStorageDriver, ISqliteDatabase } from '@mtcute/core' + +import { Database } from '@db/sqlite' + +export interface SqliteStorageDriverOptions { + /** + * By default, WAL mode is enabled, which + * significantly improves performance. + * [Learn more](https://bun.sh/docs/api/sqlite#wal-mode) + * + * However, you might encounter some issues, + * and if you do, you can disable WAL by passing `true` + * + * @default false + */ + disableWal?: boolean +} + +export class SqliteStorageDriver extends BaseSqliteStorageDriver { + constructor( + readonly filename = ':memory:', + readonly params?: SqliteStorageDriverOptions, + ) { + super() + } + + _createDatabase(): ISqliteDatabase { + const db = new Database(this.filename, { + int64: true, + }) + + if (!this.params?.disableWal) { + db.exec('PRAGMA journal_mode = WAL;') + } + + return db as ISqliteDatabase + } +} diff --git a/packages/deno/src/sqlite/index.ts b/packages/deno/src/sqlite/index.ts new file mode 100644 index 00000000..882b228d --- /dev/null +++ b/packages/deno/src/sqlite/index.ts @@ -0,0 +1,14 @@ +import { BaseSqliteStorage } from '@mtcute/core' + +import { SqliteStorageDriver, SqliteStorageDriverOptions } from './driver.js' + +export { SqliteStorageDriver } from './driver.js' + +export class SqliteStorage extends BaseSqliteStorage { + constructor( + readonly filename = ':memory:', + readonly params?: SqliteStorageDriverOptions, + ) { + super(new SqliteStorageDriver(filename, params)) + } +} diff --git a/packages/deno/src/sqlite/sqlite.test.ts b/packages/deno/src/sqlite/sqlite.test.ts new file mode 100644 index 00000000..0a9d9924 --- /dev/null +++ b/packages/deno/src/sqlite/sqlite.test.ts @@ -0,0 +1,31 @@ +import { afterAll, beforeAll, describe } from 'vitest' + +import { LogManager } from '@mtcute/core/utils.js' +import { + testAuthKeysRepository, + testKeyValueRepository, + testPeersRepository, + testRefMessagesRepository, +} from '@mtcute/test' + +if (import.meta.env.TEST_ENV === 'deno') { + const { SqliteStorage } = await import('./index.js') + + describe('SqliteStorage', () => { + const storage = new SqliteStorage(':memory:') + + beforeAll(async () => { + storage.driver.setup(new LogManager()) + await storage.driver.load() + }) + + testAuthKeysRepository(storage.authKeys) + testKeyValueRepository(storage.kv, storage.driver) + testPeersRepository(storage.peers, storage.driver) + testRefMessagesRepository(storage.refMessages, storage.driver) + + afterAll(() => storage.driver.destroy()) + }) +} else { + describe.skip('SqliteStorage', () => {}) +} diff --git a/packages/deno/src/utils.ts b/packages/deno/src/utils.ts new file mode 100644 index 00000000..3356b98c --- /dev/null +++ b/packages/deno/src/utils.ts @@ -0,0 +1 @@ +export * from '@mtcute/core/utils.js' diff --git a/packages/deno/src/utils/crypto.test.ts b/packages/deno/src/utils/crypto.test.ts new file mode 100644 index 00000000..4095b5e2 --- /dev/null +++ b/packages/deno/src/utils/crypto.test.ts @@ -0,0 +1,13 @@ +import { describe } from 'vitest' + +import { testCryptoProvider } from '@mtcute/test' + +if (import.meta.env.TEST_ENV === 'deno') { + describe('DenoCryptoProvider', async () => { + const { DenoCryptoProvider } = await import('./crypto.js') + + testCryptoProvider(new DenoCryptoProvider()) + }) +} else { + describe.skip('DenoCryptoProvider', () => {}) +} diff --git a/packages/deno/src/utils/crypto.ts b/packages/deno/src/utils/crypto.ts new file mode 100644 index 00000000..04b7b21b --- /dev/null +++ b/packages/deno/src/utils/crypto.ts @@ -0,0 +1,97 @@ +/* eslint-disable no-restricted-globals */ +import { Buffer } from 'node:buffer' +import { createCipheriv, createHash, createHmac, pbkdf2 } from 'node:crypto' +import { deflateSync, gunzipSync } from 'node:zlib' + +import { BaseCryptoProvider, IAesCtr, ICryptoProvider, IEncryptionScheme } from '@mtcute/core/utils.js' +import { getWasmUrl, ige256Decrypt, ige256Encrypt, initSync } from '@mtcute/wasm' +// node:crypto is properly implemented in deno, so we can just use it +// largely just copy-pasting from @mtcute/node + +const toUint8Array = (buf: Buffer | Uint8Array): Uint8Array => { + if (!Buffer.isBuffer(buf)) return buf + + return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength) +} + +export class DenoCryptoProvider extends BaseCryptoProvider implements ICryptoProvider { + async initialize(): Promise { + // eslint-disable-next-line no-restricted-globals + const wasm = await fetch(getWasmUrl()).then((res) => res.arrayBuffer()) + initSync(wasm) + } + + createAesCtr(key: Uint8Array, iv: Uint8Array): IAesCtr { + const cipher = createCipheriv(`aes-${key.length * 8}-ctr`, key, iv) + + const update = (data: Uint8Array) => toUint8Array(cipher.update(data)) + + return { + process: update, + } + } + + pbkdf2( + password: Uint8Array, + salt: Uint8Array, + iterations: number, + keylen = 64, + algo = 'sha512', + ): Promise { + return new Promise((resolve, reject) => + pbkdf2(password, salt, iterations, keylen, algo, (err: Error | null, buf: Uint8Array) => + err !== null ? reject(err) : resolve(toUint8Array(buf)), + ), + ) + } + + sha1(data: Uint8Array): Uint8Array { + return toUint8Array(createHash('sha1').update(data).digest()) + } + + sha256(data: Uint8Array): Uint8Array { + return toUint8Array(createHash('sha256').update(data).digest()) + } + + hmacSha256(data: Uint8Array, key: Uint8Array): Uint8Array { + return toUint8Array(createHmac('sha256', key).update(data).digest()) + } + + createAesIge(key: Uint8Array, iv: Uint8Array): IEncryptionScheme { + return { + encrypt(data: Uint8Array): Uint8Array { + return ige256Encrypt(data, key, iv) + }, + decrypt(data: Uint8Array): Uint8Array { + return ige256Decrypt(data, key, iv) + }, + } + } + + gzip(data: Uint8Array, maxSize: number): Uint8Array | null { + try { + // telegram accepts both zlib and gzip, but zlib is faster and has less overhead, so we use it here + return toUint8Array( + deflateSync(data, { + maxOutputLength: maxSize, + }), + ) + // hot path, avoid additional runtime checks + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + if (e.code === 'ERR_BUFFER_TOO_LARGE') { + return null + } + + throw e + } + } + + gunzip(data: Uint8Array): Uint8Array { + return toUint8Array(gunzipSync(data)) + } + + randomFill(buf: Uint8Array) { + crypto.getRandomValues(buf) + } +} diff --git a/packages/deno/src/utils/exit-hook.ts b/packages/deno/src/utils/exit-hook.ts new file mode 100644 index 00000000..aa43b595 --- /dev/null +++ b/packages/deno/src/utils/exit-hook.ts @@ -0,0 +1,21 @@ +const callbacks = new Set<() => void>() + +let registered = false + +export function beforeExit(fn: () => void): () => void { + if (!registered) { + registered = true + + window.addEventListener('unload', () => { + for (const callback of callbacks) { + callback() + } + }) + } + + callbacks.add(fn) + + return () => { + callbacks.delete(fn) + } +} diff --git a/packages/deno/src/utils/normalize-file.ts b/packages/deno/src/utils/normalize-file.ts new file mode 100644 index 00000000..7e5b10e8 --- /dev/null +++ b/packages/deno/src/utils/normalize-file.ts @@ -0,0 +1,50 @@ +import { ReadStream } from 'node:fs' +import { stat } from 'node:fs/promises' +import { basename } from 'node:path' +import { Readable as NodeReadable } from 'node:stream' + +import { UploadFileLike } from '@mtcute/core' +import { extractFileName } from '@mtcute/core/utils.js' + +export async function normalizeFile(file: UploadFileLike) { + if (typeof file === 'string') { + const fd = await Deno.open(file, { read: true }) + + return { + file: fd.readable, + fileSize: (await fd.stat()).size, + fileName: extractFileName(file), + } + } + + if (file instanceof Deno.FsFile) { + const stat = await file.stat() + + return { + file: file.readable, + // https://github.com/denoland/deno/issues/23591 + // fileName: ..., + fileSize: stat.size, + } + } + + // while these are not Deno-specific, they still may happen + if (file instanceof ReadStream) { + const fileName = basename(file.path.toString()) + const fileSize = await stat(file.path.toString()).then((stat) => stat.size) + + return { + file: NodeReadable.toWeb(file) as unknown as ReadableStream, + fileName, + fileSize, + } + } + + if (file instanceof NodeReadable) { + return { + file: NodeReadable.toWeb(file) as unknown as ReadableStream, + } + } + + return null +} diff --git a/packages/deno/src/utils/tcp.ts b/packages/deno/src/utils/tcp.ts new file mode 100644 index 00000000..0bdcd6ef --- /dev/null +++ b/packages/deno/src/utils/tcp.ts @@ -0,0 +1,135 @@ +import EventEmitter from 'events' + +import { IntermediatePacketCodec, IPacketCodec, ITelegramTransport, MtcuteError, TransportState } from '@mtcute/core' +import { BasicDcOption, ICryptoProvider, Logger } from '@mtcute/core/utils.js' + +import { writeAll } from '@std/io/write-all' + +/** + * Base for TCP transports. + * Subclasses must provide packet codec in `_packetCodec` property + */ +export abstract class BaseTcpTransport extends EventEmitter implements ITelegramTransport { + protected _currentDc: BasicDcOption | null = null + protected _state: TransportState = TransportState.Idle + protected _socket: Deno.TcpConn | null = null + + abstract _packetCodec: IPacketCodec + protected _crypto!: ICryptoProvider + protected log!: Logger + + packetCodecInitialized = false + + private _updateLogPrefix() { + if (this._currentDc) { + this.log.prefix = `[TCP:${this._currentDc.ipAddress}:${this._currentDc.port}] ` + } else { + this.log.prefix = '[TCP:disconnected] ' + } + } + + setup(crypto: ICryptoProvider, log: Logger): void { + this._crypto = crypto + this.log = log.create('tcp') + this._updateLogPrefix() + } + + state(): TransportState { + return this._state + } + + currentDc(): BasicDcOption | null { + return this._currentDc + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + connect(dc: BasicDcOption, testMode: boolean): void { + if (this._state !== TransportState.Idle) { + throw new MtcuteError('Transport is not IDLE') + } + + if (!this.packetCodecInitialized) { + this._packetCodec.setup?.(this._crypto, this.log) + this._packetCodec.on('error', (err) => this.emit('error', err)) + this._packetCodec.on('packet', (buf) => this.emit('message', buf)) + this.packetCodecInitialized = true + } + + this._state = TransportState.Connecting + this._currentDc = dc + this._updateLogPrefix() + + this.log.debug('connecting to %j', dc) + + Deno.connect({ + hostname: dc.ipAddress, + port: dc.port, + transport: 'tcp', + }) + .then(this.handleConnect.bind(this)) + .catch((err) => { + this.handleError(err) + this.close() + }) + } + + close(): void { + if (this._state === TransportState.Idle) return + this.log.info('connection closed') + + this._state = TransportState.Idle + this._socket?.close() + this._socket = null + this._currentDc = null + this._packetCodec.reset() + this.emit('close') + } + + handleError(error: unknown): void { + this.log.error('error: %s', error) + this.emit('error', error) + } + + async handleConnect(socket: Deno.TcpConn): Promise { + this._socket = socket + this.log.info('connected') + + try { + const packet = await this._packetCodec.tag() + + if (packet.length) { + await writeAll(this._socket, packet) + } + + this._state = TransportState.Ready + this.emit('ready') + + const reader = this._socket.readable.getReader() + + while (true) { + const { done, value } = await reader.read() + if (done) break + + this._packetCodec.feed(value) + } + } catch (e) { + this.handleError(e) + } + + this.close() + } + + async send(bytes: Uint8Array): Promise { + const framed = await this._packetCodec.encode(bytes) + + if (this._state !== TransportState.Ready) { + throw new MtcuteError('Transport is not READY') + } + + await writeAll(this._socket!, framed) + } +} + +export class TcpTransport extends BaseTcpTransport { + _packetCodec = new IntermediatePacketCodec() +} diff --git a/packages/deno/src/worker.ts b/packages/deno/src/worker.ts new file mode 100644 index 00000000..64e34db3 --- /dev/null +++ b/packages/deno/src/worker.ts @@ -0,0 +1,71 @@ +/// +import { setPlatform } from '@mtcute/core/platform.js' +import { + ClientMessageHandler, + RespondFn, + SendFn, + SomeWorker, + TelegramWorker as TelegramWorkerBase, + TelegramWorkerOptions, + TelegramWorkerPort as TelegramWorkerPortBase, + TelegramWorkerPortOptions, + WorkerCustomMethods, + WorkerMessageHandler, +} from '@mtcute/core/worker.js' + +import { DenoPlatform } from './platform.js' + +export type { TelegramWorkerOptions, TelegramWorkerPortOptions, WorkerCustomMethods } + +let _registered = false + +export class TelegramWorker extends TelegramWorkerBase { + registerWorker(handler: WorkerMessageHandler): RespondFn { + if (_registered) { + throw new Error('TelegramWorker must be created only once') + } + + _registered = true + + if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) { + const respond: RespondFn = self.postMessage.bind(self) + + // eslint-disable-next-line + self.addEventListener('message', (message) => handler((message as any).data, respond)) + + return respond + } + + throw new Error('TelegramWorker must be created from a worker') + } +} + +const platform = new DenoPlatform() + +export class TelegramWorkerPort extends TelegramWorkerPortBase { + constructor(readonly options: TelegramWorkerPortOptions) { + setPlatform(platform) + super(options) + } + + connectToWorker(worker: SomeWorker, handler: ClientMessageHandler): [SendFn, () => void] { + if (worker instanceof Worker) { + const send: SendFn = worker.postMessage.bind(worker) + + const messageHandler = (ev: MessageEvent) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + handler(ev.data) + } + + worker.addEventListener('message', messageHandler) + + return [ + send, + () => { + worker.removeEventListener('message', messageHandler) + }, + ] + } + throw new Error('Only workers are supported') + } +} diff --git a/packages/deno/tsconfig.json b/packages/deno/tsconfig.json new file mode 100644 index 00000000..66079ea1 --- /dev/null +++ b/packages/deno/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + }, + "include": [ + "./src", + ], + "references": [ + { "path": "../core" }, + { "path": "../dispatcher" }, + { "path": "../html-parser" }, + { "path": "../markdown-parser" } + ] +} diff --git a/packages/deno/typedoc.cjs b/packages/deno/typedoc.cjs new file mode 100644 index 00000000..e1f51e65 --- /dev/null +++ b/packages/deno/typedoc.cjs @@ -0,0 +1,10 @@ +module.exports = { + extends: ['../../.config/typedoc/config.base.cjs'], + entryPoints: ['./src/index.ts'], + externalPattern: [ + '../core/**', + '../html-parser/**', + '../markdown-parser/**', + '../sqlite/**', + ], +} diff --git a/packages/dispatcher/tsconfig.json b/packages/dispatcher/tsconfig.json index 6a3f7a53..919db0b1 100644 --- a/packages/dispatcher/tsconfig.json +++ b/packages/dispatcher/tsconfig.json @@ -6,5 +6,8 @@ }, "include": [ "./src", + ], + "references": [ + { "path": "../core" }, ] } diff --git a/packages/node/src/common-internals-node/platform.ts b/packages/node/src/common-internals-node/platform.ts index afd58299..f74035a5 100644 --- a/packages/node/src/common-internals-node/platform.ts +++ b/packages/node/src/common-internals-node/platform.ts @@ -17,7 +17,7 @@ export class NodePlatform implements ICorePlatform { declare normalizeFile: typeof normalizeFile getDeviceModel(): string { - return `${os.type()} ${os.arch()} ${os.release()}` + return `Node.js/${process.version} (${os.type()} ${os.arch()})` } getDefaultLogLevel(): number | null { diff --git a/packages/node/src/utils/crypto.ts b/packages/node/src/utils/crypto.ts index a04d1f13..96709dfa 100644 --- a/packages/node/src/utils/crypto.ts +++ b/packages/node/src/utils/crypto.ts @@ -4,7 +4,6 @@ import { readFile } from 'fs/promises' import { createRequire } from 'module' import { deflateSync, gunzipSync } from 'zlib' -import { MaybePromise } from '@mtcute/core' import { BaseCryptoProvider, IAesCtr, ICryptoProvider, IEncryptionScheme } from '@mtcute/core/utils.js' import { ige256Decrypt, ige256Encrypt, initSync } from '@mtcute/wasm' @@ -25,7 +24,7 @@ export abstract class BaseNodeCryptoProvider extends BaseCryptoProvider { iterations: number, keylen = 64, algo = 'sha512', - ): MaybePromise { + ): Promise { return new Promise((resolve, reject) => pbkdf2(password, salt, iterations, keylen, algo, (err: Error | null, buf: Uint8Array) => err !== null ? reject(err) : resolve(buf), diff --git a/packages/web/src/encodings/base64.ts b/packages/web/src/common-internals-web/base64.ts similarity index 100% rename from packages/web/src/encodings/base64.ts rename to packages/web/src/common-internals-web/base64.ts diff --git a/packages/web/src/encodings/hex.ts b/packages/web/src/common-internals-web/hex.ts similarity index 100% rename from packages/web/src/encodings/hex.ts rename to packages/web/src/common-internals-web/hex.ts diff --git a/packages/web/src/logging.ts b/packages/web/src/common-internals-web/logging.ts similarity index 100% rename from packages/web/src/logging.ts rename to packages/web/src/common-internals-web/logging.ts diff --git a/packages/web/src/common-internals-web/readme.md b/packages/web/src/common-internals-web/readme.md new file mode 100644 index 00000000..b03e8b77 --- /dev/null +++ b/packages/web/src/common-internals-web/readme.md @@ -0,0 +1,2 @@ +this folder is for common code across `@mtcute/web` and `@mtcute/deno`. +it is symlinked into `@mtcute/deno` \ No newline at end of file diff --git a/packages/web/src/encodings/utf8.ts b/packages/web/src/common-internals-web/utf8.ts similarity index 90% rename from packages/web/src/encodings/utf8.ts rename to packages/web/src/common-internals-web/utf8.ts index a4cb1168..8febf7e8 100644 --- a/packages/web/src/encodings/utf8.ts +++ b/packages/web/src/common-internals-web/utf8.ts @@ -9,7 +9,7 @@ export function utf8ByteLength(str: string) { const code = str.charCodeAt(i) if (code > 0x7f && code <= 0x7ff) s++ else if (code > 0x7ff && code <= 0xffff) s += 2 - if (code >= 0xDC00 && code <= 0xDFFF) i-- //trail surrogate + if (code >= 0xdc00 && code <= 0xdfff) i-- //trail surrogate } return s diff --git a/packages/web/src/idb/driver.ts b/packages/web/src/idb/driver.ts index a35d7a1f..7a26d5df 100644 --- a/packages/web/src/idb/driver.ts +++ b/packages/web/src/idb/driver.ts @@ -1,3 +1,5 @@ +/// +/// import { BaseStorageDriver, MtUnsupportedError } from '@mtcute/core' import { txToPromise } from './utils.js' diff --git a/packages/web/src/platform.ts b/packages/web/src/platform.ts index d90f2c3a..84d388e7 100644 --- a/packages/web/src/platform.ts +++ b/packages/web/src/platform.ts @@ -1,10 +1,10 @@ import { ICorePlatform } from '@mtcute/core/platform.js' -import { base64Decode, base64Encode } from './encodings/base64.js' -import { hexDecode, hexEncode } from './encodings/hex.js' -import { utf8ByteLength, utf8Decode, utf8Encode } from './encodings/utf8.js' +import { base64Decode, base64Encode } from './common-internals-web/base64.js' +import { hexDecode, hexEncode } from './common-internals-web/hex.js' +import { defaultLoggingHandler } from './common-internals-web/logging.js' +import { utf8ByteLength, utf8Decode, utf8Encode } from './common-internals-web/utf8.js' import { beforeExit } from './exit-hook.js' -import { defaultLoggingHandler } from './logging.js' export class WebPlatform implements ICorePlatform { // ICorePlatform @@ -52,7 +52,6 @@ export class WebPlatform implements ICorePlatform { declare utf8Decode: typeof utf8Decode declare hexEncode: typeof hexEncode declare hexDecode: typeof hexDecode - declare base64Encode: typeof base64Encode declare base64Decode: typeof base64Decode } diff --git a/packages/web/src/worker.ts b/packages/web/src/worker.ts index e45f7463..23f9dcfc 100644 --- a/packages/web/src/worker.ts +++ b/packages/web/src/worker.ts @@ -1,3 +1,5 @@ +/// +/// import { setPlatform } from '@mtcute/core/platform.js' import { ClientMessageHandler, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c2a5e5d..e52cc9e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -231,6 +231,31 @@ importers: specifier: workspace:^ version: link:../test + packages/deno: + dependencies: + '@db/sqlite': + specifier: npm:@jsr/db__sqlite@0.11.1 + version: /@jsr/db__sqlite@0.11.1 + '@mtcute/core': + specifier: workspace:^ + version: link:../core + '@mtcute/html-parser': + specifier: workspace:^ + version: link:../html-parser + '@mtcute/markdown-parser': + specifier: workspace:^ + version: link:../markdown-parser + '@mtcute/wasm': + specifier: workspace:^ + version: link:../wasm + '@std/io': + specifier: npm:@jsr/std__io@0.223.0 + version: /@jsr/std__io@0.223.0 + devDependencies: + '@mtcute/test': + specifier: workspace:^ + version: link:../test + packages/dispatcher: dependencies: '@mtcute/core': @@ -1189,6 +1214,86 @@ packages: '@jridgewell/sourcemap-codec': 1.4.11 dev: true + /@jsr/db__sqlite@0.11.1: + resolution: {integrity: sha512-6IKyfi+TQan431kwOy3WrdzgKwITuDdSKfq6nkWINfNWknPQ+lQ6/R008bAUUz+AwW8e0prxi3IMMxzELUV8Lw==, tarball: https://npm.jsr.io/~/8/@jsr/db__sqlite/0.11.1.tgz} + dependencies: + '@jsr/denosaurs__plug': 1.0.6 + '@jsr/std__path': 0.217.0 + dev: false + + /@jsr/denosaurs__plug@1.0.6: + resolution: {integrity: sha512-2uqvX2xpDy5W76jJVKazXvHuh5WPNg8eUV+2u+Hcn5XLwKqWGr/xj4wQFRMXrS12Xhya+ToZdUg4gxLh+XOOCg==, tarball: https://npm.jsr.io/~/8/@jsr/denosaurs__plug/1.0.6.tgz} + dependencies: + '@jsr/std__encoding': 0.221.0 + '@jsr/std__fmt': 0.221.0 + '@jsr/std__fs': 0.221.0 + '@jsr/std__path': 0.221.0 + dev: false + + /@jsr/std__assert@0.217.0: + resolution: {integrity: sha512-RCQbXJeUVCgDGEPsrO57CI9Cgbo9NAWsJUhZ7vrHgtD//Ic32YmUQazdGKPZzao5Zn8dP6xV4Nma3HHZC5ySTw==, tarball: https://npm.jsr.io/~/8/@jsr/std__assert/0.217.0.tgz} + dependencies: + '@jsr/std__fmt': 0.217.0 + dev: false + + /@jsr/std__assert@0.221.0: + resolution: {integrity: sha512-2B+5fq4Rar8NmLms7sv9YfYlMukZDTNMQV5fXjtZvnaKc8ljt+59UsMtIjTXFsDwsAx7VoxkMKmmdHxlP+h5JA==, tarball: https://npm.jsr.io/~/8/@jsr/std__assert/0.221.0.tgz} + dependencies: + '@jsr/std__fmt': 0.221.0 + dev: false + + /@jsr/std__assert@0.223.0: + resolution: {integrity: sha512-9FWOoAQN1uF5SliWw3IgdXmk2usz5txvausX4sLAASHfQMbUSCe1akcD7HgFV01J/2Mr9TfCjPvsSUzuuASouQ==, tarball: https://npm.jsr.io/~/8/@jsr/std__assert/0.223.0.tgz} + dependencies: + '@jsr/std__fmt': 0.223.0 + dev: false + + /@jsr/std__bytes@0.223.0: + resolution: {integrity: sha512-BBjhj0uFlB3+AVEmaPygEwY5CL5mj3vSZlusC8xxjCRNWDYGukfQT/F5GOTTfjeaq7njduk7TYe6e5cDg659yg==, tarball: https://npm.jsr.io/~/8/@jsr/std__bytes/0.223.0.tgz} + dev: false + + /@jsr/std__encoding@0.221.0: + resolution: {integrity: sha512-FT5i/WHNtXJvqOITDK0eOVIyyOphqtxwhzo5PiVWoYTFmUuFcRYKas39GT1UQDi4s24FcHd2deQEBbi3tPAj1Q==, tarball: https://npm.jsr.io/~/8/@jsr/std__encoding/0.221.0.tgz} + dev: false + + /@jsr/std__fmt@0.217.0: + resolution: {integrity: sha512-L3mVYP7DsujrJ001SvPr4Fl/Fu0e3uzgHJ6NYTRUk7sgi9k7YKeLOLVwRijUX7qIsp3Ourp2DyAHHgYDgT4GcQ==, tarball: https://npm.jsr.io/~/8/@jsr/std__fmt/0.217.0.tgz} + dev: false + + /@jsr/std__fmt@0.221.0: + resolution: {integrity: sha512-VLqM052U78LQ11p/KfqI49a2/sDbKtHFHuxO/h+3Cnvhze9beIZU4Lg3Gpu8rGYjB2YS6CfXzKXHuyAJn5FJFg==, tarball: https://npm.jsr.io/~/8/@jsr/std__fmt/0.221.0.tgz} + dev: false + + /@jsr/std__fmt@0.223.0: + resolution: {integrity: sha512-J6SVTw/l3C4hOwEuqnZ4ZHD1jVIIZt09fb5LP9CMGyVGNnoW8/lxJvCNhIOv+3ZXC1ErGlIzW4bgYSxHwbvSaQ==, tarball: https://npm.jsr.io/~/8/@jsr/std__fmt/0.223.0.tgz} + dev: false + + /@jsr/std__fs@0.221.0: + resolution: {integrity: sha512-2XMlO67zQlKoxbCsfGOBVlnyWhMMdOzYUWfajvggfw2p+yITd9hJj9+tpfiwLf/88CzknhlMLwSCamSYjHKloA==, tarball: https://npm.jsr.io/~/8/@jsr/std__fs/0.221.0.tgz} + dependencies: + '@jsr/std__assert': 0.221.0 + '@jsr/std__path': 0.221.0 + dev: false + + /@jsr/std__io@0.223.0: + resolution: {integrity: sha512-K+OXJHsIf9227aYgNTaapEkpphHrI+oYVkl14UV+le+Fk9MzkJmebU0XAU6krgVS283mW7VPJsXVV3gD5JWvJw==, tarball: https://npm.jsr.io/~/8/@jsr/std__io/0.223.0.tgz} + dependencies: + '@jsr/std__assert': 0.223.0 + '@jsr/std__bytes': 0.223.0 + dev: false + + /@jsr/std__path@0.217.0: + resolution: {integrity: sha512-OkP+yiBJpFZKTH3gHqlepYU3TQzXM/UjEQ0U1gYw8BwVr87TwKfzwAb1WT1vY/Bs8NXScvuP4Kpu/UhEsNHD3A==, tarball: https://npm.jsr.io/~/8/@jsr/std__path/0.217.0.tgz} + dependencies: + '@jsr/std__assert': 0.217.0 + dev: false + + /@jsr/std__path@0.221.0: + resolution: {integrity: sha512-uOWaY4cWp28CFBSisr8M/92FtpyjiFO0+wQSH7GgmiXQUls+vALqdCGewFkunG8RfA/25RGdot5hFXedmtPdOg==, tarball: https://npm.jsr.io/~/8/@jsr/std__path/0.221.0.tgz} + dependencies: + '@jsr/std__assert': 0.221.0 + dev: false + /@ljharb/through@2.3.11: resolution: {integrity: sha512-ccfcIDlogiXNq5KcbAwbaO7lMh3Tm1i3khMPYpxlK8hH/W53zN81KM9coerRLOnTGu3nfXIniAmQbRI9OxbC0w==} engines: {node: '>= 0.4'} diff --git a/scripts/build-package.js b/scripts/build-package.js index fc7bec73..c5898ad6 100644 --- a/scripts/build-package.js +++ b/scripts/build-package.js @@ -499,6 +499,10 @@ if (IS_JSR) { for (const [name, version] of Object.entries(builtPkgJson.dependencies)) { if (name.startsWith('@mtcute/')) { importMap[name] = `jsr:${name}@${version}` + } else if (version.startsWith('npm:@jsr/')) { + const jsrName = version.slice(9).split('@')[0].replace('__', '/') + const jsrVersion = version.slice(9).split('@')[1] + importMap[name] = `jsr:@${jsrName}@${jsrVersion}` } else { importMap[name] = `npm:${name}@${version}` } diff --git a/scripts/fetch-deno-dts.mjs b/scripts/fetch-deno-dts.mjs new file mode 100644 index 00000000..89e7e90d --- /dev/null +++ b/scripts/fetch-deno-dts.mjs @@ -0,0 +1,33 @@ +import { createHash } from 'crypto' +import * as fs from 'fs/promises' +import { dirname } from 'path' +import { fileURLToPath } from 'url' + +const DTS_URL = 'https://github.com/denoland/deno/releases/download/v1.42.4/lib.deno.d.ts' +const SHA256 = '554b5da7baf05e5693ca064fcf1665b0b847743ccfd0db89cb6f2388f2de0276' +const LIB_TARGET = fileURLToPath(new URL('../node_modules/@types/deno/index.d.ts', import.meta.url)) + +const stat = await fs.stat(LIB_TARGET).catch(() => null) + +if (stat?.isFile()) { + const sha256 = createHash('sha256').update(await fs.readFile(LIB_TARGET)).digest('hex') + + if (sha256 === SHA256) { + console.log('lib.deno.d.ts is up to date') + process.exit(0) + } +} + +const stream = await fetch(DTS_URL) +const dts = await stream.text() + +const sha256 = createHash('sha256').update(dts).digest('hex') + +if (sha256 !== SHA256) { + console.error(`lib.deno.d.ts SHA256 mismatch: expected ${SHA256}, got ${sha256}`) + process.exit(1) +} + +await fs.mkdir(dirname(LIB_TARGET), { recursive: true }).catch(() => null) +await fs.writeFile(LIB_TARGET, dts) +console.log('lib.deno.d.ts updated') diff --git a/scripts/publish.js b/scripts/publish.js index e07c75c2..d3a483f4 100644 --- a/scripts/publish.js +++ b/scripts/publish.js @@ -25,6 +25,7 @@ const JSR_EXCEPTIONS = { bun: 'never', 'create-bot': 'never', 'crypto-node': 'never', + deno: 'only', node: 'never', 'http-proxy': 'never', 'socks-proxy': 'never', diff --git a/scripts/remove-jsr-sourcefiles.mjs b/scripts/remove-jsr-sourcefiles.mjs new file mode 100644 index 00000000..3ed84a26 --- /dev/null +++ b/scripts/remove-jsr-sourcefiles.mjs @@ -0,0 +1,23 @@ +import * as fs from 'fs' +import { globSync } from 'glob' +import { join } from 'path' +import { fileURLToPath } from 'url' + +// for whatever reason, jsr's npm compatibility jayer doesn't remove +// original typescript source files, which results in type errors when +// trying to build the project. this script removes all source files from @jsr/* +// https://discord.com/channels/684898665143206084/1203185670508515399/1234222204044967967 + +const nodeModules = fileURLToPath(new URL('../node_modules', import.meta.url)) + +let count = 0 + +for (const file of globSync(join(nodeModules, '.pnpm/**/node_modules/@jsr/**/*.ts'))) { + if (file.endsWith('.d.ts')) continue + if (!fs.existsSync(file)) continue + + fs.unlinkSync(file) + count++ +} + +console.log(`[jsr] removed ${count} source files`) diff --git a/tsconfig.json b/tsconfig.json index 92538b93..326dd224 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "composite": true, "types": [ "node", + "deno", "vite/client" ], "lib": [