@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
22import fs from 'fs' ;
33import os from 'os' ;
44import path from 'path' ;
5+ import { npm , yarn } from 'global-dirs' ;
56import { ZipFile as YazlZipFile } from 'yazl' ;
67import { diffCommands , enumZipEntries , readEntry } from '../src/diff' ;
78import type { CommandContext } from '../src/types' ;
@@ -12,9 +13,9 @@ type ZipContent = {
1213} ;
1314
1415type DiffContextOptions = {
15- output : string ;
16- customDiff : ( oldSource ?: Buffer , newSource ?: Buffer ) => Buffer ;
17- } ;
16+ output ?: unknown ;
17+ customDiff ? : ( oldSource ?: Buffer , newSource ?: Buffer ) => Buffer ;
18+ } & Record < string , unknown > ;
1819
1920function mkTempDir ( prefix : string ) : string {
2021 return fs . mkdtempSync ( path . join ( os . tmpdir ( ) , prefix ) ) ;
@@ -72,6 +73,15 @@ function createContext(
7273 } ;
7374}
7475
76+ function hasDiffModule ( pkgName : string ) : boolean {
77+ try {
78+ require . resolve ( pkgName , { paths : [ '.' , npm . packages , yarn . packages ] } ) ;
79+ return true ;
80+ } catch {
81+ return false ;
82+ }
83+ }
84+
7585describe ( 'diff commands' , ( ) => {
7686 let tempRoot = '' ;
7787
@@ -217,4 +227,153 @@ describe('diff commands', () => {
217227 expect ( diffMeta . copies [ 'assets/icon.png' ] ) . toBe ( '' ) ;
218228 expect ( diffMeta . copies [ 'assets/new-name.png' ] ) . toBe ( 'assets/old-name.png' ) ;
219229 } ) ;
230+
231+ test ( 'diff supports explicit next directories and avoids duplicate additions' , async ( ) => {
232+ const originPath = path . join ( tempRoot , 'origin-dir.ppk' ) ;
233+ const nextPath = path . join ( tempRoot , 'next-dir.ppk' ) ;
234+ const outputPath = path . join ( tempRoot , 'out' , 'dir-diff.ppk' ) ;
235+
236+ await createZip ( originPath , {
237+ 'index.bundlejs' : 'old-bundle' ,
238+ } ) ;
239+ await createZip ( nextPath , {
240+ 'index.bundlejs' : 'new-bundle' ,
241+ 'extra/' : '' ,
242+ 'extra/new.txt' : 'new-file' ,
243+ } ) ;
244+
245+ await diffCommands . diff (
246+ createContext ( [ originPath , nextPath ] , {
247+ output : outputPath ,
248+ customDiff : ( ) => Buffer . from ( 'patch' ) ,
249+ } ) ,
250+ ) ;
251+
252+ const result = await readZipContent ( outputPath ) ;
253+ const extraDirCount = result . entries . filter (
254+ ( entry ) => entry === 'extra/' ,
255+ ) . length ;
256+ expect ( extraDirCount ) . toBe ( 1 ) ;
257+ expect ( result . files [ 'extra/new.txt' ] ?. toString ( 'utf-8' ) ) . toBe ( 'new-file' ) ;
258+ } ) ;
259+
260+ test ( 'diffFromApk throws when origin package bundle is missing' , async ( ) => {
261+ const originPath = path . join ( tempRoot , 'origin-missing-bundle.apk' ) ;
262+ const nextPath = path . join ( tempRoot , 'next-for-apk.ppk' ) ;
263+ const outputPath = path . join ( tempRoot , 'out' , 'apk-missing-bundle.ppk' ) ;
264+
265+ await createZip ( originPath , {
266+ 'assets/other.txt' : 'no-bundle' ,
267+ } ) ;
268+ await createZip ( nextPath , {
269+ 'index.bundlejs' : 'new-bundle' ,
270+ } ) ;
271+
272+ await expect (
273+ diffCommands . diffFromApk (
274+ createContext ( [ originPath , nextPath ] , {
275+ output : outputPath ,
276+ customDiff : ( ) => Buffer . from ( 'patch' ) ,
277+ } ) ,
278+ ) ,
279+ ) . rejects . toThrow ( ) ;
280+ } ) ;
281+
282+ test ( 'diffFromApk writes directory entries from next package' , async ( ) => {
283+ const originPath = path . join ( tempRoot , 'origin-dir.apk' ) ;
284+ const nextPath = path . join ( tempRoot , 'next-dir-apk.ppk' ) ;
285+ const outputPath = path . join ( tempRoot , 'out' , 'apk-dir-diff.ppk' ) ;
286+
287+ await createZip ( originPath , {
288+ 'assets/index.android.bundle' : 'old-bundle' ,
289+ } ) ;
290+ await createZip ( nextPath , {
291+ 'index.bundlejs' : 'new-bundle' ,
292+ 'assets/' : '' ,
293+ 'assets/new.txt' : 'new-file' ,
294+ } ) ;
295+
296+ await diffCommands . diffFromApk (
297+ createContext ( [ originPath , nextPath ] , {
298+ output : outputPath ,
299+ customDiff : ( oldSource , newSource ) =>
300+ Buffer . from (
301+ `patch:${ oldSource ?. toString ( 'utf-8' ) } :${ newSource ?. toString ( 'utf-8' ) } ` ,
302+ ) ,
303+ } ) ,
304+ ) ;
305+
306+ const result = await readZipContent ( outputPath ) ;
307+ expect ( result . entries ) . toContain ( 'assets/' ) ;
308+ expect ( result . files [ 'assets/new.txt' ] ?. toString ( 'utf-8' ) ) . toBe ( 'new-file' ) ;
309+ } ) ;
310+
311+ test ( 'diffFromIpa ignores non-payload files when resolving origin package path' , async ( ) => {
312+ const originPath = path . join ( tempRoot , 'origin-non-payload.ipa' ) ;
313+ const nextPath = path . join ( tempRoot , 'next-non-payload.ppk' ) ;
314+ const outputPath = path . join ( tempRoot , 'out' , 'non-payload-diff.ppk' ) ;
315+
316+ await createZip ( originPath , {
317+ 'Random/ignored.txt' : 'ignored' ,
318+ 'Payload/MyApp.app/main.jsbundle' : 'old-bundle' ,
319+ 'Payload/MyApp.app/assets/icon.png' : 'same-icon' ,
320+ } ) ;
321+ await createZip ( nextPath , {
322+ 'index.bundlejs' : 'new-bundle' ,
323+ 'assets/icon.png' : 'same-icon' ,
324+ } ) ;
325+
326+ await diffCommands . diffFromIpa (
327+ createContext ( [ originPath , nextPath ] , {
328+ output : outputPath ,
329+ customDiff : ( ) => Buffer . from ( 'patch' ) ,
330+ } ) ,
331+ ) ;
332+
333+ const result = await readZipContent ( outputPath ) ;
334+ const diffMeta = JSON . parse (
335+ result . files [ '__diff.json' ] . toString ( 'utf-8' ) ,
336+ ) as {
337+ copies : Record < string , string > ;
338+ } ;
339+ expect ( diffMeta . copies [ 'assets/icon.png' ] ) . toBe ( '' ) ;
340+ } ) ;
341+
342+ test ( 'diff throws when output option is not string' , async ( ) => {
343+ await expect (
344+ diffCommands . diff (
345+ createContext ( [ 'origin.ppk' , 'next.ppk' ] , {
346+ output : 123 ,
347+ customDiff : ( ) => Buffer . from ( 'patch' ) ,
348+ } ) ,
349+ ) ,
350+ ) . rejects . toThrow ( 'Output path is required.' ) ;
351+ } ) ;
352+
353+ test ( 'hdiff/diff require engine modules when customDiff is not provided' , async ( ) => {
354+ const hasHdiff = hasDiffModule ( 'node-hdiffpatch' ) ;
355+ const hasBsdiff = hasDiffModule ( 'node-bsdiff' ) ;
356+
357+ if ( ! hasHdiff ) {
358+ await expect (
359+ diffCommands . hdiff (
360+ createContext ( [ 'origin.ppk' , 'next.ppk' ] , {
361+ output : path . join ( tempRoot , 'out' , 'hdiff.ppk' ) ,
362+ } ) ,
363+ ) ,
364+ ) . rejects . toThrow ( / n o d e - h d i f f p a t c h / ) ;
365+ }
366+
367+ if ( ! hasBsdiff ) {
368+ await expect (
369+ diffCommands . diff (
370+ createContext ( [ 'origin.ppk' , 'next.ppk' ] , {
371+ output : path . join ( tempRoot , 'out' , 'diff.ppk' ) ,
372+ } ) ,
373+ ) ,
374+ ) . rejects . toThrow ( / n o d e - b s d i f f / ) ;
375+ }
376+
377+ expect ( true ) . toBe ( true ) ;
378+ } ) ;
220379} ) ;
0 commit comments