@@ -23,7 +23,8 @@ const mockHashes: Map<string, string> = new Map([
2323] ) ;
2424
2525// Mock function for customizing repo changes in each test
26- const mockGetRepoChanges = jest . fn < Map < string , IFileDiffStatus > , [ ] > ( ) ;
26+ const mockGetRepoChanges : jest . MockedFunction < typeof import ( '@rushstack/package-deps-hash' ) . getRepoChanges > =
27+ jest . fn ( ) ;
2728
2829jest . mock ( `@rushstack/package-deps-hash` , ( ) => {
2930 return {
@@ -47,16 +48,22 @@ jest.mock(`@rushstack/package-deps-hash`, () => {
4748 hashFilesAsync ( rootDirectory : string , filePaths : Iterable < string > ) : ReadonlyMap < string , string > {
4849 return new Map ( Array . from ( filePaths , ( filePath : string ) => [ filePath , filePath ] ) ) ;
4950 } ,
50- getRepoChanges ( ) : Map < string , IFileDiffStatus > {
51- return mockGetRepoChanges ( ) ;
51+ getRepoChanges (
52+ currentWorkingDirectory : string ,
53+ revision ?: string ,
54+ gitPath ?: string
55+ ) : Map < string , IFileDiffStatus > {
56+ return mockGetRepoChanges ( currentWorkingDirectory , revision , gitPath ) ;
5257 }
5358 } ;
5459} ) ;
5560
5661const { Git : OriginalGit } = jest . requireActual ( '../Git' ) ;
5762
5863// Mock function for getBlobContentAsync to be customized in each test
59- const mockGetBlobContentAsync = jest . fn < Promise < string > , [ { blobSpec : string ; repositoryRoot : string } ] > ( ) ;
64+ const mockGetBlobContentAsync : jest . MockedFunction <
65+ typeof import ( '../Git' ) . Git . prototype . getBlobContentAsync
66+ > = jest . fn ( ) ;
6067
6168/** Mock Git to test `getChangedProjectsAsync` */
6269jest . mock ( '../Git' , ( ) => {
@@ -110,7 +117,7 @@ import { resolve } from 'node:path';
110117import type { IDetailedRepoState , IFileDiffStatus } from '@rushstack/package-deps-hash' ;
111118import { StringBufferTerminalProvider , Terminal } from '@rushstack/terminal' ;
112119
113- import { ProjectChangeAnalyzer } from '../ProjectChangeAnalyzer' ;
120+ import { ProjectChangeAnalyzer , isPackageJsonVersionOnlyChange } from '../ProjectChangeAnalyzer' ;
114121import { RushConfiguration } from '../../api/RushConfiguration' ;
115122import type {
116123 IInputsSnapshot ,
@@ -370,6 +377,178 @@ describe(ProjectChangeAnalyzer.name, () => {
370377 } ) ;
371378 expect ( changedProjects . has ( rushConfiguration . getProjectByName ( 'b' ) ! ) ) . toBe ( true ) ;
372379 } ) ;
380+
381+ it ( 'excludeVersionOnlyChanges does not exclude projects when package.json and other files changed' , async ( ) => {
382+ const rootDir : string = resolve ( __dirname , 'repo' ) ;
383+ const rushConfiguration : RushConfiguration = RushConfiguration . loadFromConfigurationFile (
384+ resolve ( rootDir , 'rush.json' )
385+ ) ;
386+
387+ // Mock package.json with only version change
388+ const oldPackageJsonContent = JSON . stringify (
389+ {
390+ name : 'c' ,
391+ version : '1.0.0' ,
392+ description : 'Test package' ,
393+ dependencies : {
394+ a : '1.0.0'
395+ }
396+ } ,
397+ null ,
398+ 2
399+ ) ;
400+
401+ const newPackageJsonContent = JSON . stringify (
402+ {
403+ name : 'c' ,
404+ version : '1.0.1' ,
405+ description : 'Test package' ,
406+ dependencies : {
407+ a : '1.0.0'
408+ }
409+ } ,
410+ null ,
411+ 2
412+ ) ;
413+
414+ // Set up mock repo changes - package.json AND another file changed for project 'c'
415+ mockGetRepoChanges . mockReturnValue (
416+ new Map < string , IFileDiffStatus > ( [
417+ [
418+ 'c/package.json' ,
419+ {
420+ mode : 'modified' ,
421+ newhash : 'newhash3' ,
422+ oldhash : 'oldhash3' ,
423+ status : 'M'
424+ }
425+ ] ,
426+ [
427+ 'c/src/index.ts' ,
428+ {
429+ mode : 'modified' ,
430+ newhash : 'newhash4' ,
431+ oldhash : 'oldhash4' ,
432+ status : 'M'
433+ }
434+ ]
435+ ] )
436+ ) ;
437+
438+ // Mock the blob content to return different versions based on the hash
439+ mockGetBlobContentAsync . mockImplementation ( ( opts : { blobSpec : string ; repositoryRoot : string } ) => {
440+ if ( opts . blobSpec === 'oldhash3' ) {
441+ return Promise . resolve ( oldPackageJsonContent ) ;
442+ } else if ( opts . blobSpec === 'newhash3' ) {
443+ return Promise . resolve ( newPackageJsonContent ) ;
444+ }
445+ return Promise . resolve ( '' ) ;
446+ } ) ;
447+
448+ const projectChangeAnalyzer : ProjectChangeAnalyzer = new ProjectChangeAnalyzer ( rushConfiguration ) ;
449+ const terminalProvider : StringBufferTerminalProvider = new StringBufferTerminalProvider ( true ) ;
450+ const terminal : Terminal = new Terminal ( terminalProvider ) ;
451+
452+ // Test with excludeVersionOnlyChanges - project should still be detected as changed because multiple files changed
453+ const changedProjects = await projectChangeAnalyzer . getChangedProjectsAsync ( {
454+ enableFiltering : false ,
455+ includeExternalDependencies : false ,
456+ targetBranchName : 'main' ,
457+ terminal,
458+ excludeVersionOnlyChanges : true
459+ } ) ;
460+ expect ( changedProjects . has ( rushConfiguration . getProjectByName ( 'c' ) ! ) ) . toBe ( true ) ;
461+ } ) ;
462+ } ) ;
463+
464+ describe ( 'isPackageJsonVersionOnlyChange' , ( ) => {
465+ it ( 'returns true when only version field changed' , ( ) => {
466+ const oldContent = JSON . stringify ( {
467+ name : 'test-package' ,
468+ version : '1.0.0' ,
469+ description : 'Test package' ,
470+ dependencies : { foo : '1.0.0' }
471+ } ) ;
472+ const newContent = JSON . stringify ( {
473+ name : 'test-package' ,
474+ version : '1.0.1' ,
475+ description : 'Test package' ,
476+ dependencies : { foo : '1.0.0' }
477+ } ) ;
478+
479+ expect ( isPackageJsonVersionOnlyChange ( oldContent , newContent ) ) . toBe ( true ) ;
480+ } ) ;
481+
482+ it ( 'returns false when other fields changed' , ( ) => {
483+ const oldContent = JSON . stringify ( {
484+ name : 'test-package' ,
485+ version : '1.0.0' ,
486+ description : 'Test package' ,
487+ dependencies : { foo : '1.0.0' }
488+ } ) ;
489+ const newContent = JSON . stringify ( {
490+ name : 'test-package' ,
491+ version : '1.0.1' ,
492+ description : 'Test package' ,
493+ dependencies : { foo : '1.0.1' }
494+ } ) ;
495+
496+ expect ( isPackageJsonVersionOnlyChange ( oldContent , newContent ) ) . toBe ( false ) ;
497+ } ) ;
498+
499+ it ( 'returns false when version field is missing in old content' , ( ) => {
500+ const oldContent = JSON . stringify ( {
501+ name : 'test-package' ,
502+ description : 'Test package'
503+ } ) ;
504+ const newContent = JSON . stringify ( {
505+ name : 'test-package' ,
506+ version : '1.0.1' ,
507+ description : 'Test package'
508+ } ) ;
509+
510+ expect ( isPackageJsonVersionOnlyChange ( oldContent , newContent ) ) . toBe ( false ) ;
511+ } ) ;
512+
513+ it ( 'returns false when version field is missing in new content' , ( ) => {
514+ const oldContent = JSON . stringify ( {
515+ name : 'test-package' ,
516+ version : '1.0.0' ,
517+ description : 'Test package'
518+ } ) ;
519+ const newContent = JSON . stringify ( {
520+ name : 'test-package' ,
521+ description : 'Test package'
522+ } ) ;
523+
524+ expect ( isPackageJsonVersionOnlyChange ( oldContent , newContent ) ) . toBe ( false ) ;
525+ } ) ;
526+
527+ it ( 'returns false when JSON is invalid' , ( ) => {
528+ const oldContent = 'invalid json' ;
529+ const newContent = '{ "name": "test" }' ;
530+
531+ expect ( isPackageJsonVersionOnlyChange ( oldContent , newContent ) ) . toBe ( false ) ;
532+ } ) ;
533+
534+ it ( 'returns true even with whitespace differences' , ( ) => {
535+ const oldContent = JSON . stringify (
536+ {
537+ name : 'test-package' ,
538+ version : '1.0.0' ,
539+ description : 'Test package'
540+ } ,
541+ null ,
542+ 2
543+ ) ;
544+ const newContent = JSON . stringify ( {
545+ name : 'test-package' ,
546+ version : '1.0.1' ,
547+ description : 'Test package'
548+ } ) ;
549+
550+ expect ( isPackageJsonVersionOnlyChange ( oldContent , newContent ) ) . toBe ( true ) ;
551+ } ) ;
373552 } ) ;
374553} ) ;
375554
0 commit comments