Skip to content

Commit cfb9756

Browse files
slapec93Gergely Békési
andauthored
refactor!: split methods to separated namespaces (#1219)
* refactor: balance module * refactor: commit new files * refactor: connectivity, stake, states, status, transaction, settlement * refactor: cheque, pin, grantee, tag, stamp, storage * refactor: download, upload * chore: remove unused files * refactor: separate api and module structure * chore: commit files * refactor: split more apis * feat: improve codemod efficiency * fix: improve codemod --------- Co-authored-by: Gergely Békési <gergely.bekesi@ethswarm.org>
1 parent 270c9ca commit cfb9756

96 files changed

Lines changed: 4204 additions & 4114 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

codemod/index.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import * as fs from 'fs'
2+
import * as path from 'path'
3+
import * as ts from 'typescript'
4+
5+
const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx']
6+
const SKIP_DIRS = new Set(['node_modules', 'dist', '.git'])
7+
8+
function walkFiles(dir: string): string[] {
9+
const files: string[] = []
10+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
11+
if (entry.isDirectory()) {
12+
if (!SKIP_DIRS.has(entry.name)) {
13+
files.push(...walkFiles(path.join(dir, entry.name)))
14+
}
15+
} else if (entry.isFile() && EXTENSIONS.some(ext => entry.name.endsWith(ext))) {
16+
files.push(path.join(dir, entry.name))
17+
}
18+
}
19+
return files
20+
}
21+
22+
function parseVersion(filename: string): number {
23+
const match = path.basename(filename).match(/^v?(\d+)/)
24+
return match ? parseInt(match[1], 10) : 0
25+
}
26+
27+
const args = process.argv.slice(2)
28+
const fromIdx = args.indexOf('--from')
29+
const fromVersion = fromIdx !== -1 ? parseVersion(args[fromIdx + 1]) : 0
30+
const target = args.find((a, i) => !a.startsWith('--') && (fromIdx === -1 || i !== fromIdx + 1))
31+
32+
if (!target) {
33+
console.error('Usage: ts-node --project codemod/tsconfig.json codemod/index.ts <path> [--from <version>]')
34+
console.error('Example: npm run codemod -- ./src --from v12')
35+
process.exit(1)
36+
}
37+
38+
const transformsDir = path.join(__dirname, 'transforms')
39+
40+
const transformFiles = fs
41+
.readdirSync(transformsDir)
42+
.filter(f => f.endsWith('.ts') && !f.endsWith('.d.ts'))
43+
.sort()
44+
.filter(f => parseVersion(f) > fromVersion)
45+
46+
if (transformFiles.length === 0) {
47+
console.log('No transforms to apply.')
48+
process.exit(0)
49+
}
50+
51+
console.log(`Applying: ${transformFiles.join(', ')}\n`)
52+
53+
// Type-aware transform: receives a parsed source file plus the program's type checker so it
54+
// can resolve receiver types (locals, imported instances, `this.<field>`, factory calls).
55+
type TransformFn = (sourceFile: ts.SourceFile, checker: ts.TypeChecker) => string | null
56+
57+
const transforms: TransformFn[] = transformFiles.map(f => {
58+
const modulePath = path.join(transformsDir, f.replace(/\.ts$/, ''))
59+
return (require(modulePath) as { transform: TransformFn }).transform
60+
})
61+
62+
const resolved = path.resolve(target)
63+
const stat = fs.statSync(resolved)
64+
const files = (stat.isDirectory() ? walkFiles(resolved) : [resolved]).map(f => path.resolve(f))
65+
const targetSet = new Set(files)
66+
67+
// Load the target project's compiler options so imports and the bee-js types resolve; fall
68+
// back to permissive defaults when no tsconfig is found nearby.
69+
function loadCompilerOptions(from: string): ts.CompilerOptions {
70+
const configPath = ts.findConfigFile(from, ts.sys.fileExists, 'tsconfig.json')
71+
72+
if (configPath) {
73+
const read = ts.readConfigFile(configPath, ts.sys.readFile)
74+
const parsed = ts.parseJsonConfigFileContent(read.config ?? {}, ts.sys, path.dirname(configPath))
75+
76+
return { ...parsed.options, noEmit: true, allowJs: true, checkJs: false }
77+
}
78+
79+
return {
80+
allowJs: true,
81+
checkJs: false,
82+
noEmit: true,
83+
target: ts.ScriptTarget.ESNext,
84+
module: ts.ModuleKind.NodeNext,
85+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
86+
}
87+
}
88+
89+
const options = loadCompilerOptions(stat.isDirectory() ? resolved : path.dirname(resolved))
90+
const changedFiles = new Set<string>()
91+
92+
// Each transform version gets a freshly built program so it sees edits from the previous one.
93+
for (const transform of transforms) {
94+
const program = ts.createProgram(files, options)
95+
const checker = program.getTypeChecker()
96+
97+
for (const sourceFile of program.getSourceFiles()) {
98+
const filePath = path.resolve(sourceFile.fileName)
99+
if (!targetSet.has(filePath)) continue
100+
101+
const result = transform(sourceFile, checker)
102+
103+
if (result !== null && result !== sourceFile.text) {
104+
fs.writeFileSync(filePath, result, 'utf8')
105+
console.log(`updated: ${filePath}`)
106+
changedFiles.add(filePath)
107+
}
108+
}
109+
}
110+
111+
console.log(`\n${changedFiles.size} file(s) updated, ${files.length - changedFiles.size} unchanged.`)

codemod/transforms/v13.ts

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import * as ts from 'typescript'
2+
3+
interface Mapping {
4+
namespace: string
5+
newName: string
6+
}
7+
8+
const METHOD_MAP: Record<string, Mapping> = {
9+
// bee.balance
10+
getAllBalances: { namespace: 'balance', newName: 'getAll' },
11+
getPeerBalance: { namespace: 'balance', newName: 'getPeer' },
12+
getPastDueConsumptionBalances: { namespace: 'balance', newName: 'getAllPastDueConsumption' },
13+
getPastDueConsumptionPeerBalance: { namespace: 'balance', newName: 'getAllPastDueConsumptionForPeer' },
14+
15+
// bee.chequebook
16+
getChequebookAddress: { namespace: 'chequebook', newName: 'getAddress' },
17+
getChequebookBalance: { namespace: 'chequebook', newName: 'getBalance' },
18+
depositBZZToChequebook: { namespace: 'chequebook', newName: 'deposit' },
19+
depositTokens: { namespace: 'chequebook', newName: 'deposit' },
20+
withdrawBZZFromChequebook: { namespace: 'chequebook', newName: 'withdraw' },
21+
withdrawTokens: { namespace: 'chequebook', newName: 'withdraw' },
22+
23+
// bee.cheque
24+
getLastCheques: { namespace: 'cheque', newName: 'getAllLatest' },
25+
getLastChequesForPeer: { namespace: 'cheque', newName: 'getAllLatestForPeer' },
26+
getLastCashoutAction: { namespace: 'cheque', newName: 'getLastCashoutAction' },
27+
cashoutLastCheque: { namespace: 'cheque', newName: 'cashoutLast' },
28+
29+
// bee.connectivity
30+
checkConnection: { namespace: 'connectivity', newName: 'checkConnection' },
31+
isConnected: { namespace: 'connectivity', newName: 'isConnected' },
32+
isGateway: { namespace: 'connectivity', newName: 'isGateway' },
33+
getNodeAddresses: { namespace: 'connectivity', newName: 'getNodeAddresses' },
34+
getBlocklist: { namespace: 'connectivity', newName: 'getBlocklist' },
35+
getPeers: { namespace: 'connectivity', newName: 'getPeers' },
36+
removePeer: { namespace: 'connectivity', newName: 'removePeer' },
37+
pingPeer: { namespace: 'connectivity', newName: 'ping' },
38+
getTopology: { namespace: 'connectivity', newName: 'getTopology' },
39+
40+
// bee.data (/bytes)
41+
uploadData: { namespace: 'data', newName: 'upload' },
42+
downloadData: { namespace: 'data', newName: 'download' },
43+
downloadReadableData: { namespace: 'data', newName: 'downloadReadable' },
44+
probeData: { namespace: 'data', newName: 'probe' },
45+
isReferenceRetrievable: { namespace: 'data', newName: 'isRetrievable' },
46+
47+
// bee.chunk (/chunks)
48+
uploadChunk: { namespace: 'chunk', newName: 'upload' },
49+
downloadChunk: { namespace: 'chunk', newName: 'download' },
50+
51+
// bee.file (single /bzz)
52+
uploadFile: { namespace: 'file', newName: 'upload' },
53+
downloadFile: { namespace: 'file', newName: 'download' },
54+
downloadReadableFile: { namespace: 'file', newName: 'downloadReadable' },
55+
56+
// bee.collection (multi-file /bzz)
57+
uploadCollection: { namespace: 'collection', newName: 'upload' },
58+
uploadFiles: { namespace: 'collection', newName: 'uploadFromFileList' },
59+
uploadFilesFromDirectory: { namespace: 'collection', newName: 'uploadFromDirectory' },
60+
streamFiles: { namespace: 'collection', newName: 'stream' },
61+
streamDirectory: { namespace: 'collection', newName: 'streamFromDirectory' },
62+
hashDirectory: { namespace: 'collection', newName: 'hashDirectory' },
63+
64+
// bee.feed
65+
makeFeedWriter: { namespace: 'feed', newName: 'makeWriter' },
66+
makeFeedReader: { namespace: 'feed', newName: 'makeReader' },
67+
createFeedManifest: { namespace: 'feed', newName: 'createManifest' },
68+
fetchLatestFeedUpdate: { namespace: 'feed', newName: 'fetchLatestUpdate' },
69+
isFeedRetrievable: { namespace: 'feed', newName: 'isRetrievable' },
70+
71+
// bee.grantee
72+
createGrantees: { namespace: 'grantee', newName: 'create' },
73+
getGrantees: { namespace: 'grantee', newName: 'get' },
74+
patchGrantees: { namespace: 'grantee', newName: 'patch' },
75+
76+
// bee.messaging
77+
pssSend: { namespace: 'messaging', newName: 'pssSend' },
78+
pssReceive: { namespace: 'messaging', newName: 'pssReceive' },
79+
pssSubscribe: { namespace: 'messaging', newName: 'pssSubscribe' },
80+
gsocSend: { namespace: 'messaging', newName: 'gsocSend' },
81+
gsocSubscribe: { namespace: 'messaging', newName: 'gsocSubscribe' },
82+
gsocMine: { namespace: 'messaging', newName: 'gsocMine' },
83+
84+
// bee.pin
85+
pin: { namespace: 'pin', newName: 'add' },
86+
unpin: { namespace: 'pin', newName: 'remove' },
87+
getAllPins: { namespace: 'pin', newName: 'getAll' },
88+
getPin: { namespace: 'pin', newName: 'get' },
89+
reuploadPinnedData: { namespace: 'pin', newName: 'reuploadData' },
90+
91+
// bee.settlement
92+
getSettlements: { namespace: 'settlement', newName: 'get' },
93+
getAllSettlements: { namespace: 'settlement', newName: 'getAll' },
94+
95+
// bee.soc
96+
makeSOCWriter: { namespace: 'soc', newName: 'makeWriter' },
97+
makeSOCReader: { namespace: 'soc', newName: 'makeReader' },
98+
99+
// bee.stake
100+
getStake: { namespace: 'stake', newName: 'get' },
101+
getWithdrawableStake: { namespace: 'stake', newName: 'getWithdrawable' },
102+
depositStake: { namespace: 'stake', newName: 'deposit' },
103+
withdrawSurplusStake: { namespace: 'stake', newName: 'withdrawSurplus' },
104+
migrateStake: { namespace: 'stake', newName: 'migrate' },
105+
getRedistributionState: { namespace: 'stake', newName: 'getRedistributionState' },
106+
107+
// bee.stamp
108+
createPostageBatch: { namespace: 'stamp', newName: 'create' },
109+
topUpBatch: { namespace: 'stamp', newName: 'topUp' },
110+
diluteBatch: { namespace: 'stamp', newName: 'dilute' },
111+
getPostageBatch: { namespace: 'stamp', newName: 'get' },
112+
getGlobalPostageBatch: { namespace: 'stamp', newName: 'getGlobal' },
113+
getPostageBatchBuckets: { namespace: 'stamp', newName: 'getBuckets' },
114+
getAllPostageBatch: { namespace: 'stamp', newName: 'getAll' },
115+
getAllGlobalPostageBatch: { namespace: 'stamp', newName: 'getAllGlobal' },
116+
getPostageBatches: { namespace: 'stamp', newName: 'getAll' },
117+
getGlobalPostageBatches: { namespace: 'stamp', newName: 'getAllGlobal' },
118+
updatePostageBatchLabel: { namespace: 'stamp', newName: 'updateLabel' },
119+
calculateTopUpForBzz: { namespace: 'stamp', newName: 'calculateTopUpForBZZ' },
120+
121+
// bee.storage
122+
buyStorage: { namespace: 'storage', newName: 'buy' },
123+
getStorageCost: { namespace: 'storage', newName: 'getCost' },
124+
extendStorage: { namespace: 'storage', newName: 'extend' },
125+
extendStorageSize: { namespace: 'storage', newName: 'extendSize' },
126+
extendStorageDuration: { namespace: 'storage', newName: 'extendDuration' },
127+
getExtensionCost: { namespace: 'storage', newName: 'getExtensionCost' },
128+
getSizeExtensionCost: { namespace: 'storage', newName: 'getSizeExtensionCost' },
129+
getDurationExtensionCost: { namespace: 'storage', newName: 'getDurationExtensionCost' },
130+
renameStorage: { namespace: 'storage', newName: 'rename' },
131+
132+
// bee.status
133+
getStatus: { namespace: 'status', newName: 'get' },
134+
getHealth: { namespace: 'status', newName: 'getHealth' },
135+
getReadiness: { namespace: 'status', newName: 'getReadiness' },
136+
getNodeInfo: { namespace: 'status', newName: 'getNodeInfo' },
137+
isSupportedExactVersion: { namespace: 'status', newName: 'isSupportedExactVersion' },
138+
isSupportedApiVersion: { namespace: 'status', newName: 'isSupportedApiVersion' },
139+
getVersions: { namespace: 'status', newName: 'getVersions' },
140+
getReserveState: { namespace: 'status', newName: 'getReserveState' },
141+
getChainState: { namespace: 'status', newName: 'getChainState' },
142+
143+
// bee.tag
144+
createTag: { namespace: 'tag', newName: 'create' },
145+
getAllTags: { namespace: 'tag', newName: 'getAll' },
146+
retrieveTag: { namespace: 'tag', newName: 'get' },
147+
deleteTag: { namespace: 'tag', newName: 'delete' },
148+
updateTag: { namespace: 'tag', newName: 'update' },
149+
150+
// bee.transaction
151+
getAllPendingTransactions: { namespace: 'transaction', newName: 'getAll' },
152+
getPendingTransaction: { namespace: 'transaction', newName: 'get' },
153+
rebroadcastPendingTransaction: { namespace: 'transaction', newName: 'rebroadcast' },
154+
cancelPendingTransaction: { namespace: 'transaction', newName: 'cancel' },
155+
156+
// bee.wallet
157+
getWalletBalance: { namespace: 'wallet', newName: 'getBalance' },
158+
withdrawBZZToExternalWallet: { namespace: 'wallet', newName: 'withdrawBZZ' },
159+
withdrawDAIToExternalWallet: { namespace: 'wallet', newName: 'withdrawDAI' },
160+
}
161+
162+
export function transform(sourceFile: ts.SourceFile, checker: ts.TypeChecker): string | null {
163+
const source = sourceFile.text
164+
165+
const isNewBee = (node?: ts.Node): boolean =>
166+
!!node && ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'Bee'
167+
168+
// Does the type annotation mention `Bee` (`Bee`, `Bee | null`, `Bee | undefined`, …)?
169+
const typeMentionsBee = (type?: ts.TypeNode): boolean => {
170+
if (!type) return false
171+
if (ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) && type.typeName.text === 'Bee') return true
172+
if (ts.isUnionTypeNode(type)) return type.types.some(typeMentionsBee)
173+
174+
return false
175+
}
176+
177+
// Syntactic pass — identify Bee bindings from annotations and `new Bee(...)` alone, so
178+
// annotated/local code migrates even when the project's deps aren't installed (types
179+
// unresolved). `beeNames` = bare identifiers; `beeFields` = `this.<field>`.
180+
const beeNames = new Set<string>()
181+
const beeFields = new Set<string>()
182+
183+
const collect = (node: ts.Node): void => {
184+
// `const bee = new Bee(...)` | `const bee: Bee = ...` | `let bee: Bee`
185+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && (typeMentionsBee(node.type) || isNewBee(node.initializer))) {
186+
beeNames.add(node.name.text)
187+
}
188+
189+
// `bee = new Bee(...)` | `this.bee = new Bee(...)`
190+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && isNewBee(node.right)) {
191+
if (ts.isIdentifier(node.left)) {
192+
beeNames.add(node.left.text)
193+
} else if (ts.isPropertyAccessExpression(node.left) && node.left.expression.kind === ts.SyntaxKind.ThisKeyword) {
194+
beeFields.add(node.left.name.text)
195+
}
196+
}
197+
198+
// function/method/ctor parameter: `(bee: Bee)`, `(beeApi: Bee | null)`; a ctor
199+
// parameter-property (has modifiers) is also reachable as `this.<name>`.
200+
if (ts.isParameter(node) && ts.isIdentifier(node.name) && typeMentionsBee(node.type)) {
201+
beeNames.add(node.name.text)
202+
if (node.modifiers?.length) beeFields.add(node.name.text)
203+
}
204+
205+
// class field: `bee = new Bee(...)` | `bee: Bee`
206+
if (ts.isPropertyDeclaration(node) && ts.isIdentifier(node.name) && (typeMentionsBee(node.type) || isNewBee(node.initializer))) {
207+
beeFields.add(node.name.text)
208+
}
209+
210+
ts.forEachChild(node, collect)
211+
}
212+
collect(sourceFile)
213+
214+
// A receiver is a Bee if identified syntactically (above) OR resolved by the type checker
215+
// to the `Bee` class declared by bee-js — the latter additionally catches imported/shared
216+
// instances and factory calls (`getBee()`) when the project's types resolve.
217+
const isBeeReceiver = (expr: ts.Expression): boolean => {
218+
if (ts.isIdentifier(expr) && beeNames.has(expr.text)) return true
219+
220+
if (
221+
ts.isPropertyAccessExpression(expr) &&
222+
expr.expression.kind === ts.SyntaxKind.ThisKeyword &&
223+
beeFields.has(expr.name.text)
224+
) {
225+
return true
226+
}
227+
228+
const symbol = checker.getTypeAtLocation(expr).getSymbol()
229+
230+
if (!symbol || symbol.getName() !== 'Bee') {
231+
return false
232+
}
233+
234+
return (symbol.getDeclarations() ?? []).some(decl => {
235+
if (!ts.isClassDeclaration(decl)) {
236+
return false
237+
}
238+
239+
const file = decl.getSourceFile().fileName
240+
241+
return /[/\\]bee-js[/\\]/.test(file) || /[/\\]bee\.(d\.)?ts$/.test(file)
242+
})
243+
}
244+
245+
const replacements: Array<{ start: number; end: number; text: string }> = []
246+
247+
const visit = (node: ts.Node): void => {
248+
// Any `<bee>.<method>` access — called or not — so bare method references migrate too.
249+
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) {
250+
const mapping = METHOD_MAP[node.name.text]
251+
252+
if (mapping && isBeeReceiver(node.expression)) {
253+
// Preserve optional chaining on the receiver: `bee?.uploadData` → `bee?.data.upload`.
254+
const separator = node.questionDotToken ? '?.' : '.'
255+
256+
replacements.push({
257+
start: node.getStart(sourceFile),
258+
end: node.getEnd(),
259+
text: `${node.expression.getText(sourceFile)}${separator}${mapping.namespace}.${mapping.newName}`,
260+
})
261+
}
262+
}
263+
264+
ts.forEachChild(node, visit)
265+
}
266+
visit(sourceFile)
267+
268+
if (replacements.length === 0) {
269+
return null
270+
}
271+
272+
// Apply from end to start to preserve positions
273+
replacements.sort((a, b) => b.start - a.start)
274+
275+
let result = source
276+
for (const { start, end, text } of replacements) {
277+
result = result.slice(0, start) + text + result.slice(end)
278+
}
279+
280+
return result
281+
}

0 commit comments

Comments
 (0)