@intentius/chant 0.0.15 → 0.0.18
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 +1 -1
- package/src/codegen/docs.ts +13 -2
- package/src/codegen/generate.test.ts +111 -0
- package/src/codegen/generate.ts +14 -3
- package/src/index.ts +1 -0
- package/src/lint/discover.test.ts +69 -0
- package/src/lint/discover.ts +112 -0
- package/src/lint/rules/index.ts +1 -0
package/package.json
CHANGED
package/src/codegen/docs.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface DocsConfig {
|
|
|
46
46
|
basePath?: string;
|
|
47
47
|
/** Root directory for resolving {{file:...}} markers in extra page content */
|
|
48
48
|
examplesDir?: string;
|
|
49
|
+
/** Extra sidebar entries appended after extraPages (supports Starlight groups) */
|
|
50
|
+
sidebarExtra?: Array<Record<string, unknown>>;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
export interface DocsResult {
|
|
@@ -285,8 +287,12 @@ export function writeDocsSite(config: DocsConfig, result: DocsResult): void {
|
|
|
285
287
|
const outDir = config.outDir;
|
|
286
288
|
const contentDir = join(outDir, "src", "content", "docs");
|
|
287
289
|
|
|
288
|
-
// Clear stale content and Astro caches so changes are picked up on next build
|
|
289
|
-
|
|
290
|
+
// Clear stale generated content and Astro caches so changes are picked up on next build.
|
|
291
|
+
// Only remove files that will be regenerated — preserve hand-written pages.
|
|
292
|
+
for (const filename of result.pages.keys()) {
|
|
293
|
+
const filePath = join(contentDir, filename);
|
|
294
|
+
rmSync(filePath, { force: true });
|
|
295
|
+
}
|
|
290
296
|
rmSync(join(outDir, ".astro"), { recursive: true, force: true });
|
|
291
297
|
rmSync(join(outDir, "node_modules", ".astro"), { recursive: true, force: true });
|
|
292
298
|
|
|
@@ -411,6 +417,11 @@ function buildSidebar(
|
|
|
411
417
|
items.push({ label: "Serialization", slug: "serialization" });
|
|
412
418
|
}
|
|
413
419
|
|
|
420
|
+
// Append raw sidebar entries (supports groups and nested items)
|
|
421
|
+
if (config.sidebarExtra) {
|
|
422
|
+
items.push(...config.sidebarExtra);
|
|
423
|
+
}
|
|
424
|
+
|
|
414
425
|
return items;
|
|
415
426
|
}
|
|
416
427
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { generatePipeline, type GeneratePipelineConfig, type ParsedResult } from "./generate";
|
|
3
|
+
import { NamingStrategy } from "./naming";
|
|
4
|
+
|
|
5
|
+
interface TestResult extends ParsedResult {
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function makeConfig(
|
|
10
|
+
schemas: Map<string, Buffer>,
|
|
11
|
+
parseOverride?: (name: string, data: Buffer) => TestResult | TestResult[] | null,
|
|
12
|
+
): GeneratePipelineConfig<TestResult> {
|
|
13
|
+
return {
|
|
14
|
+
fetchSchemas: async () => schemas,
|
|
15
|
+
parseSchema: parseOverride ?? ((name, _data) => ({
|
|
16
|
+
name,
|
|
17
|
+
propertyTypes: [],
|
|
18
|
+
enums: [],
|
|
19
|
+
})),
|
|
20
|
+
createNaming: (results) => new NamingStrategy(
|
|
21
|
+
results.map((r) => ({ typeName: r.name, propertyTypes: r.propertyTypes })),
|
|
22
|
+
{
|
|
23
|
+
priorityNames: {},
|
|
24
|
+
priorityAliases: {},
|
|
25
|
+
priorityPropertyAliases: {},
|
|
26
|
+
serviceAbbreviations: {},
|
|
27
|
+
shortName: (t) => t,
|
|
28
|
+
serviceName: () => "",
|
|
29
|
+
},
|
|
30
|
+
),
|
|
31
|
+
generateRegistry: () => "{}",
|
|
32
|
+
generateTypes: () => "// types",
|
|
33
|
+
generateRuntimeIndex: () => "// index",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("generatePipeline", () => {
|
|
38
|
+
test("processes single-result parseSchema", async () => {
|
|
39
|
+
const schemas = new Map([
|
|
40
|
+
["TypeA", Buffer.from("a")],
|
|
41
|
+
["TypeB", Buffer.from("b")],
|
|
42
|
+
]);
|
|
43
|
+
const result = await generatePipeline(makeConfig(schemas));
|
|
44
|
+
expect(result.resources).toBe(2);
|
|
45
|
+
expect(result.warnings).toHaveLength(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("supports parseSchema returning arrays", async () => {
|
|
49
|
+
const schemas = new Map([
|
|
50
|
+
["combined", Buffer.from("data")],
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const config = makeConfig(schemas, (_name, _data) => [
|
|
54
|
+
{ name: "TypeA", propertyTypes: [{ name: "PropA" }], enums: [] },
|
|
55
|
+
{ name: "TypeB", propertyTypes: [{ name: "PropB" }], enums: [] },
|
|
56
|
+
{ name: "TypeC", propertyTypes: [], enums: [{}] },
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const result = await generatePipeline(config);
|
|
60
|
+
expect(result.resources).toBe(3);
|
|
61
|
+
expect(result.properties).toBe(2);
|
|
62
|
+
expect(result.enums).toBe(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("handles parseSchema returning null (skip)", async () => {
|
|
66
|
+
const schemas = new Map([
|
|
67
|
+
["skip-me", Buffer.from("x")],
|
|
68
|
+
["keep-me", Buffer.from("y")],
|
|
69
|
+
]);
|
|
70
|
+
const config = makeConfig(schemas, (name, _data) => {
|
|
71
|
+
if (name === "skip-me") return null;
|
|
72
|
+
return { name, propertyTypes: [], enums: [] };
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = await generatePipeline(config);
|
|
76
|
+
expect(result.resources).toBe(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("handles mixed single and array returns", async () => {
|
|
80
|
+
const schemas = new Map([
|
|
81
|
+
["single", Buffer.from("s")],
|
|
82
|
+
["multi", Buffer.from("m")],
|
|
83
|
+
]);
|
|
84
|
+
const config = makeConfig(schemas, (name, _data) => {
|
|
85
|
+
if (name === "multi") {
|
|
86
|
+
return [
|
|
87
|
+
{ name: "MultiA", propertyTypes: [], enums: [] },
|
|
88
|
+
{ name: "MultiB", propertyTypes: [], enums: [] },
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
return { name, propertyTypes: [], enums: [] };
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const result = await generatePipeline(config);
|
|
95
|
+
expect(result.resources).toBe(3);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("collects parse errors as warnings", async () => {
|
|
99
|
+
const schemas = new Map([
|
|
100
|
+
["bad", Buffer.from("x")],
|
|
101
|
+
]);
|
|
102
|
+
const config = makeConfig(schemas, () => {
|
|
103
|
+
throw new Error("parse failed");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = await generatePipeline(config);
|
|
107
|
+
expect(result.resources).toBe(0);
|
|
108
|
+
expect(result.warnings).toHaveLength(1);
|
|
109
|
+
expect(result.warnings[0].error).toContain("parse failed");
|
|
110
|
+
});
|
|
111
|
+
});
|
package/src/codegen/generate.ts
CHANGED
|
@@ -48,8 +48,13 @@ export interface GeneratePipelineConfig<T extends ParsedResult> {
|
|
|
48
48
|
/** Fetch or provide raw schema data. */
|
|
49
49
|
fetchSchemas: (opts: { force?: boolean }) => Promise<Map<string, Buffer>>;
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Parse a single schema buffer into results. Returns null to skip.
|
|
53
|
+
*
|
|
54
|
+
* May return an array when a single schema file produces multiple results
|
|
55
|
+
* (e.g. K8s OpenAPI spec, GitLab CI schema).
|
|
56
|
+
*/
|
|
57
|
+
parseSchema: (typeName: string, data: Buffer) => T | T[] | null;
|
|
53
58
|
|
|
54
59
|
/** Create a naming strategy from the parsed results. */
|
|
55
60
|
createNaming: (results: T[]) => NamingStrategy;
|
|
@@ -119,7 +124,13 @@ export async function generatePipeline<T extends ParsedResult>(
|
|
|
119
124
|
for (const [typeName, data] of schemas) {
|
|
120
125
|
try {
|
|
121
126
|
const result = config.parseSchema(typeName, data);
|
|
122
|
-
if (result)
|
|
127
|
+
if (result) {
|
|
128
|
+
if (Array.isArray(result)) {
|
|
129
|
+
results.push(...result);
|
|
130
|
+
} else {
|
|
131
|
+
results.push(result);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
123
134
|
} catch (err) {
|
|
124
135
|
warnings.push({
|
|
125
136
|
file: typeName,
|
package/src/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ export * from "./lint/selectors";
|
|
|
33
33
|
export * from "./lint/named-checks";
|
|
34
34
|
export * from "./lint/post-synth";
|
|
35
35
|
export * from "./lint/rule-loader";
|
|
36
|
+
export * from "./lint/discover";
|
|
36
37
|
export * from "./import/parser";
|
|
37
38
|
export * from "./import/generator";
|
|
38
39
|
export * from "./lexicon";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { discoverLintRules, discoverPostSynthChecks } from "./discover";
|
|
5
|
+
|
|
6
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
describe("discoverLintRules", () => {
|
|
9
|
+
test("returns empty array for non-existent directory", () => {
|
|
10
|
+
const rules = discoverLintRules("/nonexistent/path", import.meta.url);
|
|
11
|
+
expect(rules).toEqual([]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("discovers rules from a real lexicon", () => {
|
|
15
|
+
// Use the Helm lexicon's rules directory as a real test case
|
|
16
|
+
const helmRulesDir = join(thisDir, "../../../../lexicons/helm/src/lint/rules");
|
|
17
|
+
const rules = discoverLintRules(helmRulesDir, import.meta.url);
|
|
18
|
+
expect(rules.length).toBeGreaterThan(0);
|
|
19
|
+
// Each discovered rule should have the expected shape
|
|
20
|
+
for (const rule of rules) {
|
|
21
|
+
expect(typeof rule.id).toBe("string");
|
|
22
|
+
expect(typeof rule.severity).toBe("string");
|
|
23
|
+
expect(typeof rule.category).toBe("string");
|
|
24
|
+
expect(typeof rule.check).toBe("function");
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("sorts rules by id", () => {
|
|
29
|
+
const helmRulesDir = join(thisDir, "../../../../lexicons/helm/src/lint/rules");
|
|
30
|
+
const rules = discoverLintRules(helmRulesDir, import.meta.url);
|
|
31
|
+
for (let i = 1; i < rules.length; i++) {
|
|
32
|
+
expect(rules[i].id.localeCompare(rules[i - 1].id)).toBeGreaterThanOrEqual(0);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("discoverPostSynthChecks", () => {
|
|
38
|
+
test("returns empty array for non-existent directory", () => {
|
|
39
|
+
const checks = discoverPostSynthChecks("/nonexistent/path", import.meta.url);
|
|
40
|
+
expect(checks).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("discovers checks from a real lexicon", () => {
|
|
44
|
+
const helmPostSynthDir = join(thisDir, "../../../../lexicons/helm/src/lint/post-synth");
|
|
45
|
+
const checks = discoverPostSynthChecks(helmPostSynthDir, import.meta.url);
|
|
46
|
+
expect(checks.length).toBeGreaterThan(0);
|
|
47
|
+
for (const check of checks) {
|
|
48
|
+
expect(typeof check.id).toBe("string");
|
|
49
|
+
expect(typeof check.description).toBe("string");
|
|
50
|
+
expect(typeof check.check).toBe("function");
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("sorts checks by id", () => {
|
|
55
|
+
const helmPostSynthDir = join(thisDir, "../../../../lexicons/helm/src/lint/post-synth");
|
|
56
|
+
const checks = discoverPostSynthChecks(helmPostSynthDir, import.meta.url);
|
|
57
|
+
for (let i = 1; i < checks.length; i++) {
|
|
58
|
+
expect(checks[i].id.localeCompare(checks[i - 1].id)).toBeGreaterThanOrEqual(0);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("does not include lint rules", () => {
|
|
63
|
+
// Post-synth checks should not pick up lint rules (which also have check())
|
|
64
|
+
const helmRulesDir = join(thisDir, "../../../../lexicons/helm/src/lint/rules");
|
|
65
|
+
const checks = discoverPostSynthChecks(helmRulesDir, import.meta.url);
|
|
66
|
+
// Lint rules have severity, post-synth checks don't — so this should be empty
|
|
67
|
+
expect(checks.length).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-discovery for lint rules and post-synth checks.
|
|
3
|
+
*
|
|
4
|
+
* Scans directories for .ts files and loads exported rules/checks,
|
|
5
|
+
* eliminating the need to manually list every rule in plugin.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readdirSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { createRequire } from "module";
|
|
11
|
+
import type { LintRule } from "./rule";
|
|
12
|
+
import type { PostSynthCheck } from "./post-synth";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Discover lint rules from a directory.
|
|
16
|
+
*
|
|
17
|
+
* Scans the directory for .ts files (excluding tests, helpers, and index files),
|
|
18
|
+
* loads each module, and collects any exported objects that look like a LintRule
|
|
19
|
+
* (has `id`, `severity`, `category`, and `check` function).
|
|
20
|
+
*
|
|
21
|
+
* @param dir - Absolute path to the rules directory
|
|
22
|
+
* @param importMetaUrl - The caller's import.meta.url (for createRequire)
|
|
23
|
+
*/
|
|
24
|
+
export function discoverLintRules(dir: string, importMetaUrl: string): LintRule[] {
|
|
25
|
+
const require = createRequire(importMetaUrl);
|
|
26
|
+
const rules: LintRule[] = [];
|
|
27
|
+
|
|
28
|
+
for (const file of listRuleFiles(dir)) {
|
|
29
|
+
try {
|
|
30
|
+
// Strip .ts extension — require() resolves without it
|
|
31
|
+
const modPath = join(dir, file.replace(/\.ts$/, ""));
|
|
32
|
+
const mod = require(modPath);
|
|
33
|
+
for (const exp of Object.values(mod)) {
|
|
34
|
+
if (isLintRule(exp)) rules.push(exp);
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// Skip files that fail to load
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return rules.sort((a, b) => a.id.localeCompare(b.id));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Discover post-synth checks from a directory.
|
|
46
|
+
*
|
|
47
|
+
* Scans the directory for .ts files (excluding tests, helpers, and index files),
|
|
48
|
+
* loads each module, and collects any exported objects that look like a PostSynthCheck
|
|
49
|
+
* (has `id`, `description`, and `check` function).
|
|
50
|
+
*
|
|
51
|
+
* @param dir - Absolute path to the post-synth rules directory
|
|
52
|
+
* @param importMetaUrl - The caller's import.meta.url (for createRequire)
|
|
53
|
+
*/
|
|
54
|
+
export function discoverPostSynthChecks(dir: string, importMetaUrl: string): PostSynthCheck[] {
|
|
55
|
+
const require = createRequire(importMetaUrl);
|
|
56
|
+
const checks: PostSynthCheck[] = [];
|
|
57
|
+
|
|
58
|
+
for (const file of listRuleFiles(dir)) {
|
|
59
|
+
try {
|
|
60
|
+
// Strip .ts extension — require() resolves without it
|
|
61
|
+
const modPath = join(dir, file.replace(/\.ts$/, ""));
|
|
62
|
+
const mod = require(modPath);
|
|
63
|
+
for (const exp of Object.values(mod)) {
|
|
64
|
+
if (isPostSynthCheck(exp)) checks.push(exp);
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Skip files that fail to load
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return checks.sort((a, b) => a.id.localeCompare(b.id));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* List rule files in a directory (excluding tests, helpers, index).
|
|
76
|
+
*/
|
|
77
|
+
function listRuleFiles(dir: string): string[] {
|
|
78
|
+
try {
|
|
79
|
+
return readdirSync(dir).filter((f) => {
|
|
80
|
+
if (!f.endsWith(".ts")) return false;
|
|
81
|
+
if (f.endsWith(".test.ts")) return false;
|
|
82
|
+
if (f === "index.ts") return false;
|
|
83
|
+
if (f.includes("helper")) return false;
|
|
84
|
+
return true;
|
|
85
|
+
});
|
|
86
|
+
} catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isLintRule(obj: unknown): obj is LintRule {
|
|
92
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
93
|
+
const o = obj as Record<string, unknown>;
|
|
94
|
+
return (
|
|
95
|
+
typeof o.id === "string" &&
|
|
96
|
+
typeof o.severity === "string" &&
|
|
97
|
+
typeof o.category === "string" &&
|
|
98
|
+
typeof o.check === "function"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isPostSynthCheck(obj: unknown): obj is PostSynthCheck {
|
|
103
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
104
|
+
const o = obj as Record<string, unknown>;
|
|
105
|
+
return (
|
|
106
|
+
typeof o.id === "string" &&
|
|
107
|
+
typeof o.description === "string" &&
|
|
108
|
+
typeof o.check === "function" &&
|
|
109
|
+
// Exclude LintRules (which also have check) by checking for PostSynthCheck-specific fields
|
|
110
|
+
typeof o.severity !== "string"
|
|
111
|
+
);
|
|
112
|
+
}
|
package/src/lint/rules/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export { evl009CompositeNoConstantRule } from "./evl009-composite-no-constant";
|
|
|
25
25
|
export { evl010CompositeNoTransformRule } from "./evl010-composite-no-transform";
|
|
26
26
|
export { cor017CompositeNameMatchRule } from "./cor017-composite-name-match";
|
|
27
27
|
export { cor018CompositePreferLexiconTypeRule } from "./cor018-composite-prefer-lexicon-type";
|
|
28
|
+
export { isInsideCompositeFactory } from "./composite-scope";
|
|
28
29
|
|
|
29
30
|
import { flatDeclarationsRule } from "./flat-declarations";
|
|
30
31
|
import { exportRequiredRule } from "./export-required";
|