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 6583e36d1..842e1718d 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.10.1
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 12de652f5..f3671b919 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.10.1
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/config/src/config.ts b/javascript/packages/config/src/config.ts
index 23f9ea0f2..743fd0ea1 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"
@@ -106,11 +106,13 @@ export type FromObjectOptions = {
}
export class Config {
- static configPath = ".herb.yml"
+ static configPaths = [".herb.yaml", ".herb.yml"]
+ static defaultConfigPath = this.configPaths[0]
private static PROJECT_INDICATORS = [
'.git',
'.herb',
+ '.herb.yaml',
'.herb.yml',
'Gemfile',
'package.json',
@@ -124,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
}
@@ -446,8 +448,18 @@ export class Config {
})
}
+ static isConfigPath(pathOrFile: string): boolean {
+ return this.configPaths.some(name => pathOrFile.endsWith(name))
+ }
+
+ static existingConfigPaths(directory: string): string[] {
+ return this.configPaths
+ .map(configPath => path.join(directory, configPath))
+ .filter(candidate => existsSync(candidate))
+ }
+
static configPathFromProjectPath(projectPath: string) {
- return path.join(projectPath, this.configPath)
+ return this.existingConfigPaths(projectPath)[0] || path.join(projectPath, this.defaultConfigPath)
}
/**
@@ -460,16 +472,16 @@ 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 {
let configPath: string
- if (pathOrFile.endsWith(this.configPath)) {
+ if (this.isConfigPath(pathOrFile)) {
configPath = pathOrFile
} else {
configPath = this.configPathFromProjectPath(pathOrFile)
@@ -485,7 +497,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
@@ -520,14 +532,10 @@ export class Config {
let firstIndicatorMatch: string | undefined
while (true) {
- const configPath = path.join(currentPath, this.configPath)
-
- 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) {
@@ -555,13 +563,13 @@ 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 {
- const configPath = pathOrFile.endsWith(this.configPath)
+ const configPath = this.isConfigPath(pathOrFile)
? pathOrFile
: this.configPathFromProjectPath(pathOrFile)
@@ -589,7 +597,7 @@ export class Config {
const { silent = false, version = DEFAULT_VERSION, createIfMissing = false, exitOnError = false } = options
try {
- if (pathOrFile.endsWith(this.configPath)) {
+ if (this.isConfigPath(pathOrFile)) {
return await this.loadFromExplicitPath(pathOrFile, silent, version, exitOnError)
}
@@ -616,7 +624,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
*/
@@ -633,7 +641,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
@@ -655,13 +663,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 }
@@ -886,14 +894,15 @@ export class Config {
let firstIndicatorMatch: string | undefined
while (true) {
- const configPath = path.join(currentPath, this.configPath)
-
- 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) {
@@ -991,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
@@ -1004,19 +1021,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 +1056,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 {
@@ -1195,7 +1181,7 @@ export class Config {
resolved.version = version
- return new Config(projectRoot, resolved, userConfigVersion)
+ return new Config(projectRoot, resolved, userConfigVersion, configPath)
}
/**
diff --git a/javascript/packages/config/test/config.test.ts b/javascript/packages/config/test/config.test.ts
index b2325cd28..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"
@@ -41,21 +41,105 @@ describe("@herb-tools/config", () => {
test("sets correct config path", () => {
const config = new Config(testDir, { version: "0.10.1" })
- 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))
+ })
+
+ 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"))
+ })
+
+ 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.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()
+ }
})
})
@@ -71,6 +155,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.10.1\n")
@@ -96,6 +187,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.10.1\n"
@@ -1393,6 +1493,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")
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")
diff --git a/javascript/packages/formatter/README.md b/javascript/packages/formatter/README.md
index 7a1c46991..d6a8a353a 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/formatter/src/cli.ts b/javascript/packages/formatter/src/cli.ts
index 2d7825dde..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"
@@ -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 ${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.yml)")
+ console.error(`⚠️ Forcing formatter run (disabled in ${basename(config.path)})`)
console.error()
}
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/language-server/src/code_action_service.ts b/javascript/packages/language-server/src/code_action_service.ts
index a262b23be..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.yml\``,
+ title: `Herb Linter: Disable \`${ruleName}\` in \`${path.basename(configPath)}\``,
kind: CodeActionKind.QuickFix,
diagnostics: [diagnostic],
edit,
command: {
- title: 'Open .herb.yml',
+ title: `Open ${path.basename(configPath)}`,
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 25add2eb3..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
@@ -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..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.yml'
-
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 = `${this.project.projectPath}/.herb.yml`
- 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}`)
@@ -207,7 +197,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)
@@ -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 = `${this.project.projectPath}/.herb.yml`
- 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, {
@@ -295,7 +276,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 +376,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..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.yml'
-
export interface LintServiceResult {
diagnostics: Diagnostic[]
}
@@ -102,22 +100,13 @@ 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 = `${this.project.projectPath}/.herb.yml`
- 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 {
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/")
diff --git a/javascript/packages/language-server/src/service.ts b/javascript/packages/language-server/src/service.ts
index ccbc34b18..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.yml 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.yml 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/linter/README.md b/javascript/packages/linter/README.md
index 1bd6afd06..220f12ddd 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/linter/src/cli.ts b/javascript/packages/linter/src/cli.ts
index 3a71a974b..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.yml 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.yml 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.yml", "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.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(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.yml 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.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(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.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(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.yml 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.yml)")
+ console.log(`⚠️ Forcing linter run (disabled in ${basename(config.path)})`)
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..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.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(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("")
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/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/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/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.
diff --git a/javascript/packages/vscode/package.json b/javascript/packages/vscode/package.json
index 31c751109..d317c9262 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/client.ts b/javascript/packages/vscode/src/client.ts
index 8cc3711e2..1a4fea51e 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-details-provider.ts b/javascript/packages/vscode/src/config-details-provider.ts
index cc7975b95..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.yml (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.yml"
+ description: "no Herb config file"
}
if (!hasConfigFile) {
@@ -110,7 +111,7 @@ export async function showConfigDetails() {
if (hasConfigFile) {
items.push({
- label: "$(edit) Edit .herb.yml",
+ label: `$(edit) Edit ${path.basename(configPath!)}`,
description: "Open configuration file",
action: async () => {
const document = await vscode.workspace.openTextDocument(configPath!)
@@ -119,7 +120,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 bbfb37bd9..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
@@ -23,7 +25,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 +37,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()
}
})
@@ -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.yml",
+ `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.yml",
+ `Click to view and fix errors in ${path.basename(this.configPath!)}`,
vscode.TreeItemCollapsibleState.None,
'configError'
)
@@ -253,7 +260,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 +288,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 +311,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 9bf6ac8cb..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,14 +47,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) ${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()
} 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()
@@ -196,7 +197,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()
diff --git a/javascript/packages/vscode/src/herb-analysis-provider.ts b/javascript/packages/vscode/src/herb-analysis-provider.ts
index 28269a326..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.yml 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/herb-settings-commands.ts b/javascript/packages/vscode/src/herb-settings-commands.ts
index 65132d651..142e09c89 100644
--- a/javascript/packages/vscode/src/herb-settings-commands.ts
+++ b/javascript/packages/vscode/src/herb-settings-commands.ts
@@ -1,11 +1,10 @@
import * as vscode from "vscode"
-import * as path from "path"
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) {
@@ -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/issue-reporter.ts b/javascript/packages/vscode/src/issue-reporter.ts
index a2551a6f3..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.yml):**
+ section += `\n\n**Herb Configuration:**
\`\`\`yaml
${settings.projectConfig}
\`\`\`
`
} else {
- section += `\n\n**Herb Configuration (.herb.yml):**
- No .herb.yml 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 ed6da1489..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.yml). 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.yml). 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'
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) {
diff --git a/lib/herb/action_view/render_analyzer.rb b/lib/herb/action_view/render_analyzer.rb
index a36ff3e00..1c83359ec 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/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/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")}...")
diff --git a/rust/herb-config/src/herb_config.rs b/rust/herb-config/src/herb_config.rs
index 8b9be0bc5..ef1f08ec9 100644
--- a/rust/herb-config/src/herb_config.rs
+++ b/rust/herb-config/src/herb_config.rs
@@ -125,8 +125,8 @@ 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 PROJECT_INDICATORS: &[&str] = &[".git", ".herb", "Gemfile", "package.json", "Rakefile", "README.md"];
+const CONFIG_FILE_NAMES: &[&str] = &[".herb.yaml", ".herb.yml"];
+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) {
@@ -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 8775550ce..923d9666b 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();
@@ -362,6 +385,30 @@ 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_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();
@@ -373,6 +420,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();
diff --git a/test/configuration_test.rb b/test/configuration_test.rb
index c2fdf66f2..2d12fcb47 100644
--- a/test/configuration_test.rb
+++ b/test/configuration_test.rb
@@ -26,6 +26,35 @@ 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 "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.10.1"