@@ -13,7 +13,7 @@ import Locator from "./locator";
1313import Logger from "./logger" ;
1414import Session from "./session" ;
1515import type { State } from "./types" ;
16- import { config , debounce } from "./utils" ;
16+ import { config , debounce , fileExists } from "./utils" ;
1717
1818export default class Biome {
1919 /**
@@ -41,6 +41,11 @@ export default class Biome {
4141 */
4242 private _configWatcher : Disposable | undefined ;
4343
44+ /**
45+ * The configuration file watcher for this Biome instance.
46+ */
47+ private _configFileWatcher : FileSystemWatcher | undefined ;
48+
4449 /**
4550 * The locator responsible for finding the Biome binary to use.
4651 */
@@ -191,13 +196,31 @@ export default class Biome {
191196
192197 this . listenForLockfilesChanges ( ) ;
193198 this . listenForConfigChanges ( ) ;
199+ this . listenForConfigFileChanges ( ) ;
194200
195201 if ( ! this . enabled ) {
196202 this . logger . info ( "Biome is disabled." ) ;
197203 this . state = "disabled" ;
198204 return ;
199205 }
200206
207+ // Check if requireConfiguration is enabled and if so, check for config file
208+ if (
209+ config ( "requireConfiguration" , {
210+ scope : this . workspaceFolder ,
211+ default : false ,
212+ } )
213+ ) {
214+ const hasConfig = await this . hasConfigurationFile ( ) ;
215+ if ( ! hasConfig ) {
216+ this . logger . info (
217+ "Biome configuration file required but not found. Biome will not be activated." ,
218+ ) ;
219+ this . state = "disabled" ;
220+ return ;
221+ }
222+ }
223+
201224 this . state = "starting" ;
202225 const binary = await this . getBinary ( ) ;
203226
@@ -443,6 +466,10 @@ export default class Biome {
443466 this . _lockfileWatcher ?. dispose ( ) ;
444467 this . _lockfileWatcher = undefined ;
445468
469+ // Dispose of the config file watcher
470+ this . _configFileWatcher ?. dispose ( ) ;
471+ this . _configFileWatcher = undefined ;
472+
446473 // Nothing to cleanup if we're a global instance
447474 if ( this . isGlobal ) {
448475 return ;
@@ -468,4 +495,208 @@ export default class Biome {
468495 public onStateChange ( callback : ( state : State ) => void | Promise < void > ) : void {
469496 this . stateChangeCallbacks . push ( callback ) ;
470497 }
498+
499+ /**
500+ * Checks if a Biome configuration file exists in the workspace.
501+ */
502+ private async hasConfigurationFile ( ) : Promise < boolean > {
503+ if ( ! this . workspaceFolder ) {
504+ return false ;
505+ }
506+
507+ // Check for custom configuration path first
508+ const customPath = config ( "configurationPath" , {
509+ scope : this . workspaceFolder ,
510+ } ) ;
511+ if ( customPath ) {
512+ const customUri = Uri . joinPath ( this . workspaceFolder . uri , customPath ) ;
513+ return await fileExists ( customUri ) ;
514+ }
515+
516+ // Check for default configuration files at workspace root
517+ const defaultConfigFiles = [ "biome.json" , "biome.jsonc" ] ;
518+ for ( const configFile of defaultConfigFiles ) {
519+ const configUri = Uri . joinPath ( this . workspaceFolder . uri , configFile ) ;
520+ if ( await fileExists ( configUri ) ) {
521+ return true ;
522+ }
523+ }
524+
525+ // If requireConfiguration is enabled, we need to check if ANY subdirectory has a config
526+ // This allows for the monorepo case where config files are in subdirectories
527+ const hasAnyConfig = await this . findConfigurationFilesInWorkspace ( ) ;
528+ return hasAnyConfig . length > 0 ;
529+ }
530+
531+ /**
532+ * Finds all Biome configuration files in the workspace.
533+ */
534+ private async findConfigurationFilesInWorkspace ( ) : Promise < Uri [ ] > {
535+ if ( ! this . workspaceFolder ) {
536+ return [ ] ;
537+ }
538+
539+ const configFiles : Uri [ ] = [ ] ;
540+ const pattern = new RelativePattern (
541+ this . workspaceFolder ,
542+ "**/biome.{json,jsonc}" ,
543+ ) ;
544+
545+ try {
546+ const files = await workspace . findFiles ( pattern , "**/node_modules/**" ) ;
547+ configFiles . push ( ...files ) ;
548+ } catch ( error ) {
549+ this . logger . warn (
550+ `Failed to search for configuration files: ${ String ( error ) } ` ,
551+ ) ;
552+ }
553+
554+ return configFiles ;
555+ }
556+
557+ /**
558+ * Finds the nearest Biome configuration file for a given file path.
559+ */
560+ public async findNearestConfigurationFile (
561+ fileUri : Uri ,
562+ ) : Promise < Uri | undefined > {
563+ if ( ! this . workspaceFolder ) {
564+ return undefined ;
565+ }
566+
567+ // Check for custom configuration path first
568+ const customPath = config ( "configurationPath" , {
569+ scope : this . workspaceFolder ,
570+ } ) ;
571+ if ( customPath ) {
572+ const customUri = Uri . joinPath ( this . workspaceFolder . uri , customPath ) ;
573+ if ( await fileExists ( customUri ) ) {
574+ return customUri ;
575+ }
576+ }
577+
578+ // Walk up the directory tree looking for configuration files
579+ let currentDir = Uri . joinPath ( fileUri , ".." ) ;
580+ const workspaceRoot = this . workspaceFolder . uri . fsPath ;
581+ const defaultConfigFiles = [ "biome.json" , "biome.jsonc" ] ;
582+
583+ while ( currentDir . fsPath . startsWith ( workspaceRoot ) ) {
584+ for ( const configFile of defaultConfigFiles ) {
585+ const configUri = Uri . joinPath ( currentDir , configFile ) ;
586+ if ( await fileExists ( configUri ) ) {
587+ return configUri ;
588+ }
589+ }
590+
591+ const parentDir = Uri . joinPath ( currentDir , ".." ) ;
592+ if ( parentDir . fsPath === currentDir . fsPath ) {
593+ break ; // Reached root
594+ }
595+ currentDir = parentDir ;
596+ }
597+
598+ return undefined ;
599+ }
600+
601+ /**
602+ * Listens for configuration file changes to restart Biome when needed.
603+ */
604+ protected listenForConfigFileChanges ( ) {
605+ if ( this . isGlobal || ! this . workspaceFolder ) {
606+ return ;
607+ }
608+
609+ // Only listen if requireConfiguration is enabled
610+ if (
611+ ! config ( "requireConfiguration" , {
612+ scope : this . workspaceFolder ,
613+ default : false ,
614+ } )
615+ ) {
616+ return ;
617+ }
618+
619+ // Watch for biome.json and biome.jsonc files anywhere in the workspace
620+ const pattern = new RelativePattern (
621+ this . workspaceFolder ,
622+ "**/biome.{json,jsonc}" ,
623+ ) ;
624+
625+ this . _configFileWatcher = workspace . createFileSystemWatcher (
626+ pattern ,
627+ false ,
628+ false ,
629+ false ,
630+ ) ;
631+
632+ // Also watch for custom configuration path if specified
633+ const customPath = config ( "configurationPath" , {
634+ scope : this . workspaceFolder ,
635+ } ) ;
636+ if ( customPath ) {
637+ const customPattern = new RelativePattern (
638+ this . workspaceFolder ,
639+ customPath ,
640+ ) ;
641+ const customWatcher = workspace . createFileSystemWatcher ( customPattern ) ;
642+
643+ // Register the same handlers for custom config file
644+ customWatcher . onDidCreate (
645+ debounce ( async ( event ) => {
646+ this . logger . info ( `📄 Configuration file "${ event . fsPath } " created.` ) ;
647+ // If we were disabled due to missing config, restart
648+ if ( this . state === "disabled" ) {
649+ await this . restart ( ) ;
650+ }
651+ } ) ,
652+ ) ;
653+
654+ customWatcher . onDidDelete (
655+ debounce ( async ( event ) => {
656+ this . logger . info ( `📄 Configuration file "${ event . fsPath } " deleted.` ) ;
657+ // Restart to re-evaluate if Biome should be disabled
658+ await this . restart ( ) ;
659+ } ) ,
660+ ) ;
661+
662+ customWatcher . onDidChange (
663+ debounce ( async ( event ) => {
664+ this . logger . info ( `📄 Configuration file "${ event . fsPath } " changed.` ) ;
665+ // Configuration content changed, restart
666+ await this . restart ( ) ;
667+ } ) ,
668+ ) ;
669+
670+ this . extension . context . subscriptions . push ( customWatcher ) ;
671+ }
672+
673+ this . _configFileWatcher . onDidCreate (
674+ debounce ( async ( event ) => {
675+ this . logger . info ( `📄 Configuration file "${ event . fsPath } " created.` ) ;
676+ // If we were disabled due to missing config, restart
677+ if ( this . state === "disabled" ) {
678+ await this . restart ( ) ;
679+ }
680+ } ) ,
681+ ) ;
682+
683+ this . _configFileWatcher . onDidDelete (
684+ debounce ( async ( event ) => {
685+ this . logger . info ( `📄 Configuration file "${ event . fsPath } " deleted.` ) ;
686+ // Restart to re-evaluate if Biome should be disabled
687+ await this . restart ( ) ;
688+ } ) ,
689+ ) ;
690+
691+ this . _configFileWatcher . onDidChange (
692+ debounce ( async ( event ) => {
693+ this . logger . info ( `📄 Configuration file "${ event . fsPath } " changed.` ) ;
694+ // Configuration content changed, restart
695+ await this . restart ( ) ;
696+ } ) ,
697+ ) ;
698+
699+ this . logger . info ( "📄 Started listening for configuration file changes." ) ;
700+ this . extension . context . subscriptions . push ( this . _configFileWatcher ) ;
701+ }
471702}
0 commit comments