feat: improve types generation

This commit is contained in:
PalmDevs 2025-08-09 01:22:42 +07:00
parent 05c0b093ea
commit ab5b131b8b
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
12 changed files with 271 additions and 182 deletions

View file

@ -13,8 +13,8 @@
"@biomejs/biome": "^2.1.3",
"@parcel/watcher": "^2.5.1",
"@react-native-clipboard/clipboard": "^1.16.3",
"@revenge-mod/types": "workspace:*",
"@types/bun": "^1.2.19",
"@uwu/treetype": "^1.1.1",
"rolldown-plugin-dts": "^0.15.4",
},
"peerDependencies": {
@ -313,8 +313,6 @@
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
"@uwu/treetype": ["@uwu/treetype@1.1.1", "", { "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "treetype": "dist/cli.js" } }, "sha512-sts8RcR8DzG6B2gnt3tbeSrn2N/ShI+3wVOWZHWtFT1fgrw/aDBbV6ePvXFobG0AKUrt/AQ5zV5E3JeX0P37eQ=="],
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
"anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="],

View file

@ -1,13 +0,0 @@
{
"name": "@revenge-mod/types",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./types.d.ts"
},
"./tsconfig": {
"default": "./tsconfig.json"
}
}
}

View file

@ -1,21 +0,0 @@
{
"compilerOptions": {
// Features
"lib": ["ESNext"],
"module": "ESNext",
"target": "ES2015",
// Bundler
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"noEmit": true,
"jsx": "preserve",
// Type checking
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noPropertyAccessFromIndexSignature": false,
"types": ["@revenge-mod/types", "react-native/types/index"]
}
}

View file

@ -6,8 +6,8 @@
"@biomejs/biome": "^2.1.3",
"@parcel/watcher": "^2.5.1",
"@react-native-clipboard/clipboard": "^1.16.3",
"@revenge-mod/types": "workspace:*",
"@types/bun": "^1.2.19",
"@uwu/treetype": "^1.1.1",
"rolldown-plugin-dts": "^0.15.4"
},
"peerDependencies": {
@ -57,4 +57,4 @@
"patchedDependencies": {
"react-native-click-outside@0.1.1": "patches/react-native-click-outside@0.1.1.patch"
}
}
}

View file

@ -1,8 +1,10 @@
{
"extends": "@revenge-mod/types/tsconfig",
"compilerOptions": {
"module": "ESNext",
"target": "ESNext",
"types": ["node/timers"]
"moduleResolution": "Bundler",
"paths": {
"pkg:*": ["../lib/*/package.json"]
}
}
}

View file

@ -1,89 +1,247 @@
import { main } from 'bun'
import chalk from 'chalk'
import { existsSync, readdirSync } from 'fs'
import { rm } from 'fs/promises'
import { exists, mkdir, rm } from 'fs/promises'
import { rolldown } from 'rolldown'
import { dts } from 'rolldown-plugin-dts'
import pkg from '../package.json'
import type { Dirent } from 'fs'
const OutputDir = 'dist/types'
const PATHS = {
lib: 'lib',
output: 'dist/types',
outputTemp: 'dist/types/.build',
tsconfig: 'tsconfig.json',
} as const
await rm(OutputDir, { recursive: true, force: true })
.then(() =>
console.debug(chalk.gray('\u{1F5BB} Deleted generated types...')),
)
.catch()
// If this file is being run directly, build the project
if (main === import.meta.filename) buildTypes()
function hasTypesExports(node: Dirent) {
return (
node.isDirectory() &&
(existsSync(`./lib/${node.name}/src/types.ts`) ||
existsSync(`./lib/${node.name}/src/types/index.ts`))
)
async function cleanup(dir: string, description: string): Promise<void> {
try {
if (await exists(dir)) {
await rm(dir, { recursive: true, force: true })
console.debug(chalk.gray(`🗑️ Deleted ${description}...`))
}
} catch (error) {
console.warn(
chalk.yellow(`⚠️ Failed to delete ${description}: ${error}`),
)
}
}
export default async function buildTypes(log = true) {
let start = performance.now()
await cleanup(PATHS.output, 'generated types')
await mkdir(PATHS.outputTemp, { recursive: true })
if (log) console.info(chalk.gray('\u{1F5CE} Generating types...'))
export default async function buildTypes(log = true): Promise<void> {
const start = performance.now()
const bundle = await rolldown({
input: {
...Object.fromEntries(
readdirSync('./lib', { withFileTypes: true })
.filter(hasTypesExports)
.map(node => [
`lib/${node.name}/types`,
`./lib/${node.name}/src/types`,
]),
),
'lib/modules/finders/filters':
'./lib/modules/src/finders/filters.ts',
types: './types/lib.ts',
},
external: Object.keys({
...pkg.dependencies,
...pkg.devDependencies,
...pkg.peerDependencies,
}),
plugins: [
dts({
tsconfig: 'tsconfig.json',
parallel: true,
emitDtsOnly: true,
eager: true,
resolve: true,
if (log) console.info(chalk.gray('🏗️ Generating types...'))
try {
const bundle = await rolldown({
input: {
...Object.fromEntries(
Libraries.flatMap(([libName, exports]) =>
exports.map(([exportName, path]) => [
`lib/${libName}${exportName && `/${exportName}`}`,
path,
]),
),
),
globals: './types/globals.consumers.ts',
},
external: Object.keys({
...pkg.dependencies,
...pkg.devDependencies,
...pkg.peerDependencies,
}),
],
})
plugins: [
dts({
tsconfig: PATHS.tsconfig,
parallel: true,
emitDtsOnly: true,
eager: true,
resolve: true,
}),
],
})
await bundle.write({
dir: 'dist/types',
})
await bundle.write({
dir: PATHS.output,
})
if (log)
console.info(
chalk.greenBright(
`\u{2714} Generated library types! ${chalk.gray(`(took ${(performance.now() - start).toFixed(2)}ms)`)}`,
),
)
start = performance.now()
Bun.spawnSync({
cmd: ['treetype', './types/lib.tt', './dist/types/modules.d.ts'],
stdout: 'inherit',
stderr: 'inherit',
})
if (log)
console.info(
chalk.greenBright(
`\u{2714} Generated import types! ${chalk.gray(`(took ${(performance.now() - start).toFixed(2)}ms)`)}`,
),
)
if (log) {
const duration = (performance.now() - start).toFixed(2)
console.info(
chalk.greenBright(
`✅ Generated library types! ${chalk.gray(`(took ${duration}ms)`)}`,
),
)
}
} catch (error) {
console.error(chalk.red('❌ Failed to generate types:'), error)
throw error
} finally {
await cleanup(PATHS.outputTemp, 'temporary build files')
}
}
import assets from 'pkg:assets'
import components from 'pkg:components'
import discord from 'pkg:discord'
import externals from 'pkg:externals'
import modules from 'pkg:modules'
import patcher from 'pkg:patcher'
import plugins from 'pkg:plugins'
import react from 'pkg:react'
import storage from 'pkg:storage'
import utils from 'pkg:utils'
import { writeFileSync } from 'fs'
type TrimLeadingDotAndMaybeSlash<T extends string> = T extends `.${infer R}`
? R extends `/${infer S}`
? S
: R
: T
type NameBinding<Exp extends string> = [original: Exp, to: string]
type Exports<Exp extends string> = Exp | NameBinding<Exp>
function library<
E extends Record<`.${string}`, { default?: string; types?: string }>,
>(
pkg: { name: string; exports: E },
exports: Exports<TrimLeadingDotAndMaybeSlash<Extract<keyof E, string>>>[],
): [name: string, exports: [exportName: string, path: string][]] {
const mapped: [exportName: string, path: string][] = []
const bindings: Record<string, [exportName: string, path: string][]> = {}
const pkgName = pkg.name.split('/').at(-1)!
for (const nameOrBinding of exports) {
const isBinding = Array.isArray(nameOrBinding)
const name = isBinding ? nameOrBinding[0] : nameOrBinding
const exportEntry = pkg.exports[`.${name ? `/${name}` : ''}`]
if (!exportEntry) {
console.warn(
chalk.yellow(
`⚠️ Export "${name}" not found in "${pkg.name}" package.json`,
),
)
continue
}
const { default: defaultPath, types } = exportEntry
const actualName = isBinding ? nameOrBinding[1] : name
const path = defaultPath ?? types
if (!path) {
console.warn(
chalk.yellow(
`⚠️ No path found for export "${name}" in "${pkg.name}"`,
),
)
continue
}
if (isBinding) (bindings[actualName] ??= []).push([actualName, name])
else {
console.debug(
chalk.gray(
`📄 ${pkgName}${name && `/${name}`} -> ${pkg.name}${name && `/${name}`}`,
),
)
mapped.push([actualName, `${PATHS.lib}/${pkgName}/${path}`])
}
}
for (const [to, froms] of Object.entries(bindings)) {
if (froms.length > 1) {
const tempFileName = Array.from(
crypto.getRandomValues(new Uint8Array(4)),
)
.map(b => b.toString(16).padStart(2, '0'))
.join('')
const tempPath = `${PATHS.outputTemp}/${tempFileName}.ts`
const content = froms
.map(([, from]) => {
const path = `${pkg.name}/${from}`
console.debug(
chalk.gray(`↔️ ${pkgName}${to && `/${to}`} -> ${path}`),
)
return `export * from '${path}'`
})
.join('\n')
try {
writeFileSync(tempPath, content)
mapped.push([to, tempPath])
} catch (error) {
console.error(
chalk.red(`❌ Failed to write temp file for ${to}:`),
error,
)
}
} else if (froms[0]) {
mapped.push(froms[0])
}
}
return [pkgName, mapped]
}
const Libraries = [
library(assets, ['', 'types']),
library(components, [
'FormSwitch',
'Page',
'SearchInput',
'TableRowAssetIcon',
'types',
]),
library(discord, [
'actions',
'common',
'common/flux',
'common/utils',
'design',
'flux',
'modules/main_tabs_v2',
'modules/settings',
'modules/settings/renderer',
'native',
'types',
]),
library(externals, [
'browserify',
'react-native-clipboard',
'react-navigation',
'shopify',
'types',
]),
library(modules, [
'finders/filters',
['finders/get', 'finders'],
['finders/lookup', 'finders'],
['finders/wait', 'finders'],
['metro/subscriptions', 'metro'],
['metro/utils', 'metro'],
'native',
'types',
]),
library(patcher, ['', 'types']),
library(plugins, ['constants', 'types']),
library(react, ['', 'jsx-runtime', 'native', 'types']),
library(storage, ['', 'types']),
library(utils, [
'callback',
'discord',
'error',
'object',
'promise',
'proxy',
'react',
'tree',
'types',
]),
] as const
if (main === import.meta.filename) await buildTypes()

View file

@ -1,19 +1,33 @@
{
"extends": "@revenge-mod/types/tsconfig",
"compilerOptions": {
"types": [
"@revenge-mod/types",
"./types",
"./types.modules",
"node/util",
"node/events"
],
"lib": ["ESNext"],
"module": "ESNext",
"target": "ES2015",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"noEmit": true,
"jsx": "preserve",
"allowJs": true,
"strict": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noPropertyAccessFromIndexSignature": false,
"types": [],
"typeRoots": [],
"paths": {
"~/*": ["./src/*"],
"~*": ["./src/*"],
"~assets/*": ["./dist/assets/generated/*"]
}
},
"include": ["lib", "src", "types"],
"include": [
"lib",
"src",
"types",
"./types/*.d.ts",
"node_modules/@types/node/events.d.ts",
"node_modules/@types/node/util.d.ts",
"node_modules/react-native/types/index.d.ts"
],
"exclude": ["**/*.test.ts"]
}

View file

View file

@ -11,4 +11,4 @@ declare global {
): void
}
export type * from '@revenge-mod/types'
export * from './globals'

View file

@ -1,5 +1,3 @@
// All of these typings are exported, be careful what you export here!
import type { Metro } from '@revenge-mod/modules/types'
import type {
ImageProps,
@ -25,6 +23,8 @@ declare module 'react' {
/// HERMES
declare global {
const HermesInternal: HermesInternalObject
function setTimeout(
cb: (...args: unknown[]) => unknown,
timeout?: number,
@ -56,6 +56,10 @@ declare global {
/// PROMISE POLYFILLS FROM: https://github.com/then/promise
/// AND: https://github.com/facebook/hermes/blob/main/lib/InternalBytecode/01-Promise.js
_h: 0 | 1 | 2
/**
* The resolved value of the promise, if it has been resolved.
*/
_j: any
}
type HermesPromiseRejectionHandler = (
@ -103,7 +107,7 @@ declare global {
declare global {
var nativeModuleProxy: Record<string, unknown>
function __turboModuleProxy(name: string): unknown
var __turboModuleProxy: ((name: string) => unknown) | undefined
function nativeLoggingHook(str: string, level: number): void
function alert(message: unknown): void
@ -111,6 +115,4 @@ declare global {
var performance: {
now(): number
}
var globalEvalWithSourceUrl: (code: string, sourceURL: string) => any
}

View file

@ -1,51 +0,0 @@
\source ../lib/plugins/src/types.ts
\import ./lib/plugins/types.d.ts
@revenge-mod from UnscopedPluginApi {
assets
components
discord {
common {
flux
utils
}
modules {
settings {
renderer
}
mainTabsV2
}
actions
design
flux
native
}
externals {
Browserify
ReactNativeClipboard
ReactNavigation
Shopify
}
modules {
finders
metro
native
}
patcher
plugins {
constants
}
react {
native
}
utils {
callback
discord
error
object
promise
proxy
react
tree
}
}