chore: moved build to @fuman/build

This commit is contained in:
alina 🌸 2024-11-16 16:00:12 +03:00
parent 0692de2179
commit 9e3e379c25
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
103 changed files with 1926 additions and 3034 deletions

View file

@ -6,7 +6,6 @@ import { globSync } from 'glob'
export function collectTestEntrypoints(params: { skipPackages: string[], skipTests: string[] }) {
const files: string[] = []
// eslint-disable-next-line no-restricted-globals
const packages = resolve(__dirname, '../../packages')
const skipTests = params.skipTests.map(path => resolve(packages, path))

View file

@ -1,14 +1,14 @@
/// <reference types="vitest" />
import { cpSync, existsSync, writeFileSync } from 'node:fs'
import { relative, resolve } from 'node:path'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
// todo
import { fumanBuild } from '@fuman/build/vite'
import type { ConfigEnv, UserConfig } from 'vite'
import { nodeExternals } from 'rollup-plugin-node-externals'
import dts from 'vite-plugin-dts'
import { processPackageJson } from './vite-utils/package-json'
const rootDir = fileURLToPath(new URL('..', import.meta.url))
export default async (env: ConfigEnv): Promise<UserConfig> => {
@ -16,15 +16,7 @@ export default async (env: ConfigEnv): Promise<UserConfig> => {
throw new Error('This config is only for building')
}
const { packageJson, entrypoints } = processPackageJson(process.cwd())
let customConfig: any
try {
const mod = await import(resolve(process.cwd(), 'build.config.js'))
customConfig = await mod.default()
} catch (e) {
if (e.code !== 'ERR_MODULE_NOT_FOUND') throw e
}
const packageJson = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf8'))
const CJS_DEPRECATION_WARNING = `
if (typeof globalThis !== 'undefined' && !globalThis._MTCUTE_CJS_DEPRECATION_WARNED) {
@ -34,69 +26,19 @@ if (typeof globalThis !== 'undefined' && !globalThis._MTCUTE_CJS_DEPRECATION_WAR
}
`.trim()
if (customConfig?.preBuild) {
await customConfig.preBuild()
}
return {
build: {
rollupOptions: {
plugins: [
...(customConfig?.rollupPluginsPre ?? []),
nodeExternals({
builtinsPrefix: 'ignore',
exclude: /^@fuman\//,
}),
{
name: 'mtcute-finalize',
name: 'mtcute-cjs-deprecated',
renderChunk(code, chunk, options) {
if (options.format !== 'cjs') return null
return `${CJS_DEPRECATION_WARNING}\n${code}`
},
async closeBundle() {
const packageDir = process.cwd()
const outDir = resolve(packageDir, 'dist')
customConfig?.finalPackageJson?.(packageJson)
writeFileSync(resolve(outDir, 'package.json'), JSON.stringify(packageJson, null, 4))
cpSync(resolve(rootDir, 'LICENSE'), resolve(outDir, 'LICENSE'))
cpSync(resolve(process.cwd(), 'README.md'), resolve(outDir, 'README.md'))
if (existsSync(resolve(outDir, 'chunks/cjs'))) {
// write {"type":"commonjs"} into chunks/cjs so that node doesn't complain
const cjsFile = resolve(outDir, 'chunks/cjs/package.json')
writeFileSync(cjsFile, JSON.stringify({ type: 'commonjs' }, null, 4))
}
for (const [name, entry] of Object.entries(entrypoints)) {
const dTsFile = resolve(outDir, `${name}.d.ts`)
if (!existsSync(dTsFile)) {
const entryTypings = resolve(outDir, entry.replace('/src/', '/').replace(/\.ts$/, '.d.ts'))
if (!existsSync(entryTypings)) continue
const relativePath = relative(outDir, entryTypings)
writeFileSync(dTsFile, `export * from './${relativePath.replace(/\.d\.ts$/, '.js')}'`)
}
cpSync(dTsFile, dTsFile.replace(/\.d\.ts$/, '.d.cts'))
}
await customConfig?.final?.({ outDir, packageDir })
},
},
...(customConfig?.rollupPluginsPost ?? []),
],
output: {
minifyInternalExports: false,
chunkFileNames: 'chunks/[format]/[hash].js',
},
external: customConfig?.external,
},
lib: {
entry: entrypoints as any,
formats: customConfig?.buildCjs === false ? ['es'] : ['es', 'cjs'],
},
minify: false,
outDir: 'dist',
@ -104,10 +46,17 @@ if (typeof globalThis !== 'undefined' && !globalThis._MTCUTE_CJS_DEPRECATION_WAR
target: 'es2022',
},
plugins: [
...(customConfig?.vitePlugins ?? []),
nodeExternals({
builtinsPrefix: 'ignore',
}),
fumanBuild({
root: rootDir,
autoSideEffectsFalse: true,
}),
dts({
// broken; see https://github.com/qmhc/vite-plugin-dts/issues/321, https://github.com/microsoft/rushstack/issues/3557
// rollupTypes: true,
insertTypesEntry: true,
}),
],
}

View file

@ -1,4 +1,3 @@
/* eslint-disable no-restricted-globals */
import { resolve } from 'node:path'
import { defineConfig } from 'vite'
@ -78,15 +77,6 @@ export default defineConfig({
return code
},
},
{
name: 'fix-events',
transform(code) {
if (!code.includes('events')) return code
return code.replace(/^import (.+?) from ['"]events['"]/gms, (_, name) => {
return `import ${name} from 'node:events'`
})
},
},
{
name: 'fix-wasm-load',
async transform(code) {

View file

@ -6,7 +6,6 @@ import { fixupCjs } from './vite-utils/fixup-cjs'
import { testSetup } from './vite-utils/test-setup-plugin'
import { collectTestEntrypoints } from './vite-utils/collect-test-entrypoints'
// eslint-disable-next-line no-restricted-globals
const POLYFILLS = resolve(__dirname, 'vite-utils/polyfills-deno.ts')
export default defineConfig({

1
.npmrc
View file

@ -1 +1,2 @@
@jsr:registry=https://npm.jsr.io
@fuman:registry=https://npm.tei.su

68
build.config.js Normal file
View file

@ -0,0 +1,68 @@
/** @type {import('@fuman/build').RootConfig} */
export default {
jsr: {
exclude: ['**/*.{test,bench,test-utils}.ts', '**/__fixtures__/**'],
sourceDir: 'src',
transformCode: (path, code) => {
// add shims for node-specific APIs and replace NodeJS.* types
// pretty fragile, but it works for now
// todo: remove this god awfulness and use `declare const` in-place instead
const typesToReplace = {
'NodeJS\\.Timeout': 'number',
'NodeJS\\.Immediate': 'number',
}
const nodeSpecificApis = {
setImmediate: '(cb: (...args: any[]) => void, ...args: any[]) => number',
clearImmediate: '(id: number) => void',
Buffer:
'{ '
+ 'concat: (...args: any[]) => Uint8Array, '
+ 'from: (data: any, encoding?: string) => { toString(encoding?: string): string }, '
+ ' }',
SharedWorker: ['type', 'never'],
WorkerGlobalScope:
'{ '
+ ' new (): typeof WorkerGlobalScope, '
+ ' postMessage: (message: any, transfer?: Transferable[]) => void, '
+ ' addEventListener: (type: "message", listener: (ev: MessageEvent) => void) => void, '
+ ' }',
process: '{ ' + 'hrtime: { bigint: () => bigint }, ' + '}',
}
for (const [name, decl_] of Object.entries(nodeSpecificApis)) {
if (code.includes(name)) {
if (name === 'Buffer' && code.includes('node:buffer')) continue
const isType = Array.isArray(decl_) && decl_[0] === 'type'
const decl = isType ? decl_[1] : decl_
if (isType) {
code = `declare type ${name} = ${decl};\n${code}`
} else {
code = `declare const ${name}: ${decl};\n${code}`
}
}
}
for (const [oldType, newType] of Object.entries(typesToReplace)) {
if (code.match(oldType)) {
code = code.replace(new RegExp(oldType, 'g'), newType)
}
}
return code
},
},
versioning: {
exclude: [
'**/*.test.ts',
'**/*.test-utils.ts',
'**/__fixtures__/**',
'**/*.md',
'typedoc.cjs',
'{scripts,dist,tests,private}/**',
],
},
viteConfig: '.config/vite.build.ts',
}

View file

@ -87,6 +87,7 @@ export default antfu({
'ts/switch-exhaustiveness-check': 'off',
'ts/restrict-template-expressions': 'off',
'ts/method-signature-style': 'off',
'style/indent-binary-ops': 'off',
},
}, {
ignores: [

View file

@ -10,7 +10,7 @@
"homepage": "https://mtcute.dev",
"repository": {
"type": "git",
"url": "https://github.com/mtcute/mtcute"
"url": "git+https://github.com/mtcute/mtcute.git"
},
"keywords": [
"telegram",
@ -26,7 +26,7 @@
"packages/*"
],
"scripts": {
"postinstall": "node scripts/validate-deps-versions.js && node scripts/remove-jsr-sourcefiles.js",
"postinstall": "fuman-build validate-workspace-deps && node scripts/remove-jsr-sourcefiles.js",
"test": "vitest --config .config/vite.ts run",
"test:dev": "vitest --config .config/vite.ts watch",
"test:ui": "vitest --config .config/vite.ts --ui",
@ -42,12 +42,13 @@
"lint:fix": "eslint --fix .",
"publish-all": "node scripts/publish.js all",
"docs": "typedoc --options .config/typedoc/config.cjs",
"build-package": "node scripts/build-package.js",
"build-package": "tsx scripts/build-package.js",
"build-package-vite": "node scripts/build-package-vite.js"
},
"devDependencies": {
"@antfu/eslint-config": "2.26.0",
"@fuman/jsr": "workspace:^",
"@fuman/build": "0.0.1",
"@fuman/utils": "0.0.1",
"@types/deno": "npm:@teidesu/deno-types@1.46.3",
"@types/node": "20.10.0",
"@types/ws": "8.5.4",

View file

@ -1,4 +1,13 @@
/** @type {import('@fuman/build/vite').CustomBuildConfig} */
export default () => ({
buildCjs: false,
viteConfig: {
build: {
lib: {
formats: ['es'],
},
rollupOptions: {
external: ['bun', 'bun:sqlite'],
},
},
},
})

View file

@ -12,20 +12,20 @@
"./utils.js": "./src/utils.ts",
"./methods.js": "./src/methods.ts"
},
"scripts": {
"docs": "typedoc",
"build": "pnpm run -w build-package bun"
},
"dependencies": {
"@mtcute/core": "workspace:^",
"@mtcute/html-parser": "workspace:^",
"@mtcute/markdown-parser": "workspace:^",
"@mtcute/wasm": "workspace:^",
"@fuman/bun": "workspace:^",
"@fuman/net": "workspace:^",
"@fuman/io": "workspace:^"
"@fuman/utils": "0.0.1",
"@fuman/bun": "0.0.1",
"@fuman/net": "0.0.1",
"@fuman/io": "0.0.1"
},
"devDependencies": {
"@mtcute/test": "workspace:^"
},
"fuman": {
"jsr": "skip"
}
}

View file

@ -11,6 +11,7 @@ import {
BaseTelegramClient as BaseTelegramClientBase,
TelegramClient as TelegramClientBase,
} from '@mtcute/core/client.js'
import { unknownToError } from '@fuman/utils'
import { downloadToFile } from './methods/download-file.js'
import { downloadAsNodeStream } from './methods/download-node-stream.js'
@ -127,7 +128,7 @@ export class TelegramClient extends TelegramClientBase {
this.start(params)
.then(then)
.catch(err => this.emitError(err))
.catch(err => this.onError.emit(unknownToError(err)))
}
downloadToFile(

View file

@ -12,6 +12,7 @@ import {
ige256Encrypt,
initSync,
} from '@mtcute/wasm'
import { u8 } from '@fuman/utils'
// we currently prefer wasm for ctr because bun mostly uses browserify polyfills for node:crypto
// which are slow AND semi-broken
@ -52,20 +53,20 @@ export class BunCryptoProvider extends BaseCryptoProvider implements ICryptoProv
algo = 'sha512',
): Promise<Uint8Array> {
return new Promise((resolve, reject) =>
pbkdf2(password, salt, iterations, keylen, algo, (err: Error | null, buf: Uint8Array) =>
err !== null ? reject(err) : resolve(buf)),
pbkdf2(password, salt, iterations, keylen, algo, (err: Error | null, buf: Buffer) =>
err !== null ? reject(err) : resolve(buf as unknown as Uint8Array)),
)
}
sha1(data: Uint8Array): Uint8Array {
const res = new Uint8Array(Bun.SHA1.byteLength)
const res = u8.alloc(Bun.SHA1.byteLength)
Bun.SHA1.hash(data, res)
return res
}
sha256(data: Uint8Array): Uint8Array {
const res = new Uint8Array(Bun.SHA256.byteLength)
const res = u8.alloc(Bun.SHA256.byteLength)
Bun.SHA256.hash(data, res)
return res
@ -90,7 +91,7 @@ export class BunCryptoProvider extends BaseCryptoProvider implements ICryptoProv
// telegram accepts both zlib and gzip, but zlib is faster and has less overhead, so we use it here
return deflateSync(data, {
maxOutputLength: maxSize,
})
}) as unknown as Uint8Array
// hot path, avoid additional runtime checks
} catch (e: any) {
if (e.code === 'ERR_BUFFER_TOO_LARGE') {
@ -102,7 +103,7 @@ export class BunCryptoProvider extends BaseCryptoProvider implements ICryptoProv
}
gunzip(data: Uint8Array): Uint8Array {
return gunzipSync(data)
return gunzipSync(data) as unknown as Uint8Array
}
randomFill(buf: Uint8Array): void {

View file

@ -14,7 +14,7 @@ import {
TelegramWorkerPort as TelegramWorkerPortBase,
} from '@mtcute/core/worker.js'
import { BunPlatform } from './platform'
import { BunPlatform } from './platform.js'
export type { TelegramWorkerOptions, WorkerCustomMethods }

View file

@ -8,13 +8,10 @@
"license": "MIT",
"sideEffects": false,
"exports": "./src/index.ts",
"scripts": {
"build": "pnpm run -w build-package convert"
},
"dependencies": {
"@mtcute/core": "workspace:^",
"@fuman/utils": "workspace:^",
"@fuman/net": "workspace:^"
"@fuman/utils": "0.0.1",
"@fuman/net": "0.0.1"
},
"devDependencies": {
"@mtcute/test": "workspace:^"

View file

@ -1,5 +1,5 @@
import { Long, MtArgumentError } from '@mtcute/core'
import { base64, typed } from '@fuman/utils'
import { base64, typed, u8 } from '@fuman/utils'
import type { PyrogramSession } from './types.js'
@ -13,32 +13,32 @@ export function serializePyrogramSession(session: PyrogramSession): string {
const userIdLong = Long.fromNumber(session.userId, true)
let u8: Uint8Array
let buf: Uint8Array
if (session.apiId === undefined) {
// old format
u8 = new Uint8Array(SESSION_STRING_SIZE_OLD)
const dv = typed.toDataView(u8)
buf = u8.alloc(SESSION_STRING_SIZE_OLD)
const dv = typed.toDataView(buf)
dv.setUint8(0, session.dcId)
dv.setUint8(1, session.isTest ? 1 : 0)
u8.set(session.authKey, 2)
buf.set(session.authKey, 2)
dv.setUint32(258, userIdLong.high)
dv.setUint32(262, userIdLong.low)
dv.setUint8(266, session.isBot ? 1 : 0)
} else {
u8 = new Uint8Array(SESSION_STRING_SIZE)
const dv = typed.toDataView(u8)
buf = u8.alloc(SESSION_STRING_SIZE)
const dv = typed.toDataView(buf)
dv.setUint8(0, session.dcId)
dv.setUint32(1, session.apiId)
dv.setUint8(5, session.isTest ? 1 : 0)
u8.set(session.authKey, 6)
buf.set(session.authKey, 6)
dv.setUint32(262, userIdLong.high)
dv.setUint32(266, userIdLong.low)
dv.setUint8(270, session.isBot ? 1 : 0)
}
return base64.encode(u8, true)
return base64.encode(buf, true)
}

View file

@ -1,5 +1,5 @@
import { MtArgumentError } from '@mtcute/core'
import { base64, typed } from '@fuman/utils'
import { base64, typed, u8 } from '@fuman/utils'
import { ip } from '@fuman/net'
import type { TelethonSession } from './types.js'
@ -10,26 +10,26 @@ export function serializeTelethonSession(session: TelethonSession): string {
}
const ipSize = session.ipv6 ? 16 : 4
const u8 = new Uint8Array(259 + ipSize)
const dv = typed.toDataView(u8)
const buf = u8.alloc(259 + ipSize)
const dv = typed.toDataView(buf)
dv.setUint8(0, session.dcId)
let pos
if (session.ipv6) {
u8.subarray(1, 17).set(ip.toBytesV6(ip.parseV6(session.ipAddress)))
buf.subarray(1, 17).set(ip.toBytesV6(ip.parseV6(session.ipAddress)))
pos = 17
} else {
u8.subarray(1, 5).set(ip.parseV4(session.ipAddress).parts)
buf.subarray(1, 5).set(ip.parseV4(session.ipAddress).parts)
pos = 5
}
dv.setUint16(pos, session.port)
pos += 2
u8.set(session.authKey, pos)
buf.set(session.authKey, pos)
let b64 = base64.encode(u8, true)
let b64 = base64.encode(buf, true)
while (b64.length % 4 !== 0) b64 += '=' // for some reason telethon uses padding
return `1${b64}`

View file

@ -5,6 +5,7 @@ import * as fs from 'node:fs'
const KNOWN_DECORATORS = ['memoizeGetters', 'makeInspectable']
/** @type {import('@fuman/build/vite').CustomBuildConfig} */
export default () => {
const networkManagerId = fileURLToPath(new URL('./src/network/network-manager.ts', import.meta.url))
const highlevelTypesDir = fileURLToPath(new URL('./src/highlevel/types', import.meta.url))
@ -18,7 +19,7 @@ export default () => {
)
return {
rollupPluginsPre: [
pluginsPre: [
{
name: 'mtcute-core-build-plugin',
transform(code, id) {
@ -70,7 +71,7 @@ export default () => {
},
},
],
finalJsr({ outDir }) {
finalizeJsr({ outDir }) {
const networkMgrFile = resolve(outDir, 'network/network-manager.ts')
const code = fs.readFileSync(networkMgrFile, 'utf8')

View file

@ -15,14 +15,13 @@
"./methods.js": "./src/highlevel/methods.ts"
},
"scripts": {
"build": "pnpm run -w build-package core",
"gen-client": "node ./scripts/generate-client.cjs",
"gen-updates": "node ./scripts/generate-updates.cjs"
},
"dependencies": {
"@fuman/io": "workspace:^",
"@fuman/net": "workspace:^",
"@fuman/utils": "workspace:^",
"@fuman/io": "0.0.1",
"@fuman/net": "0.0.1",
"@fuman/utils": "0.0.1",
"@mtcute/file-id": "workspace:^",
"@mtcute/tl": "workspace:^",
"@mtcute/tl-runtime": "workspace:^",

View file

@ -579,8 +579,10 @@ withParams(params: RpcCallOptions): this\n`)
it.type = { kind: ts.SyntaxKind.StringKeyword }
} else if (
it.initializer.kind === ts.SyntaxKind.NumericLiteral
|| (it.initializer.kind === ts.SyntaxKind.Identifier
&& (it.initializer.escapedText === 'NaN' || it.initializer.escapedText === 'Infinity'))
|| (
it.initializer.kind === ts.SyntaxKind.Identifier
&& (it.initializer.escapedText === 'NaN' || it.initializer.escapedText === 'Infinity')
)
) {
it.type = { kind: ts.SyntaxKind.NumberKeyword }
} else {
@ -723,8 +725,6 @@ withParams(params: RpcCallOptions): this\n`)
'call',
'importSession',
'exportSession',
'onError',
'emitError',
'handleClientUpdate',
'getApiCrenetials',
'getPoolSize',

View file

@ -8,7 +8,7 @@ describe('BaseTelegramClient', () => {
const session = await client.exportSession()
expect(session).toMatchInlineSnapshot(
`"AwQAAAAXAgIADjE0OS4xNTQuMTY3LjUwALsBAAAXAgICDzE0OS4xNTQuMTY3LjIyMrsBAAD-AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"`,
'"AwQAAAAXAgIADjE0OS4xNTQuMTY3LjUwALsBAAAXAgICDzE0OS4xNTQuMTY3LjIyMrsBAAD-AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"',
)
})
})

View file

@ -290,18 +290,7 @@ export class BaseTelegramClient implements ITelegramClient {
})
}
/**
* Register an error handler for the client
*
* @param handler Error handler.
*/
onError(handler: (err: unknown) => void): void {
this.mt.onError(handler)
}
emitError(err: unknown): void {
this.mt.emitError(err)
}
onError: Emitter<Error> = new Emitter()
handleClientUpdate(updates: tl.TypeUpdates, noDispatch?: boolean): void {
this.updates?.handleClientUpdate(updates, noDispatch)

View file

@ -6584,12 +6584,6 @@ TelegramClient.prototype.importSession = function (...args) {
TelegramClient.prototype.exportSession = function (...args) {
return this._client.exportSession(...args)
}
TelegramClient.prototype.onError = function (...args) {
return this._client.onError(...args)
}
TelegramClient.prototype.emitError = function (...args) {
return this._client.emitError(...args)
}
TelegramClient.prototype.handleClientUpdate = function (...args) {
return this._client.handleClientUpdate(...args)
}

View file

@ -52,13 +52,12 @@ export interface ITelegramClient {
): Promise<tl.RpcCallReturn[T['_']]>
importSession(session: string | StringSessionData, force?: boolean): Promise<void>
exportSession(): Promise<string>
onError(handler: (err: unknown) => void): void
emitError(err: unknown): void
handleClientUpdate(updates: tl.TypeUpdates, noDispatch?: boolean): void
onServerUpdate: Emitter<tl.TypeUpdates>
onRawUpdate: Emitter<RawUpdateInfo>
onConnectionState: Emitter<ConnectionState>
onError: Emitter<Error>
getApiCrenetials(): Promise<{ id: number, hash: string }>
// todo - this is only used for file dl/ul, which should probably be moved

View file

@ -1,36 +1,41 @@
import type { tl } from '@mtcute/tl'
import { AsyncResource, asNonNull } from '@fuman/utils'
import { MtTypeAssertionError } from '../../types/errors.js'
import { Reloadable } from '../../utils/reloadable.js'
import { tlJsonToJson } from '../../utils/tl-json.js'
import type { BaseTelegramClient } from '../base.js'
import type { AppConfigSchema } from '../types/misc/app-config.js'
export class AppConfigManager {
constructor(private client: BaseTelegramClient) {}
private _reloadable = new Reloadable<tl.help.RawAppConfig>({
reload: this._reload.bind(this),
getExpiresAt: () => 3_600_000,
disableAutoReload: true,
})
private async _reload(old?: tl.help.RawAppConfig) {
private _resource
constructor(private client: BaseTelegramClient) {
this._resource = new AsyncResource<tl.help.RawAppConfig>({
fetcher: async ({ current }) => {
const res = await this.client.call({
_: 'help.getAppConfig',
hash: old?.hash ?? 0,
hash: current?.hash ?? 0,
})
if (res._ === 'help.appConfigNotModified') return old!
if (res._ === 'help.appConfigNotModified') {
return {
data: asNonNull(current),
expiresIn: 3_600_000,
}
}
return res
return {
data: res,
expiresIn: 3_600_000,
}
},
})
}
private _object?: AppConfigSchema
async get(): Promise<AppConfigSchema> {
if (!this._reloadable.isStale && this._object) return this._object
if (!this._resource.isStale && this._object) return this._object
const obj = tlJsonToJson((await this._reloadable.get()).config)
const obj = tlJsonToJson((await this._resource.get()).config)
if (!obj || typeof obj !== 'object') {
throw new MtTypeAssertionError('appConfig', 'object', typeof obj)

View file

@ -67,6 +67,7 @@ function _initializeClient(this: TelegramClient, opts: TelegramClientOptions) {
Object.defineProperty(this, 'onServerUpdate', { value: this._client.onServerUpdate })
Object.defineProperty(this, 'onRawUpdate', { value: this._client.onServerUpdate })
Object.defineProperty(this, 'onConnectionState', { value: this._client.onConnectionState })
Object.defineProperty(this, 'onError', { value: this._client.onError })
if (!opts.disableUpdates) {
const skipConversationUpdates = opts.skipConversationUpdates ?? true

View file

@ -1,3 +1,5 @@
import { unknownToError } from '@fuman/utils'
import type { ITelegramClient } from '../../client.types.js'
import type { User } from '../../types/index.js'
@ -22,5 +24,5 @@ export function run(
): void {
start(client, params)
.then(then)
.catch(err => client.emitError(err))
.catch(err => client.onError.emit(unknownToError(err)))
}

View file

@ -4,10 +4,11 @@ import { Deferred, base64 } from '@fuman/utils'
import type { MaybePromise } from '../../../types/utils.js'
import { sleepWithAbort } from '../../../utils/misc-utils.js'
import { assertTypeIs } from '../../../utils/type-assertions.js'
import type { ITelegramClient, ServerUpdateHandler } from '../../client.types.js'
import type { ITelegramClient } from '../../client.types.js'
import type { MaybeDynamic } from '../../types/index.js'
import { User } from '../../types/index.js'
import { resolveMaybeDynamic } from '../../utils/misc-utils.js'
import type { RawUpdateInfo } from '../../updates/types.js'
import { checkPassword } from './check-password.js'
@ -53,26 +54,19 @@ export async function signInQr(
let waiter: Deferred<void> | undefined
// crutch we need to wait for the updateLoginToken update.
// we replace the server update handler temporarily because:
// - updates manager may be disabled, in which case `onUpdate` will never be called
// - even if the updates manager is enabled, it won't start until we're logged in
//
// todo: how can we make this more clean?
const originalHandler = client.getServerUpdateHandler()
const onUpdate: ServerUpdateHandler = (upd) => {
if (upd._ === 'updateShort' && upd.update._ === 'updateLoginToken') {
// todo: we should probably make this into an await-able function
const onUpdate = ({ update }: RawUpdateInfo) => {
if (update._ === 'updateLoginToken') {
onQrScanned?.()
waiter?.resolve()
client.onServerUpdate(originalHandler)
client.onRawUpdate.remove(onUpdate)
}
}
client.onServerUpdate(onUpdate)
client.onRawUpdate.add(onUpdate)
abortSignal?.addEventListener('abort', () => {
client.onServerUpdate(originalHandler)
client.onRawUpdate.remove(onUpdate)
waiter?.reject(abortSignal.reason)
})
@ -180,6 +174,6 @@ export async function signInQr(
return new User(self)
} finally {
client.onServerUpdate(originalHandler)
client.onRawUpdate.remove(onUpdate)
}
}

View file

@ -1,6 +1,6 @@
import type { tl } from '@mtcute/tl'
import type { IReadable } from '@fuman/io'
import { read } from '@fuman/io'
import { read, webReadableToFuman } from '@fuman/io'
import { AsyncLock } from '@fuman/utils'
import { MtArgumentError } from '../../../types/errors.js'
@ -175,7 +175,7 @@ export async function uploadFile(
}
if (file instanceof ReadableStream) {
file = read.async.fromWeb(file)
file = webReadableToFuman(file)
} else if (!(typeof file === 'object' && 'read' in file)) { // IReadable
throw new MtArgumentError('Could not convert input `file` to stream!')
}

View file

@ -1,5 +1,6 @@
import type { tl } from '@mtcute/tl'
import { TlBinaryReader, TlBinaryWriter, TlSerializationCounter } from '@mtcute/tl-runtime'
import { u8 } from '@fuman/utils'
import type { IKeyValueRepository } from '../../../storage/repository/key-value.js'
import type { ServiceOptions } from '../../../storage/service/base.js'
@ -18,7 +19,7 @@ export interface CurrentUserInfo {
const KV_CURRENT_USER = 'current_user'
function serialize(info: CurrentUserInfo | null): Uint8Array {
if (!info) return new Uint8Array(0)
if (!info) return u8.alloc(0)
const hasUsernames = info.usernames.length > 0

View file

@ -1,4 +1,4 @@
import { typed } from '@fuman/utils'
import { typed, u8 } from '@fuman/utils'
import type { IKeyValueRepository } from '../../../storage/repository/key-value.js'
import type { ServiceOptions } from '../../../storage/service/base.js'
@ -28,7 +28,7 @@ export class UpdatesStateService extends BaseService {
}
private async _setInt(key: string, val: number): Promise<void> {
const buf = new Uint8Array(4)
const buf = u8.alloc(4)
typed.toDataView(buf).setInt32(0, val, true)
await this._kv.set(key, buf)
@ -73,7 +73,7 @@ export class UpdatesStateService extends BaseService {
}
async setChannelPts(channelId: number, pts: number): Promise<void> {
const buf = new Uint8Array(4)
const buf = u8.alloc(4)
typed.toDataView(buf).setUint32(0, pts, true)
await this._kv.set(KV_CHANNEL_PREFIX + channelId, buf)

View file

@ -1,5 +1,5 @@
import type { tl } from '@mtcute/tl'
import { AsyncLock, Deferred, Deque, timers } from '@fuman/utils'
import { AsyncLock, Deferred, Deque, timers, unknownToError } from '@fuman/utils'
import { MtArgumentError, MtTimeoutError } from '../../types/errors.js'
import type { MaybePromise } from '../../types/utils.js'
@ -564,7 +564,7 @@ export class Conversation {
this._queuedNewMessage.popFront()
}
} catch (e: unknown) {
this.client.emitError(e)
this.client.onError.emit(unknownToError(e))
}
this._lastMessage = this._lastReceivedMessage = msg.id
@ -594,7 +594,7 @@ export class Conversation {
this._pendingEditMessage.delete(msg.id)
}
})().catch((e) => {
this.client.emitError(e)
this.client.onError.emit(unknownToError(e))
})
}

View file

@ -1,6 +1,6 @@
import { tl } from '@mtcute/tl'
import Long from 'long'
import { AsyncLock, ConditionVariable, Deque, timers } from '@fuman/utils'
import { AsyncLock, ConditionVariable, Deque, timers, unknownToError } from '@fuman/utils'
import { MtArgumentError } from '../../types/errors.js'
import type { MaybePromise } from '../../types/utils.js'
@ -195,7 +195,7 @@ export class UpdatesManager {
notifyLoggedIn(self: CurrentUserInfo): void {
this.auth = self
this.startLoop().catch(err => this.client.emitError(err))
this.startLoop().catch(err => this.client.onError.emit(unknownToError(err)))
}
notifyLoggedOut(): void {
@ -238,7 +238,7 @@ export class UpdatesManager {
this.updatesLoopActive = true
timers.clearInterval(this.keepAliveInterval)
this.keepAliveInterval = timers.setInterval(this._onKeepAlive, KEEP_ALIVE_INTERVAL)
this._loop().catch(err => this.client.emitError(err))
this._loop().catch(err => this.client.onError.emit(unknownToError(err)))
if (this.catchUpOnStart) {
this.catchUp()
@ -1245,20 +1245,20 @@ export class UpdatesManager {
// we just needed to apply new pts values
return
case 'updateDcOptions': {
const config = client.mt.network.config.getNow()
const config = client.mt.network.config.getCached()
if (config) {
client.mt.network.config.setData({
...config,
dcOptions: upd.dcOptions,
})
}, config.expires * 1000)
} else {
client.mt.network.config.update(true).catch(err => client.emitError(err))
client.mt.network.config.update(true).catch(err => client.onError.emit(unknownToError(err)))
}
break
}
case 'updateConfig':
client.mt.network.config.update(true).catch(err => client.emitError(err))
client.mt.network.config.update(true).catch(err => client.onError.emit(unknownToError(err)))
break
case 'updateUserName':
// todo

View file

@ -1,4 +1,4 @@
import { typed } from '@fuman/utils'
import { typed, u8 } from '@fuman/utils'
/**
* Decode 5-bit encoded voice message waveform into
@ -49,7 +49,7 @@ export function decodeWaveform(wf: Uint8Array): number[] {
export function encodeWaveform(wf: number[]): Uint8Array {
const bitsCount = wf.length * 5
const bytesCount = ~~((bitsCount + 7) / 8)
const result = new Uint8Array(bytesCount + 1)
const result = u8.alloc(bytesCount + 1)
const dv = typed.toDataView(result)
// Write each 0-31 unsigned char as 5 bit to result.

View file

@ -1,18 +1,20 @@
import type { tl } from '@mtcute/tl'
import { Emitter } from '@fuman/utils'
import type { RpcCallOptions } from '../../network/network-manager.js'
import type { MustEqual } from '../../types/utils.js'
import { LogManager } from '../../utils/logger.js'
import type { ConnectionState, ITelegramClient, ServerUpdateHandler } from '../client.types.js'
import type { ConnectionState, ITelegramClient } from '../client.types.js'
import { PeersIndex } from '../types/peers/peers-index.js'
import type { RawUpdateHandler } from '../updates/types.js'
import type { ICorePlatform } from '../../types/platform'
import type { RawUpdateInfo } from '../updates/types.js'
import { AppConfigManagerProxy } from './app-config.js'
import { WorkerInvoker } from './invoker.js'
import type { ClientMessageHandler, SendFn, SomeWorker, WorkerCustomMethods } from './protocol.js'
import { deserializeResult } from './protocol.js'
import { TelegramStorageProxy } from './storage.js'
import { deserializeError } from './errors.js'
export interface TelegramWorkerPortOptions {
worker: SomeWorker
@ -104,33 +106,10 @@ export abstract class TelegramWorkerPort<Custom extends WorkerCustomMethods> imp
abstract connectToWorker(worker: SomeWorker, handler: ClientMessageHandler): [SendFn, () => void]
private _serverUpdatesHandler: ServerUpdateHandler = () => {}
onServerUpdate(handler: ServerUpdateHandler): void {
this._serverUpdatesHandler = handler
}
getServerUpdateHandler(): ServerUpdateHandler {
return this._serverUpdatesHandler
}
private _errorHandler: (err: unknown) => void = () => {}
onError(handler: (err: unknown) => void): void {
this._errorHandler = handler
}
emitError(err: unknown): void {
this._errorHandler(err)
}
private _updateHandler: RawUpdateHandler = () => {}
onUpdate(handler: RawUpdateHandler): void {
this._updateHandler = handler
}
private _connectionStateHandler: (state: ConnectionState) => void = () => {}
onConnectionState(handler: (state: ConnectionState) => void): void {
this._connectionStateHandler = handler
}
onServerUpdate: Emitter<tl.TypeUpdates> = new Emitter()
onRawUpdate: Emitter<RawUpdateInfo> = new Emitter()
onConnectionState: Emitter<ConnectionState> = new Emitter()
onError: Emitter<Error> = new Emitter()
private _onMessage: ClientMessageHandler = (message) => {
switch (message.type) {
@ -138,22 +117,22 @@ export abstract class TelegramWorkerPort<Custom extends WorkerCustomMethods> imp
this.log.handler(message.color, message.level, message.tag, message.fmt, message.args)
break
case 'server_update':
this._serverUpdatesHandler(deserializeResult(message.update))
this.onServerUpdate.emit(deserializeResult(message.update))
break
case 'conn_state':
this._connectionStateHandler(message.state)
this.onConnectionState.emit(message.state)
break
case 'update': {
const peers = new PeersIndex(deserializeResult(message.users), deserializeResult(message.chats))
peers.hasMin = message.hasMin
this._updateHandler(deserializeResult(message.update), peers)
this.onRawUpdate.emit({ update: deserializeResult(message.update), peers })
break
}
case 'result':
this._invoker.handleResult(message)
break
case 'error':
this.emitError(message.error)
this.onError.emit(deserializeError(message.error))
break
case 'stop':
this._abortController.abort()

View file

@ -31,7 +31,7 @@ export type WorkerOutboundMessage =
chats: SerializedResult<Map<number, tl.TypeChat>>
hasMin: boolean
}
| { type: 'error', error: unknown }
| { type: 'error', error: SerializedError }
| { type: 'stop' }
| { type: 'conn_state', state: ConnectionState }
| {

View file

@ -55,13 +55,13 @@ export abstract class TelegramWorker<T extends WorkerCustomMethods> {
fmt,
args,
})
client.onError(err =>
client.onError.add(err =>
this.broadcast({
type: 'error',
error: err,
error: serializeError(err),
}),
)
client.onConnectionState(state =>
client.onConnectionState.add(state =>
this.broadcast({
type: 'conn_state',
state,
@ -70,7 +70,7 @@ export abstract class TelegramWorker<T extends WorkerCustomMethods> {
client.stopSignal.addEventListener('abort', () => this.broadcast({ type: 'stop' }))
if (client.updates) {
client.onUpdate((update, peers) =>
client.onRawUpdate.add(({ update, peers }) =>
this.broadcast({
type: 'update',
update: serializeResult(update),
@ -80,7 +80,7 @@ export abstract class TelegramWorker<T extends WorkerCustomMethods> {
}),
)
} else {
client.onServerUpdate(update =>
client.onServerUpdate.add(update =>
this.broadcast({
type: 'server_update',
update: serializeResult(update),

View file

@ -44,7 +44,7 @@ export class AuthKey {
let padding = (16 /* header size */ + message.length + 12) /* min padding */ % 16
padding = 12 + (padding ? 16 - padding : 0)
const buf = new Uint8Array(16 + message.length + padding)
const buf = u8.alloc(16 + message.length + padding)
const dv = typed.toDataView(buf)
dv.setInt32(0, serverSalt.low, true)

View file

@ -128,13 +128,13 @@ function rsaPad(data: Uint8Array, crypto: ICryptoProvider, key: TlPublicKey): Ui
throw new MtArgumentError('Failed to pad: too big data')
}
const dataPadded = new Uint8Array(192)
const dataPadded = u8.alloc(192)
dataPadded.set(data, 0)
crypto.randomFill(dataPadded.subarray(data.length))
data = dataPadded
for (;;) {
const aesIv = new Uint8Array(32)
const aesIv = u8.alloc(32)
const aesKey = crypto.randomBytes(32)
@ -417,7 +417,7 @@ export async function doAuthorization(
}
if (dhGen._ === 'mt_dh_gen_retry') {
const expectedHash = crypto.sha1(u8.concat3(newNonce, new Uint8Array([2]), authKeyAuxHash))
const expectedHash = crypto.sha1(u8.concat3(newNonce, [2], authKeyAuxHash))
if (!typed.equal(expectedHash.subarray(4, 20), dhGen.newNonceHash2)) {
throw new MtSecurityError('Step 4: invalid retry nonce hash from server')

View file

@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createStub } from '@mtcute/test'
import type { tl } from '@mtcute/tl'
import type { AsyncResourceContext } from '@fuman/utils'
import { ConfigManager } from './config-manager.js'
@ -9,13 +10,20 @@ describe('ConfigManager', () => {
expires: 300,
})
const getConfig = vi.fn()
const fakePerfNow = vi.fn(() => Date.now())
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(0)
vi.stubGlobal('performance', {
now: fakePerfNow,
})
getConfig.mockClear().mockImplementation(() => Promise.resolve(config))
})
afterEach(() => void vi.useRealTimers())
afterEach(() => {
vi.useRealTimers()
vi.unstubAllGlobals()
})
it('should fetch initial config', async () => {
const cm = new ConfigManager(getConfig)
@ -24,7 +32,7 @@ describe('ConfigManager', () => {
expect(getConfig).toHaveBeenCalledTimes(1)
expect(fetchedConfig).toEqual(config)
expect(cm.getNow()).toEqual(config)
expect(cm.getCached()).toEqual(config)
})
it('should automatically update config', async () => {
@ -47,7 +55,7 @@ describe('ConfigManager', () => {
const cm = new ConfigManager(getConfig)
expect(cm.isStale).toBe(true)
cm.setData(config)
cm.setData(config, config.expires * 1000)
expect(cm.isStale).toBe(false)
vi.setSystemTime(300_000)
@ -84,15 +92,23 @@ describe('ConfigManager', () => {
it('should call listeners on config update', async () => {
const cm = new ConfigManager(getConfig)
const listener = vi.fn()
cm.onReload(listener)
cm.onUpdated.add(listener)
await cm.update()
const call = structuredClone(listener.mock.calls[0][0]) as AsyncResourceContext<tl.RawConfig>
vi.setSystemTime(300_000)
cm.onReload(listener)
cm.onUpdated.remove(listener)
await cm.update()
expect(listener).toHaveBeenCalledOnce()
expect(listener).toHaveBeenCalledWith(config)
expect(call).toEqual({
abort: {},
current: config,
currentExpiresAt: 300_000,
currentFetchedAt: 0,
isBackground: false,
})
})
it('should correctly destroy', async () => {

View file

@ -1,18 +1,24 @@
import { AsyncResource } from '@fuman/utils'
import type { tl } from '@mtcute/tl'
import { Reloadable } from '../utils/reloadable.js'
/**
* Config manager is responsible for keeping
* the current server configuration up-to-date
* and providing methods to find the best DC
* option for the current session.
*/
export class ConfigManager extends Reloadable<tl.RawConfig> {
export class ConfigManager extends AsyncResource<tl.RawConfig> {
constructor(update: () => Promise<tl.RawConfig>) {
super({
reload: update,
getExpiresAt: data => data.expires * 1000,
fetcher: async () => {
const res = await update()
return {
data: res,
expiresIn: res.expires * 1000 - Date.now(),
}
},
autoReload: true,
})
}
@ -26,7 +32,9 @@ export class ConfigManager extends Reloadable<tl.RawConfig> {
}): Promise<tl.RawDcOption | undefined> {
if (this.isStale) await this.update()
const options = this._data!.dcOptions.filter((opt) => {
const data = this.getCached()!
const options = data.dcOptions.filter((opt) => {
if (opt.tcpoOnly) return false // unsupported
if (opt.ipv6 && !params.allowIpv6) return false
if (opt.mediaOnly && !params.allowMedia) return false

View file

@ -1,6 +1,6 @@
import type { tl } from '@mtcute/tl'
import type { Middleware } from '@fuman/utils'
import type { Middleware } from '../../utils/composer.js'
import type { RpcCallMiddleware, RpcCallMiddlewareContext } from '../network-manager.js'
/**

View file

@ -2,7 +2,7 @@ import Long from 'long'
import type { mtp, tl } from '@mtcute/tl'
import type { TlBinaryWriter, TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime'
import { TlSerializationCounter } from '@mtcute/tl-runtime'
import { type Deferred, Deque, LruSet } from '@fuman/utils'
import { type Deferred, Deque, LruSet, timers } from '@fuman/utils'
import { MtcuteError } from '../types/index.js'
import type {
@ -16,7 +16,6 @@ import {
compareLongs,
getRandomInt,
randomLong,
timers,
} from '../utils/index.js'
import { AuthKey } from './auth-key.js'

View file

@ -47,32 +47,32 @@ function _isQuadraticResidue(a: bigint): boolean {
}
function executeTlsOperations(h: TlsHelloWriter): void {
h.string(new Uint8Array([0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xFC, 0x03, 0x03]))
h.string([0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xFC, 0x03, 0x03])
h.zero(32)
h.string(new Uint8Array([0x20]))
h.string([0x20])
h.random(32)
h.string(new Uint8Array([0x00, 0x20]))
h.string([0x00, 0x20])
h.grease(0)
/* eslint-disable antfu/consistent-list-newline */
h.string(new Uint8Array([
h.string([
0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0xC0, 0x2B, 0xC0, 0x2F, 0xC0, 0x2C,
0xC0, 0x30, 0xCC, 0xA9, 0xCC, 0xA8, 0xC0, 0x13, 0xC0, 0x14, 0x00, 0x9C,
0x00, 0x9D, 0x00, 0x2F, 0x00, 0x35, 0x01, 0x00, 0x01, 0x93,
]))
])
h.grease(2)
h.string(new Uint8Array([0x00, 0x00, 0x00, 0x00]))
h.string([0x00, 0x00, 0x00, 0x00])
h.beginScope()
h.beginScope()
h.string(new Uint8Array([0x00]))
h.string([0x00])
h.beginScope()
h.domain()
h.endScope()
h.endScope()
h.endScope()
h.string(new Uint8Array([0x00, 0x17, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x08]))
h.string([0x00, 0x17, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x08])
h.grease(4)
h.string(
new Uint8Array([
[
0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, 0x00, 0x0B, 0x00, 0x02, 0x01, 0x00,
0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0E, 0x00, 0x0C, 0x02, 0x68,
0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2F, 0x31, 0x2E, 0x31, 0x00, 0x05,
@ -80,16 +80,16 @@ function executeTlsOperations(h: TlsHelloWriter): void {
0x10, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05,
0x01, 0x08, 0x06, 0x06, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x33, 0x00,
0x2B, 0x00, 0x29,
]),
],
)
h.grease(4)
h.string(new Uint8Array([0x00, 0x01, 0x00, 0x00, 0x1D, 0x00, 0x20]))
h.string([0x00, 0x01, 0x00, 0x00, 0x1D, 0x00, 0x20])
h.key()
h.string(new Uint8Array([0x00, 0x2D, 0x00, 0x02, 0x01, 0x01, 0x00, 0x2B, 0x00, 0x0B, 0x0A]))
h.string([0x00, 0x2D, 0x00, 0x02, 0x01, 0x01, 0x00, 0x2B, 0x00, 0x0B, 0x0A])
h.grease(6)
h.string(new Uint8Array([0x03, 0x04, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, 0x1B, 0x00, 0x03, 0x02, 0x00, 0x02]))
h.string([0x03, 0x04, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, 0x1B, 0x00, 0x03, 0x02, 0x00, 0x02])
h.grease(3)
h.string(new Uint8Array([0x00, 0x01, 0x00, 0x00, 0x15]))
h.string([0x00, 0x01, 0x00, 0x00, 0x15])
/* eslint-enable */
}
@ -124,12 +124,12 @@ class TlsHelloWriter {
domain: Uint8Array,
) {
this._domain = domain
this.buf = new Uint8Array(size)
this.buf = u8.alloc(size)
this.dv = typed.toDataView(this.buf)
this._grease = initGrease(this.crypto, 7)
}
string(buf: Uint8Array) {
string(buf: ArrayLike<number>) {
this.buf.set(buf, this.pos)
this.pos += buf.length
}

View file

@ -167,7 +167,7 @@ export abstract class BaseMtProxyTransport implements TelegramTransport {
u8.concat([
helloRand,
respBuf.slice(0, 11),
new Uint8Array(32),
u8.alloc(32),
respBuf.slice(11 + 32),
]),
this._rawSecret,

View file

@ -1,5 +1,5 @@
import type { mtp, tl } from '@mtcute/tl'
import { Deferred, Emitter } from '@fuman/utils'
import { Deferred, Emitter, unknownToError } from '@fuman/utils'
import type { Logger } from '../utils/index.js'
@ -151,7 +151,7 @@ export class MultiSessionConnection {
})
})
.catch((err) => {
this.onError.emit(err)
this.onError.emit(unknownToError(err))
})
}
@ -342,8 +342,8 @@ export class MultiSessionConnection {
}
}
changeTransport(factory: TelegramTransport): void {
this._connections.forEach(conn => conn.changeTransport(factory))
async changeTransport(factory: TelegramTransport): Promise<void> {
await Promise.all(this._connections.map(conn => conn.changeTransport(factory)))
}
getPoolSize(): number {

View file

@ -2,12 +2,11 @@ import type { mtp, tl } from '@mtcute/tl'
import type { TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime'
import type Long from 'long'
import { type ReconnectionStrategy, defaultReconnectionStrategy } from '@fuman/net'
import { Deferred } from '@fuman/utils'
import type { AsyncResourceContext, ComposedMiddleware, Middleware } from '@fuman/utils'
import { Deferred, composeMiddlewares } from '@fuman/utils'
import type { StorageManager } from '../storage/storage.js'
import { MtArgumentError, MtUnsupportedError, MtcuteError } from '../types/index.js'
import type { ComposedMiddleware, Middleware } from '../utils/composer.js'
import { composeMiddlewares } from '../utils/composer.js'
import type { DcOptions, ICryptoProvider, Logger } from '../utils/index.js'
import { assertTypeIs, isTlRpcError } from '../utils/type-assertions.js'
import type { ICorePlatform } from '../types/platform'
@ -279,7 +278,7 @@ export class DcConnectionManager {
mainCount = this._mainCountOverride
if (mainCount === 0) {
mainCount = this.manager.config.getNow()?.tmpSessions ?? 1
mainCount = this.manager.config.getCached()?.tmpSessions ?? 1
}
} else {
mainCount = 1
@ -519,7 +518,7 @@ export class NetworkManager {
this.call = this._composeCall(params.middlewares)
this._onConfigChanged = this._onConfigChanged.bind(this)
config.onReload(this._onConfigChanged)
config.onUpdated.add(this._onConfigChanged)
this._log = params.log.create('network')
this._storage = params.storage
@ -749,9 +748,9 @@ export class NetworkManager {
dc.downloadSmall.resetSessions()
}
private _onConfigChanged(config: tl.RawConfig): void {
if (config.tmpSessions) {
this._primaryDc?.setMainConnectionCount(config.tmpSessions)
private _onConfigChanged({ current }: AsyncResourceContext<tl.RawConfig>): void {
if (current?.tmpSessions) {
this._primaryDc?.setMainConnectionCount(current.tmpSessions)
}
}
@ -866,12 +865,12 @@ export class NetworkManager {
return res
}
changeTransport(transport: TelegramTransport): void {
async changeTransport(transport: TelegramTransport): Promise<void> {
for (const dc of this._dcConnections.values()) {
dc.main.changeTransport(transport)
dc.upload.changeTransport(transport)
dc.download.changeTransport(transport)
dc.downloadSmall.changeTransport(transport)
await dc.main.changeTransport(transport)
await dc.upload.changeTransport(transport)
await dc.download.changeTransport(transport)
await dc.downloadSmall.changeTransport(transport)
}
}
@ -902,7 +901,7 @@ export class NetworkManager {
await dc.destroy()
}
this._dcConnections.clear()
this.config.offReload(this._onConfigChanged)
this.config.onUpdated.remove(this._onConfigChanged)
this._resetOnNetworkChange?.()
}

View file

@ -3,7 +3,7 @@ import type { mtp } from '@mtcute/tl'
import { tl } from '@mtcute/tl'
import type { TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime'
import { TlBinaryReader, TlBinaryWriter, TlSerializationCounter } from '@mtcute/tl-runtime'
import { Deferred, Emitter, u8 } from '@fuman/utils'
import { Deferred, Emitter, timers, u8 } from '@fuman/utils'
import { MtArgumentError, MtTimeoutError, MtcuteError } from '../types/index.js'
import { createAesIgeForMessageOld } from '../utils/crypto/mtproto.js'
@ -13,7 +13,6 @@ import {
longFromBuffer,
randomLong,
removeFromLongArray,
timers,
} from '../utils/index.js'
import type { ICorePlatform } from '../types/platform'

View file

@ -4,8 +4,8 @@ import { defaultTestCryptoProvider, useFakeMathRandom } from '@mtcute/test'
import { hex } from '@fuman/utils'
import { Bytes, write } from '@fuman/io'
import { IntermediatePacketCodec, PaddedIntermediatePacketCodec } from './intermediate'
import { TransportError } from './abstract'
import { IntermediatePacketCodec, PaddedIntermediatePacketCodec } from './intermediate.js'
import { TransportError } from './abstract.js'
describe('IntermediatePacketCodec', () => {
it('should return correct tag', () => {

View file

@ -179,8 +179,8 @@ describe('ObfuscatedPacketCodec', () => {
await codec.tag()
expect(codec.decode(Bytes.from(hex.decode(msg1)), false)).rejects.toThrow(TransportError)
expect(codec.decode(Bytes.from(hex.decode(msg2)), false)).rejects.toThrow(TransportError)
await expect(codec.decode(Bytes.from(hex.decode(msg1)), false)).rejects.toThrow(TransportError)
await expect(codec.decode(Bytes.from(hex.decode(msg2)), false)).rejects.toThrow(TransportError)
})
it('should correctly reset', async () => {

View file

@ -1,10 +1,9 @@
export type MaybePromise<T> = T | Promise<T>
export type { MaybeArray, MaybePromise } from '@fuman/utils'
export type PartialExcept<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>
export type PartialOnly<T, K extends keyof T> = Partial<Pick<T, K>> & Omit<T, K>
export type AnyToNever<T> = any extends T ? never : T
export type MaybeArray<T> = T | T[]
export type MustEqual<T, V> = (() => T) extends () => V ? ((() => V) extends () => T ? T : V) : V
export type PublicPart<T> = { [K in keyof T]: T[K] }

View file

@ -1,3 +1,5 @@
import { u8 } from '@fuman/utils'
import type { MaybePromise } from '../../types/index.js'
import { factorizePQSync } from './factorization.js'
@ -50,7 +52,7 @@ export abstract class BaseCryptoProvider {
}
randomBytes(size: number): Uint8Array {
const buf = new Uint8Array(size)
const buf = u8.alloc(size)
this.randomFill(buf)
return buf

View file

@ -42,7 +42,7 @@ export async function computeNewPasswordHash(
): Promise<Uint8Array> {
assertTypeIs('account.getPassword', algo, 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow')
const salt1 = new Uint8Array(algo.salt1.length + 32)
const salt1 = u8.alloc(algo.salt1.length + 32)
salt1.set(algo.salt1)
crypto.randomFill(salt1.subarray(algo.salt1.length))
;(algo as tl.Mutable<typeof algo>).salt1 = salt1

View file

@ -6,7 +6,6 @@ import { timers } from '@fuman/utils'
*/
export class EarlyTimer {
private _timeout?: timers.Timer
private _immediate?: timers.Immediate
private _timeoutTs?: number
private _handler: () => void = () => {}
@ -20,13 +19,11 @@ export class EarlyTimer {
* (basically `setImmediate()`)
*/
emitWhenIdle(): void {
if (this._immediate) return
timers.clearTimeout(this._timeout)
this._timeoutTs = Date.now()
if (typeof timers.setImmediate !== 'undefined') {
this._immediate = timers.setImmediate(this.emitNow)
if (typeof queueMicrotask !== 'undefined') {
queueMicrotask(this.emitNow)
} else {
this._timeout = timers.setTimeout(this.emitNow, 0)
}
@ -68,12 +65,7 @@ export class EarlyTimer {
* Cancel the timer
*/
reset(): void {
if (this._immediate) {
timers.clearImmediate(this._immediate)
this._immediate = undefined
} else {
timers.clearTimeout(this._timeout)
}
this._timeoutTs = undefined
}

View file

@ -1,5 +1,3 @@
import * as timers from './timers.js'
export * from '../highlevel/utils/index.js'
// todo: remove after 1.0.0
export * from '../highlevel/storage/service/current-user.js'
@ -8,7 +6,6 @@ export * from '../storage/service/base.js'
export * from '../storage/service/default-dcs.js'
// end todo
export * from './bigint-utils.js'
export * from './composer.js'
export * from './crypto/index.js'
export * from './dcs.js'
export * from './early-timer.js'
@ -23,4 +20,3 @@ export * from './sorted-array.js'
export * from './tl-json.js'
export * from './type-assertions.js'
export * from '@mtcute/tl-runtime'
export { timers }

View file

@ -1,61 +1 @@
/* eslint-disable no-restricted-globals, ts/no-implied-eval */
// timers typings are mixed up across different runtimes, which leads
// to the globals being typed incorrectly.
// instead, we can treat the timers as opaque objects, and expose
// them through the `timers` esm namespace.
// this has near-zero runtime cost, but makes everything type-safe
//
// NB: we are using wrapper functions instead of...
// - directly exposing the globals because the standard doesn't allow that
// - .bind()-ing because it makes it harder to mock the timer globals
export interface Timer { readonly __type: 'Timer' }
export interface Interval { readonly __type: 'Interval' }
export interface Immediate { readonly __type: 'Immediate' }
const setTimeoutWrap = (
(...args: Parameters<typeof setTimeout>) => setTimeout(...args)
) as unknown as <T extends (...args: any[]) => any>(
fn: T, ms: number, ...args: Parameters<T>
) => Timer
const setIntervalWrap = (
(...args: Parameters<typeof setInterval>) => setInterval(...args)
) as unknown as <T extends (...args: any[]) => any>(
fn: T, ms: number, ...args: Parameters<T>
) => Interval
let setImmediateWrap: any
if (typeof setImmediate !== 'undefined') {
setImmediateWrap = (...args: Parameters<typeof setImmediate>) => setImmediate(...args)
} else {
// eslint-disable-next-line
setImmediateWrap = (fn: (...args: any[]) => void, ...args: any[]) => setTimeout(fn, 0, ...args)
}
const setImmediateWrapExported = setImmediateWrap as <T extends (...args: any[]) => any>(
fn: T, ...args: Parameters<T>
) => Immediate
const clearTimeoutWrap = (
(...args: Parameters<typeof clearTimeout>) => clearTimeout(...args)
) as unknown as (timer?: Timer) => void
const clearIntervalWrap = (
(...args: Parameters<typeof clearInterval>) => clearInterval(...args)
) as unknown as (timer?: Interval) => void
let clearImmediateWrap: any
if (typeof clearImmediate !== 'undefined') {
clearImmediateWrap = (...args: Parameters<typeof clearImmediate>) => clearImmediate(...args)
} else {
clearImmediateWrap = (timer: number) => clearTimeout(timer)
}
const clearImmediateWrapExported = clearImmediateWrap as (timer?: Immediate) => void
export {
setTimeoutWrap as setTimeout,
setIntervalWrap as setInterval,
setImmediateWrapExported as setImmediate,
clearTimeoutWrap as clearTimeout,
clearIntervalWrap as clearInterval,
clearImmediateWrapExported as clearImmediate,
}
export { timers } from '@fuman/utils'

View file

@ -1,9 +1,16 @@
import { resolve } from 'node:path'
import { cpSync } from 'node:fs'
/** @type {import('@fuman/build/vite').CustomBuildConfig} */
export default () => ({
buildCjs: false,
final({ outDir, packageDir }) {
viteConfig: {
build: {
lib: {
formats: ['es'],
},
},
},
finalize({ outDir, packageDir }) {
cpSync(resolve(packageDir, 'template'), resolve(outDir, 'template'), { recursive: true })
},
})

View file

@ -10,7 +10,6 @@
"create-bot": "./src/main.ts"
},
"scripts": {
"build": "pnpm run -w build-package create-bot",
"run": "tsx src/main.ts",
"run:deno": "node scripts/generate-import-map.js && deno run --import-map=./scripts/import-map.json -A --unstable-sloppy-imports src/main.ts"
},
@ -26,5 +25,8 @@
"@types/cross-spawn": "^6.0.6",
"@types/inquirer": "^9.0.6",
"@types/openurl": "^1.0.3"
},
"fuman": {
"jsr": "skip"
}
}

View file

@ -1,4 +1,4 @@
/* eslint-disable import/no-relative-packages, no-console, no-restricted-globals */
/* eslint-disable no-console, no-restricted-globals */
import { createHash } from 'node:crypto'
import path from 'node:path'
import * as fs from 'node:fs'
@ -155,8 +155,9 @@ async function extractArtifacts(artifacts) {
)
}
/** @type {import('@fuman/build/vite').CustomBuildConfig} */
export default () => ({
async final({ packageDir, outDir }) {
async finalize({ packageDir, outDir }) {
const libDir = path.resolve(packageDir, 'lib')
if (!SKIP_PREBUILT) {

View file

@ -12,19 +12,19 @@
"./native.js": "./src/native.cjs"
},
"scripts": {
"build": "pnpm run -w build-package crypto-node",
"install": "node-gyp-build",
"rebuild": "node-gyp configure && node-gyp -j 16 rebuild",
"clean": "node-gyp clean"
},
"keepScripts": [
"install"
],
"dependencies": {
"@mtcute/node": "workspace:^",
"node-gyp-build": "4.8.1"
},
"devDependencies": {
"@mtcute/test": "workspace:^"
},
"fuman": {
"jsr": "skip",
"keepScripts": ["install"]
}
}

View file

@ -1,7 +1,8 @@
import * as fs from 'node:fs'
/** @type {import('@fuman/build/vite').CustomBuildConfig} */
export default () => ({
finalJsr({ outDir }) {
finalizeJsr({ outDir }) {
// jsr doesn't support symlinks, so we need to copy the files manually
const real = fs.realpathSync(`${outDir}/common-internals-web`)
fs.unlinkSync(`${outDir}/common-internals-web`)

View file

@ -12,16 +12,11 @@
"./utils.js": "./src/utils.ts",
"./methods.js": "./src/methods.ts"
},
"main": "src/index.ts",
"scripts": {
"docs": "typedoc",
"build": "pnpm run -w build-package deno"
},
"dependencies": {
"@db/sqlite": "npm:@jsr/db__sqlite@0.12.0",
"@fuman/deno": "workspace:^",
"@fuman/net": "workspace:^",
"@fuman/io": "workspace:^",
"@fuman/utils": "0.0.1",
"@fuman/net": "0.0.1",
"@fuman/io": "0.0.1",
"@mtcute/core": "workspace:^",
"@mtcute/html-parser": "workspace:^",
"@mtcute/markdown-parser": "workspace:^",
@ -30,5 +25,8 @@
},
"devDependencies": {
"@mtcute/test": "workspace:^"
},
"fuman": {
"jsr": "only"
}
}

View file

@ -11,6 +11,7 @@ import {
BaseTelegramClient as BaseTelegramClientBase,
TelegramClient as TelegramClientBase,
} from '@mtcute/core/client.js'
import { unknownToError } from '@fuman/utils'
import { downloadToFile } from './methods/download-file.js'
import { DenoPlatform } from './platform.js'
@ -128,7 +129,7 @@ export class TelegramClient extends TelegramClientBase {
this.start(params)
.then(then)
.catch(err => this.emitError(err))
.catch(err => this.onError.emit(unknownToError(err)))
}
downloadToFile(

View file

@ -1,5 +1,6 @@
import type { HttpProxySettings as FumanHttpProxySettings, ITcpConnection, SocksProxySettings, TcpEndpoint } from '@fuman/net'
import { performHttpProxyHandshake, performSocksHandshake } from '@fuman/net'
// @ts-expect-error wip
import { connectTcp, connectTls } from '@fuman/deno'
import { BaseMtProxyTransport, type ITelegramConnection, IntermediatePacketCodec, type TelegramTransport } from '@mtcute/core'
import type { BasicDcOption } from '@mtcute/core/utils.js'

View file

@ -1,3 +1,4 @@
// @ts-expect-error wip
import { connectTcp } from '@fuman/deno'
import type { ITcpConnection } from '@fuman/net'
import { IntermediatePacketCodec, type TelegramTransport } from '@mtcute/core'

View file

@ -9,12 +9,11 @@
"sideEffects": false,
"exports": "./src/index.ts",
"scripts": {
"build": "pnpm run -w build-package dispatcher",
"gen-updates": "node ./scripts/generate.cjs"
},
"dependencies": {
"@mtcute/core": "workspace:^",
"@fuman/utils": "workspace:^",
"@fuman/utils": "0.0.1",
"events": "3.2.0"
},
"devDependencies": {

View file

@ -29,6 +29,7 @@ import {
MtArgumentError,
} from '@mtcute/core'
import type { TelegramClient } from '@mtcute/core/client.js'
import { unknownToError } from '@fuman/utils'
import type { UpdateContext } from './context/base.js'
import type { BusinessMessageContext } from './context/business-message.js'
@ -328,7 +329,8 @@ export class Dispatcher<State extends object = never> {
// order does not matter in the dispatcher,
// so we can handle each update in its own task
this.dispatchRawUpdateNow(update, peers).catch(err => this._client!.emitError(err))
this.dispatchRawUpdateNow(update, peers)
.catch(err => this._client!.onError.emit(unknownToError(err)))
}
/**
@ -400,7 +402,8 @@ export class Dispatcher<State extends object = never> {
// order does not matter in the dispatcher,
// so we can handle each update in its own task
this.dispatchUpdateNow(update).catch(err => this._client!.emitError(err))
this.dispatchUpdateNow(update)
.catch(err => this._client!.onError.emit(unknownToError(err)))
}
/**

View file

@ -1,6 +1,6 @@
import { asyncResettable, timers } from '@mtcute/core/utils.js'
import { asyncResettable } from '@mtcute/core/utils.js'
import type { MaybePromise } from '@mtcute/core'
import { LruMap } from '@fuman/utils'
import { LruMap, timers } from '@fuman/utils'
import type { IStateStorageProvider } from './provider.js'

View file

@ -10,12 +10,9 @@
"exports": {
".": "./src/index.ts"
},
"scripts": {
"build": "pnpm run -w build-package file-id"
},
"dependencies": {
"@mtcute/tl-runtime": "workspace:^",
"@fuman/utils": "workspace:^",
"@fuman/utils": "0.0.1",
"long": "5.2.3"
}
}

View file

@ -1,5 +1,5 @@
import { TlBinaryWriter } from '@mtcute/tl-runtime'
import { base64, utf8 } from '@fuman/utils'
import { base64, u8, utf8 } from '@fuman/utils'
import { tdFileId as td } from './types.js'
import { assertNever, telegramRleEncode } from './utils.js'
@ -105,7 +105,7 @@ export function toFileId(location: Omit<td.RawFullRemoteFileLocation, '_'>): str
}
const result = telegramRleEncode(writer.result())
const withSuffix = new Uint8Array(result.length + SUFFIX.length)
const withSuffix = u8.alloc(result.length + SUFFIX.length)
withSuffix.set(result)
withSuffix.set(SUFFIX, result.length)

View file

@ -8,9 +8,6 @@
"license": "MIT",
"sideEffects": false,
"exports": "./src/index.ts",
"scripts": {
"build": "pnpm run -w build-package html-parser"
},
"dependencies": {
"@mtcute/core": "workspace:^",
"htmlparser2": "^6.0.1",

View file

@ -8,17 +8,8 @@
"license": "MIT",
"sideEffects": false,
"exports": "./src/index.ts",
"scripts": {
"build": "pnpm run -w build-package i18n"
},
"devDependencies": {
"@mtcute/core": "workspace:^",
"@mtcute/dispatcher": "workspace:^"
},
"jsrOnlyFields": {
"dependencies": {
"@mtcute/core": "workspace:^",
"@mtcute/dispatcher": "workspace:^"
}
}
}

View file

@ -8,9 +8,6 @@
"license": "MIT",
"sideEffects": false,
"exports": "./src/index.ts",
"scripts": {
"build": "pnpm run -w build-package markdown-parser"
},
"dependencies": {
"@mtcute/core": "workspace:^",
"long": "5.2.3"

View file

@ -1,12 +1,18 @@
import { fileURLToPath } from 'node:url'
/** @type {import('@fuman/build/vite').CustomBuildConfig} */
export default () => {
const clientId = fileURLToPath(new URL('./src/client.ts', import.meta.url))
// const buildingCjs = false
return {
viteConfig: {
build: {
rollupOptions: {
external: ['@mtcute/crypto-node'],
rollupPluginsPre: [
},
},
},
pluginsPre: [
{
// very much a crutch, but it works
// i couldn't figure out a way to hook into the esm->cjs transform,

View file

@ -12,21 +12,21 @@
"./utils.js": "./src/utils.ts",
"./methods.js": "./src/methods.ts"
},
"scripts": {
"docs": "typedoc",
"build": "pnpm run -w build-package node"
},
"dependencies": {
"@mtcute/core": "workspace:^",
"@mtcute/html-parser": "workspace:^",
"@mtcute/markdown-parser": "workspace:^",
"@mtcute/wasm": "workspace:^",
"@fuman/net": "workspace:^",
"@fuman/node": "workspace:^",
"@fuman/utils": "0.0.1",
"@fuman/net": "0.0.1",
"@fuman/node": "0.0.1",
"better-sqlite3": "11.3.0"
},
"devDependencies": {
"@mtcute/test": "workspace:^",
"@types/better-sqlite3": "7.6.4"
},
"fuman": {
"jsr": "skip"
}
}

View file

@ -11,6 +11,7 @@ import {
BaseTelegramClient as BaseTelegramClientBase,
TelegramClient as TelegramClientBase,
} from '@mtcute/core/client.js'
import { unknownToError } from '@fuman/utils'
import { downloadToFile } from './methods/download-file.js'
import { downloadAsNodeStream } from './methods/download-node-stream.js'
@ -139,7 +140,7 @@ export class TelegramClient extends TelegramClientBase {
this.start(params)
.then(then)
.catch(err => this.emitError(err))
.catch(err => this.onError.emit(unknownToError(err)))
}
downloadToFile(

View file

@ -2,7 +2,7 @@ import type { Readable } from 'node:stream'
import type { FileDownloadLocation, FileDownloadParameters, ITelegramClient } from '@mtcute/core'
import { downloadAsStream } from '@mtcute/core/methods.js'
import { webStreamToNode } from '@fuman/node'
import { webReadableToNode } from '@fuman/node'
/**
* Download a remote file as a Node.js Readable stream.
@ -14,5 +14,5 @@ export function downloadAsNodeStream(
location: FileDownloadLocation,
params?: FileDownloadParameters,
): Readable {
return webStreamToNode(downloadAsStream(client, location, params))
return webReadableToNode(downloadAsStream(client, location, params))
}

View file

@ -4,7 +4,7 @@ import { basename } from 'node:path'
import { Readable } from 'node:stream'
import type { UploadFileLike } from '@mtcute/core'
import { nodeStreamToWeb } from '@fuman/node'
import { nodeReadableToFuman } from '@fuman/node'
export async function normalizeFile(file: UploadFileLike): Promise<{
file: UploadFileLike
@ -20,7 +20,7 @@ export async function normalizeFile(file: UploadFileLike): Promise<{
const fileSize = await stat(file.path.toString()).then(stat => stat.size)
return {
file: nodeStreamToWeb(file),
file: nodeReadableToFuman(file),
fileName,
fileSize,
}
@ -28,7 +28,7 @@ export async function normalizeFile(file: UploadFileLike): Promise<{
if (file instanceof Readable) {
return {
file: nodeStreamToWeb(file),
file: nodeReadableToFuman(file),
}
}

View file

@ -1 +1,10 @@
export default () => ({ buildCjs: false })
/** @type {import('@fuman/build/vite').CustomBuildConfig} */
export default () => ({
viteConfig: {
build: {
lib: {
formats: ['es'],
},
},
},
})

View file

@ -8,9 +8,6 @@
"license": "MIT",
"sideEffects": false,
"exports": "./src/index.ts",
"scripts": {
"build": "pnpm run -w build-package test"
},
"peerDependencies": {
"@mtcute/core": "workspace:^",
"@mtcute/node": "workspace:^",
@ -28,13 +25,16 @@
},
"dependencies": {
"long": "5.2.3",
"@fuman/utils": "workspace:^",
"@fuman/net": "workspace:^"
"@fuman/utils": "0.0.1",
"@fuman/net": "0.0.1"
},
"devDependencies": {
"@mtcute/tl-utils": "workspace:^"
},
"browser": {
"./src/platform.js": "./src/platform.web.js"
},
"fuman": {
"jsr": "skip"
}
}

View file

@ -33,7 +33,7 @@ export class StubTelegramClient extends BaseTelegramClient {
onMessage: (data, dcId) => {
if (!this._onRawMessage) {
if (this._responders.size) {
this.emitError(new Error('Unexpected outgoing message'))
this.onError.emit(new Error('Unexpected outgoing message'))
}
return
@ -288,9 +288,11 @@ export class StubTelegramClient extends BaseTelegramClient {
let error: unknown
this.onError((err) => {
const handler = (err: Error) => {
error = err
})
}
this.onError.add(handler)
try {
await fn()
@ -300,6 +302,8 @@ export class StubTelegramClient extends BaseTelegramClient {
await this.close()
this.onError.remove(handler)
if (error) {
throw error
}

View file

@ -1,6 +1,7 @@
import Long from 'long'
import type { tl } from '@mtcute/tl'
import type { TlArgument } from '@mtcute/tl-utils'
import { u8 } from '@fuman/utils'
import { getEntriesMap } from './schema.js'
@ -21,13 +22,13 @@ function getDefaultFor(arg: TlArgument): unknown {
case 'long':
return Long.ZERO
case 'int128':
return new Uint8Array(16)
return u8.alloc(16)
case 'int256':
return new Uint8Array(32)
return u8.alloc(32)
case 'string':
return ''
case 'bytes':
return new Uint8Array(0)
return u8.alloc(0)
case 'Bool':
case 'bool':
return false

View file

@ -10,12 +10,8 @@
"exports": {
".": "./src/index.ts"
},
"scripts": {
"docs": "typedoc",
"build": "pnpm run -w build-package tl-runtime"
},
"dependencies": {
"long": "5.2.3",
"@fuman/utils": "workspace:^"
"@fuman/utils": "0.0.1"
}
}

View file

@ -11,10 +11,6 @@
".": "./src/index.ts",
"./json.js": "./src/json/index.ts"
},
"scripts": {
"docs": "typedoc",
"build": "pnpm run -w build-package tl-utils"
},
"dependencies": {
"@mtcute/tl-runtime": "workspace:^",
"crc-32": "1.2.0"

View file

@ -5,7 +5,7 @@ import { generateWriterCodeForTlEntries } from './codegen/writer.js'
import { parseTlToEntries } from './parse.js'
function evalForResult<T>(js: string): T {
// eslint-disable-next-line ts/no-implied-eval, no-new-func
// eslint-disable-next-line ts/no-implied-eval, no-new-func, ts/no-unsafe-call
return new Function(js)() as T
}

View file

@ -13,7 +13,8 @@
"gen-code": "tsx scripts/gen-code.ts",
"gen-rsa": "tsx scripts/gen-rsa-keys.ts",
"fetch-and-gen": "pnpm run fetch-api && pnpm run gen-code",
"build": "pnpm run -w build-package tl"
"build": "tsx scripts/build-package.ts",
"build:jsr": "JSR=1 tsx scripts/build-package.ts"
},
"dependencies": {
"long": "5.2.3"
@ -22,7 +23,7 @@
"@mtcute/core": "workspace:^",
"@mtcute/node": "workspace:^",
"@mtcute/tl-utils": "workspace:^",
"@fuman/utils": "workspace:^",
"@fuman/utils": "0.0.1",
"@types/js-yaml": "^4.0.5",
"cheerio": "1.0.0-rc.12",
"csv-parse": "^5.5.0",
@ -31,7 +32,7 @@
"typedoc": {
"entryPoint": "index.d.ts"
},
"jsrOnlyFields": {
"exports": {}
"fuman": {
"ownVersioning": true
}
}

View file

@ -0,0 +1,107 @@
import * as fsp from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { resolve } from 'node:path'
import { parsePackageJsonFile, processPackageJson } from '@fuman/build'
import { packageJsonToDeno } from '@fuman/build/jsr'
async function transformFile(file: string, transform: (content: string, file: string) => string) {
const content = await fsp.readFile(file, 'utf8')
const res = transform(content, file)
if (res != null) await fsp.writeFile(file, res)
}
// create package by copying all the needed files
const packageDir = fileURLToPath(new URL('../', import.meta.url))
const outDir = process.env.FUMAN_BUILD_OUT ?? fileURLToPath(new URL('../dist', import.meta.url))
await fsp.rm(outDir, { recursive: true, force: true })
const files = [
'binary/reader.d.ts',
'binary/reader.js',
'binary/rsa-keys.d.ts',
'binary/rsa-keys.js',
'binary/writer.d.ts',
'binary/writer.js',
'index.d.ts',
'index.js',
'raw-errors.json',
'mtp-schema.json',
'api-schema.json',
'app-config.json',
'README.md',
]
await fsp.mkdir(resolve(outDir, 'binary'), { recursive: true })
for (const f of files) {
await fsp.copyFile(resolve(packageDir, f), resolve(outDir, f))
}
await fsp.cp(new URL('../../../LICENSE', import.meta.url), resolve(outDir, 'LICENSE'), { recursive: true })
const { packageJson, packageJsonOrig } = processPackageJson({
packageJson: await parsePackageJsonFile(resolve(packageDir, 'package.json')),
workspaceVersions: {},
rootPackageJson: await parsePackageJsonFile(resolve(packageDir, '../../package.json')),
})
if (process.env.JSR) {
// jsr doesn't support cjs, so we'll need to add some shims
// todo: remove this god awfulness when tl esm rewrite
await transformFile(resolve(outDir, 'index.js'), (content) => {
return [
'/// <reference types="./index.d.ts" />',
'const exports = {};',
content,
'export const tl = exports.tl;',
'export const mtp = exports.mtp;',
].join('\n')
})
await transformFile(resolve(outDir, 'binary/reader.js'), (content) => {
return [
'/// <reference types="./reader.d.ts" />',
'const exports = {};',
content,
'export const __tlReaderMap = exports.__tlReaderMap;',
].join('\n')
})
await transformFile(resolve(outDir, 'binary/writer.js'), (content) => {
return [
'/// <reference types="./writer.d.ts" />',
'const exports = {};',
content,
'export const __tlWriterMap = exports.__tlWriterMap;',
].join('\n')
})
await transformFile(resolve(outDir, 'binary/rsa-keys.js'), (content) => {
return [
'/// <reference types="./rsa-keys.d.ts" />',
'const exports = {};',
content,
'export const __publicKeyIndex = exports.__publicKeyIndex;',
].join('\n')
})
// patch deno.json to add some export maps
const denoJson = packageJsonToDeno({
packageJson,
packageJsonOrig,
workspaceVersions: {},
buildDirName: 'dist',
})
denoJson.exports = {}
for (const f of files) {
if (!f.match(/\.js(?:on)?$/)) continue
if (f === 'index.js') {
denoJson.exports['.'] = './index.js'
} else {
denoJson.exports[`./${f}`] = `./${f}`
}
}
await fsp.writeFile(resolve(outDir, 'deno.json'), JSON.stringify(denoJson, null, 2))
} else {
await fsp.writeFile(resolve(outDir, 'package.json'), JSON.stringify(packageJson, null, 2))
}

View file

@ -1,11 +1,12 @@
import { resolve } from 'node:path'
import * as fs from 'node:fs'
/** @type {import('@fuman/build/vite').CustomBuildConfig} */
export default () => ({
finalPackageJson(pkg) {
pkg.exports['./mtcute.wasm'] = './mtcute.wasm'
finalizePackageJson({ packageJson }) {
packageJson.exports['./mtcute.wasm'] = './mtcute.wasm'
},
final({ packageDir, outDir }) {
finalize({ packageDir, outDir }) {
fs.cpSync(resolve(packageDir, 'src/mtcute.wasm'), resolve(outDir, 'mtcute.wasm'))
},
})

View file

@ -12,17 +12,12 @@
"./mtcute.wasm": "./src/mtcute.wasm"
},
"scripts": {
"docs": "typedoc",
"build": "pnpm run -w build-package wasm",
"build:wasm": "docker build --output=lib --target=binaries lib"
},
"devDependencies": {
"@mtcute/core": "workspace:^",
"@mtcute/node": "workspace:^",
"@mtcute/web": "workspace:^",
"@fuman/utils": "workspace:^"
},
"jsrOnlyFields": {
"exports": "./src/index.ts"
"@fuman/utils": "0.0.1"
}
}

View file

@ -51,7 +51,6 @@ export function initSync(module: SyncInitInput): void {
module = new WebAssembly.Instance(module)
}
// eslint-disable-next-line
wasm = (module as unknown as WebAssembly.Instance).exports as unknown as MtcuteWasmModule
initCommon()
}

View file

@ -12,14 +12,10 @@
"./utils.js": "./src/utils.ts",
"./methods.js": "./src/methods.ts"
},
"scripts": {
"docs": "typedoc",
"build": "pnpm run -w build-package web"
},
"dependencies": {
"@mtcute/core": "workspace:^",
"@mtcute/wasm": "workspace:^",
"@fuman/net": "workspace:^",
"@fuman/net": "0.0.1",
"events": "3.2.0"
},
"devDependencies": {

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,3 @@
packages:
- packages/*
- private/fuman/packages/*
- '!e2e/*'

View file

@ -1,210 +0,0 @@
import { fileURLToPath } from 'node:url'
import * as fs from 'node:fs'
import * as cp from 'node:child_process'
import { resolve } from 'node:path'
import { populateFromUpstream } from '@fuman/jsr'
import * as glob from 'glob'
import ts from 'typescript'
import { processPackageJson } from '../.config/vite-utils/package-json.js'
export function packageJsonToDeno({ packageJson, packageJsonOrig }) {
// https://jsr.io/docs/package-configuration
const importMap = {}
const exports = {}
if (packageJson.dependencies) {
for (const [name, version] of Object.entries(packageJson.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}`
}
}
}
if (packageJsonOrig.exports) {
let tmpExports
if (typeof packageJsonOrig.exports === 'string') {
tmpExports = { '.': packageJsonOrig.exports }
} else if (typeof packageJsonOrig.exports !== 'object') {
throw new TypeError('package.json exports must be an object')
} else {
tmpExports = packageJsonOrig.exports
}
for (const [name, value] of Object.entries(tmpExports)) {
if (typeof value !== 'string') {
throw new TypeError(`package.json exports value must be a string: ${name}`)
}
if (value.endsWith('.wasm')) continue
exports[name] = value
.replace(/^\.\/src\//, './')
.replace(/\.js$/, '.ts')
}
}
return {
name: packageJson.name,
version: packageJson.version,
exports,
exclude: ['**/*.test.ts', '**/*.test-utils.ts', '**/__fixtures__/**'],
imports: importMap,
publish: {
exclude: ['!../dist'], // lol
},
...packageJson.denoJson,
}
}
export async function runJsrBuildSync(packageName) {
const packageDir = fileURLToPath(new URL(`../packages/${packageName}`, import.meta.url))
const outDir = fileURLToPath(new URL(`../packages/${packageName}/dist/jsr`, import.meta.url))
fs.rmSync(outDir, { recursive: true, force: true })
fs.mkdirSync(outDir, { recursive: true })
console.log('[i] Copying sources...')
fs.cpSync(resolve(packageDir, 'src'), outDir, { recursive: true })
const printer = ts.createPrinter()
for (const f of glob.sync(resolve(outDir, '**/*.ts'))) {
let fileContent = fs.readFileSync(f, 'utf8')
let changed = false
// replace .js imports with .ts
const file = ts.createSourceFile(f, fileContent, ts.ScriptTarget.ESNext, true)
let changedTs = false
for (const imp of file.statements) {
if (imp.kind !== ts.SyntaxKind.ImportDeclaration && imp.kind !== ts.SyntaxKind.ExportDeclaration) {
continue
}
if (imp.kind === ts.SyntaxKind.ExportDeclaration && !imp.moduleSpecifier) {
continue
}
const mod = imp.moduleSpecifier.text
if (mod[0] === '.' && mod.endsWith('.js')) {
changedTs = true
imp.moduleSpecifier = {
kind: ts.SyntaxKind.StringLiteral,
text: `${mod.slice(0, -3)}.ts`,
}
}
}
if (changedTs) {
fileContent = printer.printFile(file)
changed = true
}
// add shims for node-specific APIs and replace NodeJS.* types
// pretty fragile, but it works for now
const typesToReplace = {
'NodeJS\\.Timeout': 'number',
'NodeJS\\.Immediate': 'number',
}
const nodeSpecificApis = {
setImmediate: '(cb: (...args: any[]) => void, ...args: any[]) => number',
clearImmediate: '(id: number) => void',
Buffer:
'{ '
+ 'concat: (...args: any[]) => Uint8Array, '
+ 'from: (data: any, encoding?: string) => { toString(encoding?: string): string }, '
+ ' }',
SharedWorker: ['type', 'never'],
WorkerGlobalScope:
'{ '
+ ' new (): typeof WorkerGlobalScope, '
+ ' postMessage: (message: any, transfer?: Transferable[]) => void, '
+ ' addEventListener: (type: "message", listener: (ev: MessageEvent) => void) => void, '
+ ' }',
process: '{ ' + 'hrtime: { bigint: () => bigint }, ' + '}',
}
for (const [name, decl_] of Object.entries(nodeSpecificApis)) {
if (fileContent.includes(name)) {
if (name === 'Buffer' && fileContent.includes('node:buffer')) continue
changed = true
const isType = Array.isArray(decl_) && decl_[0] === 'type'
const decl = isType ? decl_[1] : decl_
if (isType) {
fileContent = `declare type ${name} = ${decl};\n${fileContent}`
} else {
fileContent = `declare const ${name}: ${decl};\n${fileContent}`
}
}
}
for (const [oldType, newType] of Object.entries(typesToReplace)) {
if (fileContent.match(oldType)) {
changed = true
fileContent = fileContent.replace(new RegExp(oldType, 'g'), newType)
}
}
if (changed) {
fs.writeFileSync(f, fileContent)
}
}
const { packageJson, packageJsonOrig } = processPackageJson(packageDir)
const denoJson = packageJsonToDeno({ packageJson, packageJsonOrig })
fs.writeFileSync(resolve(outDir, 'deno.json'), JSON.stringify(denoJson, null, 2))
fs.cpSync(new URL('../LICENSE', import.meta.url), resolve(outDir, 'LICENSE'), { recursive: true })
if (process.env.E2E) {
// populate dependencies, if any
const depsToPopulate = []
for (const dep of Object.values(denoJson.imports)) {
if (!dep.startsWith('jsr:')) continue
if (dep.startsWith('jsr:@mtcute/')) continue
depsToPopulate.push(dep.slice(4))
}
if (depsToPopulate.length) {
console.log('[i] Populating %d dependencies...', depsToPopulate.length)
await populateFromUpstream({
downstream: process.env.JSR_URL,
token: process.env.JSR_TOKEN,
unstable_createViaApi: true,
packages: depsToPopulate,
})
}
}
let customConfig
try {
customConfig = await (await import(resolve(packageDir, 'build.config.js'))).default()
} catch {}
if (customConfig) {
await customConfig.finalJsr?.({ packageDir, outDir })
}
console.log('[i] Trying to publish with --dry-run')
cp.execSync('deno publish --dry-run --allow-dirty --quiet', { cwd: outDir, stdio: 'inherit' })
console.log('[v] All good!')
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const PACKAGE_NAME = process.argv[2]
if (!PACKAGE_NAME) {
throw new Error('package name not specified')
}
await runJsrBuildSync(PACKAGE_NAME)
}

View file

@ -1,22 +0,0 @@
/* eslint-disable node/prefer-global/process */
import * as cp from 'node:child_process'
import { fileURLToPath } from 'node:url'
const configPath = fileURLToPath(new URL('../.config/vite.build.ts', import.meta.url))
export function runViteBuildSync(packageName) {
cp.execSync(`pnpm exec vite build --config "${configPath}"`, {
stdio: 'inherit',
cwd: fileURLToPath(new URL(`../packages/${packageName}`, import.meta.url)),
})
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const PACKAGE_NAME = process.argv[2]
if (!PACKAGE_NAME) {
throw new Error('package name not specified')
}
runViteBuildSync(PACKAGE_NAME)
}

View file

@ -1,117 +0,0 @@
import * as fs from 'node:fs'
import { fileURLToPath } from 'node:url'
import { resolve } from 'node:path'
import { processPackageJson } from '../.config/vite-utils/package-json.js'
import { packageJsonToDeno, runJsrBuildSync } from './build-package-jsr.js'
import { runViteBuildSync } from './build-package-vite.js'
if (process.argv.length < 3) {
console.log('Usage: build-package.js <package name>')
process.exit(0)
}
const IS_JSR = process.env.JSR === '1'
const packageName = process.argv[2]
function transformFile(file, transform) {
const content = fs.readFileSync(file, 'utf8')
const res = transform(content, file)
if (res != null) fs.writeFileSync(file, res)
}
if (packageName === 'tl') {
// create package by copying all the needed files
const packageDir = fileURLToPath(new URL('../packages/tl', import.meta.url))
let outDir = fileURLToPath(new URL('../packages/tl/dist', import.meta.url))
if (IS_JSR) outDir = resolve(outDir, 'jsr')
fs.rmSync(outDir, { recursive: true, force: true })
const files = [
'binary/reader.d.ts',
'binary/reader.js',
'binary/rsa-keys.d.ts',
'binary/rsa-keys.js',
'binary/writer.d.ts',
'binary/writer.js',
'index.d.ts',
'index.js',
'raw-errors.json',
'mtp-schema.json',
'api-schema.json',
'app-config.json',
'README.md',
]
fs.mkdirSync(resolve(outDir, 'binary'), { recursive: true })
for (const f of files) {
fs.copyFileSync(resolve(packageDir, f), resolve(outDir, f))
}
fs.cpSync(new URL('../LICENSE', import.meta.url), resolve(outDir, 'LICENSE'), { recursive: true })
const { packageJson, packageJsonOrig } = processPackageJson(packageDir)
if (IS_JSR) {
// jsr doesn't support cjs, so we'll need to add some shims
// todo: remove this god awfulness when tl esm rewrite
transformFile(resolve(outDir, 'index.js'), (content) => {
return [
'/// <reference types="./index.d.ts" />',
'const exports = {};',
content,
'export const tl = exports.tl;',
'export const mtp = exports.mtp;',
].join('\n')
})
transformFile(resolve(outDir, 'binary/reader.js'), (content) => {
return [
'/// <reference types="./reader.d.ts" />',
'const exports = {};',
content,
'export const __tlReaderMap = exports.__tlReaderMap;',
].join('\n')
})
transformFile(resolve(outDir, 'binary/writer.js'), (content) => {
return [
'/// <reference types="./writer.d.ts" />',
'const exports = {};',
content,
'export const __tlWriterMap = exports.__tlWriterMap;',
].join('\n')
})
transformFile(resolve(outDir, 'binary/rsa-keys.js'), (content) => {
return [
'/// <reference types="./rsa-keys.d.ts" />',
'const exports = {};',
content,
'export const __publicKeyIndex = exports.__publicKeyIndex;',
].join('\n')
})
// patch deno.json to add some export maps
const denoJson = packageJsonToDeno({ packageJson, packageJsonOrig })
denoJson.exports = {}
for (const f of files) {
if (!f.match(/\.js(?:on)?$/)) continue
if (f === 'index.js') {
denoJson.exports['.'] = './index.js'
} else {
denoJson.exports[`./${f}`] = `./${f}`
}
}
fs.writeFileSync(resolve(outDir, 'deno.json'), JSON.stringify(denoJson, null, 2))
} else {
fs.writeFileSync(resolve(outDir, 'package.json'), JSON.stringify(packageJson, null, 2))
}
} else {
if (IS_JSR) {
await runJsrBuildSync(packageName)
} else {
runViteBuildSync(packageName)
}
}

View file

@ -1,86 +0,0 @@
import { appendFileSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
import { EOL } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { inc, rcompare } from 'semver'
const __dirname = dirname(new URL(import.meta.url).pathname)
function collectPackageJsons() {
return readdirSync(join(__dirname, '../packages'))
.filter(s => !s.startsWith('.'))
.map((name) => {
try {
return JSON.parse(readFileSync(join(__dirname, '../packages', name, 'package.json'), 'utf-8'))
} catch (e) {
if (e.code !== 'ENOENT') throw e
return null
}
})
.filter(Boolean)
}
function bumpVersions(packages, kind) {
const pkgJsons = collectPackageJsons()
const maxVersion = pkgJsons
.filter(it => it.name !== '@mtcute/tl')
.map(it => it.version)
.sort(rcompare)[0]
const nextVersion = inc(maxVersion, kind)
console.log('[i] Bumping versions to %s', nextVersion)
for (const pkg of packages) {
if (pkg === 'tl') continue // own versioning
const pkgJson = pkgJsons.find(it => it.name === `@mtcute/${pkg}`)
if (!pkgJson) {
console.error(`Package ${pkg} not found!`)
process.exit(1)
}
pkgJson.version = nextVersion
writeFileSync(
join(__dirname, '../packages', pkg, 'package.json'),
`${JSON.stringify(pkgJson, null, 2)}\n`,
)
}
const rootPkgJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'))
rootPkgJson.version = nextVersion
writeFileSync(join(__dirname, '../package.json'), `${JSON.stringify(rootPkgJson, null, 2)}\n`)
return nextVersion
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const kind = process.argv[2]
const packages = process.argv[3]
if (!packages || !kind) {
console.log('Usage: bump-version.js <major|minor|patch> <package1,package2>')
process.exit(1)
}
const packagesList = packages.split(',')
if (packagesList.length === 0) {
console.error('No packages specified!')
process.exit(1)
}
if (kind === 'major' && packagesList.length !== collectPackageJsons().length) {
console.error('Cannot bump major version only for some packages!')
process.exit(1)
}
const ver = bumpVersions(packagesList, kind)
if (process.env.GITHUB_OUTPUT) {
appendFileSync(process.env.GITHUB_OUTPUT, `version=${ver}${EOL}`)
}
}
export { bumpVersions }

View file

@ -1,111 +0,0 @@
import { execSync } from 'node:child_process'
import { appendFileSync, existsSync } from 'node:fs'
import { EOL } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { findChangedFilesSince, getLatestTag } from './git-utils.js'
import { listPackages } from './publish.js'
getTsconfigFiles.cache = {}
const __dirname = dirname(new URL(import.meta.url).pathname)
function getTsconfigFiles(pkg) {
if (!existsSync(join(__dirname, `../packages/${pkg}/tsconfig.json`))) {
throw new Error(`[!] ${pkg} does not have a tsconfig.json`)
}
if (pkg in getTsconfigFiles.cache) return getTsconfigFiles.cache[pkg]
console.log('[i] Getting tsconfig files for %s', pkg)
const res = execSync('pnpm exec tsc --showConfig', {
encoding: 'utf8',
stdio: 'pipe',
cwd: join(__dirname, `../packages/${pkg}`),
})
const json = JSON.parse(res)
return (getTsconfigFiles.cache[pkg] = json.files.map(it => it.replace(/^\.\//, '')))
}
function isMeaningfulChange(pkg, path) {
// some magic heuristics stuff
if (path.match(/\.(md|test(?:-utils)?\.ts)$/i)) return false
if (getTsconfigFiles(pkg).includes(path)) {
console.log('[i] %s: %s is in tsconfig', pkg, path)
return true
}
if (path.match(/typedoc\.cjs$/i)) return false
if (path.match(/^(scripts|dist|tests|private)\//i)) return false
console.log('[i] %s: %s is a meaningful change', pkg, path)
// to be safe
return true
}
function findChangedPackagesSince(tag, until) {
const packages = new Set(listPackages(true))
const changedFiles = findChangedFilesSince(tag, until)
const changedPackages = new Set()
for (const file of changedFiles) {
const [dir, pkgname, ...rest] = file.split('/')
if (dir !== 'packages') continue
if (!packages.has(pkgname)) continue
// already checked, no need to check again
if (changedPackages.has(pkgname)) continue
const relpath = rest.join('/')
if (isMeaningfulChange(pkgname, relpath)) {
changedPackages.add(pkgname)
}
}
return Array.from(changedPackages)
}
export { findChangedPackagesSince, getLatestTag }
if (process.argv[1] === fileURLToPath(import.meta.url) && process.env.CI && process.env.GITHUB_OUTPUT) {
const kind = process.argv[2]
const input = process.argv[3]
if (!input) {
// for simpler flow, one can pass all or package list as the first argument,
// and they will be returned as is, so that we can later simply
// use the outputs of this script
console.log('Usage: find-updated-packages.js <packages>')
process.exit(1)
}
if (kind === 'major' && input !== 'all') {
throw new Error('For major releases, all packages must be published')
}
console.log('[i] Determining packages to publish...')
let res
if (input === 'all') {
res = listPackages(true)
} else if (input === 'updated') {
const tag = getLatestTag()
console.log('[i] Latest tag is %s', tag)
res = findChangedPackagesSince(tag)
} else {
res = input.split(',')
}
console.log('[i] Will publish:', res)
appendFileSync(process.env.GITHUB_OUTPUT, `modified=${res.join(',')}${EOL}`)
}

View file

@ -1,38 +0,0 @@
// node scripts/gen-deps-graph.js | dot -Tsvg > deps.svg
import { getPackageJsons } from './utils.js'
const packageJsons = await getPackageJsons()
function getMtcuteName(name) {
if (!name.startsWith('@mtcute/')) return null
return name.slice(8)
}
const output = []
for (const pkg of packageJsons) {
if (!pkg) continue
const name = getMtcuteName(pkg.name)
if (!name) continue
for (const dep of Object.keys(pkg.dependencies || {})) {
const depName = getMtcuteName(dep)
if (!depName) continue
output.push(`"${name}" -> "${depName}"`)
}
for (const dep of Object.keys(pkg.devDependencies || {})) {
const depName = getMtcuteName(dep)
if (!depName) continue
output.push(`"${name}" -> "${depName}" [style=dashed,color=grey]`)
}
}
console.log('digraph {')
console.log(output.join('\n'))
console.log('}')

View file

@ -1,74 +0,0 @@
import { randomUUID } from 'node:crypto'
import { appendFileSync } from 'node:fs'
import { EOL } from 'node:os'
import { fileURLToPath } from 'node:url'
import { findChangedFilesSince, getCommitsSince, getLatestTag, parseConventionalCommit } from './git-utils.js'
function generateChangelog(onlyPackages) {
const byPackage = {}
for (const commit of getCommitsSince(getLatestTag())) {
const parsed = parseConventionalCommit(commit.msg)
if (!parsed) {
console.warn('[warn] Failed to parse commit message: %s', commit.msg)
continue
}
const { type, breaking } = parsed
if ((!type || ['chore', 'ci', 'docs', 'test'].includes(type)) && !breaking) continue
const changed = findChangedFilesSince(`${commit.hash}~1`, commit.hash)
let line = `- ${commit.hash}: ${breaking ? '**❗ BREAKING** ' : ''}${commit.msg}`
if (breaking && commit.description) {
line
+= `\n${
commit.description
.trim()
.split('\n')
.map(line => ` ${line}`)
.join('\n')}`
}
for (const file of changed) {
if (!file.startsWith('packages/')) continue
const pkg = file.split('/')[1]
if (onlyPackages && !onlyPackages.includes(pkg)) continue
if (!byPackage[pkg]) byPackage[pkg] = {}
byPackage[pkg][commit.hash] = line
// console.log('including %s in %s because of %s', commit.hash, pkg, file)
}
}
let ret = ''
for (const [pkg, lines] of Object.entries(byPackage)) {
ret += `### ${pkg}\n`
ret += Object.values(lines).join('\n')
ret += '\n\n'
}
return ret
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
let onlyPackages = null
if (process.argv[2]) {
onlyPackages = process.argv[2].split(',')
}
const res = generateChangelog(onlyPackages)
if (process.env.CI && process.env.GITHUB_OUTPUT) {
const delim = `---${randomUUID()}---${EOL}`
appendFileSync(process.env.GITHUB_OUTPUT, `changelog<<${delim}${res}${delim}`)
} else {
console.log(res)
}
}

View file

@ -1,76 +0,0 @@
import { execSync } from 'node:child_process'
function getLatestTag() {
try {
const res = execSync('git describe --abbrev=0 --tags', { encoding: 'utf8', stdio: 'pipe' }).trim()
return res
} catch (e) {
if (e.stderr.match(/^fatal: (No names found|No tags can describe)/i)) {
// no tags found, let's just return the first commit
return execSync('git rev-list --max-parents=0 HEAD', { encoding: 'utf8' }).trim()
}
throw e
}
}
function findChangedFilesSince(tag, until = 'HEAD') {
return execSync(`git diff --name-only ${tag} ${until}`, { encoding: 'utf8', stdio: 'pipe' }).trim().split('\n')
}
function getCommitsSince(tag, until = 'HEAD') {
const delim = `---${Math.random().toString(36).slice(2)}---`
const lines = execSync(`git log --pretty="format:%H %s%n%b%n${delim}" ${tag}..${until}`, { encoding: 'utf8', stdio: 'pipe' })
.trim()
.split('\n')
const items = []
let current = null
for (const line of lines) {
if (line === delim) {
if (current) items.push(current)
current = null
} else if (current) {
if (current.description) current.description += '\n'
current.description += line
} else {
const [hash, ...msg] = line.split(' ')
current = { hash, msg: msg.join(' '), description: '' }
}
}
if (current) items.push(current)
return items.reverse()
}
function getCurrentCommit() {
return execSync('git rev-parse HEAD', { encoding: 'utf8', stdio: 'pipe' }).trim()
}
function getCurrentBranch() {
return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', stdio: 'pipe' }).trim()
}
function parseConventionalCommit(msg) {
const match = msg.match(/^(\w+)(?:\(([^)]+)\))?(!?): (.+)$/)
if (!match) return null
const [, type, scope, breaking, subject] = match
return { type, scope, breaking: Boolean(breaking), subject }
}
export {
findChangedFilesSince,
getCommitsSince,
getCurrentBranch,
getCurrentCommit,
getLatestTag,
parseConventionalCommit,
}

Some files were not shown because too many files have changed in this diff Show more