@intentius/chant 0.0.3 → 0.0.5
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 +10 -351
- package/package.json +1 -1
- package/src/bench.test.ts +3 -54
- package/src/build.ts +14 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +12 -2
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +22 -18
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +12 -2
- package/src/cli/commands/import.test.ts +1 -1
- package/src/cli/commands/init-lexicon.ts +34 -20
- package/src/cli/commands/init.test.ts +10 -14
- package/src/cli/commands/init.ts +2 -7
- package/src/cli/commands/lint.ts +9 -33
- package/src/cli/main.ts +1 -1
- package/src/codegen/docs-interpolation.test.ts +77 -0
- package/src/codegen/docs.ts +80 -5
- package/src/codegen/generate-registry.test.ts +1 -1
- package/src/codegen/generate-registry.ts +3 -3
- package/src/codegen/package.ts +28 -1
- package/src/codegen/validate.ts +16 -0
- package/src/discovery/collect.ts +7 -0
- package/src/discovery/files.ts +6 -6
- package/src/discovery/import.ts +1 -1
- package/src/index.ts +0 -1
- package/src/lint/engine.ts +1 -5
- package/src/lint/rule.ts +0 -18
- package/src/lint/rules/evl009-composite-no-constant.test.ts +24 -8
- package/src/lint/rules/evl009-composite-no-constant.ts +50 -29
- package/src/lint/rules/index.ts +1 -22
- package/src/stack-output.ts +3 -3
- package/src/barrel.test.ts +0 -157
- package/src/barrel.ts +0 -101
- package/src/lint/rules/barrel-import-style.test.ts +0 -80
- package/src/lint/rules/barrel-import-style.ts +0 -59
- package/src/lint/rules/enforce-barrel-import.test.ts +0 -169
- package/src/lint/rules/enforce-barrel-import.ts +0 -81
- package/src/lint/rules/enforce-barrel-ref.test.ts +0 -114
- package/src/lint/rules/enforce-barrel-ref.ts +0 -75
- package/src/lint/rules/evl006-barrel-usage.test.ts +0 -63
- package/src/lint/rules/evl006-barrel-usage.ts +0 -95
- package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +0 -118
- package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +0 -140
- package/src/lint/rules/prefer-namespace-import.test.ts +0 -102
- package/src/lint/rules/prefer-namespace-import.ts +0 -63
- package/src/lint/rules/stale-barrel-types.ts +0 -60
- package/src/project/scan.test.ts +0 -178
- package/src/project/scan.ts +0 -182
- package/src/project/sync.test.ts +0 -87
- package/src/project/sync.ts +0 -46
|
@@ -22,14 +22,13 @@ describe("initCommand", () => {
|
|
|
22
22
|
expect(result.createdFiles).toContain("tsconfig.json");
|
|
23
23
|
expect(result.createdFiles).toContain("chant.config.ts");
|
|
24
24
|
expect(result.createdFiles).toContain(".gitignore");
|
|
25
|
-
expect(result.createdFiles).toContain("src/_.ts");
|
|
26
25
|
expect(result.createdFiles).toContain("src/config.ts");
|
|
27
26
|
expect(result.createdFiles).toContain("src/data-bucket.ts");
|
|
28
27
|
expect(result.createdFiles).toContain("src/logs-bucket.ts");
|
|
29
28
|
});
|
|
30
29
|
});
|
|
31
30
|
|
|
32
|
-
test("aws source files use
|
|
31
|
+
test("aws source files use direct imports", async () => {
|
|
33
32
|
await withTestDir(async (testDir) => {
|
|
34
33
|
const options: InitOptions = {
|
|
35
34
|
path: testDir,
|
|
@@ -41,15 +40,15 @@ describe("initCommand", () => {
|
|
|
41
40
|
await initCommand(options);
|
|
42
41
|
|
|
43
42
|
const configContent = readFileSync(join(testDir, "src", "config.ts"), "utf-8");
|
|
44
|
-
expect(configContent).toContain('
|
|
43
|
+
expect(configContent).toContain('from "@intentius/chant-lexicon-aws"');
|
|
45
44
|
|
|
46
45
|
const dataBucketContent = readFileSync(join(testDir, "src", "data-bucket.ts"), "utf-8");
|
|
47
|
-
expect(dataBucketContent).toContain('
|
|
48
|
-
expect(dataBucketContent).toContain('
|
|
46
|
+
expect(dataBucketContent).toContain('from "@intentius/chant-lexicon-aws"');
|
|
47
|
+
expect(dataBucketContent).toContain('from "./config"');
|
|
49
48
|
|
|
50
49
|
const logsBucketContent = readFileSync(join(testDir, "src", "logs-bucket.ts"), "utf-8");
|
|
51
|
-
expect(logsBucketContent).toContain('
|
|
52
|
-
expect(logsBucketContent).toContain('
|
|
50
|
+
expect(logsBucketContent).toContain('from "@intentius/chant-lexicon-aws"');
|
|
51
|
+
expect(logsBucketContent).toContain('from "./config"');
|
|
53
52
|
});
|
|
54
53
|
});
|
|
55
54
|
|
|
@@ -177,7 +176,7 @@ describe("initCommand", () => {
|
|
|
177
176
|
});
|
|
178
177
|
});
|
|
179
178
|
|
|
180
|
-
test("
|
|
179
|
+
test("does not generate barrel file", async () => {
|
|
181
180
|
await withTestDir(async (testDir) => {
|
|
182
181
|
const options: InitOptions = {
|
|
183
182
|
path: testDir,
|
|
@@ -188,13 +187,11 @@ describe("initCommand", () => {
|
|
|
188
187
|
|
|
189
188
|
await initCommand(options);
|
|
190
189
|
|
|
190
|
+
// No _.ts barrel — direct imports are used instead
|
|
191
191
|
const barrelPath = join(testDir, "src", "_.ts");
|
|
192
|
-
expect(existsSync(barrelPath)).toBe(
|
|
192
|
+
expect(existsSync(barrelPath)).toBe(false);
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
expect(barrelContent).toContain('export * from "./config"');
|
|
196
|
-
|
|
197
|
-
// No index.ts — barrel re-exports cause duplicate entity errors during build
|
|
194
|
+
// No index.ts either
|
|
198
195
|
const indexPath = join(testDir, "src", "index.ts");
|
|
199
196
|
expect(existsSync(indexPath)).toBe(false);
|
|
200
197
|
});
|
|
@@ -218,7 +215,6 @@ describe("initCommand", () => {
|
|
|
218
215
|
expect(coreContent).toContain("Value<T>");
|
|
219
216
|
expect(coreContent).toContain("Serializer");
|
|
220
217
|
expect(coreContent).toContain("ChantConfig");
|
|
221
|
-
expect(coreContent).toContain("barrel");
|
|
222
218
|
|
|
223
219
|
const corePkg = join(testDir, ".chant", "types", "core", "package.json");
|
|
224
220
|
expect(existsSync(corePkg)).toBe(true);
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -214,8 +214,6 @@ export interface ChantConfig {
|
|
|
214
214
|
};
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
/** Barrel proxy — lazy-loads all sibling exports */
|
|
218
|
-
export declare function barrel(dir: string): Record<string, unknown>;
|
|
219
217
|
`;
|
|
220
218
|
}
|
|
221
219
|
|
|
@@ -345,7 +343,7 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
|
|
|
345
343
|
warnings,
|
|
346
344
|
);
|
|
347
345
|
|
|
348
|
-
// Generate source files from plugin (
|
|
346
|
+
// Generate source files from plugin (if available)
|
|
349
347
|
let sourceFiles: Record<string, string> = {};
|
|
350
348
|
try {
|
|
351
349
|
const plugin = await loadPlugin(options.lexicon);
|
|
@@ -353,10 +351,7 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
|
|
|
353
351
|
sourceFiles = plugin.initTemplates();
|
|
354
352
|
}
|
|
355
353
|
} catch {
|
|
356
|
-
// Plugin not yet installed —
|
|
357
|
-
sourceFiles = {
|
|
358
|
-
"_.ts": "// Barrel — re-export shared config here\n",
|
|
359
|
-
};
|
|
354
|
+
// Plugin not yet installed — no source files to scaffold
|
|
360
355
|
}
|
|
361
356
|
for (const [filename, content] of Object.entries(sourceFiles)) {
|
|
362
357
|
writeIfNotExists(
|
package/src/cli/commands/lint.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolve, join } from "path";
|
|
2
2
|
import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
|
|
3
3
|
import { runLint } from "../../lint/engine";
|
|
4
|
-
import type { LintRule, LintDiagnostic, LintFix
|
|
4
|
+
import type { LintRule, LintDiagnostic, LintFix } from "../../lint/rule";
|
|
5
5
|
import { loadPlugins, resolveProjectLexicons } from "../plugins";
|
|
6
6
|
import { formatStylish, formatJson, formatSarif } from "../reporters/stylish";
|
|
7
7
|
import { loadLocalRules } from "../../lint/rule-loader";
|
|
@@ -241,54 +241,30 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
|
|
|
241
241
|
// Get all TypeScript files
|
|
242
242
|
const files = getTypeScriptFiles(infraPath);
|
|
243
243
|
|
|
244
|
-
// Build barrel exports context for EVL008
|
|
245
|
-
let runOptions: LintRunOptions | undefined;
|
|
246
|
-
try {
|
|
247
|
-
const { scanProject } = require("../../project/scan");
|
|
248
|
-
const scan = scanProject(infraPath);
|
|
249
|
-
const barrelExports = new Set<string>(scan.exports.map((e: { name: string }) => e.name));
|
|
250
|
-
const projectExports = new Map<string, { file: string; className: string }>();
|
|
251
|
-
for (const exp of scan.exports) {
|
|
252
|
-
projectExports.set(exp.name, { file: exp.file, className: exp.className });
|
|
253
|
-
}
|
|
254
|
-
runOptions = { barrelExports, projectExports, projectScan: scan };
|
|
255
|
-
} catch {
|
|
256
|
-
// No barrel file found — EVL008/COR016 will be no-ops
|
|
257
|
-
}
|
|
258
|
-
|
|
259
244
|
// Run lint — use per-file rules when overrides are present
|
|
260
245
|
let diagnostics: LintDiagnostic[];
|
|
261
246
|
if (options.rules) {
|
|
262
|
-
diagnostics = await runLint(files, options.rules, undefined
|
|
247
|
+
diagnostics = await runLint(files, options.rules, undefined);
|
|
263
248
|
} else if (hasOverrides) {
|
|
264
249
|
diagnostics = [];
|
|
265
250
|
for (const file of files) {
|
|
266
251
|
const relativePath = file.slice(infraPath.length + 1);
|
|
267
252
|
const { rules: fileRules, ruleOptions } = getDefaultRules(infraPath, relativePath, allRules);
|
|
268
|
-
const fileDiagnostics = await runLint([file], fileRules, ruleOptions
|
|
253
|
+
const fileDiagnostics = await runLint([file], fileRules, ruleOptions);
|
|
269
254
|
diagnostics.push(...fileDiagnostics);
|
|
270
255
|
}
|
|
271
256
|
} else {
|
|
272
257
|
const { rules, ruleOptions } = getDefaultRules(infraPath, undefined, allRules);
|
|
273
|
-
diagnostics = await runLint(files, rules, ruleOptions
|
|
258
|
+
diagnostics = await runLint(files, rules, ruleOptions);
|
|
274
259
|
}
|
|
275
260
|
|
|
276
261
|
// Apply fixes if requested
|
|
277
262
|
if (options.fix) {
|
|
278
|
-
//
|
|
279
|
-
for (const diag of diagnostics) {
|
|
280
|
-
if (diag.fix?.kind === "write-file" && diag.fix.params) {
|
|
281
|
-
const path = diag.fix.params.path as string;
|
|
282
|
-
const content = diag.fix.params.content as string;
|
|
283
|
-
writeFileSync(path, content, "utf-8");
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Group remaining fixes by file (exclude write-file fixes)
|
|
263
|
+
// Group fixes by file
|
|
288
264
|
const fixesByFile = new Map<string, LintFix[]>();
|
|
289
265
|
|
|
290
266
|
for (const diag of diagnostics) {
|
|
291
|
-
if (diag.fix
|
|
267
|
+
if (diag.fix) {
|
|
292
268
|
const existing = fixesByFile.get(diag.file) ?? [];
|
|
293
269
|
existing.push(diag.fix);
|
|
294
270
|
fixesByFile.set(diag.file, existing);
|
|
@@ -303,18 +279,18 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
|
|
|
303
279
|
// Re-lint after fixes to get updated diagnostics
|
|
304
280
|
let postFixDiagnostics: LintDiagnostic[];
|
|
305
281
|
if (options.rules) {
|
|
306
|
-
postFixDiagnostics = await runLint(files, options.rules, undefined
|
|
282
|
+
postFixDiagnostics = await runLint(files, options.rules, undefined);
|
|
307
283
|
} else if (hasOverrides) {
|
|
308
284
|
postFixDiagnostics = [];
|
|
309
285
|
for (const file of files) {
|
|
310
286
|
const relativePath = file.slice(infraPath.length + 1);
|
|
311
287
|
const { rules: fileRules, ruleOptions } = getDefaultRules(infraPath, relativePath, allRules);
|
|
312
|
-
const fileDiagnostics = await runLint([file], fileRules, ruleOptions
|
|
288
|
+
const fileDiagnostics = await runLint([file], fileRules, ruleOptions);
|
|
313
289
|
postFixDiagnostics.push(...fileDiagnostics);
|
|
314
290
|
}
|
|
315
291
|
} else {
|
|
316
292
|
const { rules, ruleOptions } = getDefaultRules(infraPath, undefined, allRules);
|
|
317
|
-
postFixDiagnostics = await runLint(files, rules, ruleOptions
|
|
293
|
+
postFixDiagnostics = await runLint(files, rules, ruleOptions);
|
|
318
294
|
}
|
|
319
295
|
diagnostics.length = 0;
|
|
320
296
|
diagnostics.push(...postFixDiagnostics);
|
package/src/cli/main.ts
CHANGED
|
@@ -161,7 +161,7 @@ const registry: CommandDef[] = [
|
|
|
161
161
|
{ name: "import", handler: runImport },
|
|
162
162
|
{ name: "init", handler: runInit },
|
|
163
163
|
{ name: "init lexicon", handler: runInitLexicon },
|
|
164
|
-
|
|
164
|
+
{ name: "update", handler: runUpdate },
|
|
165
165
|
{ name: "doctor", handler: runDoctor },
|
|
166
166
|
|
|
167
167
|
// Dev subcommands
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { expandFileMarkers } from "./docs";
|
|
6
|
+
|
|
7
|
+
describe("expandFileMarkers", () => {
|
|
8
|
+
let dir: string;
|
|
9
|
+
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
dir = mkdtempSync(join(tmpdir(), "docs-interp-"));
|
|
12
|
+
mkdirSync(join(dir, "sub"), { recursive: true });
|
|
13
|
+
writeFileSync(
|
|
14
|
+
join(dir, "example.ts"),
|
|
15
|
+
'import { Bucket } from "@intentius/chant-lexicon-aws";\n\nexport const bucket = new Bucket({\n bucketName: "test",\n});\n',
|
|
16
|
+
);
|
|
17
|
+
writeFileSync(
|
|
18
|
+
join(dir, "sub", "nested.ts"),
|
|
19
|
+
"line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\n",
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterAll(() => {
|
|
24
|
+
rmSync(dir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("expands full file marker", () => {
|
|
28
|
+
const result = expandFileMarkers("Before\n\n{{file:example.ts}}\n\nAfter", dir);
|
|
29
|
+
expect(result).toContain('```typescript title="example.ts"');
|
|
30
|
+
expect(result).toContain('import { Bucket } from "@intentius/chant-lexicon-aws";');
|
|
31
|
+
expect(result).toContain("```");
|
|
32
|
+
expect(result).toStartWith("Before\n\n");
|
|
33
|
+
expect(result).toEndWith("\n\nAfter");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("expands line range", () => {
|
|
37
|
+
const result = expandFileMarkers("{{file:sub/nested.ts:3-5}}", dir);
|
|
38
|
+
expect(result).toContain('```typescript title="nested.ts"');
|
|
39
|
+
expect(result).toContain("line3\nline4\nline5");
|
|
40
|
+
expect(result).not.toContain("line2");
|
|
41
|
+
expect(result).not.toContain("line6");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("supports title override", () => {
|
|
45
|
+
const result = expandFileMarkers("{{file:example.ts|title=my-file.ts}}", dir);
|
|
46
|
+
expect(result).toContain('```typescript title="my-file.ts"');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("supports line range with title override", () => {
|
|
50
|
+
const result = expandFileMarkers(
|
|
51
|
+
"{{file:sub/nested.ts:2-4|title=snippet.ts}}",
|
|
52
|
+
dir,
|
|
53
|
+
);
|
|
54
|
+
expect(result).toContain('```typescript title="snippet.ts"');
|
|
55
|
+
expect(result).toContain("line2\nline3\nline4");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("throws on missing file", () => {
|
|
59
|
+
expect(() => expandFileMarkers("{{file:nope.ts}}", dir)).toThrow(
|
|
60
|
+
/file not found/,
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("leaves content without markers unchanged", () => {
|
|
65
|
+
const input = "No markers here\n\n```typescript\ncode\n```\n";
|
|
66
|
+
expect(expandFileMarkers(input, dir)).toBe(input);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("expands multiple markers", () => {
|
|
70
|
+
const result = expandFileMarkers(
|
|
71
|
+
"{{file:example.ts}}\n\n{{file:sub/nested.ts:1-2}}",
|
|
72
|
+
dir,
|
|
73
|
+
);
|
|
74
|
+
expect(result).toContain('title="example.ts"');
|
|
75
|
+
expect(result).toContain('title="nested.ts"');
|
|
76
|
+
});
|
|
77
|
+
});
|
package/src/codegen/docs.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface DocsConfig {
|
|
|
39
39
|
srcDir?: string;
|
|
40
40
|
/** Base path for the generated Astro site (e.g. '/lexicons/aws/') */
|
|
41
41
|
basePath?: string;
|
|
42
|
+
/** Root directory for resolving {{file:...}} markers in extra page content */
|
|
43
|
+
examplesDir?: string;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export interface DocsResult {
|
|
@@ -84,6 +86,66 @@ interface RuleMeta {
|
|
|
84
86
|
type: "lint" | "post-synth";
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
// ── File marker interpolation ──────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Expand `{{file:path.ts}}` markers in content with fenced code blocks.
|
|
93
|
+
*
|
|
94
|
+
* Supported forms:
|
|
95
|
+
* - `{{file:path.ts}}` — full file
|
|
96
|
+
* - `{{file:path.ts:5-12}}` — lines 5–12 (1-based, inclusive)
|
|
97
|
+
* - `{{file:path.ts|title=custom.ts}}` — override the code block title
|
|
98
|
+
* - `{{file:path.ts:5-12|title=custom.ts}}` — both
|
|
99
|
+
*/
|
|
100
|
+
export function expandFileMarkers(content: string, examplesDir: string): string {
|
|
101
|
+
return content.replace(
|
|
102
|
+
/\{\{file:([^}]+)\}\}/g,
|
|
103
|
+
(_match, spec: string) => {
|
|
104
|
+
// Parse options after |
|
|
105
|
+
let filePart = spec;
|
|
106
|
+
let title: string | undefined;
|
|
107
|
+
const pipeIdx = spec.indexOf("|");
|
|
108
|
+
if (pipeIdx !== -1) {
|
|
109
|
+
filePart = spec.substring(0, pipeIdx);
|
|
110
|
+
const opts = spec.substring(pipeIdx + 1);
|
|
111
|
+
const titleMatch = opts.match(/title=([^\s|]+)/);
|
|
112
|
+
if (titleMatch) title = titleMatch[1];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Parse line range after :digits-digits at end of filePart
|
|
116
|
+
let lineStart: number | undefined;
|
|
117
|
+
let lineEnd: number | undefined;
|
|
118
|
+
const rangeMatch = filePart.match(/^(.+):(\d+)-(\d+)$/);
|
|
119
|
+
if (rangeMatch) {
|
|
120
|
+
filePart = rangeMatch[1];
|
|
121
|
+
lineStart = parseInt(rangeMatch[2], 10);
|
|
122
|
+
lineEnd = parseInt(rangeMatch[3], 10);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const filePath = join(examplesDir, filePart);
|
|
126
|
+
let fileContent: string;
|
|
127
|
+
try {
|
|
128
|
+
fileContent = readFileSync(filePath, "utf-8");
|
|
129
|
+
} catch {
|
|
130
|
+
throw new Error(`File marker {{file:${spec}}} — file not found: ${filePath}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Extract line range if specified
|
|
134
|
+
if (lineStart !== undefined && lineEnd !== undefined) {
|
|
135
|
+
const lines = fileContent.split("\n");
|
|
136
|
+
fileContent = lines.slice(lineStart - 1, lineEnd).join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Determine language from extension
|
|
140
|
+
const ext = filePart.substring(filePart.lastIndexOf(".") + 1);
|
|
141
|
+
const lang = ext === "ts" || ext === "tsx" ? "typescript" : ext;
|
|
142
|
+
const displayTitle = title ?? filePart.substring(filePart.lastIndexOf("/") + 1);
|
|
143
|
+
|
|
144
|
+
return `\`\`\`${lang} title="${displayTitle}"\n${fileContent.trimEnd()}\n\`\`\``;
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
87
149
|
// ── Pipeline ───────────────────────────────────────────────────────
|
|
88
150
|
|
|
89
151
|
/**
|
|
@@ -128,15 +190,20 @@ export function docsPipeline(config: DocsConfig): DocsResult {
|
|
|
128
190
|
// Generate pages
|
|
129
191
|
const pages = new Map<string, string>();
|
|
130
192
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
193
|
+
let overviewContent = generateOverview(config, manifest, resources, properties, serviceGroups, rules);
|
|
194
|
+
if (config.examplesDir) {
|
|
195
|
+
overviewContent = expandFileMarkers(overviewContent, config.examplesDir);
|
|
196
|
+
}
|
|
197
|
+
pages.set("index.mdx", overviewContent);
|
|
135
198
|
const suppress = new Set(config.suppressPages ?? []);
|
|
136
199
|
|
|
137
200
|
// Extra pages from lexicon config
|
|
138
201
|
if (config.extraPages) {
|
|
139
202
|
for (const page of config.extraPages) {
|
|
203
|
+
let content = page.content;
|
|
204
|
+
if (config.examplesDir) {
|
|
205
|
+
content = expandFileMarkers(content, config.examplesDir);
|
|
206
|
+
}
|
|
140
207
|
pages.set(
|
|
141
208
|
`${page.slug}.mdx`,
|
|
142
209
|
[
|
|
@@ -145,7 +212,7 @@ export function docsPipeline(config: DocsConfig): DocsResult {
|
|
|
145
212
|
page.description ? `description: "${page.description}"` : "",
|
|
146
213
|
"---",
|
|
147
214
|
"",
|
|
148
|
-
|
|
215
|
+
content,
|
|
149
216
|
"",
|
|
150
217
|
]
|
|
151
218
|
.filter(Boolean)
|
|
@@ -300,7 +367,15 @@ function buildSidebar(
|
|
|
300
367
|
config: DocsConfig,
|
|
301
368
|
result: DocsResult,
|
|
302
369
|
): Array<Record<string, unknown>> {
|
|
370
|
+
// Starlight prepends basePath to every sidebar `link`, so a site-root-relative
|
|
371
|
+
// path like "/chant/" becomes "/chant/lexicons/aws/chant/" — a 404. Instead
|
|
372
|
+
// we use relative traversal: "../../" is prepended to become
|
|
373
|
+
// "/chant/lexicons/aws/../../" which the browser resolves to "/chant/".
|
|
374
|
+
const segments = (config.basePath ?? "/").replace(/^\/|\/$/g, "").split("/");
|
|
375
|
+
const backLink = segments.length > 1 ? "../".repeat(segments.length - 1) : "/";
|
|
376
|
+
|
|
303
377
|
const items: Array<Record<string, unknown>> = [
|
|
378
|
+
{ label: "← chant docs", link: backLink },
|
|
304
379
|
{ label: "Overview", slug: "index" },
|
|
305
380
|
];
|
|
306
381
|
|
|
@@ -69,7 +69,7 @@ describe("buildRegistry", () => {
|
|
|
69
69
|
typeName: "Test::S3::Bucket",
|
|
70
70
|
attributes: [],
|
|
71
71
|
properties: [],
|
|
72
|
-
propertyTypes: [{ name: "Bucket_Versioning",
|
|
72
|
+
propertyTypes: [{ name: "Bucket_Versioning", specType: "Versioning" }],
|
|
73
73
|
},
|
|
74
74
|
];
|
|
75
75
|
const naming = makeNaming(resources);
|
|
@@ -15,11 +15,11 @@ export interface RegistryResource {
|
|
|
15
15
|
typeName: string;
|
|
16
16
|
attributes: { name: string }[];
|
|
17
17
|
properties: { name: string; constraints: PropertyConstraints }[];
|
|
18
|
-
propertyTypes: { name: string;
|
|
18
|
+
propertyTypes: { name: string; specType: string }[];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface RegistryConfig<E> {
|
|
22
|
-
/** Short name extractor (e.g.
|
|
22
|
+
/** Short name extractor (e.g. shortName from the spec type). */
|
|
23
23
|
shortName: (typeName: string) => string;
|
|
24
24
|
/** Build a resource entry from parsed data. */
|
|
25
25
|
buildEntry: (
|
|
@@ -80,7 +80,7 @@ export function buildRegistry<E>(
|
|
|
80
80
|
for (const pt of r.propertyTypes) {
|
|
81
81
|
const defName = extractDefName(pt.name, shortName);
|
|
82
82
|
const ptName = propertyTypeName(tsName, defName);
|
|
83
|
-
const ptEntry = config.buildPropertyEntry(typeName, pt.
|
|
83
|
+
const ptEntry = config.buildPropertyEntry(typeName, pt.specType);
|
|
84
84
|
entries[ptName] = ptEntry;
|
|
85
85
|
|
|
86
86
|
if (ptAliases) {
|
package/src/codegen/package.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* assemble BundleSpec → compute integrity → attach metadata.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { readFileSync, readdirSync } from "fs";
|
|
8
|
+
import { readFileSync, readdirSync, writeFileSync, mkdirSync } from "fs";
|
|
9
9
|
import { join } from "path";
|
|
10
10
|
import type { BundleSpec, LexiconManifest } from "../lexicon";
|
|
11
11
|
import { computeIntegrity } from "../lexicon-integrity";
|
|
@@ -147,6 +147,33 @@ export function collectRules(
|
|
|
147
147
|
return rules;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Write a BundleSpec to the given dist directory.
|
|
152
|
+
*
|
|
153
|
+
* Creates the directory structure and writes all artifacts:
|
|
154
|
+
* manifest.json, meta.json, types/index.d.ts, rules/*, skills/*, integrity.json.
|
|
155
|
+
*/
|
|
156
|
+
export function writeBundleSpec(spec: BundleSpec, distDir: string): void {
|
|
157
|
+
mkdirSync(join(distDir, "types"), { recursive: true });
|
|
158
|
+
mkdirSync(join(distDir, "rules"), { recursive: true });
|
|
159
|
+
mkdirSync(join(distDir, "skills"), { recursive: true });
|
|
160
|
+
|
|
161
|
+
writeFileSync(join(distDir, "manifest.json"), JSON.stringify(spec.manifest, null, 2));
|
|
162
|
+
writeFileSync(join(distDir, "meta.json"), spec.registry);
|
|
163
|
+
writeFileSync(join(distDir, "types", "index.d.ts"), spec.typesDTS);
|
|
164
|
+
|
|
165
|
+
for (const [name, content] of spec.rules) {
|
|
166
|
+
writeFileSync(join(distDir, "rules", name), content);
|
|
167
|
+
}
|
|
168
|
+
for (const [name, content] of spec.skills) {
|
|
169
|
+
writeFileSync(join(distDir, "skills", name), content);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (spec.integrity) {
|
|
173
|
+
writeFileSync(join(distDir, "integrity.json"), JSON.stringify(spec.integrity, null, 2));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
150
177
|
/**
|
|
151
178
|
* Collect skills from a plugin's skill definitions.
|
|
152
179
|
*/
|
package/src/codegen/validate.ts
CHANGED
|
@@ -141,3 +141,19 @@ export async function validateLexiconArtifacts(config: LexiconValidationConfig):
|
|
|
141
141
|
checks,
|
|
142
142
|
};
|
|
143
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Print validation results to stderr and throw on failure.
|
|
147
|
+
*/
|
|
148
|
+
export function printValidationResult(result: ValidateResult): void {
|
|
149
|
+
for (const check of result.checks) {
|
|
150
|
+
const status = check.ok ? "OK" : "FAIL";
|
|
151
|
+
const msg = check.error ? ` — ${check.error}` : "";
|
|
152
|
+
console.error(` [${status}] ${check.name}${msg}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!result.success) {
|
|
156
|
+
throw new Error("Validation failed");
|
|
157
|
+
}
|
|
158
|
+
console.error("All validation checks passed.");
|
|
159
|
+
}
|
package/src/discovery/collect.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { isDeclarable, type Declarable } from "../declarable";
|
|
2
2
|
import { isCompositeInstance, expandComposite } from "../composite";
|
|
3
|
+
import { isLexiconOutput } from "../lexicon-output";
|
|
3
4
|
import { DiscoveryError } from "../errors";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Collects all declarable entities from imported modules.
|
|
7
8
|
* CompositeInstance exports are expanded into individual entities
|
|
8
9
|
* with `{exportName}_{memberName}` naming.
|
|
10
|
+
* LexiconOutput exports are also collected so that build() can
|
|
11
|
+
* extract them and pass them to the serializer.
|
|
9
12
|
*
|
|
10
13
|
* @param modules - Array of module records with their exports
|
|
11
14
|
* @returns Map of export name to Declarable entity
|
|
@@ -43,6 +46,10 @@ export function collectEntities(
|
|
|
43
46
|
}
|
|
44
47
|
entities.set(expandedName, entity);
|
|
45
48
|
}
|
|
49
|
+
} else if (isLexiconOutput(value)) {
|
|
50
|
+
// LexiconOutput is not a Declarable but build() expects to find them
|
|
51
|
+
// in the entities map so it can collect and pass them to serializers
|
|
52
|
+
entities.set(name, value as unknown as Declarable);
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
55
|
}
|
package/src/discovery/files.ts
CHANGED
|
@@ -30,12 +30,12 @@ export async function findInfraFiles(path: string): Promise<string[]> {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
if (entry.isDirectory()) {
|
|
33
|
-
// Child project boundary — a directory with its own
|
|
34
|
-
// separate scope, but only if we've already found the project's
|
|
35
|
-
// source root
|
|
36
|
-
//
|
|
37
|
-
const
|
|
38
|
-
if (existsSync(
|
|
33
|
+
// Child project boundary — a directory with its own chant.config.ts
|
|
34
|
+
// is a separate scope, but only if we've already found the project's
|
|
35
|
+
// own source root. The first config directory is the source root, not
|
|
36
|
+
// a child project.
|
|
37
|
+
const configPath = join(fullPath, "chant.config.ts");
|
|
38
|
+
if (existsSync(configPath)) {
|
|
39
39
|
if (sourceRoot === null) {
|
|
40
40
|
sourceRoot = fullPath;
|
|
41
41
|
} else {
|
package/src/discovery/import.ts
CHANGED
|
@@ -11,7 +11,7 @@ export async function importModule(
|
|
|
11
11
|
path: string
|
|
12
12
|
): Promise<Record<string, unknown>> {
|
|
13
13
|
try {
|
|
14
|
-
return
|
|
14
|
+
return await import(path);
|
|
15
15
|
} catch (error) {
|
|
16
16
|
const message =
|
|
17
17
|
error instanceof Error ? error.message : "Unknown import error";
|
package/src/index.ts
CHANGED
|
@@ -42,7 +42,6 @@ export * from "./lexicon-schema";
|
|
|
42
42
|
export * from "./config";
|
|
43
43
|
export * from "./validation";
|
|
44
44
|
export * from "./project-validation";
|
|
45
|
-
export { barrel } from "./barrel";
|
|
46
45
|
export * from "./codegen/naming";
|
|
47
46
|
export * from "./codegen/fetch";
|
|
48
47
|
export * from "./codegen/generate";
|
package/src/lint/engine.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LintRule, LintDiagnostic, LintContext
|
|
1
|
+
import type { LintRule, LintDiagnostic, LintContext } from "./rule";
|
|
2
2
|
import { parseFile } from "./parser";
|
|
3
3
|
import { readFileSync } from "fs";
|
|
4
4
|
|
|
@@ -120,7 +120,6 @@ export async function runLint(
|
|
|
120
120
|
files: string[],
|
|
121
121
|
rules: LintRule[],
|
|
122
122
|
ruleOptions?: Map<string, Record<string, unknown>>,
|
|
123
|
-
runOptions?: LintRunOptions,
|
|
124
123
|
): Promise<LintDiagnostic[]> {
|
|
125
124
|
const allDiagnostics: LintDiagnostic[] = [];
|
|
126
125
|
const allRuleIds = new Set(rules.map((r) => r.id));
|
|
@@ -140,9 +139,6 @@ export async function runLint(
|
|
|
140
139
|
entities: [],
|
|
141
140
|
filePath,
|
|
142
141
|
lexicon: undefined,
|
|
143
|
-
barrelExports: runOptions?.barrelExports,
|
|
144
|
-
projectExports: runOptions?.projectExports,
|
|
145
|
-
projectScan: runOptions?.projectScan,
|
|
146
142
|
};
|
|
147
143
|
|
|
148
144
|
// Execute each rule
|
package/src/lint/rule.ts
CHANGED
|
@@ -56,24 +56,6 @@ export interface LintContext {
|
|
|
56
56
|
filePath: string;
|
|
57
57
|
/** Optional lexicon context (undefined for core rules) */
|
|
58
58
|
lexicon?: string;
|
|
59
|
-
/** Export names from the barrel file (for EVL008) */
|
|
60
|
-
barrelExports?: Set<string>;
|
|
61
|
-
/** All project exports keyed by name (for EVL008) */
|
|
62
|
-
projectExports?: Map<string, { file: string; className: string }>;
|
|
63
|
-
/** Project scan result (for COR016) */
|
|
64
|
-
projectScan?: import("../project/scan").ProjectScan;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Options for extending the lint context with project-level information
|
|
69
|
-
*/
|
|
70
|
-
export interface LintRunOptions {
|
|
71
|
-
/** Export names from the barrel file */
|
|
72
|
-
barrelExports?: Set<string>;
|
|
73
|
-
/** All project exports keyed by name */
|
|
74
|
-
projectExports?: Map<string, { file: string; className: string }>;
|
|
75
|
-
/** Project scan result (for COR016) */
|
|
76
|
-
projectScan?: import("../project/scan").ProjectScan;
|
|
77
59
|
}
|
|
78
60
|
|
|
79
61
|
/**
|