From 8c4f8452122425e45965a37cf2b6c724226273c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 13:25:18 +0200 Subject: [PATCH 01/24] Config: Remove .herb.yaml rejection Co-Authored-By: Claude Opus 4.6 (1M context) --- javascript/packages/config/src/config.ts | 31 ------------------------ 1 file changed, 31 deletions(-) diff --git a/javascript/packages/config/src/config.ts b/javascript/packages/config/src/config.ts index 23f9ea0f2..2771ac64a 100644 --- a/javascript/packages/config/src/config.ts +++ b/javascript/packages/config/src/config.ts @@ -1004,19 +1004,6 @@ export class Config { silent: boolean, version: string ): Promise { - const yamlPath = path.join(projectRoot, '.herb.yaml') - - try { - await fs.access(yamlPath) - - console.error(`\n✗ Found \`.herb.yaml\` file at ${yamlPath}`) - console.error(` Please rename it to \`.herb.yml\`\n`) - - process.exit(1) - } catch { - // File doesn't exist - } - const configPath = this.configPathFromProjectPath(projectRoot) try { @@ -1052,24 +1039,6 @@ export class Config { const version = options?.version const projectPath = options?.projectPath - if (projectPath) { - try { - const yamlPath = path.join(projectPath, '.herb.yaml') - await fs.access(yamlPath) - - errors.push({ - message: 'Found .herb.yaml file. Please rename to .herb.yml', - path: [], - code: 'wrong_file_extension', - severity: 'warning', - line: 0, - column: 0 - }) - } catch { - // .herb.yaml doesn't exist - } - } - let parsed: any try { From b5e04f9ba5fbb882899b82d9fc1b2ee15ee58412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 13:32:04 +0200 Subject: [PATCH 02/24] Config: Introduce configPaths array and defaultConfigPath Co-Authored-By: Claude Opus 4.6 (1M context) --- javascript/packages/config/src/config.ts | 16 +++++++++------- .../packages/config/test/config.test.ts | 19 ++++++++++++++----- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/javascript/packages/config/src/config.ts b/javascript/packages/config/src/config.ts index 2771ac64a..ce51233f9 100644 --- a/javascript/packages/config/src/config.ts +++ b/javascript/packages/config/src/config.ts @@ -106,11 +106,13 @@ export type FromObjectOptions = { } export class Config { - static configPath = ".herb.yml" + static configPaths = [".herb.yml", ".herb.yaml"] + static defaultConfigPath = this.configPaths[0] private static PROJECT_INDICATORS = [ '.git', '.herb', + '.herb.yaml', '.herb.yml', 'Gemfile', 'package.json', @@ -447,7 +449,7 @@ export class Config { } static configPathFromProjectPath(projectPath: string) { - return path.join(projectPath, this.configPath) + return path.join(projectPath, this.defaultConfigPath) } /** @@ -469,7 +471,7 @@ export class Config { try { let configPath: string - if (pathOrFile.endsWith(this.configPath)) { + if (pathOrFile.endsWith(this.defaultConfigPath)) { configPath = pathOrFile } else { configPath = this.configPathFromProjectPath(pathOrFile) @@ -520,7 +522,7 @@ export class Config { let firstIndicatorMatch: string | undefined while (true) { - const configPath = path.join(currentPath, this.configPath) + const configPath = path.join(currentPath, this.defaultConfigPath) try { fsSync.accessSync(configPath) @@ -561,7 +563,7 @@ export class Config { * @returns string - The raw YAML content */ static readRawYaml(pathOrFile: string): string { - const configPath = pathOrFile.endsWith(this.configPath) + const configPath = pathOrFile.endsWith(this.defaultConfigPath) ? pathOrFile : this.configPathFromProjectPath(pathOrFile) @@ -589,7 +591,7 @@ export class Config { const { silent = false, version = DEFAULT_VERSION, createIfMissing = false, exitOnError = false } = options try { - if (pathOrFile.endsWith(this.configPath)) { + if (pathOrFile.endsWith(this.defaultConfigPath)) { return await this.loadFromExplicitPath(pathOrFile, silent, version, exitOnError) } @@ -886,7 +888,7 @@ export class Config { let firstIndicatorMatch: string | undefined while (true) { - const configPath = path.join(currentPath, this.configPath) + const configPath = path.join(currentPath, this.defaultConfigPath) try { await fs.access(configPath) diff --git a/javascript/packages/config/test/config.test.ts b/javascript/packages/config/test/config.test.ts index be5ca5d9c..07636341b 100644 --- a/javascript/packages/config/test/config.test.ts +++ b/javascript/packages/config/test/config.test.ts @@ -41,21 +41,30 @@ describe("@herb-tools/config", () => { test("sets correct config path", () => { const config = new Config(testDir, { version: "0.9.7" }) - expect(config.path).toBe(join(testDir, ".herb.yml")) + expect(config.path).toBe(join(testDir, Config.defaultConfigPath)) + }) + + test("defaultConfigPath is the first entry in configPaths", () => { + expect(Config.defaultConfigPath).toBe(Config.configPaths[0]) + }) + + test("configPaths includes both .yaml and .yml", () => { + expect(Config.configPaths).toContain(".herb.yaml") + expect(Config.configPaths).toContain(".herb.yml") }) }) describe("Config.configPathFromProjectPath", () => { test("returns correct path for project directory", () => { const configPath = Config.configPathFromProjectPath(testDir) - expect(configPath).toBe(join(testDir, ".herb.yml")) + expect(configPath).toBe(join(testDir, Config.defaultConfigPath)) }) - test("appends .herb.yml to any path (including explicit .herb.yml)", () => { - const herbYmlPath = join(testDir, ".herb.yml") + test("appends defaultConfigPath to any path (including explicit config path)", () => { + const herbYmlPath = join(testDir, Config.defaultConfigPath) const configPath = Config.configPathFromProjectPath(herbYmlPath) - expect(configPath).toBe(join(herbYmlPath, ".herb.yml")) + expect(configPath).toBe(join(herbYmlPath, Config.defaultConfigPath)) }) }) From 55e699168cbb2d97b62a96a31555b82725df5767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 13:33:31 +0200 Subject: [PATCH 03/24] Config: Add isConfigPath helper and accept explicit .herb.yaml paths Co-Authored-By: Claude Opus 4.6 (1M context) --- javascript/packages/config/src/config.ts | 10 +++++++--- javascript/packages/config/test/config.test.ts | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/javascript/packages/config/src/config.ts b/javascript/packages/config/src/config.ts index ce51233f9..f38b28354 100644 --- a/javascript/packages/config/src/config.ts +++ b/javascript/packages/config/src/config.ts @@ -448,6 +448,10 @@ export class Config { }) } + static isConfigPath(pathOrFile: string): boolean { + return this.configPaths.some(name => pathOrFile.endsWith(name)) + } + static configPathFromProjectPath(projectPath: string) { return path.join(projectPath, this.defaultConfigPath) } @@ -471,7 +475,7 @@ export class Config { try { let configPath: string - if (pathOrFile.endsWith(this.defaultConfigPath)) { + if (this.isConfigPath(pathOrFile)) { configPath = pathOrFile } else { configPath = this.configPathFromProjectPath(pathOrFile) @@ -563,7 +567,7 @@ export class Config { * @returns string - The raw YAML content */ static readRawYaml(pathOrFile: string): string { - const configPath = pathOrFile.endsWith(this.defaultConfigPath) + const configPath = this.isConfigPath(pathOrFile) ? pathOrFile : this.configPathFromProjectPath(pathOrFile) @@ -591,7 +595,7 @@ export class Config { const { silent = false, version = DEFAULT_VERSION, createIfMissing = false, exitOnError = false } = options try { - if (pathOrFile.endsWith(this.defaultConfigPath)) { + if (this.isConfigPath(pathOrFile)) { return await this.loadFromExplicitPath(pathOrFile, silent, version, exitOnError) } diff --git a/javascript/packages/config/test/config.test.ts b/javascript/packages/config/test/config.test.ts index 07636341b..4e06350e7 100644 --- a/javascript/packages/config/test/config.test.ts +++ b/javascript/packages/config/test/config.test.ts @@ -80,6 +80,13 @@ describe("@herb-tools/config", () => { expect(Config.exists(testDir)).toBe(true) }) + test("handles explicit .herb.yaml path", () => { + const configPath = join(testDir, ".herb.yaml") + writeFileSync(configPath, "version: 0.9.7\n") + + expect(Config.exists(configPath)).toBe(true) + }) + test("handles explicit .herb.yml path", () => { const configPath = join(testDir, ".herb.yml") writeFileSync(configPath, "version: 0.9.7\n") @@ -105,6 +112,15 @@ describe("@herb-tools/config", () => { expect(rawYaml).toBe(yamlContent) }) + test("handles explicit .herb.yaml path", () => { + const configPath = join(testDir, ".herb.yaml") + const yamlContent = "version: 0.9.7\n" + writeFileSync(configPath, yamlContent) + + const rawYaml = Config.readRawYaml(configPath) + expect(rawYaml).toBe(yamlContent) + }) + test("handles explicit .herb.yml path", () => { const configPath = join(testDir, ".herb.yml") const yamlContent = "version: 0.9.7\n" From a912e053cb02f7ca3ddd9330676ab431a1da5a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 13:38:35 +0200 Subject: [PATCH 04/24] Config: Discover existing .herb.yaml in configPathFromProjectPath Co-Authored-By: Claude Opus 4.6 (1M context) --- javascript/packages/config/src/config.ts | 8 ++++++++ javascript/packages/config/test/config.test.ts | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/javascript/packages/config/src/config.ts b/javascript/packages/config/src/config.ts index f38b28354..4b6ec45c7 100644 --- a/javascript/packages/config/src/config.ts +++ b/javascript/packages/config/src/config.ts @@ -453,6 +453,14 @@ export class Config { } static configPathFromProjectPath(projectPath: string) { + for (const configPath of this.configPaths) { + const candidate = path.join(projectPath, configPath) + + if (require('fs').existsSync(candidate)) { + return candidate + } + } + return path.join(projectPath, this.defaultConfigPath) } diff --git a/javascript/packages/config/test/config.test.ts b/javascript/packages/config/test/config.test.ts index 4e06350e7..676d72199 100644 --- a/javascript/packages/config/test/config.test.ts +++ b/javascript/packages/config/test/config.test.ts @@ -66,6 +66,20 @@ describe("@herb-tools/config", () => { expect(configPath).toBe(join(herbYmlPath, Config.defaultConfigPath)) }) + + test("finds existing .herb.yaml file", () => { + writeFileSync(join(testDir, ".herb.yaml"), "version: 0.9.7\n") + + const configPath = Config.configPathFromProjectPath(testDir) + expect(configPath).toBe(join(testDir, ".herb.yaml")) + }) + + test("finds existing .herb.yml file", () => { + writeFileSync(join(testDir, ".herb.yml"), "version: 0.9.7\n") + + const configPath = Config.configPathFromProjectPath(testDir) + expect(configPath).toBe(join(testDir, ".herb.yml")) + }) }) describe("Config.exists", () => { From aab382869e45c8098bcadfe2bb7d98bf78dcf1d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 13:57:05 +0200 Subject: [PATCH 05/24] Config: Discover .herb.yaml in findConfigFile and findProjectRootSync Co-Authored-By: Claude Opus 4.6 (1M context) --- javascript/packages/config/src/config.ts | 29 +++++++++---------- .../config/test/find-project-root.test.ts | 22 ++++++++++++++ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/javascript/packages/config/src/config.ts b/javascript/packages/config/src/config.ts index 4b6ec45c7..bfd59d7ac 100644 --- a/javascript/packages/config/src/config.ts +++ b/javascript/packages/config/src/config.ts @@ -534,14 +534,10 @@ export class Config { let firstIndicatorMatch: string | undefined while (true) { - const configPath = path.join(currentPath, this.defaultConfigPath) - - try { - fsSync.accessSync(configPath) - - return currentPath - } catch { - // Config not in this directory, continue + for (const configPath of this.configPaths) { + if (fsSync.existsSync(path.join(currentPath, configPath))) { + return currentPath + } } if (!firstIndicatorMatch) { @@ -900,14 +896,15 @@ export class Config { let firstIndicatorMatch: string | undefined while (true) { - const configPath = path.join(currentPath, this.defaultConfigPath) - - try { - await fs.access(configPath) - - return { configPath, projectRoot: currentPath } - } catch { - // Config not in this directory, continue + for (const configPath of this.configPaths) { + const candidate = path.join(currentPath, configPath) + + try { + await fs.access(candidate) + return { configPath: candidate, projectRoot: currentPath } + } catch { + // Config file not found in this directory, try next + } } if (!firstIndicatorMatch) { diff --git a/javascript/packages/config/test/find-project-root.test.ts b/javascript/packages/config/test/find-project-root.test.ts index dcc5163a7..8d7ceeedf 100644 --- a/javascript/packages/config/test/find-project-root.test.ts +++ b/javascript/packages/config/test/find-project-root.test.ts @@ -3,6 +3,28 @@ import { mkdirSync, writeFileSync, rmSync } from "fs" import { join } from "path" import { Config } from "../src/config.js" +describe("findProjectRootSync with .herb.yaml", () => { + const tempDir = join(process.cwd(), "tmp-test-find-project-root-yaml") + + beforeAll(() => { + mkdirSync(join(tempDir, "app", "views"), { recursive: true }) + + writeFileSync(join(tempDir, ".herb.yaml"), "linter:\n enabled: true\n") + writeFileSync(join(tempDir, "app", "views", "index.html.erb"), "
\n") + }) + + afterAll(() => { + rmSync(tempDir, { recursive: true, force: true }) + }) + + test("finds project root with .herb.yaml", () => { + const filePath = join(tempDir, "app", "views", "index.html.erb") + const projectRoot = Config.findProjectRootSync(filePath) + + expect(projectRoot).toBe(tempDir) + }) +}) + describe("findProjectRootSync", () => { const tempDir = join(process.cwd(), "tmp-test-find-project-root") From 911474832053943ba0aeaf55d4a8ec479108ad6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 18:24:57 +0200 Subject: [PATCH 06/24] Ruby: Accept .herb.yaml in config discovery Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/herb/configuration.rb | 3 ++- test/configuration_test.rb | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/herb/configuration.rb b/lib/herb/configuration.rb index d80042346..083907bb4 100644 --- a/lib/herb/configuration.rb +++ b/lib/herb/configuration.rb @@ -11,11 +11,12 @@ class Configuration VALID_FRAMEWORKS = OPTIONS["framework"]["values"].freeze #: Array[String] VALID_TEMPLATE_ENGINES = OPTIONS["template_engine"]["values"].freeze #: Array[String] - CONFIG_FILENAMES = [".herb.yml"].freeze + CONFIG_FILENAMES = [".herb.yaml", ".herb.yml"].freeze PROJECT_INDICATORS = [ ".git", ".herb", + ".herb.yaml", ".herb.yml", "Gemfile", "package.json", diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 53d8c00b4..94666ac5a 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -26,6 +26,21 @@ def write_config(content, filename = ".herb.yml") assert_equal Herb::Configuration::DEFAULTS["files"]["exclude"], config.file_exclude_patterns end + test "loads configuration from .herb.yaml" do + write_config(<<~YAML, ".herb.yaml") + version: "0.9.7" + files: + include: + - "**/*.custom.erb" + YAML + + config = Herb::Configuration.load(@temp_dir) + + assert_equal File.join(@temp_dir, ".herb.yaml"), config.config_path.to_s + assert_equal "0.9.7", config.version + assert_includes config.file_include_patterns, "**/*.custom.erb" + end + test "loads configuration from .herb.yml" do write_config(<<~YAML) version: "0.9.7" From bdb6f326d0892960345e5fe11b18c733e8d3715a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 18:35:45 +0200 Subject: [PATCH 07/24] Rust: Accept .herb.yaml in config discovery Co-Authored-By: Claude Opus 4.6 (1M context) --- rust/herb-config/src/herb_config.rs | 12 +++++++----- rust/herb-config/tests/herb_config_test.rs | 11 +++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/rust/herb-config/src/herb_config.rs b/rust/herb-config/src/herb_config.rs index 8b9be0bc5..de00d63bd 100644 --- a/rust/herb-config/src/herb_config.rs +++ b/rust/herb-config/src/herb_config.rs @@ -125,7 +125,7 @@ pub const DEFAULT_INCLUDE_PATTERNS: &[&str] = &[ pub const DEFAULT_EXCLUDE_PATTERNS: &[&str] = &["coverage/**/*", "log/**/*", "node_modules/**/*", "storage/**/*", "tmp/**/*", "vendor/**/*"]; -const CONFIG_FILE_NAME: &str = ".herb.yml"; +const CONFIG_FILE_NAMES: &[&str] = &[".herb.yaml", ".herb.yml"]; const PROJECT_INDICATORS: &[&str] = &[".git", ".herb", "Gemfile", "package.json", "Rakefile", "README.md"]; impl HerbConfig { @@ -172,9 +172,11 @@ impl HerbConfig { let mut current = start.to_path_buf(); loop { - let config_path = current.join(CONFIG_FILE_NAME); - if config_path.exists() { - return Some(config_path); + for config_name in CONFIG_FILE_NAMES { + let candidate = current.join(config_name); + if candidate.exists() { + return Some(candidate); + } } if !current.pop() { @@ -195,7 +197,7 @@ impl HerbConfig { let mut current = start.to_path_buf(); loop { - if current.join(CONFIG_FILE_NAME).exists() { + if CONFIG_FILE_NAMES.iter().any(|name| current.join(name).exists()) { return current; } diff --git a/rust/herb-config/tests/herb_config_test.rs b/rust/herb-config/tests/herb_config_test.rs index 5606e02e8..6bb57bb6f 100644 --- a/rust/herb-config/tests/herb_config_test.rs +++ b/rust/herb-config/tests/herb_config_test.rs @@ -362,6 +362,17 @@ fn load_from_path_returns_error_for_invalid_yaml() { assert!(result.is_err()); } +#[test] +fn find_config_file_finds_yaml_extension() { + let dir = tempfile::tempdir().unwrap(); + let config_path = dir.path().join(".herb.yaml"); + fs::write(&config_path, "version: 0.9.7\n").unwrap(); + + let found = HerbConfig::find_config_file(dir.path()); + + assert_eq!(found, Some(config_path)); +} + #[test] fn find_config_file_finds_config_in_current_dir() { let dir = tempfile::tempdir().unwrap(); From a778bb4d11e846a837040ffdf65bb53472c4aac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 18:42:38 +0200 Subject: [PATCH 08/24] Language Server: Accept .herb.yaml in file watchers and detection Co-Authored-By: Claude Opus 4.6 (1M context) --- javascript/packages/language-server/src/config_service.ts | 2 +- javascript/packages/language-server/src/diagnostics.ts | 3 ++- .../packages/language-server/src/formatting_service.ts | 6 +++--- javascript/packages/language-server/src/linter_service.ts | 2 +- javascript/packages/language-server/src/server.ts | 3 ++- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/javascript/packages/language-server/src/config_service.ts b/javascript/packages/language-server/src/config_service.ts index 25add2eb3..635e4e880 100644 --- a/javascript/packages/language-server/src/config_service.ts +++ b/javascript/packages/language-server/src/config_service.ts @@ -18,7 +18,7 @@ export class ConfigService { async validateDocument(document: TextDocument): Promise { const diagnostics: Diagnostic[] = [] - if (!document.uri.endsWith('.herb.yml')) { + if (!Config.isConfigPath(document.uri)) { return diagnostics } diff --git a/javascript/packages/language-server/src/diagnostics.ts b/javascript/packages/language-server/src/diagnostics.ts index 360547e0b..e0258ee2c 100644 --- a/javascript/packages/language-server/src/diagnostics.ts +++ b/javascript/packages/language-server/src/diagnostics.ts @@ -1,5 +1,6 @@ import { TextDocument } from "vscode-languageserver-textdocument" import { Connection, Diagnostic } from "vscode-languageserver/node" +import { Config } from "@herb-tools/config" import { ParserService } from "./parser_service" import { LinterService } from "./linter_service" @@ -31,7 +32,7 @@ export class Diagnostics { async validate(textDocument: TextDocument) { let allDiagnostics: Diagnostic[] = [] - if (textDocument.uri.endsWith('.herb.yml')) { + if (Config.isConfigPath(textDocument.uri)) { allDiagnostics = await this.configService.validateDocument(textDocument) } else { const parseResult = this.parserService.parseDocument(textDocument) diff --git a/javascript/packages/language-server/src/formatting_service.ts b/javascript/packages/language-server/src/formatting_service.ts index 5e21f4c06..217d51f1f 100644 --- a/javascript/packages/language-server/src/formatting_service.ts +++ b/javascript/packages/language-server/src/formatting_service.ts @@ -207,7 +207,7 @@ export class FormattingService { } private shouldFormatFile(filePath: string): boolean { - if (filePath.endsWith('.herb.yml')) return false + if (Config.isConfigPath(filePath)) return false if (!this.config) return true const hasConfigFile = Config.exists(this.config.projectPath) @@ -295,7 +295,7 @@ export class FormattingService { } async formatDocument(params: DocumentFormattingParams): Promise { - if (params.textDocument.uri.endsWith('.herb.yml')) { + if (Config.isConfigPath(params.textDocument.uri)) { return [] } @@ -395,7 +395,7 @@ export class FormattingService { } async formatRange(params: DocumentRangeFormattingParams): Promise { - if (params.textDocument.uri.endsWith('.herb.yml')) { + if (Config.isConfigPath(params.textDocument.uri)) { return [] } diff --git a/javascript/packages/language-server/src/linter_service.ts b/javascript/packages/language-server/src/linter_service.ts index 46e81f37a..954f5d8fb 100644 --- a/javascript/packages/language-server/src/linter_service.ts +++ b/javascript/packages/language-server/src/linter_service.ts @@ -117,7 +117,7 @@ export class LinterService { private shouldLintFile(uri: string): boolean { const filePath = uri.replace(/^file:\/\//, '') - if (filePath.endsWith('.herb.yml')) return false + if (Config.isConfigPath(filePath)) return false const config = this.settings.projectConfig if (!config) return true diff --git a/javascript/packages/language-server/src/server.ts b/javascript/packages/language-server/src/server.ts index 1b075bb4e..d9158e62d 100644 --- a/javascript/packages/language-server/src/server.ts +++ b/javascript/packages/language-server/src/server.ts @@ -98,6 +98,7 @@ export class Server { this.connection.client.register(DidChangeWatchedFilesNotification.type, { watchers: [ ...patterns, + { globPattern: `**/.herb.yaml` }, { globPattern: `**/.herb.yml` }, { globPattern: `**/.herb/rules/**/*.mjs` }, { globPattern: `**/.herb/rewriters/**/*.mjs` }, @@ -128,7 +129,7 @@ export class Server { this.connection.onDidChangeWatchedFiles(async (params) => { for (const event of params.changes) { - const isConfigChange = event.uri.endsWith("/.herb.yml") + const isConfigChange = Config.isConfigPath(event.uri) const isCustomRuleChange = event.uri.includes("/.herb/rules/") const isCustomRewriterChange = event.uri.includes("/.herb/rewriters/") From aadcc3405b53ccdfd1370dd6f770ff3e5faa7ca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 18:43:33 +0200 Subject: [PATCH 09/24] Language Server: Use configPathFromProjectPath for config file resolution Co-Authored-By: Claude Opus 4.6 (1M context) --- javascript/packages/language-server/src/formatting_service.ts | 4 ++-- javascript/packages/language-server/src/linter_service.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/javascript/packages/language-server/src/formatting_service.ts b/javascript/packages/language-server/src/formatting_service.ts index 217d51f1f..d01a42ac7 100644 --- a/javascript/packages/language-server/src/formatting_service.ts +++ b/javascript/packages/language-server/src/formatting_service.ts @@ -171,7 +171,7 @@ export class FormattingService { if (this.settings.hasShowDocumentCapability) { this.connection.window.showWarningMessage(message, { title: OPEN_CONFIG_ACTION }).then(action => { if (action?.title === OPEN_CONFIG_ACTION) { - const configPath = `${this.project.projectPath}/.herb.yml` + const configPath = Config.configPathFromProjectPath(this.project.projectPath) this.connection.window.showDocument({ uri: `file://${configPath}`, takeFocus: true }) } }) @@ -257,7 +257,7 @@ export class FormattingService { if (this.settings.hasShowDocumentCapability) { this.connection.window.showWarningMessage(message, { title: OPEN_CONFIG_ACTION }).then(action => { if (action?.title === OPEN_CONFIG_ACTION) { - const configPath = `${this.project.projectPath}/.herb.yml` + const configPath = Config.configPathFromProjectPath(this.project.projectPath) this.connection.window.showDocument({ uri: `file://${configPath}`, takeFocus: true }) } }) diff --git a/javascript/packages/language-server/src/linter_service.ts b/javascript/packages/language-server/src/linter_service.ts index 954f5d8fb..11e5451b7 100644 --- a/javascript/packages/language-server/src/linter_service.ts +++ b/javascript/packages/language-server/src/linter_service.ts @@ -105,7 +105,7 @@ export class LinterService { if (this.settings.hasShowDocumentCapability) { this.connection.window.showWarningMessage(message, { title: OPEN_CONFIG_ACTION }).then(action => { if (action?.title === OPEN_CONFIG_ACTION) { - const configPath = `${this.project.projectPath}/.herb.yml` + const configPath = Config.configPathFromProjectPath(this.project.projectPath) this.connection.window.showDocument({ uri: `file://${configPath}`, takeFocus: true }) } }) From 63b63028ea324bcaf5b491270cbc902b2ae89c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 18:45:55 +0200 Subject: [PATCH 10/24] VS Code: Accept .herb.yaml in file watchers and detection Co-Authored-By: Claude Opus 4.6 (1M context) --- javascript/packages/vscode/src/client.ts | 2 +- javascript/packages/vscode/src/config-provider.ts | 4 ++-- javascript/packages/vscode/src/extension.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/javascript/packages/vscode/src/client.ts b/javascript/packages/vscode/src/client.ts index 71369531a..b3a4399f0 100644 --- a/javascript/packages/vscode/src/client.ts +++ b/javascript/packages/vscode/src/client.ts @@ -152,7 +152,7 @@ export class Client { documentSelector: [ { scheme: "file", language: "erb" }, { scheme: "file", language: "html" }, - { scheme: "file", language: "yaml", pattern: "**/.herb.yml" }, + { scheme: "file", language: "yaml", pattern: "**/.herb.{yaml,yml}" }, ], synchronize: { fileEvents: workspace.createFileSystemWatcher("**/.clientrc"), diff --git a/javascript/packages/vscode/src/config-provider.ts b/javascript/packages/vscode/src/config-provider.ts index bbfb37bd9..965819ca4 100644 --- a/javascript/packages/vscode/src/config-provider.ts +++ b/javascript/packages/vscode/src/config-provider.ts @@ -23,7 +23,7 @@ export class HerbConfigProvider implements vscode.TreeDataProvider { }) if (this.workspaceRoot) { - const herbYmlPattern = new vscode.RelativePattern(this.workspaceRoot, ".herb.yml") + const herbYmlPattern = new vscode.RelativePattern(this.workspaceRoot, ".herb.{yaml,yml}") const watcher = vscode.workspace.createFileSystemWatcher(herbYmlPattern) watcher.onDidCreate(() => void this.refresh()) @@ -35,7 +35,7 @@ export class HerbConfigProvider implements vscode.TreeDataProvider { context.subscriptions.push( vscode.workspace.onDidSaveTextDocument((document) => { - if (document.fileName.endsWith('.herb.yml')) { + if (Config.isConfigPath(document.fileName)) { void this.refresh() } }) diff --git a/javascript/packages/vscode/src/extension.ts b/javascript/packages/vscode/src/extension.ts index 9bf6ac8cb..ea88c22cc 100644 --- a/javascript/packages/vscode/src/extension.ts +++ b/javascript/packages/vscode/src/extension.ts @@ -196,7 +196,7 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(fileWatcher) - const configWatcher = vscode.workspace.createFileSystemWatcher('**/.herb.yml') + const configWatcher = vscode.workspace.createFileSystemWatcher('**/.herb.{yaml,yml}') configWatcher.onDidCreate(async () => { await updateConfigStatusBarItem() From ae99aef46d0f76f8efba3e4d1f425fcf33bbd1d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 18:50:22 +0200 Subject: [PATCH 11/24] VS Code: Use configPathFromProjectPath for config file resolution Co-Authored-By: Claude Opus 4.6 (1M context) --- javascript/packages/vscode/src/herb-settings-commands.ts | 3 +-- javascript/packages/vscode/src/utils.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/javascript/packages/vscode/src/herb-settings-commands.ts b/javascript/packages/vscode/src/herb-settings-commands.ts index 65132d651..4a4ecc380 100644 --- a/javascript/packages/vscode/src/herb-settings-commands.ts +++ b/javascript/packages/vscode/src/herb-settings-commands.ts @@ -1,5 +1,4 @@ import * as vscode from "vscode" -import * as path from "path" import { Config } from "@herb-tools/config" import { HerbConfigProvider } from "./config-provider" @@ -236,7 +235,7 @@ export class HerbSettingsCommands { if (!workspaceRoot) { return } - const configPath = path.join(workspaceRoot, ".herb.yml") + const configPath = Config.configPathFromProjectPath(workspaceRoot) try { const document = await vscode.workspace.openTextDocument(configPath) diff --git a/javascript/packages/vscode/src/utils.ts b/javascript/packages/vscode/src/utils.ts index 61d9446e7..10bcb9c64 100644 --- a/javascript/packages/vscode/src/utils.ts +++ b/javascript/packages/vscode/src/utils.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode" -import * as path from "path" import { promises as fs } from "fs" +import { Config } from "@herb-tools/config" export interface EnvironmentInfo { extensionVersion: string @@ -44,7 +44,7 @@ export async function getHerbSettings(): Promise { try { const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath if (workspaceRoot) { - const configPath = path.join(workspaceRoot, '.herb.yml') + const configPath = Config.configPathFromProjectPath(workspaceRoot) projectConfig = await fs.readFile(configPath, 'utf8') } } catch (_error) { From a34c0834a468f14b8ad8f05d46f57225b0a02e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 18:57:07 +0200 Subject: [PATCH 12/24] Config: Default to .herb.yaml Co-Authored-By: Claude Opus 4.6 (1M context) --- javascript/packages/config/src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascript/packages/config/src/config.ts b/javascript/packages/config/src/config.ts index bfd59d7ac..3c13f81b2 100644 --- a/javascript/packages/config/src/config.ts +++ b/javascript/packages/config/src/config.ts @@ -106,7 +106,7 @@ export type FromObjectOptions = { } export class Config { - static configPaths = [".herb.yml", ".herb.yaml"] + static configPaths = [".herb.yaml", ".herb.yml"] static defaultConfigPath = this.configPaths[0] private static PROJECT_INDICATORS = [ From 71c8d361c159ea8c8dd83e21d5ca4dea3cc3a7ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 18:58:53 +0200 Subject: [PATCH 13/24] Update user-facing references from .herb.yml to .herb.yaml Co-Authored-By: Claude Opus 4.6 (1M context) --- javascript/packages/config/src/config.ts | 20 ++++++++--------- javascript/packages/formatter/src/cli.ts | 12 +++++----- .../src/code_action_service.ts | 4 ++-- .../language-server/src/config_service.ts | 2 +- .../language-server/src/formatting_service.ts | 2 +- .../language-server/src/linter_service.ts | 2 +- .../packages/language-server/src/service.ts | 4 ++-- javascript/packages/linter/src/cli.ts | 22 +++++++++---------- .../linter/src/cli/argument-parser.ts | 12 +++++----- .../linter/src/cli/summary-reporter.ts | 2 +- javascript/packages/linter/src/linter.ts | 2 +- javascript/packages/printer/src/cli.ts | 2 +- .../src/built-ins/tailwind-class-sorter.ts | 2 +- javascript/packages/vscode/package.json | 10 ++++----- .../vscode/src/config-details-provider.ts | 8 +++---- .../packages/vscode/src/config-provider.ts | 10 ++++----- javascript/packages/vscode/src/extension.ts | 6 ++--- .../vscode/src/herb-analysis-provider.ts | 2 +- .../vscode/src/herb-settings-commands.ts | 2 +- .../packages/vscode/src/issue-reporter.ts | 6 ++--- .../packages/vscode/src/tree-item-builder.ts | 4 ++-- lib/herb/action_view/render_analyzer.rb | 4 ++-- lib/herb/cli.rb | 2 +- lib/herb/project.rb | 2 +- 24 files changed, 72 insertions(+), 72 deletions(-) diff --git a/javascript/packages/config/src/config.ts b/javascript/packages/config/src/config.ts index 3c13f81b2..5d3561891 100644 --- a/javascript/packages/config/src/config.ts +++ b/javascript/packages/config/src/config.ts @@ -474,10 +474,10 @@ export class Config { } /** - * Check if a .herb.yml config file exists at the given path. + * Check if config file exists at the given path. * * @param pathOrFile - Path to directory or explicit config file path - * @returns True if .herb.yml exists at the location, false otherwise + * @returns True if config file exists at the location, false otherwise */ static exists(pathOrFile: string): boolean { try { @@ -499,7 +499,7 @@ export class Config { /** * Find the project root by walking up from a given path. - * Looks for .herb.yml first, then falls back to project indicators + * Looks for config file first, then falls back to project indicators * (.git, Gemfile, package.json, etc.) * * @param startPath - File or directory path to start searching from @@ -565,9 +565,9 @@ export class Config { /** * Read raw YAML content from a config file. - * Handles both explicit .herb.yml paths and directory paths. + * Handles both explicit config file paths and directory paths. * - * @param pathOrFile - Path to .herb.yml file or directory containing it + * @param pathOrFile - Path to config file or directory containing it * @returns string - The raw YAML content */ static readRawYaml(pathOrFile: string): string { @@ -626,7 +626,7 @@ export class Config { * Load config for editor/language server use (silent mode, no file creation). * This is a convenience method for the common pattern used in editors. * - * @param pathOrFile - Directory path or explicit .herb.yml file path + * @param pathOrFile - Directory path or explicit config file path * @param version - Optional version string (defaults to package version) * @returns Config instance or throws on errors */ @@ -643,7 +643,7 @@ export class Config { * Load config for CLI use (may create file, show errors). * This is a convenience method for the common pattern used in CLI tools. * - * @param pathOrFile - Directory path or explicit .herb.yml file path + * @param pathOrFile - Directory path or explicit config file path * @param version - Optional version string (defaults to package version) * @param createIfMissing - Whether to create config if missing (default: false) * @returns Config instance or throws on errors @@ -665,13 +665,13 @@ export class Config { * Mutate an existing config file by reading it, validating, merging with a mutation, and writing back. * This preserves the user's YAML file structure and only writes what's explicitly configured. * - * @param configPath - Path to the .herb.yml file + * @param configPath - Path to config file * @param mutation - Partial config to merge (e.g., { linter: { rules: { "rule-name": { enabled: false } } } }) * @returns Promise * * @example - * // Disable a rule in .herb.yml - * await Config.mutateConfigFile('/path/to/.herb.yml', { + * // Disable rule in config file + * await Config.mutateConfigFile('/path/to/.herb.yaml', { * linter: { * rules: { * 'html-img-require-alt': { enabled: false } diff --git a/javascript/packages/formatter/src/cli.ts b/javascript/packages/formatter/src/cli.ts index 2d7825dde..f49eb9147 100644 --- a/javascript/packages/formatter/src/cli.ts +++ b/javascript/packages/formatter/src/cli.ts @@ -50,9 +50,9 @@ export class CLI { -c, --check check if files are formatted without modifying them -h, --help show help -v, --version show version - --init create a .herb.yml configuration file in the current directory - --config-file explicitly specify path to .herb.yml config file - --force force formatting even if disabled in .herb.yml + --init create a .herb.yaml configuration file in the current directory + --config-file explicitly specify path to .herb.yaml config file + --force force formatting even if disabled in .herb.yaml --indent-width number of spaces per indentation level (default: 2) --max-line-length maximum line length before wrapping (default: 80) @@ -196,15 +196,15 @@ export class CLI { const formatterConfig = config.formatter || {} if (hasConfigFile && formatterConfig.enabled === false && !isForceMode) { - console.log("Formatter is disabled in .herb.yml configuration.") - console.log("To enable formatting, set formatter.enabled: true in .herb.yml") + console.log("Formatter is disabled in .herb.yaml configuration.") + console.log("To enable formatting, set formatter.enabled: true in .herb.yaml") console.log("Or use --force to format anyway.") process.exit(0) } if (isForceMode && hasConfigFile && formatterConfig.enabled === false) { - console.error("⚠️ Forcing formatter run (disabled in .herb.yml)") + console.error("⚠️ Forcing formatter run (disabled in .herb.yaml)") console.error() } diff --git a/javascript/packages/language-server/src/code_action_service.ts b/javascript/packages/language-server/src/code_action_service.ts index a262b23be..cb0888eb6 100644 --- a/javascript/packages/language-server/src/code_action_service.ts +++ b/javascript/packages/language-server/src/code_action_service.ts @@ -245,12 +245,12 @@ export class CodeActionService { const configUri = `file://${configPath}` const action: CodeAction = { - title: `Herb Linter: Disable \`${ruleName}\` in \`.herb.yml\``, + title: `Herb Linter: Disable \`${ruleName}\` in \`.herb.yaml\``, kind: CodeActionKind.QuickFix, diagnostics: [diagnostic], edit, command: { - title: 'Open .herb.yml', + title: 'Open .herb.yaml', command: 'vscode.open', arguments: [configUri] } diff --git a/javascript/packages/language-server/src/config_service.ts b/javascript/packages/language-server/src/config_service.ts index 635e4e880..e77709db7 100644 --- a/javascript/packages/language-server/src/config_service.ts +++ b/javascript/packages/language-server/src/config_service.ts @@ -6,7 +6,7 @@ import { lintToDignosticSeverity } from "./utils" import { version } from "../package.json" /** - * Configuration service for validation of .herb.yml configuration files + * Configuration service for validation of .herb.yaml configuration files */ export class ConfigService { private projectPath?: string diff --git a/javascript/packages/language-server/src/formatting_service.ts b/javascript/packages/language-server/src/formatting_service.ts index d01a42ac7..546f30767 100644 --- a/javascript/packages/language-server/src/formatting_service.ts +++ b/javascript/packages/language-server/src/formatting_service.ts @@ -8,7 +8,7 @@ import { Settings } from "./settings" import { Config } from "@herb-tools/config" import { version } from "../package.json" -const OPEN_CONFIG_ACTION = 'Open .herb.yml' +const OPEN_CONFIG_ACTION = 'Open .herb.yaml' export class FormattingService { private connection: Connection diff --git a/javascript/packages/language-server/src/linter_service.ts b/javascript/packages/language-server/src/linter_service.ts index 11e5451b7..660469613 100644 --- a/javascript/packages/language-server/src/linter_service.ts +++ b/javascript/packages/language-server/src/linter_service.ts @@ -11,7 +11,7 @@ import { Project } from "./project" import { lintToDignosticSeverity, lintToDignosticTags } from "./utils" import { lspRangeFromLocation } from "./range_utils" -const OPEN_CONFIG_ACTION = 'Open .herb.yml' +const OPEN_CONFIG_ACTION = 'Open .herb.yaml' export interface LintServiceResult { diagnostics: Diagnostic[] diff --git a/javascript/packages/language-server/src/service.ts b/javascript/packages/language-server/src/service.ts index ccbc34b18..04c7f78d7 100644 --- a/javascript/packages/language-server/src/service.ts +++ b/javascript/packages/language-server/src/service.ts @@ -77,7 +77,7 @@ export class Service { if (this.config.version && this.config.version !== version) { this.connection.console.warn( `Config file version (${this.config.version}) does not match current version (${version}). ` + - `Consider updating your .herb.yml file.` + `Consider updating your .herb.yaml file.` ) } } catch (error) { @@ -122,7 +122,7 @@ export class Service { if (this.config.version && this.config.version !== version) { this.connection.console.warn( `Config file version (${this.config.version}) does not match current version (${version}). ` + - `Consider updating your .herb.yml file.` + `Consider updating your .herb.yaml file.` ) } } catch (error) { diff --git a/javascript/packages/linter/src/cli.ts b/javascript/packages/linter/src/cli.ts index 3a71a974b..95def84e8 100644 --- a/javascript/packages/linter/src/cli.ts +++ b/javascript/packages/linter/src/cli.ts @@ -178,7 +178,7 @@ export class CLI { const configPath = configFile || this.projectPath if (!Config.exists(configPath)) { - console.error(`\n✗ No .herb.yml found. Run ${colorize("herb-lint --init", "cyan")} first.\n`) + console.error(`\n✗ No .herb.yaml found. Run ${colorize("herb-lint --init", "cyan")} first.\n`) process.exit(1) } @@ -186,7 +186,7 @@ export class CLI { const configVersion = config.configVersion if (configVersion === version) { - console.log(`\n✓ Your .herb.yml is already at version ${version}. Nothing to upgrade.\n`) + console.log(`\n✓ Your .herb.yaml is already at version ${version}. Nothing to upgrade.\n`) process.exit(0) } @@ -253,7 +253,7 @@ export class CLI { content = content.replace(/^version:\s*.+$/m, `version: ${version}`) await fs.writeFile(config.path, content, "utf-8") - console.log(`\n${colorize("✓", "brightGreen")} Updated ${colorize(".herb.yml", "cyan")} version from ${colorize(configVersion ?? "unversioned", "cyan")} to ${colorize(version, "cyan")}`) + console.log(`\n${colorize("✓", "brightGreen")} Updated ${colorize(".herb.yaml", "cyan")} version from ${colorize(configVersion ?? "unversioned", "cyan")} to ${colorize(version, "cyan")}`) if (rulesToEnable.length > 0) { console.log(`\n${colorize("✓", "brightGreen")} Enabled ${colorize(String(rulesToEnable.length), "bold")} new ${rulesToEnable.length === 1 ? "rule" : "rules"} (no offenses found):\n`) @@ -273,7 +273,7 @@ export class CLI { console.log(` ${colorize("✗", "red")} ${colorize(rule.ruleName, "white")} ${colorize(`(${offenseCount} ${offenseCount === 1 ? "offense" : "offenses"})`, "gray")}`) } - console.log(`\n When you're ready, review the disabled ${rulesToDisable.length === 1 ? "rule" : "rules"} in your ${colorize(".herb.yml", "cyan")} and re-enable them after fixing the offenses.`) + console.log(`\n When you're ready, review the disabled ${rulesToDisable.length === 1 ? "rule" : "rules"} in your ${colorize(".herb.yaml", "cyan")} and re-enable them after fixing the offenses.`) } if (skippedByVersion.length === 0) { @@ -288,7 +288,7 @@ export class CLI { const configPath = configFile || this.projectPath if (!Config.exists(configPath)) { - console.error(`\n✗ No .herb.yml found. Run ${colorize("herb-lint --init", "cyan")} first.\n`) + console.error(`\n✗ No .herb.yaml found. Run ${colorize("herb-lint --init", "cyan")} first.\n`) process.exit(1) } @@ -336,13 +336,13 @@ export class CLI { const totalOffenses = Array.from(failingRules.values()).reduce((sum, count) => sum + count, 0) const sortedRules = Array.from(failingRules.entries()).sort((a, b) => b[1] - a[1]) - console.log(`\n${colorize("!", "yellow")} Found ${colorize(String(totalOffenses), "bold")} ${totalOffenses === 1 ? "offense" : "offenses"} across ${colorize(String(failingRules.size), "bold")} ${failingRules.size === 1 ? "rule" : "rules"}. Disabled in ${colorize(".herb.yml", "cyan")}:\n`) + console.log(`\n${colorize("!", "yellow")} Found ${colorize(String(totalOffenses), "bold")} ${totalOffenses === 1 ? "offense" : "offenses"} across ${colorize(String(failingRules.size), "bold")} ${failingRules.size === 1 ? "rule" : "rules"}. Disabled in ${colorize(".herb.yaml", "cyan")}:\n`) for (const [ruleName, count] of sortedRules) { console.log(` ${colorize("✗", "red")} ${colorize(ruleName, "white")} ${colorize(`(${count} ${count === 1 ? "offense" : "offenses"})`, "gray")}`) } - console.log(`\n When you're ready, review the disabled rules in your ${colorize(".herb.yml", "cyan")} and re-enable them after fixing the offenses.\n`) + console.log(`\n When you're ready, review the disabled rules in your ${colorize(".herb.yaml", "cyan")} and re-enable them after fixing the offenses.\n`) process.exit(0) } @@ -366,11 +366,11 @@ export class CLI { await this.beforeProcess() if (linterConfig.enabled === false && !force) { - this.exitWithInfo("Linter is disabled in .herb.yml configuration. Use --force to lint anyway.", formatOption, 0, { startTime, startDate, showTiming }) + this.exitWithInfo("Linter is disabled in .herb.yaml configuration. Use --force to lint anyway.", formatOption, 0, { startTime, startDate, showTiming }) } if (force && linterConfig.enabled === false) { - console.log("⚠️ Forcing linter run (disabled in .herb.yml)") + console.log("⚠️ Forcing linter run (disabled in .herb.yaml)") console.log() } @@ -442,8 +442,8 @@ export class CLI { if (!Config.exists(this.projectPath) && formatOption !== 'json' && !useGitHubActions) { console.log("") - console.log(` ${colorize("TIP:", "bold")} Run ${colorize("herb-lint --init", "cyan")} to create a ${colorize(".herb.yml", "cyan")} and lock the ${colorize("version", "cyan")}.`) - console.log(` This ensures upgrading Herb won't enable new rules until you update the ${colorize("version", "cyan")} in ${colorize(".herb.yml", "cyan")}.`) + console.log(` ${colorize("TIP:", "bold")} Run ${colorize("herb-lint --init", "cyan")} to create a ${colorize(".herb.yaml", "cyan")} and lock the ${colorize("version", "cyan")}.`) + console.log(` This ensures upgrading Herb won't enable new rules until you update the ${colorize("version", "cyan")} in ${colorize(".herb.yaml", "cyan")}.`) } await this.afterProcess(results, outputOptions) diff --git a/javascript/packages/linter/src/cli/argument-parser.ts b/javascript/packages/linter/src/cli/argument-parser.ts index 044a77f1a..2f16262a2 100644 --- a/javascript/packages/linter/src/cli/argument-parser.ts +++ b/javascript/packages/linter/src/cli/argument-parser.ts @@ -38,17 +38,17 @@ export class ArgumentParser { Usage: herb-lint [files|directories|glob-patterns...] [options] Arguments: - files Files, directories, or glob patterns to lint (defaults to configured extensions in .herb.yml) + files Files, directories, or glob patterns to lint (defaults to configured extensions in .herb.yaml) Multiple arguments are supported (e.g., herb-lint file1.erb file2.erb dir/ "**/*.erb") Options: -h, --help show help -v, --version show version - --init create a .herb.yml configuration file in the current directory - --upgrade update .herb.yml version and disable all newly introduced rules - --disable-failing lint the codebase and disable all rules that have offenses in .herb.yml - -c, --config-file explicitly specify path to .herb.yml config file - --force force linting even if disabled in .herb.yml + --init create a .herb.yaml configuration file in the current directory + --upgrade update .herb.yaml version and disable all newly introduced rules + --disable-failing lint the codebase and disable all rules that have offenses in .herb.yaml + -c, --config-file explicitly specify path to .herb.yaml config file + --force force linting even if disabled in .herb.yaml --fix automatically fix auto-correctable offenses --fix-unsafely also apply unsafe auto-fixes (implies --fix) --ignore-disable-comments report offenses even when suppressed with <%# herb:disable %> comments diff --git a/javascript/packages/linter/src/cli/summary-reporter.ts b/javascript/packages/linter/src/cli/summary-reporter.ts index 363ba1387..ec4c0d346 100644 --- a/javascript/packages/linter/src/cli/summary-reporter.ts +++ b/javascript/packages/linter/src/cli/summary-reporter.ts @@ -163,7 +163,7 @@ export class SummaryReporter { console.log("") console.log(` ${colorize(`New rules available:`, "bold")}`) - console.log(` Your ${colorize(".herb.yml", "cyan")} version is ${colorize(configVersion!, "cyan")}. ${colorize(String(ruleCount), "bold")} new ${this.pluralize(ruleCount, "rule")} ${ruleCount === 1 ? "is" : "are"} disabled to ease upgrades:`) + console.log(` Your ${colorize(".herb.yaml", "cyan")} version is ${colorize(configVersion!, "cyan")}. ${colorize(String(ruleCount), "bold")} new ${this.pluralize(ruleCount, "rule")} ${ruleCount === 1 ? "is" : "are"} disabled to ease upgrades:`) if (configPath) { console.log(` ${colorize("from Herb config:", "gray")} ${colorize(configPath, "cyan")}`) diff --git a/javascript/packages/linter/src/linter.ts b/javascript/packages/linter/src/linter.ts index d0dad7381..cb414f17c 100644 --- a/javascript/packages/linter/src/linter.ts +++ b/javascript/packages/linter/src/linter.ts @@ -125,7 +125,7 @@ export class Linter { * * @param allRules - All available rule classes to filter from * @param userRulesConfig - Optional user configuration for rules - * @param configVersion - Optional version from the user's .herb.yml for version-gated filtering + * @param configVersion - Optional version from the user's .herb.yaml for version-gated filtering * @returns Object with enabled rules and rules skipped due to version gating */ static filterRulesByConfig( diff --git a/javascript/packages/printer/src/cli.ts b/javascript/packages/printer/src/cli.ts index f797fa011..b24416eea 100644 --- a/javascript/packages/printer/src/cli.ts +++ b/javascript/packages/printer/src/cli.ts @@ -83,7 +83,7 @@ export class CLI { Options: -i, --input Input file path -o, --output Output file path (defaults to stdout) - --config-file Explicitly specify path to .herb.yml config file + --config-file Explicitly specify path to .herb.yaml config file --verify Verify that output matches input exactly --stats Show parsing and printing statistics --glob Treat input as glob pattern diff --git a/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts b/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts index 1f2946917..7cf0bc398 100644 --- a/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts +++ b/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts @@ -347,7 +347,7 @@ export class TailwindClassSorterRewriter extends ASTRewriter { throw new Error( `Tailwind CSS is not installed in this project. ` + `To use the Tailwind class sorter, install Tailwind CSS itself using: npm install -D tailwindcss, ` + - `or remove the "tailwind-class-sorter" rewriter from your .herb.yml config file.\n` + + `or remove the "tailwind-class-sorter" rewriter from your .herb.yaml config file.\n` + `If "tailwindcss" is already part of your package.json, make sure your NPM dependencies are installed.\n` + `Original error: ${errorMessage}.` ) diff --git a/javascript/packages/vscode/package.json b/javascript/packages/vscode/package.json index 761dc0041..9f54718ee 100644 --- a/javascript/packages/vscode/package.json +++ b/javascript/packages/vscode/package.json @@ -54,7 +54,7 @@ "type": "boolean", "default": true, "description": "Enable/disable the Herb Linter. The linter provides comprehensive HTML+ERB validation, enforces best practices, and catches common errors with a set of configurable rules, like proper tag nesting, required attributes, and ERB usage patterns.", - "markdownDescription": "Enable/disable the [Herb Linter](https://herb-tools.dev/projects/linter). The linter provides comprehensive HTML+ERB validation, enforces best practices, and catches common errors with a set of configurable [rules](https://herb-tools.dev/linter/rules/), like proper tag nesting, required attributes, and ERB usage patterns.\n\n**Note**: This is a personal default. If a `.herb.yml` file exists in your project, its configuration takes precedence." + "markdownDescription": "Enable/disable the [Herb Linter](https://herb-tools.dev/projects/linter). The linter provides comprehensive HTML+ERB validation, enforces best practices, and catches common errors with a set of configurable [rules](https://herb-tools.dev/linter/rules/), like proper tag nesting, required attributes, and ERB usage patterns.\n\n**Note**: This is a personal default. If a `.herb.yaml` file exists in your project, its configuration takes precedence." }, "languageServerHerb.linter.fixOnSave": { "scope": "resource", @@ -68,21 +68,21 @@ "type": "boolean", "default": false, "description": "Enable/disable document formatting using the Herb Formatter (Experimental Preview). WARNING: This formatter is experimental and may potentially corrupt files. Only use on files that can be restored via git or other version control.", - "markdownDescription": "Enable/disable document formatting using the [Herb Formatter](https://herb-tools.dev/projects/formatter) **(Experimental Preview)**. ⚠️ **WARNING**: This formatter is experimental and may potentially corrupt files. Only use on files that can be restored via git or other version control.\n\n**Note**: This is a personal default. If a `.herb.yml` file exists in your project, its configuration takes precedence." + "markdownDescription": "Enable/disable document formatting using the [Herb Formatter](https://herb-tools.dev/projects/formatter) **(Experimental Preview)**. ⚠️ **WARNING**: This formatter is experimental and may potentially corrupt files. Only use on files that can be restored via git or other version control.\n\n**Note**: This is a personal default. If a `.herb.yaml` file exists in your project, its configuration takes precedence." }, "languageServerHerb.formatter.indentWidth": { "scope": "resource", "type": "number", "default": 2, "description": "Number of spaces for each indentation level.", - "markdownDescription": "Number of spaces for each indentation level.\n\n**Note**: This is a personal default. If a `.herb.yml` file exists in your project, its configuration takes precedence." + "markdownDescription": "Number of spaces for each indentation level.\n\n**Note**: This is a personal default. If a `.herb.yaml` file exists in your project, its configuration takes precedence." }, "languageServerHerb.formatter.maxLineLength": { "scope": "resource", "type": "number", "default": 80, "description": "Maximum line length before wrapping.", - "markdownDescription": "Maximum line length before wrapping.\n\n**Note**: This is a personal default. If a `.herb.yml` file exists in your project, its configuration takes precedence." + "markdownDescription": "Maximum line length before wrapping.\n\n**Note**: This is a personal default. If a `.herb.yaml` file exists in your project, its configuration takes precedence." }, "languageServerHerb.trace.server": { "scope": "window", @@ -259,7 +259,7 @@ }, { "view": "herbConfiguration", - "contents": "Create a Herb configuration file to share Herb settings with your team.\n\n[Create .herb.yml](command:herb.createConfig)", + "contents": "Create a Herb configuration file to share Herb settings with your team.\n\n[Create .herb.yaml](command:herb.createConfig)", "when": "!herb.hasProjectConfig" } ] diff --git a/javascript/packages/vscode/src/config-details-provider.ts b/javascript/packages/vscode/src/config-details-provider.ts index cc7975b95..d0c26568c 100644 --- a/javascript/packages/vscode/src/config-details-provider.ts +++ b/javascript/packages/vscode/src/config-details-provider.ts @@ -29,13 +29,13 @@ export async function showConfigDetails() { const configSourceItem: ConfigQuickPickItem = hasConfigFile ? { label: "$(file-code) Herb Configuration Source", - detail: ".herb.yml (project configuration overrides VS Code settings)", + detail: ".herb.yaml (project configuration overrides VS Code settings)", description: "" } : { label: "$(settings-gear) Herb Configuration Source", detail: "VS Code Settings (personal settings)", - description: "no .herb.yml" + description: "no .herb.yaml" } if (!hasConfigFile) { @@ -110,7 +110,7 @@ export async function showConfigDetails() { if (hasConfigFile) { items.push({ - label: "$(edit) Edit .herb.yml", + label: "$(edit) Edit .herb.yaml", description: "Open configuration file", action: async () => { const document = await vscode.workspace.openTextDocument(configPath!) @@ -119,7 +119,7 @@ export async function showConfigDetails() { }) } else { items.push({ - label: "$(new-file) Create .herb.yml", + label: "$(new-file) Create .herb.yaml", description: "Create project configuration file", action: async () => { await vscode.commands.executeCommand('herb.createConfig') diff --git a/javascript/packages/vscode/src/config-provider.ts b/javascript/packages/vscode/src/config-provider.ts index 965819ca4..0e1885c10 100644 --- a/javascript/packages/vscode/src/config-provider.ts +++ b/javascript/packages/vscode/src/config-provider.ts @@ -124,7 +124,7 @@ export class HerbConfigProvider implements vscode.TreeDataProvider { if (this.config) { const configFileItem = new ConfigItem( "Project Settings", - "Configuration from .herb.yml", + "Configuration from .herb.yaml", vscode.TreeItemCollapsibleState.None, 'configFile' ) @@ -181,7 +181,7 @@ export class HerbConfigProvider implements vscode.TreeDataProvider { } else if (this.configError) { const errorItem = new ConfigItem( "Configuration has errors", - "Click to view and fix errors in .herb.yml", + "Click to view and fix errors in .herb.yaml", vscode.TreeItemCollapsibleState.None, 'configError' ) @@ -253,7 +253,7 @@ export class HerbConfigProvider implements vscode.TreeDataProvider { items.push(formatterItem) const createConfigItem = new ConfigItem( - "Create .herb.yml", + "Create .herb.yaml", "Share settings with your project", vscode.TreeItemCollapsibleState.None, 'createConfig' @@ -281,7 +281,7 @@ export class HerbConfigProvider implements vscode.TreeDataProvider { await Config.mutateConfigFile(configPath, {}) - vscode.window.showInformationMessage("Created Herb configuration file (.herb.yml)") + vscode.window.showInformationMessage("Created Herb configuration file (.herb.yaml)") await this.refresh() const document = await vscode.workspace.openTextDocument(configPath) @@ -304,7 +304,7 @@ export class HerbConfigProvider implements vscode.TreeDataProvider { await vscode.window.showTextDocument(document) } catch (_error) { const result = await vscode.window.showInformationMessage( - "Herb configuration file (.herb.yml) not found. Would you like to create one?", + "Herb configuration file (.herb.yaml) not found. Would you like to create one?", "Create Config", "Cancel" ) diff --git a/javascript/packages/vscode/src/extension.ts b/javascript/packages/vscode/src/extension.ts index ea88c22cc..d53d07961 100644 --- a/javascript/packages/vscode/src/extension.ts +++ b/javascript/packages/vscode/src/extension.ts @@ -46,14 +46,14 @@ async function updateConfigStatusBarItem() { try { await vscode.workspace.fs.stat(vscode.Uri.file(configPath)) - configStatusBarItem.text = '$(file-code) .herb.yml (Project Settings)' - configStatusBarItem.tooltip = 'Herb configuration loaded from .herb.yml (overrides VS Code settings)\n\nClick to view configuration details' + configStatusBarItem.text = '$(file-code) .herb.yaml (Project Settings)' + configStatusBarItem.tooltip = 'Herb configuration loaded from .herb.yaml (overrides VS Code settings)\n\nClick to view configuration details' configStatusBarItem.command = 'herb.showConfigDetails' configStatusBarItem.show() } catch (_error) { configStatusBarItem.text = '$(settings-gear) Herb (Personal Settings)' - configStatusBarItem.tooltip = 'Herb using personal VS Code settings\n\nClick to view configuration details or create .herb.yml' + configStatusBarItem.tooltip = 'Herb using personal VS Code settings\n\nClick to view configuration details or create .herb.yaml' configStatusBarItem.command = 'herb.showConfigDetails' configStatusBarItem.show() diff --git a/javascript/packages/vscode/src/herb-analysis-provider.ts b/javascript/packages/vscode/src/herb-analysis-provider.ts index 28269a326..82f41320d 100644 --- a/javascript/packages/vscode/src/herb-analysis-provider.ts +++ b/javascript/packages/vscode/src/herb-analysis-provider.ts @@ -76,7 +76,7 @@ export class HerbAnalysisProvider implements TreeDataProvider { } } catch (_error) { window.showErrorMessage( - 'Cannot run Herb analysis: Configuration file has errors. Please fix .herb.yml and try again.', + 'Cannot run Herb analysis: Configuration file has errors. Please fix .herb.yaml and try again.', 'Edit Config' ).then(selection => { if (selection === 'Edit Config') { diff --git a/javascript/packages/vscode/src/herb-settings-commands.ts b/javascript/packages/vscode/src/herb-settings-commands.ts index 4a4ecc380..142e09c89 100644 --- a/javascript/packages/vscode/src/herb-settings-commands.ts +++ b/javascript/packages/vscode/src/herb-settings-commands.ts @@ -4,7 +4,7 @@ import { Config } from "@herb-tools/config" import { HerbConfigProvider } from "./config-provider" /** - * Commands to modify .herb.yml settings directly through VS Code + * Commands to modify .herb.yaml settings directly through VS Code */ export class HerbSettingsCommands { constructor(private context: vscode.ExtensionContext, private configProvider?: HerbConfigProvider) { diff --git a/javascript/packages/vscode/src/issue-reporter.ts b/javascript/packages/vscode/src/issue-reporter.ts index a2551a6f3..01b57cc7b 100644 --- a/javascript/packages/vscode/src/issue-reporter.ts +++ b/javascript/packages/vscode/src/issue-reporter.ts @@ -30,14 +30,14 @@ function createSettingsSection(settings: PersonalHerbSettings): string { ` if (settings.projectConfig) { - section += `\n\n**Herb Configuration (.herb.yml):** + section += `\n\n**Herb Configuration (.herb.yaml):** \`\`\`yaml ${settings.projectConfig} \`\`\` ` } else { - section += `\n\n**Herb Configuration (.herb.yml):** - No .herb.yml file found in workspace. + section += `\n\n**Herb Configuration (.herb.yaml):** + No .herb.yaml file found in workspace. ` } diff --git a/javascript/packages/vscode/src/tree-item-builder.ts b/javascript/packages/vscode/src/tree-item-builder.ts index ed6da1489..4f38c1c04 100644 --- a/javascript/packages/vscode/src/tree-item-builder.ts +++ b/javascript/packages/vscode/src/tree-item-builder.ts @@ -101,7 +101,7 @@ export class TreeItemBuilder { ) item.iconPath = new vscode.ThemeIcon('circle-slash', new vscode.ThemeColor('charts.gray')) - item.tooltip = 'Linting is disabled in the Herb configuration (.herb.yml). Click to toggle linter setting.' + item.tooltip = 'Linting is disabled in the Herb configuration (.herb.yaml). Click to toggle linter setting.' item.command = { command: 'herb.toggleLinter', title: 'Toggle Linter' @@ -347,7 +347,7 @@ export class TreeItemBuilder { ) item.iconPath = new vscode.ThemeIcon('circle-slash', new vscode.ThemeColor('charts.gray')) - item.tooltip = 'Formatting is disabled in the Herb configuration (.herb.yml). Click to toggle formatter setting.' + item.tooltip = 'Formatting is disabled in the Herb configuration (.herb.yaml). Click to toggle formatter setting.' item.command = { command: 'herb.toggleFormatter', title: 'Toggle Formatter' diff --git a/lib/herb/action_view/render_analyzer.rb b/lib/herb/action_view/render_analyzer.rb index 61a10739b..d61550af5 100644 --- a/lib/herb/action_view/render_analyzer.rb +++ b/lib/herb/action_view/render_analyzer.rb @@ -41,7 +41,7 @@ def check! if configuration.config_path puts "#{green("\u2713")} Using Herb config file at #{dimmed(configuration.config_path.to_s)}" else - puts dimmed("No .herb.yml found, using defaults") + puts dimmed("No .herb.yaml found, using defaults") end puts dimmed("Checking render calls in #{erb_files.count} #{pluralize(erb_files.count, "file")}...") @@ -234,7 +234,7 @@ def graph! if configuration.config_path puts "#{green("\u2713")} Using Herb config file at #{dimmed(configuration.config_path.to_s)}" else - puts dimmed("No .herb.yml found, using defaults") + puts dimmed("No .herb.yaml found, using defaults") end puts dimmed("Building render graph for #{erb_files.count} #{pluralize(erb_files.count, "file")}...") diff --git a/lib/herb/cli.rb b/lib/herb/cli.rb index 588c26b14..11cbc3b6c 100644 --- a/lib/herb/cli.rb +++ b/lib/herb/cli.rb @@ -380,7 +380,7 @@ def run_actionview_command The project at '#{project}' is not configured to use ActionView (current framework: '#{config.framework}'). - To enable ActionView support, add the following to your `.herb.yml`: + To enable ActionView support, add the following to your `.herb.yaml`: framework: actionview MESSAGE diff --git a/lib/herb/project.rb b/lib/herb/project.rb index 712b28841..6b22e6814 100644 --- a/lib/herb/project.rb +++ b/lib/herb/project.rb @@ -205,7 +205,7 @@ def analyze! if configuration.config_path puts "#{green("✓")} Using Herb config file at #{dimmed(configuration.config_path)}" else - puts dimmed("No .herb.yml found, using defaults") + puts dimmed("No .herb.yaml found, using defaults") end puts dimmed("Analyzing #{files.count} #{pluralize(files.count, "file")}...") From 45c613b9886c06381850996104751f954c4ef796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 19:13:51 +0200 Subject: [PATCH 14/24] Config: Add precedence test for .herb.yaml over .herb.yml Co-Authored-By: Claude Opus 4.6 (1M context) --- javascript/packages/config/test/config.test.ts | 8 ++++++++ rust/herb-config/tests/herb_config_test.rs | 13 +++++++++++++ test/configuration_test.rb | 14 ++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/javascript/packages/config/test/config.test.ts b/javascript/packages/config/test/config.test.ts index 676d72199..275741093 100644 --- a/javascript/packages/config/test/config.test.ts +++ b/javascript/packages/config/test/config.test.ts @@ -80,6 +80,14 @@ describe("@herb-tools/config", () => { const configPath = Config.configPathFromProjectPath(testDir) expect(configPath).toBe(join(testDir, ".herb.yml")) }) + + test("prefers .herb.yaml over .herb.yml when both exist", () => { + writeFileSync(join(testDir, ".herb.yaml"), "version: 0.9.7\n") + writeFileSync(join(testDir, ".herb.yml"), "version: 0.9.7\n") + + const configPath = Config.configPathFromProjectPath(testDir) + expect(configPath).toBe(join(testDir, ".herb.yaml")) + }) }) describe("Config.exists", () => { diff --git a/rust/herb-config/tests/herb_config_test.rs b/rust/herb-config/tests/herb_config_test.rs index 6bb57bb6f..4f3d5bfc7 100644 --- a/rust/herb-config/tests/herb_config_test.rs +++ b/rust/herb-config/tests/herb_config_test.rs @@ -373,6 +373,19 @@ fn find_config_file_finds_yaml_extension() { assert_eq!(found, Some(config_path)); } +#[test] +fn find_config_file_prefers_yaml_over_yml() { + let dir = tempfile::tempdir().unwrap(); + let yaml_path = dir.path().join(".herb.yaml"); + let yml_path = dir.path().join(".herb.yml"); + fs::write(&yaml_path, "version: 0.9.7\n").unwrap(); + fs::write(&yml_path, "version: 0.8.0\n").unwrap(); + + let found = HerbConfig::find_config_file(dir.path()); + + assert_eq!(found, Some(yaml_path)); +} + #[test] fn find_config_file_finds_config_in_current_dir() { let dir = tempfile::tempdir().unwrap(); diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 94666ac5a..461723dd5 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -41,6 +41,20 @@ def write_config(content, filename = ".herb.yml") assert_includes config.file_include_patterns, "**/*.custom.erb" end + test "prefers .herb.yaml over .herb.yml when both exist" do + write_config(<<~YAML, ".herb.yaml") + version: "0.9.7" + YAML + write_config(<<~YAML) + version: "0.8.0" + YAML + + config = Herb::Configuration.load(@temp_dir) + + assert_equal File.join(@temp_dir, ".herb.yaml"), config.config_path.to_s + assert_equal "0.9.7", config.version + end + test "loads configuration from .herb.yml" do write_config(<<~YAML) version: "0.9.7" From 6720f8408764a823db77c2e6c3dc0021da9bbc08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 19:23:00 +0200 Subject: [PATCH 15/24] Config: Add load test with .herb.yaml Co-Authored-By: Claude Opus 4.6 (1M context) --- javascript/packages/config/test/config.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/javascript/packages/config/test/config.test.ts b/javascript/packages/config/test/config.test.ts index 275741093..1a276997d 100644 --- a/javascript/packages/config/test/config.test.ts +++ b/javascript/packages/config/test/config.test.ts @@ -1440,6 +1440,15 @@ describe("@herb-tools/config", () => { expect(config.configVersion).toBeUndefined() }) + test("load preserves user config version from .herb.yaml", async () => { + createTestFile(testDir, ".herb.yaml", "version: 0.8.0\n\nlinter:\n enabled: true\n") + + const config = await Config.load(testDir, { version: "0.9.7", silent: true }) + + expect(config.version).toBe("0.9.7") + expect(config.configVersion).toBe("0.8.0") + }) + test("load preserves user config version from .herb.yml", async () => { createTestFile(testDir, ".herb.yml", "version: 0.8.0\n\nlinter:\n enabled: true\n") From d40fd52930074289d7e1a67e2ec1f63bd6553c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 19:35:28 +0200 Subject: [PATCH 16/24] Rust: Add find_project_root test with .herb.yaml Co-Authored-By: Claude Opus 4.6 (1M context) --- rust/herb-config/tests/herb_config_test.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/rust/herb-config/tests/herb_config_test.rs b/rust/herb-config/tests/herb_config_test.rs index 4f3d5bfc7..e6d397c13 100644 --- a/rust/herb-config/tests/herb_config_test.rs +++ b/rust/herb-config/tests/herb_config_test.rs @@ -397,6 +397,20 @@ fn find_config_file_finds_config_in_current_dir() { assert_eq!(found, Some(config_path)); } +#[test] +fn find_project_root_with_yaml_extension() { + let dir = tempfile::tempdir().unwrap(); + let config_path = dir.path().join(".herb.yaml"); + fs::write(&config_path, "version: 0.9.7\n").unwrap(); + + let sub_dir = dir.path().join("app").join("views"); + fs::create_dir_all(&sub_dir).unwrap(); + + let root = HerbConfig::find_project_root(&sub_dir); + + assert_eq!(root, dir.path().to_path_buf()); +} + #[test] fn find_config_file_walks_up_directory_tree() { let dir = tempfile::tempdir().unwrap(); From 1d21678c62146b7480d27384722c7f6996029018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 19:45:09 +0200 Subject: [PATCH 17/24] Rust: Add load_from_path test with .herb.yaml Co-Authored-By: Claude Opus 4.6 (1M context) --- rust/herb-config/tests/herb_config_test.rs | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/rust/herb-config/tests/herb_config_test.rs b/rust/herb-config/tests/herb_config_test.rs index e6d397c13..29d1dc44a 100644 --- a/rust/herb-config/tests/herb_config_test.rs +++ b/rust/herb-config/tests/herb_config_test.rs @@ -311,6 +311,29 @@ fn to_formatter_config_uses_defaults() { assert_eq!(formatter_config.max_line_length, 80); } +#[test] +fn load_from_path_parses_yaml_extension() { + let dir = tempfile::tempdir().unwrap(); + let config_path = dir.path().join(".herb.yaml"); + fs::write( + &config_path, + r#" +version: 0.9.7 +linter: + enabled: true +formatter: + enabled: false +"#, + ) + .unwrap(); + + let config = HerbConfig::load_from_path(&config_path).unwrap(); + + assert_eq!(config.version, Some("0.9.7".into())); + assert!(config.linter.enabled); + assert!(!config.formatter.enabled); +} + #[test] fn load_from_path_parses_yaml() { let dir = tempfile::tempdir().unwrap(); From 0d806079968accec4039c9723893c06f6dda28b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 19:45:32 +0200 Subject: [PATCH 18/24] Rust: Add config filenames to PROJECT_INDICATORS Co-Authored-By: Claude Opus 4.6 (1M context) --- rust/herb-config/src/herb_config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/herb-config/src/herb_config.rs b/rust/herb-config/src/herb_config.rs index de00d63bd..ef1f08ec9 100644 --- a/rust/herb-config/src/herb_config.rs +++ b/rust/herb-config/src/herb_config.rs @@ -126,7 +126,7 @@ pub const DEFAULT_INCLUDE_PATTERNS: &[&str] = &[ pub const DEFAULT_EXCLUDE_PATTERNS: &[&str] = &["coverage/**/*", "log/**/*", "node_modules/**/*", "storage/**/*", "tmp/**/*", "vendor/**/*"]; const CONFIG_FILE_NAMES: &[&str] = &[".herb.yaml", ".herb.yml"]; -const PROJECT_INDICATORS: &[&str] = &[".git", ".herb", "Gemfile", "package.json", "Rakefile", "README.md"]; +const PROJECT_INDICATORS: &[&str] = &[".git", ".herb", ".herb.yaml", ".herb.yml", "Gemfile", "package.json", "Rakefile", "README.md"]; impl HerbConfig { pub fn load(start_path: &Path, explicit_config_file: Option<&str>) -> (Self, Option) { From 5d1caa920a0db1cae33931426d3c6325d50379d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 19:46:44 +0200 Subject: [PATCH 19/24] Docs: Update references from .herb.yml to .herb.yaml Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/docs/configuration.md | 44 +++++++++---------- docs/docs/projects/dev-server.md | 2 +- docs/docs/projects/engine.md | 6 +-- javascript/packages/config/README.md | 10 ++--- javascript/packages/formatter/README.md | 10 ++--- javascript/packages/language-server/README.md | 6 +-- javascript/packages/linter/README.md | 12 ++--- .../docs/rules/erb-strict-locals-required.md | 4 +- .../linter/docs/rules/html-no-self-closing.md | 4 +- javascript/packages/rewriter/README.md | 6 +-- javascript/packages/vscode/README.md | 2 +- 11 files changed, 53 insertions(+), 53 deletions(-) diff --git a/docs/docs/configuration.md b/docs/docs/configuration.md index beb158161..5817f4e17 100644 --- a/docs/docs/configuration.md +++ b/docs/docs/configuration.md @@ -1,14 +1,14 @@ # Configuration -Herb uses a `.herb.yml` configuration file to customize how the tools behave in your project. This configuration is shared across all Herb tools including the linter, formatter, and language server. +Herb uses a `.herb.yaml` configuration file to customize how the tools behave in your project. This configuration is shared across all Herb tools including the linter, formatter, and language server. ## Configuration File Location -The configuration file should be placed in your project root as `.herb.yml`: +The configuration file should be placed in your project root as `.herb.yaml`: ``` your-project/ -├── .herb.yml # Herb configuration +├── .herb.yaml # Herb configuration ├── Gemfile ├── app/ └── ... @@ -18,7 +18,7 @@ your-project/ Configuration settings are applied in the following order (highest to lowest priority): -1. **Project configuration** (`.herb.yml` file) +1. **Project configuration** (`.herb.yaml` file) 2. **Editor settings** (VS Code workspace/user settings) 3. **Default settings** @@ -26,7 +26,7 @@ Configuration settings are applied in the following order (highest to lowest pri ### Default Behavior (No Config File) -If no `.herb.yml` file exists in your project: +If no `.herb.yaml` file exists in your project: - **Language Server**: Uses built-in defaults and works out-of-the-box - **Linter**: Enabled with all rules (automatic exclusion of `parser-no-errors` in language server only) @@ -34,12 +34,12 @@ If no `.herb.yml` file exists in your project: - **Editor settings**: VS Code user/workspace settings are respected ::: tip Recommended for Projects -If you're using Herb in a project with multiple developers, it's highly recommended to create a `.herb.yml` configuration file and commit it to your repository. This ensures all team members use consistent linting rules and formatting settings, preventing configuration drift and maintaining code quality standards across the project. +If you're using Herb in a project with multiple developers, it's highly recommended to create a `.herb.yaml` configuration file and commit it to your repository. This ensures all team members use consistent linting rules and formatting settings, preventing configuration drift and maintaining code quality standards across the project. ::: ### Creating a Configuration File -To create a `.herb.yml` configuration file in your project, run either CLI tool with the `--init` flag: +To create a `.herb.yaml` configuration file in your project, run either CLI tool with the `--init` flag: ```bash # Create config using the linter @@ -59,13 +59,13 @@ This will generate a configuration file with sensible defaults: The CLIs support a `--force` flag to override project configuration: ```bash -# Force linting even if disabled in .herb.yml +# Force linting even if disabled in .herb.yaml herb-lint --force app/views/ # Force linting on a file excluded by configuration herb-lint --force app/views/excluded-file.html.erb -# Force formatting even if disabled in .herb.yml +# Force formatting even if disabled in .herb.yaml herb-format --force --check app/views/ ``` @@ -75,7 +75,7 @@ When using `--force` on an explicitly specified file that is excluded by configu Configure the linter behavior and rules: -```yaml [.herb.yml] +```yaml [.herb.yaml] linter: enabled: true # Enable/disable linter globally @@ -182,7 +182,7 @@ linter: Example: -```yaml [.herb.yml] +```yaml [.herb.yaml] linter: rules: # This rule only runs on component files, excluding legacy ones @@ -206,7 +206,7 @@ linter: Configure the template engine behavior: -```yaml [.herb.yml] +```yaml [.herb.yaml] engine: validators: security: true # Enable/disable security validation (default: true) @@ -226,7 +226,7 @@ When a validator is disabled (`false`), the engine skips it entirely during comp ### Validation Mode -The `validation_mode` controls how the engine handles validation results. This is set programmatically when creating an `Herb::Engine` instance, not in `.herb.yml`: +The `validation_mode` controls how the engine handles validation results. This is set programmatically when creating an `Herb::Engine` instance, not in `.herb.yaml`: - **`:raise`** (default): Raises an exception when validation errors are found - **`:overlay`**: Renders validation errors as an in-browser overlay (used by [ReActionView](https://github.com/marcoroth/reactionview) in development) @@ -249,7 +249,7 @@ Herb::Engine.new(source, validation_mode: :none) ### Overriding Validators Programmatically -Validator settings from `.herb.yml` can be overridden when creating an engine instance: +Validator settings from `.herb.yaml` can be overridden when creating an engine instance: **Disable security validator for this template only** ```ruby @@ -269,7 +269,7 @@ Herb::Engine.new(source, validators: { Configure the formatter behavior: -```yaml [.herb.yml] +```yaml [.herb.yaml] formatter: enabled: false # Disabled by default (experimental) indentWidth: 2 # Number of spaces for indentation @@ -294,14 +294,14 @@ formatter: - **`exclude`**: Array of glob patterns - Additional patterns to exclude from formatting (additive to defaults) ::: warning Experimental Feature -The formatter is currently experimental. Enable it in `.herb.yml` and test thoroughly before using in production. +The formatter is currently experimental. Enable it in `.herb.yaml` and test thoroughly before using in production. ::: ## Top-Level File Configuration Global file configuration that applies to both linter and formatter: -```yaml [.herb.yml] +```yaml [.herb.yaml] files: # Additional glob patterns to include (additive to defaults, applies to all tools) include: @@ -326,7 +326,7 @@ File patterns are merged in the following order: Example: -```yaml [.herb.yml] +```yaml [.herb.yaml] files: include: - '**/*.xml.erb' # Applies to all tools @@ -379,7 +379,7 @@ bundle exec herb config ../other-project/ Running `bundle exec herb config .` displays: - **Project root**: The detected project root directory -- **Config file**: Path to the `.herb.yml` file (or "(using defaults)" if none) +- **Config file**: Path to the `.herb.yaml` file (or "(using defaults)" if none) - **Include patterns**: All patterns used to find files - **Exclude patterns**: All patterns used to exclude files - **Files**: List of included and excluded files with their status @@ -390,7 +390,7 @@ Example output: Herb Configuration Project root: /path/to/project -Config file: /path/to/project/.herb.yml +Config file: /path/to/project/.herb.yaml Include patterns: + **/*.herb @@ -434,7 +434,7 @@ Example output: Herb Configuration for Linter Project root: /path/to/project -Config file: /path/to/project/.herb.yml +Config file: /path/to/project/.herb.yaml Include patterns (files + linter): + **/*.html.erb @@ -456,7 +456,7 @@ Files for linter (40 included, 7 excluded): ::: tip Debugging Configuration The `bundle exec herb config` command is useful for: -- Verifying your `.herb.yml` is being read correctly +- Verifying your `.herb.yaml` is being read correctly - Understanding why certain files are included or excluded - Debugging tool-specific patterns - Confirming the project root detection diff --git a/docs/docs/projects/dev-server.md b/docs/docs/projects/dev-server.md index 8c566d577..c84b52cc3 100644 --- a/docs/docs/projects/dev-server.md +++ b/docs/docs/projects/dev-server.md @@ -36,7 +36,7 @@ The dev server consists of two parts: Herb: 0.9.7 Project: /path/to/project - Config: .herb.yml + Config: .herb.yaml Files: 453 templates indexed WebSocket: ws://localhost:8592 diff --git a/docs/docs/projects/engine.md b/docs/docs/projects/engine.md index 19b530b34..3028c0816 100644 --- a/docs/docs/projects/engine.md +++ b/docs/docs/projects/engine.md @@ -50,7 +50,7 @@ In addition to Erubi options, `Herb::Engine` supports: ## Validators -The engine runs validators on parsed templates to catch errors before compilation. Each validator can be enabled or disabled via [`.herb.yml` configuration](/configuration#engine-configuration) or per-instance overrides. +The engine runs validators on parsed templates to catch errors before compilation. Each validator can be enabled or disabled via [`.herb.yaml` configuration](/configuration#engine-configuration) or per-instance overrides. | Validator | Description | |---|---| @@ -63,7 +63,7 @@ Disable security validator for this template: Herb::Engine.new(source, validators: { security: false }) ``` -See [Engine Configuration](/configuration#engine-configuration) for `.herb.yml` configuration. +See [Engine Configuration](/configuration#engine-configuration) for `.herb.yaml` configuration. ## Validation Mode @@ -77,4 +77,4 @@ Controls how the engine presents validation results: [ReActionView](https://github.com/marcoroth/reactionview) registers `Herb::Engine` as the template handler for `.html.erb` and `.html.herb` files in Rails. It uses `validation_mode: :overlay` so validation errors appear as in-browser overlays during development instead of raising exceptions. -Validator settings from `.herb.yml` are respected automatically — no ReActionView-specific configuration needed. +Validator settings from `.herb.yaml` are respected automatically — no ReActionView-specific configuration needed. diff --git a/javascript/packages/config/README.md b/javascript/packages/config/README.md index 39c69f600..55547e6a9 100644 --- a/javascript/packages/config/README.md +++ b/javascript/packages/config/README.md @@ -28,11 +28,11 @@ bun add @herb-tools/config ## Usage -### `.herb.yml` +### `.herb.yaml` -The configuration is stored in a `.herb.yml` file in the project root: +The configuration is stored in a `.herb.yaml` file in the project root: -```yaml [.herb.yml] +```yaml [.herb.yaml] version: 0.9.7 linter: @@ -51,8 +51,8 @@ formatter: The Herb tools follow this configuration priority: -1. **Project configuration** (`.herb.yml` file) - Highest priority +1. **Project configuration** (`.herb.yaml` file) - Highest priority 2. **Editor settings** (VS Code workspace/user settings) 3. **Default settings** - Fallback when no other configuration exists -This allows teams to share consistent settings via `.herb.yml` while still allowing individual developer preferences when no project configuration exists. +This allows teams to share consistent settings via `.herb.yaml` while still allowing individual developer preferences when no project configuration exists. diff --git a/javascript/packages/formatter/README.md b/javascript/packages/formatter/README.md index 25ba384ee..7b21e0025 100644 --- a/javascript/packages/formatter/README.md +++ b/javascript/packages/formatter/README.md @@ -145,7 +145,7 @@ herb-format templates/ **Initialize configuration:** ```bash -# Create a .herb.yml configuration file +# Create a .herb.yaml configuration file herb-format --init ``` @@ -186,7 +186,7 @@ herb-format --version ## Configuration -Create a `.herb.yml` file in your project root to configure the formatter: +Create a `.herb.yaml` file in your project root to configure the formatter: ```bash herb-format --init @@ -194,7 +194,7 @@ herb-format --init ### Basic Configuration -```yaml [.herb.yml] +```yaml [.herb.yaml] formatter: enabled: true # Must be enabled for formatting to work indentWidth: 2 @@ -276,9 +276,9 @@ The `<%# herb:formatter ignore %>` directive must be an exact match. Extra text The formatter supports **rewriters** that allow you to transform templates before and after formatting. -Configure rewriters in your `.herb.yml`: +Configure rewriters in your `.herb.yaml`: -```yaml [.herb.yml] +```yaml [.herb.yaml] formatter: enabled: true indentWidth: 2 diff --git a/javascript/packages/language-server/README.md b/javascript/packages/language-server/README.md index 131d657cc..63bc45ff2 100644 --- a/javascript/packages/language-server/README.md +++ b/javascript/packages/language-server/README.md @@ -110,14 +110,14 @@ npx @herb-tools/language-server --stdio ## Configuration -The language server can be configured using a `.herb.yml` file in your project root. This configuration is shared across all Herb tools including the linter, formatter, and language server. +The language server can be configured using a `.herb.yaml` file in your project root. This configuration is shared across all Herb tools including the linter, formatter, and language server. See the [Configuration documentation](https://herb-tools.dev/configuration) for full details. ### Example Configuration ```yaml -# .herb.yml +# .herb.yaml linter: enabled: true @@ -127,4 +127,4 @@ formatter: maxLineLength: 80 ``` -**Note**: VS Code users can also control settings through `languageServerHerb.*` settings in VS Code preferences. Project configuration in `.herb.yml` takes precedence over editor settings. +**Note**: VS Code users can also control settings through `languageServerHerb.*` settings in VS Code preferences. Project configuration in `.herb.yaml` takes precedence over editor settings. diff --git a/javascript/packages/linter/README.md b/javascript/packages/linter/README.md index c6fc4fb01..98e5a8d9a 100644 --- a/javascript/packages/linter/README.md +++ b/javascript/packages/linter/README.md @@ -136,7 +136,7 @@ npx @herb-tools/linter "**/*.xml.erb" **Initialize configuration:** ```bash -# Create a .herb.yml configuration file +# Create a .herb.yaml configuration file npx @herb-tools/linter --init ``` @@ -206,8 +206,8 @@ npx @herb-tools/linter template.html.erb --fail-level hint By default, the linter exits with code `1` only when errors are present. The `--fail-level` option allows you to control this behavior for CI/CD pipelines where you want stricter enforcement. Valid values are: `error` (default), `warning`, `info`, `hint`. -This can also be configured in `.herb.yml`: -```yaml [.herb.yml] +This can also be configured in `.herb.yaml`: +```yaml [.herb.yaml] linter: failLevel: warning ``` @@ -429,7 +429,7 @@ The `<%# herb:linter ignore %>` directive must be an exact match. Extra text or ## Configuration -Create a `.herb.yml` file in your project root to configure the linter: +Create a `.herb.yaml` file in your project root to configure the linter: ```bash npx @herb-tools/linter --init @@ -437,7 +437,7 @@ npx @herb-tools/linter --init ### Basic Configuration -```yaml [.herb.yml] +```yaml [.herb.yaml] linter: enabled: true @@ -468,7 +468,7 @@ linter: Apply rules to specific files using `include`, `only`, and `exclude` patterns: -```yaml [.herb.yml] +```yaml [.herb.yaml] linter: rules: # Apply rule only to component files diff --git a/javascript/packages/linter/docs/rules/erb-strict-locals-required.md b/javascript/packages/linter/docs/rules/erb-strict-locals-required.md index 9238d03d4..4b2a7077a 100644 --- a/javascript/packages/linter/docs/rules/erb-strict-locals-required.md +++ b/javascript/packages/linter/docs/rules/erb-strict-locals-required.md @@ -27,9 +27,9 @@ This rule encourages partials to be explicit about what they expect. Partials th ## Configuration -This rule is disabled by default. To enable it, add to your [`.herb.yml`](/configuration): +This rule is disabled by default. To enable it, add to your [`.herb.yaml`](/configuration): -```yaml [.herb.yml] +```yaml [.herb.yaml] linter: rules: erb-strict-locals-required: diff --git a/javascript/packages/linter/docs/rules/html-no-self-closing.md b/javascript/packages/linter/docs/rules/html-no-self-closing.md index fce7c98d0..0362aefff 100644 --- a/javascript/packages/linter/docs/rules/html-no-self-closing.md +++ b/javascript/packages/linter/docs/rules/html-no-self-closing.md @@ -28,9 +28,9 @@ XHTML and HTML styles. This rule is automatically disabled for ActionMailer view files (paths matching `**/views/**/*_mailer/**/*`) because older email clients, particularly legacy versions of Microsoft Outlook, require self-closing syntax for proper rendering of void elements like `
`. ::: -If you want to enable this rule for email templates as well, you can override the default exclusion in your `.herb.yml` configuration: +If you want to enable this rule for email templates as well, you can override the default exclusion in your `.herb.yaml` configuration: -```yaml [.herb.yml] +```yaml [.herb.yaml] linter: rules: html-no-self-closing: diff --git a/javascript/packages/rewriter/README.md b/javascript/packages/rewriter/README.md index 135a43e44..b6fef8b53 100644 --- a/javascript/packages/rewriter/README.md +++ b/javascript/packages/rewriter/README.md @@ -207,11 +207,11 @@ By default, rewriters are auto-discovered from: `.herb/rewriters/**/*.mjs` Custom rewriters must use the `.mjs` extension to avoid Node.js module type warnings. The `.mjs` extension explicitly marks files as ES modules. ::: -#### Configuring in `.herb.yml` +#### Configuring in `.herb.yaml` Reference custom rewriters by their name in your configuration: -```yaml [.herb.yml] +```yaml [.herb.yaml] formatter: enabled: true rewriter: @@ -339,4 +339,4 @@ class MyRewriter extends StringRewriter { - [Formatter Documentation](/projects/formatter) - Using rewriters with the formatter - [Core Documentation](/projects/core) - AST node types and visitor pattern -- [Config Documentation](/projects/config) - Configuring rewriters in `.herb.yml` +- [Config Documentation](/projects/config) - Configuring rewriters in `.herb.yaml` diff --git a/javascript/packages/vscode/README.md b/javascript/packages/vscode/README.md index dfe51e961..66d1404d2 100644 --- a/javascript/packages/vscode/README.md +++ b/javascript/packages/vscode/README.md @@ -18,7 +18,7 @@ If you are looking to use Herb in another editor, check out the instructions on ## Configuration -The extension can be configured through VS Code settings or a `.herb.yml` file in your project root. Project configuration in `.herb.yml` takes precedence over VS Code settings. +The extension can be configured through VS Code settings or a `.herb.yaml` file in your project root. Project configuration in `.herb.yaml` takes precedence over VS Code settings. See the [Configuration documentation](https://herb-tools.dev/configuration) for full details. From 89cbcb7ffcad01a6ca25ade396ba961d8f263fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Thu, 23 Apr 2026 20:48:07 +0200 Subject: [PATCH 20/24] Config: Preserve explicit config path in Config constructor Co-Authored-By: Claude Opus 4.6 (1M context) --- javascript/packages/config/src/config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/javascript/packages/config/src/config.ts b/javascript/packages/config/src/config.ts index 5d3561891..848a57349 100644 --- a/javascript/packages/config/src/config.ts +++ b/javascript/packages/config/src/config.ts @@ -126,8 +126,8 @@ export class Config { public config: HerbConfig public readonly configVersion: string | undefined - constructor(projectPath: string, config: HerbConfig, configVersion?: string) { - this.path = Config.configPathFromProjectPath(projectPath) + constructor(projectPath: string, config: HerbConfig, configVersion?: string, configPath?: string) { + this.path = configPath || Config.configPathFromProjectPath(projectPath) this.config = config this.configVersion = configVersion } @@ -1175,7 +1175,7 @@ export class Config { resolved.version = version - return new Config(projectRoot, resolved, userConfigVersion) + return new Config(projectRoot, resolved, userConfigVersion, configPath) } /** From 6a31cde6dba40bafa943add109f629e7fbe3440a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Fri, 3 Jul 2026 16:13:37 +0200 Subject: [PATCH 21/24] Config: Use imported existsSync instead of require('fs') in configPathFromProjectPath The bare require('fs') call was preserved verbatim in the ESM bundle (rollup format 'esm', fs external), so any Node ESM consumer constructing a Config without an explicit configPath crashed with "ReferenceError: require is not defined". Co-Authored-By: Claude Fable 5 --- javascript/packages/config/src/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/javascript/packages/config/src/config.ts b/javascript/packages/config/src/config.ts index 848a57349..09bcd919d 100644 --- a/javascript/packages/config/src/config.ts +++ b/javascript/packages/config/src/config.ts @@ -1,6 +1,6 @@ import path from "path" -import { promises as fs } from "fs" +import { promises as fs, existsSync } from "fs" import { stringify, parse, parseDocument, isMap } from "yaml" import { ZodError } from "zod" import { fromZodError } from "zod-validation-error" @@ -456,7 +456,7 @@ export class Config { for (const configPath of this.configPaths) { const candidate = path.join(projectPath, configPath) - if (require('fs').existsSync(candidate)) { + if (existsSync(candidate)) { return candidate } } From 21bd245f35df03c78e05c6627b93b3f53282d7da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Fri, 3 Jul 2026 17:28:40 +0200 Subject: [PATCH 22/24] VS Code / Language Server: Derive UI labels from the resolved config path Status bar, config views, quick-fix titles, warning-popup buttons, and version-mismatch warnings hardcoded .herb.yaml even when the loaded config file was the legacy .herb.yml, pointing users at a file that doesn't exist in their project. Messages that direct the user to the config file now use the basename of the resolved path; messages that only describe configuration state drop the filename. Creation flows intentionally keep the .herb.yaml default. Co-Authored-By: Claude Fable 5 --- .../src/code_action_service.ts | 6 +++-- .../language-server/src/formatting_service.ts | 25 +++---------------- .../language-server/src/linter_service.ts | 15 ++--------- .../packages/language-server/src/service.ts | 6 +++-- .../packages/language-server/src/utils.ts | 23 +++++++++++++++++ .../vscode/src/config-details-provider.ts | 7 +++--- .../packages/vscode/src/config-provider.ts | 11 ++++++-- javascript/packages/vscode/src/extension.ts | 5 ++-- .../vscode/src/herb-analysis-provider.ts | 3 ++- .../packages/vscode/src/issue-reporter.ts | 6 ++--- .../packages/vscode/src/tree-item-builder.ts | 4 +-- 11 files changed, 59 insertions(+), 52 deletions(-) diff --git a/javascript/packages/language-server/src/code_action_service.ts b/javascript/packages/language-server/src/code_action_service.ts index cb0888eb6..b9b51e4bc 100644 --- a/javascript/packages/language-server/src/code_action_service.ts +++ b/javascript/packages/language-server/src/code_action_service.ts @@ -1,3 +1,5 @@ +import * as path from "path" + import { CodeAction, CodeActionKind, CodeActionParams, Diagnostic, Range, TextEdit, WorkspaceEdit, CreateFile, TextDocumentEdit, OptionalVersionedTextDocumentIdentifier } from "vscode-languageserver/node" import { TextDocument } from "vscode-languageserver-textdocument" @@ -245,12 +247,12 @@ export class CodeActionService { const configUri = `file://${configPath}` const action: CodeAction = { - title: `Herb Linter: Disable \`${ruleName}\` in \`.herb.yaml\``, + title: `Herb Linter: Disable \`${ruleName}\` in \`${path.basename(configPath)}\``, kind: CodeActionKind.QuickFix, diagnostics: [diagnostic], edit, command: { - title: 'Open .herb.yaml', + title: `Open ${path.basename(configPath)}`, command: 'vscode.open', arguments: [configUri] } diff --git a/javascript/packages/language-server/src/formatting_service.ts b/javascript/packages/language-server/src/formatting_service.ts index 546f30767..8dfc97b76 100644 --- a/javascript/packages/language-server/src/formatting_service.ts +++ b/javascript/packages/language-server/src/formatting_service.ts @@ -5,11 +5,10 @@ import { ASTRewriter, StringRewriter } from "@herb-tools/rewriter" import { CustomRewriterLoader, builtinRewriters, isASTRewriterClass, isStringRewriterClass } from "@herb-tools/rewriter/loader" import { Project } from "./project" import { Settings } from "./settings" +import { showConfigWarningMessage } from "./utils" import { Config } from "@herb-tools/config" import { version } from "../package.json" -const OPEN_CONFIG_ACTION = 'Open .herb.yaml' - export class FormattingService { private connection: Connection private documents: TextDocuments @@ -168,16 +167,7 @@ export class FormattingService { ? `Herb Rewriter: ${warnings[0]}` : `Herb Rewriters (${warnings.length} failed):\n${warnings.map((w, i) => `${i + 1}. ${w}`).join('\n')}` - if (this.settings.hasShowDocumentCapability) { - this.connection.window.showWarningMessage(message, { title: OPEN_CONFIG_ACTION }).then(action => { - if (action?.title === OPEN_CONFIG_ACTION) { - const configPath = Config.configPathFromProjectPath(this.project.projectPath) - this.connection.window.showDocument({ uri: `file://${configPath}`, takeFocus: true }) - } - }) - } else { - this.connection.window.showWarningMessage(message) - } + showConfigWarningMessage(this.connection, message, this.project.projectPath, this.settings.hasShowDocumentCapability) } } catch (error) { this.connection.console.error(`Failed to load rewriters: ${error}`) @@ -254,16 +244,7 @@ export class FormattingService { ? `Herb Rewriter "${failedList[0][0]}" is not available: ${failedList[0][1]}` : `Herb Rewriters (${failedList.length} not available):\n${failedList.map(([name, error], i) => `${i + 1}. ${name}: ${error}`).join('\n')}` - if (this.settings.hasShowDocumentCapability) { - this.connection.window.showWarningMessage(message, { title: OPEN_CONFIG_ACTION }).then(action => { - if (action?.title === OPEN_CONFIG_ACTION) { - const configPath = Config.configPathFromProjectPath(this.project.projectPath) - this.connection.window.showDocument({ uri: `file://${configPath}`, takeFocus: true }) - } - }) - } else { - this.connection.window.showWarningMessage(message) - } + showConfigWarningMessage(this.connection, message, this.project.projectPath, this.settings.hasShowDocumentCapability) } const formatter = Formatter.from(this.project.herbBackend, config, { diff --git a/javascript/packages/language-server/src/linter_service.ts b/javascript/packages/language-server/src/linter_service.ts index 660469613..69763bc6b 100644 --- a/javascript/packages/language-server/src/linter_service.ts +++ b/javascript/packages/language-server/src/linter_service.ts @@ -8,11 +8,9 @@ import { Config } from "@herb-tools/config" import { Settings } from "./settings" import { Project } from "./project" -import { lintToDignosticSeverity, lintToDignosticTags } from "./utils" +import { lintToDignosticSeverity, lintToDignosticTags, showConfigWarningMessage } from "./utils" import { lspRangeFromLocation } from "./range_utils" -const OPEN_CONFIG_ACTION = 'Open .herb.yaml' - export interface LintServiceResult { diagnostics: Diagnostic[] } @@ -102,16 +100,7 @@ export class LinterService { ? `Failed to load custom linter rules: ${failures[0][1]}` : `Failed to load custom linter rules:\n${failures.map(([_, error], i) => `${i + 1}. ${error}`).join('\n')}` - if (this.settings.hasShowDocumentCapability) { - this.connection.window.showWarningMessage(message, { title: OPEN_CONFIG_ACTION }).then(action => { - if (action?.title === OPEN_CONFIG_ACTION) { - const configPath = Config.configPathFromProjectPath(this.project.projectPath) - this.connection.window.showDocument({ uri: `file://${configPath}`, takeFocus: true }) - } - }) - } else { - this.connection.window.showWarningMessage(message) - } + showConfigWarningMessage(this.connection, message, this.project.projectPath, this.settings.hasShowDocumentCapability) } private shouldLintFile(uri: string): boolean { diff --git a/javascript/packages/language-server/src/service.ts b/javascript/packages/language-server/src/service.ts index 04c7f78d7..57ad1a651 100644 --- a/javascript/packages/language-server/src/service.ts +++ b/javascript/packages/language-server/src/service.ts @@ -1,3 +1,5 @@ +import * as path from "path" + import { Connection, InitializeParams } from "vscode-languageserver/node" import { Settings, PersonalHerbSettings } from "./settings" @@ -77,7 +79,7 @@ export class Service { if (this.config.version && this.config.version !== version) { this.connection.console.warn( `Config file version (${this.config.version}) does not match current version (${version}). ` + - `Consider updating your .herb.yaml file.` + `Consider updating your ${path.basename(this.config.path)} file.` ) } } catch (error) { @@ -122,7 +124,7 @@ export class Service { if (this.config.version && this.config.version !== version) { this.connection.console.warn( `Config file version (${this.config.version}) does not match current version (${version}). ` + - `Consider updating your .herb.yaml file.` + `Consider updating your ${path.basename(this.config.path)} file.` ) } } catch (error) { diff --git a/javascript/packages/language-server/src/utils.ts b/javascript/packages/language-server/src/utils.ts index f71f56aa8..c72c3d69f 100644 --- a/javascript/packages/language-server/src/utils.ts +++ b/javascript/packages/language-server/src/utils.ts @@ -1,4 +1,9 @@ +import * as path from "path" + import { DiagnosticSeverity, DiagnosticTag } from "vscode-languageserver/node" +import { Config } from "@herb-tools/config" + +import type { Connection } from "vscode-languageserver/node" import type { LintSeverity } from "@herb-tools/linter" import type { DiagnosticSeverity as HerbDiagnosticSeverity, DiagnosticTag as HerbDiagnosticTag } from "@herb-tools/core" @@ -23,6 +28,24 @@ export function lintToDignosticSeverity(severity: LintSeverity | HerbDiagnosticS } } +export function showConfigWarningMessage(connection: Connection, message: string, projectPath: string, canShowDocument: boolean): void { + if (!canShowDocument) { + connection.window.showWarningMessage(message) + + return + } + + const openConfigAction = `Open ${path.basename(Config.configPathFromProjectPath(projectPath))}` + + connection.window.showWarningMessage(message, { title: openConfigAction }).then(action => { + if (action?.title === openConfigAction) { + const configPath = Config.configPathFromProjectPath(projectPath) + + connection.window.showDocument({ uri: `file://${configPath}`, takeFocus: true }) + } + }) +} + export function lintToDignosticTags(tags?: HerbDiagnosticTag[]): DiagnosticTag[] { if (!tags) return [] diff --git a/javascript/packages/vscode/src/config-details-provider.ts b/javascript/packages/vscode/src/config-details-provider.ts index d0c26568c..98eee1a12 100644 --- a/javascript/packages/vscode/src/config-details-provider.ts +++ b/javascript/packages/vscode/src/config-details-provider.ts @@ -1,4 +1,5 @@ import * as vscode from "vscode" +import * as path from "path" import { Config } from "@herb-tools/config" interface ConfigQuickPickItem extends vscode.QuickPickItem { @@ -29,13 +30,13 @@ export async function showConfigDetails() { const configSourceItem: ConfigQuickPickItem = hasConfigFile ? { label: "$(file-code) Herb Configuration Source", - detail: ".herb.yaml (project configuration overrides VS Code settings)", + detail: `${path.basename(configPath!)} (project configuration overrides VS Code settings)`, description: "" } : { label: "$(settings-gear) Herb Configuration Source", detail: "VS Code Settings (personal settings)", - description: "no .herb.yaml" + description: "no Herb config file" } if (!hasConfigFile) { @@ -110,7 +111,7 @@ export async function showConfigDetails() { if (hasConfigFile) { items.push({ - label: "$(edit) Edit .herb.yaml", + label: `$(edit) Edit ${path.basename(configPath!)}`, description: "Open configuration file", action: async () => { const document = await vscode.workspace.openTextDocument(configPath!) diff --git a/javascript/packages/vscode/src/config-provider.ts b/javascript/packages/vscode/src/config-provider.ts index 0e1885c10..97b4a0560 100644 --- a/javascript/packages/vscode/src/config-provider.ts +++ b/javascript/packages/vscode/src/config-provider.ts @@ -1,4 +1,5 @@ import * as vscode from "vscode" +import * as path from "path" import { Config } from "@herb-tools/config" export class HerbConfigProvider implements vscode.TreeDataProvider { @@ -6,6 +7,7 @@ export class HerbConfigProvider implements vscode.TreeDataProvider { readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event private config: Config | null = null + private configPath: string | null = null private workspaceRoot: string | undefined private onConfigChangeCallback?: () => void | Promise private extensionVersion: string @@ -78,6 +80,7 @@ export class HerbConfigProvider implements vscode.TreeDataProvider { private async loadConfig() { if (!this.workspaceRoot) { this.config = null + this.configPath = null this.configError = null return } @@ -88,11 +91,14 @@ export class HerbConfigProvider implements vscode.TreeDataProvider { await vscode.workspace.fs.stat(vscode.Uri.file(configPath)) } catch { this.config = null + this.configPath = null this.configError = null vscode.commands.executeCommand('setContext', 'herb.hasProjectConfig', false) return } + this.configPath = configPath + try { this.config = await Config.loadForEditor(configPath, this.extensionVersion) this.configError = null @@ -113,6 +119,7 @@ export class HerbConfigProvider implements vscode.TreeDataProvider { } } catch (error) { this.config = null + this.configPath = configPath this.configError = error instanceof Error ? error.message : String(error) vscode.commands.executeCommand('setContext', 'herb.hasProjectConfig', false) } @@ -124,7 +131,7 @@ export class HerbConfigProvider implements vscode.TreeDataProvider { if (this.config) { const configFileItem = new ConfigItem( "Project Settings", - "Configuration from .herb.yaml", + `Configuration from ${path.basename(this.config.path)}`, vscode.TreeItemCollapsibleState.None, 'configFile' ) @@ -181,7 +188,7 @@ export class HerbConfigProvider implements vscode.TreeDataProvider { } else if (this.configError) { const errorItem = new ConfigItem( "Configuration has errors", - "Click to view and fix errors in .herb.yaml", + `Click to view and fix errors in ${path.basename(this.configPath!)}`, vscode.TreeItemCollapsibleState.None, 'configError' ) diff --git a/javascript/packages/vscode/src/extension.ts b/javascript/packages/vscode/src/extension.ts index d53d07961..76c44f1e9 100644 --- a/javascript/packages/vscode/src/extension.ts +++ b/javascript/packages/vscode/src/extension.ts @@ -1,4 +1,5 @@ import * as vscode from "vscode" +import * as path from "path" import type { TextEdit } from "vscode-languageclient/node" import { Config } from "@herb-tools/config" @@ -46,8 +47,8 @@ async function updateConfigStatusBarItem() { try { await vscode.workspace.fs.stat(vscode.Uri.file(configPath)) - configStatusBarItem.text = '$(file-code) .herb.yaml (Project Settings)' - configStatusBarItem.tooltip = 'Herb configuration loaded from .herb.yaml (overrides VS Code settings)\n\nClick to view configuration details' + configStatusBarItem.text = `$(file-code) ${path.basename(configPath)} (Project Settings)` + configStatusBarItem.tooltip = `Herb configuration loaded from ${path.basename(configPath)} (overrides VS Code settings)\n\nClick to view configuration details` configStatusBarItem.command = 'herb.showConfigDetails' configStatusBarItem.show() diff --git a/javascript/packages/vscode/src/herb-analysis-provider.ts b/javascript/packages/vscode/src/herb-analysis-provider.ts index 82f41320d..8bb578d65 100644 --- a/javascript/packages/vscode/src/herb-analysis-provider.ts +++ b/javascript/packages/vscode/src/herb-analysis-provider.ts @@ -4,6 +4,7 @@ import { AnalysisService } from './analysis-service' import { VersionService } from './version-service' import { workspace, window, commands, EventEmitter, ProgressLocation } from 'vscode' +import * as path from 'path' import { Config } from "@herb-tools/config" @@ -76,7 +77,7 @@ export class HerbAnalysisProvider implements TreeDataProvider { } } catch (_error) { window.showErrorMessage( - 'Cannot run Herb analysis: Configuration file has errors. Please fix .herb.yaml and try again.', + `Cannot run Herb analysis: Configuration file has errors. Please fix ${path.basename(Config.configPathFromProjectPath(workspaceRoot))} and try again.`, 'Edit Config' ).then(selection => { if (selection === 'Edit Config') { diff --git a/javascript/packages/vscode/src/issue-reporter.ts b/javascript/packages/vscode/src/issue-reporter.ts index 01b57cc7b..45282ae01 100644 --- a/javascript/packages/vscode/src/issue-reporter.ts +++ b/javascript/packages/vscode/src/issue-reporter.ts @@ -30,14 +30,14 @@ function createSettingsSection(settings: PersonalHerbSettings): string { ` if (settings.projectConfig) { - section += `\n\n**Herb Configuration (.herb.yaml):** + section += `\n\n**Herb Configuration:** \`\`\`yaml ${settings.projectConfig} \`\`\` ` } else { - section += `\n\n**Herb Configuration (.herb.yaml):** - No .herb.yaml file found in workspace. + section += `\n\n**Herb Configuration:** + No Herb config file found in workspace. ` } diff --git a/javascript/packages/vscode/src/tree-item-builder.ts b/javascript/packages/vscode/src/tree-item-builder.ts index 4f38c1c04..344925b88 100644 --- a/javascript/packages/vscode/src/tree-item-builder.ts +++ b/javascript/packages/vscode/src/tree-item-builder.ts @@ -101,7 +101,7 @@ export class TreeItemBuilder { ) item.iconPath = new vscode.ThemeIcon('circle-slash', new vscode.ThemeColor('charts.gray')) - item.tooltip = 'Linting is disabled in the Herb configuration (.herb.yaml). Click to toggle linter setting.' + item.tooltip = 'Linting is disabled in the Herb configuration. Click to toggle linter setting.' item.command = { command: 'herb.toggleLinter', title: 'Toggle Linter' @@ -347,7 +347,7 @@ export class TreeItemBuilder { ) item.iconPath = new vscode.ThemeIcon('circle-slash', new vscode.ThemeColor('charts.gray')) - item.tooltip = 'Formatting is disabled in the Herb configuration (.herb.yaml). Click to toggle formatter setting.' + item.tooltip = 'Formatting is disabled in the Herb configuration. Click to toggle formatter setting.' item.command = { command: 'herb.toggleFormatter', title: 'Toggle Formatter' From 7b3bb6deae11be22e6f575612e63284f2199ab60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Fri, 3 Jul 2026 17:28:40 +0200 Subject: [PATCH 23/24] CLI: Reference the resolved config filename in linter and formatter messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Messages like "disabled in .herb.yaml" named the wrong file in projects using legacy .herb.yml — and following "set formatter.enabled: true in .herb.yaml" literally would create a second config file that shadows the existing one. Runtime messages now interpolate the loaded config's basename; only creation flows (--init tips, help text) keep the .herb.yaml default, and "No .herb.yaml found" becomes "No Herb config file found" since neither file exists at that point. Co-Authored-By: Claude Fable 5 --- javascript/packages/formatter/src/cli.ts | 8 ++++---- javascript/packages/linter/src/cli.ts | 20 +++++++++---------- .../linter/src/cli/summary-reporter.ts | 10 +++++----- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/javascript/packages/formatter/src/cli.ts b/javascript/packages/formatter/src/cli.ts index f49eb9147..8762c7b35 100644 --- a/javascript/packages/formatter/src/cli.ts +++ b/javascript/packages/formatter/src/cli.ts @@ -1,7 +1,7 @@ import dedent from "dedent" import { readFileSync, writeFileSync, statSync, existsSync } from "fs" import { glob } from "tinyglobby" -import { resolve, relative } from "path" +import { resolve, relative, basename } from "path" import { Herb } from "@herb-tools/node-wasm" import { Config, addHerbExtensionRecommendation, getExtensionsJsonRelativePath } from "@herb-tools/config" @@ -196,15 +196,15 @@ export class CLI { const formatterConfig = config.formatter || {} if (hasConfigFile && formatterConfig.enabled === false && !isForceMode) { - console.log("Formatter is disabled in .herb.yaml configuration.") - console.log("To enable formatting, set formatter.enabled: true in .herb.yaml") + console.log(`Formatter is disabled in ${basename(config.path)} configuration.`) + console.log(`To enable formatting, set formatter.enabled: true in ${basename(config.path)}`) console.log("Or use --force to format anyway.") process.exit(0) } if (isForceMode && hasConfigFile && formatterConfig.enabled === false) { - console.error("⚠️ Forcing formatter run (disabled in .herb.yaml)") + console.error(`⚠️ Forcing formatter run (disabled in ${basename(config.path)})`) console.error() } diff --git a/javascript/packages/linter/src/cli.ts b/javascript/packages/linter/src/cli.ts index 95def84e8..81225f036 100644 --- a/javascript/packages/linter/src/cli.ts +++ b/javascript/packages/linter/src/cli.ts @@ -3,7 +3,7 @@ import { Herb } from "@herb-tools/node-wasm" import { Config, addHerbExtensionRecommendation, getExtensionsJsonRelativePath } from "@herb-tools/config" import { existsSync, statSync } from "fs" -import { resolve, relative } from "path" +import { resolve, relative, basename } from "path" import { colorize } from "@herb-tools/highlighter" import { Linter } from "./linter.js" @@ -178,7 +178,7 @@ export class CLI { const configPath = configFile || this.projectPath if (!Config.exists(configPath)) { - console.error(`\n✗ No .herb.yaml found. Run ${colorize("herb-lint --init", "cyan")} first.\n`) + console.error(`\n✗ No Herb config file found. Run ${colorize("herb-lint --init", "cyan")} first.\n`) process.exit(1) } @@ -186,7 +186,7 @@ export class CLI { const configVersion = config.configVersion if (configVersion === version) { - console.log(`\n✓ Your .herb.yaml is already at version ${version}. Nothing to upgrade.\n`) + console.log(`\n✓ Your ${basename(config.path)} is already at version ${version}. Nothing to upgrade.\n`) process.exit(0) } @@ -253,7 +253,7 @@ export class CLI { content = content.replace(/^version:\s*.+$/m, `version: ${version}`) await fs.writeFile(config.path, content, "utf-8") - console.log(`\n${colorize("✓", "brightGreen")} Updated ${colorize(".herb.yaml", "cyan")} version from ${colorize(configVersion ?? "unversioned", "cyan")} to ${colorize(version, "cyan")}`) + console.log(`\n${colorize("✓", "brightGreen")} Updated ${colorize(basename(config.path), "cyan")} version from ${colorize(configVersion ?? "unversioned", "cyan")} to ${colorize(version, "cyan")}`) if (rulesToEnable.length > 0) { console.log(`\n${colorize("✓", "brightGreen")} Enabled ${colorize(String(rulesToEnable.length), "bold")} new ${rulesToEnable.length === 1 ? "rule" : "rules"} (no offenses found):\n`) @@ -273,7 +273,7 @@ export class CLI { console.log(` ${colorize("✗", "red")} ${colorize(rule.ruleName, "white")} ${colorize(`(${offenseCount} ${offenseCount === 1 ? "offense" : "offenses"})`, "gray")}`) } - console.log(`\n When you're ready, review the disabled ${rulesToDisable.length === 1 ? "rule" : "rules"} in your ${colorize(".herb.yaml", "cyan")} and re-enable them after fixing the offenses.`) + console.log(`\n When you're ready, review the disabled ${rulesToDisable.length === 1 ? "rule" : "rules"} in your ${colorize(basename(config.path), "cyan")} and re-enable them after fixing the offenses.`) } if (skippedByVersion.length === 0) { @@ -288,7 +288,7 @@ export class CLI { const configPath = configFile || this.projectPath if (!Config.exists(configPath)) { - console.error(`\n✗ No .herb.yaml found. Run ${colorize("herb-lint --init", "cyan")} first.\n`) + console.error(`\n✗ No Herb config file found. Run ${colorize("herb-lint --init", "cyan")} first.\n`) process.exit(1) } @@ -336,13 +336,13 @@ export class CLI { const totalOffenses = Array.from(failingRules.values()).reduce((sum, count) => sum + count, 0) const sortedRules = Array.from(failingRules.entries()).sort((a, b) => b[1] - a[1]) - console.log(`\n${colorize("!", "yellow")} Found ${colorize(String(totalOffenses), "bold")} ${totalOffenses === 1 ? "offense" : "offenses"} across ${colorize(String(failingRules.size), "bold")} ${failingRules.size === 1 ? "rule" : "rules"}. Disabled in ${colorize(".herb.yaml", "cyan")}:\n`) + console.log(`\n${colorize("!", "yellow")} Found ${colorize(String(totalOffenses), "bold")} ${totalOffenses === 1 ? "offense" : "offenses"} across ${colorize(String(failingRules.size), "bold")} ${failingRules.size === 1 ? "rule" : "rules"}. Disabled in ${colorize(basename(config.path), "cyan")}:\n`) for (const [ruleName, count] of sortedRules) { console.log(` ${colorize("✗", "red")} ${colorize(ruleName, "white")} ${colorize(`(${count} ${count === 1 ? "offense" : "offenses"})`, "gray")}`) } - console.log(`\n When you're ready, review the disabled rules in your ${colorize(".herb.yaml", "cyan")} and re-enable them after fixing the offenses.\n`) + console.log(`\n When you're ready, review the disabled rules in your ${colorize(basename(config.path), "cyan")} and re-enable them after fixing the offenses.\n`) process.exit(0) } @@ -366,11 +366,11 @@ export class CLI { await this.beforeProcess() if (linterConfig.enabled === false && !force) { - this.exitWithInfo("Linter is disabled in .herb.yaml configuration. Use --force to lint anyway.", formatOption, 0, { startTime, startDate, showTiming }) + this.exitWithInfo(`Linter is disabled in ${basename(config.path)} configuration. Use --force to lint anyway.`, formatOption, 0, { startTime, startDate, showTiming }) } if (force && linterConfig.enabled === false) { - console.log("⚠️ Forcing linter run (disabled in .herb.yaml)") + console.log(`⚠️ Forcing linter run (disabled in ${basename(config.path)})`) console.log() } diff --git a/javascript/packages/linter/src/cli/summary-reporter.ts b/javascript/packages/linter/src/cli/summary-reporter.ts index ec4c0d346..b37e4c7e8 100644 --- a/javascript/packages/linter/src/cli/summary-reporter.ts +++ b/javascript/packages/linter/src/cli/summary-reporter.ts @@ -1,3 +1,5 @@ +import { basename } from "path" + import { colorize, hyperlink } from "@herb-tools/highlighter" import { UNRELEASED_VERSION, compareSemver } from "../semver.js" @@ -156,18 +158,16 @@ export class SummaryReporter { const { rulesSkippedByVersion: skippedRules, configVersion, configPath, hasConfigFile, toolVersion } = data if (!skippedRules || skippedRules.length === 0) return - if (!hasConfigFile) return + if (!hasConfigFile || !configPath) return const ruleCount = skippedRules.length const suggestedVersion = toolVersion || configVersion || "latest" console.log("") console.log(` ${colorize(`New rules available:`, "bold")}`) - console.log(` Your ${colorize(".herb.yaml", "cyan")} version is ${colorize(configVersion!, "cyan")}. ${colorize(String(ruleCount), "bold")} new ${this.pluralize(ruleCount, "rule")} ${ruleCount === 1 ? "is" : "are"} disabled to ease upgrades:`) + console.log(` Your ${colorize(basename(configPath), "cyan")} version is ${colorize(configVersion!, "cyan")}. ${colorize(String(ruleCount), "bold")} new ${this.pluralize(ruleCount, "rule")} ${ruleCount === 1 ? "is" : "are"} disabled to ease upgrades:`) - if (configPath) { - console.log(` ${colorize("from Herb config:", "gray")} ${colorize(configPath, "cyan")}`) - } + console.log(` ${colorize("from Herb config:", "gray")} ${colorize(configPath, "cyan")}`) console.log("") From 6d71dd6635b00ca1bf1c97943a60a50ab397f90d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Fri, 3 Jul 2026 17:28:40 +0200 Subject: [PATCH 24/24] Config: Warn when both .herb.yaml and .herb.yml exist Discovery silently prefers .herb.yaml, so a stray second config file would shadow all settings in the other with no indication anywhere. The CLI now prints a warning when it finds more than one config file in the project root. Co-Authored-By: Claude Fable 5 --- javascript/packages/config/src/config.ts | 24 +++++--- .../packages/config/test/config.test.ts | 55 ++++++++++++++++++- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/javascript/packages/config/src/config.ts b/javascript/packages/config/src/config.ts index 09bcd919d..743fd0ea1 100644 --- a/javascript/packages/config/src/config.ts +++ b/javascript/packages/config/src/config.ts @@ -452,16 +452,14 @@ export class Config { return this.configPaths.some(name => pathOrFile.endsWith(name)) } - static configPathFromProjectPath(projectPath: string) { - for (const configPath of this.configPaths) { - const candidate = path.join(projectPath, configPath) - - if (existsSync(candidate)) { - return candidate - } - } + static existingConfigPaths(directory: string): string[] { + return this.configPaths + .map(configPath => path.join(directory, configPath)) + .filter(candidate => existsSync(candidate)) + } - return path.join(projectPath, this.defaultConfigPath) + static configPathFromProjectPath(projectPath: string) { + return this.existingConfigPaths(projectPath)[0] || path.join(projectPath, this.defaultConfigPath) } /** @@ -1002,6 +1000,14 @@ export class Config { if (!silent) { console.error(`✓ Using Herb config file at ${configPath}`) + + const ignoredNames = this.existingConfigPaths(projectRoot) + .filter(existingPath => path.resolve(existingPath) !== path.resolve(configPath)) + .map(existingPath => path.basename(existingPath)) + + if (ignoredNames.length > 0) { + console.error(`⚠ Multiple Herb config files found: using ${path.basename(configPath)}, ignoring ${ignoredNames.join(", ")}`) + } } return config diff --git a/javascript/packages/config/test/config.test.ts b/javascript/packages/config/test/config.test.ts index 70f1aa42c..8042304c3 100644 --- a/javascript/packages/config/test/config.test.ts +++ b/javascript/packages/config/test/config.test.ts @@ -1,5 +1,5 @@ import dedent from "dedent" -import { describe, test, expect, beforeEach, afterEach } from "vitest" +import { describe, test, expect, beforeEach, afterEach, vi } from "vitest" import { mkdirSync, writeFileSync, rmSync, existsSync } from "fs" import { join } from "path" import { tmpdir } from "os" @@ -90,6 +90,59 @@ describe("@herb-tools/config", () => { }) }) + describe("Config.existingConfigPaths", () => { + test("returns empty array when no config file exists", () => { + expect(Config.existingConfigPaths(testDir)).toEqual([]) + }) + + test("returns single existing config file", () => { + writeFileSync(join(testDir, ".herb.yml"), "version: 0.10.1\n") + + expect(Config.existingConfigPaths(testDir)).toEqual([join(testDir, ".herb.yml")]) + }) + + test("returns both config files in precedence order when both exist", () => { + writeFileSync(join(testDir, ".herb.yaml"), "version: 0.10.1\n") + writeFileSync(join(testDir, ".herb.yml"), "version: 0.10.1\n") + + expect(Config.existingConfigPaths(testDir)).toEqual([ + join(testDir, ".herb.yaml"), + join(testDir, ".herb.yml") + ]) + }) + }) + + describe("Config.load with multiple config files", () => { + test("warns when both .herb.yaml and .herb.yml exist", async () => { + writeFileSync(join(testDir, ".herb.yaml"), "version: 0.10.1\n") + writeFileSync(join(testDir, ".herb.yml"), "version: 0.10.1\n") + + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + try { + await Config.load(testDir) + + expect(errorSpy).toHaveBeenCalledWith("⚠ Multiple Herb config files found: using .herb.yaml, ignoring .herb.yml") + } finally { + errorSpy.mockRestore() + } + }) + + test("does not warn when only one config file exists", async () => { + writeFileSync(join(testDir, ".herb.yaml"), "version: 0.10.1\n") + + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + try { + await Config.load(testDir) + + expect(errorSpy).not.toHaveBeenCalledWith(expect.stringContaining("Multiple Herb config files found")) + } finally { + errorSpy.mockRestore() + } + }) + }) + describe("Config.exists", () => { test("returns false when config file does not exist", () => { expect(Config.exists(testDir)).toBe(false)