Skip to content

Commit 33fd2bc

Browse files
committed
Initial hack at implementing requireConfiguration
1 parent ebb725f commit 33fd2bc

File tree

2 files changed

+328
-2
lines changed

2 files changed

+328
-2
lines changed

src/biome.ts

Lines changed: 232 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Locator from "./locator";
1313
import Logger from "./logger";
1414
import Session from "./session";
1515
import type { State } from "./types";
16-
import { config, debounce } from "./utils";
16+
import { config, debounce, fileExists } from "./utils";
1717

1818
export 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

Comments
 (0)