11import * as fs from 'fs/promises' ;
22import * as path from 'path' ;
3+ import * as yaml from 'js-yaml' ;
34
45import type { ContentDiagnostic } from '@gitbook/content-engine' ;
56
67const IGNORED_DIRECTORIES = new Set ( [ 'node_modules' , '.git' ] ) ;
78
9+ interface GitBookRepoConfig {
10+ /** Absolute path of the GitBook content root. */
11+ root : string ;
12+ /** File name of the table of contents (navigation, not a page). */
13+ summaryName : string ;
14+ }
15+
16+ /**
17+ * Read the repository's .gitbook.yaml, when present: it defines the content
18+ * root and the structure files, exactly as Git Sync interprets them.
19+ */
20+ async function loadGitBookRepoConfig ( cwd : string ) : Promise < GitBookRepoConfig | null > {
21+ for ( const name of [ '.gitbook.yaml' , '.gitbook.yml' ] ) {
22+ try {
23+ const raw = await fs . readFile ( path . join ( cwd , name ) , 'utf8' ) ;
24+ const parsed = yaml . load ( raw ) as {
25+ root ?: string ;
26+ structure ?: { summary ?: string } ;
27+ } | null ;
28+ return {
29+ root : path . resolve ( cwd , parsed ?. root ?? '.' ) ,
30+ summaryName : path . basename ( parsed ?. structure ?. summary ?? 'SUMMARY.md' ) ,
31+ } ;
32+ } catch {
33+ // Try the next name.
34+ }
35+ }
36+ return null ;
37+ }
38+
839const DIFF_PREVIEW_LINES = 12 ;
940
1041const useColor = process . stdout . isTTY === true ;
@@ -24,12 +55,13 @@ async function loadEngine() {
2455
2556/**
2657 * Expand a list of files and directories into the markdown files they contain.
27- * `excludeSummary` skips SUMMARY.md files: GitBook parses them as navigation,
28- * not as pages, so page-level lint/format do not apply to them.
58+ * `excludeSummaryName` skips the table of contents file (SUMMARY.md by
59+ * default): GitBook parses it as navigation, not as a page, so page-level
60+ * lint/format do not apply to it. Explicitly passed files are always kept.
2961 */
3062async function collectMarkdownFiles (
3163 inputs : string [ ] ,
32- options : { excludeSummary ?: boolean } = { } ,
64+ options : { excludeSummaryName ?: string } = { } ,
3365) : Promise < string [ ] > {
3466 const files : string [ ] = [ ] ;
3567
@@ -45,7 +77,7 @@ async function collectMarkdownFiles(
4577 await visit ( path . join ( input , entry . name ) ) ;
4678 } else if (
4779 entry . name . endsWith ( '.md' ) &&
48- ! ( options . excludeSummary && entry . name === 'SUMMARY.md' )
80+ entry . name !== options . excludeSummaryName
4981 ) {
5082 files . push ( path . join ( input , entry . name ) ) ;
5183 }
@@ -62,6 +94,20 @@ async function collectMarkdownFiles(
6294 return files . sort ( ) ;
6395}
6496
97+ /**
98+ * Resolve the files to check for page-level commands (lint, format): when no
99+ * input is given and the repository has a .gitbook.yaml, the configured
100+ * content root is used, so files outside it (e.g. the GitHub-facing README)
101+ * are not treated as GitBook content.
102+ */
103+ async function collectPageFiles ( inputs : string [ ] ) : Promise < string [ ] > {
104+ const config = await loadGitBookRepoConfig ( process . cwd ( ) ) ;
105+ const defaults = config ? [ config . root ] : [ '.' ] ;
106+ return await collectMarkdownFiles ( inputs . length > 0 ? inputs : defaults , {
107+ excludeSummaryName : config ?. summaryName ?? 'SUMMARY.md' ,
108+ } ) ;
109+ }
110+
65111function printDiff ( diagnostic : ContentDiagnostic ) {
66112 const lines : string [ ] = [ ] ;
67113 if ( diagnostic . actual ) {
@@ -103,9 +149,7 @@ export async function lintContentFiles(
103149) : Promise < number > {
104150 const { lintContent } = await loadEngine ( ) ;
105151
106- const files = await collectMarkdownFiles ( inputs . length > 0 ? inputs : [ '.' ] , {
107- excludeSummary : true ,
108- } ) ;
152+ const files = await collectPageFiles ( inputs ) ;
109153 if ( files . length === 0 ) {
110154 console . log ( 'No markdown files found.' ) ;
111155 return 0 ;
@@ -145,9 +189,12 @@ export async function brokenLinksContentFiles(
145189) : Promise < number > {
146190 const { checkBrokenLinks } = await loadEngine ( ) ;
147191
148- const root = process . cwd ( ) ;
149- // SUMMARY.md is included here: it is the navigation file, its links matter.
150- const files = await collectMarkdownFiles ( inputs . length > 0 ? inputs : [ '.' ] ) ;
192+ // Links resolve against the GitBook content root when the repository
193+ // declares one. The table of contents is included: it is the navigation
194+ // file, its links matter.
195+ const config = await loadGitBookRepoConfig ( process . cwd ( ) ) ;
196+ const root = config ?. root ?? process . cwd ( ) ;
197+ const files = await collectMarkdownFiles ( inputs . length > 0 ? inputs : [ root ] ) ;
151198 if ( files . length === 0 ) {
152199 console . log ( 'No markdown files found.' ) ;
153200 return 0 ;
@@ -203,9 +250,7 @@ export async function formatContentFiles(
203250) : Promise < number > {
204251 const { formatContent } = await loadEngine ( ) ;
205252
206- const files = await collectMarkdownFiles ( inputs . length > 0 ? inputs : [ '.' ] , {
207- excludeSummary : true ,
208- } ) ;
253+ const files = await collectPageFiles ( inputs ) ;
209254 if ( files . length === 0 ) {
210255 console . log ( 'No markdown files found.' ) ;
211256 return 0 ;
0 commit comments