tdl-install-types
Version:
Generate TypeScript (and Flow) types for TDLib
307 lines (275 loc) • 10.1 kB
JavaScript
const fs = require('node:fs')
const path = require('node:path')
const { createRequire } = require('node:module')
const fetch = (...args) => import('node-fetch')
.then(({ default: fetch }) => fetch(...args))
const { generate } = require('./gen')
const packageVersion = require('../package.json').version
const argv = [...process.argv].slice(2)
const help = `\
Usage: tdl-install-types [<options>] [<target>]
Generate TypeScript (and optionally Flow) types for TDLib,
potentially fetching the necessary schema from TDLib's GitHub repository.
These types can be used with the tdl library.
By default, a tdlib-types.d.ts file is created that you can git-commit.
<target> can be one of:
- a file path to the .so/.dylib/.dll tdjson shared library file
- a file path to the td_api.tl file
- a git ref in the TDLib repository
- "prebuilt-tdlib": special value to generate types for the installed
version of prebuilt-tdlib (this is the default)
Examples:
tdl-install-types ./libtdjson.so # generate types for this shared library
tdl-install-types prebuilt-tdlib # use version of installed prebuilt-tdlib
tdl-install-types # similar to above: try to use prebuilt-tdlib
tdl-install-types 0ada45c3618108a62806ce7d9ab435a18dac1aab # commit hash
tdl-install-types v1.8.0 # git tag in the TDLib repository
tdl-install-types td_api.tl # generate directly from the tl file
npx tdl-install-types # can be called via npx without installing
Options:
--lib
Interpret <target> as a file path to the tdjson shared library. This is
the default behavior if <target> ends with .so, .dylib, or .dll.
Can fail if the heuristics cannot find the version inside the shared
library, especially for older TDLib versions, or if TDLib was built
outside of the git repository.
--tl
Interpret <target> as a file path to a td_api.tl file describing the TDLib
schema in the TL language. This is the default behavior if <target> ends
with .tl.
--git-ref
Interpret <target> as a git ref (e.g., a commit hash or a git tag or a
branch name) in the TDLib repository. This is the default behavior if
neither of the above conditions is met and <target> is not prebuilt-tdlib.
-o <path>
Output generated TypeScript types to <path>.
Defaults to tdlib-types.d.ts in the current working directory.
--flow
Also generate Flow types besides the TypeScript types.
--flow-output <filepath>
Output generated Flow types to <filepath>.
Defaults to flow-typed/tdlib-types_vx.x.x.js.
--github-repo <username>/<repository>
Set the TDLib GitHub repository.
Can be set, for example, to the repository of tdlight.
Defaults to tdlib/td.
--override-version <version>
Override TDLib version that is generated in the comment header.
--prebuilt-tdlib Deprecated. Same as setting <target> to prebuilt-tdlib.
-h, --help Print this text and exit.
-v, --version Print version and exit.`
const LIB = 'lib'
const GIT_REF = 'git-ref'
const TL = 'tl'
const PREBUILT_TDLIB = 'prebuilt-tdlib'
let target
let type
let tsOutput = 'tdlib-types.d.ts'
let flow = false
let flowOutput = 'flow-typed/tdlib-types_vx.x.x.js'
let githubRepo = 'tdlib/td'
let defaultTarget = false
let overriddenVersion = null
function parseArgs () {
if (argv.includes('--help') || argv.includes('-h')) {
console.log(help)
return false
}
if (argv.includes('--version') || argv.includes('-v')) {
console.log(packageVersion)
return false
}
for (let i = 0; i < argv.length; i++) {
const arg = argv[i]
switch (arg) {
case '--lib':
type = LIB
break
case '--tl':
type = TL
break
case '--git-ref':
type = GIT_REF
break
case '--prebuilt-tdlib':
type = PREBUILT_TDLIB
target = 'prebuilt-tdlib'
break
case '-o':
if (!argv[i + 1]) {
console.error(`${arg} expects a file path`)
process.exitCode = 1
return false
}
tsOutput = argv[i + 1]
i++
break
case '--flow':
flow = true
break
case '--flow-output':
if (!argv[i + 1]) {
console.error(`${arg} expects a file path`)
process.exitCode = 1
return false
}
flowOutput = argv[i + 1]
i++
break
case '--github-repo':
if (!argv[i + 1] || argv[i + 1].startsWith('-')) {
console.error(`${arg} expects a value`)
process.exitCode = 1
return false
}
githubRepo = argv[i + 1]
i++
break
case '--override-version':
if (!argv[i + 1]) {
console.error(`${arg} expects a version string`)
process.exitCode = 1
return false
}
overriddenVersion = argv[i + 1]
i++
break
default:
if (arg.startsWith('-')) {
console.error(`WARN Unrecognized option ${arg}`)
break
}
if (target) {
console.error('Too many arguments')
return false
}
target = arg
if (type != null) break
if (target == 'prebuilt-tdlib')
type = PREBUILT_TDLIB
else if (target.endsWith('.so') || target.endsWith('.dylib') || target.endsWith('.dll'))
type = LIB
else if (target.endsWith('.tl'))
type = TL
else
type = GIT_REF
}
}
if (!target) {
type = PREBUILT_TDLIB
defaultTarget = true
}
return true
}
function writeFile (file, contents) {
const dir = path.dirname(file)
if (dir !== '.' && dir !== '..')
fs.mkdirSync(dir, { recursive: true })
fs.writeFileSync(file, contents)
}
function fromSchema (schema, { ver, hash } = {}) {
if (overriddenVersion != null)
ver = overriddenVersion
let line1 = '// Types for TDLib'
if (ver) line1 += ` v${ver}`
if (hash) line1 += ` (${hash})`
const line2 = `// Generated using tdl-install-types v${packageVersion}`
const header = [line1, line2]
const tsGenerated = generate(schema, 'typescript', header)
writeFile(tsOutput, tsGenerated)
if (flow) {
const flowGenerated = generate(schema, 'flow', header)
writeFile(flowOutput, flowGenerated)
}
}
async function fetchTdVersion (ref) {
try {
const url = `https://raw.githubusercontent.com/${githubRepo}/${ref}/CMakeLists.txt`
const res = await fetch(url)
const text = await res.text()
if (text.includes('404: Not Found')) throw Error('404 Not found')
const version = text.match(/TDLib VERSION ([\w.]+) /)?.[1]
if (!version) throw Error('Could not find version in CMakeLists.txt')
return version
} catch (e) {
console.error(`WARN Failed to get TDLib version: ${e?.message || String(e)}`)
return '<unknown>'
}
}
async function fromGitRef (ref, ver = overriddenVersion) {
const url = `https://raw.githubusercontent.com/${githubRepo}/${ref}/td/generate/scheme/td_api.tl`
const res = await fetch(url)
const schema = await res.text()
if (schema.length < 2000)
throw Error(`Failed to fetch schema from ${url}\n ${schema}`)
ver ??= await fetchTdVersion(ref)
const hash = (ref.length >= 30 && ref !== ver) ? ref : null
fromSchema(schema, { ver, hash })
}
// > $ strings libtdjson.dylib | grep -iE '^[0123456789abcdef]{40}$'
// > 66234ae2537a99ec0eaf7b0857245a6e5c2d2bc9
function fromDynamicLib (lib) {
// Try to parse the hash/version from the so file itself
const data = fs.readFileSync(lib).toString('binary')
const hash = data.match(/[^\dabcdef]([\dabcdef]{40})[^\dabcdef]/)?.[1]
const version = data.match(/\W([1234]\.\d{1,2}\.\d{1,3})\W/)?.[1]
// console.log('hash', hash)
// console.log('version', version)
if (version?.endsWith?.('.0')) {
const parsed = version.split('.').map(Number)
const useVersionRef = (parsed?.[0] === 1 && parsed?.[1] < 8)
|| (!hash && parsed.every(x => !Number.isNaN(x)))
if (useVersionRef)
return fromGitRef('v' + version, overriddenVersion ?? version)
}
if (hash) {
console.log(`INFO Using TDLib ${hash}`)
return fromGitRef(hash)
}
throw Error('Could not determine TDLib commit hash')
}
async function fromPrebuiltTdlib () {
try {
// Find path of the prebuilt-tdlib module
const cwdRequire = createRequire(path.resolve('index.js'))
const prebuiltTdlib = path.dirname(cwdRequire.resolve('prebuilt-tdlib'))
// Try to use the tdlib.commit and tdlib.version fields in package.json
const packageJsonPath = path.join(prebuiltTdlib, 'package.json')
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString())
const tdlibCommit = packageJson?.tdlib?.commit
const tdlibVersion = overriddenVersion ?? packageJson?.tdlib?.version
if (typeof tdlibCommit === 'string' && typeof tdlibVersion === 'string') {
await fromGitRef(tdlibCommit, tdlibVersion)
return
}
// If not existent, fallback to executing getTdjson() and extracting tdlib
// commit from the shared library
const tdjsonFile = cwdRequire('prebuilt-tdlib').getTdjson()
await fromDynamicLib(tdjsonFile)
} catch (e) {
if (defaultTarget && e?.code === 'MODULE_NOT_FOUND') {
console.error('Error: Cannot find prebuilt-tdlib. Help:')
console.error(help)
} else if (defaultTarget) {
console.error('Could not generate types for prebuilt-tdlib:')
console.error(e?.message || String(e))
console.error(help)
} else {
console.error(String(e))
}
process.exitCode = 1
}
}
const shouldContinue = parseArgs()
if (!shouldContinue) {
// Nothing, exit
} else if (type === PREBUILT_TDLIB) {
fromPrebuiltTdlib()
} else if (type === TL) {
const schema = fs.readFileSync(target).toString()
fromSchema(schema)
} else if (type === GIT_REF) {
fromGitRef(target).catch(console.error)
} else if (type === LIB) {
fromDynamicLib(target)
}