@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,79 @@
|
|
|
1
|
+
import { describe, test, expect, spyOn } from "bun:test";
|
|
2
|
+
import { resolveDependsOn } from "./resource-attributes";
|
|
3
|
+
import { DECLARABLE_MARKER, type Declarable } from "./declarable";
|
|
4
|
+
|
|
5
|
+
function mockDeclarable(type = "AWS::S3::Bucket"): Declarable {
|
|
6
|
+
return {
|
|
7
|
+
[DECLARABLE_MARKER]: true,
|
|
8
|
+
lexicon: "aws",
|
|
9
|
+
entityType: type,
|
|
10
|
+
kind: "resource",
|
|
11
|
+
} as Declarable;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("resolveDependsOn", () => {
|
|
15
|
+
test("resolves a single Declarable to its logical name", () => {
|
|
16
|
+
const bucket = mockDeclarable();
|
|
17
|
+
const entityNames = new Map<Declarable, string>([[bucket, "MyBucket"]]);
|
|
18
|
+
|
|
19
|
+
const result = resolveDependsOn(bucket, entityNames, "MyResource");
|
|
20
|
+
expect(result).toEqual(["MyBucket"]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("resolves an array of Declarables", () => {
|
|
24
|
+
const bucket = mockDeclarable("AWS::S3::Bucket");
|
|
25
|
+
const role = mockDeclarable("AWS::IAM::Role");
|
|
26
|
+
const entityNames = new Map<Declarable, string>([
|
|
27
|
+
[bucket, "MyBucket"],
|
|
28
|
+
[role, "MyRole"],
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const result = resolveDependsOn([bucket, role], entityNames, "MyResource");
|
|
32
|
+
expect(result).toEqual(["MyBucket", "MyRole"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("passes through a single string", () => {
|
|
36
|
+
const entityNames = new Map<Declarable, string>();
|
|
37
|
+
const result = resolveDependsOn("ExternalResource", entityNames, "MyResource");
|
|
38
|
+
expect(result).toEqual(["ExternalResource"]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("passes through an array of strings", () => {
|
|
42
|
+
const entityNames = new Map<Declarable, string>();
|
|
43
|
+
const result = resolveDependsOn(["ResA", "ResB"], entityNames, "MyResource");
|
|
44
|
+
expect(result).toEqual(["ResA", "ResB"]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("handles mixed strings and Declarables", () => {
|
|
48
|
+
const bucket = mockDeclarable();
|
|
49
|
+
const entityNames = new Map<Declarable, string>([[bucket, "MyBucket"]]);
|
|
50
|
+
|
|
51
|
+
const result = resolveDependsOn(["ManualRef", bucket], entityNames, "MyResource");
|
|
52
|
+
expect(result).toEqual(["ManualRef", "MyBucket"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("warns and skips Declarable not found in entityNames", () => {
|
|
56
|
+
const bucket = mockDeclarable();
|
|
57
|
+
const entityNames = new Map<Declarable, string>(); // bucket not registered
|
|
58
|
+
const spy = spyOn(console, "warn").mockImplementation(() => {});
|
|
59
|
+
|
|
60
|
+
const result = resolveDependsOn(bucket, entityNames, "MyResource");
|
|
61
|
+
expect(result).toEqual([]);
|
|
62
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
63
|
+
expect(spy.mock.calls[0][0]).toContain("MyResource");
|
|
64
|
+
|
|
65
|
+
spy.mockRestore();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("returns empty array for empty array input", () => {
|
|
69
|
+
const entityNames = new Map<Declarable, string>();
|
|
70
|
+
const result = resolveDependsOn([], entityNames, "MyResource");
|
|
71
|
+
expect(result).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("skips non-string non-Declarable values silently", () => {
|
|
75
|
+
const entityNames = new Map<Declarable, string>();
|
|
76
|
+
const result = resolveDependsOn([42, null, true] as unknown[], entityNames, "MyResource");
|
|
77
|
+
expect(result).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for resolving resource-level attributes (DependsOn, Condition, etc.).
|
|
3
|
+
*
|
|
4
|
+
* Shared by lexicon serializers — the DependsOn resolution logic converts
|
|
5
|
+
* Declarable object references to logical names.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Declarable } from "./declarable";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a DependsOn value (Declarable, string, or mixed array) into
|
|
12
|
+
* an array of logical resource names.
|
|
13
|
+
*
|
|
14
|
+
* - Strings pass through as-is (user-specified logical names)
|
|
15
|
+
* - Declarable objects are looked up in the entityNames map
|
|
16
|
+
* - Unknown values emit a console warning and are skipped
|
|
17
|
+
*/
|
|
18
|
+
export function resolveDependsOn(
|
|
19
|
+
deps: unknown,
|
|
20
|
+
entityNames: Map<Declarable, string>,
|
|
21
|
+
resourceName: string,
|
|
22
|
+
): string[] {
|
|
23
|
+
const items = Array.isArray(deps) ? deps : [deps];
|
|
24
|
+
const resolved: string[] = [];
|
|
25
|
+
|
|
26
|
+
for (const dep of items) {
|
|
27
|
+
if (typeof dep === "string") {
|
|
28
|
+
resolved.push(dep);
|
|
29
|
+
} else if (typeof dep === "object" && dep !== null && "entityType" in dep) {
|
|
30
|
+
const depName = entityNames.get(dep as Declarable);
|
|
31
|
+
if (depName) {
|
|
32
|
+
resolved.push(depName);
|
|
33
|
+
} else {
|
|
34
|
+
console.warn(
|
|
35
|
+
`[chant] warning: DependsOn in "${resourceName}" references a declarable not found in the build — is the target resource exported?`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return resolved;
|
|
42
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime adapter — abstracts Bun-specific APIs so chant can run on Bun or Node.js.
|
|
3
|
+
*
|
|
4
|
+
* Auto-detects the host runtime and delegates to the appropriate implementation.
|
|
5
|
+
* The `target` parameter (from config) controls what gets spawned (bun vs node/npx/npm),
|
|
6
|
+
* not which adapter class is used.
|
|
7
|
+
*/
|
|
8
|
+
import { dirname } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { createHash } from "crypto";
|
|
11
|
+
import { execFile } from "child_process";
|
|
12
|
+
// @ts-ignore — picomatch has no types declaration
|
|
13
|
+
import picomatch from "picomatch";
|
|
14
|
+
|
|
15
|
+
export interface SpawnResult {
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
exitCode: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RuntimeCommands {
|
|
22
|
+
/** Runtime binary: "bun" | "node" */
|
|
23
|
+
runner: string;
|
|
24
|
+
/** Package executor: "bunx" | "npx" */
|
|
25
|
+
exec: string;
|
|
26
|
+
/** Pack command: ["bun", "pm", "pack"] | ["npm", "pack"] */
|
|
27
|
+
packCmd: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RuntimeAdapter {
|
|
31
|
+
readonly name: "bun" | "node";
|
|
32
|
+
/** Hash content and return hex string */
|
|
33
|
+
hash(content: string): string;
|
|
34
|
+
/** Algorithm name recorded in integrity.json */
|
|
35
|
+
readonly hashAlgorithm: string;
|
|
36
|
+
/** Test whether filePath matches a glob pattern */
|
|
37
|
+
globMatch(pattern: string, filePath: string): boolean;
|
|
38
|
+
/** Spawn a child process and collect output */
|
|
39
|
+
spawn(cmd: string[], opts?: { cwd?: string }): Promise<SpawnResult>;
|
|
40
|
+
/** Commands to use when spawning package manager / executor */
|
|
41
|
+
readonly commands: RuntimeCommands;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Bun adapter ──────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
class BunRuntimeAdapter implements RuntimeAdapter {
|
|
47
|
+
readonly name = "bun" as const;
|
|
48
|
+
readonly hashAlgorithm = "xxhash64";
|
|
49
|
+
readonly commands: RuntimeCommands;
|
|
50
|
+
|
|
51
|
+
constructor(target: "bun" | "node") {
|
|
52
|
+
this.commands =
|
|
53
|
+
target === "node"
|
|
54
|
+
? { runner: "node", exec: "npx", packCmd: ["npm", "pack"] }
|
|
55
|
+
: { runner: "bun", exec: "bunx", packCmd: ["bun", "pm", "pack"] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
hash(content: string): string {
|
|
59
|
+
return Bun.hash(content).toString(16);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
globMatch(pattern: string, filePath: string): boolean {
|
|
63
|
+
const glob = new Bun.Glob(pattern);
|
|
64
|
+
return glob.match(filePath);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async spawn(cmd: string[], opts?: { cwd?: string }): Promise<SpawnResult> {
|
|
68
|
+
const proc = Bun.spawn(cmd, {
|
|
69
|
+
cwd: opts?.cwd,
|
|
70
|
+
stdout: "pipe",
|
|
71
|
+
stderr: "pipe",
|
|
72
|
+
});
|
|
73
|
+
const [stdout, stderr] = await Promise.all([
|
|
74
|
+
new Response(proc.stdout).text(),
|
|
75
|
+
new Response(proc.stderr).text(),
|
|
76
|
+
]);
|
|
77
|
+
const exitCode = await proc.exited;
|
|
78
|
+
return { stdout, stderr, exitCode };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Node adapter ─────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
class NodeRuntimeAdapter implements RuntimeAdapter {
|
|
85
|
+
readonly name = "node" as const;
|
|
86
|
+
readonly hashAlgorithm = "sha256";
|
|
87
|
+
readonly commands: RuntimeCommands;
|
|
88
|
+
|
|
89
|
+
constructor(target: "bun" | "node") {
|
|
90
|
+
this.commands =
|
|
91
|
+
target === "bun"
|
|
92
|
+
? { runner: "bun", exec: "bunx", packCmd: ["bun", "pm", "pack"] }
|
|
93
|
+
: { runner: "node", exec: "npx", packCmd: ["npm", "pack"] };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
hash(content: string): string {
|
|
97
|
+
return createHash("sha256").update(content).digest("hex");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
globMatch(pattern: string, filePath: string): boolean {
|
|
101
|
+
return picomatch(pattern)(filePath);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async spawn(cmd: string[], opts?: { cwd?: string }): Promise<SpawnResult> {
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
execFile(cmd[0], cmd.slice(1), { cwd: opts?.cwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
107
|
+
resolve({
|
|
108
|
+
stdout: stdout ?? "",
|
|
109
|
+
stderr: stderr ?? "",
|
|
110
|
+
exitCode: err ? (err as any).code ?? 1 : 0,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Singleton ────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
let _runtime: RuntimeAdapter | undefined;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Detect whether we're running under Bun.
|
|
123
|
+
*/
|
|
124
|
+
function isBun(): boolean {
|
|
125
|
+
return typeof globalThis.Bun !== "undefined";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Initialize the runtime adapter singleton.
|
|
130
|
+
*
|
|
131
|
+
* @param target - Which commands to spawn ("bun" or "node"). Defaults to auto-detect.
|
|
132
|
+
* Controls the `commands` property, not which adapter class is used.
|
|
133
|
+
*/
|
|
134
|
+
export function initRuntime(target?: "bun" | "node"): RuntimeAdapter {
|
|
135
|
+
const resolvedTarget = target ?? (isBun() ? "bun" : "node");
|
|
136
|
+
_runtime = isBun()
|
|
137
|
+
? new BunRuntimeAdapter(resolvedTarget)
|
|
138
|
+
: new NodeRuntimeAdapter(resolvedTarget);
|
|
139
|
+
return _runtime;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get the runtime adapter. Lazily initializes with auto-detection if not yet set.
|
|
144
|
+
*/
|
|
145
|
+
export function getRuntime(): RuntimeAdapter {
|
|
146
|
+
if (!_runtime) {
|
|
147
|
+
return initRuntime();
|
|
148
|
+
}
|
|
149
|
+
return _runtime;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Convert `import.meta.url` to a directory path.
|
|
154
|
+
* Works on both Bun and Node (replaces Bun-only `import.meta.dir`).
|
|
155
|
+
*/
|
|
156
|
+
export function moduleDir(importMetaUrl: string): string {
|
|
157
|
+
return dirname(fileURLToPath(importMetaUrl));
|
|
158
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -22,13 +22,14 @@ export function createResource(
|
|
|
22
22
|
type: string,
|
|
23
23
|
lexicon: string,
|
|
24
24
|
attrMap: Record<string, string>,
|
|
25
|
-
): new (props: Record<string, unknown>) => Record<string, unknown> {
|
|
26
|
-
const ResourceClass = function (this: Record<string, unknown>, props: Record<string, unknown>) {
|
|
25
|
+
): new (props: Record<string, unknown>, attributes?: Record<string, unknown>) => Record<string, unknown> {
|
|
26
|
+
const ResourceClass = function (this: Record<string, unknown>, props: Record<string, unknown>, attributes?: Record<string, unknown>) {
|
|
27
27
|
Object.defineProperty(this, DECLARABLE_MARKER, { value: true, enumerable: false });
|
|
28
28
|
Object.defineProperty(this, "lexicon", { value: lexicon, enumerable: false });
|
|
29
29
|
Object.defineProperty(this, "entityType", { value: type, enumerable: false });
|
|
30
30
|
Object.defineProperty(this, "kind", { value: "resource", enumerable: false });
|
|
31
31
|
Object.defineProperty(this, "props", { value: props ?? {}, enumerable: false, configurable: true });
|
|
32
|
+
Object.defineProperty(this, "attributes", { value: attributes ?? {}, enumerable: false, configurable: true });
|
|
32
33
|
|
|
33
34
|
// Create AttrRef instances for each attribute
|
|
34
35
|
// Must be enumerable so getAttributes() can discover them for resolveAttrRefs()
|
|
@@ -39,7 +40,7 @@ export function createResource(
|
|
|
39
40
|
writable: false,
|
|
40
41
|
});
|
|
41
42
|
}
|
|
42
|
-
} as unknown as new (props: Record<string, unknown>) => Record<string, unknown>;
|
|
43
|
+
} as unknown as new (props: Record<string, unknown>, attributes?: Record<string, unknown>) => Record<string, unknown>;
|
|
43
44
|
|
|
44
45
|
// Set the constructor name for debugging
|
|
45
46
|
Object.defineProperty(ResourceClass, "name", { value: type.split("::").pop() ?? type });
|
|
@@ -93,15 +93,6 @@ describe("walkValue", () => {
|
|
|
93
93
|
expect(walkValue({ a: 1, b: { c: 2 } }, names, mockVisitor)).toEqual({ a: 1, b: { c: 2 } });
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
test("applies transformKey when provided", () => {
|
|
97
|
-
const visitor: SerializerVisitor = {
|
|
98
|
-
...mockVisitor,
|
|
99
|
-
transformKey: (k) => k.toUpperCase(),
|
|
100
|
-
};
|
|
101
|
-
const names = new Map<Declarable, string>();
|
|
102
|
-
expect(walkValue({ foo: 1, bar: 2 }, names, visitor)).toEqual({ FOO: 1, BAR: 2 });
|
|
103
|
-
});
|
|
104
|
-
|
|
105
96
|
test("complex nested structure", () => {
|
|
106
97
|
const resource = makeDeclarable("Test::Role");
|
|
107
98
|
const ref = new AttrRef(resource, "arn");
|
package/src/serializer-walker.ts
CHANGED
|
@@ -17,8 +17,6 @@ export interface SerializerVisitor {
|
|
|
17
17
|
resourceRef(logicalName: string): unknown;
|
|
18
18
|
/** Format a property-level Declarable by walking its props. */
|
|
19
19
|
propertyDeclarable(entity: Declarable, walk: (v: unknown) => unknown): unknown;
|
|
20
|
-
/** Optional key transformation (e.g. camelCase → PascalCase). */
|
|
21
|
-
transformKey?(key: string): string;
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
/**
|
|
@@ -73,7 +71,7 @@ export function walkValue(
|
|
|
73
71
|
if (typeof value === "object") {
|
|
74
72
|
const result: Record<string, unknown> = {};
|
|
75
73
|
for (const [key, val] of Object.entries(value)) {
|
|
76
|
-
const outKey =
|
|
74
|
+
const outKey = key;
|
|
77
75
|
result[outKey] = walkValue(val, entityNames, visitor);
|
|
78
76
|
}
|
|
79
77
|
return result;
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, cpSync } from "fs";
|
|
2
|
-
import { join, basename } from "path";
|
|
3
|
-
|
|
4
|
-
export interface Snapshot {
|
|
5
|
-
timestamp: string;
|
|
6
|
-
resources: number;
|
|
7
|
-
path: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* List available generation snapshots.
|
|
12
|
-
*/
|
|
13
|
-
export function listSnapshots(snapshotsDir: string): Snapshot[] {
|
|
14
|
-
if (!existsSync(snapshotsDir)) return [];
|
|
15
|
-
|
|
16
|
-
return readdirSync(snapshotsDir)
|
|
17
|
-
.filter((d) => !d.startsWith("."))
|
|
18
|
-
.sort()
|
|
19
|
-
.reverse()
|
|
20
|
-
.map((dir) => {
|
|
21
|
-
const fullPath = join(snapshotsDir, dir);
|
|
22
|
-
const metaPath = join(fullPath, "meta.json");
|
|
23
|
-
let resources = 0;
|
|
24
|
-
if (existsSync(metaPath)) {
|
|
25
|
-
try {
|
|
26
|
-
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
27
|
-
resources = meta.resources ?? 0;
|
|
28
|
-
} catch {}
|
|
29
|
-
}
|
|
30
|
-
return { timestamp: dir, resources, path: fullPath };
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Restore a snapshot to the generated directory.
|
|
36
|
-
*/
|
|
37
|
-
export function restoreSnapshot(timestamp: string, generatedDir: string): void {
|
|
38
|
-
const snapshotsDir = join(generatedDir, "..", "..", ".snapshots");
|
|
39
|
-
const snapshotDir = join(snapshotsDir, timestamp);
|
|
40
|
-
if (!existsSync(snapshotDir)) {
|
|
41
|
-
throw new Error(`Snapshot not found: ${timestamp}`);
|
|
42
|
-
}
|
|
43
|
-
mkdirSync(generatedDir, { recursive: true });
|
|
44
|
-
cpSync(snapshotDir, generatedDir, { recursive: true });
|
|
45
|
-
}
|
package/src/codegen/case.test.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { toCamelCase, toPascalCase } from "./case";
|
|
3
|
-
|
|
4
|
-
describe("toCamelCase", () => {
|
|
5
|
-
test("lowercases first character", () => {
|
|
6
|
-
expect(toCamelCase("BucketName")).toBe("bucketName");
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
test("preserves already-camelCase", () => {
|
|
10
|
-
expect(toCamelCase("already")).toBe("already");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
test("handles single character", () => {
|
|
14
|
-
expect(toCamelCase("A")).toBe("a");
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe("toPascalCase", () => {
|
|
19
|
-
test("uppercases first character", () => {
|
|
20
|
-
expect(toPascalCase("bucketName")).toBe("BucketName");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("preserves already-PascalCase", () => {
|
|
24
|
-
expect(toPascalCase("Already")).toBe("Already");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("handles single character", () => {
|
|
28
|
-
expect(toPascalCase("a")).toBe("A");
|
|
29
|
-
});
|
|
30
|
-
});
|
package/src/codegen/case.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Case conversion utilities for codegen.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export function toCamelCase(name: string): string {
|
|
6
|
-
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function toPascalCase(name: string): string {
|
|
10
|
-
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
11
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { mkdirSync, writeFileSync, rmSync, readFileSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { tmpdir } from "os";
|
|
5
|
-
import { snapshotArtifacts, saveSnapshot, restoreSnapshot, listSnapshots } from "./rollback";
|
|
6
|
-
|
|
7
|
-
function makeTempDir(): string {
|
|
8
|
-
const dir = join(tmpdir(), `chant-rollback-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
9
|
-
mkdirSync(dir, { recursive: true });
|
|
10
|
-
return dir;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
describe("rollback", () => {
|
|
14
|
-
test("snapshot captures generated files with default artifact names", () => {
|
|
15
|
-
const dir = makeTempDir();
|
|
16
|
-
const genDir = join(dir, "generated");
|
|
17
|
-
mkdirSync(genDir, { recursive: true });
|
|
18
|
-
|
|
19
|
-
writeFileSync(join(genDir, "lexicon.json"), '{"Bucket":{"kind":"resource"}}');
|
|
20
|
-
writeFileSync(join(genDir, "index.d.ts"), "declare class Bucket {}");
|
|
21
|
-
writeFileSync(join(genDir, "index.ts"), "export const Bucket = {};");
|
|
22
|
-
|
|
23
|
-
const snapshot = snapshotArtifacts(genDir);
|
|
24
|
-
expect(snapshot.files["lexicon.json"]).toBeDefined();
|
|
25
|
-
expect(snapshot.files["index.d.ts"]).toBeDefined();
|
|
26
|
-
expect(snapshot.files["index.ts"]).toBeDefined();
|
|
27
|
-
expect(snapshot.hashes["lexicon.json"]).toBeDefined();
|
|
28
|
-
expect(snapshot.resourceCount).toBe(1);
|
|
29
|
-
|
|
30
|
-
rmSync(dir, { recursive: true, force: true });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("snapshot uses custom artifact names", () => {
|
|
34
|
-
const dir = makeTempDir();
|
|
35
|
-
const genDir = join(dir, "generated");
|
|
36
|
-
mkdirSync(genDir, { recursive: true });
|
|
37
|
-
|
|
38
|
-
writeFileSync(join(genDir, "my-lexicon.json"), '{"Resource":{"kind":"resource"}}');
|
|
39
|
-
writeFileSync(join(genDir, "types.d.ts"), "declare class Resource {}");
|
|
40
|
-
|
|
41
|
-
const snapshot = snapshotArtifacts(genDir, ["my-lexicon.json", "types.d.ts"]);
|
|
42
|
-
expect(snapshot.files["my-lexicon.json"]).toBeDefined();
|
|
43
|
-
expect(snapshot.files["types.d.ts"]).toBeDefined();
|
|
44
|
-
expect(snapshot.resourceCount).toBe(1);
|
|
45
|
-
|
|
46
|
-
rmSync(dir, { recursive: true, force: true });
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("save and list snapshots", () => {
|
|
50
|
-
const dir = makeTempDir();
|
|
51
|
-
const snapshotsDir = join(dir, ".snapshots");
|
|
52
|
-
|
|
53
|
-
const snapshot = {
|
|
54
|
-
timestamp: "2025-01-01T00:00:00.000Z",
|
|
55
|
-
files: { "test.json": "{}" },
|
|
56
|
-
hashes: { "test.json": "abc123" },
|
|
57
|
-
resourceCount: 0,
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
saveSnapshot(snapshot, snapshotsDir);
|
|
61
|
-
const list = listSnapshots(snapshotsDir);
|
|
62
|
-
expect(list.length).toBe(1);
|
|
63
|
-
expect(list[0].timestamp).toBe("2025-01-01T00:00:00.000Z");
|
|
64
|
-
|
|
65
|
-
rmSync(dir, { recursive: true, force: true });
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("restore snapshot overwrites generated files", () => {
|
|
69
|
-
const dir = makeTempDir();
|
|
70
|
-
const genDir = join(dir, "generated");
|
|
71
|
-
const snapshotsDir = join(dir, ".snapshots");
|
|
72
|
-
mkdirSync(genDir, { recursive: true });
|
|
73
|
-
|
|
74
|
-
writeFileSync(join(genDir, "lexicon.json"), '{"original":true}');
|
|
75
|
-
|
|
76
|
-
const snapshot = snapshotArtifacts(genDir);
|
|
77
|
-
const snapshotPath = saveSnapshot(snapshot, snapshotsDir);
|
|
78
|
-
|
|
79
|
-
writeFileSync(join(genDir, "lexicon.json"), '{"modified":true}');
|
|
80
|
-
expect(readFileSync(join(genDir, "lexicon.json"), "utf-8")).toBe('{"modified":true}');
|
|
81
|
-
|
|
82
|
-
restoreSnapshot(snapshotPath, genDir);
|
|
83
|
-
expect(readFileSync(join(genDir, "lexicon.json"), "utf-8")).toBe('{"original":true}');
|
|
84
|
-
|
|
85
|
-
rmSync(dir, { recursive: true, force: true });
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("listSnapshots returns empty for nonexistent dir", () => {
|
|
89
|
-
const list = listSnapshots("/nonexistent/path");
|
|
90
|
-
expect(list).toHaveLength(0);
|
|
91
|
-
});
|
|
92
|
-
});
|
package/src/codegen/rollback.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Artifact snapshot and restore for generation rollback.
|
|
3
|
-
*/
|
|
4
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "fs";
|
|
5
|
-
import { join } from "path";
|
|
6
|
-
import { hashArtifact } from "../lexicon-integrity";
|
|
7
|
-
|
|
8
|
-
export interface ArtifactSnapshot {
|
|
9
|
-
timestamp: string;
|
|
10
|
-
files: Record<string, string>;
|
|
11
|
-
hashes: Record<string, string>;
|
|
12
|
-
resourceCount: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface SnapshotInfo {
|
|
16
|
-
path: string;
|
|
17
|
-
timestamp: string;
|
|
18
|
-
resourceCount: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const DEFAULT_ARTIFACT_NAMES = ["lexicon.json", "index.d.ts", "index.ts"];
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Snapshot current generated artifacts.
|
|
25
|
-
*
|
|
26
|
-
* @param generatedDir - Directory containing generated artifacts
|
|
27
|
-
* @param artifactNames - List of filenames to snapshot (defaults to generic names)
|
|
28
|
-
*/
|
|
29
|
-
export function snapshotArtifacts(
|
|
30
|
-
generatedDir: string,
|
|
31
|
-
artifactNames: string[] = DEFAULT_ARTIFACT_NAMES,
|
|
32
|
-
): ArtifactSnapshot {
|
|
33
|
-
const files: Record<string, string> = {};
|
|
34
|
-
const hashes: Record<string, string> = {};
|
|
35
|
-
let resourceCount = 0;
|
|
36
|
-
|
|
37
|
-
for (const entry of artifactNames) {
|
|
38
|
-
const path = join(generatedDir, entry);
|
|
39
|
-
if (existsSync(path)) {
|
|
40
|
-
const content = readFileSync(path, "utf-8");
|
|
41
|
-
files[entry] = content;
|
|
42
|
-
hashes[entry] = hashArtifact(content);
|
|
43
|
-
|
|
44
|
-
// Count resources in any .json artifact
|
|
45
|
-
if (entry.endsWith(".json")) {
|
|
46
|
-
try {
|
|
47
|
-
const parsed = JSON.parse(content);
|
|
48
|
-
resourceCount = Object.values(parsed).filter(
|
|
49
|
-
(e: any) => e && typeof e === "object" && e.kind === "resource"
|
|
50
|
-
).length;
|
|
51
|
-
} catch {}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
timestamp: new Date().toISOString(),
|
|
58
|
-
files,
|
|
59
|
-
hashes,
|
|
60
|
-
resourceCount,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Save a snapshot to the .snapshots directory.
|
|
66
|
-
*/
|
|
67
|
-
export function saveSnapshot(snapshot: ArtifactSnapshot, snapshotsDir: string): string {
|
|
68
|
-
mkdirSync(snapshotsDir, { recursive: true });
|
|
69
|
-
|
|
70
|
-
const filename = `${snapshot.timestamp.replace(/[:.]/g, "-")}.json`;
|
|
71
|
-
const path = join(snapshotsDir, filename);
|
|
72
|
-
writeFileSync(path, JSON.stringify(snapshot, null, 2));
|
|
73
|
-
return path;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Restore a snapshot to the generated directory.
|
|
78
|
-
*/
|
|
79
|
-
export function restoreSnapshot(snapshotPath: string, generatedDir: string): void {
|
|
80
|
-
const raw = readFileSync(snapshotPath, "utf-8");
|
|
81
|
-
const snapshot: ArtifactSnapshot = JSON.parse(raw);
|
|
82
|
-
|
|
83
|
-
mkdirSync(generatedDir, { recursive: true });
|
|
84
|
-
for (const [filename, content] of Object.entries(snapshot.files)) {
|
|
85
|
-
writeFileSync(join(generatedDir, filename), content);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* List available snapshots.
|
|
91
|
-
*/
|
|
92
|
-
export function listSnapshots(snapshotsDir: string): SnapshotInfo[] {
|
|
93
|
-
if (!existsSync(snapshotsDir)) return [];
|
|
94
|
-
|
|
95
|
-
const entries = readdirSync(snapshotsDir)
|
|
96
|
-
.filter((f) => f.endsWith(".json"))
|
|
97
|
-
.sort()
|
|
98
|
-
.reverse();
|
|
99
|
-
|
|
100
|
-
const snapshots: SnapshotInfo[] = [];
|
|
101
|
-
for (const entry of entries) {
|
|
102
|
-
try {
|
|
103
|
-
const path = join(snapshotsDir, entry);
|
|
104
|
-
const raw = readFileSync(path, "utf-8");
|
|
105
|
-
const data = JSON.parse(raw);
|
|
106
|
-
snapshots.push({
|
|
107
|
-
path,
|
|
108
|
-
timestamp: data.timestamp,
|
|
109
|
-
resourceCount: data.resourceCount ?? 0,
|
|
110
|
-
});
|
|
111
|
-
} catch {}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return snapshots;
|
|
115
|
-
}
|