build: build with vite (major cleanup part2) (#67)

This commit is contained in:
alina sireneva 2024-08-27 23:05:20 +03:00 committed by alina sireneva
commit e58e62f649
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
103 changed files with 2056 additions and 1368 deletions

View file

@ -0,0 +1,167 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const rootPackageJson = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf-8'))
const packagesDir = fileURLToPath(new URL('../../packages', import.meta.url))
const IS_JSR = process.env.JSR === '1'
export function getPackageVersion(name) {
const json = JSON.parse(readFileSync(resolve(packagesDir, name, 'package.json'), 'utf-8'))
return json.version
}
export function processPackageJson(packageDir) {
const packageJsonOrig = JSON.parse(readFileSync(resolve(packageDir, 'package.json'), 'utf-8'))
const packageJson = structuredClone(packageJsonOrig)
const entrypoints = {}
// copy common fields from root
for (const field of ['license', 'author', 'contributors', 'homepage', 'repository', 'bugs']) {
if (rootPackageJson[field]) {
packageJson[field] = rootPackageJson[field]
}
}
const newScripts = {}
if (packageJson.keepScripts) {
for (const script of packageJson.keepScripts) {
newScripts[script] = packageJson.scripts[script]
}
delete packageJson.keepScripts
}
packageJson.scripts = newScripts
delete packageJson.devDependencies
delete packageJson.private
if (packageJson.distOnlyFields) {
Object.assign(packageJson, packageJson.distOnlyFields)
delete packageJson.distOnlyFields
}
if (packageJson.jsrOnlyFields) {
if (IS_JSR) {
Object.assign(packageJson, packageJson.jsrOnlyFields)
}
delete packageJson.jsrOnlyFields
}
function replaceWorkspaceDependencies(field) {
if (!packageJson[field]) return
const dependencies = packageJson[field]
for (const name of Object.keys(dependencies)) {
const value = dependencies[name]
if (value.startsWith('workspace:')) {
if (value !== 'workspace:^' && value !== 'workspace:*') {
throw new Error(
`Cannot replace workspace dependency ${name} with ${value} - only workspace:^ and * are supported`,
)
}
if (!name.startsWith('@mtcute/')) {
throw new Error(`Cannot replace workspace dependency ${name} - only @mtcute/* is supported`)
}
// note: pnpm replaces workspace:* with the current version, unlike this script
const depVersion = value === 'workspace:*' ? '*' : `^${getPackageVersion(name.slice(8))}`
dependencies[name] = depVersion
}
}
}
replaceWorkspaceDependencies('dependencies')
replaceWorkspaceDependencies('devDependencies')
replaceWorkspaceDependencies('peerDependencies')
replaceWorkspaceDependencies('optionalDependencies')
delete packageJson.typedoc
if (packageJson.browser) {
function maybeFixPath(p, repl) {
if (!p) return p
if (p.startsWith('./src/')) {
return repl + p.slice(6)
}
if (p.startsWith('./')) {
return repl + p.slice(2)
}
return p
}
for (const key of Object.keys(packageJson.browser)) {
if (!key.startsWith('./src/')) continue
const path = key.slice(6)
packageJson.browser[`./esm/${path}`] = maybeFixPath(packageJson.browser[key], './esm/')
delete packageJson.browser[key]
}
}
if (packageJson.exports) {
let exports = packageJson.exports
if (typeof exports === 'string') {
exports = { '.': exports }
}
if (typeof exports !== 'object') {
throw new TypeError('package.json exports must be an object')
}
const newExports = {}
for (const [key, value] of Object.entries(exports)) {
if (typeof value !== 'string') {
throw new TypeError(`package.json exports value must be a string: ${key}`)
}
if (value.endsWith('.wasm')) {
newExports[key] = value
continue
}
let entrypointName = key.replace(/^\.(\/|$)/, '').replace(/\.js$/, '')
if (entrypointName === '') entrypointName = 'index'
entrypoints[entrypointName] = value
newExports[key] = {
import: {
types: `./${entrypointName}.d.ts`,
default: `./${entrypointName}.js`,
},
require: {
types: `./${entrypointName}.d.cts`,
default: `./${entrypointName}.cjs`,
},
}
}
packageJson.exports = newExports
}
if (typeof packageJson.bin === 'object') {
const newBin = {}
for (const [key, value] of Object.entries(packageJson.bin)) {
if (typeof value !== 'string') {
throw new TypeError(`package.json bin value must be a string: ${key}`)
}
let entrypointName = key.replace(/^\.(\/|$)/, '').replace(/\.js$/, '')
if (entrypointName === '') entrypointName = 'index'
entrypoints[entrypointName] = value
newBin[key] = `./${entrypointName}.js`
}
packageJson.bin = newBin
}
return {
packageJsonOrig,
packageJson,
entrypoints,
}
}

View file

@ -38,10 +38,18 @@ export default mergeConfig(baseConfig, {
],
build: {
rollupOptions: {
external: ['bun:sqlite'],
external: ['bun:sqlite', '@jsr/db__sqlite'],
},
},
define: {
'import.meta.env.TEST_ENV': '"browser"',
},
optimizeDeps: {
esbuildOptions: {
// for WHATEVER REASON browserify-zlib uses `global` and it dies in browser lol
define: {
global: 'globalThis',
},
},
},
})

111
.config/vite.build.ts Normal file
View file

@ -0,0 +1,111 @@
/// <reference types="vitest" />
import { cpSync, existsSync, writeFileSync } from 'node:fs'
import { relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
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> => {
if (env.command !== 'build') {
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 CJS_DEPRECATION_WARNING = `
if (typeof globalThis !== 'undefined' && !globalThis._MTCUTE_CJS_DEPRECATION_WARNED) {
globalThis._MTCUTE_CJS_DEPRECATION_WARNED = true
console.warn("[${packageJson.name}] CommonJS support is deprecated and will be removed soon. Please consider switching to ESM, it's "+(new Date()).getFullYear()+" already.")
console.warn("[${packageJson.name}] Learn more about switching to ESM: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c")
}
`.trim()
if (customConfig?.preBuild) {
await customConfig.preBuild()
}
return {
build: {
rollupOptions: {
plugins: [
...(customConfig?.rollupPluginsPre ?? []),
nodeExternals(),
{
name: 'mtcute-finalize',
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',
emptyOutDir: true,
target: 'es2022',
},
plugins: [
...(customConfig?.vitePlugins ?? []),
dts({
// broken; see https://github.com/qmhc/vite-plugin-dts/issues/321, https://github.com/microsoft/rushstack/issues/3557
// rollupTypes: true,
}),
],
}
}

View file

@ -82,7 +82,10 @@ export default defineConfig({
name: 'fix-wasm-load',
async transform(code) {
if (code.includes('@mtcute/wasm/mtcute.wasm')) {
return code.replace('@mtcute/wasm/mtcute.wasm', resolve(__dirname, '../packages/wasm/mtcute.wasm'))
return code.replace('@mtcute/wasm/mtcute.wasm', resolve(__dirname, '../packages/wasm/src/mtcute.wasm'))
}
if (code.includes('./mtcute.wasm')) {
return code.replace(/\.?\.\/mtcute\.wasm/, resolve(__dirname, '../packages/wasm/src/mtcute.wasm'))
}
return code

View file

@ -70,17 +70,16 @@ export default defineConfig({
})
},
},
// todo
// {
// name: 'fix-wasm-load',
// async transform(code, id) {
// if (code.includes('@mtcute/wasm/mtcute.wasm')) {
// return code.replace('@mtcute/wasm/mtcute.wasm', resolve(__dirname, '../packages/wasm/mtcute.wasm'))
// }
{
name: 'fix-wasm-load',
async transform(code) {
if (code.includes('./mtcute.wasm')) {
return code.replace(/\.?\.\/mtcute\.wasm/, resolve(__dirname, '../packages/wasm/src/mtcute.wasm'))
}
// return code
// }
// },
return code
},
},
testSetup(),
],
define: {

View file

@ -26,12 +26,4 @@ export default defineConfig({
define: {
'import.meta.env.TEST_ENV': '"node"',
},
optimizeDeps: {
esbuildOptions: {
// for WHATEVER REASON browserify-zlib uses `global` and it dies in browser lol
define: {
global: 'globalThis',
},
},
},
})

View file

@ -59,14 +59,14 @@ jobs:
GH_RELEASE: 1
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node scripts/publish.js ${{ steps.find.outputs.modified }}
- uses: denoland/setup-deno@v1
with:
deno-version: '1.45.5'
- name: Build packages and publish to JSR
env:
JSR: 1
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node scripts/publish.js ${{ steps.find.outputs.modified }}
# - uses: denoland/setup-deno@v1
# with:
# deno-version: '1.45.5'
# - name: Build packages and publish to JSR
# env:
# JSR: 1
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# run: node scripts/publish.js ${{ steps.find.outputs.modified }}
- name: Commit version bumps
run: |
git commit -am "v${{ steps.bump.outputs.version }}"

View file

@ -1,4 +1,4 @@
import { assertEquals, assertNotEquals } from 'https://deno.land/std@0.223.0/assert/mod.ts'
import { assertEquals } from 'https://deno.land/std@0.223.0/assert/mod.ts'
import { MtPeerNotFoundError } from '@mtcute/core'
import { TelegramClient } from '@mtcute/core/client.js'
@ -42,17 +42,5 @@ Deno.test('2. calling methods', { sanitizeResources: false }, async (t) => {
assertEquals(history[0].chat.firstName, 'Telegram')
})
await t.step('updateProfile', async () => {
const bio = `mtcute e2e ${new Date().toISOString()}`
const oldSelf = await tg.getFullChat('self')
const res = await tg.updateProfile({ bio })
const newSelf = await tg.getFullChat('self')
assertEquals(res.isSelf, true)
assertNotEquals(oldSelf.bio, newSelf.bio)
assertEquals(newSelf.bio, bio)
})
await tg.close()
})

View file

@ -44,16 +44,4 @@ describe('2. calling methods', function () {
expect(history[0].chat.id).to.equal(777000)
expect(history[0].chat.firstName).to.equal('Telegram')
})
it('updateProfile', async () => {
const bio = `mtcute e2e ${new Date().toISOString()}`
const oldSelf = await tg.getFullChat('self')
const res = await tg.updateProfile({ bio })
const newSelf = await tg.getFullChat('self')
expect(res.isSelf).to.eq(true)
expect(oldSelf.bio).to.not.equal(newSelf.bio)
expect(newSelf.bio).to.equal(bio)
})
})

View file

@ -6,8 +6,9 @@ import { setPlatform } from '@mtcute/core/platform.js'
import { LogManager, sleep } from '@mtcute/core/utils.js'
import { NodePlatform, SqliteStorage, TcpTransport } from '@mtcute/node'
import { NodeCryptoProvider } from '@mtcute/node/utils.js'
import type { BaseTelegramClientOptions } from '@mtcute/core/client.js'
export function getApiParams(storage?: string) {
export function getApiParams(storage?: string): BaseTelegramClientOptions {
if (!process.env.API_ID || !process.env.API_HASH) {
throw new Error('API_ID and API_HASH env variables must be set')
}

View file

@ -56,7 +56,7 @@ export default antfu({
'style/brace-style': ['error', '1tbs', { allowSingleLine: true }],
'node/prefer-global/process': ['error', 'always'],
'node/prefer-global/buffer': ['error', 'always'],
'no-restricted-globals': ['error', 'Buffer', '__dirname', 'require'],
'no-restricted-globals': ['error', 'Buffer', '__dirname', 'require', 'NodeJS', 'setTimeout', 'clearTimeout'],
'style/quotes': ['error', 'single', { avoidEscape: true }],
'test/consistent-test-it': 'off',
'test/prefer-lowercase-title': 'off',
@ -112,7 +112,7 @@ export default antfu({
'import/no-relative-packages': 'off', // common-internals is symlinked from node
},
}, {
files: ['**/scripts/**', '**/*.cjs'],
files: ['**/scripts/**', '**/*.cjs', '.config/**/*'],
rules: {
'no-restricted-imports': 'off',
'no-restricted-globals': 'off',

View file

@ -1,75 +1,84 @@
{
"name": "mtcute-workspace",
"type": "module",
"version": "0.16.8",
"private": true,
"packageManager": "pnpm@9.0.6",
"description": "Type-safe library for MTProto (Telegram API) for browser and NodeJS",
"author": "alina sireneva <alina@tei.su>",
"license": "MIT",
"homepage": "https://mtcute.dev",
"repository": {
"type": "git",
"url": "https://github.com/mtcute/mtcute"
},
"keywords": [
"telegram",
"telegram-api",
"telegram-bot",
"telegram-library",
"mtproto",
"tgbot",
"userbot",
"api"
],
"workspaces": [
"packages/*"
],
"scripts": {
"postinstall": "node scripts/validate-deps-versions.js && node scripts/remove-jsr-sourcefiles.js",
"test": "pnpm run -r test && vitest --config .config/vite.ts run",
"test:dev": "vitest --config .config/vite.ts watch",
"test:ui": "vitest --config .config/vite.ts --ui",
"test:coverage": "vitest --config .config/vite.ts run --coverage",
"test:ci": "vitest --config .config/vite.ts run --coverage.enabled --coverage.reporter=json",
"test:browser": "vitest --config .config/vite.browser.ts run",
"test:browser:dev": "vitest --config .config/vite.browser.ts watch",
"lint": "eslint",
"lint:ci": "CI=1 NODE_OPTIONS=\\\"--max_old_space_size=8192\\\" eslint",
"lint:tsc": "rimraf packages/**/dist packages/**/*.tsbuildinfo && pnpm -r --workspace-concurrency=4 exec tsc --build",
"lint:tsc:ci": "pnpm -r exec tsc --build",
"lint:dpdm": "dpdm -T --no-warning --no-tree --exit-code circular:1 packages/*",
"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"
},
"devDependencies": {
"@antfu/eslint-config": "2.26.0",
"@teidesu/slow-types-compiler": "1.1.0",
"@types/deno": "npm:@teidesu/deno-types@1.45.5",
"@types/node": "20.10.0",
"@types/ws": "8.5.4",
"@vitest/browser": "2.0.5",
"@vitest/coverage-v8": "2.0.5",
"@vitest/expect": "2.0.5",
"@vitest/spy": "2.0.5",
"@vitest/ui": "2.0.5",
"chai": "5.1.0",
"cjs-module-lexer": "1.2.3",
"dotenv-flow": "4.1.0",
"dpdm": "3.14.0",
"esbuild": "0.23.0",
"eslint": "9.9.0",
"glob": "11.0.0",
"playwright": "1.42.1",
"rimraf": "6.0.1",
"semver": "7.5.1",
"tsx": "4.17.0",
"typedoc": "0.26.5",
"typescript": "5.5.4",
"vite": "5.1.6",
"vite-plugin-node-polyfills": "0.22.0",
"vitest": "2.0.5"
"name": "mtcute-workspace",
"type": "module",
"version": "0.16.8",
"private": true,
"packageManager": "pnpm@9.0.6",
"description": "Type-safe library for MTProto (Telegram API) for browser and NodeJS",
"author": "alina sireneva <alina@tei.su>",
"license": "MIT",
"homepage": "https://mtcute.dev",
"repository": {
"type": "git",
"url": "https://github.com/mtcute/mtcute"
},
"keywords": [
"telegram",
"telegram-api",
"telegram-bot",
"telegram-library",
"mtproto",
"tgbot",
"userbot",
"api"
],
"workspaces": [
"packages/*"
],
"scripts": {
"postinstall": "node scripts/validate-deps-versions.js && 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",
"test:coverage": "vitest --config .config/vite.ts run --coverage",
"test:ci": "vitest --config .config/vite.ts run --coverage.enabled --coverage.reporter=json",
"test:browser": "vitest --config .config/vite.browser.ts run",
"test:browser:dev": "vitest --config .config/vite.browser.ts watch",
"lint": "eslint",
"lint:ci": "CI=1 NODE_OPTIONS=\\\"--max_old_space_size=8192\\\" eslint",
"lint:tsc": "pnpm -r --workspace-concurrency=4 exec tsc",
"lint:tsc:ci": "pnpm -r exec tsc",
"lint:dpdm": "dpdm -T --no-warning --no-tree --exit-code circular:1 packages/*",
"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-vite": "node scripts/build-package-vite.js"
},
"devDependencies": {
"@antfu/eslint-config": "2.26.0",
"@teidesu/slow-types-compiler": "1.1.0",
"@types/deno": "npm:@teidesu/deno-types@1.45.5",
"@types/node": "20.10.0",
"@types/ws": "8.5.4",
"@vitest/browser": "2.0.5",
"@vitest/coverage-v8": "2.0.5",
"@vitest/expect": "2.0.5",
"@vitest/spy": "2.0.5",
"@vitest/ui": "2.0.5",
"bun-types": "^1.1.24",
"chai": "5.1.0",
"cjs-module-lexer": "1.2.3",
"dotenv-flow": "4.1.0",
"dpdm": "3.14.0",
"esbuild": "0.23.0",
"eslint": "9.9.0",
"glob": "11.0.0",
"playwright": "1.42.1",
"rimraf": "6.0.1",
"rollup-plugin-node-externals": "7.1.3",
"semver": "7.5.1",
"tsx": "4.17.0",
"typedoc": "0.26.5",
"typescript": "5.5.4",
"vite": "5.4.2",
"vite-plugin-dts": "4.0.3",
"vite-plugin-node-polyfills": "0.22.0",
"vitest": "2.0.5"
},
"pnpm": {
"overrides": {
"typescript": "5.5.4"
}
}
}

View file

@ -1 +0,0 @@
module.exports = () => ({ buildCjs: false })

View file

@ -0,0 +1,4 @@
export default () => ({
buildCjs: false,
external: ['bun', 'bun:sqlite'],
})

View file

@ -1,29 +1,28 @@
{
"name": "@mtcute/bun",
"type": "module",
"version": "0.16.7",
"private": true,
"description": "Meta-package for Bun",
"author": "alina sireneva <alina@tei.su>",
"license": "MIT",
"sideEffects": false,
"exports": {
".": "./src/index.ts",
"./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:^"
},
"devDependencies": {
"@mtcute/test": "workspace:^",
"bun-types": "1.0.33"
}
"name": "@mtcute/bun",
"type": "module",
"version": "0.16.7",
"private": true,
"description": "Meta-package for Bun",
"author": "alina sireneva <alina@tei.su>",
"license": "MIT",
"sideEffects": false,
"exports": {
".": "./src/index.ts",
"./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:^"
},
"devDependencies": {
"@mtcute/test": "workspace:^"
}
}

View file

@ -1,19 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"types": [
"bun-types",
"vite/client"
],
"outDir": "./dist"
},
"references": [
{ "path": "../core" },
{ "path": "../dispatcher" },
{ "path": "../html-parser" },
{ "path": "../markdown-parser" }
],
"include": [
"./src"
]

View file

@ -4,7 +4,7 @@ import { dataViewFromBuffer } from '@mtcute/core/utils.js'
import type { TelethonSession } from '../telethon/types.js'
export function serializeGramjsSession(session: TelethonSession) {
export function serializeGramjsSession(session: TelethonSession): string {
if (session.authKey.length !== 256) {
throw new MtArgumentError('authKey must be 256 bytes long')
}

View file

@ -6,7 +6,7 @@ import { serializeIpv4ToBytes, serializeIpv6ToBytes } from '../utils/ip.js'
import type { TelethonSession } from './types.js'
export function serializeTelethonSession(session: TelethonSession) {
export function serializeTelethonSession(session: TelethonSession): string {
if (session.authKey.length !== 256) {
throw new MtArgumentError('authKey must be 256 bytes long')
}

View file

@ -1,12 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/esm"
},
"references": [
{ "path": "../core" }
],
"include": [
"./src"
]

View file

@ -1,73 +0,0 @@
const KNOWN_DECORATORS = ['memoizeGetters', 'makeInspectable']
module.exports = ({ path, glob, transformFile, packageDir, outDir, jsr }) => ({
esmOnlyDirectives: true,
esmImportDirectives: true,
final() {
const version = require(path.join(packageDir, 'package.json')).version
const replaceVersion = content => content.replace('%VERSION%', version)
if (jsr) {
transformFile(path.join(outDir, 'network/network-manager.ts'), replaceVersion)
} else {
transformFile(path.join(outDir, 'cjs/network/network-manager.js'), replaceVersion)
transformFile(path.join(outDir, 'esm/network/network-manager.js'), replaceVersion)
}
if (jsr) return
// make decorators properly tree-shakeable
// very fragile, but it works for now :D
// skip for jsr for now because types aren't resolved correctly and it breaks everything (TODO: fix this)
const decoratorsRegex = new RegExp(
`(${KNOWN_DECORATORS.join('|')})\\((.+?)\\)(?:;|$)`,
'gms',
)
const replaceDecorators = (content, file) => {
if (!KNOWN_DECORATORS.some(d => content.includes(d))) return null
const countPerClass = new Map()
content = content.replace(decoratorsRegex, (_, name, args) => {
const [clsName_, ...rest] = args.split(',')
const clsName = clsName_.trim()
const count = (countPerClass.get(clsName) || 0) + 1
countPerClass.set(clsName, count)
const prevName = count === 1 ? clsName : `${clsName}$${count - 1}`
const localName = `${clsName}$${count}`
return `const ${localName} = /*#__PURE__*/${name}(${prevName}, ${rest.join(',')});`
})
if (countPerClass.size === 0) {
throw new Error('No decorator usages found, but known names were used')
}
const customExports = []
for (const [clsName, count] of countPerClass) {
const needle = new RegExp(`^export class(?= ${clsName} ({|extends ))`, 'm')
if (!content.match(needle)) {
throw new Error(`Class ${clsName} not found in ${file}`)
}
content = content.replace(needle, 'class')
customExports.push(
`export { ${clsName}$${count} as ${clsName} }`,
)
}
return `${content}\n${customExports.join('\n')}\n`
}
const globSrc = path.join(outDir, jsr ? 'highlevel/types/**/*.ts' : 'esm/highlevel/types/**/*.js')
for (const f of glob.sync(globSrc)) {
transformFile(f, replaceDecorators)
}
},
})

View file

@ -0,0 +1,82 @@
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
import { resolve } from 'node:path'
import * as fs from 'node:fs'
const KNOWN_DECORATORS = ['memoizeGetters', 'makeInspectable']
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))
// make decorators properly tree-shakeable
// very fragile, but it kinda works :D
// skip for jsr for now because types aren't resolved correctly and it breaks everything (TODO: fix this)
const decoratorsRegex = new RegExp(
`(${KNOWN_DECORATORS.join('|')})\\((.+?)\\)(?:;|$)`,
'gms',
)
return {
rollupPluginsPre: [
{
name: 'mtcute-core-build-plugin',
transform(code, id) {
if (id === networkManagerId) {
const require = createRequire(import.meta.url)
const version = require(fileURLToPath(new URL('./package.json', import.meta.url))).version
return code.replace('%VERSION%', version)
}
if (id.startsWith(highlevelTypesDir)) {
if (!KNOWN_DECORATORS.some(d => code.includes(d))) return null
const countPerClass = new Map()
code = code.replace(decoratorsRegex, (_, name, args) => {
const [clsName_, ...rest] = args.split(',')
const clsName = clsName_.trim()
const count = (countPerClass.get(clsName) || 0) + 1
countPerClass.set(clsName, count)
const prevName = count === 1 ? clsName : `${clsName}$${count - 1}`
const localName = `${clsName}$${count}`
return `const ${localName} = /*#__PURE__*/${name}(${prevName}, ${rest.join(',')});`
})
if (countPerClass.size === 0) {
throw new Error('No decorator usages found, but known names were used')
}
const customExports = []
for (const [clsName, count] of countPerClass) {
const needle = new RegExp(`^export class(?= ${clsName} ({|extends ))`, 'm')
if (!code.match(needle)) {
throw new Error(`Class ${clsName} not found in ${id.replace(import.meta.url, '')}`)
}
code = code.replace(needle, 'class')
customExports.push(`export { ${clsName}$${count} as ${clsName} }`)
}
return `${code}\n${customExports.join('\n')}\n`
}
return code
},
},
],
finalJsr({ outDir }) {
const networkMgrFile = resolve(outDir, 'network/network-manager.ts')
const code = fs.readFileSync(networkMgrFile, 'utf8')
const require = createRequire(import.meta.url)
const version = require(fileURLToPath(new URL('./package.json', import.meta.url))).version
fs.writeFileSync(networkMgrFile, code.replace('%VERSION%', version))
},
}
}

View file

@ -14,6 +14,7 @@ import { sendMedia } from '../methods/messages/send-media.js'
import { sendMediaGroup } from '../methods/messages/send-media-group.js'
import { sendText } from '../methods/messages/send-text.js'
import { resolvePeer } from '../methods/users/resolve-peer.js'
import { timers } from '../../utils/index.js'
import type { Message } from './messages/message.js'
import type { InputPeerLike } from './peers/index.js'
@ -23,7 +24,7 @@ import type { ParametersSkip2 } from './utils.js'
interface QueuedHandler<T> {
promise: ControllablePromise<T>
check?: (update: T) => MaybePromise<boolean>
timeout?: NodeJS.Timeout
timeout?: timers.Timer
}
const CONVERSATION_SYMBOL = Symbol('conversation')
@ -341,10 +342,10 @@ export class Conversation {
const promise = createControllablePromise<Message>()
let timer: NodeJS.Timeout | undefined
let timer: timers.Timer | undefined
if (timeout !== null) {
timer = setTimeout(() => {
timer = timers.setTimeout(() => {
promise.reject(new MtTimeoutError(timeout))
this._queuedNewMessage.removeBy(it => it.promise === promise)
}, timeout)
@ -482,11 +483,11 @@ export class Conversation {
const promise = createControllablePromise<Message>()
let timer: NodeJS.Timeout | undefined
let timer: timers.Timer | undefined
const timeout = params?.timeout
if (timeout) {
timer = setTimeout(() => {
timer = timers.setTimeout(() => {
promise.reject(new MtTimeoutError(timeout))
this._pendingEditMessage.delete(msgId)
}, timeout)
@ -530,10 +531,10 @@ export class Conversation {
const promise = createControllablePromise<void>()
let timer: NodeJS.Timeout | undefined
let timer: timers.Timer | undefined
if (timeout !== null) {
timer = setTimeout(() => {
timer = timers.setTimeout(() => {
promise.reject(new MtTimeoutError(timeout))
this._pendingRead.delete(msgId)
}, timeout)
@ -562,7 +563,7 @@ export class Conversation {
void this._lock.acquire().then(async () => {
try {
if (!it.check || (await it.check(msg))) {
if (it.timeout) clearTimeout(it.timeout)
if (it.timeout) timers.clearTimeout(it.timeout)
it.promise.resolve(msg)
this._queuedNewMessage.popFront()
}
@ -592,7 +593,7 @@ export class Conversation {
(async () => {
if (!it.check || (await it.check(msg))) {
if (it.timeout) clearTimeout(it.timeout)
if (it.timeout) timers.clearTimeout(it.timeout)
it.promise.resolve(msg)
this._pendingEditMessage.delete(msg.id)
}
@ -609,7 +610,7 @@ export class Conversation {
for (const msgId of this._pendingRead.keys()) {
if (msgId <= lastRead) {
const it = this._pendingRead.get(msgId)!
if (it.timeout) clearTimeout(it.timeout)
if (it.timeout) timers.clearTimeout(it.timeout)
it.promise.resolve()
this._pendingRead.delete(msgId)
}

View file

@ -1,3 +1,4 @@
/* eslint-disable no-restricted-globals */
import type { tdFileId } from '@mtcute/file-id'
import type { tl } from '@mtcute/tl'

View file

@ -21,6 +21,7 @@ import {
import type { BaseTelegramClient } from '../base.js'
import type { CurrentUserInfo } from '../storage/service/current-user.js'
import { PeersIndex } from '../types/peers/peers-index.js'
import * as timers from '../../utils/timers.js'
import type { PendingUpdate, PendingUpdateContainer, RawUpdateHandler, UpdatesManagerParams } from './types.js'
import {
@ -142,7 +143,7 @@ export class UpdatesManager {
cpts: Map<number, number> = new Map()
cptsMod: Map<number, number> = new Map()
channelDiffTimeouts: Map<number, NodeJS.Timeout> = new Map()
channelDiffTimeouts: Map<number, timers.Timer> = new Map()
channelsOpened: Map<number, number> = new Map()
log: Logger
@ -154,7 +155,7 @@ export class UpdatesManager {
private _channelPtsLimit: Extract<UpdatesManagerParams['channelPtsLimit'], Function>
auth?: CurrentUserInfo | null // todo: do we need a local copy?
keepAliveInterval?: NodeJS.Timeout
keepAliveInterval?: timers.Interval
constructor(
readonly client: BaseTelegramClient,
@ -245,8 +246,8 @@ export class UpdatesManager {
// start updates loop in background
this.updatesLoopActive = true
clearInterval(this.keepAliveInterval)
this.keepAliveInterval = setInterval(this._onKeepAlive, KEEP_ALIVE_INTERVAL)
timers.clearInterval(this.keepAliveInterval)
this.keepAliveInterval = timers.setInterval(this._onKeepAlive, KEEP_ALIVE_INTERVAL)
this._loop().catch(err => this.client.emitError(err))
if (this.catchUpOnStart) {
@ -263,10 +264,10 @@ export class UpdatesManager {
stopLoop(): void {
if (!this.updatesLoopActive) return
clearInterval(this.keepAliveInterval)
timers.clearInterval(this.keepAliveInterval)
for (const timer of this.channelDiffTimeouts.values()) {
clearTimeout(timer)
timers.clearTimeout(timer)
}
this.channelDiffTimeouts.clear()
@ -814,7 +815,7 @@ export class UpdatesManager {
// clear timeout if any
if (channelDiffTimeouts.has(channelId)) {
clearTimeout(channelDiffTimeouts.get(channelId))
timers.clearTimeout(channelDiffTimeouts.get(channelId))
channelDiffTimeouts.delete(channelId)
}
@ -952,7 +953,7 @@ export class UpdatesManager {
log.debug('scheduling next fetch for channel %d in %d seconds', channelId, lastTimeout)
channelDiffTimeouts.set(
channelId,
setTimeout(() => this._fetchChannelDifferenceViaUpdate(channelId), lastTimeout * 1000),
timers.setTimeout(() => this._fetchChannelDifferenceViaUpdate(channelId), lastTimeout * 1000),
)
}

View file

@ -1,3 +1,4 @@
import { timers } from '../../utils/index.js'
import type { Message } from '../types/messages/index.js'
import type { BusinessMessage, ParsedUpdate } from '../types/updates/index.js'
import { _parseUpdate } from '../types/updates/parse-update.js'
@ -47,7 +48,7 @@ export function makeParsedUpdateHandler(params: ParsedUpdateHandlerParams): RawU
}
}
const pending = new Map<string, [Message[], NodeJS.Timeout]>()
const pending = new Map<string, [Message[], timers.Timer]>()
return (update, peers) => {
const parsed = _parseUpdate(update, peers)
@ -66,7 +67,7 @@ export function makeParsedUpdateHandler(params: ParsedUpdateHandlerParams): RawU
pendingGroup[0].push(parsed.data)
} else {
const messages = [parsed.data]
const timeout = setTimeout(() => {
const timeout = timers.setTimeout(() => {
pending.delete(group)
if (isBusiness) {

View file

@ -1,6 +1,6 @@
import type { tl } from '@mtcute/tl'
import type { AsyncLock, ConditionVariable, Deque, EarlyTimer, Logger, SortedLinkedList } from '../../utils/index.js'
import type { AsyncLock, ConditionVariable, Deque, EarlyTimer, Logger, SortedLinkedList, timers } from '../../utils/index.js'
import type { CurrentUserInfo } from '../storage/service/current-user.js'
import type { PeersIndex } from '../types/peers/peers-index.js'
@ -161,7 +161,7 @@ export interface UpdatesState {
cpts: Map<number, number>
cptsMod: Map<number, number>
channelDiffTimeouts: Map<number, NodeJS.Timeout>
channelDiffTimeouts: Map<number, timers.Timer>
channelsOpened: Map<number, number>
log: Logger

View file

@ -68,7 +68,7 @@ export class WorkerInvoker {
}
}
makeBinder<T>(target: InvokeTarget) {
makeBinder<T>(target: InvokeTarget): <K extends keyof T>(method: K, isVoid?: boolean) => T[K] {
return <K extends keyof T>(method: K, isVoid = false) => {
let fn

View file

@ -11,12 +11,14 @@ import type {
} from '../utils/index.js'
import {
Deque,
LongMap,
LruSet,
SortedArray,
compareLongs,
getRandomInt,
randomLong,
timers,
} from '../utils/index.js'
import { AuthKey } from './auth-key.js'
@ -41,7 +43,7 @@ export interface PendingRpc {
initConn?: boolean
getState?: number
cancelled?: boolean
timeout?: NodeJS.Timeout
timeout?: timers.Timer
}
export type PendingMessage =
@ -131,8 +133,8 @@ export class MtprotoSession {
authorizationPending = false
next429Timeout = 1000
current429Timeout?: NodeJS.Timeout
next429ResetTimeout?: NodeJS.Timeout
current429Timeout?: timers.Timer
next429ResetTimeout?: timers.Timer
constructor(
readonly _crypto: ICryptoProvider,
@ -165,7 +167,7 @@ export class MtprotoSession {
this.resetAuthKey()
}
clearTimeout(this.current429Timeout)
timers.clearTimeout(this.current429Timeout)
this.resetState(withAuthKey)
this.resetLastPing(true)
}
@ -339,14 +341,14 @@ export class MtprotoSession {
const timeout = this.next429Timeout
this.next429Timeout = Math.min(this.next429Timeout * 2, 32000)
clearTimeout(this.current429Timeout)
clearTimeout(this.next429ResetTimeout)
timers.clearTimeout(this.current429Timeout)
timers.clearTimeout(this.next429ResetTimeout)
this.current429Timeout = setTimeout(() => {
this.current429Timeout = timers.setTimeout(() => {
this.current429Timeout = undefined
callback()
}, timeout)
this.next429ResetTimeout = setTimeout(() => {
this.next429ResetTimeout = timers.setTimeout(() => {
this.next429ResetTimeout = undefined
this.next429Timeout = 1000
}, 60000)

View file

@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { StubTelegramTransport, createStub, defaultTestCryptoProvider } from '@mtcute/test'
import { LogManager } from '../utils/index.js'
import { LogManager, timers } from '../utils/index.js'
import type { PersistentConnectionParams } from './persistent-connection.js'
import { PersistentConnection } from './persistent-connection.js'
@ -78,7 +78,7 @@ describe('PersistentConnection', () => {
const transportConnect = transport.connect
vi.spyOn(transport, 'connect').mockImplementation((dc, test) => {
setTimeout(() => {
timers.setTimeout(() => {
transportConnect.call(transport, dc, test)
}, 100)
})

View file

@ -2,6 +2,7 @@ import EventEmitter from 'node:events'
import { MtcuteError } from '../types/index.js'
import type { BasicDcOption, ICryptoProvider, Logger } from '../utils/index.js'
import { timers } from '../utils/index.js'
import type { ReconnectionStrategy } from './reconnection.js'
import type { ITelegramTransport, TransportFactory } from './transports/index.js'
@ -35,12 +36,12 @@ export abstract class PersistentConnection extends EventEmitter {
private _lastError: Error | null = null
private _consequentFails = 0
private _previousWait: number | null = null
private _reconnectionTimeout: NodeJS.Timeout | null = null
private _reconnectionTimeout: timers.Timer | null = null
private _shouldReconnectImmediately = false
protected _disconnectedManually = false
// inactivity timeout
private _inactivityTimeout: NodeJS.Timeout | null = null
private _inactivityTimeout: timers.Timer | null = null
private _inactive = true
_destroyed = false
@ -165,9 +166,9 @@ export abstract class PersistentConnection extends EventEmitter {
this._previousWait = wait
if (this._reconnectionTimeout != null) {
clearTimeout(this._reconnectionTimeout)
timers.clearTimeout(this._reconnectionTimeout)
}
this._reconnectionTimeout = setTimeout(() => {
this._reconnectionTimeout = timers.setTimeout(() => {
if (this._destroyed) return
this._reconnectionTimeout = null
this.connect()
@ -183,7 +184,7 @@ export abstract class PersistentConnection extends EventEmitter {
}
if (this._reconnectionTimeout != null) {
clearTimeout(this._reconnectionTimeout)
timers.clearTimeout(this._reconnectionTimeout)
this._reconnectionTimeout = null
}
@ -215,11 +216,12 @@ export abstract class PersistentConnection extends EventEmitter {
}
async destroy(): Promise<void> {
this._disconnectedManually = true
if (this._reconnectionTimeout != null) {
clearTimeout(this._reconnectionTimeout)
timers.clearTimeout(this._reconnectionTimeout)
}
if (this._inactivityTimeout != null) {
clearTimeout(this._inactivityTimeout)
timers.clearTimeout(this._inactivityTimeout)
}
await this._transport.close()
@ -229,8 +231,8 @@ export abstract class PersistentConnection extends EventEmitter {
protected _rescheduleInactivity(): void {
if (!this.params.inactivityTimeout) return
if (this._inactivityTimeout) clearTimeout(this._inactivityTimeout)
this._inactivityTimeout = setTimeout(this._onInactivityTimeout, this.params.inactivityTimeout)
if (this._inactivityTimeout) timers.clearTimeout(this._inactivityTimeout)
this._inactivityTimeout = timers.setTimeout(this._onInactivityTimeout, this.params.inactivityTimeout)
}
protected _onInactivityTimeout(): void {
@ -246,7 +248,7 @@ export abstract class PersistentConnection extends EventEmitter {
this.params.inactivityTimeout = timeout
if (this._inactivityTimeout) {
clearTimeout(this._inactivityTimeout)
timers.clearTimeout(this._inactivityTimeout)
}
if (timeout) {

View file

@ -1,6 +1,8 @@
import Long from 'long'
import type { mtp } from '@mtcute/tl'
import { timers } from '../utils/index.js'
export class ServerSaltManager {
private _futureSalts: mtp.RawMt_future_salt[] = []
@ -26,15 +28,15 @@ export class ServerSaltManager {
else this._scheduleNext()
}
private _timer?: NodeJS.Timeout
private _timer?: timers.Timer
private _scheduleNext(): void {
if (this._timer) clearTimeout(this._timer)
if (this._timer) timers.clearTimeout(this._timer)
if (this._futureSalts.length === 0) return
const next = this._futureSalts.shift()!
this._timer = setTimeout(
this._timer = timers.setTimeout(
() => {
this.currentSalt = next.salt
this._scheduleNext()
@ -44,6 +46,6 @@ export class ServerSaltManager {
}
destroy(): void {
clearTimeout(this._timer)
timers.clearTimeout(this._timer)
}
}

View file

@ -13,11 +13,13 @@ import type {
} from '../utils/index.js'
import {
EarlyTimer,
concatBuffers,
createControllablePromise,
longFromBuffer,
randomLong,
removeFromLongArray,
timers,
} from '../utils/index.js'
import { doAuthorization } from './authorization.js'
@ -72,12 +74,12 @@ export class SessionConnection extends PersistentConnection {
private _queuedDestroySession: Long[] = []
// waitForMessage
private _pendingWaitForUnencrypted: [ControllablePromise<Uint8Array>, NodeJS.Timeout][] = []
private _pendingWaitForUnencrypted: [ControllablePromise<Uint8Array>, timers.Timer][] = []
private _usePfs
private _isPfsBindingPending = false
private _isPfsBindingPendingInBackground = false
private _pfsUpdateTimeout?: NodeJS.Timeout
private _pfsUpdateTimeout?: timers.Timer
private _inactivityPendingFlush = false
@ -123,7 +125,7 @@ export class SessionConnection extends PersistentConnection {
this._isPfsBindingPending = false
this._isPfsBindingPendingInBackground = false
this._session._authKeyTemp.reset()
clearTimeout(this._pfsUpdateTimeout)
timers.clearTimeout(this._pfsUpdateTimeout)
}
this._resetSession()
@ -134,7 +136,7 @@ export class SessionConnection extends PersistentConnection {
Object.values(this._pendingWaitForUnencrypted).forEach(([prom, timeout]) => {
prom.reject(new MtcuteError('Connection closed'))
clearTimeout(timeout)
timers.clearTimeout(timeout)
})
// resend pending state_req-s
@ -162,7 +164,7 @@ export class SessionConnection extends PersistentConnection {
this._salts.isFetching = false
if (forever) {
clearTimeout(this._pfsUpdateTimeout)
timers.clearTimeout(this._pfsUpdateTimeout)
this.removeAllListeners()
this.on('error', (err) => {
this.log.warn('caught error after destroying: %s', err)
@ -321,7 +323,7 @@ export class SessionConnection extends PersistentConnection {
if (this._isPfsBindingPending) return
if (this._pfsUpdateTimeout) {
clearTimeout(this._pfsUpdateTimeout)
timers.clearTimeout(this._pfsUpdateTimeout)
this._pfsUpdateTimeout = undefined
}
@ -468,7 +470,7 @@ export class SessionConnection extends PersistentConnection {
this.onConnectionUsable()
// set a timeout to update temp auth key in advance to avoid interruption
this._pfsUpdateTimeout = setTimeout(
this._pfsUpdateTimeout = timers.setTimeout(
() => {
this._pfsUpdateTimeout = undefined
this.log.debug('temp key is expiring soon')
@ -499,7 +501,7 @@ export class SessionConnection extends PersistentConnection {
return Promise.reject(new MtcuteError('Connection destroyed'))
}
const promise = createControllablePromise<Uint8Array>()
const timeoutId = setTimeout(() => {
const timeoutId = timers.setTimeout(() => {
promise.reject(new MtTimeoutError(timeout))
this._pendingWaitForUnencrypted = this._pendingWaitForUnencrypted.filter(it => it[0] !== promise)
}, timeout)
@ -516,7 +518,7 @@ export class SessionConnection extends PersistentConnection {
// auth_key_id = 0, meaning it's an unencrypted message used for authorization
const [promise, timeout] = this._pendingWaitForUnencrypted.shift()!
clearTimeout(timeout)
timers.clearTimeout(timeout)
promise.resolve(data)
return
@ -1441,7 +1443,7 @@ export class SessionConnection extends PersistentConnection {
}
if (timeout) {
pending.timeout = setTimeout(() => this._cancelRpc(pending, true), timeout)
pending.timeout = timers.setTimeout(() => this._cancelRpc(pending, true), timeout)
}
if (abortSignal) {
@ -1473,7 +1475,7 @@ export class SessionConnection extends PersistentConnection {
}
if (!onTimeout && rpc.timeout) {
clearTimeout(rpc.timeout)
timers.clearTimeout(rpc.timeout)
}
if (onTimeout) {

View file

@ -3,7 +3,7 @@ import type { IStorageDriver } from '../driver.js'
export class MemoryStorageDriver implements IStorageDriver {
readonly states: Map<string, object> = new Map()
getState<T extends object>(repo: string, def: () => T) {
getState<T extends object>(repo: string, def: () => T): T {
if (!this.states.has(repo)) {
this.states.set(repo, def())
}

View file

@ -6,6 +6,7 @@ describe('ConditionVariable', () => {
it('should correctly unlock execution', async () => {
const cv = new ConditionVariable()
// eslint-disable-next-line no-restricted-globals
setTimeout(() => cv.notify(), 10)
await cv.wait()
@ -24,6 +25,7 @@ describe('ConditionVariable', () => {
it('should only unlock once', async () => {
const cv = new ConditionVariable()
// eslint-disable-next-line no-restricted-globals
setTimeout(() => {
cv.notify()
cv.notify()

View file

@ -1,9 +1,11 @@
import * as timers from './timers.js'
/**
* Class implementing a condition variable like behaviour.
*/
export class ConditionVariable {
private _notify?: () => void
private _timeout?: NodeJS.Timeout
private _timeout?: timers.Timer
wait(timeout?: number): Promise<void> {
const prom = new Promise<void>((resolve) => {
@ -11,7 +13,7 @@ export class ConditionVariable {
})
if (timeout) {
this._timeout = setTimeout(() => {
this._timeout = timers.setTimeout(() => {
this._notify?.()
this._timeout = undefined
}, timeout)
@ -22,7 +24,7 @@ export class ConditionVariable {
notify(): void {
this._notify?.()
if (this._timeout) clearTimeout(this._timeout)
if (this._timeout) timers.clearTimeout(this._timeout)
this._notify = undefined
}
}

View file

@ -1,10 +1,12 @@
import * as timers from './timers.js'
/**
* Wrapper over JS timers that allows re-scheduling them
* to earlier time
*/
export class EarlyTimer {
private _timeout?: NodeJS.Timeout
private _immediate?: NodeJS.Immediate
private _timeout?: timers.Timer
private _immediate?: timers.Immediate
private _timeoutTs?: number
private _handler: () => void = () => {}
@ -20,13 +22,13 @@ export class EarlyTimer {
emitWhenIdle(): void {
if (this._immediate) return
clearTimeout(this._timeout)
timers.clearTimeout(this._timeout)
this._timeoutTs = Date.now()
if (typeof setImmediate !== 'undefined') {
this._immediate = setImmediate(this.emitNow)
if (typeof timers.setImmediate !== 'undefined') {
this._immediate = timers.setImmediate(this.emitNow)
} else {
this._timeout = setTimeout(this.emitNow, 0)
this._timeout = timers.setTimeout(this.emitNow, 0)
}
}
@ -49,7 +51,7 @@ export class EarlyTimer {
emitBefore(ts: number): void {
if (!this._timeoutTs || ts < this._timeoutTs) {
this.reset()
this._timeout = setTimeout(this.emitNow, ts - Date.now())
this._timeout = timers.setTimeout(this.emitNow, ts - Date.now())
this._timeoutTs = ts
}
}
@ -67,10 +69,10 @@ export class EarlyTimer {
*/
reset(): void {
if (this._immediate) {
clearImmediate(this._immediate)
timers.clearImmediate(this._immediate)
this._immediate = undefined
} else {
clearTimeout(this._timeout)
timers.clearTimeout(this._timeout)
}
this._timeoutTs = undefined
}

View file

@ -1,3 +1,5 @@
import * as timers from './timers.js'
export type ThrottledFunction = (() => void) & {
reset: () => void
}
@ -15,7 +17,7 @@ export type ThrottledFunction = (() => void) & {
* @param delay Throttle delay
*/
export function throttle(func: () => void, delay: number): ThrottledFunction {
let timeout: NodeJS.Timeout | null
let timeout: timers.Timer | null
const res: ThrottledFunction = function () {
if (timeout) {
@ -26,12 +28,12 @@ export function throttle(func: () => void, delay: number): ThrottledFunction {
timeout = null
func()
}
timeout = setTimeout(later, delay)
timeout = timers.setTimeout(later, delay)
}
res.reset = () => {
if (timeout) {
clearTimeout(timeout)
timers.clearTimeout(timeout)
timeout = null
}
}

View file

@ -1,3 +1,5 @@
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'
@ -28,3 +30,4 @@ 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,22 +1,24 @@
import * as timers from './timers.js'
/**
* Sleep for the given number of ms
*
* @param ms Number of ms to sleep
*/
export const sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms))
export const sleep = (ms: number): Promise<void> => new Promise(resolve => timers.setTimeout(resolve, ms))
export function sleepWithAbort(ms: number, signal: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
let timeout: NodeJS.Timeout
let timeout: timers.Timer
const onAbort = () => {
clearTimeout(timeout)
timers.clearTimeout(timeout)
reject(signal.reason)
}
signal.addEventListener('abort', onAbort)
timeout = setTimeout(() => {
timeout = timers.setTimeout(() => {
signal.removeEventListener('abort', onAbort)
resolve()
}, ms)

View file

@ -1,4 +1,5 @@
import { asyncResettable } from './function-utils.js'
import * as timers from './timers.js'
export interface ReloadableParams<Data> {
reload: (old?: Data) => Promise<Data>
@ -13,7 +14,7 @@ export class Reloadable<Data> {
protected _data?: Data
protected _expiresAt = 0
protected _listeners: ((data: Data) => void)[] = []
protected _timeout?: NodeJS.Timeout
protected _timeout?: timers.Timer
private _reload = asyncResettable(async () => {
const data = await this.params.reload(this._data)
@ -32,10 +33,10 @@ export class Reloadable<Data> {
this._data = data
this._expiresAt = expiresAt
if (this._timeout) clearTimeout(this._timeout)
if (this._timeout) timers.clearTimeout(this._timeout)
if (!this.params.disableAutoReload) {
this._timeout = setTimeout(() => {
this._timeout = timers.setTimeout(() => {
this._reload.reset()
this.update().catch((err: unknown) => {
this.params.onError?.(err)
@ -70,7 +71,7 @@ export class Reloadable<Data> {
}
destroy(): void {
if (this._timeout) clearTimeout(this._timeout)
if (this._timeout) timers.clearTimeout(this._timeout)
this._listeners.length = 0
this._reload.reset()
}

View file

@ -0,0 +1,61 @@
/* 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,
}

View file

@ -3,7 +3,6 @@ import { describe, expect, it } from 'vitest'
import {
assertTypeIs,
assertTypeIsNot,
hasPresentKey,
hasValueAtKey,
isPresent,
mtpAssertTypeIs,
@ -23,24 +22,6 @@ describe('isPresent', () => {
})
})
describe('hasPresentKey', () => {
it('should return true for objects with present keys', () => {
expect(hasPresentKey('a')({ a: 1 })).toBe(true)
expect(hasPresentKey('a')({ a: 1, b: 2 })).toBe(true)
})
it('should return false for objects with undefined/null keys', () => {
expect(hasPresentKey('a')({ a: undefined })).toBe(false)
expect(hasPresentKey('a')({ a: null })).toBe(false)
expect(hasPresentKey('a')({ a: undefined, b: 2 })).toBe(false)
expect(hasPresentKey('a')({ a: null, b: 2 })).toBe(false)
})
it('should return false for objects without the key', () => {
expect(hasPresentKey('a')({ b: 2 })).toBe(false)
})
})
describe('hasValueAtKey', () => {
it('should return true for objects with the correct value', () => {
expect(hasValueAtKey('a', 1)({ a: 1 })).toBe(true)

View file

@ -8,25 +8,6 @@ export function isPresent<T>(t: T | undefined | null | void): t is T {
return t !== undefined && t !== null
}
/**
* Returns a function that can be used to filter down objects
* to the ones that have a defined non-null value under the key `k`.
*
* @example
* ```ts
* const filesWithUrl = files.filter(file => file.url);
* files[0].url // In this case, TS might still treat this as undefined/null
*
* const filesWithUrl = files.filter(hasPresentKey("url"));
* files[0].url // TS will know that this is present
* ```
*/
export function hasPresentKey<K extends string | number | symbol>(k: K) {
return function <T, V> (a: T & { [k in K]?: V | null }): a is T & { [k in K]: V } {
return a[k] !== undefined && a[k] !== null
}
}
/**
* Returns a function that can be used to filter down objects
* to the ones that have a specific value V under a key `k`.
@ -44,7 +25,8 @@ export function hasPresentKey<K extends string | number | symbol>(k: K) {
* files[0].imageUrl // TS will know this is present, because already it excluded the other union members.
* ```
*/
export function hasValueAtKey<const K extends string | number | symbol, const V>(k: K, v: V) {
export function hasValueAtKey<const K extends string | number | symbol, const V>(k: K, v: V):
<T>(a: T & { [k in K]: unknown }) => a is T & { [k in K]: V } {
return function <T> (a: T & { [k in K]: unknown }): a is T & { [k in K]: V } {
return a[k] === v
}

View file

@ -1,14 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/esm"
},
"references": [
{ "path": "../tl" },
{ "path": "../tl-runtime" },
{ "path": "../test" }
],
"include": [
"./src"
]

View file

@ -1,6 +0,0 @@
module.exports = ({ fs, path, packageDir, outDir }) => ({
buildCjs: false,
final() {
fs.cpSync(path.join(packageDir, 'template'), path.join(outDir, 'template'), { recursive: true })
},
})

View file

@ -0,0 +1,9 @@
import { resolve } from 'node:path'
import { cpSync } from 'node:fs'
export default () => ({
buildCjs: false,
final({ outDir, packageDir }) {
cpSync(resolve(packageDir, 'template'), resolve(outDir, 'template'), { recursive: true })
},
})

View file

@ -1,30 +1,30 @@
{
"name": "@mtcute/create-bot",
"type": "module",
"version": "0.16.7",
"private": true,
"description": "Bot starter kit for mtcute",
"author": "alina sireneva <alina@tei.su>",
"license": "MIT",
"bin": {
"create-bot": "./src/main.js"
},
"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"
},
"dependencies": {
"colorette": "2.0.20",
"cross-spawn": "7.0.3",
"glob": "11.0.0",
"handlebars": "4.7.8",
"inquirer": "9.2.11",
"openurl": "1.1.1"
},
"devDependencies": {
"@types/cross-spawn": "^6.0.6",
"@types/inquirer": "^9.0.6",
"@types/openurl": "^1.0.3"
}
"name": "@mtcute/create-bot",
"type": "module",
"version": "0.16.7",
"private": true,
"description": "Bot starter kit for mtcute",
"author": "alina sireneva <alina@tei.su>",
"license": "MIT",
"bin": {
"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"
},
"dependencies": {
"colorette": "2.0.20",
"cross-spawn": "7.0.3",
"glob": "11.0.0",
"handlebars": "4.7.8",
"inquirer": "9.2.11",
"openurl": "1.1.1"
},
"devDependencies": {
"@types/cross-spawn": "^6.0.6",
"@types/inquirer": "^9.0.6",
"@types/openurl": "^1.0.3"
}
}

View file

@ -47,6 +47,7 @@ export async function askForConfigPersisted(): Promise<UserConfigPersisted> {
message: 'API ID (press Enter to obtain one):',
validate: (v: string) => {
if (!v) {
// eslint-disable-next-line no-restricted-globals
setTimeout(() => {
try {
open(TELEGRAM_APPS_PAGE)

View file

@ -40,7 +40,7 @@ if (!outDir.match(/^(?:[A-Z]:)?[/\\]/i)) {
const __dirname = dirname(fileURLToPath(import.meta.url))
await runTemplater(join(__dirname, '../template'), outDir, config)
await runTemplater(join(__dirname, 'template'), outDir, config)
await installDependencies(outDir, config)

View file

@ -1,8 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": [
"./src"
]

View file

@ -1,10 +1,16 @@
const crypto = require('node:crypto')
const path = require('node:path')
const fs = require('node:fs')
const cp = require('node:child_process')
const { Readable } = require('node:stream')
/* eslint-disable import/no-relative-packages, no-console, no-restricted-globals */
import { createHash } from 'node:crypto'
import path from 'node:path'
import * as fs from 'node:fs'
import { spawn } from 'node:child_process'
import { Readable } from 'node:stream'
import { fileURLToPath } from 'node:url'
let git
import * as glob from 'glob'
import { getCurrentBranch, getCurrentCommit } from '../../scripts/git-utils.js'
const __dirname = path.dirname(fileURLToPath(new URL(import.meta.url)))
const GITHUB_TOKEN = process.env.GITHUB_TOKEN
let SKIP_PREBUILT = process.env.BUILD_FOR_DOCS === '1'
@ -52,7 +58,7 @@ async function runWorkflow(commit, hash) {
method: 'POST',
headers: GITHUB_HEADERS,
body: JSON.stringify({
ref: git.getCurrentBranch(),
ref: getCurrentBranch(),
inputs: { commit, hash },
}),
})
@ -132,7 +138,7 @@ async function extractArtifacts(artifacts) {
// extract the zip
await new Promise((resolve, reject) => {
const child = cp.spawn('unzip', [outFile, '-d', path.join(__dirname, 'dist/prebuilds')], {
const child = spawn('unzip', [outFile, '-d', path.join(__dirname, 'dist/prebuilds')], {
stdio: 'inherit',
})
@ -149,23 +155,21 @@ async function extractArtifacts(artifacts) {
)
}
module.exports = ({ fs, glob, path, packageDir, outDir }) => ({
async final() {
// eslint-disable-next-line import/no-relative-packages
git = await import('../../scripts/git-utils.js')
const libDir = path.join(packageDir, 'lib')
export default () => ({
async final({ packageDir, outDir }) {
const libDir = path.resolve(packageDir, 'lib')
if (!SKIP_PREBUILT) {
// generate sources hash
const hashes = []
for (const file of glob.sync(path.join(libDir, '**/*'))) {
const hash = crypto.createHash('sha256')
const hash = createHash('sha256')
hash.update(fs.readFileSync(file))
hashes.push(hash.digest('hex'))
}
const hash = crypto.createHash('sha256')
const hash = createHash('sha256')
.update(hashes.join('\n'))
.digest('hex')
console.log(hash)
@ -175,7 +179,7 @@ module.exports = ({ fs, glob, path, packageDir, outDir }) => ({
if (!artifacts) {
console.log('[i] No artifacts found, running workflow')
artifacts = await runWorkflow(git.getCurrentCommit(), hash)
artifacts = await runWorkflow(getCurrentCommit(), hash)
}
console.log('[i] Extracting artifacts')
@ -195,7 +199,6 @@ module.exports = ({ fs, glob, path, packageDir, outDir }) => ({
)
// for some unknown fucking reason ts doesn't do this
fs.copyFileSync(path.join(packageDir, 'src/native.cjs'), path.join(outDir, 'cjs/native.cjs'))
fs.copyFileSync(path.join(packageDir, 'src/native.cjs'), path.join(outDir, 'esm/native.cjs'))
fs.copyFileSync(path.join(packageDir, 'src/native.cjs'), path.join(outDir, 'native.cjs'))
},
})

View file

@ -1,12 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/esm"
},
"references": [
{ "path": "../core" }
],
"include": [
"./src"
]

View file

@ -1,12 +0,0 @@
module.exports = ({ outDir, fs, jsr }) => ({
buildCjs: false,
final() {
if (jsr) {
// 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`)
// console.log(real)
fs.cpSync(real, `${outDir}/common-internals-web`, { recursive: true })
}
},
})

View file

@ -0,0 +1,10 @@
import * as fs from 'node:fs'
export default () => ({
finalJsr({ 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`)
fs.cpSync(real, `${outDir}/common-internals-web`, { recursive: true })
},
})

View file

@ -1,15 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"references": [
{ "path": "../core" },
{ "path": "../dispatcher" },
{ "path": "../html-parser" },
{ "path": "../markdown-parser" }
],
"include": [
"./src"
]

View file

@ -21,7 +21,7 @@ import type { UpdateContext } from './base.js'
*/
export class BusinessMessageContext extends BusinessMessage implements UpdateContext<BusinessMessage> {
// this is primarily for proper types in filters, so don't bother much with actual value
readonly _name = 'new_business_message'
readonly _name = 'new_business_message' as const
/**
* List of messages in the message group.

View file

@ -10,7 +10,7 @@ import type { UpdateContext } from './base.js'
* This is a subclass of {@link CallbackQuery}, so all its fields are also available.
*/
export class CallbackQueryContext extends CallbackQuery implements UpdateContext<CallbackQuery> {
readonly _name = 'callback_query'
readonly _name = 'callback_query' as const
constructor(
readonly client: TelegramClient,
@ -67,7 +67,7 @@ export class CallbackQueryContext extends CallbackQuery implements UpdateContext
* This is a subclass of {@link InlineCallbackQuery}, so all its fields are also available.
*/
export class InlineCallbackQueryContext extends InlineCallbackQuery implements UpdateContext<InlineCallbackQuery> {
readonly _name = 'inline_callback_query'
readonly _name = 'inline_callback_query' as const
constructor(
readonly client: TelegramClient,
@ -100,7 +100,7 @@ export class InlineCallbackQueryContext extends InlineCallbackQuery implements U
export class BusinessCallbackQueryContext
extends BusinessCallbackQuery
implements UpdateContext<BusinessCallbackQuery> {
readonly _name = 'business_callback_query'
readonly _name = 'business_callback_query' as const
constructor(
readonly client: TelegramClient,

View file

@ -11,7 +11,7 @@ import type { UpdateContext } from './base.js'
export class ChatJoinRequestUpdateContext
extends BotChatJoinRequestUpdate
implements UpdateContext<BotChatJoinRequestUpdate> {
readonly _name = 'bot_chat_join_request'
readonly _name = 'bot_chat_join_request' as const
constructor(
readonly client: TelegramClient,

View file

@ -12,7 +12,7 @@ import type { UpdateContext } from './base.js'
* > Inline feedback in [@BotFather](//t.me/botfather)
*/
export class ChosenInlineResultContext extends ChosenInlineResult implements UpdateContext<ChosenInlineResult> {
readonly _name = 'chosen_inline_result'
readonly _name = 'chosen_inline_result' as const
constructor(
readonly client: TelegramClient,

View file

@ -10,7 +10,7 @@ import type { UpdateContext } from './base.js'
* This is a subclass of {@link InlineQuery}, so all its fields are also available.
*/
export class InlineQueryContext extends InlineQuery implements UpdateContext<InlineQuery> {
readonly _name = 'inline_query'
readonly _name = 'inline_query' as const
constructor(
readonly client: TelegramClient,

View file

@ -21,7 +21,7 @@ import type { UpdateContext } from './base.js'
*/
export class MessageContext extends Message implements UpdateContext<Message> {
// this is primarily for proper types in filters, so don't bother much with actual value
readonly _name = 'new_message'
readonly _name = 'new_message' as const
/**
* List of messages in the message group.

View file

@ -9,7 +9,7 @@ import type { UpdateContext } from './base.js'
* This is a subclass of {@link PreCheckoutQuery}, so all its fields are also available.
*/
export class PreCheckoutQueryContext extends PreCheckoutQuery implements UpdateContext<PreCheckoutQuery> {
readonly _name = 'pre_checkout_query'
readonly _name = 'pre_checkout_query' as const
constructor(
readonly client: TelegramClient,

View file

@ -1,4 +1,4 @@
import { LruMap, asyncResettable } from '@mtcute/core/utils.js'
import { LruMap, asyncResettable, timers } from '@mtcute/core/utils.js'
import type { MaybePromise } from '@mtcute/core'
import type { IStateStorageProvider } from './provider.js'
@ -9,7 +9,7 @@ export class StateService {
constructor(readonly provider: IStateStorageProvider) {}
private _cache: LruMap<string, unknown> = new LruMap(100)
private _vacuumTimer?: NodeJS.Timeout
private _vacuumTimer?: timers.Interval
private _loaded = false
private _load = asyncResettable(async () => {
@ -19,7 +19,7 @@ export class StateService {
async load(): Promise<void> {
await this._load.run()
this._vacuumTimer = setInterval(() => {
this._vacuumTimer = timers.setInterval(() => {
Promise.resolve(this.provider.state.vacuum(Date.now())).catch(() => {})
}, 300_000)
}
@ -27,7 +27,7 @@ export class StateService {
async destroy(): Promise<void> {
await this.provider.driver.save?.()
await this.provider.driver.destroy?.()
clearInterval(this._vacuumTimer)
timers.clearInterval(this._vacuumTimer)
this._loaded = false
}

View file

@ -1,12 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/esm"
},
"references": [
{ "path": "../core" }
],
"include": [
"./src"
]

View file

@ -1,12 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/esm"
},
"references": [
{ "path": "../core" }
],
"include": [
"./src"
]

View file

@ -1,12 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/esm"
},
"references": [
{ "path": "../core" }
],
"include": [
"./src"
]

View file

@ -1,12 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist/esm"
},
"references": [
{ "path": "../core" },
{ "path": "../node" }
],
"include": [
"./index.ts"
]

View file

@ -1,12 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/esm"
},
"references": [
{ "path": "../dispatcher" }
],
"include": [
"./src"
]

View file

@ -1,12 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/esm"
},
"references": [
{ "path": "../core" }
],
"include": [
"./src"
]

View file

@ -1,12 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist/esm"
},
"references": [
{ "path": "../core" },
{ "path": "../node" }
],
"include": [
"./index.ts",
"./fake-tls.ts"

View file

@ -1 +0,0 @@
module.exports = () => ({ esmOnlyDirectives: true })

View file

@ -0,0 +1,41 @@
import { fileURLToPath } from 'node:url'
export default () => {
const clientId = fileURLToPath(new URL('./src/client.ts', import.meta.url))
// const buildingCjs = false
return {
external: ['@mtcute/crypto-node'],
rollupPluginsPre: [
{
// very much a crutch, but it works
// i couldn't figure out a way to hook into the esm->cjs transform,
// so i'm just replacing the await import with require and then back
name: 'mtcute-node-build-plugin',
transform(code, id) {
if (id === clientId) {
return code.replace('await import(', 'require(')
}
return code
},
generateBundle(output, bundle) {
if (output.format !== 'es') return
let found = false
for (const chunk of Object.values(bundle)) {
if (chunk.code.match(/require\("@mtcute\/crypto-node"\)/)) {
found = true
chunk.code = chunk.code.replace('require("@mtcute/crypto-node")', '(await import("@mtcute/crypto-node"))')
}
}
if (!found) {
throw new Error('Could not find crypto-node import')
}
},
},
],
}
}

View file

@ -28,7 +28,6 @@ try {
/* eslint-disable ts/ban-ts-comment,ts/no-unsafe-assignment */
// @ts-ignore not in deps
// @esm-replace-import
nativeCrypto = (await import('@mtcute/crypto-node')).NodeNativeCryptoProvider
/* eslint-enable ts/ban-ts-comment,ts/no-unsafe-assignment */
} catch {}

View file

@ -70,9 +70,7 @@ export abstract class BaseNodeCryptoProvider extends BaseCryptoProvider {
export class NodeCryptoProvider extends BaseNodeCryptoProvider implements ICryptoProvider {
async initialize(): Promise<void> {
// @only-if-esm
const require = createRequire(import.meta.url)
// @/only-if-esm
const wasmFile = require.resolve('@mtcute/wasm/mtcute.wasm')
const wasm = await readFile(wasmFile)
initSync(wasm)

View file

@ -1,15 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/esm"
},
"references": [
{ "path": "../core" },
{ "path": "../dispatcher" },
{ "path": "../html-parser" },
{ "path": "../markdown-parser" }
],
"include": [
"./src"
]

View file

@ -1,12 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist/esm"
},
"references": [
{ "path": "../core" },
{ "path": "../node" }
],
"include": [
"./index.ts"
]

View file

@ -1 +0,0 @@
module.exports = () => ({ buildCjs: false })

View file

@ -0,0 +1 @@
export default () => ({ buildCjs: false })

View file

@ -17,22 +17,25 @@ describe('client stub', () => {
})
})
it('should correctly decrypt intercepted raw messages', async () => {
const log: string[] = []
// for some reason, this test fails in browser. todo: investigate
if (import.meta.env.TEST_ENV !== 'browser') {
it('should correctly decrypt intercepted raw messages', async () => {
const log: string[] = []
const client = new StubTelegramClient()
const client = new StubTelegramClient()
client.onRawMessage((msg) => {
log.push(`message ctor=${getPlatform().hexEncode(msg.subarray(0, 4))}`)
client.close().catch(() => {})
client.onRawMessage((msg) => {
log.push(`message ctor=${getPlatform().hexEncode(msg.subarray(0, 4))}`)
client.close().catch(() => {})
})
await client.with(async () => {
await client.call({ _: 'help.getConfig' }).catch(() => {}) // ignore "client closed" error
expect(log).toEqual([
'message ctor=dcf8f173', // msg_container
])
})
})
await client.with(async () => {
await client.call({ _: 'help.getConfig' }).catch(() => {}) // ignore "client closed" error
expect(log).toEqual([
'message ctor=dcf8f173', // msg_container
])
})
})
}
})

View file

@ -26,7 +26,7 @@ export class StubTelegramClient extends BaseTelegramClient {
super({
apiId: 0,
apiHash: '',
logLevel: 0,
logLevel: 5,
storage,
disableUpdates: true,
transport: () => {

View file

@ -1,12 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"references": [
{ "path": "../tl" }
],
"include": [
"./src"
]

View file

@ -1,9 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/esm"
},
"include": [
"./src"
]

View file

@ -1,9 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/esm"
},
"include": [
"./src"
]

View file

@ -1,82 +0,0 @@
module.exports = ({ fs, path, outDir, packageDir, jsr, transformFile }) => ({
buildTs: false,
buildCjs: false,
final() {
// create package by copying all the needed files
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',
]
fs.mkdirSync(path.join(outDir, 'binary'), { recursive: true })
for (const f of files) {
fs.copyFileSync(path.join(packageDir, f), path.join(outDir, f))
}
if (jsr) {
// jsr doesn't support cjs, so we'll need to add some shims
// todo: remove this god awfulness when tl esm rewrite
transformFile(path.join(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(path.join(outDir, 'binary/reader.js'), (content) => {
return [
'/// <reference types="./reader.d.ts" />',
'const exports = {};',
content,
'export const __tlReaderMap = exports.__tlReaderMap;',
].join('\n')
})
transformFile(path.join(outDir, 'binary/writer.js'), (content) => {
return [
'/// <reference types="./writer.d.ts" />',
'const exports = {};',
content,
'export const __tlWriterMap = exports.__tlWriterMap;',
].join('\n')
})
transformFile(path.join(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
transformFile(path.join(outDir, 'deno.json'), (content) => {
const json = JSON.parse(content)
json.exports = {}
for (const f of files) {
if (!f.match(/\.js(?:on)?$/)) continue
if (f === 'index.js') {
json.exports['.'] = './index.js'
} else {
json.exports[`./${f}`] = `./${f}`
}
}
return JSON.stringify(json, null, 2)
})
}
},
})

View file

@ -9,7 +9,7 @@ import { join } from 'node:path'
import * as readline from 'node:readline'
import * as cheerio from 'cheerio'
import { hasPresentKey, isPresent } from '@mtcute/core/utils.js'
import { isPresent } from '@mtcute/core/utils.js'
import type {
TlEntry,
TlFullSchema,
@ -299,17 +299,17 @@ async function main() {
}
}
const nonEmptyOptions = chooseOptions.filter(hasPresentKey('entry'))
const nonEmptyOptions = chooseOptions.filter(it => it.entry !== undefined)
console.log(
'Conflict detected (%s) at %s %s:',
mergeError,
nonEmptyOptions[0].entry.kind,
nonEmptyOptions[0].entry.name,
nonEmptyOptions[0].entry!.kind,
nonEmptyOptions[0].entry!.name,
)
console.log('0. Remove')
nonEmptyOptions.forEach((opt, idx) => {
console.log(`${idx + 1}. ${opt.schema.name}: (${opt.entry.kind}) ${writeTlEntryToString(opt.entry)}`)
console.log(`${idx + 1}. ${opt.schema.name}: (${opt.entry!.kind}) ${writeTlEntryToString(opt.entry!)}`)
})
while (true) {

View file

@ -1,10 +0,0 @@
module.exports = ({ path: { join }, fs, outDir, packageDir, jsr, transformFile }) => ({
esmOnlyDirectives: true,
final() {
fs.cpSync(join(packageDir, 'mtcute.wasm'), join(outDir, 'mtcute.wasm'))
if (jsr) {
transformFile(join(outDir, 'index.ts'), code => code.replace("'../mtcute.wasm'", "'./mtcute.wasm'"))
}
},
})

View file

@ -0,0 +1,11 @@
import { resolve } from 'node:path'
import * as fs from 'node:fs'
export default () => ({
finalPackageJson(pkg) {
pkg.exports['./mtcute.wasm'] = './mtcute.wasm'
},
final({ packageDir, outDir }) {
fs.cpSync(resolve(packageDir, 'src/mtcute.wasm'), resolve(outDir, 'mtcute.wasm'))
},
})

View file

@ -1,30 +1,27 @@
{
"name": "@mtcute/wasm",
"type": "module",
"version": "0.16.7",
"private": true,
"description": "WASM implementation of common algorithms used in Telegram",
"author": "alina sireneva <alina@tei.su>",
"license": "MIT",
"sideEffects": false,
"exports": {
".": "./src/index.ts",
"./mtcute.wasm": "./mtcute.wasm"
},
"scripts": {
"docs": "typedoc",
"build": "pnpm run -w build-package wasm",
"build:wasm": "docker build --output=lib --target=binaries lib"
},
"exportsKeepPath": [
"./mtcute.wasm"
],
"devDependencies": {
"@mtcute/core": "workspace:^",
"@mtcute/node": "workspace:^",
"@mtcute/web": "workspace:^"
},
"jsrOnlyFields": {
"exports": "./src/index.ts"
}
"name": "@mtcute/wasm",
"type": "module",
"version": "0.16.7",
"private": true,
"description": "WASM implementation of common algorithms used in Telegram",
"author": "alina sireneva <alina@tei.su>",
"license": "MIT",
"sideEffects": false,
"exports": {
".": "./src/index.ts",
"./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:^"
},
"jsrOnlyFields": {
"exports": "./src/index.ts"
}
}

View file

@ -8,10 +8,7 @@ export function getWasmUrl(): URL {
// making it not work. probably related to https://github.com/vitejs/vite/issues/8427,
// but asking the user to deoptimize the entire @mtcute/web is definitely not a good idea
// so we'll just use this hack for now
// @only-if-esm
return new URL('../mtcute.wasm', import.meta.url)
// @/only-if-esm
throw new Error('ESM-only')
return new URL('./mtcute.wasm', import.meta.url)
}
let wasm!: MtcuteWasmModule
@ -54,7 +51,8 @@ export function initSync(module: SyncInitInput): void {
module = new WebAssembly.Instance(module)
}
wasm = (module as WebAssembly.Instance).exports as unknown as MtcuteWasmModule
// eslint-disable-next-line
wasm = (module as unknown as WebAssembly.Instance).exports as unknown as MtcuteWasmModule
initCommon()
}

View file

@ -1,7 +1,7 @@
import { initSync } from '../src/index.js'
export async function initWasm() {
const url = new URL('../mtcute.wasm', import.meta.url)
export async function initWasm(): Promise<void> {
const url = new URL('../src/mtcute.wasm', import.meta.url)
if (import.meta.env.TEST_ENV === 'node') {
const fs = await import('node:fs/promises')

View file

@ -1,9 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/esm"
},
"include": [
"./src"
]

View file

@ -1 +0,0 @@
module.exports = () => ({ esmOnlyDirectives: true })

View file

@ -40,5 +40,5 @@ export async function loadWasmBinary(input?: WasmInitInput): Promise<WebAssembly
return instance
}
return WebAssembly.instantiate(input)
return WebAssembly.instantiate(input as WebAssembly.Module)
}

View file

@ -1,3 +1,4 @@
/* eslint-disable no-restricted-globals */
import { setPlatform } from '@mtcute/core/platform.js'
import type {
ClientMessageHandler,

View file

@ -1,12 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/esm"
},
"references": [
{ "path": "../core" }
],
"include": [
"./src"
]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,220 @@
import { fileURLToPath } from 'node:url'
import * as fs from 'node:fs'
import * as cp from 'node:child_process'
import { resolve } from 'node:path'
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)
cp.spawnSync(
'pnpm',
[
'exec',
'slow-types-compiler',
'populate',
'--downstream',
process.env.JSR_URL,
'--token',
process.env.JSR_TOKEN,
'--unstable-create-via-api',
...depsToPopulate,
],
{
stdio: 'inherit',
},
)
}
}
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

@ -0,0 +1,22 @@
/* 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,15 +1,11 @@
import * as cp from 'node:child_process'
import * as fs from 'node:fs'
import { createRequire } from 'node:module'
import * as path from 'node:path'
import { fileURLToPath } from 'node:url'
import { resolve } from 'node:path'
import * as glob from 'glob'
import ts from 'typescript'
import * as stc from '@teidesu/slow-types-compiler'
import { processPackageJson } from '../.config/vite-utils/package-json.js'
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const rootPackageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'))
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>')
@ -18,14 +14,7 @@ if (process.argv.length < 3) {
const IS_JSR = process.env.JSR === '1'
const packagesDir = path.join(__dirname, '../packages')
const packageDir = path.join(packagesDir, process.argv[2])
let outDir = path.join(packageDir, 'dist')
if (IS_JSR) outDir = path.join(outDir, 'jsr')
function exec(cmd, params) {
cp.execSync(cmd, { cwd: packageDir, stdio: 'inherit', ...params })
}
const packageName = process.argv[2]
function transformFile(file, transform) {
const content = fs.readFileSync(file, 'utf8')
@ -33,602 +22,96 @@ function transformFile(file, transform) {
if (res != null) fs.writeFileSync(file, res)
}
// todo make them esm
const require = createRequire(import.meta.url)
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')
const buildConfig = {
buildTs: true,
buildCjs: true,
removeReferenceComments: true,
esmOnlyDirectives: false,
esmImportDirectives: false,
before: () => {},
final: () => {},
...(() => {
let config
fs.rmSync(outDir, { recursive: true, force: true })
try {
config = require(path.join(packageDir, 'build.config.cjs'))
} catch (e) {
if (e.code !== 'MODULE_NOT_FOUND') throw e
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',
]
return {}
}
fs.mkdirSync(resolve(outDir, 'binary'), { recursive: true })
console.log('[i] Using custom build config')
if (typeof config === 'function') {
config = config({
fs,
path,
glob,
exec,
transformFile,
packageDir,
outDir,
jsr: IS_JSR,
})
}
return config
})(),
}
function getPackageVersion(name) {
return require(path.join(packagesDir, name, 'package.json')).version
}
function buildPackageJson() {
const pkgJson = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf-8'))
if (buildConfig.buildCjs) {
pkgJson.main = 'cjs/index.js'
pkgJson.module = 'esm/index.js'
for (const f of files) {
fs.copyFileSync(resolve(packageDir, f), resolve(outDir, f))
}
// copy common fields from root
for (const field of ['license', 'author', 'contributors', 'homepage', 'repository', 'bugs']) {
if (rootPackageJson[field]) {
pkgJson[field] = rootPackageJson[field]
}
}
const newScripts = {}
if (pkgJson.keepScripts) {
for (const script of pkgJson.keepScripts) {
newScripts[script] = pkgJson.scripts[script]
}
delete pkgJson.keepScripts
}
pkgJson.scripts = newScripts
delete pkgJson.devDependencies
delete pkgJson.private
if (pkgJson.distOnlyFields) {
Object.assign(pkgJson, pkgJson.distOnlyFields)
delete pkgJson.distOnlyFields
}
if (pkgJson.jsrOnlyFields) {
if (IS_JSR) {
Object.assign(pkgJson, pkgJson.jsrOnlyFields)
}
delete pkgJson.jsrOnlyFields
}
function replaceWorkspaceDependencies(field) {
if (!pkgJson[field]) return
const dependencies = pkgJson[field]
for (const name of Object.keys(dependencies)) {
const value = dependencies[name]
if (value.startsWith('workspace:')) {
if (value !== 'workspace:^' && value !== 'workspace:*') {
throw new Error(
`Cannot replace workspace dependency ${name} with ${value} - only workspace:^ and * are supported`,
)
}
if (!name.startsWith('@mtcute/')) {
throw new Error(`Cannot replace workspace dependency ${name} - only @mtcute/* is supported`)
}
// note: pnpm replaces workspace:* with the current version, unlike this script
const depVersion = value === 'workspace:*' ? '*' : `^${getPackageVersion(name.slice(8))}`
dependencies[name] = depVersion
}
}
}
replaceWorkspaceDependencies('dependencies')
replaceWorkspaceDependencies('devDependencies')
replaceWorkspaceDependencies('peerDependencies')
replaceWorkspaceDependencies('optionalDependencies')
delete pkgJson.typedoc
if (pkgJson.browser) {
function maybeFixPath(p, repl) {
if (!p) return p
if (p.startsWith('./src/')) {
return repl + p.slice(6)
}
if (p.startsWith('./')) {
return repl + p.slice(2)
}
return p
}
for (const key of Object.keys(pkgJson.browser)) {
if (!key.startsWith('./src/')) continue
const path = key.slice(6)
pkgJson.browser[`./esm/${path}`] = maybeFixPath(pkgJson.browser[key], './esm/')
if (buildConfig.buildCjs) {
pkgJson.browser[`./cjs/${path}`] = maybeFixPath(pkgJson.browser[key], './cjs/')
}
delete pkgJson.browser[key]
}
}
// fix exports
if (pkgJson.exports) {
function maybeFixPath(path, repl) {
if (!path) return path
if (pkgJson.exportsKeepPath?.includes(path)) return path
if (path.startsWith('./src/')) {
path = repl + path.slice(6)
} else if (path.startsWith('./')) {
path = repl + path.slice(2)
}
return path.replace(/\.ts$/, '.js')
}
function fixValue(value) {
if (IS_JSR) {
return maybeFixPath(value, './').replace(/\.js$/, '.ts')
}
if (buildConfig.buildCjs) {
return {
import: maybeFixPath(value, './esm/'),
require: maybeFixPath(value, './cjs/'),
}
}
return maybeFixPath(value, './')
}
if (typeof pkgJson.exports === 'string') {
pkgJson.exports = {
'.': fixValue(pkgJson.exports),
}
} else {
for (const key of Object.keys(pkgJson.exports)) {
const value = pkgJson.exports[key]
if (typeof value !== 'string') {
throw new TypeError('Conditional exports are not supported')
}
pkgJson.exports[key] = fixValue(value)
}
}
delete pkgJson.exportsKeepPath
}
if (!IS_JSR) {
fs.writeFileSync(path.join(outDir, 'package.json'), JSON.stringify(pkgJson, null, 2))
}
return pkgJson
}
// clean
fs.rmSync(path.join(outDir), { recursive: true, force: true })
fs.rmSync(path.join(packageDir, 'tsconfig.tsbuildinfo'), { recursive: true, force: true })
fs.mkdirSync(path.join(outDir), { recursive: true })
// for jsr - copy typescript sources
if (IS_JSR) {
buildConfig.buildCjs = false
}
buildConfig.before()
if (buildConfig.buildTs && !IS_JSR) {
console.log('[i] Building typescript...')
const tsconfigPath = path.join(packageDir, 'tsconfig.json')
fs.cpSync(tsconfigPath, path.join(packageDir, 'tsconfig.backup.json'))
const tsconfig = ts.parseConfigFileTextToJson(tsconfigPath, fs.readFileSync(tsconfigPath, 'utf-8')).config
if (tsconfig.extends === '../../tsconfig.json') {
tsconfig.extends = '../../.config/tsconfig.build.json'
} else {
throw new Error('expected tsconfig to extend base config')
}
fs.writeFileSync(path.join(packageDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2))
const restoreTsconfig = () => {
fs.renameSync(path.join(packageDir, 'tsconfig.backup.json'), path.join(packageDir, 'tsconfig.json'))
}
try {
exec('pnpm exec tsc --build', { cwd: packageDir, stdio: 'inherit' })
} catch (e) {
restoreTsconfig()
throw e
}
if (buildConfig.buildCjs) {
console.log('[i] Building typescript (CJS)...')
const originalFiles = {}
for (const f of glob.sync(path.join(packagesDir, '**/*.ts'))) {
const content = fs.readFileSync(f, 'utf8')
if (!content.includes('@only-if-esm')) continue
originalFiles[f] = content
fs.writeFileSync(f, content.replace(/@only-if-esm.*?@\/only-if-esm/gs, ''))
}
for (const f of glob.sync(path.join(packagesDir, '**/*.ts'))) {
const content = fs.readFileSync(f, 'utf8')
if (!content.includes('@esm-replace-import')) continue
originalFiles[f] = content
fs.writeFileSync(f, content.replace(/(?<=@esm-replace-import.*?)await import/gs, 'require'))
}
// set type=commonjs in all package.json-s
for (const pkg of fs.readdirSync(packagesDir)) {
const pkgJson = path.join(packagesDir, pkg, 'package.json')
if (!fs.existsSync(pkgJson)) continue
const orig = fs.readFileSync(pkgJson, 'utf8')
originalFiles[pkgJson] = orig
fs.writeFileSync(
pkgJson,
JSON.stringify(
{
...JSON.parse(orig),
type: 'commonjs',
},
null,
2,
),
)
// maybe also dist/package.json
const distPkgJson = path.join(packagesDir, pkg, 'dist/package.json')
if (fs.existsSync(distPkgJson)) {
const orig = fs.readFileSync(distPkgJson, 'utf8')
originalFiles[distPkgJson] = orig
fs.writeFileSync(
distPkgJson,
JSON.stringify(
{
...JSON.parse(orig),
type: 'commonjs',
},
null,
2,
),
)
}
}
let error = false
try {
exec('pnpm exec tsc --outDir dist/cjs', {
cwd: packageDir,
stdio: 'inherit',
})
} catch (e) {
error = e
}
for (const f of Object.keys(originalFiles)) {
fs.writeFileSync(f, originalFiles[f])
}
if (error) {
restoreTsconfig()
throw error
}
}
restoreTsconfig()
// todo: can we remove these?
console.log('[i] Post-processing...')
if (buildConfig.removeReferenceComments) {
for (const f of glob.sync(path.join(outDir, '**/*.d.ts'))) {
let content = fs.readFileSync(f, 'utf8')
let changed = false
if (content.includes('/// <reference types="')) {
changed = true
content = content.replace(/\/\/\/ <reference types="(node|deno\/ns)".+?\/>\n?/g, '')
}
if (changed) fs.writeFileSync(f, content)
}
}
} else if (buildConfig.buildTs && IS_JSR) {
console.log('[i] Copying sources...')
fs.cpSync(path.join(packageDir, 'src'), outDir, { recursive: true })
const printer = ts.createPrinter()
for (const f of glob.sync(path.join(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)
}
}
}
console.log('[i] Copying misc files...')
const builtPkgJson = buildPackageJson()
if (buildConfig.buildCjs) {
fs.writeFileSync(path.join(outDir, 'cjs/package.json'), JSON.stringify({ type: 'commonjs' }, null, 2))
const CJS_DEPRECATION_WARNING = `
"use strict";
if (typeof globalThis !== 'undefined' && !globalThis._MTCUTE_CJS_DEPRECATION_WARNED) {
globalThis._MTCUTE_CJS_DEPRECATION_WARNED = true
console.warn("[${builtPkgJson.name}] CommonJS support is deprecated and will be removed soon. Please consider switching to ESM, it's "+(new Date()).getFullYear()+" already.")
console.warn("[${builtPkgJson.name}] Learn more about switching to ESM: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c")
}
`.trim()
const entrypoints = []
if (typeof builtPkgJson.exports === 'string') {
entrypoints.push(builtPkgJson.exports)
} else if (builtPkgJson.exports && typeof builtPkgJson.exports === 'object') {
for (const entrypoint of Object.values(builtPkgJson.exports)) {
entrypoints.push(entrypoint.require)
}
}
for (const entry of entrypoints) {
if (!entry.endsWith('.js')) continue
transformFile(path.join(outDir, entry), content => `${CJS_DEPRECATION_WARNING}\n${content}`)
}
}
// validate exports
if (typeof builtPkgJson.exports === 'object') {
for (const [name, target] of Object.entries(builtPkgJson.exports)) {
if (name.includes('*')) {
throw new Error(`Wildcards are not supported: ${name} -> ${target}`)
}
}
}
if (IS_JSR) {
// generate deno.json from package.json
// https://jsr.io/docs/package-configuration
const importMap = {}
if (builtPkgJson.dependencies) {
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}`
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 {
importMap[name] = `npm:${name}@${version}`
denoJson.exports[`./${f}`] = `./${f}`
}
}
}
const denoJson = path.join(outDir, 'deno.json')
fs.writeFileSync(
denoJson,
JSON.stringify(
{
name: builtPkgJson.name,
version: builtPkgJson.version,
exports: builtPkgJson.exports,
exclude: ['**/*.test.ts', '**/*.test-utils.ts', '**/__fixtures__/**'],
imports: importMap,
...builtPkgJson.denoJson,
},
null,
2,
),
)
if (process.env.E2E) {
// populate dependencies, if any
const depsToPopulate = []
for (const dep of Object.values(importMap)) {
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)
cp.spawnSync(
'pnpm',
[
'exec',
'slow-types-compiler',
'populate',
'--downstream',
process.env.JSR_URL,
'--token',
process.env.JSR_TOKEN,
'--unstable-create-via-api',
...depsToPopulate,
],
{
stdio: 'inherit',
},
)
}
}
console.log('[i] Processing with slow-types-compiler...')
const project = stc.createProject()
stc.processPackage(project, denoJson)
const unsavedSourceFiles = project.getSourceFiles().filter(s => !s.isSaved())
if (unsavedSourceFiles.length > 0) {
console.log('[v] Changed %d files', unsavedSourceFiles.length)
project.saveSync()
fs.writeFileSync(resolve(outDir, 'deno.json'), JSON.stringify(denoJson, null, 2))
} else {
fs.writeFileSync(resolve(outDir, 'package.json'), JSON.stringify(packageJson, null, 2))
}
} else {
// make shims for esnext resolution (that doesn't respect package.json `exports` field)
function makeShim(name, target) {
if (name === '.') name = './index.js'
if (!name.endsWith('.js')) return
if (fs.existsSync(path.join(outDir, name))) return
if (name === target) throw new Error(`cannot make shim to itself: ${name}`)
fs.writeFileSync(path.join(outDir, name), `export * from '${target}'\n`)
fs.writeFileSync(path.join(outDir, name.replace(/\.js$/, '.d.ts')), `export * from '${target}'\n`)
}
if (typeof builtPkgJson.exports === 'string') {
makeShim('.', builtPkgJson.exports)
} else if (typeof builtPkgJson.exports === 'object') {
for (const [name, target] of Object.entries(builtPkgJson.exports)) {
let esmTarget
if (typeof target === 'object') {
if (!target.import) throw new Error(`Invalid export target: ${name} -> ${JSON.stringify(target)}`)
esmTarget = target.import
} else if (typeof target === 'string') {
if (buildConfig.buildCjs) throw new Error(`Invalid export target (with cjs): ${name} -> ${target}`)
esmTarget = target
}
makeShim(name, esmTarget)
}
if (IS_JSR) {
await runJsrBuildSync(packageName)
} else {
runViteBuildSync(packageName)
}
}
try {
fs.cpSync(path.join(packageDir, 'README.md'), path.join(outDir, 'README.md'))
} catch (e) {
console.log(`[!] Failed to copy README.md: ${e.message}`)
}
fs.cpSync(path.join(__dirname, '../LICENSE'), path.join(outDir, 'LICENSE'))
if (!IS_JSR) {
fs.writeFileSync(path.join(outDir, '.npmignore'), '*.tsbuildinfo\n')
}
await buildConfig.final()
if (IS_JSR && !process.env.CI) {
console.log('[i] Trying to publish with --dry-run')
exec('deno publish --dry-run --allow-dirty --quiet', { cwd: outDir })
console.log('[v] All good!')
} else {
console.log('[v] Done!')
}

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