Skip to content

Commit 9eaefc3

Browse files
authored
Simplify scopedPackages short circuit logic (#1136)
1 parent ea39398 commit 9eaefc3

11 files changed

Lines changed: 71 additions & 85 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Simplify internal handling of determining in-scope packages",
4+
"packageName": "beachball",
5+
"email": "elcraig@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

src/__functional__/monorepo/getScopedPackages.test.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/unbound-method */
12
import { describe, expect, it, beforeAll, afterAll } from '@jest/globals';
23
import { getScopedPackages } from '../../monorepo/getScopedPackages';
34
import type { PackageInfos } from '../../types/PackageInfo';
@@ -18,10 +19,33 @@ describe('getScopedPackages', () => {
1819
removeTempDir(root);
1920
});
2021

21-
it('returns true when no scope is provided', () => {
22+
it('short circuits when no scope is provided', () => {
2223
const scopedPackages = getScopedPackages({ path: root }, packageInfos);
2324
expect(scopedPackages).toEqual(new Set(Object.keys(packageInfos)));
24-
expect(scopedPackages.allInScope).toBe(true);
25+
26+
// If all are in scope, the returned Set's `has` method is overridden to short circuit.
27+
expect(scopedPackages.has).not.toBe(Set.prototype.has);
28+
expect(scopedPackages.has('foo')).toBe(true);
29+
// But it still returns false if the package doesn't exist
30+
expect(scopedPackages.has('nonexistent')).toBe(false);
31+
});
32+
33+
it('short circuits if all in scope', () => {
34+
const scopedPackages = getScopedPackages(
35+
{
36+
path: root,
37+
scope: ['packages/**/*'],
38+
},
39+
packageInfos
40+
);
41+
42+
expect(scopedPackages).toEqual(new Set(Object.keys(packageInfos)));
43+
44+
// If all are in scope, the returned Set's `has` method is overridden to short circuit.
45+
expect(scopedPackages.has).not.toBe(Set.prototype.has);
46+
expect(scopedPackages.has('foo')).toBe(true);
47+
// But it still returns false if the package doesn't exist
48+
expect(scopedPackages.has('nonexistent')).toBe(false);
2549
});
2650

2751
it('can scope packages', () => {
@@ -34,7 +58,7 @@ describe('getScopedPackages', () => {
3458
);
3559

3660
expect(scopedPackages).toEqual(new Set(['a', 'b']));
37-
expect(scopedPackages.allInScope).toBeUndefined();
61+
expect(scopedPackages.has).toBe(Set.prototype.has);
3862
});
3963

4064
it('can scope with excluded packages', () => {
@@ -47,7 +71,7 @@ describe('getScopedPackages', () => {
4771
);
4872

4973
expect(scopedPackages).toEqual(new Set(['bar', 'baz', 'foo']));
50-
expect(scopedPackages.allInScope).toBeUndefined();
74+
expect(scopedPackages.has).toBe(Set.prototype.has);
5175
});
5276

5377
it('can mix and match with excluded packages', () => {
@@ -60,6 +84,6 @@ describe('getScopedPackages', () => {
6084
);
6185

6286
expect(scopedPackages).toEqual(new Set(['bar', 'baz']));
63-
expect(scopedPackages.allInScope).toBeUndefined();
87+
expect(scopedPackages.has).toBe(Set.prototype.has);
6488
});
6589
});
Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { describe, it, expect } from '@jest/globals';
2-
import type { BumpInfo } from '../../types/BumpInfo';
32
import { cloneObject } from '../../object/cloneObject';
4-
import { makePackageInfos } from '../../__fixtures__/packageInfos';
5-
import { getChange } from '../../__fixtures__/changeFiles';
63

74
describe('cloneObject', () => {
85
it.each<[string, object | unknown[]]>([
@@ -12,8 +9,6 @@ describe('cloneObject', () => {
129
['object with null prototype', Object.assign(Object.create(null), { a: 1, b: '2', c: true })],
1310
['empty array', []],
1411
['array', [1, '2', true]],
15-
['set', new Set([1, 2, 3])],
16-
['object of sets', { a: new Set([1, 2, 3]), b: new Set(['a', 'b', 'c']) }],
1712
])('clones %s', (desc, val) => {
1813
const cloned = cloneObject(val);
1914
expect(cloned).toEqual(val);
@@ -44,35 +39,6 @@ describe('cloneObject', () => {
4439
expect(() => cloneObject(new Map())).toThrow('Unsupported object type found while cloning bump info: Map');
4540
class Foo {}
4641
expect(() => cloneObject(new Foo())).toThrow('Unsupported object type found while cloning bump info: Foo');
47-
});
48-
49-
it('clones bump info structure', () => {
50-
const original: BumpInfo = {
51-
// There's no attempt at consistency because it doesn't matter here
52-
calculatedChangeTypes: { pkgA: 'minor', pkgB: 'patch' },
53-
packageInfos: makePackageInfos({ a: { dependencies: { b: '^1.0.0' } }, b: {} }),
54-
changeFileChangeInfos: [
55-
{ change: getChange('a'), changeFile: '' },
56-
{ change: getChange('b'), changeFile: '' },
57-
],
58-
packageGroups: { group1: { packageNames: ['a', 'b'], disallowedChangeTypes: null } },
59-
dependentChangedBy: { a: new Set(['b']) },
60-
modifiedPackages: new Set(['a']),
61-
scopedPackages: new Set(['a', 'b']),
62-
};
63-
64-
const cloned = cloneObject(original);
65-
expect(cloned).toEqual(original);
66-
expect(cloned).not.toBe(original);
67-
expect(cloned.packageInfos).not.toBe(original.packageInfos);
68-
expect(cloned.packageInfos.a).not.toBe(original.packageInfos.a);
69-
expect(cloned.changeFileChangeInfos).not.toBe(original.changeFileChangeInfos);
70-
expect(cloned.changeFileChangeInfos[0]).not.toBe(original.changeFileChangeInfos[0]);
71-
expect(cloned.changeFileChangeInfos[0].change).not.toBe(original.changeFileChangeInfos[0].change);
72-
expect(cloned.packageGroups).not.toBe(original.packageGroups);
73-
expect(cloned.dependentChangedBy).not.toBe(original.dependentChangedBy);
74-
expect(cloned.dependentChangedBy.a).not.toBe(original.dependentChangedBy.a);
75-
expect(cloned.modifiedPackages).not.toBe(original.modifiedPackages);
76-
expect(cloned.scopedPackages).not.toBe(original.scopedPackages);
42+
expect(() => cloneObject(new Set())).toThrow('Unsupported object type found while cloning bump info: Set');
7743
});
7844
});

src/bump/setDependentVersions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function setDependentVersions(
2020
const dependentChangedBy: BumpInfo['dependentChangedBy'] = {};
2121

2222
for (const [pkgName, info] of Object.entries(packageInfos)) {
23-
if (!scopedPackages.allInScope && !scopedPackages.has(pkgName)) {
23+
if (!scopedPackages.has(pkgName)) {
2424
continue; // out of scope
2525
}
2626

src/changefile/getChangedPackages.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function isPackageIncluded(
4848
: // This is a package-only option (can't be set at repo level or via CLI)
4949
packageInfo.packageOptions?.shouldPublish === false
5050
? `${packageInfo.name} has beachball.shouldPublish=false`
51-
: !scopedPackages.allInScope && !scopedPackages.has(packageInfo.name)
51+
: !scopedPackages.has(packageInfo.name)
5252
? `${packageInfo.name} is out of scope`
5353
: ''; // not ignored
5454

src/changefile/readChangeFiles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export function readChangeFiles(
103103
}
104104

105105
// Add the change to the final list if it's valid and in scope
106-
if (!warningType && (scopedPackages.allInScope || scopedPackages.has(change.packageName))) {
106+
if (!warningType && scopedPackages.has(change.packageName)) {
107107
changeSet.push({ changeFile, change });
108108
}
109109
}

src/commands/sync.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,7 @@ export async function sync(options: BeachballOptions, context?: SyncCommandConte
2121
const packageInfos = context?.originalPackageInfos ?? getPackageInfos(options.path);
2222
const scopedPackages = context?.scopedPackages ?? getScopedPackages(options, packageInfos);
2323

24-
const infos = new Map(
25-
Object.entries(packageInfos).filter(
26-
([pkg, info]) => !info.private && (scopedPackages.allInScope || scopedPackages.has(pkg))
27-
)
28-
);
24+
const infos = new Map(Object.entries(packageInfos).filter(([pkg, info]) => !info.private && scopedPackages.has(pkg)));
2925

3026
console.log(`Getting versions from registry for ${infos.size} package(s)...`);
3127

src/monorepo/getScopedPackages.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,32 @@ export function getScopedPackages(
1111
packageInfos: PackageInfos
1212
): ScopedPackages {
1313
const { scope, path: cwd } = options;
14-
if (!scope) {
15-
const result: ScopedPackages = new Set(Object.keys(packageInfos));
16-
result.allInScope = true;
17-
return result;
18-
}
1914

20-
let includeScopes: string[] | true = scope.filter(s => !s.startsWith('!'));
21-
// If there were no include scopes, include all paths by default
22-
includeScopes = includeScopes.length ? includeScopes : true;
15+
const packageNames = Object.keys(packageInfos);
16+
let result: Set<string>;
17+
18+
if (scope) {
19+
let includeScopes: string[] | true = scope.filter(s => !s.startsWith('!'));
20+
// If there were no include scopes, include all paths by default
21+
includeScopes = includeScopes.length ? includeScopes : true;
22+
23+
const excludeScopes = scope.filter(s => s.startsWith('!'));
2324

24-
const excludeScopes = scope.filter(s => s.startsWith('!'));
25+
result = new Set(
26+
packageNames.filter(pkgName => {
27+
const packagePath = path.dirname(packageInfos[pkgName].packageJsonPath);
2528

26-
const result = Object.keys(packageInfos).filter(pkgName => {
27-
const packagePath = path.dirname(packageInfos[pkgName].packageJsonPath);
29+
return isPathIncluded(path.relative(cwd, packagePath), includeScopes, excludeScopes);
30+
})
31+
);
32+
} else {
33+
result = new Set(packageNames);
34+
}
35+
36+
if (result.size === packageNames.length) {
37+
// Override .has() to always return true unless the package doesn't exist
38+
result.has = packageName => !!packageInfos[packageName];
39+
}
2840

29-
return isPathIncluded(path.relative(cwd, packagePath), includeScopes, excludeScopes);
30-
});
31-
return new Set(result);
41+
return result;
3242
}

src/object/cloneObject.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import type { ScopedPackages } from '../types/PackageInfo';
2-
31
/**
4-
* Clone an object, fast.
5-
* Currently only handles data types expected in `BumpInfo` but could be expanded if needed.
2+
* Clone an object, fast. Currently only handles JSON-like objects (not class instances)
3+
* but could be expanded if needed.
64
*
75
* This is decently faster than `structuredClone` or `JSON.parse(JSON.stringify())` on a
86
* very large object (bump info can be huge in certain repos). https://jsperf.app/rugosa/5
@@ -29,18 +27,6 @@ export function cloneObject<T extends object>(obj: T): T {
2927
return clone;
3028
}
3129

32-
if (obj instanceof Set) {
33-
const cloned = new Set(
34-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
35-
Array.from(obj).map(item => (item && typeof item === 'object' ? cloneObject(item) : item))
36-
) as T;
37-
// special logic to clone a custom set property
38-
if ((obj as ScopedPackages).allInScope) {
39-
(cloned as ScopedPackages).allInScope = true;
40-
}
41-
return cloned;
42-
}
43-
4430
if (obj.constructor?.name && obj.constructor.name !== 'Object') {
4531
throw new Error(`Unsupported object type found while cloning bump info: ${obj.constructor.name}`);
4632
}

src/publish/getPackagesToPublish.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function getPackagesToPublish(
3737
skipReason = 'has change type none';
3838
} else if (packageInfo.private) {
3939
skipReason = 'is private';
40-
} else if (!scopedPackages.allInScope && !scopedPackages.has(pkg)) {
40+
} else if (!scopedPackages.has(pkg)) {
4141
skipReason = 'is out-of-scope';
4242
} else if (!changeType && !newPackages?.includes(pkg)) {
4343
skipReason = 'is not bumped (no calculated change type)';

0 commit comments

Comments
 (0)