@wpelevator/wp-release 0.9.0 → 0.10.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.
package/README.md CHANGED
@@ -142,6 +142,46 @@ changelogEntry: async ( { changeset, commit } ) => {
142
142
  },
143
143
  ```
144
144
 
145
+ Configure `dependents` to re-release packages that bundle a package when it releases. The dependents come from what the packages declare: a package is a dependent of a source when the spec that it declares in its npm `dependencies` or `optionalDependencies` (or the sections that the policy adds) resolves the current version of the source or points at the workspace, and it re-releases when the next version of the source falls outside the declared range, or always with `when: 'always'`. A pin to an older version selects a registry release instead and `changeset-status` warns about the drift until the pin resolves the package again. Versioning a dependent rewrites its declared specs of the source to the released version, keeping their range prefixes, so an exact pin matches the released workspace again and the built bundle matches the changelog. Wildcards and unpinned ranges accept the release without one, like the `devDependencies` of a package. The derived dependents appear in `changeset-status`, `changeset-version` and `changeset-tag` like any other release, and a dependent that releases for its own reasons keeps its own bump and appends the bundle note:
146
+
147
+ ```js
148
+ dependents: {
149
+ bump: 'patch', // Default
150
+ exclude: [ 'actions/wp-release-sign' ], // Package paths under root, or globs over them
151
+ },
152
+ ```
153
+
154
+ Excluded packages still release with their own changesets and stay out of the automatic re-releases.
155
+
156
+ The bundle changelog entry defaults to `Bundle <source> <version>.` and is formatted with `dependents.changelog( { package, source } )`:
157
+
158
+ ```js
159
+ dependents: {
160
+ changelog: ( { source } ) => `Bundle ${ source.package.name } ${ source.version }.`,
161
+ },
162
+ ```
163
+
164
+ `dependents.sections` lists the npm dependency sections that declare a source, defaulting to `dependencies` and `optionalDependencies`; add `devDependencies` for dependencies that end up in the dependent's build, or `peerDependencies` to scan them.
165
+
166
+ The `dependents.dependency` hooks extend the built-in npm scan instead of replacing it, so that both work behind the same range rule and rewrite:
167
+
168
+ ```js
169
+ dependents: {
170
+ dependency: {
171
+ // Specs that declare the source outside package.json, like a Composer
172
+ // require in a vendor-isolated directory.
173
+ find: async ( { package: dependent, source } ) => {
174
+ return [ { file: 'vendor-isolated/composer.json', name: '…', spec: '^0.3' } ];
175
+ },
176
+ // Update the declaration to the released version. Call `remember( file )`
177
+ // before writing a file, so failed versioning rolls it back.
178
+ update: async ( { package: dependent, source, specs, version, remember } ) => { /* … */ },
179
+ },
180
+ },
181
+ ```
182
+
183
+ After versioning, run `npm install --package-lock-only` so that the root lockfile resolves the rewritten pins to the workspace links again; the `Prepare Release` workflow (`prepare-release.yml` in this monorepo) does it with its release pull request.
184
+
145
185
  To release packages separately, for example with a release pull request for each, pass `--package <name>`, repeatable, to version only those packages. A changeset that also names other packages keeps only their entries and its summary, so it still applies to their releases with the same bump. Changesets without packages are left for a run without `--package`.
146
186
 
147
187
  The `changeset-version` result lists each release with its `package`, `bump`, `currentVersion`, `nextVersion` and `changelog` entries, a Markdown list like the one added to `CHANGELOG.md`. Use it to name and describe the release pull request in a repository script, or call `versionChangesets()` from the library and format the entries of each release with `formatChangelogEntries( release.changesets )`.
@@ -69,6 +69,79 @@ export interface ChangesetPackageGroup {
69
69
  /** Version source and files to update for every discovered package. */
70
70
  version: PackageVersionConfig;
71
71
  }
72
+ /** Inputs supplied to a dependents changelog formatter. */
73
+ export interface DependentsChangelogInput {
74
+ /** The dependent package being re-released. */
75
+ package: ChangesetPackage;
76
+ /** The released source package that the dependent bundles. */
77
+ source: {
78
+ package: ChangesetPackage;
79
+ /** Version of the source release. */
80
+ version: string;
81
+ };
82
+ }
83
+ /** Inputs supplied to a dependents dependency finder. */
84
+ export interface DependentsFindInput {
85
+ /** The dependent package. */
86
+ package: ChangesetPackage;
87
+ /** The source package. */
88
+ source: ChangesetPackage;
89
+ }
90
+ /** A dependency of a source that a package declares outside package.json. */
91
+ export interface DependentsSpec {
92
+ /** Manifest file that declares the dependency, relative to the dependent package. */
93
+ file: string;
94
+ /** Identifier that holds the spec, like a Composer package name. */
95
+ name: string;
96
+ /** Declared spec of the source. */
97
+ spec: string;
98
+ }
99
+ /** Inputs supplied to a dependents dependency update. */
100
+ export interface DependentsUpdateInput {
101
+ /** The dependent package whose declaration gets updated. */
102
+ package: ChangesetPackage;
103
+ /** The released source package. */
104
+ source: ChangesetPackage;
105
+ /** Released version of the source. */
106
+ version: string;
107
+ /** The specs to update, with their manifest files and identifiers. */
108
+ specs: DependentsSpec[];
109
+ /** Snapshot a file before writing it, so that failed versioning rolls it back. */
110
+ remember: (file: string) => Promise<void>;
111
+ }
112
+ /** How packages find and update their dependency of a source. */
113
+ export interface DependentsDependencyOptions {
114
+ /** Specs of the source that a package declares outside package.json, added to the built-in npm scan, so that the range rule and the rewrite apply to them the same way. */
115
+ find?: (input: DependentsFindInput) => Promise<DependentsSpec[]>;
116
+ /** Update a package's declared dependency of a released source to the released version; defaults to rewriting the found specs, keeping their range prefixes. Call `remember()` with every file path before writing it. */
117
+ update?: (input: DependentsUpdateInput) => Promise<void>;
118
+ }
119
+ /** npm dependency section that the dependents of a policy scan. */
120
+ export type DependentsSection = 'dependencies' | 'optionalDependencies' | 'peerDependencies' | 'devDependencies';
121
+ /**
122
+ * Packages that bundle another configured package, re-released whenever that
123
+ * source releases, so that their shipped build includes the new version. The
124
+ * dependents come from the dependencies that each package declares, like the
125
+ * npm packages depending on another through package.json: a package is a
126
+ * dependent of a source when its declared spec resolves the source's current
127
+ * version or points at the workspace, and it releases when the next version
128
+ * of the source falls outside its declared range, or always with
129
+ * `when: 'always'`.
130
+ */
131
+ export interface DependentsConfig {
132
+ /** Bump applied to dependents that release without their own changeset; defaults to patch. */
133
+ bump?: 'patch' | 'minor' | 'major';
134
+ /** Package paths, identifiers or globs never re-released as dependents; they still release with their own changesets. */
135
+ exclude?: string[];
136
+ /** Whether dependents release only when the next source version falls outside their declared ranges, or always; defaults to out-of-range. */
137
+ when?: 'out-of-range' | 'always';
138
+ /** npm dependency sections scanned for sources; defaults to dependencies and optionalDependencies, with devDependencies for dependencies that get bundled into the dependent's build. */
139
+ sections?: DependentsSection[];
140
+ /** Overrides of the built-in npm lookup and spec rewrite, for dependencies that other manifests declare. */
141
+ dependency?: DependentsDependencyOptions;
142
+ /** Changelog entry added to the dependent, for example a bundle note; defaults to "Bundle <source> <version>." */
143
+ changelog?: (input: DependentsChangelogInput) => string | Promise<string>;
144
+ }
72
145
  /** JavaScript configuration for changeset-aware releases. */
73
146
  export interface ChangesetConfig {
74
147
  /** Branch receiving ordinary changes. */
@@ -91,6 +164,8 @@ export interface ChangesetConfig {
91
164
  releaseNotes?: (input: ReleaseNotesInput) => string | Promise<string>;
92
165
  /** Format the CHANGELOG.md entry of a changeset, for example to credit its pull request. */
93
166
  changelogEntry?: (input: ChangelogEntryInput) => string | Promise<string>;
167
+ /** Groups of packages re-released whenever one of their source packages releases. */
168
+ dependents?: DependentsConfig;
94
169
  }
95
170
  /** Inputs supplied to a custom next-version strategy. */
96
171
  export interface NextVersionInput {
@@ -198,6 +198,54 @@ function validateChangesetConfig(config, configFile) {
198
198
  'function' !== typeof candidate.changelogEntry) {
199
199
  throw new ValidationError(`wp-release config ${configFile}.changelogEntry must be a function.`);
200
200
  }
201
+ if (undefined !== candidate.dependents &&
202
+ ('object' !== typeof candidate.dependents ||
203
+ Array.isArray(candidate.dependents) ||
204
+ null === candidate.dependents)) {
205
+ throw new ValidationError(`wp-release config ${configFile}.dependents must be an object.`);
206
+ }
207
+ const dependentsOptions = candidate.dependents;
208
+ if (undefined !== dependentsOptions?.bump &&
209
+ !['patch', 'minor', 'major'].includes(dependentsOptions.bump)) {
210
+ throw new ValidationError(`wp-release config ${configFile}.dependents.bump must be patch, minor or major.`);
211
+ }
212
+ if (undefined !== dependentsOptions?.exclude &&
213
+ (!Array.isArray(dependentsOptions.exclude) ||
214
+ !dependentsOptions.exclude.length ||
215
+ dependentsOptions.exclude.some((item) => 'string' !== typeof item || !item.trim()))) {
216
+ throw new ValidationError(`wp-release config ${configFile}.dependents.exclude must be an array of non-empty strings.`);
217
+ }
218
+ if (undefined !== dependentsOptions?.when &&
219
+ !['out-of-range', 'always'].includes(dependentsOptions.when)) {
220
+ throw new ValidationError(`wp-release config ${configFile}.dependents.when must be out-of-range or always.`);
221
+ }
222
+ if (undefined !== dependentsOptions?.sections &&
223
+ (!Array.isArray(dependentsOptions.sections) ||
224
+ !dependentsOptions.sections.length ||
225
+ dependentsOptions.sections.some((section) => ![
226
+ 'dependencies',
227
+ 'optionalDependencies',
228
+ 'peerDependencies',
229
+ 'devDependencies',
230
+ ].includes(section)))) {
231
+ throw new ValidationError(`wp-release config ${configFile}.dependents.sections must be a list of dependencies, optionalDependencies, peerDependencies or devDependencies.`);
232
+ }
233
+ if (undefined !== dependentsOptions?.changelog &&
234
+ 'function' !== typeof dependentsOptions.changelog) {
235
+ throw new ValidationError(`wp-release config ${configFile}.dependents.changelog must be a function.`);
236
+ }
237
+ if (undefined !== dependentsOptions?.dependency) {
238
+ if ('object' !== typeof dependentsOptions.dependency ||
239
+ null === dependentsOptions.dependency) {
240
+ throw new ValidationError(`wp-release config ${configFile}.dependents.dependency must be an object.`);
241
+ }
242
+ ['find', 'update'].forEach((key) => {
243
+ if (undefined !== dependentsOptions.dependency[key] &&
244
+ 'function' !== typeof dependentsOptions.dependency[key]) {
245
+ throw new ValidationError(`wp-release config ${configFile}.dependents.dependency.${key} must be a function.`);
246
+ }
247
+ });
248
+ }
201
249
  if (undefined !== candidate.releaseNotes &&
202
250
  'function' !== typeof candidate.releaseNotes) {
203
251
  throw new ValidationError(`wp-release config ${configFile}.releaseNotes must be a function.`);
@@ -8,6 +8,8 @@ export interface ChangesetVersionInput extends ChangesetRepository {
8
8
  date?: string;
9
9
  /** Version only these packages, leaving the other changesets pending. */
10
10
  packages?: string[];
11
+ /** Whether the selected sources release their configured dependents; defaults to true. */
12
+ dependents?: boolean;
11
13
  }
12
14
  /** Versioned releases, the applied changesets and whether files were changed. */
13
15
  export interface VersionChangesetsResult {
@@ -93,4 +95,5 @@ export declare function getChangesetStatus(input: ChangesetRepository & {
93
95
  changedPackages: string[];
94
96
  changedPackageFiles: Record<string, string[]>;
95
97
  missingChangesets: string[];
98
+ warnings: string[];
96
99
  }>;
@@ -8,7 +8,11 @@ import { findMainPluginFile } from "./package-meta.js";
8
8
  import { findConstantValues } from "./version-check.js";
9
9
  import { formatChangelogList, formatChangeset, getChangesetBump, getNextVersion, parseChangeset, } from "./changesets.js";
10
10
  import { ValidationError } from "./errors.js";
11
+ import semverSatisfies from 'semver/functions/satisfies.js';
12
+ import validRange from 'semver/ranges/valid.js';
11
13
  const execFileAsync = promisify(execFile);
14
+ /** npm dependency sections whose declared dependency re-releases the dependent. */
15
+ const DEFAULT_SECTIONS = ['dependencies', 'optionalDependencies'];
12
16
  /**
13
17
  * Discover configured packages, pending changesets, and their proposed versions.
14
18
  *
@@ -71,8 +75,238 @@ async function getReleaseContext(input) {
71
75
  }
72
76
  });
73
77
  });
78
+ await applyDependents(releases, packages, config);
74
79
  return { config, packages, plan: { changesets, releases } };
75
80
  }
81
+ /**
82
+ * Add releases for dependents of packages that release, applying the
83
+ * configured dependents policy until the plan settles.
84
+ *
85
+ * @param releases Planned releases, mutated in place.
86
+ * @param packages Discovered packages.
87
+ * @param config Repository release configuration.
88
+ */
89
+ async function applyDependents(releases, packages, config) {
90
+ const rule = config.dependents;
91
+ if (!rule) {
92
+ return;
93
+ }
94
+ // Dependents may themselves be dependents of other sources, so expand until stable.
95
+ let grew = true;
96
+ while (grew) {
97
+ grew = false;
98
+ for (const source of [...releases]) {
99
+ for (const dependent of packages) {
100
+ if (await deriveDependentRelease(source, dependent, releases, rule)) {
101
+ grew = true;
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+ /**
108
+ * Plan the release of a package that bundles a source package, or add the
109
+ * source to the bundle list of its own pending release.
110
+ *
111
+ * @param source Package release of the source.
112
+ * @param dependent Package that would bundle the source.
113
+ * @param releases Planned releases, mutated in place.
114
+ * @param rule Dependents policy.
115
+ * @return Whether a new dependent release was planned.
116
+ */
117
+ async function deriveDependentRelease(source, dependent, releases, rule) {
118
+ if (dependent.name === source.package.name ||
119
+ isExcludedDependent(dependent, rule)) {
120
+ return false;
121
+ }
122
+ const specs = await findDependencySpecs(dependent, source.package, rule);
123
+ if (!specs.length || !dependentNeedsRelease(specs, source, rule)) {
124
+ return false;
125
+ }
126
+ const release = releases.find((item) => item.package.name === dependent.name);
127
+ if (release) {
128
+ const sources = release.sources ?? [];
129
+ if (!sources.includes(source.package.name)) {
130
+ // A dependent that releases for its own reasons still notes the bundle.
131
+ release.sources = [...sources, source.package.name];
132
+ }
133
+ return false;
134
+ }
135
+ const bump = rule.bump ?? 'patch';
136
+ releases.push({
137
+ package: dependent,
138
+ bump,
139
+ currentVersion: dependent.currentVersion,
140
+ nextVersion: getNextVersion(dependent.currentVersion, bump),
141
+ changesets: [],
142
+ sources: [source.package.name],
143
+ });
144
+ return true;
145
+ }
146
+ /**
147
+ * Find the specs through which a package declares a dependency on another.
148
+ *
149
+ * A spec is a dependency of the source that the current version resolves,
150
+ * like a range that covers it or a workspace spec, or one that the
151
+ * configured hook reports. A pin like an older version of the source
152
+ * selects a registry release instead, which keeps the local code out of
153
+ * the dependent's build.
154
+ *
155
+ * @param dependent Package potentially declaring the dependency.
156
+ * @param source Released source package.
157
+ * @param rule Dependents policy, with its dependency hook and sections.
158
+ * @return The declared specs, with their manifest files and package names.
159
+ */
160
+ async function findDependencySpecs(dependent, source, rule) {
161
+ const sourceManifest = await readJsonSafe(path.join(source.directory, 'package.json'));
162
+ if ('string' !== typeof sourceManifest?.name) {
163
+ return rule?.dependency?.find
164
+ ? findDependencyHookSpecs(dependent, source, rule)
165
+ : [];
166
+ }
167
+ const identifier = sourceManifest.name;
168
+ const specs = [];
169
+ const parsed = await readJsonSafe(path.join(dependent.directory, 'package.json'));
170
+ for (const section of dependentsSections(rule)) {
171
+ const sectionSpecs = parsed?.[section];
172
+ const declared = 'object' === typeof sectionSpecs &&
173
+ null !== sectionSpecs &&
174
+ Object.hasOwn(sectionSpecs, identifier)
175
+ ? String(sectionSpecs[identifier])
176
+ : undefined;
177
+ if (undefined !== declared &&
178
+ isDependencyOfSource(declared, source.currentVersion)) {
179
+ specs.push({
180
+ file: path.join(dependent.directory, 'package.json'),
181
+ name: identifier,
182
+ spec: declared,
183
+ });
184
+ }
185
+ }
186
+ if (rule?.dependency?.find) {
187
+ specs.push(...(await findDependencyHookSpecs(dependent, source, rule)));
188
+ }
189
+ return specs;
190
+ }
191
+ /**
192
+ * Find the specs that the configured dependency hook reports.
193
+ *
194
+ * @param dependent Package potentially declaring the dependency.
195
+ * @param source Released source package.
196
+ * @param rule Dependents policy.
197
+ * @return The hook's specs, with absolute manifest paths and package names.
198
+ */
199
+ async function findDependencyHookSpecs(dependent, source, rule) {
200
+ const found = await rule.dependency?.find?.({
201
+ package: dependent,
202
+ source,
203
+ });
204
+ return (found ?? []).map((entry) => ({
205
+ file: path.resolve(dependent.directory, entry.file),
206
+ name: entry.name,
207
+ spec: entry.spec,
208
+ }));
209
+ }
210
+ /**
211
+ * Check whether a spec points at the workspace copy of a package, by
212
+ * resolving the current version through its range or its workspace protocol.
213
+ *
214
+ * @param spec Declared spec of the source.
215
+ * @param currentVersion Current version of the source package.
216
+ * @return Whether the spec is a dependency of the source.
217
+ */
218
+ function isDependencyOfSource(spec, currentVersion) {
219
+ if (spec.startsWith('workspace:')) {
220
+ return true;
221
+ }
222
+ const range = effectiveRange(spec, currentVersion);
223
+ return (validRange(range) !== null && semverSatisfies(currentVersion, range));
224
+ }
225
+ /**
226
+ * The dependency sections that a dependents policy scans.
227
+ *
228
+ * @param rule Dependents policy.
229
+ * @return Dependency section names.
230
+ */
231
+ function dependentsSections(rule) {
232
+ return (rule?.sections ?? DEFAULT_SECTIONS);
233
+ }
234
+ /**
235
+ * Check whether a dependent has to release when its declared source releases.
236
+ *
237
+ * @param specs Declared specs of the source in the dependent.
238
+ * @param source Released source package.
239
+ * @param rule Dependents policy.
240
+ * @return Whether the dependent needs a release.
241
+ */
242
+ function dependentNeedsRelease(specs, source, rule) {
243
+ if ('always' === rule.when) {
244
+ return true;
245
+ }
246
+ // Without a range that accepts the next version, like an exact pin, the
247
+ // dependent has to release and rewrite its specs.
248
+ return specs.some(({ spec }) => isOutOfRange(source.nextVersion, spec, source.currentVersion));
249
+ }
250
+ /**
251
+ * Check whether a version falls outside a declared range.
252
+ *
253
+ * @param nextVersion Next version of the source package.
254
+ * @param spec Declared spec of the source).
255
+ * @param currentVersion Current version of the source package.
256
+ * @return Whether the next version falls outside the range.
257
+ */
258
+ function isOutOfRange(nextVersion, spec, currentVersion) {
259
+ const range = effectiveRange(spec, currentVersion);
260
+ if (validRange(range) === null) {
261
+ // Unrecognized ranges of dependencies like git URLs are left to the consumer.
262
+ return false;
263
+ }
264
+ return !semverSatisfies(nextVersion, range);
265
+ }
266
+ /**
267
+ * Resolve a dependency spec to the range it selects for an internal package.
268
+ *
269
+ * @param spec Declared spec.
270
+ * @param currentVersion Current version of the source package.
271
+ * @return The effective semver range.
272
+ */
273
+ function effectiveRange(spec, currentVersion) {
274
+ if (!spec.startsWith('workspace:')) {
275
+ return spec;
276
+ }
277
+ // Workspace specs resolve to the exact published version, and to a
278
+ // prefixed one with their operator.
279
+ const bare = spec.slice('workspace:'.length);
280
+ if ('*' === bare) {
281
+ return currentVersion;
282
+ }
283
+ if ('^' === bare || '~' === bare) {
284
+ return `${bare}${currentVersion}`;
285
+ }
286
+ return bare;
287
+ }
288
+ /**
289
+ * Check whether the dependents policy excludes a package.
290
+ *
291
+ * @param dependent Candidate dependent package.
292
+ * @param rule Dependents policy.
293
+ * @return Whether the package is excluded from re-releases.
294
+ */
295
+ function isExcludedDependent(dependent, rule) {
296
+ return (rule.exclude ?? []).some((pattern) => pattern === dependent.name ||
297
+ globRegExp(pattern).test(dependent.path));
298
+ }
299
+ async function readJsonSafe(file) {
300
+ try {
301
+ return JSON.parse(await readFile(file, 'utf8'));
302
+ }
303
+ catch (error) {
304
+ if ('ENOENT' !== error.code) {
305
+ throw error;
306
+ }
307
+ return undefined;
308
+ }
309
+ }
76
310
  /**
77
311
  * Create a Changesets-compatible Markdown file.
78
312
  *
@@ -123,9 +357,21 @@ export async function versionChangesets(input) {
123
357
  throw new ValidationError('No wp-release config found. Add wp-release.config.js or pass --config <path>.');
124
358
  }
125
359
  const { config, packages } = context;
126
- const releasePlan = input.packages
127
- ? selectReleasePlan(context.plan, packages, input.packages)
128
- : context.plan;
360
+ const withDependents = input.dependents ?? true;
361
+ let releasePlan = context.plan;
362
+ if (input.packages) {
363
+ releasePlan = expandDependentsPlan(await selectReleasePlan(context.plan, packages, input.packages, config, withDependents));
364
+ }
365
+ else if (!withDependents) {
366
+ releasePlan = stripDependentsPlan(context.plan);
367
+ }
368
+ // A dependent must not release while one of the packages it depends on has
369
+ // pending changesets that aren't released in the same run, or it would
370
+ // bundle unreleased code under a changelog that cannot credit it. The
371
+ // check only applies to repositories that adopted the dependents policy.
372
+ if (withDependents && config.dependents) {
373
+ await assertDependentsSourcesReleased(releasePlan, context.plan, packages, config);
374
+ }
129
375
  const date = input.date ?? new Date().toISOString().slice(0, 10);
130
376
  const changesetDirectory = path.resolve(input.cwd, config.changesets ?? '.changeset');
131
377
  const releases = await getVersionedReleases(input.cwd, config, releasePlan.releases, changesetDirectory);
@@ -140,6 +386,19 @@ export async function versionChangesets(input) {
140
386
  for (const target of configured.versionConfig.to) {
141
387
  await writeVersion(configured.directory, target, release.nextVersion, journal);
142
388
  }
389
+ for (const sourceName of release.sources ?? []) {
390
+ const source = releasePlan.releases.find((item) => item.package.name === sourceName);
391
+ if (!source) {
392
+ throw new ValidationError(`The dependent package "${release.package.name}" bundles unreleased package "${sourceName}".`);
393
+ }
394
+ await updateDependencyDeclaration({
395
+ dependent: release.package,
396
+ source: source.package,
397
+ version: source.nextVersion,
398
+ rule: config.dependents,
399
+ journal,
400
+ });
401
+ }
143
402
  await verifyVersionTargets(input.cwd, configured, release.nextVersion);
144
403
  await prependChangelog(configured.directory, release, date, config.readmeChangelog ?? false, config.changelogInsertAfter, journal);
145
404
  }
@@ -159,9 +418,183 @@ export async function versionChangesets(input) {
159
418
  changesets: releasePlan.changesets,
160
419
  releases,
161
420
  date,
162
- changed: !input.dryRun && releasePlan.changesets.length > 0,
421
+ changed: !input.dryRun &&
422
+ (releasePlan.changesets.length > 0 ||
423
+ releasePlan.releases.length > 0),
163
424
  };
164
425
  }
426
+ /**
427
+ * Drop bundle sources that aren't released by the selected plan.
428
+ *
429
+ * @param plan Selected release plan.
430
+ * @return Release plan with dependents bundling only its released sources.
431
+ */
432
+ function expandDependentsPlan(plan) {
433
+ const released = new Set(plan.releases.map((release) => release.package.name));
434
+ plan.releases.forEach((release) => {
435
+ if (release.sources) {
436
+ release.sources = release.sources.filter((name) => released.has(name));
437
+ if (!release.sources.length) {
438
+ delete release.sources;
439
+ }
440
+ }
441
+ });
442
+ return plan;
443
+ }
444
+ /**
445
+ * Remove the derived dependent releases of a plan, keeping only the releases
446
+ * of packages that have their own changesets.
447
+ *
448
+ * @param plan Release plan of all pending changesets.
449
+ * @return Release plan without derived dependent releases.
450
+ */
451
+ function stripDependentsPlan(plan) {
452
+ return {
453
+ ...plan,
454
+ releases: plan.releases
455
+ .filter((release) => release.changesets.length > 0)
456
+ .map(({ sources, ...release }) => release),
457
+ };
458
+ }
459
+ /**
460
+ * Fail when a planned release bundles a source package that has pending
461
+ * changesets released by a later run, so that no dependent ships unreleased
462
+ * code under a changelog that cannot credit it.
463
+ *
464
+ * @param plan Planned releases.
465
+ * @param fullPlan Release plan of all pending changesets.
466
+ * @param packages Discovered packages.
467
+ * @param config Repository release configuration.
468
+ * @throws {ValidationError} If a source of a planned release stays pending.
469
+ */
470
+ async function assertDependentsSourcesReleased(plan, fullPlan, packages, config) {
471
+ const releasing = new Set(plan.releases.map((release) => release.package.name));
472
+ const pending = new Set(fullPlan.releases.map((release) => release.package.name));
473
+ for (const release of plan.releases) {
474
+ // Dependencies built into the dependent, like bundled or vendored
475
+ // directories, stay out of the check, which only guards released
476
+ // packages that the dependent declares.
477
+ const names = await collectDependencyNames(release.package, packages, config);
478
+ for (const name of names) {
479
+ const blocked = pending.has(name) &&
480
+ !releasing.has(name) &&
481
+ name !== release.package.name;
482
+ if (blocked) {
483
+ throw new ValidationError(`Cannot release package "${release.package.name}" while its source "${name}" has pending changesets; release them together or pass --no-dependents.`);
484
+ }
485
+ }
486
+ }
487
+ }
488
+ /**
489
+ * Collect the names of the configured sources that a package declares, by the
490
+ * dependency sections of the policy, without resolving the specs against the
491
+ * versions. The changeset check uses this to see through a pin that the
492
+ * versioning will rewrite after the source releases.
493
+ *
494
+ * @param pkg Planned package.
495
+ * @param packages Discovered packages.
496
+ * @param config Repository release configuration.
497
+ * @return Release package names the package declares.
498
+ */
499
+ async function collectDeclarationNames(pkg, packages, config) {
500
+ const names = [];
501
+ for (const source of packages) {
502
+ if (pkg.name !== source.name &&
503
+ (await findNpmSpecs(pkg, source, dependentsSections(config.dependents))).length > 0) {
504
+ names.push(source.name);
505
+ }
506
+ }
507
+ return names;
508
+ }
509
+ /**
510
+ * Collect the names of the configured sources that a package builds against,
511
+ * through specs that resolve the current version or point at the workspace.
512
+ *
513
+ * @param pkg Planned package.
514
+ * @param packages Discovered packages.
515
+ * @param config Repository release configuration.
516
+ * @return Release package names the package declares.
517
+ */
518
+ async function collectDependencyNames(pkg, packages, config) {
519
+ const names = [];
520
+ for (const source of packages) {
521
+ if (pkg.name !== source.name &&
522
+ (await findDependencySpecs(pkg, source, config.dependents))
523
+ .length > 0) {
524
+ names.push(source.name);
525
+ }
526
+ }
527
+ return names;
528
+ }
529
+ /**
530
+ * Update how a dependent declares its dependency on a released source, with
531
+ * the configured hook or the built-in spec rewrite.
532
+ *
533
+ * @param input Dependent, source, released version, policy and journal.
534
+ * @param input.dependent The released dependent package.
535
+ * @param input.source Released source package that the dependent bundles.
536
+ * @param input.version Released version of the source.
537
+ * @param input.rule Dependents policy with its sections and dependency hooks.
538
+ * @param input.journal File journal for rolling back.
539
+ */
540
+ async function updateDependencyDeclaration(input) {
541
+ const { dependent, source, version, rule, journal } = input;
542
+ const specs = await findDependencySpecs(dependent, source, rule);
543
+ const update = rule?.dependency?.update;
544
+ if (update) {
545
+ await update({
546
+ package: dependent,
547
+ source,
548
+ version,
549
+ specs,
550
+ remember: (file) => rememberFile(journal, file),
551
+ });
552
+ return;
553
+ }
554
+ for (const { file, name, spec } of specs) {
555
+ const nextSpec = updatedSpec(spec, version);
556
+ if (null === nextSpec &&
557
+ isOutOfRange(version, spec, source.currentVersion)) {
558
+ 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}".`);
559
+ }
560
+ const changed = undefined !== nextSpec && nextSpec !== spec;
561
+ if (changed) {
562
+ const contents = await readFile(file, 'utf8');
563
+ const declared = new RegExp(`"${escapeRegExp(name)}"\\s*:\\s*"${escapeRegExp(spec)}"`);
564
+ if (!declared.test(contents)) {
565
+ throw new ValidationError(`The dependency spec of "${name}" in ${dependent.name}'s ${file} doesn't match "${spec}"; rewrite it to "${nextSpec}".`);
566
+ }
567
+ await rememberFile(journal, file);
568
+ await writeFile(file, contents.replace(declared, `"${name}": "${nextSpec}"`));
569
+ }
570
+ }
571
+ }
572
+ /**
573
+ * Update a dependency spec to the released version, keeping its shape, like
574
+ * the caret of ^0.9.0 or the x of 0.9.x.
575
+ *
576
+ * @param spec Declared spec of the source.
577
+ * @param version Released version of the source.
578
+ * @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.
579
+ */
580
+ function updatedSpec(spec, version) {
581
+ if (spec.startsWith('workspace:')) {
582
+ // Workspace specs resolve to the local package, so the manifest
583
+ // keeps them instead of a published version.
584
+ return undefined;
585
+ }
586
+ const prefix = /^[=\^~]/.exec(spec)?.[0] ?? '';
587
+ const bare = spec.slice(prefix.length).trim();
588
+ const partial = /^(v?\d+(?:\.\d+)*)(\.x)?$/.exec(bare);
589
+ if (partial?.[1]) {
590
+ const depth = partial[1].split('.').length;
591
+ const stem = version.split('.').slice(0, depth).join('.');
592
+ // npm partials like 1.x pin the major, and 0.9 pins the major.minor.
593
+ return `${prefix}${stem}${partial[2] ?? ''}`;
594
+ }
595
+ // Wildcards and keyword specs like * or latest already accept the release.
596
+ return null;
597
+ }
165
598
  /**
166
599
  * Format the changelog entries of each release, with the configured
167
600
  * formatter and the commit that added each changeset.
@@ -180,15 +613,47 @@ async function getVersionedReleases(cwd, config, releases, changesetDirectory) {
180
613
  commits.set(changeset.fileName, cached);
181
614
  return cached;
182
615
  };
616
+ const releasesByName = new Map(releases.map((release) => [release.package.name, release]));
183
617
  return Promise.all(releases.map(async (release) => {
184
618
  const entries = [];
185
619
  // Format one at a time, so that formatters calling an API aren't rate limited.
186
620
  for (const changeset of release.changesets) {
187
621
  entries.push(await getChangelogEntry(config, release.package, changeset, getCommit));
188
622
  }
623
+ for (const sourceName of release.sources ?? []) {
624
+ const source = releasesByName.get(sourceName);
625
+ if (!source) {
626
+ throw new ValidationError(`The dependent package "${release.package.name}" bundles unreleased package "${sourceName}".`);
627
+ }
628
+ entries.push(await getDependentsChangelogEntry(config, release.package, source));
629
+ }
189
630
  return { ...release, changelog: formatChangelogList(entries) };
190
631
  }));
191
632
  }
633
+ /**
634
+ * Format the bundle changelog entry of a dependent release.
635
+ *
636
+ * @param config Repository release configuration.
637
+ * @param pkg Dependent package being re-released.
638
+ * @param source Released source package bundled by the dependent.
639
+ * @return Changelog entry.
640
+ */
641
+ async function getDependentsChangelogEntry(config, pkg, source) {
642
+ if (!config.dependents?.changelog) {
643
+ return `Bundle ${source.package.name.split('/').pop()} ${source.nextVersion}.`;
644
+ }
645
+ const entry = await config.dependents.changelog({
646
+ package: pkg,
647
+ source: {
648
+ package: source.package,
649
+ version: source.nextVersion,
650
+ },
651
+ });
652
+ if ('string' !== typeof entry) {
653
+ throw new ValidationError(`The dependents changelog formatter didn't return a string for package "${pkg.name}".`);
654
+ }
655
+ return entry.trim();
656
+ }
192
657
  async function getChangelogEntry(config, pkg, changeset, getCommit) {
193
658
  const summary = changeset.summary.trim();
194
659
  if (!summary || !config.changelogEntry) {
@@ -226,24 +691,74 @@ async function getChangesetCommit(cwd, file) {
226
691
  .split('\x1f');
227
692
  return hash ? { hash, shortHash, subject } : undefined;
228
693
  }
694
+ /**
695
+ * Add a package to the selected release plan when it bundles a selected source.
696
+ *
697
+ * @param source Selected package release.
698
+ * @param dependent Package that would bundle the source.
699
+ * @param packages Discovered packages.
700
+ * @param rule Dependents policy.
701
+ * @param selected Selected package names, mutated in place.
702
+ * @return Whether the package was selected.
703
+ */
704
+ async function selectDependentRelease(source, dependent, packages, rule, selected) {
705
+ const skip = selected.has(dependent.name) ||
706
+ dependent.name === source.package.name ||
707
+ isExcludedDependent(dependent, rule);
708
+ if (skip) {
709
+ return false;
710
+ }
711
+ const specs = await findDependencySpecs(dependent, source.package, rule);
712
+ if (!specs.length || !dependentNeedsRelease(specs, source, rule)) {
713
+ return false;
714
+ }
715
+ selected.add(dependent.name);
716
+ return true;
717
+ }
229
718
  /**
230
719
  * Limit a release plan to some packages and the changesets that name them.
231
720
  *
232
- * @param plan Release plan of all pending changesets.
233
- * @param packages Configured packages.
234
- * @param names Packages to version.
235
- * @return Release plan of the named packages.
721
+ * @param plan Release plan of all pending changesets.
722
+ * @param packages Configured packages.
723
+ * @param names Packages to version.
724
+ * @param config Repository release configuration.
725
+ * @param withDependents Whether the selection also releases the dependents of the selected sources.
726
+ * @return Release plan of the selected packages and their dependents.
236
727
  * @throws {ValidationError} For unknown packages.
237
728
  */
238
- function selectReleasePlan(plan, packages, names) {
729
+ async function selectReleasePlan(plan, packages, names, config, withDependents = true) {
239
730
  names.forEach((name) => {
240
731
  if (!packages.some((pkg) => pkg.name === name)) {
241
732
  throw new ValidationError(`Unknown package "${name}".`);
242
733
  }
243
734
  });
735
+ // Dependents that need a release close over the selected sources.
736
+ const selected = new Set(names);
737
+ let grew = true;
738
+ // Dependents may depend on other selected dependents, so expand until stable.
739
+ while (grew && withDependents && config.dependents) {
740
+ grew = false;
741
+ const selectedSources = plan.releases.filter((release) => selected.has(release.package.name));
742
+ for (const source of selectedSources) {
743
+ for (const dependent of packages) {
744
+ if (await selectDependentRelease(source, dependent, packages, config.dependents, selected)) {
745
+ grew = true;
746
+ }
747
+ }
748
+ }
749
+ }
750
+ const selectedNames = [...selected];
244
751
  return {
245
- releases: plan.releases.filter((release) => names.includes(release.package.name)),
246
- changesets: plan.changesets.filter((changeset) => names.some((name) => Object.hasOwn(changeset.packages, name))),
752
+ releases: plan.releases.filter((release) => {
753
+ if (!selected.has(release.package.name)) {
754
+ return false;
755
+ }
756
+ // Explicitly selected dependents without their own changesets only
757
+ // release when their source releases in the same selection.
758
+ return (release.changesets.length > 0 ||
759
+ (release.sources ?? []).some((name) => selected.has(name)));
760
+ }),
761
+ changesets: plan.changesets.filter((changeset) => selectedNames.some((name) => Object.hasOwn(changeset.packages, name))),
247
762
  };
248
763
  }
249
764
  /**
@@ -510,6 +1025,8 @@ export async function getChangesetStatus(input) {
510
1025
  }
511
1026
  const { plan } = context;
512
1027
  const { changesets, releases } = plan;
1028
+ const config = context.config;
1029
+ const { packages } = context;
513
1030
  const since = input.since ??
514
1031
  (context.config.baseBranch
515
1032
  ? `origin/${context.config.baseBranch}`
@@ -522,17 +1039,102 @@ export async function getChangesetStatus(input) {
522
1039
  ? await readChangesetsSince(input.cwd, context.config, changesets, since)
523
1040
  : [];
524
1041
  const hasEmptyChangeset = changesetsInDiff.some((changeset) => 0 === Object.keys(changeset.packages).length);
1042
+ // A dependent releases with its sources, which the release pull request
1043
+ // already announces when this pull request's changesets release one of
1044
+ // them. Exempting its package manifest pin change keeps the check from
1045
+ // demanding an entry for what versioning rewrites anyway; documentation
1046
+ // and code changes still need their own changesets.
1047
+ const bundled = new Set((await Promise.all(changedPackages
1048
+ .filter((name) => !changesetsInDiff.some((item) => Object.hasOwn(item.packages, name)))
1049
+ .map(async (name) => {
1050
+ if (!config.dependents ||
1051
+ !(changed.filesByPackage[name] ?? []).every((file) => path.basename(file) === 'package.json')) {
1052
+ return undefined;
1053
+ }
1054
+ const configured = packages.find((pkg) => pkg.name === name);
1055
+ if (!configured) {
1056
+ return undefined;
1057
+ }
1058
+ const sources = await collectDeclarationNames(configured, packages, config);
1059
+ return changesetsInDiff.some((item) => sources.some((source) => Object.hasOwn(item.packages, source)))
1060
+ ? name
1061
+ : undefined;
1062
+ }))).filter((name) => undefined !== name));
525
1063
  const missingChangesets = hasEmptyChangeset
526
1064
  ? []
527
- : changedPackages.filter((name) => !changesetsInDiff.some((item) => Object.hasOwn(item.packages, name)));
1065
+ : changedPackages.filter((name) => !changesetsInDiff.some((item) => Object.hasOwn(item.packages, name)) && !bundled.has(name));
1066
+ // A pin that doesn't resolve the local package selects a registry release
1067
+ // instead, which the dependents policy won't re-release automatically.
1068
+ const warnings = config.dependents
1069
+ ? await collectPinDriftWarnings(context.config, context.packages)
1070
+ : [];
528
1071
  return {
529
1072
  changesets,
530
1073
  releases,
531
1074
  changedPackages,
532
1075
  changedPackageFiles: changed.filesByPackage,
533
1076
  missingChangesets,
1077
+ warnings,
534
1078
  };
535
1079
  }
1080
+ /**
1081
+ * Warn about declared pins of configured packages that don't resolve the
1082
+ * current version of the package they name.
1083
+ *
1084
+ * @param config Repository release configuration.
1085
+ * @param packages Discovered packages.
1086
+ * @return Warnings, one per package pair.
1087
+ */
1088
+ async function collectPinDriftWarnings(config, packages) {
1089
+ const warnings = [];
1090
+ for (const dependent of packages) {
1091
+ const others = packages.filter((source) => source.name !== dependent.name);
1092
+ for (const source of others) {
1093
+ const driftedPin = (await findNpmSpecs(dependent, source, config.dependents?.sections ?? DEFAULT_SECTIONS))
1094
+ .filter(({ spec }) => !spec.startsWith('workspace:') &&
1095
+ isOutOfRange(source.currentVersion, spec, source.currentVersion))
1096
+ .at(0);
1097
+ if (driftedPin) {
1098
+ 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.`);
1099
+ }
1100
+ }
1101
+ }
1102
+ return warnings;
1103
+ }
1104
+ /**
1105
+ * Find the npm specs that a package declares for a source, without the
1106
+ * dependency filtering of dependents links, for drift warnings.
1107
+ *
1108
+ * @param dependent Package potentially declaring the dependency.
1109
+ * @param source Package the dependency is declared of.
1110
+ * @param sections Dependency sections to scan.
1111
+ * @return The declared specs.
1112
+ */
1113
+ async function findNpmSpecs(dependent, source, sections) {
1114
+ const sourceManifest = await readJsonSafe(path.join(source.directory, 'package.json'));
1115
+ if ('string' !== typeof sourceManifest?.name) {
1116
+ return [];
1117
+ }
1118
+ const identifier = sourceManifest.name;
1119
+ const parsed = await readJsonSafe(path.join(dependent.directory, 'package.json'));
1120
+ const specs = [];
1121
+ for (const section of sections) {
1122
+ const sectionSpecs = parsed?.[section];
1123
+ const declared = 'object' === typeof sectionSpecs &&
1124
+ null !== sectionSpecs &&
1125
+ Object.hasOwn(sectionSpecs, identifier)
1126
+ ? String(sectionSpecs[identifier])
1127
+ : undefined;
1128
+ if (undefined !== declared) {
1129
+ specs.push({
1130
+ file: path.join(dependent.directory, 'package.json'),
1131
+ name: identifier,
1132
+ spec: declared,
1133
+ });
1134
+ }
1135
+ }
1136
+ return specs;
1137
+ }
536
1138
  async function requireConfig(input) {
537
1139
  const loaded = await loadChangesetConfig(input);
538
1140
  if (!loaded) {
@@ -31,6 +31,8 @@ export interface PackageRelease {
31
31
  currentVersion: string;
32
32
  nextVersion: string;
33
33
  changesets: Changeset[];
34
+ /** Names of released source packages that this dependent release bundles; absent for changeset-driven releases. */
35
+ sources?: string[];
34
36
  }
35
37
  /** A package release with its formatted changelog entries, once versioned. */
36
38
  export interface VersionedRelease extends PackageRelease {
@@ -65,12 +65,17 @@ export declare const changesetVersionArgs: {
65
65
  readonly type: "string";
66
66
  readonly multiple: true;
67
67
  };
68
+ readonly 'no-dependents': {
69
+ readonly type: "boolean";
70
+ };
68
71
  };
69
72
  /** Parsed input for `changeset-version`. */
70
73
  export interface ChangesetVersionArgs {
71
74
  dryRun: boolean;
72
75
  date?: string;
73
76
  packages?: string[];
77
+ /** Whether the selected sources release their configured dependents; defaults to true. */
78
+ dependents: boolean;
74
79
  }
75
80
  /**
76
81
  * Validate and normalize a release date option.
@@ -75,6 +75,7 @@ export const changesetVersionArgs = {
75
75
  'dry-run': { type: 'boolean' },
76
76
  date: { type: 'string' },
77
77
  package: { type: 'string', multiple: true },
78
+ 'no-dependents': { type: 'boolean' },
78
79
  };
79
80
  /**
80
81
  * Validate and normalize a release date option.
@@ -111,6 +112,7 @@ export function parseChangesetVersionArgs(values) {
111
112
  }
112
113
  return {
113
114
  dryRun: getBoolean(values, 'dry-run'),
115
+ dependents: !getBoolean(values, 'no-dependents'),
114
116
  ...(date ? { date } : {}),
115
117
  ...(packages.length ? { packages } : {}),
116
118
  };
@@ -13,6 +13,7 @@ export interface ChangesetStatusResult {
13
13
  changedPackages: string[];
14
14
  changedPackageFiles: Record<string, string[]>;
15
15
  missingChangesets: string[];
16
+ warnings: string[];
16
17
  }
17
18
  declare const _default: {
18
19
  name: string;
@@ -37,6 +38,7 @@ declare const _default: {
37
38
  changedPackages: string[];
38
39
  changedPackageFiles: Record<string, string[]>;
39
40
  missingChangesets: string[];
41
+ warnings: string[];
40
42
  };
41
43
  }>;
42
44
  };
@@ -20,8 +20,11 @@ An empty changeset added since the comparison base counts as an explicit no-rele
20
20
  logger.summary(() => {
21
21
  const rows = result.changedPackages.map((name) => {
22
22
  let status = 'no release requested';
23
- if (result.releases.some((release) => release.package.name === name)) {
24
- status = 'changeset found';
23
+ const release = result.releases.find((item) => item.package.name === name);
24
+ if (release) {
25
+ status = release.changesets.length
26
+ ? 'changeset found'
27
+ : 'releases with its sources';
25
28
  }
26
29
  if (result.missingChangesets.includes(name)) {
27
30
  status = 'missing changeset';
@@ -49,6 +52,7 @@ An empty changeset added since the comparison base counts as an explicit no-rele
49
52
  else if (!result.releases.length) {
50
53
  logger.info('No pending package releases.');
51
54
  }
55
+ result.warnings.forEach((warning) => logger.warning(warning));
52
56
  return {
53
57
  exitCode: result.missingChangesets.length ? 2 : 0,
54
58
  result: {
@@ -26,6 +26,9 @@ declare const _default: {
26
26
  readonly type: "string";
27
27
  readonly multiple: true;
28
28
  };
29
+ readonly 'no-dependents': {
30
+ readonly type: "boolean";
31
+ };
29
32
  };
30
33
  run({ values, cwd, configFile, logger }: import("./command.ts").CommandContext): Promise<{
31
34
  exitCode: number;
@@ -3,12 +3,13 @@ import { getChangesetVersionSummary } from "../summary.js";
3
3
  import { parseChangesetVersionArgs, changesetVersionArgs, } from "./changeset-args.js";
4
4
  export default {
5
5
  name: 'changeset-version',
6
- usage: 'changeset-version [--dry-run] [--date <YYYY-MM-DD>] [--package <name>]',
6
+ usage: 'changeset-version [--dry-run] [--date <YYYY-MM-DD>] [--package <name>] [--no-dependents]',
7
7
  summary: 'Apply pending version bumps and changelog entries.',
8
8
  help: `Options:
9
9
  --dry-run Print the release plan without writing files
10
10
  --date <YYYY-MM-DD> Changelog date (default: today)
11
11
  --package <name> Version only this package (repeatable)
12
+ --no-dependents Leave the configured dependents of the selected sources out
12
13
 
13
14
  Updates configured version files, prepends changelog entries and removes the
14
15
  applied changeset files. With --package, a changeset that also names other
package/dist/index.d.ts CHANGED
@@ -43,7 +43,7 @@ export type { ExtractZipInput, ZipEntryInfo, ZipEntryType } from './unzip.ts';
43
43
  export type { FindVersionMismatchesInput, GitTagInput, VersionCheckResult, VersionMismatch, } from './version-check.ts';
44
44
  export type { Changeset, ChangesetBump, ChangesetPackages, ChangesetConfig, ChangesetPackageType, ChangesetPackage, PackageRelease, VersionedRelease, ChangesetReleasePlan, } from './changesets.ts';
45
45
  export type { ChangesetRepository, ChangesetVersionInput, VersionChangesetsResult, ChangesetTagInput, ChangesetTagResult, } from './changeset-release.ts';
46
- export type { WpReleaseConfig, ChangesetPackageGroup, PackageVersionConfig, PackageVersionSource, PackageVersionTarget, PackageIdSource, NextVersionInput, ReleaseCommit, ReleaseNotesInput, ChangelogEntryInput, PackageJsonVersionOptions, ComposerJsonVersionOptions, WordPressVersionFileOptions, GitTagVersionOptions, PhpConstantVersionOptions, LoadChangesetConfigInput, } from './changeset-config.ts';
46
+ export type { WpReleaseConfig, ChangesetPackageGroup, PackageVersionConfig, PackageVersionSource, PackageVersionTarget, PackageIdSource, NextVersionInput, ReleaseCommit, ReleaseNotesInput, ChangelogEntryInput, DependentsChangelogInput, DependentsConfig, PackageJsonVersionOptions, ComposerJsonVersionOptions, WordPressVersionFileOptions, GitTagVersionOptions, PhpConstantVersionOptions, LoadChangesetConfigInput, } from './changeset-config.ts';
47
47
  export type { SvnCheckoutInput, SvnCommandInput, SvnCommandOutput, SvnCommitInput, SvnCopyInput, SvnDepth, SvnOptions, SvnPathsInput, SvnPropsetInput, SvnRunner, SvnStatusEntry, SvnStatusType, SvnUpdateInput, } from './svn.ts';
48
48
  export type { SyncDirectoryInput, SyncResult } from './sync.ts';
49
49
  export type { SummaryContext } from './summary.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpelevator/wp-release",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Package, checksum, sign, verify and version-check WordPress plugins and themes, and deploy plugins to WordPress.org SVN",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,11 +41,13 @@
41
41
  ],
42
42
  "dependencies": {
43
43
  "ignore": "^7.0.12",
44
+ "semver": "^7.7.1",
44
45
  "yauzl": "^3.4.0",
45
46
  "yazl": "^3.3.1"
46
47
  },
47
48
  "devDependencies": {
48
49
  "@types/node": "^22.20.5",
50
+ "@types/semver": "^7.8.0",
49
51
  "@types/yauzl": "^3.4.0",
50
52
  "@types/yazl": "^3.3.1",
51
53
  "@wordpress/scripts": "^33.0",