11#!/usr/bin/env node
22
33/**
4- * Preview the version bumps that `changeset version` will produce.
5- *
6- * Workflow:
7- * 1. Snapshot every workspace package's current version
8- * 2. Run `changeset version` (mutates package.json files)
9- * 3. Diff against the snapshot
10- * 4. Print a markdown summary (or write to --output file)
11- *
12- * This script is meant to run in CI on a disposable checkout — it does NOT
13- * revert the changes it makes.
4+ * Uses `@changesets/get-release-plan` to get the version bumps and formats it as markdown.
145 */
156
16- import { execSync } from 'node:child_process'
17- import { readdirSync , readFileSync , writeFileSync } from 'node:fs'
18- import { join , resolve } from 'node:path'
7+ import { writeFileSync } from 'node:fs'
8+ import { resolve } from 'node:path'
199import { parseArgs } from 'node:util'
10+ import getReleasePlan from '@changesets/get-release-plan'
2011
2112const ROOT = resolve ( import . meta. dirname , '..' , '..' )
2213
23- const PACKAGES_DIR = join ( ROOT , 'packages' )
24-
25- function readPackageVersions ( ) {
26- const versions = new Map ( )
27- for ( const dir of readdirSync ( PACKAGES_DIR , { withFileTypes : true } ) ) {
28- if ( ! dir . isDirectory ( ) ) continue
29- const pkgPath = join ( PACKAGES_DIR , dir . name , 'package.json' )
30- try {
31- const pkg = JSON . parse ( readFileSync ( pkgPath , 'utf8' ) )
32- if ( pkg . name && pkg . version && pkg . private !== true ) {
33- versions . set ( pkg . name , pkg . version )
34- }
35- } catch {
36- // skip packages without a valid package.json
37- }
38- }
39- return versions
14+ function reasonRank ( reason ) {
15+ return reason === 'Changeset' ? 2 : 1
4016}
4117
42- function readChangesetEntries ( ) {
43- const changesetDir = join ( ROOT , '.changeset' )
44- const explicit = new Map ( )
45- for ( const file of readdirSync ( changesetDir ) ) {
46- if ( file === 'config.json' || file === 'README.md' || ! file . endsWith ( '.md' ) )
47- continue
48- const content = readFileSync ( join ( changesetDir , file ) , 'utf8' )
49- const frontmatterMatch = content . match ( / ^ - - - \n ( [ \s \S ] * ?) \n - - - / )
50- if ( ! frontmatterMatch ) continue
51- for ( const line of frontmatterMatch [ 1 ] . split ( '\n' ) ) {
52- const match = line . match ( / ^ [ ' " ] ? ( [ ^ ' " ] + ) [ ' " ] ? \s * : \s * ( m a j o r | m i n o r | p a t c h ) / )
53- if ( match ) {
54- const [ , name , bump ] = match
55- const existing = explicit . get ( name )
56- // keep the highest bump if a package appears in multiple changesets
57- if ( ! existing || bumpRank ( bump ) > bumpRank ( existing ) ) {
58- explicit . set ( name , bump )
59- }
60- }
61- }
62- }
63- return explicit
64- }
65-
66- function bumpRank ( bump ) {
67- return bump === 'major' ? 3 : bump === 'minor' ? 2 : 1
68- }
69-
70- function bumpType ( oldVersion , newVersion ) {
71- const [ oMaj , oMin ] = oldVersion . split ( '.' ) . map ( Number )
72- const [ nMaj , nMin ] = newVersion . split ( '.' ) . map ( Number )
73- if ( nMaj > oMaj ) return 'major'
74- if ( nMin > oMin ) return 'minor'
75- return 'patch'
76- }
77-
78- function main ( ) {
18+ async function main ( ) {
7919 const { values } = parseArgs ( {
8020 args : process . argv . slice ( 2 ) ,
8121 options : {
@@ -85,10 +25,10 @@ function main() {
8525 allowPositionals : false ,
8626 } )
8727
88- // 1. Read explicit changeset entries
89- const explicit = readChangesetEntries ( )
28+ const releasePlan = await getReleasePlan ( ROOT )
29+ const releases = releasePlan . releases
9030
91- if ( explicit . size === 0 ) {
31+ if ( releases . length === 0 ) {
9232 const msg =
9333 'No changeset entries found. Merging this PR will not cause a version bump for any packages.\n'
9434 process . stdout . write ( msg )
@@ -99,42 +39,19 @@ function main() {
9939 return
10040 }
10141
102- // 2. Snapshot current versions
103- const before = readPackageVersions ( )
104-
105- // 3. Temporarily swap changeset config to skip changelog generation
106- // (the GitHub changelog plugin requires a token we don't need for previews)
107- const configPath = join ( ROOT , '.changeset' , 'config.json' )
108- const originalConfig = readFileSync ( configPath , 'utf8' )
109- try {
110- const config = JSON . parse ( originalConfig )
111- config . changelog = false
112- writeFileSync ( configPath , JSON . stringify ( config , null , 2 ) )
113-
114- // 4. Run changeset version
115- execSync ( 'pnpm changeset version' , { cwd : ROOT , stdio : 'pipe' } )
116- } finally {
117- // Always restore the original config
118- writeFileSync ( configPath , originalConfig )
119- }
120-
121- // 5. Read new versions
122- const after = readPackageVersions ( )
123-
12442 // 6. Diff
12543 const bumps = [ ]
126- for ( const [ name , newVersion ] of after ) {
127- const oldVersion = before . get ( name )
128- if ( ! oldVersion || oldVersion === newVersion ) continue
129- const bump = bumpType ( oldVersion , newVersion )
130- const source = explicit . has ( name ) ? explicit . get ( name ) : 'dependency'
131- bumps . push ( { name, oldVersion, newVersion, bump, source } )
44+ for ( const release of releases ) {
45+ if ( release . oldVersion === release . newVersion ) continue
46+ const reason = release . changesets . length !== 0 ? 'Changeset' : 'Dependent'
47+ bumps . push ( { ...release , reason } )
13248 }
13349
134- // Sort: major first, then minor, then patch; within each group alphabetical
50+ // Order by reason and name
13551 bumps . sort (
13652 ( a , b ) =>
137- bumpRank ( b . bump ) - bumpRank ( a . bump ) || a . name . localeCompare ( b . name ) ,
53+ reasonRank ( b . reason ) - reasonRank ( a . reason ) ||
54+ a . name . localeCompare ( b . name ) ,
13855 )
13956
14057 // 7. Build markdown
@@ -145,41 +62,53 @@ function main() {
14562 'No version changes detected. Merging this PR will not cause a version bump for any packages.' ,
14663 )
14764 } else {
148- const explicitBumps = bumps . filter ( ( b ) => b . source !== 'dependency' )
149- const dependencyBumps = bumps . filter ( ( b ) => b . source === 'dependency' )
65+ const majorBumps = bumps . filter ( ( b ) => b . type === 'major' )
66+ const minorBumps = bumps . filter ( ( b ) => b . type === 'minor' )
67+ const patchBumps = bumps . filter ( ( b ) => b . type === 'patch' )
68+ const directBumps = bumps . filter ( ( b ) => b . reason === 'Changeset' )
69+ const indirectBumps = bumps . filter ( ( b ) => b . reason === 'Dependent' )
15070
15171 lines . push (
152- `**${ explicitBumps . length } ** package(s) bumped directly, **${ dependencyBumps . length } ** bumped as dependents.` ,
72+ `**${ directBumps . length } ** package(s) bumped directly, **${ indirectBumps . length } ** bumped as dependents.` ,
15373 )
15474 lines . push ( '' )
15575
156- if ( explicitBumps . length > 0 ) {
157- lines . push ( '### Direct bumps' )
76+ if ( majorBumps . length > 0 ) {
77+ lines . push ( '### 🟥 Major bumps' )
78+ lines . push ( '' )
79+ lines . push ( '| Package | Version | Reason |' )
80+ lines . push ( '| --- | --- | --- |' )
81+ for ( const b of majorBumps ) {
82+ lines . push (
83+ `| \`${ b . name } \` | ${ b . oldVersion } → ${ b . newVersion } | ${ b . reason } |` ,
84+ )
85+ }
86+ lines . push ( '' )
87+ }
88+
89+ if ( minorBumps . length > 0 ) {
90+ lines . push ( '### 🟨 Minor bumps' )
15891 lines . push ( '' )
159- lines . push ( '| Package | Bump | Version |' )
92+ lines . push ( '| Package | Version | Reason |' )
16093 lines . push ( '| --- | --- | --- |' )
161- for ( const b of explicitBumps ) {
94+ for ( const b of minorBumps ) {
16295 lines . push (
163- `| \`${ b . name } \` | ** ${ b . bump } ** | ${ b . oldVersion } → ${ b . newVersion } |` ,
96+ `| \`${ b . name } \` | ${ b . oldVersion } → ${ b . newVersion } | ${ b . reason } |` ,
16497 )
16598 }
16699 lines . push ( '' )
167100 }
168101
169- if ( dependencyBumps . length > 0 ) {
170- lines . push (
171- '<details>' ,
172- `<summary>Dependency bumps (${ dependencyBumps . length } )</summary>` ,
173- '' ,
174- '| Package | Bump | Version |' ,
175- '| --- | --- | --- |' ,
176- )
177- for ( const b of dependencyBumps ) {
102+ if ( patchBumps . length > 0 ) {
103+ lines . push ( '### 🟩 Patch bumps' )
104+ lines . push ( '' )
105+ lines . push ( '| Package | Version | Reason |' )
106+ lines . push ( '| --- | --- | --- |' )
107+ for ( const b of patchBumps ) {
178108 lines . push (
179- `| \`${ b . name } \` | ${ b . bump } | ${ b . oldVersion } → ${ b . newVersion } |` ,
109+ `| \`${ b . name } \` | ${ b . oldVersion } → ${ b . newVersion } | ${ b . reason } |` ,
180110 )
181111 }
182- lines . push ( '' , '</details>' )
183112 }
184113 }
185114
0 commit comments