@intentius/chant 0.0.5 → 0.0.8
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/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/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.ts +14 -3
- 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/main.ts +13 -3
- package/src/cli/mcp/server.test.ts +207 -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 +107 -0
- package/src/cli/mcp/tools/search.ts +98 -0
- package/src/codegen/generate-registry.test.ts +1 -1
- package/src/codegen/generate-registry.ts +2 -3
- package/src/codegen/generate-typescript.test.ts +6 -6
- package/src/codegen/generate-typescript.ts +2 -6
- package/src/codegen/generate.ts +1 -12
- package/src/codegen/typecheck.ts +6 -11
- package/src/config.ts +4 -0
- package/src/index.ts +1 -1
- package/src/lexicon-integrity.ts +5 -4
- package/src/lexicon.ts +2 -6
- package/src/lint/config.ts +8 -6
- package/src/runtime-adapter.ts +158 -0
- 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
|
@@ -51,7 +51,7 @@ export function writeResourceClass(
|
|
|
51
51
|
const attrs = [...attributes].sort((a, b) => a.name.localeCompare(b.name));
|
|
52
52
|
for (const a of attrs) {
|
|
53
53
|
const attrType = resolveConstructorType(a.type, remap);
|
|
54
|
-
lines.push(` readonly ${
|
|
54
|
+
lines.push(` readonly ${a.name}: ${attrType};`);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
lines.push("}");
|
|
@@ -98,7 +98,7 @@ export function writeConstructor(
|
|
|
98
98
|
if (p.description) {
|
|
99
99
|
lines.push(` /** ${p.description} */`);
|
|
100
100
|
}
|
|
101
|
-
lines.push(` ${
|
|
101
|
+
lines.push(` ${p.name}${optional}: ${tsType};`);
|
|
102
102
|
}
|
|
103
103
|
lines.push(" });");
|
|
104
104
|
}
|
|
@@ -155,7 +155,3 @@ export function resolveConstructorType(tsType: string, remap: Map<string, string
|
|
|
155
155
|
|
|
156
156
|
return tsType;
|
|
157
157
|
}
|
|
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/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;
|
package/src/config.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { LintConfig } from "./lint/config";
|
|
|
7
7
|
* Zod schema for ChantConfig validation.
|
|
8
8
|
*/
|
|
9
9
|
export const ChantConfigSchema = z.object({
|
|
10
|
+
runtime: z.enum(["bun", "node"]).optional(),
|
|
10
11
|
lexicons: z.array(z.string().min(1)).optional(),
|
|
11
12
|
lint: z.record(z.string(), z.unknown()).optional(),
|
|
12
13
|
}).passthrough();
|
|
@@ -17,6 +18,9 @@ export const ChantConfigSchema = z.object({
|
|
|
17
18
|
* Loaded from `chant.config.ts` (preferred) or `chant.config.json`.
|
|
18
19
|
*/
|
|
19
20
|
export interface ChantConfig {
|
|
21
|
+
/** JS runtime to use for spawned commands: "bun" (default) or "node" */
|
|
22
|
+
runtime?: "bun" | "node";
|
|
23
|
+
|
|
20
24
|
/** Lexicon package names to load (e.g. ["aws"]) */
|
|
21
25
|
lexicons?: string[];
|
|
22
26
|
|
package/src/index.ts
CHANGED
|
@@ -47,11 +47,11 @@ export * from "./codegen/fetch";
|
|
|
47
47
|
export * from "./codegen/generate";
|
|
48
48
|
export * from "./codegen/package";
|
|
49
49
|
export * from "./codegen/typecheck";
|
|
50
|
-
export * from "./codegen/rollback";
|
|
51
50
|
export * from "./codegen/coverage";
|
|
52
51
|
export * from "./codegen/validate";
|
|
53
52
|
export * from "./codegen/docs";
|
|
54
53
|
export * from "./runtime";
|
|
54
|
+
export * from "./runtime-adapter";
|
|
55
55
|
export * from "./stack-output";
|
|
56
56
|
export * from "./child-project";
|
|
57
57
|
export * from "./lsp/types";
|
package/src/lexicon-integrity.ts
CHANGED
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
* Content hashing and integrity verification for lexicon artifacts.
|
|
3
3
|
*/
|
|
4
4
|
import type { BundleSpec } from "./lexicon";
|
|
5
|
+
import { getRuntime } from "./runtime-adapter";
|
|
5
6
|
|
|
6
7
|
export interface ArtifactIntegrity {
|
|
7
|
-
algorithm:
|
|
8
|
+
algorithm: string;
|
|
8
9
|
artifacts: Record<string, string>;
|
|
9
10
|
composite: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
* Hash a single artifact's content using
|
|
14
|
+
* Hash a single artifact's content using the runtime's hash algorithm.
|
|
14
15
|
*/
|
|
15
16
|
export function hashArtifact(content: string): string {
|
|
16
|
-
return
|
|
17
|
+
return getRuntime().hash(content);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -39,7 +40,7 @@ export function computeIntegrity(spec: BundleSpec): ArtifactIntegrity {
|
|
|
39
40
|
const compositeInput = sorted.map(([k, v]) => `${k}:${v}`).join("\n");
|
|
40
41
|
const composite = hashArtifact(compositeInput);
|
|
41
42
|
|
|
42
|
-
return { algorithm:
|
|
43
|
+
return { algorithm: getRuntime().hashAlgorithm, artifacts, composite };
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|
package/src/lexicon.ts
CHANGED
|
@@ -101,7 +101,7 @@ export interface IntrinsicDef {
|
|
|
101
101
|
* Plugin interface for lexicon packages.
|
|
102
102
|
*
|
|
103
103
|
* Required lifecycle methods enforce consistency: every lexicon must support
|
|
104
|
-
* generate, validate, coverage,
|
|
104
|
+
* generate, validate, coverage, and package operations.
|
|
105
105
|
*/
|
|
106
106
|
export interface LexiconPlugin {
|
|
107
107
|
// ── Required ──────────────────────────────────────────────
|
|
@@ -123,9 +123,6 @@ export interface LexiconPlugin {
|
|
|
123
123
|
/** Package lexicon into distributable tarball */
|
|
124
124
|
package(options?: { verbose?: boolean; force?: boolean }): Promise<void>;
|
|
125
125
|
|
|
126
|
-
/** List or restore generation snapshots */
|
|
127
|
-
rollback(options?: { restore?: string; verbose?: boolean }): Promise<void>;
|
|
128
|
-
|
|
129
126
|
// ── Optional extensions ───────────────────────────────────
|
|
130
127
|
/** Return lint rules provided by this lexicon */
|
|
131
128
|
lintRules?(): LintRule[];
|
|
@@ -206,7 +203,6 @@ export function isLexiconPlugin(value: unknown): value is LexiconPlugin {
|
|
|
206
203
|
typeof obj.generate === "function" &&
|
|
207
204
|
typeof obj.validate === "function" &&
|
|
208
205
|
typeof obj.coverage === "function" &&
|
|
209
|
-
typeof obj.package === "function"
|
|
210
|
-
typeof obj.rollback === "function"
|
|
206
|
+
typeof obj.package === "function"
|
|
211
207
|
);
|
|
212
208
|
}
|
package/src/lint/config.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "fs";
|
|
2
2
|
import { join, dirname, resolve } from "path";
|
|
3
|
+
import { createRequire } from "module";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import type { Severity, RuleConfig } from "./rule";
|
|
6
|
+
import { moduleDir, getRuntime } from "../runtime-adapter";
|
|
5
7
|
import strictPreset from "./presets/strict.json";
|
|
6
8
|
|
|
7
9
|
/** Mapping of built-in preset names to their file paths */
|
|
8
10
|
const BUILTIN_PRESETS: Record<string, string> = {
|
|
9
|
-
"@intentius/chant/lint/presets/strict": resolve(import.meta.
|
|
10
|
-
"@intentius/chant/lint/presets/relaxed": resolve(import.meta.
|
|
11
|
+
"@intentius/chant/lint/presets/strict": resolve(moduleDir(import.meta.url), "presets/strict.json"),
|
|
12
|
+
"@intentius/chant/lint/presets/relaxed": resolve(moduleDir(import.meta.url), "presets/relaxed.json"),
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
// ── Zod schemas for lint config validation ─────────────────────────
|
|
@@ -307,11 +309,12 @@ function loadConfigFile(configPath: string, visited: Set<string> = new Set()): L
|
|
|
307
309
|
* @returns Loaded and merged configuration, or default config if not found
|
|
308
310
|
*/
|
|
309
311
|
export function loadConfig(dir: string): LintConfig {
|
|
310
|
-
// Try chant.config.ts first — Bun
|
|
312
|
+
// Try chant.config.ts first — Bun has native require() for .ts, Node uses tsx's loader
|
|
311
313
|
const tsConfigPath = join(dir, "chant.config.ts");
|
|
312
314
|
if (existsSync(tsConfigPath)) {
|
|
313
315
|
try {
|
|
314
|
-
const
|
|
316
|
+
const _require = createRequire(join(dir, "package.json"));
|
|
317
|
+
const mod = _require(tsConfigPath);
|
|
315
318
|
const config = mod.default ?? mod.config ?? mod;
|
|
316
319
|
if (typeof config === "object" && config !== null) {
|
|
317
320
|
// ChantConfig format: extract lint property
|
|
@@ -362,8 +365,7 @@ export function resolveRulesForFile(config: LintConfig, filePath: string): Recor
|
|
|
362
365
|
|
|
363
366
|
for (const override of config.overrides) {
|
|
364
367
|
const matches = override.files.some((pattern) => {
|
|
365
|
-
|
|
366
|
-
return glob.match(filePath);
|
|
368
|
+
return getRuntime().globMatch(pattern, filePath);
|
|
367
369
|
});
|
|
368
370
|
|
|
369
371
|
if (matches) {
|
|
@@ -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
|
+
}
|
|
@@ -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
|
-
});
|