11const fs = require ( 'fs' ) ;
22const path = require ( 'path' ) ;
33
4- function getPositionForItem ( itemPath ) {
5- const stat = fs . statSync ( itemPath ) ;
6- if ( stat . isDirectory ( ) ) {
7- const categoryFile = path . join ( itemPath , '_category_.json' ) ;
8- if ( fs . existsSync ( categoryFile ) ) {
9- const category = JSON . parse ( fs . readFileSync ( categoryFile , 'utf8' ) ) ;
10- return category . position || Infinity ;
11- }
12- return Infinity ;
13- } else if ( itemPath . endsWith ( '.md' ) ) {
14- const { frontmatter } = extractFrontmatterAndContent ( itemPath ) ;
15- return getSidebarPosition ( frontmatter ) ;
16- }
17- return Infinity ;
4+ const DOC_EXTS = new Set ( [ '.md' , '.mdx' ] ) ;
5+
6+ function ensureDir ( dir ) {
7+ fs . mkdirSync ( dir , { recursive : true } ) ;
8+ }
9+
10+ function isDocFile ( p ) {
11+ return DOC_EXTS . has ( path . extname ( p ) ) ;
1812}
1913
2014function extractFrontmatterAndContent ( filePath ) {
21- const content = fs . readFileSync ( filePath , 'utf8' ) ;
22- const frontmatterMatch = content . match ( / ^ - - - \n ( [ \s \S ] * ?) \n - - - \n ( [ \s \S ] * ) $ / ) ;
23- if ( ! frontmatterMatch ) {
24- return { frontmatter : '' , content : content } ;
25- }
26- const frontmatter = frontmatterMatch [ 1 ] ;
27- const body = frontmatterMatch [ 2 ] ;
28- return { frontmatter, content : body } ;
15+ const raw = fs . readFileSync ( filePath , 'utf8' ) . replace ( / ^ \uFEFF / , '' ) ; // strip BOM
16+ // more tolerant: frontmatter is optional; allow \r\n
17+ const match = raw . match ( / ^ - - - \r ? \n ( [ \s \S ] * ?) \r ? \n - - - \r ? \n ( [ \s \S ] * ) $ / ) ;
18+ if ( ! match ) return { frontmatter : '' , content : raw } ;
19+ return { frontmatter : match [ 1 ] , content : match [ 2 ] } ;
2920}
3021
3122function getSidebarPosition ( frontmatter ) {
32- const match = frontmatter . match ( / s i d e b a r _ p o s i t i o n : \s * ( \d + ) / ) ;
33- return match ? parseInt ( match [ 1 ] , 10 ) : Infinity ;
23+ const m = frontmatter . match ( / s i d e b a r _ p o s i t i o n : \s * ( \d + ) / ) ;
24+ return m ? parseInt ( m [ 1 ] , 10 ) : Infinity ;
3425}
3526
36- function collectContent ( dirPath ) {
37- const items = fs . readdirSync ( dirPath ) . map ( item => path . join ( dirPath , item ) ) ;
27+ function getCategoryPosition ( dirPath ) {
28+ const categoryFile = path . join ( dirPath , '_category_.json' ) ;
29+ if ( ! fs . existsSync ( categoryFile ) ) return Infinity ;
30+ try {
31+ const category = JSON . parse ( fs . readFileSync ( categoryFile , 'utf8' ) ) ;
32+ return Number . isFinite ( category . position ) ? category . position : Infinity ;
33+ } catch {
34+ return Infinity ;
35+ }
36+ }
3837
39- // Filter to directories and .md files
40- const validItems = items . filter ( item => {
41- const stat = fs . statSync ( item ) ;
42- return stat . isDirectory ( ) || item . endsWith ( '.md' ) ;
38+ function getPositionForItem ( itemPath ) {
39+ const stat = fs . statSync ( itemPath ) ;
40+ if ( stat . isDirectory ( ) ) return getCategoryPosition ( itemPath ) ;
41+ if ( isDocFile ( itemPath ) ) {
42+ const { frontmatter } = extractFrontmatterAndContent ( itemPath ) ;
43+ return getSidebarPosition ( frontmatter ) ;
44+ }
45+ return Infinity ;
46+ }
47+
48+ function collectContent ( rootDir , dirPath = rootDir ) {
49+ const items = fs . readdirSync ( dirPath ) . map ( name => path . join ( dirPath , name ) ) ;
50+
51+ const validItems = items . filter ( p => {
52+ const stat = fs . statSync ( p ) ;
53+ return stat . isDirectory ( ) || isDocFile ( p ) ;
4354 } ) ;
4455
45- // Sort by position
46- validItems . sort ( ( a , b ) => getPositionForItem ( a ) - getPositionForItem ( b ) ) ;
56+ validItems . sort ( ( a , b ) => {
57+ const pa = getPositionForItem ( a ) ;
58+ const pb = getPositionForItem ( b ) ;
59+ if ( pa !== pb ) return pa - pb ;
60+ // deterministic tie-breaker
61+ return a . localeCompare ( b ) ;
62+ } ) ;
4763
4864 let combined = '' ;
65+
4966 for ( const item of validItems ) {
5067 const stat = fs . statSync ( item ) ;
68+
5169 if ( stat . isDirectory ( ) ) {
52- combined += collectContent ( item ) ;
53- } else if ( item . endsWith ( '.md' ) ) {
70+ combined += collectContent ( rootDir , item ) ;
71+ continue ;
72+ }
73+
74+ if ( isDocFile ( item ) ) {
5475 const { content } = extractFrontmatterAndContent ( item ) ;
55- combined += content + '\n\n' ;
76+ const rel = path . relative ( rootDir , item ) . replace ( / \\ / g, '/' ) ;
77+ combined += `\n<!-- SOURCE: ${ rel } -->\n` ;
78+ combined += content . trimEnd ( ) + '\n' ;
5679 }
5780 }
5881
@@ -64,11 +87,21 @@ function main() {
6487 const buildDir = path . join ( __dirname , '..' , 'build' ) ;
6588 const outputFile = path . join ( buildDir , 'llms.txt' ) ;
6689
67- const combinedContent = collectContent ( docsDir ) ;
90+ ensureDir ( buildDir ) ;
91+
92+ const header = `# Durable Workflow – LLM Documentation Bundle
93+ # Source: https://durable-workflow.com
94+ # Generated from /docs
95+ # Purpose: AI-assisted reasoning, code generation, and Q&A
96+ # Canonical URL: https://durable-workflow.com/llms.txt
97+
98+ ` ;
6899
69- fs . writeFileSync ( outputFile , combinedContent , 'utf8' ) ;
100+ const combinedContent = collectContent ( docsDir ) . trimStart ( ) + '\n' ;
101+ const fullContent = header + combinedContent ;
102+ fs . writeFileSync ( outputFile , fullContent , 'utf8' ) ;
70103
71- console . log ( 'llms.txt generated successfully' ) ;
104+ console . log ( 'llms.txt generated successfully:' , outputFile ) ;
72105}
73106
74- main ( ) ;
107+ main ( ) ;
0 commit comments