Skip to content

Commit dfd0a78

Browse files
authored
feat(version): add --independent-subpackages option, closes #491 (#495)
* feat(version): add `--independent-subpackages` option, closes #491
1 parent 84fafa8 commit dfd0a78

File tree

10 files changed

+160
-20
lines changed

10 files changed

+160
-20
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"preview:watch:files": "lerna watch --glob=\"src/**/*.ts\" --scope=@lerna-lite/listable --include-dependents -- cross-env-shell echo $LERNA_FILE_CHANGES in package $LERNA_PACKAGE_NAME\"",
2323
"preview:publish-alpha-dry-run": "lerna publish --dry-run --exact --include-merged-tags --preid alpha --dist-tag next prerelease",
2424
"preview:publish": "lerna publish from-package --dry-run",
25-
"preview:version": "lerna version --dry-run",
25+
"preview:version": "lerna version --dry-run --independent-subpackages",
2626
"preview:roll-new-release": "pnpm build && pnpm new-version --dry-run && pnpm new-publish --dry-run",
2727
"new-version": "lerna version",
2828
"new-publish": "lerna publish from-package",

packages/cli/schemas/lerna-schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,9 @@
586586
"exact": {
587587
"$ref": "#/$defs/commandOptions/shared/exact"
588588
},
589+
"independentSubpackages": {
590+
"$ref": "#/$defs/commandOptions/shared/independentSubpackages"
591+
},
589592
"forcePublish": {
590593
"$ref": "#/$defs/commandOptions/shared/forcePublish"
591594
},
@@ -1531,6 +1534,10 @@
15311534
"type": "boolean",
15321535
"description": "During `lerna add`, save the exact version of the newly added package. During `lerna version`, specify cross-dependency version numbers exactly rather than with a caret (^)."
15331536
},
1537+
"independentSubpackages": {
1538+
"type": "boolean",
1539+
"description": "Exclude sub-packages when versioning"
1540+
},
15341541
"ignorePrepublish": {
15351542
"type": "boolean",
15361543
"description": "During `lerna publish`, when true, disable deprecated 'prepublish' lifecycle script."

packages/cli/src/cli-commands/cli-version-commands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ export default {
111111
describe: 'Specify cross-dependency version numbers exactly rather than with a caret (^).',
112112
type: 'boolean',
113113
},
114+
'independent-subpackages': {
115+
describe: 'Exclude sub-packages when versioning',
116+
type: 'boolean',
117+
},
114118
'force-publish': {
115119
describe: 'Always include targeted packages in versioning operations, skipping default logic.',
116120
// type must remain ambiguous because it is overloaded (boolean _or_ string _or_ array)

packages/core/src/models/command-options.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,9 @@ export interface VersionCommandOption {
245245
/** Specify cross-dependency version numbers exactly rather than with a caret (^). */
246246
exact?: boolean;
247247

248+
/** optionally exclude sub-packages when versioning */
249+
independentSubpackages?: boolean;
250+
248251
/** Always include targeted packages in versioning operations, skipping default logic. */
249252
forcePublish?: boolean | string;
250253

packages/core/src/models/interfaces.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@ export interface UpdateCollectorOptions {
219219
conventionalCommits?: boolean;
220220
conventionalGraduate?: boolean | string;
221221
excludeDependents?: boolean;
222+
223+
/** optionally exclude sub-packages when versioning */
224+
independentSubpackages?: boolean;
222225
}
223226

224227
export type RemoteClientType = 'gitlab' | 'github';

packages/core/src/utils/collect-updates/__tests__/collect-updates.spec.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import globby from 'globby';
12
import { Package } from '../../../package';
23

34
jest.mock('../../describe-ref');
@@ -72,7 +73,9 @@ describe('collectUpdates()', () => {
7273
]);
7374
expect(hasTags).toHaveBeenLastCalledWith(execOpts, '');
7475
expect(describeRefSync).toHaveBeenLastCalledWith(execOpts, undefined, false);
75-
expect(makeDiffPredicate).toHaveBeenLastCalledWith('v1.0.0', execOpts, undefined);
76+
expect(makeDiffPredicate).toHaveBeenLastCalledWith('v1.0.0', execOpts, undefined, {
77+
independentSubpackages: undefined,
78+
});
7679
});
7780

7881
it('returns node with changes in independent mode', () => {
@@ -93,7 +96,9 @@ describe('collectUpdates()', () => {
9396
]);
9497
expect(hasTags).toHaveBeenLastCalledWith(execOpts, '*@*');
9598
expect(describeRefSync).toHaveBeenLastCalledWith(execOpts, undefined, false);
96-
expect(makeDiffPredicate).toHaveBeenLastCalledWith('v1.0.0', execOpts, undefined);
99+
expect(makeDiffPredicate).toHaveBeenLastCalledWith('v1.0.0', execOpts, undefined, {
100+
independentSubpackages: undefined,
101+
});
97102
});
98103

99104
it('returns changed node and their dependents', () => {
@@ -363,7 +368,9 @@ describe('collectUpdates()', () => {
363368
expect.objectContaining({ name: 'package-dag-2a' }),
364369
expect.objectContaining({ name: 'package-dag-3' }),
365370
]);
366-
expect(makeDiffPredicate).toHaveBeenLastCalledWith('deadbeef^..deadbeef', execOpts, undefined);
371+
expect(makeDiffPredicate).toHaveBeenLastCalledWith('deadbeef^..deadbeef', execOpts, undefined, {
372+
independentSubpackages: undefined,
373+
});
367374
});
368375

369376
it('uses revision provided by --since <ref>', () => {
@@ -375,7 +382,9 @@ describe('collectUpdates()', () => {
375382
since: 'beefcafe',
376383
});
377384

378-
expect(makeDiffPredicate).toHaveBeenLastCalledWith('beefcafe', execOpts, undefined);
385+
expect(makeDiffPredicate).toHaveBeenLastCalledWith('beefcafe', execOpts, undefined, {
386+
independentSubpackages: undefined,
387+
});
379388
});
380389

381390
it('does not exit early on tagged release when --since <ref> is passed', () => {
@@ -410,6 +419,23 @@ describe('collectUpdates()', () => {
410419
ignoreChanges: ['**/README.md'],
411420
});
412421

413-
expect(makeDiffPredicate).toHaveBeenLastCalledWith('v1.0.0', execOpts, ['**/README.md']);
422+
expect(makeDiffPredicate).toHaveBeenLastCalledWith('v1.0.0', execOpts, ['**/README.md'], {
423+
independentSubpackages: undefined,
424+
});
425+
});
426+
427+
it('excludes packages when --independent-subpackages option is enabled', () => {
428+
jest.spyOn(globby, 'sync').mockImplementationOnce(() => ['packages/pkg-2/and-another-thing/package.json']);
429+
const graph = buildGraph();
430+
const pkgs = graph.rawPackageList;
431+
const execOpts = { cwd: '/test' };
432+
433+
collectUpdates(pkgs, graph, execOpts, {
434+
independentSubpackages: true,
435+
});
436+
437+
expect(makeDiffPredicate).toHaveBeenLastCalledWith('v1.0.0', execOpts, undefined, {
438+
independentSubpackages: true,
439+
});
414440
});
415441
});

packages/core/src/utils/collect-updates/__tests__/lib-make-diff-predicate.spec.ts

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
jest.mock('../../../child-process');
22

3+
import globby from 'globby';
4+
35
// mocked modules
46
import * as childProcesses from '../../../child-process';
57

@@ -18,7 +20,7 @@ test('git diff call', () => {
1820
'packages/pkg-1/README.md',
1921
]);
2022

21-
const hasDiff = makeDiffPredicate('v1.0.0', { cwd: '/test' });
23+
const hasDiff = makeDiffPredicate('v1.0.0', { cwd: '/test' }, undefined, {});
2224
const result = hasDiff({
2325
location: '/test/packages/pkg-1',
2426
});
@@ -34,7 +36,7 @@ test('git diff call', () => {
3436
test('empty diff', () => {
3537
setup('');
3638

37-
const hasDiff = makeDiffPredicate('v1.0.0', { cwd: '/test' });
39+
const hasDiff = makeDiffPredicate('v1.0.0', { cwd: '/test' }, undefined, {});
3840
const result = hasDiff({
3941
location: '/test/packages/pkg-1',
4042
});
@@ -45,7 +47,7 @@ test('empty diff', () => {
4547
test('rooted package', () => {
4648
setup('package.json');
4749

48-
const hasDiff = makeDiffPredicate('deadbeef', { cwd: '/test' });
50+
const hasDiff = makeDiffPredicate('deadbeef', { cwd: '/test' }, undefined, {});
4951
const result = hasDiff({
5052
location: '/test',
5153
});
@@ -63,7 +65,7 @@ test('ignore changes (globstars)', () => {
6365
'packages/pkg-2/examples/and-another-thing/package.json',
6466
]);
6567

66-
const hasDiff = makeDiffPredicate('v1.0.0', { cwd: '/test' }, ['**/examples/**', '*.md']);
68+
const hasDiff = makeDiffPredicate('v1.0.0', { cwd: '/test' }, ['**/examples/**', '*.md'], {});
6769
const result = hasDiff({
6870
location: '/test/packages/pkg-2',
6971
});
@@ -74,10 +76,62 @@ test('ignore changes (globstars)', () => {
7476
test('ignore changes (match base)', () => {
7577
setup('packages/pkg-3/README.md');
7678

77-
const hasDiff = makeDiffPredicate('v1.0.0', { cwd: '/test' }, ['*.md']);
79+
const hasDiff = makeDiffPredicate('v1.0.0', { cwd: '/test' }, ['*.md'], {});
7880
const result = hasDiff({
7981
location: '/test/packages/pkg-3',
8082
});
8183

8284
expect(result).toBe(false);
8385
});
86+
87+
test('exclude subpackages when --independent-subpackages option is enabled and nested package.json is found', () => {
88+
jest.spyOn(globby, 'sync').mockImplementationOnce(() => ['packages/pkg-2/and-another-thing/package.json']);
89+
90+
setup([
91+
'packages/pkg-2/package.json',
92+
'packages/pkg-2/do-a-thing/index.js',
93+
'packages/pkg-2/and-another-thing/package.json',
94+
]);
95+
96+
const hasDiff = makeDiffPredicate('v1.0.0', { cwd: '/test' }, ['**/examples/**', '*.md'], {
97+
independentSubpackages: true,
98+
});
99+
const result = hasDiff({
100+
location: '/test/packages/pkg-2',
101+
});
102+
103+
expect(result).toBe(true);
104+
expect(childProcesses.execSync).toHaveBeenLastCalledWith(
105+
'git',
106+
['diff', '--name-only', 'v1.0.0', '--', 'packages/pkg-2', ':^packages/pkg-2/packages/pkg-2/and-another-thing'],
107+
{
108+
cwd: '/test',
109+
}
110+
);
111+
});
112+
113+
test('not exclude any subpackages when --independent-subpackages option is enabled but no nested package.json are found', () => {
114+
jest.spyOn(globby, 'sync').mockImplementationOnce(() => []);
115+
116+
setup([
117+
'packages/pkg-2/package.json',
118+
'packages/pkg-2/do-a-thing/index.js',
119+
'packages/pkg-2/and-another-thing/method.js',
120+
]);
121+
122+
const hasDiff = makeDiffPredicate('v1.0.0', { cwd: '/test' }, ['**/examples/**', '*.md'], {
123+
independentSubpackages: true,
124+
});
125+
const result = hasDiff({
126+
location: '/test/packages/pkg-2',
127+
});
128+
129+
expect(result).toBe(true);
130+
expect(childProcesses.execSync).toHaveBeenLastCalledWith(
131+
'git',
132+
['diff', '--name-only', 'v1.0.0', '--', 'packages/pkg-2'],
133+
{
134+
cwd: '/test',
135+
}
136+
);
137+
});

packages/core/src/utils/collect-updates/collect-updates.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,14 @@ export function collectUpdates(
2323
commandOptions: UpdateCollectorOptions,
2424
dryRun = false
2525
) {
26-
const { forcePublish, conventionalCommits, conventionalGraduate, excludeDependents, isIndependent } = commandOptions;
26+
const {
27+
forcePublish,
28+
conventionalCommits,
29+
conventionalGraduate,
30+
excludeDependents,
31+
independentSubpackages,
32+
isIndependent,
33+
} = commandOptions;
2734

2835
// If --conventional-commits and --conventional-graduate are both set, ignore --force-publish
2936
const useConventionalGraduate = conventionalCommits && conventionalGraduate;
@@ -96,7 +103,9 @@ export function collectUpdates(
96103

97104
log.info('', `Looking for changed packages since ${committish}`);
98105

99-
const hasDiff = makeDiffPredicate(committish as string, execOpts, commandOptions.ignoreChanges as string[]);
106+
const hasDiff = makeDiffPredicate(committish as string, execOpts, commandOptions.ignoreChanges as string[], {
107+
independentSubpackages,
108+
});
100109
const needsBump =
101110
!commandOptions.bump || commandOptions.bump.startsWith('pre')
102111
? () => false

packages/core/src/utils/collect-updates/lib/make-diff-predicate.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import globby, { GlobbyOptions } from 'globby';
12
import log from 'npmlog';
23
import minimatch from 'minimatch';
34
import path from 'path';
@@ -11,7 +12,12 @@ import { ExecOpts } from '../../../models';
1112
* @param {import("@lerna/child-process").ExecOpts} execOpts
1213
* @param {string[]} ignorePatterns
1314
*/
14-
export function makeDiffPredicate(committish: string, execOpts: ExecOpts, ignorePatterns: string[] = []) {
15+
export function makeDiffPredicate(
16+
committish: string,
17+
execOpts: ExecOpts,
18+
ignorePatterns: string[] = [],
19+
diffOpts: { independentSubpackages?: boolean }
20+
) {
1521
const ignoreFilters = new Set(
1622
ignorePatterns.map((p) =>
1723
minimatch.filter(`!${p}`, {
@@ -27,7 +33,7 @@ export function makeDiffPredicate(committish: string, execOpts: ExecOpts, ignore
2733
}
2834

2935
return function hasDiffSinceThatIsntIgnored(/** @type {import("@lerna/package-graph").PackageGraphNode} */ node) {
30-
const diff = diffSinceIn(committish, node.location, execOpts);
36+
const diff = diffSinceIn(committish, node.location, execOpts, diffOpts);
3137

3238
if (diff === '') {
3339
log.silly('', 'no diff found in %s', node.name);
@@ -56,17 +62,36 @@ export function makeDiffPredicate(committish: string, execOpts: ExecOpts, ignore
5662
/**
5763
* @param {string} committish
5864
* @param {string} location
59-
* @param {import("@lerna/child-process").ExecOpts} opts
65+
* @param {import("@lerna/child-process").ExecOpts} execOpts
6066
*/
61-
function diffSinceIn(committish, location, opts) {
67+
function diffSinceIn(
68+
committish: string,
69+
location: string,
70+
execOpts: ExecOpts,
71+
diffOpts: { independentSubpackages?: boolean }
72+
) {
6273
const args = ['diff', '--name-only', committish];
63-
const formattedLocation = slash(path.relative(opts.cwd, location));
74+
const formattedLocation = slash(path.relative(execOpts.cwd, location));
6475

6576
if (formattedLocation) {
6677
// avoid same-directory path.relative() === ""
67-
args.push('--', formattedLocation);
78+
let independentSubpackages: string[] = [];
79+
80+
// optionally exclude sub-packages
81+
if (diffOpts?.independentSubpackages) {
82+
independentSubpackages = globby
83+
.sync('**/*/package.json', {
84+
cwd: formattedLocation,
85+
nodir: true,
86+
ignore: '**/node_modules/**',
87+
} as GlobbyOptions)
88+
.map((file) => `:^${formattedLocation}/${path.dirname(file)}`);
89+
}
90+
91+
// avoid same-directory path.relative() === ""
92+
args.push('--', formattedLocation, ...independentSubpackages);
6893
}
6994

7095
log.silly('checking diff', formattedLocation);
71-
return execSync('git', args, opts);
96+
return execSync('git', args, execOpts);
7297
}

packages/version/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ Running `lerna version --conventional-commits` without the above flags will rele
8383
- [`--changelog-version-message <msg>`](#--changelog-version-message-msg) (new)
8484
- [`--create-release <type>`](#--create-release-type)
8585
- [`--exact`](#--exact)
86+
- [`--independent-subpackages`](#--independent-subpackages)
8687
- [`--force-publish`](#--force-publish)
8788
- [`--git-tag-command <cmd>`](#--git-tag-command-cmd) (new)
8889
- [`--dry-run`](#--dry-run) (new)
@@ -417,6 +418,14 @@ When run with this flag, `lerna version` will specify updated dependencies in up
417418

418419
For more information, see the package.json [dependencies](https://docs.npmjs.com/files/package.json#dependencies) documentation.
419420

421+
### `--independent-subpackages`
422+
423+
```sh
424+
lerna version --independent-subpackages
425+
```
426+
427+
If `package B`, being a child of `package A`, has changes they will normally both get bumped although `package A` itself is eventually unchanged. If this flag is enabled and only `package B` was actually changed, `package A` will not get bumped if it does not have any changes on its own.
428+
420429
### `--force-publish`
421430

422431
```sh

0 commit comments

Comments
 (0)