@@ -242,8 +242,259 @@ export function transform(sourceFile: ts.SourceFile, checker: ts.TypeChecker): s
242242 } )
243243 }
244244
245+ // ChunkBuilder.hash() now returns a Reference, not a raw Uint8Array.
246+ const isChunkBuilderReceiver = ( expr : ts . Expression ) : boolean => {
247+ const symbol = checker . getTypeAtLocation ( expr ) . getSymbol ( )
248+
249+ if ( ! symbol || symbol . getName ( ) !== 'ChunkBuilder' ) {
250+ return false
251+ }
252+
253+ return ( symbol . getDeclarations ( ) ?? [ ] ) . some ( decl => {
254+ if ( ! ts . isClassDeclaration ( decl ) ) {
255+ return false
256+ }
257+
258+ const file = decl . getSourceFile ( ) . fileName
259+
260+ return / [ / \\ ] c o r e - s d k [ / \\ ] / . test ( file ) || / [ / \\ ] s p l i t t e r \. ( d \. ) ? t s $ / . test ( file )
261+ } )
262+ }
263+
245264 const replacements : Array < { start : number ; end : number ; text : string } > = [ ]
246265
266+ // MerkleTree was removed in v13; ChunkSplitter is the replacement.
267+ let merkleTreeLocalName : string | null = null
268+ let merkleTreeImportNameNode : ts . Identifier | null = null
269+
270+ for ( const stmt of sourceFile . statements ) {
271+ if (
272+ ts . isImportDeclaration ( stmt ) &&
273+ ts . isStringLiteral ( stmt . moduleSpecifier ) &&
274+ stmt . moduleSpecifier . text === '@ethersphere/bee-js' &&
275+ stmt . importClause ?. namedBindings &&
276+ ts . isNamedImports ( stmt . importClause . namedBindings )
277+ ) {
278+ for ( const element of stmt . importClause . namedBindings . elements ) {
279+ const importedNameNode = element . propertyName ?? element . name
280+
281+ if ( importedNameNode . text === 'MerkleTree' ) {
282+ // Aliased imports keep their local name unchanged.
283+ replacements . push ( {
284+ start : importedNameNode . getStart ( sourceFile ) ,
285+ end : importedNameNode . getEnd ( ) ,
286+ text : 'ChunkSplitter' ,
287+ } )
288+
289+ if ( ! element . propertyName ) {
290+ merkleTreeLocalName = element . name . text
291+ merkleTreeImportNameNode = element . name
292+ }
293+ }
294+ }
295+ }
296+
297+ // @upcoming /swarm-core is now @ethersphere/core-sdk.
298+ if (
299+ ( ts . isImportDeclaration ( stmt ) || ts . isExportDeclaration ( stmt ) ) &&
300+ stmt . moduleSpecifier &&
301+ ts . isStringLiteral ( stmt . moduleSpecifier ) &&
302+ stmt . moduleSpecifier . text === '@upcoming/swarm-core'
303+ ) {
304+ const quote = stmt . moduleSpecifier . getText ( sourceFile ) [ 0 ] !
305+ replacements . push ( {
306+ start : stmt . moduleSpecifier . getStart ( sourceFile ) ,
307+ end : stmt . moduleSpecifier . getEnd ( ) ,
308+ text : `${ quote } @ethersphere/core-sdk${ quote } ` ,
309+ } )
310+ }
311+ }
312+
313+ // Syntactic fallback: MerkleTree no longer type-checks, so isChunkBuilderReceiver alone
314+ // won't find it.
315+ const unwrap = ( expr : ts . Expression ) : ts . Expression => {
316+ while ( ts . isParenthesizedExpression ( expr ) || ts . isAwaitExpression ( expr ) ) {
317+ expr = expr . expression
318+ }
319+
320+ return expr
321+ }
322+
323+ const isChunkSplitterRootCall = ( expr : ts . Expression ) : boolean => {
324+ const inner = unwrap ( expr )
325+
326+ return (
327+ merkleTreeLocalName !== null &&
328+ ts . isCallExpression ( inner ) &&
329+ ts . isPropertyAccessExpression ( inner . expression ) &&
330+ ts . isIdentifier ( inner . expression . expression ) &&
331+ inner . expression . expression . text === merkleTreeLocalName &&
332+ ( inner . expression . name . text === 'root' || inner . expression . name . text === 'finalize' )
333+ )
334+ }
335+
336+ const chunkBuilderVarNames = new Set < string > ( )
337+
338+ const collectChunkBuilderVars = ( node : ts . Node ) : void => {
339+ if (
340+ ts . isVariableDeclaration ( node ) &&
341+ ts . isIdentifier ( node . name ) &&
342+ node . initializer &&
343+ isChunkSplitterRootCall ( node . initializer )
344+ ) {
345+ chunkBuilderVarNames . add ( node . name . text )
346+ }
347+
348+ ts . forEachChild ( node , collectChunkBuilderVars )
349+ }
350+
351+ if ( merkleTreeLocalName ) {
352+ collectChunkBuilderVars ( sourceFile )
353+ }
354+
355+ // bee-js's MantarayNode.collect()/.find()/.findClosest() return core-sdk's MantarayNode,
356+ // not its own.
357+ const isMantarayNodeType = ( type : ts . Type , fileTest : RegExp ) : boolean => {
358+ const symbol = type . getSymbol ( )
359+
360+ if ( ! symbol || symbol . getName ( ) !== 'MantarayNode' ) {
361+ return false
362+ }
363+
364+ return ( symbol . getDeclarations ( ) ?? [ ] ) . some (
365+ decl => ts . isClassDeclaration ( decl ) && fileTest . test ( decl . getSourceFile ( ) . fileName ) ,
366+ )
367+ }
368+ const isBeeMantarayNodeType = ( type : ts . Type ) : boolean => isMantarayNodeType ( type , / [ / \\ ] b e e - j s [ / \\ ] / )
369+ const isCoreMantarayNodeType = ( type : ts . Type ) : boolean => isMantarayNodeType ( type , / [ / \\ ] c o r e - s d k [ / \\ ] / )
370+
371+ // Tracks for-of loop vars whose element type is core-sdk's MantarayNode.
372+ const coreFlavoredNames = new Set < string > ( )
373+
374+ const collectCoreFlavoredBindings = ( node : ts . Node ) : void => {
375+ if ( ts . isForOfStatement ( node ) && ts . isVariableDeclarationList ( node . initializer ) ) {
376+ const decl = node . initializer . declarations [ 0 ]
377+
378+ if ( decl && ts . isIdentifier ( decl . name ) && isCoreMantarayNodeType ( checker . getTypeAtLocation ( decl . name ) ) ) {
379+ coreFlavoredNames . add ( decl . name . text )
380+ }
381+ }
382+
383+ ts . forEachChild ( node , collectCoreFlavoredBindings )
384+ }
385+ collectCoreFlavoredBindings ( sourceFile )
386+
387+ let needsCoreMantarayNodeImport = false
388+
389+ // Rewrites a parameter's type when called with a core-flavored argument.
390+ const rewriteMantarayNodeParameters = ( node : ts . Node ) : void => {
391+ if ( ts . isCallExpression ( node ) ) {
392+ node . arguments . forEach ( ( arg , index ) => {
393+ if ( ! ts . isIdentifier ( arg ) || ! coreFlavoredNames . has ( arg . text ) ) {
394+ return
395+ }
396+
397+ const signature = checker . getResolvedSignature ( node )
398+ const decl = signature ?. getDeclaration ( )
399+ const param = decl && 'parameters' in decl ? decl . parameters [ index ] : undefined
400+
401+ if (
402+ param ?. type &&
403+ ts . isTypeReferenceNode ( param . type ) &&
404+ isBeeMantarayNodeType ( checker . getTypeFromTypeNode ( param . type ) )
405+ ) {
406+ replacements . push ( {
407+ start : param . type . getStart ( sourceFile ) ,
408+ end : param . type . getEnd ( ) ,
409+ text : 'CoreMantarayNode' ,
410+ } )
411+ needsCoreMantarayNodeImport = true
412+ }
413+ } )
414+ }
415+
416+ ts . forEachChild ( node , rewriteMantarayNodeParameters )
417+ }
418+
419+ // Rewrites a Map/Array/Set's value type when a core-flavored value is inserted.
420+ const rewriteMantarayNodeContainers = ( node : ts . Node ) : void => {
421+ if (
422+ ts . isCallExpression ( node ) &&
423+ ts . isPropertyAccessExpression ( node . expression ) &&
424+ [ 'set' , 'push' , 'add' ] . includes ( node . expression . name . text )
425+ ) {
426+ const valueArg = node . expression . name . text === 'set' ? node . arguments [ 1 ] : node . arguments [ 0 ]
427+
428+ if ( valueArg && ts . isIdentifier ( valueArg ) && coreFlavoredNames . has ( valueArg . text ) ) {
429+ const receiverSymbol = checker . getSymbolAtLocation ( node . expression . expression )
430+ const receiverDecl = receiverSymbol ?. declarations ?. find ( ts . isVariableDeclaration )
431+
432+ // Type args may be on the declaration or the constructor call.
433+ const typeArguments =
434+ receiverDecl ?. type && ts . isTypeReferenceNode ( receiverDecl . type )
435+ ? receiverDecl . type . typeArguments
436+ : receiverDecl ?. initializer && ts . isNewExpression ( receiverDecl . initializer )
437+ ? receiverDecl . initializer . typeArguments
438+ : undefined
439+
440+ if ( typeArguments ) {
441+ for ( const typeArg of typeArguments ) {
442+ if ( ts . isTypeReferenceNode ( typeArg ) && isBeeMantarayNodeType ( checker . getTypeFromTypeNode ( typeArg ) ) ) {
443+ replacements . push ( {
444+ start : typeArg . getStart ( sourceFile ) ,
445+ end : typeArg . getEnd ( ) ,
446+ text : 'CoreMantarayNode' ,
447+ } )
448+ needsCoreMantarayNodeImport = true
449+ }
450+ }
451+ }
452+ }
453+ }
454+
455+ ts . forEachChild ( node , rewriteMantarayNodeContainers )
456+ }
457+
458+ if ( coreFlavoredNames . size > 0 ) {
459+ rewriteMantarayNodeParameters ( sourceFile )
460+ rewriteMantarayNodeContainers ( sourceFile )
461+ }
462+
463+ if ( needsCoreMantarayNodeImport ) {
464+ const coreSdkImport = sourceFile . statements . find (
465+ ( stmt ) : stmt is ts . ImportDeclaration =>
466+ ts . isImportDeclaration ( stmt ) &&
467+ ts . isStringLiteral ( stmt . moduleSpecifier ) &&
468+ stmt . moduleSpecifier . text === '@ethersphere/core-sdk' ,
469+ )
470+
471+ if ( coreSdkImport ?. importClause ?. namedBindings && ts . isNamedImports ( coreSdkImport . importClause . namedBindings ) ) {
472+ const elements = coreSdkImport . importClause . namedBindings . elements
473+ const lastSpecifier = elements [ elements . length - 1 ]
474+
475+ if ( lastSpecifier ) {
476+ replacements . push ( {
477+ start : lastSpecifier . getEnd ( ) ,
478+ end : lastSpecifier . getEnd ( ) ,
479+ text : ', MantarayNode as CoreMantarayNode' ,
480+ } )
481+ }
482+ } else {
483+ const beeJsImport = sourceFile . statements . find (
484+ ( stmt ) : stmt is ts . ImportDeclaration =>
485+ ts . isImportDeclaration ( stmt ) &&
486+ ts . isStringLiteral ( stmt . moduleSpecifier ) &&
487+ stmt . moduleSpecifier . text === '@ethersphere/bee-js' ,
488+ )
489+ const insertAt = beeJsImport ? beeJsImport . getEnd ( ) : 0
490+ replacements . push ( {
491+ start : insertAt ,
492+ end : insertAt ,
493+ text : `\nimport { MantarayNode as CoreMantarayNode } from '@ethersphere/core-sdk'` ,
494+ } )
495+ }
496+ }
497+
247498 const visit = ( node : ts . Node ) : void => {
248499 // Any `<bee>.<method>` access — called or not — so bare method references migrate too.
249500 if ( ts . isPropertyAccessExpression ( node ) && ts . isIdentifier ( node . name ) ) {
@@ -261,6 +512,66 @@ export function transform(sourceFile: ts.SourceFile, checker: ts.TypeChecker): s
261512 }
262513 }
263514
515+ // jest.spyOn uses a string literal, so the rewrite above doesn't catch it.
516+ if (
517+ ts . isCallExpression ( node ) &&
518+ ts . isPropertyAccessExpression ( node . expression ) &&
519+ node . expression . name . text === 'spyOn' &&
520+ node . arguments . length >= 2
521+ ) {
522+ const [ receiverArg , methodArg ] = node . arguments
523+
524+ if ( receiverArg && methodArg && ts . isStringLiteralLike ( methodArg ) ) {
525+ const mapping = METHOD_MAP [ methodArg . text ]
526+
527+ if ( mapping && isBeeReceiver ( receiverArg ) ) {
528+ const quote = methodArg . getText ( sourceFile ) [ 0 ] !
529+ replacements . push ( {
530+ start : receiverArg . getStart ( sourceFile ) ,
531+ end : receiverArg . getEnd ( ) ,
532+ text : `${ receiverArg . getText ( sourceFile ) } .${ mapping . namespace } ` ,
533+ } )
534+ replacements . push ( {
535+ start : methodArg . getStart ( sourceFile ) ,
536+ end : methodArg . getEnd ( ) ,
537+ text : `${ quote } ${ mapping . newName } ${ quote } ` ,
538+ } )
539+ }
540+ }
541+ }
542+
543+ // ChunkBuilder.hash() now returns a Reference; append the conversion back.
544+ if (
545+ merkleTreeLocalName &&
546+ ts . isCallExpression ( node ) &&
547+ ts . isPropertyAccessExpression ( node . expression ) &&
548+ node . expression . name . text === 'hash' &&
549+ node . arguments . length === 0 &&
550+ ( isChunkSplitterRootCall ( node . expression . expression ) ||
551+ ( ts . isIdentifier ( node . expression . expression ) && chunkBuilderVarNames . has ( node . expression . expression . text ) ) ||
552+ isChunkBuilderReceiver ( node . expression . expression ) )
553+ ) {
554+ replacements . push ( {
555+ start : node . getEnd ( ) ,
556+ end : node . getEnd ( ) ,
557+ text : '.toUint8Array()' ,
558+ } )
559+ }
560+
561+ // Renames remaining MerkleTree usages (import itself already handled above).
562+ if (
563+ merkleTreeLocalName &&
564+ ts . isIdentifier ( node ) &&
565+ node . text === merkleTreeLocalName &&
566+ node !== merkleTreeImportNameNode
567+ ) {
568+ replacements . push ( {
569+ start : node . getStart ( sourceFile ) ,
570+ end : node . getEnd ( ) ,
571+ text : 'ChunkSplitter' ,
572+ } )
573+ }
574+
264575 ts . forEachChild ( node , visit )
265576 }
266577 visit ( sourceFile )
0 commit comments