Skip to content

Commit 6c1b744

Browse files
committed
Support catalog versions
1 parent be6dca6 commit 6c1b744

17 files changed

Lines changed: 678 additions & 106 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Support catalog versions",
4+
"packageName": "beachball",
5+
"email": "elcraig@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"prompts": "^2.4.2",
6161
"semver": "^7.0.0",
6262
"toposort": "^2.0.2",
63-
"workspace-tools": "^0.40.0",
63+
"workspace-tools": "^0.40.2",
6464
"yargs-parser": "^21.0.0"
6565
},
6666
"devDependencies": {

src/__e2e__/bump.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { createCommandContext } from '../monorepo/createCommandContext';
1717
import type { CommandContext } from '../types/CommandContext';
1818
import type { BumpInfo } from '../types/BumpInfo';
1919
import { deepFreeze } from '../__fixtures__/object';
20+
import { catalogsToYaml, type Catalogs } from 'workspace-tools';
2021

2122
//
2223
// These tests use git repos and are slow, so besides a few basic scenarios, this file should
@@ -485,6 +486,48 @@ describe('bump command', () => {
485486
expect(readChangelogJson(repo.pathTo('packages/pkg-2'))).toBeNull();
486487
});
487488

489+
it('bumps dependents with catalog: deps', async () => {
490+
const monorepo: RepoFixture['folders'] = {
491+
packages: {
492+
'pkg-1': { version: '1.0.0' },
493+
'pkg-2': { version: '1.0.0', dependencies: { 'pkg-1': 'catalog:' } },
494+
'pkg-3': { version: '1.0.0', dependencies: { 'pkg-2': 'catalog:' } },
495+
},
496+
};
497+
const catalogs: Catalogs = {
498+
default: { 'pkg-1': 'workspace:~', 'pkg-2': 'workspace:^1.0.0' },
499+
};
500+
repositoryFactory = new RepositoryFactory({
501+
folders: monorepo,
502+
// This isn't currently read by bump() but should be present for completeness
503+
extraFiles: { '.yarnrc.yml': catalogsToYaml(catalogs) },
504+
});
505+
repo = repositoryFactory.cloneRepository();
506+
507+
const { options, parsedOptions } = getOptions({
508+
bumpDeps: true,
509+
});
510+
generateChangeFiles([{ packageName: 'pkg-1', type: 'minor' }], options);
511+
repo.push();
512+
513+
// The bumpInfo object is covered by the similar test in bumpInMemory.test.ts
514+
const { originalPackageInfos } = await bumpWrapper(parsedOptions);
515+
516+
const packageInfos = getPackageInfos(parsedOptions);
517+
518+
// All the dependent packages are bumped despite the catalog: dep specs
519+
expect(packageInfos['pkg-1'].version).toBe('1.1.0');
520+
// catalog: ranges aren't changed
521+
expect(packageInfos['pkg-2']).toEqual({ ...originalPackageInfos['pkg-2'], version: '1.0.1' });
522+
expect(packageInfos['pkg-3']).toEqual({ ...originalPackageInfos['pkg-3'], version: '1.0.1' });
523+
524+
expect(readChangelogJson(repo.pathTo('packages/pkg-1'))).not.toBeNull();
525+
// Current behavior: dependentChangedBy misses catalog: deps, so there are no changelog entries
526+
// https://github.com/microsoft/beachball/issues/981
527+
expect(readChangelogJson(repo.pathTo('packages/pkg-2'))).toBeNull();
528+
expect(readChangelogJson(repo.pathTo('packages/pkg-3'))).toBeNull();
529+
});
530+
488531
// Explicit tests for sync/async hooks aren't necessary, especially since these are slow tests.
489532
// Async is slightly trickier, so test that.
490533
it('calls prebump/postbump hooks', async () => {

src/__e2e__/publishE2E.test.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { describe, expect, it, afterEach, jest } from '@jest/globals';
22
import fs from 'fs';
33
import path from 'path';
4-
import { addGitObserver, clearGitObservers } from 'workspace-tools';
4+
import { addGitObserver, catalogsToYaml, clearGitObservers, type Catalogs } from 'workspace-tools';
55
import { generateChangeFiles, getChangeFiles } from '../__fixtures__/changeFiles';
66
import { defaultBranchName, defaultRemoteBranchName } from '../__fixtures__/gitDefaults';
77
import { initMockLogs } from '../__fixtures__/mockLogs';
88
import type { Repository } from '../__fixtures__/repository';
9-
import { type PackageJsonFixture, RepositoryFactory } from '../__fixtures__/repositoryFactory';
9+
import { type PackageJsonFixture, type RepoFixture, RepositoryFactory } from '../__fixtures__/repositoryFactory';
1010
import { publish } from '../commands/publish';
1111
import type { ParsedOptions, RepoOptions } from '../types/BeachballOptions';
1212
import { _mockNpmPublish, initNpmMock } from '../__fixtures__/mockNpm';
@@ -62,6 +62,7 @@ describe('publish command (e2e)', () => {
6262
deepFreezeProperties(context.bumpInfo);
6363
deepFreezeProperties(context.originalPackageInfos);
6464
await publish(parsedOptions.options, context);
65+
return context;
6566
}
6667

6768
afterEach(() => {
@@ -334,6 +335,72 @@ describe('publish command (e2e)', () => {
334335
expect(newPackageInfos.foo.version).toBe('1.0.0');
335336
});
336337

338+
// Combine workspace/catalog/file cases since these tests are slow
339+
it('publishes packages with workspace: and catalog: deps and replaces versions', async () => {
340+
const monorepo: RepoFixture['folders'] = {
341+
packages: {
342+
'pkg-1': { version: '1.0.0' },
343+
'pkg-2': { version: '1.0.0', dependencies: { 'pkg-1': 'workspace:~', react: 'catalog:react18' } },
344+
'pkg-3': { version: '1.0.0', dependencies: { 'pkg-2': 'workspace:^1.0.0' } },
345+
'pkg-4': {
346+
version: '1.0.0',
347+
dependencies: { 'pkg-1': 'catalog:' },
348+
devDependencies: { 'pkg-2': 'file:../pkg-2' },
349+
},
350+
},
351+
};
352+
const catalogs: Catalogs = {
353+
default: { 'pkg-1': 'workspace:~' }, // only yarn supports workspace: inside catalog
354+
named: { react18: { react: '^18.0.0' } },
355+
};
356+
repositoryFactory = new RepositoryFactory({
357+
folders: monorepo,
358+
extraFiles: { '.yarnrc.yml': catalogsToYaml(catalogs) },
359+
});
360+
repo = repositoryFactory.cloneRepository();
361+
362+
const { options, parsedOptions } = getOptions({ bumpDeps: true, fetch: false });
363+
generateChangeFiles([{ packageName: 'pkg-1', type: 'minor' }], options);
364+
repo.push();
365+
366+
const { originalPackageInfos } = await publishWrapper(parsedOptions);
367+
repo.checkout(defaultBranchName);
368+
repo.pull();
369+
370+
expect(repo.getCurrentTags()).toEqual(['pkg-1_v1.1.0', 'pkg-2_v1.0.1', 'pkg-3_v1.0.1', 'pkg-4_v1.0.1']);
371+
372+
// All the dependent packages are bumped despite the workspace: dep specs.
373+
// The literal workspace: specs are preserved in git.
374+
const packageInfos = getPackageInfos(parsedOptions);
375+
expect(packageInfos['pkg-1'].version).toBe('1.1.0');
376+
// workspace:~ and catalog: ranges aren't changed
377+
expect(packageInfos['pkg-2']).toEqual({ ...originalPackageInfos['pkg-2'], version: '1.0.1' });
378+
expect(packageInfos['pkg-2'].version).toBe('1.0.1');
379+
// workspace: range with number is updated
380+
expect(packageInfos['pkg-3'].dependencies).toEqual({ 'pkg-2': 'workspace:^1.0.1' });
381+
// catalog: range isn't changed
382+
expect(packageInfos['pkg-4']).toEqual({ ...originalPackageInfos['pkg-4'], version: '1.0.1' });
383+
384+
// The changelogs are adequately covered by the similar bump test.
385+
386+
// Verify that the published packages have the actual resolved versions
387+
expect(npmMock.getPublishedPackage('pkg-1')).toMatchObject({ version: '1.1.0' });
388+
expect(npmMock.getPublishedPackage('pkg-2')).toMatchObject({
389+
version: '1.0.1',
390+
dependencies: { 'pkg-1': '~1.1.0', react: '^18.0.0' },
391+
});
392+
expect(npmMock.getPublishedPackage('pkg-3')).toMatchObject({
393+
version: '1.0.1',
394+
dependencies: { 'pkg-2': '^1.0.1' },
395+
});
396+
expect(npmMock.getPublishedPackage('pkg-4')).toMatchObject({
397+
version: '1.0.1',
398+
dependencies: { 'pkg-1': '~1.1.0' },
399+
// file: deps aren't currently replaced (definitely fine for dev deps, questionable for prod)
400+
devDependencies: { 'pkg-2': 'file:../pkg-2' },
401+
});
402+
});
403+
337404
// These tests are slow, so combine pre and post hooks
338405
it('respects prepublish/postpublish hooks', async () => {
339406
repositoryFactory = new RepositoryFactory('monorepo');
@@ -499,6 +566,7 @@ describe('publish command (e2e)', () => {
499566
});
500567

501568
// Just test postpublish (prepublish should have the same logic)
569+
// TODO: possibly move to in-memory test
502570
it('respects concurrency limit for publish hooks', async () => {
503571
const packagesToPublish = ['pkg1', 'pkg2', 'pkg3', 'pkg4'];
504572
type ExtraPackageJsonFixture = PackageJsonFixture & { customAfterPublish?: { notify: string } };

src/__fixtures__/repository.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ ${gitResult.stderr.toString()}`);
187187
return result.stdout.trim();
188188
}
189189

190-
/** Get tags pointing to the current HEAD commit */
190+
/** Get sorted list of tags pointing to the current HEAD commit */
191191
getCurrentTags(): string[] {
192192
const tagsResult = this.git(['tag', '--points-at', 'HEAD']);
193193
const trimmedResult = tagsResult.stdout.trim();

src/__fixtures__/repositoryFactory.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ export type RepoFixture = {
5151

5252
/** Description to include in the temp folder name */
5353
tempDescription?: string;
54+
55+
/**
56+
* Mapping from extra file relative path to contents.
57+
* (A `yarn.lock` file is written automatically.)
58+
*/
59+
extraFiles?: Record<string, string>;
5460
};
5561

5662
/** Repo fixture with all required props filled out */
@@ -233,6 +239,14 @@ export class RepositoryFactory {
233239
writeJson(path.join(pkgFolder, 'package.json'), packageJson);
234240
}
235241
}
242+
243+
if (fixture.extraFiles) {
244+
for (const [relativePath, contents] of Object.entries(fixture.extraFiles)) {
245+
const fullPath = tmpRepo.pathTo(parentFolder, relativePath);
246+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
247+
fs.writeFileSync(fullPath, contents);
248+
}
249+
}
236250
}
237251
tmpRepo.commitAll(`committing fixture`);
238252

src/__tests__/bump/bumpInMemory.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,11 +312,43 @@ describe('bumpInMemory', () => {
312312
expect(packageInfos['pkg-1'].version).toBe('1.1.0');
313313
// workspace:~ range isn't changed
314314
expect(packageInfos['pkg-2']).toEqual({ ...originalPackageInfos['pkg-2'], version: '1.0.1' });
315-
expect(packageInfos['pkg-2'].version).toBe('1.0.1');
316315
// workspace: range with number is updated
317316
expect(packageInfos['pkg-3'].dependencies).toEqual({ 'pkg-2': 'workspace:^1.0.1' });
318317
});
319318

319+
it('bumps dependents with catalog: deps', () => {
320+
const { bumpInfo, originalPackageInfos } = gatherBumpInfoWrapper({
321+
// Say there's a catalog like this:
322+
// catalog:
323+
// pkg-1: workspace:~
324+
// pkg-2: workspace:^1.0.0
325+
packageFolders: {
326+
'pkg-1': { version: '1.0.0' },
327+
// both of these are detected as dependent bumps but currently missed from changelog
328+
'pkg-2': { version: '1.0.0', dependencies: { 'pkg-1': 'catalog:' } },
329+
'pkg-3': { version: '1.0.0', dependencies: { 'pkg-2': 'catalog:' } },
330+
},
331+
repoOptions: { bumpDeps: true },
332+
changes: ['pkg-1'],
333+
});
334+
335+
const { packageInfos, modifiedPackages, calculatedChangeTypes, dependentChangedBy } = bumpInfo;
336+
expect(modifiedPackages).toEqual(new Set(['pkg-1', 'pkg-2', 'pkg-3']));
337+
expect(calculatedChangeTypes).toEqual({ 'pkg-1': 'minor', 'pkg-2': 'patch', 'pkg-3': 'patch' });
338+
expect(dependentChangedBy).toEqual({
339+
// Current behavior: dependentChangedBy misses all catalog: deps pointing to workspace: versions
340+
// https://github.com/microsoft/beachball/issues/981
341+
// 'pkg-2': new Set(['pkg-1']),
342+
// 'pkg-3': new Set(['pkg-2']),
343+
});
344+
345+
// All the dependent packages are bumped despite the catalog: dep specs
346+
expect(packageInfos['pkg-1'].version).toBe('1.1.0');
347+
// catalog: ranges aren't changed
348+
expect(packageInfos['pkg-2']).toEqual({ ...originalPackageInfos['pkg-2'], version: '1.0.1' });
349+
expect(packageInfos['pkg-3']).toEqual({ ...originalPackageInfos['pkg-3'], version: '1.0.1' });
350+
});
351+
320352
it('bumps to prerelease using prefix, and uses prerelease version for dependents', () => {
321353
const { bumpInfo } = gatherBumpInfoWrapper({
322354
packageFolders: {

src/__tests__/bump/bumpMinSemverRange.test.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,74 +3,74 @@ import { bumpMinSemverRange } from '../../bump/bumpMinSemverRange';
33

44
describe('bumpMinSemverRange', () => {
55
it('preserves *', () => {
6-
const result = bumpMinSemverRange('1.0.0', '*');
6+
const result = bumpMinSemverRange({ newVersion: '1.0.0', currentRange: '*' });
77
expect(result).toBe('*');
88
});
99

1010
it('preserves file: protocol with relative path', () => {
11-
const result = bumpMinSemverRange('1.0.0', 'file:../local-package');
11+
const result = bumpMinSemverRange({ newVersion: '1.0.0', currentRange: 'file:../local-package' });
1212
expect(result).toBe('file:../local-package');
1313
});
1414

1515
it('preserves file: protocol with absolute path', () => {
16-
const result = bumpMinSemverRange('1.0.0', 'file:/absolute/path/to/package');
16+
const result = bumpMinSemverRange({ newVersion: '1.0.0', currentRange: 'file:/absolute/path/to/package' });
1717
expect(result).toBe('file:/absolute/path/to/package');
1818
});
1919

2020
it('preserves catalog: protocol', () => {
21-
let result = bumpMinSemverRange('1.0.0', 'catalog:');
21+
let result = bumpMinSemverRange({ newVersion: '1.0.0', currentRange: 'catalog:' });
2222
expect(result).toBe('catalog:');
23-
result = bumpMinSemverRange('1.0.0', 'catalog:foo');
23+
result = bumpMinSemverRange({ newVersion: '1.0.0', currentRange: 'catalog:foo' });
2424
expect(result).toBe('catalog:foo');
2525
});
2626

2727
it.each(['~', '^'])('preserves %s and bumps to new version', prefix => {
28-
const result = bumpMinSemverRange('1.3.0', `${prefix}1.2.0`);
28+
const result = bumpMinSemverRange({ newVersion: '1.3.0', currentRange: `${prefix}1.2.0` });
2929
expect(result).toBe(`${prefix}1.3.0`);
3030
});
3131

3232
it('returns range from new version to next major with >=', () => {
33-
const result = bumpMinSemverRange('1.3.0', '>=1.2.0 <2.0.0');
33+
const result = bumpMinSemverRange({ newVersion: '1.3.0', currentRange: '>=1.2.0 <2.0.0' });
3434
expect(result).toBe('>=1.3.0 <2.0.0');
3535
});
3636

3737
it('returns range from new version to next major with >', () => {
38-
const result = bumpMinSemverRange('1.3.0', '>1.2.0 <2.0.0');
38+
const result = bumpMinSemverRange({ newVersion: '1.3.0', currentRange: '>1.2.0 <2.0.0' });
3939
expect(result).toBe('>=1.3.0 <2.0.0');
4040
});
4141

4242
it('returns range from new version to next major with -', () => {
43-
const result = bumpMinSemverRange('1.3.0', '1.2.0 - 2.0.0');
43+
const result = bumpMinSemverRange({ newVersion: '1.3.0', currentRange: '1.2.0 - 2.0.0' });
4444
expect(result).toBe('1.3.0 - 2.0.0');
4545
});
4646

4747
it.each(['workspace:*', 'workspace:~', 'workspace:^'])('preserves %s', workspaceVersion => {
48-
const result = bumpMinSemverRange('1.3.0', workspaceVersion);
48+
const result = bumpMinSemverRange({ newVersion: '1.3.0', currentRange: workspaceVersion });
4949
expect(result).toBe(workspaceVersion);
5050
});
5151

5252
it('bumps workspace:~x.y.z to workspace range with new version', () => {
53-
const result = bumpMinSemverRange('1.2.1', 'workspace:~1.2.0');
53+
const result = bumpMinSemverRange({ newVersion: '1.2.1', currentRange: 'workspace:~1.2.0' });
5454
expect(result).toBe('workspace:~1.2.1');
5555
});
5656

5757
it('bumps workspace:^x.y.z to workspace range with new version', () => {
58-
const result = bumpMinSemverRange('1.3.0', 'workspace:^1.2.0');
58+
const result = bumpMinSemverRange({ newVersion: '1.3.0', currentRange: 'workspace:^1.2.0' });
5959
expect(result).toBe('workspace:^1.3.0');
6060
});
6161

6262
it('uses the new version for exact version match', () => {
63-
const result = bumpMinSemverRange('1.3.0', '1.2.0');
63+
const result = bumpMinSemverRange({ newVersion: '1.3.0', currentRange: '1.2.0' });
6464
expect(result).toBe('1.3.0');
6565
});
6666

6767
it('uses the new version if unknown non-semver format', () => {
68-
const result = bumpMinSemverRange('1.3.0', '#1.2.0');
68+
const result = bumpMinSemverRange({ newVersion: '1.3.0', currentRange: '#1.2.0' });
6969
expect(result).toBe('1.3.0');
7070
});
7171

7272
it('preserves unrecognized range if new version satisfies it', () => {
73-
const result = bumpMinSemverRange('1.3.0', '1');
73+
const result = bumpMinSemverRange({ newVersion: '1.3.0', currentRange: '1' });
7474
expect(result).toBe('1');
7575
});
7676
});

src/__tests__/bump/setDependentVersions.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,23 @@ describe('setDependentVersions', () => {
172172
expect(bumpInfo.packageInfos['pkg-b'].dependencies!['pkg-a']).toBe('workspace:*');
173173
expect(result).toEqual({}); // should have pkg-b depending on pkg-a
174174
});
175+
176+
// Documenting this issue
177+
// https://github.com/microsoft/beachball/issues/981
178+
it('currently misses bumps of catalog: ranges', () => {
179+
const bumpInfo = makeBumpInfo({
180+
packageInfos: {
181+
'pkg-a': { version: '1.1.0' },
182+
// Assume there's a catalog like this:
183+
// catalog:
184+
// pkg-a: ^1.1.0
185+
'pkg-b': { version: '1.0.1', dependencies: { 'pkg-a': 'catalog:' } },
186+
},
187+
});
188+
189+
const result = setDependentVersions(bumpInfo, {});
190+
191+
expect(bumpInfo.packageInfos['pkg-b'].dependencies!['pkg-a']).toBe('catalog:');
192+
expect(result).toEqual({}); // should have pkg-b depending on pkg-a
193+
});
175194
});

0 commit comments

Comments
 (0)