@intentius/chant 0.0.18 → 0.0.24
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/bin/chant +4 -1
- package/package.json +20 -1
- package/src/build.test.ts +4 -2
- package/src/build.ts +3 -0
- package/src/builder.test.ts +3 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -3
- package/src/cli/commands/build.ts +5 -12
- package/src/cli/commands/diff.test.ts +2 -1
- package/src/cli/commands/diff.ts +2 -1
- package/src/cli/commands/init-lexicon/templates/codegen.ts +188 -0
- package/src/cli/commands/init-lexicon/templates/docs.ts +81 -0
- package/src/cli/commands/init-lexicon/templates/examples.ts +35 -0
- package/src/cli/commands/init-lexicon/templates/lint.ts +30 -0
- package/src/cli/commands/init-lexicon/templates/lsp.ts +39 -0
- package/src/cli/commands/init-lexicon/templates/plugin.ts +110 -0
- package/src/cli/commands/init-lexicon/templates/project.ts +182 -0
- package/src/cli/commands/init-lexicon/templates/spec.ts +57 -0
- package/src/cli/commands/init-lexicon/templates/tests.ts +70 -0
- package/src/cli/commands/init-lexicon.test.ts +0 -9
- package/src/cli/commands/init-lexicon.ts +12 -868
- package/src/cli/commands/init.ts +2 -20
- package/src/cli/conflict-check.test.ts +43 -0
- package/src/cli/handlers/build.ts +3 -3
- package/src/cli/handlers/lint.ts +2 -2
- package/src/cli/handlers/spell.ts +396 -0
- package/src/cli/handlers/state.ts +230 -0
- package/src/cli/lsp/server.test.ts +4 -0
- package/src/cli/main.ts +37 -3
- package/src/cli/mcp/resource-handlers.ts +227 -0
- package/src/cli/mcp/server.test.ts +13 -9
- package/src/cli/mcp/server.ts +24 -199
- package/src/cli/mcp/state-tools.ts +138 -0
- package/src/cli/mcp/tools/build.ts +2 -1
- package/src/cli/mcp/types.ts +45 -0
- package/src/cli/plugins.ts +1 -1
- package/src/cli/reporters/stylish.test.ts +2 -2
- package/src/cli/reporters/stylish.ts +1 -1
- package/src/codegen/docs-file-markers.ts +69 -0
- package/src/codegen/docs-rule-scanning.ts +159 -0
- package/src/codegen/docs-sections.ts +159 -0
- package/src/codegen/docs-sidebar.ts +56 -0
- package/src/codegen/docs-types.ts +79 -0
- package/src/codegen/docs.ts +9 -495
- package/src/composite.test.ts +76 -1
- package/src/composite.ts +37 -0
- package/src/config.ts +4 -0
- package/src/declarable.test.ts +2 -1
- package/src/declarable.ts +1 -1
- package/src/discovery/collect.test.ts +34 -0
- package/src/discovery/collect.ts +12 -0
- package/src/discovery/graph.test.ts +40 -0
- package/src/discovery/import.test.ts +5 -5
- package/src/discovery/resolve.test.ts +20 -0
- package/src/discovery/resolve.ts +2 -2
- package/src/index.ts +2 -0
- package/src/lexicon-plugin-helpers.ts +130 -0
- package/src/lexicon.ts +24 -0
- package/src/lint/rule-options.test.ts +3 -3
- package/src/lint/rule-registry.test.ts +1 -1
- package/src/lint/rules/composite-scope.ts +1 -1
- package/src/serializer-walker.ts +2 -1
- package/src/spell/discovery.ts +183 -0
- package/src/spell/index.ts +3 -0
- package/src/spell/prompt.ts +133 -0
- package/src/spell/types.ts +89 -0
- package/src/state/digest.ts +88 -0
- package/src/state/git.ts +317 -0
- package/src/state/index.ts +4 -0
- package/src/state/snapshot.ts +179 -0
- package/src/state/types.ts +59 -0
- package/src/toml-emit.ts +182 -0
- package/src/toml-parse.ts +370 -0
- package/src/toml-utils.ts +60 -0
- package/src/toml.ts +5 -602
- package/src/types.ts +2 -1
- package/src/utils.test.ts +16 -3
- package/src/utils.ts +31 -1
- package/src/validation.test.ts +11 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/getting-started.mdx +0 -6
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/lint-rules.mdx +0 -6
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/serialization.mdx +0 -6
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/actions/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/composites/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +0 -11
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +0 -10
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +0 -10
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/post-synth/.gitkeep +0 -0
package/src/codegen/docs.ts
CHANGED
|
@@ -7,151 +7,18 @@
|
|
|
7
7
|
* (service grouping, resource type URLs, custom overview content).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { readFileSync,
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync } from "fs";
|
|
11
11
|
import { join } from "path";
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// ── Types ──────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
export interface DocsConfig {
|
|
21
|
-
/** Lexicon name (used for page titles and paths) */
|
|
22
|
-
name: string;
|
|
23
|
-
/** Display name (e.g. "AWS CloudFormation") */
|
|
24
|
-
displayName: string;
|
|
25
|
-
/** Short description of what this lexicon targets */
|
|
26
|
-
description: string;
|
|
27
|
-
/** Path to dist/ directory containing manifest.json and meta.json */
|
|
28
|
-
distDir: string;
|
|
29
|
-
/** Output directory for generated .mdx files */
|
|
30
|
-
outDir: string;
|
|
31
|
-
/** Lexicon-specific overview content (markdown) */
|
|
32
|
-
overview?: string;
|
|
33
|
-
/** Output format description (e.g. "CloudFormation JSON template") */
|
|
34
|
-
outputFormat?: string;
|
|
35
|
-
/** Custom service grouping from resource type (e.g. "AWS::S3::Bucket" → "S3") */
|
|
36
|
-
serviceFromType?: (resourceType: string) => string;
|
|
37
|
-
/** Custom sections to append to overview page */
|
|
38
|
-
extraSections?: Array<{ title: string; content: string }>;
|
|
39
|
-
/** Standalone pages added to the sidebar after Overview */
|
|
40
|
-
extraPages?: Array<{ slug: string; title: string; description?: string; content: string; sidebar?: boolean }>;
|
|
41
|
-
/** Slugs of auto-generated pages to suppress (e.g. "pseudo-parameters") */
|
|
42
|
-
suppressPages?: string[];
|
|
43
|
-
/** Source directory for scanning rule files (defaults to srcDir sibling of distDir) */
|
|
44
|
-
srcDir?: string;
|
|
45
|
-
/** Base path for the generated Astro site (e.g. '/lexicons/aws/') */
|
|
46
|
-
basePath?: string;
|
|
47
|
-
/** Root directory for resolving {{file:...}} markers in extra page content */
|
|
48
|
-
examplesDir?: string;
|
|
49
|
-
/** Extra sidebar entries appended after extraPages (supports Starlight groups) */
|
|
50
|
-
sidebarExtra?: Array<Record<string, unknown>>;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface DocsResult {
|
|
54
|
-
pages: Map<string, string>;
|
|
55
|
-
stats: {
|
|
56
|
-
resources: number;
|
|
57
|
-
properties: number;
|
|
58
|
-
services: number;
|
|
59
|
-
rules: number;
|
|
60
|
-
intrinsics: number;
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
interface ManifestJSON {
|
|
65
|
-
name: string;
|
|
66
|
-
version: string;
|
|
67
|
-
namespace?: string;
|
|
68
|
-
intrinsics?: Array<{
|
|
69
|
-
name: string;
|
|
70
|
-
description?: string;
|
|
71
|
-
outputKey?: string;
|
|
72
|
-
isTag?: boolean;
|
|
73
|
-
}>;
|
|
74
|
-
pseudoParameters?: Record<string, string>;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
interface MetaEntry {
|
|
78
|
-
resourceType: string;
|
|
79
|
-
kind: "resource" | "property";
|
|
80
|
-
lexicon: string;
|
|
81
|
-
attrs?: Record<string, string>;
|
|
82
|
-
propertyConstraints?: Record<string, unknown>;
|
|
83
|
-
createOnly?: string[];
|
|
84
|
-
writeOnly?: string[];
|
|
85
|
-
primaryIdentifier?: string[];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
interface RuleMeta {
|
|
89
|
-
id: string;
|
|
90
|
-
severity: string;
|
|
91
|
-
category: string;
|
|
92
|
-
description: string;
|
|
93
|
-
type: "lint" | "post-synth";
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ── File marker interpolation ──────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Expand `{{file:path.ts}}` markers in content with fenced code blocks.
|
|
100
|
-
*
|
|
101
|
-
* Supported forms:
|
|
102
|
-
* - `{{file:path.ts}}` — full file
|
|
103
|
-
* - `{{file:path.ts:5-12}}` — lines 5–12 (1-based, inclusive)
|
|
104
|
-
* - `{{file:path.ts|title=custom.ts}}` — override the code block title
|
|
105
|
-
* - `{{file:path.ts:5-12|title=custom.ts}}` — both
|
|
106
|
-
*/
|
|
107
|
-
export function expandFileMarkers(content: string, examplesDir: string): string {
|
|
108
|
-
return content.replace(
|
|
109
|
-
/\{\{file:([^}]+)\}\}/g,
|
|
110
|
-
(_match, spec: string) => {
|
|
111
|
-
// Parse options after |
|
|
112
|
-
let filePart = spec;
|
|
113
|
-
let title: string | undefined;
|
|
114
|
-
const pipeIdx = spec.indexOf("|");
|
|
115
|
-
if (pipeIdx !== -1) {
|
|
116
|
-
filePart = spec.substring(0, pipeIdx);
|
|
117
|
-
const opts = spec.substring(pipeIdx + 1);
|
|
118
|
-
const titleMatch = opts.match(/title=([^\s|]+)/);
|
|
119
|
-
if (titleMatch) title = titleMatch[1];
|
|
120
|
-
}
|
|
13
|
+
import { expandFileMarkers } from "./docs-file-markers";
|
|
14
|
+
import { scanRules, generateRules } from "./docs-rule-scanning";
|
|
15
|
+
import { buildSidebar } from "./docs-sidebar";
|
|
16
|
+
import { generateOverview, generateIntrinsics, generatePseudoParameters, generateSerialization } from "./docs-sections";
|
|
17
|
+
import type { DocsConfig, DocsResult, ManifestJSON, MetaEntry } from "./docs-types";
|
|
121
18
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const rangeMatch = filePart.match(/^(.+):(\d+)-(\d+)$/);
|
|
126
|
-
if (rangeMatch) {
|
|
127
|
-
filePart = rangeMatch[1];
|
|
128
|
-
lineStart = parseInt(rangeMatch[2], 10);
|
|
129
|
-
lineEnd = parseInt(rangeMatch[3], 10);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const filePath = join(examplesDir, filePart);
|
|
133
|
-
let fileContent: string;
|
|
134
|
-
try {
|
|
135
|
-
fileContent = readFileSync(filePath, "utf-8");
|
|
136
|
-
} catch {
|
|
137
|
-
throw new Error(`File marker {{file:${spec}}} — file not found: ${filePath}`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Extract line range if specified
|
|
141
|
-
if (lineStart !== undefined && lineEnd !== undefined) {
|
|
142
|
-
const lines = fileContent.split("\n");
|
|
143
|
-
fileContent = lines.slice(lineStart - 1, lineEnd).join("\n");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Determine language from extension
|
|
147
|
-
const ext = filePart.substring(filePart.lastIndexOf(".") + 1);
|
|
148
|
-
const lang = ext === "ts" || ext === "tsx" ? "typescript" : ext;
|
|
149
|
-
const displayTitle = title ?? filePart.substring(filePart.lastIndexOf("/") + 1);
|
|
150
|
-
|
|
151
|
-
return `\`\`\`${lang} title="${displayTitle}"\n${fileContent.trimEnd()}\n\`\`\``;
|
|
152
|
-
},
|
|
153
|
-
);
|
|
154
|
-
}
|
|
19
|
+
// Re-export all public types and functions so existing importers continue to work.
|
|
20
|
+
export { expandFileMarkers } from "./docs-file-markers";
|
|
21
|
+
export type { DocsConfig, DocsResult } from "./docs-types";
|
|
155
22
|
|
|
156
23
|
// ── Pipeline ───────────────────────────────────────────────────────
|
|
157
24
|
|
|
@@ -373,356 +240,3 @@ export default defineConfig({${config.basePath ? `\n base: '${config.basePath}'
|
|
|
373
240
|
`,
|
|
374
241
|
);
|
|
375
242
|
}
|
|
376
|
-
|
|
377
|
-
function buildSidebar(
|
|
378
|
-
config: DocsConfig,
|
|
379
|
-
result: DocsResult,
|
|
380
|
-
): Array<Record<string, unknown>> {
|
|
381
|
-
// Starlight prepends basePath to every sidebar `link`, so a site-root-relative
|
|
382
|
-
// path like "/chant/" becomes "/chant/lexicons/aws/chant/" — a 404. Instead
|
|
383
|
-
// we use relative traversal: "../../" is prepended to become
|
|
384
|
-
// "/chant/lexicons/aws/../../" which the browser resolves to "/chant/".
|
|
385
|
-
const segments = (config.basePath ?? "/").replace(/^\/|\/$/g, "").split("/");
|
|
386
|
-
const backLink = segments.length > 1 ? "../".repeat(segments.length - 1) : "/";
|
|
387
|
-
|
|
388
|
-
const items: Array<Record<string, unknown>> = [
|
|
389
|
-
{ label: "← chant docs", link: backLink },
|
|
390
|
-
{ label: "Overview", slug: "index" },
|
|
391
|
-
];
|
|
392
|
-
|
|
393
|
-
const suppress = new Set(config.suppressPages ?? []);
|
|
394
|
-
const extraSlugs = new Set((config.extraPages ?? []).map((p) => p.slug));
|
|
395
|
-
|
|
396
|
-
// Extra pages from lexicon config (appear after Overview)
|
|
397
|
-
if (config.extraPages) {
|
|
398
|
-
for (const page of config.extraPages) {
|
|
399
|
-
if (page.sidebar === false) continue;
|
|
400
|
-
items.push({ label: page.title, slug: page.slug });
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (!suppress.has("intrinsics") && !extraSlugs.has("intrinsics") && result.pages.has("intrinsics.mdx")) {
|
|
405
|
-
items.push({ label: "Intrinsics", slug: "intrinsics" });
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
if (!suppress.has("pseudo-parameters") && !extraSlugs.has("pseudo-parameters") && result.pages.has("pseudo-parameters.mdx")) {
|
|
409
|
-
items.push({ label: "Pseudo-Parameters", slug: "pseudo-parameters" });
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (!suppress.has("rules") && !extraSlugs.has("rules") && !extraSlugs.has("lint-rules") && result.pages.has("rules.mdx")) {
|
|
413
|
-
items.push({ label: "Lint Rules", slug: "rules" });
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (!suppress.has("serialization") && !extraSlugs.has("serialization") && result.pages.has("serialization.mdx")) {
|
|
417
|
-
items.push({ label: "Serialization", slug: "serialization" });
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Append raw sidebar entries (supports groups and nested items)
|
|
421
|
-
if (config.sidebarExtra) {
|
|
422
|
-
items.push(...config.sidebarExtra);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
return items;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// ── Page generators ────────────────────────────────────────────────
|
|
429
|
-
|
|
430
|
-
function generateOverview(
|
|
431
|
-
config: DocsConfig,
|
|
432
|
-
manifest: ManifestJSON,
|
|
433
|
-
resources: Map<string, MetaEntry>,
|
|
434
|
-
properties: Map<string, MetaEntry>,
|
|
435
|
-
serviceGroups: Map<string, Map<string, MetaEntry>>,
|
|
436
|
-
rules: RuleMeta[],
|
|
437
|
-
): string {
|
|
438
|
-
const lines: string[] = [
|
|
439
|
-
"---",
|
|
440
|
-
`title: "${config.displayName}"`,
|
|
441
|
-
`description: "${config.description}"`,
|
|
442
|
-
"---",
|
|
443
|
-
"",
|
|
444
|
-
config.overview ?? `Reference documentation for the **${config.displayName}** lexicon.`,
|
|
445
|
-
"",
|
|
446
|
-
"## At a Glance",
|
|
447
|
-
"",
|
|
448
|
-
`| Metric | Count |`,
|
|
449
|
-
`|--------|-------|`,
|
|
450
|
-
`| Resources | ${resources.size} |`,
|
|
451
|
-
`| Property types | ${properties.size} |`,
|
|
452
|
-
`| Services | ${serviceGroups.size} |`,
|
|
453
|
-
`| Intrinsic functions | ${manifest.intrinsics?.length ?? 0} |`,
|
|
454
|
-
`| Pseudo-parameters | ${Object.keys(manifest.pseudoParameters ?? {}).length} |`,
|
|
455
|
-
`| Lint rules | ${rules.length} |`,
|
|
456
|
-
"",
|
|
457
|
-
`**Lexicon version:** ${manifest.version} `,
|
|
458
|
-
`**Namespace:** \`${manifest.namespace ?? manifest.name}\``,
|
|
459
|
-
"",
|
|
460
|
-
];
|
|
461
|
-
|
|
462
|
-
const suppress = new Set(config.suppressPages ?? []);
|
|
463
|
-
|
|
464
|
-
// Extra pages listed first in reference links
|
|
465
|
-
if (config.extraPages && config.extraPages.length > 0) {
|
|
466
|
-
for (const page of config.extraPages) {
|
|
467
|
-
if (page.sidebar === false) continue;
|
|
468
|
-
lines.push(`- [${page.title}](./${page.slug})`);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (!suppress.has("intrinsics") && manifest.intrinsics && manifest.intrinsics.length > 0) {
|
|
473
|
-
lines.push(
|
|
474
|
-
`- [Intrinsic Functions](./intrinsics) — ${manifest.intrinsics.length} built-in functions`,
|
|
475
|
-
);
|
|
476
|
-
}
|
|
477
|
-
if (
|
|
478
|
-
!suppress.has("pseudo-parameters") &&
|
|
479
|
-
manifest.pseudoParameters &&
|
|
480
|
-
Object.keys(manifest.pseudoParameters).length > 0
|
|
481
|
-
) {
|
|
482
|
-
lines.push(
|
|
483
|
-
`- [Pseudo-Parameters](./pseudo-parameters) — ${Object.keys(manifest.pseudoParameters).length} pseudo-parameters`,
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
const overviewExtraSlugs = new Set((config.extraPages ?? []).map((p) => p.slug));
|
|
487
|
-
if (!suppress.has("rules") && !overviewExtraSlugs.has("lint-rules") && rules.length > 0) {
|
|
488
|
-
lines.push(`- [Lint Rules](./rules) — ${rules.length} rules`);
|
|
489
|
-
}
|
|
490
|
-
if (!suppress.has("serialization")) {
|
|
491
|
-
lines.push(`- [Serialization](./serialization) — output format details`);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (config.extraSections) {
|
|
495
|
-
for (const section of config.extraSections) {
|
|
496
|
-
lines.push("", `## ${section.title}`, "", section.content);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
lines.push("");
|
|
501
|
-
return lines.join("\n");
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function generateIntrinsics(
|
|
505
|
-
config: DocsConfig,
|
|
506
|
-
manifest: ManifestJSON,
|
|
507
|
-
): string {
|
|
508
|
-
const intrinsics = manifest.intrinsics!;
|
|
509
|
-
const lines: string[] = [
|
|
510
|
-
"---",
|
|
511
|
-
`title: "Intrinsic Functions"`,
|
|
512
|
-
`description: "Built-in intrinsic functions for the ${config.displayName} lexicon"`,
|
|
513
|
-
"---",
|
|
514
|
-
"",
|
|
515
|
-
`The ${config.displayName} lexicon provides **${intrinsics.length}** intrinsic functions.`,
|
|
516
|
-
"",
|
|
517
|
-
"| Function | Description | Output Key | Tag? |",
|
|
518
|
-
"|----------|-------------|------------|------|",
|
|
519
|
-
];
|
|
520
|
-
|
|
521
|
-
for (const fn of intrinsics) {
|
|
522
|
-
const tag = fn.isTag ? "Yes" : "No";
|
|
523
|
-
lines.push(
|
|
524
|
-
`| \`${fn.name}\` | ${escapeMdx(fn.description ?? "—")} | \`${fn.outputKey ?? fn.name}\` | ${tag} |`,
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
lines.push("");
|
|
529
|
-
return lines.join("\n");
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function generatePseudoParameters(
|
|
533
|
-
config: DocsConfig,
|
|
534
|
-
manifest: ManifestJSON,
|
|
535
|
-
): string {
|
|
536
|
-
const params = manifest.pseudoParameters!;
|
|
537
|
-
const entries = Object.entries(params);
|
|
538
|
-
const lines: string[] = [
|
|
539
|
-
"---",
|
|
540
|
-
`title: "Pseudo-Parameters"`,
|
|
541
|
-
`description: "Pseudo-parameters available in the ${config.displayName} lexicon"`,
|
|
542
|
-
"---",
|
|
543
|
-
"",
|
|
544
|
-
`The ${config.displayName} lexicon provides **${entries.length}** pseudo-parameters — predefined values available in every stack without explicit declaration.`,
|
|
545
|
-
"",
|
|
546
|
-
"| Name | Value |",
|
|
547
|
-
"|------|-------|",
|
|
548
|
-
];
|
|
549
|
-
|
|
550
|
-
for (const [name, value] of entries) {
|
|
551
|
-
lines.push(`| \`${name}\` | \`${value}\` |`);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
lines.push("");
|
|
555
|
-
return lines.join("\n");
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function generateRules(config: DocsConfig, rules: RuleMeta[]): string {
|
|
559
|
-
const lintRules = rules.filter((r) => r.type === "lint");
|
|
560
|
-
const postSynthRules = rules.filter((r) => r.type === "post-synth");
|
|
561
|
-
|
|
562
|
-
const lines: string[] = [
|
|
563
|
-
"---",
|
|
564
|
-
`title: "Lint Rules"`,
|
|
565
|
-
`description: "Lint rules and post-synth checks provided by the ${config.displayName} lexicon"`,
|
|
566
|
-
"---",
|
|
567
|
-
"",
|
|
568
|
-
`The ${config.displayName} lexicon provides **${rules.length}** rules: ${lintRules.length} lint rules and ${postSynthRules.length} post-synth checks.`,
|
|
569
|
-
"",
|
|
570
|
-
];
|
|
571
|
-
|
|
572
|
-
if (lintRules.length > 0) {
|
|
573
|
-
lines.push(
|
|
574
|
-
"## Lint Rules",
|
|
575
|
-
"",
|
|
576
|
-
"| ID | Severity | Category | Description |",
|
|
577
|
-
"|----|----------|----------|-------------|",
|
|
578
|
-
);
|
|
579
|
-
for (const rule of lintRules.sort((a, b) => a.id.localeCompare(b.id))) {
|
|
580
|
-
lines.push(
|
|
581
|
-
`| \`${rule.id}\` | ${rule.severity} | ${rule.category} | ${escapeMdx(rule.description)} |`,
|
|
582
|
-
);
|
|
583
|
-
}
|
|
584
|
-
lines.push("");
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
if (postSynthRules.length > 0) {
|
|
588
|
-
lines.push(
|
|
589
|
-
"## Post-Synth Checks",
|
|
590
|
-
"",
|
|
591
|
-
"Post-synth checks validate the serialized output after the build pipeline completes.",
|
|
592
|
-
"",
|
|
593
|
-
"| ID | Description |",
|
|
594
|
-
"|----|-------------|",
|
|
595
|
-
);
|
|
596
|
-
for (const rule of postSynthRules.sort((a, b) =>
|
|
597
|
-
a.id.localeCompare(b.id),
|
|
598
|
-
)) {
|
|
599
|
-
lines.push(`| \`${rule.id}\` | ${escapeMdx(rule.description)} |`);
|
|
600
|
-
}
|
|
601
|
-
lines.push("");
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
return lines.join("\n");
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
function generateSerialization(config: DocsConfig): string {
|
|
608
|
-
const lines: string[] = [
|
|
609
|
-
"---",
|
|
610
|
-
`title: "Serialization"`,
|
|
611
|
-
`description: "Output format for the ${config.displayName} lexicon"`,
|
|
612
|
-
"---",
|
|
613
|
-
"",
|
|
614
|
-
];
|
|
615
|
-
|
|
616
|
-
if (config.outputFormat) {
|
|
617
|
-
lines.push(config.outputFormat);
|
|
618
|
-
} else {
|
|
619
|
-
lines.push(
|
|
620
|
-
`The ${config.displayName} lexicon serializes resources into its native output format during the build step.`,
|
|
621
|
-
"",
|
|
622
|
-
"See the [Serialization](/serialization/output-formats) guide for general information about output formats in chant.",
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
lines.push("");
|
|
627
|
-
return lines.join("\n");
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// ── Rule scanning ──────────────────────────────────────────────────
|
|
631
|
-
|
|
632
|
-
/**
|
|
633
|
-
* Scan lint rule and post-synth check source files to extract metadata.
|
|
634
|
-
* Uses regex to find id, severity, category, and description from source.
|
|
635
|
-
*/
|
|
636
|
-
function scanRules(srcDir: string): RuleMeta[] {
|
|
637
|
-
const rules: RuleMeta[] = [];
|
|
638
|
-
|
|
639
|
-
// Scan lint rules
|
|
640
|
-
scanDir(join(srcDir, "lint", "rules"), "lint", rules);
|
|
641
|
-
|
|
642
|
-
// Scan post-synth checks
|
|
643
|
-
scanDir(join(srcDir, "lint", "post-synth"), "post-synth", rules);
|
|
644
|
-
|
|
645
|
-
return rules;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function scanDir(dir: string, type: "lint" | "post-synth", out: RuleMeta[]): void {
|
|
649
|
-
let entries: string[];
|
|
650
|
-
try {
|
|
651
|
-
entries = readdirSync(dir);
|
|
652
|
-
} catch {
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
for (const entry of entries) {
|
|
657
|
-
if (!entry.endsWith(".ts")) continue;
|
|
658
|
-
if (entry.endsWith(".test.ts")) continue;
|
|
659
|
-
if (entry === "index.ts") continue;
|
|
660
|
-
// Skip utility files (no rule definitions)
|
|
661
|
-
if (entry === "cf-refs.ts") continue;
|
|
662
|
-
|
|
663
|
-
let content: string;
|
|
664
|
-
try {
|
|
665
|
-
content = readFileSync(join(dir, entry), "utf-8");
|
|
666
|
-
} catch {
|
|
667
|
-
continue;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
if (type === "lint") {
|
|
671
|
-
// Extract from LintRule objects: id, severity, category
|
|
672
|
-
const idMatch = content.match(/id:\s*"([^"]+)"/);
|
|
673
|
-
const severityMatch = content.match(/severity:\s*"([^"]+)"/);
|
|
674
|
-
const categoryMatch = content.match(/category:\s*"([^"]+)"/);
|
|
675
|
-
|
|
676
|
-
if (idMatch) {
|
|
677
|
-
// Try to get description from JSDoc comment
|
|
678
|
-
const descMatch = content.match(
|
|
679
|
-
new RegExp(`\\*\\s*${idMatch[1]}:\\s*(.+?)\\n`),
|
|
680
|
-
);
|
|
681
|
-
|
|
682
|
-
out.push({
|
|
683
|
-
id: idMatch[1],
|
|
684
|
-
severity: severityMatch?.[1] ?? "warning",
|
|
685
|
-
category: categoryMatch?.[1] ?? "general",
|
|
686
|
-
description:
|
|
687
|
-
descMatch?.[1]?.trim() ??
|
|
688
|
-
extractDescriptionFromComment(content, idMatch[1]),
|
|
689
|
-
type: "lint",
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
} else {
|
|
693
|
-
// Extract from PostSynthCheck objects: id, description
|
|
694
|
-
const idMatch = content.match(/id:\s*"([^"]+)"/);
|
|
695
|
-
const descMatch = content.match(/description:\s*"([^"]+)"/);
|
|
696
|
-
|
|
697
|
-
if (idMatch) {
|
|
698
|
-
out.push({
|
|
699
|
-
id: idMatch[1],
|
|
700
|
-
severity: "error",
|
|
701
|
-
category: "post-synth",
|
|
702
|
-
description: descMatch?.[1] ?? idMatch[1],
|
|
703
|
-
type: "post-synth",
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function extractDescriptionFromComment(
|
|
711
|
-
content: string,
|
|
712
|
-
ruleId: string,
|
|
713
|
-
): string {
|
|
714
|
-
// Try to extract from the first line after the rule ID in JSDoc
|
|
715
|
-
const pattern = new RegExp(
|
|
716
|
-
`${ruleId}[:\\s]+([^\\n]+?)\\n\\s*\\*\\s*\\n\\s*\\*\\s*(.+?)\\n`,
|
|
717
|
-
);
|
|
718
|
-
const match = content.match(pattern);
|
|
719
|
-
if (match) return match[1].trim();
|
|
720
|
-
|
|
721
|
-
// Fallback: use text after "ruleId:" in JSDoc
|
|
722
|
-
const simpleMatch = content.match(
|
|
723
|
-
new RegExp(`\\*\\s*${ruleId}:\\s*(.+?)(?:\\n|\\*)`),
|
|
724
|
-
);
|
|
725
|
-
if (simpleMatch) return simpleMatch[1].trim();
|
|
726
|
-
|
|
727
|
-
return ruleId;
|
|
728
|
-
}
|
package/src/composite.test.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
withDefaults,
|
|
10
10
|
propagate,
|
|
11
11
|
SHARED_PROPS,
|
|
12
|
+
mergeDefaults,
|
|
12
13
|
} from "./composite";
|
|
13
14
|
import { DECLARABLE_MARKER, type Declarable } from "./declarable";
|
|
14
15
|
import { AttrRef } from "./attrref";
|
|
@@ -116,7 +117,7 @@ describe("Composite", () => {
|
|
|
116
117
|
});
|
|
117
118
|
|
|
118
119
|
const instance = MyComp({});
|
|
119
|
-
const roleProps = instance.members.role
|
|
120
|
+
const roleProps = (instance.members.role as MockResource).props;
|
|
120
121
|
expect(roleProps.bucketArn).toBeInstanceOf(AttrRef);
|
|
121
122
|
// The AttrRef's parent should be the bucket instance
|
|
122
123
|
expect((roleProps.bucketArn as AttrRef).attribute).toBe("Arn");
|
|
@@ -491,3 +492,77 @@ describe("propagate", () => {
|
|
|
491
492
|
expect((expanded.get("sBucket") as any).props.name).toBe("data");
|
|
492
493
|
});
|
|
493
494
|
});
|
|
495
|
+
|
|
496
|
+
describe("mergeDefaults", () => {
|
|
497
|
+
test("returns base unchanged when no overrides", () => {
|
|
498
|
+
const base = { a: 1, b: "hello" };
|
|
499
|
+
expect(mergeDefaults(base)).toBe(base);
|
|
500
|
+
expect(mergeDefaults(base, undefined)).toBe(base);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("scalar override wins", () => {
|
|
504
|
+
const result = mergeDefaults({ a: 1, b: 2 }, { a: 10 });
|
|
505
|
+
expect(result).toEqual({ a: 10, b: 2 });
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("undefined values in overrides are skipped", () => {
|
|
509
|
+
const result = mergeDefaults({ a: 1, b: 2 }, { a: undefined });
|
|
510
|
+
expect(result).toEqual({ a: 1, b: 2 });
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test("arrays are concatenated (base + overrides)", () => {
|
|
514
|
+
const result = mergeDefaults(
|
|
515
|
+
{ tags: [{ key: "env", value: "prod" }] },
|
|
516
|
+
{ tags: [{ key: "team", value: "alpha" }] },
|
|
517
|
+
);
|
|
518
|
+
expect(result.tags).toEqual([
|
|
519
|
+
{ key: "env", value: "prod" },
|
|
520
|
+
{ key: "team", value: "alpha" },
|
|
521
|
+
]);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("object override deep merges nested objects", () => {
|
|
525
|
+
const result = mergeDefaults(
|
|
526
|
+
{ config: { a: 1, b: 2 } },
|
|
527
|
+
{ config: { a: 10 } as any },
|
|
528
|
+
);
|
|
529
|
+
expect(result.config).toEqual({ a: 10, b: 2 } as any);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test("new keys from overrides are added", () => {
|
|
533
|
+
const result = mergeDefaults(
|
|
534
|
+
{ a: 1 } as Record<string, unknown>,
|
|
535
|
+
{ b: 2 },
|
|
536
|
+
);
|
|
537
|
+
expect(result).toEqual({ a: 1, b: 2 });
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("does not mutate the base object", () => {
|
|
541
|
+
const base = { a: 1, b: [1, 2] };
|
|
542
|
+
const result = mergeDefaults(base, { a: 10, b: [3] });
|
|
543
|
+
expect(base.a).toBe(1);
|
|
544
|
+
expect(base.b).toEqual([1, 2]);
|
|
545
|
+
expect(result.a).toBe(10);
|
|
546
|
+
expect(result.b).toEqual([1, 2, 3]);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test("empty overrides returns a shallow copy", () => {
|
|
550
|
+
const base = { a: 1 };
|
|
551
|
+
const result = mergeDefaults(base, {});
|
|
552
|
+
expect(result).toEqual({ a: 1 });
|
|
553
|
+
expect(result).not.toBe(base);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("mixed: some scalars overridden, some arrays concatenated, some unchanged", () => {
|
|
557
|
+
const result = mergeDefaults(
|
|
558
|
+
{ name: "orig", count: 5, tags: ["a"], config: { x: 1 } },
|
|
559
|
+
{ name: "new", tags: ["b"], config: undefined },
|
|
560
|
+
);
|
|
561
|
+
expect(result).toEqual({
|
|
562
|
+
name: "new",
|
|
563
|
+
count: 5,
|
|
564
|
+
tags: ["a", "b"],
|
|
565
|
+
config: { x: 1 },
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
});
|
package/src/composite.ts
CHANGED
|
@@ -243,3 +243,40 @@ export function resource<T extends Declarable, P>(
|
|
|
243
243
|
): T {
|
|
244
244
|
return new Type(props, attributes);
|
|
245
245
|
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Shallow-merges override values into a base props object.
|
|
249
|
+
*
|
|
250
|
+
* Merge semantics:
|
|
251
|
+
* - `undefined` values in overrides are skipped
|
|
252
|
+
* - Arrays: concatenated (base + overrides), matching `propagate()` semantics
|
|
253
|
+
* - Scalars / objects: override wins
|
|
254
|
+
*
|
|
255
|
+
* No deep merge — too dangerous with IaC props where nested objects
|
|
256
|
+
* (e.g. policy documents) should be replaced wholesale.
|
|
257
|
+
*/
|
|
258
|
+
export function mergeDefaults<T extends Record<string, unknown>>(
|
|
259
|
+
base: T,
|
|
260
|
+
overrides?: Partial<T>,
|
|
261
|
+
): T {
|
|
262
|
+
if (!overrides) return base;
|
|
263
|
+
const result = { ...base };
|
|
264
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
265
|
+
if (value === undefined) continue;
|
|
266
|
+
const existing = result[key as keyof T];
|
|
267
|
+
if (Array.isArray(existing) && Array.isArray(value)) {
|
|
268
|
+
(result as any)[key] = [...existing, ...value];
|
|
269
|
+
} else if (
|
|
270
|
+
existing != null && typeof existing === "object" && !Array.isArray(existing) &&
|
|
271
|
+
value != null && typeof value === "object" && !Array.isArray(value)
|
|
272
|
+
) {
|
|
273
|
+
(result as any)[key] = mergeDefaults(
|
|
274
|
+
existing as Record<string, unknown>,
|
|
275
|
+
value as Record<string, unknown>,
|
|
276
|
+
);
|
|
277
|
+
} else {
|
|
278
|
+
(result as any)[key] = value;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { LintConfig } from "./lint/config";
|
|
|
9
9
|
export const ChantConfigSchema = z.object({
|
|
10
10
|
runtime: z.enum(["bun", "node"]).optional(),
|
|
11
11
|
lexicons: z.array(z.string().min(1)).optional(),
|
|
12
|
+
environments: z.array(z.string().min(1)).optional(),
|
|
12
13
|
lint: z.record(z.string(), z.unknown()).optional(),
|
|
13
14
|
}).passthrough();
|
|
14
15
|
|
|
@@ -24,6 +25,9 @@ export interface ChantConfig {
|
|
|
24
25
|
/** Lexicon package names to load (e.g. ["aws"]) */
|
|
25
26
|
lexicons?: string[];
|
|
26
27
|
|
|
28
|
+
/** Environment names (e.g. ["staging", "prod"]) */
|
|
29
|
+
environments?: string[];
|
|
30
|
+
|
|
27
31
|
/** Lint configuration (rules, extends, overrides, plugins) */
|
|
28
32
|
lint?: LintConfig;
|
|
29
33
|
}
|
package/src/declarable.test.ts
CHANGED
|
@@ -12,7 +12,8 @@ describe("DECLARABLE_MARKER", () => {
|
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
test("uses Symbol.for for global registry", () => {
|
|
15
|
-
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
expect(DECLARABLE_MARKER).toBe(Symbol.for("chant.declarable") as any);
|
|
16
17
|
});
|
|
17
18
|
});
|
|
18
19
|
|
package/src/declarable.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const DECLARABLE_MARKER = Symbol.for("chant.declarable");
|
|
|
9
9
|
export interface Declarable {
|
|
10
10
|
readonly lexicon: string;
|
|
11
11
|
readonly entityType: string;
|
|
12
|
-
readonly kind?: "resource" | "property";
|
|
12
|
+
readonly kind?: "resource" | "property" | "output";
|
|
13
13
|
readonly [DECLARABLE_MARKER]: true;
|
|
14
14
|
}
|
|
15
15
|
|