@herb-tools/config 0.8.10 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{src/config-schema.js → config-schema.js} +9 -0
- package/dist/config-schema.js.map +1 -0
- package/dist/{src/config.js → config.js} +73 -83
- package/dist/config.js.map +1 -0
- package/dist/herb-config.cjs +87 -87
- package/dist/herb-config.cjs.map +1 -1
- package/dist/herb-config.esm.js +88 -88
- package/dist/herb-config.esm.js.map +1 -1
- package/dist/index.js.map +1 -0
- package/dist/{src/merge.js → merge.js} +1 -1
- package/dist/merge.js.map +1 -0
- package/dist/types/config-schema.d.ts +19 -0
- package/dist/types/config.d.ts +16 -6
- package/dist/{src/vscode.js → vscode.js} +1 -1
- package/dist/vscode.js.map +1 -0
- package/package.json +2 -2
- package/src/config-schema.ts +11 -0
- package/src/config-template.yml +7 -1
- package/src/config.ts +90 -87
- package/src/merge.ts +1 -1
- package/src/vscode.ts +1 -1
- package/dist/package.json +0 -49
- package/dist/src/config-schema.js.map +0 -1
- package/dist/src/config.js.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/merge.js.map +0 -1
- package/dist/src/vscode.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/dist/types/src/config-schema.d.ts +0 -102
- package/dist/types/src/config.d.ts +0 -365
- package/dist/types/src/index.d.ts +0 -5
- package/dist/types/src/merge.d.ts +0 -11
- package/dist/types/src/vscode.d.ts +0 -13
- /package/dist/{src/index.js → index.js} +0 -0
package/dist/herb-config.esm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path, { join } from 'path';
|
|
2
2
|
import { promises, existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
3
|
-
import { parseDocument, stringify, isMap
|
|
3
|
+
import { parse as parse$1, parseDocument, stringify, isMap } from 'yaml';
|
|
4
4
|
import picomatch from 'picomatch';
|
|
5
5
|
import { glob } from 'tinyglobby';
|
|
6
6
|
|
|
@@ -4798,9 +4798,18 @@ const FormatterConfigSchema = object({
|
|
|
4798
4798
|
maxLineLength: number().int().positive().optional().describe("Maximum line length before wrapping"),
|
|
4799
4799
|
rewriter: RewriterConfigSchema.describe("Rewriter configuration for pre and post-format transformations"),
|
|
4800
4800
|
}).strict().optional();
|
|
4801
|
+
const ValidatorsConfigSchema = object({
|
|
4802
|
+
security: boolean().optional().describe("Enable or disable the security validator (default: true)"),
|
|
4803
|
+
nesting: boolean().optional().describe("Enable or disable the nesting validator (default: true)"),
|
|
4804
|
+
accessibility: boolean().optional().describe("Enable or disable the accessibility validator (default: true)"),
|
|
4805
|
+
}).strict().optional();
|
|
4806
|
+
const EngineConfigSchema = object({
|
|
4807
|
+
validators: ValidatorsConfigSchema.describe("Per-validator enable/disable configuration"),
|
|
4808
|
+
}).strict().optional();
|
|
4801
4809
|
const HerbConfigSchema = object({
|
|
4802
4810
|
version: string().describe("Configuration file version"),
|
|
4803
4811
|
files: FilesConfigSchema.describe("Top-level file configuration"),
|
|
4812
|
+
engine: EngineConfigSchema.describe("Engine configuration"),
|
|
4804
4813
|
linter: LinterConfigSchema,
|
|
4805
4814
|
formatter: FormatterConfigSchema,
|
|
4806
4815
|
}).strict();
|
|
@@ -4824,7 +4833,7 @@ function deepMerge(target, source) {
|
|
|
4824
4833
|
continue;
|
|
4825
4834
|
}
|
|
4826
4835
|
if (Array.isArray(sourceValue)) {
|
|
4827
|
-
if (key === 'include' && Array.isArray(targetValue)) {
|
|
4836
|
+
if ((key === 'include' || key === 'exclude') && Array.isArray(targetValue)) {
|
|
4828
4837
|
output[key] = [...targetValue, ...sourceValue];
|
|
4829
4838
|
}
|
|
4830
4839
|
else {
|
|
@@ -4842,13 +4851,16 @@ function deepMerge(target, source) {
|
|
|
4842
4851
|
return output;
|
|
4843
4852
|
}
|
|
4844
4853
|
|
|
4845
|
-
var version = "0.
|
|
4854
|
+
var version = "0.9.1";
|
|
4846
4855
|
var packageJson = {
|
|
4847
4856
|
version: version};
|
|
4848
4857
|
|
|
4849
|
-
var configTemplate = "# This file configures Herb for your project and team.\n# Settings here take precedence over individual editor preferences.\n#\n# Herb is a suite of tools for HTML+ERB templates including:\n# - Linter: Validates templates and enforces best practices\n# - Formatter: Auto-formats templates with intelligent indentation\n# - Language Server: Provides IDE support (VS Code, Zed, Neovim, etc.)\n#\n# Website: https://herb-tools.dev\n# Configuration: https://herb-tools.dev/configuration\n# GitHub Repo: https://github.com/marcoroth/herb\n#\n\nversion: 0.
|
|
4858
|
+
var configTemplate = "# This file configures Herb for your project and team.\n# Settings here take precedence over individual editor preferences.\n#\n# Herb is a suite of tools for HTML+ERB templates including:\n# - Linter: Validates templates and enforces best practices\n# - Formatter: Auto-formats templates with intelligent indentation\n# - Language Server: Provides IDE support (VS Code, Zed, Neovim, etc.)\n#\n# Website: https://herb-tools.dev\n# Configuration: https://herb-tools.dev/configuration\n# GitHub Repo: https://github.com/marcoroth/herb\n#\n\nversion: 0.9.1\n\n# files:\n# # Additional patterns beyond the defaults (**.html, **.rhtml, **.html.erb, etc.)\n# include:\n# - '**/*.xml.erb'\n# - 'custom/**/*.html'\n#\n# # Patterns to exclude (can exclude defaults too)\n# exclude:\n# - 'public/**/*'\n# - 'tmp/**/*'\n\n# engine:\n# validators:\n# security: true # default: true\n# nesting: true # default: true\n# accessibility: true # default: true\n\nlinter:\n enabled: true\n\n # # Exit with error code when diagnostics of this severity or higher are present\n # # Valid values: error (default), warning, info, hint\n # failLevel: warning\n\n # # Additional patterns beyond the defaults for linting\n # include:\n # - '**/*.xml.erb'\n #\n # # Patterns to exclude from linting\n # exclude:\n # - 'app/views/admin/**/*'\n\n # rules:\n # erb-no-extra-newline:\n # enabled: false\n #\n # # Rules can have 'include', 'only', and 'exclude' patterns\n # some-rule:\n # # Additional patterns to check (additive, ignored when 'only' is present)\n # include:\n # - 'app/components/**/*'\n # # Don't apply this rule to files matching these patterns\n # exclude:\n # - 'app/views/admin/**/*'\n #\n # another-rule:\n # # Only apply this rule to files matching these patterns (overrides all 'include')\n # only:\n # - 'app/views/**/*'\n # # Exclude still applies even with 'only'\n # exclude:\n # - 'app/views/admin/**/*'\n\nformatter:\n enabled: false\n indentWidth: 2\n maxLineLength: 80\n\n # # Additional patterns beyond the defaults for formatting\n # include:\n # - '**/*.xml.erb'\n #\n # # Patterns to exclude from formatting\n # exclude:\n # - 'app/views/admin/**/*'\n\n # # Rewriters modify templates during formatting\n # rewriter:\n # # Pre-format rewriters (modify AST before formatting)\n # pre:\n # - tailwind-class-sorter\n # # Post-format rewriters (modify formatted output string)\n # post: []\n";
|
|
4859
|
+
|
|
4860
|
+
var defaultsYaml = "files:\n include:\n - \"**/*.herb\"\n - \"**/*.html.erb\"\n - \"**/*.html.herb\"\n - \"**/*.html\"\n - \"**/*.html+*.erb\"\n - \"**/*.rhtml\"\n - \"**/*.turbo_stream.erb\"\n\n exclude:\n - \"coverage/**/*\"\n - \"log/**/*\"\n - \"node_modules/**/*\"\n - \"storage/**/*\"\n - \"tmp/**/*\"\n - \"vendor/**/*\"\n\nengine:\n validators:\n security: true\n nesting: true\n accessibility: true\n\nlinter:\n enabled: true\n rules: {}\n\nformatter:\n enabled: false\n indentWidth: 2\n maxLineLength: 80\n";
|
|
4850
4861
|
|
|
4851
4862
|
const DEFAULT_VERSION = packageJson.version;
|
|
4863
|
+
const PARSED_DEFAULTS = parse$1(defaultsYaml);
|
|
4852
4864
|
class Config {
|
|
4853
4865
|
static configPath = ".herb.yml";
|
|
4854
4866
|
static PROJECT_INDICATORS = [
|
|
@@ -4922,8 +4934,9 @@ class Config {
|
|
|
4922
4934
|
}
|
|
4923
4935
|
/**
|
|
4924
4936
|
* Get the files configuration for a specific tool.
|
|
4925
|
-
*
|
|
4926
|
-
* Include
|
|
4937
|
+
* Both include and exclude patterns are additive:
|
|
4938
|
+
* - Include: defaults + files.include + tool.include
|
|
4939
|
+
* - Exclude: defaults + files.exclude + tool.exclude
|
|
4927
4940
|
* @param tool - The tool to get files config for ('linter' or 'formatter')
|
|
4928
4941
|
* @returns The merged files configuration
|
|
4929
4942
|
*/
|
|
@@ -4933,7 +4946,9 @@ class Config {
|
|
|
4933
4946
|
const topLevelInclude = topLevelFiles.include || [];
|
|
4934
4947
|
const toolInclude = toolConfig?.include || [];
|
|
4935
4948
|
const include = [...topLevelInclude, ...toolInclude];
|
|
4936
|
-
const
|
|
4949
|
+
const topLevelExclude = topLevelFiles.exclude || [];
|
|
4950
|
+
const toolExclude = toolConfig?.exclude || [];
|
|
4951
|
+
const exclude = [...topLevelExclude, ...toolExclude];
|
|
4937
4952
|
return {
|
|
4938
4953
|
include,
|
|
4939
4954
|
exclude
|
|
@@ -5017,7 +5032,7 @@ class Config {
|
|
|
5017
5032
|
}
|
|
5018
5033
|
/**
|
|
5019
5034
|
* Check if a tool (linter or formatter) is enabled for a specific file path.
|
|
5020
|
-
* Respects
|
|
5035
|
+
* Respects the tool's enabled state and all exclude patterns (defaults + files.exclude + tool.exclude).
|
|
5021
5036
|
* @param filePath - The file path to check
|
|
5022
5037
|
* @param tool - The tool to check ('linter' or 'formatter')
|
|
5023
5038
|
* @returns true if the tool is enabled for this path
|
|
@@ -5027,8 +5042,8 @@ class Config {
|
|
|
5027
5042
|
if (!isEnabled) {
|
|
5028
5043
|
return false;
|
|
5029
5044
|
}
|
|
5030
|
-
const
|
|
5031
|
-
const excludePatterns =
|
|
5045
|
+
const filesConfig = this.getFilesConfigForTool(tool);
|
|
5046
|
+
const excludePatterns = filesConfig.exclude || [];
|
|
5032
5047
|
return !this.isPathExcluded(filePath, excludePatterns);
|
|
5033
5048
|
}
|
|
5034
5049
|
/**
|
|
@@ -5051,11 +5066,11 @@ class Config {
|
|
|
5051
5066
|
}
|
|
5052
5067
|
/**
|
|
5053
5068
|
* Check if a specific rule is enabled for a specific file path.
|
|
5054
|
-
* Respects
|
|
5069
|
+
* Respects rule.enabled, rule.include, rule.only, and rule.exclude patterns.
|
|
5055
5070
|
*
|
|
5056
5071
|
* Pattern precedence:
|
|
5057
|
-
* - If rule.only
|
|
5058
|
-
* - If rule.only
|
|
5072
|
+
* - If rule.only or rule.include matches the path: bypasses parent excludes (linter.exclude, files.exclude, defaults)
|
|
5073
|
+
* - If no rule.only/include patterns or path doesn't match: respects all parent excludes
|
|
5059
5074
|
* - rule.exclude is always applied regardless of 'only' or 'include'
|
|
5060
5075
|
*
|
|
5061
5076
|
* @param ruleName - The name of the rule to check
|
|
@@ -5063,7 +5078,7 @@ class Config {
|
|
|
5063
5078
|
* @returns true if the rule is enabled for this path
|
|
5064
5079
|
*/
|
|
5065
5080
|
isRuleEnabledForPath(ruleName, filePath) {
|
|
5066
|
-
if (!this.
|
|
5081
|
+
if (!this.isLinterEnabled) {
|
|
5067
5082
|
return false;
|
|
5068
5083
|
}
|
|
5069
5084
|
if (this.isRuleDisabled(ruleName)) {
|
|
@@ -5073,13 +5088,25 @@ class Config {
|
|
|
5073
5088
|
const ruleOnlyPatterns = ruleConfig?.only || [];
|
|
5074
5089
|
const ruleIncludePatterns = ruleConfig?.include || [];
|
|
5075
5090
|
const ruleExcludePatterns = ruleConfig?.exclude || [];
|
|
5091
|
+
let bypassParentExcludes = false;
|
|
5076
5092
|
if (ruleOnlyPatterns.length > 0) {
|
|
5077
|
-
if (
|
|
5093
|
+
if (this.isPathIncluded(filePath, ruleOnlyPatterns)) {
|
|
5094
|
+
bypassParentExcludes = true;
|
|
5095
|
+
}
|
|
5096
|
+
else {
|
|
5078
5097
|
return false;
|
|
5079
5098
|
}
|
|
5080
5099
|
}
|
|
5081
5100
|
else if (ruleIncludePatterns.length > 0) {
|
|
5082
|
-
if (
|
|
5101
|
+
if (this.isPathIncluded(filePath, ruleIncludePatterns)) {
|
|
5102
|
+
bypassParentExcludes = true;
|
|
5103
|
+
}
|
|
5104
|
+
else {
|
|
5105
|
+
return false;
|
|
5106
|
+
}
|
|
5107
|
+
}
|
|
5108
|
+
if (!bypassParentExcludes) {
|
|
5109
|
+
if (!this.isLinterEnabledForPath(filePath)) {
|
|
5083
5110
|
return false;
|
|
5084
5111
|
}
|
|
5085
5112
|
}
|
|
@@ -5416,7 +5443,7 @@ class Config {
|
|
|
5416
5443
|
result.push('');
|
|
5417
5444
|
}
|
|
5418
5445
|
}
|
|
5419
|
-
if (/^ [a-z][\w-]*:/.test(line) && prevLine &&
|
|
5446
|
+
if (/^ [a-z][\w-]*:/.test(line) && prevLine && prevLine.startsWith(' ')) {
|
|
5420
5447
|
result.push('');
|
|
5421
5448
|
}
|
|
5422
5449
|
result.push(line);
|
|
@@ -5570,7 +5597,7 @@ class Config {
|
|
|
5570
5597
|
console.error(`✓ Created default configuration at ${configPath}`);
|
|
5571
5598
|
}
|
|
5572
5599
|
}
|
|
5573
|
-
catch (
|
|
5600
|
+
catch (_error) {
|
|
5574
5601
|
if (!silent) {
|
|
5575
5602
|
console.error(`⚠ Could not create config file at ${configPath}, using defaults in-memory`);
|
|
5576
5603
|
}
|
|
@@ -5659,62 +5686,57 @@ class Config {
|
|
|
5659
5686
|
* Read, parse, and validate config file
|
|
5660
5687
|
*/
|
|
5661
5688
|
static async readAndValidateConfig(configPath, projectRoot, version, exitOnError = false) {
|
|
5689
|
+
const content = await promises.readFile(configPath, "utf8");
|
|
5690
|
+
let parsed;
|
|
5662
5691
|
try {
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5692
|
+
parsed = parse$1(content);
|
|
5693
|
+
}
|
|
5694
|
+
catch (error) {
|
|
5695
|
+
if (exitOnError) {
|
|
5696
|
+
console.error(`\n✗ Invalid YAML syntax in ${configPath}`);
|
|
5697
|
+
if (error instanceof Error) {
|
|
5698
|
+
console.error(` ${error.message}\n`);
|
|
5699
|
+
}
|
|
5700
|
+
process.exit(1);
|
|
5667
5701
|
}
|
|
5668
|
-
|
|
5702
|
+
else {
|
|
5703
|
+
throw new Error(`Invalid YAML syntax in ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5704
|
+
}
|
|
5705
|
+
}
|
|
5706
|
+
if (parsed === null || parsed === undefined) {
|
|
5707
|
+
parsed = {};
|
|
5708
|
+
}
|
|
5709
|
+
if (!parsed.version) {
|
|
5710
|
+
parsed.version = version;
|
|
5711
|
+
}
|
|
5712
|
+
try {
|
|
5713
|
+
HerbConfigSchema.parse(parsed);
|
|
5714
|
+
}
|
|
5715
|
+
catch (error) {
|
|
5716
|
+
if (error instanceof ZodError) {
|
|
5717
|
+
const validationError = fromZodError(error, {
|
|
5718
|
+
prefix: `Configuration errors in ${configPath}`,
|
|
5719
|
+
});
|
|
5669
5720
|
if (exitOnError) {
|
|
5670
|
-
console.error(`\n✗
|
|
5671
|
-
if (error instanceof Error) {
|
|
5672
|
-
console.error(` ${error.message}\n`);
|
|
5673
|
-
}
|
|
5721
|
+
console.error(`\n✗ ${validationError.toString()}\n`);
|
|
5674
5722
|
process.exit(1);
|
|
5675
5723
|
}
|
|
5676
5724
|
else {
|
|
5677
|
-
throw new Error(
|
|
5678
|
-
}
|
|
5679
|
-
}
|
|
5680
|
-
if (parsed === null || parsed === undefined) {
|
|
5681
|
-
parsed = {};
|
|
5682
|
-
}
|
|
5683
|
-
if (!parsed.version) {
|
|
5684
|
-
parsed.version = version;
|
|
5685
|
-
}
|
|
5686
|
-
try {
|
|
5687
|
-
HerbConfigSchema.parse(parsed);
|
|
5688
|
-
}
|
|
5689
|
-
catch (error) {
|
|
5690
|
-
if (error instanceof ZodError) {
|
|
5691
|
-
const validationError = fromZodError(error, {
|
|
5692
|
-
prefix: `Configuration errors in ${configPath}`,
|
|
5693
|
-
});
|
|
5694
|
-
if (exitOnError) {
|
|
5695
|
-
console.error(`\n✗ ${validationError.toString()}\n`);
|
|
5696
|
-
process.exit(1);
|
|
5697
|
-
}
|
|
5698
|
-
else {
|
|
5699
|
-
throw new Error(validationError.toString());
|
|
5700
|
-
}
|
|
5725
|
+
throw new Error(validationError.toString());
|
|
5701
5726
|
}
|
|
5702
|
-
throw error;
|
|
5703
|
-
}
|
|
5704
|
-
if (parsed.version && parsed.version !== version) {
|
|
5705
|
-
console.error(`\n⚠️ Configuration version mismatch in ${configPath}`);
|
|
5706
|
-
console.error(` Config version: ${parsed.version}`);
|
|
5707
|
-
console.error(` Current version: ${version}`);
|
|
5708
|
-
console.error(` Consider updating your .herb.yml file.\n`);
|
|
5709
5727
|
}
|
|
5710
|
-
const defaults = this.getDefaultConfig(version);
|
|
5711
|
-
const resolved = deepMerge(defaults, parsed);
|
|
5712
|
-
resolved.version = version;
|
|
5713
|
-
return new Config(projectRoot, resolved);
|
|
5714
|
-
}
|
|
5715
|
-
catch (error) {
|
|
5716
5728
|
throw error;
|
|
5717
5729
|
}
|
|
5730
|
+
if (parsed.version && parsed.version !== version) {
|
|
5731
|
+
console.error(`\n⚠️ Configuration version mismatch in ${configPath}`);
|
|
5732
|
+
console.error(` Config version: ${parsed.version}`);
|
|
5733
|
+
console.error(` Current version: ${version}`);
|
|
5734
|
+
console.error(` Consider updating your .herb.yml file.\n`);
|
|
5735
|
+
}
|
|
5736
|
+
const defaults = this.getDefaultConfig(version);
|
|
5737
|
+
const resolved = deepMerge(defaults, parsed);
|
|
5738
|
+
resolved.version = version;
|
|
5739
|
+
return new Config(projectRoot, resolved);
|
|
5718
5740
|
}
|
|
5719
5741
|
/**
|
|
5720
5742
|
* Get default configuration object
|
|
@@ -5722,29 +5744,7 @@ class Config {
|
|
|
5722
5744
|
static getDefaultConfig(version = DEFAULT_VERSION) {
|
|
5723
5745
|
return {
|
|
5724
5746
|
version,
|
|
5725
|
-
|
|
5726
|
-
include: [
|
|
5727
|
-
'**/*.html',
|
|
5728
|
-
'**/*.rhtml',
|
|
5729
|
-
'**/*.html.erb',
|
|
5730
|
-
'**/*.html+*.erb',
|
|
5731
|
-
'**/*.turbo_stream.erb'
|
|
5732
|
-
],
|
|
5733
|
-
exclude: [
|
|
5734
|
-
'node_modules/**/*',
|
|
5735
|
-
'vendor/bundle/**/*',
|
|
5736
|
-
'coverage/**/*',
|
|
5737
|
-
]
|
|
5738
|
-
},
|
|
5739
|
-
linter: {
|
|
5740
|
-
enabled: true,
|
|
5741
|
-
rules: {}
|
|
5742
|
-
},
|
|
5743
|
-
formatter: {
|
|
5744
|
-
enabled: false,
|
|
5745
|
-
indentWidth: 2,
|
|
5746
|
-
maxLineLength: 80
|
|
5747
|
-
}
|
|
5747
|
+
...PARSED_DEFAULTS
|
|
5748
5748
|
};
|
|
5749
5749
|
}
|
|
5750
5750
|
}
|
|
@@ -5784,7 +5784,7 @@ function readExtensionsJson(filePath) {
|
|
|
5784
5784
|
}
|
|
5785
5785
|
return parsed;
|
|
5786
5786
|
}
|
|
5787
|
-
catch (
|
|
5787
|
+
catch (_error) {
|
|
5788
5788
|
console.warn(`Warning: Could not parse ${filePath}, creating new file`);
|
|
5789
5789
|
return { recommendations: [] };
|
|
5790
5790
|
}
|