1+ #!/usr/bin/env node
2+
3+ /**
4+ * Generate llms.txt for the Narou API wrapper library using TypeDoc JSON output
5+ * This script creates a comprehensive documentation file for LLMs
6+ */
7+
8+ import fs from 'fs/promises' ;
9+ import path from 'path' ;
10+ import { fileURLToPath } from 'url' ;
11+
12+ const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
13+ const rootDir = path . join ( __dirname , '..' ) ;
14+
15+ async function readFile ( filePath ) {
16+ try {
17+ return await fs . readFile ( filePath , 'utf8' ) ;
18+ } catch ( error ) {
19+ console . warn ( `Warning: Could not read ${ filePath } : ${ error . message } ` ) ;
20+ return null ;
21+ }
22+ }
23+
24+ function formatComment ( comment ) {
25+ if ( ! comment || ! comment . summary ) return '' ;
26+ return comment . summary . map ( part => part . text || '' ) . join ( '' ) ;
27+ }
28+
29+ function formatType ( type ) {
30+ if ( ! type ) return 'unknown' ;
31+
32+ switch ( type . type ) {
33+ case 'intrinsic' :
34+ return type . name ;
35+ case 'reference' :
36+ return type . name ;
37+ case 'array' :
38+ return `${ formatType ( type . elementType ) } []` ;
39+ case 'union' :
40+ return type . types . map ( formatType ) . join ( ' | ' ) ;
41+ case 'literal' :
42+ return JSON . stringify ( type . value ) ;
43+ default :
44+ return type . name || 'unknown' ;
45+ }
46+ }
47+
48+ function formatParameters ( parameters ) {
49+ if ( ! parameters || parameters . length === 0 ) return '' ;
50+
51+ return parameters . map ( param => {
52+ const optional = param . flags ?. isOptional ? '?' : '' ;
53+ const type = formatType ( param . type ) ;
54+ const comment = formatComment ( param . comment ) ;
55+ return `- \`${ param . name } ${ optional } : ${ type } \` - ${ comment } ` ;
56+ } ) . join ( '\n' ) ;
57+ }
58+
59+ function formatFunction ( func ) {
60+ const comment = formatComment ( func . comment ) ;
61+ const signatures = func . signatures || [ ] ;
62+
63+ let result = `### ${ func . name } \n\n` ;
64+ if ( comment ) {
65+ result += `${ comment } \n\n` ;
66+ }
67+
68+ signatures . forEach ( sig => {
69+ const params = formatParameters ( sig . parameters ) ;
70+ const returnType = formatType ( sig . type ) ;
71+
72+ result += `**Signature:** \`${ sig . name } (${ sig . parameters ?. map ( p => `${ p . name } ${ p . flags ?. isOptional ? '?' : '' } : ${ formatType ( p . type ) } ` ) . join ( ', ' ) || '' } ): ${ returnType } \`\n\n` ;
73+
74+ if ( params ) {
75+ result += `**Parameters:**\n${ params } \n\n` ;
76+ }
77+ } ) ;
78+
79+ return result ;
80+ }
81+
82+ function formatClass ( cls ) {
83+ const comment = formatComment ( cls . comment ) ;
84+ let result = `## ${ cls . name } \n\n` ;
85+
86+ if ( comment ) {
87+ result += `${ comment } \n\n` ;
88+ }
89+
90+ const methods = cls . children ?. filter ( child => child . kind === 2048 ) || [ ] ; // Method kind
91+ const properties = cls . children ?. filter ( child => child . kind === 1024 ) || [ ] ; // Property kind
92+
93+ if ( properties . length > 0 ) {
94+ result += `### Properties\n\n` ;
95+ properties . forEach ( prop => {
96+ const propComment = formatComment ( prop . comment ) ;
97+ const propType = formatType ( prop . type ) ;
98+ result += `- \`${ prop . name } : ${ propType } \` - ${ propComment } \n` ;
99+ } ) ;
100+ result += '\n' ;
101+ }
102+
103+ if ( methods . length > 0 ) {
104+ result += `### Methods\n\n` ;
105+ methods . forEach ( method => {
106+ result += formatFunction ( method ) ;
107+ } ) ;
108+ }
109+
110+ return result ;
111+ }
112+
113+ function formatInterface ( iface ) {
114+ const comment = formatComment ( iface . comment ) ;
115+ let result = `## ${ iface . name } \n\n` ;
116+
117+ if ( comment ) {
118+ result += `${ comment } \n\n` ;
119+ }
120+
121+ const properties = iface . children || [ ] ;
122+ if ( properties . length > 0 ) {
123+ result += `### Properties\n\n` ;
124+ properties . forEach ( prop => {
125+ const propComment = formatComment ( prop . comment ) ;
126+ const propType = formatType ( prop . type ) ;
127+ const optional = prop . flags ?. isOptional ? '?' : '' ;
128+ result += `- \`${ prop . name } ${ optional } : ${ propType } \` - ${ propComment } \n` ;
129+ } ) ;
130+ result += '\n' ;
131+ }
132+
133+ return result ;
134+ }
135+
136+ async function processTypeDocJson ( jsonPath ) {
137+ const jsonContent = await readFile ( jsonPath ) ;
138+ if ( ! jsonContent ) return '' ;
139+
140+ const apiDoc = JSON . parse ( jsonContent ) ;
141+ let result = '' ;
142+
143+ // Process main exports
144+ const children = apiDoc . children || [ ] ;
145+
146+ const classes = children . filter ( child => child . kind === 128 ) ; // Class kind
147+ const interfaces = children . filter ( child => child . kind === 256 ) ; // Interface kind
148+ const functions = children . filter ( child => child . kind === 64 ) ; // Function kind
149+ const enums = children . filter ( child => child . kind === 8 ) ; // Enum kind
150+ const types = children . filter ( child => child . kind === 4194304 ) ; // Type alias kind
151+
152+ if ( classes . length > 0 ) {
153+ result += `# Classes\n\n` ;
154+ classes . forEach ( cls => {
155+ result += formatClass ( cls ) ;
156+ } ) ;
157+ }
158+
159+ if ( interfaces . length > 0 ) {
160+ result += `# Interfaces\n\n` ;
161+ interfaces . forEach ( iface => {
162+ result += formatInterface ( iface ) ;
163+ } ) ;
164+ }
165+
166+ if ( functions . length > 0 ) {
167+ result += `# Functions\n\n` ;
168+ functions . forEach ( func => {
169+ result += formatFunction ( func ) ;
170+ } ) ;
171+ }
172+
173+ if ( enums . length > 0 ) {
174+ result += `# Enums\n\n` ;
175+ enums . forEach ( enumItem => {
176+ const comment = formatComment ( enumItem . comment ) ;
177+ result += `## ${ enumItem . name } \n\n` ;
178+ if ( comment ) {
179+ result += `${ comment } \n\n` ;
180+ }
181+
182+ const members = enumItem . children || [ ] ;
183+ members . forEach ( member => {
184+ const memberComment = formatComment ( member . comment ) ;
185+ result += `- \`${ member . name } \` - ${ memberComment } \n` ;
186+ } ) ;
187+ result += '\n' ;
188+ } ) ;
189+ }
190+
191+ if ( types . length > 0 ) {
192+ result += `# Type Aliases\n\n` ;
193+ types . forEach ( type => {
194+ const comment = formatComment ( type . comment ) ;
195+ const typeInfo = formatType ( type . type ) ;
196+ result += `## ${ type . name } \n\n` ;
197+ if ( comment ) {
198+ result += `${ comment } \n\n` ;
199+ }
200+ result += `\`\`\`typescript\ntype ${ type . name } = ${ typeInfo } \n\`\`\`\n\n` ;
201+ } ) ;
202+ }
203+
204+ return result ;
205+ }
206+
207+ async function generateLlmsTxt ( ) {
208+ console . log ( 'Generating llms.txt from TypeDoc JSON...' ) ;
209+
210+ const sections = [ ] ;
211+
212+ // Header
213+ sections . push ( `# Narou API Wrapper - LLM Documentation
214+
215+ This is a TypeScript library that provides a fluent interface wrapper for the Narou (小説家になろう) developer APIs.
216+ It supports both Node.js (using fetch) and browser environments (using JSONP).
217+
218+ Generated on: ${ new Date ( ) . toISOString ( ) }
219+
220+ ` ) ;
221+
222+ // README
223+ const readme = await readFile ( path . join ( rootDir , 'README.md' ) ) ;
224+ if ( readme ) {
225+ sections . push ( `## README
226+
227+ ${ readme }
228+
229+ ` ) ;
230+ }
231+
232+ // Package.json for dependency information
233+ const packageJson = await readFile ( path . join ( rootDir , 'package.json' ) ) ;
234+ if ( packageJson ) {
235+ sections . push ( `## Package Information
236+
237+ \`\`\`json
238+ ${ packageJson }
239+ \`\`\`
240+
241+ ` ) ;
242+ }
243+
244+ // TypeDoc API Documentation
245+ const apiJsonPath = path . join ( rootDir , 'docs' , 'api.json' ) ;
246+ const apiDocumentation = await processTypeDocJson ( apiJsonPath ) ;
247+ if ( apiDocumentation ) {
248+ sections . push ( `## API Documentation
249+
250+ ${ apiDocumentation }
251+
252+ ` ) ;
253+ }
254+
255+ // Supported APIs info
256+ sections . push ( `## Supported APIs
257+
258+ This library wraps the following Narou developer APIs:
259+
260+ - [なろう小説 API](https://dev.syosetu.com/man/api/) - Novel search API
261+ - [なろう小説ランキング API](https://dev.syosetu.com/man/rankapi/) - Novel ranking API
262+ - [なろう殿堂入り API](https://dev.syosetu.com/man/rankinapi/) - Ranking history API
263+ - [なろう R18 小説 API](https://dev.syosetu.com/xman/api/) - R18 novel search API
264+ - [なろうユーザ検索 API](https://dev.syosetu.com/man/userapi/) - User search API
265+
266+ ## Architecture
267+
268+ The library uses a builder pattern with fluent interfaces for API construction.
269+ It provides dual environment support through different entry points:
270+
271+ - \`src/index.ts\` - Node.js entry point using fetch
272+ - \`src/index.browser.ts\` - Browser entry point using JSONP
273+
274+ The core architecture includes:
275+ - Abstract base classes for builders
276+ - Environment-specific API implementations
277+ - Type-safe field selection with TypeScript generics
278+ - Comprehensive test coverage with MSW mocking
279+
280+ ## Usage Examples
281+
282+ \`\`\`typescript
283+ import { search, ranking, rankingHistory, searchR18 } from "narou";
284+
285+ // Basic search
286+ const results = await search("異世界")
287+ .genre(Genre.RenaiIsekai)
288+ .order(Order.FavoriteNovelCount)
289+ .execute();
290+
291+ // Ranking
292+ const ranking = await ranking()
293+ .date(new Date())
294+ .type(RankingType.Daily)
295+ .execute();
296+
297+ // R18 search
298+ const r18Results = await searchR18("word")
299+ .r18Site(R18Site.Nocturne)
300+ .execute();
301+ \`\`\`
302+
303+ ` ) ;
304+
305+ const llmsTxt = sections . join ( '' ) ;
306+
307+ // Write to docs directory
308+ const docsDir = path . join ( rootDir , 'docs' ) ;
309+ await fs . mkdir ( docsDir , { recursive : true } ) ;
310+ await fs . writeFile ( path . join ( docsDir , 'llms.txt' ) , llmsTxt ) ;
311+
312+ console . log ( 'Generated llms.txt successfully!' ) ;
313+ console . log ( `File size: ${ ( llmsTxt . length / 1024 ) . toFixed ( 1 ) } KB` ) ;
314+ }
315+
316+ generateLlmsTxt ( ) . catch ( console . error ) ;
0 commit comments