1- /* eslint-disable n/prefer-global/process, unicorn/no-process-exit */
1+ /* eslint-disable n/prefer-global/process, unicorn/no-process-exit, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument */
22import { readFileSync , writeFileSync } from 'node:fs' ;
33import { execSync } from 'node:child_process' ;
4+ import { Project , type JSDocableNode } from 'ts-morph' ;
45// Import index.ts to populate the test data via side effect
56// eslint-disable-next-line import-x/no-unassigned-import
67import './index.ts' ;
@@ -17,16 +18,64 @@ if (dtsContent.includes(marker)) {
1718 process . exit ( 1 ) ;
1819}
1920
20- // Process each exported function
21- const lines = dtsContent . split ( '\n' ) ;
22- const outputLines : string [ ] = [ ] ;
21+ // Create a ts-morph project and load the file
22+ const project = new Project ( ) ;
23+ const sourceFile = project . createSourceFile ( dtsPath , dtsContent , { overwrite : true } ) ;
24+
2325let examplesAdded = 0 ;
2426
25- for ( const line of lines ) {
26- // Check if this is a function declaration
27- const match = / ^ e x p o r t d e c l a r e c o n s t ( \w + ) : / . exec ( line ) ;
28- if ( match ) {
29- const functionName = match [ 1 ] ;
27+ /**
28+ * Add example URLs to a JSDocable node (e.g., variable statement or type alias)
29+ */
30+ function addExamplesToNode ( node : JSDocableNode , urlExamples : string [ ] ) : void {
31+ const jsDoc = node . getJsDocs ( ) . at ( 0 ) ;
32+
33+ if ( jsDoc ) {
34+ // Add @example tags to existing JSDoc
35+ const existingTags = jsDoc . getTags ( ) ;
36+ const description = jsDoc . getDescription ( ) . trim ( ) ;
37+
38+ // Build new JSDoc content
39+ const newJsDocLines : string [ ] = [ ] ;
40+ if ( description ) {
41+ newJsDocLines . push ( description ) ;
42+ }
43+
44+ // Add existing tags (that aren't @example tags)
45+ for ( const tag of existingTags ) {
46+ if ( tag . getTagName ( ) !== 'example' ) {
47+ newJsDocLines . push ( tag . getText ( ) ) ;
48+ }
49+ }
50+
51+ // Add new @example tags
52+ for ( const url of urlExamples ) {
53+ newJsDocLines . push ( `@example ${ url } ` ) ;
54+ }
55+
56+ // Replace the JSDoc
57+ jsDoc . remove ( ) ;
58+ node . addJsDoc ( newJsDocLines . join ( '\n' ) ) ;
59+ } else {
60+ // Create new JSDoc with examples
61+ const jsDocLines : string [ ] = [ ] ;
62+ for ( const url of urlExamples ) {
63+ jsDocLines . push ( `@example ${ url } ` ) ;
64+ }
65+
66+ node . addJsDoc ( jsDocLines . join ( '\n' ) ) ;
67+ }
68+ }
69+
70+ // Process each exported variable declaration (these are the function declarations)
71+ for ( const statement of sourceFile . getVariableStatements ( ) ) {
72+ // Only process exported statements
73+ if ( ! statement . isExported ( ) ) {
74+ continue ;
75+ }
76+
77+ for ( const declaration of statement . getDeclarations ( ) ) {
78+ const functionName = declaration . getName ( ) ;
3079
3180 // Get the tests/examples for this function
3281 const examples = getTests ( functionName ) ;
@@ -37,102 +86,44 @@ for (const line of lines) {
3786 const urlExamples = examples . filter ( ( url : string ) => url . startsWith ( 'http' ) ) ;
3887
3988 if ( urlExamples . length > 0 ) {
40- // Check if there's an existing JSDoc block immediately before this line
41- let jsDocumentEndIndex = - 1 ;
42- let jsDocumentStartIndex = - 1 ;
43- let isSingleLineJsDocument = false ;
44-
45- // Look backwards from outputLines to find JSDoc
46- for ( let index = outputLines . length - 1 ; index >= 0 ; index -- ) {
47- const previousLine = outputLines [ index ] ;
48- const trimmed = previousLine . trim ( ) ;
49-
50- if ( trimmed === '' ) {
51- continue ; // Skip empty lines
52- }
53-
54- // Check for single-line JSDoc: /** ... */
55- if ( trimmed . startsWith ( '/**' ) && trimmed . endsWith ( '*/' ) && trimmed . length > 5 ) {
56- jsDocumentStartIndex = index ;
57- jsDocumentEndIndex = index ;
58- isSingleLineJsDocument = true ;
59- break ;
60- }
61-
62- // Check for multi-line JSDoc ending
63- if ( trimmed === '*/' ) {
64- jsDocumentEndIndex = index ;
65- // Now find the start of this JSDoc
66- for ( let k = index - 1 ; k >= 0 ; k -- ) {
67- if ( outputLines [ k ] . trim ( ) . startsWith ( '/**' ) ) {
68- jsDocumentStartIndex = k ;
69- break ;
70- }
71- }
72-
73- break ;
74- }
75-
76- // If we hit a non-JSDoc line, there's no JSDoc block
77- break ;
78- }
79-
80- if ( jsDocumentStartIndex >= 0 && jsDocumentEndIndex >= 0 ) {
81- // Extend existing JSDoc block
82- if ( isSingleLineJsDocument ) {
83- // Convert single-line to multi-line and add examples
84- const singleLineContent = outputLines [ jsDocumentStartIndex ] ;
85- // Extract the comment text without /** and */
86- const commentText = singleLineContent . trim ( ) . slice ( 3 , - 2 ) . trim ( ) ;
87-
88- // Replace the single line with multi-line format
89- outputLines [ jsDocumentStartIndex ] = '/**' ;
90- if ( commentText ) {
91- outputLines . splice ( jsDocumentStartIndex + 1 , 0 , ` * ${ commentText } ` ) ;
92- }
93-
94- // Add examples after the existing content
95- const insertIndex = jsDocumentStartIndex + ( commentText ? 2 : 1 ) ;
96- for ( const url of urlExamples ) {
97- outputLines . splice ( insertIndex + urlExamples . indexOf ( url ) , 0 , ` * @example ${ url } ` ) ;
98- }
99-
100- outputLines . splice ( insertIndex + urlExamples . length , 0 , ' */' ) ;
101- examplesAdded += urlExamples . length ;
102- } else {
103- // Insert @example lines before the closing */
104- for ( const url of urlExamples ) {
105- outputLines . splice ( jsDocumentEndIndex , 0 , ` * @example ${ url } ` ) ;
106- }
107-
108- examplesAdded += urlExamples . length ;
109- }
110- } else {
111- // Add new JSDoc comment with examples before the declaration
112- outputLines . push ( '/**' ) ;
113- for ( const url of urlExamples ) {
114- outputLines . push ( ` * @example ${ url } ` ) ;
115- }
116-
117- outputLines . push ( ' */' ) ;
118- examplesAdded += urlExamples . length ;
119- }
89+ addExamplesToNode ( statement , urlExamples ) ;
90+ examplesAdded += urlExamples . length ;
12091 }
12192 }
12293 }
123-
124- outputLines . push ( line ) ;
12594}
12695
127- // Add marker at the beginning
128- const finalContent = `${ marker } \n${ outputLines . join ( '\n' ) } ` ;
96+ // Also process exported type aliases (like RepoExplorerInfo)
97+ for ( const typeAlias of sourceFile . getTypeAliases ( ) ) {
98+ if ( ! typeAlias . isExported ( ) ) {
99+ continue ;
100+ }
101+
102+ const typeName = typeAlias . getName ( ) ;
103+
104+ // Get the tests/examples for this type (unlikely but keeping consistency)
105+ const examples = getTests ( typeName ) ;
106+
107+ if ( examples && examples . length > 0 && examples [ 0 ] !== 'combinedTestOnly' ) {
108+ const urlExamples = examples . filter ( ( url : string ) => url . startsWith ( 'http' ) ) ;
109+
110+ if ( urlExamples . length > 0 ) {
111+ addExamplesToNode ( typeAlias , urlExamples ) ;
112+ examplesAdded += urlExamples . length ;
113+ }
114+ }
115+ }
129116
130117// Validate that we added some examples
131118if ( examplesAdded === 0 ) {
132119 console . error ( '❌ Error: No examples were added. This likely indicates a problem with the script.' ) ;
133120 process . exit ( 1 ) ;
134121}
135122
123+ // Get the modified content and add marker
124+ const modifiedContent = sourceFile . getFullText ( ) ;
125+ const finalContent = `${ marker } \n${ modifiedContent } ` ;
126+
136127// Write the modified content back
137128writeFileSync ( dtsPath , finalContent , 'utf8' ) ;
138129
0 commit comments