Skip to content

Commit c3c623d

Browse files
committed
Support catalog versions
1 parent be6dca6 commit c3c623d

17 files changed

Lines changed: 700 additions & 86 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: 111 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,113 @@ 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:~' } },
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:~' },
354+
};
355+
repositoryFactory = new RepositoryFactory({
356+
folders: monorepo,
357+
extraFiles: { '.yarnrc.yml': catalogsToYaml(catalogs) },
358+
});
359+
repo = repositoryFactory.cloneRepository();
360+
361+
const { options, parsedOptions } = getOptions({ bumpDeps: true, fetch: false });
362+
generateChangeFiles([{ packageName: 'pkg-1', type: 'minor' }], options);
363+
repo.push();
364+
365+
const { originalPackageInfos } = await publishWrapper(parsedOptions);
366+
repo.checkout(defaultBranchName);
367+
repo.pull();
368+
369+
expect(repo.getCurrentTags()).toEqual(['pkg-1_v1.1.0', 'pkg-2_v1.0.1', 'pkg-3_v1.0.1', 'pkg-4_v1.0.1']);
370+
371+
// All the dependent packages are bumped despite the workspace: dep specs.
372+
// The literal workspace: specs are preserved in git.
373+
const packageInfos = getPackageInfos(parsedOptions);
374+
expect(packageInfos['pkg-1'].version).toBe('1.1.0');
375+
// workspace:~ range isn't changed
376+
expect(packageInfos['pkg-2']).toEqual({ ...originalPackageInfos['pkg-2'], version: '1.0.1' });
377+
expect(packageInfos['pkg-2'].version).toBe('1.0.1');
378+
// workspace: range with number is updated
379+
expect(packageInfos['pkg-3'].dependencies).toEqual({ 'pkg-2': 'workspace:^1.0.1' });
380+
// catalog: range isn't changed
381+
expect(packageInfos['pkg-4']).toEqual({ ...originalPackageInfos['pkg-4'], version: '1.0.1' });
382+
383+
// The changelogs are adequately covered by the similar bump test.
384+
385+
// Verify that the published packages have the actual resolved versions
386+
expect(npmMock.getPublishedPackage('pkg-1')).toMatchObject({ version: '1.1.0' });
387+
expect(npmMock.getPublishedPackage('pkg-2')).toMatchObject({
388+
version: '1.0.1',
389+
dependencies: { 'pkg-1': '~1.1.0' },
390+
});
391+
expect(npmMock.getPublishedPackage('pkg-3')).toMatchObject({
392+
version: '1.0.1',
393+
dependencies: { 'pkg-2': '^1.0.1' },
394+
});
395+
expect(npmMock.getPublishedPackage('pkg-4')).toMatchObject({
396+
version: '1.0.1',
397+
dependencies: { 'pkg-1': '~1.1.0' },
398+
// file: deps aren't currently replaced (definitely fine for dev deps, questionable for prod)
399+
devDependencies: { 'pkg-2': 'file:../pkg-2' },
400+
});
401+
});
402+
403+
// it('bumps dependents with catalog: deps', async () => {
404+
// const monorepo: RepoFixture['folders'] = {
405+
// packages: {
406+
// 'pkg-1': { version: '1.0.0' },
407+
// 'pkg-2': { version: '1.0.0', dependencies: { 'pkg-1': 'catalog:' } },
408+
// 'pkg-3': { version: '1.0.0', dependencies: { 'pkg-2': 'catalog:' } },
409+
// },
410+
// };
411+
// const catalogs: Catalogs = {
412+
// default: { 'pkg-1': 'workspace:~', 'pkg-2': 'workspace:^1.0.0' },
413+
// };
414+
// repositoryFactory = new RepositoryFactory({
415+
// folders: monorepo,
416+
// // This isn't currently read by bump() but should be present for completeness
417+
// extraFiles: { '.yarnrc.yml': catalogsToYaml(catalogs) },
418+
// });
419+
// repo = repositoryFactory.cloneRepository();
420+
421+
// const { options, parsedOptions } = getOptions({
422+
// bumpDeps: true,
423+
// });
424+
// generateChangeFiles([{ packageName: 'pkg-1', type: 'minor' }], options);
425+
// repo.push();
426+
427+
// // The bumpInfo object is covered by the similar test in bumpInMemory.test.ts
428+
// const { originalPackageInfos } = await bumpWrapper(parsedOptions);
429+
430+
// const packageInfos = getPackageInfos(parsedOptions);
431+
432+
// // All the dependent packages are bumped despite the catalog: dep specs
433+
// expect(packageInfos['pkg-1'].version).toBe('1.1.0');
434+
// // catalog: ranges aren't changed
435+
// expect(packageInfos['pkg-2']).toEqual({ ...originalPackageInfos['pkg-2'], version: '1.0.1' });
436+
// expect(packageInfos['pkg-3']).toEqual({ ...originalPackageInfos['pkg-3'], version: '1.0.1' });
437+
438+
// expect(readChangelogJson(repo.pathTo('packages/pkg-1'))).not.toBeNull();
439+
// // Current behavior: dependentChangedBy misses catalog: deps, so there are no changelog entries
440+
// // https://github.com/microsoft/beachball/issues/981
441+
// expect(readChangelogJson(repo.pathTo('packages/pkg-2'))).toBeNull();
442+
// expect(readChangelogJson(repo.pathTo('packages/pkg-3'))).toBeNull();
443+
// });
444+
337445
// These tests are slow, so combine pre and post hooks
338446
it('respects prepublish/postpublish hooks', async () => {
339447
repositoryFactory = new RepositoryFactory('monorepo');
@@ -499,6 +607,7 @@ describe('publish command (e2e)', () => {
499607
});
500608

501609
// Just test postpublish (prepublish should have the same logic)
610+
// TODO: possibly move to in-memory test
502611
it('respects concurrency limit for publish hooks', async () => {
503612
const packagesToPublish = ['pkg1', 'pkg2', 'pkg3', 'pkg4'];
504613
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
});

0 commit comments

Comments
 (0)