@wpelevator/wp-release 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +101 -4
  2. package/dist/changelog.d.ts +45 -0
  3. package/dist/changelog.js +246 -0
  4. package/dist/changeset-config.d.ts +75 -0
  5. package/dist/changeset-config.js +48 -0
  6. package/dist/changeset-dependents.d.ts +132 -0
  7. package/dist/changeset-dependents.js +540 -0
  8. package/dist/changeset-files.d.ts +41 -0
  9. package/dist/changeset-files.js +100 -0
  10. package/dist/changeset-packages.d.ts +59 -0
  11. package/dist/changeset-packages.js +186 -0
  12. package/dist/changeset-release.d.ts +155 -18
  13. package/dist/changeset-release.js +366 -1090
  14. package/dist/changesets.d.ts +2 -0
  15. package/dist/commands/changeset-args.d.ts +5 -4
  16. package/dist/commands/changeset-args.js +2 -5
  17. package/dist/commands/changeset-status.d.ts +2 -0
  18. package/dist/commands/changeset-status.js +6 -2
  19. package/dist/commands/changeset-tag.d.ts +0 -3
  20. package/dist/commands/changeset-tag.js +8 -10
  21. package/dist/commands/changeset-version.d.ts +3 -0
  22. package/dist/commands/changeset-version.js +2 -1
  23. package/dist/file-journal.d.ts +22 -0
  24. package/dist/file-journal.js +56 -0
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.js +1 -1
  27. package/dist/json-files.d.ts +15 -0
  28. package/dist/json-files.js +35 -0
  29. package/dist/package-meta.js +1 -3
  30. package/dist/regexp.d.ts +7 -0
  31. package/dist/regexp.js +9 -0
  32. package/dist/release-repository.d.ts +102 -0
  33. package/dist/release-repository.js +159 -0
  34. package/dist/release-tags.d.ts +56 -0
  35. package/dist/release-tags.js +89 -0
  36. package/dist/tag-format.d.ts +20 -0
  37. package/dist/tag-format.js +51 -0
  38. package/dist/version-check.js +1 -3
  39. package/dist/version-files.d.ts +36 -0
  40. package/dist/version-files.js +195 -0
  41. package/dist/versions.d.ts +25 -0
  42. package/dist/versions.js +74 -0
  43. package/package.json +3 -1
@@ -0,0 +1,132 @@
1
+ import type { ChangesetConfig, DependentsConfig } from './changeset-config.ts';
2
+ import type { ChangedPackages, ConfiguredPackage } from './changeset-packages.ts';
3
+ import type { Changeset, ChangesetPackage, ChangesetReleasePlan, PackageRelease } from './changesets.ts';
4
+ import type { FileJournal } from './file-journal.ts';
5
+ /**
6
+ * A declared dependency of a package on another configured package.
7
+ */
8
+ export interface DependencySpec {
9
+ /** Absolute path of the manifest that declares the dependency. */
10
+ file: string;
11
+ /** Dependency name in the manifest, like the npm package name. */
12
+ name: string;
13
+ /** Declared spec, like ^1.2.0 or workspace:*. */
14
+ spec: string;
15
+ }
16
+ /**
17
+ * Add releases for dependents of packages that release, applying the
18
+ * configured dependents policy until the plan settles.
19
+ *
20
+ * @param releases Planned releases, mutated in place.
21
+ * @param packages Discovered packages.
22
+ * @param config Repository release configuration.
23
+ */
24
+ export declare function applyDependents(releases: PackageRelease[], packages: ConfiguredPackage[], config: ChangesetConfig): Promise<void>;
25
+ /**
26
+ * Limit a release plan to some packages and the changesets that name them.
27
+ *
28
+ * @param plan Release plan of all pending changesets.
29
+ * @param packages Configured packages.
30
+ * @param names Packages to version.
31
+ * @param config Repository release configuration.
32
+ * @param withDependents Whether the selection also releases the dependents of the selected sources.
33
+ * @return Release plan of the selected packages and their dependents.
34
+ * @throws {ValidationError} For unknown packages.
35
+ */
36
+ export declare function selectReleasePlan(plan: ChangesetReleasePlan, packages: ConfiguredPackage[], names: string[], config: ChangesetConfig, withDependents?: boolean): Promise<ChangesetReleasePlan>;
37
+ /**
38
+ * Drop bundle sources that aren't released by the selected plan.
39
+ *
40
+ * @param plan Selected release plan.
41
+ * @return Release plan with dependents bundling only its released sources.
42
+ */
43
+ export declare function expandDependentsPlan(plan: ChangesetReleasePlan): ChangesetReleasePlan;
44
+ /**
45
+ * Remove the derived dependent releases of a plan, keeping only the releases
46
+ * of packages that have their own changesets.
47
+ *
48
+ * @param plan Release plan of all pending changesets.
49
+ * @return Release plan without derived dependent releases.
50
+ */
51
+ export declare function stripDependentsPlan(plan: ChangesetReleasePlan): ChangesetReleasePlan;
52
+ /**
53
+ * Fail when a planned release bundles a source package that has pending
54
+ * changesets released by a later run, so that no dependent ships unreleased
55
+ * code under a changelog that cannot credit it.
56
+ *
57
+ * @param plan Planned releases.
58
+ * @param fullPlan Release plan of all pending changesets.
59
+ * @param packages Discovered packages.
60
+ * @param config Repository release configuration.
61
+ * @throws {ValidationError} If a source of a planned release stays pending.
62
+ */
63
+ export declare function assertDependentsSourcesReleased(plan: ChangesetReleasePlan, fullPlan: ChangesetReleasePlan, packages: ConfiguredPackage[], config: ChangesetConfig): Promise<void>;
64
+ /**
65
+ * Find the changed packages that need no changeset of their own, because the
66
+ * dependents policy releases them with their sources. A dependent releases
67
+ * with its sources, which the release pull request already announces when
68
+ * the changesets of the diff release one of them. Exempting its package
69
+ * manifest pin change keeps the check from demanding an entry for what
70
+ * versioning rewrites anyway; documentation and code changes still need
71
+ * their own changesets.
72
+ *
73
+ * @param changed Changed packages and their files.
74
+ * @param changesetsInDiff Changesets added or changed by the diff.
75
+ * @param packages Discovered packages.
76
+ * @param config Repository release configuration.
77
+ * @return Names of the packages whose only changes are manifests of released dependents.
78
+ */
79
+ export declare function findBundledPinChanges(changed: ChangedPackages, changesetsInDiff: Changeset[], packages: ConfiguredPackage[], config: ChangesetConfig): Promise<Set<string>>;
80
+ /**
81
+ * Check whether a changeset names a package.
82
+ *
83
+ * @param changesets Changesets.
84
+ * @param name Package name.
85
+ * @return Whether one of the changesets names the package.
86
+ */
87
+ export declare function hasChangeset(changesets: Changeset[], name: string): boolean;
88
+ /**
89
+ * Update how a dependent declares its dependency on a released source, with
90
+ * the configured hook or the built-in spec rewrite.
91
+ *
92
+ * @param input Dependent, source, released version, policy and journal.
93
+ * @param input.dependent The released dependent package.
94
+ * @param input.source Released source package that the dependent bundles.
95
+ * @param input.version Released version of the source.
96
+ * @param input.rule Dependents policy with its sections and dependency hooks.
97
+ * @param input.journal File journal for rolling back.
98
+ */
99
+ export declare function updateDependencyDeclaration(input: {
100
+ dependent: ChangesetPackage;
101
+ source: ChangesetPackage;
102
+ version: string;
103
+ rule?: DependentsConfig;
104
+ journal: FileJournal;
105
+ }): Promise<void>;
106
+ /**
107
+ * Update a dependency spec to the released version, keeping its shape, like
108
+ * the caret of ^0.9.0 or the x of 0.9.x.
109
+ *
110
+ * @param spec Declared spec of the source.
111
+ * @param version Released version of the source.
112
+ * @return The updated spec, undefined when nothing has to change, like a wildcard or a workspace spec, or null when the spec can't be rewritten.
113
+ */
114
+ export declare function updatedSpec(spec: string, version: string): string | null | undefined;
115
+ /**
116
+ * Warn about declared pins of configured packages that don't resolve the
117
+ * current version of the package they name.
118
+ *
119
+ * @param config Repository release configuration.
120
+ * @param packages Discovered packages.
121
+ * @return Warnings, one per package pair.
122
+ */
123
+ export declare function collectPinDriftWarnings(config: ChangesetConfig, packages: ConfiguredPackage[]): Promise<string[]>;
124
+ /**
125
+ * Check whether a spec points at the workspace copy of a package, by
126
+ * resolving the current version through its range or its workspace protocol.
127
+ *
128
+ * @param spec Declared spec of the source.
129
+ * @param currentVersion Current version of the source package.
130
+ * @return Whether the spec is a dependency of the source.
131
+ */
132
+ export declare function isDependencyOfSource(spec: string, currentVersion: string): boolean;
@@ -0,0 +1,540 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import semverSatisfies from 'semver/functions/satisfies.js';
4
+ import validRange from 'semver/ranges/valid.js';
5
+ import { assertKnownPackages, globRegExp } from "./changeset-packages.js";
6
+ import { getNextVersion } from "./changesets.js";
7
+ import { ValidationError } from "./errors.js";
8
+ import { readJsonIfExists } from "./json-files.js";
9
+ import { escapeRegExp } from "./regexp.js";
10
+ /** npm dependency sections whose declared dependency re-releases the dependent. */
11
+ const DEFAULT_SECTIONS = ['dependencies', 'optionalDependencies'];
12
+ /**
13
+ * Add releases for dependents of packages that release, applying the
14
+ * configured dependents policy until the plan settles.
15
+ *
16
+ * @param releases Planned releases, mutated in place.
17
+ * @param packages Discovered packages.
18
+ * @param config Repository release configuration.
19
+ */
20
+ export async function applyDependents(releases, packages, config) {
21
+ const rule = config.dependents;
22
+ if (!rule) {
23
+ return;
24
+ }
25
+ // Dependents may themselves be dependents of other sources, so expand until stable.
26
+ let grew = true;
27
+ while (grew) {
28
+ grew = false;
29
+ for (const source of [...releases]) {
30
+ for (const dependent of packages) {
31
+ if (await deriveDependentRelease(source, dependent, releases, rule)) {
32
+ grew = true;
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ /**
39
+ * Plan the release of a package that bundles a source package, or add the
40
+ * source to the bundle list of its own pending release.
41
+ *
42
+ * @param source Package release of the source.
43
+ * @param dependent Package that would bundle the source.
44
+ * @param releases Planned releases, mutated in place.
45
+ * @param rule Dependents policy.
46
+ * @return Whether a new dependent release was planned.
47
+ */
48
+ async function deriveDependentRelease(source, dependent, releases, rule) {
49
+ if (dependent.name === source.package.name ||
50
+ isExcludedDependent(dependent, rule)) {
51
+ return false;
52
+ }
53
+ const specs = await findDependencySpecs(dependent, source.package, rule);
54
+ if (!specs.length || !dependentNeedsRelease(specs, source, rule)) {
55
+ return false;
56
+ }
57
+ const release = releases.find((item) => item.package.name === dependent.name);
58
+ if (release) {
59
+ const sources = release.sources ?? [];
60
+ if (!sources.includes(source.package.name)) {
61
+ // A dependent that releases for its own reasons still notes the bundle.
62
+ release.sources = [...sources, source.package.name];
63
+ }
64
+ return false;
65
+ }
66
+ const bump = rule.bump ?? 'patch';
67
+ releases.push({
68
+ package: dependent,
69
+ bump,
70
+ currentVersion: dependent.currentVersion,
71
+ nextVersion: getNextVersion(dependent.currentVersion, bump),
72
+ changesets: [],
73
+ sources: [source.package.name],
74
+ });
75
+ return true;
76
+ }
77
+ /**
78
+ * Limit a release plan to some packages and the changesets that name them.
79
+ *
80
+ * @param plan Release plan of all pending changesets.
81
+ * @param packages Configured packages.
82
+ * @param names Packages to version.
83
+ * @param config Repository release configuration.
84
+ * @param withDependents Whether the selection also releases the dependents of the selected sources.
85
+ * @return Release plan of the selected packages and their dependents.
86
+ * @throws {ValidationError} For unknown packages.
87
+ */
88
+ export async function selectReleasePlan(plan, packages, names, config, withDependents = true) {
89
+ assertKnownPackages(packages, names);
90
+ // Dependents that need a release close over the selected sources.
91
+ const selected = new Set(names);
92
+ let grew = true;
93
+ // Dependents may depend on other selected dependents, so expand until stable.
94
+ while (grew && withDependents && config.dependents) {
95
+ grew = false;
96
+ const selectedSources = plan.releases.filter((release) => selected.has(release.package.name));
97
+ for (const source of selectedSources) {
98
+ for (const dependent of packages) {
99
+ if (await selectDependentRelease(source, dependent, packages, config.dependents, selected)) {
100
+ grew = true;
101
+ }
102
+ }
103
+ }
104
+ }
105
+ const selectedNames = [...selected];
106
+ return {
107
+ releases: plan.releases.filter((release) => {
108
+ if (!selected.has(release.package.name)) {
109
+ return false;
110
+ }
111
+ // Explicitly selected dependents without their own changesets only
112
+ // release when their source releases in the same selection.
113
+ return (release.changesets.length > 0 ||
114
+ (release.sources ?? []).some((name) => selected.has(name)));
115
+ }),
116
+ changesets: plan.changesets.filter((changeset) => selectedNames.some((name) => Object.hasOwn(changeset.packages, name))),
117
+ };
118
+ }
119
+ /**
120
+ * Add a package to the selected release plan when it bundles a selected source.
121
+ *
122
+ * @param source Selected package release.
123
+ * @param dependent Package that would bundle the source.
124
+ * @param packages Discovered packages.
125
+ * @param rule Dependents policy.
126
+ * @param selected Selected package names, mutated in place.
127
+ * @return Whether the package was selected.
128
+ */
129
+ async function selectDependentRelease(source, dependent, packages, rule, selected) {
130
+ const skip = selected.has(dependent.name) ||
131
+ dependent.name === source.package.name ||
132
+ isExcludedDependent(dependent, rule);
133
+ if (skip) {
134
+ return false;
135
+ }
136
+ const specs = await findDependencySpecs(dependent, source.package, rule);
137
+ if (!specs.length || !dependentNeedsRelease(specs, source, rule)) {
138
+ return false;
139
+ }
140
+ selected.add(dependent.name);
141
+ return true;
142
+ }
143
+ /**
144
+ * Drop bundle sources that aren't released by the selected plan.
145
+ *
146
+ * @param plan Selected release plan.
147
+ * @return Release plan with dependents bundling only its released sources.
148
+ */
149
+ export function expandDependentsPlan(plan) {
150
+ const released = new Set(plan.releases.map((release) => release.package.name));
151
+ plan.releases.forEach((release) => {
152
+ if (release.sources) {
153
+ release.sources = release.sources.filter((name) => released.has(name));
154
+ if (!release.sources.length) {
155
+ delete release.sources;
156
+ }
157
+ }
158
+ });
159
+ return plan;
160
+ }
161
+ /**
162
+ * Remove the derived dependent releases of a plan, keeping only the releases
163
+ * of packages that have their own changesets.
164
+ *
165
+ * @param plan Release plan of all pending changesets.
166
+ * @return Release plan without derived dependent releases.
167
+ */
168
+ export function stripDependentsPlan(plan) {
169
+ return {
170
+ ...plan,
171
+ releases: plan.releases
172
+ .filter((release) => release.changesets.length > 0)
173
+ .map(({ sources, ...release }) => release),
174
+ };
175
+ }
176
+ /**
177
+ * Fail when a planned release bundles a source package that has pending
178
+ * changesets released by a later run, so that no dependent ships unreleased
179
+ * code under a changelog that cannot credit it.
180
+ *
181
+ * @param plan Planned releases.
182
+ * @param fullPlan Release plan of all pending changesets.
183
+ * @param packages Discovered packages.
184
+ * @param config Repository release configuration.
185
+ * @throws {ValidationError} If a source of a planned release stays pending.
186
+ */
187
+ export async function assertDependentsSourcesReleased(plan, fullPlan, packages, config) {
188
+ const releasing = new Set(plan.releases.map((release) => release.package.name));
189
+ const pending = new Set(fullPlan.releases.map((release) => release.package.name));
190
+ for (const release of plan.releases) {
191
+ // Dependencies built into the dependent, like bundled or vendored
192
+ // directories, stay out of the check, which only guards released
193
+ // packages that the dependent declares.
194
+ const names = await collectDependencyNames(release.package, packages, config);
195
+ for (const name of names) {
196
+ const blocked = pending.has(name) &&
197
+ !releasing.has(name) &&
198
+ name !== release.package.name;
199
+ if (blocked) {
200
+ throw new ValidationError(`Cannot release package "${release.package.name}" while its source "${name}" has pending changesets; release them together or pass --no-dependents.`);
201
+ }
202
+ }
203
+ }
204
+ }
205
+ /**
206
+ * Find the changed packages that need no changeset of their own, because the
207
+ * dependents policy releases them with their sources. A dependent releases
208
+ * with its sources, which the release pull request already announces when
209
+ * the changesets of the diff release one of them. Exempting its package
210
+ * manifest pin change keeps the check from demanding an entry for what
211
+ * versioning rewrites anyway; documentation and code changes still need
212
+ * their own changesets.
213
+ *
214
+ * @param changed Changed packages and their files.
215
+ * @param changesetsInDiff Changesets added or changed by the diff.
216
+ * @param packages Discovered packages.
217
+ * @param config Repository release configuration.
218
+ * @return Names of the packages whose only changes are manifests of released dependents.
219
+ */
220
+ export async function findBundledPinChanges(changed, changesetsInDiff, packages, config) {
221
+ const candidates = changed.packages.filter((pkg) => undefined !== config.dependents &&
222
+ !hasChangeset(changesetsInDiff, pkg.name) &&
223
+ (changed.filesByPackage[pkg.name] ?? []).every((file) => 'package.json' === path.basename(file)));
224
+ const bundled = await Promise.all(candidates.map(async (pkg) => {
225
+ const sources = await collectDeclarationNames(pkg, packages, config);
226
+ return sources.some((source) => hasChangeset(changesetsInDiff, source))
227
+ ? pkg.name
228
+ : undefined;
229
+ }));
230
+ return new Set(bundled.filter((name) => undefined !== name));
231
+ }
232
+ /**
233
+ * Check whether a changeset names a package.
234
+ *
235
+ * @param changesets Changesets.
236
+ * @param name Package name.
237
+ * @return Whether one of the changesets names the package.
238
+ */
239
+ export function hasChangeset(changesets, name) {
240
+ return changesets.some((changeset) => Object.hasOwn(changeset.packages, name));
241
+ }
242
+ /**
243
+ * Collect the names of the configured sources that a package declares, by the
244
+ * dependency sections of the policy, without resolving the specs against the
245
+ * versions. The changeset check uses this to see through a pin that the
246
+ * versioning will rewrite after the source releases.
247
+ *
248
+ * @param pkg Planned package.
249
+ * @param packages Discovered packages.
250
+ * @param config Repository release configuration.
251
+ * @return Release package names the package declares.
252
+ */
253
+ async function collectDeclarationNames(pkg, packages, config) {
254
+ const names = [];
255
+ for (const source of packages) {
256
+ if (pkg.name !== source.name &&
257
+ (await findNpmSpecs(pkg, source, dependentsSections(config.dependents))).length > 0) {
258
+ names.push(source.name);
259
+ }
260
+ }
261
+ return names;
262
+ }
263
+ /**
264
+ * Collect the names of the configured sources that a package builds against,
265
+ * through specs that resolve the current version or point at the workspace.
266
+ *
267
+ * @param pkg Planned package.
268
+ * @param packages Discovered packages.
269
+ * @param config Repository release configuration.
270
+ * @return Release package names the package declares.
271
+ */
272
+ async function collectDependencyNames(pkg, packages, config) {
273
+ const names = [];
274
+ for (const source of packages) {
275
+ if (pkg.name !== source.name &&
276
+ (await findDependencySpecs(pkg, source, config.dependents))
277
+ .length > 0) {
278
+ names.push(source.name);
279
+ }
280
+ }
281
+ return names;
282
+ }
283
+ /**
284
+ * Update how a dependent declares its dependency on a released source, with
285
+ * the configured hook or the built-in spec rewrite.
286
+ *
287
+ * @param input Dependent, source, released version, policy and journal.
288
+ * @param input.dependent The released dependent package.
289
+ * @param input.source Released source package that the dependent bundles.
290
+ * @param input.version Released version of the source.
291
+ * @param input.rule Dependents policy with its sections and dependency hooks.
292
+ * @param input.journal File journal for rolling back.
293
+ */
294
+ export async function updateDependencyDeclaration(input) {
295
+ const { dependent, source, version, rule, journal } = input;
296
+ const specs = await findDependencySpecs(dependent, source, rule);
297
+ const update = rule?.dependency?.update;
298
+ if (update) {
299
+ await update({
300
+ package: dependent,
301
+ source,
302
+ version,
303
+ specs,
304
+ remember: (file) => journal.remember(file),
305
+ });
306
+ return;
307
+ }
308
+ for (const { file, name, spec } of specs) {
309
+ const nextSpec = updatedSpec(spec, version);
310
+ if (null === nextSpec &&
311
+ isOutOfRange(version, spec, source.currentVersion)) {
312
+ throw new ValidationError(`Cannot rewrite the dependency spec "${spec}" of "${name}" in ${dependent.name}'s ${file}; declare a range or pin that versioning rewrites, like "^${version}".`);
313
+ }
314
+ const changed = undefined !== nextSpec && nextSpec !== spec;
315
+ if (changed) {
316
+ const contents = await readFile(file, 'utf8');
317
+ const declared = new RegExp(`"${escapeRegExp(name)}"\\s*:\\s*"${escapeRegExp(spec)}"`);
318
+ if (!declared.test(contents)) {
319
+ throw new ValidationError(`The dependency spec of "${name}" in ${dependent.name}'s ${file} doesn't match "${spec}"; rewrite it to "${nextSpec}".`);
320
+ }
321
+ await journal.remember(file);
322
+ await writeFile(file, contents.replace(declared, `"${name}": "${nextSpec}"`));
323
+ }
324
+ }
325
+ }
326
+ /**
327
+ * Update a dependency spec to the released version, keeping its shape, like
328
+ * the caret of ^0.9.0 or the x of 0.9.x.
329
+ *
330
+ * @param spec Declared spec of the source.
331
+ * @param version Released version of the source.
332
+ * @return The updated spec, undefined when nothing has to change, like a wildcard or a workspace spec, or null when the spec can't be rewritten.
333
+ */
334
+ export function updatedSpec(spec, version) {
335
+ if (spec.startsWith('workspace:')) {
336
+ // Workspace specs resolve to the local package, so the manifest
337
+ // keeps them instead of a published version.
338
+ return undefined;
339
+ }
340
+ const prefix = /^[=\^~]/.exec(spec)?.[0] ?? '';
341
+ const bare = spec.slice(prefix.length).trim();
342
+ const partial = /^(v?\d+(?:\.\d+)*)(\.x)?$/.exec(bare);
343
+ if (partial?.[1]) {
344
+ const depth = partial[1].split('.').length;
345
+ const stem = version.split('.').slice(0, depth).join('.');
346
+ // npm partials like 1.x pin the major, and 0.9 pins the major.minor.
347
+ return `${prefix}${stem}${partial[2] ?? ''}`;
348
+ }
349
+ // Wildcards and keyword specs like * or latest already accept the release.
350
+ return null;
351
+ }
352
+ /**
353
+ * Warn about declared pins of configured packages that don't resolve the
354
+ * current version of the package they name.
355
+ *
356
+ * @param config Repository release configuration.
357
+ * @param packages Discovered packages.
358
+ * @return Warnings, one per package pair.
359
+ */
360
+ export async function collectPinDriftWarnings(config, packages) {
361
+ const warnings = [];
362
+ for (const dependent of packages) {
363
+ const others = packages.filter((source) => source.name !== dependent.name);
364
+ for (const source of others) {
365
+ const driftedPin = (await findNpmSpecs(dependent, source, dependentsSections(config.dependents)))
366
+ .filter(({ spec }) => !spec.startsWith('workspace:') &&
367
+ isOutOfRange(source.currentVersion, spec, source.currentVersion))
368
+ .at(0);
369
+ if (driftedPin) {
370
+ warnings.push(`"${dependent.name}" pins "${driftedPin.name}@${driftedPin.spec}", but the package is at ${source.currentVersion}; the dependents policy won't re-release it until the pin resolves the package.`);
371
+ }
372
+ }
373
+ }
374
+ return warnings;
375
+ }
376
+ /**
377
+ * Check whether a spec points at the workspace copy of a package, by
378
+ * resolving the current version through its range or its workspace protocol.
379
+ *
380
+ * @param spec Declared spec of the source.
381
+ * @param currentVersion Current version of the source package.
382
+ * @return Whether the spec is a dependency of the source.
383
+ */
384
+ export function isDependencyOfSource(spec, currentVersion) {
385
+ if (spec.startsWith('workspace:')) {
386
+ return true;
387
+ }
388
+ const range = effectiveRange(spec, currentVersion);
389
+ return (validRange(range) !== null && semverSatisfies(currentVersion, range));
390
+ }
391
+ /**
392
+ * Find the specs through which a package declares a dependency on another.
393
+ *
394
+ * A spec is a dependency of the source that the current version resolves,
395
+ * like a range that covers it or a workspace spec, or one that the
396
+ * configured hook reports. A pin like an older version of the source
397
+ * selects a registry release instead, which keeps the local code out of
398
+ * the dependent's build.
399
+ *
400
+ * @param dependent Package potentially declaring the dependency.
401
+ * @param source Released source package.
402
+ * @param rule Dependents policy, with its dependency hook and sections.
403
+ * @return The declared specs, with their manifest files and package names.
404
+ */
405
+ async function findDependencySpecs(dependent, source, rule) {
406
+ const specs = (await findNpmSpecs(dependent, source, dependentsSections(rule))).filter(({ spec }) => isDependencyOfSource(spec, source.currentVersion));
407
+ return rule?.dependency?.find
408
+ ? [
409
+ ...specs,
410
+ ...(await findDependencyHookSpecs(dependent, source, rule)),
411
+ ]
412
+ : specs;
413
+ }
414
+ /**
415
+ * Find the specs that the configured dependency hook reports.
416
+ *
417
+ * @param dependent Package potentially declaring the dependency.
418
+ * @param source Released source package.
419
+ * @param rule Dependents policy.
420
+ * @return The hook's specs, with absolute manifest paths and package names.
421
+ */
422
+ async function findDependencyHookSpecs(dependent, source, rule) {
423
+ const found = await rule.dependency?.find?.({
424
+ package: dependent,
425
+ source,
426
+ });
427
+ return (found ?? []).map((entry) => ({
428
+ file: path.resolve(dependent.directory, entry.file),
429
+ name: entry.name,
430
+ spec: entry.spec,
431
+ }));
432
+ }
433
+ /**
434
+ * Find the npm specs that a package declares for a source, without the
435
+ * dependency filtering of dependents links, for drift warnings.
436
+ *
437
+ * @param dependent Package potentially declaring the dependency.
438
+ * @param source Package the dependency is declared of.
439
+ * @param sections Dependency sections to scan.
440
+ * @return The declared specs.
441
+ */
442
+ async function findNpmSpecs(dependent, source, sections) {
443
+ const sourceManifest = await readJsonIfExists(path.join(source.directory, 'package.json'));
444
+ if ('string' !== typeof sourceManifest?.name) {
445
+ return [];
446
+ }
447
+ const identifier = sourceManifest.name;
448
+ const parsed = await readJsonIfExists(path.join(dependent.directory, 'package.json'));
449
+ const specs = [];
450
+ for (const section of sections) {
451
+ const sectionSpecs = parsed?.[section];
452
+ const declared = 'object' === typeof sectionSpecs &&
453
+ null !== sectionSpecs &&
454
+ Object.hasOwn(sectionSpecs, identifier)
455
+ ? String(sectionSpecs[identifier])
456
+ : undefined;
457
+ if (undefined !== declared) {
458
+ specs.push({
459
+ file: path.join(dependent.directory, 'package.json'),
460
+ name: identifier,
461
+ spec: declared,
462
+ });
463
+ }
464
+ }
465
+ return specs;
466
+ }
467
+ /**
468
+ * The dependency sections that a dependents policy scans.
469
+ *
470
+ * @param rule Dependents policy.
471
+ * @return Dependency section names.
472
+ */
473
+ function dependentsSections(rule) {
474
+ return (rule?.sections ?? DEFAULT_SECTIONS);
475
+ }
476
+ /**
477
+ * Check whether a dependent has to release when its declared source releases.
478
+ *
479
+ * @param specs Declared specs of the source in the dependent.
480
+ * @param source Released source package.
481
+ * @param rule Dependents policy.
482
+ * @return Whether the dependent needs a release.
483
+ */
484
+ function dependentNeedsRelease(specs, source, rule) {
485
+ if ('always' === rule.when) {
486
+ return true;
487
+ }
488
+ // Without a range that accepts the next version, like an exact pin, the
489
+ // dependent has to release and rewrite its specs.
490
+ return specs.some(({ spec }) => isOutOfRange(source.nextVersion, spec, source.currentVersion));
491
+ }
492
+ /**
493
+ * Check whether a version falls outside a declared range.
494
+ *
495
+ * @param nextVersion Next version of the source package.
496
+ * @param spec Declared spec of the source).
497
+ * @param currentVersion Current version of the source package.
498
+ * @return Whether the next version falls outside the range.
499
+ */
500
+ function isOutOfRange(nextVersion, spec, currentVersion) {
501
+ const range = effectiveRange(spec, currentVersion);
502
+ if (validRange(range) === null) {
503
+ // Unrecognized ranges of dependencies like git URLs are left to the consumer.
504
+ return false;
505
+ }
506
+ return !semverSatisfies(nextVersion, range);
507
+ }
508
+ /**
509
+ * Resolve a dependency spec to the range it selects for an internal package.
510
+ *
511
+ * @param spec Declared spec.
512
+ * @param currentVersion Current version of the source package.
513
+ * @return The effective semver range.
514
+ */
515
+ function effectiveRange(spec, currentVersion) {
516
+ if (!spec.startsWith('workspace:')) {
517
+ return spec;
518
+ }
519
+ // Workspace specs resolve to the exact published version, and to a
520
+ // prefixed one with their operator.
521
+ const bare = spec.slice('workspace:'.length);
522
+ if ('*' === bare) {
523
+ return currentVersion;
524
+ }
525
+ if ('^' === bare || '~' === bare) {
526
+ return `${bare}${currentVersion}`;
527
+ }
528
+ return bare;
529
+ }
530
+ /**
531
+ * Check whether the dependents policy excludes a package.
532
+ *
533
+ * @param dependent Candidate dependent package.
534
+ * @param rule Dependents policy.
535
+ * @return Whether the package is excluded from re-releases.
536
+ */
537
+ function isExcludedDependent(dependent, rule) {
538
+ return (rule.exclude ?? []).some((pattern) => pattern === dependent.name ||
539
+ globRegExp(pattern).test(dependent.path));
540
+ }