@intentius/chant 0.0.5 → 0.0.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/bin/chant +20 -0
- package/package.json +18 -17
- package/src/bench.test.ts +1 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +0 -25
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +0 -25
- package/src/cli/commands/build.ts +1 -2
- package/src/cli/commands/doctor.ts +8 -3
- package/src/cli/commands/import.ts +2 -2
- package/src/cli/commands/init-lexicon.test.ts +0 -3
- package/src/cli/commands/init-lexicon.ts +1 -79
- package/src/cli/commands/init.test.ts +44 -4
- package/src/cli/commands/init.ts +69 -26
- package/src/cli/commands/lint.ts +27 -13
- package/src/cli/commands/list.ts +2 -2
- package/src/cli/commands/update.ts +5 -3
- package/src/cli/conflict-check.test.ts +0 -1
- package/src/cli/handlers/dev.ts +1 -9
- package/src/cli/handlers/init.ts +1 -0
- package/src/cli/lsp/server.ts +1 -1
- package/src/cli/main.ts +17 -3
- package/src/cli/mcp/server.test.ts +233 -4
- package/src/cli/mcp/server.ts +6 -0
- package/src/cli/mcp/tools/explain.ts +134 -0
- package/src/cli/mcp/tools/scaffold.ts +125 -0
- package/src/cli/mcp/tools/search.ts +98 -0
- package/src/cli/registry.ts +1 -0
- package/src/cli/reporters/stylish.test.ts +212 -1
- package/src/cli/reporters/stylish.ts +133 -36
- package/src/codegen/docs-rules.test.ts +112 -0
- package/src/codegen/docs-rules.ts +129 -0
- package/src/codegen/docs.ts +3 -1
- package/src/codegen/generate-registry.test.ts +1 -1
- package/src/codegen/generate-registry.ts +2 -3
- package/src/codegen/generate-typescript.test.ts +70 -6
- package/src/codegen/generate-typescript.ts +15 -9
- package/src/codegen/generate.ts +1 -12
- package/src/codegen/package.ts +1 -1
- package/src/codegen/typecheck.ts +6 -11
- package/src/composite.test.ts +83 -16
- package/src/composite.ts +7 -5
- package/src/config.ts +4 -0
- package/src/detectLexicon.test.ts +2 -2
- package/src/discovery/collect.test.ts +2 -2
- package/src/discovery/collect.ts +1 -1
- package/src/index.ts +2 -1
- package/src/lexicon-integrity.ts +5 -4
- package/src/lexicon-schema.ts +8 -0
- package/src/lexicon.ts +15 -7
- package/src/lint/config.ts +8 -6
- package/src/lint/declarative.ts +6 -0
- package/src/lint/engine.test.ts +287 -11
- package/src/lint/engine.ts +101 -23
- package/src/lint/rule-registry.test.ts +112 -0
- package/src/lint/rule-registry.ts +118 -0
- package/src/lint/rule.ts +8 -0
- package/src/lint/rules/cor017-composite-name-match.ts +2 -1
- package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +4 -3
- package/src/lint/rules/declarable-naming-convention.ts +1 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +1 -0
- package/src/lint/rules/evl002-control-flow-resource.ts +1 -0
- package/src/lint/rules/evl003-dynamic-property-access.ts +1 -0
- package/src/lint/rules/evl004-spread-non-const.ts +1 -0
- package/src/lint/rules/evl005-resource-block-body.ts +1 -0
- package/src/lint/rules/evl007-invalid-siblings.ts +1 -0
- package/src/lint/rules/evl009-composite-no-constant.ts +1 -0
- package/src/lint/rules/evl010-composite-no-transform.ts +1 -0
- package/src/lint/rules/export-required.ts +1 -0
- package/src/lint/rules/file-declarable-limit.ts +1 -0
- package/src/lint/rules/flat-declarations.test.ts +8 -7
- package/src/lint/rules/flat-declarations.ts +2 -3
- package/src/lint/rules/no-cyclic-declarable-ref.ts +1 -0
- package/src/lint/rules/no-redundant-type-import.ts +1 -0
- package/src/lint/rules/no-redundant-value-cast.ts +1 -0
- package/src/lint/rules/no-string-ref.ts +1 -0
- package/src/lint/rules/no-unused-declarable-import.ts +1 -0
- package/src/lint/rules/no-unused-declarable.test.ts +8 -0
- package/src/lint/rules/no-unused-declarable.ts +4 -0
- package/src/lint/rules/single-concern-file.ts +1 -0
- package/src/lsp/lexicon-providers.ts +7 -0
- package/src/lsp/types.ts +1 -0
- package/src/resource-attributes.test.ts +79 -0
- package/src/resource-attributes.ts +42 -0
- package/src/runtime-adapter.ts +158 -0
- package/src/runtime.ts +4 -3
- package/src/serializer-walker.test.ts +0 -9
- package/src/serializer-walker.ts +1 -3
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +0 -45
- package/src/codegen/case.test.ts +0 -30
- package/src/codegen/case.ts +0 -11
- package/src/codegen/rollback.test.ts +0 -92
- package/src/codegen/rollback.ts +0 -115
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { generateRuleCatalog, generateRuleDetailPage } from "./docs-rules";
|
|
3
|
+
import type { RuleEntry } from "../lint/rule-registry";
|
|
4
|
+
|
|
5
|
+
const mockEntries: RuleEntry[] = [
|
|
6
|
+
{
|
|
7
|
+
id: "COR001",
|
|
8
|
+
description: "No inline objects in Declarable constructors",
|
|
9
|
+
category: "style",
|
|
10
|
+
defaultSeverity: "warning",
|
|
11
|
+
source: "core",
|
|
12
|
+
phase: "pre-synth",
|
|
13
|
+
hasAutoFix: false,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "WAW018",
|
|
17
|
+
description: "S3 bucket missing public access block",
|
|
18
|
+
category: "security",
|
|
19
|
+
defaultSeverity: "error",
|
|
20
|
+
source: "aws",
|
|
21
|
+
phase: "post-synth",
|
|
22
|
+
hasAutoFix: false,
|
|
23
|
+
helpUri: "https://chant.dev/lint-rules/waw018",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "COR008",
|
|
27
|
+
description: "Export required for declarable instances",
|
|
28
|
+
category: "correctness",
|
|
29
|
+
defaultSeverity: "warning",
|
|
30
|
+
source: "core",
|
|
31
|
+
phase: "pre-synth",
|
|
32
|
+
hasAutoFix: true,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
describe("generateRuleCatalog", () => {
|
|
37
|
+
test("generates valid MDX with frontmatter", () => {
|
|
38
|
+
const mdx = generateRuleCatalog(mockEntries);
|
|
39
|
+
|
|
40
|
+
expect(mdx).toContain("---");
|
|
41
|
+
expect(mdx).toContain('title: "Rule Reference"');
|
|
42
|
+
expect(mdx).toContain("**3** rules");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("groups rules by category", () => {
|
|
46
|
+
const mdx = generateRuleCatalog(mockEntries);
|
|
47
|
+
|
|
48
|
+
expect(mdx).toContain("## Correctness");
|
|
49
|
+
expect(mdx).toContain("## Security");
|
|
50
|
+
expect(mdx).toContain("## Style");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("includes table headers", () => {
|
|
54
|
+
const mdx = generateRuleCatalog(mockEntries);
|
|
55
|
+
|
|
56
|
+
expect(mdx).toContain("| ID | Description | Severity | Phase | Fix |");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("renders rule entries with correct data", () => {
|
|
60
|
+
const mdx = generateRuleCatalog(mockEntries);
|
|
61
|
+
|
|
62
|
+
expect(mdx).toContain("COR001");
|
|
63
|
+
expect(mdx).toContain("No inline objects");
|
|
64
|
+
expect(mdx).toContain("WAW018");
|
|
65
|
+
expect(mdx).toContain("public access");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("marks auto-fix rules", () => {
|
|
69
|
+
const mdx = generateRuleCatalog(mockEntries);
|
|
70
|
+
const lines = mdx.split("\n");
|
|
71
|
+
const cor008Line = lines.find((l) => l.includes("COR008"));
|
|
72
|
+
expect(cor008Line).toContain("Yes");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("renders helpUri as link", () => {
|
|
76
|
+
const mdx = generateRuleCatalog(mockEntries);
|
|
77
|
+
expect(mdx).toContain("[`WAW018`](https://chant.dev/lint-rules/waw018)");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("generateRuleDetailPage", () => {
|
|
82
|
+
test("generates valid MDX with frontmatter", () => {
|
|
83
|
+
const mdx = generateRuleDetailPage(mockEntries[0]);
|
|
84
|
+
|
|
85
|
+
expect(mdx).toContain("---");
|
|
86
|
+
expect(mdx).toContain("COR001");
|
|
87
|
+
expect(mdx).toContain("No inline objects");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("includes metadata table", () => {
|
|
91
|
+
const mdx = generateRuleDetailPage(mockEntries[0]);
|
|
92
|
+
|
|
93
|
+
expect(mdx).toContain("| **Severity** | warning |");
|
|
94
|
+
expect(mdx).toContain("| **Category** | style |");
|
|
95
|
+
expect(mdx).toContain("| **Phase** | pre-synth |");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("includes configuration example", () => {
|
|
99
|
+
const mdx = generateRuleDetailPage(mockEntries[0]);
|
|
100
|
+
|
|
101
|
+
expect(mdx).toContain("## Configuration");
|
|
102
|
+
expect(mdx).toContain('"COR001"');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("includes disable syntax", () => {
|
|
106
|
+
const mdx = generateRuleDetailPage(mockEntries[0]);
|
|
107
|
+
|
|
108
|
+
expect(mdx).toContain("## Disabling");
|
|
109
|
+
expect(mdx).toContain("chant-disable COR001");
|
|
110
|
+
expect(mdx).toContain("chant-disable-next-line COR001 -- reason");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule documentation auto-generation.
|
|
3
|
+
*
|
|
4
|
+
* Generates MDX pages from a RuleEntry registry:
|
|
5
|
+
* - A catalog page with all rules grouped by category
|
|
6
|
+
* - Per-rule detail pages with configuration examples and disable syntax
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { RuleEntry } from "../lint/rule-registry";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a rule catalog MDX page with all rules grouped by category.
|
|
13
|
+
*/
|
|
14
|
+
export function generateRuleCatalog(
|
|
15
|
+
entries: RuleEntry[],
|
|
16
|
+
displayName = "chant",
|
|
17
|
+
): string {
|
|
18
|
+
const lines: string[] = [
|
|
19
|
+
"---",
|
|
20
|
+
`title: "Rule Reference"`,
|
|
21
|
+
`description: "Complete reference of all lint rules and post-synth checks"`,
|
|
22
|
+
"---",
|
|
23
|
+
"",
|
|
24
|
+
`This page lists all **${entries.length}** rules available in ${displayName}.`,
|
|
25
|
+
"",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// Group by category
|
|
29
|
+
const byCategory = new Map<string, RuleEntry[]>();
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const cat = entry.category;
|
|
32
|
+
const existing = byCategory.get(cat) ?? [];
|
|
33
|
+
existing.push(entry);
|
|
34
|
+
byCategory.set(cat, existing);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Render each category
|
|
38
|
+
const categoryOrder = ["correctness", "security", "style", "performance"];
|
|
39
|
+
const sortedCategories = [...byCategory.keys()].sort(
|
|
40
|
+
(a, b) => (categoryOrder.indexOf(a) ?? 99) - (categoryOrder.indexOf(b) ?? 99),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
for (const cat of sortedCategories) {
|
|
44
|
+
const catEntries = byCategory.get(cat)!;
|
|
45
|
+
const title = cat.charAt(0).toUpperCase() + cat.slice(1);
|
|
46
|
+
|
|
47
|
+
lines.push(
|
|
48
|
+
`## ${title}`,
|
|
49
|
+
"",
|
|
50
|
+
"| ID | Description | Severity | Phase | Fix |",
|
|
51
|
+
"|----|-------------|----------|-------|-----|",
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
for (const entry of catEntries.sort((a, b) => a.id.localeCompare(b.id))) {
|
|
55
|
+
const fix = entry.hasAutoFix ? "Yes" : "";
|
|
56
|
+
const link = entry.helpUri
|
|
57
|
+
? `[\`${entry.id}\`](${entry.helpUri})`
|
|
58
|
+
: `\`${entry.id}\``;
|
|
59
|
+
lines.push(
|
|
60
|
+
`| ${link} | ${entry.description} | ${entry.defaultSeverity} | ${entry.phase} | ${fix} |`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
lines.push("");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return lines.join("\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate a per-rule detail MDX page.
|
|
72
|
+
*/
|
|
73
|
+
export function generateRuleDetailPage(entry: RuleEntry): string {
|
|
74
|
+
const lines: string[] = [
|
|
75
|
+
"---",
|
|
76
|
+
`title: "${entry.id}: ${entry.description}"`,
|
|
77
|
+
`description: "${entry.description}"`,
|
|
78
|
+
"---",
|
|
79
|
+
"",
|
|
80
|
+
`# ${entry.id}`,
|
|
81
|
+
"",
|
|
82
|
+
entry.description,
|
|
83
|
+
"",
|
|
84
|
+
"| Property | Value |",
|
|
85
|
+
"|----------|-------|",
|
|
86
|
+
`| **ID** | \`${entry.id}\` |`,
|
|
87
|
+
`| **Severity** | ${entry.defaultSeverity} |`,
|
|
88
|
+
`| **Category** | ${entry.category} |`,
|
|
89
|
+
`| **Phase** | ${entry.phase} |`,
|
|
90
|
+
`| **Source** | ${entry.source} |`,
|
|
91
|
+
`| **Auto-fix** | ${entry.hasAutoFix ? "Yes" : "No"} |`,
|
|
92
|
+
"",
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
// Configuration example
|
|
96
|
+
lines.push(
|
|
97
|
+
"## Configuration",
|
|
98
|
+
"",
|
|
99
|
+
"Override severity in your `chant.config.ts`:",
|
|
100
|
+
"",
|
|
101
|
+
"```ts",
|
|
102
|
+
"// chant.config.ts",
|
|
103
|
+
`rules: {`,
|
|
104
|
+
` "${entry.id}": "warning", // or "error", "info", "off"`,
|
|
105
|
+
`}`,
|
|
106
|
+
"```",
|
|
107
|
+
"",
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Disable syntax
|
|
111
|
+
lines.push(
|
|
112
|
+
"## Disabling",
|
|
113
|
+
"",
|
|
114
|
+
"Suppress this rule with inline comments:",
|
|
115
|
+
"",
|
|
116
|
+
"```ts",
|
|
117
|
+
`// chant-disable ${entry.id}`,
|
|
118
|
+
`// ... entire file suppressed for ${entry.id}`,
|
|
119
|
+
"",
|
|
120
|
+
`const x = 1; // chant-disable-line ${entry.id}`,
|
|
121
|
+
"",
|
|
122
|
+
`// chant-disable-next-line ${entry.id} -- reason for suppression`,
|
|
123
|
+
`const y = 2;`,
|
|
124
|
+
"```",
|
|
125
|
+
"",
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return lines.join("\n");
|
|
129
|
+
}
|
package/src/codegen/docs.ts
CHANGED
|
@@ -32,7 +32,7 @@ export interface DocsConfig {
|
|
|
32
32
|
/** Custom sections to append to overview page */
|
|
33
33
|
extraSections?: Array<{ title: string; content: string }>;
|
|
34
34
|
/** Standalone pages added to the sidebar after Overview */
|
|
35
|
-
extraPages?: Array<{ slug: string; title: string; description?: string; content: string }>;
|
|
35
|
+
extraPages?: Array<{ slug: string; title: string; description?: string; content: string; sidebar?: boolean }>;
|
|
36
36
|
/** Slugs of auto-generated pages to suppress (e.g. "pseudo-parameters") */
|
|
37
37
|
suppressPages?: string[];
|
|
38
38
|
/** Source directory for scanning rule files (defaults to srcDir sibling of distDir) */
|
|
@@ -385,6 +385,7 @@ function buildSidebar(
|
|
|
385
385
|
// Extra pages from lexicon config (appear after Overview)
|
|
386
386
|
if (config.extraPages) {
|
|
387
387
|
for (const page of config.extraPages) {
|
|
388
|
+
if (page.sidebar === false) continue;
|
|
388
389
|
items.push({ label: page.title, slug: page.slug });
|
|
389
390
|
}
|
|
390
391
|
}
|
|
@@ -447,6 +448,7 @@ function generateOverview(
|
|
|
447
448
|
// Extra pages listed first in reference links
|
|
448
449
|
if (config.extraPages && config.extraPages.length > 0) {
|
|
449
450
|
for (const page of config.extraPages) {
|
|
451
|
+
if (page.sidebar === false) continue;
|
|
450
452
|
lines.push(`- [${page.title}](./${page.slug})`);
|
|
451
453
|
}
|
|
452
454
|
}
|
|
@@ -51,7 +51,7 @@ describe("buildRegistry", () => {
|
|
|
51
51
|
const entries = buildRegistry(resources, naming, testConfig);
|
|
52
52
|
|
|
53
53
|
expect(entries["Bucket"]).toBeDefined();
|
|
54
|
-
expect(entries["Bucket"].attrs).toEqual({
|
|
54
|
+
expect(entries["Bucket"].attrs).toEqual({ Arn: "Arn", DomainName: "DomainName" });
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
test("omits attrs when empty", () => {
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
|
|
9
9
|
import type { NamingStrategy } from "./naming";
|
|
10
10
|
import { propertyTypeName, extractDefName } from "./naming";
|
|
11
|
-
import { toCamelCase } from "./case";
|
|
12
11
|
import { constraintsIsEmpty, type PropertyConstraints } from "./json-schema";
|
|
13
12
|
|
|
14
13
|
export interface RegistryResource {
|
|
@@ -47,12 +46,12 @@ export function buildRegistry<E>(
|
|
|
47
46
|
const tsName = naming.resolve(typeName);
|
|
48
47
|
if (!tsName) continue;
|
|
49
48
|
|
|
50
|
-
// Build attrs map:
|
|
49
|
+
// Build attrs map: name → raw name (identity mapping)
|
|
51
50
|
let attrs: Record<string, string> | undefined;
|
|
52
51
|
if (r.attributes.length > 0) {
|
|
53
52
|
attrs = {};
|
|
54
53
|
for (const a of r.attributes) {
|
|
55
|
-
attrs[
|
|
54
|
+
attrs[a.name] = a.name;
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
|
|
@@ -18,8 +18,8 @@ describe("writeResourceClass", () => {
|
|
|
18
18
|
);
|
|
19
19
|
const output = lines.join("\n");
|
|
20
20
|
expect(output).toContain("export declare class Bucket {");
|
|
21
|
-
expect(output).toContain("
|
|
22
|
-
expect(output).toContain("readonly
|
|
21
|
+
expect(output).toContain("BucketName: string;");
|
|
22
|
+
expect(output).toContain("readonly Arn: string;");
|
|
23
23
|
expect(output).toContain("}");
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -34,7 +34,7 @@ describe("writeResourceClass", () => {
|
|
|
34
34
|
remap,
|
|
35
35
|
);
|
|
36
36
|
const output = lines.join("\n");
|
|
37
|
-
expect(output).toContain("readonly
|
|
37
|
+
expect(output).toContain("readonly Config: BucketConfig;");
|
|
38
38
|
});
|
|
39
39
|
});
|
|
40
40
|
|
|
@@ -48,7 +48,7 @@ describe("writePropertyClass", () => {
|
|
|
48
48
|
);
|
|
49
49
|
const output = lines.join("\n");
|
|
50
50
|
expect(output).toContain("export declare class BucketConfig {");
|
|
51
|
-
expect(output).toContain("
|
|
51
|
+
expect(output).toContain("Enabled?: boolean;");
|
|
52
52
|
expect(output).toContain("}");
|
|
53
53
|
});
|
|
54
54
|
});
|
|
@@ -71,8 +71,8 @@ describe("writeConstructor", () => {
|
|
|
71
71
|
undefined,
|
|
72
72
|
);
|
|
73
73
|
const output = lines.join("\n");
|
|
74
|
-
const reqIdx = output.indexOf("
|
|
75
|
-
const optIdx = output.indexOf("
|
|
74
|
+
const reqIdx = output.indexOf("Required:");
|
|
75
|
+
const optIdx = output.indexOf("Optional?:");
|
|
76
76
|
expect(reqIdx).toBeLessThan(optIdx);
|
|
77
77
|
});
|
|
78
78
|
|
|
@@ -88,6 +88,70 @@ describe("writeConstructor", () => {
|
|
|
88
88
|
});
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
describe("writeConstructor with resourceAttributesType", () => {
|
|
92
|
+
test("empty props with attributes type emits both params", () => {
|
|
93
|
+
const lines: string[] = [];
|
|
94
|
+
writeConstructor(lines, [], undefined, "CFResourceAttributes");
|
|
95
|
+
expect(lines.join("\n")).toContain(
|
|
96
|
+
"constructor(props: Record<string, unknown>, attributes?: CFResourceAttributes);",
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("props with attributes type emits second param after closing brace", () => {
|
|
101
|
+
const lines: string[] = [];
|
|
102
|
+
writeConstructor(
|
|
103
|
+
lines,
|
|
104
|
+
[{ name: "BucketName", type: "string", required: true }],
|
|
105
|
+
undefined,
|
|
106
|
+
"CFResourceAttributes",
|
|
107
|
+
);
|
|
108
|
+
const output = lines.join("\n");
|
|
109
|
+
expect(output).toContain("constructor(props: {");
|
|
110
|
+
expect(output).toContain("BucketName: string;");
|
|
111
|
+
expect(output).toContain("}, attributes?: CFResourceAttributes);");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("without attributes type, closing brace has no second param", () => {
|
|
115
|
+
const lines: string[] = [];
|
|
116
|
+
writeConstructor(
|
|
117
|
+
lines,
|
|
118
|
+
[{ name: "Name", type: "string", required: true }],
|
|
119
|
+
undefined,
|
|
120
|
+
);
|
|
121
|
+
const output = lines.join("\n");
|
|
122
|
+
expect(output).toContain(" });");
|
|
123
|
+
expect(output).not.toContain("attributes?");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("writeResourceClass with resourceAttributesType", () => {
|
|
128
|
+
test("resource class constructor includes attributes param", () => {
|
|
129
|
+
const lines: string[] = [];
|
|
130
|
+
writeResourceClass(
|
|
131
|
+
lines,
|
|
132
|
+
"Bucket",
|
|
133
|
+
[{ name: "BucketName", type: "string", required: false }],
|
|
134
|
+
[{ name: "Arn", type: "string" }],
|
|
135
|
+
undefined,
|
|
136
|
+
"CFResourceAttributes",
|
|
137
|
+
);
|
|
138
|
+
const output = lines.join("\n");
|
|
139
|
+
expect(output).toContain("}, attributes?: CFResourceAttributes);");
|
|
140
|
+
expect(output).toContain("readonly Arn: string;");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("property class does not get attributes param", () => {
|
|
144
|
+
const lines: string[] = [];
|
|
145
|
+
writePropertyClass(
|
|
146
|
+
lines,
|
|
147
|
+
"BucketConfig",
|
|
148
|
+
[{ name: "Enabled", type: "boolean", required: false }],
|
|
149
|
+
);
|
|
150
|
+
const output = lines.join("\n");
|
|
151
|
+
expect(output).not.toContain("attributes?");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
91
155
|
describe("writeEnumType", () => {
|
|
92
156
|
test("writes single-line for short enum", () => {
|
|
93
157
|
const lines: string[] = [];
|
|
@@ -42,16 +42,17 @@ export function writeResourceClass(
|
|
|
42
42
|
properties: DtsProperty[],
|
|
43
43
|
attributes: DtsAttribute[],
|
|
44
44
|
remap?: Map<string, string>,
|
|
45
|
+
resourceAttributesType?: string,
|
|
45
46
|
): void {
|
|
46
47
|
lines.push("");
|
|
47
48
|
lines.push(`export declare class ${tsName} {`);
|
|
48
|
-
writeConstructor(lines, properties, remap);
|
|
49
|
+
writeConstructor(lines, properties, remap, resourceAttributesType);
|
|
49
50
|
|
|
50
51
|
// Attributes as readonly properties (sorted)
|
|
51
52
|
const attrs = [...attributes].sort((a, b) => a.name.localeCompare(b.name));
|
|
52
53
|
for (const a of attrs) {
|
|
53
54
|
const attrType = resolveConstructorType(a.type, remap);
|
|
54
|
-
lines.push(` readonly ${
|
|
55
|
+
lines.push(` readonly ${a.name}: ${attrType};`);
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
lines.push("}");
|
|
@@ -79,9 +80,14 @@ export function writeConstructor(
|
|
|
79
80
|
lines: string[],
|
|
80
81
|
props: DtsProperty[],
|
|
81
82
|
remap: Map<string, string> | undefined,
|
|
83
|
+
resourceAttributesType?: string,
|
|
82
84
|
): void {
|
|
83
85
|
if (props.length === 0) {
|
|
84
|
-
|
|
86
|
+
if (resourceAttributesType) {
|
|
87
|
+
lines.push(` constructor(props: Record<string, unknown>, attributes?: ${resourceAttributesType});`);
|
|
88
|
+
} else {
|
|
89
|
+
lines.push(" constructor(props: Record<string, unknown>);");
|
|
90
|
+
}
|
|
85
91
|
return;
|
|
86
92
|
}
|
|
87
93
|
|
|
@@ -98,9 +104,13 @@ export function writeConstructor(
|
|
|
98
104
|
if (p.description) {
|
|
99
105
|
lines.push(` /** ${p.description} */`);
|
|
100
106
|
}
|
|
101
|
-
lines.push(` ${
|
|
107
|
+
lines.push(` ${p.name}${optional}: ${tsType};`);
|
|
108
|
+
}
|
|
109
|
+
if (resourceAttributesType) {
|
|
110
|
+
lines.push(` }, attributes?: ${resourceAttributesType});`);
|
|
111
|
+
} else {
|
|
112
|
+
lines.push(" });");
|
|
102
113
|
}
|
|
103
|
-
lines.push(" });");
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
/**
|
|
@@ -155,7 +165,3 @@ export function resolveConstructorType(tsType: string, remap: Map<string, string
|
|
|
155
165
|
|
|
156
166
|
return tsType;
|
|
157
167
|
}
|
|
158
|
-
|
|
159
|
-
function toCamelCase(name: string): string {
|
|
160
|
-
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
161
|
-
}
|
package/src/codegen/generate.ts
CHANGED
|
@@ -180,26 +180,15 @@ export interface WriteConfig {
|
|
|
180
180
|
generatedSubdir?: string;
|
|
181
181
|
/** Map of filename → content to write. */
|
|
182
182
|
files: Record<string, string>;
|
|
183
|
-
/** Optional snapshot function called before overwriting. */
|
|
184
|
-
snapshot?: (generatedDir: string) => void;
|
|
185
183
|
}
|
|
186
184
|
|
|
187
185
|
/**
|
|
188
|
-
* Write generated artifacts to disk
|
|
186
|
+
* Write generated artifacts to disk.
|
|
189
187
|
*/
|
|
190
188
|
export function writeGeneratedArtifacts(config: WriteConfig): void {
|
|
191
189
|
const generatedDir = join(config.baseDir, config.generatedSubdir ?? "src/generated");
|
|
192
190
|
mkdirSync(generatedDir, { recursive: true });
|
|
193
191
|
|
|
194
|
-
// Auto-snapshot before overwriting
|
|
195
|
-
if (config.snapshot) {
|
|
196
|
-
try {
|
|
197
|
-
config.snapshot(generatedDir);
|
|
198
|
-
} catch {
|
|
199
|
-
// Best effort — don't fail generation if snapshot fails
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
192
|
for (const [filename, content] of Object.entries(config.files)) {
|
|
204
193
|
writeFileSync(join(generatedDir, filename), content);
|
|
205
194
|
}
|
package/src/codegen/package.ts
CHANGED
|
@@ -113,7 +113,7 @@ export async function packagePipeline(
|
|
|
113
113
|
/**
|
|
114
114
|
* Collect lint rule source files from a lexicon package.
|
|
115
115
|
* Auto-discovers .ts files in the specified directories,
|
|
116
|
-
* skipping test files,
|
|
116
|
+
* skipping test files, re-export files (index.ts), and non-.ts files.
|
|
117
117
|
*/
|
|
118
118
|
export function collectRules(
|
|
119
119
|
srcDir: string,
|
package/src/codegen/typecheck.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { writeFileSync, mkdirSync, rmSync } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { tmpdir } from "os";
|
|
7
|
+
import { getRuntime } from "../runtime-adapter";
|
|
7
8
|
|
|
8
9
|
export interface TypeCheckResult {
|
|
9
10
|
ok: boolean;
|
|
@@ -39,17 +40,11 @@ export async function typecheckDTS(content: string): Promise<TypeCheckResult> {
|
|
|
39
40
|
writeFileSync(join(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
|
|
40
41
|
|
|
41
42
|
// Run tsc
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const [stdout, stderr] = await Promise.all([
|
|
49
|
-
new Response(proc.stdout).text(),
|
|
50
|
-
new Response(proc.stderr).text(),
|
|
51
|
-
]);
|
|
52
|
-
const exitCode = await proc.exited;
|
|
43
|
+
const rt = getRuntime();
|
|
44
|
+
const { stdout, stderr, exitCode } = await rt.spawn(
|
|
45
|
+
[rt.commands.exec, "tsc", "--noEmit", "--project", "tsconfig.json"],
|
|
46
|
+
{ cwd: dir },
|
|
47
|
+
);
|
|
53
48
|
|
|
54
49
|
// Parse diagnostics from stdout (tsc writes errors to stdout)
|
|
55
50
|
const output = stdout + stderr;
|