@@ -151,62 +151,77 @@ function addExamplesToSourceFile(
151151 const sourceContent = fs . readFileSync ( sourceFilePath , 'utf-8' ) ;
152152 const sourceFile = ts . createSourceFile ( sourceFilePath , sourceContent , ts . ScriptTarget . Latest , true ) ;
153153
154- let updatedContent = sourceContent ;
155-
156154 const classNode = sourceFile . statements . find (
157155 stmt => ts . isClassDeclaration ( stmt ) && stmt . name ?. text === className
158156 ) as ts . ClassDeclaration | undefined ;
159157
160- if ( classNode ) {
161- const classStart = classNode . getStart ( sourceFile ) ;
162- const classEnd = classNode . getEnd ( ) ;
163- const classText = classNode . getFullText ( sourceFile ) ;
158+ if ( ! classNode ) return ;
164159
165- // Extract annotation content
166- const existingCommentMatch = classText . match ( / \/ \* \* ( [ \s \S ] * ?) \* \/ / ) ;
167- if ( ! existingCommentMatch ) {
168- console . warn ( `No existing comment found for class: ${ className } ` ) ;
169- return ;
170- }
160+ // getFullStart() includes leading comments/trivia
161+ const fullStart = classNode . getFullStart ( ) ;
162+ const classStart = classNode . getStart ( sourceFile ) ; // usually points at `export class ...`
171163
172- const existingCommentInner = existingCommentMatch [ 1 ] . replace ( / ^ \n \* / , '' ) ; // Extract comment content (excluding `/**` and `*/`)
173-
174- // Replace @example part
175- const exampleSection = examples
176- . map (
177- example => {
178- const indentedBody = ' ' + example . body ;
179- return ` * @example\n * \/\/ ${ example . name } \n${ indentedBody
180- . split ( '\n' )
181- . map ( line => {
182- if ( line . trim ( ) === '' ) return ` *`
183- return ` * ${ line } ` } )
184- . join ( '\n' ) } `
185- }
186- )
187- . join ( '\n' ) + '\n ' ;
188-
189- let newComment = '' ;
190- if ( existingCommentInner . includes ( '@example' ) ) {
191- newComment = existingCommentInner . replace ( / \* @ e x a m p l e [ \s \S ] * ?(? = \* \/ | $ ) / g, exampleSection ) ;
192- } else {
193- newComment = existingCommentInner + `${ exampleSection . trimStart ( ) } ` ;
194- }
164+ // Search only in the leading trivia region for the nearest JSDoc block
165+ const leadingRegion = sourceContent . slice ( fullStart , classStart ) ;
166+ const commentStartInLeading = leadingRegion . lastIndexOf ( '/**' ) ;
167+ if ( commentStartInLeading === - 1 ) {
168+ console . warn ( `No existing comment found for class: ${ className } ` ) ;
169+ return ;
170+ }
171+ const commentStart = fullStart + commentStartInLeading ;
172+
173+ const commentEnd = sourceContent . indexOf ( '*/' , commentStart ) ;
174+ if ( commentEnd === - 1 || commentEnd > classStart ) {
175+ console . warn ( `Malformed comment for class: ${ className } ` ) ;
176+ return ;
177+ }
178+ const commentEndInclusive = commentEnd + 2 ;
179+
180+ const existingCommentBlock = sourceContent . slice ( commentStart , commentEndInclusive ) ;
181+ const existingCommentMatch = existingCommentBlock . match ( / \/ \* \* ( [ \s \S ] * ?) \* \/ / ) ;
182+ if ( ! existingCommentMatch ) {
183+ console . warn ( `No existing comment found for class: ${ className } ` ) ;
184+ return ;
185+ }
195186
187+ const existingCommentInner = existingCommentMatch [ 1 ] ; // keep inner as-is, including leading newline
196188
197- // Replace original content
198- updatedContent =
199- sourceContent . slice ( 0 , classStart - existingCommentInner . length - 3 ) +
200- newComment +
201- classText . slice ( existingCommentMatch [ 0 ] . length ) . trim ( ) +
202- sourceContent . slice ( classEnd ) ;
189+ const exampleSection =
190+ examples
191+ . map ( example => {
192+ const indentedBody = ' ' + example . body ;
193+ return ` * @example\n * \/\/ ${ example . name } \n${ indentedBody
194+ . split ( '\n' )
195+ . map ( line => ( line . trim ( ) === '' ? ` *` : ` * ${ line } ` ) )
196+ . join ( '\n' ) } `;
197+ } )
198+ . join ( '\n' ) + '\n' ;
199+
200+ let newInner : string ;
201+ if ( existingCommentInner . includes ( '@example' ) ) {
202+ // Replace from the first " * @example" to the end of the block (before */)
203+ newInner = existingCommentInner . replace ( / ^ \s * \* \s @ e x a m p l e [ \s \S ] * $ / m, exampleSection . trimEnd ( ) ) ;
204+ // Important: the regex above may not catch if @example is not at line start exactly with spaces.
205+ // A safer approach:
206+ newInner = existingCommentInner . replace ( / \* @ e x a m p l e [ \s \S ] * ?(? = \* \/ | $ ) / g, exampleSection ) ;
207+ } else {
208+ newInner = existingCommentInner . replace ( / \s * $ / , '\n' ) + exampleSection ;
203209 }
204210
211+ // Rebuild full comment block
212+ const newCommentBlock = `/**${ newInner . replace ( / ^ \n ? / , '\n' ) . replace ( / \s * $ / , '\n' ) } */` ;
213+
214+ const updatedContent =
215+ sourceContent . slice ( 0 , commentStart ) +
216+ newCommentBlock +
217+ sourceContent . slice ( commentEndInclusive ) ;
218+
205219 fs . writeFileSync ( sourceFilePath , updatedContent , 'utf-8' ) ;
206220 console . log ( `Updated examples in ${ sourceFilePath } ` ) ;
207221}
208222
209223
224+
210225/**
211226 * Process all test files and update README.md and source files.
212227 */
0 commit comments