@intentius/chant 0.1.8 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -64,4 +64,32 @@ describe("rule-loader", () => {
64
64
  // Clean up
65
65
  rmSync(join(RULES_DIR, "my-rule.test.ts"));
66
66
  });
67
+
68
+ test("walks up to find .chant/rules/ when invoked from a sub-stack dir", async () => {
69
+ // Project layout: TEST_DIR (root, has .chant/rules/) -> src/east/.
70
+ const subStack = join(TEST_DIR, "src", "east");
71
+ mkdirSync(subStack, { recursive: true });
72
+ writeFileSync(join(TEST_DIR, "package.json"), `{ "name": "fixture" }`);
73
+ try {
74
+ const rules = await loadLocalRules(subStack);
75
+ expect(rules).toHaveLength(1);
76
+ expect(rules[0].id).toBe("LOCAL001");
77
+ } finally {
78
+ rmSync(join(TEST_DIR, "src"), { recursive: true });
79
+ rmSync(join(TEST_DIR, "package.json"));
80
+ }
81
+ });
82
+
83
+ test("stops at the nearest package.json — does not climb into an unrelated parent", async () => {
84
+ const innerProject = join(TEST_DIR, "inner-proj");
85
+ const innerSubDir = join(innerProject, "src");
86
+ mkdirSync(innerSubDir, { recursive: true });
87
+ writeFileSync(join(innerProject, "package.json"), `{ "name": "inner" }`);
88
+ try {
89
+ const rules = await loadLocalRules(innerSubDir);
90
+ expect(rules).toHaveLength(0);
91
+ } finally {
92
+ rmSync(innerProject, { recursive: true });
93
+ }
94
+ });
67
95
  });
@@ -1,5 +1,5 @@
1
1
  import { readdirSync, existsSync } from "fs";
2
- import { join, resolve } from "path";
2
+ import { dirname, join, resolve } from "path";
3
3
  import type { LintRule } from "./rule";
4
4
 
5
5
  /**
@@ -19,18 +19,51 @@ function isLintRule(value: unknown): value is LintRule {
19
19
  }
20
20
 
21
21
  /**
22
- * Load local lint rules from `.chant/rules/` directory.
22
+ * Walk up from `from` until a directory containing `.chant/rules/` is found,
23
+ * or until we hit the filesystem root or a non-project boundary (a directory
24
+ * with `package.json` but no `.chant/rules/` — that's the project root and
25
+ * we stop there even if no rules dir exists).
23
26
  *
24
- * Scans for `.ts` files, dynamically imports each, and collects
25
- * all exports that conform to the LintRule interface.
27
+ * Returns the absolute path to the discovered `.chant/rules/` directory, or
28
+ * null if none was found before we crossed the project root.
29
+ */
30
+ function findRulesDir(from: string): string | null {
31
+ let cur = resolve(from);
32
+ while (true) {
33
+ const candidate = join(cur, ".chant", "rules");
34
+ if (existsSync(candidate)) {
35
+ return candidate;
36
+ }
37
+ // Stop at the nearest project root (where package.json sits) — going
38
+ // above it would pick up rules belonging to an unrelated parent project.
39
+ if (existsSync(join(cur, "package.json"))) {
40
+ return null;
41
+ }
42
+ const parent = dirname(cur);
43
+ if (parent === cur) {
44
+ return null;
45
+ }
46
+ cur = parent;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Load local lint rules from `.chant/rules/`.
52
+ *
53
+ * Scans for `.ts` files in the nearest `.chant/rules/` directory found by
54
+ * walking up from `projectDir` (stopping at the closest `package.json` to
55
+ * avoid leaking into unrelated parent projects). Dynamically imports each
56
+ * file and collects all exports that conform to the LintRule interface.
26
57
  *
27
- * @param projectDir - Root directory of the project
28
- * @returns Array of LintRule objects found in `.chant/rules/`
58
+ * @param projectDir - Directory the lint command was invoked against. May be
59
+ * a sub-stack of a larger project the rules dir is
60
+ * resolved by walking up.
61
+ * @returns Array of LintRule objects found in the discovered `.chant/rules/`.
29
62
  */
30
63
  export async function loadLocalRules(projectDir: string): Promise<LintRule[]> {
31
- const rulesDir = join(projectDir, ".chant", "rules");
64
+ const rulesDir = findRulesDir(projectDir);
32
65
 
33
- if (!existsSync(rulesDir)) {
66
+ if (rulesDir === null) {
34
67
  return [];
35
68
  }
36
69