aria-ease 6.6.0 → 6.8.0

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.
Files changed (29) hide show
  1. package/README.md +81 -16
  2. package/bin/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +87 -35
  3. package/bin/chunk-VPBHLMAS.js +127 -0
  4. package/bin/cli.cjs +380 -231
  5. package/bin/cli.js +8 -123
  6. package/bin/configLoader-XRF6VM4J.js +7 -0
  7. package/{dist/contractTestRunnerPlaywright-PC6JOYYV.js → bin/contractTestRunnerPlaywright-UAOFNS7Z.js} +98 -59
  8. package/bin/{test-LP723IXM.js → test-WRIJHN6H.js} +65 -24
  9. package/dist/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +87 -35
  10. package/dist/configLoader-IT4PWCJB.js +128 -0
  11. package/{bin/contractTestRunnerPlaywright-PC6JOYYV.js → dist/contractTestRunnerPlaywright-UAOFNS7Z.js} +98 -59
  12. package/dist/index.cjs +404 -125
  13. package/dist/index.d.cts +6 -1
  14. package/dist/index.d.ts +6 -1
  15. package/dist/index.js +83 -29
  16. package/dist/src/menu/index.cjs +18 -5
  17. package/dist/src/menu/index.js +18 -5
  18. package/dist/src/utils/test/aria-contracts/accordion/accordion.contract.json +13 -19
  19. package/dist/src/utils/test/aria-contracts/combobox/combobox.listbox.contract.json +5 -5
  20. package/dist/src/utils/test/aria-contracts/menu/menu.contract.json +45 -20
  21. package/dist/src/utils/test/aria-contracts/tabs/tabs.contract.json +3 -3
  22. package/dist/src/utils/test/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +85 -36
  23. package/dist/src/utils/test/configLoader-LD4RV2WQ.js +126 -0
  24. package/dist/src/utils/test/{contractTestRunnerPlaywright-RGKMGXND.js → contractTestRunnerPlaywright-IRJOAEMT.js} +94 -58
  25. package/dist/src/utils/test/index.cjs +380 -119
  26. package/dist/src/utils/test/index.d.cts +7 -1
  27. package/dist/src/utils/test/index.d.ts +7 -1
  28. package/dist/src/utils/test/index.js +61 -23
  29. package/package.json +1 -1
package/README.md CHANGED
@@ -13,19 +13,15 @@ Stop treating accessibility as an afterthought. Aria-Ease engineers accessibilit
13
13
 
14
14
  Aria-Ease isn't a utility library. **It's an accessibility infrastructure** that integrates into every phase of your frontend engineering lifecycle:
15
15
 
16
- | Phase | Feature | Status | Impact |
17
- | ------------------ | --------------------------------------------- | ------------ | --------------------------------------- |
18
- | **🔧 Development** | Component utilities for accessible patterns | ✅ Available | Build it right from the start |
19
- | **⚡ Linting** | ESLint rules to enforce accessible coding | 🚧 Roadmap | Catch mistakes as you type |
20
- | **🔍 Pre-Deploy** | Axe-core powered static accessibility audit | ✅ Available | Verify before it ships |
21
- | **🧪 Testing** | WAI-ARIA APG contract testing with Playwright | ✅ Available | 26 combobox assertions in ~2 seconds |
22
- | **🚀 CI/CD** | Accessibility as deployment gatekeeper | ✅ Available | Block inaccessible code from production |
23
- | **📊 Production** | Real user signal monitoring and replay | 🚧 Roadmap | Understand how users actually interact |
24
- | **📈 Insights** | Dashboard for reporting and analytics | 🚧 Roadmap | Visualize accessibility health |
25
-
26
- **The philosophy:** By the time your app reaches manual testing, there should only be minute, non-automatable aspects left to verify.
27
-
28
- **The reality:** Code that fails accessibility checks cannot reach production. Period.
16
+ | Phase | Feature | Status | Impact |
17
+ | ------------------ | --------------------------------------------- | ------------ | --------------------------------------------- |
18
+ | **🔧 Development** | Component utilities for accessible patterns | ✅ Available | Build it right from the start |
19
+ | **⚡ Linting** | ESLint rules to enforce accessible coding | 🚧 Roadmap | Catch mistakes as you type |
20
+ | **🔍 Pre-Deploy** | Axe-core powered static accessibility audit | ✅ Available | Verify before it ships |
21
+ | **🧪 Testing** | WAI-ARIA APG contract testing with Playwright | ✅ Available | Fast, determinic component accessibility test |
22
+ | **🚀 CI/CD** | Accessibility as deployment gatekeeper | ✅ Available | Block inaccessible code from production |
23
+ | **📊 Production** | Real user signal monitoring and replay | 🚧 Roadmap | Understand how users actually interact |
24
+ | **📈 Insights** | Dashboard for reporting and analytics | 🚧 Roadmap | Visualize accessibility health |
29
25
 
30
26
  ---
31
27
 
@@ -35,7 +31,7 @@ Aria-Ease isn't a utility library. **It's an accessibility infrastructure** that
35
31
 
36
32
  **Traditional approach:** Build features → Manual testing → Find accessibility issues → Fix them → Manual testing again → Ship (maybe)
37
33
 
38
- **Aria-Ease approach:** Build with accessible utilities → Automated audits catch issues → Contract tests verify behavior → CI/CD gates deployment → Ship with confidence
34
+ **Aria-Ease approach:** Build with accessible utilities → Automated audits catch issues → Contract tests verify deterministic component behaviors → CI/CD gates deployment → Ship with confidence
39
35
 
40
36
  ### What Makes This Different?
41
37
 
@@ -74,7 +70,7 @@ npx aria-ease test
74
70
  # ✓ 26 assertions in ~1 second in CI
75
71
  ```
76
72
 
77
- **Why this matters:** Before, verifying a combobox meant manual keyboard testing across browsers. Now, it's automated, fast, and repeatable. You can boast about executing 26 combobox interaction assertions in ~2 seconds.
73
+ **Why this matters:** Before, verifying a combobox meant testing every interaction manually. Now, Aria-Ease automates the deterministic aspects of testing a combobox; keyboard interaction, ARIA states update, visibility, semantic roles.
78
74
 
79
75
  #### 4. **CI/CD Integration** (Available Now)
80
76
 
@@ -158,6 +154,9 @@ export default {
158
154
  out: "./accessibility-reports",
159
155
  },
160
156
  },
157
+ test: {
158
+ strictness: "balanced", // 'minimal' | 'balanced' | 'strict' | 'paranoid'
159
+ },
161
160
  };
162
161
  ```
163
162
 
@@ -662,6 +661,72 @@ describe("Shopify User Menu Accessibility Test", () => {
662
661
  });
663
662
  ```
664
663
 
664
+ ### Strictness Modes
665
+
666
+ Aria-Ease supports strictness policies to define how APG specs are enforced.
667
+
668
+ - `minimal`
669
+ - `required` -> error
670
+ - `recommended` -> ignore
671
+ - `optional` -> ignore
672
+ - `balanced` (default)
673
+ - `required` -> error
674
+ - `recommended` -> warning
675
+ - `optional` -> ignore
676
+ - `strict`
677
+ - `required` -> error
678
+ - `recommended` -> error
679
+ - `optional` -> warning
680
+ - `paranoid`
681
+ - `required` -> error
682
+ - `recommended` -> error
683
+ - `optional` -> error
684
+
685
+ Configure strictness globally in `ariaease.config.*`:
686
+
687
+ ```javascript
688
+ export default {
689
+ test: {
690
+ strictness: "balanced",
691
+ },
692
+ };
693
+ ```
694
+
695
+ Or define strictness per component (recommended when components have different maturity levels):
696
+
697
+ ```javascript
698
+ export default {
699
+ test: {
700
+ strictness: "balanced", // fallback
701
+ components: [
702
+ {
703
+ name: "menu",
704
+ strictness: "strict",
705
+ },
706
+ {
707
+ name: "accordion",
708
+ strictness: "minimal",
709
+ },
710
+ ],
711
+ },
712
+ };
713
+ ```
714
+
715
+ `path` is optional and not required for strictness resolution.
716
+
717
+ Or override per test call:
718
+
719
+ ```javascript
720
+ await testUiComponent(
721
+ "menu",
722
+ null,
723
+ "http://localhost:5173/test-harness?component=menu",
724
+ {
725
+ strictness: "strict",
726
+ },
727
+ );
728
+ ```
729
+
665
730
  ---
666
731
 
667
732
  ## 📦 Bundle Size
@@ -989,7 +1054,7 @@ npx aria-ease test
989
1054
  - Deterministic JSON contracts (no flaky selectors)
990
1055
  - Runs headless in CI
991
1056
 
992
- **Compare to manual testing:** Manually verifying keyboard interactions for a combobox (arrow keys, typing, Enter, Escape, Home/End, etc.) across browsers, verifying WAI-ARIA roles, states and properties = 20-30 minutes. Aria-Ease = 4 seconds.
1057
+ **Compare to manual testing:** Manually verifying keyboard interactions for a combobox listbox (arrow keys, typing, Enter, Escape, Home/End, etc.) across browsers, verifying WAI-ARIA roles, states and properties = 20-30 minutes. Aria-Ease = ~2 seconds.
993
1058
 
994
1059
  ### The Moment of Truth
995
1060
 
@@ -24,10 +24,11 @@ var ContractReporter = class {
24
24
  componentName = "";
25
25
  staticPasses = 0;
26
26
  staticFailures = 0;
27
+ staticWarnings = 0;
27
28
  dynamicResults = [];
28
29
  totalTests = 0;
29
30
  skipped = 0;
30
- optionalSuggestions = 0;
31
+ warnings = 0;
31
32
  isPlaywright = false;
32
33
  apgUrl = "https://www.w3.org/WAI/ARIA/apg/";
33
34
  hasPrintedStaticSection = false;
@@ -54,23 +55,27 @@ ${"\u2550".repeat(60)}`);
54
55
  this.log(`${"\u2550".repeat(60)}
55
56
  `);
56
57
  }
57
- reportStatic(passes, failures) {
58
+ reportStatic(passes, failures, warnings = 0) {
58
59
  this.staticPasses = passes;
59
60
  this.staticFailures = failures;
61
+ this.staticWarnings = warnings;
60
62
  }
61
63
  /**
62
64
  * Report individual static test pass
63
65
  */
64
- reportStaticTest(description, passed, failureMessage) {
66
+ reportStaticTest(description, status, failureMessage, level) {
65
67
  if (!this.hasPrintedStaticSection) {
66
68
  this.log(`${"\u2500".repeat(60)}`);
67
69
  this.log(`\u{1F9EA} Static Assertions`);
68
70
  this.log(`${"\u2500".repeat(60)}`);
69
71
  this.hasPrintedStaticSection = true;
70
72
  }
71
- const icon = passed ? "\u2713" : "\u2717";
73
+ const icon = status === "pass" ? "\u2713" : status === "warn" ? "\u26A0" : status === "skip" ? "\u25CB" : "\u2717";
72
74
  this.log(` ${icon} ${description}`);
73
- if (!passed && failureMessage) {
75
+ if (level) {
76
+ this.log(` \u21B3 level=${level}`);
77
+ }
78
+ if ((status === "fail" || status === "warn" || status === "skip") && failureMessage) {
74
79
  this.log(` \u21B3 ${failureMessage}`);
75
80
  }
76
81
  }
@@ -89,23 +94,26 @@ ${"\u2550".repeat(60)}`);
89
94
  description: test.description,
90
95
  status,
91
96
  failureMessage,
92
- isOptional: test.isOptional
97
+ level: test.level
93
98
  };
94
99
  if (status === "skip") {
95
100
  result.skipReason = "Requires real browser (addEventListener events)";
96
101
  }
97
102
  this.dynamicResults.push(result);
98
- const icons = { pass: "\u2713", fail: "\u2717", skip: "\u25CB", "optional-fail": "\u25CB" };
99
- const prefix = test.isOptional ? "[OPTIONAL] " : "";
100
- this.log(` ${icons[status]} ${prefix}${test.description}`);
103
+ const icons = { pass: "\u2713", fail: "\u2717", warn: "\u26A0", skip: "\u25CB" };
104
+ const levelPrefix = test.level ? `[${test.level.toUpperCase()}] ` : "";
105
+ this.log(` ${icons[status]} ${levelPrefix}${test.description}`);
101
106
  if (status === "skip" && !this.isPlaywright) {
102
107
  this.log(` \u21B3 Skipped in jsdom (runs in Playwright)`);
103
108
  }
104
- if (status === "fail" && failureMessage && !test.isOptional) {
109
+ if (status === "fail" && failureMessage) {
105
110
  this.log(` \u21B3 ${failureMessage}`);
106
111
  }
107
- if (status === "optional-fail") {
108
- this.log(` \u21B3 Not implemented (recommended for enhanced UX)`);
112
+ if (status === "warn" && failureMessage) {
113
+ this.log(` \u21B3 ${failureMessage}`);
114
+ }
115
+ if (status === "skip" && failureMessage) {
116
+ this.log(` \u21B3 ${failureMessage}`);
109
117
  }
110
118
  }
111
119
  /**
@@ -129,29 +137,29 @@ ${"\u2500".repeat(60)}`);
129
137
  this.log("");
130
138
  });
131
139
  }
132
- /**
133
- * Report optional features that aren't implemented
134
- */
135
- reportOptionalSuggestions() {
136
- const suggestions = this.dynamicResults.filter((r) => r.status === "optional-fail");
137
- if (suggestions.length === 0) return;
140
+ reportWarnings() {
141
+ const warnings = this.dynamicResults.filter((r) => r.status === "warn");
142
+ if (warnings.length === 0 && this.staticWarnings === 0) return;
138
143
  this.log(`
139
144
  ${"\u2500".repeat(60)}`);
140
- this.log(`\u{1F4A1} Optional Enhancements (${suggestions.length}):
145
+ this.log(`\u26A0\uFE0F Warnings (${this.staticWarnings + warnings.length}):
141
146
  `);
142
- this.log(`These features are optional per APG guidelines but recommended`);
143
- this.log(`for improved user experience and keyboard interaction:
147
+ this.log(`These checks are failing but treated as warnings under the active strictness mode.
144
148
  `);
145
- suggestions.forEach((test, index) => {
149
+ warnings.forEach((test, index) => {
146
150
  this.log(`${index + 1}. ${test.description}`);
147
151
  if (test.failureMessage) {
148
152
  this.log(` \u21B3 ${test.failureMessage}`);
149
153
  }
154
+ if (test.level) {
155
+ this.log(` \u21B3 level=${test.level}`);
156
+ }
150
157
  });
151
- this.log(`
152
- \u2728 Consider implementing these for better accessibility`);
153
- this.log(` Reference: ${this.apgUrl}
158
+ if (this.apgUrl) {
159
+ this.log(`
160
+ Reference: ${this.apgUrl}
154
161
  `);
162
+ }
155
163
  }
156
164
  /**
157
165
  * Report skipped tests with helpful context
@@ -181,41 +189,42 @@ ${"\u2500".repeat(60)}`);
181
189
  const duration = Date.now() - this.startTime;
182
190
  const dynamicPasses = this.dynamicResults.filter((r) => r.status === "pass").length;
183
191
  const dynamicFailures = this.dynamicResults.filter((r) => r.status === "fail").length;
192
+ const dynamicWarnings = this.dynamicResults.filter((r) => r.status === "warn").length;
184
193
  this.skipped = this.dynamicResults.filter((r) => r.status === "skip").length;
185
- this.optionalSuggestions = this.dynamicResults.filter((r) => r.status === "optional-fail").length;
194
+ this.warnings = this.staticWarnings + dynamicWarnings;
186
195
  const totalPasses = this.staticPasses + dynamicPasses;
187
196
  const totalFailures = this.staticFailures + dynamicFailures;
188
- const totalRun = totalPasses + totalFailures;
197
+ const totalRun = totalPasses + totalFailures + this.warnings;
189
198
  if (failures.length > 0) {
190
199
  this.reportFailures(failures);
191
200
  }
192
- this.reportOptionalSuggestions();
201
+ this.reportWarnings();
193
202
  this.reportSkipped();
194
203
  this.log(`
195
204
  ${"\u2550".repeat(60)}`);
196
205
  this.log(`\u{1F4CA} Summary
197
206
  `);
198
- if (totalFailures === 0 && this.skipped === 0 && this.optionalSuggestions === 0) {
207
+ if (totalFailures === 0 && this.skipped === 0 && this.warnings === 0) {
199
208
  this.log(`\u2705 All ${totalRun} tests passed!`);
200
209
  this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
201
210
  } else if (totalFailures === 0) {
202
- this.log(`\u2705 ${totalPasses}/${totalRun} required tests passed`);
211
+ this.log(`\u2705 ${totalPasses}/${totalRun} tests passed`);
203
212
  if (this.skipped > 0) {
204
213
  this.log(`\u25CB ${this.skipped} tests skipped`);
205
214
  }
206
- if (this.optionalSuggestions > 0) {
207
- this.log(`\u{1F4A1} ${this.optionalSuggestions} optional enhancement${this.optionalSuggestions > 1 ? "s" : ""} suggested`);
215
+ if (this.warnings > 0) {
216
+ this.log(`\u26A0\uFE0F ${this.warnings} warning${this.warnings > 1 ? "s" : ""}`);
208
217
  }
209
218
  this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
210
219
  } else {
211
220
  this.log(`\u274C ${totalFailures} test${totalFailures > 1 ? "s" : ""} failed`);
212
221
  this.log(`\u2705 ${totalPasses} test${totalPasses > 1 ? "s" : ""} passed`);
222
+ if (this.warnings > 0) {
223
+ this.log(`\u26A0\uFE0F ${this.warnings} warning${this.warnings > 1 ? "s" : ""}`);
224
+ }
213
225
  if (this.skipped > 0) {
214
226
  this.log(`\u25CB ${this.skipped} test${this.skipped > 1 ? "s" : ""} skipped`);
215
227
  }
216
- if (this.optionalSuggestions > 0) {
217
- this.log(`\u{1F4A1} ${this.optionalSuggestions} optional enhancement${this.optionalSuggestions > 1 ? "s" : ""} suggested`);
218
- }
219
228
  }
220
229
  this.log(`\u23F1\uFE0F Duration: ${duration}ms`);
221
230
  this.log(`${"\u2550".repeat(60)}
@@ -250,6 +259,46 @@ ${"\u2550".repeat(60)}`);
250
259
  }
251
260
  };
252
261
 
262
+ // src/utils/test/src/strictness.ts
263
+ var FALLBACK_LEVEL = "required";
264
+ function normalizeLevel(level) {
265
+ if (level === "required" || level === "recommended" || level === "optional") {
266
+ return level;
267
+ }
268
+ return FALLBACK_LEVEL;
269
+ }
270
+ function normalizeStrictness(strictness) {
271
+ if (strictness === "minimal" || strictness === "balanced" || strictness === "strict" || strictness === "paranoid") {
272
+ return strictness;
273
+ }
274
+ return "balanced";
275
+ }
276
+ function resolveEnforcement(level, strictness) {
277
+ const matrix = {
278
+ minimal: {
279
+ required: "error",
280
+ recommended: "ignore",
281
+ optional: "ignore"
282
+ },
283
+ balanced: {
284
+ required: "error",
285
+ recommended: "warning",
286
+ optional: "ignore"
287
+ },
288
+ strict: {
289
+ required: "error",
290
+ recommended: "error",
291
+ optional: "warning"
292
+ },
293
+ paranoid: {
294
+ required: "error",
295
+ recommended: "error",
296
+ optional: "error"
297
+ }
298
+ };
299
+ return matrix[strictness][level];
300
+ }
301
+
253
302
  // src/utils/test/src/playwrightTestHarness.ts
254
303
  import { chromium } from "playwright";
255
304
  var sharedBrowser = null;
@@ -297,6 +346,9 @@ async function closeSharedBrowser() {
297
346
  export {
298
347
  contract_default,
299
348
  ContractReporter,
349
+ normalizeLevel,
350
+ normalizeStrictness,
351
+ resolveEnforcement,
300
352
  createTestPage,
301
353
  closeSharedBrowser
302
354
  };
@@ -0,0 +1,127 @@
1
+ // src/utils/cli/configLoader.ts
2
+ import path from "path";
3
+ import fs from "fs-extra";
4
+ function validateConfig(config) {
5
+ const errors = [];
6
+ if (!config || typeof config !== "object") {
7
+ errors.push("Config must be an object");
8
+ return { valid: false, errors };
9
+ }
10
+ const cfg = config;
11
+ if (cfg.audit !== void 0) {
12
+ if (typeof cfg.audit !== "object" || cfg.audit === null) {
13
+ errors.push("audit must be an object");
14
+ } else {
15
+ if (cfg.audit.urls !== void 0) {
16
+ if (!Array.isArray(cfg.audit.urls)) {
17
+ errors.push("audit.urls must be an array");
18
+ } else if (cfg.audit.urls.some((url) => typeof url !== "string")) {
19
+ errors.push("audit.urls must contain only strings");
20
+ }
21
+ }
22
+ if (cfg.audit.output !== void 0) {
23
+ if (typeof cfg.audit.output !== "object") {
24
+ errors.push("audit.output must be an object");
25
+ } else {
26
+ const output = cfg.audit.output;
27
+ if (output.format !== void 0) {
28
+ if (!["json", "csv", "html", "all"].includes(output.format)) {
29
+ errors.push("audit.output.format must be one of: json, csv, html, all");
30
+ }
31
+ }
32
+ if (output.out !== void 0 && typeof output.out !== "string") {
33
+ errors.push("audit.output.out must be a string");
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ if (cfg.test !== void 0) {
40
+ if (typeof cfg.test !== "object" || cfg.test === null) {
41
+ errors.push("test must be an object");
42
+ } else {
43
+ if (cfg.test.components !== void 0) {
44
+ if (!Array.isArray(cfg.test.components)) {
45
+ errors.push("test.components must be an array");
46
+ } else {
47
+ cfg.test.components.forEach((comp, idx) => {
48
+ if (typeof comp !== "object" || comp === null) {
49
+ errors.push(`test.components[${idx}] must be an object`);
50
+ } else {
51
+ if (typeof comp.name !== "string") {
52
+ errors.push(`test.components[${idx}].name must be a string`);
53
+ }
54
+ if (comp.path !== void 0 && typeof comp.path !== "string") {
55
+ errors.push(`test.components[${idx}].path must be a string when provided`);
56
+ }
57
+ if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
58
+ errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
59
+ }
60
+ }
61
+ });
62
+ }
63
+ }
64
+ if (cfg.test.strictness !== void 0) {
65
+ if (!["minimal", "balanced", "strict", "paranoid"].includes(cfg.test.strictness)) {
66
+ errors.push("test.strictness must be one of: minimal, balanced, strict, paranoid");
67
+ }
68
+ }
69
+ }
70
+ }
71
+ return { valid: errors.length === 0, errors };
72
+ }
73
+ async function loadConfigFile(filePath) {
74
+ try {
75
+ const ext = path.extname(filePath);
76
+ if (ext === ".json") {
77
+ const content = await fs.readFile(filePath, "utf-8");
78
+ return JSON.parse(content);
79
+ } else if ([".js", ".mjs", ".cjs", ".ts"].includes(ext)) {
80
+ const imported = await import(filePath);
81
+ return imported.default || imported;
82
+ }
83
+ return null;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+ async function loadConfig(cwd = process.cwd()) {
89
+ const configNames = [
90
+ "ariaease.config.js",
91
+ "ariaease.config.mjs",
92
+ "ariaease.config.cjs",
93
+ "ariaease.config.json",
94
+ "ariaease.config.ts"
95
+ ];
96
+ let loadedConfig = null;
97
+ let foundPath = null;
98
+ const errors = [];
99
+ for (const name of configNames) {
100
+ const configPath = path.resolve(cwd, name);
101
+ if (await fs.pathExists(configPath)) {
102
+ foundPath = configPath;
103
+ loadedConfig = await loadConfigFile(configPath);
104
+ if (loadedConfig === null) {
105
+ errors.push(`Found config at ${name} but failed to load it. Check for syntax errors.`);
106
+ continue;
107
+ }
108
+ const validation = validateConfig(loadedConfig);
109
+ if (!validation.valid) {
110
+ errors.push(`Config validation failed in ${name}:`);
111
+ errors.push(...validation.errors.map((err) => ` - ${err}`));
112
+ loadedConfig = null;
113
+ continue;
114
+ }
115
+ break;
116
+ }
117
+ }
118
+ return {
119
+ config: loadedConfig || {},
120
+ configPath: loadedConfig ? foundPath : null,
121
+ errors
122
+ };
123
+ }
124
+
125
+ export {
126
+ loadConfig
127
+ };