11#!/usr/bin/env node
2- import { mkdir , readdir , rm , writeFile } from 'node:fs/promises' ;
2+ import { execFileSync } from 'node:child_process' ;
3+ import { mkdir , readdir , readFile , stat , writeFile } from 'node:fs/promises' ;
34import path from 'node:path' ;
45import process from 'node:process' ;
56import { fileURLToPath } from 'node:url' ;
@@ -13,6 +14,8 @@ const prNumber = process.env.PR_NUMBER?.trim() ?? 'unknown';
1314const prTitle = process . env . PR_TITLE ?. trim ( ) ?? '' ;
1415const prBody = process . env . PR_BODY ?. trim ( ) ?? '' ;
1516const labelName = process . env . LABEL_NAME ?. trim ( ) . toLowerCase ( ) ?? '' ;
17+ const baseSha = process . env . BASE_SHA ?. trim ( ) ?? '' ;
18+ const headSha = process . env . HEAD_SHA ?. trim ( ) ?? '' ;
1619
1720if ( ! prTitle ) {
1821 console . error ( 'PR title is required.' ) ;
@@ -44,26 +47,157 @@ if (labelName === 'add-changeset-major') {
4447 releaseType = 'major' ;
4548}
4649
47- const fileName = `pr-${ prNumber } .md` ;
48- const filePath = path . join ( changesetDir , fileName ) ;
50+ const workspacePackages = await getWorkspacePackages ( repoRoot ) ;
51+ const affectedPackages = await getAffectedPackages (
52+ repoRoot ,
53+ workspacePackages ,
54+ baseSha ,
55+ headSha ,
56+ ) ;
4957
50- const existingFiles = await readdir ( changesetDir ) ;
58+ if ( affectedPackages . length === 0 ) {
59+ console . log ( 'No workspace packages were detected from the PR diff; skipping changeset creation.' ) ;
60+ process . exit ( 0 ) ;
61+ }
5162
52- await Promise . all (
53- existingFiles
54- . filter ( ( name ) => name . startsWith ( `pr-${ prNumber } ` ) && name . endsWith ( '.md' ) )
55- . map ( ( name ) => rm ( path . join ( changesetDir , name ) , { force : true } ) ) ,
56- ) ;
63+ await mkdir ( changesetDir , { recursive : true } ) ;
64+ const existingFiles = await listChangesetFiles ( changesetDir ) ;
65+
66+ execFileSync ( 'pnpm' , [ 'exec' , 'changeset' , 'add' , '--empty' , '--message' , prTitle ] , {
67+ cwd : repoRoot ,
68+ stdio : 'pipe' ,
69+ } ) ;
70+
71+ const createdChangesetFile = await getNewestChangesetFile ( changesetDir , existingFiles ) ;
72+ if ( ! createdChangesetFile ) {
73+ console . error ( 'Changesets CLI did not create a changeset file.' ) ;
74+ process . exit ( 1 ) ;
75+ }
5776
58- const packages = [ 'react-docgen' , 'react-docgen-cli' ] ;
59- const frontmatter = packages
77+ const filePath = path . join ( changesetDir , createdChangesetFile ) ;
78+ const frontmatter = affectedPackages
6079 . map ( ( pkg ) => `"${ pkg } ": ${ releaseType } ` )
6180 . join ( '\n' ) ;
6281const content = `---\n${ frontmatter } \n---\n\n${ prTitle } \n` ;
63-
64- await mkdir ( changesetDir , { recursive : true } ) ;
6582await writeFile ( filePath , content , 'utf8' ) ;
6683
6784console . log (
68- `Created changeset ${ path . relative ( repoRoot , filePath ) } with release type ${ releaseType } .` ,
85+ `Created changeset ${ path . relative ( repoRoot , filePath ) } for ${ affectedPackages . join ( ', ' ) } with release type ${ releaseType } .` ,
6986) ;
87+
88+ async function getWorkspacePackages ( repoRootPath ) {
89+ const packagesDir = path . join ( repoRootPath , 'packages' ) ;
90+ const entries = await readdir ( packagesDir , { withFileTypes : true } ) ;
91+
92+ return (
93+ await Promise . all (
94+ entries
95+ . filter ( ( entry ) => entry . isDirectory ( ) )
96+ . map ( async ( entry ) => {
97+ const packageJsonPath = path . join ( packagesDir , entry . name , 'package.json' ) ;
98+ try {
99+ const packageJsonContent = await readFile ( packageJsonPath , 'utf8' ) ;
100+ const packageJson = JSON . parse ( packageJsonContent ) ;
101+ return packageJson . name ? { name : packageJson . name , dirName : entry . name } : null ;
102+ } catch {
103+ return null ;
104+ }
105+ } ) ,
106+ )
107+ ) . filter ( Boolean ) ;
108+ }
109+
110+ async function getAffectedPackages ( repoRootPath , workspacePackagesList , baseShaValue , headShaValue ) {
111+ const changedFiles = await getChangedFiles ( repoRootPath , baseShaValue , headShaValue ) ;
112+ const affectedPackageNames = new Set ( ) ;
113+ const ignoredPackages = new Set ( [ '@react-docgen-internal/benchmark' , '@react-docgen-internal/website' ] ) ;
114+
115+ for ( const changedFile of changedFiles ) {
116+ const normalizedPath = changedFile . replace ( / \\ / g, '/' ) ;
117+ const rootChanges = [ 'package.json' , 'pnpm-workspace.yaml' , 'nx.json' , 'tsconfig.base.json' , 'pnpm-lock.yaml' ] ;
118+
119+ if ( rootChanges . includes ( normalizedPath ) ) {
120+ for ( const workspacePackage of workspacePackagesList ) {
121+ if ( ! ignoredPackages . has ( workspacePackage . name ) ) {
122+ affectedPackageNames . add ( workspacePackage . name ) ;
123+ }
124+ }
125+ continue ;
126+ }
127+
128+ for ( const workspacePackage of workspacePackagesList ) {
129+ const packagePrefix = `packages/${ workspacePackage . dirName } /` ;
130+ if (
131+ ! ignoredPackages . has ( workspacePackage . name ) &&
132+ ( normalizedPath === `packages/${ workspacePackage . dirName } ` || normalizedPath . startsWith ( packagePrefix ) )
133+ ) {
134+ affectedPackageNames . add ( workspacePackage . name ) ;
135+ }
136+ }
137+ }
138+
139+ return [ ...affectedPackageNames ] . sort ( ) ;
140+ }
141+
142+ async function getChangedFiles ( repoRootPath , baseShaValue , headShaValue ) {
143+ const baseRef = baseShaValue || 'HEAD~1' ;
144+ const headRef = headShaValue || 'HEAD' ;
145+
146+ try {
147+ const diffOutput = execFileSync ( 'git' , [ 'diff' , '--name-only' , baseRef , headRef ] , {
148+ cwd : repoRootPath ,
149+ encoding : 'utf8' ,
150+ } ) ;
151+ const files = diffOutput
152+ . split ( / \r ? \n / )
153+ . map ( ( line ) => line . trim ( ) )
154+ . filter ( Boolean ) ;
155+ if ( files . length > 0 ) {
156+ return files ;
157+ }
158+ } catch {
159+ // fall through to the working tree state below
160+ }
161+
162+ try {
163+ const statusOutput = execFileSync ( 'git' , [ 'status' , '--porcelain' ] , {
164+ cwd : repoRootPath ,
165+ encoding : 'utf8' ,
166+ } ) ;
167+ return statusOutput
168+ . split ( / \r ? \n / )
169+ . map ( ( line ) => line . trim ( ) )
170+ . filter ( Boolean )
171+ . map ( ( line ) => line . slice ( 3 ) . trim ( ) )
172+ . filter ( Boolean ) ;
173+ } catch {
174+ return [ ] ;
175+ }
176+ }
177+
178+ async function listChangesetFiles ( changesetDirectory ) {
179+ const entries = await readdir ( changesetDirectory , { withFileTypes : true } ) ;
180+ return entries
181+ . filter ( ( entry ) => entry . isFile ( ) && entry . name . endsWith ( '.md' ) )
182+ . map ( ( entry ) => entry . name )
183+ . sort ( ) ;
184+ }
185+
186+ async function getNewestChangesetFile ( changesetDirectory , existingFiles ) {
187+ const currentFiles = await listChangesetFiles ( changesetDirectory ) ;
188+ const newFiles = currentFiles . filter ( ( file ) => ! existingFiles . includes ( file ) ) ;
189+
190+ if ( newFiles . length === 0 ) {
191+ return null ;
192+ }
193+
194+ const filesWithStats = await Promise . all (
195+ newFiles . map ( async ( file ) => ( {
196+ name : file ,
197+ mtimeMs : ( await stat ( path . join ( changesetDirectory , file ) ) ) . mtimeMs ,
198+ } ) ) ,
199+ ) ;
200+
201+ filesWithStats . sort ( ( left , right ) => right . mtimeMs - left . mtimeMs ) ;
202+ return filesWithStats [ 0 ] ?. name ?? null ;
203+ }
0 commit comments