@@ -5,6 +5,7 @@ import { ensureDir, git, loadConfig, now, posix, projectPaths, sha256, writeJson
55
66const HARD_DIRS = new Set ( [ '.git' , '.docgen' , '.commandcode' , 'docs' , 'node_modules' , 'target' , 'build' , 'dist' , 'coverage' , 'vendor' ] ) ;
77const BINARY_EXTENSIONS = new Set ( [ '.png' , '.jpg' , '.jpeg' , '.gif' , '.webp' , '.bmp' , '.ico' , '.tif' , '.tiff' , '.avif' , '.heic' , '.psd' , '.mp3' , '.wav' , '.flac' , '.aac' , '.ogg' , '.mp4' , '.mov' , '.avi' , '.mkv' , '.webm' , '.pdf' , '.doc' , '.docx' , '.xls' , '.xlsx' , '.ppt' , '.pptx' , '.zip' , '.gz' , '.tgz' , '.bz2' , '.xz' , '.7z' , '.rar' , '.tar' , '.jar' , '.war' , '.ear' , '.class' , '.dll' , '.exe' , '.so' , '.dylib' , '.o' , '.a' , '.lib' , '.woff' , '.woff2' , '.ttf' , '.otf' , '.eot' , '.bin' , '.dat' , '.db' , '.sqlite' , '.sqlite3' , '.p12' , '.pfx' , '.jks' , '.keystore' , '.apk' , '.ipa' , '.iso' , '.dmg' , '.img' , '.wasm' , '.pyc' , '.pyo' ] ) ;
8+ const ruleCache = new Map ( ) ;
89
910function globRegex ( pattern ) {
1011 let source = posix ( pattern . trim ( ) ) . replace ( / ^ \. \/ / , '' ) ;
@@ -18,29 +19,35 @@ function globRegex(pattern) {
1819 else if ( c === '?' ) out += '[^/]' ;
1920 else out += c . replace ( / [ | \\ { } ( ) [ \] ^ $ + ? . ] / g, '\\$&' ) ;
2021 }
21- return new RegExp ( anchored ? `^${ out } $` : `(?:^|/)${ out } $` ) ;
22+ return new RegExp ( anchored ? `^${ out } (?:/.*)? $` : `(?:^|/)${ out } (?:/.*)? $` ) ;
2223}
2324
24- function loadDocgenIgnore ( root ) {
25- const file = path . join ( root , '.docgenignore' ) ;
26- if ( ! fs . existsSync ( file ) ) return [ ] ;
27- return fs . readFileSync ( file , 'utf8' ) . split ( / \r ? \n / ) . map ( ( line ) => line . trim ( ) ) . filter ( ( line ) => line && ! line . startsWith ( '#' ) ) . map ( ( line ) => ( { negate : line . startsWith ( '!' ) , regex : globRegex ( line . replace ( / ^ ! / , '' ) ) , raw : line } ) ) ;
25+ function loadRules ( file ) {
26+ if ( ruleCache . has ( file ) ) return ruleCache . get ( file ) ;
27+ const result = ! fs . existsSync ( file ) ? [ ] : fs . readFileSync ( file , 'utf8' ) . split ( / \r ? \n / ) . map ( ( line ) => line . trim ( ) ) . filter ( ( line ) => line && ! line . startsWith ( '#' ) ) . map ( ( line ) => ( { negate : line . startsWith ( '!' ) , regex : globRegex ( line . replace ( / ^ ! / , '' ) ) , raw : line } ) ) ;
28+ ruleCache . set ( file , result ) ; return result ;
2829}
2930
30- function ignoredByDocgen ( rel , rules ) {
31+ function ignoredByRules ( rel , rules ) {
3132 let ignored = false ;
3233 for ( const rule of rules ) if ( rule . regex . test ( rel ) ) ignored = ! rule . negate ;
3334 return ignored ;
3435}
3536
37+ function ignoredByFallbackGitignore ( root , rel ) {
38+ const parts = rel . split ( '/' ) ; let ignored = false ;
39+ for ( let depth = 0 ; depth < parts . length ; depth ++ ) {
40+ const base = parts . slice ( 0 , depth ) . join ( '/' ) ; const local = parts . slice ( depth ) . join ( '/' ) ;
41+ for ( const rule of loadRules ( path . join ( root , base , '.gitignore' ) ) ) if ( rule . regex . test ( local ) ) ignored = ! rule . negate ;
42+ }
43+ return ignored ;
44+ }
45+
3646function walk ( root ) {
3747 const out = [ ] ; const stack = [ root ] ;
3848 while ( stack . length ) {
3949 const dir = stack . pop ( ) ;
4050 for ( const entry of fs . readdirSync ( dir , { withFileTypes : true } ) ) {
41- if ( entry . name . startsWith ( '.' ) && entry . name !== '.env' && entry . name !== '.env.example' ) {
42- if ( entry . isDirectory ( ) || [ '.gitignore' , '.docgenignore' ] . includes ( entry . name ) === false ) continue ;
43- }
4451 const full = path . join ( dir , entry . name ) ; const rel = posix ( path . relative ( root , full ) ) ;
4552 if ( entry . isDirectory ( ) ) { if ( ! HARD_DIRS . has ( entry . name ) ) stack . push ( full ) ; }
4653 else out . push ( rel ) ;
@@ -51,10 +58,10 @@ function walk(root) {
5158
5259function candidatePaths ( root ) {
5360 const insideGit = git ( root , [ 'rev-parse' , '--is-inside-work-tree' ] ) === 'true' ;
54- if ( ! insideGit ) return walk ( root ) ;
61+ if ( ! insideGit ) return { paths : walk ( root ) , insideGit : false } ;
5562 const result = spawnSync ( 'git' , [ '-C' , root , 'ls-files' , '-co' , '--exclude-standard' , '-z' ] , { encoding : 'buffer' , stdio : [ 'ignore' , 'pipe' , 'ignore' ] } ) ;
56- if ( result . status !== 0 ) return walk ( root ) ;
57- return result . stdout . toString ( 'utf8' ) . split ( '\0' ) . filter ( Boolean ) . map ( posix ) . sort ( ) ;
63+ if ( result . status !== 0 ) return { paths : walk ( root ) , insideGit : false } ;
64+ return { paths : result . stdout . toString ( 'utf8' ) . split ( '\0' ) . filter ( Boolean ) . map ( posix ) . sort ( ) , insideGit : true } ;
5865}
5966
6067function binaryMagic ( buffer ) {
@@ -80,17 +87,14 @@ export function classifyFile(root, rel, config = loadConfig(root)) {
8087 return { included : true , reason : null , size : stat . size , extension : ext || '<none>' } ;
8188}
8289
83- export function buildInventory ( root , { force = false } = { } ) {
84- const paths = projectPaths ( root ) ; const config = loadConfig ( root ) ;
85- if ( ! force && fs . existsSync ( paths . inventory ) ) {
86- const current = JSON . parse ( fs . readFileSync ( paths . inventory , 'utf8' ) ) ;
87- if ( current . schemaVersion === '2.0' ) return current ;
88- }
89- const rules = loadDocgenIgnore ( root ) ; const included = [ ] ; const excluded = [ ] ;
90- for ( const rel of candidatePaths ( root ) ) {
90+ export function buildInventory ( root ) {
91+ ruleCache . clear ( ) ;
92+ const paths = projectPaths ( root ) ; const config = loadConfig ( root ) ; const candidates = candidatePaths ( root ) ; const docgenRules = loadRules ( path . join ( root , '.docgenignore' ) ) ; const included = [ ] ; const excluded = [ ] ;
93+ for ( const rel of candidates . paths ) {
9194 const top = rel . split ( '/' ) [ 0 ] ;
9295 if ( HARD_DIRS . has ( top ) ) { excluded . push ( { path : rel , reason : 'hard-exclusion' } ) ; continue ; }
93- if ( ignoredByDocgen ( rel , rules ) ) { excluded . push ( { path : rel , reason : '.docgenignore' } ) ; continue ; }
96+ if ( ! candidates . insideGit && config . ignore ?. useGitignore !== false && ignoredByFallbackGitignore ( root , rel ) ) { excluded . push ( { path : rel , reason : '.gitignore' } ) ; continue ; }
97+ if ( config . ignore ?. useDocgenignore !== false && ignoredByRules ( rel , docgenRules ) ) { excluded . push ( { path : rel , reason : '.docgenignore' } ) ; continue ; }
9498 const result = classifyFile ( root , rel , config ) ;
9599 if ( ! result . included ) { excluded . push ( { path : rel , reason : result . reason , size : result . size ?? 0 } ) ; continue ; }
96100 const content = fs . readFileSync ( path . join ( root , rel ) ) ; included . push ( { path : rel , size : result . size , extension : result . extension , hash : sha256 ( content ) } ) ;
0 commit comments